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

View File

@ -75,7 +75,7 @@ class KNXBinarySensor(KnxEntity, BinarySensorEntity, RestoreEntity):
if ( if (
last_state := await self.async_get_last_state() last_state := await self.async_get_last_state()
) and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE): ) 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 @property
def is_on(self) -> bool: def is_on(self) -> bool:

View File

@ -6,7 +6,7 @@ from typing import Any
from xknx import XKNX from xknx import XKNX
from xknx.devices import Climate as XknxClimate, ClimateMode as XknxClimateMode 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 import config_entries
from homeassistant.components.climate import ( from homeassistant.components.climate import (
@ -80,7 +80,7 @@ def _create_climate(xknx: XKNX, config: ConfigType) -> XknxClimate:
group_address_operation_mode_protection=config.get( group_address_operation_mode_protection=config.get(
ClimateSchema.CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS 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 ClimateSchema.CONF_OPERATION_MODE_NIGHT_ADDRESS
), ),
group_address_operation_mode_comfort=config.get( group_address_operation_mode_comfort=config.get(
@ -199,10 +199,12 @@ class KNXClimate(KnxEntity, ClimateEntity):
self.async_write_ha_state() self.async_write_ha_state()
return return
if self._device.mode is not None and self._device.mode.supports_controller_mode: if (
knx_controller_mode = HVACControllerMode( self._device.mode is not None
CONTROLLER_MODES_INV.get(self._last_hvac_mode) 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) await self._device.mode.set_controller_mode(knx_controller_mode)
self.async_write_ha_state() self.async_write_ha_state()
@ -234,7 +236,7 @@ class KNXClimate(KnxEntity, ClimateEntity):
return HVACMode.OFF return HVACMode.OFF
if self._device.mode is not None and self._device.mode.supports_controller_mode: if self._device.mode is not None and self._device.mode.supports_controller_mode:
hvac_mode = CONTROLLER_MODES.get( 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: if hvac_mode is not HVACMode.OFF:
self._last_hvac_mode = hvac_mode self._last_hvac_mode = hvac_mode
@ -247,7 +249,7 @@ class KNXClimate(KnxEntity, ClimateEntity):
ha_controller_modes: list[HVACMode | None] = [] ha_controller_modes: list[HVACMode | None] = []
if self._device.mode is not None: if self._device.mode is not None:
ha_controller_modes.extend( 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 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: async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set controller mode.""" """Set controller mode."""
if self._device.mode is not None and self._device.mode.supports_controller_mode: if self._device.mode is not None and self._device.mode.supports_controller_mode:
knx_controller_mode = HVACControllerMode( knx_controller_mode = CONTROLLER_MODES_INV.get(hvac_mode)
CONTROLLER_MODES_INV.get(hvac_mode)
)
if knx_controller_mode in self._device.mode.controller_modes: if knx_controller_mode in self._device.mode.controller_modes:
await self._device.mode.set_controller_mode(knx_controller_mode) await self._device.mode.set_controller_mode(knx_controller_mode)
@ -298,7 +298,7 @@ class KNXClimate(KnxEntity, ClimateEntity):
Requires ClimateEntityFeature.PRESET_MODE. Requires ClimateEntityFeature.PRESET_MODE.
""" """
if self._device.mode is not None and self._device.mode.supports_operation_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 return None
@property @property
@ -311,15 +311,18 @@ class KNXClimate(KnxEntity, ClimateEntity):
return None return None
presets = [ presets = [
PRESET_MODES.get(operation_mode.value) PRESET_MODES.get(operation_mode)
for operation_mode in self._device.mode.operation_modes for operation_mode in self._device.mode.operation_modes
] ]
return list(filter(None, presets)) return list(filter(None, presets))
async def async_set_preset_mode(self, preset_mode: str) -> None: async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode.""" """Set new preset mode."""
if self._device.mode is not None and self._device.mode.supports_operation_mode: if (
knx_operation_mode = HVACOperationMode(PRESET_MODES_INV.get(preset_mode)) 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) await self._device.mode.set_operation_mode(knx_operation_mode)
self.async_write_ha_state() self.async_write_ha_state()
@ -333,7 +336,15 @@ class KNXClimate(KnxEntity, ClimateEntity):
return attr return attr
async def async_added_to_hass(self) -> None: 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() await super().async_added_to_hass()
if self._device.mode is not None: if self._device.mode is not None:
self._device.mode.register_device_updated_cb(self.after_update_callback) 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 enum import Enum
from typing import Final, TypedDict from typing import Final, TypedDict
from xknx.dpt.dpt_20 import HVACControllerMode, HVACOperationMode
from xknx.telegram import Telegram from xknx.telegram import Telegram
from homeassistant.components.climate import ( 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. # Map KNX controller modes to HA modes. This list might not be complete.
CONTROLLER_MODES: Final = { CONTROLLER_MODES: Final = {
# Map DPT 20.105 HVAC control modes # Map DPT 20.105 HVAC control modes
"Auto": HVACMode.AUTO, HVACControllerMode.AUTO: HVACMode.AUTO,
"Heat": HVACMode.HEAT, HVACControllerMode.HEAT: HVACMode.HEAT,
"Cool": HVACMode.COOL, HVACControllerMode.COOL: HVACMode.COOL,
"Off": HVACMode.OFF, HVACControllerMode.OFF: HVACMode.OFF,
"Fan only": HVACMode.FAN_ONLY, HVACControllerMode.FAN_ONLY: HVACMode.FAN_ONLY,
"Dry": HVACMode.DRY, HVACControllerMode.DEHUMIDIFICATION: HVACMode.DRY,
} }
CURRENT_HVAC_ACTIONS: Final = { CURRENT_HVAC_ACTIONS: Final = {
@ -176,9 +177,9 @@ CURRENT_HVAC_ACTIONS: Final = {
PRESET_MODES: Final = { PRESET_MODES: Final = {
# Map DPT 20.102 HVAC operating modes to HA presets # Map DPT 20.102 HVAC operating modes to HA presets
"Auto": PRESET_NONE, HVACOperationMode.AUTO: PRESET_NONE,
"Frost Protection": PRESET_ECO, HVACOperationMode.BUILDING_PROTECTION: PRESET_ECO,
"Night": PRESET_SLEEP, HVACOperationMode.ECONOMY: PRESET_SLEEP,
"Standby": PRESET_AWAY, HVACOperationMode.STANDBY: PRESET_AWAY,
"Comfort": PRESET_COMFORT, HVACOperationMode.COMFORT: PRESET_COMFORT,
} }

View File

@ -3,11 +3,10 @@
from __future__ import annotations from __future__ import annotations
from datetime import date as dt_date from datetime import date as dt_date
import time
from typing import Final
from xknx import XKNX 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 import config_entries
from homeassistant.components.date import DateEntity from homeassistant.components.date import DateEntity
@ -33,8 +32,6 @@ from .const import (
) )
from .knx_entity import KnxEntity from .knx_entity import KnxEntity
_DATE_TRANSLATION_FORMAT: Final = "%Y-%m-%d"
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
@ -45,15 +42,14 @@ async def async_setup_entry(
xknx: XKNX = hass.data[DOMAIN].xknx xknx: XKNX = hass.data[DOMAIN].xknx
config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.DATE] 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 a XKNX DateTime object to be used within XKNX."""
return XknxDateTime( return XknxDateDevice(
xknx, xknx,
name=config[CONF_NAME], name=config[CONF_NAME],
broadcast_type="DATE",
localtime=False, localtime=False,
group_address=config[KNX_ADDRESS], group_address=config[KNX_ADDRESS],
group_address_state=config.get(CONF_STATE_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.""" """Representation of a KNX date."""
_device: XknxDateTime _device: XknxDateDevice
def __init__(self, xknx: XKNX, config: ConfigType) -> None: def __init__(self, xknx: XKNX, config: ConfigType) -> None:
"""Initialize a KNX time.""" """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 := await self.async_get_last_state()) is not None
and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
): ):
self._device.remote_value.value = time.strptime( self._device.remote_value.value = XKNXDate.from_date(
last_state.state, _DATE_TRANSLATION_FORMAT dt_date.fromisoformat(last_state.state)
) )
@property @property
def native_value(self) -> dt_date | None: def native_value(self) -> dt_date | None:
"""Return the latest value.""" """Return the latest value."""
if (time_struct := self._device.remote_value.value) is None: return self._device.value
return None
return dt_date(
year=time_struct.tm_year,
month=time_struct.tm_mon,
day=time_struct.tm_mday,
)
async def async_set_value(self, value: dt_date) -> None: async def async_set_value(self, value: dt_date) -> None:
"""Change the value.""" """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 datetime import datetime
from xknx import XKNX 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 import config_entries
from homeassistant.components.datetime import DateTimeEntity from homeassistant.components.datetime import DateTimeEntity
@ -42,15 +43,16 @@ async def async_setup_entry(
xknx: XKNX = hass.data[DOMAIN].xknx xknx: XKNX = hass.data[DOMAIN].xknx
config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.DATETIME] 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 a XKNX DateTime object to be used within XKNX."""
return XknxDateTime( return XknxDateTimeDevice(
xknx, xknx,
name=config[CONF_NAME], name=config[CONF_NAME],
broadcast_type="DATETIME",
localtime=False, localtime=False,
group_address=config[KNX_ADDRESS], group_address=config[KNX_ADDRESS],
group_address_state=config.get(CONF_STATE_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.""" """Representation of a KNX datetime."""
_device: XknxDateTime _device: XknxDateTimeDevice
def __init__(self, xknx: XKNX, config: ConfigType) -> None: def __init__(self, xknx: XKNX, config: ConfigType) -> None:
"""Initialize a KNX time.""" """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 := await self.async_get_last_state()) is not None
and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
): ):
self._device.remote_value.value = ( self._device.remote_value.value = XKNXDateTime.from_datetime(
datetime.fromisoformat(last_state.state) datetime.fromisoformat(last_state.state).astimezone(
.astimezone(dt_util.get_default_time_zone()) dt_util.get_default_time_zone()
.timetuple() )
) )
@property @property
def native_value(self) -> datetime | None: def native_value(self) -> datetime | None:
"""Return the latest value.""" """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 None
return datetime( return naive_dt.replace(tzinfo=dt_util.get_default_time_zone())
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(),
)
async def async_set_value(self, value: datetime) -> None: async def async_set_value(self, value: datetime) -> None:
"""Change the value.""" """Change the value."""
await self._device.set( await self._device.set(value.astimezone(dt_util.get_default_time_zone()))
value.astimezone(dt_util.get_default_time_zone()).timetuple()
)

View File

@ -19,6 +19,7 @@ class KNXInterfaceDevice:
def __init__(self, hass: HomeAssistant, entry: ConfigEntry, xknx: XKNX) -> None: def __init__(self, hass: HomeAssistant, entry: ConfigEntry, xknx: XKNX) -> None:
"""Initialize interface device class.""" """Initialize interface device class."""
self.hass = hass
self.device_registry = dr.async_get(hass) self.device_registry = dr.async_get(hass)
self.gateway_descriptor: GatewayDescriptor | None = None self.gateway_descriptor: GatewayDescriptor | None = None
self.xknx = xknx self.xknx = xknx
@ -46,7 +47,7 @@ class KNXInterfaceDevice:
else None, 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.""" """Call invoked after a KNX connection state change was received."""
if state is XknxConnectionState.CONNECTED: 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 import logging
from xknx import XKNX 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.dpt import DPTNumeric, DPTString
from xknx.exceptions import ConversionError from xknx.exceptions import ConversionError
from xknx.remote_value import RemoteValueSensor from xknx.remote_value import RemoteValueSensor
@ -60,6 +60,7 @@ def create_knx_exposure(
xknx=xknx, xknx=xknx,
config=config, config=config,
) )
exposure.async_register()
return exposure return exposure
@ -87,25 +88,23 @@ class KNXExposeSensor:
self.value_template.hass = hass 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 = ExposeSensor(
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(
xknx=self.xknx, xknx=self.xknx,
name=name, name=f"{self.entity_id}__{self.expose_attribute or "state"}",
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,
cooldown=config[ExposeSchema.CONF_KNX_EXPOSE_COOLDOWN], cooldown=config[ExposeSchema.CONF_KNX_EXPOSE_COOLDOWN],
) )
@callback
def async_register(self) -> None:
"""Register listener."""
self._remove_listener = async_track_state_change_event( self._remove_listener = async_track_state_change_event(
self.hass, [self.entity_id], self._async_entity_changed self.hass, [self.entity_id], self._async_entity_changed
) )
return device self.xknx.devices.async_add(self.device)
self._init_expose_state()
@callback @callback
def _init_expose_state(self) -> None: def _init_expose_state(self) -> None:
@ -118,12 +117,12 @@ class KNXExposeSensor:
_LOGGER.exception("Error during sending of expose sensor value") _LOGGER.exception("Error during sending of expose sensor value")
@callback @callback
def shutdown(self) -> None: def async_remove(self) -> None:
"""Prepare for deletion.""" """Prepare for deletion."""
if self._remove_listener is not None: if self._remove_listener is not None:
self._remove_listener() self._remove_listener()
self._remove_listener = None 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: def _get_expose_value(self, state: State | None) -> bool | int | float | str | None:
"""Extract value from state.""" """Extract value from state."""
@ -196,21 +195,28 @@ class KNXExposeTime:
def __init__(self, xknx: XKNX, config: ConfigType) -> None: def __init__(self, xknx: XKNX, config: ConfigType) -> None:
"""Initialize of Expose class.""" """Initialize of Expose class."""
self.xknx = xknx 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] 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, self.xknx,
name=expose_type.capitalize(), name=expose_type.capitalize(),
broadcast_type=expose_type.upper(),
localtime=True, localtime=True,
group_address=config[KNX_ADDRESS], group_address=config[KNX_ADDRESS],
) )
@callback @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.""" """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.""" """Request a state update from KNX bus."""
await self._device.sync() 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.""" """Call after device was updated."""
self.async_write_ha_state() self.async_write_ha_state()
async def async_added_to_hass(self) -> None: 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) self._device.register_device_updated_cb(self.after_update_callback)
# will remove all callbacks and xknx tasks self._device.xknx.devices.async_add(self._device)
self.async_on_remove(self._device.shutdown)
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: if self._device.supports_brightness:
return self._device.current_brightness return self._device.current_brightness
if self._device.current_xyy_color is not None: if self._device.current_xyy_color is not None:
_, brightness = self._device.current_xyy_color return self._device.current_xyy_color.brightness
return brightness
if self._device.supports_color or self._device.supports_rgbw: if self._device.supports_color or self._device.supports_rgbw:
rgb, white = self._device.current_color rgb, white = self._device.current_color
if rgb is None: if rgb is None:
@ -363,8 +362,7 @@ class _KnxLight(KnxEntity, LightEntity):
def xy_color(self) -> tuple[float, float] | None: def xy_color(self) -> tuple[float, float] | None:
"""Return the xy color value [float, float].""" """Return the xy color value [float, float]."""
if self._device.current_xyy_color is not None: if self._device.current_xyy_color is not None:
xy_color, _ = self._device.current_xyy_color return self._device.current_xyy_color.color
return xy_color
return None return None
@property @property

View File

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

View File

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

View File

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

View File

@ -208,7 +208,7 @@ class KNXSystemSensor(SensorEntity):
return True return True
return self.knx.xknx.connection_manager.state is XknxConnectionState.CONNECTED 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.""" """Call after device was updated."""
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -8,6 +8,7 @@ from typing import TYPE_CHECKING
import voluptuous as vol import voluptuous as vol
from xknx.dpt import DPTArray, DPTBase, DPTBinary from xknx.dpt import DPTArray, DPTBase, DPTBinary
from xknx.exceptions import ConversionError
from xknx.telegram import Telegram from xknx.telegram import Telegram
from xknx.telegram.address import parse_device_group_address from xknx.telegram.address import parse_device_group_address
from xknx.telegram.apci import GroupValueRead, GroupValueResponse, GroupValueWrite from xknx.telegram.apci import GroupValueRead, GroupValueResponse, GroupValueWrite
@ -31,7 +32,7 @@ from .const import (
SERVICE_KNX_SEND, SERVICE_KNX_SEND,
) )
from .expose import create_knx_exposure 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: if TYPE_CHECKING:
from . import KNXModule from . import KNXModule
@ -95,7 +96,7 @@ SERVICE_KNX_EVENT_REGISTER_SCHEMA = vol.Schema(
cv.ensure_list, cv.ensure_list,
[ga_validator], [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, 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) transcoder := DPTBase.parse_transcoder(dpt)
): ):
knx_module.group_address_transcoder.update( knx_module.group_address_transcoder.update(
{ {_address: transcoder for _address in group_addresses}
_address: transcoder # type: ignore[type-abstract]
for _address in group_addresses
}
) )
for group_address in group_addresses: for group_address in group_addresses:
if group_address in knx_module.knx_event_callback.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." f"Could not find exposure for '{group_address}' to remove."
) from err ) from err
removed_exposure.shutdown() removed_exposure.async_remove()
return return
if group_address in knx_module.service_exposures: if group_address in knx_module.service_exposures:
@ -186,7 +184,7 @@ async def service_exposure_register_modify(
group_address, group_address,
replaced_exposure.device.name, replaced_exposure.device.name,
) )
replaced_exposure.shutdown() replaced_exposure.async_remove()
exposure = create_knx_exposure(knx_module.hass, knx_module.xknx, call.data) exposure = create_knx_exposure(knx_module.hass, knx_module.xknx, call.data)
knx_module.service_exposures[group_address] = exposure knx_module.service_exposures[group_address] = exposure
_LOGGER.debug( _LOGGER.debug(
@ -204,7 +202,7 @@ SERVICE_KNX_SEND_SCHEMA = vol.Any(
[ga_validator], [ga_validator],
), ),
vol.Required(SERVICE_KNX_ATTR_PAYLOAD): cv.match_all, 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, 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: if attr_type is not None:
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 ServiceValidationError(
payload = transcoder.to_knx(attr_payload) 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): elif isinstance(attr_payload, int):
payload = DPTBinary(attr_payload) payload = DPTBinary(attr_payload)
else: else:

View File

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

View File

@ -3,11 +3,11 @@
from __future__ import annotations from __future__ import annotations
from datetime import time as dt_time from datetime import time as dt_time
import time as time_time
from typing import Final from typing import Final
from xknx import XKNX 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 import config_entries
from homeassistant.components.time import TimeEntity from homeassistant.components.time import TimeEntity
@ -45,15 +45,14 @@ async def async_setup_entry(
xknx: XKNX = hass.data[DOMAIN].xknx xknx: XKNX = hass.data[DOMAIN].xknx
config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.TIME] 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 a XKNX DateTime object to be used within XKNX."""
return XknxDateTime( return XknxTimeDevice(
xknx, xknx,
name=config[CONF_NAME], name=config[CONF_NAME],
broadcast_type="TIME",
localtime=False, localtime=False,
group_address=config[KNX_ADDRESS], group_address=config[KNX_ADDRESS],
group_address_state=config.get(CONF_STATE_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.""" """Representation of a KNX time."""
_device: XknxDateTime _device: XknxTimeDevice
def __init__(self, xknx: XKNX, config: ConfigType) -> None: def __init__(self, xknx: XKNX, config: ConfigType) -> None:
"""Initialize a KNX time.""" """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 := await self.async_get_last_state()) is not None
and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
): ):
self._device.remote_value.value = time_time.strptime( self._device.remote_value.value = XknxTime.from_time(
last_state.state, _TIME_TRANSLATION_FORMAT dt_time.fromisoformat(last_state.state)
) )
@property @property
def native_value(self) -> dt_time | None: def native_value(self) -> dt_time | None:
"""Return the latest value.""" """Return the latest value."""
if (time_struct := self._device.remote_value.value) is None: return self._device.value
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
)
async def async_set_value(self, value: dt_time) -> None: async def async_set_value(self, value: dt_time) -> None:
"""Change the value.""" """Change the value."""
time_struct = time_time.strptime( await self._device.set(value)
value.strftime(_TIME_TRANSLATION_FORMAT),
_TIME_TRANSLATION_FORMAT,
)
await self._device.set(time_struct)

View File

@ -18,7 +18,7 @@ from homeassistant.helpers.typing import ConfigType, VolDictType
from .const import DOMAIN from .const import DOMAIN
from .schema import ga_validator from .schema import ga_validator
from .telegrams import SIGNAL_KNX_TELEGRAM, TelegramDict, decode_telegram_payload 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" TRIGGER_TELEGRAM: Final = "telegram"
@ -44,7 +44,7 @@ TELEGRAM_TRIGGER_SCHEMA: VolDictType = {
TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
{ {
vol.Required(CONF_PLATFORM): PLATFORM_TYPE_TRIGGER_TELEGRAM, 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, **TELEGRAM_TRIGGER_SCHEMA,
} }
) )
@ -99,7 +99,7 @@ async def async_attach_trigger(
): ):
decoded_payload = decode_telegram_payload( decoded_payload = decode_telegram_payload(
payload=telegram.payload.value, # type: ignore[union-attr] # checked via payload_apci 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 # overwrite decoded payload values in telegram_dict
telegram_trigger_data = {**trigger_data, **telegram_dict, **decoded_payload} 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 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] 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) 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: def ga_validator(value: Any) -> str | int:

View File

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

View File

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

View File

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

View File

@ -123,25 +123,21 @@ async def test_binary_sensor_ignore_internal_state(
# receive initial ON telegram # receive initial ON telegram
await knx.receive_write("1/1/1", True) await knx.receive_write("1/1/1", True)
await knx.receive_write("2/2/2", True) await knx.receive_write("2/2/2", True)
await hass.async_block_till_done()
assert len(events) == 2 assert len(events) == 2
# receive second ON telegram - ignore_internal_state shall force state_changed event # receive second ON telegram - ignore_internal_state shall force state_changed event
await knx.receive_write("1/1/1", True) await knx.receive_write("1/1/1", True)
await knx.receive_write("2/2/2", True) await knx.receive_write("2/2/2", True)
await hass.async_block_till_done()
assert len(events) == 3 assert len(events) == 3
# receive first OFF telegram # receive first OFF telegram
await knx.receive_write("1/1/1", False) await knx.receive_write("1/1/1", False)
await knx.receive_write("2/2/2", False) await knx.receive_write("2/2/2", False)
await hass.async_block_till_done()
assert len(events) == 5 assert len(events) == 5
# receive second OFF telegram - ignore_internal_state shall force state_changed event # receive second OFF telegram - ignore_internal_state shall force state_changed event
await knx.receive_write("1/1/1", False) await knx.receive_write("1/1/1", False)
await knx.receive_write("2/2/2", False) await knx.receive_write("2/2/2", False)
await hass.async_block_till_done()
assert len(events) == 6 assert len(events) == 6
@ -166,21 +162,17 @@ async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit) -> No
# receive initial ON telegram # receive initial ON telegram
await knx.receive_write("2/2/2", True) 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) # no change yet - still in 1 sec context (additional async_block_till_done needed for time change)
assert len(events) == 0 assert len(events) == 0
state = hass.states.get("binary_sensor.test") state = hass.states.get("binary_sensor.test")
assert state.state is STATE_OFF assert state.state is STATE_OFF
assert state.attributes.get("counter") == 0 assert state.attributes.get("counter") == 0
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=context_timeout)) 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() 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 changed twice after context timeout - once to ON with counter 1 and once to counter 0
state = hass.states.get("binary_sensor.test") state = hass.states.get("binary_sensor.test")
assert state.state is STATE_ON assert state.state is STATE_ON
assert state.attributes.get("counter") == 0 assert state.attributes.get("counter") == 0
# additional async_block_till_done needed event capture
await hass.async_block_till_done()
assert len(events) == 2 assert len(events) == 2
event = events.pop(0).data event = events.pop(0).data
assert event.get("new_state").attributes.get("counter") == 1 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 assert state.attributes.get("counter") == 0
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=context_timeout)) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=context_timeout))
await knx.xknx.task_registry.block_till_done() await knx.xknx.task_registry.block_till_done()
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.test") state = hass.states.get("binary_sensor.test")
assert state.state is STATE_ON assert state.state is STATE_ON
assert state.attributes.get("counter") == 0 assert state.attributes.get("counter") == 0
@ -230,12 +221,10 @@ async def test_binary_sensor_reset(hass: HomeAssistant, knx: KNXTestKit) -> None
# receive ON telegram # receive ON telegram
await knx.receive_write("2/2/2", True) await knx.receive_write("2/2/2", True)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.test") state = hass.states.get("binary_sensor.test")
assert state.state is STATE_ON assert state.state is STATE_ON
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1)) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1))
await hass.async_block_till_done() await hass.async_block_till_done()
await hass.async_block_till_done()
# state reset after after timeout # state reset after after timeout
state = hass.states.get("binary_sensor.test") state = hass.states.get("binary_sensor.test")
assert state.state is STATE_OFF 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.assert_telegram_count(0)
await knx.receive_write(_ADDRESS, False) await knx.receive_write(_ADDRESS, False)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.test") state = hass.states.get("binary_sensor.test")
assert state.state is STATE_OFF 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 # inverted is on, make sure the state is off after it
await knx.receive_write(_ADDRESS, True) await knx.receive_write(_ADDRESS, True)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.test") state = hass.states.get("binary_sensor.test")
assert state.state is STATE_OFF 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 # read temperature state
await knx.assert_read("1/2/3") await knx.assert_read("1/2/3")
await knx.receive_response("1/2/3", RAW_FLOAT_20_0) 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 # read on/off state
await knx.assert_read("1/2/9") await knx.assert_read("1/2/9")
await knx.receive_response("1/2/9", 1) 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 # turn off
await hass.services.async_call( 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 # 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 # 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.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.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 # 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( await hass.services.async_call(
@ -254,17 +249,14 @@ async def test_climate_preset_mode(
) )
events = async_capture_events(hass, "state_changed") 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 # 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 # 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.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.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() events.clear()
# set preset mode # set preset mode
@ -294,8 +286,6 @@ async def test_climate_preset_mode(
assert len(knx.xknx.devices[1].device_updated_cbs) == 2 assert len(knx.xknx.devices[1].device_updated_cbs) == 2
# test removing also removes hooks # test removing also removes hooks
entity_registry.async_remove("climate.test") entity_registry.async_remove("climate.test")
await hass.async_block_till_done()
# If we remove the entity the underlying devices should disappear too # If we remove the entity the underlying devices should disappear too
assert len(knx.xknx.devices) == 0 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", {}) assert await async_setup_component(hass, "homeassistant", {})
await hass.async_block_till_done()
await hass.async_block_till_done()
# read states state updater # read states state updater
await knx.assert_read("1/2/7")
await knx.assert_read("1/2/3") 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") 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.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 # verify update entity retriggers group value reads to the bus
await hass.services.async_call( 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 # read states state updater
await knx.assert_read("1/2/3") await knx.assert_read("1/2/3")
await knx.assert_read("1/2/5") 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", CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110",
}, },
) )
await hass.async_block_till_done()
assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["type"] is FlowResultType.CREATE_ENTRY
assert result3["title"] == "Routing as 1.1.110" assert result3["title"] == "Routing as 1.1.110"
assert result3["data"] == { assert result3["data"] == {
@ -259,7 +258,6 @@ async def test_routing_setup_advanced(
CONF_KNX_LOCAL_IP: "192.168.1.112", CONF_KNX_LOCAL_IP: "192.168.1.112",
}, },
) )
await hass.async_block_till_done()
assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["type"] is FlowResultType.CREATE_ENTRY
assert result3["title"] == "Routing as 1.1.110" assert result3["title"] == "Routing as 1.1.110"
assert result3["data"] == { assert result3["data"] == {
@ -350,7 +348,6 @@ async def test_routing_secure_manual_setup(
CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE: 2000, 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["type"] is FlowResultType.CREATE_ENTRY
assert secure_routing_manual["title"] == "Secure Routing as 0.0.123" assert secure_routing_manual["title"] == "Secure Routing as 0.0.123"
assert secure_routing_manual["data"] == { assert secure_routing_manual["data"] == {
@ -419,7 +416,6 @@ async def test_routing_secure_keyfile(
CONF_KNX_KNXKEY_PASSWORD: "password", CONF_KNX_KNXKEY_PASSWORD: "password",
}, },
) )
await hass.async_block_till_done()
assert routing_secure_knxkeys["type"] is FlowResultType.CREATE_ENTRY assert routing_secure_knxkeys["type"] is FlowResultType.CREATE_ENTRY
assert routing_secure_knxkeys["title"] == "Secure Routing as 0.0.123" assert routing_secure_knxkeys["title"] == "Secure Routing as 0.0.123"
assert routing_secure_knxkeys["data"] == { assert routing_secure_knxkeys["data"] == {
@ -552,7 +548,6 @@ async def test_tunneling_setup_manual(
result2["flow_id"], result2["flow_id"],
user_input, user_input,
) )
await hass.async_block_till_done()
assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["type"] is FlowResultType.CREATE_ENTRY
assert result3["title"] == title assert result3["title"] == title
assert result3["data"] == config_entry_data assert result3["data"] == config_entry_data
@ -681,7 +676,6 @@ async def test_tunneling_setup_manual_request_description_error(
CONF_PORT: 3671, CONF_PORT: 3671,
}, },
) )
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Tunneling TCP @ 192.168.0.1" assert result["title"] == "Tunneling TCP @ 192.168.0.1"
assert result["data"] == { assert result["data"] == {
@ -772,7 +766,6 @@ async def test_tunneling_setup_for_local_ip(
CONF_KNX_LOCAL_IP: "192.168.1.112", CONF_KNX_LOCAL_IP: "192.168.1.112",
}, },
) )
await hass.async_block_till_done()
assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["type"] is FlowResultType.CREATE_ENTRY
assert result3["title"] == "Tunneling UDP @ 192.168.0.2" assert result3["title"] == "Tunneling UDP @ 192.168.0.2"
assert result3["data"] == { assert result3["data"] == {
@ -821,7 +814,6 @@ async def test_tunneling_setup_for_multiple_found_gateways(
tunnel_flow["flow_id"], tunnel_flow["flow_id"],
{CONF_KNX_GATEWAY: str(gateway)}, {CONF_KNX_GATEWAY: str(gateway)},
) )
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"] == { assert result["data"] == {
**DEFAULT_ENTRY_DATA, **DEFAULT_ENTRY_DATA,
@ -905,7 +897,6 @@ async def test_form_with_automatic_connection_handling(
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
}, },
) )
await hass.async_block_till_done()
assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["type"] is FlowResultType.CREATE_ENTRY
assert result2["title"] == CONF_KNX_AUTOMATIC.capitalize() assert result2["title"] == CONF_KNX_AUTOMATIC.capitalize()
assert result2["data"] == { 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", 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["type"] is FlowResultType.CREATE_ENTRY
assert secure_tunnel_manual["data"] == { assert secure_tunnel_manual["data"] == {
**DEFAULT_ENTRY_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}, {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["type"] is FlowResultType.CREATE_ENTRY
assert secure_knxkeys["data"] == { assert secure_knxkeys["data"] == {
**DEFAULT_ENTRY_DATA, **DEFAULT_ENTRY_DATA,
@ -1201,7 +1190,6 @@ async def test_options_flow_connection_type(
CONF_KNX_GATEWAY: str(gateway), CONF_KNX_GATEWAY: str(gateway),
}, },
) )
await hass.async_block_till_done()
assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["type"] is FlowResultType.CREATE_ENTRY
assert not result3["data"] assert not result3["data"]
assert mock_config_entry.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"}, {CONF_KNX_TUNNEL_ENDPOINT_IA: "1.0.1"},
) )
await hass.async_block_till_done()
assert secure_knxkeys["type"] is FlowResultType.CREATE_ENTRY assert secure_knxkeys["type"] is FlowResultType.CREATE_ENTRY
assert mock_config_entry.data == { assert mock_config_entry.data == {
**DEFAULT_ENTRY_DATA, **DEFAULT_ENTRY_DATA,
@ -1352,7 +1339,6 @@ async def test_options_communication_settings(
CONF_KNX_TELEGRAM_LOG_SIZE: 3000, CONF_KNX_TELEGRAM_LOG_SIZE: 3000,
}, },
) )
await hass.async_block_till_done()
assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["type"] is FlowResultType.CREATE_ENTRY
assert not result2.get("data") assert not result2.get("data")
assert mock_config_entry.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", CONF_KNX_KNXKEY_PASSWORD: "password",
}, },
) )
await hass.async_block_till_done()
assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["type"] is FlowResultType.CREATE_ENTRY
assert not result2.get("data") assert not result2.get("data")
assert mock_config_entry.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", CONF_KNX_TUNNEL_ENDPOINT_IA: "1.0.1",
}, },
) )
await hass.async_block_till_done()
assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["type"] is FlowResultType.CREATE_ENTRY
assert not result3.get("data") assert not result3.get("data")
assert mock_config_entry.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( await knx.assert_write(
test_address, 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") state = hass.states.get("datetime.test")
assert state.state == "2020-01-02T03:04:05+00:00" 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.receive_read(test_address)
await knx.assert_response( await knx.assert_response(
test_address, test_address,
(0x7A, 0x03, 0x03, 0x84, 0x04, 0x05, 0x20, 0x80), (0x7A, 0x03, 0x03, 0x04, 0x04, 0x05, 0x24, 0x00),
) )
# don't respond to passive address # 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 ( assert (
"Unnamed automation failed to setup triggers and has been disabled: " "Unnamed automation failed to setup triggers and has been disabled: "
"extra keys not allowed @ data['invalid']. Got None" "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") events = async_capture_events(hass, "knx_event")
async def test_event_data(address, payload, value=None): async def test_event_data(address, payload, value=None):
await hass.async_block_till_done()
assert len(events) == 1 assert len(events) == 1
event = events.pop() event = events.pop()
assert event.data["data"] == payload assert event.data["data"] == payload
@ -69,7 +68,6 @@ async def test_knx_event(
) )
# no event received # no event received
await hass.async_block_till_done()
assert len(events) == 0 assert len(events) == 0
# receive telegrams for group addresses matching the filter # 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("0/5/0", True)
await knx.receive_write("1/7/0", True) await knx.receive_write("1/7/0", True)
await knx.receive_write("2/6/6", True) await knx.receive_write("2/6/6", True)
await hass.async_block_till_done()
assert len(events) == 0 assert len(events) == 0
# receive telegrams with wrong payload length # receive telegrams with wrong payload length

View File

@ -1,9 +1,8 @@
"""Test KNX expose.""" """Test KNX expose."""
from datetime import timedelta from datetime import timedelta
import time
from unittest.mock import patch
from freezegun import freeze_time
import pytest import pytest
from homeassistant.components.knx import CONF_KNX_EXPOSE, DOMAIN, KNX_ADDRESS 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( async def test_expose_with_date(
localtime, hass: HomeAssistant, knx: KNXTestKit hass: HomeAssistant, knx: KNXTestKit, time_type: str, raw: tuple[int, ...]
) -> None: ) -> None:
"""Test an expose with a date.""" """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( await knx.setup_integration(
{ {
CONF_KNX_EXPOSE: { CONF_KNX_EXPOSE: {
CONF_TYPE: "datetime", CONF_TYPE: time_type,
KNX_ADDRESS: "1/1/8", 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.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) entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1 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 await hass.config_entries.async_remove(config_entry.entry_id)
assert unlink_mock.call_count == 3 assert unlink_mock.call_count == 3
rmdir_mock.assert_called_once() rmdir_mock.assert_called_once()
await hass.async_block_till_done()
assert hass.config_entries.async_entries() == [] assert hass.config_entries.async_entries() == []
assert config_entry.state is ConfigEntryState.NOT_LOADED 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 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 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()
await hass.async_block_till_done()
assert len(events) == 4 # 3 not always_available + 3 force_update - 2 disabled assert len(events) == 4 # 3 not always_available + 3 force_update - 2 disabled
events.clear() events.clear()
knx.xknx.current_address = IndividualAddress("1.1.1") 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, state=XknxConnectionState.CONNECTED,
connection_type=XknxConnectionType.TUNNEL_UDP, 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()
await hass.async_block_till_done()
assert len(events) == 6 # all diagnostic sensors - counters are reset on connect assert len(events) == 6 # all diagnostic sensors - counters are reset on connect
for entity_id, test_state in ( for entity_id, test_state in (
@ -111,7 +105,6 @@ async def test_removed_entity(
"sensor.knx_interface_connection_established", "sensor.knx_interface_connection_established",
disabled_by=er.RegistryEntryDisabler.USER, disabled_by=er.RegistryEntryDisabler.USER,
) )
await hass.async_block_till_done()
unregister_mock.assert_called_once() unregister_mock.assert_called_once()

View File

@ -92,9 +92,7 @@ async def test_light_brightness(hass: HomeAssistant, knx: KNXTestKit) -> None:
) )
# StateUpdater initialize state # StateUpdater initialize state
await knx.assert_read(test_brightness_state) await knx.assert_read(test_brightness_state)
await knx.xknx.connection_manager.connection_state_changed( knx.xknx.connection_manager.connection_state_changed(XknxConnectionState.CONNECTED)
XknxConnectionState.CONNECTED
)
# turn on light via brightness # turn on light via brightness
await hass.services.async_call( await hass.services.async_call(
"light", "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( await hass.services.async_call(
"notify", "notify", {"target": "test", "message": "I love KNX"}, blocking=True "notify", "notify", {"target": "test", "message": "I love KNX"}, blocking=True
) )
await knx.assert_write( await knx.assert_write(
"1/0/0", "1/0/0",
(73, 32, 108, 111, 118, 101, 32, 75, 78, 88, 0, 0, 0, 0), (73, 32, 108, 111, 118, 101, 32, 75, 78, 88, 0, 0, 0, 0),
) )
await hass.services.async_call( await hass.services.async_call(
"notify", "notify",
"notify", "notify",
@ -41,7 +37,6 @@ async def test_legacy_notify_service_simple(
}, },
blocking=True, blocking=True,
) )
await knx.assert_write( await knx.assert_write(
"1/0/0", "1/0/0",
(73, 32, 108, 111, 118, 101, 32, 75, 78, 88, 44, 32, 98, 117), (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( await hass.services.async_call(
"notify", "notify", {"message": "Gänsefüßchen"}, blocking=True "notify", "notify", {"message": "Gänsefüßchen"}, blocking=True
) )
await knx.assert_write( await knx.assert_write(
"1/0/0", "1/0/0",
# "G?nsef??chen" # "G?nsef??chen"
@ -95,7 +87,6 @@ async def test_notify_simple(hass: HomeAssistant, knx: KNXTestKit) -> None:
} }
} }
) )
await hass.services.async_call( await hass.services.async_call(
notify.DOMAIN, notify.DOMAIN,
notify.SERVICE_SEND_MESSAGE, notify.SERVICE_SEND_MESSAGE,

View File

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

View File

@ -154,7 +154,6 @@ async def test_event_register(hass: HomeAssistant, knx: KNXTestKit) -> None:
# no event registered # no event registered
await knx.receive_write(test_address, True) await knx.receive_write(test_address, True)
await hass.async_block_till_done()
assert len(events) == 0 assert len(events) == 0
# register event with `type` # register event with `type`
@ -165,7 +164,6 @@ async def test_event_register(hass: HomeAssistant, knx: KNXTestKit) -> None:
blocking=True, blocking=True,
) )
await knx.receive_write(test_address, (0x04, 0xD2)) await knx.receive_write(test_address, (0x04, 0xD2))
await hass.async_block_till_done()
assert len(events) == 1 assert len(events) == 1
typed_event = events.pop() typed_event = events.pop()
assert typed_event.data["data"] == (0x04, 0xD2) assert typed_event.data["data"] == (0x04, 0xD2)
@ -179,7 +177,6 @@ async def test_event_register(hass: HomeAssistant, knx: KNXTestKit) -> None:
blocking=True, blocking=True,
) )
await knx.receive_write(test_address, True) await knx.receive_write(test_address, True)
await hass.async_block_till_done()
assert len(events) == 0 assert len(events) == 0
# register event without `type` # 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, True)
await knx.receive_write(test_address, False) await knx.receive_write(test_address, False)
await hass.async_block_till_done()
assert len(events) == 2 assert len(events) == 2
untyped_event_2 = events.pop() untyped_event_2 = events.pop()
assert untyped_event_2.data["data"] is False 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 ( assert (
"Unnamed automation failed to setup triggers and has been disabled: " "Unnamed automation failed to setup triggers and has been disabled: "
"extra keys not allowed @ data['invalid']. Got None" "extra keys not allowed @ data['invalid']. Got None"

View File

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