diff --git a/homeassistant/components/airzone_cloud/climate.py b/homeassistant/components/airzone_cloud/climate.py index 89e528a0fbf..8995967f9a0 100644 --- a/homeassistant/components/airzone_cloud/climate.py +++ b/homeassistant/components/airzone_cloud/climate.py @@ -16,10 +16,12 @@ from aioairzone_cloud.const import ( AZD_AIDOOS, AZD_GROUPS, AZD_HUMIDITY, + AZD_INSTALLATIONS, AZD_MASTER, AZD_MODE, AZD_MODES, AZD_NUM_DEVICES, + AZD_NUM_GROUPS, AZD_POWER, AZD_TEMP, AZD_TEMP_SET, @@ -47,6 +49,7 @@ from .entity import ( AirzoneAidooEntity, AirzoneEntity, AirzoneGroupEntity, + AirzoneInstallationEntity, AirzoneZoneEntity, ) @@ -112,6 +115,17 @@ async def async_setup_entry( ) ) + # Installations + for inst_id, inst_data in coordinator.data.get(AZD_INSTALLATIONS, {}).items(): + if inst_data[AZD_NUM_GROUPS] > 1: + entities.append( + AirzoneInstallationClimate( + coordinator, + inst_id, + inst_data, + ) + ) + # Zones for zone_id, zone_data in coordinator.data.get(AZD_ZONES, {}).items(): entities.append( @@ -133,6 +147,34 @@ class AirzoneClimate(AirzoneEntity, ClimateEntity): _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE _attr_temperature_unit = UnitOfTemperature.CELSIUS + @callback + def _handle_coordinator_update(self) -> None: + """Update attributes when the coordinator updates.""" + self._async_update_attrs() + super()._handle_coordinator_update() + + @callback + def _async_update_attrs(self) -> None: + """Update climate attributes.""" + self._attr_current_temperature = self.get_airzone_value(AZD_TEMP) + self._attr_current_humidity = self.get_airzone_value(AZD_HUMIDITY) + self._attr_hvac_action = HVAC_ACTION_LIB_TO_HASS[ + self.get_airzone_value(AZD_ACTION) + ] + if self.get_airzone_value(AZD_POWER): + self._attr_hvac_mode = HVAC_MODE_LIB_TO_HASS[ + self.get_airzone_value(AZD_MODE) + ] + else: + self._attr_hvac_mode = HVACMode.OFF + self._attr_max_temp = self.get_airzone_value(AZD_TEMP_SET_MAX) + self._attr_min_temp = self.get_airzone_value(AZD_TEMP_SET_MIN) + self._attr_target_temperature = self.get_airzone_value(AZD_TEMP_SET) + + +class AirzoneDeviceClimate(AirzoneClimate): + """Define an Airzone Cloud Device base class.""" + async def async_turn_on(self) -> None: """Turn the entity on.""" params = { @@ -163,92 +205,9 @@ class AirzoneClimate(AirzoneEntity, ClimateEntity): } await self._async_update_params(params) - @callback - def _handle_coordinator_update(self) -> None: - """Update attributes when the coordinator updates.""" - self._async_update_attrs() - super()._handle_coordinator_update() - @callback - def _async_update_attrs(self) -> None: - """Update climate attributes.""" - self._attr_current_temperature = self.get_airzone_value(AZD_TEMP) - self._attr_current_humidity = self.get_airzone_value(AZD_HUMIDITY) - self._attr_hvac_action = HVAC_ACTION_LIB_TO_HASS[ - self.get_airzone_value(AZD_ACTION) - ] - if self.get_airzone_value(AZD_POWER): - self._attr_hvac_mode = HVAC_MODE_LIB_TO_HASS[ - self.get_airzone_value(AZD_MODE) - ] - else: - self._attr_hvac_mode = HVACMode.OFF - self._attr_max_temp = self.get_airzone_value(AZD_TEMP_SET_MAX) - self._attr_min_temp = self.get_airzone_value(AZD_TEMP_SET_MIN) - self._attr_target_temperature = self.get_airzone_value(AZD_TEMP_SET) - - -class AirzoneAidooClimate(AirzoneAidooEntity, AirzoneClimate): - """Define an Airzone Cloud Aidoo climate.""" - - def __init__( - self, - coordinator: AirzoneUpdateCoordinator, - aidoo_id: str, - aidoo_data: dict, - ) -> None: - """Initialize Airzone Cloud Aidoo climate.""" - super().__init__(coordinator, aidoo_id, aidoo_data) - - self._attr_unique_id = aidoo_id - self._attr_target_temperature_step = self.get_airzone_value(AZD_TEMP_STEP) - self._attr_hvac_modes = [ - HVAC_MODE_LIB_TO_HASS[mode] for mode in self.get_airzone_value(AZD_MODES) - ] - if HVACMode.OFF not in self._attr_hvac_modes: - self._attr_hvac_modes += [HVACMode.OFF] - - self._async_update_attrs() - - async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: - """Set hvac mode.""" - params: dict[str, Any] = {} - if hvac_mode == HVACMode.OFF: - params[API_POWER] = { - API_VALUE: False, - } - else: - mode = HVAC_MODE_HASS_TO_LIB[hvac_mode] - params[API_MODE] = { - API_VALUE: mode.value, - } - params[API_POWER] = { - API_VALUE: True, - } - await self._async_update_params(params) - - -class AirzoneGroupClimate(AirzoneGroupEntity, AirzoneClimate): - """Define an Airzone Cloud Group climate.""" - - def __init__( - self, - coordinator: AirzoneUpdateCoordinator, - group_id: str, - group_data: dict, - ) -> None: - """Initialize Airzone Cloud Group climate.""" - super().__init__(coordinator, group_id, group_data) - - self._attr_unique_id = group_id - self._attr_target_temperature_step = self.get_airzone_value(AZD_TEMP_STEP) - self._attr_hvac_modes = [ - HVAC_MODE_LIB_TO_HASS[mode] for mode in self.get_airzone_value(AZD_MODES) - ] - if HVACMode.OFF not in self._attr_hvac_modes: - self._attr_hvac_modes += [HVACMode.OFF] - - self._async_update_attrs() +class AirzoneDeviceGroupClimate(AirzoneClimate): + """Define an Airzone Cloud DeviceGroup base class.""" async def async_turn_on(self) -> None: """Turn the entity on.""" @@ -294,7 +253,93 @@ class AirzoneGroupClimate(AirzoneGroupEntity, AirzoneClimate): await self._async_update_params(params) -class AirzoneZoneClimate(AirzoneZoneEntity, AirzoneClimate): +class AirzoneAidooClimate(AirzoneAidooEntity, AirzoneDeviceClimate): + """Define an Airzone Cloud Aidoo climate.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + aidoo_id: str, + aidoo_data: dict, + ) -> None: + """Initialize Airzone Cloud Aidoo climate.""" + super().__init__(coordinator, aidoo_id, aidoo_data) + + self._attr_unique_id = aidoo_id + self._attr_target_temperature_step = self.get_airzone_value(AZD_TEMP_STEP) + self._attr_hvac_modes = [ + HVAC_MODE_LIB_TO_HASS[mode] for mode in self.get_airzone_value(AZD_MODES) + ] + if HVACMode.OFF not in self._attr_hvac_modes: + self._attr_hvac_modes += [HVACMode.OFF] + + self._async_update_attrs() + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set hvac mode.""" + params: dict[str, Any] = {} + if hvac_mode == HVACMode.OFF: + params[API_POWER] = { + API_VALUE: False, + } + else: + mode = HVAC_MODE_HASS_TO_LIB[hvac_mode] + params[API_MODE] = { + API_VALUE: mode.value, + } + params[API_POWER] = { + API_VALUE: True, + } + await self._async_update_params(params) + + +class AirzoneGroupClimate(AirzoneGroupEntity, AirzoneDeviceGroupClimate): + """Define an Airzone Cloud Group climate.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + group_id: str, + group_data: dict, + ) -> None: + """Initialize Airzone Cloud Group climate.""" + super().__init__(coordinator, group_id, group_data) + + self._attr_unique_id = group_id + self._attr_target_temperature_step = self.get_airzone_value(AZD_TEMP_STEP) + self._attr_hvac_modes = [ + HVAC_MODE_LIB_TO_HASS[mode] for mode in self.get_airzone_value(AZD_MODES) + ] + if HVACMode.OFF not in self._attr_hvac_modes: + self._attr_hvac_modes += [HVACMode.OFF] + + self._async_update_attrs() + + +class AirzoneInstallationClimate(AirzoneInstallationEntity, AirzoneDeviceGroupClimate): + """Define an Airzone Cloud Installation climate.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + inst_id: str, + inst_data: dict, + ) -> None: + """Initialize Airzone Cloud Installation climate.""" + super().__init__(coordinator, inst_id, inst_data) + + self._attr_unique_id = inst_id + self._attr_target_temperature_step = self.get_airzone_value(AZD_TEMP_STEP) + self._attr_hvac_modes = [ + HVAC_MODE_LIB_TO_HASS[mode] for mode in self.get_airzone_value(AZD_MODES) + ] + if HVACMode.OFF not in self._attr_hvac_modes: + self._attr_hvac_modes += [HVACMode.OFF] + + self._async_update_attrs() + + +class AirzoneZoneClimate(AirzoneZoneEntity, AirzoneDeviceClimate): """Define an Airzone Cloud Zone climate.""" def __init__( diff --git a/homeassistant/components/airzone_cloud/entity.py b/homeassistant/components/airzone_cloud/entity.py index 749d4615e65..d5dd0cfcfb4 100644 --- a/homeassistant/components/airzone_cloud/entity.py +++ b/homeassistant/components/airzone_cloud/entity.py @@ -10,6 +10,7 @@ from aioairzone_cloud.const import ( AZD_AVAILABLE, AZD_FIRMWARE, AZD_GROUPS, + AZD_INSTALLATIONS, AZD_NAME, AZD_SYSTEM_ID, AZD_SYSTEMS, @@ -132,6 +133,48 @@ class AirzoneGroupEntity(AirzoneEntity): self.coordinator.async_set_updated_data(self.coordinator.airzone.data()) +class AirzoneInstallationEntity(AirzoneEntity): + """Define an Airzone Cloud Installation entity.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + inst_id: str, + inst_data: dict[str, Any], + ) -> None: + """Initialize.""" + super().__init__(coordinator) + + self.inst_id = inst_id + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, inst_id)}, + manufacturer=MANUFACTURER, + name=inst_data[AZD_NAME], + ) + + def get_airzone_value(self, key: str) -> Any: + """Return Installation value by key.""" + value = None + if inst := self.coordinator.data[AZD_INSTALLATIONS].get(self.inst_id): + value = inst.get(key) + return value + + async def _async_update_params(self, params: dict[str, Any]) -> None: + """Send Installation parameters to Cloud API.""" + _LOGGER.debug("installation=%s: update_params=%s", self.name, params) + try: + await self.coordinator.airzone.api_set_installation_id_params( + self.inst_id, params + ) + except AirzoneCloudError as error: + raise HomeAssistantError( + f"Failed to set {self.name} params: {error}" + ) from error + + self.coordinator.async_set_updated_data(self.coordinator.airzone.data()) + + class AirzoneSystemEntity(AirzoneEntity): """Define an Airzone Cloud System entity.""" diff --git a/tests/components/airzone_cloud/test_climate.py b/tests/components/airzone_cloud/test_climate.py index 56c563a8680..34336866339 100644 --- a/tests/components/airzone_cloud/test_climate.py +++ b/tests/components/airzone_cloud/test_climate.py @@ -74,6 +74,25 @@ async def test_airzone_create_climates(hass: HomeAssistant) -> None: assert state.attributes.get(ATTR_TARGET_TEMP_STEP) == API_TEMPERATURE_STEP assert state.attributes.get(ATTR_TEMPERATURE) == 24.0 + # Installations + state = hass.states.get("climate.house") + assert state.state == HVACMode.COOL + assert state.attributes[ATTR_CURRENT_HUMIDITY] == 27 + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.0 + assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.COOLING + assert state.attributes[ATTR_HVAC_MODES] == [ + HVACMode.HEAT_COOL, + HVACMode.COOL, + HVACMode.HEAT, + HVACMode.FAN_ONLY, + HVACMode.DRY, + HVACMode.OFF, + ] + assert state.attributes[ATTR_MAX_TEMP] == 30 + assert state.attributes[ATTR_MIN_TEMP] == 15 + assert state.attributes[ATTR_TARGET_TEMP_STEP] == API_TEMPERATURE_STEP + assert state.attributes[ATTR_TEMPERATURE] == 23.3 + # Zones state = hass.states.get("climate.dormitorio") assert state.state == HVACMode.OFF @@ -165,6 +184,39 @@ async def test_airzone_climate_turn_on_off(hass: HomeAssistant) -> None: state = hass.states.get("climate.group") assert state.state == HVACMode.OFF + # Installations + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_put_installation", + return_value=None, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "climate.house", + }, + blocking=True, + ) + + state = hass.states.get("climate.house") + assert state.state == HVACMode.COOL + + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_put_installation", + return_value=None, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: "climate.house", + }, + blocking=True, + ) + + state = hass.states.get("climate.house") + assert state.state == HVACMode.OFF + # Zones with patch( "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", @@ -274,6 +326,41 @@ async def test_airzone_climate_set_hvac_mode(hass: HomeAssistant) -> None: state = hass.states.get("climate.group") assert state.state == HVACMode.OFF + # Installations + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_put_installation", + return_value=None, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: "climate.house", + ATTR_HVAC_MODE: HVACMode.DRY, + }, + blocking=True, + ) + + state = hass.states.get("climate.house") + assert state.state == HVACMode.DRY + + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_put_installation", + return_value=None, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: "climate.house", + ATTR_HVAC_MODE: HVACMode.OFF, + }, + blocking=True, + ) + + state = hass.states.get("climate.house") + assert state.state == HVACMode.OFF + # Zones with patch( "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", @@ -356,6 +443,24 @@ async def test_airzone_climate_set_temp(hass: HomeAssistant) -> None: state = hass.states.get("climate.group") assert state.attributes.get(ATTR_TEMPERATURE) == 20.5 + # Installations + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_put_installation", + return_value=None, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: "climate.house", + ATTR_TEMPERATURE: 20.5, + }, + blocking=True, + ) + + state = hass.states.get("climate.house") + assert state.attributes[ATTR_TEMPERATURE] == 20.5 + # Zones with patch( "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", @@ -416,6 +521,24 @@ async def test_airzone_climate_set_temp_error(hass: HomeAssistant) -> None: state = hass.states.get("climate.group") assert state.attributes.get(ATTR_TEMPERATURE) == 24.0 + # Installations + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_put_installation", + side_effect=AirzoneCloudError, + ), pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: "climate.house", + ATTR_TEMPERATURE: 20.5, + }, + blocking=True, + ) + + state = hass.states.get("climate.house") + assert state.attributes[ATTR_TEMPERATURE] == 23.3 + # Zones with patch( "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device",