1
mirror of https://github.com/home-assistant/core synced 2024-07-30 21:18:57 +02:00
ha-core/homeassistant/components/intesishome/climate.py

479 lines
16 KiB
Python

"""Support for IntesisHome and airconwithme Smart AC Controllers."""
import logging
from random import randrange
from typing import NamedTuple
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]
),
}
)
class SwingSettings(NamedTuple):
"""Settings for swing mode."""
vvane: str
hvane: str
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: SwingSettings(vvane=IH_SWING_STOP, hvane=IH_SWING_STOP),
SWING_BOTH: SwingSettings(vvane=IH_SWING_SWING, hvane=IH_SWING_SWING),
SWING_HORIZONTAL: SwingSettings(vvane=IH_SWING_STOP, hvane=IH_SWING_SWING),
SWING_VERTICAL: SwingSettings(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 extra_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."""
if swing_settings := MAP_SWING_TO_IH.get(swing_mode):
await self._controller.set_vertical_vane(
self._device_id, swing_settings.vvane
)
await self._controller.set_horizontal_vane(
self._device_id, swing_settings.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 def try_connect(_now):
await self._controller.connect()
async_call_later(self.hass, reconnect_minutes * 60, try_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