Add value_template option to KNX expose (#117732)
* Add value_template option to KNX expose * template exception handling
This commit is contained in:
parent
622d1e4c50
commit
70cf176d93
3 changed files with 85 additions and 18 deletions
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Add table
Reference in a new issue