Compare commits

..

4 Commits

23 changed files with 266 additions and 165 deletions

View File

@ -260,6 +260,7 @@ data:
[left_button_action: <service call>]
[dismissable: <TRUE/false>]
[dismiss_action: <service call>]
[autoclose: <true/FALSE>]
[timeout: <number>]
[timeout_action: <service call>]
[style: <string>]
@ -272,6 +273,7 @@ If `size` is `wide` or `fullscreen` the card will be displayed wider or covering
`right_button` and `left_button` specify the text of two action buttons. \
When either action button is clicked, the dialog is closed and the service specified as `right_button_action` or `left_button_action` is called. \
If `dismissable` is false, the dialog cannot be closed by the user without clicking either action button. If it is true and the dialog is dismissed, `dismiss_action` is called. \
If `autoclose` is true the dialog will close automatically when the mouse, screen or keyboard is touched, at which point `dismiss_action` will be called. \
If `timeout` is specified the dialog will close automatically after `timeout` milliseconds, at which point `timeout_action` will be called. \
Finally, `style` lets you specify some CSS styles to apply to the dialog itself (to style a card in the dialog check out [card-mod](https://github.com/thomasloven/lovelace-card-mod))

View File

@ -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)

View File

@ -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)

View File

@ -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)
)
@ -156,7 +176,17 @@ def getBrowser(hass, browserID, *, create=True):
def deleteBrowser(hass, browserID):
"""Delete a browser by BrowserID."""
browsers = hass.data[DOMAIN][DATA_BROWSERS]
if browserID in browsers:
browsers[browserID].delete(hass)
del browsers[browserID]
def getBrowserByConnection(hass, connection):
"""Get the browser that has a given connection open."""
browsers = hass.data[DOMAIN][DATA_BROWSERS]
for k, v in browsers.items():
if any([c[0] == connection for c in v.connection]):
return v

View File

