/* Copyright (C) 2023-2024 Daniel Dakhno, José Rebelo This file is part of Gadgetbridge. Gadgetbridge is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Gadgetbridge is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services; import android.content.Intent; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Calendar; import java.util.HashMap; import java.util.List; import java.util.Map; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService; import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils; public class ZeppOsAlarmsService extends AbstractZeppOsService { private static final Logger LOG = LoggerFactory.getLogger(ZeppOsAlarmsService.class); public static final short ENDPOINT = 0x000f; public static final byte CMD_CAPABILITIES_REQUEST = 0x01; public static final byte CMD_CAPABILITIES_RESPONSE = 0x02; public static final byte CMD_CREATE = 0x03; public static final byte CMD_CREATE_ACK = 0x04; public static final byte CMD_DELETE = 0x05; public static final byte CMD_DELETE_ACK = 0x06; public static final byte CMD_UPDATE = 0x07; public static final byte CMD_UPDATE_ACK = 0x08; public static final byte CMD_REQUEST = 0x09; public static final byte CMD_RESPONSE = 0x0a; public static final byte CMD_NOTIFY_CHANGE = 0x0f; public static final int IDX_FLAGS = 0; public static final int IDX_POSITION = 1; public static final int IDX_HOUR = 2; public static final int IDX_MINUTE = 3; public static final int IDX_REPETITION = 4; public static final int FLAG_SMART = 0x01; public static final int FLAG_UNKNOWN_2 = 0x02; public static final int FLAG_ENABLED = 0x04; public ZeppOsAlarmsService(final ZeppOsSupport support) { super(support, false); } @Override public short getEndpoint() { return ENDPOINT; } @Override public void handlePayload(final byte[] payload) { switch (payload[0]) { case CMD_CREATE_ACK: LOG.info("Alarm create ACK, status = {}", payload[1]); return; case CMD_DELETE_ACK: LOG.info("Alarm delete ACK, status = {}", payload[1]); return; case CMD_UPDATE_ACK: LOG.info("Alarm update ACK, status = {}", payload[1]); return; case CMD_NOTIFY_CHANGE: LOG.info("Alarms changed on band"); requestAlarms(); return; case CMD_RESPONSE: LOG.info("Got alarms from band"); decodeAndUpdateAlarms(payload); return; default: LOG.warn("Unexpected alarms payload byte {}", String.format("0x%02x", payload[0])); } } private void requestAlarms() { try { final TransactionBuilder builder = new TransactionBuilder("request alarms"); requestAlarms(builder); builder.queue(getSupport().getQueue()); } catch (final Exception e) { LOG.error("Failed to request alarms", e); } } public void requestAlarms(final TransactionBuilder builder) { LOG.info("Requesting alarms"); write(builder, new byte[]{CMD_REQUEST}); } public void sendAlarm(final Alarm alarm, final TransactionBuilder builder) { final DeviceCoordinator coordinator = getSupport().getDevice().getDeviceCoordinator(); final Calendar calendar = AlarmUtils.toCalendar(alarm); final byte[] alarmMessage; if (!alarm.getUnused()) { int alarmFlags = 0; if (alarm.getEnabled()) { alarmFlags = FLAG_ENABLED; } if (coordinator.supportsSmartWakeup(getSupport().getDevice(), alarm.getPosition()) && alarm.getSmartWakeup()) { alarmFlags |= FLAG_SMART; } alarmMessage = new byte[]{ CMD_CREATE, (byte) 0x01, // ? (byte) alarmFlags, (byte) alarm.getPosition(), (byte) calendar.get(Calendar.HOUR_OF_DAY), (byte) calendar.get(Calendar.MINUTE), (byte) alarm.getRepetition(), (byte) 0x00, // ? (byte) 0x00, // ? (byte) 0x00, // ? (byte) 0x00, // ?, this is usually 0 in the create command, 1 in the watch response (byte) 0x00, // ? }; } else { // Delete it from the band alarmMessage = new byte[]{ CMD_DELETE, (byte) 0x01, // ? (byte) alarm.getPosition() }; } write(builder, alarmMessage); } private void decodeAndUpdateAlarms(final byte[] payload) { final int numAlarms = payload[1]; if (payload.length != 2 + numAlarms * 10) { LOG.warn("Unexpected payload length of {} for {} alarms", payload.length, numAlarms); return; } // Map of alarm position to Alarm, as returned by the band final Map payloadAlarms = new HashMap<>(); for (int i = 0; i < numAlarms; i++) { final Alarm alarm = parseAlarm(payload, 2 + i * 10); payloadAlarms.put(alarm.getPosition(), alarm); } final List dbAlarms = DBHelper.getAlarms(getSupport().getDevice()); int numUpdatedAlarms = 0; for (nodomain.freeyourgadget.gadgetbridge.entities.Alarm alarm : dbAlarms) { final int pos = alarm.getPosition(); final Alarm updatedAlarm = payloadAlarms.get(pos); final boolean alarmNeedsUpdate = updatedAlarm == null || alarm.getUnused() != updatedAlarm.getUnused() || alarm.getEnabled() != updatedAlarm.getEnabled() || alarm.getSmartWakeup() != updatedAlarm.getSmartWakeup() || alarm.getHour() != updatedAlarm.getHour() || alarm.getMinute() != updatedAlarm.getMinute() || alarm.getRepetition() != updatedAlarm.getRepetition(); if (alarmNeedsUpdate) { numUpdatedAlarms++; LOG.info("Updating alarm index={}, unused={}", pos, updatedAlarm == null); alarm.setUnused(updatedAlarm == null); if (updatedAlarm != null) { alarm.setEnabled(updatedAlarm.getEnabled()); alarm.setSmartWakeup(updatedAlarm.getSmartWakeup()); alarm.setHour(updatedAlarm.getHour()); alarm.setMinute(updatedAlarm.getMinute()); alarm.setRepetition(updatedAlarm.getRepetition()); } DBHelper.store(alarm); } } if (numUpdatedAlarms > 0) { final Intent intent = new Intent(DeviceService.ACTION_SAVE_ALARMS); LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); } } private Alarm parseAlarm(final byte[] payload, final int offset) { final nodomain.freeyourgadget.gadgetbridge.entities.Alarm alarm = new nodomain.freeyourgadget.gadgetbridge.entities.Alarm(); alarm.setUnused(false); // If the band sent it, it's not unused alarm.setPosition(payload[offset + IDX_POSITION]); alarm.setEnabled((payload[offset + IDX_FLAGS] & FLAG_ENABLED) > 0); alarm.setSmartWakeup((payload[offset + IDX_FLAGS] & FLAG_SMART) > 0); alarm.setHour(payload[offset + IDX_HOUR]); alarm.setMinute(payload[offset + IDX_MINUTE]); alarm.setRepetition(payload[offset + IDX_REPETITION]); return alarm; } }