* Remove unnecessary exception re-wraps * Preserve exception chains on re-raise We slap "from cause" to almost all possible cases here. In some cases it could conceivably be better to do "from None" if we really want to hide the cause. However those should be in the minority, and "from cause" should be an improvement over the corresponding raise without a "from" in all cases anyway. The only case where we raise from None here is in plex, where the exception for an original invalid SSL cert is not the root cause for failure to validate a newly fetched one. Follow local convention on exception variable names if there is a consistent one, otherwise `err` to match with majority of codebase. * Fix mistaken re-wrap in homematicip_cloud/hap.py Missed the difference between HmipConnectionError and HmipcConnectionError. * Do not hide original error on plex new cert validation error Original is not the cause for the new one, but showing old in the traceback is useful nevertheless.
468 lines
16 KiB
Python
468 lines
16 KiB
Python
"""Support for IntesisHome and airconwithme Smart AC Controllers."""
|
|
import logging
|
|
from random import randrange
|
|
|
|
from pyintesishome import IHAuthenticationError, IHConnectionError, IntesisHome
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity
|
|
from homeassistant.components.climate.const import (
|
|
ATTR_HVAC_MODE,
|
|
HVAC_MODE_COOL,
|
|
HVAC_MODE_DRY,
|
|
HVAC_MODE_FAN_ONLY,
|
|
HVAC_MODE_HEAT,
|
|
HVAC_MODE_HEAT_COOL,
|
|
HVAC_MODE_OFF,
|
|
PRESET_BOOST,
|
|
PRESET_COMFORT,
|
|
PRESET_ECO,
|
|
SUPPORT_FAN_MODE,
|
|
SUPPORT_PRESET_MODE,
|
|
SUPPORT_SWING_MODE,
|
|
SUPPORT_TARGET_TEMPERATURE,
|
|
SWING_BOTH,
|
|
SWING_HORIZONTAL,
|
|
SWING_OFF,
|
|
SWING_VERTICAL,
|
|
)
|
|
from homeassistant.const import (
|
|
ATTR_TEMPERATURE,
|
|
CONF_DEVICE,
|
|
CONF_PASSWORD,
|
|
CONF_USERNAME,
|
|
TEMP_CELSIUS,
|
|
)
|
|
from homeassistant.exceptions import PlatformNotReady
|
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.helpers.event import async_call_later
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
IH_DEVICE_INTESISHOME = "IntesisHome"
|
|
IH_DEVICE_AIRCONWITHME = "airconwithme"
|
|
|
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|
{
|
|
vol.Required(CONF_USERNAME): cv.string,
|
|
vol.Required(CONF_PASSWORD): cv.string,
|
|
vol.Optional(CONF_DEVICE, default=IH_DEVICE_INTESISHOME): vol.In(
|
|
[IH_DEVICE_AIRCONWITHME, IH_DEVICE_INTESISHOME]
|
|
),
|
|
}
|
|
)
|
|
|
|
MAP_IH_TO_HVAC_MODE = {
|
|
"auto": HVAC_MODE_HEAT_COOL,
|
|
"cool": HVAC_MODE_COOL,
|
|
"dry": HVAC_MODE_DRY,
|
|
"fan": HVAC_MODE_FAN_ONLY,
|
|
"heat": HVAC_MODE_HEAT,
|
|
"off": HVAC_MODE_OFF,
|
|
}
|
|
MAP_HVAC_MODE_TO_IH = {v: k for k, v in MAP_IH_TO_HVAC_MODE.items()}
|
|
|
|
MAP_IH_TO_PRESET_MODE = {
|
|
"eco": PRESET_ECO,
|
|
"comfort": PRESET_COMFORT,
|
|
"powerful": PRESET_BOOST,
|
|
}
|
|
MAP_PRESET_MODE_TO_IH = {v: k for k, v in MAP_IH_TO_PRESET_MODE.items()}
|
|
|
|
IH_SWING_STOP = "auto/stop"
|
|
IH_SWING_SWING = "swing"
|
|
MAP_SWING_TO_IH = {
|
|
SWING_OFF: {"vvane": IH_SWING_STOP, "hvane": IH_SWING_STOP},
|
|
SWING_BOTH: {"vvane": IH_SWING_SWING, "hvane": IH_SWING_SWING},
|
|
SWING_HORIZONTAL: {"vvane": IH_SWING_STOP, "hvane": IH_SWING_SWING},
|
|
SWING_VERTICAL: {"vvane": IH_SWING_SWING, "hvane": IH_SWING_STOP},
|
|
}
|
|
|
|
|
|
MAP_STATE_ICONS = {
|
|
HVAC_MODE_COOL: "mdi:snowflake",
|
|
HVAC_MODE_DRY: "mdi:water-off",
|
|
HVAC_MODE_FAN_ONLY: "mdi:fan",
|
|
HVAC_MODE_HEAT: "mdi:white-balance-sunny",
|
|
HVAC_MODE_HEAT_COOL: "mdi:cached",
|
|
}
|
|
|
|
|
|
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
|
"""Create the IntesisHome climate devices."""
|
|
ih_user = config[CONF_USERNAME]
|
|
ih_pass = config[CONF_PASSWORD]
|
|
device_type = config[CONF_DEVICE]
|
|
|
|
controller = IntesisHome(
|
|
ih_user,
|
|
ih_pass,
|
|
hass.loop,
|
|
websession=async_get_clientsession(hass),
|
|
device_type=device_type,
|
|
)
|
|
try:
|
|
await controller.poll_status()
|
|
except IHAuthenticationError:
|
|
_LOGGER.error("Invalid username or password")
|
|
return
|
|
except IHConnectionError as ex:
|
|
_LOGGER.error("Error connecting to the %s server", device_type)
|
|
raise PlatformNotReady from ex
|
|
|
|
ih_devices = controller.get_devices()
|
|
if ih_devices:
|
|
async_add_entities(
|
|
[
|
|
IntesisAC(ih_device_id, device, controller)
|
|
for ih_device_id, device in ih_devices.items()
|
|
],
|
|
True,
|
|
)
|
|
else:
|
|
_LOGGER.error(
|
|
"Error getting device list from %s API: %s",
|
|
device_type,
|
|
controller.error_message,
|
|
)
|
|
await controller.stop()
|
|
|
|
|
|
class IntesisAC(ClimateEntity):
|
|
"""Represents an Intesishome air conditioning device."""
|
|
|
|
def __init__(self, ih_device_id, ih_device, controller):
|
|
"""Initialize the thermostat."""
|
|
self._controller = controller
|
|
self._device_id = ih_device_id
|
|
self._ih_device = ih_device
|
|
self._device_name = ih_device.get("name")
|
|
self._device_type = controller.device_type
|
|
self._connected = None
|
|
self._setpoint_step = 1
|
|
self._current_temp = None
|
|
self._max_temp = None
|
|
self._hvac_mode_list = []
|
|
self._min_temp = None
|
|
self._target_temp = None
|
|
self._outdoor_temp = None
|
|
self._hvac_mode = None
|
|
self._preset = None
|
|
self._preset_list = [PRESET_ECO, PRESET_COMFORT, PRESET_BOOST]
|
|
self._run_hours = None
|
|
self._rssi = None
|
|
self._swing_list = [SWING_OFF]
|
|
self._vvane = None
|
|
self._hvane = None
|
|
self._power = False
|
|
self._fan_speed = None
|
|
self._support = 0
|
|
self._power_consumption_heat = None
|
|
self._power_consumption_cool = None
|
|
|
|
# Setpoint support
|
|
if controller.has_setpoint_control(ih_device_id):
|
|
self._support |= SUPPORT_TARGET_TEMPERATURE
|
|
|
|
# Setup swing list
|
|
if controller.has_vertical_swing(ih_device_id):
|
|
self._swing_list.append(SWING_VERTICAL)
|
|
if controller.has_horizontal_swing(ih_device_id):
|
|
self._swing_list.append(SWING_HORIZONTAL)
|
|
if SWING_HORIZONTAL in self._swing_list and SWING_VERTICAL in self._swing_list:
|
|
self._swing_list.append(SWING_BOTH)
|
|
if len(self._swing_list) > 1:
|
|
self._support |= SUPPORT_SWING_MODE
|
|
|
|
# Setup fan speeds
|
|
self._fan_modes = controller.get_fan_speed_list(ih_device_id)
|
|
if self._fan_modes:
|
|
self._support |= SUPPORT_FAN_MODE
|
|
|
|
# Preset support
|
|
if ih_device.get("climate_working_mode"):
|
|
self._support |= SUPPORT_PRESET_MODE
|
|
|
|
# Setup HVAC modes
|
|
modes = controller.get_mode_list(ih_device_id)
|
|
if modes:
|
|
mode_list = [MAP_IH_TO_HVAC_MODE[mode] for mode in modes]
|
|
self._hvac_mode_list.extend(mode_list)
|
|
self._hvac_mode_list.append(HVAC_MODE_OFF)
|
|
|
|
async def async_added_to_hass(self):
|
|
"""Subscribe to event updates."""
|
|
_LOGGER.debug("Added climate device with state: %s", repr(self._ih_device))
|
|
await self._controller.add_update_callback(self.async_update_callback)
|
|
try:
|
|
await self._controller.connect()
|
|
except IHConnectionError as ex:
|
|
_LOGGER.error("Exception connecting to IntesisHome: %s", ex)
|
|
raise PlatformNotReady from ex
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of the AC device."""
|
|
return self._device_name
|
|
|
|
@property
|
|
def temperature_unit(self):
|
|
"""Intesishome API uses celsius on the backend."""
|
|
return TEMP_CELSIUS
|
|
|
|
@property
|
|
def device_state_attributes(self):
|
|
"""Return the device specific state attributes."""
|
|
attrs = {}
|
|
if self._outdoor_temp:
|
|
attrs["outdoor_temp"] = self._outdoor_temp
|
|
if self._power_consumption_heat:
|
|
attrs["power_consumption_heat_kw"] = round(
|
|
self._power_consumption_heat / 1000, 1
|
|
)
|
|
if self._power_consumption_cool:
|
|
attrs["power_consumption_cool_kw"] = round(
|
|
self._power_consumption_cool / 1000, 1
|
|
)
|
|
|
|
return attrs
|
|
|
|
@property
|
|
def unique_id(self):
|
|
"""Return unique ID for this device."""
|
|
return self._device_id
|
|
|
|
@property
|
|
def target_temperature_step(self) -> float:
|
|
"""Return whether setpoint should be whole or half degree precision."""
|
|
return self._setpoint_step
|
|
|
|
@property
|
|
def preset_modes(self):
|
|
"""Return a list of HVAC preset modes."""
|
|
return self._preset_list
|
|
|
|
@property
|
|
def preset_mode(self):
|
|
"""Return the current preset mode."""
|
|
return self._preset
|
|
|
|
async def async_set_temperature(self, **kwargs):
|
|
"""Set new target temperature."""
|
|
temperature = kwargs.get(ATTR_TEMPERATURE)
|
|
hvac_mode = kwargs.get(ATTR_HVAC_MODE)
|
|
|
|
if hvac_mode:
|
|
await self.async_set_hvac_mode(hvac_mode)
|
|
|
|
if temperature:
|
|
_LOGGER.debug("Setting %s to %s degrees", self._device_type, temperature)
|
|
await self._controller.set_temperature(self._device_id, temperature)
|
|
self._target_temp = temperature
|
|
|
|
# Write updated temperature to HA state to avoid flapping (API confirmation is slow)
|
|
self.async_write_ha_state()
|
|
|
|
async def async_set_hvac_mode(self, hvac_mode):
|
|
"""Set operation mode."""
|
|
_LOGGER.debug("Setting %s to %s mode", self._device_type, hvac_mode)
|
|
if hvac_mode == HVAC_MODE_OFF:
|
|
self._power = False
|
|
await self._controller.set_power_off(self._device_id)
|
|
# Write changes to HA, API can be slow to push changes
|
|
self.async_write_ha_state()
|
|
return
|
|
|
|
# First check device is turned on
|
|
if not self._controller.is_on(self._device_id):
|
|
self._power = True
|
|
await self._controller.set_power_on(self._device_id)
|
|
|
|
# Set the mode
|
|
await self._controller.set_mode(self._device_id, MAP_HVAC_MODE_TO_IH[hvac_mode])
|
|
|
|
# Send the temperature again in case changing modes has changed it
|
|
if self._target_temp:
|
|
await self._controller.set_temperature(self._device_id, self._target_temp)
|
|
|
|
# Updates can take longer than 2 seconds, so update locally
|
|
self._hvac_mode = hvac_mode
|
|
self.async_write_ha_state()
|
|
|
|
async def async_set_fan_mode(self, fan_mode):
|
|
"""Set fan mode (from quiet, low, medium, high, auto)."""
|
|
await self._controller.set_fan_speed(self._device_id, fan_mode)
|
|
|
|
# Updates can take longer than 2 seconds, so update locally
|
|
self._fan_speed = fan_mode
|
|
self.async_write_ha_state()
|
|
|
|
async def async_set_preset_mode(self, preset_mode):
|
|
"""Set preset mode."""
|
|
ih_preset_mode = MAP_PRESET_MODE_TO_IH.get(preset_mode)
|
|
await self._controller.set_preset_mode(self._device_id, ih_preset_mode)
|
|
|
|
async def async_set_swing_mode(self, swing_mode):
|
|
"""Set the vertical vane."""
|
|
swing_settings = MAP_SWING_TO_IH.get(swing_mode)
|
|
if swing_settings:
|
|
await self._controller.set_vertical_vane(
|
|
self._device_id, swing_settings.get("vvane")
|
|
)
|
|
await self._controller.set_horizontal_vane(
|
|
self._device_id, swing_settings.get("hvane")
|
|
)
|
|
|
|
async def async_update(self):
|
|
"""Copy values from controller dictionary to climate device."""
|
|
# Update values from controller's device dictionary
|
|
self._connected = self._controller.is_connected
|
|
self._current_temp = self._controller.get_temperature(self._device_id)
|
|
self._fan_speed = self._controller.get_fan_speed(self._device_id)
|
|
self._power = self._controller.is_on(self._device_id)
|
|
self._min_temp = self._controller.get_min_setpoint(self._device_id)
|
|
self._max_temp = self._controller.get_max_setpoint(self._device_id)
|
|
self._rssi = self._controller.get_rssi(self._device_id)
|
|
self._run_hours = self._controller.get_run_hours(self._device_id)
|
|
self._target_temp = self._controller.get_setpoint(self._device_id)
|
|
self._outdoor_temp = self._controller.get_outdoor_temperature(self._device_id)
|
|
|
|
# Operation mode
|
|
mode = self._controller.get_mode(self._device_id)
|
|
self._hvac_mode = MAP_IH_TO_HVAC_MODE.get(mode)
|
|
|
|
# Preset mode
|
|
preset = self._controller.get_preset_mode(self._device_id)
|
|
self._preset = MAP_IH_TO_PRESET_MODE.get(preset)
|
|
|
|
# Swing mode
|
|
# Climate module only supports one swing setting.
|
|
self._vvane = self._controller.get_vertical_swing(self._device_id)
|
|
self._hvane = self._controller.get_horizontal_swing(self._device_id)
|
|
|
|
# Power usage
|
|
self._power_consumption_heat = self._controller.get_heat_power_consumption(
|
|
self._device_id
|
|
)
|
|
self._power_consumption_cool = self._controller.get_cool_power_consumption(
|
|
self._device_id
|
|
)
|
|
|
|
async def async_will_remove_from_hass(self):
|
|
"""Shutdown the controller when the device is being removed."""
|
|
await self._controller.stop()
|
|
|
|
@property
|
|
def icon(self):
|
|
"""Return the icon for the current state."""
|
|
icon = None
|
|
if self._power:
|
|
icon = MAP_STATE_ICONS.get(self._hvac_mode)
|
|
return icon
|
|
|
|
async def async_update_callback(self, device_id=None):
|
|
"""Let HA know there has been an update from the controller."""
|
|
# Track changes in connection state
|
|
if not self._controller.is_connected and self._connected:
|
|
# Connection has dropped
|
|
self._connected = False
|
|
reconnect_minutes = 1 + randrange(10)
|
|
_LOGGER.error(
|
|
"Connection to %s API was lost. Reconnecting in %i minutes",
|
|
self._device_type,
|
|
reconnect_minutes,
|
|
)
|
|
# Schedule reconnection
|
|
async_call_later(
|
|
self.hass, reconnect_minutes * 60, self._controller.connect()
|
|
)
|
|
|
|
if self._controller.is_connected and not self._connected:
|
|
# Connection has been restored
|
|
self._connected = True
|
|
_LOGGER.debug("Connection to %s API was restored", self._device_type)
|
|
|
|
if not device_id or self._device_id == device_id:
|
|
# Update all devices if no device_id was specified
|
|
_LOGGER.debug(
|
|
"%s API sent a status update for device %s",
|
|
self._device_type,
|
|
device_id,
|
|
)
|
|
self.async_schedule_update_ha_state(True)
|
|
|
|
@property
|
|
def min_temp(self):
|
|
"""Return the minimum temperature for the current mode of operation."""
|
|
return self._min_temp
|
|
|
|
@property
|
|
def max_temp(self):
|
|
"""Return the maximum temperature for the current mode of operation."""
|
|
return self._max_temp
|
|
|
|
@property
|
|
def should_poll(self):
|
|
"""Poll for updates if pyIntesisHome doesn't have a socket open."""
|
|
return False
|
|
|
|
@property
|
|
def hvac_modes(self):
|
|
"""List of available operation modes."""
|
|
return self._hvac_mode_list
|
|
|
|
@property
|
|
def fan_mode(self):
|
|
"""Return whether the fan is on."""
|
|
return self._fan_speed
|
|
|
|
@property
|
|
def swing_mode(self):
|
|
"""Return current swing mode."""
|
|
if self._vvane == IH_SWING_SWING and self._hvane == IH_SWING_SWING:
|
|
swing = SWING_BOTH
|
|
elif self._vvane == IH_SWING_SWING:
|
|
swing = SWING_VERTICAL
|
|
elif self._hvane == IH_SWING_SWING:
|
|
swing = SWING_HORIZONTAL
|
|
else:
|
|
swing = SWING_OFF
|
|
return swing
|
|
|
|
@property
|
|
def fan_modes(self):
|
|
"""List of available fan modes."""
|
|
return self._fan_modes
|
|
|
|
@property
|
|
def swing_modes(self):
|
|
"""List of available swing positions."""
|
|
return self._swing_list
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
"""If the device hasn't been able to connect, mark as unavailable."""
|
|
return self._connected or self._connected is None
|
|
|
|
@property
|
|
def current_temperature(self):
|
|
"""Return the current temperature."""
|
|
return self._current_temp
|
|
|
|
@property
|
|
def hvac_mode(self):
|
|
"""Return the current mode of operation if unit is on."""
|
|
if self._power:
|
|
return self._hvac_mode
|
|
return HVAC_MODE_OFF
|
|
|
|
@property
|
|
def target_temperature(self):
|
|
"""Return the current setpoint temperature if unit is on."""
|
|
return self._target_temp
|
|
|
|
@property
|
|
def supported_features(self):
|
|
"""Return the list of supported features."""
|
|
return self._support
|