diff --git a/homeassistant/components/actron_air/__init__.py b/homeassistant/components/actron_air/__init__.py index 456c34ff6fb..f8b460dd027 100644 --- a/homeassistant/components/actron_air/__init__.py +++ b/homeassistant/components/actron_air/__init__.py @@ -1,11 +1,7 @@ """The Actron Air integration.""" -from actron_neo_api import ( - ActronAirACSystem, - ActronAirAPI, - ActronAirAPIError, - ActronAirAuthError, -) +from actron_neo_api import ActronAirAPI, ActronAirAPIError, ActronAirAuthError +from actron_neo_api.models.system import ActronAirSystemInfo from homeassistant.const import CONF_API_TOKEN, Platform from homeassistant.core import HomeAssistant @@ -25,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> """Set up Actron Air integration from a config entry.""" api = ActronAirAPI(refresh_token=entry.data[CONF_API_TOKEN]) - systems: list[ActronAirACSystem] = [] + systems: list[ActronAirSystemInfo] = [] try: systems = await api.get_ac_systems() @@ -44,9 +40,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> system_coordinators: dict[str, ActronAirSystemCoordinator] = {} for system in systems: coordinator = ActronAirSystemCoordinator(hass, entry, api, system) - _LOGGER.debug("Setting up coordinator for system: %s", system["serial"]) + _LOGGER.debug("Setting up coordinator for system: %s", system.serial) await coordinator.async_config_entry_first_refresh() - system_coordinators[system["serial"]] = coordinator + system_coordinators[system.serial] = coordinator entry.runtime_data = ActronAirRuntimeData( api=api, diff --git a/homeassistant/components/actron_air/climate.py b/homeassistant/components/actron_air/climate.py index 8c928fcc5a9..9284065bebf 100644 --- a/homeassistant/components/actron_air/climate.py +++ b/homeassistant/components/actron_air/climate.py @@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .coordinator import ActronAirConfigEntry, ActronAirSystemCoordinator -from .entity import ActronAirAcEntity, ActronAirZoneEntity, handle_actron_api_errors +from .entity import ActronAirAcEntity, ActronAirZoneEntity, actron_air_command PARALLEL_UPDATES = 0 @@ -136,19 +136,19 @@ class ActronSystemClimate(ActronAirAcEntity, ActronAirClimateEntity): """Return the target temperature.""" return self._status.user_aircon_settings.temperature_setpoint_cool_c - @handle_actron_api_errors + @actron_air_command 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) await self._status.user_aircon_settings.set_fan_mode(api_fan_mode) - @handle_actron_api_errors + @actron_air_command async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the HVAC mode.""" ac_mode = HVAC_MODE_MAPPING_HA_TO_ACTRONAIR.get(hvac_mode) await self._status.ac_system.set_system_mode(ac_mode) - @handle_actron_api_errors + @actron_air_command async def async_set_temperature(self, **kwargs: Any) -> None: """Set the temperature.""" temp = kwargs.get(ATTR_TEMPERATURE) @@ -212,13 +212,13 @@ class ActronZoneClimate(ActronAirZoneEntity, ActronAirClimateEntity): """Return the target temperature.""" return self._zone.temperature_setpoint_cool_c - @handle_actron_api_errors + @actron_air_command async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the HVAC mode.""" is_enabled = hvac_mode != HVACMode.OFF await self._zone.enable(is_enabled) - @handle_actron_api_errors + @actron_air_command async def async_set_temperature(self, **kwargs: Any) -> None: """Set the temperature.""" await self._zone.set_temperature(temperature=kwargs.get(ATTR_TEMPERATURE)) diff --git a/homeassistant/components/actron_air/config_flow.py b/homeassistant/components/actron_air/config_flow.py index 3faefe7590f..4bbbadb296c 100644 --- a/homeassistant/components/actron_air/config_flow.py +++ b/homeassistant/components/actron_air/config_flow.py @@ -38,10 +38,10 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.error("OAuth2 flow failed: %s", err) return self.async_abort(reason="oauth2_error") - self._device_code = device_code_response["device_code"] - self._user_code = device_code_response["user_code"] - self._verification_uri = device_code_response["verification_uri_complete"] - self._expires_minutes = str(device_code_response["expires_in"] // 60) + self._device_code = device_code_response.device_code + self._user_code = device_code_response.user_code + self._verification_uri = device_code_response.verification_uri_complete + self._expires_minutes = str(device_code_response.expires_in // 60) async def _wait_for_authorization() -> None: """Wait for the user to authorize the device.""" diff --git a/homeassistant/components/actron_air/coordinator.py b/homeassistant/components/actron_air/coordinator.py index a69f7ab56b0..f23486a84f9 100644 --- a/homeassistant/components/actron_air/coordinator.py +++ b/homeassistant/components/actron_air/coordinator.py @@ -6,12 +6,12 @@ from dataclasses import dataclass from datetime import timedelta from actron_neo_api import ( - ActronAirACSystem, ActronAirAPI, ActronAirAPIError, ActronAirAuthError, ActronAirStatus, ) +from actron_neo_api.models.system import ActronAirSystemInfo from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -38,7 +38,7 @@ class ActronAirRuntimeData: type ActronAirConfigEntry = ConfigEntry[ActronAirRuntimeData] -class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]): +class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirStatus]): """System coordinator for Actron Air integration.""" def __init__( @@ -46,7 +46,7 @@ class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]): hass: HomeAssistant, entry: ActronAirConfigEntry, api: ActronAirAPI, - system: ActronAirACSystem, + system: ActronAirSystemInfo, ) -> None: """Initialize the coordinator.""" super().__init__( @@ -57,7 +57,7 @@ class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]): config_entry=entry, ) self.system = system - self.serial_number = system["serial"] + self.serial_number = system.serial self.api = api 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 index 7f62c53516e..008d00aa491 100644 --- a/homeassistant/components/actron_air/entity.py +++ b/homeassistant/components/actron_air/entity.py @@ -14,10 +14,14 @@ from .const import DOMAIN from .coordinator import ActronAirSystemCoordinator -def handle_actron_api_errors[_EntityT: ActronAirEntity, **_P]( +def actron_air_command[_EntityT: ActronAirEntity, **_P]( func: Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, Any]], ) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]: - """Decorate Actron Air API calls to handle ActronAirAPIError exceptions.""" + """Decorator for Actron Air API calls. + + Handles ActronAirAPIError exceptions, and requests a coordinator update + to update the status of the devices as soon as possible. + """ @wraps(func) async def wrapper(self: _EntityT, *args: _P.args, **kwargs: _P.kwargs) -> None: @@ -30,6 +34,7 @@ def handle_actron_api_errors[_EntityT: ActronAirEntity, **_P]( translation_key="api_error", translation_placeholders={"error": str(err)}, ) from err + self.coordinator.async_set_updated_data(self.coordinator.data) return wrapper diff --git a/homeassistant/components/actron_air/manifest.json b/homeassistant/components/actron_air/manifest.json index 724ff101cb9..1fdf7ad1aa6 100644 --- a/homeassistant/components/actron_air/manifest.json +++ b/homeassistant/components/actron_air/manifest.json @@ -13,5 +13,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "quality_scale": "silver", - "requirements": ["actron-neo-api==0.4.1"] + "requirements": ["actron-neo-api==0.5.0"] } diff --git a/homeassistant/components/actron_air/switch.py b/homeassistant/components/actron_air/switch.py index 44efe6c9f74..113be86171d 100644 --- a/homeassistant/components/actron_air/switch.py +++ b/homeassistant/components/actron_air/switch.py @@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .coordinator import ActronAirConfigEntry, ActronAirSystemCoordinator -from .entity import ActronAirAcEntity, handle_actron_api_errors +from .entity import ActronAirAcEntity, actron_air_command PARALLEL_UPDATES = 0 @@ -105,12 +105,12 @@ class ActronAirSwitch(ActronAirAcEntity, SwitchEntity): """Return true if the switch is on.""" return self.entity_description.is_on_fn(self.coordinator) - @handle_actron_api_errors + @actron_air_command async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self.entity_description.set_fn(self.coordinator, True) - @handle_actron_api_errors + @actron_air_command async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self.entity_description.set_fn(self.coordinator, False) diff --git a/requirements_all.txt b/requirements_all.txt index eddfa5dc31d..983c75143ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -133,7 +133,7 @@ WSDiscovery==2.1.2 accuweather==5.1.0 # homeassistant.components.actron_air -actron-neo-api==0.4.1 +actron-neo-api==0.5.0 # homeassistant.components.adax adax==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e12b89a1cd..bccb5316c43 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -124,7 +124,7 @@ WSDiscovery==2.1.2 accuweather==5.1.0 # homeassistant.components.actron_air -actron-neo-api==0.4.1 +actron-neo-api==0.5.0 # homeassistant.components.adax adax==0.4.0 diff --git a/tests/components/actron_air/conftest.py b/tests/components/actron_air/conftest.py index 0b4f2002938..f17be5782d1 100644 --- a/tests/components/actron_air/conftest.py +++ b/tests/components/actron_air/conftest.py @@ -4,6 +4,8 @@ import asyncio from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch +from actron_neo_api.models.auth import ActronAirDeviceCode +from actron_neo_api.models.system import ActronAirSystemInfo import pytest from homeassistant.components.actron_air.const import DOMAIN @@ -31,12 +33,14 @@ def mock_actron_api() -> Generator[AsyncMock]: api = mock_api.return_value # Mock device code request - api.request_device_code.return_value = { - "device_code": "test_device_code", - "user_code": "ABC123", - "verification_uri_complete": "https://example.com/device", - "expires_in": 1800, - } + api.request_device_code.return_value = ActronAirDeviceCode( + device_code="test_device_code", + user_code="ABC123", + verification_uri="https://example.com", + verification_uri_complete="https://example.com/device", + expires_in=1800, + interval=5, + ) # Mock successful token polling (with a small delay to test progress) async def slow_poll_for_token(device_code): @@ -58,7 +62,7 @@ def mock_actron_api() -> Generator[AsyncMock]: # Mock get_ac_systems api.get_ac_systems = AsyncMock( - return_value=[{"serial": "123456", "name": "Test System"}] + return_value=[ActronAirSystemInfo(serial="123456")] ) # Mock state manager