diff --git a/custom_components/browser_mod/__init__.py b/custom_components/browser_mod/__init__.py index a9de310..74ea48e 100644 --- a/custom_components/browser_mod/__init__.py +++ b/custom_components/browser_mod/__init__.py @@ -1,73 +1,45 @@ import logging -from homeassistant import config_entries - -from .mod_view import setup_view -from .connection import setup_connection -from .service import setup_service +from .store import BrowserModStore +from .mod_view import async_setup_view +from .connection import async_setup_connection from .const import ( DOMAIN, DATA_DEVICES, - DATA_ALIASES, DATA_ADDERS, - CONFIG_DEVICES, - DATA_CONFIG, - DATA_SETUP_COMPLETE, + DATA_STORE ) -COMPONENTS = [ - "media_player", - "sensor", - "binary_sensor", - "light", - "camera", -] +from .coordinator import Coordinator _LOGGER = logging.getLogger(__name__) async def async_setup(hass, config): - if not hass.config_entries.async_entries(DOMAIN): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={} - ) - ) - - aliases = {} - for d in config[DOMAIN].get(CONFIG_DEVICES, {}): - name = config[DOMAIN][CONFIG_DEVICES][d].get("name", None) - if name: - aliases[name] = d.replace("_", "-") + store = BrowserModStore(hass) + await store.load() hass.data[DOMAIN] = { DATA_DEVICES: {}, - DATA_ALIASES: aliases, DATA_ADDERS: {}, - DATA_CONFIG: config[DOMAIN], - DATA_SETUP_COMPLETE: False, + DATA_STORE: store, } - await setup_connection(hass, config) - setup_view(hass) - - for component in COMPONENTS: - hass.async_create_task( - hass.helpers.discovery.async_load_platform(component, DOMAIN, {}, config) - ) - - await setup_service(hass) - - hass.data[DOMAIN][DATA_SETUP_COMPLETE] = True - - for device in hass.data[DOMAIN][DATA_DEVICES].values(): - device.trigger_update() - return True 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 async_setup_connection(hass) + await async_setup_view(hass) + + return True for component in COMPONENTS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(config_entry, component) diff --git a/custom_components/browser_mod/binary_sensor.py b/custom_components/browser_mod/binary_sensor.py index 543fcf2..537e87b 100644 --- a/custom_components/browser_mod/binary_sensor.py +++ b/custom_components/browser_mod/binary_sensor.py @@ -1,60 +1,25 @@ -from datetime import datetime +from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.const import ( - STATE_UNAVAILABLE, - ATTR_BATTERY_CHARGING, - ATTR_BATTERY_LEVEL, - STATE_ON, - STATE_OFF, -) -from homeassistant.components.binary_sensor import DEVICE_CLASS_MOTION - -from .helpers import setup_platform, BrowserModEntity - -PLATFORM = "binary_sensor" +from .const import DOMAIN, DATA_ADDERS +from .helpers import BrowserModEntity2 -async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - return setup_platform(hass, config, async_add_devices, PLATFORM, BrowserModSensor) - +async def async_setup_platform(hass, config_entry, async_add_entities, discoveryInfo = None): + hass.data[DOMAIN][DATA_ADDERS]["binary_sensor"] = async_add_entities async def async_setup_entry(hass, config_entry, async_add_entities): await async_setup_platform(hass, {}, async_add_entities) -class BrowserModSensor(BrowserModEntity): - domain = PLATFORM +class BrowserBinarySensor(BrowserModEntity2, BinarySensorEntity): - def __init__(self, hass, connection, deviceID, alias=None): - super().__init__(hass, connection, deviceID, alias) - self.last_seen = None - - def updated(self): - self.last_seen = datetime.now() - self.schedule_update_ha_state() - - @property - def state(self): - if not self.connection.connection: - return STATE_UNAVAILABLE - if self.data.get("motion", False): - return STATE_ON - return STATE_OFF + def __init__(self, coordinator, deviceID, parameter, name): + super().__init__(coordinator, deviceID, name) + self.parameter = parameter @property def is_on(self): - return not self.data.get("motion", False) - - @property - def device_class(self): - return DEVICE_CLASS_MOTION - - @property - def extra_state_attributes(self): - return { - "type": "browser_mod", - "last_seen": self.last_seen, - ATTR_BATTERY_LEVEL: self.data.get("battery", None), - ATTR_BATTERY_CHARGING: self.data.get("charging", None), - **self.data, - } + data = self._data + data = data.get("browser", {}) + data = data.get(self.parameter, None) + return data diff --git a/custom_components/browser_mod/browser_mod.js b/custom_components/browser_mod/browser_mod.js index d71e68e..eeb66d5 100644 --- a/custom_components/browser_mod/browser_mod.js +++ b/custom_components/browser_mod/browser_mod.js @@ -1,25 +1,25 @@ -const ID_STORAGE_KEY = 'lovelace-player-device-id'; +const ID_STORAGE_KEY$1 = 'lovelace-player-device-id'; function _deviceID() { - if(!localStorage[ID_STORAGE_KEY]) + if(!localStorage[ID_STORAGE_KEY$1]) { const s4 = () => { return Math.floor((1+Math.random())*100000).toString(16).substring(1); }; if(window['fully'] && typeof fully.getDeviceId === "function") - localStorage[ID_STORAGE_KEY] = fully.getDeviceId(); + localStorage[ID_STORAGE_KEY$1] = fully.getDeviceId(); else - localStorage[ID_STORAGE_KEY] = `${s4()}${s4()}-${s4()}${s4()}`; + localStorage[ID_STORAGE_KEY$1] = `${s4()}${s4()}-${s4()}${s4()}`; } - return localStorage[ID_STORAGE_KEY]; + return localStorage[ID_STORAGE_KEY$1]; } let deviceID = _deviceID(); const setDeviceID = (id) => { if(id === null) return; if(id === "clear") { - localStorage.removeItem(ID_STORAGE_KEY); + localStorage.removeItem(ID_STORAGE_KEY$1); } else { - localStorage[ID_STORAGE_KEY] = id; + localStorage[ID_STORAGE_KEY$1] = id; } deviceID = _deviceID(); }; @@ -44,7 +44,7 @@ async function hass_loaded() { return true; } -function hass() { +function hass$1() { if(document.querySelector('hc-main')) return document.querySelector('hc-main').hass; @@ -53,7 +53,7 @@ function hass() { return undefined; } -function provideHass(element) { +function provideHass$1(element) { if(document.querySelector('hc-main')) return document.querySelector('hc-main').provideHass(element); @@ -62,32 +62,6 @@ function provideHass(element) { return undefined; } - -function lovelace() { - var root = document.querySelector("hc-main"); - if(root) { - var ll = root._lovelaceConfig; - ll.current_view = root._lovelacePath; - return ll; - } - - root = document.querySelector("home-assistant"); - root = root && root.shadowRoot; - root = root && root.querySelector("home-assistant-main"); - root = root && root.shadowRoot; - root = root && root.querySelector("app-drawer-layout partial-panel-resolver"); - root = root && root.shadowRoot || root; - root = root && root.querySelector("ha-panel-lovelace"); - root = root && root.shadowRoot; - root = root && root.querySelector("hui-root"); - if (root) { - var ll = root.lovelace; - ll.current_view = root.___curView; - return ll; - } - - return null; -} function lovelace_view() { var root = document.querySelector("hc-main"); if(root) { @@ -127,14 +101,14 @@ async function load_lovelace() { await ppr.routerOptions.routes.tmp.load(); if(!customElements.get("ha-panel-lovelace")) return false; const p = document.createElement("ha-panel-lovelace"); - p.hass = hass(); + p.hass = hass$1(); if(p.hass === undefined) { await new Promise(resolve => { window.addEventListener('connection-status', (ev) => { resolve(); }, {once: true}); }); - p.hass = hass(); + p.hass = hass$1(); } p.panel = {config: {mode: null}}; p._fetchConfig(); @@ -218,15 +192,6 @@ new Promise(async (resolve, reject) => { } }); -async function closePopUp() { - const root = document.querySelector("home-assistant") || document.querySelector("hc-root"); - fireEvent("hass-more-info", {entityId: "."}, root); - const el = await selectTree(root, "$ card-tools-popup"); - - if(el) - el.closeDialog(); -} - async function popUp(title, card, large=false, style={}, fullscreen=false) { if(!customElements.get("card-tools-popup")) { @@ -412,7 +377,7 @@ async function popUp(title, card, large=false, style={}, fullscreen=false) { root.shadowRoot.insertBefore(el,mi); else root.shadowRoot.appendChild(el); - provideHass(el); + provideHass$1(el); } if(!window._moreInfoDialogListener) { @@ -650,456 +615,316 @@ __decorate([ customElements.define("browser-player", BrowserPlayer); })(); -class BrowserModConnection { - async connect() { - const isCast = document.querySelector("hc-main") !== null; - if (!isCast) { - while (!window.hassConnection) - await new Promise((resolve) => window.setTimeout(resolve, 100)); - this.connection = (await window.hassConnection).conn; - } - else { - this.connection = hass().connection; - } - this.connection.subscribeMessage((msg) => this.msg_callback(msg), { - type: "browser_mod/connect", - deviceID: deviceID, - }); - provideHass(this); - } - get connected() { - return this.connection !== undefined; - } - msg_callback(message) { - console.log(message); - } - sendUpdate(data) { - if (!this.connected) - return; - this.connection.sendMessage({ - type: "browser_mod/update", - deviceID, - data, - }); - } +async function _hass_base_el() { + await Promise.race([ + customElements.whenDefined("home-assistant"), + customElements.whenDefined("hc-main"), + ]); + const element = customElements.get("home-assistant") + ? "home-assistant" + : "hc-main"; + while (!document.querySelector(element)) + await new Promise((r) => window.setTimeout(r, 100)); + return document.querySelector(element); +} +async function hass() { + const base = await _hass_base_el(); + while (!base.hass) + await new Promise((r) => window.setTimeout(r, 100)); + return base.hass; +} +async function provideHass(el) { + const base = await _hass_base_el(); + base.provideHass(el); } -const BrowserModMediaPlayerMixin = (C) => class extends C { - constructor() { - super(); - this.player = new Audio(); - for (const event of ["play", "pause", "ended", "volumechange"]) { - this.player.addEventListener(event, () => this.player_update()); +const ID_STORAGE_KEY = "browser_mod-device-id"; +const ConnectionMixin = (SuperClass) => { + class BrowserModConnection extends SuperClass { + constructor() { + super(...arguments); + this.connected = false; + this.connectionPromise = new Promise(resolve => { this._connectionResolve = resolve; }); } - window.addEventListener("click", () => { - if (!this.player.ended) + LOG(...args) { + const dt = new Date(); + console.log(`${dt.toLocaleTimeString()}`, ...args); + } + fireEvent(event, detail = undefined) { + this.dispatchEvent(new CustomEvent(event, { detail })); + } + incoming_message(msg) { + var _a; + if (msg.command) { + this.LOG("Command:", msg); + this.fireEvent(`command-${msg.command}`, msg); + } + else if (msg.result) { + this.update_config(msg.result); + } + (_a = this._connectionResolve) === null || _a === void 0 ? void 0 : _a.call(this); + } + update_config(cfg) { + var _a; + this.LOG("Receive:", cfg); + let update = false; + if (!this.registered && ((_a = cfg.devices) === null || _a === void 0 ? void 0 : _a[this.deviceID])) { + update = true; + } + this._data = cfg; + if (!this.connected) { + this.connected = true; + this.fireEvent("browser-mod-connected"); + } + this.fireEvent("browser-mod-config-update"); + if (update) + this.sendUpdate({}); + } + async connect() { + const conn = (await hass()).connection; + this.connection = conn; + // Subscribe to configuration updates + conn.subscribeMessage((msg) => this.incoming_message(msg), { + type: "browser_mod/connect", + deviceID: this.deviceID, + }); + // Keep connection status up to date + conn.addEventListener("disconnected", () => { + this.connected = false; + this.fireEvent("browser-mod-disconnected"); + }); + conn.addEventListener("ready", () => { + this.connected = true; + this.fireEvent("browser-mod-connected"); + this.sendUpdate({}); + }); + provideHass(this); + } + get config() { + var _a, _b; + return (_b = (_a = this._data) === null || _a === void 0 ? void 0 : _a.config) !== null && _b !== void 0 ? _b : {}; + } + get devices() { + var _a, _b; + return (_b = (_a = this._data) === null || _a === void 0 ? void 0 : _a.devices) !== null && _b !== void 0 ? _b : []; + } + get registered() { + var _a; + return ((_a = this.devices) === null || _a === void 0 ? void 0 : _a[this.deviceID]) !== undefined; + } + set registered(reg) { + (async () => { + if (reg) { + if (this.registered) + return; + await this.connection.sendMessage({ + type: "browser_mod/register", + deviceID: this.deviceID, + }); + } + else { + if (!this.registered) + return; + await this.connection.sendMessage({ + type: "browser_mod/unregister", + deviceID: this.deviceID, + }); + } + })(); + } + get meta() { + if (!this.registered) + return null; + return this.devices[this.deviceID].meta; + } + set meta(value) { + (async () => { + await this.connection.sendMessage({ + type: "browser_mod/reregister", + deviceID: this.deviceID, + data: Object.assign(Object.assign({}, this.devices[this.deviceID]), { meta: value }) + }); + })(); + } + sendUpdate(data) { + if (!this.connected || !this.registered) + return; + this.LOG("Send:", data); + this.connection.sendMessage({ + type: "browser_mod/update", + deviceID: this.deviceID, + data, + }); + } + get deviceID() { + if (localStorage[ID_STORAGE_KEY]) + return localStorage[ID_STORAGE_KEY]; + this.deviceID = ""; + return this.deviceID; + } + set deviceID(id) { + var _a, _b; + function _createDeviceID() { + var _a, _b; + const s4 = () => { + return Math.floor((1 + Math.random()) * 100000) + .toString(16) + .substring(1); + }; + return (_b = (_a = window.fully) === null || _a === void 0 ? void 0 : _a.getDeviceId()) !== null && _b !== void 0 ? _b : `${s4()}${s4()}-${s4()}${s4()}`; + } + if (id === "") + id = _createDeviceID(); + const oldID = localStorage[ID_STORAGE_KEY]; + localStorage[ID_STORAGE_KEY] = id; + this.fireEvent("browser-mod-config-update"); + if (((_a = this.devices) === null || _a === void 0 ? void 0 : _a[oldID]) !== undefined && ((_b = this.devices) === null || _b === void 0 ? void 0 : _b[this.deviceID]) === undefined) { + (async () => { + await this.connection.sendMessage({ + type: "browser_mod/reregister", + deviceID: oldID, + data: Object.assign(Object.assign({}, this.devices[oldID]), { deviceID: this.deviceID }) + }); + })(); + } + // TODO: Send update to backend to update device + } + } + return BrowserModConnection; +}; + +const ScreenSaverMixin = (SuperClass) => { + class ScreenSaverMixinClass extends SuperClass { + constructor() { + super(); + this._listeners = {}; + this._brightness = 255; + const panel = this._panel = document.createElement("div"); + panel.setAttribute("browser-mod", ""); + panel.attachShadow({ mode: "open" }); + const styleEl = document.createElement("style"); + styleEl.innerHTML = ` + :host { + background: rgba(0,0,0, var(--darkness)); + position: fixed; + left: 0; + top: 0; + bottom: 0; + right: 0; + width: 100%; + height: 100%; + z-index: 10000; + display: block; + pointer-events: none; + } + :host([dark]) { + background: rgba(0,0,0,1); + } + `; + panel.shadowRoot.appendChild(styleEl); + document.body.appendChild(panel); + this.addEventListener("command-screen_off", () => this._screen_off()); + this.addEventListener("command-screen_on", (ev) => this._screen_on(ev)); + this.connectionPromise.then(() => this._screen_on()); + } + _screen_off() { + this._panel.setAttribute("dark", ""); + this.sendUpdate({ + screen_on: false, + screen_brightness: 0, + }); + const l = () => this._screen_on(); + for (const ev of ["pointerdown", "pointermove", "keydown"]) { + this._listeners[ev] = l; + window.addEventListener(ev, l); + } + } + _screen_on(ev = undefined) { + var _a; + if ((_a = ev === null || ev === void 0 ? void 0 : ev.detail) === null || _a === void 0 ? void 0 : _a.brightness) { + this._brightness = ev.detail.brightness; + this._panel.style.setProperty("--darkness", 1 - ev.detail.brightness / 255); + } + this._panel.removeAttribute("dark"); + this.sendUpdate({ + screen_on: true, + screen_brightness: this._brightness, + }); + for (const ev of ["pointerdown", "pointermove", "keydown"]) { + if (this._listeners[ev]) { + window.removeEventListener(ev, this._listeners[ev]); + this._listeners[ev] = undefined; + } + } + } + } + return ScreenSaverMixinClass; +}; + +const MediaPlayerMixin = (SuperClass) => { + return class MediaPlayerMixinClass extends SuperClass { + constructor() { + super(); + this.player = new Audio(); + this._player_enabled = false; + for (const ev of ["play", "pause", "ended", "volumechange"]) { + this.player.addEventListener(ev, () => this._player_update()); + } + window.addEventListener("pointerdown", () => { + this._player_enabled = true; + if (!this.player.ended) + this.player.play(); + }, { once: true }); + this.addEventListener("command-player-play", (ev) => { + var _a; + if ((_a = ev.detail) === null || _a === void 0 ? void 0 : _a.media_content_id) + this.player.src = ev.detail.media_content_id; this.player.play(); - }, { - once: true, - }); - } - player_update(ev) { - this.sendUpdate({ - player: { - volume: this.player.volume, - muted: this.player.muted, - src: this.player.src, - state: this.player_state, - }, - }); - } - get player_state() { - if (!this.player.src) - return "stopped"; - if (this.player.ended) - return "stopped"; - if (this.player.paused) - return "paused"; - return "playing"; - } - player_play(src) { - if (src) - this.player.src = src; - this.player.play(); - } - player_pause() { - this.player.pause(); - } - player_stop() { - this.player.pause(); - this.player.src = null; - } - player_set_volume(level) { - if (level === undefined) - return; - this.player.volume = level; - } - player_mute(mute) { - if (mute === undefined) - mute = !this.player.muted; - this.player.muted = Boolean(mute); - } -}; - -const FullyKioskMixin = (C) => class extends C { - get isFully() { - return window.fully !== undefined; - } - constructor() { - super(); - if (!this.isFully) - return; - this._fullyMotion = false; - this._motionTimeout = undefined; - for (const ev of [ - "screenOn", - "screenOff", - "pluggedAC", - "pluggedUSB", - "onBatteryLevelChanged", - "unplugged", - "networkReconnect", - "onMotion", - ]) { - window.fully.bind(ev, `window.browser_mod.fully_update("${ev}");`); - } - window.fully.bind("onScreensaverStart", `window.browser_mod.fully_screensaver = true; window.browser_mod.screen_update();`); - window.fully.bind("onScreensaverStop", `window.browser_mod.fully_screensaver = false; window.browser_mod.screen_update();`); - this._keepingAlive = false; - } - fully_update(event) { - if (!this.isFully) - return; - if (event === "screenOn") { - window.clearTimeout(this._keepAliveTimer); - if (!this._keepingAlive) - this.screen_update(); - } - else if (event === "screenOff") { - this.screen_update(); - this._keepingAlive = false; - if (this.config.force_stay_awake) { - this._keepAliveTimer = window.setTimeout(() => { - this._keepingAlive = true; - window.fully.turnScreenOn(); - window.fully.turnScreenOff(); - }, 270000); - } - } - else if (event === "onMotion") { - this.fullyMotionTriggered(); - } - this.sendUpdate({ - fully: { - battery: window.fully.getBatteryLevel(), - charging: window.fully.isPlugged(), - motion: this._fullyMotion, - ip: window.fully.getIp4Address(), - }, - }); - } - startCamera() { - if (this._fullyCameraTimer !== undefined) - return; - this._fullyCameraTimer = window.setInterval(() => { - this.sendUpdate({ - camera: window.fully.getCamshotJpgBase64(), }); - }, 200); - } - stopCamera() { - window.clearInterval(this._fullyCameraTimer); - this._fullyCameraTimer = undefined; - } - fullyMotionTriggered() { - if (this._keepingAlive) - return; - this._fullyMotion = true; - this.startCamera(); - clearTimeout(this._motionTimeout); - this._motionTimeout = setTimeout(() => { - this._fullyMotion = false; - this.stopCamera(); - this.fully_update(); - }, 5000); - this.fully_update(); - } -}; - -const BrowserModCameraMixin = (C) => class extends C { - setup_camera() { - console.log("Starting camera"); - if (this._video) - return; - this._video = document.createElement("video"); - this._video.autoplay = true; - this._video.playsInline = true; - this._video.style.display = "none"; - this._canvas = document.createElement("canvas"); - this._canvas.style.display = "none"; - document.body.appendChild(this._video); - document.body.appendChild(this._canvas); - if (!navigator.mediaDevices) - return; - console.log("Starting devices"); - navigator.mediaDevices - .getUserMedia({ video: true, audio: false }) - .then((stream) => { - this._video.srcObject = stream; - this._video.play(); - this.update_camera(); - }); - this._camera_framerate = 2; - window.addEventListener("click", () => { - if (this._video.ended || this._video.paused) - this._video.play(); - }, { - once: true, - }); - } - update_camera() { - this._canvas.width = this._video.videoWidth; - this._canvas.height = this._video.videoHeight; - const context = this._canvas.getContext("2d"); - context.drawImage(this._video, 0, 0, this._video.videoWidth, this._video.videoHeight); - this.sendUpdate({ - camera: this._canvas.toDataURL("image/jpeg"), - }); - setTimeout(() => this.update_camera(), Math.round(1000 / this._camera_framerate)); - } -}; - -const BrowserModScreensaverMixin = (C) => class extends C { - constructor() { - super(); - this._blackout_panel = document.createElement("div"); - this._screenSaver = undefined; - this._screenSaverTimer = undefined; - this._screenSaverTimeOut = 0; - this._screenSaver = { - fn: undefined, - clearfn: undefined, - timer: undefined, - timeout: undefined, - listeners: {}, - active: false, - }; - this._blackout_panel.style.cssText = ` - position: fixed; - left: 0; - top: 0; - padding: 0; - margin: 0; - width: 100%; - height: 100%; - background: black; - display: none; - `; - document.body.appendChild(this._blackout_panel); - } - screensaver_set(fn, clearfn, time) { - this._ss_clear(); - this._screenSaver = { - fn, - clearfn, - timer: undefined, - timeout: time, - listeners: {}, - active: false, - }; - const l = () => this.screensaver_update(); - for (const event of ["mousemove", "mousedown", "keydown", "touchstart"]) { - window.addEventListener(event, l); - this._screenSaver.listeners[event] = l; - } - this._screenSaver.timer = window.setTimeout(() => this._ss_run(), time * 1000); - } - screensaver_update() { - if (this._screenSaver.active) { - this.screensaver_stop(); - } - else { - window.clearTimeout(this._screenSaver.timer); - this._screenSaver.timer = window.setTimeout(() => this._ss_run(), this._screenSaver.timeout * 1000); - } - } - screensaver_stop() { - this._ss_clear(); - this._screenSaver.active = false; - if (this._screenSaver.clearfn) - this._screenSaver.clearfn(); - if (this._screenSaver.timeout) { - this.screensaver_set(this._screenSaver.fn, this._screenSaver.clearfn, this._screenSaver.timeout); - } - } - _ss_clear() { - window.clearTimeout(this._screenSaverTimer); - for (const [k, v] of Object.entries(this._screenSaver.listeners)) { - window.removeEventListener(k, v); - } - } - _ss_run() { - this._screenSaver.active = true; - this._screenSaver.fn(); - } - do_blackout(timeout) { - this.screensaver_set(() => { - if (this.isFully) { - if (this.config.screensaver) - window.fully.startScreensaver(); + this.addEventListener("command-player-pause", (ev) => this.player.pause()); + this.addEventListener("command-player-stop", (ev) => { + this.player.src = null; + this.player.pause(); + }); + this.addEventListener("command-player-set-volume", (ev) => { + var _a; + if (((_a = ev.detail) === null || _a === void 0 ? void 0 : _a.volume_level) === undefined) + return; + this.player.volume = ev.detail.volume_level; + }); + this.addEventListener("command-player-mute", (ev) => { + var _a; + if (((_a = ev.detail) === null || _a === void 0 ? void 0 : _a.mute) !== undefined) + this.player.muted = Boolean(ev.detail.mute); else - window.fully.turnScreenOff(true); - } - else { - this._blackout_panel.style.display = "block"; - } - this.screen_update(); - }, () => { - if ((this._blackout_panel.style.display = "block")) - this._blackout_panel.style.display = "none"; - if (this.isFully) { - if (!window.fully.getScreenOn()) - window.fully.turnScreenOn(); - window.fully.stopScreensaver(); - } - this.screen_update(); - }, timeout || 0); - } - no_blackout() { - if (this.isFully) { - window.fully.turnScreenOn(); - window.fully.stopScreensaver(); - } - this.screensaver_stop(); - } - screen_update() { - this.sendUpdate({ - screen: { - blackout: this.isFully - ? this.fully_screensaver !== undefined - ? this.fully_screensaver - : !window.fully.getScreenOn() - : Boolean(this._blackout_panel.style.display === "block"), - brightness: this.isFully - ? window.fully.getScreenBrightness() - : undefined, - }, - }); - } -}; - -async function moreInfo(entity, large=false) { - const root = document.querySelector("hc-main") || document.querySelector("home-assistant"); - fireEvent("hass-more-info", {entityId: entity}, root); - const el = await selectTree(root, "$ ha-more-info-dialog"); - if(el) - el.large = large; - return el; -} - -const BrowserModPopupsMixin = (C) => class extends C { - constructor() { - super(); - if (document.querySelector("home-assistant")) - document - .querySelector("home-assistant") - .addEventListener("hass-more-info", (ev) => this._popup_card(ev)); - const isCast = document.querySelector("hc-main") !== null; - if (!isCast) - load_lovelace(); - } - _popup_card(ev) { - if (!lovelace()) - return; - if (!ev.detail || !ev.detail.entityId) - return; - const data = Object.assign(Object.assign({}, lovelace().config.popup_cards), lovelace().config.views[lovelace().current_view].popup_cards); - const d = data[ev.detail.entityId]; - if (!d) - return; - this.do_popup(d); - window.setTimeout(() => { - fireEvent("hass-more-info", { entityID: "." }, ha_element()); - }, 50); - } - do_popup(cfg) { - if (!(cfg.title || cfg.auto_close || cfg.hide_header)) { - console.error("browser_mod: popup: Must specify title, auto_close or hide_header."); - return; - } - if (!cfg.card) { - console.error("browser_mod: popup: No card specified"); - return; - } - const open = () => { - popUp(cfg.title, cfg.card, cfg.large, cfg.style, cfg.auto_close || cfg.hide_header); - }; - if (cfg.auto_close) { - this.screensaver_set(open, closePopUp, cfg.time); - } - else { - open(); - } - } - do_close_popup() { - this.screensaver_stop(); - closePopUp(); - } - do_more_info(entity_id, large) { - if (!entity_id) - return; - moreInfo(entity_id, large); - } - do_toast(message, duration) { - if (!message) - return; - fireEvent("hass-notification", { - message, - duration: parseInt(duration), - }, ha_element()); - } -}; - -const BrowserModBrowserMixin = (C) => class extends C { - constructor() { - super(); - document.addEventListener("visibilitychange", () => this.sensor_update()); - window.addEventListener("location-changed", () => this.sensor_update()); - window.setInterval(() => this.sensor_update(), 10000); - } - sensor_update() { - const update = async () => { - var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l; - const battery = (_b = (_a = navigator).getBattery) === null || _b === void 0 ? void 0 : _b.call(_a); - this.sendUpdate({ - browser: { - path: window.location.pathname, - visibility: document.visibilityState, - userAgent: navigator.userAgent, - currentUser: (_d = (_c = this.hass) === null || _c === void 0 ? void 0 : _c.user) === null || _d === void 0 ? void 0 : _d.name, - fullyKiosk: this.isFully, - width: window.innerWidth, - height: window.innerHeight, - battery_level: (_f = (_e = window.fully) === null || _e === void 0 ? void 0 : _e.getBatteryLevel()) !== null && _f !== void 0 ? _f : (battery === null || battery === void 0 ? void 0 : battery.level) * 100, - charging: (_h = (_g = window.fully) === null || _g === void 0 ? void 0 : _g.isPlugged()) !== null && _h !== void 0 ? _h : battery === null || battery === void 0 ? void 0 : battery.charging, - darkMode: (_k = (_j = this.hass) === null || _j === void 0 ? void 0 : _j.themes) === null || _k === void 0 ? void 0 : _k.darkMode, - userData: (_l = this.hass) === null || _l === void 0 ? void 0 : _l.user, - config: this.config, - }, + this.player.muted = !this.player.muted; }); - }; - update(); - } - do_navigate(path) { - if (!path) - return; - history.pushState(null, "", path); - fireEvent("location-changed", {}, ha_element()); - } + this.connectionPromise.then(() => this._player_update()); + } + _player_update() { + const state = this._player_enabled + ? this.player.src + ? this.player.ended + ? "stopped" + : this.player.paused + ? "paused" + : "playing" + : "stopped" + : "unavailable"; + this.sendUpdate({ + player: { + volume: this.player.volume, + muted: this.player.muted, + src: this.player.src, + state, + } + }); + } + }; }; var name = "browser_mod"; -var version = "1.5.3"; +var version = "2.0.0b0"; var description = ""; var scripts = { build: "rollup -c", @@ -1137,15 +962,15 @@ var pjson = { dependencies: dependencies }; -const ext = (baseClass, mixins) => mixins.reduceRight((base, mixin) => mixin(base), baseClass); -class BrowserMod extends ext(BrowserModConnection, [ - BrowserModBrowserMixin, - BrowserModPopupsMixin, - BrowserModScreensaverMixin, - BrowserModCameraMixin, - FullyKioskMixin, - BrowserModMediaPlayerMixin, -]) { +// export class BrowserMod extends ext(BrowserModConnection, [ +// BrowserModBrowserMixin, +// BrowserModPopupsMixin, +// BrowserModScreensaverMixin, +// BrowserModCameraMixin, +// FullyKioskMixin, +// BrowserModMediaPlayerMixin, +// ]) { +class BrowserMod extends MediaPlayerMixin(ScreenSaverMixin(ConnectionMixin(EventTarget))) { constructor() { super(); this.entity_id = deviceID.replace("-", "_"); @@ -1225,21 +1050,6 @@ class BrowserMod extends ext(BrowserModConnection, [ let service_data = _replaceThis(JSON.parse(JSON.stringify(msg.service_data))); this.hass.callService(domain, service, service_data); } - update(msg = null) { - if (msg) { - if (msg.name) { - this.entity_id = msg.name.toLowerCase(); - } - if (msg.camera && !this.isFully) { - this.setup_camera(); - } - this.config = Object.assign(Object.assign({}, this.config), msg); - } - this.player_update(); - this.fully_update(); - this.screen_update(); - this.sensor_update(); - } } (async () => { await hass_loaded(); diff --git a/custom_components/browser_mod/browser_mod_panel.js b/custom_components/browser_mod/browser_mod_panel.js index a1a5fee..81ad178 100644 --- a/custom_components/browser_mod/browser_mod_panel.js +++ b/custom_components/browser_mod/browser_mod_panel.js @@ -1,161 +1,67 @@ +/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */ + +function __decorate(decorators, target, key, desc) { + var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); + else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; +} + /** * @license * Copyright 2019 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const t$1=window.ShadowRoot&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,e$2=Symbol(),n$3=new Map;class s$3{constructor(t,n){if(this._$cssResult$=!0,n!==e$2)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t;}get styleSheet(){let e=n$3.get(this.cssText);return t$1&&void 0===e&&(n$3.set(this.cssText,e=new CSSStyleSheet),e.replaceSync(this.cssText)),e}toString(){return this.cssText}}const o$3=t=>new s$3("string"==typeof t?t:t+"",e$2),r$2=(t,...n)=>{const o=1===t.length?t[0]:n.reduce(((e,n,s)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(n)+t[s+1]),t[0]);return new s$3(o,e$2)},i$1=(e,n)=>{t$1?e.adoptedStyleSheets=n.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):n.forEach((t=>{const n=document.createElement("style"),s=window.litNonce;void 0!==s&&n.setAttribute("nonce",s),n.textContent=t.cssText,e.appendChild(n);}));},S$1=t$1?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const n of t.cssRules)e+=n.cssText;return o$3(e)})(t):t; +const t$1=window.ShadowRoot&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,e$3=Symbol(),n$4=new Map;class s$3{constructor(t,n){if(this._$cssResult$=!0,n!==e$3)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t;}get styleSheet(){let e=n$4.get(this.cssText);return t$1&&void 0===e&&(n$4.set(this.cssText,e=new CSSStyleSheet),e.replaceSync(this.cssText)),e}toString(){return this.cssText}}const o$3=t=>new s$3("string"==typeof t?t:t+"",e$3),r$2=(t,...n)=>{const o=1===t.length?t[0]:n.reduce(((e,n,s)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(n)+t[s+1]),t[0]);return new s$3(o,e$3)},i$2=(e,n)=>{t$1?e.adoptedStyleSheets=n.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):n.forEach((t=>{const n=document.createElement("style"),s=window.litNonce;void 0!==s&&n.setAttribute("nonce",s),n.textContent=t.cssText,e.appendChild(n);}));},S$1=t$1?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const n of t.cssRules)e+=n.cssText;return o$3(e)})(t):t; /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause - */var s$2;const e$1=window.trustedTypes,r$1=e$1?e$1.emptyScript:"",h$1=window.reactiveElementPolyfillSupport,o$2={toAttribute(t,i){switch(i){case Boolean:t=t?r$1:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t);}return t},fromAttribute(t,i){let s=t;switch(i){case Boolean:s=null!==t;break;case Number:s=null===t?null:Number(t);break;case Object:case Array:try{s=JSON.parse(t);}catch(t){s=null;}}return s}},n$2=(t,i)=>i!==t&&(i==i||t==t),l$2={attribute:!0,type:String,converter:o$2,reflect:!1,hasChanged:n$2};class a$1 extends HTMLElement{constructor(){super(),this._$Et=new Map,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Ei=null,this.o();}static addInitializer(t){var i;null!==(i=this.l)&&void 0!==i||(this.l=[]),this.l.push(t);}static get observedAttributes(){this.finalize();const t=[];return this.elementProperties.forEach(((i,s)=>{const e=this._$Eh(s,i);void 0!==e&&(this._$Eu.set(e,s),t.push(e));})),t}static createProperty(t,i=l$2){if(i.state&&(i.attribute=!1),this.finalize(),this.elementProperties.set(t,i),!i.noAccessor&&!this.prototype.hasOwnProperty(t)){const s="symbol"==typeof t?Symbol():"__"+t,e=this.getPropertyDescriptor(t,s,i);void 0!==e&&Object.defineProperty(this.prototype,t,e);}}static getPropertyDescriptor(t,i,s){return {get(){return this[i]},set(e){const r=this[t];this[i]=e,this.requestUpdate(t,r,s);},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)||l$2}static finalize(){if(this.hasOwnProperty("finalized"))return !1;this.finalized=!0;const t=Object.getPrototypeOf(this);if(t.finalize(),this.elementProperties=new Map(t.elementProperties),this._$Eu=new Map,this.hasOwnProperty("properties")){const t=this.properties,i=[...Object.getOwnPropertyNames(t),...Object.getOwnPropertySymbols(t)];for(const s of i)this.createProperty(s,t[s]);}return this.elementStyles=this.finalizeStyles(this.styles),!0}static finalizeStyles(i){const s=[];if(Array.isArray(i)){const e=new Set(i.flat(1/0).reverse());for(const i of e)s.unshift(S$1(i));}else void 0!==i&&s.push(S$1(i));return s}static _$Eh(t,i){const s=i.attribute;return !1===s?void 0:"string"==typeof s?s:"string"==typeof t?t.toLowerCase():void 0}o(){var t;this._$Ep=new Promise((t=>this.enableUpdating=t)),this._$AL=new Map,this._$Em(),this.requestUpdate(),null===(t=this.constructor.l)||void 0===t||t.forEach((t=>t(this)));}addController(t){var i,s;(null!==(i=this._$Eg)&&void 0!==i?i:this._$Eg=[]).push(t),void 0!==this.renderRoot&&this.isConnected&&(null===(s=t.hostConnected)||void 0===s||s.call(t));}removeController(t){var i;null===(i=this._$Eg)||void 0===i||i.splice(this._$Eg.indexOf(t)>>>0,1);}_$Em(){this.constructor.elementProperties.forEach(((t,i)=>{this.hasOwnProperty(i)&&(this._$Et.set(i,this[i]),delete this[i]);}));}createRenderRoot(){var t;const s=null!==(t=this.shadowRoot)&&void 0!==t?t:this.attachShadow(this.constructor.shadowRootOptions);return i$1(s,this.constructor.elementStyles),s}connectedCallback(){var t;void 0===this.renderRoot&&(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(t=this._$Eg)||void 0===t||t.forEach((t=>{var i;return null===(i=t.hostConnected)||void 0===i?void 0:i.call(t)}));}enableUpdating(t){}disconnectedCallback(){var t;null===(t=this._$Eg)||void 0===t||t.forEach((t=>{var i;return null===(i=t.hostDisconnected)||void 0===i?void 0:i.call(t)}));}attributeChangedCallback(t,i,s){this._$AK(t,s);}_$ES(t,i,s=l$2){var e,r;const h=this.constructor._$Eh(t,s);if(void 0!==h&&!0===s.reflect){const n=(null!==(r=null===(e=s.converter)||void 0===e?void 0:e.toAttribute)&&void 0!==r?r:o$2.toAttribute)(i,s.type);this._$Ei=t,null==n?this.removeAttribute(h):this.setAttribute(h,n),this._$Ei=null;}}_$AK(t,i){var s,e,r;const h=this.constructor,n=h._$Eu.get(t);if(void 0!==n&&this._$Ei!==n){const t=h.getPropertyOptions(n),l=t.converter,a=null!==(r=null!==(e=null===(s=l)||void 0===s?void 0:s.fromAttribute)&&void 0!==e?e:"function"==typeof l?l:null)&&void 0!==r?r:o$2.fromAttribute;this._$Ei=n,this[n]=a(i,t.type),this._$Ei=null;}}requestUpdate(t,i,s){let e=!0;void 0!==t&&(((s=s||this.constructor.getPropertyOptions(t)).hasChanged||n$2)(this[t],i)?(this._$AL.has(t)||this._$AL.set(t,i),!0===s.reflect&&this._$Ei!==t&&(void 0===this._$EC&&(this._$EC=new Map),this._$EC.set(t,s))):e=!1),!this.isUpdatePending&&e&&(this._$Ep=this._$E_());}async _$E_(){this.isUpdatePending=!0;try{await this._$Ep;}catch(t){Promise.reject(t);}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){var t;if(!this.isUpdatePending)return;this.hasUpdated,this._$Et&&(this._$Et.forEach(((t,i)=>this[i]=t)),this._$Et=void 0);let i=!1;const s=this._$AL;try{i=this.shouldUpdate(s),i?(this.willUpdate(s),null===(t=this._$Eg)||void 0===t||t.forEach((t=>{var i;return null===(i=t.hostUpdate)||void 0===i?void 0:i.call(t)})),this.update(s)):this._$EU();}catch(t){throw i=!1,this._$EU(),t}i&&this._$AE(s);}willUpdate(t){}_$AE(t){var i;null===(i=this._$Eg)||void 0===i||i.forEach((t=>{var i;return null===(i=t.hostUpdated)||void 0===i?void 0:i.call(t)})),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t);}_$EU(){this._$AL=new Map,this.isUpdatePending=!1;}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$Ep}shouldUpdate(t){return !0}update(t){void 0!==this._$EC&&(this._$EC.forEach(((t,i)=>this._$ES(i,this[i],t))),this._$EC=void 0),this._$EU();}updated(t){}firstUpdated(t){}}a$1.finalized=!0,a$1.elementProperties=new Map,a$1.elementStyles=[],a$1.shadowRootOptions={mode:"open"},null==h$1||h$1({ReactiveElement:a$1}),(null!==(s$2=globalThis.reactiveElementVersions)&&void 0!==s$2?s$2:globalThis.reactiveElementVersions=[]).push("1.3.1"); + */var s$2;const e$2=window.trustedTypes,r$1=e$2?e$2.emptyScript:"",h$1=window.reactiveElementPolyfillSupport,o$2={toAttribute(t,i){switch(i){case Boolean:t=t?r$1:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t);}return t},fromAttribute(t,i){let s=t;switch(i){case Boolean:s=null!==t;break;case Number:s=null===t?null:Number(t);break;case Object:case Array:try{s=JSON.parse(t);}catch(t){s=null;}}return s}},n$3=(t,i)=>i!==t&&(i==i||t==t),l$2={attribute:!0,type:String,converter:o$2,reflect:!1,hasChanged:n$3};class a$1 extends HTMLElement{constructor(){super(),this._$Et=new Map,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Ei=null,this.o();}static addInitializer(t){var i;null!==(i=this.l)&&void 0!==i||(this.l=[]),this.l.push(t);}static get observedAttributes(){this.finalize();const t=[];return this.elementProperties.forEach(((i,s)=>{const e=this._$Eh(s,i);void 0!==e&&(this._$Eu.set(e,s),t.push(e));})),t}static createProperty(t,i=l$2){if(i.state&&(i.attribute=!1),this.finalize(),this.elementProperties.set(t,i),!i.noAccessor&&!this.prototype.hasOwnProperty(t)){const s="symbol"==typeof t?Symbol():"__"+t,e=this.getPropertyDescriptor(t,s,i);void 0!==e&&Object.defineProperty(this.prototype,t,e);}}static getPropertyDescriptor(t,i,s){return {get(){return this[i]},set(e){const r=this[t];this[i]=e,this.requestUpdate(t,r,s);},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)||l$2}static finalize(){if(this.hasOwnProperty("finalized"))return !1;this.finalized=!0;const t=Object.getPrototypeOf(this);if(t.finalize(),this.elementProperties=new Map(t.elementProperties),this._$Eu=new Map,this.hasOwnProperty("properties")){const t=this.properties,i=[...Object.getOwnPropertyNames(t),...Object.getOwnPropertySymbols(t)];for(const s of i)this.createProperty(s,t[s]);}return this.elementStyles=this.finalizeStyles(this.styles),!0}static finalizeStyles(i){const s=[];if(Array.isArray(i)){const e=new Set(i.flat(1/0).reverse());for(const i of e)s.unshift(S$1(i));}else void 0!==i&&s.push(S$1(i));return s}static _$Eh(t,i){const s=i.attribute;return !1===s?void 0:"string"==typeof s?s:"string"==typeof t?t.toLowerCase():void 0}o(){var t;this._$Ep=new Promise((t=>this.enableUpdating=t)),this._$AL=new Map,this._$Em(),this.requestUpdate(),null===(t=this.constructor.l)||void 0===t||t.forEach((t=>t(this)));}addController(t){var i,s;(null!==(i=this._$Eg)&&void 0!==i?i:this._$Eg=[]).push(t),void 0!==this.renderRoot&&this.isConnected&&(null===(s=t.hostConnected)||void 0===s||s.call(t));}removeController(t){var i;null===(i=this._$Eg)||void 0===i||i.splice(this._$Eg.indexOf(t)>>>0,1);}_$Em(){this.constructor.elementProperties.forEach(((t,i)=>{this.hasOwnProperty(i)&&(this._$Et.set(i,this[i]),delete this[i]);}));}createRenderRoot(){var t;const s=null!==(t=this.shadowRoot)&&void 0!==t?t:this.attachShadow(this.constructor.shadowRootOptions);return i$2(s,this.constructor.elementStyles),s}connectedCallback(){var t;void 0===this.renderRoot&&(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(t=this._$Eg)||void 0===t||t.forEach((t=>{var i;return null===(i=t.hostConnected)||void 0===i?void 0:i.call(t)}));}enableUpdating(t){}disconnectedCallback(){var t;null===(t=this._$Eg)||void 0===t||t.forEach((t=>{var i;return null===(i=t.hostDisconnected)||void 0===i?void 0:i.call(t)}));}attributeChangedCallback(t,i,s){this._$AK(t,s);}_$ES(t,i,s=l$2){var e,r;const h=this.constructor._$Eh(t,s);if(void 0!==h&&!0===s.reflect){const n=(null!==(r=null===(e=s.converter)||void 0===e?void 0:e.toAttribute)&&void 0!==r?r:o$2.toAttribute)(i,s.type);this._$Ei=t,null==n?this.removeAttribute(h):this.setAttribute(h,n),this._$Ei=null;}}_$AK(t,i){var s,e,r;const h=this.constructor,n=h._$Eu.get(t);if(void 0!==n&&this._$Ei!==n){const t=h.getPropertyOptions(n),l=t.converter,a=null!==(r=null!==(e=null===(s=l)||void 0===s?void 0:s.fromAttribute)&&void 0!==e?e:"function"==typeof l?l:null)&&void 0!==r?r:o$2.fromAttribute;this._$Ei=n,this[n]=a(i,t.type),this._$Ei=null;}}requestUpdate(t,i,s){let e=!0;void 0!==t&&(((s=s||this.constructor.getPropertyOptions(t)).hasChanged||n$3)(this[t],i)?(this._$AL.has(t)||this._$AL.set(t,i),!0===s.reflect&&this._$Ei!==t&&(void 0===this._$EC&&(this._$EC=new Map),this._$EC.set(t,s))):e=!1),!this.isUpdatePending&&e&&(this._$Ep=this._$E_());}async _$E_(){this.isUpdatePending=!0;try{await this._$Ep;}catch(t){Promise.reject(t);}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){var t;if(!this.isUpdatePending)return;this.hasUpdated,this._$Et&&(this._$Et.forEach(((t,i)=>this[i]=t)),this._$Et=void 0);let i=!1;const s=this._$AL;try{i=this.shouldUpdate(s),i?(this.willUpdate(s),null===(t=this._$Eg)||void 0===t||t.forEach((t=>{var i;return null===(i=t.hostUpdate)||void 0===i?void 0:i.call(t)})),this.update(s)):this._$EU();}catch(t){throw i=!1,this._$EU(),t}i&&this._$AE(s);}willUpdate(t){}_$AE(t){var i;null===(i=this._$Eg)||void 0===i||i.forEach((t=>{var i;return null===(i=t.hostUpdated)||void 0===i?void 0:i.call(t)})),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t);}_$EU(){this._$AL=new Map,this.isUpdatePending=!1;}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$Ep}shouldUpdate(t){return !0}update(t){void 0!==this._$EC&&(this._$EC.forEach(((t,i)=>this._$ES(i,this[i],t))),this._$EC=void 0),this._$EU();}updated(t){}firstUpdated(t){}}a$1.finalized=!0,a$1.elementProperties=new Map,a$1.elementStyles=[],a$1.shadowRootOptions={mode:"open"},null==h$1||h$1({ReactiveElement:a$1}),(null!==(s$2=globalThis.reactiveElementVersions)&&void 0!==s$2?s$2:globalThis.reactiveElementVersions=[]).push("1.3.1"); /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -var t;const i=globalThis.trustedTypes,s$1=i?i.createPolicy("lit-html",{createHTML:t=>t}):void 0,e=`lit$${(Math.random()+"").slice(9)}$`,o$1="?"+e,n$1=`<${o$1}>`,l$1=document,h=(t="")=>l$1.createComment(t),r=t=>null===t||"object"!=typeof t&&"function"!=typeof t,d=Array.isArray,u=t=>{var i;return d(t)||"function"==typeof(null===(i=t)||void 0===i?void 0:i[Symbol.iterator])},c=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,v=/-->/g,a=/>/g,f=/>|[ \n \r](?:([^\s"'>=/]+)([ \n \r]*=[ \n \r]*(?:[^ \n \r"'`<>=]|("|')|))|$)/g,_=/'/g,m=/"/g,g=/^(?:script|style|textarea|title)$/i,p=t=>(i,...s)=>({_$litType$:t,strings:i,values:s}),$=p(1),b=Symbol.for("lit-noChange"),w=Symbol.for("lit-nothing"),T=new WeakMap,x=(t,i,s)=>{var e,o;const n=null!==(e=null==s?void 0:s.renderBefore)&&void 0!==e?e:i;let l=n._$litPart$;if(void 0===l){const t=null!==(o=null==s?void 0:s.renderBefore)&&void 0!==o?o:null;n._$litPart$=l=new N(i.insertBefore(h(),t),t,void 0,null!=s?s:{});}return l._$AI(t),l},A=l$1.createTreeWalker(l$1,129,null,!1),C=(t,i)=>{const o=t.length-1,l=[];let h,r=2===i?"":"",d=c;for(let i=0;i"===u[0]?(d=null!=h?h:c,p=-1):void 0===u[1]?p=-2:(p=d.lastIndex-u[2].length,o=u[1],d=void 0===u[3]?f:'"'===u[3]?m:_):d===m||d===_?d=f:d===v||d===a?d=c:(d=f,h=void 0);const y=d===f&&t[i+1].startsWith("/>")?" ":"";r+=d===c?s+n$1:p>=0?(l.push(o),s.slice(0,p)+"$lit$"+s.slice(p)+e+y):s+e+(-2===p?(l.push(void 0),i):y);}const u=r+(t[o]||"")+(2===i?"":"");if(!Array.isArray(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return [void 0!==s$1?s$1.createHTML(u):u,l]};class E{constructor({strings:t,_$litType$:s},n){let l;this.parts=[];let r=0,d=0;const u=t.length-1,c=this.parts,[v,a]=C(t,s);if(this.el=E.createElement(v,n),A.currentNode=this.el.content,2===s){const t=this.el.content,i=t.firstChild;i.remove(),t.append(...i.childNodes);}for(;null!==(l=A.nextNode())&&c.length0){l.textContent=i?i.emptyScript:"";for(let i=0;i2||""!==s[0]||""!==s[1]?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=w;}get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}_$AI(t,i=this,s,e){const o=this.strings;let n=!1;if(void 0===o)t=P(this,t,i,0),n=!r(t)||t!==this._$AH&&t!==b,n&&(this._$AH=t);else {const e=t;let l,h;for(t=o[0],l=0;lt}):void 0,e$1=`lit$${(Math.random()+"").slice(9)}$`,o$1="?"+e$1,n$2=`<${o$1}>`,l$1=document,h=(t="")=>l$1.createComment(t),r=t=>null===t||"object"!=typeof t&&"function"!=typeof t,d=Array.isArray,u=t=>{var i;return d(t)||"function"==typeof(null===(i=t)||void 0===i?void 0:i[Symbol.iterator])},c=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,v=/-->/g,a=/>/g,f=/>|[ \n \r](?:([^\s"'>=/]+)([ \n \r]*=[ \n \r]*(?:[^ \n \r"'`<>=]|("|')|))|$)/g,_=/'/g,m=/"/g,g=/^(?:script|style|textarea|title)$/i,p=t=>(i,...s)=>({_$litType$:t,strings:i,values:s}),$=p(1),b=Symbol.for("lit-noChange"),w=Symbol.for("lit-nothing"),T=new WeakMap,x=(t,i,s)=>{var e,o;const n=null!==(e=null==s?void 0:s.renderBefore)&&void 0!==e?e:i;let l=n._$litPart$;if(void 0===l){const t=null!==(o=null==s?void 0:s.renderBefore)&&void 0!==o?o:null;n._$litPart$=l=new N(i.insertBefore(h(),t),t,void 0,null!=s?s:{});}return l._$AI(t),l},A=l$1.createTreeWalker(l$1,129,null,!1),C=(t,i)=>{const o=t.length-1,l=[];let h,r=2===i?"":"",d=c;for(let i=0;i"===u[0]?(d=null!=h?h:c,p=-1):void 0===u[1]?p=-2:(p=d.lastIndex-u[2].length,o=u[1],d=void 0===u[3]?f:'"'===u[3]?m:_):d===m||d===_?d=f:d===v||d===a?d=c:(d=f,h=void 0);const y=d===f&&t[i+1].startsWith("/>")?" ":"";r+=d===c?s+n$2:p>=0?(l.push(o),s.slice(0,p)+"$lit$"+s.slice(p)+e$1+y):s+e$1+(-2===p?(l.push(void 0),i):y);}const u=r+(t[o]||"")+(2===i?"":"");if(!Array.isArray(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return [void 0!==s$1?s$1.createHTML(u):u,l]};class E{constructor({strings:t,_$litType$:s},n){let l;this.parts=[];let r=0,d=0;const u=t.length-1,c=this.parts,[v,a]=C(t,s);if(this.el=E.createElement(v,n),A.currentNode=this.el.content,2===s){const t=this.el.content,i=t.firstChild;i.remove(),t.append(...i.childNodes);}for(;null!==(l=A.nextNode())&&c.length0){l.textContent=i$1?i$1.emptyScript:"";for(let i=0;i2||""!==s[0]||""!==s[1]?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=w;}get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}_$AI(t,i=this,s,e){const o=this.strings;let n=!1;if(void 0===o)t=P(this,t,i,0),n=!r(t)||t!==this._$AH&&t!==b,n&&(this._$AH=t);else {const e=t;let l,h;for(t=o[0],l=0;l { - return Math.floor((1+Math.random())*100000).toString(16).substring(1); - }; - if(window['fully'] && typeof fully.getDeviceId === "function") - localStorage[ID_STORAGE_KEY] = fully.getDeviceId(); - else - localStorage[ID_STORAGE_KEY] = `${s4()}${s4()}-${s4()}${s4()}`; - } - return localStorage[ID_STORAGE_KEY]; -} -let deviceID = _deviceID(); +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const i=(i,e)=>"method"===e.kind&&e.descriptor&&!("value"in e.descriptor)?{...e,finisher(n){n.createProperty(e.key,i);}}:{kind:"field",key:Symbol(),placement:"own",descriptor:{},originalKey:e.key,initializer(){"function"==typeof e.initializer&&(this[e.key]=e.initializer.call(this));},finisher(n){n.createProperty(e.key,i);}};function e(e){return (n,t)=>void 0!==t?((i,e,n)=>{e.constructor.createProperty(n,i);})(e,n,t):i(e,n)} -const setDeviceID = (id) => { - if(id === null) return; - if(id === "clear") { - localStorage.removeItem(ID_STORAGE_KEY); - } else { - localStorage[ID_STORAGE_KEY] = id; - } - deviceID = _deviceID(); -}; +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */var n;null!=(null===(n=window.HTMLSlotElement)||void 0===n?void 0:n.prototype.assignedElements)?(o,n)=>o.assignedElements(n):(o,n)=>o.assignedNodes(n).filter((o=>o.nodeType===Node.ELEMENT_NODE)); -const params = new URLSearchParams(window.location.search); -if(params.get('deviceID')) { - setDeviceID(params.get('deviceID')); -} - -class BrowserModPanel extends s { - render() { - return $ ` - - - - -
Browser Mod Settingss
-
-
- - - -
-
-

Enable

- -
- Enable this browser as a Device in Home Assistant -
-

DeviceID

-
- - The device ID is a unique identifier for your browser/device - combination. -
-

Enable Camera

- -
- Get Camera input from this device (hardware dependent) -
-
-
- Update -
-
- - -
-
- - -
-
-

Cool function

- -
- Enabling this will cause cool stuff to happen. -
-

Another function

- -
- Enabling this will cause less cool stuff to happen. -
-
-
-
- `; - } - static get styles() { - return [ - ...customElements.get("ha-config-dashboard").styles, - r$2 ` - :host { - --app-header-background-color: var(--sidebar-background-color); - --app-header-text-color: var(--sidebar-text-color); - --app-header-border-bottom: 1px solid var(--divider-color); - } - .card-actions { - display: flex; - } - .spacer { - flex-grow: 1; - } - ha-textfield { - width: 250px; - display: block; - margin-top: 8px; - } - .option { - display: flex; - margin-top: 16px; - } - .option h3 { - flex-grow: 1; - margin: 0; - } - .option ha-switch { - margin-top: 0.25em; - margin-right: 7px; - margin-left: 0.5em; - } - `, - ]; - } -} +// Loads in ha-config-dashboard which is used to copy styling const loadDevTools = async () => { - var _a, _b, _c, _d, _e, _f, _g; + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l; if (customElements.get("ha-config-dashboard")) return; const ppResolver = document.createElement("partial-panel-resolver"); @@ -167,9 +73,208 @@ const loadDevTools = async () => { ]); await ((_c = (_b = (_a = routes === null || routes === void 0 ? void 0 : routes.routes) === null || _a === void 0 ? void 0 : _a.a) === null || _b === void 0 ? void 0 : _b.load) === null || _c === void 0 ? void 0 : _c.call(_b)); const configRouter = document.createElement("ha-panel-config"); - await ((_g = (_f = (_e = (_d = configRouter === null || configRouter === void 0 ? void 0 : configRouter.routerOptions) === null || _d === void 0 ? void 0 : _d.routes) === null || _e === void 0 ? void 0 : _e.dashboard) === null || _f === void 0 ? void 0 : _f.load) === null || _g === void 0 ? void 0 : _g.call(_f)); + await ((_g = (_f = (_e = (_d = configRouter === null || configRouter === void 0 ? void 0 : configRouter.routerOptions) === null || _d === void 0 ? void 0 : _d.routes) === null || _e === void 0 ? void 0 : _e.dashboard) === null || _f === void 0 ? void 0 : _f.load) === null || _g === void 0 ? void 0 : _g.call(_f)); // Load ha-config-dashboard + await ((_l = (_k = (_j = (_h = configRouter === null || configRouter === void 0 ? void 0 : configRouter.routerOptions) === null || _h === void 0 ? void 0 : _h.routes) === null || _j === void 0 ? void 0 : _j.cloud) === null || _k === void 0 ? void 0 : _k.load) === null || _l === void 0 ? void 0 : _l.call(_k)); // Load ha-settings-row await customElements.whenDefined("ha-config-dashboard"); -}; +}; + +const bmWindow = window; loadDevTools().then(() => { + class BrowserModPanel extends s { + toggleRegister() { + var _a; + if (!((_a = window.browser_mod) === null || _a === void 0 ? void 0 : _a.connected)) + return; + window.browser_mod.registered = !window.browser_mod.registered; + } + changeDeviceID(ev) { + window.browser_mod.deviceID = ev.target.value; + } + unregister_device(ev) { + const deviceID = ev.currentTarget.deviceID; + if (deviceID === window.browser_mod.deviceID) + window.browser_mod.registered = false; + else + window.browser_mod.connection.sendMessage({ + type: "browser_mod/unregister", + deviceID, + }); + } + firstUpdated() { + window.browser_mod.addEventListener("browser-mod-config-update", () => this.requestUpdate()); + } + render() { + var _a, _b, _c; + return $ ` + + + + +
Browser Mod Settings
+
+
+ + + +

+
This Browser
+ ${((_a = bmWindow.browser_mod) === null || _a === void 0 ? void 0 : _a.connected) + ? $ ` + + ` + : $ ` + + `} +

+
Browser-mod not connected.
+
+ + Enable + Enable this browser as a Device in Home Assistant + + + + + DeviceID + A unique identifier for this browser-device + combination. + + + + + Enable camera + Get camera input from this device (hardware + dependent) + + +
+
+ + +
+ ${Object.keys(window.browser_mod.devices).map((d) => $ ` + ${d} + + Last connected: + + + + + + + + + `)} +
+
+ + +
+ + Auto enable devices + + + + User sidebar + Save sidebar as default for current user + (${this.hass.user.name}) + Save + + + Global sidebar + Save sidebar as default for all users + Save + +
+
+
+
+ `; + } + static get styles() { + var _a, _b; + return [ + ...((_b = (_a = customElements.get("ha-config-dashboard")) === null || _a === void 0 ? void 0 : _a.styles) !== null && _b !== void 0 ? _b : []), + r$2 ` + :host { + --app-header-background-color: var(--sidebar-background-color); + --app-header-text-color: var(--sidebar-text-color); + --app-header-border-bottom: 1px solid var(--divider-color); + --ha-card-border-radius: var(--ha-config-card-border-radius, 8px); + } + .card-header { + display: flex; + justify-content: space-between; + } + .card-actions { + display: flex; + } + .spacer { + flex-grow: 1; + } + ha-textfield { + width: 250px; + display: block; + margin-top: 8px; + } + .option { + display: flex; + margin-top: 16px; + } + .option h3 { + flex-grow: 1; + margin: 0; + } + .option ha-switch { + margin-top: 0.25em; + margin-right: 7px; + margin-left: 0.5em; + } + ha-icon-button > * { + display: flex; + } + `, + ]; + } + } + __decorate([ + e() + ], BrowserModPanel.prototype, "hass", void 0); + __decorate([ + e() + ], BrowserModPanel.prototype, "narrow", void 0); + __decorate([ + e() + ], BrowserModPanel.prototype, "connection", void 0); customElements.define("browser-mod-panel", BrowserModPanel); }); diff --git a/custom_components/browser_mod/config_flow.py b/custom_components/browser_mod/config_flow.py index c380286..43d0a61 100644 --- a/custom_components/browser_mod/config_flow.py +++ b/custom_components/browser_mod/config_flow.py @@ -3,9 +3,12 @@ from homeassistant import config_entries from .const import DOMAIN -class BrowserModConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +@config_entries.HANDLERS.register(DOMAIN) +class BrowserModConfigFlow(config_entries.ConfigFlow): - VERSION = 1 + VERSION = 2 - async def async_step_import(self, import_info): - return self.async_create_entry(title="Browser Mod", data={}) + async def async_step_user(self, user_input=None): + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + return self.async_create_entry(title="", data={}) diff --git a/custom_components/browser_mod/connection.py b/custom_components/browser_mod/connection.py index c6a1b73..3260834 100644 --- a/custom_components/browser_mod/connection.py +++ b/custom_components/browser_mod/connection.py @@ -1,5 +1,6 @@ import logging import voluptuous as vol +from datetime import datetime, timezone from homeassistant.components.websocket_api import ( websocket_command, @@ -8,115 +9,138 @@ from homeassistant.components.websocket_api import ( async_register_command, ) -from .const import WS_CONNECT, WS_UPDATE +from homeassistant.components import websocket_api + +from .const import WS_CONNECT, WS_REGISTER, WS_UNREGISTER, WS_REREGISTER, WS_UPDATE, DOMAIN from .helpers import get_devices, create_entity, get_config, is_setup_complete +from .coordinator import Coordinator +from .device import getDevice + _LOGGER = logging.getLogger(__name__) -async def setup_connection(hass, config): - @websocket_command( +async def async_setup_connection(hass): + @websocket_api.websocket_command( { vol.Required("type"): WS_CONNECT, vol.Required("deviceID"): str, } ) - def handle_connect(hass, connection, msg): + @websocket_api.async_response + async def handle_connect(hass, connection, msg): deviceID = msg["deviceID"] + store = hass.data[DOMAIN]["store"] - device = get_devices(hass).get(deviceID, BrowserModConnection(hass, deviceID)) - device.connect(connection, msg["id"]) - get_devices(hass)[deviceID] = device + def listener(data): + connection.send_message(event_message(msg["id"], {"result": data})) - connection.send_message(result_message(msg["id"])) + connection.subscriptions[msg["id"]] = store.add_listener(listener) + connection.send_result(msg["id"]) - @websocket_command( + if store.get_device(deviceID).enabled: + dev = getDevice(hass, deviceID) + dev.connection = (connection, msg["id"]) + await store.set_device(deviceID, + last_seen=datetime.now( + tz=timezone.utc + ).isoformat() + ) + listener(store.asdict()) + + + @websocket_api.websocket_command( + { + vol.Required("type"): WS_REGISTER, + vol.Required("deviceID"): str, + } + ) + @websocket_api.async_response + async def handle_register(hass, connection, msg): + deviceID = msg["deviceID"] + store = hass.data[DOMAIN]["store"] + await store.set_device(deviceID, + enabled=True + ) + connection.send_result(msg["id"]) + + + @websocket_api.websocket_command( + { + vol.Required("type"): WS_UNREGISTER, + vol.Required("deviceID"): str, + } + ) + @websocket_api.async_response + async def handle_unregister(hass, connection, msg): + deviceID = msg["deviceID"] + store = hass.data[DOMAIN]["store"] + devices = hass.data[DOMAIN]["devices"] + + if deviceID in devices: + devices[deviceID].delete(hass) + del devices[deviceID] + + await store.delete_device(deviceID) + + connection.send_result(msg["id"]) + + @websocket_api.websocket_command( + { + vol.Required("type"): WS_REREGISTER, + vol.Required("deviceID"): str, + vol.Required("data"): dict, + } + ) + @websocket_api.async_response + async def handle_reregister(hass, connection, msg): + deviceID = msg["deviceID"] + store = hass.data[DOMAIN]["store"] + devices = hass.data[DOMAIN]["devices"] + + data = msg["data"] + del data["last_seen"] + device = {} + if "deviceID" in data: + newDeviceID = data["deviceID"] + del data["deviceID"] + + oldDevice = store.get_device(deviceID) + if oldDevice: + device = oldDevice.asdict() + await store.delete_device(deviceID) + + if deviceID in devices: + devices[deviceID].delete(hass) + del devices[deviceID] + + deviceID = newDeviceID + + device.update(data) + await store.set_device(deviceID, **device) + + + @websocket_api.websocket_command( { vol.Required("type"): WS_UPDATE, vol.Required("deviceID"): str, vol.Optional("data"): dict, } ) - def handle_update(hass, connection, msg): - devices = get_devices(hass) + @websocket_api.async_response + async def handle_update(hass, connection, msg): deviceID = msg["deviceID"] - if deviceID in devices and is_setup_complete(hass): - devices[deviceID].update(msg.get("data", None)) + store = hass.data[DOMAIN]["store"] + devices = hass.data[DOMAIN]["devices"] + + if store.get_device(deviceID).enabled: + dev = getDevice(hass, deviceID) + dev.data.update(msg.get("data", {})) + dev.coordinator.async_set_updated_data(dev.data) + async_register_command(hass, handle_connect) - async_register_command(hass, handle_update) - - -class BrowserModConnection: - def __init__(self, hass, deviceID): - self.hass = hass - self.deviceID = deviceID - self.connection = [] - - self.media_player = None - self.screen = None - self.sensor = None - self.fully = None - self.camera = None - - def connect(self, connection, cid): - self.connection.append((connection, cid)) - self.trigger_update() - - def disconnect(): - self.connection.remove((connection, cid)) - - connection.subscriptions[cid] = disconnect - - def send(self, command, **kwargs): - if self.connection: - connection, cid = self.connection[-1] - connection.send_message( - event_message( - cid, - { - "command": command, - **kwargs, - }, - ) - ) - - def trigger_update(self): - if is_setup_complete(self.hass): - self.send("update", **get_config(self.hass, self.deviceID)) - - def update(self, data): - if data.get("browser"): - self.sensor = self.sensor or create_entity( - self.hass, "sensor", self.deviceID, self - ) - if self.sensor: - self.sensor.data = data.get("browser") - - if data.get("player"): - self.media_player = self.media_player or create_entity( - self.hass, "media_player", self.deviceID, self - ) - if self.media_player: - self.media_player.data = data.get("player") - - if data.get("screen"): - self.screen = self.screen or create_entity( - self.hass, "light", self.deviceID, self - ) - if self.screen: - self.screen.data = data.get("screen") - - if data.get("fully"): - self.fully = self.fully or create_entity( - self.hass, "binary_sensor", self.deviceID, self - ) - if self.fully: - self.fully.data = data.get("fully") - - if data.get("camera"): - self.camera = self.camera or create_entity( - self.hass, "camera", self.deviceID, self - ) - if self.camera: - self.camera.data = data.get("camera") + async_register_command(hass, handle_register) + async_register_command(hass, handle_unregister) + async_register_command(hass, handle_reregister) + async_register_command(hass, handle_update) \ No newline at end of file diff --git a/custom_components/browser_mod/const.py b/custom_components/browser_mod/const.py index fb3f446..c083a44 100644 --- a/custom_components/browser_mod/const.py +++ b/custom_components/browser_mod/const.py @@ -3,11 +3,10 @@ DOMAIN = "browser_mod" FRONTEND_SCRIPT_URL = "/browser_mod.js" SETTINGS_PANEL_URL = "/browser_mod_panel.js" -DATA_EXTRA_MODULE_URL = "frontend_extra_module_url" - DATA_DEVICES = "devices" -DATA_ALIASES = "aliases" DATA_ADDERS = "adders" +DATA_STORE = "store" +DATA_ALIASES = "aliases" DATA_CONFIG = "config" DATA_SETUP_COMPLETE = "setup_complete" @@ -21,6 +20,10 @@ WS_CONNECT = "{}/connect".format(WS_ROOT) WS_UPDATE = "{}/update".format(WS_ROOT) WS_CAMERA = "{}/camera".format(WS_ROOT) +WS_REGISTER = f"{WS_ROOT}/register" +WS_UNREGISTER = f"{WS_ROOT}/unregister" +WS_REREGISTER = f"{WS_ROOT}/reregister" + USER_COMMANDS = [ "debug", "popup", diff --git a/custom_components/browser_mod/coordinator.py b/custom_components/browser_mod/coordinator.py new file mode 100644 index 0000000..deabf3a --- /dev/null +++ b/custom_components/browser_mod/coordinator.py @@ -0,0 +1,15 @@ +import logging + +from homeassistant.helpers.update_coordinator import (CoordinatorEntity, DataUpdateCoordinator, UpdateFailed) + +_LOGGER = logging.getLogger(__name__) + +class Coordinator(DataUpdateCoordinator): + + def __init__(self, hass, deviceID): + super().__init__( + hass, + _LOGGER, + name="Browser Mod Coordinator", + ) + self.deviceID = deviceID diff --git a/custom_components/browser_mod/device.py b/custom_components/browser_mod/device.py new file mode 100644 index 0000000..a9fbcce --- /dev/null +++ b/custom_components/browser_mod/device.py @@ -0,0 +1,108 @@ +import logging + +from homeassistant.components.websocket_api import event_message +from homeassistant.helpers import device_registry, entity_registry + +from .const import DOMAIN, DATA_ADDERS +from .coordinator import Coordinator +from .sensor import BrowserSensor +from .light import BrowserModLight +from .binary_sensor import BrowserBinarySensor +from .media_player import BrowserModPlayer + +_LOGGER = logging.getLogger(__name__) + +BROWSER_SENSORS = { + "battery_level", () +} + + + +class BrowserModDevice: + """ A Browser_mod device. """ + + def __init__(self, hass, deviceID): + """ """ + self.deviceID = deviceID + self.coordinator = Coordinator(hass, deviceID) + self.entities = [] + self.data = {} + self.setup_sensors(hass) + self.connection = None + + def setup_sensors(self, hass): + """ Create all entities associated with the device. """ + + coordinator = self.coordinator + deviceID = self.deviceID + + sensors = [ + ("battery_level", "Browser battery", "%", "battery"), + ("path", "Browser path"), + ("userAgent", "Browser userAgent"), + ("visibility", "Browser visibility"), + ("currentUser", "Browser user"), + ("height", "Browser height", "px"), + ("width", "Browser width", "px"), + ] + adder = hass.data[DOMAIN][DATA_ADDERS]["sensor"] + new = [BrowserSensor(coordinator, deviceID, *s) for s in sensors] + adder(new) + self.entities += new + + binary_sensors = [ + ("charging", "Browser charging"), + ("darkMode", "Browser dark mode"), + ("fullyKiosk", "Browser FullyKiosk"), + ] + adder = hass.data[DOMAIN][DATA_ADDERS]["binary_sensor"] + new = [BrowserBinarySensor(coordinator, deviceID, *s) for s in binary_sensors] + adder(new) + self.entities += new + + adder = hass.data[DOMAIN][DATA_ADDERS]["light"] + new = [BrowserModLight(coordinator, deviceID, self)] + adder(new) + self.entities += new + + adder = hass.data[DOMAIN][DATA_ADDERS]["media_player"] + new = [BrowserModPlayer(coordinator, deviceID, self)] + adder(new) + self.entities += new + + def send(self, command, **kwargs): + """ Send a command to this device. """ + if self.connection is None: return + + connection, cid = self.connection + + connection.send_message( + event_message( + cid, + { + "command": command, + **kwargs, + }, + ) + ) + + def delete(self, hass): + """ Delete device and associated entities. """ + dr = device_registry.async_get(hass) + er = entity_registry.async_get(hass) + + for e in self.entities: + er.async_remove(e.entity_id) + + device = dr.async_get_device({(DOMAIN, self.deviceID)}) + dr.async_remove_device(device.id) + + +def getDevice(hass, deviceID): + """ Get or create device by deviceID. """ + devices = hass.data[DOMAIN]["devices"] + if deviceID in devices: + return devices[deviceID] + + devices[deviceID] = BrowserModDevice(hass, deviceID) + return devices[deviceID] \ No newline at end of file diff --git a/custom_components/browser_mod/helpers.py b/custom_components/browser_mod/helpers.py index 546cc06..e60a2a1 100644 --- a/custom_components/browser_mod/helpers.py +++ b/custom_components/browser_mod/helpers.py @@ -1,6 +1,7 @@ import logging from homeassistant.helpers.entity import Entity, async_generate_entity_id +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( DOMAIN, @@ -15,6 +16,8 @@ from .const import ( DATA_SETUP_COMPLETE, ) +from .coordinator import Coordinator + _LOGGER = logging.getLogger(__name__) @@ -69,6 +72,45 @@ def setup_platform(hass, config, async_add_devices, platform, cls): def is_setup_complete(hass): return hass.data[DOMAIN][DATA_SETUP_COMPLETE] +class BrowserModEntity2(CoordinatorEntity): + + def __init__(self, coordinator, deviceID, name): + super().__init__(coordinator) + self.deviceID = deviceID + self._name = name + + @property + def _data(self): + return self.coordinator.data or {} + + @property + def device_info(self): + return { + "identifiers": {(DOMAIN, self.deviceID)}, + "name": self.deviceID, + "manufacturer": "Browser Mod", + } + + @property + def extra_state_attributes(self): + return { + "type": "browser_mod", + "deviceID": self.deviceID, + } + + @property + def name(self): + return self._name + @property + def has_entity_name(self): + return True + @property + def entity_registry_visible_default(self): + return False + @property + def unique_id(self): + return f"{self.deviceID}-{self._name.replace(' ','_')}" + class BrowserModEntity(Entity): def __init__(self, hass, connection, deviceID, alias=None): diff --git a/custom_components/browser_mod/light.py b/custom_components/browser_mod/light.py index d1305e3..f86bcab 100644 --- a/custom_components/browser_mod/light.py +++ b/custom_components/browser_mod/light.py @@ -1,64 +1,43 @@ -from datetime import datetime +from homeassistant.components.light import LightEntity, ColorMode -from homeassistant.const import STATE_UNAVAILABLE, STATE_ON, STATE_OFF -from homeassistant.components.light import LightEntity, SUPPORT_BRIGHTNESS - -from .helpers import setup_platform, BrowserModEntity - -PLATFORM = "light" +from .helpers import setup_platform, BrowserModEntity2 +from .const import DOMAIN, DATA_ADDERS -async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - return setup_platform(hass, config, async_add_devices, PLATFORM, BrowserModLight) - +async def async_setup_platform(hass, config_entry, async_add_entities, discoveryInfo = None): + hass.data[DOMAIN][DATA_ADDERS]["light"] = async_add_entities async def async_setup_entry(hass, config_entry, async_add_entities): await async_setup_platform(hass, {}, async_add_entities) -class BrowserModLight(LightEntity, BrowserModEntity): - domain = PLATFORM +class BrowserModLight(BrowserModEntity2, LightEntity): - def __init__(self, hass, connection, deviceID, alias=None): - super().__init__(hass, connection, deviceID, alias) - self.last_seen = None - - def updated(self): - self.last_seen = datetime.now() - self.schedule_update_ha_state() + def __init__(self, coordinator, deviceID, device): + super().__init__(coordinator, deviceID, "Screen") + self.device = device @property - def state(self): - if not self.connection.connection: - return STATE_UNAVAILABLE - if self.data.get("blackout", False): - return STATE_OFF - return STATE_ON + def entity_registry_visible_default(self): + return True @property def is_on(self): - return not self.data.get("blackout", False) + return self._data.get("screen_on", None) @property - def extra_state_attributes(self): - return { - "type": "browser_mod", - "deviceID": self.deviceID, - "last_seen": self.last_seen, - } - + def supported_color_modes(self): + return {ColorMode.BRIGHTNESS} @property - def supported_features(self): - if self.data.get("brightness", False): - return SUPPORT_BRIGHTNESS - return 0 + def color_mode(self): + return ColorMode.BRIGHTNESS @property def brightness(self): - return self.data.get("brightness", None) + return self._data.get("screen_brightness", 1) def turn_on(self, **kwargs): - self.connection.send("no-blackout", **kwargs) + self.device.send("screen_on", **kwargs) def turn_off(self, **kwargs): - self.connection.send("blackout") + self.device.send("screen_off") diff --git a/custom_components/browser_mod/manifest.json b/custom_components/browser_mod/manifest.json index 75828cb..bc60a6c 100644 --- a/custom_components/browser_mod/manifest.json +++ b/custom_components/browser_mod/manifest.json @@ -5,6 +5,7 @@ "dependencies": ["panel_custom", "websocket_api", "http", "frontend"], "codeowners": [], "requirements": [], - "version": "1.5.3", - "iot_class": "local_push" + "version": "2.0b0", + "iot_class": "local_push", + "config_flow": true } diff --git a/custom_components/browser_mod/media_player.py b/custom_components/browser_mod/media_player.py index 4d59f53..7d9695d 100644 --- a/custom_components/browser_mod/media_player.py +++ b/custom_components/browser_mod/media_player.py @@ -25,47 +25,35 @@ from homeassistant.const import ( STATE_UNKNOWN, ) -from .helpers import setup_platform, BrowserModEntity - -_LOGGER = logging.getLogger(__name__) - -PLATFORM = "media_player" +from .helpers import BrowserModEntity2 +from .const import DOMAIN, DATA_ADDERS -async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - return setup_platform(hass, config, async_add_devices, PLATFORM, BrowserModPlayer) - +async def async_setup_platform(hass, config_entry, async_add_entities, discoveryInfo = None): + hass.data[DOMAIN][DATA_ADDERS]["media_player"] = async_add_entities async def async_setup_entry(hass, config_entry, async_add_entities): await async_setup_platform(hass, {}, async_add_entities) -class BrowserModPlayer(MediaPlayerEntity, BrowserModEntity): - domain = PLATFORM +class BrowserModPlayer(BrowserModEntity2, MediaPlayerEntity): - def __init__(self, hass, connection, deviceID, alias=None): - super().__init__(hass, connection, deviceID, alias) - self.last_seen = None - - def updated(self): - self.schedule_update_ha_state() + def __init__(self, coordinator, deviceID, device): + super().__init__(coordinator, deviceID, None) + self.device = device @property - def extra_state_attributes(self): - return { - "type": "browser_mod", - "deviceID": self.deviceID, - } + def unique_id(self): + return f"{self.deviceID}-player" @property def state(self): - if not self.connection.connection: - return STATE_UNAVAILABLE - state = self.data.get("state", "unknown") + state = self._data.get("player", {}).get("state") return { "playing": STATE_PLAYING, "paused": STATE_PAUSED, "stopped": STATE_IDLE, + "unavailable": STATE_UNAVAILABLE, }.get(state, STATE_UNKNOWN) @property @@ -82,30 +70,27 @@ class BrowserModPlayer(MediaPlayerEntity, BrowserModEntity): @property def volume_level(self): - return self.data.get("volume", 0) + return self._data.get("player", {}).get("volume", 0) @property def is_volume_muted(self): - return self.data.get("muted", False) + return self._data.get("player", {}).get("muted", False) - @property - def media_content_id(self): - return self.data.get("src", "") def set_volume_level(self, volume): - self.connection.send("set_volume", volume_level=volume) + self.device.send("player-set-volume", volume_level=volume) def mute_volume(self, mute): - self.connection.send("mute", mute=mute) + self.device.send("player-mute", mute=mute) async def async_play_media(self, media_type, media_id, **kwargs): if media_source.is_media_source_id(media_id): media_type = MEDIA_TYPE_URL - play_item = await media_source.async_resolve_media(self.hass, media_id) + play_item = await media_source.async_resolve_media(self.hass, media_id, self.entity_id) media_id = play_item.url if media_type in (MEDIA_TYPE_URL, MEDIA_TYPE_MUSIC): media_id = async_process_play_media_url(self.hass, media_id) - self.connection.send("play", media_content_id=media_id) + self.device.send("player-play", media_content_id=media_id) async def async_browse_media(self, media_content_type=None, media_content_id=None): """Implement the websocket media browsing helper.""" @@ -116,10 +101,10 @@ class BrowserModPlayer(MediaPlayerEntity, BrowserModEntity): ) def media_play(self): - self.connection.send("play") + self.device.send("player-play") def media_pause(self): - self.connection.send("pause") + self.device.send("player-pause") def media_stop(self): - self.connection.send("stop") + self.device.send("player-stop") diff --git a/custom_components/browser_mod/mod_view.py b/custom_components/browser_mod/mod_view.py index 37e772a..96746d1 100644 --- a/custom_components/browser_mod/mod_view.py +++ b/custom_components/browser_mod/mod_view.py @@ -1,9 +1,15 @@ -from .const import FRONTEND_SCRIPT_URL, DATA_EXTRA_MODULE_URL, SETTINGS_PANEL_URL +from homeassistant.components.frontend import add_extra_js_url + +from .const import FRONTEND_SCRIPT_URL, SETTINGS_PANEL_URL -def setup_view(hass): - url_set = hass.data[DATA_EXTRA_MODULE_URL] - url_set.add(FRONTEND_SCRIPT_URL) +async def async_setup_view(hass): + + 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) hass.components.frontend.async_register_built_in_panel( component_name="custom", @@ -18,11 +24,6 @@ def setup_view(hass): } }, ) - - hass.http.register_static_path( - FRONTEND_SCRIPT_URL, - hass.config.path("custom_components/browser_mod/browser_mod.js"), - ) hass.http.register_static_path( SETTINGS_PANEL_URL, hass.config.path("custom_components/browser_mod/browser_mod_panel.js"), diff --git a/custom_components/browser_mod/sensor.py b/custom_components/browser_mod/sensor.py index 5e7630a..8df9f99 100644 --- a/custom_components/browser_mod/sensor.py +++ b/custom_components/browser_mod/sensor.py @@ -1,42 +1,48 @@ -from datetime import datetime +from homeassistant.components.sensor import SensorEntity -from homeassistant.const import STATE_UNAVAILABLE - -from .helpers import setup_platform, BrowserModEntity - -PLATFORM = "sensor" +from .const import DOMAIN, DATA_ADDERS +from .helpers import BrowserModEntity2 -async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - return setup_platform(hass, config, async_add_devices, PLATFORM, BrowserModSensor) - +async def async_setup_platform(hass, config_entry, async_add_entities, discoveryInfo = None): + hass.data[DOMAIN][DATA_ADDERS]["sensor"] = async_add_entities async def async_setup_entry(hass, config_entry, async_add_entities): await async_setup_platform(hass, {}, async_add_entities) -class BrowserModSensor(BrowserModEntity): - domain = PLATFORM - - def __init__(self, hass, connection, deviceID, alias=None): - super().__init__(hass, connection, deviceID, alias) - self.last_seen = None - - def updated(self): - self.last_seen = datetime.now() - self.schedule_update_ha_state() +class BrowserSensor(BrowserModEntity2, SensorEntity): + def __init__(self, coordinator, deviceID, parameter, + name, + unit_of_measurement = None, + device_class = None, + ): + super().__init__(coordinator, deviceID, name) + self.parameter = parameter + self._device_class = device_class + self._unit_of_measurement = unit_of_measurement @property - def state(self): - if not self.connection.connection: - return STATE_UNAVAILABLE - return len(self.connection.connection) + def native_value(self): + data = self._data + data = data.get("browser", {}) + data = data.get(self.parameter, None) + return data + @property + def device_class(self): + return self._device_class + @property + def native_unit_of_measurement(self): + return self._unit_of_measurement @property def extra_state_attributes(self): - return { - "type": "browser_mod", - "last_seen": self.last_seen, - "deviceID": self.deviceID, - **self.data, - } + 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") + + return retval \ No newline at end of file diff --git a/custom_components/browser_mod/store.py b/custom_components/browser_mod/store.py new file mode 100644 index 0000000..cb5f8cc --- /dev/null +++ b/custom_components/browser_mod/store.py @@ -0,0 +1,92 @@ +import logging +import attr +from dataclasses import dataclass + +from homeassistant.loader import bind_hass + +STORAGE_VERSION = 1 +STORAGE_KEY = "browser_mod.storage" + +LISTENER_STORAGE_KEY = "browser_mod.config_listeners" + +_LOGGER = logging.getLogger(__name__) + + +@attr.s +class DeviceStoreData: + last_seen = attr.ib(type=int, default=0) + enabled = attr.ib(type=bool, default=False) + camera = attr.ib(type=bool, default=False) + meta = attr.ib(type=str, default="default") + + @classmethod + def from_dict(cls, data): + return cls(**data) + + def asdict(self): + return attr.asdict(self) + +@attr.s +class ConfigStoreData: + devices = attr.ib(type=dict[str: DeviceStoreData], factory=dict) + version = attr.ib(type=str, default="2.0") + + @classmethod + def from_dict(cls, data): + devices = {k: DeviceStoreData.from_dict(v) for k,v in data["devices"].items()} + return cls(**(data | { + "devices": devices, + } + )) + + def asdict(self): + return attr.asdict(self) + +class BrowserModStore: + def __init__(self, hass): + self.store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + self.listeners = [] + self.data = None + self.dirty = False + + async def save(self): + if self.dirty: + await self.store.async_save(attr.asdict(self.data)) + self.dirty = False + + async def load(self): + self.data = ConfigStoreData.from_dict(await self.store.async_load()) + if self.data is None: + self.data = ConfigStoreData() + self.save() + self.dirty = False + + async def updated(self): + self.dirty = True + for l in self.listeners: + l(attr.asdict(self.data)) + await self.save() + + def asdict(self): + return self.data.asdict() + + def add_listener(self, callback): + self.listeners.append(callback) + + def remove_listener(): + self.listeners.remove(callback) + + return remove_listener + + def get_device(self, deviceID): + return self.data.devices.get(deviceID, DeviceStoreData()) + + async def set_device(self, deviceID, **data): + device = self.data.devices.get(deviceID, DeviceStoreData()) + device.__dict__.update(data) + self.data.devices[deviceID] = device + await self.updated() + + async def delete_device(self, deviceID): + del self.data.devices[deviceID] + await self.updated() diff --git a/js/config_panel/helpers.ts b/js/config_panel/helpers.ts new file mode 100644 index 0000000..66d8b48 --- /dev/null +++ b/js/config_panel/helpers.ts @@ -0,0 +1,16 @@ +// Loads in ha-config-dashboard which is used to copy styling +export const loadDevTools = async () => { + if (customElements.get("ha-config-dashboard")) return; + const ppResolver = document.createElement("partial-panel-resolver"); + const routes = (ppResolver as any).getRoutes([ + { + component_name: "config", + url_path: "a", + }, + ]); + await routes?.routes?.a?.load?.(); + const configRouter = document.createElement("ha-panel-config"); + await (configRouter as any)?.routerOptions?.routes?.dashboard?.load?.(); // Load ha-config-dashboard + await (configRouter as any)?.routerOptions?.routes?.cloud?.load?.(); // Load ha-settings-row + await customElements.whenDefined("ha-config-dashboard"); +}; diff --git a/js/config_panel/main.ts b/js/config_panel/main.ts index f2d03cb..21566c6 100644 --- a/js/config_panel/main.ts +++ b/js/config_panel/main.ts @@ -1,124 +1,205 @@ import { LitElement, html, css } from "lit"; -import { deviceID } from "card-tools/src/deviceID"; +import { property } from "lit/decorators.js"; +import { loadDevTools } from "./helpers"; -class BrowserModPanel extends LitElement { - hass; - narrow; - render() { - return html` - - - - -
Browser Mod Settingss
-
-
- - - -
-
-

Enable

- -
- Enable this browser as a Device in Home Assistant -
-

DeviceID

-
- - The device ID is a unique identifier for your browser/device - combination. -
-

Enable Camera

- -
- Get Camera input from this device (hardware dependent) -
-
-
- Update -
-
- - -
-
- - -
-
-

Cool function

- -
- Enabling this will cause cool stuff to happen. -
-

Another function

- -
- Enabling this will cause less cool stuff to happen. -
-
-
-
- `; - } - - static get styles() { - return [ - ...(customElements.get("ha-config-dashboard") as any).styles, - css` - :host { - --app-header-background-color: var(--sidebar-background-color); - --app-header-text-color: var(--sidebar-text-color); - --app-header-border-bottom: 1px solid var(--divider-color); - } - .card-actions { - display: flex; - } - .spacer { - flex-grow: 1; - } - ha-textfield { - width: 250px; - display: block; - margin-top: 8px; - } - .option { - display: flex; - margin-top: 16px; - } - .option h3 { - flex-grow: 1; - margin: 0; - } - .option ha-switch { - margin-top: 0.25em; - margin-right: 7px; - margin-left: 0.5em; - } - `, - ]; - } -} - -const loadDevTools = async () => { - if (customElements.get("ha-config-dashboard")) return; - const ppResolver = document.createElement("partial-panel-resolver"); - const routes = (ppResolver as any).getRoutes([ - { - component_name: "config", - url_path: "a", - }, - ]); - await routes?.routes?.a?.load?.(); - const configRouter = document.createElement("ha-panel-config"); - await (configRouter as any)?.routerOptions?.routes?.dashboard?.load?.(); - await customElements.whenDefined("ha-config-dashboard"); -}; +const bmWindow = window as any; loadDevTools().then(() => { + class BrowserModPanel extends LitElement { + @property() hass; + @property() narrow; + @property() connection; + + toggleRegister() { + if (!window.browser_mod?.connected) return; + window.browser_mod.registered = !window.browser_mod.registered; + } + changeDeviceID(ev) { + window.browser_mod.deviceID = ev.target.value; + } + + unregister_device(ev) { + const deviceID = ev.currentTarget.deviceID; + if (deviceID === window.browser_mod.deviceID) + window.browser_mod.registered = false; + else + window.browser_mod.connection.sendMessage({ + type: "browser_mod/unregister", + deviceID, + }); + } + + firstUpdated() { + window.browser_mod.addEventListener("browser-mod-config-update", () => + this.requestUpdate() + ); + } + + render() { + return html` + + + + +
Browser Mod Settings
+
+
+ + + +

+
This Browser
+ ${bmWindow.browser_mod?.connected + ? html` + + ` + : html` + + `} +

+
Browser-mod not connected.
+
+ + Enable + Enable this browser as a Device in Home Assistant + + + + + DeviceID + A unique identifier for this browser-device + combination. + + + + + Enable camera + Get camera input from this device (hardware + dependent) + + +
+
+ + +
+ ${Object.keys(window.browser_mod.devices).map( + (d) => html` + ${d} + + Last connected: + + + + + + + + + ` + )} +
+
+ + +
+ + Auto enable devices + + + + User sidebar + Save sidebar as default for current user + (${this.hass.user.name}) + Save + + + Global sidebar + Save sidebar as default for all users + Save + +
+
+
+
+ `; + } + + static get styles() { + return [ + ...((customElements.get("ha-config-dashboard") as any)?.styles ?? []), + css` + :host { + --app-header-background-color: var(--sidebar-background-color); + --app-header-text-color: var(--sidebar-text-color); + --app-header-border-bottom: 1px solid var(--divider-color); + --ha-card-border-radius: var(--ha-config-card-border-radius, 8px); + } + .card-header { + display: flex; + justify-content: space-between; + } + .card-actions { + display: flex; + } + .spacer { + flex-grow: 1; + } + ha-textfield { + width: 250px; + display: block; + margin-top: 8px; + } + .option { + display: flex; + margin-top: 16px; + } + .option h3 { + flex-grow: 1; + margin: 0; + } + .option ha-switch { + margin-top: 0.25em; + margin-right: 7px; + margin-left: 0.5em; + } + ha-icon-button > * { + display: flex; + } + `, + ]; + } + } + customElements.define("browser-mod-panel", BrowserModPanel); }); diff --git a/js/helpers.ts b/js/helpers.ts new file mode 100644 index 0000000..cca423f --- /dev/null +++ b/js/helpers.ts @@ -0,0 +1,63 @@ +const TIMEOUT_ERROR = "SLECTTREE-TIMEOUT"; + +async function _await_el(el) { + if (el.localName?.includes("-")) + await customElements.whenDefined(el.localName); + if (el.updateComplete) await el.updateComplete; +} + +async function _selectTree(root, path, all = false) { + let el = [root]; + if (typeof path === "string") { + path = path.split(/(\$| )/); + } + while (path[path.length - 1] === "") path.pop(); + for (const [i, p] of path.entries()) { + const e = el[0]; + if (!e) return null; + + if (!p.trim().length) continue; + + _await_el(e); + el = p === "$" ? [e.shadowRoot] : e.querySelectorAll(p); + } + return all ? el : el[0]; +} + +export async function selectTree(root, path, all = false, timeout = 10000) { + return Promise.race([ + _selectTree(root, path, all), + new Promise((_, reject) => + setTimeout(() => reject(new Error(TIMEOUT_ERROR)), timeout) + ), + ]).catch((err) => { + if (!err.message || err.message !== TIMEOUT_ERROR) throw err; + return null; + }); +} + +async function _hass_base_el() { + await Promise.race([ + customElements.whenDefined("home-assistant"), + customElements.whenDefined("hc-main"), + ]); + + const element = customElements.get("home-assistant") + ? "home-assistant" + : "hc-main"; + + while (!document.querySelector(element)) + await new Promise((r) => window.setTimeout(r, 100)); + return document.querySelector(element); +} + +export async function hass() { + const base: any = await _hass_base_el(); + while (!base.hass) await new Promise((r) => window.setTimeout(r, 100)); + return base.hass; +} + +export async function provideHass(el) { + const base: any = await _hass_base_el(); + base.provideHass(el); +} diff --git a/js/plugin/browser.ts b/js/plugin/browser.ts index 1614b73..e2d9d2f 100644 --- a/js/plugin/browser.ts +++ b/js/plugin/browser.ts @@ -8,7 +8,10 @@ export const BrowserModBrowserMixin = (C) => document.addEventListener("visibilitychange", () => this.sensor_update()); window.addEventListener("location-changed", () => this.sensor_update()); - window.setInterval(() => this.sensor_update(), 10000); + this.addEventListener("browser-mod-connected", () => + this.sensor_update() + ); + // window.setInterval(() => this.sensor_update(), 10000); } sensor_update() { @@ -28,7 +31,7 @@ export const BrowserModBrowserMixin = (C) => charging: window.fully?.isPlugged() ?? battery?.charging, darkMode: this.hass?.themes?.darkMode, userData: this.hass?.user, - config: this.config, + // config: this.config, }, }); }; diff --git a/js/plugin/connection.ts b/js/plugin/connection.ts index 1a487d4..58ce150 100644 --- a/js/plugin/connection.ts +++ b/js/plugin/connection.ts @@ -1,42 +1,175 @@ -import { deviceID } from "card-tools/src/deviceID"; -import { hass, provideHass } from "card-tools/src/hass"; +import { hass, provideHass } from "../helpers"; -export class BrowserModConnection { - hass; - connection; +const ID_STORAGE_KEY = "browser_mod-device-id"; - async connect() { - const isCast = document.querySelector("hc-main") !== null; - if (!isCast) { - while (!window.hassConnection) - await new Promise((resolve) => window.setTimeout(resolve, 100)); - this.connection = (await window.hassConnection).conn; - } else { - this.connection = hass().connection; +export const ConnectionMixin = (SuperClass) => { + class BrowserModConnection extends SuperClass { + public hass; + public connection; + private _data; + public connected = false; + private _connectionResolve; + public connectionPromise = new Promise(resolve => { this._connectionResolve = resolve; }); + + LOG(...args) { + const dt = new Date(); + console.log(`${dt.toLocaleTimeString()}`, ...args); } - this.connection.subscribeMessage((msg) => this.msg_callback(msg), { - type: "browser_mod/connect", - deviceID: deviceID, - }); + private fireEvent(event, detail = undefined) { + this.dispatchEvent(new CustomEvent(event, { detail })); + } - provideHass(this); + private incoming_message(msg) { + if (msg.command) { + this.LOG("Command:", msg); + this.fireEvent(`command-${msg.command}`, msg) + } else if (msg.result) { + this.update_config(msg.result); + } + this._connectionResolve?.(); + } + + private update_config(cfg) { + this.LOG("Receive:", cfg); + + let update = false; + if (!this.registered && cfg.devices?.[this.deviceID]) { + update = true; + } + this._data = cfg; + + if (!this.connected) { + this.connected = true; + this.fireEvent("browser-mod-connected"); + } + + this.fireEvent("browser-mod-config-update"); + + if (update) this.sendUpdate({}); + } + + async connect() { + const conn = (await hass()).connection; + this.connection = conn; + + // Subscribe to configuration updates + conn.subscribeMessage((msg) => this.incoming_message(msg), { + type: "browser_mod/connect", + deviceID: this.deviceID, + }); + + // Keep connection status up to date + conn.addEventListener("disconnected", () => { + this.connected = false; + this.fireEvent("browser-mod-disconnected"); + }); + conn.addEventListener("ready", () => { + this.connected = true; + this.fireEvent("browser-mod-connected"); + this.sendUpdate({}); + }); + + provideHass(this); + } + + get config() { + return this._data?.config ?? {}; + } + + get devices() { + return this._data?.devices ?? []; + } + + get registered() { + return this.devices?.[this.deviceID] !== undefined; + } + + set registered(reg) { + (async () => { + if (reg) { + if (this.registered) return; + await this.connection.sendMessage({ + type: "browser_mod/register", + deviceID: this.deviceID, + }); + } else { + if (!this.registered) return; + await this.connection.sendMessage({ + type: "browser_mod/unregister", + deviceID: this.deviceID, + }); + } + })(); + } + + get meta() { + if (!this.registered) return null; + return this.devices[this.deviceID].meta; + } + set meta(value) { + (async () => { + await this.connection.sendMessage({ + type: "browser_mod/reregister", + deviceID: this.deviceID, + data: { + ...this.devices[this.deviceID], + meta: value, + } + }) + })() + } + + sendUpdate(data) { + if (!this.connected || !this.registered) return; + + const dt = new Date(); + this.LOG("Send:", data); + + this.connection.sendMessage({ + type: "browser_mod/update", + deviceID: this.deviceID, + data, + }); + } + + get deviceID() { + if (localStorage[ID_STORAGE_KEY]) return localStorage[ID_STORAGE_KEY]; + this.deviceID = ""; + return this.deviceID; + } + set deviceID(id) { + function _createDeviceID() { + const s4 = () => { + return Math.floor((1 + Math.random()) * 100000) + .toString(16) + .substring(1); + }; + return window.fully?.getDeviceId() ?? `${s4()}${s4()}-${s4()}${s4()}`; + } + + if (id === "") id = _createDeviceID(); + const oldID = localStorage[ID_STORAGE_KEY]; + localStorage[ID_STORAGE_KEY] = id; + + this.fireEvent("browser-mod-config-update"); + + if (this.devices?.[oldID] !== undefined && this.devices?.[this.deviceID] === undefined) { + (async () => { + await this.connection.sendMessage({ + type: "browser_mod/reregister", + deviceID: oldID, + data: { + ...this.devices[oldID], + deviceID: this.deviceID, + } + }) + })() + } + + // TODO: Send update to backend to update device + } } - get connected() { - return this.connection !== undefined; - } - - msg_callback(message) { - console.log(message); - } - - sendUpdate(data) { - if (!this.connected) return; - this.connection.sendMessage({ - type: "browser_mod/update", - deviceID, - data, - }); - } + return BrowserModConnection; } diff --git a/js/plugin/main.ts b/js/plugin/main.ts index cda68a4..d720734 100644 --- a/js/plugin/main.ts +++ b/js/plugin/main.ts @@ -5,8 +5,10 @@ import { fireEvent } from "card-tools/src/event"; import { ha_element, hass_loaded } from "card-tools/src/hass"; import "./browser-player"; -import { BrowserModConnection } from "./connection"; -import { BrowserModMediaPlayerMixin } from "./mediaPlayer"; +// import { BrowserModConnection } from "./connection"; +import { ConnectionMixin } from "./connection"; +import { ScreenSaverMixin } from "./screensaver"; +import { MediaPlayerMixin } from "./mediaPlayer"; import { FullyKioskMixin } from "./fullyKiosk"; import { BrowserModCameraMixin } from "./camera"; import { BrowserModScreensaverMixin } from "./screensaver"; @@ -17,14 +19,15 @@ import pjson from "../../package.json"; const ext = (baseClass, mixins) => mixins.reduceRight((base, mixin) => mixin(base), baseClass); -export class BrowserMod extends ext(BrowserModConnection, [ - BrowserModBrowserMixin, - BrowserModPopupsMixin, - BrowserModScreensaverMixin, - BrowserModCameraMixin, - FullyKioskMixin, - BrowserModMediaPlayerMixin, -]) { +// export class BrowserMod extends ext(BrowserModConnection, [ +// BrowserModBrowserMixin, +// BrowserModPopupsMixin, +// BrowserModScreensaverMixin, +// BrowserModCameraMixin, +// FullyKioskMixin, +// BrowserModMediaPlayerMixin, +// ]) { +export class BrowserMod extends MediaPlayerMixin(ScreenSaverMixin(ConnectionMixin(EventTarget))) { constructor() { super(); this.entity_id = deviceID.replace("-", "_"); @@ -121,21 +124,21 @@ export class BrowserMod extends ext(BrowserModConnection, [ this.hass.callService(domain, service, service_data); } - update(msg = null) { - if (msg) { - if (msg.name) { - this.entity_id = msg.name.toLowerCase(); - } - if (msg.camera && !this.isFully) { - this.setup_camera(); - } - this.config = { ...this.config, ...msg }; - } - this.player_update(); - this.fully_update(); - this.screen_update(); - this.sensor_update(); - } + // update(msg = null) { + // if (msg) { + // if (msg.name) { + // this.entity_id = msg.name.toLowerCase(); + // } + // if (msg.camera && !this.isFully) { + // this.setup_camera(); + // } + // this.config = { ...this.config, ...msg }; + // } + // this.player_update(); + // this.fully_update(); + // this.screen_update(); + // this.sensor_update(); + // } } (async () => { diff --git a/js/plugin/mediaPlayer.ts b/js/plugin/mediaPlayer.ts index 8b0f0e2..efd1275 100644 --- a/js/plugin/mediaPlayer.ts +++ b/js/plugin/mediaPlayer.ts @@ -1,59 +1,71 @@ -export const BrowserModMediaPlayerMixin = (C) => - class extends C { +export const MediaPlayerMixin = (SuperClass) => { + return class MediaPlayerMixinClass extends SuperClass { + + public player; + private _player_enabled; + constructor() { super(); - this.player = new Audio(); - for (const event of ["play", "pause", "ended", "volumechange"]) { - this.player.addEventListener(event, () => this.player_update()); + this.player = new Audio(); + this._player_enabled = false; + + for (const ev of ["play", "pause", "ended", "volumechange"]) { + this.player.addEventListener(ev, () => this._player_update()); } - window.addEventListener( - "click", - () => { + window.addEventListener("pointerdown", () => { + this._player_enabled = true; if (!this.player.ended) this.player.play(); }, - { - once: true, - } + { once: true } ); + + this.addEventListener("command-player-play", (ev) => { + if (ev.detail?.media_content_id) + this.player.src = ev.detail.media_content_id; + this.player.play(); + }); + this.addEventListener("command-player-pause", (ev) => this.player.pause()); + this.addEventListener("command-player-stop", (ev) => { + this.player.src = null; + this.player.pause(); + }); + this.addEventListener("command-player-set-volume", (ev) => { + if (ev.detail?.volume_level === undefined) return; + this.player.volume = ev.detail.volume_level; + }); + this.addEventListener("command-player-mute", (ev) => { + if (ev.detail?.mute !== undefined) + this.player.muted = Boolean(ev.detail.mute); + else + this.player.muted = !this.player.muted; + }); + + this.connectionPromise.then(() => this._player_update()); } - player_update(ev?) { + private _player_update() { + const state = + this._player_enabled + ? this.player.src + ? this.player.ended + ? "stopped" + : this.player.paused + ? "paused" + : "playing" + : "stopped" + : "unavailable" + ; this.sendUpdate({ player: { volume: this.player.volume, muted: this.player.muted, src: this.player.src, - state: this.player_state, - }, - }); - } + state, + } + }) - get player_state() { - if (!this.player.src) return "stopped"; - if (this.player.ended) return "stopped"; - if (this.player.paused) return "paused"; - return "playing"; } - - player_play(src) { - if (src) this.player.src = src; - this.player.play(); - } - player_pause() { - this.player.pause(); - } - player_stop() { - this.player.pause(); - this.player.src = null; - } - player_set_volume(level) { - if (level === undefined) return; - this.player.volume = level; - } - player_mute(mute) { - if (mute === undefined) mute = !this.player.muted; - this.player.muted = Boolean(mute); - } - }; + } +} \ No newline at end of file diff --git a/js/plugin/screensaver.ts b/js/plugin/screensaver.ts index a1b0678..8e132f7 100644 --- a/js/plugin/screensaver.ts +++ b/js/plugin/screensaver.ts @@ -1,3 +1,82 @@ +export const ScreenSaverMixin = (SuperClass) => { + class ScreenSaverMixinClass extends SuperClass { + + private _panel; + private _listeners = {}; + private _brightness = 255; + + constructor() { + super(); + + const panel = this._panel = document.createElement("div") + panel.setAttribute("browser-mod", ""); + panel.attachShadow({ mode: "open" }); + const styleEl = document.createElement("style"); + styleEl.innerHTML = ` + :host { + background: rgba(0,0,0, var(--darkness)); + position: fixed; + left: 0; + top: 0; + bottom: 0; + right: 0; + width: 100%; + height: 100%; + z-index: 10000; + display: block; + pointer-events: none; + } + :host([dark]) { + background: rgba(0,0,0,1); + } + ` + panel.shadowRoot.appendChild(styleEl); + document.body.appendChild(panel); + + this.addEventListener("command-screen_off", () => this._screen_off()); + this.addEventListener("command-screen_on", (ev) => this._screen_on(ev)); + + this.connectionPromise.then(() => this._screen_on()); + } + + private _screen_off() { + this._panel.setAttribute("dark", ""); + + this.sendUpdate({ + screen_on: false, + screen_brightness: 0, + }); + + const l = () => this._screen_on(); + for (const ev of ["pointerdown", "pointermove", "keydown"]) { + this._listeners[ev] = l; + window.addEventListener(ev, l); + } + } + + private _screen_on(ev=undefined) { + if (ev?.detail?.brightness) { + this._brightness = ev.detail.brightness; + this._panel.style.setProperty("--darkness", 1-ev.detail.brightness/255) + } + this._panel.removeAttribute("dark"); + this.sendUpdate({ + screen_on: true, + screen_brightness: this._brightness, + }); + + for (const ev of ["pointerdown", "pointermove", "keydown"]) { + if (this._listeners[ev]) { + window.removeEventListener(ev, this._listeners[ev]) + this._listeners[ev] = undefined; + } + } + } + + } + return ScreenSaverMixinClass +} + export const BrowserModScreensaverMixin = (C) => class extends C { constructor() { diff --git a/package.json b/package.json index ea085ac..0c5b1e4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "browser_mod", "private": true, - "version": "1.5.3", + "version": "2.0.0b0", "description": "", "scripts": { "build": "rollup -c", @@ -25,4 +25,4 @@ "dependencies": { "card-tools": "github:thomasloven/lovelace-card-tools" } -} +} \ No newline at end of file diff --git a/test/configuration.yaml b/test/configuration.yaml index 8a746bd..5b857e9 100644 --- a/test/configuration.yaml +++ b/test/configuration.yaml @@ -2,16 +2,21 @@ default_config: demo: -browser_mod: - devices: - camdevice: - camera: true - testdevice: - alias: test - fully: - force_stay_awake: true - fully2: - screensaver: true +logger: + default: warning + logs: + custom_components.browser_mod: info + +# browser_mod: +# devices: +# camdevice: +# camera: true +# testdevice: +# alias: test +# fully: +# force_stay_awake: true +# fully2: +# screensaver: true lovelace: mode: storage