Update xknx to 2.9.0 (#91282)

* Update xknx to 2.8.0

* add tests for validators

* Update strings.json

* Update xknx to 2.9.0
This commit is contained in:
Matthias Alphart 2023-04-22 18:25:14 +02:00 committed by GitHub
parent 2e9dc209f9
commit 95d44e100b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 127 additions and 49 deletions

View File

@ -12,7 +12,7 @@ from xknx import XKNX
from xknx.core import XknxConnectionState from xknx.core import XknxConnectionState
from xknx.core.telegram_queue import TelegramQueue from xknx.core.telegram_queue import TelegramQueue
from xknx.dpt import DPTArray, DPTBase, DPTBinary from xknx.dpt import DPTArray, DPTBase, DPTBinary
from xknx.exceptions import ConversionError, XKNXException from xknx.exceptions import ConversionError, CouldNotParseTelegram, XKNXException
from xknx.io import ConnectionConfig, ConnectionType, SecureConfig from xknx.io import ConnectionConfig, ConnectionType, SecureConfig
from xknx.telegram import AddressFilter, Telegram from xknx.telegram import AddressFilter, Telegram
from xknx.telegram.address import ( from xknx.telegram.address import (
@ -513,31 +513,29 @@ class KNXModule:
) )
): ):
data = telegram.payload.value.value data = telegram.payload.value.value
if transcoder := (
if isinstance(data, tuple): self._group_address_transcoder.get(telegram.destination_address)
if transcoder := ( or next(
self._group_address_transcoder.get(telegram.destination_address) (
or next( _transcoder
for _filter, _transcoder in self._address_filter_transcoder.items()
if _filter.match(telegram.destination_address)
),
None,
)
):
try:
value = transcoder.from_knx(telegram.payload.value)
except (ConversionError, CouldNotParseTelegram) as err:
_LOGGER.warning(
( (
_transcoder "Error in `knx_event` at decoding type '%s' from"
for _filter, _transcoder in self._address_filter_transcoder.items() " telegram %s\n%s"
if _filter.match(telegram.destination_address)
), ),
None, transcoder.__name__,
telegram,
err,
) )
):
try:
value = transcoder.from_knx(data)
except ConversionError as err:
_LOGGER.warning(
(
"Error in `knx_event` at decoding type '%s' from"
" telegram %s\n%s"
),
transcoder.__name__,
telegram,
err,
)
self.hass.bus.async_fire( self.hass.bus.async_fire(
"knx_event", "knx_event",
@ -656,7 +654,7 @@ class KNXModule:
transcoder = DPTBase.parse_transcoder(attr_type) transcoder = DPTBase.parse_transcoder(attr_type)
if transcoder is None: if transcoder is None:
raise ValueError(f"Invalid type for knx.send service: {attr_type}") raise ValueError(f"Invalid type for knx.send service: {attr_type}")
payload = DPTArray(transcoder.to_knx(attr_payload)) payload = transcoder.to_knx(attr_payload)
elif isinstance(attr_payload, int): elif isinstance(attr_payload, int):
payload = DPTBinary(attr_payload) payload = DPTBinary(attr_payload)
else: else:

View File

@ -9,10 +9,15 @@ from typing import Any, Final
import voluptuous as vol import voluptuous as vol
from xknx import XKNX from xknx import XKNX
from xknx.exceptions.exception import CommunicationError, InvalidSecureConfiguration from xknx.exceptions.exception import (
CommunicationError,
InvalidSecureConfiguration,
XKNXException,
)
from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT
from xknx.io.gateway_scanner import GatewayDescriptor, GatewayScanner from xknx.io.gateway_scanner import GatewayDescriptor, GatewayScanner
from xknx.io.self_description import request_description from xknx.io.self_description import request_description
from xknx.io.util import validate_ip as xknx_validate_ip
from xknx.secure.keyring import Keyring, XMLInterface, sync_load_keyring from xknx.secure.keyring import Keyring, XMLInterface, sync_load_keyring
from homeassistant.components.file_upload import process_uploaded_file from homeassistant.components.file_upload import process_uploaded_file
@ -258,21 +263,25 @@ class KNXCommonFlow(ABC, FlowHandler):
if user_input is not None: if user_input is not None:
try: try:
_host = ip_v4_validator(user_input[CONF_HOST], multicast=False) _host = user_input[CONF_HOST]
except vol.Invalid: _host_ip = await xknx_validate_ip(_host)
ip_v4_validator(_host_ip, multicast=False)
except (vol.Invalid, XKNXException):
errors[CONF_HOST] = "invalid_ip_address" errors[CONF_HOST] = "invalid_ip_address"
if _local_ip := user_input.get(CONF_KNX_LOCAL_IP): _local_ip = None
if _local := user_input.get(CONF_KNX_LOCAL_IP):
try: try:
_local_ip = ip_v4_validator(_local_ip, multicast=False) _local_ip = await xknx_validate_ip(_local)
except vol.Invalid: ip_v4_validator(_local_ip, multicast=False)
except (vol.Invalid, XKNXException):
errors[CONF_KNX_LOCAL_IP] = "invalid_ip_address" errors[CONF_KNX_LOCAL_IP] = "invalid_ip_address"
selected_tunnelling_type = user_input[CONF_KNX_TUNNELING_TYPE] selected_tunnelling_type = user_input[CONF_KNX_TUNNELING_TYPE]
if not errors: if not errors:
try: try:
self._selected_tunnel = await request_description( self._selected_tunnel = await request_description(
gateway_ip=_host, gateway_ip=_host_ip,
gateway_port=user_input[CONF_PORT], gateway_port=user_input[CONF_PORT],
local_ip=_local_ip, local_ip=_local_ip,
route_back=user_input[CONF_KNX_ROUTE_BACK], route_back=user_input[CONF_KNX_ROUTE_BACK],
@ -296,7 +305,7 @@ class KNXCommonFlow(ABC, FlowHandler):
host=_host, host=_host,
port=user_input[CONF_PORT], port=user_input[CONF_PORT],
route_back=user_input[CONF_KNX_ROUTE_BACK], route_back=user_input[CONF_KNX_ROUTE_BACK],
local_ip=_local_ip, local_ip=_local,
device_authentication=None, device_authentication=None,
user_id=None, user_id=None,
user_password=None, user_password=None,
@ -636,10 +645,11 @@ class KNXCommonFlow(ABC, FlowHandler):
ip_v4_validator(_multicast_group, multicast=True) ip_v4_validator(_multicast_group, multicast=True)
except vol.Invalid: except vol.Invalid:
errors[CONF_KNX_MCAST_GRP] = "invalid_ip_address" errors[CONF_KNX_MCAST_GRP] = "invalid_ip_address"
if _local_ip := user_input.get(CONF_KNX_LOCAL_IP): if _local := user_input.get(CONF_KNX_LOCAL_IP):
try: try:
_local_ip = await xknx_validate_ip(_local)
ip_v4_validator(_local_ip, multicast=False) ip_v4_validator(_local_ip, multicast=False)
except vol.Invalid: except (vol.Invalid, XKNXException):
errors[CONF_KNX_LOCAL_IP] = "invalid_ip_address" errors[CONF_KNX_LOCAL_IP] = "invalid_ip_address"
if not errors: if not errors:
@ -653,7 +663,7 @@ class KNXCommonFlow(ABC, FlowHandler):
individual_address=_individual_address, individual_address=_individual_address,
multicast_group=_multicast_group, multicast_group=_multicast_group,
multicast_port=_multicast_port, multicast_port=_multicast_port,
local_ip=_local_ip, local_ip=_local,
device_authentication=None, device_authentication=None,
user_id=None, user_id=None,
user_password=None, user_password=None,

View File

@ -9,5 +9,5 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["xknx"], "loggers": ["xknx"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["xknx==2.7.0"] "requirements": ["xknx==2.9.0"]
} }

View File

@ -10,7 +10,7 @@ from typing import Any, ClassVar, Final
import voluptuous as vol import voluptuous as vol
from xknx.devices.climate import SetpointShiftMode from xknx.devices.climate import SetpointShiftMode
from xknx.dpt import DPTBase, DPTNumeric, DPTString from xknx.dpt import DPTBase, DPTNumeric, DPTString
from xknx.exceptions import ConversionError, CouldNotParseAddress from xknx.exceptions import ConversionError, CouldNotParseAddress, CouldNotParseTelegram
from xknx.telegram.address import IndividualAddress, parse_device_group_address from xknx.telegram.address import IndividualAddress, parse_device_group_address
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
@ -185,13 +185,13 @@ def button_payload_sub_validator(entity_config: OrderedDict) -> OrderedDict:
raise vol.Invalid(f"'type: {_type}' is not a valid sensor type.") raise vol.Invalid(f"'type: {_type}' is not a valid sensor type.")
entity_config[CONF_PAYLOAD_LENGTH] = transcoder.payload_length entity_config[CONF_PAYLOAD_LENGTH] = transcoder.payload_length
try: try:
entity_config[CONF_PAYLOAD] = int.from_bytes( _dpt_payload = transcoder.to_knx(_payload)
transcoder.to_knx(_payload), byteorder="big" _raw_payload = transcoder.validate_payload(_dpt_payload)
) except (ConversionError, CouldNotParseTelegram) as ex:
except ConversionError as ex:
raise vol.Invalid( raise vol.Invalid(
f"'payload: {_payload}' not valid for 'type: {_type}'" f"'payload: {_payload}' not valid for 'type: {_type}'"
) from ex ) from ex
entity_config[CONF_PAYLOAD] = int.from_bytes(_raw_payload, byteorder="big")
return entity_config return entity_config
_payload = entity_config[CONF_PAYLOAD] _payload = entity_config[CONF_PAYLOAD]

View File

@ -23,13 +23,13 @@
"port": "[%key:common::config_flow::data::port%]", "port": "[%key:common::config_flow::data::port%]",
"host": "[%key:common::config_flow::data::host%]", "host": "[%key:common::config_flow::data::host%]",
"route_back": "Route back / NAT mode", "route_back": "Route back / NAT mode",
"local_ip": "Local IP of Home Assistant" "local_ip": "Local IP interface"
}, },
"data_description": { "data_description": {
"port": "Port of the KNX/IP tunneling device.", "port": "Port of the KNX/IP tunneling device.",
"host": "IP address of the KNX/IP tunneling device.", "host": "IP address or hostname of the KNX/IP tunneling device.",
"route_back": "Enable if your KNXnet/IP tunneling server is behind NAT. Only applies for UDP connections.", "route_back": "Enable if your KNXnet/IP tunneling server is behind NAT. Only applies for UDP connections.",
"local_ip": "Leave blank to use auto-discovery." "local_ip": "Local IP or interface name used for the connection from Home Assistant. Leave blank to use auto-discovery."
} }
}, },
"secure_key_source": { "secure_key_source": {
@ -93,11 +93,11 @@
"routing_secure": "Use KNX IP Secure", "routing_secure": "Use KNX IP Secure",
"multicast_group": "Multicast group", "multicast_group": "Multicast group",
"multicast_port": "Multicast port", "multicast_port": "Multicast port",
"local_ip": "Local IP of Home Assistant" "local_ip": "[%key:component::knx::config::step::manual_tunnel::data::local_ip%]"
}, },
"data_description": { "data_description": {
"individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`", "individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`",
"local_ip": "Leave blank to use auto-discovery." "local_ip": "[%key:component::knx::config::step::manual_tunnel::data_description::local_ip%]"
} }
} }
}, },

