From 4083bd3c9401d986b898b73cbde6b38d6b3cb283 Mon Sep 17 00:00:00 2001 From: Kurt Chrisford <92524101+kclif9@users.noreply.github.com> Date: Tue, 30 Dec 2025 01:57:46 +1000 Subject: [PATCH] Refactor Actron Air climate and switch entities to inherit from a new base entity class (#159540) Co-authored-by: Joostlek --- .../components/actron_air/climate.py | 66 ++++--------------- .../components/actron_air/coordinator.py | 9 ++- homeassistant/components/actron_air/entity.py | 63 ++++++++++++++++++ .../components/actron_air/strings.json | 3 + homeassistant/components/actron_air/switch.py | 12 +--- 5 files changed, 89 insertions(+), 64 deletions(-) create mode 100644 homeassistant/components/actron_air/entity.py diff --git a/homeassistant/components/actron_air/climate.py b/homeassistant/components/actron_air/climate.py index 6e0e6e0389e..e998902c002 100644 --- a/homeassistant/components/actron_air/climate.py +++ b/homeassistant/components/actron_air/climate.py @@ -15,12 +15,10 @@ from homeassistant.components.climate import ( ) from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN from .coordinator import ActronAirConfigEntry, ActronAirSystemCoordinator +from .entity import ActronAirAcEntity, ActronAirZoneEntity PARALLEL_UPDATES = 0 @@ -56,8 +54,7 @@ async def async_setup_entry( for coordinator in system_coordinators.values(): status = coordinator.data - name = status.ac_system.system_name - entities.append(ActronSystemClimate(coordinator, name)) + entities.append(ActronSystemClimate(coordinator)) entities.extend( ActronZoneClimate(coordinator, zone) @@ -68,10 +65,9 @@ async def async_setup_entry( async_add_entities(entities) -class BaseClimateEntity(CoordinatorEntity[ActronAirSystemCoordinator], ClimateEntity): +class ActronAirClimateEntity(ClimateEntity): """Base class for Actron Air climate entities.""" - _attr_has_entity_name = True _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE @@ -83,43 +79,17 @@ class BaseClimateEntity(CoordinatorEntity[ActronAirSystemCoordinator], ClimateEn _attr_fan_modes = list(FAN_MODE_MAPPING_ACTRONAIR_TO_HA.values()) _attr_hvac_modes = list(HVAC_MODE_MAPPING_ACTRONAIR_TO_HA.values()) + +class ActronSystemClimate(ActronAirAcEntity, ActronAirClimateEntity): + """Representation of the Actron Air system.""" + def __init__( self, coordinator: ActronAirSystemCoordinator, - name: str, ) -> None: """Initialize an Actron Air unit.""" super().__init__(coordinator) - self._serial_number = coordinator.serial_number - - -class ActronSystemClimate(BaseClimateEntity): - """Representation of the Actron Air system.""" - - _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE - | ClimateEntityFeature.FAN_MODE - | ClimateEntityFeature.TURN_ON - | ClimateEntityFeature.TURN_OFF - ) - - def __init__( - self, - coordinator: ActronAirSystemCoordinator, - name: str, - ) -> None: - """Initialize an Actron Air unit.""" - super().__init__(coordinator, name) - serial_number = coordinator.serial_number - self._attr_unique_id = serial_number - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, serial_number)}, - name=self._status.ac_system.system_name, - manufacturer="Actron Air", - model_id=self._status.ac_system.master_wc_model, - sw_version=self._status.ac_system.master_wc_firmware_version, - serial_number=serial_number, - ) + self._attr_unique_id = self._serial_number @property def min_temp(self) -> float: @@ -168,7 +138,7 @@ class ActronSystemClimate(BaseClimateEntity): async def async_set_fan_mode(self, fan_mode: str) -> None: """Set a new fan mode.""" - api_fan_mode = FAN_MODE_MAPPING_HA_TO_ACTRONAIR.get(fan_mode.lower()) + api_fan_mode = FAN_MODE_MAPPING_HA_TO_ACTRONAIR.get(fan_mode) await self._status.user_aircon_settings.set_fan_mode(api_fan_mode) async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: @@ -182,7 +152,7 @@ class ActronSystemClimate(BaseClimateEntity): await self._status.user_aircon_settings.set_temperature(temperature=temp) -class ActronZoneClimate(BaseClimateEntity): +class ActronZoneClimate(ActronAirZoneEntity, ActronAirClimateEntity): """Representation of a zone within the Actron Air system.""" _attr_supported_features = ( @@ -197,18 +167,8 @@ class ActronZoneClimate(BaseClimateEntity): zone: ActronAirZone, ) -> None: """Initialize an Actron Air unit.""" - super().__init__(coordinator, zone.title) - serial_number = coordinator.serial_number - self._zone_id: int = zone.zone_id - self._attr_unique_id: str = f"{serial_number}_zone_{zone.zone_id}" - self._attr_device_info: DeviceInfo = DeviceInfo( - identifiers={(DOMAIN, self._attr_unique_id)}, - name=zone.title, - manufacturer="Actron Air", - model="Zone", - suggested_area=zone.title, - via_device=(DOMAIN, serial_number), - ) + super().__init__(coordinator, zone) + self._attr_unique_id: str = self._zone_identifier @property def min_temp(self) -> float: @@ -256,4 +216,4 @@ class ActronZoneClimate(BaseClimateEntity): async def async_set_temperature(self, **kwargs: Any) -> None: """Set the temperature.""" - await self._zone.set_temperature(temperature=kwargs["temperature"]) + await self._zone.set_temperature(temperature=kwargs.get(ATTR_TEMPERATURE)) diff --git a/homeassistant/components/actron_air/coordinator.py b/homeassistant/components/actron_air/coordinator.py index 6071fe9b8eb..a69f7ab56b0 100644 --- a/homeassistant/components/actron_air/coordinator.py +++ b/homeassistant/components/actron_air/coordinator.py @@ -8,6 +8,7 @@ from datetime import timedelta from actron_neo_api import ( ActronAirACSystem, ActronAirAPI, + ActronAirAPIError, ActronAirAuthError, ActronAirStatus, ) @@ -15,7 +16,7 @@ from actron_neo_api import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util from .const import _LOGGER, DOMAIN @@ -70,6 +71,12 @@ class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]): translation_domain=DOMAIN, translation_key="auth_error", ) from err + except ActronAirAPIError as err: + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="update_error", + translation_placeholders={"error": repr(err)}, + ) from err self.status = self.api.state_manager.get_status(self.serial_number) self.last_seen = dt_util.utcnow() diff --git a/homeassistant/components/actron_air/entity.py b/homeassistant/components/actron_air/entity.py new file mode 100644 index 00000000000..1c13f17d8c4 --- /dev/null +++ b/homeassistant/components/actron_air/entity.py @@ -0,0 +1,63 @@ +"""Base entity classes for Actron Air integration.""" + +from actron_neo_api import ActronAirZone + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import ActronAirSystemCoordinator + + +class ActronAirEntity(CoordinatorEntity[ActronAirSystemCoordinator]): + """Base class for Actron Air entities.""" + + _attr_has_entity_name = True + + def __init__(self, coordinator: ActronAirSystemCoordinator) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self._serial_number = coordinator.serial_number + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return not self.coordinator.is_device_stale() + + +class ActronAirAcEntity(ActronAirEntity): + """Base class for Actron Air entities.""" + + def __init__(self, coordinator: ActronAirSystemCoordinator) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._serial_number)}, + name=coordinator.data.ac_system.system_name, + manufacturer="Actron Air", + model_id=coordinator.data.ac_system.master_wc_model, + sw_version=coordinator.data.ac_system.master_wc_firmware_version, + serial_number=self._serial_number, + ) + + +class ActronAirZoneEntity(ActronAirEntity): + """Base class for Actron Air zone entities.""" + + def __init__( + self, + coordinator: ActronAirSystemCoordinator, + zone: ActronAirZone, + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self._zone_id: int = zone.zone_id + self._zone_identifier = f"{self._serial_number}_zone_{zone.zone_id}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._zone_identifier)}, + name=zone.title, + manufacturer="Actron Air", + model="Zone", + suggested_area=zone.title, + via_device=(DOMAIN, self._serial_number), + ) diff --git a/homeassistant/components/actron_air/strings.json b/homeassistant/components/actron_air/strings.json index b7a94efad0a..00ca2073179 100644 --- a/homeassistant/components/actron_air/strings.json +++ b/homeassistant/components/actron_air/strings.json @@ -51,6 +51,9 @@ "exceptions": { "auth_error": { "message": "Authentication failed, please reauthenticate" + }, + "update_error": { + "message": "An error occurred while retrieving data from the Actron Air API: {error}" } } } diff --git a/homeassistant/components/actron_air/switch.py b/homeassistant/components/actron_air/switch.py index b886d82e5f9..8ed50386bd0 100644 --- a/homeassistant/components/actron_air/switch.py +++ b/homeassistant/components/actron_air/switch.py @@ -7,12 +7,10 @@ from typing import Any from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN from .coordinator import ActronAirConfigEntry, ActronAirSystemCoordinator +from .entity import ActronAirAcEntity PARALLEL_UPDATES = 0 @@ -74,10 +72,9 @@ async def async_setup_entry( ) -class ActronAirSwitch(CoordinatorEntity[ActronAirSystemCoordinator], SwitchEntity): +class ActronAirSwitch(ActronAirAcEntity, SwitchEntity): """Actron Air switch.""" - _attr_has_entity_name = True _attr_entity_category = EntityCategory.CONFIG entity_description: ActronAirSwitchEntityDescription @@ -90,11 +87,6 @@ class ActronAirSwitch(CoordinatorEntity[ActronAirSystemCoordinator], SwitchEntit super().__init__(coordinator) self.entity_description = description self._attr_unique_id = f"{coordinator.serial_number}_{description.key}" - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, coordinator.serial_number)}, - manufacturer="Actron Air", - name=coordinator.data.ac_system.system_name, - ) @property def is_on(self) -> bool: