Time and Time Zones¶
Who this is for: anyone scheduling against a gateway that is not in their own time zone, or coordinating several gateways across zones. If your operator, your automation host and your hardware are all in one zone, most of this reduces to "it just works" — but the traps below still apply across a DST boundary.
Evidence tiers per AP-14. Counts are from the HAR corpus (2025-02 → 2026-03).
The short answer¶
One call gives you the gateway's own clock and zone together:
info = await client.get_device_info() # GET getDeviceInfoV2
result = info["result"]
result["deviceTime"] # "2025-08-07 22:05:06" — gateway LOCAL wall clock
result["zoneInfo"] # "Australia/Sydney" — IANA zone name
CONFIRMED: both fields present at result level in 2,485 of 2,505
captured getDeviceInfoV2 responses (99.2%). get_home_gateway_list() carries
the same pair (163 samples), which is where discover() reads them.
Three clocks, and which one wins¶
| Clock | Where it comes from | Authoritative for |
|---|---|---|
| Gateway local | deviceTime, zoneInfo |
Everything the gateway does. TOU windows, schedules, the day boundary for daily energy totals |
| Client local | your machine | Nothing on the device. Only CLI header timestamps |
| Epoch / absolute | timestamp, activeTime, installTime, createTime |
Ordering events, comparing across gateways |
The gateway acts on its own clock, not yours. A TOU block written as 16:00
starts at 16:00 where the gateway is.
Field inventory¶
| Field | Endpoint | Example | Tier |
|---|---|---|---|
deviceTime |
getDeviceInfoV2, getHomeGatewayList |
"2025-08-07 22:05:06" |
CONFIRMED (2,808 samples) |
zoneInfo |
getDeviceInfoV2, getGatewayTouListV2, getHomeGatewayList |
"Australia/Sydney" |
CONFIRMED (4,087) |
startHourTime / endHourTime |
TOU dispatch | "16:00", "24:00" |
CONFIRMED (4,867 each) |
timestamp |
getDeviceCompositeInfo |
1754568303 |
CONFIRMED (19,892) |
dst |
getEquipmentLocationDetail |
0 |
CONFIRMED but thin (67) |
timeZone (numeric offset) |
listAppUserDevice, getStormList |
10.0 |
ASSUMED — 23 and 7 samples |
Formats are naive — no offset, ever¶
deviceTimeis 19 characters,YYYY-MM-DD HH:MM:SS. NoZ, no+10:00. Verified across 325 samples.- TOU block times are
HH:MM. No offset, no date.24:00is a valid end value.
So no timestamp the gateway hands you carries its own zone. You must pair
it with zoneInfo yourself. A naive string parsed by a host in another zone is
silently wrong, not an error.
The traps¶
1. Epoch fields are rendered in your local time¶
discover() and support convert activeTime / installTime / createTime
with datetime.fromtimestamp(ts / 1000.0) — no tz argument, so Python
uses the client's zone.
Managing a Californian gateway from Sydney, an install timestamp near midnight
renders as the wrong date. This is tracked as
DEF-EPOCH-RENDERED-IN-CLIENT-TZ and is not yet fixed; until then, treat those
dates as approximate and derive from the raw epoch if the day matters.
2. TOU schedules are written in gateway local time¶
CONFIRMED by the app itself (Smart Circuits → Timing Supply, 2026-09-18):
"The system will be scheduled using the aGate time zone."
FranklinWH states this in the scheduling screen, which settles what this guide had inferred: schedule times are the gateway's wall clock, not the operator's.
When you set a schedule, the HH:MM values you send are interpreted by the
gateway in its zone. There is no conversion anywhere in this library — the
strings pass through untouched. Building a schedule from your own local clock
puts the blocks in the wrong place by the offset between you and the hardware.
Convert deliberately:
from zoneinfo import ZoneInfo
info = (await client.get_device_info())["result"]
gw_zone = ZoneInfo(info["zoneInfo"])
local_1600 = my_datetime.astimezone(gw_zone).strftime("%H:%M")
3. DST shifts the schedule, not the wall clock¶
HH:MM is wall-clock, so a block at 16:00 stays at 16:00 across a DST
transition and moves by an hour in absolute terms. If you are coordinating a
gateway against an external signal — a market price, another site, a grid event
— that hour appears and disappears twice a year.
Two further cautions on the transition days themselves:
- A local time in the skipped hour does not exist in spring; one in the repeated
hour is ambiguous in autumn.
zoneinforesolves both, naive strings do not. - ASSUMED: whether the numeric
timeZonefield tracks DST or stays at standard time is unverified. The corpus shows10.0for Sydney (AEST, UTC+10) alongsidedst: 0, which is consistent — but with 23 and 7 samples, and none observed across a transition, this is a guess. PreferzoneInfoplus a real tz database. Do not compute offsets fromtimeZone.
4. The gateway's clock can be wrong¶
deviceTime is what the gateway believes, which is what it acts on. Comparing
it against UTC tells you whether it has drifted:
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
info = (await client.get_device_info())["result"]
gw_now = datetime.strptime(info["deviceTime"], "%Y-%m-%d %H:%M:%S") \
.replace(tzinfo=ZoneInfo(info["zoneInfo"]))
skew = (datetime.now(timezone.utc) - gw_now).total_seconds()
A large skew means schedule boundaries will not fire when you expect. Note
this includes API round-trip latency, so a few seconds is normal.
Coordinating several gateways¶
Resolve each gateway's zone from that gateway, never from a config file or from the operator's own clock:
async def gateway_now(client):
r = (await client.get_device_info())["result"]
return (
datetime.strptime(r["deviceTime"], "%Y-%m-%d %H:%M:%S")
.replace(tzinfo=ZoneInfo(r["zoneInfo"])),
r["zoneInfo"],
)
Rules that hold across zones:
- Compare in UTC, act in local. Convert every gateway's time to UTC to
decide ordering; convert back to that gateway's zone to write
HH:MM. - Never reuse one gateway's
HH:MMon another.16:00in Sydney is not16:00in California — re-derive per gateway from a common absolute instant. - Day boundaries differ. Daily energy totals roll over at each gateway's own midnight, so "today" is not the same window across a fleet.
- Re-read
zoneInforather than caching it. It is site configuration and can be changed in the app.
What the CLI shows¶
franklinwh-cli discover # Timezone (Site) and Device Time (aGate)
franklinwh-cli diag # Timezone and Device Time, together
franklinwh-cli --json support # timezone + deviceTime
discover reports the two in different sections, some distance apart; they come
from one API response.
Not established¶
- Whether numeric
timeZonetracks DST (see trap 3). - Whether the gateway applies DST itself or relies on the cloud — no capture spans a transition.
- How the gateway resolves a TOU boundary inside a skipped or repeated hour.
- Whether
deviceTimeis NTP-disciplined or free-running between syncs.
Per AP-14 these are open questions, not assumptions to build on. A capture spanning a DST changeover would settle the first three.