From 5459f9d998b49790dd6b07190ef6ce712ba4f1aa Mon Sep 17 00:00:00 2001 From: Bluetooth Devices Bot Date: Tue, 23 Jun 2026 06:08:55 +0000 Subject: [PATCH 1/2] fix: add late-arriving thermostats and automations on update _update_devices and _update_automations only patched entities already known from the first fetch; a thermostat or automation that first appears on a later poll was silently dropped. Some systems under-report devices on the initial poll, so this is the same class of bug as #149 (late zones) one level up. Make both methods add-only: append any id not yet tracked while preserving existing objects that consumers hold references to. --- nexia/home.py | 68 ++++++++++++++++++++++++++++++---------------- tests/test_home.py | 52 +++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 24 deletions(-) diff --git a/nexia/home.py b/nexia/home.py index 6ad8556..8eb43a3 100644 --- a/nexia/home.py +++ b/nexia/home.py @@ -462,24 +462,35 @@ def _update_devices(self): _LOGGER.debug("Found %d potential thermostat devices", len(children)) if self.thermostats is None: self.thermostats = [] - for child in children: - nexia_thermostat = NexiaThermostat(self, child) - zone_ids = nexia_thermostat.get_zone_ids() - if not zone_ids: - # No zones (likely an xl624 which is not supported at this time) - _LOGGER.warning( - "Skipping thermostat %s (%s) - no zones found", - child.get("id", "Unknown"), - child.get("name", "Unknown"), - ) - continue - _LOGGER.debug( - "Adding thermostat %s (%s) with %d zones", - nexia_thermostat.thermostat_id, - nexia_thermostat.get_name(), - len(zone_ids), + + # Append thermostats that first appear on a later fetch. Some systems + # under-report devices on the initial poll, so a thermostat may be + # absent at construction and only show up on a subsequent update. The + # library never removes thermostats, so add-only is safe — mirrors the + # late-zone handling in NexiaThermostat.update_thermostat_json (#149). + existing_ids = {thermostat.thermostat_id for thermostat in self.thermostats} + for child in children: + child_id = child.get("id") + if child_id is None or child_id in existing_ids: + continue + nexia_thermostat = NexiaThermostat(self, child) + zone_ids = nexia_thermostat.get_zone_ids() + if not zone_ids: + # No zones (likely an xl624 which is not supported at this time) + _LOGGER.warning( + "Skipping thermostat %s (%s) - no zones found", + child.get("id", "Unknown"), + child.get("name", "Unknown"), ) - self.thermostats.append(nexia_thermostat) + continue + _LOGGER.debug( + "Adding thermostat %s (%s) with %d zones", + nexia_thermostat.thermostat_id, + nexia_thermostat.get_name(), + len(zone_ids), + ) + self.thermostats.append(nexia_thermostat) + existing_ids.add(child_id) thermostat_updates_by_id = { child["id"]: child for child in children if "id" in child @@ -492,18 +503,27 @@ def _update_devices(self): def _update_automations(self) -> None: self.last_update = datetime.datetime.now() # noqa: DTZ005 # naive local time is the existing public behavior + assert self.automations_json is not None # noqa: S101 # type-narrowing invariant if self.automations is None: self.automations = [] - for automation_json in self.automations_json: - self.automations.append(NexiaAutomation(self, automation_json)) - return - automation_updates_by_id = {} - assert self.automations_json is not None # noqa: S101 # type-narrowing invariant + # Append automations that first appear on a later fetch, for the same + # reason as thermostats above: the library never removes automations, + # so add-only keeps a late-arriving automation from being dropped. + existing_ids = {automation.automation_id for automation in self.automations} for automation_json in self.automations_json: - automation_updates_by_id[automation_json["id"]] = automation_json - + automation_id = automation_json.get("id") + if automation_id is None or automation_id in existing_ids: + continue + self.automations.append(NexiaAutomation(self, automation_json)) + existing_ids.add(automation_id) + + automation_updates_by_id = { + automation_json["id"]: automation_json + for automation_json in self.automations_json + if "id" in automation_json + } for automation in self.automations: if automation.automation_id in automation_updates_by_id: automation.update_automation_json( diff --git a/tests/test_home.py b/tests/test_home.py index 029a3d4..c40e5ce 100644 --- a/tests/test_home.py +++ b/tests/test_home.py @@ -186,6 +186,58 @@ async def test_update(aiohttp_session: aiohttp.ClientSession) -> None: nexia.update_from_json(devices_json) +async def test_late_thermostat_and_automation_are_added( + aiohttp_session: aiohttp.ClientSession, +) -> None: + """A thermostat/automation that first appears on a later fetch must be added. + + Some systems under-report devices on the initial poll, so an entity can be + absent at construction and only show up on a subsequent update. _update_devices + and _update_automations must append the late arrivals instead of only patching + already-known ids (#149 fixed this for zones; the same gap existed one level up, + for whole thermostats and automations). Existing objects must be preserved so + consumers that hold references (e.g. Home Assistant) keep working. + """ + reference = NexiaHome(aiohttp_session) + house = json.loads(await load_fixture("mobile_houses_123456.json")) + reference.update_from_json(house) + full_devices = list(reference.devices_json) + full_automations = list(reference.automations_json) + late_thermostat_id = reference.get_thermostat_ids()[-1] + late_automation_id = reference.get_automation_ids()[-1] + + # Simulate an initial poll that under-reported one thermostat + one automation. + nexia = NexiaHome(aiohttp_session) + nexia.devices_json = [ + d for d in full_devices if d.get("id") != late_thermostat_id + ] + nexia.automations_json = [ + a for a in full_automations if a.get("id") != late_automation_id + ] + nexia._update_devices() + nexia._update_automations() + assert late_thermostat_id not in nexia.get_thermostat_ids() + assert late_automation_id not in nexia.get_automation_ids() + + existing_thermostat = nexia.thermostats[0] + existing_automation = nexia.automations[0] + + # A later poll now reports everything. + nexia.devices_json = full_devices + nexia.automations_json = full_automations + nexia._update_devices() + nexia._update_automations() + + assert late_thermostat_id in nexia.get_thermostat_ids() + assert late_automation_id in nexia.get_automation_ids() + # Pre-existing objects are reused, not rebuilt. + assert nexia.thermostats[0] is existing_thermostat + assert ( + nexia.get_automation_by_id(existing_automation.automation_id) + is existing_automation + ) + + async def test_idle_thermo(aiohttp_session: aiohttp.ClientSession) -> None: """Get methods for an idle thermostat.""" nexia = NexiaHome(aiohttp_session) From 6c7d63d7fe6f356959c7b9c8bdef668ea9dbf691 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 06:09:36 +0000 Subject: [PATCH 2/2] chore(pre-commit.ci): auto fixes --- tests/test_home.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_home.py b/tests/test_home.py index c40e5ce..3de191f 100644 --- a/tests/test_home.py +++ b/tests/test_home.py @@ -208,9 +208,7 @@ async def test_late_thermostat_and_automation_are_added( # Simulate an initial poll that under-reported one thermostat + one automation. nexia = NexiaHome(aiohttp_session) - nexia.devices_json = [ - d for d in full_devices if d.get("id") != late_thermostat_id - ] + nexia.devices_json = [d for d in full_devices if d.get("id") != late_thermostat_id] nexia.automations_json = [ a for a in full_automations if a.get("id") != late_automation_id ]