diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index a37b085c0dc..90fdde84f3b 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -8,11 +8,11 @@ import voluptuous as vol from homeassistant.components import cover from homeassistant.components.media_player import DEVICE_CLASS_TV from homeassistant.const import ( - ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, CONF_TYPE, DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, - TEMP_FAHRENHEIT) + ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + ATTR_UNIT_OF_MEASUREMENT, CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, + CONF_TYPE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_TEMPERATURE, EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.util import get_local_ip @@ -22,8 +22,10 @@ from .const import ( BRIDGE_NAME, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FEATURE_LIST, CONF_FILTER, CONF_SAFE_MODE, DEFAULT_AUTO_START, DEFAULT_PORT, DEFAULT_SAFE_MODE, DEVICE_CLASS_CO, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, - DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START, TYPE_FAUCET, TYPE_OUTLET, - TYPE_SHOWER, TYPE_SPRINKLER, TYPE_SWITCH, TYPE_VALVE) + DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START, + SERVICE_HOMEKIT_RESET_ACCESSORY, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, + TYPE_SPRINKLER, TYPE_SWITCH, TYPE_VALVE) + from .util import ( show_setup_message, validate_entity_config, validate_media_player_features) @@ -60,6 +62,10 @@ CONFIG_SCHEMA = vol.Schema({ }) }, extra=vol.ALLOW_EXTRA) +RESET_ACCESSORY_SERVICE_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids +}) + async def async_setup(hass, config): """Set up the HomeKit component.""" @@ -78,6 +84,21 @@ async def async_setup(hass, config): entity_config, safe_mode) await hass.async_add_executor_job(homekit.setup) + def handle_homekit_reset_accessory(service): + """Handle start HomeKit service call.""" + if homekit.status != STATUS_RUNNING: + _LOGGER.warning( + 'HomeKit is not running. Either it is waiting to be ' + 'started or has been stopped.') + return + + entity_ids = service.data.get('entity_id') + homekit.reset_accessories(entity_ids) + + hass.services.async_register(DOMAIN, SERVICE_HOMEKIT_RESET_ACCESSORY, + handle_homekit_reset_accessory, + schema=RESET_ACCESSORY_SERVICE_SCHEMA) + if auto_start: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, homekit.start) return True @@ -229,6 +250,23 @@ class HomeKit(): _LOGGER.debug('Safe_mode selected') self.driver.safe_mode = True + def reset_accessories(self, entity_ids): + """Reset the accessory to load the latest configuration.""" + removed = [] + for entity_id in entity_ids: + aid = generate_aid(entity_id) + if aid not in self.bridge.accessories: + _LOGGER.warning('Could not reset accessory. entity_id ' + 'not found %s', entity_id) + continue + acc = self.remove_bridge_accessory(aid) + removed.append(acc) + self.driver.config_changed() + + for acc in removed: + self.bridge.add_accessory(acc) + self.driver.config_changed() + def add_bridge_accessory(self, state): """Try adding accessory to bridge if configured beforehand.""" if not state or not self._filter(state.entity_id): @@ -239,6 +277,13 @@ class HomeKit(): if acc is not None: self.bridge.add_accessory(acc) + def remove_bridge_accessory(self, aid): + """Try adding accessory to bridge if configured beforehand.""" + acc = None + if aid in self.bridge.accessories: + acc = self.bridge.accessories.pop(aid) + return acc + def start(self, *args): """Start the accessory driver.""" if self.status != STATUS_READY: diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 11c0314abf2..ce0659ddc73 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -36,6 +36,7 @@ EVENT_HOMEKIT_CHANGED = 'homekit_state_change' # #### HomeKit Component Services #### SERVICE_HOMEKIT_START = 'start' +SERVICE_HOMEKIT_RESET_ACCESSORY = 'reset_accessory' # #### String Constants #### BRIDGE_MODEL = 'Bridge' diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 4dbb6351ee7..d3f97efc3d2 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -4,21 +4,24 @@ from unittest.mock import patch, ANY, Mock import pytest from homeassistant import setup + from homeassistant.components.homekit import ( generate_aid, HomeKit, MAX_DEVICES, STATUS_READY, STATUS_RUNNING, STATUS_STOPPED, STATUS_WAIT) from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import ( CONF_AUTO_START, CONF_SAFE_MODE, BRIDGE_NAME, DEFAULT_PORT, - DEFAULT_SAFE_MODE, DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START) + DEFAULT_SAFE_MODE, DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START, + SERVICE_HOMEKIT_RESET_ACCESSORY) from homeassistant.const import ( - CONF_NAME, CONF_IP_ADDRESS, CONF_PORT, + ATTR_ENTITY_ID, CONF_NAME, CONF_IP_ADDRESS, CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from homeassistant.core import State from homeassistant.helpers.entityfilter import generate_filter from tests.components.homekit.common import patch_debounce + IP_ADDRESS = '127.0.0.1' PATH_HOMEKIT = 'homeassistant.components.homekit' @@ -164,6 +167,19 @@ async def test_homekit_add_accessory(): mock_bridge.add_accessory.assert_called_with('acc') +async def test_homekit_remove_accessory(): + """Remove accessory from bridge.""" + homekit = HomeKit('hass', None, None, None, lambda entity_id: True, {}, + None) + homekit.driver = 'driver' + homekit.bridge = mock_bridge = Mock() + mock_bridge.accessories = {'light.demo': 'acc'} + + acc = homekit.remove_bridge_accessory('light.demo') + assert acc == 'acc' + assert len(mock_bridge.accessories) == 0 + + async def test_homekit_entity_filter(hass): """Test the entity filter.""" entity_filter = generate_filter(['cover'], ['demo.test'], [], []) @@ -235,6 +251,36 @@ async def test_homekit_stop(hass): assert homekit.driver.stop.called is True +async def test_homekit_reset_accessories(hass): + """Test adding too many accessories to HomeKit.""" + entity_id = 'light.demo' + homekit = HomeKit(hass, None, None, None, {}, {entity_id: {}}, None) + homekit.bridge = Mock() + + with patch(PATH_HOMEKIT + '.HomeKit', return_value=homekit), \ + patch(PATH_HOMEKIT + '.HomeKit.setup'), \ + patch('pyhap.accessory.Bridge.add_accessory') as \ + mock_add_accessory, \ + patch('pyhap.accessory_driver.AccessoryDriver.config_changed') as \ + hk_driver_config_changed: + + assert await setup.async_setup_component( + hass, DOMAIN, {DOMAIN: {}}) + + aid = generate_aid(entity_id) + homekit.bridge.accessories = {aid: 'acc'} + homekit.status = STATUS_RUNNING + + await hass.services.async_call( + DOMAIN, SERVICE_HOMEKIT_RESET_ACCESSORY, + {ATTR_ENTITY_ID: entity_id}, blocking=True) + await hass.async_block_till_done() + + assert 2 == hk_driver_config_changed.call_count + assert mock_add_accessory.called + homekit.status = STATUS_READY + + async def test_homekit_too_many_accessories(hass, hk_driver): """Test adding too many accessories to HomeKit.""" homekit = HomeKit(hass, None, None, None, None, None, None)