View File

@ -2664,7 +2664,7 @@ xbox-webapi==2.0.11
xiaomi-ble==0.17.0 xiaomi-ble==0.17.0
# homeassistant.components.knx # homeassistant.components.knx
xknx==2.7.0 xknx==2.9.0
# homeassistant.components.bluesound # homeassistant.components.bluesound
# homeassistant.components.fritz # homeassistant.components.fritz

View File

@ -1922,7 +1922,7 @@ xbox-webapi==2.0.11
xiaomi-ble==0.17.0 xiaomi-ble==0.17.0
# homeassistant.components.knx # homeassistant.components.knx
xknx==2.7.0 xknx==2.9.0
# homeassistant.components.bluesound # homeassistant.components.bluesound
# homeassistant.components.fritz # homeassistant.components.fritz

View File

@ -1,9 +1,13 @@
"""Test KNX button.""" """Test KNX button."""
from datetime import timedelta from datetime import timedelta
import logging
import pytest
from homeassistant.components.knx.const import ( from homeassistant.components.knx.const import (
CONF_PAYLOAD, CONF_PAYLOAD,
CONF_PAYLOAD_LENGTH, CONF_PAYLOAD_LENGTH,
DOMAIN,
KNX_ADDRESS, KNX_ADDRESS,
) )
from homeassistant.components.knx.schema import ButtonSchema from homeassistant.components.knx.schema import ButtonSchema
@ -86,3 +90,49 @@ async def test_button_type(hass: HomeAssistant, knx: KNXTestKit) -> None:
"button", "press", {"entity_id": "button.test"}, blocking=True "button", "press", {"entity_id": "button.test"}, blocking=True
) )
await knx.assert_write("1/2/3", (0x0C, 0x33)) await knx.assert_write("1/2/3", (0x0C, 0x33))
@pytest.mark.parametrize(
("conf_type", "conf_value", "error_msg"),
[
(
"2byte_float",
"not_valid",
"'payload: not_valid' not valid for 'type: 2byte_float'",
),
(
"not_valid",
3,
"type 'not_valid' is not a valid DPT identifier",
),
],
)
async def test_button_invalid(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
knx: KNXTestKit,
conf_type: str,
conf_value: str,
error_msg: str,
) -> None:
"""Test KNX button with configured payload that can't be encoded."""
with caplog.at_level(logging.ERROR):
await knx.setup_integration(
{
ButtonSchema.PLATFORM: {
CONF_NAME: "test",
KNX_ADDRESS: "1/2/3",
ButtonSchema.CONF_VALUE: conf_value,
CONF_TYPE: conf_type,
}
}
)
assert len(caplog.messages) == 2
record = caplog.records[0]
assert record.levelname == "ERROR"
assert f"Invalid config for [knx]: {error_msg}" in record.message
record = caplog.records[1]
assert record.levelname == "ERROR"
assert "Setup failed for knx: Invalid config." in record.message
assert hass.states.get("button.test") is None
assert hass.data.get(DOMAIN) is None

