Skip to content

Commit 04f83bc

Browse files
kclif9joostlek
andauthored
Add actron_air climate integration (home-assistant#134740)
Co-authored-by: Joost Lekkerkerker <[email protected]>
1 parent f0756af commit 04f83bc

File tree

17 files changed

+906
-0
lines changed

17 files changed

+906
-0
lines changed

CODEOWNERS

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""The Actron Air integration."""
2+
3+
from actron_neo_api import (
4+
ActronAirNeoACSystem,
5+
ActronNeoAPI,
6+
ActronNeoAPIError,
7+
ActronNeoAuthError,
8+
)
9+
10+
from homeassistant.const import CONF_API_TOKEN, Platform
11+
from homeassistant.core import HomeAssistant
12+
13+
from .const import _LOGGER
14+
from .coordinator import (
15+
ActronAirConfigEntry,
16+
ActronAirRuntimeData,
17+
ActronAirSystemCoordinator,
18+
)
19+
20+
PLATFORM = [Platform.CLIMATE]
21+
22+
23+
async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> bool:
24+
"""Set up Actron Air integration from a config entry."""
25+
26+
api = ActronNeoAPI(refresh_token=entry.data[CONF_API_TOKEN])
27+
systems: list[ActronAirNeoACSystem] = []
28+
29+
try:
30+
systems = await api.get_ac_systems()
31+
await api.update_status()
32+
except ActronNeoAuthError:
33+
_LOGGER.error("Authentication error while setting up Actron Air integration")
34+
raise
35+
except ActronNeoAPIError as err:
36+
_LOGGER.error("API error while setting up Actron Air integration: %s", err)
37+
raise
38+
39+
system_coordinators: dict[str, ActronAirSystemCoordinator] = {}
40+
for system in systems:
41+
coordinator = ActronAirSystemCoordinator(hass, entry, api, system)
42+
_LOGGER.debug("Setting up coordinator for system: %s", system["serial"])
43+
await coordinator.async_config_entry_first_refresh()
44+
system_coordinators[system["serial"]] = coordinator
45+
46+
entry.runtime_data = ActronAirRuntimeData(
47+
api=api,
48+
system_coordinators=system_coordinators,
49+
)
50+
51+
await hass.config_entries.async_forward_entry_setups(entry, PLATFORM)
52+
return True
53+
54+
55+
async def async_unload_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> bool:
56+
"""Unload a config entry."""
57+
return await hass.config_entries.async_unload_platforms(entry, PLATFORM)
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
"""Climate platform for Actron Air integration."""
2+
3+
from typing import Any
4+
5+
from actron_neo_api import ActronAirNeoStatus, ActronAirNeoZone
6+
7+
from homeassistant.components.climate import (
8+
FAN_AUTO,
9+
FAN_HIGH,
10+
FAN_LOW,
11+
FAN_MEDIUM,
12+
ClimateEntity,
13+
ClimateEntityFeature,
14+
HVACMode,
15+
)
16+
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
17+
from homeassistant.core import HomeAssistant
18+
from homeassistant.helpers.device_registry import DeviceInfo
19+
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
20+
from homeassistant.helpers.update_coordinator import CoordinatorEntity
21+
22+
from .const import DOMAIN
23+
from .coordinator import ActronAirConfigEntry, ActronAirSystemCoordinator
24+
25+
PARALLEL_UPDATES = 0
26+
27+
FAN_MODE_MAPPING_ACTRONAIR_TO_HA = {
28+
"AUTO": FAN_AUTO,
29+
"LOW": FAN_LOW,
30+
"MED": FAN_MEDIUM,
31+
"HIGH": FAN_HIGH,
32+
}
33+
FAN_MODE_MAPPING_HA_TO_ACTRONAIR = {
34+
v: k for k, v in FAN_MODE_MAPPING_ACTRONAIR_TO_HA.items()
35+
}
36+
HVAC_MODE_MAPPING_ACTRONAIR_TO_HA = {
37+
"COOL": HVACMode.COOL,
38+
"HEAT": HVACMode.HEAT,
39+
"FAN": HVACMode.FAN_ONLY,
40+
"AUTO": HVACMode.AUTO,
41+
"OFF": HVACMode.OFF,
42+
}
43+
HVAC_MODE_MAPPING_HA_TO_ACTRONAIR = {
44+
v: k for k, v in HVAC_MODE_MAPPING_ACTRONAIR_TO_HA.items()
45+
}
46+
47+
48+
async def async_setup_entry(
49+
hass: HomeAssistant,
50+
entry: ActronAirConfigEntry,
51+
async_add_entities: AddConfigEntryEntitiesCallback,
52+
) -> None:
53+
"""Set up Actron Air climate entities."""
54+
system_coordinators = entry.runtime_data.system_coordinators
55+
entities: list[ClimateEntity] = []
56+
57+
for coordinator in system_coordinators.values():
58+
status = coordinator.data
59+
name = status.ac_system.system_name
60+
entities.append(ActronSystemClimate(coordinator, name))
61+
62+
entities.extend(
63+
ActronZoneClimate(coordinator, zone)
64+
for zone in status.remote_zone_info
65+
if zone.exists
66+
)
67+
68+
async_add_entities(entities)
69+
70+
71+
class BaseClimateEntity(CoordinatorEntity[ActronAirSystemCoordinator], ClimateEntity):
72+
"""Base class for Actron Air climate entities."""
73+
74+
_attr_has_entity_name = True
75+
_attr_temperature_unit = UnitOfTemperature.CELSIUS
76+
_attr_supported_features = (
77+
ClimateEntityFeature.TARGET_TEMPERATURE
78+
| ClimateEntityFeature.FAN_MODE
79+
| ClimateEntityFeature.TURN_ON
80+
| ClimateEntityFeature.TURN_OFF
81+
)
82+
_attr_name = None
83+
_attr_fan_modes = list(FAN_MODE_MAPPING_ACTRONAIR_TO_HA.values())
84+
_attr_hvac_modes = list(HVAC_MODE_MAPPING_ACTRONAIR_TO_HA.values())
85+
86+
def __init__(
87+
self,
88+
coordinator: ActronAirSystemCoordinator,
89+
name: str,
90+
) -> None:
91+
"""Initialize an Actron Air unit."""
92+
super().__init__(coordinator)
93+
self._serial_number = coordinator.serial_number
94+
95+
96+
class ActronSystemClimate(BaseClimateEntity):
97+
"""Representation of the Actron Air system."""
98+
99+
_attr_supported_features = (
100+
ClimateEntityFeature.TARGET_TEMPERATURE
101+
| ClimateEntityFeature.FAN_MODE
102+
| ClimateEntityFeature.TURN_ON
103+
| ClimateEntityFeature.TURN_OFF
104+
)
105+
106+
def __init__(
107+
self,
108+
coordinator: ActronAirSystemCoordinator,
109+
name: str,
110+
) -> None:
111+
"""Initialize an Actron Air unit."""
112+
super().__init__(coordinator, name)
113+
serial_number = coordinator.serial_number
114+
self._attr_unique_id = serial_number
115+
self._attr_device_info = DeviceInfo(
116+
identifiers={(DOMAIN, serial_number)},
117+
name=self._status.ac_system.system_name,
118+
manufacturer="Actron Air",
119+
model_id=self._status.ac_system.master_wc_model,
120+
sw_version=self._status.ac_system.master_wc_firmware_version,
121+
serial_number=serial_number,
122+
)
123+
124+
@property
125+
def min_temp(self) -> float:
126+
"""Return the minimum temperature that can be set."""
127+
return self._status.min_temp
128+
129+
@property
130+
def max_temp(self) -> float:
131+
"""Return the maximum temperature that can be set."""
132+
return self._status.max_temp
133+
134+
@property
135+
def _status(self) -> ActronAirNeoStatus:
136+
"""Get the current status from the coordinator."""
137+
return self.coordinator.data
138+
139+
@property
140+
def hvac_mode(self) -> HVACMode | None:
141+
"""Return the current HVAC mode."""
142+
if not self._status.user_aircon_settings.is_on:
143+
return HVACMode.OFF
144+
145+
mode = self._status.user_aircon_settings.mode
146+
return HVAC_MODE_MAPPING_ACTRONAIR_TO_HA.get(mode)
147+
148+
@property
149+
def fan_mode(self) -> str | None:
150+
"""Return the current fan mode."""
151+
fan_mode = self._status.user_aircon_settings.fan_mode
152+
return FAN_MODE_MAPPING_ACTRONAIR_TO_HA.get(fan_mode)
153+
154+
@property
155+
def current_humidity(self) -> float:
156+
"""Return the current humidity."""
157+
return self._status.master_info.live_humidity_pc
158+
159+
@property
160+
def current_temperature(self) -> float:
161+
"""Return the current temperature."""
162+
return self._status.master_info.live_temp_c
163+
164+
@property
165+
def target_temperature(self) -> float:
166+
"""Return the target temperature."""
167+
return self._status.user_aircon_settings.temperature_setpoint_cool_c
168+
169+
async def async_set_fan_mode(self, fan_mode: str) -> None:
170+
"""Set a new fan mode."""
171+
api_fan_mode = FAN_MODE_MAPPING_HA_TO_ACTRONAIR.get(fan_mode.lower())
172+
await self._status.user_aircon_settings.set_fan_mode(api_fan_mode)
173+
174+
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
175+
"""Set the HVAC mode."""
176+
ac_mode = HVAC_MODE_MAPPING_HA_TO_ACTRONAIR.get(hvac_mode)
177+
await self._status.ac_system.set_system_mode(ac_mode)
178+
179+
async def async_set_temperature(self, **kwargs: Any) -> None:
180+
"""Set the temperature."""
181+
temp = kwargs.get(ATTR_TEMPERATURE)
182+
await self._status.user_aircon_settings.set_temperature(temperature=temp)
183+
184+
185+
class ActronZoneClimate(BaseClimateEntity):
186+
"""Representation of a zone within the Actron Air system."""
187+
188+
_attr_supported_features = (
189+
ClimateEntityFeature.TARGET_TEMPERATURE
190+
| ClimateEntityFeature.TURN_ON
191+
| ClimateEntityFeature.TURN_OFF
192+
)
193+
194+
def __init__(
195+
self,
196+
coordinator: ActronAirSystemCoordinator,
197+
zone: ActronAirNeoZone,
198+
) -> None:
199+
"""Initialize an Actron Air unit."""
200+
super().__init__(coordinator, zone.title)
201+
serial_number = coordinator.serial_number
202+
self._zone_id: int = zone.zone_id
203+
self._attr_unique_id: str = f"{serial_number}_zone_{zone.zone_id}"
204+
self._attr_device_info: DeviceInfo = DeviceInfo(
205+
identifiers={(DOMAIN, self._attr_unique_id)},
206+
name=zone.title,
207+
manufacturer="Actron Air",
208+
model="Zone",
209+
suggested_area=zone.title,
210+
via_device=(DOMAIN, serial_number),
211+
)
212+
213+
@property
214+
def min_temp(self) -> float:
215+
"""Return the minimum temperature that can be set."""
216+
return self._zone.min_temp
217+
218+
@property
219+
def max_temp(self) -> float:
220+
"""Return the maximum temperature that can be set."""
221+
return self._zone.max_temp
222+
223+
@property
224+
def _zone(self) -> ActronAirNeoZone:
225+
"""Get the current zone data from the coordinator."""
226+
status = self.coordinator.data
227+
return status.zones[self._zone_id]
228+
229+
@property
230+
def hvac_mode(self) -> HVACMode | None:
231+
"""Return the current HVAC mode."""
232+
if self._zone.is_active:
233+
mode = self._zone.hvac_mode
234+
return HVAC_MODE_MAPPING_ACTRONAIR_TO_HA.get(mode)
235+
return HVACMode.OFF
236+
237+
@property
238+
def current_humidity(self) -> float | None:
239+
"""Return the current humidity."""
240+
return self._zone.humidity
241+
242+
@property
243+
def current_temperature(self) -> float | None:
244+
"""Return the current temperature."""
245+
return self._zone.live_temp_c
246+
247+
@property
248+
def target_temperature(self) -> float | None:
249+
"""Return the target temperature."""
250+
return self._zone.temperature_setpoint_cool_c
251+
252+
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
253+
"""Set the HVAC mode."""
254+
is_enabled = hvac_mode != HVACMode.OFF
255+
await self._zone.enable(is_enabled)
256+
257+
async def async_set_temperature(self, **kwargs: Any) -> None:
258+
"""Set the temperature."""
259+
await self._zone.set_temperature(temperature=kwargs["temperature"])

0 commit comments

Comments
 (0)