1 """Support for the iZone HVAC."""
3 from __future__
import annotations
5 from collections.abc
import Callable, Mapping
7 from typing
import Any, Concatenate
9 from pizone
import Controller, Zone
10 import voluptuous
as vol
42 DATA_DISCOVERY_SERVICE,
43 DISPATCH_CONTROLLER_DISCONNECTED,
44 DISPATCH_CONTROLLER_DISCOVERED,
45 DISPATCH_CONTROLLER_RECONNECTED,
46 DISPATCH_CONTROLLER_UPDATE,
51 type _FuncType[_T, **_P, _R] = Callable[Concatenate[_T, _P], _R]
53 _LOGGER = logging.getLogger(__name__)
56 Controller.Fan.LOW: FAN_LOW,
57 Controller.Fan.MED: FAN_MEDIUM,
58 Controller.Fan.HIGH: FAN_HIGH,
59 Controller.Fan.TOP: FAN_TOP,
60 Controller.Fan.AUTO: FAN_AUTO,
63 ATTR_AIRFLOW =
"airflow"
65 IZONE_SERVICE_AIRFLOW_MIN =
"airflow_min"
66 IZONE_SERVICE_AIRFLOW_MAX =
"airflow_max"
68 IZONE_SERVICE_AIRFLOW_SCHEMA: VolDictType = {
69 vol.Required(ATTR_AIRFLOW): vol.All(
70 vol.Coerce(int), vol.Range(min=0, max=100), msg=
"invalid airflow"
76 hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback
78 """Initialize an IZone Controller."""
79 disco = hass.data[DATA_DISCOVERY_SERVICE]
83 """Register the controller device and the containing zones."""
84 conf: ConfigType |
None = hass.data.get(DATA_CONFIG)
87 if conf
and ctrl.device_uid
in conf[CONF_EXCLUDE]:
88 _LOGGER.debug(
"Controller UID=%s ignored as excluded", ctrl.device_uid)
90 _LOGGER.debug(
"Controller UID=%s discovered", ctrl.device_uid)
97 for controller
in disco.pi_disco.controllers.values():
101 config.async_on_unload(
105 platform = entity_platform.async_get_current_platform()
106 platform.async_register_entity_service(
107 IZONE_SERVICE_AIRFLOW_MIN,
108 IZONE_SERVICE_AIRFLOW_SCHEMA,
109 "async_set_airflow_min",
111 platform.async_register_entity_service(
112 IZONE_SERVICE_AIRFLOW_MAX,
113 IZONE_SERVICE_AIRFLOW_SCHEMA,
114 "async_set_airflow_max",
118 def _return_on_connection_error[_DeviceT: ControllerDevice | ZoneDevice, **_P, _R, _T](
120 ) -> Callable[[_FuncType[_DeviceT, _P, _R]], _FuncType[_DeviceT, _P, _R | _T]]:
121 def wrap(func: _FuncType[_DeviceT, _P, _R]) -> _FuncType[_DeviceT, _P, _R | _T]:
122 def wrapped_f(self: _DeviceT, *args: _P.args, **kwargs: _P.kwargs) -> _R | _T:
123 if not self.available:
126 return func(self, *args, **kwargs)
127 except ConnectionError:
136 """Representation of iZone Controller."""
138 _attr_precision = PRECISION_TENTHS
139 _attr_should_poll =
False
140 _attr_temperature_unit = UnitOfTemperature.CELSIUS
141 _attr_has_entity_name =
True
143 _attr_target_temperature_step = 0.5
144 _enable_turn_on_off_backwards_compatibility =
False
146 def __init__(self, controller: Controller) ->
None:
147 """Initialise ControllerDevice."""
151 ClimateEntityFeature.FAN_MODE
152 | ClimateEntityFeature.TURN_OFF
153 | ClimateEntityFeature.TURN_ON
161 controller.ras_mode ==
"master"
162 and controller.zone_ctrl > controller.zones_total
163 )
or controller.ras_mode ==
"RAS":
167 HVACMode.COOL: Controller.Mode.COOL,
168 HVACMode.HEAT: Controller.Mode.HEAT,
169 HVACMode.HEAT_COOL: Controller.Mode.AUTO,
170 HVACMode.FAN_ONLY: Controller.Mode.VENT,
171 HVACMode.DRY: Controller.Mode.DRY,
173 if controller.free_air_enabled:
177 for fan
in controller.fan_modes:
182 identifiers={(IZONE, controller.device_uid)},
183 manufacturer=
"IZone",
184 model=controller.sys_type,
185 name=f
"iZone Controller {controller.device_uid}",
190 for zone
in controller.zones:
194 """Call on adding to hass."""
198 def controller_disconnected(ctrl: Controller, ex: Exception) ->
None:
199 """Disconnected from controller."""
206 self.
hasshass, DISPATCH_CONTROLLER_DISCONNECTED, controller_disconnected
211 def controller_reconnected(ctrl: Controller) ->
None:
212 """Reconnected to controller."""
219 self.
hasshass, DISPATCH_CONTROLLER_RECONNECTED, controller_reconnected
224 def controller_update(ctrl: Controller) ->
None:
225 """Handle controller data updates."""
234 self.
hasshass, DISPATCH_CONTROLLER_UPDATE, controller_update
239 def set_available(self, available: bool, ex: Exception |
None =
None) ->
None:
240 """Set availability for the controller.
242 Also sets zone availability as they follow the same availability.
248 _LOGGER.warning(
"Reconnected controller %s ", self.
_controller_controller.device_uid)
251 "Controller %s disconnected due to exception: %s",
258 for zone
in self.
zoneszones.values():
259 if zone.hass
is not None:
260 zone.async_schedule_update_ha_state()
264 """Return the optional state attributes."""
266 "supply_temperature": show_temp(
272 "temp_setpoint": show_temp(
278 "control_zone": self.
_controller_controller.zone_ctrl,
283 "control_zone_setpoint": show_temp(
293 """Return current operation ie. heat, cool, idle."""
296 if (mode := self.
_controller_controller.mode) == Controller.Mode.FREE_AIR:
297 return HVACMode.FAN_ONLY
301 raise RuntimeError(
"Should be unreachable")
304 @_return_on_connection_error([])
306 """Return the list of available operation modes."""
308 return [HVACMode.OFF, HVACMode.FAN_ONLY]
312 @_return_on_connection_error(PRESET_NONE)
314 """Eco mode is external air."""
315 return PRESET_ECO
if self.
_controller_controller.free_air
else PRESET_NONE
318 @_return_on_connection_error([PRESET_NONE])
320 """Available preset modes, normal or eco."""
322 return [PRESET_NONE, PRESET_ECO]
326 @_return_on_connection_error()
328 """Return the current temperature."""
329 if self.
_controller_controller.mode == Controller.Mode.FREE_AIR:
335 """Return the zone that currently controls the AC unit (if target temp not set by controller)."""
339 zone = next((z
for z
in self.
zoneszones.values()
if z.zone_index == zone_ctrl),
None)
346 """Return the temperature setpoint of the zone that currently controls the AC unit (if target temp not set by controller)."""
350 zone = next((z
for z
in self.
zoneszones.values()
if z.zone_index == zone_ctrl),
None)
353 return zone.target_temperature
356 @_return_on_connection_error()
358 """Return the temperature we try to reach (either from control zone or master unit)."""
365 """Return the current supply, or in duct, temperature."""
370 """Return the fan setting."""
371 return _IZONE_FAN_TO_HA[self.
_controller_controller.fan]
375 """Return the list of available fan modes."""
379 @_return_on_connection_error(0.0)
381 """Return the minimum temperature."""
385 @_return_on_connection_error(50.0)
387 """Return the maximum temperature."""
391 """Catch any connection errors and set unavailable."""
394 except ConnectionError
as ex:
400 """Set new target temperature."""
404 if (temp := kwargs.get(ATTR_TEMPERATURE))
is not None:
408 """Set new target fan mode."""
413 """Set new target operation mode."""
414 if hvac_mode == HVACMode.OFF:
425 """Set the preset mode."""
427 self.
_controller_controller.set_free_air(preset_mode == PRESET_ECO)
431 """Turn the entity on."""
436 """Representation of iZone Zone."""
438 _attr_precision = PRECISION_TENTHS
439 _attr_should_poll =
False
440 _attr_has_entity_name =
True
442 _attr_temperature_unit = UnitOfTemperature.CELSIUS
443 _attr_target_temperature_step = 0.5
444 _attr_supported_features = (
445 ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
448 def __init__(self, controller: ControllerDevice, zone: Zone) ->
None:
449 """Initialise ZoneDevice."""
453 if zone.type != Zone.Type.AUTO:
455 HVACMode.OFF: Zone.Mode.CLOSE,
456 HVACMode.FAN_ONLY: Zone.Mode.OPEN,
460 HVACMode.OFF: Zone.Mode.CLOSE,
461 HVACMode.FAN_ONLY: Zone.Mode.OPEN,
462 HVACMode.HEAT_COOL: Zone.Mode.AUTO,
466 assert controller.unique_id
469 (IZONE, controller.unique_id, zone.index)
471 manufacturer=
"IZone",
472 model=zone.type.name.title(),
473 name=zone.name.title(),
474 via_device=(IZONE, controller.unique_id),
478 """Call on adding to hass."""
481 def controller_update(ctrl: Controller) ->
None:
482 """Handle controller data updates."""
483 if ctrl.device_uid != self.
_controller_controller.unique_id:
491 self.
hasshass, DISPATCH_CONTROLLER_UPDATE, controller_update
496 def zone_update(ctrl: Controller, zone: Zone) ->
None:
497 """Handle zone data updates."""
498 if zone
is not self.
_zone_zone:
510 """Return True if entity is available."""
514 @_return_on_connection_error(ClimateEntityFeature(0))
516 """Return the list of supported features."""
517 if self.
_zone_zone.mode == Zone.Mode.AUTO:
523 """Return current operation ie. heat, cool, idle."""
524 mode = self.
_zone_zone.mode
532 """Return the list of available operation modes."""
537 """Return the current temperature."""
538 return self.
_zone_zone.temp_current
542 """Return the temperature we try to reach."""
543 if self.
_zone_zone.type != Zone.Type.AUTO:
545 return self.
_zone_zone.temp_setpoint
549 """Return the minimum temperature."""
554 """Return the maximum temperature."""
559 """Return the minimum air flow."""
560 return self.
_zone_zone.airflow_min
564 """Return the maximum air flow."""
565 return self.
_zone_zone.airflow_max
568 """Set new airflow minimum."""
570 self.
_zone_zone.set_airflow_min(
int(kwargs[ATTR_AIRFLOW]))
575 """Set new airflow maximum."""
577 self.
_zone_zone.set_airflow_max(
int(kwargs[ATTR_AIRFLOW]))
582 """Set new target temperature."""
583 if self.
_zone_zone.mode != Zone.Mode.AUTO:
585 if (temp := kwargs.get(ATTR_TEMPERATURE))
is not None:
586 await self.
_controller_controller.wrap_and_catch(self.
_zone_zone.set_temp_setpoint(temp))
589 """Set new target operation mode."""
591 await self.
_controller_controller.wrap_and_catch(self.
_zone_zone.set_mode(mode))
596 """Return true if on."""
597 return self.
_zone_zone.mode != Zone.Mode.CLOSE
600 """Turn device on (open zone)."""
601 if self.
_zone_zone.type == Zone.Type.AUTO:
602 await self.
_controller_controller.wrap_and_catch(self.
_zone_zone.set_mode(Zone.Mode.AUTO))
604 await self.
_controller_controller.wrap_and_catch(self.
_zone_zone.set_mode(Zone.Mode.OPEN))
608 """Turn device off (close zone)."""
609 await self.
_controller_controller.wrap_and_catch(self.
_zone_zone.set_mode(Zone.Mode.CLOSE))
614 """Return the zone index for matching to CtrlZone."""
615 return self.
_zone_zone.index
619 """Return the optional state attributes."""
621 "airflow_max": self.
_zone_zone.airflow_max,
622 "airflow_min": self.
_zone_zone.airflow_min,
str temperature_unit(self)
ClimateEntityFeature supported_features(self)
def control_zone_name(self)
None __init__(self, Controller controller)
None async_set_preset_mode(self, str preset_mode)
float|None control_zone_setpoint(self)
def wrap_and_catch(self, coro)
list[HVACMode] hvac_modes(self)
float supply_temperature(self)
list[str]|None fan_modes(self)
Mapping[str, Any] extra_state_attributes(self)
None async_set_temperature(self, **Any kwargs)
None async_added_to_hass(self)
float|None current_temperature(self)
None set_available(self, bool available, Exception|None ex=None)
float|None target_temperature(self)
None async_set_hvac_mode(self, HVACMode hvac_mode)
list[str] preset_modes(self)
None async_set_fan_mode(self, str fan_mode)
def async_set_airflow_min(self, **kwargs)
None __init__(self, ControllerDevice controller, Zone zone)
float current_temperature(self)
def async_set_airflow_max(self, **kwargs)
None async_set_temperature(self, **Any kwargs)
ClimateEntityFeature supported_features(self)
None async_turn_off(self)
float|None target_temperature(self)
None async_set_hvac_mode(self, HVACMode hvac_mode)
None async_added_to_hass(self)
Mapping[str, Any] extra_state_attributes(self)
tuple _attr_supported_features
list[HVACMode] hvac_modes(self)
None async_schedule_update_ha_state(self, bool force_refresh=False)
None async_write_ha_state(self)
int|None supported_features(self)
None async_on_remove(self, CALLBACK_TYPE func)
FibaroController init_controller(Mapping[str, Any] data)
None async_setup_entry(HomeAssistant hass, ConfigEntry config, AddEntitiesCallback async_add_entities)
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)