Add the ability to reload homekit from yaml (#39326)

This commit is contained in:
J. Nick Koston 2020-08-28 09:46:45 -05:00 committed by GitHub
parent 400741006b
commit 414a59ae9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 182 additions and 18 deletions

View file

@ -30,12 +30,14 @@ from homeassistant.const import (
DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_HUMIDITY,
EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STARTED,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
SERVICE_RELOAD,
) )
from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.core import CoreState, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady, Unauthorized from homeassistant.exceptions import ConfigEntryNotReady, Unauthorized
from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers import device_registry, entity_registry
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import BASE_FILTER_SCHEMA, FILTER_SCHEMA from homeassistant.helpers.entityfilter import BASE_FILTER_SCHEMA, FILTER_SCHEMA
from homeassistant.helpers.reload import async_integration_yaml_config
from homeassistant.loader import async_get_integration from homeassistant.loader import async_get_integration
from homeassistant.util import get_local_ip from homeassistant.util import get_local_ip
@ -150,23 +152,7 @@ async def async_setup(hass: HomeAssistant, config: dict):
entries_by_name = {entry.data[CONF_NAME]: entry for entry in current_entries} entries_by_name = {entry.data[CONF_NAME]: entry for entry in current_entries}
for index, conf in enumerate(config[DOMAIN]): for index, conf in enumerate(config[DOMAIN]):
bridge_name = conf[CONF_NAME] if _async_update_config_entry_if_from_yaml(hass, entries_by_name, conf):
if (
bridge_name in entries_by_name
and entries_by_name[bridge_name].source == SOURCE_IMPORT
):
entry = entries_by_name[bridge_name]
# If they alter the yaml config we import the changes
# since there currently is no practical way to support
# all the options in the UI at this time.
data = conf.copy()
options = {}
for key in CONFIG_OPTIONS:
options[key] = data[key]
del data[key]
hass.config_entries.async_update_entry(entry, data=data, options=options)
continue continue
conf[CONF_ENTRY_INDEX] = index conf[CONF_ENTRY_INDEX] = index
@ -181,6 +167,36 @@ async def async_setup(hass: HomeAssistant, config: dict):
return True return True
@callback
def _async_update_config_entry_if_from_yaml(hass, entries_by_name, conf):
"""Update a config entry with the latest yaml.
Returns True if a matching config entry was found
Returns False if there is no matching config entry
"""
bridge_name = conf[CONF_NAME]
if (
bridge_name in entries_by_name
and entries_by_name[bridge_name].source == SOURCE_IMPORT
):
entry = entries_by_name[bridge_name]
# If they alter the yaml config we import the changes
# since there currently is no practical way to support
# all the options in the UI at this time.
data = conf.copy()
options = {}
for key in CONFIG_OPTIONS:
options[key] = data[key]
del data[key]
hass.config_entries.async_update_entry(entry, data=data, options=options)
return True
return False
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up HomeKit from a config entry.""" """Set up HomeKit from a config entry."""
_async_import_options_from_data_if_missing(hass, entry) _async_import_options_from_data_if_missing(hass, entry)
@ -349,6 +365,32 @@ def _async_register_events_and_services(hass: HomeAssistant):
DOMAIN, SERVICE_HOMEKIT_START, async_handle_homekit_service_start DOMAIN, SERVICE_HOMEKIT_START, async_handle_homekit_service_start
) )
async def _handle_homekit_reload(service):
"""Handle start HomeKit service call."""
config = await async_integration_yaml_config(hass, DOMAIN)
if not config or DOMAIN not in config:
return
current_entries = hass.config_entries.async_entries(DOMAIN)
entries_by_name = {entry.data[CONF_NAME]: entry for entry in current_entries}
for conf in config[DOMAIN]:
_async_update_config_entry_if_from_yaml(hass, entries_by_name, conf)
reload_tasks = [
hass.config_entries.async_reload(entry.entry_id)
for entry in current_entries
]
await asyncio.gather(*reload_tasks)
hass.helpers.service.async_register_admin_service(
DOMAIN,
SERVICE_RELOAD,
_handle_homekit_reload,
)
class HomeKit: class HomeKit:
"""Class to handle all actions between HomeKit and Home Assistant.""" """Class to handle all actions between HomeKit and Home Assistant."""

View file

@ -3,6 +3,9 @@
start: start:
description: Starts the HomeKit driver. description: Starts the HomeKit driver.
reload:
description: Reload homekit and re-process yaml configuration.
reset_accessory: reset_accessory:
description: Reset a HomeKit accessory. This can be useful when changing a media_players device class to tv, linking a battery, or whenever Home Assistant adds support for new HomeKit features to existing entities. description: Reset a HomeKit accessory. This can be useful when changing a media_players device class to tv, linking a battery, or whenever Home Assistant adds support for new HomeKit features to existing entities.
fields: fields:

View file

@ -2,7 +2,7 @@
import asyncio import asyncio
import logging import logging
from typing import Iterable, Optional from typing import Any, Dict, Iterable, Optional
from homeassistant import config as conf_util from homeassistant import config as conf_util
from homeassistant.const import SERVICE_RELOAD from homeassistant.const import SERVICE_RELOAD
@ -59,6 +59,17 @@ async def async_reload_integration_platforms(
await platform.async_setup(p_config) # type: ignore await platform.async_setup(p_config) # type: ignore
async def async_integration_yaml_config(
hass: HomeAssistantType, integration_name: str
) -> Optional[Dict[Any, Any]]:
"""Fetch the latest yaml configuration for an integration."""
integration = await async_get_integration(hass, integration_name)
return await conf_util.async_process_component_config(
hass, await conf_util.async_hass_config_yaml(hass), integration
)
@callback @callback
def async_get_platform( def async_get_platform(
hass: HomeAssistantType, integration_name: str, integration_platform_name: str hass: HomeAssistantType, integration_name: str, integration_platform_name: str

View file

@ -4,6 +4,7 @@ from typing import Dict
import pytest import pytest
from homeassistant import config as hass_config
from homeassistant.components import zeroconf from homeassistant.components import zeroconf
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
DEVICE_CLASS_BATTERY_CHARGING, DEVICE_CLASS_BATTERY_CHARGING,
@ -49,6 +50,7 @@ from homeassistant.const import (
DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_HUMIDITY,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
SERVICE_RELOAD,
STATE_ON, STATE_ON,
UNIT_PERCENTAGE, UNIT_PERCENTAGE,
) )
@ -1181,3 +1183,69 @@ async def test_homekit_finds_linked_humidity_sensors(
"linked_humidity_sensor": "sensor.humidifier_humidity_sensor", "linked_humidity_sensor": "sensor.humidifier_humidity_sensor",
}, },
) )
async def test_reload(hass):
"""Test we can reload from yaml."""
entry = MockConfigEntry(
domain=DOMAIN,
source=SOURCE_IMPORT,
data={CONF_NAME: "reloadable", CONF_PORT: 12345},
options={},
)
entry.add_to_hass(hass)
with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit:
mock_homekit.return_value = homekit = Mock()
type(homekit).async_start = AsyncMock()
assert await async_setup_component(
hass, "homekit", {"homekit": {CONF_NAME: "reloadable", CONF_PORT: 12345}}
)
await hass.async_block_till_done()
mock_homekit.assert_any_call(
hass,
"reloadable",
12345,
None,
ANY,
{},
DEFAULT_SAFE_MODE,
None,
entry.entry_id,
)
assert mock_homekit().setup.called is True
yaml_path = os.path.join(
_get_fixtures_base_path(),
"fixtures",
"homekit/configuration.yaml",
)
with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path), patch(
f"{PATH_HOMEKIT}.HomeKit"
) as mock_homekit2:
mock_homekit2.return_value = homekit = Mock()
type(homekit).async_start = AsyncMock()
await hass.services.async_call(
"homekit",
SERVICE_RELOAD,
{},
blocking=True,
)
await hass.async_block_till_done()
mock_homekit2.assert_any_call(
hass,
"reloadable",
45678,
None,
ANY,
{},
DEFAULT_SAFE_MODE,
None,
entry.entry_id,
)
assert mock_homekit2().setup.called is True
def _get_fixtures_base_path():
return os.path.dirname(os.path.dirname(os.path.dirname(__file__)))

View file

@ -0,0 +1,4 @@
test_domain:
- name: one
- name: two

View file

@ -0,0 +1,3 @@
homekit:
- name: reloadable
port: 45678

View file

@ -2,11 +2,14 @@
import logging import logging
from os import path from os import path
import pytest
from homeassistant import config from homeassistant import config
from homeassistant.const import SERVICE_RELOAD from homeassistant.const import SERVICE_RELOAD
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.reload import ( from homeassistant.helpers.reload import (
async_get_platform, async_get_platform,
async_integration_yaml_config,
async_reload_integration_platforms, async_reload_integration_platforms,
async_setup_reload_service, async_setup_reload_service,
) )
@ -106,5 +109,35 @@ async def test_setup_reload_service(hass):
assert len(setup_called) == 2 assert len(setup_called) == 2
async def test_async_integration_yaml_config(hass):
"""Test loading yaml config for an integration."""
mock_integration(hass, MockModule(DOMAIN))
yaml_path = path.join(
_get_fixtures_base_path(),
"fixtures",
f"helpers/{DOMAIN}_configuration.yaml",
)
with patch.object(config, "YAML_CONFIG_FILE", yaml_path):
processed_config = await async_integration_yaml_config(hass, DOMAIN)
assert processed_config == {DOMAIN: [{"name": "one"}, {"name": "two"}]}
async def test_async_integration_missing_yaml_config(hass):
"""Test loading missing yaml config for an integration."""
mock_integration(hass, MockModule(DOMAIN))
yaml_path = path.join(
_get_fixtures_base_path(),
"fixtures",
"helpers/does_not_exist_configuration.yaml",
)
with pytest.raises(FileNotFoundError), patch.object(
config, "YAML_CONFIG_FILE", yaml_path
):
await async_integration_yaml_config(hass, DOMAIN)
def _get_fixtures_base_path(): def _get_fixtures_base_path():
return path.dirname(path.dirname(__file__)) return path.dirname(path.dirname(__file__))