diff --git a/homeassistant/components/airzone_cloud/climate.py b/homeassistant/components/airzone_cloud/climate.py index af6c38b80b4..a86440bad20 100644 --- a/homeassistant/components/airzone_cloud/climate.py +++ b/homeassistant/components/airzone_cloud/climate.py @@ -7,16 +7,19 @@ from aioairzone_cloud.common import OperationAction, OperationMode, TemperatureU from aioairzone_cloud.const import ( API_MODE, API_OPTS, + API_PARAMS, API_POWER, API_SETPOINT, API_UNITS, API_VALUE, AZD_ACTION, AZD_AIDOOS, + AZD_GROUPS, AZD_HUMIDITY, AZD_MASTER, AZD_MODE, AZD_MODES, + AZD_NUM_DEVICES, AZD_POWER, AZD_TEMP, AZD_TEMP_SET, @@ -40,7 +43,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .coordinator import AirzoneUpdateCoordinator -from .entity import AirzoneAidooEntity, AirzoneEntity, AirzoneZoneEntity +from .entity import ( + AirzoneAidooEntity, + AirzoneEntity, + AirzoneGroupEntity, + AirzoneZoneEntity, +) HVAC_ACTION_LIB_TO_HASS: Final[dict[OperationAction, HVACAction]] = { OperationAction.COOLING: HVACAction.COOLING, @@ -93,6 +101,17 @@ async def async_setup_entry( ) ) + # Groups + for group_id, group_data in coordinator.data.get(AZD_GROUPS, {}).items(): + if group_data[AZD_NUM_DEVICES] > 1: + entities.append( + AirzoneGroupClimate( + coordinator, + group_id, + group_data, + ) + ) + # Zones for zone_id, zone_data in coordinator.data.get(AZD_ZONES, {}).items(): entities.append( @@ -208,6 +227,72 @@ class AirzoneAidooClimate(AirzoneAidooEntity, AirzoneClimate): 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() + + async def async_turn_on(self) -> None: + """Turn the entity on.""" + params = { + API_PARAMS: { + API_POWER: True, + }, + } + await self._async_update_params(params) + + async def async_turn_off(self) -> None: + """Turn the entity off.""" + params = { + API_PARAMS: { + API_POWER: False, + }, + } + await self._async_update_params(params) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + params: dict[str, Any] = {} + if ATTR_TEMPERATURE in kwargs: + params[API_PARAMS] = { + API_SETPOINT: kwargs[ATTR_TEMPERATURE], + } + params[API_OPTS] = { + API_UNITS: TemperatureUnit.CELSIUS.value, + } + await self._async_update_params(params) + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set hvac mode.""" + params: dict[str, Any] = { + API_PARAMS: {}, + } + if hvac_mode == HVACMode.OFF: + params[API_PARAMS][API_POWER] = False + else: + mode = HVAC_MODE_HASS_TO_LIB[hvac_mode] + params[API_PARAMS][API_MODE] = mode.value + params[API_PARAMS][API_POWER] = True + await self._async_update_params(params) + + class AirzoneZoneClimate(AirzoneZoneEntity, AirzoneClimate): """Define an Airzone Cloud Zone climate.""" diff --git a/homeassistant/components/airzone_cloud/entity.py b/homeassistant/components/airzone_cloud/entity.py index b304f06d42b..749d4615e65 100644 --- a/homeassistant/components/airzone_cloud/entity.py +++ b/homeassistant/components/airzone_cloud/entity.py @@ -9,6 +9,7 @@ from aioairzone_cloud.const import ( AZD_AIDOOS, AZD_AVAILABLE, AZD_FIRMWARE, + AZD_GROUPS, AZD_NAME, AZD_SYSTEM_ID, AZD_SYSTEMS, @@ -89,6 +90,48 @@ class AirzoneAidooEntity(AirzoneEntity): self.coordinator.async_set_updated_data(self.coordinator.airzone.data()) +class AirzoneGroupEntity(AirzoneEntity): + """Define an Airzone Cloud Group entity.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + group_id: str, + group_data: dict[str, Any], + ) -> None: + """Initialize.""" + super().__init__(coordinator) + + self.group_id = group_id + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, group_id)}, + manufacturer=MANUFACTURER, + name=group_data[AZD_NAME], + ) + + def get_airzone_value(self, key: str) -> Any: + """Return Group value by key.""" + value = None + if group := self.coordinator.data[AZD_GROUPS].get(self.group_id): + value = group.get(key) + return value + + async def _async_update_params(self, params: dict[str, Any]) -> None: + """Send Group parameters to Cloud API.""" + _LOGGER.debug("group=%s: update_params=%s", self.name, params) + try: + await self.coordinator.airzone.api_set_group_id_params( + self.group_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 fcd60f605ba..56c563a8680 100644 --- a/tests/components/airzone_cloud/test_climate.py +++ b/tests/components/airzone_cloud/test_climate.py @@ -56,6 +56,24 @@ 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) == 22.0 + # Groups + state = hass.states.get("climate.group") + assert state.state == HVACMode.COOL + assert state.attributes.get(ATTR_CURRENT_HUMIDITY) == 27 + assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == 22.5 + assert state.attributes.get(ATTR_HVAC_ACTION) == HVACAction.COOLING + assert state.attributes.get(ATTR_HVAC_MODES) == [ + HVACMode.COOL, + HVACMode.HEAT, + HVACMode.FAN_ONLY, + HVACMode.DRY, + HVACMode.OFF, + ] + assert state.attributes.get(ATTR_MAX_TEMP) == 30 + assert state.attributes.get(ATTR_MIN_TEMP) == 15 + assert state.attributes.get(ATTR_TARGET_TEMP_STEP) == API_TEMPERATURE_STEP + assert state.attributes.get(ATTR_TEMPERATURE) == 24.0 + # Zones state = hass.states.get("climate.dormitorio") assert state.state == HVACMode.OFF @@ -114,6 +132,39 @@ async def test_airzone_climate_turn_on_off(hass: HomeAssistant) -> None: state = hass.states.get("climate.bron") assert state.state == HVACMode.HEAT + # Groups + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_put_group", + return_value=None, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "climate.group", + }, + blocking=True, + ) + + state = hass.states.get("climate.group") + assert state.state == HVACMode.COOL + + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_put_group", + return_value=None, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: "climate.group", + }, + blocking=True, + ) + + state = hass.states.get("climate.group") + assert state.state == HVACMode.OFF + # Zones with patch( "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", @@ -188,6 +239,41 @@ async def test_airzone_climate_set_hvac_mode(hass: HomeAssistant) -> None: state = hass.states.get("climate.bron") assert state.state == HVACMode.OFF + # Groups + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_put_group", + return_value=None, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: "climate.group", + ATTR_HVAC_MODE: HVACMode.DRY, + }, + blocking=True, + ) + + state = hass.states.get("climate.group") + assert state.state == HVACMode.DRY + + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_put_group", + return_value=None, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: "climate.group", + ATTR_HVAC_MODE: HVACMode.OFF, + }, + blocking=True, + ) + + state = hass.states.get("climate.group") + assert state.state == HVACMode.OFF + # Zones with patch( "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", @@ -252,6 +338,24 @@ async def test_airzone_climate_set_temp(hass: HomeAssistant) -> None: await async_init_integration(hass) + # Groups + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_put_group", + return_value=None, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: "climate.group", + ATTR_TEMPERATURE: 20.5, + }, + blocking=True, + ) + + state = hass.states.get("climate.group") + assert state.attributes.get(ATTR_TEMPERATURE) == 20.5 + # Zones with patch( "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", @@ -294,6 +398,24 @@ async def test_airzone_climate_set_temp_error(hass: HomeAssistant) -> None: state = hass.states.get("climate.bron") assert state.attributes.get(ATTR_TEMPERATURE) == 22.0 + # Groups + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_put_group", + side_effect=AirzoneCloudError, + ), pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: "climate.group", + ATTR_TEMPERATURE: 20.5, + }, + blocking=True, + ) + + state = hass.states.get("climate.group") + assert state.attributes.get(ATTR_TEMPERATURE) == 24.0 + # Zones with patch( "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device",