From 947735292a178a75194042a049841343b6b66a6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Lov=C3=A9n?= Date: Mon, 25 Jul 2022 22:03:54 +0000 Subject: [PATCH] Refactoring and cleanup --- custom_components/browser_mod/__init__.py | 7 +- .../browser_mod/binary_sensor.py | 13 +- custom_components/browser_mod/browser.py | 28 +++- custom_components/browser_mod/browser_mod.js | 4 +- custom_components/browser_mod/camera.py | 2 +- custom_components/browser_mod/connection.py | 122 +++++++++--------- custom_components/browser_mod/const.py | 43 +++--- custom_components/browser_mod/coordinator.py | 17 --- .../browser_mod/{helpers.py => entities.py} | 0 custom_components/browser_mod/light.py | 2 +- custom_components/browser_mod/media_player.py | 2 +- custom_components/browser_mod/mod_view.py | 13 +- custom_components/browser_mod/sensor.py | 5 +- custom_components/browser_mod/service.py | 12 +- custom_components/browser_mod/store.py | 2 +- js/plugin/connection.ts | 4 +- 16 files changed, 132 insertions(+), 144 deletions(-) delete mode 100644 custom_components/browser_mod/coordinator.py rename custom_components/browser_mod/{helpers.py => entities.py} (100%) diff --git a/custom_components/browser_mod/__init__.py b/custom_components/browser_mod/__init__.py index 6751a86..fc6222e 100644 --- a/custom_components/browser_mod/__init__.py +++ b/custom_components/browser_mod/__init__.py @@ -25,11 +25,8 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config_entry): - await hass.config_entries.async_forward_entry_setup(config_entry, "sensor") - await hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor") - await hass.config_entries.async_forward_entry_setup(config_entry, "light") - await hass.config_entries.async_forward_entry_setup(config_entry, "media_player") - await hass.config_entries.async_forward_entry_setup(config_entry, "camera") + for domain in ["sensor", "binary_sensor", "light", "media_player", "camera"]: + await hass.config_entries.async_forward_entry_setup(config_entry, domain) await async_setup_connection(hass) await async_setup_view(hass) diff --git a/custom_components/browser_mod/binary_sensor.py b/custom_components/browser_mod/binary_sensor.py index 544be5b..a6c0ded 100644 --- a/custom_components/browser_mod/binary_sensor.py +++ b/custom_components/browser_mod/binary_sensor.py @@ -1,7 +1,7 @@ from homeassistant.components.binary_sensor import BinarySensorEntity -from .const import DOMAIN, DATA_ADDERS -from .helpers import BrowserModEntity +from .const import DATA_BROWSERS, DOMAIN, DATA_ADDERS +from .entities import BrowserModEntity async def async_setup_platform( @@ -22,10 +22,7 @@ class BrowserBinarySensor(BrowserModEntity, BinarySensorEntity): @property def is_on(self): - data = self._data - data = data.get("browser", {}) - data = data.get(self.parameter, None) - return data + return self._data.get(DATA_BROWSERS, {}).get(self.parameter, None) class ActivityBinarySensor(BrowserModEntity, BinarySensorEntity): @@ -47,6 +44,4 @@ class ActivityBinarySensor(BrowserModEntity, BinarySensorEntity): @property def is_on(self): - data = self._data - data = data.get("activity", False) - return data + return self._data.get("activity", False) diff --git a/custom_components/browser_mod/browser.py b/custom_components/browser_mod/browser.py index ae2c014..bb24283 100644 --- a/custom_components/browser_mod/browser.py +++ b/custom_components/browser_mod/browser.py @@ -2,9 +2,9 @@ import logging from homeassistant.components.websocket_api import event_message from homeassistant.helpers import device_registry, entity_registry +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DATA_BROWSERS, DOMAIN, DATA_ADDERS -from .coordinator import Coordinator from .sensor import BrowserSensor from .light import BrowserModLight from .binary_sensor import BrowserBinarySensor, ActivityBinarySensor @@ -14,11 +14,23 @@ from .camera import BrowserModCamera _LOGGER = logging.getLogger(__name__) +class Coordinator(DataUpdateCoordinator): + def __init__(self, hass, browserID): + super().__init__( + hass, + _LOGGER, + name="Browser Mod Coordinator", + ) + self.browserID = browserID + + class BrowserModBrowser: - """A Browser_mod browser.""" + """A Browser_mod browser. + Handles the Home Assistant device corresponding to a registered Browser. + Creates and updates entities based on available data. + """ def __init__(self, hass, browserID): - """ """ self.browserID = browserID self.coordinator = Coordinator(hass, browserID) self.entities = {} @@ -29,11 +41,13 @@ class BrowserModBrowser: self.update_entities(hass) def update(self, hass, newData): + """Update state of all related entities.""" self.data.update(newData) self.update_entities(hass) self.coordinator.async_set_updated_data(self.data) def update_settings(self, hass, settings): + """Update Browser settings and entities if needed.""" self.settings = settings self.update_entities(hass) @@ -44,6 +58,7 @@ class BrowserModBrowser: browserID = self.browserID def _assert_browser_sensor(type, name, *properties): + """Create a browser state sensor if it does not already exist""" if name in self.entities: return adder = hass.data[DOMAIN][DATA_ADDERS][type] @@ -58,6 +73,7 @@ class BrowserModBrowser: _assert_browser_sensor("sensor", "currentUser", "Browser user") _assert_browser_sensor("sensor", "width", "Browser width", "px") _assert_browser_sensor("sensor", "height", "Browser height", "px") + # Don't create battery sensor unless battery level is reported if self.data.get("browser", {}).get("battery_level", None) is not None: _assert_browser_sensor( "sensor", "battery_level", "Browser battery", "%", "battery" @@ -65,6 +81,7 @@ class BrowserModBrowser: _assert_browser_sensor("binary_sensor", "darkMode", "Browser dark mode") _assert_browser_sensor("binary_sensor", "fullyKiosk", "Browser FullyKiosk") + # Don't create a charging sensor unless charging state is reported if self.data.get("browser", {}).get("charging", None) is not None: _assert_browser_sensor("binary_sensor", "charging", "Browser charging") @@ -117,7 +134,7 @@ class BrowserModBrowser: ) def delete(self, hass): - """Delete browser and associated entities.""" + """Delete the device and associated entities.""" dr = device_registry.async_get(hass) er = entity_registry.async_get(hass) @@ -131,12 +148,15 @@ class BrowserModBrowser: @property def connection(self): + """The current websocket connections for this Browser.""" return self._connections def open_connection(self, connection, cid): + """Add a websocket connection.""" self._connections.append((connection, cid)) def close_connection(self, connection): + """Close a websocket connection.""" self._connections = list( filter(lambda v: v[0] != connection, self._connections) ) diff --git a/custom_components/browser_mod/browser_mod.js b/custom_components/browser_mod/browser_mod.js index a574ee2..1487fd7 100644 --- a/custom_components/browser_mod/browser_mod.js +++ b/custom_components/browser_mod/browser_mod.js @@ -427,7 +427,7 @@ const ConnectionMixin = (SuperClass) => { } async _reregister(newData = {}) { await this.connection.sendMessage({ - type: "browser_mod/reregister", + type: "browser_mod/register", browserID: this.browserID, data: Object.assign(Object.assign({}, this.browsers[this.browserID]), newData), }); @@ -519,7 +519,7 @@ const ConnectionMixin = (SuperClass) => { ((_b = this.browsers) === null || _b === void 0 ? void 0 : _b[this.browserID]) === undefined) { (async () => { await this.connection.sendMessage({ - type: "browser_mod/reregister", + type: "browser_mod/register", browserID: oldID, data: Object.assign(Object.assign({}, this.browsers[oldID]), { browserID: this.browserID }), }); diff --git a/custom_components/browser_mod/camera.py b/custom_components/browser_mod/camera.py index d9dfabd..11c0689 100644 --- a/custom_components/browser_mod/camera.py +++ b/custom_components/browser_mod/camera.py @@ -2,7 +2,7 @@ import base64 from homeassistant.components.camera import Camera -from .helpers import BrowserModEntity +from .entities import BrowserModEntity from .const import DOMAIN, DATA_ADDERS import logging diff --git a/custom_components/browser_mod/connection.py b/custom_components/browser_mod/connection.py index 9c75900..3794929 100644 --- a/custom_components/browser_mod/connection.py +++ b/custom_components/browser_mod/connection.py @@ -10,10 +10,14 @@ from homeassistant.components.websocket_api import ( from homeassistant.components import websocket_api from .const import ( + BROWSER_ID, + DATA_STORE, WS_CONNECT, + WS_LOG, + WS_RECALL_ID, WS_REGISTER, + WS_SETTINGS, WS_UNREGISTER, - WS_REREGISTER, WS_UPDATE, DOMAIN, ) @@ -32,44 +36,74 @@ async def async_setup_connection(hass): ) @websocket_api.async_response async def handle_connect(hass, connection, msg): - browserID = msg["browserID"] - store = hass.data[DOMAIN]["store"] + """Connect to Browser Mod and subscribe to settings updates.""" + browserID = msg[BROWSER_ID] + store = hass.data[DOMAIN][DATA_STORE] - def listener(data): + def send_update(data): connection.send_message(event_message(msg["id"], {"result": data})) - store_listener = store.add_listener(listener) + store_listener = store.add_listener(send_update) - def unsubscriber(): + def close_connection(): store_listener() dev = getBrowser(hass, browserID, create=False) if dev: dev.close_connection(connection) - connection.subscriptions[msg["id"]] = unsubscriber + connection.subscriptions[msg["id"]] = close_connection connection.send_result(msg["id"]) - if store.get_browser(browserID).enabled: + if store.get_browser(browserID).registered: dev = getBrowser(hass, browserID) dev.update_settings(hass, store.get_browser(browserID).asdict()) dev.open_connection(connection, msg["id"]) await store.set_browser( browserID, last_seen=datetime.now(tz=timezone.utc).isoformat() ) - listener(store.asdict()) + send_update(store.asdict()) @websocket_api.websocket_command( { vol.Required("type"): WS_REGISTER, vol.Required("browserID"): str, + vol.Optional("data"): dict, } ) @websocket_api.async_response async def handle_register(hass, connection, msg): - browserID = msg["browserID"] - store = hass.data[DOMAIN]["store"] - await store.set_browser(browserID, enabled=True) - connection.send_result(msg["id"]) + """Register a Browser.""" + browserID = msg[BROWSER_ID] + store = hass.data[DOMAIN][DATA_STORE] + + browserSettings = {"registered": True} + data = msg.get("data", {}) + if "last_seen" in data: + del data["last_seen"] + if BROWSER_ID in data: + # Change ID of registered browser + newBrowserID = data[BROWSER_ID] + del data[BROWSER_ID] + + # Copy data from old browser and delete it from store + if oldBrowserSettings := store.get_browser(browserID): + browserSettings = oldBrowserSettings.asdict() + await store.delete_browser(browserID) + + # Delete the old Browser device + deleteBrowser(hass, browserID) + + # Use the new browserID from now on + browserID = newBrowserID + + # Create and/or update Browser device + dev = getBrowser(hass, browserID) + dev.update_settings(hass, data) + + # Create or update store data + if data is not None: + browserSettings.update(data) + await store.set_browser(browserID, **browserSettings) @websocket_api.websocket_command( { @@ -79,49 +113,15 @@ async def async_setup_connection(hass): ) @websocket_api.async_response async def handle_unregister(hass, connection, msg): - browserID = msg["browserID"] - store = hass.data[DOMAIN]["store"] + """Unregister a Browser.""" + browserID = msg[BROWSER_ID] + store = hass.data[DOMAIN][DATA_STORE] deleteBrowser(hass, browserID) await store.delete_browser(browserID) connection.send_result(msg["id"]) - @websocket_api.websocket_command( - { - vol.Required("type"): WS_REREGISTER, - vol.Required("browserID"): str, - vol.Required("data"): dict, - } - ) - @websocket_api.async_response - async def handle_reregister(hass, connection, msg): - browserID = msg["browserID"] - store = hass.data[DOMAIN]["store"] - - data = msg["data"] - del data["last_seen"] - browserSettings = {} - - if "browserID" in data: - newBrowserID = data["browserID"] - del data["browserID"] - - oldBrowserSetting = store.get_browser(browserID) - if oldBrowserSetting: - browserSettings = oldBrowserSetting.asdict() - await store.delete_browser(browserID) - - deleteBrowser(hass, browserID) - - browserID = newBrowserID - - if (dev := getBrowser(hass, browserID, create=False)) is not None: - dev.update_settings(hass, data) - - browserSettings.update(data) - await store.set_browser(browserID, **browserSettings) - @websocket_api.websocket_command( { vol.Required("type"): WS_UPDATE, @@ -131,16 +131,17 @@ async def async_setup_connection(hass): ) @websocket_api.async_response async def handle_update(hass, connection, msg): - browserID = msg["browserID"] - store = hass.data[DOMAIN]["store"] + """Receive state updates from a Browser.""" + browserID = msg[BROWSER_ID] + store = hass.data[DOMAIN][DATA_STORE] - if store.get_browser(browserID).enabled: + if store.get_browser(browserID).registered: dev = getBrowser(hass, browserID) dev.update(hass, msg.get("data", {})) @websocket_api.websocket_command( { - vol.Required("type"): "browser_mod/settings", + vol.Required("type"): WS_SETTINGS, vol.Required("key"): str, vol.Optional("value"): vol.Any(int, str, bool, list, object, None), vol.Optional("user"): str, @@ -148,7 +149,8 @@ async def async_setup_connection(hass): ) @websocket_api.async_response async def handle_settings(hass, connection, msg): - store = hass.data[DOMAIN]["store"] + """Change user or global settings.""" + store = hass.data[DOMAIN][DATA_STORE] if "user" in msg: # Set user setting await store.set_user_settings( @@ -161,10 +163,11 @@ async def async_setup_connection(hass): @websocket_api.websocket_command( { - vol.Required("type"): "browser_mod/recall_id", + vol.Required("type"): WS_RECALL_ID, } ) def handle_recall_id(hass, connection, msg): + """Recall browserID of Browser with the current connection.""" dev = getBrowserByConnection(hass, connection) if dev: connection.send_message( @@ -174,18 +177,17 @@ async def async_setup_connection(hass): @websocket_api.websocket_command( { - vol.Required("type"): "browser_mod/log", + vol.Required("type"): WS_LOG, vol.Required("message"): str, } ) def handle_log(hass, connection, msg): - _LOGGER.info("LOG MESSAGE") - _LOGGER.info(msg["message"]) + """Print a debug message.""" + _LOGGER.info(f"LOG MESSAGE: {msg['message']}") async_register_command(hass, handle_connect) async_register_command(hass, handle_register) async_register_command(hass, handle_unregister) - async_register_command(hass, handle_reregister) async_register_command(hass, handle_update) async_register_command(hass, handle_settings) async_register_command(hass, handle_recall_id) diff --git a/custom_components/browser_mod/const.py b/custom_components/browser_mod/const.py index 6130de7..d00b5f2 100644 --- a/custom_components/browser_mod/const.py +++ b/custom_components/browser_mod/const.py @@ -1,42 +1,31 @@ DOMAIN = "browser_mod" +BROWSER_ID = "browserID" + FRONTEND_SCRIPT_URL = "/browser_mod.js" SETTINGS_PANEL_URL = "/browser_mod_panel.js" DATA_BROWSERS = "browsers" DATA_ADDERS = "adders" DATA_STORE = "store" -DATA_ALIASES = "aliases" -DATA_CONFIG = "config" -DATA_SETUP_COMPLETE = "setup_complete" - -CONFIG_DEVICES = "devices" -CONFIG_PREFIX = "prefix" -CONFIG_DISABLE = "disable" -CONFIG_DISABLE_ALL = "all" WS_ROOT = DOMAIN -WS_CONNECT = "{}/connect".format(WS_ROOT) -WS_UPDATE = "{}/update".format(WS_ROOT) -WS_CAMERA = "{}/camera".format(WS_ROOT) - +WS_CONNECT = f"{WS_ROOT}/connect" WS_REGISTER = f"{WS_ROOT}/register" WS_UNREGISTER = f"{WS_ROOT}/unregister" -WS_REREGISTER = f"{WS_ROOT}/reregister" +WS_UPDATE = f"{WS_ROOT}/update" +WS_SETTINGS = f"{WS_ROOT}/settings" +WS_RECALL_ID = f"{WS_ROOT}/recall_id" +WS_LOG = f"{WS_ROOT}/log" -USER_COMMANDS = [ - "debug", - "popup", - "close-popup", - "navigate", - "more-info", - "set-theme", - "lovelace-reload", - "window-reload", - "blackout", - "no-blackout", - "toast", - "commands", - "call_service", +BROWSER_MOD_SERVICES = [ + "sequence", "delay", + "popup", + "more_info", + "close_popup", + "navigate", + "refresh", + "console", + "javascript", ] diff --git a/custom_components/browser_mod/coordinator.py b/custom_components/browser_mod/coordinator.py deleted file mode 100644 index 6803563..0000000 --- a/custom_components/browser_mod/coordinator.py +++ /dev/null @@ -1,17 +0,0 @@ -import logging - -from homeassistant.helpers.update_coordinator import ( - DataUpdateCoordinator, -) - -_LOGGER = logging.getLogger(__name__) - - -class Coordinator(DataUpdateCoordinator): - def __init__(self, hass, browserID): - super().__init__( - hass, - _LOGGER, - name="Browser Mod Coordinator", - ) - self.browserID = browserID diff --git a/custom_components/browser_mod/helpers.py b/custom_components/browser_mod/entities.py similarity index 100% rename from custom_components/browser_mod/helpers.py rename to custom_components/browser_mod/entities.py diff --git a/custom_components/browser_mod/light.py b/custom_components/browser_mod/light.py index faa7149..5ad1a62 100644 --- a/custom_components/browser_mod/light.py +++ b/custom_components/browser_mod/light.py @@ -1,6 +1,6 @@ from homeassistant.components.light import LightEntity, ColorMode -from .helpers import BrowserModEntity +from .entities import BrowserModEntity from .const import DOMAIN, DATA_ADDERS diff --git a/custom_components/browser_mod/media_player.py b/custom_components/browser_mod/media_player.py index 888bb74..cfad492 100644 --- a/custom_components/browser_mod/media_player.py +++ b/custom_components/browser_mod/media_player.py @@ -24,7 +24,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) -from .helpers import BrowserModEntity +from .entities import BrowserModEntity from .const import DOMAIN, DATA_ADDERS diff --git a/custom_components/browser_mod/mod_view.py b/custom_components/browser_mod/mod_view.py index 8e54c09..e23bb4f 100644 --- a/custom_components/browser_mod/mod_view.py +++ b/custom_components/browser_mod/mod_view.py @@ -9,12 +9,18 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_view(hass): + # Serve the Browser Mod controller and add it as extra_module_url hass.http.register_static_path( FRONTEND_SCRIPT_URL, hass.config.path("custom_components/browser_mod/browser_mod.js"), ) add_extra_js_url(hass, FRONTEND_SCRIPT_URL) + # Serve the Browser Mod Settings panel and register it as a panel + hass.http.register_static_path( + SETTINGS_PANEL_URL, + hass.config.path("custom_components/browser_mod/browser_mod_panel.js"), + ) hass.components.frontend.async_register_built_in_panel( component_name="custom", sidebar_title="Browser Mod", @@ -28,10 +34,6 @@ async def async_setup_view(hass): } }, ) - hass.http.register_static_path( - SETTINGS_PANEL_URL, - hass.config.path("custom_components/browser_mod/browser_mod_panel.js"), - ) # Also load Browser Mod as a lovelace resource so it's accessible to Cast resources = hass.data["lovelace"]["resources"] @@ -39,14 +41,17 @@ async def async_setup_view(hass): if not resources.loaded: await resources.async_load() resources.loaded = True + frontend_added = False for r in resources.async_items(): if r["url"].startswith(FRONTEND_SCRIPT_URL): frontend_added = True continue + # While going through the resources, also preload card-mod if it is found if "card-mod.js" in r["url"]: add_extra_js_url(hass, r["url"]) + if not frontend_added: await resources.async_create_item( { diff --git a/custom_components/browser_mod/sensor.py b/custom_components/browser_mod/sensor.py index 7b89d02..771a134 100644 --- a/custom_components/browser_mod/sensor.py +++ b/custom_components/browser_mod/sensor.py @@ -1,7 +1,7 @@ from homeassistant.components.sensor import SensorEntity from .const import DOMAIN, DATA_ADDERS -from .helpers import BrowserModEntity +from .entities import BrowserModEntity async def async_setup_platform( @@ -48,12 +48,15 @@ class BrowserSensor(BrowserModEntity, SensorEntity): @property def extra_state_attributes(self): retval = super().extra_state_attributes + if self.parameter == "currentUser": retval["userData"] = self._data.get("browser", {}).get("userData") + if self.parameter == "path": retval["pathSegments"] = ( self._data.get("browser", {}).get("path", "").split("/") ) + if self.parameter == "userAgent": retval["userAgent"] = self._data.get("browser", {}).get("userAgent") diff --git a/custom_components/browser_mod/service.py b/custom_components/browser_mod/service.py index 1ec4cb5..9ea2b74 100644 --- a/custom_components/browser_mod/service.py +++ b/custom_components/browser_mod/service.py @@ -3,6 +3,7 @@ import logging from homeassistant.helpers import device_registry from .const import ( + BROWSER_MOD_SERVICES, DOMAIN, DATA_BROWSERS, ) @@ -59,12 +60,5 @@ async def async_setup_services(hass): call_service(service, browsers, data) - hass.services.async_register(DOMAIN, "sequence", handle_service) - hass.services.async_register(DOMAIN, "delay", handle_service) - hass.services.async_register(DOMAIN, "popup", handle_service) - hass.services.async_register(DOMAIN, "more_info", handle_service) - hass.services.async_register(DOMAIN, "close_popup", handle_service) - hass.services.async_register(DOMAIN, "navigate", handle_service) - hass.services.async_register(DOMAIN, "refresh", handle_service) - hass.services.async_register(DOMAIN, "console", handle_service) - hass.services.async_register(DOMAIN, "javascript", handle_service) + for service in BROWSER_MOD_SERVICES: + hass.services.async_register(DOMAIN, service, handle_service) diff --git a/custom_components/browser_mod/store.py b/custom_components/browser_mod/store.py index 95cce54..648aa38 100644 --- a/custom_components/browser_mod/store.py +++ b/custom_components/browser_mod/store.py @@ -30,7 +30,7 @@ class SettingsStoreData: @attr.s class BrowserStoreData: last_seen = attr.ib(type=int, default=0) - enabled = attr.ib(type=bool, default=False) + registered = attr.ib(type=bool, default=False) camera = attr.ib(type=bool, default=False) settings = attr.ib(type=SettingsStoreData, factory=SettingsStoreData) meta = attr.ib(type=str, default="default") diff --git a/js/plugin/connection.ts b/js/plugin/connection.ts index 5b7b0f5..873e628 100644 --- a/js/plugin/connection.ts +++ b/js/plugin/connection.ts @@ -114,7 +114,7 @@ export const ConnectionMixin = (SuperClass) => { private async _reregister(newData = {}) { await this.connection.sendMessage({ - type: "browser_mod/reregister", + type: "browser_mod/register", browserID: this.browserID, data: { ...this.browsers[this.browserID], @@ -215,7 +215,7 @@ export const ConnectionMixin = (SuperClass) => { ) { (async () => { await this.connection.sendMessage({ - type: "browser_mod/reregister", + type: "browser_mod/register", browserID: oldID, data: { ...this.browsers[oldID],