Manual trigger entity and refactor command_line switch (#91506)
* TriggerEntity to CoordinatorTriggerEntity * _render_templates * split manual vs coordinator * name * ManualTriggerEntity * value * Remove ManualTriggerEntity * ManualTriggerEntity * process_manual_data * Add test * imports * Move ManualTriggerEntity * cmd_line switch * Review comments * Fix templating * Review comments * Remove unneeded logging
This commit is contained in:
parent
2e65b77b2b
commit
6ad4e13b38
4 changed files with 147 additions and 23 deletions
|
@ -16,7 +16,9 @@ from homeassistant.const import (
|
||||||
CONF_COMMAND_ON,
|
CONF_COMMAND_ON,
|
||||||
CONF_COMMAND_STATE,
|
CONF_COMMAND_STATE,
|
||||||
CONF_FRIENDLY_NAME,
|
CONF_FRIENDLY_NAME,
|
||||||
|
CONF_ICON,
|
||||||
CONF_ICON_TEMPLATE,
|
CONF_ICON_TEMPLATE,
|
||||||
|
CONF_NAME,
|
||||||
CONF_SWITCHES,
|
CONF_SWITCHES,
|
||||||
CONF_UNIQUE_ID,
|
CONF_UNIQUE_ID,
|
||||||
CONF_VALUE_TEMPLATE,
|
CONF_VALUE_TEMPLATE,
|
||||||
|
@ -26,6 +28,7 @@ import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.reload import async_setup_reload_service
|
from homeassistant.helpers.reload import async_setup_reload_service
|
||||||
from homeassistant.helpers.template import Template
|
from homeassistant.helpers.template import Template
|
||||||
|
from homeassistant.helpers.template_entity import ManualTriggerEntity
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, PLATFORMS
|
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, PLATFORMS
|
||||||
|
@ -65,26 +68,26 @@ async def async_setup_platform(
|
||||||
switches = []
|
switches = []
|
||||||
|
|
||||||
for object_id, device_config in devices.items():
|
for object_id, device_config in devices.items():
|
||||||
|
trigger_entity_config = {
|
||||||
|
CONF_UNIQUE_ID: device_config.get(CONF_UNIQUE_ID),
|
||||||
|
CONF_NAME: Template(device_config.get(CONF_FRIENDLY_NAME, object_id), hass),
|
||||||
|
CONF_ICON: device_config.get(CONF_ICON_TEMPLATE),
|
||||||
|
}
|
||||||
|
|
||||||
value_template: Template | None = device_config.get(CONF_VALUE_TEMPLATE)
|
value_template: Template | None = device_config.get(CONF_VALUE_TEMPLATE)
|
||||||
|
|
||||||
if value_template is not None:
|
if value_template is not None:
|
||||||
value_template.hass = hass
|
value_template.hass = hass
|
||||||
|
|
||||||
icon_template: Template | None = device_config.get(CONF_ICON_TEMPLATE)
|
|
||||||
if icon_template is not None:
|
|
||||||
icon_template.hass = hass
|
|
||||||
|
|
||||||
switches.append(
|
switches.append(
|
||||||
CommandSwitch(
|
CommandSwitch(
|
||||||
|
trigger_entity_config,
|
||||||
object_id,
|
object_id,
|
||||||
device_config.get(CONF_FRIENDLY_NAME, object_id),
|
|
||||||
device_config[CONF_COMMAND_ON],
|
device_config[CONF_COMMAND_ON],
|
||||||
device_config[CONF_COMMAND_OFF],
|
device_config[CONF_COMMAND_OFF],
|
||||||
device_config.get(CONF_COMMAND_STATE),
|
device_config.get(CONF_COMMAND_STATE),
|
||||||
icon_template,
|
|
||||||
value_template,
|
value_template,
|
||||||
device_config[CONF_COMMAND_TIMEOUT],
|
device_config[CONF_COMMAND_TIMEOUT],
|
||||||
device_config.get(CONF_UNIQUE_ID),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -95,32 +98,28 @@ async def async_setup_platform(
|
||||||
async_add_entities(switches)
|
async_add_entities(switches)
|
||||||
|
|
||||||
|
|
||||||
class CommandSwitch(SwitchEntity):
|
class CommandSwitch(ManualTriggerEntity, SwitchEntity):
|
||||||
"""Representation a switch that can be toggled using shell commands."""
|
"""Representation a switch that can be toggled using shell commands."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
config: ConfigType,
|
||||||
object_id: str,
|
object_id: str,
|
||||||
friendly_name: str,
|
|
||||||
command_on: str,
|
command_on: str,
|
||||||
command_off: str,
|
command_off: str,
|
||||||
command_state: str | None,
|
command_state: str | None,
|
||||||
icon_template: Template | None,
|
|
||||||
value_template: Template | None,
|
value_template: Template | None,
|
||||||
timeout: int,
|
timeout: int,
|
||||||
unique_id: str | None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the switch."""
|
"""Initialize the switch."""
|
||||||
|
super().__init__(self.hass, config)
|
||||||
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
||||||
self._attr_name = friendly_name
|
|
||||||
self._attr_is_on = False
|
self._attr_is_on = False
|
||||||
self._command_on = command_on
|
self._command_on = command_on
|
||||||
self._command_off = command_off
|
self._command_off = command_off
|
||||||
self._command_state = command_state
|
self._command_state = command_state
|
||||||
self._icon_template = icon_template
|
|
||||||
self._value_template = value_template
|
self._value_template = value_template
|
||||||
self._timeout = timeout
|
self._timeout = timeout
|
||||||
self._attr_unique_id = unique_id
|
|
||||||
self._attr_should_poll = bool(command_state)
|
self._attr_should_poll = bool(command_state)
|
||||||
|
|
||||||
async def _switch(self, command: str) -> bool:
|
async def _switch(self, command: str) -> bool:
|
||||||
|
@ -169,17 +168,15 @@ class CommandSwitch(SwitchEntity):
|
||||||
"""Update device state."""
|
"""Update device state."""
|
||||||
if self._command_state:
|
if self._command_state:
|
||||||
payload = str(await self.hass.async_add_executor_job(self._query_state))
|
payload = str(await self.hass.async_add_executor_job(self._query_state))
|
||||||
if self._icon_template:
|
value = None
|
||||||
self._attr_icon = (
|
|
||||||
self._icon_template.async_render_with_possible_json_value(payload)
|
|
||||||
)
|
|
||||||
if self._value_template:
|
if self._value_template:
|
||||||
payload = self._value_template.async_render_with_possible_json_value(
|
value = self._value_template.async_render_with_possible_json_value(
|
||||||
payload, None
|
payload, None
|
||||||
)
|
)
|
||||||
self._attr_is_on = None
|
self._attr_is_on = None
|
||||||
if payload:
|
if payload or value:
|
||||||
self._attr_is_on = payload.lower() == "true"
|
self._attr_is_on = (value or payload).lower() == "true"
|
||||||
|
self._process_manual_data(payload)
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Turn the device on."""
|
"""Turn the device on."""
|
||||||
|
|
|
@ -29,6 +29,7 @@ from homeassistant.const import (
|
||||||
)
|
)
|
||||||
from homeassistant.core import Context, CoreState, Event, HomeAssistant, State, callback
|
from homeassistant.core import Context, CoreState, Event, HomeAssistant, State, callback
|
||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.exceptions import TemplateError
|
||||||
|
from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads
|
||||||
|
|
||||||
from . import config_validation as cv
|
from . import config_validation as cv
|
||||||
from .entity import Entity
|
from .entity import Entity
|
||||||
|
@ -487,9 +488,8 @@ class TriggerBaseEntity(Entity):
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_PICTURE,
|
CONF_PICTURE,
|
||||||
):
|
):
|
||||||
if itm not in config:
|
if itm not in config or config[itm] is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if config[itm].is_static:
|
if config[itm].is_static:
|
||||||
self._static_rendered[itm] = config[itm].template
|
self._static_rendered[itm] = config[itm].template
|
||||||
else:
|
else:
|
||||||
|
@ -597,3 +597,36 @@ class TriggerBaseEntity(Entity):
|
||||||
"Error rendering %s template for %s: %s", key, self.entity_id, err
|
"Error rendering %s template for %s: %s", key, self.entity_id, err
|
||||||
)
|
)
|
||||||
self._rendered = self._static_rendered
|
self._rendered = self._static_rendered
|
||||||
|
|
||||||
|
|
||||||
|
class ManualTriggerEntity(TriggerBaseEntity):
|
||||||
|
"""Template entity based on manual trigger data."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config: dict,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the entity."""
|
||||||
|
TriggerBaseEntity.__init__(self, hass, config)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _process_manual_data(self, value: str | None = None) -> None:
|
||||||
|
"""Process new data manually.
|
||||||
|
|
||||||
|
Implementing class should call this last in update method to render templates.
|
||||||
|
Ex: self._process_manual_data(payload)
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.async_write_ha_state()
|
||||||
|
this = None
|
||||||
|
if state := self.hass.states.get(self.entity_id):
|
||||||
|
this = state.as_dict()
|
||||||
|
|
||||||
|
run_variables: dict[str, Any] = {"value": value}
|
||||||
|
# Silently try if variable is a json and store result in `value_json` if it is.
|
||||||
|
with contextlib.suppress(*JSON_DECODE_EXCEPTIONS):
|
||||||
|
run_variables["value_json"] = json_loads(run_variables["value"])
|
||||||
|
variables = {"this": this, **(run_variables or {})}
|
||||||
|
|
||||||
|
self._render_templates(variables)
|
||||||
|
|
|
@ -440,3 +440,57 @@ async def test_command_failure(
|
||||||
DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "switch.test"}, blocking=True
|
DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "switch.test"}, blocking=True
|
||||||
)
|
)
|
||||||
assert "return code 33" in caplog.text
|
assert "return code 33" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_templating(hass: HomeAssistant) -> None:
|
||||||
|
"""Test with templating."""
|
||||||
|
with tempfile.TemporaryDirectory() as tempdirname:
|
||||||
|
path = os.path.join(tempdirname, "switch_status")
|
||||||
|
await setup_test_entity(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
"test": {
|
||||||
|
"command_state": f"cat {path}",
|
||||||
|
"command_on": f"echo 1 > {path}",
|
||||||
|
"command_off": f"echo 0 > {path}",
|
||||||
|
"value_template": '{{ value=="1" }}',
|
||||||
|
"icon_template": (
|
||||||
|
'{% if this.state=="on" %} mdi:on {% else %} mdi:off {% endif %}'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"test2": {
|
||||||
|
"command_state": f"cat {path}",
|
||||||
|
"command_on": f"echo 1 > {path}",
|
||||||
|
"command_off": f"echo 0 > {path}",
|
||||||
|
"value_template": '{{ value=="1" }}',
|
||||||
|
"icon_template": (
|
||||||
|
'{% if states("switch.test2")=="on" %} mdi:on {% else %} mdi:off {% endif %}'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
entity_state = hass.states.get("switch.test")
|
||||||
|
entity_state2 = hass.states.get("switch.test2")
|
||||||
|
assert entity_state.state == STATE_OFF
|
||||||
|
assert entity_state2.state == STATE_OFF
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
{ATTR_ENTITY_ID: "switch.test"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
{ATTR_ENTITY_ID: "switch.test2"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
entity_state = hass.states.get("switch.test")
|
||||||
|
entity_state2 = hass.states.get("switch.test2")
|
||||||
|
assert entity_state.state == STATE_ON
|
||||||
|
assert entity_state.attributes.get("icon") == "mdi:on"
|
||||||
|
assert entity_state2.state == STATE_ON
|
||||||
|
assert entity_state2.attributes.get("icon") == "mdi:on"
|
||||||
|
|
40
tests/components/template/test_manual_trigger_entity.py
Normal file
40
tests/components/template/test_manual_trigger_entity.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
"""Test template trigger entity."""
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import template
|
||||||
|
from homeassistant.helpers.template_entity import ManualTriggerEntity
|
||||||
|
|
||||||
|
|
||||||
|
async def test_template_entity_requires_hass_set(hass: HomeAssistant) -> None:
|
||||||
|
"""Test manual trigger template entity."""
|
||||||
|
config = {
|
||||||
|
"name": template.Template("test_entity", hass),
|
||||||
|
"icon": template.Template(
|
||||||
|
'{% if value=="on" %} mdi:on {% else %} mdi:off {% endif %}', hass
|
||||||
|
),
|
||||||
|
"picture": template.Template(
|
||||||
|
'{% if value=="on" %} /local/picture_on {% else %} /local/picture_off {% endif %}',
|
||||||
|
hass,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
entity = ManualTriggerEntity(hass, config)
|
||||||
|
entity.entity_id = "test.entity"
|
||||||
|
hass.states.async_set("test.entity", "on")
|
||||||
|
await entity.async_added_to_hass()
|
||||||
|
|
||||||
|
entity._process_manual_data("on")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert entity.name == "test_entity"
|
||||||
|
assert entity.icon == "mdi:on"
|
||||||
|
assert entity.entity_picture == "/local/picture_on"
|
||||||
|
|
||||||
|
hass.states.async_set("test.entity", "off")
|
||||||
|
await entity.async_added_to_hass()
|
||||||
|
entity._process_manual_data("off")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert entity.name == "test_entity"
|
||||||
|
assert entity.icon == "mdi:off"
|
||||||
|
assert entity.entity_picture == "/local/picture_off"
|
Loading…
Add table
Add a link
Reference in a new issue