Add support for alarm system, switch and thermostat to homekit (#12819)
* Added support for security system, switch and thermostat * Processing review * Only perform set call when the call didn't come from HomeKit * Added support for alarm_code * Take into account review remarks * Provide tests for HomeKit security systems, switches and thermostats * Support STATE_AUTO * Guard if state exists * Improve support for thermostat auto mode * Provide both high and low at the same time for home assistant * Set default values within accepted ranges * Added tests for auto mode * Fix thermostat test error * Use attributes.get instead of indexing for safety * Avoid hardcoded attributes in testspull/12965/head
parent
35bae1eef2
commit
4218b31e7b
|
@ -13,6 +13,8 @@ from homeassistant.const import (
|
|||
ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, CONF_PORT,
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT,
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.components.climate import (
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||
from homeassistant.util import get_local_ip
|
||||
from homeassistant.util.decorator import Registry
|
||||
|
||||
|
@ -67,7 +69,8 @@ def import_types():
|
|||
"""Import all types from files in the HomeKit directory."""
|
||||
_LOGGER.debug("Import type files.")
|
||||
# pylint: disable=unused-variable
|
||||
from . import covers, sensors # noqa F401
|
||||
from . import ( # noqa F401
|
||||
covers, security_systems, sensors, switches, thermostats)
|
||||
|
||||
|
||||
def get_accessory(hass, state):
|
||||
|
@ -87,6 +90,27 @@ def get_accessory(hass, state):
|
|||
state.entity_id, 'Window')
|
||||
return TYPES['Window'](hass, state.entity_id, state.name)
|
||||
|
||||
elif state.domain == 'alarm_control_panel':
|
||||
_LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id,
|
||||
'SecuritySystem')
|
||||
return TYPES['SecuritySystem'](hass, state.entity_id, state.name)
|
||||
|
||||
elif state.domain == 'climate':
|
||||
support_auto = False
|
||||
features = state.attributes.get(ATTR_SUPPORTED_FEATURES)
|
||||
# Check if climate device supports auto mode
|
||||
if (features & SUPPORT_TARGET_TEMPERATURE_HIGH) \
|
||||
and (features & SUPPORT_TARGET_TEMPERATURE_LOW):
|
||||
support_auto = True
|
||||
_LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, 'Thermostat')
|
||||
return TYPES['Thermostat'](hass, state.entity_id,
|
||||
state.name, support_auto)
|
||||
|
||||
elif state.domain == 'switch' or state.domain == 'remote' \
|
||||
or state.domain == 'input_boolean':
|
||||
_LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, 'Switch')
|
||||
return TYPES['Switch'](hass, state.entity_id, state.name)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
|
|
@ -4,21 +4,33 @@ MANUFACTURER = 'HomeAssistant'
|
|||
# Services
|
||||
SERV_ACCESSORY_INFO = 'AccessoryInformation'
|
||||
SERV_BRIDGING_STATE = 'BridgingState'
|
||||
SERV_SECURITY_SYSTEM = 'SecuritySystem'
|
||||
SERV_SWITCH = 'Switch'
|
||||
SERV_TEMPERATURE_SENSOR = 'TemperatureSensor'
|
||||
SERV_THERMOSTAT = 'Thermostat'
|
||||
SERV_WINDOW_COVERING = 'WindowCovering'
|
||||
|
||||
# Characteristics
|
||||
CHAR_ACC_IDENTIFIER = 'AccessoryIdentifier'
|
||||
CHAR_CATEGORY = 'Category'
|
||||
CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature'
|
||||
CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState'
|
||||
CHAR_CURRENT_POSITION = 'CurrentPosition'
|
||||
CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState'
|
||||
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
|
||||
CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature'
|
||||
CHAR_LINK_QUALITY = 'LinkQuality'
|
||||
CHAR_MANUFACTURER = 'Manufacturer'
|
||||
CHAR_MODEL = 'Model'
|
||||
CHAR_ON = 'On'
|
||||
CHAR_POSITION_STATE = 'PositionState'
|
||||
CHAR_REACHABLE = 'Reachable'
|
||||
CHAR_SERIAL_NUMBER = 'SerialNumber'
|
||||
CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState'
|
||||
CHAR_TARGET_POSITION = 'TargetPosition'
|
||||
CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState'
|
||||
CHAR_TARGET_TEMPERATURE = 'TargetTemperature'
|
||||
CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits'
|
||||
|
||||
# Properties
|
||||
PROP_CELSIUS = {'minValue': -273, 'maxValue': 999}
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
"""Class to hold all alarm control panel accessories."""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
|
||||
ATTR_ENTITY_ID, ATTR_CODE)
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory, add_preload_service
|
||||
from .const import (
|
||||
SERV_SECURITY_SYSTEM, CHAR_CURRENT_SECURITY_STATE,
|
||||
CHAR_TARGET_SECURITY_STATE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
HASS_TO_HOMEKIT = {STATE_ALARM_DISARMED: 3, STATE_ALARM_ARMED_HOME: 0,
|
||||
STATE_ALARM_ARMED_AWAY: 1, STATE_ALARM_ARMED_NIGHT: 2}
|
||||
HOMEKIT_TO_HASS = {c: s for s, c in HASS_TO_HOMEKIT.items()}
|
||||
STATE_TO_SERVICE = {STATE_ALARM_DISARMED: 'alarm_disarm',
|
||||
STATE_ALARM_ARMED_HOME: 'alarm_arm_home',
|
||||
STATE_ALARM_ARMED_AWAY: 'alarm_arm_away',
|
||||
STATE_ALARM_ARMED_NIGHT: 'alarm_arm_night'}
|
||||
|
||||
|
||||
@TYPES.register('SecuritySystem')
|
||||
class SecuritySystem(HomeAccessory):
|
||||
"""Generate an SecuritySystem accessory for an alarm control panel."""
|
||||
|
||||
def __init__(self, hass, entity_id, display_name, alarm_code=None):
|
||||
"""Initialize a SecuritySystem accessory object."""
|
||||
super().__init__(display_name, entity_id, 'ALARM_SYSTEM')
|
||||
|
||||
self._hass = hass
|
||||
self._entity_id = entity_id
|
||||
self._alarm_code = alarm_code
|
||||
|
||||
self.flag_target_state = False
|
||||
|
||||
self.service_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM)
|
||||
self.char_current_state = self.service_alarm. \
|
||||
get_characteristic(CHAR_CURRENT_SECURITY_STATE)
|
||||
self.char_current_state.value = 3
|
||||
self.char_target_state = self.service_alarm. \
|
||||
get_characteristic(CHAR_TARGET_SECURITY_STATE)
|
||||
self.char_target_state.value = 3
|
||||
|
||||
self.char_target_state.setter_callback = self.set_security_state
|
||||
|
||||
def run(self):
|
||||
"""Method called be object after driver is started."""
|
||||
state = self._hass.states.get(self._entity_id)
|
||||
self.update_security_state(new_state=state)
|
||||
|
||||
async_track_state_change(self._hass, self._entity_id,
|
||||
self.update_security_state)
|
||||
|
||||
def set_security_state(self, value):
|
||||
"""Move security state to value if call came from HomeKit."""
|
||||
_LOGGER.debug("%s: Set security state to %d",
|
||||
self._entity_id, value)
|
||||
self.flag_target_state = True
|
||||
hass_value = HOMEKIT_TO_HASS[value]
|
||||
service = STATE_TO_SERVICE[hass_value]
|
||||
|
||||
params = {ATTR_ENTITY_ID: self._entity_id}
|
||||
if self._alarm_code is not None:
|
||||
params[ATTR_CODE] = self._alarm_code
|
||||
self._hass.services.call('alarm_control_panel', service, params)
|
||||
|
||||
def update_security_state(self, entity_id=None,
|
||||
old_state=None, new_state=None):
|
||||
"""Update security state after state changed."""
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
hass_state = new_state.state
|
||||
if hass_state not in HASS_TO_HOMEKIT:
|
||||
return
|
||||
current_security_state = HASS_TO_HOMEKIT[hass_state]
|
||||
self.char_current_state.set_value(current_security_state)
|
||||
_LOGGER.debug("%s: Updated current state to %s (%d)",
|
||||
self._entity_id, hass_state,
|
||||
current_security_state)
|
||||
|
||||
if not self.flag_target_state:
|
||||
self.char_target_state.set_value(current_security_state,
|
||||
should_callback=False)
|
||||
elif self.char_target_state.get_value() \
|
||||
== self.char_current_state.get_value():
|
||||
self.flag_target_state = False
|
|
@ -0,0 +1,62 @@
|
|||
"""Class to hold all switch accessories."""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import split_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory, add_preload_service
|
||||
from .const import SERV_SWITCH, CHAR_ON
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@TYPES.register('Switch')
|
||||
class Switch(HomeAccessory):
|
||||
"""Generate a Switch accessory."""
|
||||
|
||||
def __init__(self, hass, entity_id, display_name):
|
||||
"""Initialize a Switch accessory object to represent a remote."""
|
||||
super().__init__(display_name, entity_id, 'SWITCH')
|
||||
|
||||
self._hass = hass
|
||||
self._entity_id = entity_id
|
||||
self._domain = split_entity_id(entity_id)[0]
|
||||
|
||||
self.flag_target_state = False
|
||||
|
||||
self.service_switch = add_preload_service(self, SERV_SWITCH)
|
||||
self.char_on = self.service_switch.get_characteristic(CHAR_ON)
|
||||
self.char_on.value = False
|
||||
self.char_on.setter_callback = self.set_state
|
||||
|
||||
def run(self):
|
||||
"""Method called be object after driver is started."""
|
||||
state = self._hass.states.get(self._entity_id)
|
||||
self.update_state(new_state=state)
|
||||
|
||||
async_track_state_change(self._hass, self._entity_id,
|
||||
self.update_state)
|
||||
|
||||
def set_state(self, value):
|
||||
"""Move switch state to value if call came from HomeKit."""
|
||||
_LOGGER.debug("%s: Set switch state to %s",
|
||||
self._entity_id, value)
|
||||
self.flag_target_state = True
|
||||
service = 'turn_on' if value else 'turn_off'
|
||||
self._hass.services.call(self._domain, service,
|
||||
{ATTR_ENTITY_ID: self._entity_id})
|
||||
|
||||
def update_state(self, entity_id=None, old_state=None, new_state=None):
|
||||
"""Update switch state after state changed."""
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
current_state = (new_state.state == 'on')
|
||||
if not self.flag_target_state:
|
||||
_LOGGER.debug("%s: Set current state to %s",
|
||||
self._entity_id, current_state)
|
||||
self.char_on.set_value(current_state, should_callback=False)
|
||||
else:
|
||||
self.flag_target_state = False
|
|
@ -0,0 +1,245 @@
|
|||
"""Class to hold all thermostat accessories."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE,
|
||||
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||
ATTR_OPERATION_MODE, ATTR_OPERATION_LIST,
|
||||
STATE_HEAT, STATE_COOL, STATE_AUTO)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT,
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory, add_preload_service
|
||||
from .const import (
|
||||
SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING,
|
||||
CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE,
|
||||
CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS,
|
||||
CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STATE_OFF = 'off'
|
||||
UNIT_HASS_TO_HOMEKIT = {TEMP_CELSIUS: 0, TEMP_FAHRENHEIT: 1}
|
||||
UNIT_HOMEKIT_TO_HASS = {c: s for s, c in UNIT_HASS_TO_HOMEKIT.items()}
|
||||
HC_HASS_TO_HOMEKIT = {STATE_OFF: 0, STATE_HEAT: 1,
|
||||
STATE_COOL: 2, STATE_AUTO: 3}
|
||||
HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()}
|
||||
|
||||
|
||||
@TYPES.register('Thermostat')
|
||||
class Thermostat(HomeAccessory):
|
||||
"""Generate a Thermostat accessory for a climate."""
|
||||
|
||||
def __init__(self, hass, entity_id, display_name, support_auto=False):
|
||||
"""Initialize a Thermostat accessory object."""
|
||||
super().__init__(display_name, entity_id, 'THERMOSTAT')
|
||||
|
||||
self._hass = hass
|
||||
self._entity_id = entity_id
|
||||
self._call_timer = None
|
||||
|
||||
self.heat_cool_flag_target_state = False
|
||||
self.temperature_flag_target_state = False
|
||||
self.coolingthresh_flag_target_state = False
|
||||
self.heatingthresh_flag_target_state = False
|
||||
|
||||
extra_chars = None
|
||||
# Add additional characteristics if auto mode is supported
|
||||
if support_auto:
|
||||
extra_chars = [CHAR_COOLING_THRESHOLD_TEMPERATURE,
|
||||
CHAR_HEATING_THRESHOLD_TEMPERATURE]
|
||||
|
||||
# Preload the thermostat service
|
||||
self.service_thermostat = add_preload_service(self, SERV_THERMOSTAT,
|
||||
extra_chars)
|
||||
|
||||
# Current and target mode characteristics
|
||||
self.char_current_heat_cool = self.service_thermostat. \
|
||||
get_characteristic(CHAR_CURRENT_HEATING_COOLING)
|
||||
self.char_current_heat_cool.value = 0
|
||||
self.char_target_heat_cool = self.service_thermostat. \
|
||||
get_characteristic(CHAR_TARGET_HEATING_COOLING)
|
||||
self.char_target_heat_cool.value = 0
|
||||
self.char_target_heat_cool.setter_callback = self.set_heat_cool
|
||||
|
||||
# Current and target temperature characteristics
|
||||
self.char_current_temp = self.service_thermostat. \
|
||||
get_characteristic(CHAR_CURRENT_TEMPERATURE)
|
||||
self.char_current_temp.value = 21.0
|
||||
self.char_target_temp = self.service_thermostat. \
|
||||
get_characteristic(CHAR_TARGET_TEMPERATURE)
|
||||
self.char_target_temp.value = 21.0
|
||||
self.char_target_temp.setter_callback = self.set_target_temperature
|
||||
|
||||
# Display units characteristic
|
||||
self.char_display_units = self.service_thermostat. \
|
||||
get_characteristic(CHAR_TEMP_DISPLAY_UNITS)
|
||||
self.char_display_units.value = 0
|
||||
|
||||
# If the device supports it: high and low temperature characteristics
|
||||
if support_auto:
|
||||
self.char_cooling_thresh_temp = self.service_thermostat. \
|
||||
get_characteristic(CHAR_COOLING_THRESHOLD_TEMPERATURE)
|
||||
self.char_cooling_thresh_temp.value = 23.0
|
||||
self.char_cooling_thresh_temp.setter_callback = \
|
||||
self.set_cooling_threshold
|
||||
|
||||
self.char_heating_thresh_temp = self.service_thermostat. \
|
||||
get_characteristic(CHAR_HEATING_THRESHOLD_TEMPERATURE)
|
||||
self.char_heating_thresh_temp.value = 19.0
|
||||
self.char_heating_thresh_temp.setter_callback = \
|
||||
self.set_heating_threshold
|
||||
else:
|
||||
self.char_cooling_thresh_temp = None
|
||||
self.char_heating_thresh_temp = None
|
||||
|
||||
def run(self):
|
||||
"""Method called be object after driver is started."""
|
||||
state = self._hass.states.get(self._entity_id)
|
||||
self.update_thermostat(new_state=state)
|
||||
|
||||
async_track_state_change(self._hass, self._entity_id,
|
||||
self.update_thermostat)
|
||||
|
||||
def set_heat_cool(self, value):
|
||||
"""Move 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
|
||||
hass_value = HC_HOMEKIT_TO_HASS[value]
|
||||
self._hass.services.call('climate', 'set_operation_mode',
|
||||
{ATTR_ENTITY_ID: self._entity_id,
|
||||
ATTR_OPERATION_MODE: hass_value})
|
||||
|
||||
def set_cooling_threshold(self, value):
|
||||
"""Set cooling threshold temp to value if call came from HomeKit."""
|
||||
_LOGGER.debug("%s: Set cooling threshold temperature to %.2f",
|
||||
self._entity_id, value)
|
||||
self.coolingthresh_flag_target_state = True
|
||||
low = self.char_heating_thresh_temp.get_value()
|
||||
self._hass.services.call(
|
||||
'climate', 'set_temperature',
|
||||
{ATTR_ENTITY_ID: self._entity_id,
|
||||
ATTR_TARGET_TEMP_HIGH: value,
|
||||
ATTR_TARGET_TEMP_LOW: low})
|
||||
|
||||
def set_heating_threshold(self, value):
|
||||
"""Set heating threshold temp to value if call came from HomeKit."""
|
||||
_LOGGER.debug("%s: Set heating threshold temperature to %.2f",
|
||||
self._entity_id, value)
|
||||
self.heatingthresh_flag_target_state = True
|
||||
# Home assistant always wants to set low and high at the same time
|
||||
high = self.char_cooling_thresh_temp.get_value()
|
||||
self._hass.services.call(
|
||||
'climate', 'set_temperature',
|
||||
{ATTR_ENTITY_ID: self._entity_id,
|
||||
ATTR_TARGET_TEMP_LOW: value,
|
||||
ATTR_TARGET_TEMP_HIGH: high})
|
||||
|
||||
def set_target_temperature(self, value):
|
||||
"""Set target temperature to value if call came from HomeKit."""
|
||||
_LOGGER.debug("%s: Set target temperature to %.2f",
|
||||
self._entity_id, value)
|
||||
self.temperature_flag_target_state = True
|
||||
self._hass.services.call(
|
||||
'climate', 'set_temperature',
|
||||
{ATTR_ENTITY_ID: self._entity_id,
|
||||
ATTR_TEMPERATURE: value})
|
||||
|
||||
def update_thermostat(self, entity_id=None,
|
||||
old_state=None, new_state=None):
|
||||
"""Update security state after state changed."""
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
# Update current temperature
|
||||
current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE)
|
||||
if current_temp is not None:
|
||||
self.char_current_temp.set_value(current_temp)
|
||||
|
||||
# Update target temperature
|
||||
target_temp = new_state.attributes.get(ATTR_TEMPERATURE)
|
||||
if target_temp is not None:
|
||||
if not self.temperature_flag_target_state:
|
||||
self.char_target_temp.set_value(target_temp,
|
||||
should_callback=False)
|
||||
else:
|
||||
self.temperature_flag_target_state = False
|
||||
|
||||
# Update cooling threshold temperature if characteristic exists
|
||||
if self.char_cooling_thresh_temp is not None:
|
||||
cooling_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH)
|
||||
if cooling_thresh is not None:
|
||||
if not self.coolingthresh_flag_target_state:
|
||||
self.char_cooling_thresh_temp.set_value(
|
||||
cooling_thresh, should_callback=False)
|
||||
else:
|
||||
self.coolingthresh_flag_target_state = False
|
||||
|
||||
# Update heating threshold temperature if characteristic exists
|
||||
if self.char_heating_thresh_temp is not None:
|
||||
heating_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_LOW)
|
||||
if heating_thresh is not None:
|
||||
if not self.heatingthresh_flag_target_state:
|
||||
self.char_heating_thresh_temp.set_value(
|
||||
heating_thresh, should_callback=False)
|
||||
else:
|
||||
self.heatingthresh_flag_target_state = False
|
||||
|
||||
# Update display units
|
||||
display_units = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
if display_units is not None \
|
||||
and display_units in UNIT_HASS_TO_HOMEKIT:
|
||||
self.char_display_units.set_value(
|
||||
UNIT_HASS_TO_HOMEKIT[display_units])
|
||||
|
||||
# Update target operation mode
|
||||
operation_mode = new_state.attributes.get(ATTR_OPERATION_MODE)
|
||||
if operation_mode is not None \
|
||||
and operation_mode in HC_HASS_TO_HOMEKIT:
|
||||
if not self.heat_cool_flag_target_state:
|
||||
self.char_target_heat_cool.set_value(
|
||||
HC_HASS_TO_HOMEKIT[operation_mode], should_callback=False)
|
||||
else:
|
||||
self.heat_cool_flag_target_state = False
|
||||
|
||||
# Set current operation mode based on temperatures and target mode
|
||||
if operation_mode == STATE_HEAT:
|
||||
if current_temp < target_temp:
|
||||
current_operation_mode = STATE_HEAT
|
||||
else:
|
||||
current_operation_mode = STATE_OFF
|
||||
elif operation_mode == STATE_COOL:
|
||||
if current_temp > target_temp:
|
||||
current_operation_mode = STATE_COOL
|
||||
else:
|
||||
current_operation_mode = STATE_OFF
|
||||
elif operation_mode == STATE_AUTO:
|
||||
# Check if auto is supported
|
||||
if self.char_cooling_thresh_temp is not None:
|
||||
lower_temp = self.char_heating_thresh_temp.get_value()
|
||||
upper_temp = self.char_cooling_thresh_temp.get_value()
|
||||
if current_temp < lower_temp:
|
||||
current_operation_mode = STATE_HEAT
|
||||
elif current_temp > upper_temp:
|
||||
current_operation_mode = STATE_COOL
|
||||
else:
|
||||
current_operation_mode = STATE_OFF
|
||||
else:
|
||||
# Check if heating or cooling are supported
|
||||
heat = STATE_HEAT in new_state.attributes[ATTR_OPERATION_LIST]
|
||||
cool = STATE_COOL in new_state.attributes[ATTR_OPERATION_LIST]
|
||||
if current_temp < target_temp and heat:
|
||||
current_operation_mode = STATE_HEAT
|
||||
elif current_temp > target_temp and cool:
|
||||
current_operation_mode = STATE_COOL
|
||||
else:
|
||||
current_operation_mode = STATE_OFF
|
||||
else:
|
||||
current_operation_mode = STATE_OFF
|
||||
|
||||
self.char_current_heat_cool.set_value(
|
||||
HC_HASS_TO_HOMEKIT[current_operation_mode])
|
|
@ -95,7 +95,10 @@ IGNORE_PACKAGES = (
|
|||
'homeassistant.components.recorder.models',
|
||||
'homeassistant.components.homekit.accessories',
|
||||
'homeassistant.components.homekit.covers',
|
||||
'homeassistant.components.homekit.sensors'
|
||||
'homeassistant.components.homekit.security_systems',
|
||||
'homeassistant.components.homekit.sensors',
|
||||
'homeassistant.components.homekit.switches',
|
||||
'homeassistant.components.homekit.thermostats'
|
||||
)
|
||||
|
||||
IGNORE_PIN = ('colorlog>2.1,<3', 'keyring>=9.3,<10.0', 'urllib3')
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
"""Test different accessory types: Security Systems."""
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.homekit.security_systems import SecuritySystem
|
||||
from homeassistant.const import (
|
||||
ATTR_SERVICE, EVENT_CALL_SERVICE,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED)
|
||||
|
||||
from tests.common import get_test_home_assistant
|
||||
from tests.mock.homekit import get_patch_paths, mock_preload_service
|
||||
|
||||
PATH_ACC, PATH_FILE = get_patch_paths('security_systems')
|
||||
|
||||
|
||||
class TestHomekitSecuritySystems(unittest.TestCase):
|
||||
"""Test class for all accessory types regarding security systems."""
|
||||
|
||||
def setUp(self):
|
||||
"""Setup things to be run when tests are started."""
|
||||
self.hass = get_test_home_assistant()
|
||||
self.events = []
|
||||
|
||||
@callback
|
||||
def record_event(event):
|
||||
"""Track called event."""
|
||||
self.events.append(event)
|
||||
|
||||
self.hass.bus.listen(EVENT_CALL_SERVICE, record_event)
|
||||
|
||||
def tearDown(self):
|
||||
"""Stop down everything that was started."""
|
||||
self.hass.stop()
|
||||
|
||||
def test_switch_set_state(self):
|
||||
"""Test if accessory and HA are updated accordingly."""
|
||||
acp = 'alarm_control_panel.testsecurity'
|
||||
|
||||
with patch(PATH_ACC, side_effect=mock_preload_service):
|
||||
with patch(PATH_FILE, side_effect=mock_preload_service):
|
||||
acc = SecuritySystem(self.hass, acp, 'SecuritySystem')
|
||||
acc.run()
|
||||
|
||||
self.assertEqual(acc.char_current_state.value, 3)
|
||||
self.assertEqual(acc.char_target_state.value, 3)
|
||||
|
||||
self.hass.states.set(acp, STATE_ALARM_ARMED_AWAY)
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(acc.char_target_state.value, 1)
|
||||
self.assertEqual(acc.char_current_state.value, 1)
|
||||
|
||||
self.hass.states.set(acp, STATE_ALARM_ARMED_HOME)
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(acc.char_target_state.value, 0)
|
||||
self.assertEqual(acc.char_current_state.value, 0)
|
||||
|
||||
self.hass.states.set(acp, STATE_ALARM_ARMED_NIGHT)
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(acc.char_target_state.value, 2)
|
||||
self.assertEqual(acc.char_current_state.value, 2)
|
||||
|
||||
self.hass.states.set(acp, STATE_ALARM_DISARMED)
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(acc.char_target_state.value, 3)
|
||||
self.assertEqual(acc.char_current_state.value, 3)
|
||||
|
||||
# Set from HomeKit
|
||||
acc.char_target_state.set_value(0)
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(
|
||||
self.events[0].data[ATTR_SERVICE], 'alarm_arm_home')
|
||||
self.assertEqual(acc.char_target_state.value, 0)
|
||||
|
||||
acc.char_target_state.set_value(1)
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(
|
||||
self.events[1].data[ATTR_SERVICE], 'alarm_arm_away')
|
||||
self.assertEqual(acc.char_target_state.value, 1)
|
||||
|
||||
acc.char_target_state.set_value(2)
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(
|
||||
self.events[2].data[ATTR_SERVICE], 'alarm_arm_night')
|
||||
self.assertEqual(acc.char_target_state.value, 2)
|
||||
|
||||
acc.char_target_state.set_value(3)
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(
|
||||
self.events[3].data[ATTR_SERVICE], 'alarm_disarm')
|
||||
self.assertEqual(acc.char_target_state.value, 3)
|
|
@ -0,0 +1,64 @@
|
|||
"""Test different accessory types: Switches."""
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.homekit.switches import Switch
|
||||
from homeassistant.const import ATTR_SERVICE, EVENT_CALL_SERVICE
|
||||
|
||||
from tests.common import get_test_home_assistant
|
||||
from tests.mock.homekit import get_patch_paths, mock_preload_service
|
||||
|
||||
PATH_ACC, PATH_FILE = get_patch_paths('switches')
|
||||
|
||||
|
||||
class TestHomekitSwitches(unittest.TestCase):
|
||||
"""Test class for all accessory types regarding switches."""
|
||||
|
||||
def setUp(self):
|
||||
"""Setup things to be run when tests are started."""
|
||||
self.hass = get_test_home_assistant()
|
||||
self.events = []
|
||||
|
||||
@callback
|
||||
def record_event(event):
|
||||
"""Track called event."""
|
||||
self.events.append(event)
|
||||
|
||||
self.hass.bus.listen(EVENT_CALL_SERVICE, record_event)
|
||||
|
||||
def tearDown(self):
|
||||
"""Stop down everything that was started."""
|
||||
self.hass.stop()
|
||||
|
||||
def test_switch_set_state(self):
|
||||
"""Test if accessory and HA are updated accordingly."""
|
||||
switch = 'switch.testswitch'
|
||||
|
||||
with patch(PATH_ACC, side_effect=mock_preload_service):
|
||||
with patch(PATH_FILE, side_effect=mock_preload_service):
|
||||
acc = Switch(self.hass, switch, 'Switch')
|
||||
acc.run()
|
||||
|
||||
self.assertEqual(acc.char_on.value, False)
|
||||
|
||||
self.hass.states.set(switch, 'on')
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(acc.char_on.value, True)
|
||||
|
||||
self.hass.states.set(switch, 'off')
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(acc.char_on.value, False)
|
||||
|
||||
# Set from HomeKit
|
||||
acc.char_on.set_value(True)
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(
|
||||
self.events[0].data[ATTR_SERVICE], 'turn_on')
|
||||
self.assertEqual(acc.char_on.value, True)
|
||||
|
||||
acc.char_on.set_value(False)
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(
|
||||
self.events[1].data[ATTR_SERVICE], 'turn_off')
|
||||
self.assertEqual(acc.char_on.value, False)
|
|
@ -0,0 +1,179 @@
|
|||
"""Test different accessory types: Thermostats."""
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE,
|
||||
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_OPERATION_MODE, STATE_HEAT, STATE_AUTO)
|
||||
from homeassistant.components.homekit.thermostats import Thermostat, STATE_OFF
|
||||
from homeassistant.const import (
|
||||
ATTR_SERVICE, EVENT_CALL_SERVICE, ATTR_SERVICE_DATA,
|
||||
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS)
|
||||
|
||||
from tests.common import get_test_home_assistant
|
||||
from tests.mock.homekit import get_patch_paths, mock_preload_service
|
||||
|
||||
PATH_ACC, PATH_FILE = get_patch_paths('thermostats')
|
||||
|
||||
|
||||
class TestHomekitThermostats(unittest.TestCase):
|
||||
"""Test class for all accessory types regarding thermostats."""
|
||||
|
||||
def setUp(self):
|
||||
"""Setup things to be run when tests are started."""
|
||||
self.hass = get_test_home_assistant()
|
||||
self.events = []
|
||||
|
||||
@callback
|
||||
def record_event(event):
|
||||
"""Track called event."""
|
||||
self.events.append(event)
|
||||
|
||||
self.hass.bus.listen(EVENT_CALL_SERVICE, record_event)
|
||||
|
||||
def tearDown(self):
|
||||
"""Stop down everything that was started."""
|
||||
self.hass.stop()
|
||||
|
||||
def test_default_thermostat(self):
|
||||
"""Test if accessory and HA are updated accordingly."""
|
||||
climate = 'climate.testclimate'
|
||||
|
||||
with patch(PATH_ACC, side_effect=mock_preload_service):
|
||||
with patch(PATH_FILE, side_effect=mock_preload_service):
|
||||
acc = Thermostat(self.hass, climate, 'Climate', False)
|
||||
acc.run()
|
||||
|
||||
self.assertEqual(acc.char_current_heat_cool.value, 0)
|
||||
self.assertEqual(acc.char_target_heat_cool.value, 0)
|
||||
self.assertEqual(acc.char_current_temp.value, 21.0)
|
||||
self.assertEqual(acc.char_target_temp.value, 21.0)
|
||||
self.assertEqual(acc.char_display_units.value, 0)
|
||||
self.assertEqual(acc.char_cooling_thresh_temp, None)
|
||||
self.assertEqual(acc.char_heating_thresh_temp, None)
|
||||
|
||||
self.hass.states.set(climate, STATE_HEAT,
|
||||
{ATTR_OPERATION_MODE: STATE_HEAT,
|
||||
ATTR_TEMPERATURE: 22.0,
|
||||
ATTR_CURRENT_TEMPERATURE: 18.0,
|
||||
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(acc.char_target_temp.value, 22.0)
|
||||
self.assertEqual(acc.char_current_heat_cool.value, 1)
|
||||
self.assertEqual(acc.char_target_heat_cool.value, 1)
|
||||
self.assertEqual(acc.char_current_temp.value, 18.0)
|
||||
self.assertEqual(acc.char_display_units.value, 0)
|
||||
|
||||
self.hass.states.set(climate, STATE_HEAT,
|
||||
{ATTR_OPERATION_MODE: STATE_HEAT,
|
||||
ATTR_TEMPERATURE: 22.0,
|
||||
ATTR_CURRENT_TEMPERATURE: 23.0,
|
||||
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(acc.char_target_temp.value, 22.0)
|
||||
self.assertEqual(acc.char_current_heat_cool.value, 0)
|
||||
self.assertEqual(acc.char_target_heat_cool.value, 1)
|
||||
self.assertEqual(acc.char_current_temp.value, 23.0)
|
||||
self.assertEqual(acc.char_display_units.value, 0)
|
||||
|
||||
self.hass.states.set(climate, STATE_OFF,
|
||||
{ATTR_OPERATION_MODE: STATE_OFF,
|
||||
ATTR_TEMPERATURE: 22.0,
|
||||
ATTR_CURRENT_TEMPERATURE: 18.0,
|
||||
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(acc.char_target_temp.value, 22.0)
|
||||
self.assertEqual(acc.char_current_heat_cool.value, 0)
|
||||
self.assertEqual(acc.char_target_heat_cool.value, 0)
|
||||
self.assertEqual(acc.char_current_temp.value, 18.0)
|
||||
self.assertEqual(acc.char_display_units.value, 0)
|
||||
|
||||
# Set from HomeKit
|
||||
acc.char_target_temp.set_value(19.0)
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(
|
||||
self.events[0].data[ATTR_SERVICE], 'set_temperature')
|
||||
self.assertEqual(
|
||||
self.events[0].data[ATTR_SERVICE_DATA][ATTR_TEMPERATURE], 19.0)
|
||||
self.assertEqual(acc.char_target_temp.value, 19.0)
|
||||
|
||||
acc.char_target_heat_cool.set_value(1)
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(
|
||||
self.events[1].data[ATTR_SERVICE], 'set_operation_mode')
|
||||
self.assertEqual(
|
||||
self.events[1].data[ATTR_SERVICE_DATA][ATTR_OPERATION_MODE],
|
||||
STATE_HEAT)
|
||||
self.assertEqual(acc.char_target_heat_cool.value, 1)
|
||||
|
||||
def test_auto_thermostat(self):
|
||||
"""Test if accessory and HA are updated accordingly."""
|
||||
climate = 'climate.testclimate'
|
||||
|
||||
acc = Thermostat(self.hass, climate, 'Climate', True)
|
||||
acc.run()
|
||||
|
||||
self.assertEqual(acc.char_cooling_thresh_temp.value, 23.0)
|
||||
self.assertEqual(acc.char_heating_thresh_temp.value, 19.0)
|
||||
|
||||
self.hass.states.set(climate, STATE_AUTO,
|
||||
{ATTR_OPERATION_MODE: STATE_AUTO,
|
||||
ATTR_TARGET_TEMP_HIGH: 22.0,
|
||||
ATTR_TARGET_TEMP_LOW: 20.0,
|
||||
ATTR_CURRENT_TEMPERATURE: 18.0,
|
||||
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(acc.char_heating_thresh_temp.value, 20.0)
|
||||
self.assertEqual(acc.char_cooling_thresh_temp.value, 22.0)
|
||||
self.assertEqual(acc.char_current_heat_cool.value, 1)
|
||||
self.assertEqual(acc.char_target_heat_cool.value, 3)
|
||||
self.assertEqual(acc.char_current_temp.value, 18.0)
|
||||
self.assertEqual(acc.char_display_units.value, 0)
|
||||
|
||||
self.hass.states.set(climate, STATE_AUTO,
|
||||
{ATTR_OPERATION_MODE: STATE_AUTO,
|
||||
ATTR_TARGET_TEMP_HIGH: 23.0,
|
||||
ATTR_TARGET_TEMP_LOW: 19.0,
|
||||
ATTR_CURRENT_TEMPERATURE: 24.0,
|
||||
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(acc.char_heating_thresh_temp.value, 19.0)
|
||||
self.assertEqual(acc.char_cooling_thresh_temp.value, 23.0)
|
||||
self.assertEqual(acc.char_current_heat_cool.value, 2)
|
||||
self.assertEqual(acc.char_target_heat_cool.value, 3)
|
||||
self.assertEqual(acc.char_current_temp.value, 24.0)
|
||||
self.assertEqual(acc.char_display_units.value, 0)
|
||||
|
||||
self.hass.states.set(climate, STATE_AUTO,
|
||||
{ATTR_OPERATION_MODE: STATE_AUTO,
|
||||
ATTR_TARGET_TEMP_HIGH: 23.0,
|
||||
ATTR_TARGET_TEMP_LOW: 19.0,
|
||||
ATTR_CURRENT_TEMPERATURE: 21.0,
|
||||
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(acc.char_heating_thresh_temp.value, 19.0)
|
||||
self.assertEqual(acc.char_cooling_thresh_temp.value, 23.0)
|
||||
self.assertEqual(acc.char_current_heat_cool.value, 0)
|
||||
self.assertEqual(acc.char_target_heat_cool.value, 3)
|
||||
self.assertEqual(acc.char_current_temp.value, 21.0)
|
||||
self.assertEqual(acc.char_display_units.value, 0)
|
||||
|
||||
# Set from HomeKit
|
||||
acc.char_heating_thresh_temp.set_value(20.0)
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(
|
||||
self.events[0].data[ATTR_SERVICE], 'set_temperature')
|
||||
self.assertEqual(
|
||||
self.events[0].data[ATTR_SERVICE_DATA][ATTR_TARGET_TEMP_LOW], 20.0)
|
||||
self.assertEqual(acc.char_heating_thresh_temp.value, 20.0)
|
||||
|
||||
acc.char_cooling_thresh_temp.set_value(25.0)
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(
|
||||
self.events[1].data[ATTR_SERVICE], 'set_temperature')
|
||||
self.assertEqual(
|
||||
self.events[1].data[ATTR_SERVICE_DATA][ATTR_TARGET_TEMP_HIGH],
|
||||
25.0)
|
||||
self.assertEqual(acc.char_cooling_thresh_temp.value, 25.0)
|
Loading…
Reference in New Issue