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 tests
pull/12965/head
maxclaey 2018-03-07 13:17:52 +01:00 committed by cdce8p
parent 35bae1eef2
commit 4218b31e7b
9 changed files with 775 additions and 2 deletions

View File

@ -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

View File

@ -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}

View File

@ -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

View File

@ -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

View File

@ -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])

View File

@ -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')

View File

@ -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)

View File

@ -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)

View File

@ -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)