From cb51553c2dd2bd6cc38beeb411f9b693f9ce4bac Mon Sep 17 00:00:00 2001 From: Yonsm Date: Mon, 9 Apr 2018 21:32:29 +0800 Subject: [PATCH] Support binary_sensor and device_tracker in HomeKit (#13735) * Support binary_sensor and device_tracker for HomeKit * Add test for get_accessory and binary sensor * Test service.display_name and char_detected.display_name * Split test to improve speed --- homeassistant/components/homekit/__init__.py | 5 ++ homeassistant/components/homekit/const.py | 23 ++++++++ .../components/homekit/type_sensors.py | 57 ++++++++++++++++++- .../homekit/test_get_accessories.py | 15 ++++- tests/components/homekit/test_type_sensors.py | 57 ++++++++++++++++++- 5 files changed, 152 insertions(+), 5 deletions(-) mode change 100644 => 100755 homeassistant/components/homekit/type_sensors.py diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 8a38c01026e..06258bcc97a 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -92,6 +92,11 @@ def get_accessory(hass, state, aid, config): return TYPES['HumiditySensor'](hass, state.entity_id, state.name, aid=aid) + elif state.domain == 'binary_sensor' or state.domain == 'device_tracker': + _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'BinarySensor') + return TYPES['BinarySensor'](hass, state.entity_id, + state.name, aid=aid) + elif state.domain == 'cover': # Only add covers that support set_cover_position features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 18d02a89e18..7136852c409 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -35,11 +35,18 @@ CATEGORY_WINDOW_COVERING = 'WINDOW_COVERING' # #### Services #### SERV_ACCESSORY_INFO = 'AccessoryInformation' +SERV_CARBON_DIOXIDE_SENSOR = 'CarbonDioxideSensor' +SERV_CARBON_MONOXIDE_SENSOR = 'CarbonMonoxideSensor' +SERV_CONTACT_SENSOR = 'ContactSensor' SERV_HUMIDITY_SENSOR = 'HumiditySensor' # CurrentRelativeHumidity | StatusActive, StatusFault, StatusTampered, # StatusLowBattery, Name +SERV_LEAK_SENSOR = 'LeakSensor' SERV_LIGHTBULB = 'Lightbulb' # On | Brightness, Hue, Saturation, Name +SERV_MOTION_SENSOR = 'MotionSensor' +SERV_OCCUPANCY_SENSOR = 'OccupancySensor' SERV_SECURITY_SYSTEM = 'SecuritySystem' +SERV_SMOKE_SENSOR = 'SmokeSensor' SERV_SWITCH = 'Switch' SERV_TEMPERATURE_SENSOR = 'TemperatureSensor' SERV_THERMOSTAT = 'Thermostat' @@ -48,7 +55,10 @@ SERV_WINDOW_COVERING = 'WindowCovering' # #### Characteristics #### CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100] +CHAR_CARBON_DIOXIDE_DETECTED = 'CarbonDioxideDetected' +CHAR_CARBON_MONOXIDE_DETECTED = 'CarbonMonoxideDetected' CHAR_COLOR_TEMPERATURE = 'ColorTemperature' +CHAR_CONTACT_SENSOR_STATE = 'ContactSensorState' CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState' CHAR_CURRENT_POSITION = 'CurrentPosition' @@ -57,13 +67,17 @@ CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState' CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' CHAR_HUE = 'Hue' # arcdegress | [0, 360] +CHAR_LEAK_DETECTED = 'LeakDetected' CHAR_MANUFACTURER = 'Manufacturer' CHAR_MODEL = 'Model' +CHAR_MOTION_DETECTED = 'MotionDetected' CHAR_NAME = 'Name' +CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected' CHAR_ON = 'On' # boolean CHAR_POSITION_STATE = 'PositionState' CHAR_SATURATION = 'Saturation' # percent CHAR_SERIAL_NUMBER = 'SerialNumber' +CHAR_SMOKE_DETECTED = 'SmokeDetected' CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState' CHAR_TARGET_POSITION = 'TargetPosition' CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState' @@ -72,3 +86,12 @@ CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits' # #### Properties #### PROP_CELSIUS = {'minValue': -273, 'maxValue': 999} + +# #### Device Class #### +DEVICE_CLASS_CO2 = 'co2' +DEVICE_CLASS_GAS = 'gas' +DEVICE_CLASS_MOISTURE = 'moisture' +DEVICE_CLASS_MOTION = 'motion' +DEVICE_CLASS_OCCUPANCY = 'occupancy' +DEVICE_CLASS_OPENING = 'opening' +DEVICE_CLASS_SMOKE = 'smoke' diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py old mode 100644 new mode 100755 index 393962eac21..b25eb784d6b --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -2,19 +2,40 @@ import logging from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) + ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, + ATTR_DEVICE_CLASS, STATE_ON, STATE_HOME) from . import TYPES from .accessories import HomeAccessory, add_preload_service from .const import ( CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, - CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS) + CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS, + DEVICE_CLASS_CO2, SERV_CARBON_DIOXIDE_SENSOR, CHAR_CARBON_DIOXIDE_DETECTED, + DEVICE_CLASS_GAS, SERV_CARBON_MONOXIDE_SENSOR, + CHAR_CARBON_MONOXIDE_DETECTED, + DEVICE_CLASS_MOISTURE, SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED, + DEVICE_CLASS_MOTION, SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED, + DEVICE_CLASS_OCCUPANCY, SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED, + DEVICE_CLASS_OPENING, SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, + DEVICE_CLASS_SMOKE, SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED) from .util import convert_to_float, temperature_to_homekit _LOGGER = logging.getLogger(__name__) +BINARY_SENSOR_SERVICE_MAP = { + DEVICE_CLASS_CO2: (SERV_CARBON_DIOXIDE_SENSOR, + CHAR_CARBON_DIOXIDE_DETECTED), + DEVICE_CLASS_GAS: (SERV_CARBON_MONOXIDE_SENSOR, + CHAR_CARBON_MONOXIDE_DETECTED), + DEVICE_CLASS_MOISTURE: (SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED), + DEVICE_CLASS_MOTION: (SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED), + DEVICE_CLASS_OCCUPANCY: (SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED), + DEVICE_CLASS_OPENING: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE), + DEVICE_CLASS_SMOKE: (SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED)} + + @TYPES.register('TemperatureSensor') class TemperatureSensor(HomeAccessory): """Generate a TemperatureSensor accessory for a temperature sensor. @@ -75,3 +96,35 @@ class HumiditySensor(HomeAccessory): self.char_humidity.set_value(humidity) _LOGGER.debug('%s: Percent set to %d%%', self.entity_id, humidity) + + +@TYPES.register('BinarySensor') +class BinarySensor(HomeAccessory): + """Generate a BinarySensor accessory as binary sensor.""" + + def __init__(self, hass, entity_id, name, **kwargs): + """Initialize a BinarySensor accessory object.""" + super().__init__(name, entity_id, CATEGORY_SENSOR, **kwargs) + + self.hass = hass + self.entity_id = entity_id + + device_class = hass.states.get(entity_id).attributes \ + .get(ATTR_DEVICE_CLASS) + service_char = BINARY_SENSOR_SERVICE_MAP[device_class] \ + if device_class in BINARY_SENSOR_SERVICE_MAP \ + else BINARY_SENSOR_SERVICE_MAP[DEVICE_CLASS_OCCUPANCY] + + service = add_preload_service(self, service_char[0]) + self.char_detected = service.get_characteristic(service_char[1]) + self.char_detected.value = 0 + + def update_state(self, entity_id=None, old_state=None, new_state=None): + """Update accessory after state change.""" + if new_state is None: + return + + state = new_state.state + detected = (state == STATE_ON) or (state == STATE_HOME) + self.char_detected.set_value(detected) + _LOGGER.debug('%s: Set to %d', self.entity_id, detected) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index e29ed85b5fc..e323431ac3f 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -9,7 +9,7 @@ from homeassistant.components.climate import ( from homeassistant.components.homekit import get_accessory, TYPES from homeassistant.const import ( ATTR_CODE, ATTR_UNIT_OF_MEASUREMENT, ATTR_SUPPORTED_FEATURES, - TEMP_CELSIUS, TEMP_FAHRENHEIT) + TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_DEVICE_CLASS) _LOGGER = logging.getLogger(__name__) @@ -63,6 +63,19 @@ class TestGetAccessories(unittest.TestCase): {ATTR_UNIT_OF_MEASUREMENT: '%'}) get_accessory(None, state, 2, {}) + def test_binary_sensor(self): + """Test binary sensor with opening class.""" + with patch.dict(TYPES, {'BinarySensor': self.mock_type}): + state = State('binary_sensor.opening', 'on', + {ATTR_DEVICE_CLASS: 'opening'}) + get_accessory(None, state, 2, {}) + + def test_device_tracker(self): + """Test binary sensor with opening class.""" + with patch.dict(TYPES, {'BinarySensor': self.mock_type}): + state = State('device_tracker.someone', 'not_home', {}) + get_accessory(None, state, 2, {}) + def test_cover_set_position(self): """Test cover with support for set_cover_position.""" with patch.dict(TYPES, {'WindowCovering': self.mock_type}): diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index c04c250613d..a6e178bb226 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -3,9 +3,10 @@ import unittest from homeassistant.components.homekit.const import PROP_CELSIUS from homeassistant.components.homekit.type_sensors import ( - TemperatureSensor, HumiditySensor) + TemperatureSensor, HumiditySensor, BinarySensor, BINARY_SENSOR_SERVICE_MAP) from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_UNIT_OF_MEASUREMENT, ATTR_DEVICE_CLASS, STATE_UNKNOWN, STATE_ON, + STATE_OFF, STATE_HOME, STATE_NOT_HOME, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import get_test_home_assistant @@ -68,3 +69,55 @@ class TestHomekitSensors(unittest.TestCase): self.hass.states.set(entity_id, '20', {ATTR_UNIT_OF_MEASUREMENT: "%"}) self.hass.block_till_done() self.assertEqual(acc.char_humidity.value, 20) + + def test_binary(self): + """Test if accessory is updated after state change.""" + entity_id = 'binary_sensor.opening' + + self.hass.states.set(entity_id, STATE_UNKNOWN, + {ATTR_DEVICE_CLASS: "opening"}) + self.hass.block_till_done() + + acc = BinarySensor(self.hass, entity_id, 'Window Opening', aid=2) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 10) # Sensor + + self.assertEqual(acc.char_detected.value, 0) + + self.hass.states.set(entity_id, STATE_ON, + {ATTR_DEVICE_CLASS: "opening"}) + self.hass.block_till_done() + self.assertEqual(acc.char_detected.value, 1) + + self.hass.states.set(entity_id, STATE_OFF, + {ATTR_DEVICE_CLASS: "opening"}) + self.hass.block_till_done() + self.assertEqual(acc.char_detected.value, 0) + + self.hass.states.set(entity_id, STATE_HOME, + {ATTR_DEVICE_CLASS: "opening"}) + self.hass.block_till_done() + self.assertEqual(acc.char_detected.value, 1) + + self.hass.states.set(entity_id, STATE_NOT_HOME, + {ATTR_DEVICE_CLASS: "opening"}) + self.hass.block_till_done() + self.assertEqual(acc.char_detected.value, 0) + + self.hass.states.remove(entity_id) + self.hass.block_till_done() + + def test_binary_device_classes(self): + """Test if services and characteristics are assigned correctly.""" + entity_id = 'binary_sensor.demo' + + for device_class, (service, char) in BINARY_SENSOR_SERVICE_MAP.items(): + self.hass.states.set(entity_id, STATE_OFF, + {ATTR_DEVICE_CLASS: device_class}) + self.hass.block_till_done() + + acc = BinarySensor(self.hass, entity_id, 'Binary Sensor', aid=2) + self.assertEqual(acc.get_service(service).display_name, service) + self.assertEqual(acc.char_detected.display_name, char)