mirror of
https://github.com/home-assistant/core.git
synced 2025-07-08 13:57:10 +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."""
|
"""The template component."""
|
||||||
import logging
|
from __future__ import annotations
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
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.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
from homeassistant.const import EVENT_HOMEASSISTANT_START, SERVICE_RELOAD
|
||||||
from homeassistant.core import CoreState, callback
|
from homeassistant.core import CoreState, Event, callback
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import (
|
||||||
discovery,
|
discovery,
|
||||||
trigger as trigger_helper,
|
trigger as trigger_helper,
|
||||||
update_coordinator,
|
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
|
from .const import CONF_TRIGGER, DOMAIN, PLATFORMS
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Set up the template integration."""
|
"""Set up the template integration."""
|
||||||
if DOMAIN in config:
|
if DOMAIN in config:
|
||||||
for conf in config[DOMAIN]:
|
await _process_config(hass, config)
|
||||||
coordinator = TriggerUpdateCoordinator(hass, conf)
|
|
||||||
await coordinator.async_setup(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
|
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 TriggerUpdateCoordinator(update_coordinator.DataUpdateCoordinator):
|
||||||
"""Class to handle incoming data."""
|
"""Class to handle incoming data."""
|
||||||
|
|
||||||
|
REMOVE_TRIGGER = object()
|
||||||
|
|
||||||
def __init__(self, hass, config):
|
def __init__(self, hass, config):
|
||||||
"""Instantiate trigger data."""
|
"""Instantiate trigger data."""
|
||||||
super().__init__(
|
super().__init__(hass, _LOGGER, name="Trigger Update Coordinator")
|
||||||
hass, logging.getLogger(__name__), name="Trigger Update Coordinator"
|
|
||||||
)
|
|
||||||
self.config = config
|
self.config = config
|
||||||
self._unsub_trigger = None
|
self._unsub_start: Callable[[], None] | None = None
|
||||||
|
self._unsub_trigger: Callable[[], None] | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self) -> Optional[str]:
|
def unique_id(self) -> str | None:
|
||||||
"""Return unique ID for the entity."""
|
"""Return unique ID for the entity."""
|
||||||
return self.config.get("unique_id")
|
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):
|
async def async_setup(self, hass_config):
|
||||||
"""Set up the trigger and create entities."""
|
"""Set up the trigger and create entities."""
|
||||||
if self.hass.state == CoreState.running:
|
if self.hass.state == CoreState.running:
|
||||||
await self._attach_triggers()
|
await self._attach_triggers()
|
||||||
else:
|
else:
|
||||||
self.hass.bus.async_listen_once(
|
self._unsub_start = self.hass.bus.async_listen_once(
|
||||||
EVENT_HOMEASSISTANT_START, self._attach_triggers
|
EVENT_HOMEASSISTANT_START, self._attach_triggers
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -65,6 +123,9 @@ class TriggerUpdateCoordinator(update_coordinator.DataUpdateCoordinator):
|
|||||||
|
|
||||||
async def _attach_triggers(self, start_event=None) -> None:
|
async def _attach_triggers(self, start_event=None) -> None:
|
||||||
"""Attach the triggers."""
|
"""Attach the triggers."""
|
||||||
|
if start_event is not None:
|
||||||
|
self._unsub_start = None
|
||||||
|
|
||||||
self._unsub_trigger = await trigger_helper.async_initialize_triggers(
|
self._unsub_trigger = await trigger_helper.async_initialize_triggers(
|
||||||
self.hass,
|
self.hass,
|
||||||
self.config[CONF_TRIGGER],
|
self.config[CONF_TRIGGER],
|
||||||
|
@ -36,7 +36,7 @@ from .const import (
|
|||||||
)
|
)
|
||||||
from .sensor import SENSOR_SCHEMA as PLATFORM_SENSOR_SCHEMA
|
from .sensor import SENSOR_SCHEMA as PLATFORM_SENSOR_SCHEMA
|
||||||
|
|
||||||
CONVERSION_PLATFORM = {
|
LEGACY_SENSOR = {
|
||||||
CONF_ICON_TEMPLATE: CONF_ICON,
|
CONF_ICON_TEMPLATE: CONF_ICON,
|
||||||
CONF_ENTITY_PICTURE_TEMPLATE: CONF_PICTURE,
|
CONF_ENTITY_PICTURE_TEMPLATE: CONF_PICTURE,
|
||||||
CONF_AVAILABILITY_TEMPLATE: CONF_AVAILABILITY,
|
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.Optional(CONF_UNIQUE_ID): cv.string,
|
||||||
vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA,
|
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):
|
async def async_validate_config(hass, config):
|
||||||
"""Validate config."""
|
"""Validate config."""
|
||||||
if DOMAIN not in config:
|
if DOMAIN not in config:
|
||||||
return config
|
return config
|
||||||
|
|
||||||
trigger_entity_configs = []
|
config_sections = []
|
||||||
|
|
||||||
for cfg in cv.ensure_list(config[DOMAIN]):
|
for cfg in cv.ensure_list(config[DOMAIN]):
|
||||||
try:
|
try:
|
||||||
cfg = TRIGGER_ENTITY_SCHEMA(cfg)
|
cfg = CONFIG_SECTION_SCHEMA(cfg)
|
||||||
cfg[CONF_TRIGGER] = await async_validate_trigger_config(
|
cfg[CONF_TRIGGER] = await async_validate_trigger_config(
|
||||||
hass, cfg[CONF_TRIGGER]
|
hass, cfg[CONF_TRIGGER]
|
||||||
)
|
)
|
||||||
@ -88,39 +115,14 @@ async def async_validate_config(hass, config):
|
|||||||
async_log_exception(err, DOMAIN, cfg, hass)
|
async_log_exception(err, DOMAIN, cfg, hass)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if CONF_SENSORS not in cfg:
|
if CONF_TRIGGER in cfg and CONF_SENSORS in cfg:
|
||||||
trigger_entity_configs.append(cfg)
|
cfg = _rewrite_legacy_to_modern_trigger_conf(cfg)
|
||||||
continue
|
|
||||||
|
|
||||||
logging.getLogger(__name__).warning(
|
config_sections.append(cfg)
|
||||||
"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)
|
|
||||||
|
|
||||||
# Create a copy of the configuration with all config for current
|
# Create a copy of the configuration with all config for current
|
||||||
# component removed and add validated config back in.
|
# component removed and add validated config back in.
|
||||||
config = config_without_domain(config, DOMAIN)
|
config = config_without_domain(config, DOMAIN)
|
||||||
config[DOMAIN] = trigger_entity_configs
|
config[DOMAIN] = config_sections
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
@ -27,7 +27,14 @@ async def test_reloadable(hass):
|
|||||||
"value_template": "{{ states.sensor.test_sensor.state }}"
|
"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()
|
await hass.async_block_till_done()
|
||||||
@ -35,8 +42,12 @@ async def test_reloadable(hass):
|
|||||||
await hass.async_start()
|
await hass.async_start()
|
||||||
await hass.async_block_till_done()
|
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 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(
|
yaml_path = path.join(
|
||||||
_get_fixtures_base_path(),
|
_get_fixtures_base_path(),
|
||||||
@ -52,11 +63,16 @@ async def test_reloadable(hass):
|
|||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
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.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 hass.states.get("sensor.watching_tv_in_master_bedroom").state == "off"
|
||||||
assert float(hass.states.get("sensor.combined_sensor_energy_usage").state) == 0
|
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):
|
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 }}"
|
"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()
|
await hass.async_block_till_done()
|
||||||
@ -82,8 +105,12 @@ async def test_reloadable_can_remove(hass):
|
|||||||
await hass.async_start()
|
await hass.async_start()
|
||||||
await hass.async_block_till_done()
|
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 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(
|
yaml_path = path.join(
|
||||||
_get_fixtures_base_path(),
|
_get_fixtures_base_path(),
|
||||||
@ -251,11 +278,12 @@ async def test_reloadable_multiple_platforms(hass):
|
|||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
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.state") is None
|
||||||
assert hass.states.get("sensor.watching_tv_in_master_bedroom").state == "off"
|
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 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):
|
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 TV" or state_attr("remote.alexander_master_bedroom","current_activity")
|
||||||
== "Watch Apple TV" %}on{% else %}off{% endif %}'
|
== "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