Update KNX frontend - add Group monitor telegram detail view (#95144)
* Use TelegramDict for WS communication * Update knx_frontend
This commit is contained in:
parent
f84887d5f8
commit
2ce23c17ca
7 changed files with 89 additions and 127 deletions
|
@ -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."""
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
|
||||||
)
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue