From 2ce23c17ca75dabbeaaa59898019f24e90a2291c Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sun, 25 Jun 2023 14:58:08 +0200 Subject: [PATCH] Update KNX frontend - add Group monitor telegram detail view (#95144) * Use TelegramDict for WS communication * Update knx_frontend --- homeassistant/components/knx/const.py | 14 --- homeassistant/components/knx/manifest.json | 2 +- homeassistant/components/knx/telegrams.py | 15 ++- homeassistant/components/knx/websocket.py | 56 ++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/knx/test_websocket.py | 125 +++++++++++---------- 7 files changed, 89 insertions(+), 127 deletions(-) diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index 5546a2d6fd9..bacd678bb99 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -114,20 +114,6 @@ class KNXConfigEntryData(TypedDict, total=False): telegram_log_size: int # not required -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): """Color temperature modes for config validation.""" diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 822b18866c7..6cb6a4ffcf7 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -13,6 +13,6 @@ "requirements": [ "xknx==2.10.0", "xknxproject==3.2.0", - "knx-frontend==2023.6.9.195839" + "knx-frontend==2023.6.23.191712" ] } diff --git a/homeassistant/components/knx/telegrams.py b/homeassistant/components/knx/telegrams.py index 5b429b0bdc1..09307794066 100644 --- a/homeassistant/components/knx/telegrams.py +++ b/homeassistant/components/knx/telegrams.py @@ -20,9 +20,13 @@ from .project import KNXProject class TelegramDict(TypedDict): """Represent a Telegram as a dict.""" + # this has to be in sync with the frontend implementation destination: str destination_name: str direction: str + dpt_main: int | None + dpt_sub: int | None + dpt_name: str | None payload: int | tuple[int, ...] | None source: str source_name: str @@ -57,7 +61,7 @@ class Telegrams: async def _xknx_telegram_cb(self, telegram: Telegram) -> None: """Handle incoming and outgoing telegrams from xknx.""" telegram_dict = self.telegram_to_dict(telegram) - self.recent_telegrams.appendleft(telegram_dict) + self.recent_telegrams.append(telegram_dict) for job in self._jobs: self.hass.async_run_hass_job(job, telegram_dict) @@ -80,6 +84,9 @@ class Telegrams: def telegram_to_dict(self, telegram: Telegram) -> TelegramDict: """Convert a Telegram to a dict.""" dst_name = "" + dpt_main = None + dpt_sub = None + dpt_name = None payload_data: int | tuple[int, ...] | None = None src_name = "" transcoder = None @@ -104,6 +111,9 @@ class Telegrams: if transcoder is not None: try: value = transcoder.from_knx(telegram.payload.value) + dpt_main = transcoder.dpt_main_number + dpt_sub = transcoder.dpt_sub_number + dpt_name = transcoder.value_type unit = transcoder.unit except XKNXException: value = "Error decoding value" @@ -112,6 +122,9 @@ class Telegrams: destination=f"{telegram.destination_address}", destination_name=dst_name, direction=telegram.direction.value, + dpt_main=dpt_main, + dpt_sub=dpt_sub, + dpt_name=dpt_name, payload=payload_data, source=f"{telegram.source_address}", source_name=src_name, diff --git a/homeassistant/components/knx/websocket.py b/homeassistant/components/knx/websocket.py index feb53ddc908..ad29fd19928 100644 --- a/homeassistant/components/knx/websocket.py +++ b/homeassistant/components/knx/websocket.py @@ -3,15 +3,14 @@ from __future__ import annotations from typing import TYPE_CHECKING, Final -from knx_frontend import entrypoint_js, is_dev_build, locate_dir +import knx_frontend as knx_panel import voluptuous as vol -from xknx.telegram import TelegramDirection from xknxproject.exceptions import XknxProjectException from homeassistant.components import panel_custom, websocket_api from homeassistant.core import HomeAssistant, callback -from .const import DOMAIN, KNXBusMonitorMessage +from .const import DOMAIN from .telegrams import TelegramDict if TYPE_CHECKING: @@ -30,19 +29,18 @@ async def register_panel(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_subscribe_telegram) if DOMAIN not in hass.data.get("frontend_panels", {}): - path = locate_dir() hass.http.register_static_path( URL_BASE, - path, - cache_headers=not is_dev_build(), + path=knx_panel.locate_dir(), + cache_headers=knx_panel.is_prod_build, ) await panel_custom.async_register_panel( hass=hass, frontend_url_path=DOMAIN, - webcomponent_name="knx-frontend", + webcomponent_name=knx_panel.webcomponent_name, sidebar_title=DOMAIN.upper(), sidebar_icon="mdi:bus-electric", - module_url=f"{URL_BASE}/{entrypoint_js()}", + module_url=f"{URL_BASE}/{knx_panel.entrypoint_js}", embed_iframe=True, require_admin=True, ) @@ -145,10 +143,7 @@ def ws_group_monitor_info( ) -> None: """Handle get info command of group monitor.""" knx: KNXModule = hass.data[DOMAIN] - recent_telegrams = [ - _telegram_dict_to_group_monitor(telegram) - for telegram in knx.telegrams.recent_telegrams - ] + recent_telegrams = [*knx.telegrams.recent_telegrams] connection.send_result( msg["id"], { @@ -178,7 +173,7 @@ def ws_subscribe_telegram( """Forward telegram to websocket subscription.""" connection.send_event( msg["id"], - _telegram_dict_to_group_monitor(telegram), + telegram, ) connection.subscriptions[msg["id"]] = knx.telegrams.async_listen_telegram( @@ -186,38 +181,3 @@ def ws_subscribe_telegram( name="KNX GroupMonitor subscription", ) connection.send_result(msg["id"]) - - -def _telegram_dict_to_group_monitor(telegram: TelegramDict) -> KNXBusMonitorMessage: - """Convert a TelegramDict to a KNXBusMonitorMessage object.""" - direction = ( - "group_monitor_incoming" - if telegram["direction"] == TelegramDirection.INCOMING.value - else "group_monitor_outgoing" - ) - - _payload = telegram["payload"] - if isinstance(_payload, tuple): - payload = f"0x{bytes(_payload).hex()}" - elif isinstance(_payload, int): - payload = f"{_payload:d}" - else: - payload = "" - - timestamp = telegram["timestamp"].strftime("%H:%M:%S.%f")[:-3] - - if (value := telegram["value"]) is not None: - unit = telegram["unit"] - value = f"{value}{' ' + unit if unit else ''}" - - return KNXBusMonitorMessage( - destination_address=telegram["destination"], - destination_text=telegram["destination_name"], - direction=direction, - payload=payload, - source_address=telegram["source"], - source_text=telegram["source_name"], - timestamp=timestamp, - type=telegram["telegramtype"], - value=value, - ) diff --git a/requirements_all.txt b/requirements_all.txt index 76cbe362ded..2546116f246 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1089,7 +1089,7 @@ kegtron-ble==0.4.0 kiwiki-client==0.1.1 # homeassistant.components.knx -knx-frontend==2023.6.9.195839 +knx-frontend==2023.6.23.191712 # homeassistant.components.konnected konnected==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6fcf65c18a1..d323757226c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -842,7 +842,7 @@ justnimbus==0.6.0 kegtron-ble==0.4.0 # homeassistant.components.knx -knx-frontend==2023.6.9.195839 +knx-frontend==2023.6.23.191712 # homeassistant.components.konnected konnected==1.2.0 diff --git a/tests/components/knx/test_websocket.py b/tests/components/knx/test_websocket.py index 115b92f70e8..76a9544552f 100644 --- a/tests/components/knx/test_websocket.py +++ b/tests/components/knx/test_websocket.py @@ -183,22 +183,22 @@ async def test_knx_subscribe_telegrams_command_recent_telegrams( recent_tgs = res["result"]["recent_telegrams"] assert len(recent_tgs) == 2 - # telegrams are sorted from newest to oldest - assert recent_tgs[0]["destination_address"] == "1/2/4" - assert recent_tgs[0]["payload"] == "1" - assert recent_tgs[0]["type"] == "GroupValueWrite" - assert ( - recent_tgs[0]["source_address"] == "0.0.0" - ) # needs to be the IA currently connected to - assert recent_tgs[0]["direction"] == "group_monitor_outgoing" - assert recent_tgs[0]["timestamp"] is not None + # telegrams are sorted from oldest to newest + assert recent_tgs[0]["destination"] == "1/3/4" + assert recent_tgs[0]["payload"] == 1 + assert recent_tgs[0]["telegramtype"] == "GroupValueWrite" + assert recent_tgs[0]["source"] == "1.2.3" + assert recent_tgs[0]["direction"] == "Incoming" + assert isinstance(recent_tgs[0]["timestamp"], str) - assert recent_tgs[1]["destination_address"] == "1/3/4" - assert recent_tgs[1]["payload"] == "1" - assert recent_tgs[1]["type"] == "GroupValueWrite" - assert recent_tgs[1]["source_address"] == "1.2.3" - assert recent_tgs[1]["direction"] == "group_monitor_incoming" - assert recent_tgs[1]["timestamp"] is not None + assert recent_tgs[1]["destination"] == "1/2/4" + assert recent_tgs[1]["payload"] == 1 + assert recent_tgs[1]["telegramtype"] == "GroupValueWrite" + assert ( + recent_tgs[1]["source"] == "0.0.0" + ) # needs to be the IA currently connected to + assert recent_tgs[1]["direction"] == "Outgoing" + assert isinstance(recent_tgs[1]["timestamp"], str) async def test_knx_subscribe_telegrams_command_no_project( @@ -231,45 +231,45 @@ async def test_knx_subscribe_telegrams_command_no_project( # 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"]["destination"] == "1/2/3" + assert res["event"]["payload"] is None + assert res["event"]["telegramtype"] == "GroupValueRead" + assert res["event"]["source"] == "1.2.3" + assert res["event"]["direction"] == "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"]["destination"] == "1/3/4" + assert res["event"]["payload"] == 1 + assert res["event"]["telegramtype"] == "GroupValueWrite" + assert res["event"]["source"] == "1.2.3" + assert res["event"]["direction"] == "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"]["destination"] == "1/3/4" + assert res["event"]["payload"] == 0 + assert res["event"]["telegramtype"] == "GroupValueWrite" + assert res["event"]["source"] == "1.2.3" + assert res["event"]["direction"] == "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"]["destination"] == "1/3/8" + assert res["event"]["payload"] == [52, 69] + assert res["event"]["telegramtype"] == "GroupValueWrite" + assert res["event"]["source"] == "1.2.3" + assert res["event"]["direction"] == "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"]["destination"] == "1/2/4" + assert res["event"]["payload"] == 1 + assert res["event"]["telegramtype"] == "GroupValueWrite" assert ( - res["event"]["source_address"] == "0.0.0" + res["event"]["source"] == "0.0.0" ) # needs to be the IA currently connected to - assert res["event"]["direction"] == "group_monitor_outgoing" + assert res["event"]["direction"] == "Outgoing" assert res["event"]["timestamp"] is not None @@ -289,42 +289,45 @@ async def test_knx_subscribe_telegrams_command_project( # 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"]["destination"] == "0/0/1" + assert res["event"]["destination_name"] == "Binary" + assert res["event"]["payload"] == 1 + assert res["event"]["telegramtype"] == "GroupValueWrite" + assert res["event"]["source"] == "1.2.3" + assert res["event"]["direction"] == "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"]["destination"] == "0/1/1" + assert res["event"]["destination_name"] == "percent" + assert res["event"]["payload"] == [ + 80, + ] + assert res["event"]["value"] == 31 + assert res["event"]["unit"] == "%" + assert res["event"]["telegramtype"] == "GroupValueWrite" + assert res["event"]["source"] == "1.1.6" assert ( - res["event"]["source_text"] + res["event"]["source_name"] == "Enertex Bayern GmbH Enertex KNX LED Dimmsequenzer 20A/5x REG" ) - assert res["event"]["direction"] == "group_monitor_incoming" + assert res["event"]["direction"] == "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"]["destination"] == "0/1/1" + assert res["event"]["destination_name"] == "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"]["telegramtype"] == "GroupValueWrite" + assert res["event"]["source"] == "1.1.6" assert ( - res["event"]["source_text"] + res["event"]["source_name"] == "Enertex Bayern GmbH Enertex KNX LED Dimmsequenzer 20A/5x REG" ) - assert res["event"]["direction"] == "group_monitor_incoming" + assert res["event"]["direction"] == "Incoming" assert res["event"]["timestamp"] is not None