Add value_template option to KNX expose (#117732)

* Add value_template option to KNX expose

* template exception handling
This commit is contained in:
Matthias Alphart 2024-05-22 00:09:42 +02:00 committed by GitHub
parent 622d1e4c50
commit 70cf176d93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 85 additions and 18 deletions

View file

@ -13,6 +13,7 @@ from xknx.remote_value import RemoteValueSensor
from homeassistant.const import ( from homeassistant.const import (
CONF_ENTITY_ID, CONF_ENTITY_ID,
CONF_VALUE_TEMPLATE,
STATE_OFF, STATE_OFF,
STATE_ON, STATE_ON,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
@ -25,7 +26,9 @@ from homeassistant.core import (
State, State,
callback, callback,
) )
from homeassistant.exceptions import TemplateError
from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, StateType from homeassistant.helpers.typing import ConfigType, StateType
from .const import CONF_RESPOND_TO_READ, KNX_ADDRESS from .const import CONF_RESPOND_TO_READ, KNX_ADDRESS
@ -79,6 +82,9 @@ class KNXExposeSensor:
) )
self.expose_default = config.get(ExposeSchema.CONF_KNX_EXPOSE_DEFAULT) self.expose_default = config.get(ExposeSchema.CONF_KNX_EXPOSE_DEFAULT)
self.expose_type: int | str = config[ExposeSchema.CONF_KNX_EXPOSE_TYPE] self.expose_type: int | str = config[ExposeSchema.CONF_KNX_EXPOSE_TYPE]
self.value_template: Template | None = config.get(CONF_VALUE_TEMPLATE)
if self.value_template is not None:
self.value_template.hass = hass
self._remove_listener: Callable[[], None] | None = None self._remove_listener: Callable[[], None] | None = None
self.device: ExposeSensor = self.async_register(config) self.device: ExposeSensor = self.async_register(config)
@ -87,13 +93,10 @@ class KNXExposeSensor:
@callback @callback
def async_register(self, config: ConfigType) -> ExposeSensor: def async_register(self, config: ConfigType) -> ExposeSensor:
"""Register listener.""" """Register listener."""
if self.expose_attribute is not None: name = f"{self.entity_id}__{self.expose_attribute or "state"}"
_name = self.entity_id + "__" + self.expose_attribute
else:
_name = self.entity_id
device = ExposeSensor( device = ExposeSensor(
xknx=self.xknx, xknx=self.xknx,
name=_name, name=name,
group_address=config[KNX_ADDRESS], group_address=config[KNX_ADDRESS],
respond_to_read=config[CONF_RESPOND_TO_READ], respond_to_read=config[CONF_RESPOND_TO_READ],
value_type=self.expose_type, value_type=self.expose_type,
@ -132,24 +135,33 @@ class KNXExposeSensor:
else: else:
value = state.state value = state.state
if self.value_template is not None:
try:
value = self.value_template.async_render_with_possible_json_value(
value, error_value=None
)
except (TemplateError, TypeError, ValueError) as err:
_LOGGER.warning(
"Error rendering value template for KNX expose %s %s: %s",
self.device.name,
self.value_template.template,
err,
)
return None
if self.expose_type == "binary": if self.expose_type == "binary":
if value in (1, STATE_ON, "True"): if value in (1, STATE_ON, "True"):
return True return True
if value in (0, STATE_OFF, "False"): if value in (0, STATE_OFF, "False"):
return False return False
if ( if value is not None and (
value is not None isinstance(self.device.sensor_value, RemoteValueSensor)
and isinstance(self.device.sensor_value, RemoteValueSensor)
and issubclass(self.device.sensor_value.dpt_class, DPTNumeric)
): ):
return float(value) if issubclass(self.device.sensor_value.dpt_class, DPTNumeric):
if ( return float(value)
value is not None if issubclass(self.device.sensor_value.dpt_class, DPTString):
and isinstance(self.device.sensor_value, RemoteValueSensor) # DPT 16.000 only allows up to 14 Bytes
and issubclass(self.device.sensor_value.dpt_class, DPTString) return str(value)[:14]
):
# DPT 16.000 only allows up to 14 Bytes
return str(value)[:14]
return value return value
async def _async_entity_changed(self, event: Event[EventStateChangedData]) -> None: async def _async_entity_changed(self, event: Event[EventStateChangedData]) -> None:

View file

@ -37,6 +37,7 @@ from homeassistant.const import (
CONF_NAME, CONF_NAME,
CONF_PAYLOAD, CONF_PAYLOAD,
CONF_TYPE, CONF_TYPE,
CONF_VALUE_TEMPLATE,
Platform, Platform,
) )
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -559,6 +560,7 @@ class ExposeSchema(KNXPlatformSchema):
vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_KNX_EXPOSE_ATTRIBUTE): cv.string, vol.Optional(CONF_KNX_EXPOSE_ATTRIBUTE): cv.string,
vol.Optional(CONF_KNX_EXPOSE_DEFAULT): cv.match_all, vol.Optional(CONF_KNX_EXPOSE_DEFAULT): cv.match_all,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
} }
) )
ENTITY_SCHEMA = vol.Any(EXPOSE_SENSOR_SCHEMA, EXPOSE_TIME_SCHEMA) ENTITY_SCHEMA = vol.Any(EXPOSE_SENSOR_SCHEMA, EXPOSE_TIME_SCHEMA)

