Add the ability to reload homekit from yaml (#39326)
This commit is contained in:
parent
400741006b
commit
414a59ae9f
7 changed files with 182 additions and 18 deletions
|
@ -30,12 +30,14 @@ from homeassistant.const import (
|
|||
DEVICE_CLASS_HUMIDITY,
|
||||
EVENT_HOMEASSISTANT_STARTED,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
SERVICE_RELOAD,
|
||||
)
|
||||
from homeassistant.core import CoreState, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, Unauthorized
|
||||
from homeassistant.helpers import device_registry, entity_registry
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
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.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}
|
||||
|
||||
for index, conf in enumerate(config[DOMAIN]):
|
||||
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)
|
||||
if _async_update_config_entry_if_from_yaml(hass, entries_by_name, conf):
|
||||
continue
|
||||
|
||||
conf[CONF_ENTRY_INDEX] = index
|
||||
|
@ -181,6 +167,36 @@ async def async_setup(hass: HomeAssistant, config: dict):
|
|||
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):
|
||||
"""Set up HomeKit from a config 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
|
||||
)
|
||||
|
||||
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 to handle all actions between HomeKit and Home Assistant."""
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
start:
|
||||
description: Starts the HomeKit driver.
|
||||
|
||||
reload:
|
||||
description: Reload homekit and re-process yaml configuration.
|
||||
|
||||
reset_accessory:
|
||||
description: Reset a HomeKit accessory. This can be useful when changing a media_player’s device class to tv, linking a battery, or whenever Home Assistant adds support for new HomeKit features to existing entities.
|
||||
fields:
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Iterable, Optional
|
||||
from typing import Any, Dict, Iterable, Optional
|
||||
|
||||
from homeassistant import config as conf_util
|
||||
from homeassistant.const import SERVICE_RELOAD
|
||||
|
@ -59,6 +59,17 @@ async def async_reload_integration_platforms(
|
|||
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
|
||||
def async_get_platform(
|
||||
hass: HomeAssistantType, integration_name: str, integration_platform_name: str
|
||||
|
|
|
@ -4,6 +4,7 @@ from typing import Dict
|
|||
|
||||
import pytest
|
||||
|
||||
from homeassistant import config as hass_config
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASS_BATTERY_CHARGING,
|
||||
|
@ -49,6 +50,7 @@ from homeassistant.const import (
|
|||
DEVICE_CLASS_HUMIDITY,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
SERVICE_RELOAD,
|
||||
STATE_ON,
|
||||
UNIT_PERCENTAGE,
|
||||
)
|
||||
|
@ -1181,3 +1183,69 @@ async def test_homekit_finds_linked_humidity_sensors(
|
|||
"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__)))
|
||||
|
|
4
tests/fixtures/helpers/test_domain_configuration.yaml
vendored
Normal file
4
tests/fixtures/helpers/test_domain_configuration.yaml
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
test_domain:
|
||||
- name: one
|
||||
- name: two
|
||||
|
3
tests/fixtures/homekit/configuration.yaml
vendored
Normal file
3
tests/fixtures/homekit/configuration.yaml
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
homekit:
|
||||
- name: reloadable
|
||||
port: 45678
|
|
@ -2,11 +2,14 @@
|
|||
import logging
|
||||
from os import path
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant import config
|
||||
from homeassistant.const import SERVICE_RELOAD
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.reload import (
|
||||
async_get_platform,
|
||||
async_integration_yaml_config,
|
||||
async_reload_integration_platforms,
|
||||
async_setup_reload_service,
|
||||
)
|
||||
|
@ -106,5 +109,35 @@ async def test_setup_reload_service(hass):
|
|||
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():
|
||||
return path.dirname(path.dirname(__file__))
|
||||
|
|
Loading…
Add table
Reference in a new issue