Add WS command frontend/subscribe_extra_js (#119833)

Co-authored-by: Robert Resch <robert@resch.dev>
This commit is contained in:
Erik Montnemery 2024-06-18 16:18:42 +02:00 committed by GitHub
parent 0ca3f25c57
commit 7940303149
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 93 additions and 6 deletions

View File

@ -2,8 +2,8 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Iterator from collections.abc import Callable, Iterator
from functools import lru_cache from functools import lru_cache, partial
import logging import logging
import os import os
import pathlib import pathlib
@ -33,6 +33,7 @@ from homeassistant.helpers.storage import Store
from homeassistant.helpers.translation import async_get_translations from homeassistant.helpers.translation import async_get_translations
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import async_get_integration, bind_hass from homeassistant.loader import async_get_integration, bind_hass
from homeassistant.util.hass_dict import HassKey
from .storage import async_setup_frontend_storage from .storage import async_setup_frontend_storage
@ -56,6 +57,10 @@ DATA_JS_VERSION = "frontend_js_version"
DATA_EXTRA_MODULE_URL = "frontend_extra_module_url" DATA_EXTRA_MODULE_URL = "frontend_extra_module_url"
DATA_EXTRA_JS_URL_ES5 = "frontend_extra_js_url_es5" DATA_EXTRA_JS_URL_ES5 = "frontend_extra_js_url_es5"
DATA_WS_SUBSCRIBERS: HassKey[set[tuple[websocket_api.ActiveConnection, int]]] = HassKey(
"frontend_ws_subscribers"
)
THEMES_STORAGE_KEY = f"{DOMAIN}_theme" THEMES_STORAGE_KEY = f"{DOMAIN}_theme"
THEMES_STORAGE_VERSION = 1 THEMES_STORAGE_VERSION = 1
THEMES_SAVE_DELAY = 60 THEMES_SAVE_DELAY = 60
@ -204,17 +209,24 @@ class UrlManager:
on hass.data on hass.data
""" """
def __init__(self, urls: list[str]) -> None: def __init__(
self,
on_change: Callable[[str, str], None],
urls: list[str],
) -> None:
"""Init the url manager.""" """Init the url manager."""
self._on_change = on_change
self.urls = frozenset(urls) self.urls = frozenset(urls)
def add(self, url: str) -> None: def add(self, url: str) -> None:
"""Add a url to the set.""" """Add a url to the set."""
self.urls = frozenset([*self.urls, url]) self.urls = frozenset([*self.urls, url])
self._on_change("added", url)
def remove(self, url: str) -> None: def remove(self, url: str) -> None:
"""Remove a url from the set.""" """Remove a url from the set."""
self.urls = self.urls - {url} self.urls = self.urls - {url}
self._on_change("removed", url)
class Panel: class Panel:
@ -363,6 +375,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
websocket_api.async_register_command(hass, websocket_get_themes) websocket_api.async_register_command(hass, websocket_get_themes)
websocket_api.async_register_command(hass, websocket_get_translations) websocket_api.async_register_command(hass, websocket_get_translations)
websocket_api.async_register_command(hass, websocket_get_version) websocket_api.async_register_command(hass, websocket_get_version)
websocket_api.async_register_command(hass, websocket_subscribe_extra_js)
hass.http.register_view(ManifestJSONView()) hass.http.register_view(ManifestJSONView())
conf = config.get(DOMAIN, {}) conf = config.get(DOMAIN, {})
@ -420,8 +433,27 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
sidebar_icon="hass:hammer", sidebar_icon="hass:hammer",
) )
hass.data[DATA_EXTRA_MODULE_URL] = UrlManager(conf.get(CONF_EXTRA_MODULE_URL, [])) @callback
hass.data[DATA_EXTRA_JS_URL_ES5] = UrlManager(conf.get(CONF_EXTRA_JS_URL_ES5, [])) def async_change_listener(
resource_type: str,
change_type: str,
url: str,
) -> None:
subscribers = hass.data[DATA_WS_SUBSCRIBERS]
json_msg = {
"change_type": change_type,
"item": {"type": resource_type, "url": url},
}
for connection, msg_id in subscribers:
connection.send_message(websocket_api.event_message(msg_id, json_msg))
hass.data[DATA_EXTRA_MODULE_URL] = UrlManager(
partial(async_change_listener, "module"), conf.get(CONF_EXTRA_MODULE_URL, [])
)
hass.data[DATA_EXTRA_JS_URL_ES5] = UrlManager(
partial(async_change_listener, "es5"), conf.get(CONF_EXTRA_JS_URL_ES5, [])
)
hass.data[DATA_WS_SUBSCRIBERS] = set()
await _async_setup_themes(hass, conf.get(CONF_THEMES)) await _async_setup_themes(hass, conf.get(CONF_THEMES))
@ -783,6 +815,24 @@ async def websocket_get_version(
connection.send_result(msg["id"], {"version": frontend}) connection.send_result(msg["id"], {"version": frontend})
@callback
@websocket_api.websocket_command({"type": "frontend/subscribe_extra_js"})
def websocket_subscribe_extra_js(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
) -> None:
"""Subscribe to URL manager updates."""
subscribers = hass.data[DATA_WS_SUBSCRIBERS]
subscribers.add((connection, msg["id"]))
@callback
def cancel_subscription() -> None:
subscribers.remove((connection, msg["id"]))
connection.subscriptions[msg["id"]] = cancel_subscription
connection.send_message(websocket_api.result_message(msg["id"]))
class PanelRespons(TypedDict): class PanelRespons(TypedDict):
"""Represent the panel response type.""" """Represent the panel response type."""

View File

@ -409,7 +409,11 @@ async def test_missing_themes(hass: HomeAssistant, ws_client) -> None:
@pytest.mark.usefixtures("mock_onboarded") @pytest.mark.usefixtures("mock_onboarded")
async def test_extra_js(hass: HomeAssistant, mock_http_client_with_extra_js) -> None: async def test_extra_js(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
mock_http_client_with_extra_js,
) -> None:
"""Test that extra javascript is loaded.""" """Test that extra javascript is loaded."""
async def get_response(): async def get_response():
@ -423,6 +427,13 @@ async def test_extra_js(hass: HomeAssistant, mock_http_client_with_extra_js) ->
assert '"/local/my_module.js"' in text assert '"/local/my_module.js"' in text
assert '"/local/my_es5.js"' in text assert '"/local/my_es5.js"' in text
client = await hass_ws_client(hass)
await client.send_json_auto_id({"type": "frontend/subscribe_extra_js"})
msg = await client.receive_json()
assert msg["success"] is True
subscription_id = msg["id"]
# Test dynamically adding and removing extra javascript # Test dynamically adding and removing extra javascript
add_extra_js_url(hass, "/local/my_module_2.js", False) add_extra_js_url(hass, "/local/my_module_2.js", False)
add_extra_js_url(hass, "/local/my_es5_2.js", True) add_extra_js_url(hass, "/local/my_es5_2.js", True)
@ -430,12 +441,38 @@ async def test_extra_js(hass: HomeAssistant, mock_http_client_with_extra_js) ->
assert '"/local/my_module_2.js"' in text assert '"/local/my_module_2.js"' in text
assert '"/local/my_es5_2.js"' in text assert '"/local/my_es5_2.js"' in text
msg = await client.receive_json()
assert msg["id"] == subscription_id
assert msg["event"] == {
"change_type": "added",
"item": {"type": "module", "url": "/local/my_module_2.js"},
}
msg = await client.receive_json()
assert msg["id"] == subscription_id
assert msg["event"] == {
"change_type": "added",
"item": {"type": "es5", "url": "/local/my_es5_2.js"},
}
remove_extra_js_url(hass, "/local/my_module_2.js", False) remove_extra_js_url(hass, "/local/my_module_2.js", False)
remove_extra_js_url(hass, "/local/my_es5_2.js", True) remove_extra_js_url(hass, "/local/my_es5_2.js", True)
text = await get_response() text = await get_response()
assert '"/local/my_module_2.js"' not in text assert '"/local/my_module_2.js"' not in text
assert '"/local/my_es5_2.js"' not in text assert '"/local/my_es5_2.js"' not in text
msg = await client.receive_json()
assert msg["id"] == subscription_id
assert msg["event"] == {
"change_type": "removed",
"item": {"type": "module", "url": "/local/my_module_2.js"},
}
msg = await client.receive_json()
assert msg["id"] == subscription_id
assert msg["event"] == {
"change_type": "removed",
"item": {"type": "es5", "url": "/local/my_es5_2.js"},
}
# Remove again should not raise # Remove again should not raise
remove_extra_js_url(hass, "/local/my_module_2.js", False) remove_extra_js_url(hass, "/local/my_module_2.js", False)
remove_extra_js_url(hass, "/local/my_es5_2.js", True) remove_extra_js_url(hass, "/local/my_es5_2.js", True)