diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index ee170f55802..9d190ac78b0 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -7,7 +7,7 @@ from xknx import XKNX from xknx.devices import Notification as XknxNotification from homeassistant.components.notify import BaseNotificationService -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -34,6 +34,7 @@ async def async_get_service( xknx, name=device_config[CONF_NAME], group_address=device_config[KNX_ADDRESS], + value_type=device_config[CONF_TYPE], ) ) return ( diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index bec5b5e5bda..57a3aacd780 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -3,12 +3,13 @@ from __future__ import annotations from abc import ABC from collections import OrderedDict +from collections.abc import Callable import ipaddress from typing import Any, ClassVar, Final import voluptuous as vol from xknx.devices.climate import SetpointShiftMode -from xknx.dpt import DPTBase, DPTNumeric +from xknx.dpt import DPTBase, DPTNumeric, DPTString from xknx.exceptions import ConversionError, CouldNotParseAddress from xknx.telegram.address import IndividualAddress, parse_device_group_address @@ -54,6 +55,28 @@ from .const import ( ################## +def dpt_subclass_validator(dpt_base_class: type[DPTBase]) -> Callable[[Any], str | int]: + """Validate that value is parsable as given sensor type.""" + + def dpt_value_validator(value: Any) -> str | int: + """Validate that value is parsable as sensor type.""" + if ( + isinstance(value, (str, int)) + and dpt_base_class.parse_transcoder(value) is not None + ): + return value + raise vol.Invalid( + f"type '{value}' is not a valid DPT identifier for {dpt_base_class.__name__}." + ) + + return dpt_value_validator + + +numeric_type_validator = dpt_subclass_validator(DPTNumeric) # type: ignore[misc] +sensor_type_validator = dpt_subclass_validator(DPTBase) # type: ignore[misc] +string_type_validator = dpt_subclass_validator(DPTString) + + def ga_validator(value: Any) -> str | int: """Validate that value is parsable as GroupAddress or InternalGroupAddress.""" if isinstance(value, (str, int)): @@ -131,13 +154,6 @@ def number_limit_sub_validator(entity_config: OrderedDict) -> OrderedDict: return entity_config -def numeric_type_validator(value: Any) -> str | int: - """Validate that value is parsable as numeric sensor type.""" - if isinstance(value, (str, int)) and DPTNumeric.parse_transcoder(value) is not None: - return value - raise vol.Invalid(f"value '{value}' is not a valid numeric sensor type.") - - def _max_payload_value(payload_length: int) -> int: if payload_length == 0: return 0x3F @@ -194,13 +210,6 @@ def select_options_sub_validator(entity_config: OrderedDict) -> OrderedDict: return entity_config -def sensor_type_validator(value: Any) -> str | int: - """Validate that value is parsable as sensor type.""" - if isinstance(value, (str, int)) and DPTBase.parse_transcoder(value) is not None: - return value - raise vol.Invalid(f"value '{value}' is not a valid sensor type.") - - sync_state_validator = vol.Any( vol.All(vol.Coerce(int), vol.Range(min=2, max=1440)), cv.boolean, @@ -733,6 +742,7 @@ class NotifySchema(KNXPlatformSchema): ENTITY_SCHEMA = vol.Schema( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_TYPE, default="latin_1"): string_type_validator, vol.Required(KNX_ADDRESS): ga_validator, } ) diff --git a/tests/components/knx/test_notify.py b/tests/components/knx/test_notify.py index ad8da5f2cc0..96ca1672523 100644 --- a/tests/components/knx/test_notify.py +++ b/tests/components/knx/test_notify.py @@ -2,7 +2,7 @@ from homeassistant.components.knx.const import KNX_ADDRESS from homeassistant.components.knx.schema import NotifySchema -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant from .conftest import KNXTestKit @@ -75,18 +75,22 @@ async def test_notify_simple(hass: HomeAssistant, knx: KNXTestKit): ) -async def test_notify_multiple_sends_to_all(hass: HomeAssistant, knx: KNXTestKit): - """Test KNX notify can send to all devices.""" +async def test_notify_multiple_sends_to_all_with_different_encodings( + hass: HomeAssistant, knx: KNXTestKit +): + """Test KNX notify `type` configuration.""" await knx.setup_integration( { NotifySchema.PLATFORM: [ { - CONF_NAME: "test", + CONF_NAME: "ASCII", KNX_ADDRESS: "1/0/0", + CONF_TYPE: "string", }, { - CONF_NAME: "test2", + CONF_NAME: "Latin-1", KNX_ADDRESS: "1/0/1", + CONF_TYPE: "latin_1", }, ] } @@ -94,44 +98,15 @@ async def test_notify_multiple_sends_to_all(hass: HomeAssistant, knx: KNXTestKit await hass.async_block_till_done() await hass.services.async_call( - "notify", "notify", {"message": "I love KNX"}, blocking=True + "notify", "notify", {"message": "Gänsefüßchen"}, blocking=True ) await knx.assert_write( "1/0/0", - ( - 0x49, - 0x20, - 0x6C, - 0x6F, - 0x76, - 0x65, - 0x20, - 0x4B, - 0x4E, - 0x58, - 0x0, - 0x0, - 0x0, - 0x0, - ), + # "G?nsef??chen" + (71, 63, 110, 115, 101, 102, 63, 63, 99, 104, 101, 110, 0, 0), ) await knx.assert_write( "1/0/1", - ( - 0x49, - 0x20, - 0x6C, - 0x6F, - 0x76, - 0x65, - 0x20, - 0x4B, - 0x4E, - 0x58, - 0x0, - 0x0, - 0x0, - 0x0, - ), + (71, 228, 110, 115, 101, 102, 252, 223, 99, 104, 101, 110, 0, 0), )