Check KNX integration is loaded on websocket calls (#123178)
parent
25cdd737a9
commit
1650cee16c
|
@ -2,7 +2,10 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Final
|
import asyncio
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
from functools import wraps
|
||||||
|
from typing import TYPE_CHECKING, Any, Final, overload
|
||||||
|
|
||||||
import knx_frontend as knx_panel
|
import knx_frontend as knx_panel
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
@ -77,21 +80,92 @@ async def register_panel(hass: HomeAssistant) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
type KnxWebSocketCommandHandler = Callable[
|
||||||
|
[HomeAssistant, KNXModule, websocket_api.ActiveConnection, dict[str, Any]], None
|
||||||
|
]
|
||||||
|
type KnxAsyncWebSocketCommandHandler = Callable[
|
||||||
|
[HomeAssistant, KNXModule, websocket_api.ActiveConnection, dict[str, Any]],
|
||||||
|
Awaitable[None],
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def provide_knx(
|
||||||
|
func: KnxAsyncWebSocketCommandHandler,
|
||||||
|
) -> websocket_api.const.AsyncWebSocketCommandHandler: ...
|
||||||
|
@overload
|
||||||
|
def provide_knx(
|
||||||
|
func: KnxWebSocketCommandHandler,
|
||||||
|
) -> websocket_api.const.WebSocketCommandHandler: ...
|
||||||
|
|
||||||
|
|
||||||
|
def provide_knx(
|
||||||
|
func: KnxAsyncWebSocketCommandHandler | KnxWebSocketCommandHandler,
|
||||||
|
) -> (
|
||||||
|
websocket_api.const.AsyncWebSocketCommandHandler
|
||||||
|
| websocket_api.const.WebSocketCommandHandler
|
||||||
|
):
|
||||||
|
"""Websocket decorator to provide a KNXModule instance."""
|
||||||
|
|
||||||
|
def _send_not_loaded_error(
|
||||||
|
connection: websocket_api.ActiveConnection, msg_id: int
|
||||||
|
) -> None:
|
||||||
|
connection.send_error(
|
||||||
|
msg_id,
|
||||||
|
websocket_api.const.ERR_HOME_ASSISTANT_ERROR,
|
||||||
|
"KNX integration not loaded.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if asyncio.iscoroutinefunction(func):
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
async def with_knx(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
connection: websocket_api.ActiveConnection,
|
||||||
|
msg: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Add KNX Module to call function."""
|
||||||
|
try:
|
||||||
|
knx: KNXModule = hass.data[DOMAIN]
|
||||||
|
except KeyError:
|
||||||
|
_send_not_loaded_error(connection, msg["id"])
|
||||||
|
return
|
||||||
|
await func(hass, knx, connection, msg)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def with_knx(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
connection: websocket_api.ActiveConnection,
|
||||||
|
msg: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Add KNX Module to call function."""
|
||||||
|
try:
|
||||||
|
knx: KNXModule = hass.data[DOMAIN]
|
||||||
|
except KeyError:
|
||||||
|
_send_not_loaded_error(connection, msg["id"])
|
||||||
|
return
|
||||||
|
func(hass, knx, connection, msg)
|
||||||
|
|
||||||
|
return with_knx
|
||||||
|
|
||||||
|
|
||||||
@websocket_api.require_admin
|
@websocket_api.require_admin
|
||||||
@websocket_api.websocket_command(
|
@websocket_api.websocket_command(
|
||||||
{
|
{
|
||||||
vol.Required("type"): "knx/info",
|
vol.Required("type"): "knx/info",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@provide_knx
|
||||||
@callback
|
@callback
|
||||||
def ws_info(
|
def ws_info(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
knx: KNXModule,
|
||||||
connection: websocket_api.ActiveConnection,
|
connection: websocket_api.ActiveConnection,
|
||||||
msg: dict,
|
msg: dict,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle get info command."""
|
"""Handle get info command."""
|
||||||
knx: KNXModule = hass.data[DOMAIN]
|
|
||||||
|
|
||||||
_project_info = None
|
_project_info = None
|
||||||
if project_info := knx.project.info:
|
if project_info := knx.project.info:
|
||||||
_project_info = {
|
_project_info = {
|
||||||
|
@ -119,13 +193,14 @@ def ws_info(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@websocket_api.async_response
|
@websocket_api.async_response
|
||||||
|
@provide_knx
|
||||||
async def ws_get_knx_project(
|
async def ws_get_knx_project(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
knx: KNXModule,
|
||||||
connection: websocket_api.ActiveConnection,
|
connection: websocket_api.ActiveConnection,
|
||||||
msg: dict,
|
msg: dict,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle get KNX project."""
|
"""Handle get KNX project."""
|
||||||
knx: KNXModule = hass.data[DOMAIN]
|
|
||||||
knxproject = await knx.project.get_knxproject()
|
knxproject = await knx.project.get_knxproject()
|
||||||
connection.send_result(
|
connection.send_result(
|
||||||
msg["id"],
|
msg["id"],
|
||||||
|
@ -145,13 +220,14 @@ async def ws_get_knx_project(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@websocket_api.async_response
|
@websocket_api.async_response
|
||||||
|
@provide_knx
|
||||||
async def ws_project_file_process(
|
async def ws_project_file_process(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
knx: KNXModule,
|
||||||
connection: websocket_api.ActiveConnection,
|
connection: websocket_api.ActiveConnection,
|
||||||
msg: dict,
|
msg: dict,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle get info command."""
|
"""Handle get info command."""
|
||||||
knx: KNXModule = hass.data[DOMAIN]
|
|
||||||
try:
|
try:
|
||||||
await knx.project.process_project_file(
|
await knx.project.process_project_file(
|
||||||
xknx=knx.xknx,
|
xknx=knx.xknx,
|
||||||
|
@ -175,13 +251,14 @@ async def ws_project_file_process(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@websocket_api.async_response
|
@websocket_api.async_response
|
||||||
|
@provide_knx
|
||||||
async def ws_project_file_remove(
|
async def ws_project_file_remove(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
knx: KNXModule,
|
||||||
connection: websocket_api.ActiveConnection,
|
connection: websocket_api.ActiveConnection,
|
||||||
msg: dict,
|
msg: dict,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle get info command."""
|
"""Handle get info command."""
|
||||||
knx: KNXModule = hass.data[DOMAIN]
|
|
||||||
await knx.project.remove_project_file()
|
await knx.project.remove_project_file()
|
||||||
connection.send_result(msg["id"])
|
connection.send_result(msg["id"])
|
||||||
|
|
||||||
|
@ -192,14 +269,15 @@ async def ws_project_file_remove(
|
||||||
vol.Required("type"): "knx/group_monitor_info",
|
vol.Required("type"): "knx/group_monitor_info",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@provide_knx
|
||||||
@callback
|
@callback
|
||||||
def ws_group_monitor_info(
|
def ws_group_monitor_info(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
knx: KNXModule,
|
||||||
connection: websocket_api.ActiveConnection,
|
connection: websocket_api.ActiveConnection,
|
||||||
msg: dict,
|
msg: dict,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle get info command of group monitor."""
|
"""Handle get info command of group monitor."""
|
||||||
knx: KNXModule = hass.data[DOMAIN]
|
|
||||||
recent_telegrams = [*knx.telegrams.recent_telegrams]
|
recent_telegrams = [*knx.telegrams.recent_telegrams]
|
||||||
connection.send_result(
|
connection.send_result(
|
||||||
msg["id"],
|
msg["id"],
|
||||||
|
@ -272,8 +350,10 @@ def ws_validate_entity(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@websocket_api.async_response
|
@websocket_api.async_response
|
||||||
|
@provide_knx
|
||||||
async def ws_create_entity(
|
async def ws_create_entity(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
knx: KNXModule,
|
||||||
connection: websocket_api.ActiveConnection,
|
connection: websocket_api.ActiveConnection,
|
||||||
msg: dict,
|
msg: dict,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -283,7 +363,6 @@ async def ws_create_entity(
|
||||||
except EntityStoreValidationException as exc:
|
except EntityStoreValidationException as exc:
|
||||||
connection.send_result(msg["id"], exc.validation_error)
|
connection.send_result(msg["id"], exc.validation_error)
|
||||||
return
|
return
|
||||||
knx: KNXModule = hass.data[DOMAIN]
|
|
||||||
try:
|
try:
|
||||||
entity_id = await knx.config_store.create_entity(
|
entity_id = await knx.config_store.create_entity(
|
||||||
# use validation result so defaults are applied
|
# use validation result so defaults are applied
|
||||||
|
@ -308,8 +387,10 @@ async def ws_create_entity(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@websocket_api.async_response
|
@websocket_api.async_response
|
||||||
|
@provide_knx
|
||||||
async def ws_update_entity(
|
async def ws_update_entity(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
knx: KNXModule,
|
||||||
connection: websocket_api.ActiveConnection,
|
connection: websocket_api.ActiveConnection,
|
||||||
msg: dict,
|
msg: dict,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -319,7 +400,6 @@ async def ws_update_entity(
|
||||||
except EntityStoreValidationException as exc:
|
except EntityStoreValidationException as exc:
|
||||||
connection.send_result(msg["id"], exc.validation_error)
|
connection.send_result(msg["id"], exc.validation_error)
|
||||||
return
|
return
|
||||||
knx: KNXModule = hass.data[DOMAIN]
|
|
||||||
try:
|
try:
|
||||||
await knx.config_store.update_entity(
|
await knx.config_store.update_entity(
|
||||||
validated_data[CONF_PLATFORM],
|
validated_data[CONF_PLATFORM],
|
||||||
|
@ -344,13 +424,14 @@ async def ws_update_entity(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@websocket_api.async_response
|
@websocket_api.async_response
|
||||||
|
@provide_knx
|
||||||
async def ws_delete_entity(
|
async def ws_delete_entity(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
knx: KNXModule,
|
||||||
connection: websocket_api.ActiveConnection,
|
connection: websocket_api.ActiveConnection,
|
||||||
msg: dict,
|
msg: dict,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Delete entity from entity store and remove it."""
|
"""Delete entity from entity store and remove it."""
|
||||||
knx: KNXModule = hass.data[DOMAIN]
|
|
||||||
try:
|
try:
|
||||||
await knx.config_store.delete_entity(msg[CONF_ENTITY_ID])
|
await knx.config_store.delete_entity(msg[CONF_ENTITY_ID])
|
||||||
except ConfigStoreException as err:
|
except ConfigStoreException as err:
|
||||||
|
@ -367,14 +448,15 @@ async def ws_delete_entity(
|
||||||
vol.Required("type"): "knx/get_entity_entries",
|
vol.Required("type"): "knx/get_entity_entries",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@provide_knx
|
||||||
@callback
|
@callback
|
||||||
def ws_get_entity_entries(
|
def ws_get_entity_entries(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
knx: KNXModule,
|
||||||
connection: websocket_api.ActiveConnection,
|
connection: websocket_api.ActiveConnection,
|
||||||
msg: dict,
|
msg: dict,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Get entities configured from entity store."""
|
"""Get entities configured from entity store."""
|
||||||
knx: KNXModule = hass.data[DOMAIN]
|
|
||||||
entity_entries = [
|
entity_entries = [
|
||||||
entry.extended_dict for entry in knx.config_store.get_entity_entries()
|
entry.extended_dict for entry in knx.config_store.get_entity_entries()
|
||||||
]
|
]
|
||||||
|
@ -388,14 +470,15 @@ def ws_get_entity_entries(
|
||||||
vol.Required(CONF_ENTITY_ID): str,
|
vol.Required(CONF_ENTITY_ID): str,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@provide_knx
|
||||||
@callback
|
@callback
|
||||||
def ws_get_entity_config(
|
def ws_get_entity_config(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
knx: KNXModule,
|
||||||
connection: websocket_api.ActiveConnection,
|
connection: websocket_api.ActiveConnection,
|
||||||
msg: dict,
|
msg: dict,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Get entity configuration from entity store."""
|
"""Get entity configuration from entity store."""
|
||||||
knx: KNXModule = hass.data[DOMAIN]
|
|
||||||
try:
|
try:
|
||||||
config_info = knx.config_store.get_entity_config(msg[CONF_ENTITY_ID])
|
config_info = knx.config_store.get_entity_config(msg[CONF_ENTITY_ID])
|
||||||
except ConfigStoreException as err:
|
except ConfigStoreException as err:
|
||||||
|
@ -414,14 +497,15 @@ def ws_get_entity_config(
|
||||||
vol.Optional("area_id"): str,
|
vol.Optional("area_id"): str,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@provide_knx
|
||||||
@callback
|
@callback
|
||||||
def ws_create_device(
|
def ws_create_device(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
knx: KNXModule,
|
||||||
connection: websocket_api.ActiveConnection,
|
connection: websocket_api.ActiveConnection,
|
||||||
msg: dict,
|
msg: dict,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Create a new KNX device."""
|
"""Create a new KNX device."""
|
||||||
knx: KNXModule = hass.data[DOMAIN]
|
|
||||||
identifier = f"knx_vdev_{ulid_now()}"
|
identifier = f"knx_vdev_{ulid_now()}"
|
||||||
device_registry = dr.async_get(hass)
|
device_registry = dr.async_get(hass)
|
||||||
_device = device_registry.async_get_or_create(
|
_device = device_registry.async_get_or_create(
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.knx import DOMAIN, KNX_ADDRESS, SwitchSchema
|
from homeassistant.components.knx import DOMAIN, KNX_ADDRESS, SwitchSchema
|
||||||
from homeassistant.components.knx.project import STORAGE_KEY as KNX_PROJECT_STORAGE_KEY
|
from homeassistant.components.knx.project import STORAGE_KEY as KNX_PROJECT_STORAGE_KEY
|
||||||
from homeassistant.const import CONF_NAME
|
from homeassistant.const import CONF_NAME
|
||||||
|
@ -355,3 +357,28 @@ async def test_knx_subscribe_telegrams_command_project(
|
||||||
)
|
)
|
||||||
assert res["event"]["direction"] == "Incoming"
|
assert res["event"]["direction"] == "Incoming"
|
||||||
assert res["event"]["timestamp"] is not None
|
assert res["event"]["timestamp"] is not None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"endpoint",
|
||||||
|
[
|
||||||
|
"knx/info", # sync ws-command
|
||||||
|
"knx/get_knx_project", # async ws-command
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_websocket_when_config_entry_unloaded(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
knx: KNXTestKit,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
endpoint: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test websocket connection when config entry is unloaded."""
|
||||||
|
await knx.setup_integration({})
|
||||||
|
await hass.config_entries.async_unload(knx.mock_config_entry.entry_id)
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
await client.send_json_auto_id({"type": endpoint})
|
||||||
|
res = await client.receive_json()
|
||||||
|
assert not res["success"]
|
||||||
|
assert res["error"]["code"] == "home_assistant_error"
|
||||||
|
assert res["error"]["message"] == "KNX integration not loaded."
|
||||||
|
|
Loading…
Reference in New Issue