Update xknx to 3.0.0 - more DPT definitions (#122891)

* Support DPTComplex objects and validate sensor types

* Gracefully start and stop xknx device objects

* Use non-awaitable XknxDevice callbacks

* Use non-awaitable xknx.TelegramQueue callbacks

* Use non-awaitable xknx.ConnectionManager callbacks

* Remove unnecessary `hass.async_block_till_done()` calls

* Wait for StateUpdater logic to proceed when receiving responses

* Update import module paths for specific DPTs

* Support Enum data types

* New HVAC mode names

* HVAC Enums instead of Enum member value strings

* New date and time devices

* Update xknx to 3.0.0

* Fix expose tests and DPTEnumData check

* ruff and mypy fixes
pull/122896/head
Matthias Alphart 2024-07-31 09:10:36 +02:00 committed by GitHub
parent 0d678120e4
commit 9351f300b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 217 additions and 286 deletions

View File

@ -2,7 +2,6 @@
from __future__ import annotations
import asyncio
import contextlib
import logging
from pathlib import Path
@ -225,7 +224,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
knx_module: KNXModule = hass.data[DOMAIN]
for exposure in knx_module.exposures:
exposure.shutdown()
exposure.async_remove()
unload_ok = await hass.config_entries.async_unload_platforms(
entry,
@ -439,13 +438,13 @@ class KNXModule:
threaded=True,
)
async def connection_state_changed_cb(self, state: XknxConnectionState) -> None:
def connection_state_changed_cb(self, state: XknxConnectionState) -> None:
"""Call invoked after a KNX connection state change was received."""
self.connected = state == XknxConnectionState.CONNECTED
if tasks := [device.after_update() for device in self.xknx.devices]:
await asyncio.gather(*tasks)
for device in self.xknx.devices:
device.after_update()
async def telegram_received_cb(self, telegram: Telegram) -> None:
def telegram_received_cb(self, telegram: Telegram) -> None:
"""Call invoked after a KNX telegram was received."""
# Not all telegrams have serializable data.
data: int | tuple[int, ...] | None = None
@ -504,10 +503,7 @@ class KNXModule:
transcoder := DPTBase.parse_transcoder(dpt)
):
self._address_filter_transcoder.update(
{
_filter: transcoder # type: ignore[type-abstract]
for _filter in _filters
}
{_filter: transcoder for _filter in _filters}
)
return self.xknx.telegram_queue.register_telegram_received_cb(

View File

@ -75,7 +75,7 @@ class KNXBinarySensor(KnxEntity, BinarySensorEntity, RestoreEntity):
if (
last_state := await self.async_get_last_state()
) and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE):
await self._device.remote_value.update_value(last_state.state == STATE_ON)
self._device.remote_value.update_value(last_state.state == STATE_ON)
@property
def is_on(self) -> bool:

View File

