mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Zepp OS: Extract world clocks to standalone service
This commit is contained in:
+8
-100
@@ -143,6 +143,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fet
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch.FetchStressManualOperation;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch.FetchDebugLogsOperation;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsCannedMessagesService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsWorldClocksService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.MediaManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.SilentMode;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarEvent;
|
||||
@@ -1166,111 +1167,18 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
|
||||
sendWorldClocks(builder, clocks);
|
||||
}
|
||||
|
||||
protected void sendWorldClocks(final TransactionBuilder builder, final List<? extends WorldClock> clocks) {
|
||||
private void sendWorldClocks(final TransactionBuilder builder, final List<? extends WorldClock> clocks) {
|
||||
final DeviceCoordinator coordinator = gbDevice.getDeviceCoordinator();
|
||||
if (coordinator.getWorldClocksSlotCount() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
baos.write(0x03);
|
||||
|
||||
if (clocks.size() != 0) {
|
||||
baos.write(clocks.size());
|
||||
int i = 0;
|
||||
for (final WorldClock clock : clocks) {
|
||||
baos.write(i++);
|
||||
baos.write(encodeWorldClock(clock));
|
||||
}
|
||||
} else {
|
||||
baos.write(0);
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
LOG.error("Unable to send world clocks to device", e);
|
||||
return;
|
||||
}
|
||||
|
||||
writeToChunked2021(builder, (short) 0x0008, baos.toByteArray(), isWorldClocksEncrypted());
|
||||
}
|
||||
|
||||
protected boolean isWorldClocksEncrypted() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private byte[] encodeWorldClock(final WorldClock clock) {
|
||||
final DeviceCoordinator coordinator = gbDevice.getDeviceCoordinator();
|
||||
|
||||
try {
|
||||
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
final TimeZone timezone = TimeZone.getTimeZone(clock.getTimeZoneId());
|
||||
final ZoneId zoneId = ZoneId.of(clock.getTimeZoneId());
|
||||
|
||||
// Usually the 3-letter city code (eg. LIS for Lisbon)
|
||||
if (clock.getCode() != null) {
|
||||
baos.write(StringUtils.truncate(clock.getCode(), 3).toUpperCase().getBytes(StandardCharsets.UTF_8));
|
||||
} else {
|
||||
baos.write(StringUtils.truncate(clock.getLabel(), 3).toUpperCase().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
baos.write(0x00);
|
||||
|
||||
// Some other string? Seems to be empty
|
||||
baos.write(0x00);
|
||||
|
||||
// The city name / label that shows up on the band
|
||||
baos.write(StringUtils.truncate(clock.getLabel(), coordinator.getWorldClocksLabelLength()).getBytes(StandardCharsets.UTF_8));
|
||||
baos.write(0x00);
|
||||
|
||||
// The raw offset from UTC, in number of 15-minute blocks
|
||||
baos.write((int) (timezone.getRawOffset() / (1000L * 60L * 15L)));
|
||||
|
||||
// Daylight savings
|
||||
final boolean useDaylightTime = timezone.useDaylightTime();
|
||||
final boolean inDaylightTime = timezone.inDaylightTime(new Date());
|
||||
byte daylightByte = 0;
|
||||
// The daylight savings offset, either currently (the previous transition) or future (the next transition), in minutes
|
||||
byte daylightOffsetMinutes = 0;
|
||||
|
||||
final ZoneRules zoneRules = zoneId.getRules();
|
||||
if (useDaylightTime) {
|
||||
final ZoneOffsetTransition transition;
|
||||
if (inDaylightTime) {
|
||||
daylightByte = 0x01;
|
||||
transition = zoneRules.previousTransition(Instant.now());
|
||||
} else {
|
||||
daylightByte = 0x02;
|
||||
transition = zoneRules.nextTransition(Instant.now());
|
||||
}
|
||||
daylightOffsetMinutes = (byte) transition.getDuration().toMinutes();
|
||||
}
|
||||
|
||||
baos.write(daylightByte);
|
||||
baos.write(daylightOffsetMinutes);
|
||||
|
||||
// The timestamp of the next daylight savings transition, if any
|
||||
final ZoneOffsetTransition nextTransition = zoneRules.nextTransition(Instant.now());
|
||||
long nextTransitionTs = 0;
|
||||
if (nextTransition != null) {
|
||||
nextTransitionTs = nextTransition
|
||||
.getDateTimeBefore()
|
||||
.atZone(zoneId)
|
||||
.toEpochSecond();
|
||||
}
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
baos.write((byte) ((nextTransitionTs >> (i * 8)) & 0xff));
|
||||
}
|
||||
|
||||
if (coordinator.supportsDisabledWorldClocks()) {
|
||||
baos.write((byte) (clock.getEnabled() ? 0x01 : 0x00));
|
||||
}
|
||||
|
||||
return baos.toByteArray();
|
||||
} catch (final IOException e) {
|
||||
throw new RuntimeException("This should never happen", e);
|
||||
}
|
||||
writeToChunked2021(
|
||||
builder,
|
||||
ZeppOsWorldClocksService.ENDPOINT,
|
||||
ZeppOsWorldClocksService.encodeWorldClocks(clocks, coordinator),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+9
-5
@@ -75,6 +75,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsMapsInsta
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsMusicInstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WorldClock;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.SleepAsAndroidSender;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsCoordinator;
|
||||
@@ -146,6 +147,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.service
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsWatchfaceService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsWeatherService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsWifiService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsWorldClocksService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
@@ -197,6 +199,7 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
|
||||
private final ZeppOsBatteryService batteryService = new ZeppOsBatteryService(this);
|
||||
private final ZeppOsWeatherService weatherService = new ZeppOsWeatherService(this);
|
||||
private final ZeppOsConnectionService connectionService = new ZeppOsConnectionService(this);
|
||||
private final ZeppOsWorldClocksService worldClocksService = new ZeppOsWorldClocksService(this);
|
||||
|
||||
private final Set<Short> mSupportedServices = new HashSet<>();
|
||||
// FIXME: We need to keep track of which services are encrypted for now, since not all of them were yet migrated to a service
|
||||
@@ -236,6 +239,7 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
|
||||
put(userInfoService.getEndpoint(), userInfoService);
|
||||
put(vibrationPatternsService.getEndpoint(), vibrationPatternsService);
|
||||
put(weatherService.getEndpoint(), weatherService);
|
||||
put(worldClocksService.getEndpoint(), worldClocksService);
|
||||
}};
|
||||
|
||||
public ZeppOsSupport() {
|
||||
@@ -456,6 +460,11 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetWorldClocks(ArrayList<? extends WorldClock> clocks) {
|
||||
worldClocksService.onSetWorldClocks(clocks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetLoyaltyCards(final ArrayList<LoyaltyCard> cards) {
|
||||
loyaltyCardService.setCards(cards);
|
||||
@@ -467,11 +476,6 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
|
||||
contactsService.setContacts((List<Contact>) contacts);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isWorldClocksEncrypted() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteNotification(final int id) {
|
||||
notificationService.deleteNotification(id);
|
||||
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
/* Copyright (C) 2025 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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.zone.ZoneOffsetTransition;
|
||||
import java.time.zone.ZoneRules;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WorldClock;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
|
||||
public class ZeppOsWorldClocksService extends AbstractZeppOsService {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsWorldClocksService.class);
|
||||
|
||||
public static final short ENDPOINT = 0x0008;
|
||||
|
||||
public static final byte CMD_CAPABILITIES_REQUEST = 0x01;
|
||||
public static final byte CMD_CAPABILITIES_RESPONSE = 0x02;
|
||||
public static final byte CMD_LIST_SET = 0x03;
|
||||
public static final byte CMD_LIST_SET_ACK = 0x04;
|
||||
public static final byte CMD_LIST_GET = 0x05;
|
||||
public static final byte CMD_LIST_RET = 0x06;
|
||||
|
||||
public ZeppOsWorldClocksService(final ZeppOsSupport support) {
|
||||
super(support, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getEndpoint() {
|
||||
return ENDPOINT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePayload(final byte[] payload) {
|
||||
//noinspection SwitchStatementWithTooFewBranches
|
||||
switch (payload[0]) {
|
||||
case CMD_LIST_SET_ACK:
|
||||
LOG.info("World clocks list set ack, status = {}", payload[1]);
|
||||
return;
|
||||
default:
|
||||
LOG.warn("Unexpected world clocks byte {}", String.format("0x%02x", payload[0]));
|
||||
}
|
||||
}
|
||||
|
||||
public void onSetWorldClocks(final ArrayList<? extends WorldClock> clocks) {
|
||||
final DeviceCoordinator coordinator = getCoordinator();
|
||||
if (coordinator.getWorldClocksSlotCount() == 0) {
|
||||
LOG.warn("Got world clocks, but device does not support them");
|
||||
return;
|
||||
}
|
||||
|
||||
write("send world clocks", encodeWorldClocks(clocks, coordinator));
|
||||
}
|
||||
|
||||
public static byte[] encodeWorldClocks(final List<? extends WorldClock> clocks, final DeviceCoordinator coordinator) {
|
||||
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
baos.write(0x03);
|
||||
baos.write(clocks.size());
|
||||
int i = 0;
|
||||
for (final WorldClock clock : clocks) {
|
||||
baos.write(i++);
|
||||
baos.write(encodeWorldClock(clock, coordinator));
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
throw new RuntimeException("This should never happen", e);
|
||||
}
|
||||
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
public static byte[] encodeWorldClock(final WorldClock clock, final DeviceCoordinator coordinator) {
|
||||
try {
|
||||
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
final TimeZone timezone = TimeZone.getTimeZone(clock.getTimeZoneId());
|
||||
final ZoneId zoneId = ZoneId.of(clock.getTimeZoneId());
|
||||
|
||||
// Usually the 3-letter city code (eg. LIS for Lisbon)
|
||||
if (clock.getCode() != null) {
|
||||
baos.write(StringUtils.truncate(clock.getCode(), 3).toUpperCase().getBytes(StandardCharsets.UTF_8));
|
||||
} else {
|
||||
baos.write(StringUtils.truncate(clock.getLabel(), 3).toUpperCase().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
baos.write(0x00);
|
||||
|
||||
// Some other string? Seems to be empty
|
||||
baos.write(0x00);
|
||||
|
||||
// The city name / label that shows up on the band
|
||||
baos.write(StringUtils.truncate(clock.getLabel(), coordinator.getWorldClocksLabelLength()).getBytes(StandardCharsets.UTF_8));
|
||||
baos.write(0x00);
|
||||
|
||||
// The raw offset from UTC, in number of 15-minute blocks
|
||||
baos.write((int) (timezone.getRawOffset() / (1000L * 60L * 15L)));
|
||||
|
||||
// Daylight savings
|
||||
final boolean useDaylightTime = timezone.useDaylightTime();
|
||||
final boolean inDaylightTime = timezone.inDaylightTime(new Date());
|
||||
byte daylightByte = 0;
|
||||
// The daylight savings offset, either currently (the previous transition) or future (the next transition), in minutes
|
||||
byte daylightOffsetMinutes = 0;
|
||||
|
||||
final ZoneRules zoneRules = zoneId.getRules();
|
||||
if (useDaylightTime) {
|
||||
final ZoneOffsetTransition transition;
|
||||
if (inDaylightTime) {
|
||||
daylightByte = 0x01;
|
||||
transition = zoneRules.previousTransition(Instant.now());
|
||||
} else {
|
||||
daylightByte = 0x02;
|
||||
transition = zoneRules.nextTransition(Instant.now());
|
||||
}
|
||||
daylightOffsetMinutes = (byte) transition.getDuration().toMinutes();
|
||||
}
|
||||
|
||||
baos.write(daylightByte);
|
||||
baos.write(daylightOffsetMinutes);
|
||||
|
||||
// The timestamp of the next daylight savings transition, if any
|
||||
final ZoneOffsetTransition nextTransition = zoneRules.nextTransition(Instant.now());
|
||||
long nextTransitionTs = 0;
|
||||
if (nextTransition != null) {
|
||||
nextTransitionTs = nextTransition
|
||||
.getDateTimeBefore()
|
||||
.atZone(zoneId)
|
||||
.toEpochSecond();
|
||||
}
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
baos.write((byte) ((nextTransitionTs >> (i * 8)) & 0xff));
|
||||
}
|
||||
|
||||
if (coordinator.supportsDisabledWorldClocks()) {
|
||||
baos.write((byte) (clock.getEnabled() ? 0x01 : 0x00));
|
||||
}
|
||||
|
||||
return baos.toByteArray();
|
||||
} catch (final IOException e) {
|
||||
throw new RuntimeException("This should never happen", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user