Skip to content
Draft
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
68 changes: 44 additions & 24 deletions nexia/home.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down
50 changes: 50 additions & 0 deletions tests/test_home.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,56 @@ 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)
Expand Down
Loading