mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +00:00
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:
parent
0d678120e4
commit
9351f300b0
@ -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(
|
||||||
|
@ -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:
|
||||||
|
@ -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()
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
|
||||||
)
|
|
||||||
|
@ -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())
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
],
|
],
|
||||||
|
@ -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(
|
||||||
{
|
{
|
||||||
|
@ -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."""
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
|
||||||
|
@ -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}
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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")
|
||||||
|
@ -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 == {
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user