mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
Allow reloading top-level template entities (#48733)
This commit is contained in:
parent
c4f9489d61
commit
09635678bc
@ -1,54 +1,112 @@
|
||||
"""The template component."""
|
||||
import logging
|
||||
from typing import Optional
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Callable
|
||||
|
||||
from homeassistant import config as conf_util
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.core import CoreState, callback
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START, SERVICE_RELOAD
|
||||
from homeassistant.core import CoreState, Event, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
discovery,
|
||||
trigger as trigger_helper,
|
||||
update_coordinator,
|
||||
)
|
||||
from homeassistant.helpers.reload import async_setup_reload_service
|
||||
from homeassistant.helpers.reload import async_reload_integration_platforms
|
||||
from homeassistant.loader import async_get_integration
|
||||
|
||||
from .const import CONF_TRIGGER, DOMAIN, PLATFORMS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the template integration."""
|
||||
if DOMAIN in config:
|
||||
for conf in config[DOMAIN]:
|
||||
coordinator = TriggerUpdateCoordinator(hass, conf)
|
||||
await coordinator.async_setup(config)
|
||||
await _process_config(hass, config)
|
||||
|
||||
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
|
||||
async def _reload_config(call: Event) -> None:
|
||||
"""Reload top-level + platforms."""
|
||||
try:
|
||||
unprocessed_conf = await conf_util.async_hass_config_yaml(hass)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error(err)
|
||||
return
|
||||
|
||||
conf = await conf_util.async_process_component_config(
|
||||
hass, unprocessed_conf, await async_get_integration(hass, DOMAIN)
|
||||
)
|
||||
|
||||
if conf is None:
|
||||
return
|
||||
|
||||
await async_reload_integration_platforms(hass, DOMAIN, PLATFORMS)
|
||||
|
||||
if DOMAIN in conf:
|
||||
await _process_config(hass, conf)
|
||||
|
||||
hass.bus.async_fire(f"event_{DOMAIN}_reloaded", context=call.context)
|
||||
|
||||
hass.helpers.service.async_register_admin_service(
|
||||
DOMAIN, SERVICE_RELOAD, _reload_config
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _process_config(hass, config):
|
||||
"""Process config."""
|
||||
coordinators: list[TriggerUpdateCoordinator] | None = hass.data.get(DOMAIN)
|
||||
|
||||
# Remove old ones
|
||||
if coordinators:
|
||||
for coordinator in coordinators:
|
||||
coordinator.async_remove()
|
||||
|
||||
async def init_coordinator(hass, conf):
|
||||
coordinator = TriggerUpdateCoordinator(hass, conf)
|
||||
await coordinator.async_setup(conf)
|
||||
return coordinator
|
||||
|
||||
hass.data[DOMAIN] = await asyncio.gather(
|
||||
*[init_coordinator(hass, conf) for conf in config[DOMAIN]]
|
||||
)
|
||||
|
||||
|
||||
class TriggerUpdateCoordinator(update_coordinator.DataUpdateCoordinator):
|
||||
"""Class to handle incoming data."""
|
||||
|
||||
REMOVE_TRIGGER = object()
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Instantiate trigger data."""
|
||||
super().__init__(
|
||||
hass, logging.getLogger(__name__), name="Trigger Update Coordinator"
|
||||
)
|
||||
super().__init__(hass, _LOGGER, name="Trigger Update Coordinator")
|
||||
self.config = config
|
||||
self._unsub_trigger = None
|
||||
self._unsub_start: Callable[[], None] | None = None
|
||||
self._unsub_trigger: Callable[[], None] | None = None
|
||||
|
||||
@property
|
||||
def unique_id(self) -> Optional[str]:
|
||||
def unique_id(self) -> str | None:
|
||||
"""Return unique ID for the entity."""
|
||||
return self.config.get("unique_id")
|
||||
|
||||
@callback
|
||||
def async_remove(self):
|
||||
"""Signal that the entities need to remove themselves."""
|
||||
if self._unsub_start:
|
||||
self._unsub_start()
|
||||
if self._unsub_trigger:
|
||||
self._unsub_trigger()
|
||||
|
||||
async def async_setup(self, hass_config):
|
||||
"""Set up the trigger and create entities."""
|
||||
if self.hass.state == CoreState.running:
|
||||
await self._attach_triggers()
|
||||
else:
|
||||
self.hass.bus.async_listen_once(
|
||||
self._unsub_start = self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, self._attach_triggers
|
||||
)
|
||||
|
||||
@ -65,6 +123,9 @@ class TriggerUpdateCoordinator(update_coordinator.DataUpdateCoordinator):
|
||||
|
||||
async def _attach_triggers(self, start_event=None) -> None:
|
||||
"""Attach the triggers."""
|
||||
if start_event is not None:
|
||||
self._unsub_start = None
|
||||
|
||||
self._unsub_trigger = await trigger_helper.async_initialize_triggers(
|
||||
self.hass,
|
||||
self.config[CONF_TRIGGER],
|
||||
|
@ -36,7 +36,7 @@ from .const import (
|
||||
)
|
||||
from .sensor import SENSOR_SCHEMA as PLATFORM_SENSOR_SCHEMA
|
||||
|
||||
CONVERSION_PLATFORM = {
|
||||
LEGACY_SENSOR = {
|
||||
CONF_ICON_TEMPLATE: CONF_ICON,
|
||||
CONF_ENTITY_PICTURE_TEMPLATE: CONF_PICTURE,
|
||||
CONF_AVAILABILITY_TEMPLATE: CONF_AVAILABILITY,
|
||||
@ -61,7 +61,7 @@ SENSOR_SCHEMA = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
TRIGGER_ENTITY_SCHEMA = vol.Schema(
|
||||
CONFIG_SECTION_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA,
|
||||
@ -71,16 +71,43 @@ TRIGGER_ENTITY_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
def _rewrite_legacy_to_modern_trigger_conf(cfg: dict):
|
||||
"""Rewrite a legacy to a modern trigger-basd conf."""
|
||||
logging.getLogger(__name__).warning(
|
||||
"The entity definition format under template: differs from the platform configuration format. See https://www.home-assistant.io/integrations/template#configuration-for-trigger-based-template-sensors"
|
||||
)
|
||||
sensor = list(cfg[SENSOR_DOMAIN]) if SENSOR_DOMAIN in cfg else []
|
||||
|
||||
for device_id, entity_cfg in cfg[CONF_SENSORS].items():
|
||||
entity_cfg = {**entity_cfg}
|
||||
|
||||
for from_key, to_key in LEGACY_SENSOR.items():
|
||||
if from_key not in entity_cfg or to_key in entity_cfg:
|
||||
continue
|
||||
|
||||
val = entity_cfg.pop(from_key)
|
||||
if isinstance(val, str):
|
||||
val = template.Template(val)
|
||||
entity_cfg[to_key] = val
|
||||
|
||||
if CONF_NAME not in entity_cfg:
|
||||
entity_cfg[CONF_NAME] = template.Template(device_id)
|
||||
|
||||
sensor.append(entity_cfg)
|
||||
|
||||
return {**cfg, "sensor": sensor}
|
||||
|
||||
|
||||
async def async_validate_config(hass, config):
|
||||
"""Validate config."""
|
||||
if DOMAIN not in config:
|
||||
return config
|
||||
|
||||
trigger_entity_configs = []
|
||||
config_sections = []
|
||||
|
||||
for cfg in cv.ensure_list(config[DOMAIN]):
|
||||
try:
|
||||
cfg = TRIGGER_ENTITY_SCHEMA(cfg)
|
||||
cfg = CONFIG_SECTION_SCHEMA(cfg)
|
||||
cfg[CONF_TRIGGER] = await async_validate_trigger_config(
|
||||
hass, cfg[CONF_TRIGGER]
|
||||
)
|
||||
@ -88,39 +115,14 @@ async def async_validate_config(hass, config):
|
||||
async_log_exception(err, DOMAIN, cfg, hass)
|
||||
continue
|
||||
|
||||
if CONF_SENSORS not in cfg:
|
||||
trigger_entity_configs.append(cfg)
|
||||
continue
|
||||
if CONF_TRIGGER in cfg and CONF_SENSORS in cfg:
|
||||
cfg = _rewrite_legacy_to_modern_trigger_conf(cfg)
|
||||
|
||||
logging.getLogger(__name__).warning(
|
||||
"The entity definition format under template: differs from the platform configuration format. See https://www.home-assistant.io/integrations/template#configuration-for-trigger-based-template-sensors"
|
||||
)
|
||||
sensor = list(cfg[SENSOR_DOMAIN]) if SENSOR_DOMAIN in cfg else []
|
||||
|
||||
for device_id, entity_cfg in cfg[CONF_SENSORS].items():
|
||||
entity_cfg = {**entity_cfg}
|
||||
|
||||
for from_key, to_key in CONVERSION_PLATFORM.items():
|
||||
if from_key not in entity_cfg or to_key in entity_cfg:
|
||||
continue
|
||||
|
||||
val = entity_cfg.pop(from_key)
|
||||
if isinstance(val, str):
|
||||
val = template.Template(val)
|
||||
entity_cfg[to_key] = val
|
||||
|
||||
if CONF_NAME not in entity_cfg:
|
||||
entity_cfg[CONF_NAME] = template.Template(device_id)
|
||||
|
||||
sensor.append(entity_cfg)
|
||||
|
||||
cfg = {**cfg, "sensor": sensor}
|
||||
|
||||
trigger_entity_configs.append(cfg)
|
||||
config_sections.append(cfg)
|
||||
|
||||
# Create a copy of the configuration with all config for current
|
||||
# component removed and add validated config back in.
|
||||
config = config_without_domain(config, DOMAIN)
|
||||
config[DOMAIN] = trigger_entity_configs
|
||||
config[DOMAIN] = config_sections
|
||||
|
||||
return config
|
||||
|
@ -27,7 +27,14 @@ async def test_reloadable(hass):
|
||||
"value_template": "{{ states.sensor.test_sensor.state }}"
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"trigger": {"platform": "event", "event_type": "event_1"},
|
||||
"sensor": {
|
||||
"name": "top level",
|
||||
"state": "{{ trigger.event.data.source }}",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
@ -35,8 +42,12 @@ async def test_reloadable(hass):
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
hass.bus.async_fire("event_1", {"source": "init"})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all()) == 3
|
||||
assert hass.states.get("sensor.state").state == "mytest"
|
||||
assert len(hass.states.async_all()) == 2
|
||||
assert hass.states.get("sensor.top_level").state == "init"
|
||||
|
||||
yaml_path = path.join(
|
||||
_get_fixtures_base_path(),
|
||||
@ -52,11 +63,16 @@ async def test_reloadable(hass):
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all()) == 3
|
||||
assert len(hass.states.async_all()) == 4
|
||||
|
||||
hass.bus.async_fire("event_2", {"source": "reload"})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("sensor.state") is None
|
||||
assert hass.states.get("sensor.top_level") is None
|
||||
assert hass.states.get("sensor.watching_tv_in_master_bedroom").state == "off"
|
||||
assert float(hass.states.get("sensor.combined_sensor_energy_usage").state) == 0
|
||||
assert hass.states.get("sensor.top_level_2").state == "reload"
|
||||
|
||||
|
||||
async def test_reloadable_can_remove(hass):
|
||||
@ -74,7 +90,14 @@ async def test_reloadable_can_remove(hass):
|
||||
"value_template": "{{ states.sensor.test_sensor.state }}"
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"trigger": {"platform": "event", "event_type": "event_1"},
|
||||
"sensor": {
|
||||
"name": "top level",
|
||||
"state": "{{ trigger.event.data.source }}",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
@ -82,8 +105,12 @@ async def test_reloadable_can_remove(hass):
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
hass.bus.async_fire("event_1", {"source": "init"})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all()) == 3
|
||||
assert hass.states.get("sensor.state").state == "mytest"
|
||||
assert len(hass.states.async_all()) == 2
|
||||
assert hass.states.get("sensor.top_level").state == "init"
|
||||
|
||||
yaml_path = path.join(
|
||||
_get_fixtures_base_path(),
|
||||
@ -251,11 +278,12 @@ async def test_reloadable_multiple_platforms(hass):
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all()) == 3
|
||||
assert len(hass.states.async_all()) == 4
|
||||
|
||||
assert hass.states.get("sensor.state") is None
|
||||
assert hass.states.get("sensor.watching_tv_in_master_bedroom").state == "off"
|
||||
assert float(hass.states.get("sensor.combined_sensor_energy_usage").state) == 0
|
||||
assert hass.states.get("sensor.top_level_2") is not None
|
||||
|
||||
|
||||
async def test_reload_sensors_that_reference_other_template_sensors(hass):
|
||||
|
@ -21,3 +21,10 @@ sensor:
|
||||
== "Watch TV" or state_attr("remote.alexander_master_bedroom","current_activity")
|
||||
== "Watch Apple TV" %}on{% else %}off{% endif %}'
|
||||
|
||||
template:
|
||||
trigger:
|
||||
platform: event
|
||||
event_type: event_2
|
||||
sensor:
|
||||
name: top level 2
|
||||
state: "{{ trigger.event.data.source }}"
|
||||
|
Loading…
x
Reference in New Issue
Block a user