Izone component (#24550)
* iZone component * Rename constants to const. * Changes as per code review. * Stop listening if discovery times out. * Unload properly * Changes as per code review * Climate 1.0 * Use dispatcher instead of listener * Free air settings * Test case for config flow. * Changes as per code review * Fix error on shutdown * Changes as per code review * Lint fix * Black formatting * Black on test * Fix test * Lint fix * Formatting * Updated requirements * Remaining patches * Per code r/v
This commit is contained in:
parent
c8fb7ce98b
commit
b68b8430a4
16 changed files with 894 additions and 0 deletions
|
@ -295,6 +295,9 @@ omit =
|
|||
homeassistant/components/iaqualink/sensor.py
|
||||
homeassistant/components/iaqualink/switch.py
|
||||
homeassistant/components/icloud/device_tracker.py
|
||||
homeassistant/components/izone/climate.py
|
||||
homeassistant/components/izone/discovery.py
|
||||
homeassistant/components/izone/__init__.py
|
||||
homeassistant/components/idteck_prox/*
|
||||
homeassistant/components/ifttt/*
|
||||
homeassistant/components/iglo/light.py
|
||||
|
|
|
@ -144,6 +144,7 @@ homeassistant/components/ios/* @robbiet480
|
|||
homeassistant/components/ipma/* @dgomes
|
||||
homeassistant/components/iqvia/* @bachya
|
||||
homeassistant/components/irish_rail_transport/* @ttroy50
|
||||
homeassistant/components/izone/* @Swamp-Ig
|
||||
homeassistant/components/jewish_calendar/* @tsvi
|
||||
homeassistant/components/keba/* @dannerph
|
||||
homeassistant/components/knx/* @Julius2342
|
||||
|
|
15
homeassistant/components/izone/.translations/en.json
Normal file
15
homeassistant/components/izone/.translations/en.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "No iZone devices found on the network.",
|
||||
"single_instance_allowed": "Only a single configuration of iZone is necessary."
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Do you want to set up iZone?",
|
||||
"title": "iZone"
|
||||
}
|
||||
},
|
||||
"title": "iZone"
|
||||
}
|
||||
}
|
67
homeassistant/components/izone/__init__.py
Normal file
67
homeassistant/components/izone/__init__.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
"""
|
||||
Platform for the iZone AC.
|
||||
|
||||
For more details about this component, please refer to the documentation
|
||||
https://home-assistant.io/components/izone/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_EXCLUDE
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||
|
||||
from .const import IZONE, DATA_CONFIG
|
||||
from .discovery import async_start_discovery_service, async_stop_discovery_service
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
IZONE: vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_EXCLUDE, default=[]): vol.All(
|
||||
cv.ensure_list, [cv.string]
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
"""Register the iZone component config."""
|
||||
conf = config.get(IZONE)
|
||||
if not conf:
|
||||
return True
|
||||
|
||||
hass.data[DATA_CONFIG] = conf
|
||||
|
||||
# Explicitly added in the config file, create a config entry.
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
IZONE, context={"source": config_entries.SOURCE_IMPORT}
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Set up from a config entry."""
|
||||
await async_start_discovery_service(hass)
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, "climate")
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry):
|
||||
"""Unload the config entry and stop discovery process."""
|
||||
await async_stop_discovery_service(hass)
|
||||
await hass.config_entries.async_forward_entry_unload(entry, "climate")
|
||||
return True
|
546
homeassistant/components/izone/climate.py
Normal file
546
homeassistant/components/izone/climate.py
Normal file
|
@ -0,0 +1,546 @@
|
|||
"""Support for the iZone HVAC."""
|
||||
import logging
|
||||
from typing import Optional, List
|
||||
|
||||
from pizone import Zone, Controller
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.climate.const import (
|
||||
HVAC_MODE_HEAT_COOL,
|
||||
HVAC_MODE_COOL,
|
||||
HVAC_MODE_DRY,
|
||||
HVAC_MODE_FAN_ONLY,
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_OFF,
|
||||
FAN_LOW,
|
||||
FAN_MEDIUM,
|
||||
FAN_HIGH,
|
||||
FAN_AUTO,
|
||||
PRESET_ECO,
|
||||
PRESET_NONE,
|
||||
SUPPORT_FAN_MODE,
|
||||
SUPPORT_PRESET_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE,
|
||||
PRECISION_HALVES,
|
||||
TEMP_CELSIUS,
|
||||
CONF_EXCLUDE,
|
||||
)
|
||||
from homeassistant.helpers.temperature import display_temp as show_temp
|
||||
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from .const import (
|
||||
DATA_DISCOVERY_SERVICE,
|
||||
IZONE,
|
||||
DISPATCH_CONTROLLER_DISCOVERED,
|
||||
DISPATCH_CONTROLLER_DISCONNECTED,
|
||||
DISPATCH_CONTROLLER_RECONNECTED,
|
||||
DISPATCH_CONTROLLER_UPDATE,
|
||||
DISPATCH_ZONE_UPDATE,
|
||||
DATA_CONFIG,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_IZONE_FAN_TO_HA = {
|
||||
Controller.Fan.LOW: FAN_LOW,
|
||||
Controller.Fan.MED: FAN_MEDIUM,
|
||||
Controller.Fan.HIGH: FAN_HIGH,
|
||||
Controller.Fan.AUTO: FAN_AUTO,
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistantType, config: ConfigType, async_add_entities
|
||||
):
|
||||
"""Initialize an IZone Controller."""
|
||||
disco = hass.data[DATA_DISCOVERY_SERVICE]
|
||||
|
||||
@callback
|
||||
def init_controller(ctrl: Controller):
|
||||
"""Register the controller device and the containing zones."""
|
||||
conf = hass.data.get(DATA_CONFIG) # type: ConfigType
|
||||
|
||||
# Filter out any entities excluded in the config file
|
||||
if conf and ctrl.device_uid in conf[CONF_EXCLUDE]:
|
||||
_LOGGER.info("Controller UID=%s ignored as excluded", ctrl.device_uid)
|
||||
return
|
||||
_LOGGER.info("Controller UID=%s discovered", ctrl.device_uid)
|
||||
|
||||
device = ControllerDevice(ctrl)
|
||||
async_add_entities([device])
|
||||
async_add_entities(device.zones.values())
|
||||
|
||||
# create any components not yet created
|
||||
for controller in disco.pi_disco.controllers.values():
|
||||
init_controller(controller)
|
||||
|
||||
# connect to register any further components
|
||||
async_dispatcher_connect(hass, DISPATCH_CONTROLLER_DISCOVERED, init_controller)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class ControllerDevice(ClimateDevice):
|
||||
"""Representation of iZone Controller."""
|
||||
|
||||
def __init__(self, controller: Controller) -> None:
|
||||
"""Initialise ControllerDevice."""
|
||||
self._controller = controller
|
||||
|
||||
self._supported_features = SUPPORT_FAN_MODE
|
||||
|
||||
if (
|
||||
controller.ras_mode == "master" and controller.zone_ctrl == 13
|
||||
) or controller.ras_mode == "RAS":
|
||||
self._supported_features |= SUPPORT_TARGET_TEMPERATURE
|
||||
|
||||
self._state_to_pizone = {
|
||||
HVAC_MODE_COOL: Controller.Mode.COOL,
|
||||
HVAC_MODE_HEAT: Controller.Mode.HEAT,
|
||||
HVAC_MODE_HEAT_COOL: Controller.Mode.AUTO,
|
||||
HVAC_MODE_FAN_ONLY: Controller.Mode.VENT,
|
||||
HVAC_MODE_DRY: Controller.Mode.DRY,
|
||||
}
|
||||
if controller.free_air_enabled:
|
||||
self._supported_features |= SUPPORT_PRESET_MODE
|
||||
|
||||
self._fan_to_pizone = {}
|
||||
for fan in controller.fan_modes:
|
||||
self._fan_to_pizone[_IZONE_FAN_TO_HA[fan]] = fan
|
||||
self._available = True
|
||||
|
||||
self._device_info = {
|
||||
"identifiers": {(IZONE, self.unique_id)},
|
||||
"name": self.name,
|
||||
"manufacturer": "IZone",
|
||||
"model": self._controller.sys_type,
|
||||
}
|
||||
|
||||
# Create the zones
|
||||
self.zones = {}
|
||||
for zone in controller.zones:
|
||||
self.zones[zone] = ZoneDevice(self, zone)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Call on adding to hass."""
|
||||
# Register for connect/disconnect/update events
|
||||
@callback
|
||||
def controller_disconnected(ctrl: Controller, ex: Exception) -> None:
|
||||
"""Disconnected from controller."""
|
||||
if ctrl is not self._controller:
|
||||
return
|
||||
self.set_available(False, ex)
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass, DISPATCH_CONTROLLER_DISCONNECTED, controller_disconnected
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def controller_reconnected(ctrl: Controller) -> None:
|
||||
"""Reconnected to controller."""
|
||||
if ctrl is not self._controller:
|
||||
return
|
||||
self.set_available(True)
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass, DISPATCH_CONTROLLER_RECONNECTED, controller_reconnected
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def controller_update(ctrl: Controller) -> None:
|
||||
"""Handle controller data updates."""
|
||||
if ctrl is not self._controller:
|
||||
return
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass, DISPATCH_CONTROLLER_UPDATE, controller_update
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return self._available
|
||||
|
||||
@callback
|
||||
def set_available(self, available: bool, ex: Exception = None) -> None:
|
||||
"""
|
||||
Set availability for the controller.
|
||||
|
||||
Also sets zone availability as they follow the same availability.
|
||||
"""
|
||||
if self.available == available:
|
||||
return
|
||||
|
||||
if available:
|
||||
_LOGGER.info("Reconnected controller %s ", self._controller.device_uid)
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Controller %s disconnected due to exception: %s",
|
||||
self._controller.device_uid,
|
||||
ex,
|
||||
)
|
||||
|
||||
self._available = available
|
||||
self.async_schedule_update_ha_state()
|
||||
for zone in self.zones.values():
|
||||
zone.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return the device info for the iZone system."""
|
||||
return self._device_info
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of the controller device."""
|
||||
return self._controller.device_uid
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the entity."""
|
||||
return f"iZone Controller {self._controller.device_uid}"
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Return True if entity has to be polled for state.
|
||||
|
||||
False if entity pushes its state to HA.
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Return the list of supported features."""
|
||||
return self._supported_features
|
||||
|
||||
@property
|
||||
def temperature_unit(self) -> str:
|
||||
"""Return the unit of measurement which this thermostat uses."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def precision(self) -> float:
|
||||
"""Return the precision of the system."""
|
||||
return PRECISION_HALVES
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the optional state attributes."""
|
||||
return {
|
||||
"supply_temperature": show_temp(
|
||||
self.hass,
|
||||
self.supply_temperature,
|
||||
self.temperature_unit,
|
||||
self.precision,
|
||||
),
|
||||
"temp_setpoint": show_temp(
|
||||
self.hass,
|
||||
self._controller.temp_setpoint,
|
||||
self.temperature_unit,
|
||||
self.precision,
|
||||
),
|
||||
}
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> str:
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if not self._controller.is_on:
|
||||
return HVAC_MODE_OFF
|
||||
mode = self._controller.mode
|
||||
for (key, value) in self._state_to_pizone.items():
|
||||
if value == mode:
|
||||
return key
|
||||
assert False, "Should be unreachable"
|
||||
|
||||
@property
|
||||
def hvac_modes(self) -> List[str]:
|
||||
"""Return the list of available operation modes."""
|
||||
if self._controller.free_air:
|
||||
return [HVAC_MODE_OFF, HVAC_MODE_FAN_ONLY]
|
||||
return [HVAC_MODE_OFF, *self._state_to_pizone]
|
||||
|
||||
@property
|
||||
def preset_mode(self):
|
||||
"""Eco mode is external air."""
|
||||
return PRESET_ECO if self._controller.free_air else PRESET_NONE
|
||||
|
||||
@property
|
||||
def preset_modes(self):
|
||||
"""Available preset modes, normal or eco."""
|
||||
if self._controller.free_air_enabled:
|
||||
return [PRESET_NONE, PRESET_ECO]
|
||||
return [PRESET_NONE]
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> Optional[float]:
|
||||
"""Return the current temperature."""
|
||||
if self._controller.mode == Controller.Mode.FREE_AIR:
|
||||
return self._controller.temp_supply
|
||||
return self._controller.temp_return
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> Optional[float]:
|
||||
"""Return the temperature we try to reach."""
|
||||
if not self._supported_features & SUPPORT_TARGET_TEMPERATURE:
|
||||
return None
|
||||
return self._controller.temp_setpoint
|
||||
|
||||
@property
|
||||
def supply_temperature(self) -> float:
|
||||
"""Return the current supply, or in duct, temperature."""
|
||||
return self._controller.temp_supply
|
||||
|
||||
@property
|
||||
def target_temperature_step(self) -> Optional[float]:
|
||||
"""Return the supported step of target temperature."""
|
||||
return 0.5
|
||||
|
||||
@property
|
||||
def fan_mode(self) -> Optional[str]:
|
||||
"""Return the fan setting."""
|
||||
return _IZONE_FAN_TO_HA[self._controller.fan]
|
||||
|
||||
@property
|
||||
def fan_modes(self) -> Optional[List[str]]:
|
||||
"""Return the list of available fan modes."""
|
||||
return list(self._fan_to_pizone)
|
||||
|
||||
@property
|
||||
def min_temp(self) -> float:
|
||||
"""Return the minimum temperature."""
|
||||
return self._controller.temp_min
|
||||
|
||||
@property
|
||||
def max_temp(self) -> float:
|
||||
"""Return the maximum temperature."""
|
||||
return self._controller.temp_max
|
||||
|
||||
async def wrap_and_catch(self, coro):
|
||||
"""Catch any connection errors and set unavailable."""
|
||||
try:
|
||||
await coro
|
||||
except ConnectionError as ex:
|
||||
self.set_available(False, ex)
|
||||
else:
|
||||
self.set_available(True)
|
||||
|
||||
async def async_set_temperature(self, **kwargs) -> None:
|
||||
"""Set new target temperature."""
|
||||
if not self.supported_features & SUPPORT_TARGET_TEMPERATURE:
|
||||
self.async_schedule_update_ha_state(True)
|
||||
return
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temp is not None:
|
||||
await self.wrap_and_catch(self._controller.set_temp_setpoint(temp))
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Set new target fan mode."""
|
||||
fan = self._fan_to_pizone[fan_mode]
|
||||
await self.wrap_and_catch(self._controller.set_fan(fan))
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
|
||||
"""Set new target operation mode."""
|
||||
if hvac_mode == HVAC_MODE_OFF:
|
||||
await self.wrap_and_catch(self._controller.set_on(False))
|
||||
return
|
||||
if not self._controller.is_on:
|
||||
await self.wrap_and_catch(self._controller.set_on(True))
|
||||
if self._controller.free_air:
|
||||
return
|
||||
mode = self._state_to_pizone[hvac_mode]
|
||||
await self.wrap_and_catch(self._controller.set_mode(mode))
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set the preset mode."""
|
||||
await self.wrap_and_catch(
|
||||
self._controller.set_free_air(preset_mode == PRESET_ECO)
|
||||
)
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the entity on."""
|
||||
await self.wrap_and_catch(self._controller.set_on(True))
|
||||
|
||||
|
||||
class ZoneDevice(ClimateDevice):
|
||||
"""Representation of iZone Zone."""
|
||||
|
||||
def __init__(self, controller: ControllerDevice, zone: Zone) -> None:
|
||||
"""Initialise ZoneDevice."""
|
||||
self._controller = controller
|
||||
self._zone = zone
|
||||
self._name = zone.name.title()
|
||||
|
||||
self._supported_features = 0
|
||||
if zone.type != Zone.Type.AUTO:
|
||||
self._state_to_pizone = {
|
||||
HVAC_MODE_OFF: Zone.Mode.CLOSE,
|
||||
HVAC_MODE_FAN_ONLY: Zone.Mode.OPEN,
|
||||
}
|
||||
else:
|
||||
self._state_to_pizone = {
|
||||
HVAC_MODE_OFF: Zone.Mode.CLOSE,
|
||||
HVAC_MODE_FAN_ONLY: Zone.Mode.OPEN,
|
||||
HVAC_MODE_HEAT_COOL: Zone.Mode.AUTO,
|
||||
}
|
||||
self._supported_features |= SUPPORT_TARGET_TEMPERATURE
|
||||
|
||||
self._device_info = {
|
||||
"identifiers": {(IZONE, controller.unique_id, zone.index)},
|
||||
"name": self.name,
|
||||
"manufacturer": "IZone",
|
||||
"via_device": (IZONE, controller.unique_id),
|
||||
"model": zone.type.name.title(),
|
||||
}
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Call on adding to hass."""
|
||||
|
||||
@callback
|
||||
def zone_update(ctrl: Controller, zone: Zone) -> None:
|
||||
"""Handle zone data updates."""
|
||||
if zone is not self._zone:
|
||||
return
|
||||
self._name = zone.name.title()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(self.hass, DISPATCH_ZONE_UPDATE, zone_update)
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return self._controller.available
|
||||
|
||||
@property
|
||||
def assumed_state(self) -> bool:
|
||||
"""Return True if unable to access real state of the entity."""
|
||||
return self._controller.assumed_state
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return the device info for the iZone system."""
|
||||
return self._device_info
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of the controller device."""
|
||||
return "{}_z{}".format(self._controller.unique_id, self._zone.index + 1)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Return True if entity has to be polled for state.
|
||||
|
||||
False if entity pushes its state to HA.
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
try:
|
||||
if self._zone.mode == Zone.Mode.AUTO:
|
||||
return self._supported_features
|
||||
return self._supported_features & ~SUPPORT_TARGET_TEMPERATURE
|
||||
except ConnectionError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement which this thermostat uses."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def precision(self):
|
||||
"""Return the precision of the system."""
|
||||
return PRECISION_HALVES
|
||||
|
||||
@property
|
||||
def hvac_mode(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
mode = self._zone.mode
|
||||
for (key, value) in self._state_to_pizone.items():
|
||||
if value == mode:
|
||||
return key
|
||||
return None
|
||||
|
||||
@property
|
||||
def hvac_modes(self):
|
||||
"""Return the list of available operation modes."""
|
||||
return list(self._state_to_pizone.keys())
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._zone.temp_current
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self._zone.type != Zone.Type.AUTO:
|
||||
return None
|
||||
return self._zone.temp_setpoint
|
||||
|
||||
@property
|
||||
def target_temperature_step(self):
|
||||
"""Return the supported step of target temperature."""
|
||||
return 0.5
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return self._controller.min_temp
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return self._controller.max_temp
|
||||
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
if self._zone.mode != Zone.Mode.AUTO:
|
||||
return
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temp is not None:
|
||||
await self._controller.wrap_and_catch(self._zone.set_temp_setpoint(temp))
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
|
||||
"""Set new target operation mode."""
|
||||
mode = self._state_to_pizone[hvac_mode]
|
||||
await self._controller.wrap_and_catch(self._zone.set_mode(mode))
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if on."""
|
||||
return self._zone.mode != Zone.Mode.CLOSE
|
||||
|
||||
async def async_turn_on(self):
|
||||
"""Turn device on (open zone)."""
|
||||
if self._zone.type == Zone.Type.AUTO:
|
||||
await self._controller.wrap_and_catch(self._zone.set_mode(Zone.Mode.AUTO))
|
||||
else:
|
||||
await self._controller.wrap_and_catch(self._zone.set_mode(Zone.Mode.OPEN))
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_turn_off(self):
|
||||
"""Turn device off (close zone)."""
|
||||
await self._controller.wrap_and_catch(self._zone.set_mode(Zone.Mode.CLOSE))
|
||||
self.async_schedule_update_ha_state()
|
45
homeassistant/components/izone/config_flow.py
Normal file
45
homeassistant/components/izone/config_flow.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
"""Config flow for izone."""
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
|
||||
from async_timeout import timeout
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from .const import IZONE, TIMEOUT_DISCOVERY, DISPATCH_CONTROLLER_DISCOVERED
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def _async_has_devices(hass):
|
||||
from .discovery import async_start_discovery_service, async_stop_discovery_service
|
||||
|
||||
controller_ready = asyncio.Event()
|
||||
async_dispatcher_connect(
|
||||
hass, DISPATCH_CONTROLLER_DISCOVERED, lambda x: controller_ready.set()
|
||||
)
|
||||
|
||||
disco = await async_start_discovery_service(hass)
|
||||
|
||||
try:
|
||||
async with timeout(TIMEOUT_DISCOVERY):
|
||||
await controller_ready.wait()
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
|
||||
if not disco.pi_disco.controllers:
|
||||
await async_stop_discovery_service(hass)
|
||||
_LOGGER.debug("No controllers found")
|
||||
return False
|
||||
|
||||
_LOGGER.debug("Controllers %s", disco.pi_disco.controllers)
|
||||
return True
|
||||
|
||||
|
||||
config_entry_flow.register_discovery_flow(
|
||||
IZONE, "iZone Aircon", _async_has_devices, config_entries.CONN_CLASS_LOCAL_POLL
|
||||
)
|
14
homeassistant/components/izone/const.py
Normal file
14
homeassistant/components/izone/const.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
"""Constants used by the izone component."""
|
||||
|
||||
IZONE = "izone"
|
||||
|
||||
DATA_DISCOVERY_SERVICE = "izone_discovery"
|
||||
DATA_CONFIG = "izone_config"
|
||||
|
||||
DISPATCH_CONTROLLER_DISCOVERED = "izone_controller_discovered"
|
||||
DISPATCH_CONTROLLER_DISCONNECTED = "izone_controller_disconnected"
|
||||
DISPATCH_CONTROLLER_RECONNECTED = "izone_controller_disconnected"
|
||||
DISPATCH_CONTROLLER_UPDATE = "izone_controller_update"
|
||||
DISPATCH_ZONE_UPDATE = "izone_zone_update"
|
||||
|
||||
TIMEOUT_DISCOVERY = 20
|
87
homeassistant/components/izone/discovery.py
Normal file
87
homeassistant/components/izone/discovery.py
Normal file
|
@ -0,0 +1,87 @@
|
|||
"""Internal discovery service for iZone AC."""
|
||||
|
||||
import logging
|
||||
import pizone
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from .const import (
|
||||
DATA_DISCOVERY_SERVICE,
|
||||
DISPATCH_CONTROLLER_DISCOVERED,
|
||||
DISPATCH_CONTROLLER_DISCONNECTED,
|
||||
DISPATCH_CONTROLLER_RECONNECTED,
|
||||
DISPATCH_CONTROLLER_UPDATE,
|
||||
DISPATCH_ZONE_UPDATE,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DiscoveryService(pizone.Listener):
|
||||
"""Discovery data and interfacing with pizone library."""
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Initialise discovery service."""
|
||||
super().__init__()
|
||||
self.hass = hass
|
||||
self.pi_disco = None
|
||||
|
||||
# Listener interface
|
||||
def controller_discovered(self, ctrl: pizone.Controller) -> None:
|
||||
"""Handle new controller discoverery."""
|
||||
async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_DISCOVERED, ctrl)
|
||||
|
||||
def controller_disconnected(self, ctrl: pizone.Controller, ex: Exception) -> None:
|
||||
"""On disconnect from controller."""
|
||||
async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_DISCONNECTED, ctrl, ex)
|
||||
|
||||
def controller_reconnected(self, ctrl: pizone.Controller) -> None:
|
||||
"""On reconnect to controller."""
|
||||
async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_RECONNECTED, ctrl)
|
||||
|
||||
def controller_update(self, ctrl: pizone.Controller) -> None:
|
||||
"""System update message is recieved from the controller."""
|
||||
async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_UPDATE, ctrl)
|
||||
|
||||
def zone_update(self, ctrl: pizone.Controller, zone: pizone.Zone) -> None:
|
||||
"""Zone update message is recieved from the controller."""
|
||||
async_dispatcher_send(self.hass, DISPATCH_ZONE_UPDATE, ctrl, zone)
|
||||
|
||||
|
||||
async def async_start_discovery_service(hass: HomeAssistantType):
|
||||
"""Set up the pizone internal discovery."""
|
||||
disco = hass.data.get(DATA_DISCOVERY_SERVICE)
|
||||
if disco:
|
||||
# Already started
|
||||
return disco
|
||||
|
||||
# discovery local services
|
||||
disco = DiscoveryService(hass)
|
||||
hass.data[DATA_DISCOVERY_SERVICE] = disco
|
||||
|
||||
# Start the pizone discovery service, disco is the listener
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
loop = hass.loop
|
||||
|
||||
disco.pi_disco = pizone.discovery(disco, loop=loop, session=session)
|
||||
await disco.pi_disco.start_discovery()
|
||||
|
||||
async def shutdown_event(event):
|
||||
await async_stop_discovery_service(hass)
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_event)
|
||||
|
||||
return disco
|
||||
|
||||
|
||||
async def async_stop_discovery_service(hass: HomeAssistantType):
|
||||
"""Stop the discovery service."""
|
||||
disco = hass.data.get(DATA_DISCOVERY_SERVICE)
|
||||
if not disco:
|
||||
return
|
||||
|
||||
await disco.pi_disco.close()
|
||||
del hass.data[DATA_DISCOVERY_SERVICE]
|
9
homeassistant/components/izone/manifest.json
Normal file
9
homeassistant/components/izone/manifest.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"domain": "izone",
|
||||
"name": "izone",
|
||||
"documentation": "https://www.home-assistant.io/components/izone",
|
||||
"requirements": [ "python-izone==1.1.1" ],
|
||||
"dependencies": [],
|
||||
"codeowners": [ "@Swamp-Ig" ],
|
||||
"config_flow": true
|
||||
}
|
15
homeassistant/components/izone/strings.json
Normal file
15
homeassistant/components/izone/strings.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"config": {
|
||||
"title": "iZone",
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "iZone",
|
||||
"description": "Do you want to set up iZone?"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "Only a single configuration of iZone is necessary.",
|
||||
"no_devices_found": "No iZone devices found on the network."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -30,6 +30,7 @@ FLOWS = [
|
|||
"ios",
|
||||
"ipma",
|
||||
"iqvia",
|
||||
"izone",
|
||||
"life360",
|
||||
"lifx",
|
||||
"linky",
|
||||
|
|
|
@ -1508,6 +1508,9 @@ python-gitlab==1.6.0
|
|||
# homeassistant.components.hp_ilo
|
||||
python-hpilo==4.3
|
||||
|
||||
# homeassistant.components.izone
|
||||
python-izone==1.1.1
|
||||
|
||||
# homeassistant.components.joaoapps_join
|
||||
python-join-api==0.0.4
|
||||
|
||||
|
|
|
@ -349,6 +349,9 @@ pyspcwebgw==0.4.0
|
|||
# homeassistant.components.darksky
|
||||
python-forecastio==1.4.0
|
||||
|
||||
# homeassistant.components.izone
|
||||
python-izone==1.1.1
|
||||
|
||||
# homeassistant.components.nest
|
||||
python-nest==4.1.0
|
||||
|
||||
|
|
|
@ -145,6 +145,7 @@ TEST_REQUIREMENTS = (
|
|||
"pyspcwebgw",
|
||||
"python_awair",
|
||||
"python-forecastio",
|
||||
"python-izone",
|
||||
"python-nest",
|
||||
"python-velbus",
|
||||
"pythonwhois",
|
||||
|
|
1
tests/components/izone/__init__.py
Normal file
1
tests/components/izone/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""IZone tests."""
|
83
tests/components/izone/test_config_flow.py
Normal file
83
tests/components/izone/test_config_flow.py
Normal file
|
@ -0,0 +1,83 @@
|
|||
"""Tests for iZone."""
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.components.izone.const import IZONE, DISPATCH_CONTROLLER_DISCOVERED
|
||||
|
||||
from tests.common import mock_coro
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_disco():
|
||||
"""Mock discovery service."""
|
||||
disco = Mock()
|
||||
disco.pi_disco = Mock()
|
||||
disco.pi_disco.controllers = {}
|
||||
yield disco
|
||||
|
||||
|
||||
def _mock_start_discovery(hass, mock_disco):
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
def do_disovered(*args):
|
||||
async_dispatcher_send(hass, DISPATCH_CONTROLLER_DISCOVERED, True)
|
||||
return mock_coro(mock_disco)
|
||||
|
||||
return do_disovered
|
||||
|
||||
|
||||
async def test_not_found(hass, mock_disco):
|
||||
"""Test not finding iZone controller."""
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.izone.discovery.async_start_discovery_service"
|
||||
) as start_disco, patch(
|
||||
"homeassistant.components.izone.discovery.async_stop_discovery_service",
|
||||
return_value=mock_coro(),
|
||||
) as stop_disco:
|
||||
start_disco.side_effect = _mock_start_discovery(hass, mock_disco)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
IZONE, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
# Confirmation form
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
stop_disco.assert_called_once()
|
||||
|
||||
|
||||
async def test_found(hass, mock_disco):
|
||||
"""Test not finding iZone controller."""
|
||||
mock_disco.pi_disco.controllers["blah"] = object()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.izone.climate.async_setup_entry",
|
||||
return_value=mock_coro(True),
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.izone.discovery.async_start_discovery_service"
|
||||
) as start_disco, patch(
|
||||
"homeassistant.components.izone.async_start_discovery_service",
|
||||
return_value=mock_coro(),
|
||||
):
|
||||
start_disco.side_effect = _mock_start_discovery(hass, mock_disco)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
IZONE, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
# Confirmation form
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_setup.assert_called_once()
|
Loading…
Add table
Reference in a new issue