Provide ability to select nexia RoomIQ sensors (#144278)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
John Hillery 2025-05-14 00:16:05 -04:00 committed by GitHub
parent 6bc6733c40
commit 9729f1f38b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 1233 additions and 4 deletions

View File

@ -65,6 +65,9 @@
"hold": {
"name": "Hold"
},
"room_iq_sensor": {
"name": "Include {sensor_name}"
},
"emergency_heat": {
"name": "Emergency heat"
}

View File

@ -2,14 +2,19 @@
from __future__ import annotations
from collections.abc import Iterable
import functools as ft
from typing import Any
from nexia.const import OPERATION_MODE_OFF
from nexia.roomiq import NexiaRoomIQHarmonizer
from nexia.sensor import NexiaSensor
from nexia.thermostat import NexiaThermostat
from nexia.zone import NexiaThermostatZone
from homeassistant.components.switch import SwitchEntity
from homeassistant.core import HomeAssistant
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import Event, HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import NexiaDataUpdateCoordinator
@ -17,6 +22,14 @@ from .entity import NexiaThermostatEntity, NexiaThermostatZoneEntity
from .types import NexiaConfigEntry
async def _stop_harmonizers(
_: Event, harmonizers: Iterable[NexiaRoomIQHarmonizer]
) -> None:
"""Run the shutdown methods when preparing to stop."""
for harmonizer in harmonizers:
await harmonizer.async_shutdown() # Never suspends
async def async_setup_entry(
hass: HomeAssistant,
config_entry: NexiaConfigEntry,
@ -25,7 +38,8 @@ async def async_setup_entry(
"""Set up switches for a Nexia device."""
coordinator = config_entry.runtime_data
nexia_home = coordinator.nexia_home
entities: list[NexiaHoldSwitch | NexiaEmergencyHeatSwitch] = []
entities: list[SwitchEntity] = []
room_iq_zones: dict[int, NexiaRoomIQHarmonizer] = {}
for thermostat_id in nexia_home.get_thermostat_ids():
thermostat: NexiaThermostat = nexia_home.get_thermostat_by_id(thermostat_id)
if thermostat.has_emergency_heat():
@ -33,8 +47,18 @@ async def async_setup_entry(
for zone_id in thermostat.get_zone_ids():
zone: NexiaThermostatZone = thermostat.get_zone_by_id(zone_id)
entities.append(NexiaHoldSwitch(coordinator, zone))
if len(zone_sensors := zone.get_sensors()) > 1:
entities.extend(
NexiaRoomIQSwitch(coordinator, zone, sensor, room_iq_zones)
for sensor in zone_sensors
)
async_add_entities(entities)
if room_iq_zones:
listener = ft.partial(_stop_harmonizers, harmonizers=room_iq_zones.values())
config_entry.async_on_unload(
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, listener)
)
class NexiaHoldSwitch(NexiaThermostatZoneEntity, SwitchEntity):
@ -68,6 +92,49 @@ class NexiaHoldSwitch(NexiaThermostatZoneEntity, SwitchEntity):
self._signal_zone_update()
class NexiaRoomIQSwitch(NexiaThermostatZoneEntity, SwitchEntity):
"""Provides Nexia RoomIQ sensor switch support."""
_attr_translation_key = "room_iq_sensor"
def __init__(
self,
coordinator: NexiaDataUpdateCoordinator,
zone: NexiaThermostatZone,
sensor: NexiaSensor,
room_iq_zones: dict[int, NexiaRoomIQHarmonizer],
) -> None:
"""Initialize the RoomIQ sensor switch."""
super().__init__(coordinator, zone, f"{sensor.id}_room_iq_sensor")
self._attr_translation_placeholders = {"sensor_name": sensor.name}
self._sensor_id = sensor.id
if zone.zone_id in room_iq_zones:
self._harmonizer = room_iq_zones[zone.zone_id]
else:
self._harmonizer = NexiaRoomIQHarmonizer(
zone, coordinator.async_refresh, self._signal_zone_update
)
room_iq_zones[zone.zone_id] = self._harmonizer
@property
def is_on(self) -> bool:
"""Return if the sensor is part of the zone average temperature."""
if self._harmonizer.request_pending():
return self._sensor_id in self._harmonizer.selected_sensor_ids
return self._zone.get_sensor_by_id(self._sensor_id).weight > 0.0
async def async_turn_on(self, **kwargs: Any) -> None:
"""Include this sensor."""
self._harmonizer.trigger_add_sensor(self._sensor_id)
self._signal_zone_update()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Remove this sensor."""
self._harmonizer.trigger_remove_sensor(self._sensor_id)
self._signal_zone_update()
class NexiaEmergencyHeatSwitch(NexiaThermostatEntity, SwitchEntity):
"""Provides Nexia emergency heat switch support."""

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,74 @@
"""The switch tests for the nexia platform."""
from homeassistant.const import STATE_ON
from freezegun.api import FrozenDateTimeFactory
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
EVENT_HOMEASSISTANT_STOP,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_OFF,
STATE_ON,
Platform,
)
from homeassistant.core import HomeAssistant
from .util import async_init_integration
from tests.common import async_fire_time_changed
async def test_hold_switch(hass: HomeAssistant) -> None:
"""Test creation of the hold switch."""
await async_init_integration(hass)
assert hass.states.get("switch.nick_office_hold").state == STATE_ON
async def test_nexia_sensor_switch(
hass: HomeAssistant, freezer: FrozenDateTimeFactory
) -> None:
"""Test NexiaRoomIQSensorSwitch."""
await async_init_integration(hass, house_fixture="nexia/sensors_xl1050_house.json")
sw1_id = f"{Platform.SWITCH}.center_nativezone_include_center"
sw1 = {ATTR_ENTITY_ID: sw1_id}
sw2_id = f"{Platform.SWITCH}.center_nativezone_include_upstairs"
sw2 = {ATTR_ENTITY_ID: sw2_id}
# Switch starts out on.
assert (entity_state := hass.states.get(sw1_id)) is not None
assert entity_state.state == STATE_ON
# Turn switch off.
await hass.services.async_call(SWITCH_DOMAIN, SERVICE_TURN_OFF, sw1, blocking=True)
assert hass.states.get(sw1_id).state == STATE_OFF
# Turn switch back on.
await hass.services.async_call(SWITCH_DOMAIN, SERVICE_TURN_ON, sw1, blocking=True)
assert hass.states.get(sw1_id).state == STATE_ON
# The other switch also starts out on.
assert (entity_state := hass.states.get(sw2_id)) is not None
assert entity_state.state == STATE_ON
# Turn both switches off, an invalid combination.
await hass.services.async_call(SWITCH_DOMAIN, SERVICE_TURN_OFF, sw1, blocking=True)
await hass.services.async_call(SWITCH_DOMAIN, SERVICE_TURN_OFF, sw2, blocking=True)
assert hass.states.get(sw1_id).state == STATE_OFF
assert hass.states.get(sw2_id).state == STATE_OFF
# Wait for switches to revert to device status.
freezer.tick(6)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get(sw1_id).state == STATE_ON
assert hass.states.get(sw2_id).state == STATE_ON
# Turn switch off.
await hass.services.async_call(SWITCH_DOMAIN, SERVICE_TURN_OFF, sw2, blocking=True)
assert hass.states.get(sw2_id).state == STATE_OFF
# Exercise shutdown path.
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
assert hass.states.get(sw2_id).state == STATE_ON

View File

@ -17,10 +17,11 @@ async def async_init_integration(
hass: HomeAssistant,
skip_setup: bool = False,
exception: Exception | None = None,
*,
house_fixture="nexia/mobile_houses_123456.json",
) -> MockConfigEntry:
"""Set up the nexia integration in Home Assistant."""
house_fixture = "nexia/mobile_houses_123456.json"
session_fixture = "nexia/session_123456.json"
sign_in_fixture = "nexia/sign_in.json"
set_fan_speed_fixture = "nexia/set_fan_speed_2293892.json"