@ -6,7 +6,7 @@ from typing import Any
from xknx import XKNX
from xknx.devices import Climate as XknxClimate, ClimateMode as XknxClimateMode
from xknx.dpt.dpt_hvac_mode import HVACControllerMode, HVACOperationMode
from xknx.dpt.dpt_20 import HVACControllerMode
from homeassistant import config_entries
from homeassistant.components.climate import (
@ -80,7 +80,7 @@ def _create_climate(xknx: XKNX, config: ConfigType) -> XknxClimate:
group_address_operation_mode_protection=config.get(
ClimateSchema.CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS
),
group_address_operation_mode_night=config.get(
group_address_operation_mode_economy=config.get(
ClimateSchema.CONF_OPERATION_MODE_NIGHT_ADDRESS
),
group_address_operation_mode_comfort=config.get(
@ -199,10 +199,12 @@ class KNXClimate(KnxEntity, ClimateEntity):
self.async_write_ha_state()
return
if self._device.mode is not None and self._device.mode.supports_controller_mode:
knx_controller_mode = HVACControllerMode(
CONTROLLER_MODES_INV.get(self._last_hvac_mode)
)
if (
self._device.mode is not None
and self._device.mode.supports_controller_mode
and (knx_controller_mode := CONTROLLER_MODES_INV.get(self._last_hvac_mode))
is not None
):
await self._device.mode.set_controller_mode(knx_controller_mode)
self.async_write_ha_state()
@ -234,7 +236,7 @@ class KNXClimate(KnxEntity, ClimateEntity):
return HVACMode.OFF
if self._device.mode is not None and self._device.mode.supports_controller_mode:
hvac_mode = CONTROLLER_MODES.get(
self._device.mode.controller_mode.value, self.default_hvac_mode
self._device.mode.controller_mode, self.default_hvac_mode
)
if hvac_mode is not HVACMode.OFF:
self._last_hvac_mode = hvac_mode
@ -247,7 +249,7 @@ class KNXClimate(KnxEntity, ClimateEntity):
ha_controller_modes: list[HVACMode | None] = []
if self._device.mode is not None:
ha_controller_modes.extend(
CONTROLLER_MODES.get(knx_controller_mode.value)
CONTROLLER_MODES.get(knx_controller_mode)
for knx_controller_mode in self._device.mode.controller_modes
)
@ -278,9 +280,7 @@ class KNXClimate(KnxEntity, ClimateEntity):
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set controller mode."""
if self._device.mode is not None and self._device.mode.supports_controller_mode:
knx_controller_mode = HVACControllerMode(
CONTROLLER_MODES_INV.get(hvac_mode)
)
knx_controller_mode = CONTROLLER_MODES_INV.get(hvac_mode)
if knx_controller_mode in self._device.mode.controller_modes:
await self._device.mode.set_controller_mode(knx_controller_mode)
@ -298,7 +298,7 @@ class KNXClimate(KnxEntity, ClimateEntity):
Requires ClimateEntityFeature.PRESET_MODE.
"""
if self._device.mode is not None and self._device.mode.supports_operation_mode:
return PRESET_MODES.get(self._device.mode.operation_mode.value, PRESET_AWAY)
return PRESET_MODES.get(self._device.mode.operation_mode, PRESET_AWAY)
return None
@property
@ -311,15 +311,18 @@ class KNXClimate(KnxEntity, ClimateEntity):
return None
presets = [
PRESET_MODES.get(operation_mode.value)
PRESET_MODES.get(operation_mode)
for operation_mode in self._device.mode.operation_modes
]
return list(filter(None, presets))
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
if self._device.mode is not None and self._device.mode.supports_operation_mode:
knx_operation_mode = HVACOperationMode(PRESET_MODES_INV.get(preset_mode))
if (
self._device.mode is not None
and self._device.mode.supports_operation_mode
and (knx_operation_mode := PRESET_MODES_INV.get(preset_mode)) is not None
):
await self._device.mode.set_operation_mode(knx_operation_mode)
self.async_write_ha_state()
@ -333,7 +336,15 @@ class KNXClimate(KnxEntity, ClimateEntity):
return attr
async def async_added_to_hass(self) -> None:
"""Store register state change callback."""
"""Store register state change callback and start device object."""
await super().async_added_to_hass()
if self._device.mode is not None:
self._device.mode.register_device_updated_cb(self.after_update_callback)
self._device.mode.xknx.devices.async_add(self._device.mode)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect device object when removed."""
if self._device.mode is not None:
self._device.mode.unregister_device_updated_cb(self.after_update_callback)
self._device.mode.xknx.devices.async_remove(self._device.mode)
await super().async_will_remove_from_hass()

View File

@ -6,6 +6,7 @@ from collections.abc import Awaitable, Callable
from enum import Enum
from typing import Final, TypedDict
from xknx.dpt.dpt_20 import HVACControllerMode, HVACOperationMode
from xknx.telegram import Telegram
from homeassistant.components.climate import (
@ -158,12 +159,12 @@ SUPPORTED_PLATFORMS_UI: Final = {Platform.SWITCH, Platform.LIGHT}
# Map KNX controller modes to HA modes. This list might not be complete.
CONTROLLER_MODES: Final = {
# Map DPT 20.105 HVAC control modes
"Auto": HVACMode.AUTO,
"Heat": HVACMode.HEAT,
"Cool": HVACMode.COOL,
"Off": HVACMode.OFF,
"Fan only": HVACMode.FAN_ONLY,
"Dry": HVACMode.DRY,
HVACControllerMode.AUTO: HVACMode.AUTO,
HVACControllerMode.HEAT: HVACMode.HEAT,
HVACControllerMode.COOL: HVACMode.COOL,
HVACControllerMode.OFF: HVACMode.OFF,
HVACControllerMode.FAN_ONLY: HVACMode.FAN_ONLY,
HVACControllerMode.DEHUMIDIFICATION: HVACMode.DRY,
}
CURRENT_HVAC_ACTIONS: Final = {
@ -176,9 +177,9 @@ CURRENT_HVAC_ACTIONS: Final = {
PRESET_MODES: Final = {
# Map DPT 20.102 HVAC operating modes to HA presets
"Auto": PRESET_NONE,
"Frost Protection": PRESET_ECO,
"Night": PRESET_SLEEP,
"Standby": PRESET_AWAY,
"Comfort": PRESET_COMFORT,
HVACOperationMode.AUTO: PRESET_NONE,
HVACOperationMode.BUILDING_PROTECTION: PRESET_ECO,
HVACOperationMode.ECONOMY: PRESET_SLEEP,
HVACOperationMode.STANDBY: PRESET_AWAY,
HVACOperationMode.COMFORT: PRESET_COMFORT,
}

View File

@ -3,11 +3,10 @@
from __future__ import annotations
from datetime import date as dt_date
import time
from typing import Final
from xknx import XKNX
from xknx.devices import DateTime as XknxDateTime
from xknx.devices import DateDevice as XknxDateDevice
from xknx.dpt.dpt_11 import KNXDate as XKNXDate
from homeassistant import config_entries
from homeassistant.components.date import DateEntity
@ -33,8 +32,6 @@ from .const import (
)
from .knx_entity import KnxEntity
_DATE_TRANSLATION_FORMAT: Final = "%Y-%m-%d"
async def async_setup_entry(
hass: HomeAssistant,
@ -45,15 +42,14 @@ async def async_setup_entry(
xknx: XKNX = hass.data[DOMAIN].xknx
config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.DATE]
async_add_entities(KNXDate(xknx, entity_config) for entity_config in config)
async_add_entities(KNXDateEntity(xknx, entity_config) for entity_config in config)
def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateTime:
def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateDevice:
"""Return a XKNX DateTime object to be used within XKNX."""
return XknxDateTime(
return XknxDateDevice(
xknx,
name=config[CONF_NAME],
broadcast_type="DATE",
localtime=False,
group_address=config[KNX_ADDRESS],
group_address_state=config.get(CONF_STATE_ADDRESS),
@ -62,10 +58,10 @@ def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateTime:
)
class KNXDate(KnxEntity, DateEntity, RestoreEntity):
class KNXDateEntity(KnxEntity, DateEntity, RestoreEntity):
"""Representation of a KNX date."""
_device: XknxDateTime
_device: XknxDateDevice
def __init__(self, xknx: XKNX, config: ConfigType) -> None:
"""Initialize a KNX time."""
@ -81,21 +77,15 @@ class KNXDate(KnxEntity, DateEntity, RestoreEntity):
and (last_state := await self.async_get_last_state()) is not None
and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
):
self._device.remote_value.value = time.strptime(
last_state.state, _DATE_TRANSLATION_FORMAT
self._device.remote_value.value = XKNXDate.from_date(
dt_date.fromisoformat(last_state.state)
)
@property
def native_value(self) -> dt_date | None:
"""Return the latest value."""
if (time_struct := self._device.remote_value.value) is None:
return None
return dt_date(
year=time_struct.tm_year,
month=time_struct.tm_mon,
day=time_struct.tm_mday,
)
return self._device.value
async def async_set_value(self, value: dt_date) -> None:
"""Change the value."""
await self._device.set(value.timetuple())
await self._device.set(value)

View File

@ -5,7 +5,8 @@ from __future__ import annotations
from datetime import datetime
from xknx import XKNX
from xknx.devices import DateTime as XknxDateTime
from xknx.devices import DateTimeDevice as XknxDateTimeDevice
from xknx.dpt.dpt_19 import KNXDateTime as XKNXDateTime
from homeassistant import config_entries
from homeassistant.components.datetime import DateTimeEntity
@ -42,15 +43,16 @@ async def async_setup_entry(
xknx: XKNX = hass.data[DOMAIN].xknx
config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.DATETIME]
async_add_entities(KNXDateTime(xknx, entity_config) for entity_config in config)
async_add_entities(
KNXDateTimeEntity(xknx, entity_config) for entity_config in config
)
def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateTime:
def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateTimeDevice:
"""Return a XKNX DateTime object to be used within XKNX."""
return XknxDateTime(
return XknxDateTimeDevice(
xknx,
name=config[CONF_NAME],
broadcast_type="DATETIME",
localtime=False,
group_address=config[KNX_ADDRESS],
group_address_state=config.get(CONF_STATE_ADDRESS),
@ -59,10 +61,10 @@ def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateTime:
)
class KNXDateTime(KnxEntity, DateTimeEntity, RestoreEntity):
class KNXDateTimeEntity(KnxEntity, DateTimeEntity, RestoreEntity):
"""Representation of a KNX datetime."""
_device: XknxDateTime
_device: XknxDateTimeDevice
def __init__(self, xknx: XKNX, config: ConfigType) -> None:
"""Initialize a KNX time."""
@ -78,29 +80,19 @@ class KNXDateTime(KnxEntity, DateTimeEntity, RestoreEntity):
and (last_state := await self.async_get_last_state()) is not None
and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
):
self._device.remote_value.value = (
datetime.fromisoformat(last_state.state)
.astimezone(dt_util.get_default_time_zone())
.timetuple()
self._device.remote_value.value = XKNXDateTime.from_datetime(
datetime.fromisoformat(last_state.state).astimezone(
dt_util.get_default_time_zone()
)
)
@property
def native_value(self) -> datetime | None:
"""Return the latest value."""
if (time_struct := self._device.remote_value.value) is None:
if (naive_dt := self._device.value) is None:
return None
return datetime(
year=time_struct.tm_year,
month=time_struct.tm_mon,
day=time_struct.tm_mday,
hour=time_struct.tm_hour,
minute=time_struct.tm_min,
second=min(time_struct.tm_sec, 59), # account for leap seconds
tzinfo=dt_util.get_default_time_zone(),
)
return naive_dt.replace(tzinfo=dt_util.get_default_time_zone())
async def async_set_value(self, value: datetime) -> None:
"""Change the value."""
await self._device.set(
value.astimezone(dt_util.get_default_time_zone()).timetuple()
)
await self._device.set(value.astimezone(dt_util.get_default_time_zone()))

View File

@ -19,6 +19,7 @@ class KNXInterfaceDevice:
def __init__(self, hass: HomeAssistant, entry: ConfigEntry, xknx: XKNX) -> None:
"""Initialize interface device class."""
self.hass = hass
self.device_registry = dr.async_get(hass)
self.gateway_descriptor: GatewayDescriptor | None = None
self.xknx = xknx
@ -46,7 +47,7 @@ class KNXInterfaceDevice:
else None,
)
async def connection_state_changed_cb(self, state: XknxConnectionState) -> None:
def connection_state_changed_cb(self, state: XknxConnectionState) -> None:
"""Call invoked after a KNX connection state change was received."""
if state is XknxConnectionState.CONNECTED:
await self.update()
self.hass.async_create_task(self.update())

View File

@ -6,7 +6,7 @@ from collections.abc import Callable
import logging
from xknx import XKNX
from xknx.devices import DateTime, ExposeSensor
from xknx.devices import DateDevice, DateTimeDevice, ExposeSensor, TimeDevice
from xknx.dpt import DPTNumeric, DPTString
from xknx.exceptions import ConversionError
from xknx.remote_value import RemoteValueSensor
@ -60,6 +60,7 @@ def create_knx_exposure(
xknx=xknx,
config=config,
)
exposure.async_register()
return exposure
@ -87,25 +88,23 @@ class KNXExposeSensor:
self.value_template.hass = hass
self._remove_listener: Callable[[], None] | None = None
self.device: ExposeSensor = self.async_register(config)
self._init_expose_state()
@callback
def async_register(self, config: ConfigType) -> ExposeSensor:
"""Register listener."""
name = f"{self.entity_id}__{self.expose_attribute or "state"}"
device = ExposeSensor(
self.device: ExposeSensor = ExposeSensor(
xknx=self.xknx,
name=name,
name=f"{self.entity_id}__{self.expose_attribute or "state"}",
group_address=config[KNX_ADDRESS],
respond_to_read=config[CONF_RESPOND_TO_READ],
value_type=self.expose_type,
cooldown=config[ExposeSchema.CONF_KNX_EXPOSE_COOLDOWN],
)
@callback
def async_register(self) -> None:
"""Register listener."""
self._remove_listener = async_track_state_change_event(
self.hass, [self.entity_id], self._async_entity_changed
)
return device
self.xknx.devices.async_add(self.device)
self._init_expose_state()
@callback
def _init_expose_state(self) -> None:
@ -118,12 +117,12 @@ class KNXExposeSensor:
_LOGGER.exception("Error during sending of expose sensor value")
@callback
def shutdown(self) -> None:
def async_remove(self) -> None:
"""Prepare for deletion."""
if self._remove_listener is not None:
self._remove_listener()
self._remove_listener = None
self.device.shutdown()
self.xknx.devices.async_remove(self.device)
def _get_expose_value(self, state: State | None) -> bool | int | float | str | None:
"""Extract value from state."""
@ -196,21 +195,28 @@ class KNXExposeTime:
def __init__(self, xknx: XKNX, config: ConfigType) -> None:
"""Initialize of Expose class."""
self.xknx = xknx
self.device: DateTime = self.async_register(config)
@callback
def async_register(self, config: ConfigType) -> DateTime:
"""Register listener."""
expose_type = config[ExposeSchema.CONF_KNX_EXPOSE_TYPE]
return DateTime(
xknx_device_cls: type[DateDevice | DateTimeDevice | TimeDevice]
match expose_type:
case ExposeSchema.CONF_DATE:
xknx_device_cls = DateDevice
case ExposeSchema.CONF_DATETIME:
xknx_device_cls = DateTimeDevice
case ExposeSchema.CONF_TIME:
xknx_device_cls = TimeDevice
self.device = xknx_device_cls(
self.xknx,
name=expose_type.capitalize(),
broadcast_type=expose_type.upper(),
localtime=True,
group_address=config[KNX_ADDRESS],
)
@callback
def shutdown(self) -> None:
def async_register(self) -> None:
"""Register listener."""
self.xknx.devices.async_add(self.device)
@callback
def async_remove(self) -> None:
"""Prepare for deletion."""
self.device.shutdown()
self.xknx.devices.async_remove(self.device)

View File

@ -36,12 +36,16 @@ class KnxEntity(Entity):
"""Request a state update from KNX bus."""
await self._device.sync()
async def after_update_callback(self, device: XknxDevice) -> None:
def after_update_callback(self, _device: XknxDevice) -> None:
"""Call after device was updated."""
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Store register state change callback."""
"""Store register state change callback and start device object."""
self._device.register_device_updated_cb(self.after_update_callback)
# will remove all callbacks and xknx tasks
self.async_on_remove(self._device.shutdown)
self._device.xknx.devices.async_add(self._device)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect device object when removed."""
self._device.unregister_device_updated_cb(self.after_update_callback)
self._device.xknx.devices.async_remove(self._device)

View File

@ -312,8 +312,7 @@ class _KnxLight(KnxEntity, LightEntity):
if self._device.supports_brightness:
return self._device.current_brightness
if self._device.current_xyy_color is not None:
_, brightness = self._device.current_xyy_color
return brightness
return self._device.current_xyy_color.brightness
if self._device.supports_color or self._device.supports_rgbw:
rgb, white = self._device.current_color
if rgb is None:
@ -363,8 +362,7 @@ class _KnxLight(KnxEntity, LightEntity):
def xy_color(self) -> tuple[float, float] | None:
"""Return the xy color value [float, float]."""
if self._device.current_xyy_color is not None:
xy_color, _ = self._device.current_xyy_color
return xy_color
return self._device.current_xyy_color.color
return None
@property

View File

@ -11,7 +11,7 @@
"loggers": ["xknx", "xknxproject"],
"quality_scale": "platinum",
"requirements": [
"xknx==2.12.2",
"xknx==3.0.0",
"xknxproject==3.7.1",
"knx-frontend==2024.7.25.204106"
],

View File

@ -9,6 +9,7 @@ from typing import ClassVar, Final
import voluptuous as vol
from xknx.devices.climate import SetpointShiftMode
from xknx.dpt import DPTBase, DPTNumeric
from xknx.dpt.dpt_20 import HVACControllerMode, HVACOperationMode
from xknx.exceptions import ConversionError, CouldNotParseTelegram
from homeassistant.components.binary_sensor import (
@ -51,12 +52,11 @@ from .const import (
CONF_RESPOND_TO_READ,
CONF_STATE_ADDRESS,
CONF_SYNC_STATE,
CONTROLLER_MODES,
KNX_ADDRESS,
PRESET_MODES,
ColorTempModes,
)
from .validation import (
dpt_base_type_validator,
ga_list_validator,
ga_validator,
numeric_type_validator,
@ -173,7 +173,7 @@ class EventSchema:
KNX_EVENT_FILTER_SCHEMA = vol.Schema(
{
vol.Required(KNX_ADDRESS): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_TYPE): sensor_type_validator,
vol.Optional(CONF_TYPE): dpt_base_type_validator,
}
)
@ -409,10 +409,10 @@ class ClimateSchema(KNXPlatformSchema):
CONF_ON_OFF_INVERT, default=DEFAULT_ON_OFF_INVERT
): cv.boolean,
vol.Optional(CONF_OPERATION_MODES): vol.All(
cv.ensure_list, [vol.In(PRESET_MODES)]
cv.ensure_list, [vol.All(vol.Upper, cv.enum(HVACOperationMode))]
),
vol.Optional(CONF_CONTROLLER_MODES): vol.All(
cv.ensure_list, [vol.In(CONTROLLER_MODES)]
cv.ensure_list, [vol.All(vol.Upper, cv.enum(HVACControllerMode))]
),
vol.Optional(
CONF_DEFAULT_CONTROLLER_MODE, default=HVACMode.HEAT
@ -535,11 +535,10 @@ class ExposeSchema(KNXPlatformSchema):
CONF_KNX_EXPOSE_BINARY = "binary"
CONF_KNX_EXPOSE_COOLDOWN = "cooldown"
CONF_KNX_EXPOSE_DEFAULT = "default"
EXPOSE_TIME_TYPES: Final = [
"time",
"date",
"datetime",
]
CONF_TIME = "time"
CONF_DATE = "date"
CONF_DATETIME = "datetime"
EXPOSE_TIME_TYPES: Final = [CONF_TIME, CONF_DATE, CONF_DATETIME]
EXPOSE_TIME_SCHEMA = vol.Schema(
{

View File

@ -81,17 +81,18 @@ class KNXSelect(KnxEntity, SelectEntity, RestoreEntity):
if not self._device.remote_value.readable and (
last_state := await self.async_get_last_state()
):
if last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE):
await self._device.remote_value.update_value(
self._option_payloads.get(last_state.state)
)
if (
last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
and (option := self._option_payloads.get(last_state.state)) is not None
):
self._device.remote_value.update_value(option)
async def after_update_callback(self, device: XknxDevice) -> None:
def after_update_callback(self, device: XknxDevice) -> None:
"""Call after device was updated."""
self._attr_current_option = self.option_from_payload(
self._device.remote_value.value
)
await super().after_update_callback(device)
super().after_update_callback(device)
def option_from_payload(self, payload: int | None) -> str | None:
"""Return the option a given payload is assigned to."""

View File

@ -208,7 +208,7 @@ class KNXSystemSensor(SensorEntity):
return True
return self.knx.xknx.connection_manager.state is XknxConnectionState.CONNECTED
async def after_update_callback(self, _: XknxConnectionState) -> None:
def after_update_callback(self, _: XknxConnectionState) -> None:
"""Call after device was updated."""
self.async_write_ha_state()

View File

@ -8,6 +8,7 @@ from typing import TYPE_CHECKING
import voluptuous as vol
from xknx.dpt import DPTArray, DPTBase, DPTBinary
from xknx.exceptions import ConversionError
from xknx.telegram import Telegram
from xknx.telegram.address import parse_device_group_address
from xknx.telegram.apci import GroupValueRead, GroupValueResponse, GroupValueWrite
@ -31,7 +32,7 @@ from .const import (
SERVICE_KNX_SEND,
)
from .expose import create_knx_exposure
from .schema import ExposeSchema, ga_validator, sensor_type_validator
from .schema import ExposeSchema, dpt_base_type_validator, ga_validator
if TYPE_CHECKING:
from . import KNXModule
@ -95,7 +96,7 @@ SERVICE_KNX_EVENT_REGISTER_SCHEMA = vol.Schema(
cv.ensure_list,
[ga_validator],
),
vol.Optional(CONF_TYPE): sensor_type_validator,
vol.Optional(CONF_TYPE): dpt_base_type_validator,
vol.Optional(SERVICE_KNX_ATTR_REMOVE, default=False): cv.boolean,
}
)
@ -125,10 +126,7 @@ async def service_event_register_modify(hass: HomeAssistant, call: ServiceCall)
transcoder := DPTBase.parse_transcoder(dpt)
):
knx_module.group_address_transcoder.update(
{
_address: transcoder # type: ignore[type-abstract]
for _address in group_addresses
}
{_address: transcoder for _address in group_addresses}
)
for group_address in group_addresses:
if group_address in knx_module.knx_event_callback.group_addresses:
@ -173,7 +171,7 @@ async def service_exposure_register_modify(
f"Could not find exposure for '{group_address}' to remove."
) from err
removed_exposure.shutdown()
removed_exposure.async_remove()
return
if group_address in knx_module.service_exposures:
@ -186,7 +184,7 @@ async def service_exposure_register_modify(
group_address,
replaced_exposure.device.name,
)
replaced_exposure.shutdown()
replaced_exposure.async_remove()
exposure = create_knx_exposure(knx_module.hass, knx_module.xknx, call.data)
knx_module.service_exposures[group_address] = exposure
_LOGGER.debug(
@ -204,7 +202,7 @@ SERVICE_KNX_SEND_SCHEMA = vol.Any(
[ga_validator],
),
vol.Required(SERVICE_KNX_ATTR_PAYLOAD): cv.match_all,
vol.Required(SERVICE_KNX_ATTR_TYPE): sensor_type_validator,
vol.Required(SERVICE_KNX_ATTR_TYPE): dpt_base_type_validator,
vol.Optional(SERVICE_KNX_ATTR_RESPONSE, default=False): cv.boolean,
}
),
@ -237,8 +235,15 @@ async def service_send_to_knx_bus(hass: HomeAssistant, call: ServiceCall) -> Non
if attr_type is not None:
transcoder = DPTBase.parse_transcoder(attr_type)
if transcoder is None:
raise ValueError(f"Invalid type for knx.send service: {attr_type}")
raise ServiceValidationError(
f"Invalid type for knx.send service: {attr_type}"
)
try:
payload = transcoder.to_knx(attr_payload)
except ConversionError as err:
raise ServiceValidationError(
f"Invalid payload for knx.send service: {err}"
) from err
elif isinstance(attr_payload, int):
payload = DPTBinary(attr_payload)
else:

View File

@ -7,6 +7,7 @@ from typing import Final, TypedDict
from xknx import XKNX
from xknx.dpt import DPTArray, DPTBase, DPTBinary
from xknx.dpt.dpt import DPTComplexData, DPTEnumData
from xknx.exceptions import XKNXException
from xknx.telegram import Telegram
from xknx.telegram.apci import GroupValueResponse, GroupValueWrite
@ -93,7 +94,7 @@ class Telegrams:
if self.recent_telegrams:
await self._history_store.async_save(list(self.recent_telegrams))
async def _xknx_telegram_cb(self, telegram: Telegram) -> None:
def _xknx_telegram_cb(self, telegram: Telegram) -> None:
"""Handle incoming and outgoing telegrams from xknx."""
telegram_dict = self.telegram_to_dict(telegram)
self.recent_telegrams.append(telegram_dict)
@ -157,6 +158,11 @@ def decode_telegram_payload(
except XKNXException:
value = "Error decoding value"
if isinstance(value, DPTComplexData):
value = value.as_dict()
elif isinstance(value, DPTEnumData):
value = value.name.lower()
return DecodedTelegramPayload(
dpt_main=transcoder.dpt_main_number,
dpt_sub=transcoder.dpt_sub_number,

View File

@ -3,11 +3,11 @@
from __future__ import annotations
from datetime import time as dt_time
import time as time_time
from typing import Final
from xknx import XKNX
from xknx.devices import DateTime as XknxDateTime
from xknx.devices import TimeDevice as XknxTimeDevice
from xknx.dpt.dpt_10 import KNXTime as XknxTime
from homeassistant import config_entries
from homeassistant.components.time import TimeEntity
@ -45,15 +45,14 @@ async def async_setup_entry(
xknx: XKNX = hass.data[DOMAIN].xknx
config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.TIME]
async_add_entities(KNXTime(xknx, entity_config) for entity_config in config)
async_add_entities(KNXTimeEntity(xknx, entity_config) for entity_config in config)
def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateTime:
def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxTimeDevice:
"""Return a XKNX DateTime object to be used within XKNX."""
return XknxDateTime(
return XknxTimeDevice(
xknx,
name=config[CONF_NAME],
broadcast_type="TIME",
localtime=False,
group_address=config[KNX_ADDRESS],
group_address_state=config.get(CONF_STATE_ADDRESS),
@ -62,10 +61,10 @@ def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateTime:
)
class KNXTime(KnxEntity, TimeEntity, RestoreEntity):
class KNXTimeEntity(KnxEntity, TimeEntity, RestoreEntity):
"""Representation of a KNX time."""
_device: XknxDateTime
_device: XknxTimeDevice
def __init__(self, xknx: XKNX, config: ConfigType) -> None:
"""Initialize a KNX time."""
@ -81,25 +80,15 @@ class KNXTime(KnxEntity, TimeEntity, RestoreEntity):
and (last_state := await self.async_get_last_state()) is not None
and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
):
self._device.remote_value.value = time_time.strptime(
last_state.state, _TIME_TRANSLATION_FORMAT
self._device.remote_value.value = XknxTime.from_time(
dt_time.fromisoformat(last_state.state)
)
@property
def native_value(self) -> dt_time | None:
"""Return the latest value."""
if (time_struct := self._device.remote_value.value) is None:
return None
return dt_time(
hour=time_struct.tm_hour,
minute=time_struct.tm_min,
second=min(time_struct.tm_sec, 59), # account for leap seconds
)
return self._device.value
async def async_set_value(self, value: dt_time) -> None:
"""Change the value."""
time_struct = time_time.strptime(
value.strftime(_TIME_TRANSLATION_FORMAT),
_TIME_TRANSLATION_FORMAT,
)
await self._device.set(time_struct)
await self._device.set(value)

View File

@ -18,7 +18,7 @@ from homeassistant.helpers.typing import ConfigType, VolDictType
from .const import DOMAIN
from .schema import ga_validator
from .telegrams import SIGNAL_KNX_TELEGRAM, TelegramDict, decode_telegram_payload
from .validation import sensor_type_validator
from .validation import dpt_base_type_validator
TRIGGER_TELEGRAM: Final = "telegram"
@ -44,7 +44,7 @@ TELEGRAM_TRIGGER_SCHEMA: VolDictType = {
TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_PLATFORM): PLATFORM_TYPE_TRIGGER_TELEGRAM,
vol.Optional(CONF_TYPE, default=None): vol.Any(sensor_type_validator, None),
vol.Optional(CONF_TYPE, default=None): vol.Any(dpt_base_type_validator, None),
**TELEGRAM_TRIGGER_SCHEMA,
}
)
@ -99,7 +99,7 @@ async def async_attach_trigger(
):
decoded_payload = decode_telegram_payload(
payload=telegram.payload.value, # type: ignore[union-attr] # checked via payload_apci
transcoder=trigger_transcoder, # type: ignore[type-abstract] # parse_transcoder don't return abstract classes
transcoder=trigger_transcoder,
)
# overwrite decoded payload values in telegram_dict
telegram_trigger_data = {**trigger_data, **telegram_dict, **decoded_payload}

View File

@ -30,9 +30,10 @@ def dpt_subclass_validator(dpt_base_class: type[DPTBase]) -> Callable[[Any], str
return dpt_value_validator
dpt_base_type_validator = dpt_subclass_validator(DPTBase) # type: ignore[type-abstract]
numeric_type_validator = dpt_subclass_validator(DPTNumeric) # type: ignore[type-abstract]
sensor_type_validator = dpt_subclass_validator(DPTBase) # type: ignore[type-abstract]
string_type_validator = dpt_subclass_validator(DPTString)
sensor_type_validator = vol.Any(numeric_type_validator, string_type_validator)
def ga_validator(value: Any) -> str | int:

View File

@ -2927,7 +2927,7 @@ xbox-webapi==2.0.11
xiaomi-ble==0.30.2
# homeassistant.components.knx
xknx==2.12.2
xknx==3.0.0
# homeassistant.components.knx
xknxproject==3.7.1

View File

@ -2310,7 +2310,7 @@ xbox-webapi==2.0.11
xiaomi-ble==0.30.2
# homeassistant.components.knx
xknx==2.12.2
xknx==3.0.0
# homeassistant.components.knx
xknxproject==3.7.1

View File

@ -83,7 +83,7 @@ class KNXTestKit:
self.xknx.rate_limit = 0
# set XknxConnectionState.CONNECTED to avoid `unavailable` entities at startup
# and start StateUpdater. This would be awaited on normal startup too.
await self.xknx.connection_manager.connection_state_changed(
self.xknx.connection_manager.connection_state_changed(
state=XknxConnectionState.CONNECTED,
connection_type=XknxConnectionType.TUNNEL_TCP,
)
@ -93,6 +93,7 @@ class KNXTestKit:
mock = Mock()
mock.start = AsyncMock(side_effect=patch_xknx_start)
mock.stop = AsyncMock()
mock.gateway_info = AsyncMock()
return mock
def fish_xknx(*args, **kwargs):
@ -151,8 +152,6 @@ class KNXTestKit:
) -> None:
"""Assert outgoing telegram. One by one in timely order."""
await self.xknx.telegrams.join()
await self.hass.async_block_till_done()
await self.hass.async_block_till_done()
try:
telegram = self._outgoing_telegrams.get_nowait()
except asyncio.QueueEmpty as err:
@ -247,6 +246,7 @@ class KNXTestKit:
GroupValueResponse(payload_value),
source=source,
)
await asyncio.sleep(0) # advance loop to allow StateUpdater to process
async def receive_write(
self,

View File

@ -123,25 +123,21 @@ async def test_binary_sensor_ignore_internal_state(
# receive initial ON telegram
await knx.receive_write("1/1/1", True)
await knx.receive_write("2/2/2", True)
await hass.async_block_till_done()
assert len(events) == 2
# receive second ON telegram - ignore_internal_state shall force state_changed event
await knx.receive_write("1/1/1", True)
await knx.receive_write("2/2/2", True)
await hass.async_block_till_done()
assert len(events) == 3
# receive first OFF telegram
await knx.receive_write("1/1/1", False)
await knx.receive_write("2/2/2", False)
await hass.async_block_till_done()
assert len(events) == 5
# receive second OFF telegram - ignore_internal_state shall force state_changed event
await knx.receive_write("1/1/1", False)
await knx.receive_write("2/2/2", False)
await hass.async_block_till_done()
assert len(events) == 6
@ -166,21 +162,17 @@ async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit) -> No
# receive initial ON telegram
await knx.receive_write("2/2/2", True)
await hass.async_block_till_done()
# no change yet - still in 1 sec context (additional async_block_till_done needed for time change)
assert len(events) == 0
state = hass.states.get("binary_sensor.test")
assert state.state is STATE_OFF
assert state.attributes.get("counter") == 0
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=context_timeout))
await hass.async_block_till_done()
await knx.xknx.task_registry.block_till_done()
# state changed twice after context timeout - once to ON with counter 1 and once to counter 0
state = hass.states.get("binary_sensor.test")
assert state.state is STATE_ON
assert state.attributes.get("counter") == 0
# additional async_block_till_done needed event capture
await hass.async_block_till_done()
assert len(events) == 2
event = events.pop(0).data
assert event.get("new_state").attributes.get("counter") == 1
@ -198,7 +190,6 @@ async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit) -> No
assert state.attributes.get("counter") == 0
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=context_timeout))
await knx.xknx.task_registry.block_till_done()
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.test")
assert state.state is STATE_ON
assert state.attributes.get("counter") == 0
@ -230,12 +221,10 @@ async def test_binary_sensor_reset(hass: HomeAssistant, knx: KNXTestKit) -> None
# receive ON telegram
await knx.receive_write("2/2/2", True)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.test")
assert state.state is STATE_ON
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1))
await hass.async_block_till_done()
await hass.async_block_till_done()
# state reset after after timeout
state = hass.states.get("binary_sensor.test")
assert state.state is STATE_OFF
@ -265,7 +254,6 @@ async def test_binary_sensor_restore_and_respond(hass: HomeAssistant, knx) -> No
await knx.assert_telegram_count(0)
await knx.receive_write(_ADDRESS, False)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.test")
assert state.state is STATE_OFF
@ -296,6 +284,5 @@ async def test_binary_sensor_restore_invert(hass: HomeAssistant, knx) -> None:
# inverted is on, make sure the state is off after it
await knx.receive_write(_ADDRESS, True)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.test")
assert state.state is STATE_OFF

View File

@ -80,12 +80,6 @@ async def test_climate_on_off(
)
}
)
await hass.async_block_till_done()
# read heat/cool state
if heat_cool_ga:
await knx.assert_read("1/2/11")
await knx.receive_response("1/2/11", 0) # cool
# read temperature state
await knx.assert_read("1/2/3")
await knx.receive_response("1/2/3", RAW_FLOAT_20_0)
@ -95,6 +89,10 @@ async def test_climate_on_off(
# read on/off state
await knx.assert_read("1/2/9")
await knx.receive_response("1/2/9", 1)
# read heat/cool state
if heat_cool_ga:
await knx.assert_read("1/2/11")
await knx.receive_response("1/2/11", 0) # cool
# turn off
await hass.services.async_call(
@ -171,18 +169,15 @@ async def test_climate_hvac_mode(
)
}
)
await hass.async_block_till_done()
# read states state updater
await knx.assert_read("1/2/7")
await knx.assert_read("1/2/3")
# StateUpdater initialize state
await knx.receive_response("1/2/7", (0x01,))
await knx.receive_response("1/2/3", RAW_FLOAT_20_0)
# StateUpdater semaphore allows 2 concurrent requests
# read target temperature state
await knx.assert_read("1/2/3")
await knx.assert_read("1/2/5")
# StateUpdater initialize state
await knx.receive_response("1/2/3", RAW_FLOAT_20_0)
await knx.receive_response("1/2/5", RAW_FLOAT_22_0)
await knx.assert_read("1/2/7")
await knx.receive_response("1/2/7", (0x01,))
# turn hvac mode to off - set_hvac_mode() doesn't send to on_off if dedicated hvac mode is available
await hass.services.async_call(
@ -254,17 +249,14 @@ async def test_climate_preset_mode(
)
events = async_capture_events(hass, "state_changed")
await hass.async_block_till_done()
# read states state updater
await knx.assert_read("1/2/7")
await knx.assert_read("1/2/3")
# StateUpdater initialize state
await knx.receive_response("1/2/7", (0x01,))
await knx.receive_response("1/2/3", RAW_FLOAT_21_0)
# StateUpdater semaphore allows 2 concurrent requests
# read target temperature state
await knx.assert_read("1/2/3")
await knx.assert_read("1/2/5")
await knx.receive_response("1/2/3", RAW_FLOAT_21_0)
await knx.receive_response("1/2/5", RAW_FLOAT_22_0)
await knx.assert_read("1/2/7")
await knx.receive_response("1/2/7", (0x01,))
events.clear()
# set preset mode
@ -294,8 +286,6 @@ async def test_climate_preset_mode(
assert len(knx.xknx.devices[1].device_updated_cbs) == 2
# test removing also removes hooks
entity_registry.async_remove("climate.test")
await hass.async_block_till_done()
# If we remove the entity the underlying devices should disappear too
assert len(knx.xknx.devices) == 0
@ -315,18 +305,15 @@ async def test_update_entity(hass: HomeAssistant, knx: KNXTestKit) -> None:
}
)
assert await async_setup_component(hass, "homeassistant", {})
await hass.async_block_till_done()
await hass.async_block_till_done()
# read states state updater
await knx.assert_read("1/2/7")
await knx.assert_read("1/2/3")
# StateUpdater initialize state
await knx.receive_response("1/2/7", (0x01,))
await knx.receive_response("1/2/3", RAW_FLOAT_21_0)
# StateUpdater semaphore allows 2 concurrent requests
await knx.assert_read("1/2/5")
# StateUpdater initialize state
await knx.receive_response("1/2/3", RAW_FLOAT_21_0)
await knx.receive_response("1/2/5", RAW_FLOAT_22_0)
await knx.assert_read("1/2/7")
await knx.receive_response("1/2/7", (0x01,))
# verify update entity retriggers group value reads to the bus
await hass.services.async_call(
@ -354,8 +341,6 @@ async def test_command_value_idle_mode(hass: HomeAssistant, knx: KNXTestKit) ->
}
}
)
await hass.async_block_till_done()
# read states state updater
await knx.assert_read("1/2/3")
await knx.assert_read("1/2/5")

View File

@ -184,7 +184,6 @@ async def test_routing_setup(
CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110",
},
)
await hass.async_block_till_done()
assert result3["type"] is FlowResultType.CREATE_ENTRY
assert result3["title"] == "Routing as 1.1.110"
assert result3["data"] == {
@ -259,7 +258,6 @@ async def test_routing_setup_advanced(
CONF_KNX_LOCAL_IP: "192.168.1.112",
},
)
await hass.async_block_till_done()
assert result3["type"] is FlowResultType.CREATE_ENTRY
assert result3["title"] == "Routing as 1.1.110"
assert result3["data"] == {
@ -350,7 +348,6 @@ async def test_routing_secure_manual_setup(
CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE: 2000,
},
)
await hass.async_block_till_done()
assert secure_routing_manual["type"] is FlowResultType.CREATE_ENTRY
assert secure_routing_manual["title"] == "Secure Routing as 0.0.123"
assert secure_routing_manual["data"] == {
@ -419,7 +416,6 @@ async def test_routing_secure_keyfile(
CONF_KNX_KNXKEY_PASSWORD: "password",
},
)
await hass.async_block_till_done()
assert routing_secure_knxkeys["type"] is FlowResultType.CREATE_ENTRY
assert routing_secure_knxkeys["title"] == "Secure Routing as 0.0.123"
assert routing_secure_knxkeys["data"] == {
@ -552,7 +548,6 @@ async def test_tunneling_setup_manual(
result2["flow_id"],
user_input,
)
await hass.async_block_till_done()
assert result3["type"] is FlowResultType.CREATE_ENTRY
assert result3["title"] == title
assert result3["data"] == config_entry_data
@ -681,7 +676,6 @@ async def test_tunneling_setup_manual_request_description_error(
CONF_PORT: 3671,
},
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Tunneling TCP @ 192.168.0.1"
assert result["data"] == {
@ -772,7 +766,6 @@ async def test_tunneling_setup_for_local_ip(
CONF_KNX_LOCAL_IP: "192.168.1.112",
},
)
await hass.async_block_till_done()
assert result3["type"] is FlowResultType.CREATE_ENTRY
assert result3["title"] == "Tunneling UDP @ 192.168.0.2"
assert result3["data"] == {
@ -821,7 +814,6 @@ async def test_tunneling_setup_for_multiple_found_gateways(
tunnel_flow["flow_id"],
{CONF_KNX_GATEWAY: str(gateway)},
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"] == {
**DEFAULT_ENTRY_DATA,
@ -905,7 +897,6 @@ async def test_form_with_automatic_connection_handling(
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
},
)
await hass.async_block_till_done()
assert result2["type"] is FlowResultType.CREATE_ENTRY
assert result2["title"] == CONF_KNX_AUTOMATIC.capitalize()
assert result2["data"] == {
@ -1040,7 +1031,6 @@ async def test_configure_secure_tunnel_manual(hass: HomeAssistant, knx_setup) ->
CONF_KNX_SECURE_DEVICE_AUTHENTICATION: "device_auth",
},
)
await hass.async_block_till_done()
assert secure_tunnel_manual["type"] is FlowResultType.CREATE_ENTRY
assert secure_tunnel_manual["data"] == {
**DEFAULT_ENTRY_DATA,
@ -1086,7 +1076,6 @@ async def test_configure_secure_knxkeys(hass: HomeAssistant, knx_setup) -> None:
{CONF_KNX_TUNNEL_ENDPOINT_IA: CONF_KNX_AUTOMATIC},
)
await hass.async_block_till_done()
assert secure_knxkeys["type"] is FlowResultType.CREATE_ENTRY
assert secure_knxkeys["data"] == {
**DEFAULT_ENTRY_DATA,
@ -1201,7 +1190,6 @@ async def test_options_flow_connection_type(
CONF_KNX_GATEWAY: str(gateway),
},
)
await hass.async_block_till_done()
assert result3["type"] is FlowResultType.CREATE_ENTRY
assert not result3["data"]
assert mock_config_entry.data == {
@ -1307,7 +1295,6 @@ async def test_options_flow_secure_manual_to_keyfile(
{CONF_KNX_TUNNEL_ENDPOINT_IA: "1.0.1"},
)
await hass.async_block_till_done()
assert secure_knxkeys["type"] is FlowResultType.CREATE_ENTRY
assert mock_config_entry.data == {
**DEFAULT_ENTRY_DATA,
@ -1352,7 +1339,6 @@ async def test_options_communication_settings(
CONF_KNX_TELEGRAM_LOG_SIZE: 3000,
},
)
await hass.async_block_till_done()
assert result2["type"] is FlowResultType.CREATE_ENTRY
assert not result2.get("data")
assert mock_config_entry.data == {
@ -1405,7 +1391,6 @@ async def test_options_update_keyfile(hass: HomeAssistant, knx_setup) -> None:
CONF_KNX_KNXKEY_PASSWORD: "password",
},
)
await hass.async_block_till_done()
assert result2["type"] is FlowResultType.CREATE_ENTRY
assert not result2.get("data")
assert mock_config_entry.data == {
@ -1463,7 +1448,6 @@ async def test_options_keyfile_upload(hass: HomeAssistant, knx_setup) -> None:
CONF_KNX_TUNNEL_ENDPOINT_IA: "1.0.1",
},
)
await hass.async_block_till_done()
assert result3["type"] is FlowResultType.CREATE_ENTRY
assert not result3.get("data")
assert mock_config_entry.data == {

View File

@ -34,7 +34,8 @@ async def test_datetime(hass: HomeAssistant, knx: KNXTestKit) -> None:
)
await knx.assert_write(
test_address,
(0x78, 0x01, 0x01, 0x73, 0x04, 0x05, 0x20, 0x80),
# service call in UTC, telegram in local time
(0x78, 0x01, 0x01, 0x13, 0x04, 0x05, 0x24, 0x00),
)
state = hass.states.get("datetime.test")
assert state.state == "2020-01-02T03:04:05+00:00"
@ -74,7 +75,7 @@ async def test_date_restore_and_respond(hass: HomeAssistant, knx: KNXTestKit) ->
await knx.receive_read(test_address)
await knx.assert_response(
test_address,
(0x7A, 0x03, 0x03, 0x84, 0x04, 0x05, 0x20, 0x80),
(0x7A, 0x03, 0x03, 0x04, 0x04, 0x05, 0x24, 0x00),
)
# don't respond to passive address

View File

@ -391,7 +391,6 @@ async def test_invalid_device_trigger(
]
},
)
await hass.async_block_till_done()
assert (
"Unnamed automation failed to setup triggers and has been disabled: "
"extra keys not allowed @ data['invalid']. Got None"

View File

@ -31,7 +31,6 @@ async def test_knx_event(
events = async_capture_events(hass, "knx_event")
async def test_event_data(address, payload, value=None):
await hass.async_block_till_done()
assert len(events) == 1
event = events.pop()
assert event.data["data"] == payload
@ -69,7 +68,6 @@ async def test_knx_event(
)
# no event received
await hass.async_block_till_done()
assert len(events) == 0
# receive telegrams for group addresses matching the filter
@ -101,7 +99,6 @@ async def test_knx_event(
await knx.receive_write("0/5/0", True)
await knx.receive_write("1/7/0", True)
await knx.receive_write("2/6/6", True)
await hass.async_block_till_done()
assert len(events) == 0
# receive telegrams with wrong payload length

View File

@ -1,9 +1,8 @@
"""Test KNX expose."""
from datetime import timedelta
import time
from unittest.mock import patch
from freezegun import freeze_time
import pytest
from homeassistant.components.knx import CONF_KNX_EXPOSE, DOMAIN, KNX_ADDRESS
@ -327,25 +326,32 @@ async def test_expose_conversion_exception(
)
@patch("time.localtime")
@freeze_time("2022-1-7 9:13:14")
@pytest.mark.parametrize(
("time_type", "raw"),
[
("time", (0xA9, 0x0D, 0x0E)), # localtime includes day of week
("date", (0x07, 0x01, 0x16)),
("datetime", (0x7A, 0x1, 0x7, 0xA9, 0xD, 0xE, 0x20, 0xC0)),
],
)
async def test_expose_with_date(
localtime, hass: HomeAssistant, knx: KNXTestKit
hass: HomeAssistant, knx: KNXTestKit, time_type: str, raw: tuple[int, ...]
) -> None:
"""Test an expose with a date."""
localtime.return_value = time.struct_time([2022, 1, 7, 9, 13, 14, 6, 0, 0])
await knx.setup_integration(
{
CONF_KNX_EXPOSE: {
CONF_TYPE: "datetime",
CONF_TYPE: time_type,
KNX_ADDRESS: "1/1/8",
}
}
)
await knx.assert_write("1/1/8", (0x7A, 0x1, 0x7, 0xE9, 0xD, 0xE, 0x20, 0x80))
await knx.assert_write("1/1/8", raw)
await knx.receive_read("1/1/8")
await knx.assert_response("1/1/8", (0x7A, 0x1, 0x7, 0xE9, 0xD, 0xE, 0x20, 0x80))
await knx.assert_response("1/1/8", raw)
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1

View File

@ -284,7 +284,6 @@ async def test_async_remove_entry(
assert await hass.config_entries.async_remove(config_entry.entry_id)
assert unlink_mock.call_count == 3
rmdir_mock.assert_called_once()
await hass.async_block_till_done()
assert hass.config_entries.async_entries() == []
assert config_entry.state is ConfigEntryState.NOT_LOADED

View File

@ -66,25 +66,19 @@ async def test_diagnostic_entities(
):
assert hass.states.get(entity_id).state == test_state
await knx.xknx.connection_manager.connection_state_changed(
knx.xknx.connection_manager.connection_state_changed(
state=XknxConnectionState.DISCONNECTED
)
await hass.async_block_till_done()
await hass.async_block_till_done()
await hass.async_block_till_done()
await hass.async_block_till_done()
assert len(events) == 4 # 3 not always_available + 3 force_update - 2 disabled
events.clear()
knx.xknx.current_address = IndividualAddress("1.1.1")
await knx.xknx.connection_manager.connection_state_changed(
knx.xknx.connection_manager.connection_state_changed(
state=XknxConnectionState.CONNECTED,
connection_type=XknxConnectionType.TUNNEL_UDP,
)
await hass.async_block_till_done()
await hass.async_block_till_done()
await hass.async_block_till_done()
await hass.async_block_till_done()
assert len(events) == 6 # all diagnostic sensors - counters are reset on connect
for entity_id, test_state in (
@ -111,7 +105,6 @@ async def test_removed_entity(
"sensor.knx_interface_connection_established",
disabled_by=er.RegistryEntryDisabler.USER,
)
await hass.async_block_till_done()
unregister_mock.assert_called_once()

View File

@ -92,9 +92,7 @@ async def test_light_brightness(hass: HomeAssistant, knx: KNXTestKit) -> None:
)
# StateUpdater initialize state
await knx.assert_read(test_brightness_state)
await knx.xknx.connection_manager.connection_state_changed(
XknxConnectionState.CONNECTED
)
knx.xknx.connection_manager.connection_state_changed(XknxConnectionState.CONNECTED)
# turn on light via brightness
await hass.services.async_call(
"light",

View File

@ -21,17 +21,13 @@ async def test_legacy_notify_service_simple(
}
}
)
await hass.async_block_till_done()
await hass.services.async_call(
"notify", "notify", {"target": "test", "message": "I love KNX"}, blocking=True
)
await knx.assert_write(
"1/0/0",
(73, 32, 108, 111, 118, 101, 32, 75, 78, 88, 0, 0, 0, 0),
)
await hass.services.async_call(
"notify",
"notify",
@ -41,7 +37,6 @@ async def test_legacy_notify_service_simple(
},
blocking=True,
)
await knx.assert_write(
"1/0/0",
(73, 32, 108, 111, 118, 101, 32, 75, 78, 88, 44, 32, 98, 117),
@ -68,12 +63,9 @@ async def test_legacy_notify_service_multiple_sends_to_all_with_different_encodi
]
}
)
await hass.async_block_till_done()
await hass.services.async_call(
"notify", "notify", {"message": "Gänsefüßchen"}, blocking=True
)
await knx.assert_write(
"1/0/0",
# "G?nsef??chen"
@ -95,7 +87,6 @@ async def test_notify_simple(hass: HomeAssistant, knx: KNXTestKit) -> None:
}
}
)
await hass.services.async_call(
notify.DOMAIN,
notify.SERVICE_SEND_MESSAGE,

View File

@ -68,25 +68,21 @@ async def test_always_callback(hass: HomeAssistant, knx: KNXTestKit) -> None:
# receive initial telegram
await knx.receive_write("1/1/1", (0x42,))
await knx.receive_write("2/2/2", (0x42,))
await hass.async_block_till_done()
assert len(events) == 2
# receive second telegram with identical payload
# always_callback shall force state_changed event
await knx.receive_write("1/1/1", (0x42,))
await knx.receive_write("2/2/2", (0x42,))
await hass.async_block_till_done()
assert len(events) == 3
# receive telegram with different payload
await knx.receive_write("1/1/1", (0xFA,))
await knx.receive_write("2/2/2", (0xFA,))
await hass.async_block_till_done()
assert len(events) == 5
# receive telegram with second payload again
# always_callback shall force state_changed event
await knx.receive_write("1/1/1", (0xFA,))
await knx.receive_write("2/2/2", (0xFA,))
await hass.async_block_till_done()
assert len(events) == 6

View File

@ -154,7 +154,6 @@ async def test_event_register(hass: HomeAssistant, knx: KNXTestKit) -> None:
# no event registered
await knx.receive_write(test_address, True)
await hass.async_block_till_done()
assert len(events) == 0
# register event with `type`
@ -165,7 +164,6 @@ async def test_event_register(hass: HomeAssistant, knx: KNXTestKit) -> None:
blocking=True,
)
await knx.receive_write(test_address, (0x04, 0xD2))
await hass.async_block_till_done()
assert len(events) == 1
typed_event = events.pop()
assert typed_event.data["data"] == (0x04, 0xD2)
@ -179,7 +177,6 @@ async def test_event_register(hass: HomeAssistant, knx: KNXTestKit) -> None:
blocking=True,
)
await knx.receive_write(test_address, True)
await hass.async_block_till_done()
assert len(events) == 0
# register event without `type`
@ -188,7 +185,6 @@ async def test_event_register(hass: HomeAssistant, knx: KNXTestKit) -> None:
)
await knx.receive_write(test_address, True)
await knx.receive_write(test_address, False)
await hass.async_block_till_done()
assert len(events) == 2
untyped_event_2 = events.pop()
assert untyped_event_2.data["data"] is False

View File

@ -334,7 +334,6 @@ async def test_invalid_trigger(
]
},
)
await hass.async_block_till_done()
assert (
"Unnamed automation failed to setup triggers and has been disabled: "
"extra keys not allowed @ data['invalid']. Got None"

View File

@ -45,12 +45,12 @@ async def test_weather(hass: HomeAssistant, knx: KNXTestKit) -> None:
# brightness
await knx.assert_read("1/1/6")
await knx.receive_response("1/1/6", (0x7C, 0x5E))
await knx.assert_read("1/1/8")
await knx.receive_response("1/1/6", (0x7C, 0x5E))
await knx.receive_response("1/1/8", (0x7C, 0x5E))
await knx.assert_read("1/1/5")
await knx.assert_read("1/1/7")
await knx.receive_response("1/1/7", (0x7C, 0x5E))
await knx.assert_read("1/1/5")
await knx.receive_response("1/1/5", (0x7C, 0x5E))
# wind speed
@ -64,10 +64,10 @@ async def test_weather(hass: HomeAssistant, knx: KNXTestKit) -> None:
# alarms
await knx.assert_read("1/1/2")
await knx.receive_response("1/1/2", False)
await knx.assert_read("1/1/3")
await knx.receive_response("1/1/3", False)
await knx.assert_read("1/1/1")
await knx.assert_read("1/1/3")
await knx.receive_response("1/1/1", False)
await knx.receive_response("1/1/3", False)
# day night
await knx.assert_read("1/1/12")