From 84b84559a4f157f3f30ccbe6de5e4347b8d54789 Mon Sep 17 00:00:00 2001 From: cpopp Date: Thu, 28 Feb 2019 12:09:04 -0600 Subject: [PATCH] Add support for homekit controller sensors (#21535) Adds support for homekit devices with temperature, humidity, and light level characteristics (such as the iHome iSS50) --- .../components/homekit_controller/__init__.py | 3 + .../components/homekit_controller/sensor.py | 153 ++++++++++++++++++ tests/components/homekit_controller/common.py | 7 +- .../homekit_controller/test_sensor.py | 79 +++++++++ 4 files changed, 240 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/homekit_controller/sensor.py create mode 100644 tests/components/homekit_controller/test_sensor.py diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index eb748a3d883..e4bfdc24ffb 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -25,6 +25,9 @@ HOMEKIT_ACCESSORY_DISPATCH = { 'window-covering': 'cover', 'lock-mechanism': 'lock', 'motion': 'binary_sensor', + 'humidity': 'sensor', + 'light': 'sensor', + 'temperature': 'sensor' } HOMEKIT_IGNORE = [ diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py new file mode 100644 index 00000000000..5af0016eb16 --- /dev/null +++ b/homeassistant/components/homekit_controller/sensor.py @@ -0,0 +1,153 @@ +"""Support for Homekit sensors.""" +from homeassistant.components.homekit_controller import ( + KNOWN_ACCESSORIES, HomeKitEntity) +from homeassistant.const import TEMP_CELSIUS + +DEPENDENCIES = ['homekit_controller'] + +HUMIDITY_ICON = 'mdi-water-percent' +TEMP_C_ICON = "mdi-temperature-celsius" +BRIGHTNESS_ICON = "mdi-brightness-6" + +UNIT_PERCENT = "%" +UNIT_LUX = "lux" + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up Homekit sensor support.""" + if discovery_info is not None: + accessory = hass.data[KNOWN_ACCESSORIES][discovery_info['serial']] + devtype = discovery_info['device-type'] + + if devtype == 'humidity': + add_entities( + [HomeKitHumiditySensor(accessory, discovery_info)], True) + elif devtype == 'temperature': + add_entities( + [HomeKitTemperatureSensor(accessory, discovery_info)], True) + elif devtype == 'light': + add_entities( + [HomeKitLightSensor(accessory, discovery_info)], True) + + +class HomeKitHumiditySensor(HomeKitEntity): + """Representation of a Homekit humidity sensor.""" + + def __init__(self, *args): + """Initialise the entity.""" + super().__init__(*args) + self._state = None + + def get_characteristic_types(self): + """Define the homekit characteristics the entity is tracking.""" + # pylint: disable=import-error + from homekit.model.characteristics import CharacteristicsTypes + + return [ + CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT + ] + + @property + def name(self): + """Return the name of the device.""" + return "{} {}".format(super().name, "Humidity") + + @property + def icon(self): + """Return the sensor icon.""" + return HUMIDITY_ICON + + @property + def unit_of_measurement(self): + """Return units for the sensor.""" + return UNIT_PERCENT + + def _update_relative_humidity_current(self, value): + self._state = value + + @property + def state(self): + """Return the current humidity.""" + return self._state + + +class HomeKitTemperatureSensor(HomeKitEntity): + """Representation of a Homekit temperature sensor.""" + + def __init__(self, *args): + """Initialise the entity.""" + super().__init__(*args) + self._state = None + + def get_characteristic_types(self): + """Define the homekit characteristics the entity is tracking.""" + # pylint: disable=import-error + from homekit.model.characteristics import CharacteristicsTypes + + return [ + CharacteristicsTypes.TEMPERATURE_CURRENT + ] + + @property + def name(self): + """Return the name of the device.""" + return "{} {}".format(super().name, "Temperature") + + @property + def icon(self): + """Return the sensor icon.""" + return TEMP_C_ICON + + @property + def unit_of_measurement(self): + """Return units for the sensor.""" + return TEMP_CELSIUS + + def _update_temperature_current(self, value): + self._state = value + + @property + def state(self): + """Return the current temperature in Celsius.""" + return self._state + + +class HomeKitLightSensor(HomeKitEntity): + """Representation of a Homekit light level sensor.""" + + def __init__(self, *args): + """Initialise the entity.""" + super().__init__(*args) + self._state = None + + def get_characteristic_types(self): + """Define the homekit characteristics the entity is tracking.""" + # pylint: disable=import-error + from homekit.model.characteristics import CharacteristicsTypes + + return [ + CharacteristicsTypes.LIGHT_LEVEL_CURRENT + ] + + @property + def name(self): + """Return the name of the device.""" + return "{} {}".format(super().name, "Light Level") + + @property + def icon(self): + """Return the sensor icon.""" + return BRIGHTNESS_ICON + + @property + def unit_of_measurement(self): + """Return units for the sensor.""" + return UNIT_LUX + + def _update_light_level_current(self, value): + self._state = value + + @property + def state(self): + """Return the current light level in lux.""" + return self._state diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index d543cf51749..9409e3affad 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -134,10 +134,12 @@ class FakeService(AbstractService): return char -async def setup_test_component(hass, services, capitalize=False): +async def setup_test_component(hass, services, capitalize=False, suffix=None): """Load a fake homekit accessory based on a homekit accessory model. If capitalize is True, property names will be in upper case. + + If suffix is set, entityId will include the suffix """ domain = None for service in services: @@ -174,4 +176,5 @@ async def setup_test_component(hass, services, capitalize=False): fire_service_discovered(hass, SERVICE_HOMEKIT, discovery_info) await hass.async_block_till_done() - return Helper(hass, '.'.join((domain, 'testdevice')), pairing, accessory) + entity = 'testdevice' if suffix is None else 'testdevice_{}'.format(suffix) + return Helper(hass, '.'.join((domain, entity)), pairing, accessory) diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py new file mode 100644 index 00000000000..c4311926636 --- /dev/null +++ b/tests/components/homekit_controller/test_sensor.py @@ -0,0 +1,79 @@ +"""Basic checks for HomeKit sensor.""" +from tests.components.homekit_controller.common import ( + FakeService, setup_test_component) + +TEMPERATURE = ('temperature', 'temperature.current') +HUMIDITY = ('humidity', 'relative-humidity.current') +LIGHT_LEVEL = ('light', 'light-level.current') + + +def create_temperature_sensor_service(): + """Define temperature characteristics.""" + service = FakeService('public.hap.service.sensor.temperature') + + cur_state = service.add_characteristic('temperature.current') + cur_state.value = 0 + + return service + + +def create_humidity_sensor_service(): + """Define humidity characteristics.""" + service = FakeService('public.hap.service.sensor.humidity') + + cur_state = service.add_characteristic('relative-humidity.current') + cur_state.value = 0 + + return service + + +def create_light_level_sensor_service(): + """Define light level characteristics.""" + service = FakeService('public.hap.service.sensor.light') + + cur_state = service.add_characteristic('light-level.current') + cur_state.value = 0 + + return service + + +async def test_temperature_sensor_read_state(hass, utcnow): + """Test reading the state of a HomeKit temperature sensor accessory.""" + sensor = create_temperature_sensor_service() + helper = await setup_test_component(hass, [sensor], suffix="temperature") + + helper.characteristics[TEMPERATURE].value = 10 + state = await helper.poll_and_get_state() + assert state.state == '10' + + helper.characteristics[TEMPERATURE].value = 20 + state = await helper.poll_and_get_state() + assert state.state == '20' + + +async def test_humidity_sensor_read_state(hass, utcnow): + """Test reading the state of a HomeKit humidity sensor accessory.""" + sensor = create_humidity_sensor_service() + helper = await setup_test_component(hass, [sensor], suffix="humidity") + + helper.characteristics[HUMIDITY].value = 10 + state = await helper.poll_and_get_state() + assert state.state == '10' + + helper.characteristics[HUMIDITY].value = 20 + state = await helper.poll_and_get_state() + assert state.state == '20' + + +async def test_light_level_sensor_read_state(hass, utcnow): + """Test reading the state of a HomeKit temperature sensor accessory.""" + sensor = create_light_level_sensor_service() + helper = await setup_test_component(hass, [sensor], suffix="light_level") + + helper.characteristics[LIGHT_LEVEL].value = 10 + state = await helper.poll_and_get_state() + assert state.state == '10' + + helper.characteristics[LIGHT_LEVEL].value = 20 + state = await helper.poll_and_get_state() + assert state.state == '20'