Update KNX frontend - add Group monitor telegram detail view (#95144)

* Use TelegramDict for WS communication

* Update knx_frontend
This commit is contained in:
Matthias Alphart 2023-06-25 14:58:08 +02:00 committed by GitHub
parent f84887d5f8
commit 2ce23c17ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 89 additions and 127 deletions

View file

@ -114,20 +114,6 @@ class KNXConfigEntryData(TypedDict, total=False):
telegram_log_size: int # not required 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): class ColorTempModes(Enum):
"""Color temperature modes for config validation.""" """Color temperature modes for config validation."""

View file

@ -13,6 +13,6 @@
"requirements": [ "requirements": [
"xknx==2.10.0", "xknx==2.10.0",
"xknxproject==3.2.0", "xknxproject==3.2.0",
"knx-frontend==2023.6.9.195839" "knx-frontend==2023.6.23.191712"
] ]
} }

View file

@ -20,9 +20,13 @@ from .project import KNXProject
class TelegramDict(TypedDict): class TelegramDict(TypedDict):
"""Represent a Telegram as a dict.""" """Represent a Telegram as a dict."""
# this has to be in sync with the frontend implementation
destination: str destination: str
destination_name: str destination_name: str
direction: str direction: str
dpt_main: int | None
dpt_sub: int | None
dpt_name: str | None
payload: int | tuple[int, ...] | None payload: int | tuple[int, ...] | None
source: str source: str
source_name: str source_name: str
@ -57,7 +61,7 @@ class Telegrams:
async def _xknx_telegram_cb(self, telegram: Telegram) -> None: async 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.appendleft(telegram_dict) self.recent_telegrams.append(telegram_dict)
for job in self._jobs: for job in self._jobs:
self.hass.async_run_hass_job(job, telegram_dict) self.hass.async_run_hass_job(job, telegram_dict)
@ -80,6 +84,9 @@ class Telegrams:
def telegram_to_dict(self, telegram: Telegram) -> TelegramDict: def telegram_to_dict(self, telegram: Telegram) -> TelegramDict:
"""Convert a Telegram to a dict.""" """Convert a Telegram to a dict."""
dst_name = "" dst_name = ""
dpt_main = None
dpt_sub = None
dpt_name = None
payload_data: int | tuple[int, ...] | None = None payload_data: int | tuple[int, ...] | None = None
src_name = "" src_name = ""
transcoder = None transcoder = None
@ -104,6 +111,9 @@ class Telegrams:
if transcoder is not None: if transcoder is not None:
try: try:
value = transcoder.from_knx(telegram.payload.value) 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 unit = transcoder.unit
except XKNXException: except XKNXException:
value = "Error decoding value" value = "Error decoding value"
@ -112,6 +122,9 @@ class Telegrams:
destination=f"{telegram.destination_address}", destination=f"{telegram.destination_address}",
destination_name=dst_name, destination_name=dst_name,
direction=telegram.direction.value, direction=telegram.direction.value,
dpt_main=dpt_main,
dpt_sub=dpt_sub,
dpt_name=dpt_name,
payload=payload_data, payload=payload_data,
source=f"{telegram.source_address}", source=f"{telegram.source_address}",
source_name=src_name, source_name=src_name,

View file

