diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 2738fafbfdb..8b0e70f616e 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -19,8 +19,8 @@ from homeassistant.util import dt as dt_util from .const import ( ATTR_DISPLAY_NAME, ATTR_VALUE, BRIDGE_MODEL, BRIDGE_SERIAL_NUMBER, CHAR_BATTERY_LEVEL, CHAR_CHARGING_STATE, CHAR_STATUS_LOW_BATTERY, - DEBOUNCE_TIMEOUT, EVENT_HOMEKIT_CHANGED, MANUFACTURER, - SERV_BATTERY_SERVICE) + CONF_LINKED_BATTERY_SENSOR, DEBOUNCE_TIMEOUT, EVENT_HOMEKIT_CHANGED, + MANUFACTURER, SERV_BATTERY_SERVICE) from .util import convert_to_float, dismiss_setup_message, show_setup_message _LOGGER = logging.getLogger(__name__) @@ -65,19 +65,25 @@ class HomeAccessory(Accessory): firmware_revision=__version__, manufacturer=MANUFACTURER, model=model, serial_number=entity_id) self.category = category - self.config = config + self.config = config or {} self.entity_id = entity_id self.hass = hass self.debounce = {} self._support_battery_level = False self._support_battery_charging = True + self.linked_battery_sensor = \ + self.config.get(CONF_LINKED_BATTERY_SENSOR) """Add battery service if available""" - battery_level = self.hass.states.get(self.entity_id).attributes \ + battery_found = self.hass.states.get(self.entity_id).attributes \ .get(ATTR_BATTERY_LEVEL) - if battery_level is None: + if self.linked_battery_sensor: + battery_found = self.hass.states.get( + self.linked_battery_sensor).state + + if battery_found is None: return - _LOGGER.debug('%s: Found battery level attribute', self.entity_id) + _LOGGER.debug('%s: Found battery level', self.entity_id) self._support_battery_level = True serv_battery = self.add_preload_service(SERV_BATTERY_SERVICE) self._char_battery = serv_battery.configure_char( @@ -104,6 +110,14 @@ class HomeAccessory(Accessory): async_track_state_change( self.hass, self.entity_id, self.update_state_callback) + if self.linked_battery_sensor: + battery_state = self.hass.states.get(self.linked_battery_sensor) + self.hass.async_add_job(self.update_linked_battery, None, None, + battery_state) + async_track_state_change( + self.hass, self.linked_battery_sensor, + self.update_linked_battery) + @ha_callback def update_state_callback(self, entity_id=None, old_state=None, new_state=None): @@ -111,10 +125,16 @@ class HomeAccessory(Accessory): _LOGGER.debug('New_state: %s', new_state) if new_state is None: return - if self._support_battery_level: + if self._support_battery_level and not self.linked_battery_sensor: self.hass.async_add_executor_job(self.update_battery, new_state) self.hass.async_add_executor_job(self.update_state, new_state) + @ha_callback + def update_linked_battery(self, entity_id=None, old_state=None, + new_state=None): + """Handle linked battery sensor state change listener callback.""" + self.hass.async_add_executor_job(self.update_battery, new_state) + def update_battery(self, new_state): """Update battery service if available. @@ -122,6 +142,8 @@ class HomeAccessory(Accessory): """ battery_level = convert_to_float( new_state.attributes.get(ATTR_BATTERY_LEVEL)) + if self.linked_battery_sensor: + battery_level = convert_to_float(new_state.state) if battery_level is None: return self._char_battery.set_value(battery_level) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 1b2a4dbf05d..4a96f0add8d 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -15,6 +15,7 @@ CONF_ENTITY_CONFIG = 'entity_config' CONF_FEATURE = 'feature' CONF_FEATURE_LIST = 'feature_list' CONF_FILTER = 'filter' +CONF_LINKED_BATTERY_SENSOR = 'linked_battery_sensor' CONF_SAFE_MODE = 'safe_mode' # #### Config Defaults #### diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 2ba5819a202..cf113e3ffe2 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol -from homeassistant.components import fan, media_player +from homeassistant.components import fan, media_player, sensor from homeassistant.const import ( ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_NAME, CONF_TYPE, TEMP_CELSIUS) from homeassistant.core import split_entity_id @@ -12,22 +12,23 @@ import homeassistant.helpers.config_validation as cv import homeassistant.util.temperature as temp_util from .const import ( - CONF_FEATURE, CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, - FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, HOMEKIT_NOTIFY_ID, TYPE_FAUCET, - TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER, TYPE_SWITCH, TYPE_VALVE) + CONF_FEATURE, CONF_FEATURE_LIST, CONF_LINKED_BATTERY_SENSOR, + FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, + HOMEKIT_NOTIFY_ID, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER, + TYPE_SWITCH, TYPE_VALVE) _LOGGER = logging.getLogger(__name__) BASIC_INFO_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_LINKED_BATTERY_SENSOR): cv.entity_domain(sensor.DOMAIN), }) FEATURE_SCHEMA = BASIC_INFO_SCHEMA.extend({ vol.Optional(CONF_FEATURE_LIST, default=None): cv.ensure_list, }) - CODE_SCHEMA = BASIC_INFO_SCHEMA.extend({ vol.Optional(ATTR_CODE, default=None): vol.Any(None, cv.string), }) diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 6f3957827eb..c9798f6302a 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -13,7 +13,7 @@ from homeassistant.components.homekit.const import ( ATTR_DISPLAY_NAME, ATTR_VALUE, BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, CHAR_FIRMWARE_REVISION, CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER, - MANUFACTURER, SERV_ACCESSORY_INFO) + CONF_LINKED_BATTERY_SENSOR, MANUFACTURER, SERV_ACCESSORY_INFO) from homeassistant.const import ( __version__, ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, ATTR_SERVICE, ATTR_NOW, EVENT_TIME_CHANGED) @@ -156,6 +156,61 @@ async def test_battery_service(hass, hk_driver, caplog): assert acc._char_charging.value == 0 +async def test_linked_battery_sensor(hass, hk_driver, caplog): + """Test battery service with linked_battery_sensor.""" + entity_id = 'homekit.accessory' + linked_battery = 'sensor.battery' + hass.states.async_set(entity_id, 'open', {ATTR_BATTERY_LEVEL: 100}) + hass.states.async_set(linked_battery, 50, None) + await hass.async_block_till_done() + + acc = HomeAccessory(hass, hk_driver, 'Battery Service', entity_id, 2, + {CONF_LINKED_BATTERY_SENSOR: linked_battery}) + acc.update_state = lambda x: None + assert acc.linked_battery_sensor == linked_battery + + 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(linked_battery, 10, None) + await hass.async_block_till_done() + assert acc._char_battery.value == 10 + assert acc._char_low_battery.value == 1 + + # Ignore battery change on entity if it has linked_battery + hass.states.async_set(entity_id, 'open', {ATTR_BATTERY_LEVEL: 90}) + await hass.async_block_till_done() + assert acc._char_battery.value == 10 + + # Test none numeric state for linked_battery + hass.states.async_set(linked_battery, 'error', None) + await hass.async_block_till_done() + assert acc._char_battery.value == 10 + assert 'ERROR' not in caplog.text + + # Test charging + hass.states.async_set(linked_battery, 20, {ATTR_BATTERY_CHARGING: True}) + await hass.async_block_till_done() + + acc = HomeAccessory(hass, hk_driver, 'Battery Service', entity_id, 2, + {CONF_LINKED_BATTERY_SENSOR: linked_battery}) + acc.update_state = lambda x: None + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc._char_battery.value == 20 + assert acc._char_low_battery.value == 0 + assert acc._char_charging.value == 1 + + hass.states.async_set(linked_battery, 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 + + async def test_call_service(hass, hk_driver, events): """Test call_service method.""" entity_id = 'homekit.accessory' diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index c86b1353c48..635b35cad51 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -3,9 +3,9 @@ import pytest import voluptuous as vol from homeassistant.components.homekit.const import ( - CONF_FEATURE, CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, - HOMEKIT_NOTIFY_ID, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER, - TYPE_SWITCH, TYPE_VALVE) + CONF_FEATURE, CONF_FEATURE_LIST, CONF_LINKED_BATTERY_SENSOR, + FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, HOMEKIT_NOTIFY_ID, TYPE_FAUCET, + TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER, TYPE_SWITCH, TYPE_VALVE) from homeassistant.components.homekit.util import ( HomeKitSpeedMapping, SpeedRange, convert_to_float, density_to_air_quality, dismiss_setup_message, show_setup_message, temperature_to_homekit, @@ -25,6 +25,9 @@ def test_validate_entity_config(): """Test validate entities.""" configs = [None, [], 'string', 12345, {'invalid_entity_id': {}}, {'demo.test': 1}, + {'binary_sensor.demo': {CONF_LINKED_BATTERY_SENSOR: None}}, + {'binary_sensor.demo': {CONF_LINKED_BATTERY_SENSOR: + 'switch.demo'}}, {'demo.test': 'test'}, {'demo.test': [1, 2]}, {'demo.test': None}, {'demo.test': {CONF_NAME: None}}, {'media_player.test': {CONF_FEATURE_LIST: [ @@ -42,6 +45,11 @@ def test_validate_entity_config(): assert vec({'demo.test': {CONF_NAME: 'Name'}}) == \ {'demo.test': {CONF_NAME: 'Name'}} + assert vec({'binary_sensor.demo': {CONF_LINKED_BATTERY_SENSOR: + 'sensor.demo_battery'}}) == \ + {'binary_sensor.demo': {CONF_LINKED_BATTERY_SENSOR: + 'sensor.demo_battery'}} + assert vec({'alarm_control_panel.demo': {}}) == \ {'alarm_control_panel.demo': {ATTR_CODE: None}} assert vec({'alarm_control_panel.demo': {ATTR_CODE: '1234'}}) == \