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 fixespull/122896/head
parent
0d678120e4
commit
9351f300b0
|
@ -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(
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()))
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
],
|
||||
|
|
|
@ -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(
|
||||
{
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 == {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in New Issue