@ -3,15 +3,14 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Final 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 import voluptuous as vol
from xknx.telegram import TelegramDirection
from xknxproject.exceptions import XknxProjectException from xknxproject.exceptions import XknxProjectException
from homeassistant.components import panel_custom, websocket_api from homeassistant.components import panel_custom, websocket_api
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from .const import DOMAIN, KNXBusMonitorMessage from .const import DOMAIN
from .telegrams import TelegramDict from .telegrams import TelegramDict
if TYPE_CHECKING: if TYPE_CHECKING:
@ -30,19 +29,18 @@ async def register_panel(hass: HomeAssistant) -> None:
websocket_api.async_register_command(hass, ws_subscribe_telegram) websocket_api.async_register_command(hass, ws_subscribe_telegram)
if DOMAIN not in hass.data.get("frontend_panels", {}): if DOMAIN not in hass.data.get("frontend_panels", {}):
path = locate_dir()
hass.http.register_static_path( hass.http.register_static_path(
URL_BASE, URL_BASE,
path, path=knx_panel.locate_dir(),
cache_headers=not is_dev_build(), cache_headers=knx_panel.is_prod_build,
) )
await panel_custom.async_register_panel( await panel_custom.async_register_panel(
hass=hass, hass=hass,
frontend_url_path=DOMAIN, frontend_url_path=DOMAIN,
webcomponent_name="knx-frontend", webcomponent_name=knx_panel.webcomponent_name,
sidebar_title=DOMAIN.upper(), sidebar_title=DOMAIN.upper(),
sidebar_icon="mdi:bus-electric", sidebar_icon="mdi:bus-electric",
module_url=f"{URL_BASE}/{entrypoint_js()}", module_url=f"{URL_BASE}/{knx_panel.entrypoint_js}",
embed_iframe=True, embed_iframe=True,
require_admin=True, require_admin=True,
) )
@ -145,10 +143,7 @@ def ws_group_monitor_info(
) -> None: ) -> None:
"""Handle get info command of group monitor.""" """Handle get info command of group monitor."""
knx: KNXModule = hass.data[DOMAIN] knx: KNXModule = hass.data[DOMAIN]
recent_telegrams = [ recent_telegrams = [*knx.telegrams.recent_telegrams]
_telegram_dict_to_group_monitor(telegram)
for telegram in knx.telegrams.recent_telegrams
]
connection.send_result( connection.send_result(
msg["id"], msg["id"],
{ {
@ -178,7 +173,7 @@ def ws_subscribe_telegram(
"""Forward telegram to websocket subscription.""" """Forward telegram to websocket subscription."""
connection.send_event( connection.send_event(
msg["id"], msg["id"],
_telegram_dict_to_group_monitor(telegram), telegram,
) )
connection.subscriptions[msg["id"]] = knx.telegrams.async_listen_telegram( connection.subscriptions[msg["id"]] = knx.telegrams.async_listen_telegram(
@ -186,38 +181,3 @@ def ws_subscribe_telegram(
name="KNX GroupMonitor subscription", name="KNX GroupMonitor subscription",
) )
connection.send_result(msg["id"]) 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,
)

View file

@ -1089,7 +1089,7 @@ kegtron-ble==0.4.0
kiwiki-client==0.1.1 kiwiki-client==0.1.1
# homeassistant.components.knx # homeassistant.components.knx
knx-frontend==2023.6.9.195839 knx-frontend==2023.6.23.191712
# homeassistant.components.konnected # homeassistant.components.konnected
konnected==1.2.0 konnected==1.2.0

View file

@ -842,7 +842,7 @@ justnimbus==0.6.0
kegtron-ble==0.4.0 kegtron-ble==0.4.0
# homeassistant.components.knx # homeassistant.components.knx
knx-frontend==2023.6.9.195839 knx-frontend==2023.6.23.191712
# homeassistant.components.konnected # homeassistant.components.konnected
konnected==1.2.0 konnected==1.2.0

View file

@ -183,22 +183,22 @@ async def test_knx_subscribe_telegrams_command_recent_telegrams(
recent_tgs = res["result"]["recent_telegrams"] recent_tgs = res["result"]["recent_telegrams"]
assert len(recent_tgs) == 2 assert len(recent_tgs) == 2
# telegrams are sorted from newest to oldest # telegrams are sorted from oldest to newest
assert recent_tgs[0]["destination_address"] == "1/2/4" assert recent_tgs[0]["destination"] == "1/3/4"
assert recent_tgs[0]["payload"] == "1" assert recent_tgs[0]["payload"] == 1
assert recent_tgs[0]["type"] == "GroupValueWrite" assert recent_tgs[0]["telegramtype"] == "GroupValueWrite"
assert ( assert recent_tgs[0]["source"] == "1.2.3"
recent_tgs[0]["source_address"] == "0.0.0" assert recent_tgs[0]["direction"] == "Incoming"
) # needs to be the IA currently connected to assert isinstance(recent_tgs[0]["timestamp"], str)
assert recent_tgs[0]["direction"] == "group_monitor_outgoing"
assert recent_tgs[0]["timestamp"] is not None
assert recent_tgs[1]["destination_address"] == "1/3/4" assert recent_tgs[1]["destination"] == "1/2/4"
assert recent_tgs[1]["payload"] == "1" assert recent_tgs[1]["payload"] == 1
assert recent_tgs[1]["type"] == "GroupValueWrite" assert recent_tgs[1]["telegramtype"] == "GroupValueWrite"
assert recent_tgs[1]["source_address"] == "1.2.3" assert (
assert recent_tgs[1]["direction"] == "group_monitor_incoming" recent_tgs[1]["source"] == "0.0.0"
assert recent_tgs[1]["timestamp"] is not None ) # 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( async def test_knx_subscribe_telegrams_command_no_project(
@ -231,45 +231,45 @@ async def test_knx_subscribe_telegrams_command_no_project(
# receive events # receive events
res = await client.receive_json() res = await client.receive_json()
assert res["event"]["destination_address"] == "1/2/3" assert res["event"]["destination"] == "1/2/3"
assert res["event"]["payload"] == "" assert res["event"]["payload"] is None
assert res["event"]["type"] == "GroupValueRead" assert res["event"]["telegramtype"] == "GroupValueRead"
assert res["event"]["source_address"] == "1.2.3" assert res["event"]["source"] == "1.2.3"
assert res["event"]["direction"] == "group_monitor_incoming" assert res["event"]["direction"] == "Incoming"
assert res["event"]["timestamp"] is not None assert res["event"]["timestamp"] is not None
res = await client.receive_json() res = await client.receive_json()
assert res["event"]["destination_address"] == "1/3/4" assert res["event"]["destination"] == "1/3/4"
assert res["event"]["payload"] == "1" assert res["event"]["payload"] == 1
assert res["event"]["type"] == "GroupValueWrite" assert res["event"]["telegramtype"] == "GroupValueWrite"
assert res["event"]["source_address"] == "1.2.3" assert res["event"]["source"] == "1.2.3"
assert res["event"]["direction"] == "group_monitor_incoming" assert res["event"]["direction"] == "Incoming"
assert res["event"]["timestamp"] is not None assert res["event"]["timestamp"] is not None
res = await client.receive_json() res = await client.receive_json()
assert res["event"]["destination_address"] == "1/3/4" assert res["event"]["destination"] == "1/3/4"
assert res["event"]["payload"] == "0" assert res["event"]["payload"] == 0
assert res["event"]["type"] == "GroupValueWrite" assert res["event"]["telegramtype"] == "GroupValueWrite"
assert res["event"]["source_address"] == "1.2.3" assert res["event"]["source"] == "1.2.3"
assert res["event"]["direction"] == "group_monitor_incoming" assert res["event"]["direction"] == "Incoming"
assert res["event"]["timestamp"] is not None assert res["event"]["timestamp"] is not None
res = await client.receive_json() res = await client.receive_json()
assert res["event"]["destination_address"] == "1/3/8" assert res["event"]["destination"] == "1/3/8"
assert res["event"]["payload"] == "0x3445" assert res["event"]["payload"] == [52, 69]
assert res["event"]["type"] == "GroupValueWrite" assert res["event"]["telegramtype"] == "GroupValueWrite"
assert res["event"]["source_address"] == "1.2.3" assert res["event"]["source"] == "1.2.3"
assert res["event"]["direction"] == "group_monitor_incoming" assert res["event"]["direction"] == "Incoming"
assert res["event"]["timestamp"] is not None assert res["event"]["timestamp"] is not None
res = await client.receive_json() res = await client.receive_json()
assert res["event"]["destination_address"] == "1/2/4" assert res["event"]["destination"] == "1/2/4"
assert res["event"]["payload"] == "1" assert res["event"]["payload"] == 1
assert res["event"]["type"] == "GroupValueWrite" assert res["event"]["telegramtype"] == "GroupValueWrite"
assert ( assert (
res["event"]["source_address"] == "0.0.0" res["event"]["source"] == "0.0.0"
) # needs to be the IA currently connected to ) # 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 assert res["event"]["timestamp"] is not None
@ -289,42 +289,45 @@ async def test_knx_subscribe_telegrams_command_project(
# incoming DPT 1 telegram # incoming DPT 1 telegram
await knx.receive_write("0/0/1", True) await knx.receive_write("0/0/1", True)
res = await client.receive_json() res = await client.receive_json()
assert res["event"]["destination_address"] == "0/0/1" assert res["event"]["destination"] == "0/0/1"
assert res["event"]["destination_text"] == "Binary" assert res["event"]["destination_name"] == "Binary"
assert res["event"]["payload"] == "1" assert res["event"]["payload"] == 1
assert res["event"]["type"] == "GroupValueWrite" assert res["event"]["telegramtype"] == "GroupValueWrite"
assert res["event"]["source_address"] == "1.2.3" assert res["event"]["source"] == "1.2.3"
assert res["event"]["direction"] == "group_monitor_incoming" assert res["event"]["direction"] == "Incoming"
assert res["event"]["timestamp"] is not None assert res["event"]["timestamp"] is not None
# incoming DPT 5 telegram # incoming DPT 5 telegram
await knx.receive_write("0/1/1", (0x50,), source="1.1.6") await knx.receive_write("0/1/1", (0x50,), source="1.1.6")
res = await client.receive_json() res = await client.receive_json()
assert res["event"]["destination_address"] == "0/1/1" assert res["event"]["destination"] == "0/1/1"
assert res["event"]["destination_text"] == "percent" assert res["event"]["destination_name"] == "percent"
assert res["event"]["payload"] == "0x50" assert res["event"]["payload"] == [
assert res["event"]["value"] == "31 %" 80,
assert res["event"]["type"] == "GroupValueWrite" ]
assert res["event"]["source_address"] == "1.1.6" assert res["event"]["value"] == 31
assert res["event"]["unit"] == "%"
assert res["event"]["telegramtype"] == "GroupValueWrite"
assert res["event"]["source"] == "1.1.6"
assert ( assert (
res["event"]["source_text"] res["event"]["source_name"]
== "Enertex Bayern GmbH Enertex KNX LED Dimmsequenzer 20A/5x REG" == "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 assert res["event"]["timestamp"] is not None
# incoming undecodable telegram (wrong payload type) # incoming undecodable telegram (wrong payload type)
await knx.receive_write("0/1/1", True, source="1.1.6") await knx.receive_write("0/1/1", True, source="1.1.6")
res = await client.receive_json() res = await client.receive_json()
assert res["event"]["destination_address"] == "0/1/1" assert res["event"]["destination"] == "0/1/1"
assert res["event"]["destination_text"] == "percent" assert res["event"]["destination_name"] == "percent"
assert res["event"]["payload"] == "1" assert res["event"]["payload"] == 1
assert res["event"]["value"] == "Error decoding value" assert res["event"]["value"] == "Error decoding value"
assert res["event"]["type"] == "GroupValueWrite" assert res["event"]["telegramtype"] == "GroupValueWrite"
assert res["event"]["source_address"] == "1.1.6" assert res["event"]["source"] == "1.1.6"
assert ( assert (
res["event"]["source_text"] res["event"]["source_name"]
== "Enertex Bayern GmbH Enertex KNX LED Dimmsequenzer 20A/5x REG" == "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 assert res["event"]["timestamp"] is not None