Add a custom panel for KNX with a group monitor (#92355)
* Add KNX panel * provide project data for the panel group monitor * upload and delete project from panel * test project store * more tests * finish tests * use integers for DPTBinary payload monitor display * add project to diagnostics * require new frontend version * update knx_frontend * review suggestions * update xknxproject to 3.1.0 --------- Co-authored-by: Marvin Wichmann <me@marvin-wichmann.de>
This commit is contained in:
parent
0f2caf864a
commit
6250b0a230
12 changed files with 1302 additions and 16 deletions
|
@ -71,6 +71,7 @@ from .const import (
|
||||||
)
|
)
|
||||||
from .device import KNXInterfaceDevice
|
from .device import KNXInterfaceDevice
|
||||||
from .expose import KNXExposeSensor, KNXExposeTime, create_knx_exposure
|
from .expose import KNXExposeSensor, KNXExposeTime, create_knx_exposure
|
||||||
|
from .project import KNXProject
|
||||||
from .schema import (
|
from .schema import (
|
||||||
BinarySensorSchema,
|
BinarySensorSchema,
|
||||||
ButtonSchema,
|
ButtonSchema,
|
||||||
|
@ -91,6 +92,7 @@ from .schema import (
|
||||||
ga_validator,
|
ga_validator,
|
||||||
sensor_type_validator,
|
sensor_type_validator,
|
||||||
)
|
)
|
||||||
|
from .websocket import register_panel
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -222,6 +224,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
|
||||||
conf = dict(conf)
|
conf = dict(conf)
|
||||||
hass.data[DATA_KNX_CONFIG] = conf
|
hass.data[DATA_KNX_CONFIG] = conf
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -304,6 +307,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
schema=SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA,
|
schema=SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await register_panel(hass)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -368,6 +373,8 @@ class KNXModule:
|
||||||
self.service_exposures: dict[str, KNXExposeSensor | KNXExposeTime] = {}
|
self.service_exposures: dict[str, KNXExposeSensor | KNXExposeTime] = {}
|
||||||
self.entry = entry
|
self.entry = entry
|
||||||
|
|
||||||
|
self.project = KNXProject(hass=hass, entry=entry)
|
||||||
|
|
||||||
self.xknx = XKNX(
|
self.xknx = XKNX(
|
||||||
connection_config=self.connection_config(),
|
connection_config=self.connection_config(),
|
||||||
rate_limit=self.entry.data[CONF_KNX_RATE_LIMIT],
|
rate_limit=self.entry.data[CONF_KNX_RATE_LIMIT],
|
||||||
|
@ -393,6 +400,7 @@ class KNXModule:
|
||||||
|
|
||||||
async def start(self) -> None:
|
async def start(self) -> None:
|
||||||
"""Start XKNX object. Connect to tunneling or Routing device."""
|
"""Start XKNX object. Connect to tunneling or Routing device."""
|
||||||
|
await self.project.load_project()
|
||||||
await self.xknx.start()
|
await self.xknx.start()
|
||||||
|
|
||||||
async def stop(self, event: Event | None = None) -> None:
|
async def stop(self, event: Event | None = None) -> None:
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
"""Constants for the KNX integration."""
|
"""Constants for the KNX integration."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
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.telegram import Telegram
|
||||||
|
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
PRESET_AWAY,
|
PRESET_AWAY,
|
||||||
PRESET_COMFORT,
|
PRESET_COMFORT,
|
||||||
|
@ -76,6 +79,9 @@ DATA_HASS_CONFIG: Final = "knx_hass_config"
|
||||||
ATTR_COUNTER: Final = "counter"
|
ATTR_COUNTER: Final = "counter"
|
||||||
ATTR_SOURCE: Final = "source"
|
ATTR_SOURCE: Final = "source"
|
||||||
|
|
||||||
|
AsyncMessageCallbackType = Callable[[Telegram], Awaitable[None]]
|
||||||
|
MessageCallbackType = Callable[[Telegram], None]
|
||||||
|
|
||||||
|
|
||||||
class KNXConfigEntryData(TypedDict, total=False):
|
class KNXConfigEntryData(TypedDict, total=False):
|
||||||
"""Config entry for the KNX integration."""
|
"""Config entry for the KNX integration."""
|
||||||
|
@ -101,6 +107,20 @@ class KNXConfigEntryData(TypedDict, total=False):
|
||||||
sync_latency_tolerance: int | None
|
sync_latency_tolerance: int | None
|
||||||
|
|
||||||
|
|
||||||
|
class KNXBusMonitorMessage(TypedDict):
|
||||||
|
"""KNX bus monitor message."""
|
||||||
|
|
||||||
|
destination_address: str
|
||||||
|
destination_text: str | None
|
||||||
|
payload: str
|
||||||
|
type: str
|
||||||
|
value: str | None
|
||||||
|
source_address: str
|
||||||
|
source_text: str | None
|
||||||
|
direction: str
|
||||||
|
timestamp: str
|
||||||
|
|
||||||
|
|
||||||
class ColorTempModes(Enum):
|
class ColorTempModes(Enum):
|
||||||
"""Color temperature modes for config validation."""
|
"""Color temperature modes for config validation."""
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,11 @@ async def async_get_config_entry_diagnostics(
|
||||||
|
|
||||||
diag["config_entry_data"] = async_redact_data(dict(config_entry.data), TO_REDACT)
|
diag["config_entry_data"] = async_redact_data(dict(config_entry.data), TO_REDACT)
|
||||||
|
|
||||||
|
if proj_info := knx_module.project.info:
|
||||||
|
diag["project_info"] = async_redact_data(proj_info, "name")
|
||||||
|
else:
|
||||||
|
diag["project_info"] = None
|
||||||
|
|
||||||
raw_config = await conf_util.async_hass_config_yaml(hass)
|
raw_config = await conf_util.async_hass_config_yaml(hass)
|
||||||
diag["configuration_yaml"] = raw_config.get(DOMAIN)
|
diag["configuration_yaml"] = raw_config.get(DOMAIN)
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
{
|
{
|
||||||
"domain": "knx",
|
"domain": "knx",
|
||||||
"name": "KNX",
|
"name": "KNX",
|
||||||
|
"after_dependencies": ["panel_custom"],
|
||||||
"codeowners": ["@Julius2342", "@farmio", "@marvin-w"],
|
"codeowners": ["@Julius2342", "@farmio", "@marvin-w"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["file_upload"],
|
"dependencies": ["file_upload", "websocket_api"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/knx",
|
"documentation": "https://www.home-assistant.io/integrations/knx",
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["xknx"],
|
"loggers": ["xknx", "xknxproject"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["xknx==2.9.0"]
|
"requirements": [
|
||||||
|
"xknx==2.9.0",
|
||||||
|
"xknxproject==3.1.0",
|
||||||
|
"knx_frontend==2023.5.2.143855"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
117
homeassistant/components/knx/project.py
Normal file
117
homeassistant/components/knx/project.py
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
"""Handle KNX project data."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import logging
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
from xknx.dpt import DPTBase
|
||||||
|
from xknxproject import XKNXProj
|
||||||
|
from xknxproject.models import (
|
||||||
|
Device,
|
||||||
|
GroupAddress as GroupAddressModel,
|
||||||
|
KNXProject as KNXProjectModel,
|
||||||
|
ProjectInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.components.file_upload import process_uploaded_file
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.storage import Store
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
STORAGE_VERSION: Final = 1
|
||||||
|
STORAGE_KEY: Final = f"{DOMAIN}/knx_project.json"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GroupAddressInfo:
|
||||||
|
"""Group address info for runtime usage."""
|
||||||
|
|
||||||
|
address: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
dpt_main: int | None
|
||||||
|
dpt_sub: int | None
|
||||||
|
transcoder: type[DPTBase] | None
|
||||||
|
|
||||||
|
|
||||||
|
def _create_group_address_info(ga_model: GroupAddressModel) -> GroupAddressInfo:
|
||||||
|
"""Convert GroupAddress dict value into GroupAddressInfo instance."""
|
||||||
|
dpt = ga_model["dpt"]
|
||||||
|
transcoder = DPTBase.transcoder_by_dpt(dpt["main"], dpt.get("sub")) if dpt else None
|
||||||
|
return GroupAddressInfo(
|
||||||
|
address=ga_model["address"],
|
||||||
|
name=ga_model["name"],
|
||||||
|
description=ga_model["description"],
|
||||||
|
transcoder=transcoder,
|
||||||
|
dpt_main=dpt["main"] if dpt else None,
|
||||||
|
dpt_sub=dpt["sub"] if dpt else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class KNXProject:
|
||||||
|
"""Manage KNX project data."""
|
||||||
|
|
||||||
|
loaded: bool
|
||||||
|
devices: dict[str, Device]
|
||||||
|
group_addresses: dict[str, GroupAddressInfo]
|
||||||
|
info: ProjectInfo | None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize project data."""
|
||||||
|
self.hass = hass
|
||||||
|
self._store = Store[KNXProjectModel](hass, STORAGE_VERSION, STORAGE_KEY)
|
||||||
|
|
||||||
|
self.initial_state()
|
||||||
|
|
||||||
|
def initial_state(self) -> None:
|
||||||
|
"""Set initial state for project data."""
|
||||||
|
self.loaded = False
|
||||||
|
self.devices = {}
|
||||||
|
self.group_addresses = {}
|
||||||
|
self.info = None
|
||||||
|
|
||||||
|
async def load_project(self, data: KNXProjectModel | None = None) -> None:
|
||||||
|
"""Load project data from storage."""
|
||||||
|
if project := data or await self._store.async_load():
|
||||||
|
self.devices = project["devices"]
|
||||||
|
self.info = project["info"]
|
||||||
|
|
||||||
|
for ga_model in project["group_addresses"].values():
|
||||||
|
ga_info = _create_group_address_info(ga_model)
|
||||||
|
self.group_addresses[ga_info.address] = ga_info
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Loaded KNX project data with %s group addresses from storage",
|
||||||
|
len(self.group_addresses),
|
||||||
|
)
|
||||||
|
self.loaded = True
|
||||||
|
|
||||||
|
async def process_project_file(self, file_id: str, password: str) -> None:
|
||||||
|
"""Process an uploaded project file."""
|
||||||
|
|
||||||
|
def _parse_project() -> KNXProjectModel:
|
||||||
|
with process_uploaded_file(self.hass, file_id) as file_path:
|
||||||
|
xknxproj = XKNXProj(
|
||||||
|
file_path,
|
||||||
|
password=password,
|
||||||
|
language=self.hass.config.language,
|
||||||
|
)
|
||||||
|
return xknxproj.parse()
|
||||||
|
|
||||||
|
project = await self.hass.async_add_executor_job(_parse_project)
|
||||||
|
await self._store.async_save(project)
|
||||||
|
await self.load_project(data=project)
|
||||||
|
|
||||||
|
async def remove_project_file(self) -> None:
|
||||||
|
"""Remove project file from storage."""
|
||||||
|
await self._store.async_remove()
|
||||||
|
self.initial_state()
|
251
homeassistant/components/knx/websocket.py
Normal file
251
homeassistant/components/knx/websocket.py
Normal file
|
@ -0,0 +1,251 @@
|
||||||
|
"""KNX Websocket API."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
from knx_frontend import get_build_id, locate_dir
|
||||||
|
import voluptuous as vol
|
||||||
|
from xknx.dpt import DPTArray
|
||||||
|
from xknx.exceptions import XKNXException
|
||||||
|
from xknx.telegram import Telegram, TelegramDirection
|
||||||
|
from xknx.telegram.apci import GroupValueRead, GroupValueResponse, GroupValueWrite
|
||||||
|
from xknxproject.exceptions import XknxProjectException
|
||||||
|
|
||||||
|
from homeassistant.components import panel_custom, websocket_api
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
DOMAIN,
|
||||||
|
AsyncMessageCallbackType,
|
||||||
|
KNXBusMonitorMessage,
|
||||||
|
MessageCallbackType,
|
||||||
|
)
|
||||||
|
from .project import KNXProject
|
||||||
|
|
||||||
|
URL_BASE: Final = "/knx_static"
|
||||||
|
|
||||||
|
|
||||||
|
async def register_panel(hass: HomeAssistant) -> None:
|
||||||
|
"""Register the KNX Panel and Websocket API."""
|
||||||
|
websocket_api.async_register_command(hass, ws_info)
|
||||||
|
websocket_api.async_register_command(hass, ws_project_file_process)
|
||||||
|
websocket_api.async_register_command(hass, ws_project_file_remove)
|
||||||
|
websocket_api.async_register_command(hass, ws_group_monitor_info)
|
||||||
|
websocket_api.async_register_command(hass, ws_subscribe_telegram)
|
||||||
|
|
||||||
|
if DOMAIN not in hass.data.get("frontend_panels", {}):
|
||||||
|
path = locate_dir()
|
||||||
|
build_id = get_build_id()
|
||||||
|
hass.http.register_static_path(
|
||||||
|
URL_BASE, path, cache_headers=(build_id != "dev")
|
||||||
|
)
|
||||||
|
await panel_custom.async_register_panel(
|
||||||
|
hass=hass,
|
||||||
|
frontend_url_path=DOMAIN,
|
||||||
|
webcomponent_name="knx-frontend",
|
||||||
|
sidebar_title=DOMAIN.upper(),
|
||||||
|
sidebar_icon="mdi:bus-electric",
|
||||||
|
module_url=f"{URL_BASE}/entrypoint-{build_id}.js",
|
||||||
|
embed_iframe=True,
|
||||||
|
require_admin=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required("type"): "knx/info",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@callback
|
||||||
|
def ws_info(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
connection: websocket_api.ActiveConnection,
|
||||||
|
msg: dict,
|
||||||
|
) -> None:
|
||||||
|
"""Handle get info command."""
|
||||||
|
xknx = hass.data[DOMAIN].xknx
|
||||||
|
|
||||||
|
_project_info = None
|
||||||
|
if project_info := hass.data[DOMAIN].project.info:
|
||||||
|
_project_info = {
|
||||||
|
"name": project_info["name"],
|
||||||
|
"last_modified": project_info["last_modified"],
|
||||||
|
"tool_version": project_info["tool_version"],
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.send_result(
|
||||||
|
msg["id"],
|
||||||
|
{
|
||||||
|
"version": xknx.version,
|
||||||
|
"connected": xknx.connection_manager.connected.is_set(),
|
||||||
|
"current_address": str(xknx.current_address),
|
||||||
|
"project": _project_info,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.require_admin
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required("type"): "knx/project_file_process",
|
||||||
|
vol.Required("file_id"): str,
|
||||||
|
vol.Required("password"): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@websocket_api.async_response
|
||||||
|
async def ws_project_file_process(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
connection: websocket_api.ActiveConnection,
|
||||||
|
msg: dict,
|
||||||
|
) -> None:
|
||||||
|
"""Handle get info command."""
|
||||||
|
knx_project = hass.data[DOMAIN].project
|
||||||
|
try:
|
||||||
|
await knx_project.process_project_file(
|
||||||
|
file_id=msg["file_id"],
|
||||||
|
password=msg["password"],
|
||||||
|
)
|
||||||
|
except (ValueError, XknxProjectException) as err:
|
||||||
|
# ValueError could raise from file_upload integration
|
||||||
|
connection.send_error(
|
||||||
|
msg["id"], websocket_api.const.ERR_HOME_ASSISTANT_ERROR, str(err)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
connection.send_result(msg["id"])
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.require_admin
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required("type"): "knx/project_file_remove",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@websocket_api.async_response
|
||||||
|
async def ws_project_file_remove(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
connection: websocket_api.ActiveConnection,
|
||||||
|
msg: dict,
|
||||||
|
) -> None:
|
||||||
|
"""Handle get info command."""
|
||||||
|
knx_project = hass.data[DOMAIN].project
|
||||||
|
await knx_project.remove_project_file()
|
||||||
|
connection.send_result(msg["id"])
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required("type"): "knx/group_monitor_info",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@callback
|
||||||
|
def ws_group_monitor_info(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
connection: websocket_api.ActiveConnection,
|
||||||
|
msg: dict,
|
||||||
|
) -> None:
|
||||||
|
"""Handle get info command of group monitor."""
|
||||||
|
project_loaded = hass.data[DOMAIN].project.loaded
|
||||||
|
connection.send_result(
|
||||||
|
msg["id"],
|
||||||
|
{"project_loaded": bool(project_loaded)},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required("type"): "knx/subscribe_telegrams",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@callback
|
||||||
|
def ws_subscribe_telegram(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
connection: websocket_api.ActiveConnection,
|
||||||
|
msg: dict,
|
||||||
|
) -> None:
|
||||||
|
"""Subscribe to incoming and outgoing KNX telegrams."""
|
||||||
|
project: KNXProject = hass.data[DOMAIN].project
|
||||||
|
|
||||||
|
async def forward_telegrams(telegram: Telegram) -> None:
|
||||||
|
"""Forward events to websocket."""
|
||||||
|
payload: str
|
||||||
|
dpt_payload = None
|
||||||
|
if isinstance(telegram.payload, (GroupValueWrite, GroupValueResponse)):
|
||||||
|
dpt_payload = telegram.payload.value
|
||||||
|
if isinstance(dpt_payload, DPTArray):
|
||||||
|
payload = f"0x{bytes(dpt_payload.value).hex()}"
|
||||||
|
else:
|
||||||
|
payload = f"{dpt_payload.value:d}"
|
||||||
|
elif isinstance(telegram.payload, GroupValueRead):
|
||||||
|
payload = ""
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
direction = (
|
||||||
|
"group_monitor_incoming"
|
||||||
|
if telegram.direction is TelegramDirection.INCOMING
|
||||||
|
else "group_monitor_outgoing"
|
||||||
|
)
|
||||||
|
dst = str(telegram.destination_address)
|
||||||
|
src = str(telegram.source_address)
|
||||||
|
bus_message: KNXBusMonitorMessage = KNXBusMonitorMessage(
|
||||||
|
destination_address=dst,
|
||||||
|
destination_text=None,
|
||||||
|
payload=payload,
|
||||||
|
type=str(telegram.payload.__class__.__name__),
|
||||||
|
value=None,
|
||||||
|
source_address=src,
|
||||||
|
source_text=None,
|
||||||
|
direction=direction,
|
||||||
|
timestamp=dt_util.as_local(dt_util.utcnow()).strftime("%H:%M:%S.%f")[:-3],
|
||||||
|
)
|
||||||
|
if project.loaded:
|
||||||
|
if ga_infos := project.group_addresses.get(dst):
|
||||||
|
bus_message["destination_text"] = ga_infos.name
|
||||||
|
if dpt_payload is not None and ga_infos.transcoder is not None:
|
||||||
|
try:
|
||||||
|
value = ga_infos.transcoder.from_knx(dpt_payload)
|
||||||
|
except XKNXException:
|
||||||
|
bus_message["value"] = "Error decoding value"
|
||||||
|
else:
|
||||||
|
unit = (
|
||||||
|
f" {ga_infos.transcoder.unit}"
|
||||||
|
if ga_infos.transcoder.unit is not None
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
bus_message["value"] = f"{value}{unit}"
|
||||||
|
if ia_infos := project.devices.get(src):
|
||||||
|
bus_message[
|
||||||
|
"source_text"
|
||||||
|
] = f"{ia_infos['manufacturer_name']} {ia_infos['name']}"
|
||||||
|
|
||||||
|
connection.send_event(
|
||||||
|
msg["id"],
|
||||||
|
bus_message,
|
||||||
|
)
|
||||||
|
|
||||||
|
connection.subscriptions[msg["id"]] = async_subscribe_telegrams(
|
||||||
|
hass, forward_telegrams
|
||||||
|
)
|
||||||
|
|
||||||
|
connection.send_result(msg["id"])
|
||||||
|
|
||||||
|
|
||||||
|
def async_subscribe_telegrams(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
telegram_callback: AsyncMessageCallbackType | MessageCallbackType,
|
||||||
|
) -> Callable[[], None]:
|
||||||
|
"""Subscribe to telegram received callback."""
|
||||||
|
xknx = hass.data[DOMAIN].xknx
|
||||||
|
|
||||||
|
unregister = xknx.telegram_queue.register_telegram_received_cb(
|
||||||
|
telegram_callback, match_for_outgoing=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def async_remove() -> None:
|
||||||
|
"""Remove callback."""
|
||||||
|
xknx.telegram_queue.unregister_telegram_received_cb(unregister)
|
||||||
|
|
||||||
|
return async_remove
|
|
@ -1021,6 +1021,9 @@ kegtron-ble==0.4.0
|
||||||
# homeassistant.components.kiwi
|
# homeassistant.components.kiwi
|
||||||
kiwiki-client==0.1.1
|
kiwiki-client==0.1.1
|
||||||
|
|
||||||
|
# homeassistant.components.knx
|
||||||
|
knx_frontend==2023.5.2.143855
|
||||||
|
|
||||||
# homeassistant.components.konnected
|
# homeassistant.components.konnected
|
||||||
konnected==1.2.0
|
konnected==1.2.0
|
||||||
|
|
||||||
|
@ -2663,6 +2666,9 @@ xiaomi-ble==0.17.0
|
||||||
# homeassistant.components.knx
|
# homeassistant.components.knx
|
||||||
xknx==2.9.0
|
xknx==2.9.0
|
||||||
|
|
||||||
|
# homeassistant.components.knx
|
||||||
|
xknxproject==3.1.0
|
||||||
|
|
||||||
# homeassistant.components.bluesound
|
# homeassistant.components.bluesound
|
||||||
# homeassistant.components.fritz
|
# homeassistant.components.fritz
|
||||||
# homeassistant.components.rest
|
# homeassistant.components.rest
|
||||||
|
|
|
@ -780,6 +780,9 @@ justnimbus==0.6.0
|
||||||
# homeassistant.components.kegtron
|
# homeassistant.components.kegtron
|
||||||
kegtron-ble==0.4.0
|
kegtron-ble==0.4.0
|
||||||
|
|
||||||
|
# homeassistant.components.knx
|
||||||
|
knx_frontend==2023.5.2.143855
|
||||||
|
|
||||||
# homeassistant.components.konnected
|
# homeassistant.components.konnected
|
||||||
konnected==1.2.0
|
konnected==1.2.0
|
||||||
|
|
||||||
|
@ -1933,6 +1936,9 @@ xiaomi-ble==0.17.0
|
||||||
# homeassistant.components.knx
|
# homeassistant.components.knx
|
||||||
xknx==2.9.0
|
xknx==2.9.0
|
||||||
|
|
||||||
|
# homeassistant.components.knx
|
||||||
|
xknxproject==3.1.0
|
||||||
|
|
||||||
# homeassistant.components.bluesound
|
# homeassistant.components.bluesound
|
||||||
# homeassistant.components.fritz
|
# homeassistant.components.fritz
|
||||||
# homeassistant.components.rest
|
# homeassistant.components.rest
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
from unittest.mock import DEFAULT, AsyncMock, Mock, patch
|
from unittest.mock import DEFAULT, AsyncMock, Mock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -11,7 +12,13 @@ from xknx.dpt import DPTArray, DPTBinary
|
||||||
from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT
|
from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT
|
||||||
from xknx.telegram import Telegram, TelegramDirection
|
from xknx.telegram import Telegram, TelegramDirection
|
||||||
from xknx.telegram.address import GroupAddress, IndividualAddress
|
from xknx.telegram.address import GroupAddress, IndividualAddress
|
||||||
from xknx.telegram.apci import APCI, GroupValueRead, GroupValueResponse, GroupValueWrite
|
from xknx.telegram.apci import (
|
||||||
|
APCI,
|
||||||
|
GroupValueRead,
|
||||||
|
GroupValueResponse,
|
||||||
|
GroupValueWrite,
|
||||||
|
IndividualAddressRead,
|
||||||
|
)
|
||||||
|
|
||||||
from homeassistant.components.knx.const import (
|
from homeassistant.components.knx.const import (
|
||||||
CONF_KNX_AUTOMATIC,
|
CONF_KNX_AUTOMATIC,
|
||||||
|
@ -26,10 +33,13 @@ from homeassistant.components.knx.const import (
|
||||||
DEFAULT_ROUTING_IA,
|
DEFAULT_ROUTING_IA,
|
||||||
DOMAIN as KNX_DOMAIN,
|
DOMAIN as KNX_DOMAIN,
|
||||||
)
|
)
|
||||||
|
from homeassistant.components.knx.project import STORAGE_KEY as KNX_PROJECT_STORAGE_KEY
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry, load_fixture
|
||||||
|
|
||||||
|
FIXTURE_PROJECT_DATA = json.loads(load_fixture("project.json", KNX_DOMAIN))
|
||||||
|
|
||||||
|
|
||||||
class KNXTestKit:
|
class KNXTestKit:
|
||||||
|
@ -181,39 +191,72 @@ class KNXTestKit:
|
||||||
return DPTBinary(payload)
|
return DPTBinary(payload)
|
||||||
return DPTArray(payload)
|
return DPTArray(payload)
|
||||||
|
|
||||||
async def _receive_telegram(self, group_address: str, payload: APCI) -> None:
|
async def _receive_telegram(
|
||||||
|
self,
|
||||||
|
group_address: str,
|
||||||
|
payload: APCI,
|
||||||
|
source: str | None = None,
|
||||||
|
) -> None:
|
||||||
"""Inject incoming KNX telegram."""
|
"""Inject incoming KNX telegram."""
|
||||||
self.xknx.telegrams.put_nowait(
|
self.xknx.telegrams.put_nowait(
|
||||||
Telegram(
|
Telegram(
|
||||||
destination_address=GroupAddress(group_address),
|
destination_address=GroupAddress(group_address),
|
||||||
direction=TelegramDirection.INCOMING,
|
direction=TelegramDirection.INCOMING,
|
||||||
payload=payload,
|
payload=payload,
|
||||||
source_address=IndividualAddress(self.INDIVIDUAL_ADDRESS),
|
source_address=IndividualAddress(source or self.INDIVIDUAL_ADDRESS),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
await self.xknx.telegrams.join()
|
await self.xknx.telegrams.join()
|
||||||
await self.hass.async_block_till_done()
|
await self.hass.async_block_till_done()
|
||||||
|
|
||||||
async def receive_read(
|
async def receive_individual_address_read(self, source: str | None = None):
|
||||||
self,
|
"""Inject incoming IndividualAddressRead telegram."""
|
||||||
group_address: str,
|
self.xknx.telegrams.put_nowait(
|
||||||
) -> None:
|
Telegram(
|
||||||
|
destination_address=IndividualAddress(self.INDIVIDUAL_ADDRESS),
|
||||||
|
direction=TelegramDirection.INCOMING,
|
||||||
|
payload=IndividualAddressRead(),
|
||||||
|
source_address=IndividualAddress(source or "1.3.5"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await self.xknx.telegrams.join()
|
||||||
|
await self.hass.async_block_till_done()
|
||||||
|
|
||||||
|
async def receive_read(self, group_address: str, source: str | None = None) -> None:
|
||||||
"""Inject incoming GroupValueRead telegram."""
|
"""Inject incoming GroupValueRead telegram."""
|
||||||
await self._receive_telegram(group_address, GroupValueRead())
|
await self._receive_telegram(
|
||||||
|
group_address,
|
||||||
|
GroupValueRead(),
|
||||||
|
source=source,
|
||||||
|
)
|
||||||
|
|
||||||
async def receive_response(
|
async def receive_response(
|
||||||
self, group_address: str, payload: int | tuple[int, ...]
|
self,
|
||||||
|
group_address: str,
|
||||||
|
payload: int | tuple[int, ...],
|
||||||
|
source: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Inject incoming GroupValueResponse telegram."""
|
"""Inject incoming GroupValueResponse telegram."""
|
||||||
payload_value = self._payload_value(payload)
|
payload_value = self._payload_value(payload)
|
||||||
await self._receive_telegram(group_address, GroupValueResponse(payload_value))
|
await self._receive_telegram(
|
||||||
|
group_address,
|
||||||
|
GroupValueResponse(payload_value),
|
||||||
|
source=source,
|
||||||
|
)
|
||||||
|
|
||||||
async def receive_write(
|
async def receive_write(
|
||||||
self, group_address: str, payload: int | tuple[int, ...]
|
self,
|
||||||
|
group_address: str,
|
||||||
|
payload: int | tuple[int, ...],
|
||||||
|
source: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Inject incoming GroupValueWrite telegram."""
|
"""Inject incoming GroupValueWrite telegram."""
|
||||||
payload_value = self._payload_value(payload)
|
payload_value = self._payload_value(payload)
|
||||||
await self._receive_telegram(group_address, GroupValueWrite(payload_value))
|
await self._receive_telegram(
|
||||||
|
group_address,
|
||||||
|
GroupValueWrite(payload_value),
|
||||||
|
source=source,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
@ -239,3 +282,13 @@ async def knx(request, hass, mock_config_entry: MockConfigEntry):
|
||||||
knx_test_kit = KNXTestKit(hass, mock_config_entry)
|
knx_test_kit = KNXTestKit(hass, mock_config_entry)
|
||||||
yield knx_test_kit
|
yield knx_test_kit
|
||||||
await knx_test_kit.assert_no_telegram()
|
await knx_test_kit.assert_no_telegram()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def load_knxproj(hass_storage):
|
||||||
|
"""Mock KNX project data."""
|
||||||
|
hass_storage[KNX_PROJECT_STORAGE_KEY] = {
|
||||||
|
"version": 1,
|
||||||
|
"data": FIXTURE_PROJECT_DATA,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
502
tests/components/knx/fixtures/project.json
Normal file
502
tests/components/knx/fixtures/project.json
Normal file
|
@ -0,0 +1,502 @@
|
||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"project_id": "P-04FF",
|
||||||
|
"name": "Fixture",
|
||||||
|
"last_modified": "2023-04-30T09:04:04.4043671Z",
|
||||||
|
"group_address_style": "ThreeLevel",
|
||||||
|
"guid": "6a019e80-5945-489e-95a3-378735c642d1",
|
||||||
|
"created_by": "ETS5",
|
||||||
|
"schema_version": "20",
|
||||||
|
"tool_version": "5.7.1428.39779",
|
||||||
|
"xknxproject_version": "3.1.0",
|
||||||
|
"language_code": "de-DE"
|
||||||
|
},
|
||||||
|
"communication_objects": {
|
||||||
|
"1.0.9/O-57_R-21": {
|
||||||
|
"name": "Ch A Current Setpoint",
|
||||||
|
"number": 57,
|
||||||
|
"text": "Kanal A - Regler",
|
||||||
|
"function_text": "aktueller Sollwert",
|
||||||
|
"description": "",
|
||||||
|
"device_address": "1.0.9",
|
||||||
|
"dpts": [
|
||||||
|
{
|
||||||
|
"main": 9,
|
||||||
|
"sub": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"object_size": "2 Bytes",
|
||||||
|
"flags": {
|
||||||
|
"read": true,
|
||||||
|
"write": false,
|
||||||
|
"communication": true,
|
||||||
|
"update": false,
|
||||||
|
"read_on_init": false,
|
||||||
|
"transmit": true
|
||||||
|
},
|
||||||
|
"group_address_links": ["0/0/2"]
|
||||||
|
},
|
||||||
|
"1.0.9/O-73_R-29": {
|
||||||
|
"name": "Ch A On/Off Request Master",
|
||||||
|
"number": 73,
|
||||||
|
"text": "Kanal A - Regler",
|
||||||
|
"function_text": "Regelung aktivieren/deaktivieren",
|
||||||
|
"description": "",
|
||||||
|
"device_address": "1.0.9",
|
||||||
|
"dpts": [
|
||||||
|
{
|
||||||
|
"main": 1,
|
||||||
|
"sub": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"object_size": "1 Bit",
|
||||||
|
"flags": {
|
||||||
|
"read": false,
|
||||||
|
"write": true,
|
||||||
|
"communication": true,
|
||||||
|
"update": false,
|
||||||
|
"read_on_init": false,
|
||||||
|
"transmit": false
|
||||||
|
},
|
||||||
|
"group_address_links": ["0/0/1"]
|
||||||
|
},
|
||||||
|
"1.1.6/O-4_R-4": {
|
||||||
|
"name": "DayNight_General_1_GO",
|
||||||
|
"number": 4,
|
||||||
|
"text": "Zeit",
|
||||||
|
"function_text": "Tag (0) / Nacht (1)",
|
||||||
|
"description": "",
|
||||||
|
"device_address": "1.1.6",
|
||||||
|
"dpts": [
|
||||||
|
{
|
||||||
|
"main": 1,
|
||||||
|
"sub": 24
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"object_size": "1 Bit",
|
||||||
|
"flags": {
|
||||||
|
"read": false,
|
||||||
|
"write": true,
|
||||||
|
"communication": true,
|
||||||
|
"update": true,
|
||||||
|
"read_on_init": false,
|
||||||
|
"transmit": true
|
||||||
|
},
|
||||||
|
"group_address_links": ["0/0/1"]
|
||||||
|
},
|
||||||
|
"1.1.6/O-1_R-1": {
|
||||||
|
"name": "Time_General_1_GO",
|
||||||
|
"number": 1,
|
||||||
|
"text": "Zeit",
|
||||||
|
"function_text": "Uhrzeit",
|
||||||
|
"description": "",
|
||||||
|
"device_address": "1.1.6",
|
||||||
|
"dpts": [
|
||||||
|
{
|
||||||
|
"main": 10,
|
||||||
|
"sub": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"object_size": "3 Bytes",
|
||||||
|
"flags": {
|
||||||
|
"read": false,
|
||||||
|
"write": true,
|
||||||
|
"communication": true,
|
||||||
|
"update": true,
|
||||||
|
"read_on_init": false,
|
||||||
|
"transmit": true
|
||||||
|
},
|
||||||
|
"group_address_links": ["0/1/2"]
|
||||||
|
},
|
||||||
|
"1.1.6/O-241_R-124": {
|
||||||
|
"name": "StatusOnOff_RGB_1_GO",
|
||||||
|
"number": 241,
|
||||||
|
"text": "RGB:",
|
||||||
|
"function_text": "Status An/Aus",
|
||||||
|
"description": "",
|
||||||
|
"device_address": "1.1.6",
|
||||||
|
"dpts": [
|
||||||
|
{
|
||||||
|
"main": 1,
|
||||||
|
"sub": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"object_size": "1 Bit",
|
||||||
|
"flags": {
|
||||||
|
"read": true,
|
||||||
|
"write": false,
|
||||||
|
"communication": true,
|
||||||
|
"update": false,
|
||||||
|
"read_on_init": false,
|
||||||
|
"transmit": true
|
||||||
|
},
|
||||||
|
"group_address_links": ["0/1/0"]
|
||||||
|
},
|
||||||
|
"2.0.5/O-107_R-61": {
|
||||||
|
"name": "UHRZEIT",
|
||||||
|
"number": 107,
|
||||||
|
"text": "Uhrzeit",
|
||||||
|
"function_text": "Eingang / Ausgang",
|
||||||
|
"description": "",
|
||||||
|
"device_address": "2.0.5",
|
||||||
|
"dpts": [
|
||||||
|
{
|
||||||
|
"main": 10,
|
||||||
|
"sub": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"object_size": "3 Bytes",
|
||||||
|
"flags": {
|
||||||
|
"read": true,
|
||||||
|
"write": true,
|
||||||
|
"communication": true,
|
||||||
|
"update": false,
|
||||||
|
"read_on_init": false,
|
||||||
|
"transmit": true
|
||||||
|
},
|
||||||
|
"group_address_links": ["0/0/3"]
|
||||||
|
},
|
||||||
|
"2.0.5/O-123_R-3923": {
|
||||||
|
"name": "T_MW_INTERN",
|
||||||
|
"number": 123,
|
||||||
|
"text": "Temp.Sensor: Messwert",
|
||||||
|
"function_text": "Ausgang",
|
||||||
|
"description": "",
|
||||||
|
"device_address": "2.0.5",
|
||||||
|
"dpts": [
|
||||||
|
{
|
||||||
|
"main": 9,
|
||||||
|
"sub": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"object_size": "2 Bytes",
|
||||||
|
"flags": {
|
||||||
|
"read": true,
|
||||||
|
"write": false,
|
||||||
|
"communication": true,
|
||||||
|
"update": false,
|
||||||
|
"read_on_init": false,
|
||||||
|
"transmit": true
|
||||||
|
},
|
||||||
|
"group_address_links": ["0/0/2"]
|
||||||
|
},
|
||||||
|
"2.0.5/O-331_R-254": {
|
||||||
|
"name": "NACHT_SA",
|
||||||
|
"number": 331,
|
||||||
|
"text": "Nacht: Schaltausgang",
|
||||||
|
"function_text": "Ausgang",
|
||||||
|
"description": "",
|
||||||
|
"device_address": "2.0.5",
|
||||||
|
"dpts": [
|
||||||
|
{
|
||||||
|
"main": 1,
|
||||||
|
"sub": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"object_size": "1 Bit",
|
||||||
|
"flags": {
|
||||||
|
"read": true,
|
||||||
|
"write": false,
|
||||||
|
"communication": true,
|
||||||
|
"update": false,
|
||||||
|
"read_on_init": false,
|
||||||
|
"transmit": true
|
||||||
|
},
|
||||||
|
"group_address_links": ["0/0/1"]
|
||||||
|
},
|
||||||
|
"2.0.15/O-1_R-0": {
|
||||||
|
"name": "Time",
|
||||||
|
"number": 1,
|
||||||
|
"text": "Uhrzeit",
|
||||||
|
"function_text": "Senden",
|
||||||
|
"description": "",
|
||||||
|
"device_address": "2.0.15",
|
||||||
|
"dpts": [
|
||||||
|
{
|
||||||
|
"main": 10,
|
||||||
|
"sub": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"object_size": "3 Bytes",
|
||||||
|
"flags": {
|
||||||
|
"read": false,
|
||||||
|
"write": false,
|
||||||
|
"communication": true,
|
||||||
|
"update": false,
|
||||||
|
"read_on_init": false,
|
||||||
|
"transmit": true
|
||||||
|
},
|
||||||
|
"group_address_links": ["0/1/2"]
|
||||||
|
},
|
||||||
|
"2.0.15/O-3_R-2": {
|
||||||
|
"name": "Trigger send date/time",
|
||||||
|
"number": 3,
|
||||||
|
"text": "Trigger sende Datum/Uhrzeit",
|
||||||
|
"function_text": "Empfangen",
|
||||||
|
"description": "",
|
||||||
|
"device_address": "2.0.15",
|
||||||
|
"dpts": [
|
||||||
|
{
|
||||||
|
"main": 1,
|
||||||
|
"sub": 17
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"object_size": "1 Bit",
|
||||||
|
"flags": {
|
||||||
|
"read": false,
|
||||||
|
"write": true,
|
||||||
|
"communication": true,
|
||||||
|
"update": false,
|
||||||
|
"read_on_init": false,
|
||||||
|
"transmit": false
|
||||||
|
},
|
||||||
|
"group_address_links": ["0/1/0"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"topology": {
|
||||||
|
"0": {
|
||||||
|
"name": "Backbone Bereich",
|
||||||
|
"description": null,
|
||||||
|
"lines": {
|
||||||
|
"0": {
|
||||||
|
"name": "Bereichslinie",
|
||||||
|
"description": null,
|
||||||
|
"devices": [],
|
||||||
|
"medium_type": "KNXnet/IP (IP)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"name": "Eins",
|
||||||
|
"description": null,
|
||||||
|
"lines": {
|
||||||
|
"0": {
|
||||||
|
"name": "Hauptlinie",
|
||||||
|
"description": null,
|
||||||
|
"devices": ["1.0.0", "1.0.9"],
|
||||||
|
"medium_type": "Twisted Pair (TP)"
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"name": "L1",
|
||||||
|
"description": null,
|
||||||
|
"devices": ["1.1.0", "1.1.1", "1.1.6"],
|
||||||
|
"medium_type": "Twisted Pair (TP)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"2": {
|
||||||
|
"name": "Zwei",
|
||||||
|
"description": null,
|
||||||
|
"lines": {
|
||||||
|
"0": {
|
||||||
|
"name": "Hauptlinie",
|
||||||
|
"description": null,
|
||||||
|
"devices": ["2.0.0", "2.0.5", "2.0.6", "2.0.15"],
|
||||||
|
"medium_type": "Twisted Pair (TP)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"devices": {
|
||||||
|
"1.0.0": {
|
||||||
|
"name": "KNX IP Router 752 secure",
|
||||||
|
"hardware_name": "KNX IP Router 752 secure",
|
||||||
|
"description": "",
|
||||||
|
"manufacturer_name": "Weinzierl Engineering GmbH",
|
||||||
|
"individual_address": "1.0.0",
|
||||||
|
"project_uid": 6,
|
||||||
|
"communication_object_ids": []
|
||||||
|
},
|
||||||
|
"1.0.9": {
|
||||||
|
"name": "HCC/S2.2.1.1 Heiz-/Kühlkreis Controller,3-Punkt,2-fach,REG",
|
||||||
|
"hardware_name": "HCC/S2.2.1.1 Heiz-/Kühlkreis Controller,3-Punkt,2-fach,REG",
|
||||||
|
"description": "",
|
||||||
|
"manufacturer_name": "ABB",
|
||||||
|
"individual_address": "1.0.9",
|
||||||
|
"project_uid": 30,
|
||||||
|
"communication_object_ids": ["1.0.9/O-57_R-21", "1.0.9/O-73_R-29"]
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"name": "Bereichs-/Linienkoppler REG",
|
||||||
|
"hardware_name": "Bereichs-/Linienkoppler REG",
|
||||||
|
"description": "",
|
||||||
|
"manufacturer_name": "Albrecht Jung",
|
||||||
|
"individual_address": "1.1.0",
|
||||||
|
"project_uid": 23,
|
||||||
|
"communication_object_ids": []
|
||||||
|
},
|
||||||
|
"1.1.1": {
|
||||||
|
"name": "SCN-IP000.03 IP Interface mit Secure",
|
||||||
|
"hardware_name": "IP Interface Secure",
|
||||||
|
"description": "",
|
||||||
|
"manufacturer_name": "MDT technologies",
|
||||||
|
"individual_address": "1.1.1",
|
||||||
|
"project_uid": 24,
|
||||||
|
"communication_object_ids": []
|
||||||
|
},
|
||||||
|
"1.1.6": {
|
||||||
|
"name": "Enertex KNX LED Dimmsequenzer 20A/5x REG",
|
||||||
|
"hardware_name": "LED Dimmsequenzer 20A/5x REG/DK",
|
||||||
|
"description": "",
|
||||||
|
"manufacturer_name": "Enertex Bayern GmbH",
|
||||||
|
"individual_address": "1.1.6",
|
||||||
|
"project_uid": 29,
|
||||||
|
"communication_object_ids": [
|
||||||
|
"1.1.6/O-4_R-4",
|
||||||
|
"1.1.6/O-1_R-1",
|
||||||
|
"1.1.6/O-241_R-124"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2.0.0": {
|
||||||
|
"name": "KNX/IP-Router",
|
||||||
|
"hardware_name": "IP Router",
|
||||||
|
"description": "",
|
||||||
|
"manufacturer_name": "GIRA Giersiepen",
|
||||||
|
"individual_address": "2.0.0",
|
||||||
|
"project_uid": 17,
|
||||||
|
"communication_object_ids": []
|
||||||
|
},
|
||||||
|
"2.0.5": {
|
||||||
|
"name": "Suntracer KNX pro",
|
||||||
|
"hardware_name": "KNX Suntracer Pro",
|
||||||
|
"description": "",
|
||||||
|
"manufacturer_name": "Elsner Elektronik GmbH",
|
||||||
|
"individual_address": "2.0.5",
|
||||||
|
"project_uid": 31,
|
||||||
|
"communication_object_ids": [
|
||||||
|
"2.0.5/O-107_R-61",
|
||||||
|
"2.0.5/O-123_R-3923",
|
||||||
|
"2.0.5/O-331_R-254"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2.0.6": {
|
||||||
|
"name": "KNX Modbus TCP Gateway 716",
|
||||||
|
"hardware_name": "KNX Modbus TCP Gateway 716",
|
||||||
|
"description": "",
|
||||||
|
"manufacturer_name": "Weinzierl Engineering GmbH",
|
||||||
|
"individual_address": "2.0.6",
|
||||||
|
"project_uid": 32,
|
||||||
|
"communication_object_ids": []
|
||||||
|
},
|
||||||
|
"2.0.15": {
|
||||||
|
"name": "KNX/IP-Router",
|
||||||
|
"hardware_name": "Router Applications",
|
||||||
|
"description": "",
|
||||||
|
"manufacturer_name": "GIRA Giersiepen",
|
||||||
|
"individual_address": "2.0.15",
|
||||||
|
"project_uid": 50,
|
||||||
|
"communication_object_ids": ["2.0.15/O-1_R-0", "2.0.15/O-3_R-2"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"group_addresses": {
|
||||||
|
"0/0/1": {
|
||||||
|
"name": "Binary",
|
||||||
|
"identifier": "GA-1",
|
||||||
|
"raw_address": 1,
|
||||||
|
"address": "0/0/1",
|
||||||
|
"project_uid": 43,
|
||||||
|
"dpt": {
|
||||||
|
"main": 1,
|
||||||
|
"sub": 1
|
||||||
|
},
|
||||||
|
"communication_object_ids": [
|
||||||
|
"1.0.9/O-73_R-29",
|
||||||
|
"1.1.6/O-4_R-4",
|
||||||
|
"2.0.5/O-331_R-254"
|
||||||
|
],
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"0/0/2": {
|
||||||
|
"name": "2-byte float",
|
||||||
|
"identifier": "GA-2",
|
||||||
|
"raw_address": 2,
|
||||||
|
"address": "0/0/2",
|
||||||
|
"project_uid": 44,
|
||||||
|
"dpt": {
|
||||||
|
"main": 9,
|
||||||
|
"sub": 1
|
||||||
|
},
|
||||||
|
"communication_object_ids": ["1.0.9/O-57_R-21", "2.0.5/O-123_R-3923"],
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"0/0/3": {
|
||||||
|
"name": "daytime",
|
||||||
|
"identifier": "GA-3",
|
||||||
|
"raw_address": 3,
|
||||||
|
"address": "0/0/3",
|
||||||
|
"project_uid": 45,
|
||||||
|
"dpt": {
|
||||||
|
"main": 10,
|
||||||
|
"sub": 1
|
||||||
|
},
|
||||||
|
"communication_object_ids": ["2.0.5/O-107_R-61"],
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"0/0/4": {
|
||||||
|
"name": "RGB color",
|
||||||
|
"identifier": "GA-7",
|
||||||
|
"raw_address": 4,
|
||||||
|
"address": "0/0/4",
|
||||||
|
"project_uid": 69,
|
||||||
|
"dpt": {
|
||||||
|
"main": 232,
|
||||||
|
"sub": 600
|
||||||
|
},
|
||||||
|
"communication_object_ids": [],
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"0/1/0": {
|
||||||
|
"name": "binary (1.017)",
|
||||||
|
"identifier": "GA-4",
|
||||||
|
"raw_address": 256,
|
||||||
|
"address": "0/1/0",
|
||||||
|
"project_uid": 47,
|
||||||
|
"dpt": {
|
||||||
|
"main": 1,
|
||||||
|
"sub": 17
|
||||||
|
},
|
||||||
|
"communication_object_ids": ["1.1.6/O-241_R-124", "2.0.15/O-3_R-2"],
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"0/1/1": {
|
||||||
|
"name": "percent",
|
||||||
|
"identifier": "GA-5",
|
||||||
|
"raw_address": 257,
|
||||||
|
"address": "0/1/1",
|
||||||
|
"project_uid": 48,
|
||||||
|
"dpt": {
|
||||||
|
"main": 5,
|
||||||
|
"sub": 1
|
||||||
|
},
|
||||||
|
"communication_object_ids": [],
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"0/1/2": {
|
||||||
|
"name": "daytime",
|
||||||
|
"identifier": "GA-6",
|
||||||
|
"raw_address": 258,
|
||||||
|
"address": "0/1/2",
|
||||||
|
"project_uid": 49,
|
||||||
|
"dpt": {
|
||||||
|
"main": 10,
|
||||||
|
"sub": 1
|
||||||
|
},
|
||||||
|
"communication_object_ids": ["1.1.6/O-1_R-1", "2.0.15/O-1_R-0"],
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"locations": {
|
||||||
|
"Neues Projekt": {
|
||||||
|
"type": "Building",
|
||||||
|
"identifier": "P-04FF-0_BP-1",
|
||||||
|
"name": "Neues Projekt",
|
||||||
|
"usage_id": null,
|
||||||
|
"usage_text": "",
|
||||||
|
"number": "",
|
||||||
|
"description": "",
|
||||||
|
"project_uid": 3,
|
||||||
|
"devices": [],
|
||||||
|
"spaces": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -55,6 +55,7 @@ async def test_diagnostics(
|
||||||
},
|
},
|
||||||
"configuration_error": None,
|
"configuration_error": None,
|
||||||
"configuration_yaml": None,
|
"configuration_yaml": None,
|
||||||
|
"project_info": None,
|
||||||
"xknx": {"current_address": "0.0.0", "version": "1.0.0"},
|
"xknx": {"current_address": "0.0.0", "version": "1.0.0"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,6 +86,7 @@ async def test_diagnostic_config_error(
|
||||||
},
|
},
|
||||||
"configuration_error": "extra keys not allowed @ data['knx']['wrong_key']",
|
"configuration_error": "extra keys not allowed @ data['knx']['wrong_key']",
|
||||||
"configuration_yaml": {"wrong_key": {}},
|
"configuration_yaml": {"wrong_key": {}},
|
||||||
|
"project_info": None,
|
||||||
"xknx": {"current_address": "0.0.0", "version": "1.0.0"},
|
"xknx": {"current_address": "0.0.0", "version": "1.0.0"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,5 +136,34 @@ async def test_diagnostic_redact(
|
||||||
},
|
},
|
||||||
"configuration_error": None,
|
"configuration_error": None,
|
||||||
"configuration_yaml": None,
|
"configuration_yaml": None,
|
||||||
|
"project_info": None,
|
||||||
"xknx": {"current_address": "0.0.0", "version": "1.0.0"},
|
"xknx": {"current_address": "0.0.0", "version": "1.0.0"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("hass_config", [{}])
|
||||||
|
async def test_diagnostics_project(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client: ClientSessionGenerator,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
knx: KNXTestKit,
|
||||||
|
mock_hass_config: None,
|
||||||
|
load_knxproj: None,
|
||||||
|
) -> None:
|
||||||
|
"""Test diagnostics."""
|
||||||
|
await knx.setup_integration({})
|
||||||
|
diag = await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry)
|
||||||
|
|
||||||
|
assert "config_entry_data" in diag
|
||||||
|
assert "configuration_error" in diag
|
||||||
|
assert "configuration_yaml" in diag
|
||||||
|
assert "project_info" in diag
|
||||||
|
assert "xknx" in diag
|
||||||
|
# project specific fields
|
||||||
|
assert "created_by" in diag["project_info"]
|
||||||
|
assert "group_address_style" in diag["project_info"]
|
||||||
|
assert "last_modified" in diag["project_info"]
|
||||||
|
assert "schema_version" in diag["project_info"]
|
||||||
|
assert "tool_version" in diag["project_info"]
|
||||||
|
assert "language_code" in diag["project_info"]
|
||||||
|
assert diag["project_info"]["name"] == "**REDACTED**"
|
||||||
|
|
282
tests/components/knx/test_websocket.py
Normal file
282
tests/components/knx/test_websocket.py
Normal file
|
@ -0,0 +1,282 @@
|
||||||
|
"""KNX Websocket Tests."""
|
||||||
|
from typing import Any
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from homeassistant.components.knx import DOMAIN, KNX_ADDRESS, SwitchSchema
|
||||||
|
from homeassistant.const import CONF_NAME
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .conftest import FIXTURE_PROJECT_DATA, KNXTestKit
|
||||||
|
|
||||||
|
from tests.typing import WebSocketGenerator
|
||||||
|
|
||||||
|
|
||||||
|
async def test_knx_info_command(
|
||||||
|
hass: HomeAssistant, knx: KNXTestKit, hass_ws_client: WebSocketGenerator
|
||||||
|
):
|
||||||
|
"""Test knx/info command."""
|
||||||
|
await knx.setup_integration({})
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
await client.send_json({"id": 6, "type": "knx/info"})
|
||||||
|
|
||||||
|
res = await client.receive_json()
|
||||||
|
assert res["success"], res
|
||||||
|
assert res["result"]["version"] is not None
|
||||||
|
assert res["result"]["connected"]
|
||||||
|
assert res["result"]["current_address"] == "0.0.0"
|
||||||
|
assert res["result"]["project"] is None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_knx_info_command_with_project(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
knx: KNXTestKit,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
load_knxproj: None,
|
||||||
|
):
|
||||||
|
"""Test knx/info command with loaded project."""
|
||||||
|
await knx.setup_integration({})
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
await client.send_json({"id": 6, "type": "knx/info"})
|
||||||
|
|
||||||
|
res = await client.receive_json()
|
||||||
|
assert res["success"], res
|
||||||
|
assert res["result"]["version"] is not None
|
||||||
|
assert res["result"]["connected"]
|
||||||
|
assert res["result"]["current_address"] == "0.0.0"
|
||||||
|
assert res["result"]["project"] is not None
|
||||||
|
assert res["result"]["project"]["name"] == "Fixture"
|
||||||
|
assert res["result"]["project"]["last_modified"] == "2023-04-30T09:04:04.4043671Z"
|
||||||
|
assert res["result"]["project"]["tool_version"] == "5.7.1428.39779"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_knx_project_file_process(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
knx: KNXTestKit,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
hass_storage: dict[str, Any],
|
||||||
|
):
|
||||||
|
"""Test knx/project_file_process command for storing and loading new data."""
|
||||||
|
_file_id = "1234"
|
||||||
|
_password = "pw-test"
|
||||||
|
_parse_result = FIXTURE_PROJECT_DATA
|
||||||
|
|
||||||
|
await knx.setup_integration({})
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
assert not hass.data[DOMAIN].project.loaded
|
||||||
|
|
||||||
|
await client.send_json(
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"type": "knx/project_file_process",
|
||||||
|
"file_id": _file_id,
|
||||||
|
"password": _password,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.knx.project.process_uploaded_file",
|
||||||
|
) as file_upload_mock, patch(
|
||||||
|
"xknxproject.XKNXProj.parse", return_value=_parse_result
|
||||||
|
) as parse_mock:
|
||||||
|
file_upload_mock.return_value.__enter__.return_value = ""
|
||||||
|
res = await client.receive_json()
|
||||||
|
|
||||||
|
file_upload_mock.assert_called_once_with(hass, _file_id)
|
||||||
|
parse_mock.assert_called_once_with()
|
||||||
|
|
||||||
|
assert res["success"], res
|
||||||
|
assert hass.data[DOMAIN].project.loaded
|
||||||
|
|
||||||
|
|
||||||
|
async def test_knx_project_file_process_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
knx: KNXTestKit,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
):
|
||||||
|
"""Test knx/project_file_process exception handling."""
|
||||||
|
await knx.setup_integration({})
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
assert not hass.data[DOMAIN].project.loaded
|
||||||
|
|
||||||
|
await client.send_json(
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"type": "knx/project_file_process",
|
||||||
|
"file_id": "1234",
|
||||||
|
"password": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.knx.project.process_uploaded_file",
|
||||||
|
) as file_upload_mock, patch(
|
||||||
|
"xknxproject.XKNXProj.parse", side_effect=ValueError
|
||||||
|
) as parse_mock:
|
||||||
|
file_upload_mock.return_value.__enter__.return_value = ""
|
||||||
|
res = await client.receive_json()
|
||||||
|
parse_mock.assert_called_once_with()
|
||||||
|
|
||||||
|
assert res["error"], res
|
||||||
|
assert not hass.data[DOMAIN].project.loaded
|
||||||
|
|
||||||
|
|
||||||
|
async def test_knx_project_file_remove(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
knx: KNXTestKit,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
load_knxproj: None,
|
||||||
|
):
|
||||||
|
"""Test knx/project_file_remove command."""
|
||||||
|
await knx.setup_integration({})
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
assert hass.data[DOMAIN].project.loaded
|
||||||
|
|
||||||
|
await client.send_json({"id": 6, "type": "knx/project_file_remove"})
|
||||||
|
with patch("homeassistant.helpers.storage.Store.async_remove") as remove_mock:
|
||||||
|
res = await client.receive_json()
|
||||||
|
remove_mock.assert_called_once_with()
|
||||||
|
|
||||||
|
assert res["success"], res
|
||||||
|
assert not hass.data[DOMAIN].project.loaded
|
||||||
|
|
||||||
|
|
||||||
|
async def test_knx_group_monitor_info_command(
|
||||||
|
hass: HomeAssistant, knx: KNXTestKit, hass_ws_client: WebSocketGenerator
|
||||||
|
):
|
||||||
|
"""Test knx/group_monitor_info command."""
|
||||||
|
await knx.setup_integration({})
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
await client.send_json({"id": 6, "type": "knx/group_monitor_info"})
|
||||||
|
|
||||||
|
res = await client.receive_json()
|
||||||
|
assert res["success"], res
|
||||||
|
assert res["result"]["project_loaded"] is False
|
||||||
|
|
||||||
|
|
||||||
|
async def test_knx_subscribe_telegrams_command_no_project(
|
||||||
|
hass: HomeAssistant, knx: KNXTestKit, hass_ws_client: WebSocketGenerator
|
||||||
|
):
|
||||||
|
"""Test knx/subscribe_telegrams command without project data."""
|
||||||
|
await knx.setup_integration(
|
||||||
|
{
|
||||||
|
SwitchSchema.PLATFORM: {
|
||||||
|
CONF_NAME: "test",
|
||||||
|
KNX_ADDRESS: "1/2/4",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
await client.send_json({"id": 6, "type": "knx/subscribe_telegrams"})
|
||||||
|
res = await client.receive_json()
|
||||||
|
assert res["success"], res
|
||||||
|
|
||||||
|
# send incoming events
|
||||||
|
await knx.receive_read("1/2/3")
|
||||||
|
await knx.receive_write("1/3/4", True)
|
||||||
|
await knx.receive_write("1/3/4", False)
|
||||||
|
await knx.receive_individual_address_read()
|
||||||
|
await knx.receive_write("1/3/8", (0x34, 0x45))
|
||||||
|
# send outgoing events
|
||||||
|
await hass.services.async_call(
|
||||||
|
"switch", "turn_on", {"entity_id": "switch.test"}, blocking=True
|
||||||
|
)
|
||||||
|
await knx.assert_write("1/2/4", 1)
|
||||||
|
|
||||||
|
# receive events
|
||||||
|
res = await client.receive_json()
|
||||||
|
assert res["event"]["destination_address"] == "1/2/3"
|
||||||
|
assert res["event"]["payload"] == ""
|
||||||
|
assert res["event"]["type"] == "GroupValueRead"
|
||||||
|
assert res["event"]["source_address"] == "1.2.3"
|
||||||
|
assert res["event"]["direction"] == "group_monitor_incoming"
|
||||||
|
assert res["event"]["timestamp"] is not None
|
||||||
|
|
||||||
|
res = await client.receive_json()
|
||||||
|
assert res["event"]["destination_address"] == "1/3/4"
|
||||||
|
assert res["event"]["payload"] == "1"
|
||||||
|
assert res["event"]["type"] == "GroupValueWrite"
|
||||||
|
assert res["event"]["source_address"] == "1.2.3"
|
||||||
|
assert res["event"]["direction"] == "group_monitor_incoming"
|
||||||
|
assert res["event"]["timestamp"] is not None
|
||||||
|
|
||||||
|
res = await client.receive_json()
|
||||||
|
assert res["event"]["destination_address"] == "1/3/4"
|
||||||
|
assert res["event"]["payload"] == "0"
|
||||||
|
assert res["event"]["type"] == "GroupValueWrite"
|
||||||
|
assert res["event"]["source_address"] == "1.2.3"
|
||||||
|
assert res["event"]["direction"] == "group_monitor_incoming"
|
||||||
|
assert res["event"]["timestamp"] is not None
|
||||||
|
|
||||||
|
res = await client.receive_json()
|
||||||
|
assert res["event"]["destination_address"] == "1/3/8"
|
||||||
|
assert res["event"]["payload"] == "0x3445"
|
||||||
|
assert res["event"]["type"] == "GroupValueWrite"
|
||||||
|
assert res["event"]["source_address"] == "1.2.3"
|
||||||
|
assert res["event"]["direction"] == "group_monitor_incoming"
|
||||||
|
assert res["event"]["timestamp"] is not None
|
||||||
|
|
||||||
|
res = await client.receive_json()
|
||||||
|
assert res["event"]["destination_address"] == "1/2/4"
|
||||||
|
assert res["event"]["payload"] == "1"
|
||||||
|
assert res["event"]["type"] == "GroupValueWrite"
|
||||||
|
assert (
|
||||||
|
res["event"]["source_address"] == "0.0.0"
|
||||||
|
) # needs to be the IA currently connected to
|
||||||
|
assert res["event"]["direction"] == "group_monitor_outgoing"
|
||||||
|
assert res["event"]["timestamp"] is not None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_knx_subscribe_telegrams_command_project(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
knx: KNXTestKit,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
load_knxproj: None,
|
||||||
|
):
|
||||||
|
"""Test knx/subscribe_telegrams command with project data."""
|
||||||
|
await knx.setup_integration({})
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
await client.send_json({"id": 6, "type": "knx/subscribe_telegrams"})
|
||||||
|
res = await client.receive_json()
|
||||||
|
assert res["success"], res
|
||||||
|
|
||||||
|
# incoming DPT 1 telegram
|
||||||
|
await knx.receive_write("0/0/1", True)
|
||||||
|
res = await client.receive_json()
|
||||||
|
assert res["event"]["destination_address"] == "0/0/1"
|
||||||
|
assert res["event"]["destination_text"] == "Binary"
|
||||||
|
assert res["event"]["payload"] == "1"
|
||||||
|
assert res["event"]["type"] == "GroupValueWrite"
|
||||||
|
assert res["event"]["source_address"] == "1.2.3"
|
||||||
|
assert res["event"]["direction"] == "group_monitor_incoming"
|
||||||
|
assert res["event"]["timestamp"] is not None
|
||||||
|
|
||||||
|
# incoming DPT 5 telegram
|
||||||
|
await knx.receive_write("0/1/1", (0x50,), source="1.1.6")
|
||||||
|
res = await client.receive_json()
|
||||||
|
assert res["event"]["destination_address"] == "0/1/1"
|
||||||
|
assert res["event"]["destination_text"] == "percent"
|
||||||
|
assert res["event"]["payload"] == "0x50"
|
||||||
|
assert res["event"]["value"] == "31 %"
|
||||||
|
assert res["event"]["type"] == "GroupValueWrite"
|
||||||
|
assert res["event"]["source_address"] == "1.1.6"
|
||||||
|
assert (
|
||||||
|
res["event"]["source_text"]
|
||||||
|
== "Enertex Bayern GmbH Enertex KNX LED Dimmsequenzer 20A/5x REG"
|
||||||
|
)
|
||||||
|
assert res["event"]["direction"] == "group_monitor_incoming"
|
||||||
|
assert res["event"]["timestamp"] is not None
|
||||||
|
|
||||||
|
# incoming undecodable telegram (wrong payload type)
|
||||||
|
await knx.receive_write("0/1/1", True, source="1.1.6")
|
||||||
|
res = await client.receive_json()
|
||||||
|
assert res["event"]["destination_address"] == "0/1/1"
|
||||||
|
assert res["event"]["destination_text"] == "percent"
|
||||||
|
assert res["event"]["payload"] == "1"
|
||||||
|
assert res["event"]["value"] == "Error decoding value"
|
||||||
|
assert res["event"]["type"] == "GroupValueWrite"
|
||||||
|
assert res["event"]["source_address"] == "1.1.6"
|
||||||
|
assert (
|
||||||
|
res["event"]["source_text"]
|
||||||
|
== "Enertex Bayern GmbH Enertex KNX LED Dimmsequenzer 20A/5x REG"
|
||||||
|
)
|
||||||
|
assert res["event"]["direction"] == "group_monitor_incoming"
|
||||||
|
assert res["event"]["timestamp"] is not None
|
Loading…
Add table
Add a link
Reference in a new issue