Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamically add Airzone entities #121891

Merged
merged 13 commits into from
Jul 13, 2024
75 changes: 48 additions & 27 deletions homeassistant/components/airzone/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,33 +82,54 @@ async def async_setup_entry(
"""Add Airzone binary sensors from a config_entry."""
coordinator = entry.runtime_data

binary_sensors: list[AirzoneBinarySensor] = [
AirzoneSystemBinarySensor(
coordinator,
description,
entry,
system_id,
system_data,
)
for system_id, system_data in coordinator.data[AZD_SYSTEMS].items()
for description in SYSTEM_BINARY_SENSOR_TYPES
if description.key in system_data
]

binary_sensors.extend(
AirzoneZoneBinarySensor(
coordinator,
description,
entry,
system_zone_id,
zone_data,
)
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items()
for description in ZONE_BINARY_SENSOR_TYPES
if description.key in zone_data
)

async_add_entities(binary_sensors)
added_systems: set[str] = set()
added_zones: set[str] = set()

def _async_entity_listener() -> None:
"""Handle additions of binary sensors."""

entities: list[AirzoneBinarySensor] = []

systems_data = coordinator.data.get(AZD_SYSTEMS, {})
received_systems = set(systems_data)
new_systems = received_systems - added_systems
if new_systems:
entities.extend(
AirzoneSystemBinarySensor(
coordinator,
description,
entry,
system_zone_id,
systems_data.get(system_zone_id),
)
for system_zone_id in new_systems
for description in SYSTEM_BINARY_SENSOR_TYPES
if description.key in systems_data.get(system_zone_id)
)
added_systems.update(new_systems)

zones_data = coordinator.data.get(AZD_ZONES, {})
received_zones = set(zones_data)
new_zones = received_zones - added_zones
if new_zones:
entities.extend(
AirzoneZoneBinarySensor(
coordinator,
description,
entry,
system_zone_id,
zones_data.get(system_zone_id),
)
for system_zone_id in new_zones
for description in ZONE_BINARY_SENSOR_TYPES
if description.key in zones_data.get(system_zone_id)
)
added_zones.update(new_zones)

async_add_entities(entities)

entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
_async_entity_listener()


class AirzoneBinarySensor(AirzoneEntity, BinarySensorEntity):
Expand Down
34 changes: 24 additions & 10 deletions homeassistant/components/airzone/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,17 +102,31 @@ async def async_setup_entry(
entry: AirzoneConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add Airzone sensors from a config_entry."""
"""Add Airzone climate from a config_entry."""
coordinator = entry.runtime_data
async_add_entities(
AirzoneClimate(
coordinator,
entry,
system_zone_id,
zone_data,
)
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items()
)

added_zones: set[str] = set()

def _async_entity_listener() -> None:
"""Handle additions of climate."""

zones_data = coordinator.data.get(AZD_ZONES, {})
received_zones = set(zones_data)
new_zones = received_zones - added_zones
if new_zones:
async_add_entities(
AirzoneClimate(
coordinator,
entry,
system_zone_id,
zones_data.get(system_zone_id),
)
for system_zone_id in new_zones
)
added_zones.update(new_zones)

entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
_async_entity_listener()


class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
Expand Down
39 changes: 26 additions & 13 deletions homeassistant/components/airzone/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,21 +83,34 @@ async def async_setup_entry(
entry: AirzoneConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add Airzone sensors from a config_entry."""
"""Add Airzone select from a config_entry."""
coordinator = entry.runtime_data

async_add_entities(
AirzoneZoneSelect(
coordinator,
description,
entry,
system_zone_id,
zone_data,
)
for description in ZONE_SELECT_TYPES
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items()
if description.key in zone_data
)
added_zones: set[str] = set()

def _async_entity_listener() -> None:
"""Handle additions of select."""

zones_data = coordinator.data.get(AZD_ZONES, {})
received_zones = set(zones_data)
new_zones = received_zones - added_zones
if new_zones:
async_add_entities(
AirzoneZoneSelect(
coordinator,
description,
entry,
system_zone_id,
zones_data.get(system_zone_id),
)
for system_zone_id in new_zones
for description in ZONE_SELECT_TYPES
if description.key in zones_data.get(system_zone_id)
)
added_zones.update(new_zones)

entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
_async_entity_listener()


class AirzoneBaseSelect(AirzoneEntity, SelectEntity):
Expand Down
49 changes: 34 additions & 15 deletions homeassistant/components/airzone/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,21 +85,37 @@ async def async_setup_entry(
"""Add Airzone sensors from a config_entry."""
coordinator = entry.runtime_data

sensors: list[AirzoneSensor] = [
AirzoneZoneSensor(
coordinator,
description,
entry,
system_zone_id,
zone_data,
)
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items()
for description in ZONE_SENSOR_TYPES
if description.key in zone_data
]
added_zones: set[str] = set()

def _async_entity_listener() -> None:
"""Handle additions of sensors."""

entities: list[AirzoneSensor] = []

zones_data = coordinator.data.get(AZD_ZONES, {})
received_zones = set(zones_data)
new_zones = received_zones - added_zones
if new_zones:
entities.extend(
AirzoneZoneSensor(
coordinator,
description,
entry,
system_zone_id,
zones_data.get(system_zone_id),
)
for system_zone_id in new_zones
for description in ZONE_SENSOR_TYPES
if description.key in zones_data.get(system_zone_id)
)
added_zones.update(new_zones)

async_add_entities(entities)

entities: list[AirzoneSensor] = []

if AZD_HOT_WATER in coordinator.data:
sensors.extend(
entities.extend(
AirzoneHotWaterSensor(
coordinator,
description,
Expand All @@ -110,7 +126,7 @@ async def async_setup_entry(
)

if AZD_WEBSERVER in coordinator.data:
sensors.extend(
entities.extend(
AirzoneWebServerSensor(
coordinator,
description,
Expand All @@ -120,7 +136,10 @@ async def async_setup_entry(
if description.key in coordinator.data[AZD_WEBSERVER]
)

async_add_entities(sensors)
async_add_entities(entities)

entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
_async_entity_listener()


class AirzoneSensor(AirzoneEntity, SensorEntity):
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/airzone/water_heater.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ async def async_setup_entry(
entry: AirzoneConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add Airzone sensors from a config_entry."""
"""Add Airzone Water Heater from a config_entry."""
coordinator = entry.runtime_data
if AZD_HOT_WATER in coordinator.data:
async_add_entities([AirzoneWaterHeater(coordinator, entry)])
Expand Down
62 changes: 61 additions & 1 deletion tests/components/airzone/test_coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
InvalidMethod,
SystemOutOfRange,
)
from freezegun.api import FrozenDateTimeFactory

from homeassistant.components.airzone.const import DOMAIN
from homeassistant.components.airzone.coordinator import SCAN_INTERVAL
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.util.dt import utcnow

from .util import CONFIG, HVAC_MOCK, HVAC_VERSION_MOCK
from .util import CONFIG, HVAC_MOCK, HVAC_MOCK_NEW_ZONES, HVAC_VERSION_MOCK

from tests.common import MockConfigEntry, async_fire_time_changed

Expand Down Expand Up @@ -64,3 +65,62 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None:

state = hass.states.get("sensor.despacho_temperature")
assert state.state == STATE_UNAVAILABLE


async def test_coordinator_new_devices(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test new devices on coordinator update."""

config_entry = MockConfigEntry(
data=CONFIG,
domain=DOMAIN,
unique_id="airzone_unique_id",
)
config_entry.add_to_hass(hass)

with (
patch(
"homeassistant.components.airzone.AirzoneLocalApi.get_dhw",
side_effect=HotWaterNotAvailable,
),
patch(
"homeassistant.components.airzone.AirzoneLocalApi.get_hvac",
return_value=HVAC_MOCK_NEW_ZONES,
) as mock_hvac,
patch(
"homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems",
side_effect=SystemOutOfRange,
),
patch(
"homeassistant.components.airzone.AirzoneLocalApi.get_version",
return_value=HVAC_VERSION_MOCK,
),
patch(
"homeassistant.components.airzone.AirzoneLocalApi.get_webserver",
side_effect=InvalidMethod,
),
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
mock_hvac.assert_called_once()
mock_hvac.reset_mock()

state = hass.states.get("sensor.salon_temperature")
assert state.state == "19.6"

state = hass.states.get("sensor.dorm_ppal_temperature")
assert state is None

mock_hvac.return_value = HVAC_MOCK
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
mock_hvac.assert_called_once()

state = hass.states.get("sensor.salon_temperature")
assert state.state == "19.6"

state = hass.states.get("sensor.dorm_ppal_temperature")
assert state.state == "21.1"
11 changes: 11 additions & 0 deletions tests/components/airzone/util.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Tests for the Airzone integration."""

from copy import deepcopy
from unittest.mock import patch

from aioairzone.const import (
Expand Down Expand Up @@ -274,6 +275,16 @@
]
}

HVAC_MOCK_NEW_ZONES = {
API_SYSTEMS: [
{
API_DATA: [
deepcopy(HVAC_MOCK[API_SYSTEMS][0][API_DATA][0]),
]
}
]
}

HVAC_DHW_MOCK = {
API_DATA: {
API_SYSTEM_ID: 0,
Expand Down