Added new climate component from Daikin (#10983)
* Added Daikin climate component * Fixed tox & hound * Place up the REQUIREMENTS var * Update .coveragerc * Removed unused customization * Prevent setting invalid operation state * Fixed hound * Small refactor according to code review * Fixed latest code review comments * Used host instead of ip_address * No real change * No real change * Fixed lint errors * More pylint fixes * Shush Hound * Applied suggested changes for temperature & humidity settings * Fixed hound * Fixed upper case texts * Fixed hound * Fixed hound * Fixed hound * Removed humidity since even the device has the feature it cant be set from API * Code review requested changes * Fixed hound * Fixed hound * Trigger update after adding device * Added Daikin sensors * Fixed hound * Fixed hound * Fixed travis * Fixed hound * Fixed hound * Fixed travis * Fixed coverage decrease issue * Do less API calls and fixed Travis failures * Distributed code from platform to climate and sensor componenets * Rename sensor state to device_attribute * Fixed hound * Updated requirements * Simplified code * Implemented requested changes * Forgot one change * Don't allow customizing temperature unit and take it from hass (FOR NOW) * Additional code review changes applied * Condensed import even more * Simplify condition check * Reordered imports * Disabled autodiscovery FOR NOW :( * Give more suggestive names to sensors
This commit is contained in:
parent
2cf5acdfd2
commit
04de22613c
6 changed files with 527 additions and 0 deletions
|
@ -266,6 +266,9 @@ omit =
|
|||
homeassistant/components/zoneminder.py
|
||||
homeassistant/components/*/zoneminder.py
|
||||
|
||||
homeassistant/components/daikin.py
|
||||
homeassistant/components/*/daikin.py
|
||||
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/canary.py
|
||||
homeassistant/components/alarm_control_panel/concord232.py
|
||||
|
|
257
homeassistant/components/climate/daikin.py
Normal file
257
homeassistant/components/climate/daikin.py
Normal file
|
@ -0,0 +1,257 @@
|
|||
"""
|
||||
Support for the Daikin HVAC.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.daikin/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_OPERATION_MODE, ATTR_FAN_MODE, ATTR_SWING_MODE,
|
||||
ATTR_CURRENT_TEMPERATURE, ClimateDevice, PLATFORM_SCHEMA,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_SWING_MODE, STATE_OFF, STATE_AUTO, STATE_HEAT, STATE_COOL,
|
||||
STATE_DRY, STATE_FAN_ONLY
|
||||
)
|
||||
from homeassistant.components.daikin import (
|
||||
daikin_api_setup,
|
||||
ATTR_TARGET_TEMPERATURE,
|
||||
ATTR_INSIDE_TEMPERATURE,
|
||||
ATTR_OUTSIDE_TEMPERATURE
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME,
|
||||
TEMP_CELSIUS,
|
||||
ATTR_TEMPERATURE
|
||||
)
|
||||
|
||||
REQUIREMENTS = ['pydaikin==0.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE |
|
||||
SUPPORT_FAN_MODE |
|
||||
SUPPORT_OPERATION_MODE |
|
||||
SUPPORT_SWING_MODE)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_NAME, default=None): cv.string,
|
||||
})
|
||||
|
||||
HA_STATE_TO_DAIKIN = {
|
||||
STATE_FAN_ONLY: 'fan',
|
||||
STATE_DRY: 'dry',
|
||||
STATE_COOL: 'cool',
|
||||
STATE_HEAT: 'hot',
|
||||
STATE_AUTO: 'auto',
|
||||
STATE_OFF: 'off',
|
||||
}
|
||||
|
||||
HA_ATTR_TO_DAIKIN = {
|
||||
ATTR_OPERATION_MODE: 'mode',
|
||||
ATTR_FAN_MODE: 'f_rate',
|
||||
ATTR_SWING_MODE: 'f_dir',
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Daikin HVAC platform."""
|
||||
if discovery_info is not None:
|
||||
host = discovery_info.get('ip')
|
||||
name = None
|
||||
_LOGGER.info("Discovered a Daikin AC on %s", host)
|
||||
else:
|
||||
host = config.get(CONF_HOST)
|
||||
name = config.get(CONF_NAME)
|
||||
_LOGGER.info("Added Daikin AC on %s", host)
|
||||
|
||||
api = daikin_api_setup(hass, host, name)
|
||||
add_devices([DaikinClimate(api)], True)
|
||||
|
||||
|
||||
class DaikinClimate(ClimateDevice):
|
||||
"""Representation of a Daikin HVAC."""
|
||||
|
||||
def __init__(self, api):
|
||||
"""Initialize the climate device."""
|
||||
from pydaikin import appliance
|
||||
|
||||
self._api = api
|
||||
self._force_refresh = False
|
||||
self._list = {
|
||||
ATTR_OPERATION_MODE: list(
|
||||
map(str.title, set(HA_STATE_TO_DAIKIN.values()))
|
||||
),
|
||||
ATTR_FAN_MODE: list(
|
||||
map(
|
||||
str.title,
|
||||
appliance.daikin_values(HA_ATTR_TO_DAIKIN[ATTR_FAN_MODE])
|
||||
)
|
||||
),
|
||||
ATTR_SWING_MODE: list(
|
||||
map(
|
||||
str.title,
|
||||
appliance.daikin_values(HA_ATTR_TO_DAIKIN[ATTR_SWING_MODE])
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
def get(self, key):
|
||||
"""Retrieve device settings from API library cache."""
|
||||
value = None
|
||||
cast_to_float = False
|
||||
|
||||
if key in [ATTR_TEMPERATURE, ATTR_INSIDE_TEMPERATURE,
|
||||
ATTR_CURRENT_TEMPERATURE]:
|
||||
value = self._api.device.values.get('htemp')
|
||||
cast_to_float = True
|
||||
if key == ATTR_TARGET_TEMPERATURE:
|
||||
value = self._api.device.values.get('stemp')
|
||||
cast_to_float = True
|
||||
elif key == ATTR_OUTSIDE_TEMPERATURE:
|
||||
value = self._api.device.values.get('otemp')
|
||||
cast_to_float = True
|
||||
elif key == ATTR_FAN_MODE:
|
||||
value = self._api.device.represent('f_rate')[1].title()
|
||||
elif key == ATTR_SWING_MODE:
|
||||
value = self._api.device.represent('f_dir')[1].title()
|
||||
elif key == ATTR_OPERATION_MODE:
|
||||
# Daikin can return also internal states auto-1 or auto-7
|
||||
# and we need to translate them as AUTO
|
||||
value = re.sub(
|
||||
'[^a-z]',
|
||||
'',
|
||||
self._api.device.represent('mode')[1]
|
||||
).title()
|
||||
|
||||
if value is None:
|
||||
_LOGGER.warning("Invalid value requested for key %s", key)
|
||||
else:
|
||||
if value == "-" or value == "--":
|
||||
value = None
|
||||
elif cast_to_float:
|
||||
try:
|
||||
value = float(value)
|
||||
except ValueError:
|
||||
value = None
|
||||
|
||||
return value
|
||||
|
||||
def set(self, settings):
|
||||
"""Set device settings using API."""
|
||||
values = {}
|
||||
|
||||
for attr in [ATTR_TEMPERATURE, ATTR_FAN_MODE, ATTR_SWING_MODE,
|
||||
ATTR_OPERATION_MODE]:
|
||||
value = settings.get(attr)
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
daikin_attr = HA_ATTR_TO_DAIKIN.get(attr)
|
||||
if daikin_attr is not None:
|
||||
if value.title() in self._list[attr]:
|
||||
values[daikin_attr] = value.lower()
|
||||
else:
|
||||
_LOGGER.error("Invalid value %s for %s", attr, value)
|
||||
|
||||
# temperature
|
||||
elif attr == ATTR_TEMPERATURE:
|
||||
try:
|
||||
values['stemp'] = str(int(value))
|
||||
except ValueError:
|
||||
_LOGGER.error("Invalid temperature %s", value)
|
||||
|
||||
if values:
|
||||
self._force_refresh = True
|
||||
self._api.device.set(values)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this AC."""
|
||||
return "{}.{}".format(self.__class__, self._api.ip_address)
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the thermostat, if any."""
|
||||
return self._api.name
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement which this thermostat uses."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self.get(ATTR_CURRENT_TEMPERATURE)
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self.get(ATTR_TARGET_TEMPERATURE)
|
||||
|
||||
@property
|
||||
def target_temperature_step(self):
|
||||
"""Return the supported step of target temperature."""
|
||||
return 1
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
self.set(kwargs)
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return self.get(ATTR_OPERATION_MODE)
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""Return the list of available operation modes."""
|
||||
return self._list.get(ATTR_OPERATION_MODE)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set HVAC mode."""
|
||||
self.set({ATTR_OPERATION_MODE: operation_mode})
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return self.get(ATTR_FAN_MODE)
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set fan mode."""
|
||||
self.set({ATTR_FAN_MODE: fan})
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""List of available fan modes."""
|
||||
return self._list.get(ATTR_FAN_MODE)
|
||||
|
||||
@property
|
||||
def current_swing_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return self.get(ATTR_SWING_MODE)
|
||||
|
||||
def set_swing_mode(self, swing_mode):
|
||||
"""Set new target temperature."""
|
||||
self.set({ATTR_SWING_MODE: swing_mode})
|
||||
|
||||
@property
|
||||
def swing_list(self):
|
||||
"""List of available swing modes."""
|
||||
return self._list.get(ATTR_SWING_MODE)
|
||||
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
self._api.update(no_throttle=self._force_refresh)
|
||||
self._force_refresh = False
|
138
homeassistant/components/daikin.py
Normal file
138
homeassistant/components/daikin.py
Normal file
|
@ -0,0 +1,138 @@
|
|||
"""
|
||||
Platform for the Daikin AC.
|
||||
|
||||
For more details about this component, please refer to the documentation
|
||||
https://home-assistant.io/components/daikin/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from socket import timeout
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.discovery import SERVICE_DAIKIN
|
||||
from homeassistant.const import (
|
||||
CONF_HOSTS, CONF_ICON, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_TYPE
|
||||
)
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.discovery import load_platform
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['pydaikin==0.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'daikin'
|
||||
HTTP_RESOURCES = ['aircon/get_sensor_info', 'aircon/get_control_info']
|
||||
|
||||
ATTR_TARGET_TEMPERATURE = 'target_temperature'
|
||||
ATTR_INSIDE_TEMPERATURE = 'inside_temperature'
|
||||
ATTR_OUTSIDE_TEMPERATURE = 'outside_temperature'
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
|
||||
COMPONENT_TYPES = ['climate', 'sensor']
|
||||
|
||||
SENSOR_TYPE_TEMPERATURE = 'temperature'
|
||||
|
||||
SENSOR_TYPES = {
|
||||
ATTR_INSIDE_TEMPERATURE: {
|
||||
CONF_NAME: 'Inside Temperature',
|
||||
CONF_ICON: 'mdi:thermometer',
|
||||
CONF_TYPE: SENSOR_TYPE_TEMPERATURE
|
||||
},
|
||||
ATTR_OUTSIDE_TEMPERATURE: {
|
||||
CONF_NAME: 'Outside Temperature',
|
||||
CONF_ICON: 'mdi:thermometer',
|
||||
CONF_TYPE: SENSOR_TYPE_TEMPERATURE
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(
|
||||
CONF_HOSTS, default=[]
|
||||
): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(
|
||||
CONF_MONITORED_CONDITIONS,
|
||||
default=list(SENSOR_TYPES.keys())
|
||||
): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)])
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Establish connection with Daikin."""
|
||||
def discovery_dispatch(service, discovery_info):
|
||||
"""Dispatcher for Daikin discovery events."""
|
||||
host = discovery_info.get('ip')
|
||||
|
||||
if daikin_api_setup(hass, host) is None:
|
||||
return
|
||||
|
||||
for component in COMPONENT_TYPES:
|
||||
load_platform(hass, component, DOMAIN, discovery_info,
|
||||
config)
|
||||
|
||||
discovery.listen(hass, SERVICE_DAIKIN, discovery_dispatch)
|
||||
|
||||
for host in config.get(DOMAIN, {}).get(CONF_HOSTS, []):
|
||||
if daikin_api_setup(hass, host) is None:
|
||||
continue
|
||||
|
||||
discovery_info = {
|
||||
'ip': host,
|
||||
CONF_MONITORED_CONDITIONS:
|
||||
config[DOMAIN][CONF_MONITORED_CONDITIONS]
|
||||
}
|
||||
load_platform(hass, 'sensor', DOMAIN, discovery_info, config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def daikin_api_setup(hass, host, name=None):
|
||||
"""Create a Daikin instance only once."""
|
||||
if DOMAIN not in hass.data:
|
||||
hass.data[DOMAIN] = {}
|
||||
|
||||
api = hass.data[DOMAIN].get(host)
|
||||
if api is None:
|
||||
from pydaikin import appliance
|
||||
|
||||
try:
|
||||
device = appliance.Appliance(host)
|
||||
except timeout:
|
||||
_LOGGER.error("Connection to Daikin could not be established")
|
||||
return False
|
||||
|
||||
if name is None:
|
||||
name = device.values['name']
|
||||
|
||||
api = DaikinApi(device, name)
|
||||
|
||||
return api
|
||||
|
||||
|
||||
class DaikinApi(object):
|
||||
"""Keep the Daikin instance in one place and centralize the update."""
|
||||
|
||||
def __init__(self, device, name):
|
||||
"""Initialize the Daikin Handle."""
|
||||
self.device = device
|
||||
self.name = name
|
||||
self.ip_address = device.ip
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self, **kwargs):
|
||||
"""Pull the latest data from Daikin."""
|
||||
try:
|
||||
for resource in HTTP_RESOURCES:
|
||||
self.device.values.update(
|
||||
self.device.get_resource(resource)
|
||||
)
|
||||
except timeout:
|
||||
_LOGGER.warning(
|
||||
"Connection failed for %s", self.ip_address
|
||||
)
|
|
@ -38,6 +38,7 @@ SERVICE_XIAOMI_GW = 'xiaomi_gw'
|
|||
SERVICE_TELLDUSLIVE = 'tellstick'
|
||||
SERVICE_HUE = 'philips_hue'
|
||||
SERVICE_DECONZ = 'deconz'
|
||||
SERVICE_DAIKIN = 'daikin'
|
||||
|
||||
SERVICE_HANDLERS = {
|
||||
SERVICE_HASS_IOS_APP: ('ios', None),
|
||||
|
|
124
homeassistant/components/sensor/daikin.py
Normal file
124
homeassistant/components/sensor/daikin.py
Normal file
|
@ -0,0 +1,124 @@
|
|||
"""
|
||||
Support for Daikin AC Sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.daikin/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.daikin import (
|
||||
SENSOR_TYPES, SENSOR_TYPE_TEMPERATURE,
|
||||
ATTR_INSIDE_TEMPERATURE, ATTR_OUTSIDE_TEMPERATURE,
|
||||
daikin_api_setup
|
||||
)
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_ICON, CONF_NAME, CONF_MONITORED_CONDITIONS, CONF_TYPE
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util.unit_system import UnitSystem
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_NAME, default=None): cv.string,
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES.keys()):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Daikin sensors."""
|
||||
if discovery_info is not None:
|
||||
host = discovery_info.get('ip')
|
||||
name = None
|
||||
monitored_conditions = discovery_info.get(
|
||||
CONF_MONITORED_CONDITIONS, list(SENSOR_TYPES.keys())
|
||||
)
|
||||
else:
|
||||
host = config[CONF_HOST]
|
||||
name = config.get(CONF_NAME)
|
||||
monitored_conditions = config[CONF_MONITORED_CONDITIONS]
|
||||
_LOGGER.info("Added Daikin AC sensor on %s", host)
|
||||
|
||||
api = daikin_api_setup(hass, host, name)
|
||||
units = hass.config.units
|
||||
sensors = []
|
||||
for monitored_state in monitored_conditions:
|
||||
sensors.append(DaikinClimateSensor(api, monitored_state, units, name))
|
||||
|
||||
add_devices(sensors, True)
|
||||
|
||||
|
||||
class DaikinClimateSensor(Entity):
|
||||
"""Representation of a Sensor."""
|
||||
|
||||
def __init__(self, api, monitored_state, units: UnitSystem, name=None):
|
||||
"""Initialize the sensor."""
|
||||
self._api = api
|
||||
self._sensor = SENSOR_TYPES.get(monitored_state)
|
||||
if name is None:
|
||||
name = "{} {}".format(self._sensor[CONF_NAME], api.name)
|
||||
|
||||
self._name = "{} {}".format(name, monitored_state.replace("_", " "))
|
||||
self._device_attribute = monitored_state
|
||||
|
||||
if self._sensor[CONF_TYPE] == SENSOR_TYPE_TEMPERATURE:
|
||||
self._unit_of_measurement = units.temperature_unit
|
||||
|
||||
def get(self, key):
|
||||
"""Retrieve device settings from API library cache."""
|
||||
value = None
|
||||
cast_to_float = False
|
||||
|
||||
if key == ATTR_INSIDE_TEMPERATURE:
|
||||
value = self._api.device.values.get('htemp')
|
||||
cast_to_float = True
|
||||
elif key == ATTR_OUTSIDE_TEMPERATURE:
|
||||
value = self._api.device.values.get('otemp')
|
||||
|
||||
if value is None:
|
||||
_LOGGER.warning("Invalid value requested for key %s", key)
|
||||
else:
|
||||
if value == "-" or value == "--":
|
||||
value = None
|
||||
elif cast_to_float:
|
||||
try:
|
||||
value = float(value)
|
||||
except ValueError:
|
||||
value = None
|
||||
|
||||
return value
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this AC."""
|
||||
return "{}.{}".format(self.__class__, self._api.ip_address)
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to use in the frontend, if any."""
|
||||
return self._sensor[CONF_ICON]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self.get(self._device_attribute)
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
self._api.update()
|
|
@ -664,6 +664,10 @@ pycsspeechtts==1.0.2
|
|||
# homeassistant.components.sensor.cups
|
||||
# pycups==1.9.73
|
||||
|
||||
# homeassistant.components.daikin
|
||||
# homeassistant.components.climate.daikin
|
||||
pydaikin==0.4
|
||||
|
||||
# homeassistant.components.deconz
|
||||
pydeconz==23
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue