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
This commit is contained in:
Yonsm 2018-04-09 21:32:29 +08:00 committed by cdce8p
parent 8beb9c2b28
commit cb51553c2d
5 changed files with 152 additions and 5 deletions

View File

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

View File

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

57
homeassistant/components/homekit/type_sensors.py Normal file → Executable file
View File

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

View File

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

View File

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