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,
|
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."""
|
||||||
|
|
|
@ -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_player’s 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_player’s device class to tv, linking a battery, or whenever Home Assistant adds support for new HomeKit features to existing entities.
|
||||||
fields:
|
fields:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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__)))
|
||||||
|
|
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
|
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__))
|
||||||
|
|
Loading…
Add table
Reference in a new issue