mirror of
https://github.com/home-assistant/core.git
synced 2025-12-20 10:59:24 +00:00
Add support for actron air que air conditioners (#156675)
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
"""The Actron Air integration."""
|
"""The Actron Air integration."""
|
||||||
|
|
||||||
from actron_neo_api import (
|
from actron_neo_api import (
|
||||||
ActronAirNeoACSystem,
|
ActronAirACSystem,
|
||||||
ActronNeoAPI,
|
ActronAirAPI,
|
||||||
ActronNeoAPIError,
|
ActronAirAPIError,
|
||||||
ActronNeoAuthError,
|
ActronAirAuthError,
|
||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant.const import CONF_API_TOKEN, Platform
|
from homeassistant.const import CONF_API_TOKEN, Platform
|
||||||
@@ -23,16 +23,16 @@ PLATFORM = [Platform.CLIMATE]
|
|||||||
async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> bool:
|
||||||
"""Set up Actron Air integration from a config entry."""
|
"""Set up Actron Air integration from a config entry."""
|
||||||
|
|
||||||
api = ActronNeoAPI(refresh_token=entry.data[CONF_API_TOKEN])
|
api = ActronAirAPI(refresh_token=entry.data[CONF_API_TOKEN])
|
||||||
systems: list[ActronAirNeoACSystem] = []
|
systems: list[ActronAirACSystem] = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
systems = await api.get_ac_systems()
|
systems = await api.get_ac_systems()
|
||||||
await api.update_status()
|
await api.update_status()
|
||||||
except ActronNeoAuthError:
|
except ActronAirAuthError:
|
||||||
_LOGGER.error("Authentication error while setting up Actron Air integration")
|
_LOGGER.error("Authentication error while setting up Actron Air integration")
|
||||||
raise
|
raise
|
||||||
except ActronNeoAPIError as err:
|
except ActronAirAPIError as err:
|
||||||
_LOGGER.error("API error while setting up Actron Air integration: %s", err)
|
_LOGGER.error("API error while setting up Actron Air integration: %s", err)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from actron_neo_api import ActronAirNeoStatus, ActronAirNeoZone
|
from actron_neo_api import ActronAirStatus, ActronAirZone
|
||||||
|
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
FAN_AUTO,
|
FAN_AUTO,
|
||||||
@@ -132,7 +132,7 @@ class ActronSystemClimate(BaseClimateEntity):
|
|||||||
return self._status.max_temp
|
return self._status.max_temp
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _status(self) -> ActronAirNeoStatus:
|
def _status(self) -> ActronAirStatus:
|
||||||
"""Get the current status from the coordinator."""
|
"""Get the current status from the coordinator."""
|
||||||
return self.coordinator.data
|
return self.coordinator.data
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ class ActronZoneClimate(BaseClimateEntity):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: ActronAirSystemCoordinator,
|
coordinator: ActronAirSystemCoordinator,
|
||||||
zone: ActronAirNeoZone,
|
zone: ActronAirZone,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize an Actron Air unit."""
|
"""Initialize an Actron Air unit."""
|
||||||
super().__init__(coordinator, zone.title)
|
super().__init__(coordinator, zone.title)
|
||||||
@@ -221,7 +221,7 @@ class ActronZoneClimate(BaseClimateEntity):
|
|||||||
return self._zone.max_temp
|
return self._zone.max_temp
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _zone(self) -> ActronAirNeoZone:
|
def _zone(self) -> ActronAirZone:
|
||||||
"""Get the current zone data from the coordinator."""
|
"""Get the current zone data from the coordinator."""
|
||||||
status = self.coordinator.data
|
status = self.coordinator.data
|
||||||
return status.zones[self._zone_id]
|
return status.zones[self._zone_id]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from actron_neo_api import ActronNeoAPI, ActronNeoAuthError
|
from actron_neo_api import ActronAirAPI, ActronAirAuthError
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import CONF_API_TOKEN
|
from homeassistant.const import CONF_API_TOKEN
|
||||||
@@ -17,7 +17,7 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialize the config flow."""
|
"""Initialize the config flow."""
|
||||||
self._api: ActronNeoAPI | None = None
|
self._api: ActronAirAPI | None = None
|
||||||
self._device_code: str | None = None
|
self._device_code: str | None = None
|
||||||
self._user_code: str = ""
|
self._user_code: str = ""
|
||||||
self._verification_uri: str = ""
|
self._verification_uri: str = ""
|
||||||
@@ -30,10 +30,10 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle the initial step."""
|
"""Handle the initial step."""
|
||||||
if self._api is None:
|
if self._api is None:
|
||||||
_LOGGER.debug("Initiating device authorization")
|
_LOGGER.debug("Initiating device authorization")
|
||||||
self._api = ActronNeoAPI()
|
self._api = ActronAirAPI()
|
||||||
try:
|
try:
|
||||||
device_code_response = await self._api.request_device_code()
|
device_code_response = await self._api.request_device_code()
|
||||||
except ActronNeoAuthError as err:
|
except ActronAirAuthError as err:
|
||||||
_LOGGER.error("OAuth2 flow failed: %s", err)
|
_LOGGER.error("OAuth2 flow failed: %s", err)
|
||||||
return self.async_abort(reason="oauth2_error")
|
return self.async_abort(reason="oauth2_error")
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
try:
|
try:
|
||||||
await self._api.poll_for_token(self._device_code)
|
await self._api.poll_for_token(self._device_code)
|
||||||
_LOGGER.debug("Authorization successful")
|
_LOGGER.debug("Authorization successful")
|
||||||
except ActronNeoAuthError as ex:
|
except ActronAirAuthError as ex:
|
||||||
_LOGGER.exception("Error while waiting for device authorization")
|
_LOGGER.exception("Error while waiting for device authorization")
|
||||||
raise CannotConnect from ex
|
raise CannotConnect from ex
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
user_data = await self._api.get_user_info()
|
user_data = await self._api.get_user_info()
|
||||||
except ActronNeoAuthError as err:
|
except ActronAirAuthError as err:
|
||||||
_LOGGER.error("Error getting user info: %s", err)
|
_LOGGER.error("Error getting user info: %s", err)
|
||||||
return self.async_abort(reason="oauth2_error")
|
return self.async_abort(reason="oauth2_error")
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from actron_neo_api import ActronAirNeoACSystem, ActronAirNeoStatus, ActronNeoAPI
|
from actron_neo_api import ActronAirACSystem, ActronAirAPI, ActronAirStatus
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@@ -23,7 +23,7 @@ ERROR_UNKNOWN = "unknown_error"
|
|||||||
class ActronAirRuntimeData:
|
class ActronAirRuntimeData:
|
||||||
"""Runtime data for the Actron Air integration."""
|
"""Runtime data for the Actron Air integration."""
|
||||||
|
|
||||||
api: ActronNeoAPI
|
api: ActronAirAPI
|
||||||
system_coordinators: dict[str, ActronAirSystemCoordinator]
|
system_coordinators: dict[str, ActronAirSystemCoordinator]
|
||||||
|
|
||||||
|
|
||||||
@@ -33,15 +33,15 @@ AUTH_ERROR_THRESHOLD = 3
|
|||||||
SCAN_INTERVAL = timedelta(seconds=30)
|
SCAN_INTERVAL = timedelta(seconds=30)
|
||||||
|
|
||||||
|
|
||||||
class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirNeoACSystem]):
|
class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]):
|
||||||
"""System coordinator for Actron Air integration."""
|
"""System coordinator for Actron Air integration."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ActronAirConfigEntry,
|
entry: ActronAirConfigEntry,
|
||||||
api: ActronNeoAPI,
|
api: ActronAirAPI,
|
||||||
system: ActronAirNeoACSystem,
|
system: ActronAirACSystem,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the coordinator."""
|
"""Initialize the coordinator."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -57,7 +57,7 @@ class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirNeoACSystem]):
|
|||||||
self.status = self.api.state_manager.get_status(self.serial_number)
|
self.status = self.api.state_manager.get_status(self.serial_number)
|
||||||
self.last_seen = dt_util.utcnow()
|
self.last_seen = dt_util.utcnow()
|
||||||
|
|
||||||
async def _async_update_data(self) -> ActronAirNeoStatus:
|
async def _async_update_data(self) -> ActronAirStatus:
|
||||||
"""Fetch updates and merge incremental changes into the full state."""
|
"""Fetch updates and merge incremental changes into the full state."""
|
||||||
await self.api.update_status()
|
await self.api.update_status()
|
||||||
self.status = self.api.state_manager.get_status(self.serial_number)
|
self.status = self.api.state_manager.get_status(self.serial_number)
|
||||||
|
|||||||
@@ -12,5 +12,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/actron_air",
|
"documentation": "https://www.home-assistant.io/integrations/actron_air",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["actron-neo-api==0.1.84"]
|
"requirements": ["actron-neo-api==0.1.87"]
|
||||||
}
|
}
|
||||||
|
|||||||
2
requirements_all.txt
generated
2
requirements_all.txt
generated
@@ -133,7 +133,7 @@ WSDiscovery==2.1.2
|
|||||||
accuweather==4.2.2
|
accuweather==4.2.2
|
||||||
|
|
||||||
# homeassistant.components.actron_air
|
# homeassistant.components.actron_air
|
||||||
actron-neo-api==0.1.84
|
actron-neo-api==0.1.87
|
||||||
|
|
||||||
# homeassistant.components.adax
|
# homeassistant.components.adax
|
||||||
adax==0.4.0
|
adax==0.4.0
|
||||||
|
|||||||
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@@ -121,7 +121,7 @@ WSDiscovery==2.1.2
|
|||||||
accuweather==4.2.2
|
accuweather==4.2.2
|
||||||
|
|
||||||
# homeassistant.components.actron_air
|
# homeassistant.components.actron_air
|
||||||
actron-neo-api==0.1.84
|
actron-neo-api==0.1.87
|
||||||
|
|
||||||
# homeassistant.components.adax
|
# homeassistant.components.adax
|
||||||
adax==0.4.0
|
adax==0.4.0
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ def mock_actron_api() -> Generator[AsyncMock]:
|
|||||||
"""Mock the Actron Air API class."""
|
"""Mock the Actron Air API class."""
|
||||||
with (
|
with (
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.actron_air.ActronNeoAPI",
|
"homeassistant.components.actron_air.ActronAirAPI",
|
||||||
autospec=True,
|
autospec=True,
|
||||||
) as mock_api,
|
) as mock_api,
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.actron_air.config_flow.ActronNeoAPI",
|
"homeassistant.components.actron_air.config_flow.ActronAirAPI",
|
||||||
new=mock_api,
|
new=mock_api,
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from unittest.mock import AsyncMock
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
from actron_neo_api import ActronNeoAuthError
|
from actron_neo_api import ActronAirAuthError
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.actron_air.const import DOMAIN
|
from homeassistant.components.actron_air.const import DOMAIN
|
||||||
@@ -76,7 +76,7 @@ async def test_user_flow_oauth2_error(hass: HomeAssistant, mock_actron_api) -> N
|
|||||||
"""Test OAuth2 flow with authentication error during device code request."""
|
"""Test OAuth2 flow with authentication error during device code request."""
|
||||||
# Override the default mock to raise an error
|
# Override the default mock to raise an error
|
||||||
mock_actron_api.request_device_code = AsyncMock(
|
mock_actron_api.request_device_code = AsyncMock(
|
||||||
side_effect=ActronNeoAuthError("OAuth2 error")
|
side_effect=ActronAirAuthError("OAuth2 error")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Start the flow
|
# Start the flow
|
||||||
@@ -95,7 +95,7 @@ async def test_user_flow_token_polling_error(
|
|||||||
"""Test OAuth2 flow with error during token polling."""
|
"""Test OAuth2 flow with error during token polling."""
|
||||||
# Override the default mock to raise an error during token polling
|
# Override the default mock to raise an error during token polling
|
||||||
mock_actron_api.poll_for_token = AsyncMock(
|
mock_actron_api.poll_for_token = AsyncMock(
|
||||||
side_effect=ActronNeoAuthError("Token polling error")
|
side_effect=ActronAirAuthError("Token polling error")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Start the config flow
|
# Start the config flow
|
||||||
|
|||||||
Reference in New Issue
Block a user