Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/release-notes/2.0.0b16.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ See the cover docs for the details and the known limits.

- **`myhome.send_message` and `myhome.sync_time` reject unknown keys.** A misspelt key such as `gw_mac` or a missing `message` raises a validation error instead of sending the frame to the default gateway; both services get text selectors in the visual editor (#189).
- **Climate zones show the setpoint in force (dimension 12).** When the thermostat regulates to another setpoint than the commanded one (dimension 14; 23.5 in force while 21.0 commanded on an F461), the entity and `local_target_temperature` now show the one in force instead of the commanded value plus the knob offset. Known limit: once a zone has reported dimension 12, a dimension 14 that is not the echo of a Home Assistant write does not move the slider until the next dimension 12 report or poll (see Known Limitations).
- **Event session liveness probe.** After 10 minutes without a bus frame on the event session, `*#13**0##` goes out on the command session; gateways that echo the reply on the event session (F461) have the session recreated when the echo is missing, and a repair issue `event_session_silent` is raised after three misses in a row. Event-triggered zone polls no longer block the event reader on a full send queue (#123, #135, #158, #164).
- **`myhome.sync_time` refuses time zones with a half-hour offset** (e.g. `Asia/Kolkata`, UTC+5:30) with a translated error. The WHO 13 time-zone field carries whole hours only; b15 (on OWNd 2.0.0b10) set the gateway clock with a truncated offset without saying so (#764).
- **`advanced: true` on a standard F411 shutter now logs a startup warning.** Those actuators have no position reporting, so the position stayed at 50 % and calibration was refused; the error now says to remove `advanced: true` (#761, #762).

Expand Down
10 changes: 10 additions & 0 deletions custom_components/myhome/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,16 @@ def claim_in_flight_dimension_where(self, who: int, dimension: int) -> str | Non
return standby._command_pool.claim_in_flight_dimension_where(who, dimension)
return self._command_pool.claim_in_flight_dimension_where(who, dimension)

def send_status_request_nowait(self, message: OWNCommand) -> asyncio.Future[float] | None:
"""Queue a status request without blocking; None when the send queue is full."""
delegated = self._delegated_target(message)
if delegated is not None:
return delegated.send_status_request_nowait(message)
standby = self._failover_target(message)
if standby is not None:
return standby.send_status_request_nowait(message)
return self._command_pool.send_status_request_nowait(message)

def _known_light_areas(self) -> list[str]:
"""Return list of known light areas."""
return self._resync_manager.known_light_areas()
Expand Down
8 changes: 7 additions & 1 deletion custom_components/myhome/gateway_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,13 @@ async def process_message(self, message: Any) -> None:
)
from OWNd.message import OWNHeatingCommand as OWNHCmd

await self.handler.send_status_request(OWNHCmd.status(where))
# Never block the event read loop on a full send queue: skip the poll instead.
if self.handler.send_status_request_nowait(OWNHCmd.status(where)) is None:
self._logger.debug(
"%s Send queue full; zone %s status poll skipped",
self.handler.log_id,
where,
)
elif isinstance(message, OWNCENPlusEvent):
event = None
if message.is_short_pressed:
Expand Down
195 changes: 188 additions & 7 deletions custom_components/myhome/gateway_sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_send
from OWNd.connection import OWNCommandSession, OWNEventSession, OWNGateway
from OWNd.message import OWNCommand, OWNMessage
from OWNd.message import OWNCommand, OWNGatewayCommand, OWNGatewayEvent, OWNMessage, OWNSignaling

from .const import LOGGER, area_of_where, is_apl_address
from .expose import is_exposed_on_bus
Expand All @@ -40,6 +40,31 @@
# leaving sessions open indefinitely. Installations desiring a generous hard ceiling
# for "TCP up, bus dead" conditions can override this constant (e.g. 43200.0 / 12h).
EVENT_INACTIVITY_TIMEOUT: float | None = None
# End-to-end liveness probe (#123, #135, #158, #164): after EVENT_PROBE_IDLE seconds
# without a bus frame (keepalive ACKs do not count), `*#13**0##` goes out on the
# command session and its reply must also arrive on the event session within
# EVENT_PROBE_TIMEOUT (live F461 2.0.11). A miss recycles the event session; after
# EVENT_PROBE_REPAIR_AFTER consecutive misses a repair issue is raised. A gateway is
# only recycled once it has echoed a probe, or when its model is known to echo,
# so a gateway that keeps the reply on the command session is never cycled.
EVENT_PROBE_IDLE: float = 600.0
EVENT_PROBE_TIMEOUT: float = 30.0
EVENT_PROBE_REPAIR_AFTER: int = 3
# Models seen to deliver the ``*#13**0##`` reply on the event session (physical F461
# 2.0.11). Fallback for OWNd profiles without an ``echoes_event_session_probe`` flag;
# the flag belongs in OWNd (#381 pattern).
EVENT_PROBE_ECHO_MODELS = frozenset({"F461"})
Comment on lines +53 to +56

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — MYHOMESERVER1 missing from EVENT_PROBE_ECHO_MODELS (and #123 / #164 coverage note):

  • PR test(trace): ingest live F461 2.0.11 2026-10-23 capture and MyHomeServer1 3.87.13 reads behind OWNd #136-#142 and MyHOME #791-#798 #799 (tests/fixtures/traces/myhomeserver1_live_2026-10-24) proved on physical MyHomeServer1 3.87.13 that *#13**0## is echoed on evt (run1_identify, run3_reads_lights_addresses_humidity). Without "MYHOMESERVER1" in EVENT_PROBE_ECHO_MODELS (until OWNd carries echoes_event_session_probe), a MyHomeServer1 whose event session is already silent from startup/reload has _probe_echo_seen == False and will never be recycled or raise event_session_silent. Once the 999 timezone handling is updated, "MYHOMESERVER1" should be included here (and noted in .github/release-notes/2.0.0b16.md).
  • Also note for issue linking: #123 was reported on an F454 and #164 on an F453, whose existing traces in tests/fixtures/traces/ are merged BusMonitor captures that do not distinguish cmd vs evt delivery for *#13**0##. Worth noting in the PR body / release notes that F454/F453 are only covered if they echo *#13**0## on evt.



def _is_time_echo(message: object) -> bool:
"""Whether ``message`` is the gateway's ``*#13**0*…##`` time reply, the probe's echo.

OWNd parses it as an ``OWNGatewayEvent`` with dimension 0; the raw-string prefix is
the fallback for a reply the pinned OWNd hands back unparsed.
"""
if isinstance(message, OWNGatewayEvent):
return message.dimension == 0
return str(message).startswith("*#13**0*")
Comment on lines +65 to +67

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Pinned OWNd==2.0.0b11 logs WARNING every 10 minutes on unparsed 0530 / 124 echoes (+ strict-typing nit):

  1. Against manifest.json's pinned OWNd==2.0.0b11 (without OWNd Fix/invalid is on for light #144), *#13**0*20*42*07*0530## (seen in Firmware Oracle who13-clock-readback on F450) and *#13**0*...*124## raise ValueError in OWNMessage.parse(), so OWNEventSession.get_next() yields the raw str. Line 67 counts it for the probe, but _dispatch_event_frames (line 329) still passes that raw str to self.handler._process_message(message) → gateway_events.py:274, emitting WARNING: Data received is not a message: '*#13**0*20*42*07*0530##' every 10 minutes on quiet plants in half-hour timezones. Either suppress that warning in gateway_events.py when _is_time_echo(message) matches, or coordinate bumping manifest.json once OWNd Fix/invalid is on for light #144 is released.
  2. Nit: return bool(message.dimension == 0) on line 66 avoids mypy --strict [no-any-return] when running scripts/typing_ratchet.py in editable environments where OWNd is untyped.



def _resolve_written(task: dict[str, Any], when: float) -> None:
Expand Down Expand Up @@ -82,6 +107,13 @@ def __init__(
self._event_session_ready: asyncio.Event = asyncio.Event()
self._event_watchdog: asyncio.Timeout | None = None
self.is_connected: bool = False
self._last_rx_at: float = time.monotonic()
self._last_echo_at: float = 0.0
self._last_probe_at: float = 0.0
self._rx_event: asyncio.Event = asyncio.Event()
self._probe_echo_seen: bool = False
self._probe_misses: int = 0
self._probe_recycle: bool = False

@property
def gateway(self) -> OWNGateway:
Expand Down Expand Up @@ -110,7 +142,8 @@ def _update_event_watchdog(self, *, progress: bool) -> None:
a bare state change only arms it when it is not already running.
"""
watchdog = self._event_watchdog
if watchdog is None or watchdog.expired():
if watchdog is None or watchdog.expired() or self._probe_recycle:
# A recycle requested by the liveness probe must not be disarmed by a late frame.
return
if self.is_connected:
watchdog.reschedule(None)
Expand Down Expand Up @@ -192,7 +225,9 @@ async def _run_event_session(self) -> bool:
await self._read_event_session(_event_session)
return False
except Exception as err:
if isinstance(err, TimeoutError) and watchdog.expired():
if isinstance(err, TimeoutError) and watchdog.expired() and self._probe_recycle:
LOGGER.debug("%s Event session closed by the liveness probe.", self.log_id)
elif isinstance(err, TimeoutError) and watchdog.expired():
LOGGER.warning(
"%s Event session stalled: disconnected with no reconnect "
"progress for %ss.",
Expand All @@ -203,6 +238,7 @@ async def _run_event_session(self) -> bool:
LOGGER.exception("%s Event listener failed.", self.log_id)
finally:
self._event_watchdog = None
self._probe_recycle = False
with contextlib.suppress(Exception):
await asyncio.shield(_event_session.close())
return not self._terminate_listener
Expand Down Expand Up @@ -245,11 +281,28 @@ async def _read_event_session(self, _event_session: OWNEventSession) -> None:
self.log_id,
)
self._update_event_watchdog(progress=True)
self._last_rx_at = time.monotonic()
probe = asyncio.get_running_loop().create_task(
self._liveness_probe(), name=f"myhome_{self.handler.mac}_event_probe"
)
try:
await self._dispatch_event_frames(_event_session, session_is_open)
finally:
probe.cancel()
with contextlib.suppress(asyncio.CancelledError, Exception):
await probe

async def _dispatch_event_frames(self, _event_session: OWNEventSession, session_is_open: Any) -> None:
"""Read ``_event_session`` until the listener terminates or the read raises."""
was_reachable = True
while not self._terminate_listener:
message = await _event_session.get_next()
self._update_event_watchdog(progress=True)
if message is not None and not isinstance(message, OWNSignaling):
self._last_rx_at = time.monotonic()
if _is_time_echo(message):
self._last_echo_at = self._last_rx_at
self._rx_event.set()
Comment on lines +301 to +305

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — event_session_silent stays stuck while the bus is active and orphans across entry reloads:

  1. Stuck during active bus traffic: _probe_misses and event_session_silent_{entry_id} are only cleared inside _probe_once() (lines 387–390). If 3 missed probes raise event_session_silent overnight and the session recovers, normal daytime bus frames arriving every few minutes keep advancing self._last_rx_at here, postponing _liveness_probe() (delay = max(_last_rx_at, _last_probe_at) + 600 - now). The "gateway stopped reporting bus events" warning remains stuck in the UI while bus events are actively flowing, until 10 minutes of total silence allow a probe to run.
  2. Orphaned on reload: On config-entry reload (e.g. Options save / Reconfigure), async_unload_entry does not delete event_session_silent_{entry_id}, and the new EventSessionRunner initializes self._probe_misses = 0. When the next probe succeeds, if self._probe_misses: at line 387 is False, so _delete_probe_issue() is never called and the issue stays orphaned for the rest of HA's uptime.

Clearing _probe_misses (and withdrawing any raised probe issue via a dedicated _probe_issue_raised: bool flag or unconditional delete on first healthy frame + in async_unload_entry) right here when any non-signaling frame arrives resolves both issues cleanly.

if message is None:
reachable = session_is_open(_event_session)
if reachable != was_reachable:
Expand Down Expand Up @@ -277,6 +330,106 @@ async def _read_event_session(self, _event_session: OWNEventSession) -> None:
except Exception:
LOGGER.exception("%s Failed to process `%s`.", self.log_id, message)

async def _liveness_probe(self) -> None:
"""Probe a silent event session end to end, for as long as it is open."""
while not self._terminate_listener:
idle = float(EVENT_PROBE_IDLE)
delay = max(self._last_rx_at, self._last_probe_at) + idle - time.monotonic()
if delay > 0:
await asyncio.sleep(delay)
continue
if not self.is_connected:
# A lost socket is OWNd's and the stall watchdog's business.
self._last_probe_at = time.monotonic()
continue
try:
if not await self._probe_once():
return
except Exception:
LOGGER.exception("%s Event session liveness probe failed.", self.log_id)

def _probe_echo_expected(self) -> bool:
"""Whether a missing echo means a dead event session on this gateway.

A seen echo is the strongest evidence; then the OWNd profile flag
``echoes_event_session_probe`` (``None`` = unverified); then the model table.
"""
profile = getattr(self.handler, "profile", None)
flag = getattr(profile, "echoes_event_session_probe", None)
model = str(getattr(self.handler, "model", "") or "").strip().upper()
if self._probe_echo_seen:
return True
if isinstance(flag, bool):
return flag
Comment on lines +360 to +363

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Explicit profile.echoes_event_session_probe is False should not be overridden by _probe_echo_seen:

Per OpenWebNet Encyclopedia ownkb:chunk:r000309, WHO 13 Dimension 0 value frames can appear on monitor sessions as server-originated or external-supervisor traffic. If a gateway whose OWNd profile is hardware-verified not to echo its own command-session *#13**0## (flag is False) happens to deliver a spontaneous/supervisor *#13**0*...## frame inside a 30 s probe window once, _probe_echo_seen latches True for the lifetime of EventSessionRunner and will falsely recycle that gateway every 10 minutes on quiet nights.

Consider checking if isinstance(flag, bool): return flag before if self._probe_echo_seen: return True so _probe_echo_seen only resolves unverified (flag is None) profiles.

return model in EVENT_PROBE_ECHO_MODELS

async def _probe_once(self) -> bool:
"""Send one probe; False when it was not echoed and the session is being recycled."""
self._last_probe_at = time.monotonic()
silent_for = self._last_probe_at - self._last_rx_at
# Never wait for room in a full queue: the probe is retried after the next idle period.
written = self.handler._command_pool.send_status_request_nowait(OWNGatewayCommand.request_time())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High — Active *#13**0## probing triggers unconfigured_timezone (999) on MyHomeServer1:

Prior to this PR, OWNGatewayCommand.request_time() (*#13**0##) and request_datetime() (*#13**22##) were never actively sent by MyHOME. Sending *#13**0## every 10 minutes on idle plants feeds the echoed OWNGatewayEvent(dimension=0) into GatewayIdentityMixin._handle_gateway_diagnostics (custom_components/myhome/identity.py:375-383), which raises unconfigured_timezone whenever dim_val[3] == "999" and only exempts F461 (WHO 1013 code 134).

Both the Firmware Oracle (F460, F461, MyHomeServer1) and PR #799's live MyHomeServer1 3.87.13 capture (tests/fixtures/traces/myhomeserver1_live_2026-10-24: *#13**0*16*19*49*999##, *#13**0*16*22*38*999## on evt) return 999 in Dimension 0 on modern Home + Project firmware (which also has no legacy web clock UI at http://<gateway_ip>). Please ensure MyHomeServer1 (and F460 if applicable) is exempted or that internal liveness probes do not raise false unconfigured_timezone repairs every 10 minutes.

if written is not None:
await asyncio.wait({written}, timeout=float(EVENT_PROBE_TIMEOUT))
if written is None or not written.done() or written.cancelled() or written.exception() is not None:
LOGGER.debug("%s Liveness probe not sent: command queue full or session unavailable.", self.log_id)
return True
written_at = written.result()
deadline = written_at + float(EVENT_PROBE_TIMEOUT)
while self._last_rx_at <= written_at and (remaining := deadline - time.monotonic()) > 0:
self._rx_event.clear()
with contextlib.suppress(TimeoutError):
await asyncio.wait_for(self._rx_event.wait(), timeout=remaining)
if self._last_rx_at > written_at:
# Any frame proves the event path; only the time reply proves this gateway echoes.
if self._last_echo_at > written_at:
self._probe_echo_seen = True
if self._probe_misses:
LOGGER.info("%s Event session answers the liveness probe again.", self.log_id)
self._delete_probe_issue()
self._probe_misses = 0
return True
if not self._probe_echo_expected():
LOGGER.debug(
"%s `*#13**0##` was not echoed on the event session; this gateway has never "
"echoed it, so the session is kept.",
self.log_id,
)
return True
self._probe_misses += 1
Comment on lines +379 to +399

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Re-check self.is_connected after waiting EVENT_PROBE_TIMEOUT:

_liveness_probe checks if not self.is_connected: before calling _probe_once(), which then waits up to 30 s here. If the TCP socket drops a few seconds after written_at (so self.is_connected flips False and OWNEventSession.get_next() is already reconnecting under the stall watchdog), waking at deadline without re-checking self.is_connected counts a false _probe_misses += 1 and forces _probe_recycle = True mid-reconnect.

Adding if not self.is_connected: return True before line 392 keeps socket drops strictly under OWNd and the stall watchdog.

LOGGER.warning(
"%s Event session silent for %.0fs and `*#13**0##` was not echoed within %.0fs "
"(miss %d); recreating the event session.",
self.log_id,
silent_for,
float(EVENT_PROBE_TIMEOUT),
self._probe_misses,
)
if self._probe_misses >= int(EVENT_PROBE_REPAIR_AFTER):
self._create_probe_issue()
self._probe_recycle = True
watchdog = self._event_watchdog
if watchdog is not None and not watchdog.expired():
watchdog.reschedule(asyncio.get_running_loop().time())
return False

def _create_probe_issue(self) -> None:
from .repairs import async_create_event_session_silent_issue

entry = getattr(self.handler, "config_entry", None)
entry_id = getattr(entry, "entry_id", None)
if isinstance(entry_id, str):
async_create_event_session_silent_issue(
self.handler.hass, entry_id, str(getattr(entry, "title", "") or self.handler.mac), self._probe_misses
)

def _delete_probe_issue(self) -> None:
from .repairs import async_delete_event_session_silent_issue

entry_id = getattr(getattr(self.handler, "config_entry", None), "entry_id", None)
if isinstance(entry_id, str):
async_delete_event_session_silent_issue(self.handler.hass, entry_id)

def close(self) -> None:
"""Signal listener termination and unblock waiting workers."""
self._terminate_listener = True
Expand Down Expand Up @@ -1217,15 +1370,28 @@ async def send_status_request(self, message: OWNCommand) -> asyncio.Future[float
"""Queue a status request; the returned future resolves to the monotonic write time."""
return await self._enqueue(message, is_status_request=True)

async def _enqueue(
self,
def send_status_request_nowait(self, message: OWNCommand) -> asyncio.Future[float] | None:
"""Queue a status request without waiting for room; None when the queue is full or closed.

For callers on the event read loop, which must never block on a full queue.
"""
envelope = self._envelope(message, is_status_request=True)
try:
self.send_buffer.put_nowait(envelope)
except (asyncio.QueueFull, getattr(asyncio, "QueueShutDown", asyncio.QueueFull)):
envelope["written"].cancel()
return None
return envelope["written"] # type: ignore[no-any-return]

@staticmethod
def _envelope(
message: OWNCommand,
*,
is_status_request: bool,
is_fade_step: bool = False,
is_cancel: bool = False,
) -> asyncio.Future[float]:
"""Put a frame on the send queue and hand back its delivery future."""
) -> dict[str, Any]:
"""The queue item for ``message``, with a fresh delivery future."""
written: asyncio.Future[float] = asyncio.get_running_loop().create_future()
envelope: dict[str, Any] = {
"message": message,
Expand All @@ -1235,6 +1401,21 @@ async def _enqueue(
}
if is_fade_step:
envelope["is_fade_step"] = True
return envelope

async def _enqueue(
self,
message: OWNCommand,
*,
is_status_request: bool,
is_fade_step: bool = False,
is_cancel: bool = False,
) -> asyncio.Future[float]:
"""Put a frame on the send queue and hand back its delivery future."""
envelope = self._envelope(
message, is_status_request=is_status_request, is_fade_step=is_fade_step, is_cancel=is_cancel
)
written: asyncio.Future[float] = envelope["written"]
await self.send_buffer.put(envelope)
LOGGER.debug(
"%s Message `%s` was successfully queued.",
Expand Down
21 changes: 21 additions & 0 deletions custom_components/myhome/repairs.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
ISSUE_GATEWAY_IDENTITY = "gateway_identity_mismatch"
ISSUE_UNKNOWN_GATEWAY_MODEL = "unknown_gateway_model"
ISSUE_UNCONFIGURED_TIMEZONE = "unconfigured_timezone"
ISSUE_EVENT_SESSION_SILENT = "event_session_silent"

ISSUE_GATEWAY_IDENTITY_CORRECTED = "gateway_identity_corrected"
ISSUE_INCOMPATIBLE_DECODER = "incompatible_decoder_platform"
Expand Down Expand Up @@ -104,6 +105,26 @@ def async_delete_unconfigured_timezone_issue(hass: HomeAssistant, entry_id: str)
async_delete_issue(hass, DOMAIN, f"{ISSUE_UNCONFIGURED_TIMEZONE}_{entry_id}")


def async_create_event_session_silent_issue(
hass: HomeAssistant, entry_id: str, gateway_name: str, misses: int
) -> None:
"""Raise a repair when the event session repeatedly fails the end-to-end liveness probe."""
async_create_issue(
hass,
DOMAIN,
f"{ISSUE_EVENT_SESSION_SILENT}_{entry_id}",
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key=ISSUE_EVENT_SESSION_SILENT,
translation_placeholders={"gateway": gateway_name, "misses": str(misses)},
)


def async_delete_event_session_silent_issue(hass: HomeAssistant, entry_id: str) -> None:
"""Delete the silent event session issue once a probe is echoed again."""
async_delete_issue(hass, DOMAIN, f"{ISSUE_EVENT_SESSION_SILENT}_{entry_id}")


def async_create_identity_issue(
hass: HomeAssistant, entry_id: str, configured: str, reported: str, code: str, source: str, official: bool
) -> None:
Expand Down
4 changes: 4 additions & 0 deletions custom_components/myhome/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,10 @@
"title": "Authentication failed for MyHOME gateway {gateway}",
"description": "The MyHOME gateway refused the configured OpenWebNet password or HMAC negotiation. Please check your credentials using the reconfigure flow."
},
"event_session_silent": {
"title": "MyHOME gateway {gateway} stopped reporting bus events",
"description": "The event connection to {gateway} stayed open, but a gateway time request sent on the command connection was not echoed on it {misses} times in a row, even after the event connection was recreated. Home Assistant may miss light, cover and sensor changes.\n\nCheck that no other client exhausts the gateway's sessions and restart the gateway. This issue clears itself when the gateway answers again."
},
"unconfigured_timezone": {
"title": "Unconfigured timezone on MyHOME gateway ({gateway})",
"description": "The MyHOME gateway {gateway} is reporting an unconfigured timezone (placeholder code '999'). This can cause date and time parsing failures or dropped gateway diagnostic messages.\n\nPlease log into the gateway web UI or MyHOME Suite and configure a valid timezone, then restart the gateway."
Expand Down
Loading
Loading