@ -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 }),
});
@ -954,6 +954,7 @@ const ServicesMixin = (SuperClass) => {
[left_button_action: <service call>]
[dismissable: <TRUE/false>]
[dismiss_action: <service call>]
[autoclose: <true/FALSE>]
[timeout: <number>]
[timeout_action: <service call>]
[style: <string>]
@ -1082,19 +1083,22 @@ const ActivityMixin = (SuperClass) => {
this.activityTriggered = false;
this._activityCooldown = 15000;
for (const ev of ["pointerdown", "pointermove", "keydown"]) {
window.addEventListener(ev, () => this.activityTrigger());
window.addEventListener(ev, () => this.activityTrigger(true));
}
this.addEventListener("fully-update", () => {
this.activityTrigger();
});
}
activityTrigger() {
activityTrigger(touched = false) {
if (!this.activityTriggered) {
this.sendUpdate({
activity: true,
});
}
this.activityTriggered = true;
if (touched) {
this.fireEvent("browser-mod-activity");
}
clearTimeout(this._activityTimeout);
this._activityTimeout = setTimeout(() => this.activityReset(), this._activityCooldown);
}
@ -1124,18 +1128,18 @@ const t={ATTRIBUTE:1,CHILD:2,PROPERTY:3,BOOLEAN_ATTRIBUTE:4,EVENT:5,ELEMENT:6},e
*/class e extends i{constructor(i){if(super(i),this.it=w,i.type!==t.CHILD)throw Error(this.constructor.directiveName+"() can only be used in child bindings")}render(r){if(r===w||null==r)return this.ft=void 0,this.it=r;if(r===b)return r;if("string"!=typeof r)throw Error(this.constructor.directiveName+"() called with a non-string value");if(r===this.it)return this.ft;this.it=r;const s=[r];return s.raw=s,this.ft={_$litType$:this.constructor.resultType,strings:s,values:[]}}}e.directiveName="unsafeHTML",e.resultType=1;const o=e$1(e);
class BrowserModPopup extends s {
closedCallback() {
var _a;
(_a = this._resolveClosed) === null || _a === void 0 ? void 0 : _a.call(this);
this._resolveClosed = undefined;
}
async closeDialog() {
this.open = false;
await new Promise((resolve) => (this._resolveClosed = resolve));
clearInterval(this._timeoutTimer);
if (this._autocloseListener) {
window.browser_mod.removeEventListener("browser-mod-activity", this._autocloseListener);
this._autocloseListener = undefined;
}
}
openDialog() {
var _a;
this.open = true;
(_a = this.dialog) === null || _a === void 0 ? void 0 : _a.show();
if (this.timeout) {
this._timeoutStart = new Date().getTime();
this._timeoutTimer = setInterval(() => {
@ -1146,8 +1150,13 @@ class BrowserModPopup extends s {
this._timeout();
}, 10);
}
this._autocloseListener = undefined;
if (this._autoclose) {
this._autocloseListener = this._dismiss.bind(this);
window.browser_mod.addEventListener("browser-mod-activity", this._autocloseListener);
}
}
async setupDialog(title, content, { right_button = undefined, right_button_action = undefined, left_button = undefined, left_button_action = undefined, dismissable = true, dismiss_action = undefined, timeout = undefined, timeout_action = undefined, size = undefined, style = undefined, } = {}) {
async setupDialog(title, content, { right_button = undefined, right_button_action = undefined, left_button = undefined, left_button_action = undefined, dismissable = true, dismiss_action = undefined, timeout = undefined, timeout_action = undefined, size = undefined, style = undefined, autoclose = false, } = {}) {
this.title = title;
if (content && typeof content === "object") {
// Create a card from config in content
@ -1177,6 +1186,7 @@ class BrowserModPopup extends s {
this.wide = size === "wide" ? "" : undefined;
this.fullscreen = size === "fullscreen" ? "" : undefined;
this._style = style;
this._autoclose = autoclose;
}
async _primary() {
var _a, _b, _c;
@ -1210,7 +1220,6 @@ class BrowserModPopup extends s {
return $ `
<ha-dialog
open
@closed=${this.closedCallback}
.heading=${this.title !== undefined}
?hideActions=${this.actions === undefined}
.scrimClickAction=${this.dismissable ? this._dismiss : ""}
@ -1399,6 +1408,9 @@ __decorate([
__decorate([
e$2()
], BrowserModPopup.prototype, "_style", void 0);
__decorate([
i$1("ha-dialog")
], BrowserModPopup.prototype, "dialog", void 0);
if (!customElements.get("browser-mod-popup"))
customElements.define("browser-mod-popup", BrowserModPopup);
const PopupMixin = (SuperClass) => {
@ -2062,12 +2074,25 @@ const BrowserIDMixin = (SuperClass) => {
}
}
}
async recall_id() {
// If the connection is still open, but the BrowserID has disappeared - recall it from the backend
// This happens e.g. when the frontend cache is reset in the Compainon app
if (!this.connection)
return;
const recalledID = await this.connection.sendMessagePromise({
type: "browser_mod/recall_id",
});
if (recalledID) {
localStorage[ID_STORAGE_KEY] = recalledID;
}
}
get browserID() {
if (document.querySelector("hc-main"))
return "CAST";
if (localStorage[ID_STORAGE_KEY])
return localStorage[ID_STORAGE_KEY];
this.browserID = "";
this.recall_id();
return this.browserID;
}
set browserID(id) {
@ -2112,16 +2137,20 @@ const BrowserIDMixin = (SuperClass) => {
x ll-custom handling
- Commands
x popup
x Auto-close
x close_popup
x more-info
x navigate
- lovelace-reload?
- Not needed
x window-reload
- screensaver ?
- Refer to automations instead
x sequence
x delay
x javascript eval
- toast?
- Replaced with popups with timeout
x Redesign services to target devices
x frontend editor for popup cards
- also screensavers
@ -2139,7 +2168,8 @@ const BrowserIDMixin = (SuperClass) => {
- Video player?
- Media_seek
- Screensavers
- IMPORTANT: FIX DEFAULT HIDING OF ENTITIES
x IMPORTANT: FIX DEFAULT HIDING OF ENTITIES
- NOFIX. Home Assistant bug
X Check functionality with CAST - may need to add frontend part as a lovelace resource
*/
class BrowserMod extends ServicesMixin(PopupMixin(ActivityMixin(BrowserStateMixin(CameraMixin(MediaPlayerMixin(ScreenSaverMixin(AutoSettingsMixin(FullyMixin(RequireInteractMixin(ConnectionMixin(BrowserIDMixin(EventTarget)))))))))))) {

View File

@ -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

View File

@ -10,15 +10,19 @@ 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,
)
from .browser import getBrowser, deleteBrowser
from .browser import getBrowser, deleteBrowser, getBrowserByConnection
_LOGGER = logging.getLogger(__name__)
@ -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(
@ -159,9 +161,34 @@ async def async_setup_connection(hass):
await store.set_global_settings(**{msg["key"]: msg.get("value", None)})
pass
@websocket_api.websocket_command(
{
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(
websocket_api.result_message(msg["id"], dev.browserID)
)
connection.send_message(websocket_api.result_message(msg["id"], None))
@websocket_api.websocket_command(
{
vol.Required("type"): WS_LOG,
vol.Required("message"): str,
}
)
def handle_log(hass, connection, msg):
"""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)
async_register_command(hass, handle_log)

View File

@ -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",
]

View File

@ -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

View File

@ -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

View File

@ -24,7 +24,7 @@ from homeassistant.const import (
STATE_UNKNOWN,
)
from .helpers import BrowserModEntity
from .entities import BrowserModEntity
from .const import DOMAIN, DATA_ADDERS

View File

@ -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(
{

View File

@ -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")

View File

@ -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)

View File

@ -101,6 +101,12 @@ popup:
description: Action to perform when popup is dismissed
selector:
object:
autoclose:
name: Auto close
description: Close the popup automatically on mouse, pointer or keyboard activity
default: false
selector:
boolean:
timeout:
name: Auto close timeout
description: Time before closing (ms)

View File

@ -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")

View File

@ -6,20 +6,23 @@ export const ActivityMixin = (SuperClass) => {
constructor() {
super();
for (const ev of ["pointerdown", "pointermove", "keydown"]) {
window.addEventListener(ev, () => this.activityTrigger());
window.addEventListener(ev, () => this.activityTrigger(true));
}
this.addEventListener("fully-update", () => {
this.activityTrigger();
});
}
activityTrigger() {
activityTrigger(touched = false) {
if (!this.activityTriggered) {
this.sendUpdate({
activity: true,
});
}
this.activityTriggered = true;
if (touched) {
this.fireEvent("browser-mod-activity");
}
clearTimeout(this._activityTimeout);
this._activityTimeout = setTimeout(
() => this.activityReset(),

View File

@ -20,10 +20,23 @@ export const BrowserIDMixin = (SuperClass) => {
}
}
async recall_id() {
// If the connection is still open, but the BrowserID has disappeared - recall it from the backend
// This happens e.g. when the frontend cache is reset in the Compainon app
if (!this.connection) return;
const recalledID = await this.connection.sendMessagePromise({
type: "browser_mod/recall_id",
});
if (recalledID) {
localStorage[ID_STORAGE_KEY] = recalledID;
}
}
get browserID() {
if (document.querySelector("hc-main")) return "CAST";
if (localStorage[ID_STORAGE_KEY]) return localStorage[ID_STORAGE_KEY];
this.browserID = "";
this.recall_id();
return this.browserID;
}
set browserID(id) {

View File

@ -16,6 +16,11 @@ export const ConnectionMixin = (SuperClass) => {
return;
const dt = new Date();
console.log(`${dt.toLocaleTimeString()}`, ...args);
this.connection.sendMessage({
type: "browser_mod/log",
message: args[0],
});
}
private fireEvent(event, detail = undefined) {
@ -109,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],
@ -210,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],

View File

@ -39,16 +39,20 @@ import { BrowserIDMixin } from "./browserID";
x ll-custom handling
- Commands
x popup
x Auto-close
x close_popup
x more-info
x navigate
- lovelace-reload?
- Not needed
x window-reload
- screensaver ?
- Refer to automations instead
x sequence
x delay
x javascript eval
- toast?
- Replaced with popups with timeout
x Redesign services to target devices
x frontend editor for popup cards
- also screensavers
@ -66,7 +70,8 @@ import { BrowserIDMixin } from "./browserID";
- Video player?
- Media_seek
- Screensavers
- IMPORTANT: FIX DEFAULT HIDING OF ENTITIES
x IMPORTANT: FIX DEFAULT HIDING OF ENTITIES
- NOFIX. Home Assistant bug
X Check functionality with CAST - may need to add frontend part as a lovelace resource
*/
export class BrowserMod extends ServicesMixin(

View File

@ -1,5 +1,5 @@
import { LitElement, html, css } from "lit";
import { property } from "lit/decorators.js";
import { property, query } from "lit/decorators.js";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { provideHass, loadLoadCardHelpers, hass_base_el } from "../helpers";
@ -16,27 +16,31 @@ class BrowserModPopup extends LitElement {
@property() dismissable;
@property({ reflect: true }) wide;
@property({ reflect: true }) fullscreen;
@property() _style;
@query("ha-dialog") dialog: any;
_autoclose;
_autocloseListener;
_actions;
timeout;
_timeoutStart;
_timeoutTimer;
@property() _style;
_resolveClosed;
closedCallback() {
this._resolveClosed?.();
this._resolveClosed = undefined;
}
async closeDialog() {
this.open = false;
await new Promise((resolve) => (this._resolveClosed = resolve));
clearInterval(this._timeoutTimer);
if (this._autocloseListener) {
window.browser_mod.removeEventListener(
"browser-mod-activity",
this._autocloseListener
);
this._autocloseListener = undefined;
}
}
openDialog() {
this.open = true;
this.dialog?.show();
if (this.timeout) {
this._timeoutStart = new Date().getTime();
this._timeoutTimer = setInterval(() => {
@ -46,6 +50,14 @@ class BrowserModPopup extends LitElement {
if (ellapsed >= this.timeout) this._timeout();
}, 10);
}
this._autocloseListener = undefined;
if (this._autoclose) {
this._autocloseListener = this._dismiss.bind(this);
window.browser_mod.addEventListener(
"browser-mod-activity",
this._autocloseListener
);
}
}
async setupDialog(
@ -62,6 +74,7 @@ class BrowserModPopup extends LitElement {
timeout_action = undefined,
size = undefined,
style = undefined,
autoclose = false,
} = {}
) {
this.title = title;
@ -94,6 +107,7 @@ class BrowserModPopup extends LitElement {
this.wide = size === "wide" ? "" : undefined;
this.fullscreen = size === "fullscreen" ? "" : undefined;
this._style = style;
this._autoclose = autoclose;
}
async _primary() {
@ -122,7 +136,6 @@ class BrowserModPopup extends LitElement {
return html`
<ha-dialog
open
@closed=${this.closedCallback}
.heading=${this.title !== undefined}
?hideActions=${this.actions === undefined}
.scrimClickAction=${this.dismissable ? this._dismiss : ""}

View File

@ -30,6 +30,7 @@ export const ServicesMixin = (SuperClass) => {
[left_button_action: <service call>]
[dismissable: <TRUE/false>]
[dismiss_action: <service call>]
[autoclose: <true/FALSE>]
[timeout: <number>]
[timeout_action: <service call>]
[style: <string>]