View file

@ -8,7 +8,12 @@ import pytest
from homeassistant.components.knx import CONF_KNX_EXPOSE, DOMAIN, KNX_ADDRESS from homeassistant.components.knx import CONF_KNX_EXPOSE, DOMAIN, KNX_ADDRESS
from homeassistant.components.knx.schema import ExposeSchema from homeassistant.components.knx.schema import ExposeSchema
from homeassistant.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_TYPE from homeassistant.const import (
CONF_ATTRIBUTE,
CONF_ENTITY_ID,
CONF_TYPE,
CONF_VALUE_TEMPLATE,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
@ -237,6 +242,54 @@ async def test_expose_cooldown(hass: HomeAssistant, knx: KNXTestKit) -> None:
await knx.assert_write("1/1/8", (3,)) await knx.assert_write("1/1/8", (3,))
async def test_expose_value_template(
hass: HomeAssistant, knx: KNXTestKit, caplog: pytest.LogCaptureFixture
) -> None:
"""Test an expose with value_template."""
entity_id = "fake.entity"
attribute = "brightness"
binary_address = "1/1/1"
percent_address = "2/2/2"
await knx.setup_integration(
{
CONF_KNX_EXPOSE: [
{
CONF_TYPE: "binary",
KNX_ADDRESS: binary_address,
CONF_ENTITY_ID: entity_id,
CONF_VALUE_TEMPLATE: "{{ not value == 'on' }}",
},
{
CONF_TYPE: "percentU8",
KNX_ADDRESS: percent_address,
CONF_ENTITY_ID: entity_id,
CONF_ATTRIBUTE: attribute,
CONF_VALUE_TEMPLATE: "{{ 255 - value }}",
},
]
},
)
# Change attribute to 0
hass.states.async_set(entity_id, "on", {attribute: 0})
await hass.async_block_till_done()
await knx.assert_write(binary_address, False)
await knx.assert_write(percent_address, (255,))
# Change attribute to 255
hass.states.async_set(entity_id, "off", {attribute: 255})
await hass.async_block_till_done()
await knx.assert_write(binary_address, True)
await knx.assert_write(percent_address, (0,))
# Change attribute to null (eg. light brightness)
hass.states.async_set(entity_id, "off", {attribute: None})
await hass.async_block_till_done()
# without explicit `None`-handling or default value this fails with
# TypeError: unsupported operand type(s) for -: 'int' and 'NoneType'
assert "Error rendering value template for KNX expose" in caplog.text
async def test_expose_conversion_exception( async def test_expose_conversion_exception(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, knx: KNXTestKit hass: HomeAssistant, caplog: pytest.LogCaptureFixture, knx: KNXTestKit
) -> None: ) -> None: