diff --git a/custom_components/browser_mod/browser_mod.js b/custom_components/browser_mod/browser_mod.js index eb2c69c..65d1bc2 100644 --- a/custom_components/browser_mod/browser_mod.js +++ b/custom_components/browser_mod/browser_mod.js @@ -937,6 +937,8 @@ const MediaPlayerMixin = (SuperClass) => { const CameraMixin = (SuperClass) => { return class CameraMixinClass extends SuperClass { + // TODO: Enable WebRTC? + // https://levelup.gitconnected.com/establishing-the-webrtc-connection-videochat-with-javascript-step-3-48d4ae0e9ea4 constructor() { super(); this._framerate = 2; @@ -1041,6 +1043,46 @@ const RequireInteractMixin = (SuperClass) => { }; }; +const BrowserStateMixin = (SuperClass) => { + return class BrowserStateMixinClass extends SuperClass { + constructor() { + super(); + document.addEventListener("visibilitychange", () => this._browser_state_update()); + window.addEventListener("location-changed", () => this._browser_state_update()); + this.connectionPromise.then(() => this._browser_state_update()); + } + _browser_state_update() { + const update = async () => { + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m; + 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 || false, + 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, + ip_address: (_m = window.fully) === null || _m === void 0 ? void 0 : _m.getIp4Address(), + }, + }); + }; + update(); + } + do_navigate(path) { + if (!path) + return; + history.pushState(null, "", path); + fireEvent("location-changed", {}, ha_element()); + } + }; +}; + var name = "browser_mod"; var version = "2.0.0b0"; var description = ""; @@ -1088,7 +1130,7 @@ var pjson = { // FullyKioskMixin, // BrowserModMediaPlayerMixin, // ]) { -class BrowserMod extends CameraMixin(MediaPlayerMixin(ScreenSaverMixin(RequireInteractMixin(ConnectionMixin(EventTarget))))) { +class BrowserMod extends BrowserStateMixin(CameraMixin(MediaPlayerMixin(ScreenSaverMixin(RequireInteractMixin(ConnectionMixin(EventTarget)))))) { constructor() { super(); this.entity_id = deviceID.replace("-", "_"); diff --git a/custom_components/browser_mod/camera.py b/custom_components/browser_mod/camera.py index 9028e8f..3a24f6e 100644 --- a/custom_components/browser_mod/camera.py +++ b/custom_components/browser_mod/camera.py @@ -35,6 +35,5 @@ class BrowserModCamera(BrowserModEntity, Camera): def camera_image(self, width=None, height=None): if "camera" not in self._data: - LOGGER.error(self._data) return None return base64.b64decode(self._data["camera"].split(",")[-1]) diff --git a/custom_components/browser_mod/connection.py b/custom_components/browser_mod/connection.py index dc3c790..fa3df1f 100644 --- a/custom_components/browser_mod/connection.py +++ b/custom_components/browser_mod/connection.py @@ -128,8 +128,7 @@ async def async_setup_connection(hass): if store.get_device(deviceID).enabled: dev = getDevice(hass, deviceID) - dev.data.update(msg.get("data", {})) - dev.coordinator.async_set_updated_data(dev.data) + dev.update(hass, msg.get("data", {})) async_register_command(hass, handle_connect) async_register_command(hass, handle_register) diff --git a/custom_components/browser_mod/device.py b/custom_components/browser_mod/device.py index 7a32e43..50840e8 100644 --- a/custom_components/browser_mod/device.py +++ b/custom_components/browser_mod/device.py @@ -21,77 +21,74 @@ class BrowserModDevice: """ """ self.deviceID = deviceID self.coordinator = Coordinator(hass, deviceID) - self.entities = [] - self.camera_entity = None + self.entities = {} self.data = {} - self.setup_sensors(hass) + self.settings = {} self.connection = None - def update_settings(self, hass, settings): - if settings.get("camera", False) and self.camera_entity is None: - self.add_camera(hass) - elif self.camera_entity and not settings.get("camera", False): - self.remove_camera(hass) + self.update_entities(hass) - def setup_sensors(self, hass): + def update(self, hass, newData): + self.data.update(newData) + self.update_entities(hass) + self.coordinator.async_set_updated_data(self.data) + + def update_settings(self, hass, settings): + self.settings = settings + self.update_entities(hass) + + def update_entities(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 + def _assert_browser_sensor(type, name, *properties): + if name in self.entities: + return + adder = hass.data[DOMAIN][DATA_ADDERS][type] + cls = {"sensor": BrowserSensor, "binary_sensor": BrowserBinarySensor}[type] + new = cls(coordinator, deviceID, name, *properties) + adder([new]) + self.entities[name] = 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 + _assert_browser_sensor("sensor", "path", "Browser path") + _assert_browser_sensor("sensor", "visibility", "Browser visibility") + _assert_browser_sensor("sensor", "userAgent", "Browser userAgent") + _assert_browser_sensor("sensor", "currentUser", "Browser user") + _assert_browser_sensor("sensor", "width", "Browser width", "px") + _assert_browser_sensor("sensor", "height", "Browser height", "px") + if self.data.get("browser", {}).get("battery_level", None) is not None: + _assert_browser_sensor( + "sensor", "battery_level", "Browser battery", "%", "battery" + ) - adder = hass.data[DOMAIN][DATA_ADDERS]["light"] - new = [BrowserModLight(coordinator, deviceID, self)] - adder(new) - self.entities += new + _assert_browser_sensor("binary_sensor", "darkMode", "Browser dark mode") + _assert_browser_sensor("binary_sensor", "fullyKiosk", "Browser FullyKiosk") + if self.data.get("browser", {}).get("charging", None) is not None: + _assert_browser_sensor("binary_sensor", "charging", "Browser charging") - adder = hass.data[DOMAIN][DATA_ADDERS]["media_player"] - new = [BrowserModPlayer(coordinator, deviceID, self)] - adder(new) - self.entities += new + if "screen" not in self.entities: + adder = hass.data[DOMAIN][DATA_ADDERS]["light"] + new = BrowserModLight(coordinator, deviceID, self) + adder([new]) + self.entities["screen"] = new - def add_camera(self, hass): - if self.camera_entity is not None: - return - coordinator = self.coordinator - deviceID = self.deviceID - adder = hass.data[DOMAIN][DATA_ADDERS]["camera"] - self.camera_entity = BrowserModCamera(coordinator, deviceID) - adder([self.camera_entity]) - self.entities.append(self.camera_entity) - pass + if "player" not in self.entities: + adder = hass.data[DOMAIN][DATA_ADDERS]["media_player"] + new = BrowserModPlayer(coordinator, deviceID, self) + adder([new]) + self.entities["player"] = new - def remove_camera(self, hass): - if self.camera_entity is None: - return - er = entity_registry.async_get(hass) - er.async_remove(self.camera_entity.entity_id) - self.entities.remove(self.camera_entity) - self.camera_entity = None - pass + if "camera" not in self.entities and self.settings.get("camera"): + adder = hass.data[DOMAIN][DATA_ADDERS]["camera"] + new = BrowserModCamera(coordinator, deviceID) + adder([new]) + self.entities["camera"] = new + if "camera" in self.entities and not self.settings.get("camera"): + er = entity_registry.async_get(hass) + er.async_remove(self.entities["camera"].entity_id) + del self.entities["camera"] def send(self, command, **kwargs): """Send a command to this device.""" @@ -115,11 +112,10 @@ class BrowserModDevice: dr = device_registry.async_get(hass) er = entity_registry.async_get(hass) - for e in self.entities: + for e in self.entities.items(): er.async_remove(e.entity_id) - self.entities = [] - self.camera_entity = None + self.entities = {} device = dr.async_get_device({(DOMAIN, self.deviceID)}) dr.async_remove_device(device.id) diff --git a/custom_components/browser_mod/helpers.py b/custom_components/browser_mod/helpers.py index 666ce00..579125e 100644 --- a/custom_components/browser_mod/helpers.py +++ b/custom_components/browser_mod/helpers.py @@ -22,10 +22,14 @@ class BrowserModEntity(CoordinatorEntity): @property def device_info(self): + config_url = {} + if ip := self._data.get("browser", {}).get("ip_address"): + config_url = {"configuration_url": f"http://{ip}:2323"} return { "identifiers": {(DOMAIN, self.deviceID)}, "name": self.deviceID, "manufacturer": "Browser Mod", + **config_url, } @property diff --git a/js/plugin/browser.ts b/js/plugin/browser.ts index e2d9d2f..178c01d 100644 --- a/js/plugin/browser.ts +++ b/js/plugin/browser.ts @@ -1,20 +1,21 @@ import { fireEvent } from "card-tools/src/event"; import { ha_element } from "card-tools/src/hass"; -export const BrowserModBrowserMixin = (C) => - class extends C { +export const BrowserStateMixin = (SuperClass) => { + return class BrowserStateMixinClass extends SuperClass { constructor() { super(); - document.addEventListener("visibilitychange", () => this.sensor_update()); - window.addEventListener("location-changed", () => this.sensor_update()); - - this.addEventListener("browser-mod-connected", () => - this.sensor_update() + document.addEventListener("visibilitychange", () => + this._browser_state_update() ); - // window.setInterval(() => this.sensor_update(), 10000); + window.addEventListener("location-changed", () => + this._browser_state_update() + ); + + this.connectionPromise.then(() => this._browser_state_update()); } - sensor_update() { + _browser_state_update() { const update = async () => { const battery = (navigator).getBattery?.(); this.sendUpdate({ @@ -23,7 +24,7 @@ export const BrowserModBrowserMixin = (C) => visibility: document.visibilityState, userAgent: navigator.userAgent, currentUser: this.hass?.user?.name, - fullyKiosk: this.isFully, + fullyKiosk: this.isFully || false, width: window.innerWidth, height: window.innerHeight, battery_level: @@ -31,7 +32,7 @@ export const BrowserModBrowserMixin = (C) => charging: window.fully?.isPlugged() ?? battery?.charging, darkMode: this.hass?.themes?.darkMode, userData: this.hass?.user, - // config: this.config, + ip_address: window.fully?.getIp4Address(), }, }); }; @@ -44,3 +45,4 @@ export const BrowserModBrowserMixin = (C) => fireEvent("location-changed", {}, ha_element()); } }; +}; diff --git a/js/plugin/camera.ts b/js/plugin/camera.ts index aa14fe3..867f479 100644 --- a/js/plugin/camera.ts +++ b/js/plugin/camera.ts @@ -4,6 +4,9 @@ export const CameraMixin = (SuperClass) => { private _canvas; private _framerate; + // TODO: Enable WebRTC? + // https://levelup.gitconnected.com/establishing-the-webrtc-connection-videochat-with-javascript-step-3-48d4ae0e9ea4 + constructor() { super(); this._framerate = 2; diff --git a/js/plugin/main.ts b/js/plugin/main.ts index 30ac223..38b7156 100644 --- a/js/plugin/main.ts +++ b/js/plugin/main.ts @@ -14,7 +14,7 @@ import { RequireInteractMixin } from "./require-interact"; import { FullyKioskMixin } from "./fullyKiosk"; import { BrowserModScreensaverMixin } from "./screensaver"; import { BrowserModPopupsMixin } from "./popups"; -import { BrowserModBrowserMixin } from "./browser"; +import { BrowserStateMixin } from "./browser"; import pjson from "../../package.json"; const ext = (baseClass, mixins) => @@ -28,9 +28,11 @@ const ext = (baseClass, mixins) => // FullyKioskMixin, // BrowserModMediaPlayerMixin, // ]) { -export class BrowserMod extends CameraMixin( - MediaPlayerMixin( - ScreenSaverMixin(RequireInteractMixin(ConnectionMixin(EventTarget))) +export class BrowserMod extends BrowserStateMixin( + CameraMixin( + MediaPlayerMixin( + ScreenSaverMixin(RequireInteractMixin(ConnectionMixin(EventTarget))) + ) ) ) { constructor() {