View File

@ -1,4 +1,7 @@
"""Test KNX events.""" """Test KNX events."""
import logging
import pytest
from homeassistant.components.knx import CONF_EVENT, CONF_TYPE, KNX_ADDRESS from homeassistant.components.knx import CONF_EVENT, CONF_TYPE, KNX_ADDRESS
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -8,7 +11,11 @@ from .conftest import KNXTestKit
from tests.common import async_capture_events from tests.common import async_capture_events
async def test_knx_event(hass: HomeAssistant, knx: KNXTestKit) -> None: async def test_knx_event(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
knx: KNXTestKit,
) -> None:
"""Test the `knx_event` event.""" """Test the `knx_event` event."""
test_group_a = "0/4/*" test_group_a = "0/4/*"
test_address_a_1 = "0/4/0" test_address_a_1 = "0/4/0"
@ -95,3 +102,16 @@ async def test_knx_event(hass: HomeAssistant, knx: KNXTestKit) -> None:
await knx.receive_write("2/6/6", True) await knx.receive_write("2/6/6", True)
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(events) == 0 assert len(events) == 0
# receive telegrams with wrong payload length
caplog.clear()
with caplog.at_level(logging.WARNING):
await knx.receive_write(test_address_a_1, (0x03, 0x2F, 0xFF))
assert len(caplog.messages) == 1
record = caplog.records[0]
assert record.levelname == "WARNING"
assert (
"Error in `knx_event` at decoding type "
"'DPT2ByteUnsigned' from telegram" in record.message
)
await test_event_data(test_address_a_1, (0x03, 0x2F, 0xFF), value=None)