Add water_heater support to HomeKit (#17614)

* Homekit add support for water_heater
* Added tests
This commit is contained in:
cdce8p 2018-10-19 21:04:05 +02:00 committed by GitHub
parent e343f5521c
commit ff33cbd22f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 281 additions and 52 deletions

View file

@ -173,6 +173,9 @@ def get_accessory(hass, driver, state, aid, config):
elif state.domain in ('automation', 'input_boolean', 'remote', 'script'):
a_type = 'Switch'
elif state.domain == 'water_heater':
a_type = 'WaterHeater'
if a_type is None:
return None

View file

@ -146,3 +146,7 @@ DEVICE_CLASS_WINDOW = 'window'
# #### Thresholds ####
THRESHOLD_CO = 25
THRESHOLD_CO2 = 1000
# #### Default values ####
DEFAULT_MIN_TEMP_WATER_HEATER = 40 # °C
DEFAULT_MAX_TEMP_WATER_HEATER = 60 # °C

View file

@ -6,13 +6,18 @@ from pyhap.const import CATEGORY_THERMOSTAT
from homeassistant.components.climate import (
ATTR_CURRENT_TEMPERATURE, ATTR_MAX_TEMP, ATTR_MIN_TEMP,
ATTR_OPERATION_LIST, ATTR_OPERATION_MODE,
ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP,
DOMAIN, SERVICE_SET_TEMPERATURE, SERVICE_SET_OPERATION_MODE, STATE_AUTO,
STATE_COOL, STATE_HEAT, SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE_HIGH,
SUPPORT_TARGET_TEMPERATURE_LOW)
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN as DOMAIN_CLIMATE,
SERVICE_SET_OPERATION_MODE as SERVICE_SET_OPERATION_MODE_THERMOSTAT,
SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_THERMOSTAT,
STATE_AUTO, STATE_COOL, STATE_HEAT, SUPPORT_ON_OFF,
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
from homeassistant.components.water_heater import (
DOMAIN as DOMAIN_WATER_HEATER,
SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_WATER_HEATER)
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_OFF, SERVICE_TURN_ON,
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE,
SERVICE_TURN_OFF, SERVICE_TURN_ON,
STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
from . import TYPES
@ -21,7 +26,9 @@ from .const import (
CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_CURRENT_HEATING_COOLING,
CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_HEATING_COOLING,
CHAR_HEATING_THRESHOLD_TEMPERATURE, CHAR_TARGET_TEMPERATURE,
CHAR_TEMP_DISPLAY_UNITS, PROP_MAX_VALUE, PROP_MIN_VALUE, SERV_THERMOSTAT)
CHAR_TEMP_DISPLAY_UNITS,
DEFAULT_MAX_TEMP_WATER_HEATER, DEFAULT_MIN_TEMP_WATER_HEATER,
PROP_MAX_VALUE, PROP_MIN_VALUE, SERV_THERMOSTAT)
from .util import temperature_to_homekit, temperature_to_states
_LOGGER = logging.getLogger(__name__)
@ -114,7 +121,7 @@ class Thermostat(HomeAccessory):
return min_temp, max_temp
def set_heat_cool(self, value):
"""Move operation mode to value if call came from HomeKit."""
"""Change operation mode to value if call came from HomeKit."""
if value in HC_HOMEKIT_TO_HASS:
_LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value)
self.heat_cool_flag_target_state = True
@ -122,13 +129,14 @@ class Thermostat(HomeAccessory):
if self.support_power_state is True:
params = {ATTR_ENTITY_ID: self.entity_id}
if hass_value == STATE_OFF:
self.call_service(DOMAIN, SERVICE_TURN_OFF, params)
self.call_service(DOMAIN_CLIMATE, SERVICE_TURN_OFF, params)
return
self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params)
self.call_service(DOMAIN_CLIMATE, SERVICE_TURN_ON, params)
params = {ATTR_ENTITY_ID: self.entity_id,
ATTR_OPERATION_MODE: hass_value}
self.call_service(
DOMAIN, SERVICE_SET_OPERATION_MODE, params, hass_value)
DOMAIN_CLIMATE, SERVICE_SET_OPERATION_MODE_THERMOSTAT,
params, hass_value)
@debounce
def set_cooling_threshold(self, value):
@ -142,9 +150,9 @@ class Thermostat(HomeAccessory):
ATTR_ENTITY_ID: self.entity_id,
ATTR_TARGET_TEMP_HIGH: temperature,
ATTR_TARGET_TEMP_LOW: temperature_to_states(low, self._unit)}
self.call_service(DOMAIN, SERVICE_SET_TEMPERATURE, params,
"cooling threshold {}{}".format(temperature,
self._unit))
self.call_service(
DOMAIN_CLIMATE, SERVICE_SET_TEMPERATURE_THERMOSTAT,
params, 'cooling threshold {}{}'.format(temperature, self._unit))
@debounce
def set_heating_threshold(self, value):
@ -158,9 +166,9 @@ class Thermostat(HomeAccessory):
ATTR_ENTITY_ID: self.entity_id,
ATTR_TARGET_TEMP_HIGH: temperature_to_states(high, self._unit),
ATTR_TARGET_TEMP_LOW: temperature}
self.call_service(DOMAIN, SERVICE_SET_TEMPERATURE, params,
"heating threshold {}{}".format(temperature,
self._unit))
self.call_service(
DOMAIN_CLIMATE, SERVICE_SET_TEMPERATURE_THERMOSTAT,
params, 'heating threshold {}{}'.format(temperature, self._unit))
@debounce
def set_target_temperature(self, value):
@ -172,12 +180,12 @@ class Thermostat(HomeAccessory):
params = {
ATTR_ENTITY_ID: self.entity_id,
ATTR_TEMPERATURE: temperature}
self.call_service(DOMAIN, SERVICE_SET_TEMPERATURE, params,
"target {}{}".format(temperature,
self._unit))
self.call_service(
DOMAIN_CLIMATE, SERVICE_SET_TEMPERATURE_THERMOSTAT,
params, 'target {}{}'.format(temperature, self._unit))
def update_state(self, new_state):
"""Update security state after state changed."""
"""Update thermostat state after state changed."""
# Update current temperature
current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE)
if isinstance(current_temp, (int, float)):
@ -268,3 +276,92 @@ class Thermostat(HomeAccessory):
self.char_current_heat_cool.set_value(
HC_HASS_TO_HOMEKIT[current_operation_mode])
@TYPES.register('WaterHeater')
class WaterHeater(HomeAccessory):
"""Generate a WaterHeater accessory for a water_heater."""
def __init__(self, *args):
"""Initialize a WaterHeater accessory object."""
super().__init__(*args, category=CATEGORY_THERMOSTAT)
self._unit = self.hass.config.units.temperature_unit
self.flag_heat_cool = False
self.flag_temperature = False
min_temp, max_temp = self.get_temperature_range()
serv_thermostat = self.add_preload_service(SERV_THERMOSTAT)
self.char_current_heat_cool = serv_thermostat.configure_char(
CHAR_CURRENT_HEATING_COOLING, value=1)
self.char_target_heat_cool = serv_thermostat.configure_char(
CHAR_TARGET_HEATING_COOLING, value=1,
setter_callback=self.set_heat_cool)
self.char_current_temp = serv_thermostat.configure_char(
CHAR_CURRENT_TEMPERATURE, value=50.0)
self.char_target_temp = serv_thermostat.configure_char(
CHAR_TARGET_TEMPERATURE, value=50.0,
properties={PROP_MIN_VALUE: min_temp,
PROP_MAX_VALUE: max_temp},
setter_callback=self.set_target_temperature)
self.char_display_units = serv_thermostat.configure_char(
CHAR_TEMP_DISPLAY_UNITS, value=0)
def get_temperature_range(self):
"""Return min and max temperature range."""
max_temp = self.hass.states.get(self.entity_id) \
.attributes.get(ATTR_MAX_TEMP)
max_temp = temperature_to_homekit(max_temp, self._unit) if max_temp \
else DEFAULT_MAX_TEMP_WATER_HEATER
min_temp = self.hass.states.get(self.entity_id) \
.attributes.get(ATTR_MIN_TEMP)
min_temp = temperature_to_homekit(min_temp, self._unit) if min_temp \
else DEFAULT_MIN_TEMP_WATER_HEATER
return min_temp, max_temp
def set_heat_cool(self, value):
"""Change operation mode to value if call came from HomeKit."""
_LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value)
self.flag_heat_cool = True
hass_value = HC_HOMEKIT_TO_HASS[value]
if hass_value != STATE_HEAT:
self.char_target_heat_cool.set_value(1) # Heat
@debounce
def set_target_temperature(self, value):
"""Set target temperature to value if call came from HomeKit."""
_LOGGER.debug('%s: Set target temperature to %.2f°C',
self.entity_id, value)
self.flag_temperature = True
temperature = temperature_to_states(value, self._unit)
params = {
ATTR_ENTITY_ID: self.entity_id,
ATTR_TEMPERATURE: temperature}
self.call_service(
DOMAIN_WATER_HEATER, SERVICE_SET_TEMPERATURE_WATER_HEATER,
params, 'target {}{}'.format(temperature, self._unit))
def update_state(self, new_state):
"""Update water_heater state after state change."""
# Update current and target temperature
temperature = new_state.attributes.get(ATTR_TEMPERATURE)
if isinstance(temperature, (int, float)):
temperature = temperature_to_homekit(temperature, self._unit)
self.char_current_temp.set_value(temperature)
if not self.flag_temperature:
self.char_target_temp.set_value(temperature)
self.flag_temperature = False
# Update display units
if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT:
self.char_display_units.set_value(UNIT_HASS_TO_HOMEKIT[self._unit])
# Update target operation mode
operation_mode = new_state.attributes.get(ATTR_OPERATION_MODE)
if operation_mode and not self.flag_heat_cool:
self.char_target_heat_cool.set_value(1) # Heat
self.flag_heat_cool = False