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": { "hold": {
"name": "Hold" "name": "Hold"
}, },
"room_iq_sensor": {
"name": "Include {sensor_name}"
},
"emergency_heat": { "emergency_heat": {
"name": "Emergency heat" "name": "Emergency heat"
} }

View File

@ -2,14 +2,19 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Iterable
import functools as ft
from typing import Any from typing import Any
from nexia.const import OPERATION_MODE_OFF from nexia.const import OPERATION_MODE_OFF
from nexia.roomiq import NexiaRoomIQHarmonizer
from nexia.sensor import NexiaSensor
from nexia.thermostat import NexiaThermostat from nexia.thermostat import NexiaThermostat
from nexia.zone import NexiaThermostatZone from nexia.zone import NexiaThermostatZone
from homeassistant.components.switch import SwitchEntity 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 homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import NexiaDataUpdateCoordinator from .coordinator import NexiaDataUpdateCoordinator
@ -17,6 +22,14 @@ from .entity import NexiaThermostatEntity, NexiaThermostatZoneEntity
from .types import NexiaConfigEntry 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( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: NexiaConfigEntry, config_entry: NexiaConfigEntry,
@ -25,7 +38,8 @@ async def async_setup_entry(
"""Set up switches for a Nexia device.""" """Set up switches for a Nexia device."""
coordinator = config_entry.runtime_data coordinator = config_entry.runtime_data
nexia_home = coordinator.nexia_home 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(): for thermostat_id in nexia_home.get_thermostat_ids():
thermostat: NexiaThermostat = nexia_home.get_thermostat_by_id(thermostat_id) thermostat: NexiaThermostat = nexia_home.get_thermostat_by_id(thermostat_id)
if thermostat.has_emergency_heat(): if thermostat.has_emergency_heat():
@ -33,8 +47,18 @@ async def async_setup_entry(
for zone_id in thermostat.get_zone_ids(): for zone_id in thermostat.get_zone_ids():
zone: NexiaThermostatZone = thermostat.get_zone_by_id(zone_id) zone: NexiaThermostatZone = thermostat.get_zone_by_id(zone_id)
entities.append(NexiaHoldSwitch(coordinator, zone)) 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) 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): class NexiaHoldSwitch(NexiaThermostatZoneEntity, SwitchEntity):
@ -68,6 +92,49 @@ class NexiaHoldSwitch(NexiaThermostatZoneEntity, SwitchEntity):
self._signal_zone_update() 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): class NexiaEmergencyHeatSwitch(NexiaThermostatEntity, SwitchEntity):
"""Provides Nexia emergency heat switch support.""" """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.""" """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 homeassistant.core import HomeAssistant
from .util import async_init_integration from .util import async_init_integration
from tests.common import async_fire_time_changed
async def test_hold_switch(hass: HomeAssistant) -> None: async def test_hold_switch(hass: HomeAssistant) -> None:
"""Test creation of the hold switch.""" """Test creation of the hold switch."""
await async_init_integration(hass) await async_init_integration(hass)
assert hass.states.get("switch.nick_office_hold").state == STATE_ON 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, hass: HomeAssistant,
skip_setup: bool = False, skip_setup: bool = False,
exception: Exception | None = None, exception: Exception | None = None,
*,
house_fixture="nexia/mobile_houses_123456.json",
) -> MockConfigEntry: ) -> MockConfigEntry:
"""Set up the nexia integration in Home Assistant.""" """Set up the nexia integration in Home Assistant."""
house_fixture = "nexia/mobile_houses_123456.json"
session_fixture = "nexia/session_123456.json" session_fixture = "nexia/session_123456.json"
sign_in_fixture = "nexia/sign_in.json" sign_in_fixture = "nexia/sign_in.json"
set_fan_speed_fixture = "nexia/set_fan_speed_2293892.json" set_fan_speed_fixture = "nexia/set_fan_speed_2293892.json"