From 1533bc1e1f5f58e07e1b9776e144218c728bf79b Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Sun, 17 Jun 2018 14:54:34 -0400 Subject: [PATCH] Add support for Homekit battery service (#14288) --- .../components/homekit/accessories.py | 51 +++++++++++++++-- homeassistant/components/homekit/const.py | 4 ++ homeassistant/const.py | 1 + tests/components/homekit/test_accessories.py | 55 ++++++++++++++++++- 4 files changed, 105 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 1b0d5ce1be4..d4e6d48c29f 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -8,7 +8,8 @@ from pyhap.accessory import Accessory, Bridge from pyhap.accessory_driver import AccessoryDriver from pyhap.const import CATEGORY_OTHER -from homeassistant.const import __version__ +from homeassistant.const import ( + __version__, ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL) from homeassistant.core import callback as ha_callback from homeassistant.core import split_entity_id from homeassistant.helpers.event import ( @@ -16,10 +17,11 @@ from homeassistant.helpers.event import ( from homeassistant.util import dt as dt_util from .const import ( - BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, - DEBOUNCE_TIMEOUT, MANUFACTURER) + BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, CHAR_BATTERY_LEVEL, + CHAR_CHARGING_STATE, CHAR_STATUS_LOW_BATTERY, DEBOUNCE_TIMEOUT, + MANUFACTURER, SERV_BATTERY_SERVICE) from .util import ( - show_setup_message, dismiss_setup_message) + convert_to_float, show_setup_message, dismiss_setup_message) _LOGGER = logging.getLogger(__name__) @@ -67,6 +69,23 @@ class HomeAccessory(Accessory): self.entity_id = entity_id self.hass = hass self.debounce = {} + self._support_battery_level = False + self._support_battery_charging = True + + """Add battery service if available""" + battery_level = self.hass.states.get(self.entity_id).attributes \ + .get(ATTR_BATTERY_LEVEL) + if battery_level is None: + return + _LOGGER.debug('%s: Found battery level attribute', self.entity_id) + self._support_battery_level = True + serv_battery = self.add_preload_service(SERV_BATTERY_SERVICE) + self._char_battery = serv_battery.configure_char( + CHAR_BATTERY_LEVEL, value=0) + self._char_charging = serv_battery.configure_char( + CHAR_CHARGING_STATE, value=2) + self._char_low_battery = serv_battery.configure_char( + CHAR_STATUS_LOW_BATTERY, value=0) async def run(self): """Method called by accessory after driver is started. @@ -85,8 +104,32 @@ class HomeAccessory(Accessory): _LOGGER.debug('New_state: %s', new_state) if new_state is None: return + if self._support_battery_level: + self.hass.async_add_job(self.update_battery, new_state) self.hass.async_add_job(self.update_state, new_state) + def update_battery(self, new_state): + """Update battery service if available. + + Only call this function if self._support_battery_level is True. + """ + battery_level = convert_to_float( + new_state.attributes.get(ATTR_BATTERY_LEVEL)) + self._char_battery.set_value(battery_level) + self._char_low_battery.set_value(battery_level < 20) + _LOGGER.debug('%s: Updated battery level to %d', self.entity_id, + battery_level) + if not self._support_battery_charging: + return + charging = new_state.attributes.get(ATTR_BATTERY_CHARGING) + if charging is None: + self._support_battery_charging = False + return + hk_charging = 1 if charging is True else 0 + self._char_charging.set_value(hk_charging) + _LOGGER.debug('%s: Updated battery charging to %d', self.entity_id, + hk_charging) + def update_state(self, new_state): """Method called on state change to update HomeKit value. diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index dec6353850e..33d2c0bfb85 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -38,6 +38,7 @@ TYPE_SWITCH = 'switch' # #### Services #### SERV_ACCESSORY_INFO = 'AccessoryInformation' SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor' +SERV_BATTERY_SERVICE = 'BatteryService' SERV_CARBON_DIOXIDE_SENSOR = 'CarbonDioxideSensor' SERV_CARBON_MONOXIDE_SENSOR = 'CarbonMonoxideSensor' SERV_CONTACT_SENSOR = 'ContactSensor' @@ -62,11 +63,13 @@ SERV_WINDOW_COVERING = 'WindowCovering' CHAR_ACTIVE = 'Active' CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity' CHAR_AIR_QUALITY = 'AirQuality' +CHAR_BATTERY_LEVEL = 'BatteryLevel' CHAR_BRIGHTNESS = 'Brightness' CHAR_CARBON_DIOXIDE_DETECTED = 'CarbonDioxideDetected' CHAR_CARBON_DIOXIDE_LEVEL = 'CarbonDioxideLevel' CHAR_CARBON_DIOXIDE_PEAK_LEVEL = 'CarbonDioxidePeakLevel' CHAR_CARBON_MONOXIDE_DETECTED = 'CarbonMonoxideDetected' +CHAR_CHARGING_STATE = 'ChargingState' CHAR_COLOR_TEMPERATURE = 'ColorTemperature' CHAR_CONTACT_SENSOR_STATE = 'ContactSensorState' CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' @@ -96,6 +99,7 @@ CHAR_ROTATION_DIRECTION = 'RotationDirection' CHAR_SATURATION = 'Saturation' CHAR_SERIAL_NUMBER = 'SerialNumber' CHAR_SMOKE_DETECTED = 'SmokeDetected' +CHAR_STATUS_LOW_BATTERY = 'StatusLowBattery' CHAR_SWING_MODE = 'SwingMode' CHAR_TARGET_DOOR_STATE = 'TargetDoorState' CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState' diff --git a/homeassistant/const.py b/homeassistant/const.py index 7f315cf616c..cb6858639f4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -254,6 +254,7 @@ ATTR_DISCOVERED = 'discovered' # Location of the device/sensor ATTR_LOCATION = 'location' +ATTR_BATTERY_CHARGING = 'battery_charging' ATTR_BATTERY_LEVEL = 'battery_level' ATTR_WAKEUP = 'wake_up_interval' diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 2ffdcb0830f..59da90cc75b 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -13,7 +13,9 @@ from homeassistant.components.homekit.const import ( BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, CHAR_FIRMWARE_REVISION, CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER, MANUFACTURER, SERV_ACCESSORY_INFO) -from homeassistant.const import __version__, ATTR_NOW, EVENT_TIME_CHANGED +from homeassistant.const import ( + __version__, ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, ATTR_NOW, + EVENT_TIME_CHANGED) import homeassistant.util.dt as dt_util @@ -88,11 +90,60 @@ async def test_home_accessory(hass, hk_driver): # Test model name from domain entity_id = 'test_model.demo' - acc = HomeAccessory('hass', hk_driver, 'test_name', entity_id, 2, None) + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = HomeAccessory(hass, hk_driver, 'test_name', entity_id, 2, None) serv = acc.services[0] # SERV_ACCESSORY_INFO assert serv.get_characteristic(CHAR_MODEL).value == 'Test Model' +async def test_battery_service(hass, hk_driver): + """Test battery service.""" + entity_id = 'homekit.accessory' + hass.states.async_set(entity_id, None, {ATTR_BATTERY_LEVEL: 50}) + await hass.async_block_till_done() + + acc = HomeAccessory(hass, hk_driver, 'Battery Service', entity_id, 2, None) + assert acc._char_battery.value == 0 + assert acc._char_low_battery.value == 0 + assert acc._char_charging.value == 2 + + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc._char_battery.value == 50 + assert acc._char_low_battery.value == 0 + assert acc._char_charging.value == 2 + + hass.states.async_set(entity_id, None, {ATTR_BATTERY_LEVEL: 15}) + await hass.async_block_till_done() + assert acc._char_battery.value == 15 + assert acc._char_low_battery.value == 1 + assert acc._char_charging.value == 2 + + # Test charging + hass.states.async_set(entity_id, None, { + ATTR_BATTERY_LEVEL: 10, ATTR_BATTERY_CHARGING: True}) + await hass.async_block_till_done() + + acc = HomeAccessory(hass, hk_driver, 'Battery Service', entity_id, 2, None) + assert acc._char_battery.value == 0 + assert acc._char_low_battery.value == 0 + assert acc._char_charging.value == 2 + + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc._char_battery.value == 10 + assert acc._char_low_battery.value == 1 + assert acc._char_charging.value == 1 + + hass.states.async_set(entity_id, None, { + ATTR_BATTERY_LEVEL: 100, ATTR_BATTERY_CHARGING: False}) + await hass.async_block_till_done() + assert acc._char_battery.value == 100 + assert acc._char_low_battery.value == 0 + assert acc._char_charging.value == 0 + + def test_home_bridge(hk_driver): """Test HomeBridge class.""" bridge = HomeBridge('hass', hk_driver)