Skip to content

feat: parse RestoreV5/V4 clock schedule fields and fix turn_clock_off wiping day+night brightness#65

Merged
dahlb merged 2 commits into
dahlb:mainfrom
MasterDevwi:restore-v5-clock-schedule
Jul 2, 2026
Merged

feat: parse RestoreV5/V4 clock schedule fields and fix turn_clock_off wiping day+night brightness#65
dahlb merged 2 commits into
dahlb:mainfrom
MasterDevwi:restore-v5-clock-schedule

Conversation

@MasterDevwi

@MasterDevwi MasterDevwi commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Summary

The current clock behavior on RestoreV5/V4 is broken. Turning the clock off does not work properly — turn_clock_off overwrites clock.i with a hard-coded 655, which wipes both the day and night brightness (they're packed into that single value).

For a consumer like Home Assistant this means a user can turn the clock light off, but can't meaningfully turn it back on (it returns at ~0% and looks dark) and changing brightness does nothing, until brightness is reset in the Hatch app. turn_clock_off also XOR-toggles the ON flag, so an "off" call can even flip the clock back on.

Separately, the device's clock schedule (turnOffMode and the on/off/dim/bright times) is never parsed, so consumers can't read the clock's mode or schedule at all.

This PR fixes the broken turn_clock_off behavior and adds parsing for the five clock schedule fields. Tested locally against real RestoreV5 hardware (fw 10.1.625) and everything works (details below).


Two related clock improvements for RestoreV5 (and RestoreV4, which subclasses it via class RestoreV4(RestoreV5): pass).

Companion PR: dahlb/ha_hatch#313 consumes these changes to add full clock display control in Home Assistant (day/night brightness, clock mode, schedule times, tap-to-show). That integration PR is opened as a draft and is blocked on this one being merged and released, since it pins the library version in its manifest.

1. Parse the clock display schedule

The device shadow's clock object reports the full display schedule, but _update_local_state only reads clock.i and clock.flags, so the schedule is unavailable to consumers:

clock: {
  i:            <32-bit: (nighttime16 << 16) | daytime16>
  flags:        <bitmask>
  turnOffMode:  "never" | "custom" | "always"   # never=Always On; custom=off-at-night; always=Always Off
  turnOffAt:    "22:00:00"           # display turns fully OFF
  turnOnAt:     "07:00:00"           # display turns back ON
  turnDimAt:    "22:00:00"           # display switches to NIGHTTIME brightness
  turnBrightAt: "07:00:00"           # display switches to DAYTIME brightness
}

This adds five instance attributes and parses them incrementally (only when the key is present, since shadow deltas are partial):

clock_turn_off_mode: str = None
clock_turn_off_at: str = None
clock_turn_on_at: str = None
clock_turn_dim_at: str = None
clock_turn_bright_at: str = None

They are also added to __repr__. This makes it possible to expose the clock mode (Always On / Off at Night / Always Off) and the on/off/dim/bright times, and to tell which brightness channel (clock_daytime vs clock_nighttime) the device is currently displaying.

2. Fix turn_clock_off wiping day+night brightness

RestoreV5.turn_clock_off() currently writes a hard-coded clock.i = 655 and XOR-toggles the clock ON flag:

self._update({"clock": {"flags": self.flags ^ RIOT_FLAGS_CLOCK_ON, "i": 655}})

On RestoreV5/V4, clock.i is a single 32-bit value packing both brightnesses: (nighttime_16bit << 16) | daytime_16bit (see pack_dual_percentages / unpack_dual_percentages in the same module). 655 unpacks to nighttime = 0%, daytime = 1%, so turning the clock off from Home Assistant destroys both stored brightnesses. The device then shows a near-black clock until the user re-sets brightness in the Hatch app.

Two problems:

  1. i: 655 overwrites the packed brightnesses instead of preserving them.
  2. flags ^ RIOT_FLAGS_CLOCK_ON (XOR) toggles the bit. If the shadow's flag state is ever out of sync, an "off" call can flip the clock back on.

Fix — clear the ON flag explicitly (AND-NOT instead of XOR) and preserve the stored brightness. clock_nighttime/clock_daytime now default to None (unknown) until the device reports clock.i, and the packed brightness is only re-sent when both channels are known — so an off call that happens before any clock state has arrived clears the flag without zeroing the device's stored brightness:

def turn_clock_off(self):
    _LOGGER.debug("Turn off clock")
    clock = {"flags": self.flags & ~RIOT_FLAGS_CLOCK_ON}
    if self.clock_nighttime is not None and self.clock_daytime is not None:
        clock["i"] = pack_dual_percentages(self.clock_nighttime, self.clock_daytime)
    self._update({"clock": clock})

pack_dual_percentages and RIOT_FLAGS_CLOCK_ON are already defined in this module, so no new imports are needed. set_clock and the clock property fall back to 0 when a channel is still unknown.

Testing

  • Offline unit test (pack/unpack round-trip + buggy-vs-fixed payload): 22/22 pass.
  • Live hardware (RestoreV5, fw 10.1.625):
    • Schedule parsing: device shadow reports turnOffMode, turnOffAt, turnOnAt, turnDimAt, turnBrightAt; all five now surface on the device object.
    • Active channel: with turnOffMode="never" (Always On), setting nighttime=10% / daytime=100% made the physical clock display dim — confirming Always On uses the nighttime channel.
    • turn_clock_off: after the patched off-call the device echoed both brightnesses preserved and is_clock_on: 0; turning it back on restored the ON state without touching brightness.

@MasterDevwi MasterDevwi changed the title Parse RestoreV5/V4 clock schedule fields and fix turn_clock_off wiping day+night brightness feat: parse RestoreV5/V4 clock schedule fields and fix turn_clock_off wiping day+night brightness Jul 2, 2026
Comment thread src/hatch_rest_api/restore_v5.py Outdated
@dahlb dahlb merged commit 83cfd5d into dahlb:main Jul 2, 2026
4 checks passed
@MasterDevwi MasterDevwi deleted the restore-v5-clock-schedule branch July 2, 2026 16:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants