diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 40d43e2e14c..ad70740536e 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -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 diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 5201e21608a..35bd25eabd3 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -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} diff --git a/homeassistant/components/homekit/security_systems.py b/homeassistant/components/homekit/security_systems.py new file mode 100644 index 00000000000..1b8f0a6820b --- /dev/null +++ b/homeassistant/components/homekit/security_systems.py @@ -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 diff --git a/homeassistant/components/homekit/switches.py b/homeassistant/components/homekit/switches.py new file mode 100644 index 00000000000..876b3406d28 --- /dev/null +++ b/homeassistant/components/homekit/switches.py @@ -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 diff --git a/homeassistant/components/homekit/thermostats.py b/homeassistant/components/homekit/thermostats.py new file mode 100644 index 00000000000..766a7e3585d --- /dev/null +++ b/homeassistant/components/homekit/thermostats.py @@ -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]) diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 087f722eed1..2d2c6bd7563 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -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') diff --git a/tests/components/homekit/test_security_systems.py b/tests/components/homekit/test_security_systems.py new file mode 100644 index 00000000000..4753e86c084 --- /dev/null +++ b/tests/components/homekit/test_security_systems.py @@ -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) diff --git a/tests/components/homekit/test_switches.py b/tests/components/homekit/test_switches.py new file mode 100644 index 00000000000..d9f2d6c1d1a --- /dev/null +++ b/tests/components/homekit/test_switches.py @@ -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) diff --git a/tests/components/homekit/test_thermostats.py b/tests/components/homekit/test_thermostats.py new file mode 100644 index 00000000000..fabffe881bb --- /dev/null +++ b/tests/components/homekit/test_thermostats.py @@ -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)