Add HomeKit Reset Accessory (#25158)

* added the ability to reset homekit accessories

* added tests for homekit reset accessory

* minor fixes
This commit is contained in:
Austin Drummond 2019-07-15 21:43:37 -04:00 committed by Martin Hjelmare
parent cca50a8339
commit c2f4f06005
3 changed files with 101 additions and 9 deletions

View File

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

View File

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

View File

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