/* Copyright (C) 2023-2024 Johannes Krude
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.casio;
import java.util.Set;
import java.util.HashSet;
import java.util.Map;
import java.util.Arrays;
import java.nio.charset.StandardCharsets;
import java.time.DayOfWeek;
import java.time.Instant;
import java.time.LocalTime;
import java.time.Month;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.zone.ZoneOffsetTransition;
import java.time.zone.ZoneOffsetTransitionRule;
import java.time.zone.ZoneRules;
public class CasioTimeZone {
/*
There are six clocks on the Casio GW-B5600 / S100
0 is the main clock
1-5 are the world clocks
0x1d 00 01 DST0 DST1 TZ0A TZ0B TZ1A TZ1B ff ff ff ff ff
0x1d 02 03 DST2 DST3 TZ2A TZ2B TZ3A TZ3B ff ff ff ff ff
0x1d 04 05 DST4 DST5 TZ4A TZ4B TZ5A TZ5B ff ff ff ff ff
DST: bitwise flags; bit0: DST on, bit1: DST auto
0x1e 0-5 TZ_A TZ_B TZ_OFF TZ_DSTOFF TZ_DSTRULES
A/B seem to be ignored by the watch
OFF & DSTOFF in 15 minute intervals
0x1f 0-5 (18 bytes ASCII TZ name)
Timezones selectable on the watch:
A B OFF DSTOFF DSTRULES
BAKER ISLAND 39 01 D0 04 00
PAGO PAGO D7 00 D4 04 00
HONOLULU 7B 00 D8 04 00
MARQUESAS ISLANDS 3A 01 DA 04 00
ANCHORAGE 0C 00 DC 04 01
LOS ANGELES A1 00 E0 04 01
DENVER 54 00 E4 04 01
CHICAGO 42 00 E8 04 01
NEW YORK CA 00 EC 04 01
HALIFAX 71 00 F0 04 01
ST.JOHN'S 0C 01 F2 04 01
RIO DE JANEIRO F1 00 F4 04 00
F.DE NORONHA 62 00 F8 04 00
PRAIA E9 00 FC 04 00
UTC 00 00 00 00 00
LONDON A0 00 00 04 02
PARIS DC 00 04 04 02
ATHENS 13 00 08 04 02
JEDDAH 85 00 0C 04 00
TEHRAN 16 01 0E 04 2B
DUBAI 5B 00 10 04 00
KABUL 88 00 12 04 00
KARACHI 8B 00 14 04 00
DELHI 52 00 16 04 00
KATHMANDU 8C 00 17 04 00
DHAKA 56 00 18 04 00
YANGON 2F 01 1A 04 00
BANGKOK 1C 00 1C 04 00
HONG KONG 7A 00 20 04 00
PYONGYANG EA 00 24 04 00
EUCLA 36 01 23 04 00
TOKYO 19 01 24 04 00
ADELAIDE 05 00 26 04 04
SYDNEY 0F 01 28 04 04
LORD HOWE ISLAND 37 01 2A 02 12
NOUMEA CD 00 2C 04 00
WELLINGTON 2B 01 30 04 05
CHATHAM ISLANDS 3F 00 33 04 17
NUKUALOFA D0 00 34 04 00
KIRITIMATI 93 00 38 04 00
Timezones NOT selectable on the watch:
A B OFF DSTOFF DSTRULES
CASABLANCA 3A 00 00 04 0F
BEIRUT 22 00 08 04 0C
JERUSALEM 86 00 08 04 2A
NORFOLK ISLAND 38 01 2C 04 04
EASTER ISLAND 5E 00 E8 04 1C
HAVANA 75 00 EC 04 15
SANTIAGO 02 01 F0 04 1B
ASUNCION 12 00 F0 04 09
PONTA DELGADA E4 00 FC 04 02
*/
private byte[] name;
private byte[] number;
private byte offset;
private byte dstOffset;
private byte dstRules;
private byte dstSetting;
// bitwise flags
final static byte DST_SETTING_ON = 1;
final static byte DST_SETTING_AUTO = 2;
private CasioTimeZone(byte[] name, byte[] number, byte offset, byte dstOffset, byte dstRules, byte dstSetting) {
this.name = name;
this.number = number;
this.offset = offset;
this.dstOffset = dstOffset;
this.dstRules = dstRules;
this.dstSetting = dstSetting;
}
static public Set requests(int slot) {
HashSet requests = new HashSet();
requests.add(new Casio2C2DSupport.FeatureRequest(Casio2C2DSupport.FEATURE_DST_WATCH_STATE, (byte) (slot/2*2)));
requests.add(new Casio2C2DSupport.FeatureRequest(Casio2C2DSupport.FEATURE_DST_SETTING, (byte) slot));
requests.add(new Casio2C2DSupport.FeatureRequest(Casio2C2DSupport.FEATURE_WORLD_CITY, (byte) slot));
return requests;
}
static public byte[] dstWatchStateBytes(int slotA, CasioTimeZone zoneA, int slotB, CasioTimeZone zoneB) {
return new byte[] {
Casio2C2DSupport.FEATURE_DST_WATCH_STATE,
(byte) slotA,
(byte) slotB,
zoneA.dstSetting,
zoneB.dstSetting,
zoneA.number[0],
zoneA.number[1],
zoneB.number[0],
zoneB.number[1],
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff};
}
public byte[] dstSettingBytes(int slot) {
return new byte[] {
Casio2C2DSupport.FEATURE_DST_SETTING,
(byte) slot,
number[0],
number[1],
offset,
dstOffset,
dstRules};
}
public byte[] worldCityBytes(int slot) {
byte[] bytes = {
Casio2C2DSupport.FEATURE_WORLD_CITY,
(byte) slot,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
System.arraycopy(name, 0, bytes, 2, Math.min(name.length, 18));
return bytes;
}
public static CasioTimeZone fromWatchResponses(Map responses, int slot) {
byte[] name = "unknown".getBytes(StandardCharsets.US_ASCII);
byte[] number = {0,0};
byte offset = 0;
byte dstOffset = 0;
byte dstRules = 0;
byte dstSetting = 0;
for (byte[] response: responses.values()) {
if (response[0] == Casio2C2DSupport.FEATURE_DST_WATCH_STATE && response.length >= 9) {
if (response[1] == slot) {
dstSetting = response[3];
number = new byte[] {response[5], response[6]};
}
if (response[2] == slot) {
dstSetting = response[4];
number = new byte[] {response[7], response[8]};
}
} else if (response[0] == Casio2C2DSupport.FEATURE_DST_SETTING && response.length >= 7 && response[1] == slot) {
number = new byte[] {response[2], response[3]};
offset = response[4];
dstOffset = response[5];
dstRules = response[6];
} else if (response[0] == Casio2C2DSupport.FEATURE_WORLD_CITY && response.length >= 2 && response[1] == slot) {
int size;
for (size = 0; size < response.length-2; size++) {
if (response[2+size] == 0) {
break;
}
}
name = Arrays.copyOfRange(response, 2, 2+size);
}
}
return new CasioTimeZone(name, number, offset, dstOffset, dstRules, dstSetting);
}
public static CasioTimeZone fromZoneId(ZoneId zone, Instant time, String zoneName) {
ZoneRules rules = zone.getRules();
byte[] name = zoneName.getBytes(StandardCharsets.US_ASCII);
byte[] number = {0,0};
byte offset = (byte) (rules.getStandardOffset(time).getTotalSeconds() / 60 / 15);
byte dstOffset = 0; // can be set only later once we now the next transition
byte dstRules = 0;
byte dstSetting = 0;
ZoneOffsetTransition next = rules.nextTransition(time);
int nextYear = next.getInstant().atZone(zone).getYear();
ZoneOffsetTransition next2 = (next == null ? null: rules.nextTransition(next.getInstant()));
int next2Year = (next2 == null ? 0 : next2.getInstant().atZone(zone).getYear());
if (next == null) {
// no DST is easy
dstSetting = DST_SETTING_AUTO;
} else {
// we need an Instant with DST on to get the dstOffset
if (rules.isDaylightSavings(time)) {
dstOffset = (byte) (rules.getDaylightSavings(time).getSeconds() / 60 / 15);
} else {
dstOffset = (byte) (rules.getDaylightSavings(next.getInstant().plusSeconds(1)).getSeconds() / 60 / 15);
}
// find Watch DST rules
dstRules = findWatchDstRules(offset, dstOffset, next, nextYear, next2, next2Year);
if (dstRules != 0) {
// DST AUTO if the watch knows at least the next transition
// otherwise will result in incorrect time between actual DST change and next sync
dstSetting |= DST_SETTING_AUTO;
}
// if DST bit is incorrect, the watch will substract or add time
if (rules.isDaylightSavings(time)) {
dstSetting |= DST_SETTING_ON;
}
}
return new CasioTimeZone(name, number, offset, dstOffset, dstRules, dstSetting);
}
// We are searching for watch DST rules which match the next two transitions.
// In case only the next transition matches a rule, the rule is still used
static byte findWatchDstRules(byte offset, byte dstOffset, ZoneOffsetTransition next, int nextYear, ZoneOffsetTransition next2, int next2Year) {
WatchDstRules candidate = null;
for (WatchDstRules r: watchDstRules) {
int match = r.matches(offset, dstOffset, next, nextYear, next2, next2Year);
if (match == 2)
return r.dstRules;
if (match == 1 && candidate == null)
candidate = r;
}
if (candidate != null)
return candidate.dstRules;
return (byte) 0;
}
static class WatchDstRules {
final byte offset;
final byte dstOffset;
final byte dstRules;
final ZoneOffsetTransitionRule ruleA;
final ZoneOffsetTransitionRule ruleB;
WatchDstRules(int offset, int dstOffset, int dstRules, ZoneOffsetTransitionRule ruleA, ZoneOffsetTransitionRule ruleB) {
this.offset = (byte) offset;
this.dstOffset = (byte) dstOffset;
this.dstRules = (byte) dstRules;
this.ruleA = ruleA;
this.ruleB = ruleB;
}
// returns how many of the next transitions match the rules
int matches(byte offset, byte dstOffset, ZoneOffsetTransition next, int nextYear, ZoneOffsetTransition next2, int next2Year) {
if (offset != this.offset || dstOffset != this.dstOffset)
return -1;
if (this.ruleA.createTransition(nextYear).equals(next)) {
if (this.ruleB.createTransition(next2Year).equals(next2))
return 2;
return 1;
}
if (this.ruleB.createTransition(nextYear).equals(next)) {
if (this.ruleA.createTransition(next2Year).equals(next2))
return 2;
return 1;
}
return 0;
}
}
// All known Watch DST Rules
// Possibly incomplete and incorrect
//
// When adding new WatchDstRules, test them:
// 1. Apply the following changes to CasioGWB5600InitOperation
// + static int nextNext = 0;
// private void setClocks(TransactionBuilder builder) {
// ZoneId tz = ZoneId.systemDefault();
// Instant now = Instant.now().plusSeconds(2);
// + now = tz.getRules().nextTransition(now).getInstant();
// + if (nextNext != 0)
// + now = tz.getRules().nextTransition(now).getInstant();
// + nextNext ^= 1;
// + now = now.minusSeconds(10);
// 2. Sync the time on the watch and observe a DST change
// 3. Repeat the time sync to obseve a second DST change
static final WatchDstRules[] watchDstRules = {
// Europe/London
new WatchDstRules(0x00, 0x04, 0x02,
ZoneOffsetTransitionRule.of(Month.MARCH, 25, DayOfWeek.SUNDAY, LocalTime.of(1, 0), false, ZoneOffsetTransitionRule.TimeDefinition.UTC, ZoneOffset.ofTotalSeconds(0), ZoneOffset.ofTotalSeconds(0), ZoneOffset.ofTotalSeconds(3600)),
ZoneOffsetTransitionRule.of(Month.OCTOBER, 25, DayOfWeek.SUNDAY, LocalTime.of(1, 0), false, ZoneOffsetTransitionRule.TimeDefinition.UTC, ZoneOffset.ofTotalSeconds(0), ZoneOffset.ofTotalSeconds(3600), ZoneOffset.ofTotalSeconds(0))),
// Europe/Paris
new WatchDstRules(0x04, 0x04, 0x02,
ZoneOffsetTransitionRule.of(Month.MARCH, 25, DayOfWeek.SUNDAY, LocalTime.of(1, 0), false, ZoneOffsetTransitionRule.TimeDefinition.UTC, ZoneOffset.ofTotalSeconds(3600), ZoneOffset.ofTotalSeconds(3600), ZoneOffset.ofTotalSeconds(7200)),
ZoneOffsetTransitionRule.of(Month.OCTOBER, 25, DayOfWeek.SUNDAY, LocalTime.of(1, 0), false, ZoneOffsetTransitionRule.TimeDefinition.UTC, ZoneOffset.ofTotalSeconds(3600), ZoneOffset.ofTotalSeconds(7200), ZoneOffset.ofTotalSeconds(3600))),
// Europe/Athen
new WatchDstRules(0x08, 0x04, 0x02,
ZoneOffsetTransitionRule.of(Month.MARCH, 25, DayOfWeek.SUNDAY, LocalTime.of(1, 0), false, ZoneOffsetTransitionRule.TimeDefinition.UTC, ZoneOffset.ofTotalSeconds(7200), ZoneOffset.ofTotalSeconds(7200), ZoneOffset.ofTotalSeconds(10800)),
ZoneOffsetTransitionRule.of(Month.OCTOBER, 25, DayOfWeek.SUNDAY, LocalTime.of(1, 0), false, ZoneOffsetTransitionRule.TimeDefinition.UTC, ZoneOffset.ofTotalSeconds(7200), ZoneOffset.ofTotalSeconds(10800), ZoneOffset.ofTotalSeconds(7200))),
// Asia/Beirut
new WatchDstRules(0x08, 0x04, 0x0C,
ZoneOffsetTransitionRule.of(Month.MARCH, 25, DayOfWeek.SUNDAY, LocalTime.of(0, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(7200), ZoneOffset.ofTotalSeconds(7200), ZoneOffset.ofTotalSeconds(10800)),
ZoneOffsetTransitionRule.of(Month.OCTOBER, 25, DayOfWeek.SUNDAY, LocalTime.of(0, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(7200), ZoneOffset.ofTotalSeconds(10800), ZoneOffset.ofTotalSeconds(7200))),
// Asia/Jerusalem
new WatchDstRules(0x08, 0x04, 0x2A,
ZoneOffsetTransitionRule.of(Month.MARCH, 23, DayOfWeek.FRIDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(7200), ZoneOffset.ofTotalSeconds(7200), ZoneOffset.ofTotalSeconds(10800)),
ZoneOffsetTransitionRule.of(Month.OCTOBER, 25, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(7200), ZoneOffset.ofTotalSeconds(10800), ZoneOffset.ofTotalSeconds(7200))),
// Australia/Adelaide
new WatchDstRules(0x26, 0x04, 0x04,
ZoneOffsetTransitionRule.of(Month.APRIL, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.STANDARD, ZoneOffset.ofTotalSeconds(34200), ZoneOffset.ofTotalSeconds(37800), ZoneOffset.ofTotalSeconds(34200)),
ZoneOffsetTransitionRule.of(Month.OCTOBER, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.STANDARD, ZoneOffset.ofTotalSeconds(34200), ZoneOffset.ofTotalSeconds(34200), ZoneOffset.ofTotalSeconds(37800))),
// Australia/Sydney
new WatchDstRules(0x28, 0x04, 0x04,
ZoneOffsetTransitionRule.of(Month.APRIL, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.STANDARD, ZoneOffset.ofTotalSeconds(36000), ZoneOffset.ofTotalSeconds(39600), ZoneOffset.ofTotalSeconds(36000)),
ZoneOffsetTransitionRule.of(Month.OCTOBER, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.STANDARD, ZoneOffset.ofTotalSeconds(36000), ZoneOffset.ofTotalSeconds(36000), ZoneOffset.ofTotalSeconds(39600))),
// Australia/Lord_Howe
new WatchDstRules(0x2A, 0x02, 0x12,
ZoneOffsetTransitionRule.of(Month.APRIL, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(37800), ZoneOffset.ofTotalSeconds(39600), ZoneOffset.ofTotalSeconds(37800)),
ZoneOffsetTransitionRule.of(Month.OCTOBER, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(37800), ZoneOffset.ofTotalSeconds(37800), ZoneOffset.ofTotalSeconds(39600))),
// Pacific/Norfolk
new WatchDstRules(0x2C, 0x04, 0x04,
ZoneOffsetTransitionRule.of(Month.APRIL, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.STANDARD, ZoneOffset.ofTotalSeconds(39600), ZoneOffset.ofTotalSeconds(43200), ZoneOffset.ofTotalSeconds(39600)),
ZoneOffsetTransitionRule.of(Month.OCTOBER, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.STANDARD, ZoneOffset.ofTotalSeconds(39600), ZoneOffset.ofTotalSeconds(39600), ZoneOffset.ofTotalSeconds(43200))),
// Pacific/Auckland
new WatchDstRules(0x30, 0x04, 0x05,
ZoneOffsetTransitionRule.of(Month.APRIL, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.STANDARD, ZoneOffset.ofTotalSeconds(43200), ZoneOffset.ofTotalSeconds(46800), ZoneOffset.ofTotalSeconds(43200)),
ZoneOffsetTransitionRule.of(Month.SEPTEMBER, 24, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.STANDARD, ZoneOffset.ofTotalSeconds(43200), ZoneOffset.ofTotalSeconds(43200), ZoneOffset.ofTotalSeconds(46800))),
// Pacific/Chatham
new WatchDstRules(0x33, 0x04, 0x17,
ZoneOffsetTransitionRule.of(Month.APRIL, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 45), false, ZoneOffsetTransitionRule.TimeDefinition.STANDARD, ZoneOffset.ofTotalSeconds(45900), ZoneOffset.ofTotalSeconds(49500), ZoneOffset.ofTotalSeconds(45900)),
ZoneOffsetTransitionRule.of(Month.SEPTEMBER, 24, DayOfWeek.SUNDAY, LocalTime.of(2, 45), false, ZoneOffsetTransitionRule.TimeDefinition.STANDARD, ZoneOffset.ofTotalSeconds(45900), ZoneOffset.ofTotalSeconds(45900), ZoneOffset.ofTotalSeconds(49500))),
// America/Anchorage
new WatchDstRules(0xDC, 0x04, 0x01,
ZoneOffsetTransitionRule.of(Month.MARCH, 8, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-32400), ZoneOffset.ofTotalSeconds(-32400), ZoneOffset.ofTotalSeconds(-28800)),
ZoneOffsetTransitionRule.of(Month.NOVEMBER, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-32400), ZoneOffset.ofTotalSeconds(-28800), ZoneOffset.ofTotalSeconds(-32400))),
// America/Los_Angeles
new WatchDstRules(0xE0, 0x04, 0x01,
ZoneOffsetTransitionRule.of(Month.MARCH, 8, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-28800), ZoneOffset.ofTotalSeconds(-28800), ZoneOffset.ofTotalSeconds(-25200)),
ZoneOffsetTransitionRule.of(Month.NOVEMBER, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-28800), ZoneOffset.ofTotalSeconds(-25200), ZoneOffset.ofTotalSeconds(-28800))),
// America/Denver
new WatchDstRules(0xE4, 0x04, 0x01,
ZoneOffsetTransitionRule.of(Month.MARCH, 8, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-25200), ZoneOffset.ofTotalSeconds(-25200), ZoneOffset.ofTotalSeconds(-21600)),
ZoneOffsetTransitionRule.of(Month.NOVEMBER, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-25200), ZoneOffset.ofTotalSeconds(-21600), ZoneOffset.ofTotalSeconds(-25200))),
// America/Chicago
new WatchDstRules(0xE8, 0x04, 0x01,
ZoneOffsetTransitionRule.of(Month.MARCH, 8, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-21600), ZoneOffset.ofTotalSeconds(-21600), ZoneOffset.ofTotalSeconds(-18000)),
ZoneOffsetTransitionRule.of(Month.NOVEMBER, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-21600), ZoneOffset.ofTotalSeconds(-18000), ZoneOffset.ofTotalSeconds(-21600))),
// Chile/EasterIsland
new WatchDstRules(0xE8, 0x04, 0x1C,
ZoneOffsetTransitionRule.of(Month.APRIL, 2, DayOfWeek.SUNDAY, LocalTime.of(3, 0), false, ZoneOffsetTransitionRule.TimeDefinition.UTC, ZoneOffset.ofTotalSeconds(-21600), ZoneOffset.ofTotalSeconds(-18000), ZoneOffset.ofTotalSeconds(-21600)),
ZoneOffsetTransitionRule.of(Month.SEPTEMBER, 2, DayOfWeek.SUNDAY, LocalTime.of(4, 0), false, ZoneOffsetTransitionRule.TimeDefinition.UTC, ZoneOffset.ofTotalSeconds(-21600), ZoneOffset.ofTotalSeconds(-21600), ZoneOffset.ofTotalSeconds(-18000))),
// America/New_York
new WatchDstRules(0xEC, 0x04, 0x01,
ZoneOffsetTransitionRule.of(Month.MARCH, 8, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-18000), ZoneOffset.ofTotalSeconds(-18000), ZoneOffset.ofTotalSeconds(-14400)),
ZoneOffsetTransitionRule.of(Month.NOVEMBER, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-18000), ZoneOffset.ofTotalSeconds(-14400), ZoneOffset.ofTotalSeconds(-18000))),
// America/Havana
new WatchDstRules(0xEC, 0x04, 0x15,
ZoneOffsetTransitionRule.of(Month.MARCH, 8, DayOfWeek.SUNDAY, LocalTime.of(0, 0), false, ZoneOffsetTransitionRule.TimeDefinition.STANDARD, ZoneOffset.ofTotalSeconds(-18000), ZoneOffset.ofTotalSeconds(-18000), ZoneOffset.ofTotalSeconds(-14400)),
ZoneOffsetTransitionRule.of(Month.NOVEMBER, 1, DayOfWeek.SUNDAY, LocalTime.of(0, 0), false, ZoneOffsetTransitionRule.TimeDefinition.STANDARD, ZoneOffset.ofTotalSeconds(-18000), ZoneOffset.ofTotalSeconds(-14400), ZoneOffset.ofTotalSeconds(-18000))),
// America/Santiago
new WatchDstRules(0xF0, 0x04, 0x1B,
ZoneOffsetTransitionRule.of(Month.APRIL, 2, DayOfWeek.SUNDAY, LocalTime.of(3, 0), false, ZoneOffsetTransitionRule.TimeDefinition.UTC, ZoneOffset.ofTotalSeconds(-14400), ZoneOffset.ofTotalSeconds(-10800), ZoneOffset.ofTotalSeconds(-14400)),
ZoneOffsetTransitionRule.of(Month.SEPTEMBER, 2, DayOfWeek.SUNDAY, LocalTime.of(4, 0), false, ZoneOffsetTransitionRule.TimeDefinition.UTC, ZoneOffset.ofTotalSeconds(-14400), ZoneOffset.ofTotalSeconds(-14400), ZoneOffset.ofTotalSeconds(-10800))),
// America/Halifax
new WatchDstRules(0xF0, 0x04, 0x01,
ZoneOffsetTransitionRule.of(Month.MARCH, 8, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-14400), ZoneOffset.ofTotalSeconds(-14400), ZoneOffset.ofTotalSeconds(-10800)),
ZoneOffsetTransitionRule.of(Month.NOVEMBER, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-14400), ZoneOffset.ofTotalSeconds(-10800), ZoneOffset.ofTotalSeconds(-14400))),
// America/Asuncion
new WatchDstRules(0xF0, 0x04, 0x09,
ZoneOffsetTransitionRule.of(Month.MARCH, 22, DayOfWeek.SUNDAY, LocalTime.of(0, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-14400), ZoneOffset.ofTotalSeconds(-10800), ZoneOffset.ofTotalSeconds(-14400)),
ZoneOffsetTransitionRule.of(Month.OCTOBER, 1, DayOfWeek.SUNDAY, LocalTime.of(0, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-14400), ZoneOffset.ofTotalSeconds(-14400), ZoneOffset.ofTotalSeconds(-10800))),
// America/St_Johns
new WatchDstRules(0xF2, 0x04, 0x01,
ZoneOffsetTransitionRule.of(Month.MARCH, 8, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-12600), ZoneOffset.ofTotalSeconds(-12600), ZoneOffset.ofTotalSeconds(-9000)),
ZoneOffsetTransitionRule.of(Month.NOVEMBER, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-12600), ZoneOffset.ofTotalSeconds(-9000), ZoneOffset.ofTotalSeconds(-12600))),
// Atlantic/Azores
new WatchDstRules(0xFC, 0x04, 0x02,
ZoneOffsetTransitionRule.of(Month.MARCH, 25, DayOfWeek.SUNDAY, LocalTime.of(1, 0), false, ZoneOffsetTransitionRule.TimeDefinition.UTC, ZoneOffset.ofTotalSeconds(-3600), ZoneOffset.ofTotalSeconds(-3600), ZoneOffset.ofTotalSeconds(0)),
ZoneOffsetTransitionRule.of(Month.OCTOBER, 25, DayOfWeek.SUNDAY, LocalTime.of(1, 0), false, ZoneOffsetTransitionRule.TimeDefinition.UTC, ZoneOffset.ofTotalSeconds(-3600), ZoneOffset.ofTotalSeconds(0), ZoneOffset.ofTotalSeconds(-3600))),
};
}