Lots of changes and modernization. WIP

This commit is contained in:
Thomas Lovén 2022-07-13 21:02:47 +00:00
parent 69e9642b4b
commit 466a5eb5e7
26 changed files with 1691 additions and 1185 deletions

View File

@ -1,73 +1,45 @@
import logging import logging
from homeassistant import config_entries from .store import BrowserModStore
from .mod_view import async_setup_view
from .mod_view import setup_view from .connection import async_setup_connection
from .connection import setup_connection
from .service import setup_service
from .const import ( from .const import (
DOMAIN, DOMAIN,
DATA_DEVICES, DATA_DEVICES,
DATA_ALIASES,
DATA_ADDERS, DATA_ADDERS,
CONFIG_DEVICES, DATA_STORE
DATA_CONFIG,
DATA_SETUP_COMPLETE,
) )
COMPONENTS = [ from .coordinator import Coordinator
"media_player",
"sensor",
"binary_sensor",
"light",
"camera",
]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_setup(hass, config): async def async_setup(hass, config):
if not hass.config_entries.async_entries(DOMAIN): store = BrowserModStore(hass)
hass.async_create_task( await store.load()
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("_", "-")
hass.data[DOMAIN] = { hass.data[DOMAIN] = {
DATA_DEVICES: {}, DATA_DEVICES: {},
DATA_ALIASES: aliases,
DATA_ADDERS: {}, DATA_ADDERS: {},
DATA_CONFIG: config[DOMAIN], DATA_STORE: store,
DATA_SETUP_COMPLETE: False,
} }
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 return True
async def async_setup_entry(hass, config_entry): 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: for component in COMPONENTS:
hass.async_create_task( hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, component) hass.config_entries.async_forward_entry_setup(config_entry, component)

View File

@ -1,60 +1,25 @@
from datetime import datetime from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.const import ( from .const import DOMAIN, DATA_ADDERS
STATE_UNAVAILABLE, from .helpers import BrowserModEntity2
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"
async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): async def async_setup_platform(hass, config_entry, async_add_entities, discoveryInfo = None):
return setup_platform(hass, config, async_add_devices, PLATFORM, BrowserModSensor) hass.data[DOMAIN][DATA_ADDERS]["binary_sensor"] = async_add_entities
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
await async_setup_platform(hass, {}, async_add_entities) await async_setup_platform(hass, {}, async_add_entities)
class BrowserModSensor(BrowserModEntity): class BrowserBinarySensor(BrowserModEntity2, BinarySensorEntity):
domain = PLATFORM
def __init__(self, hass, connection, deviceID, alias=None): def __init__(self, coordinator, deviceID, parameter, name):
super().__init__(hass, connection, deviceID, alias) super().__init__(coordinator, deviceID, name)
self.last_seen = None self.parameter = parameter
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
@property @property
def is_on(self): def is_on(self):
return not self.data.get("motion", False) data = self._data
data = data.get("browser", {})
@property data = data.get(self.parameter, None)
def device_class(self): return data
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,
}

View File

@ -1,25 +1,25 @@
const ID_STORAGE_KEY = 'lovelace-player-device-id'; const ID_STORAGE_KEY$1 = 'lovelace-player-device-id';
function _deviceID() { function _deviceID() {
if(!localStorage[ID_STORAGE_KEY]) if(!localStorage[ID_STORAGE_KEY$1])
{ {
const s4 = () => { const s4 = () => {
return Math.floor((1+Math.random())*100000).toString(16).substring(1); return Math.floor((1+Math.random())*100000).toString(16).substring(1);
}; };
if(window['fully'] && typeof fully.getDeviceId === "function") if(window['fully'] && typeof fully.getDeviceId === "function")
localStorage[ID_STORAGE_KEY] = fully.getDeviceId(); localStorage[ID_STORAGE_KEY$1] = fully.getDeviceId();
else 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(); let deviceID = _deviceID();
const setDeviceID = (id) => { const setDeviceID = (id) => {
if(id === null) return; if(id === null) return;
if(id === "clear") { if(id === "clear") {
localStorage.removeItem(ID_STORAGE_KEY); localStorage.removeItem(ID_STORAGE_KEY$1);
} else { } else {
localStorage[ID_STORAGE_KEY] = id; localStorage[ID_STORAGE_KEY$1] = id;
} }
deviceID = _deviceID(); deviceID = _deviceID();
}; };
@ -44,7 +44,7 @@ async function hass_loaded() {
return true; return true;
} }
function hass() { function hass$1() {
if(document.querySelector('hc-main')) if(document.querySelector('hc-main'))
return document.querySelector('hc-main').hass; return document.querySelector('hc-main').hass;
@ -53,7 +53,7 @@ function hass() {
return undefined; return undefined;
} }
function provideHass(element) { function provideHass$1(element) {
if(document.querySelector('hc-main')) if(document.querySelector('hc-main'))
return document.querySelector('hc-main').provideHass(element); return document.querySelector('hc-main').provideHass(element);
@ -62,32 +62,6 @@ function provideHass(element) {
return undefined; 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() { function lovelace_view() {
var root = document.querySelector("hc-main"); var root = document.querySelector("hc-main");
if(root) { if(root) {
@ -127,14 +101,14 @@ async function load_lovelace() {
await ppr.routerOptions.routes.tmp.load(); await ppr.routerOptions.routes.tmp.load();
if(!customElements.get("ha-panel-lovelace")) return false; if(!customElements.get("ha-panel-lovelace")) return false;
const p = document.createElement("ha-panel-lovelace"); const p = document.createElement("ha-panel-lovelace");
p.hass = hass(); p.hass = hass$1();
if(p.hass === undefined) { if(p.hass === undefined) {
await new Promise(resolve => { await new Promise(resolve => {
window.addEventListener('connection-status', (ev) => { window.addEventListener('connection-status', (ev) => {
resolve(); resolve();
}, {once: true}); }, {once: true});
}); });
p.hass = hass(); p.hass = hass$1();
} }
p.panel = {config: {mode: null}}; p.panel = {config: {mode: null}};
p._fetchConfig(); 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) { async function popUp(title, card, large=false, style={}, fullscreen=false) {
if(!customElements.get("card-tools-popup")) 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); root.shadowRoot.insertBefore(el,mi);
else else
root.shadowRoot.appendChild(el); root.shadowRoot.appendChild(el);
provideHass(el); provideHass$1(el);
} }
if(!window._moreInfoDialogListener) { if(!window._moreInfoDialogListener) {
@ -650,456 +615,316 @@ __decorate([
customElements.define("browser-player", BrowserPlayer); customElements.define("browser-player", BrowserPlayer);
})(); })();
class BrowserModConnection { 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 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; });
}
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() { async connect() {
const isCast = document.querySelector("hc-main") !== null; const conn = (await hass()).connection;
if (!isCast) { this.connection = conn;
while (!window.hassConnection) // Subscribe to configuration updates
await new Promise((resolve) => window.setTimeout(resolve, 100)); conn.subscribeMessage((msg) => this.incoming_message(msg), {
this.connection = (await window.hassConnection).conn;
}
else {
this.connection = hass().connection;
}
this.connection.subscribeMessage((msg) => this.msg_callback(msg), {
type: "browser_mod/connect", type: "browser_mod/connect",
deviceID: deviceID, 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); provideHass(this);
} }
get connected() { get config() {
return this.connection !== undefined; var _a, _b;
return (_b = (_a = this._data) === null || _a === void 0 ? void 0 : _a.config) !== null && _b !== void 0 ? _b : {};
} }
msg_callback(message) { get devices() {
console.log(message); 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) { sendUpdate(data) {
if (!this.connected) if (!this.connected || !this.registered)
return; return;
this.LOG("Send:", data);
this.connection.sendMessage({ this.connection.sendMessage({
type: "browser_mod/update", type: "browser_mod/update",
deviceID, deviceID: this.deviceID,
data, 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 BrowserModMediaPlayerMixin = (C) => class extends C { 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() { constructor() {
super(); super();
this.player = new Audio(); this.player = new Audio();
for (const event of ["play", "pause", "ended", "volumechange"]) { this._player_enabled = false;
this.player.addEventListener(event, () => this.player_update()); 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) if (!this.player.ended)
this.player.play(); this.player.play();
}, { }, { once: true });
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();
}); });
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
this.player.muted = !this.player.muted;
});
this.connectionPromise.then(() => this._player_update());
} }
player_update(ev) { _player_update() {
const state = this._player_enabled
? this.player.src
? this.player.ended
? "stopped"
: this.player.paused
? "paused"
: "playing"
: "stopped"
: "unavailable";
this.sendUpdate({ this.sendUpdate({
player: { player: {
volume: this.player.volume, volume: this.player.volume,
muted: this.player.muted, muted: this.player.muted,
src: this.player.src, 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);
}
};
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();
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,
},
});
};
update();
}
do_navigate(path) {
if (!path)
return;
history.pushState(null, "", path);
fireEvent("location-changed", {}, ha_element());
}
}; };
var name = "browser_mod"; var name = "browser_mod";
var version = "1.5.3"; var version = "2.0.0b0";
var description = ""; var description = "";
var scripts = { var scripts = {
build: "rollup -c", build: "rollup -c",
@ -1137,15 +962,15 @@ var pjson = {
dependencies: dependencies dependencies: dependencies
}; };
const ext = (baseClass, mixins) => mixins.reduceRight((base, mixin) => mixin(base), baseClass); // export class BrowserMod extends ext(BrowserModConnection, [
class BrowserMod extends ext(BrowserModConnection, [ // BrowserModBrowserMixin,
BrowserModBrowserMixin, // BrowserModPopupsMixin,
BrowserModPopupsMixin, // BrowserModScreensaverMixin,
BrowserModScreensaverMixin, // BrowserModCameraMixin,
BrowserModCameraMixin, // FullyKioskMixin,
FullyKioskMixin, // BrowserModMediaPlayerMixin,
BrowserModMediaPlayerMixin, // ]) {
]) { class BrowserMod extends MediaPlayerMixin(ScreenSaverMixin(ConnectionMixin(EventTarget))) {
constructor() { constructor() {
super(); super();
this.entity_id = deviceID.replace("-", "_"); 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))); let service_data = _replaceThis(JSON.parse(JSON.stringify(msg.service_data)));
this.hass.callService(domain, service, 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 () => { (async () => {
await hass_loaded(); await hass_loaded();

File diff suppressed because one or more lines are too long

View File

@ -3,9 +3,12 @@ from homeassistant import config_entries
from .const import DOMAIN 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): async def async_step_user(self, user_input=None):
return self.async_create_entry(title="Browser Mod", data={}) if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
return self.async_create_entry(title="", data={})

View File

@ -1,5 +1,6 @@
import logging import logging
import voluptuous as vol import voluptuous as vol
from datetime import datetime, timezone
from homeassistant.components.websocket_api import ( from homeassistant.components.websocket_api import (
websocket_command, websocket_command,
@ -8,115 +9,138 @@ from homeassistant.components.websocket_api import (
async_register_command, 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 .helpers import get_devices, create_entity, get_config, is_setup_complete
from .coordinator import Coordinator
from .device import getDevice
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def setup_connection(hass, config): async def async_setup_connection(hass):
@websocket_command( @websocket_api.websocket_command(
{ {
vol.Required("type"): WS_CONNECT, vol.Required("type"): WS_CONNECT,
vol.Required("deviceID"): str, vol.Required("deviceID"): str,
} }
) )
def handle_connect(hass, connection, msg): @websocket_api.async_response
async def handle_connect(hass, connection, msg):
deviceID = msg["deviceID"] deviceID = msg["deviceID"]
store = hass.data[DOMAIN]["store"]
device = get_devices(hass).get(deviceID, BrowserModConnection(hass, deviceID)) def listener(data):
device.connect(connection, msg["id"]) connection.send_message(event_message(msg["id"], {"result": data}))
get_devices(hass)[deviceID] = device
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("type"): WS_UPDATE,
vol.Required("deviceID"): str, vol.Required("deviceID"): str,
vol.Optional("data"): dict, vol.Optional("data"): dict,
} }
) )
def handle_update(hass, connection, msg): @websocket_api.async_response
devices = get_devices(hass) async def handle_update(hass, connection, msg):
deviceID = msg["deviceID"] deviceID = msg["deviceID"]
if deviceID in devices and is_setup_complete(hass): store = hass.data[DOMAIN]["store"]
devices[deviceID].update(msg.get("data", None)) 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_connect)
async_register_command(hass, handle_register)
async_register_command(hass, handle_unregister)
async_register_command(hass, handle_reregister)
async_register_command(hass, handle_update) async_register_command(hass, handle_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")

View File

@ -3,11 +3,10 @@ DOMAIN = "browser_mod"
FRONTEND_SCRIPT_URL = "/browser_mod.js" FRONTEND_SCRIPT_URL = "/browser_mod.js"
SETTINGS_PANEL_URL = "/browser_mod_panel.js" SETTINGS_PANEL_URL = "/browser_mod_panel.js"
DATA_EXTRA_MODULE_URL = "frontend_extra_module_url"
DATA_DEVICES = "devices" DATA_DEVICES = "devices"
DATA_ALIASES = "aliases"
DATA_ADDERS = "adders" DATA_ADDERS = "adders"
DATA_STORE = "store"
DATA_ALIASES = "aliases"
DATA_CONFIG = "config" DATA_CONFIG = "config"
DATA_SETUP_COMPLETE = "setup_complete" DATA_SETUP_COMPLETE = "setup_complete"
@ -21,6 +20,10 @@ WS_CONNECT = "{}/connect".format(WS_ROOT)
WS_UPDATE = "{}/update".format(WS_ROOT) WS_UPDATE = "{}/update".format(WS_ROOT)
WS_CAMERA = "{}/camera".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 = [ USER_COMMANDS = [
"debug", "debug",
"popup", "popup",

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import logging import logging
from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.entity import Entity, async_generate_entity_id
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ( from .const import (
DOMAIN, DOMAIN,
@ -15,6 +16,8 @@ from .const import (
DATA_SETUP_COMPLETE, DATA_SETUP_COMPLETE,
) )
from .coordinator import Coordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -69,6 +72,45 @@ def setup_platform(hass, config, async_add_devices, platform, cls):
def is_setup_complete(hass): def is_setup_complete(hass):
return hass.data[DOMAIN][DATA_SETUP_COMPLETE] 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): class BrowserModEntity(Entity):
def __init__(self, hass, connection, deviceID, alias=None): def __init__(self, hass, connection, deviceID, alias=None):

View File

@ -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 .helpers import setup_platform, BrowserModEntity2
from homeassistant.components.light import LightEntity, SUPPORT_BRIGHTNESS from .const import DOMAIN, DATA_ADDERS
from .helpers import setup_platform, BrowserModEntity
PLATFORM = "light"
async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): async def async_setup_platform(hass, config_entry, async_add_entities, discoveryInfo = None):
return setup_platform(hass, config, async_add_devices, PLATFORM, BrowserModLight) hass.data[DOMAIN][DATA_ADDERS]["light"] = async_add_entities
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
await async_setup_platform(hass, {}, async_add_entities) await async_setup_platform(hass, {}, async_add_entities)
class BrowserModLight(LightEntity, BrowserModEntity): class BrowserModLight(BrowserModEntity2, LightEntity):
domain = PLATFORM
def __init__(self, hass, connection, deviceID, alias=None): def __init__(self, coordinator, deviceID, device):
super().__init__(hass, connection, deviceID, alias) super().__init__(coordinator, deviceID, "Screen")
self.last_seen = None self.device = device
def updated(self):
self.last_seen = datetime.now()
self.schedule_update_ha_state()
@property @property
def state(self): def entity_registry_visible_default(self):
if not self.connection.connection: return True
return STATE_UNAVAILABLE
if self.data.get("blackout", False):
return STATE_OFF
return STATE_ON
@property @property
def is_on(self): def is_on(self):
return not self.data.get("blackout", False) return self._data.get("screen_on", None)
@property @property
def extra_state_attributes(self): def supported_color_modes(self):
return { return {ColorMode.BRIGHTNESS}
"type": "browser_mod",
"deviceID": self.deviceID,
"last_seen": self.last_seen,
}
@property @property
def supported_features(self): def color_mode(self):
if self.data.get("brightness", False): return ColorMode.BRIGHTNESS
return SUPPORT_BRIGHTNESS
return 0
@property @property
def brightness(self): def brightness(self):
return self.data.get("brightness", None) return self._data.get("screen_brightness", 1)
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
self.connection.send("no-blackout", **kwargs) self.device.send("screen_on", **kwargs)
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
self.connection.send("blackout") self.device.send("screen_off")

View File

@ -5,6 +5,7 @@
"dependencies": ["panel_custom", "websocket_api", "http", "frontend"], "dependencies": ["panel_custom", "websocket_api", "http", "frontend"],
"codeowners": [], "codeowners": [],
"requirements": [], "requirements": [],
"version": "1.5.3", "version": "2.0b0",
"iot_class": "local_push" "iot_class": "local_push",
"config_flow": true
} }

View File

@ -25,47 +25,35 @@ from homeassistant.const import (
STATE_UNKNOWN, STATE_UNKNOWN,
) )
from .helpers import setup_platform, BrowserModEntity from .helpers import BrowserModEntity2
from .const import DOMAIN, DATA_ADDERS
_LOGGER = logging.getLogger(__name__)
PLATFORM = "media_player"
async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): async def async_setup_platform(hass, config_entry, async_add_entities, discoveryInfo = None):
return setup_platform(hass, config, async_add_devices, PLATFORM, BrowserModPlayer) hass.data[DOMAIN][DATA_ADDERS]["media_player"] = async_add_entities
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
await async_setup_platform(hass, {}, async_add_entities) await async_setup_platform(hass, {}, async_add_entities)
class BrowserModPlayer(MediaPlayerEntity, BrowserModEntity): class BrowserModPlayer(BrowserModEntity2, MediaPlayerEntity):
domain = PLATFORM
def __init__(self, hass, connection, deviceID, alias=None): def __init__(self, coordinator, deviceID, device):
super().__init__(hass, connection, deviceID, alias) super().__init__(coordinator, deviceID, None)
self.last_seen = None self.device = device
def updated(self):
self.schedule_update_ha_state()
@property @property
def extra_state_attributes(self): def unique_id(self):
return { return f"{self.deviceID}-player"
"type": "browser_mod",
"deviceID": self.deviceID,
}
@property @property
def state(self): def state(self):
if not self.connection.connection: state = self._data.get("player", {}).get("state")
return STATE_UNAVAILABLE
state = self.data.get("state", "unknown")
return { return {
"playing": STATE_PLAYING, "playing": STATE_PLAYING,
"paused": STATE_PAUSED, "paused": STATE_PAUSED,
"stopped": STATE_IDLE, "stopped": STATE_IDLE,
"unavailable": STATE_UNAVAILABLE,
}.get(state, STATE_UNKNOWN) }.get(state, STATE_UNKNOWN)
@property @property
@ -82,30 +70,27 @@ class BrowserModPlayer(MediaPlayerEntity, BrowserModEntity):
@property @property
def volume_level(self): def volume_level(self):
return self.data.get("volume", 0) return self._data.get("player", {}).get("volume", 0)
@property @property
def is_volume_muted(self): 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): 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): 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): async def async_play_media(self, media_type, media_id, **kwargs):
if media_source.is_media_source_id(media_id): if media_source.is_media_source_id(media_id):
media_type = MEDIA_TYPE_URL 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 media_id = play_item.url
if media_type in (MEDIA_TYPE_URL, MEDIA_TYPE_MUSIC): if media_type in (MEDIA_TYPE_URL, MEDIA_TYPE_MUSIC):
media_id = async_process_play_media_url(self.hass, media_id) 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): async def async_browse_media(self, media_content_type=None, media_content_id=None):
"""Implement the websocket media browsing helper.""" """Implement the websocket media browsing helper."""
@ -116,10 +101,10 @@ class BrowserModPlayer(MediaPlayerEntity, BrowserModEntity):
) )
def media_play(self): def media_play(self):
self.connection.send("play") self.device.send("player-play")
def media_pause(self): def media_pause(self):
self.connection.send("pause") self.device.send("player-pause")
def media_stop(self): def media_stop(self):
self.connection.send("stop") self.device.send("player-stop")

View File

@ -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): async def async_setup_view(hass):
url_set = hass.data[DATA_EXTRA_MODULE_URL]
url_set.add(FRONTEND_SCRIPT_URL) hass.http.register_static_path(
FRONTEND_SCRIPT_URL,
hass.config.path("custom_components/browser_mod/browser_mod.js"),
)
add_extra_js_url(hass, FRONTEND_SCRIPT_URL)
hass.components.frontend.async_register_built_in_panel( hass.components.frontend.async_register_built_in_panel(
component_name="custom", 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( hass.http.register_static_path(
SETTINGS_PANEL_URL, SETTINGS_PANEL_URL,
hass.config.path("custom_components/browser_mod/browser_mod_panel.js"), hass.config.path("custom_components/browser_mod/browser_mod_panel.js"),

View File

@ -1,42 +1,48 @@
from datetime import datetime from homeassistant.components.sensor import SensorEntity
from homeassistant.const import STATE_UNAVAILABLE from .const import DOMAIN, DATA_ADDERS
from .helpers import BrowserModEntity2
from .helpers import setup_platform, BrowserModEntity
PLATFORM = "sensor"
async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): async def async_setup_platform(hass, config_entry, async_add_entities, discoveryInfo = None):
return setup_platform(hass, config, async_add_devices, PLATFORM, BrowserModSensor) hass.data[DOMAIN][DATA_ADDERS]["sensor"] = async_add_entities
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
await async_setup_platform(hass, {}, async_add_entities) await async_setup_platform(hass, {}, async_add_entities)
class BrowserModSensor(BrowserModEntity): class BrowserSensor(BrowserModEntity2, SensorEntity):
domain = PLATFORM def __init__(self, coordinator, deviceID, parameter,
name,
def __init__(self, hass, connection, deviceID, alias=None): unit_of_measurement = None,
super().__init__(hass, connection, deviceID, alias) device_class = None,
self.last_seen = None ):
super().__init__(coordinator, deviceID, name)
def updated(self): self.parameter = parameter
self.last_seen = datetime.now() self._device_class = device_class
self.schedule_update_ha_state() self._unit_of_measurement = unit_of_measurement
@property @property
def state(self): def native_value(self):
if not self.connection.connection: data = self._data
return STATE_UNAVAILABLE data = data.get("browser", {})
return len(self.connection.connection) 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 @property
def extra_state_attributes(self): def extra_state_attributes(self):
return { retval = super().extra_state_attributes
"type": "browser_mod", if self.parameter == "currentUser":
"last_seen": self.last_seen, retval["userData"] = self._data.get("browser", {}).get("userData")
"deviceID": self.deviceID, if self.parameter == "path":
**self.data, retval["pathSegments"] = self._data.get("browser", {}).get("path", "").split("/")
} if self.parameter == "userAgent":
retval["userAgent"] = self._data.get("browser", {}).get("userAgent")
return retval

View File

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

View File

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

View File

@ -1,9 +1,40 @@
import { LitElement, html, css } from "lit"; import { LitElement, html, css } from "lit";
import { deviceID } from "card-tools/src/deviceID"; import { property } from "lit/decorators.js";
import { loadDevTools } from "./helpers";
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()
);
}
class BrowserModPanel extends LitElement {
hass;
narrow;
render() { render() {
return html` return html`
<ha-app-layout> <ha-app-layout>
@ -13,52 +44,110 @@ class BrowserModPanel extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
></ha-menu-button> ></ha-menu-button>
<div main-title>Browser Mod Settingss</div> <div main-title>Browser Mod Settings</div>
</app-toolbar> </app-toolbar>
</app-header> </app-header>
<ha-config-section .narrow=${this.narrow} full-width> <ha-config-section .narrow=${this.narrow} full-width>
<ha-card header="This Browser"> <ha-card outlined>
<h1 class="card-header">
<div class="name">This Browser</div>
${bmWindow.browser_mod?.connected
? html`
<ha-icon
class="icon"
.icon=${"mdi:check-circle-outline"}
style="color: var(--success-color, green);"
></ha-icon>
`
: html`
<ha-icon
class="icon"
.icon=${"mdi:circle-outline"}
style="color: var(--error-color, red);"
></ha-icon>
`}
</h1>
<div class="card-content">Browser-mod not connected.</div>
<div class="card-content"> <div class="card-content">
<div class="option"> <ha-settings-row>
<h3>Enable</h3> <span slot="heading">Enable</span>
<span slot="description"
>Enable this browser as a Device in Home Assistant</span
>
<ha-switch
.checked=${window.browser_mod?.registered}
@change=${this.toggleRegister}
></ha-switch>
</ha-settings-row>
<ha-settings-row>
<span slot="heading">DeviceID</span>
<span slot="description"
>A unique identifier for this browser-device
combination.</span
>
<ha-textfield
.value=${window.browser_mod?.deviceID}
@change=${this.changeDeviceID}
></ha-textfield>
</ha-settings-row>
<ha-settings-row>
<span slot="heading">Enable camera</span>
<span slot="description"
>Get camera input from this device (hardware
dependent)</span
>
<ha-switch> </ha-switch>
</ha-settings-row>
</div>
</ha-card>
<ha-card header="Registered devices" outlined>
<div class="card-content">
${Object.keys(window.browser_mod.devices).map(
(d) => html` <ha-settings-row>
<span slot="heading"> ${d} </span>
<span slot="description">
Last connected:
<ha-relative-time
.hass=${this.hass}
.datetime=${window.browser_mod.devices[d].last_seen}
></ha-relative-time>
</span>
<ha-icon-button .deviceID=${d} @click=${this.unregister_device}>
<ha-icon .icon=${"mdi:delete"}></ha-icon>
</ha-icon-button>
<ha-icon-button>
<ha-icon .icon=${"mdi:wrench"}></ha-icon>
</ha-icon-button>
</ha-settings-row>`
)}
</div>
</ha-card>
<ha-card outlined header="Tweaks">
<div class="card-content">
<ha-settings-row>
<span slot="heading">Auto enable devices</span>
<ha-switch></ha-switch> <ha-switch></ha-switch>
</div> </ha-settings-row>
Enable this browser as a Device in Home Assistant <ha-settings-row>
<div class="option"> <span slot="heading">User sidebar</span>
<h3>DeviceID</h3> <span slot="description"
</div> >Save sidebar as default for current user
<ha-textfield .value=${deviceID}> </ha-textfield> (${this.hass.user.name})</span
The device ID is a unique identifier for your browser/device >
combination. <mwc-button>Save</mwc-button>
<div class="option"> </ha-settings-row>
<h3>Enable Camera</h3> <ha-settings-row>
<ha-switch> </ha-switch> <span slot="heading">Global sidebar</span>
</div> <span slot="description"
Get Camera input from this device (hardware dependent) >Save sidebar as default for all users</span
</div> >
<div class="card-actions"> <mwc-button>Save</mwc-button>
<div class="spacer"></div> </ha-settings-row>
<mwc-button>Update</mwc-button>
</div>
</ha-card>
<ha-card header="Current User">
<div class="card-content"></div>
</ha-card>
<ha-card header="Tweaks">
<div class="card-content">
<div class="option">
<h3>Cool function</h3>
<ha-switch> </ha-switch>
</div>
Enabling this will cause cool stuff to happen.
<div class="option">
<h3>Another function</h3>
<ha-switch> </ha-switch>
</div>
Enabling this will cause less cool stuff to happen.
</div> </div>
</ha-card> </ha-card>
</ha-config-section> </ha-config-section>
@ -68,12 +157,17 @@ class BrowserModPanel extends LitElement {
static get styles() { static get styles() {
return [ return [
...(customElements.get("ha-config-dashboard") as any).styles, ...((customElements.get("ha-config-dashboard") as any)?.styles ?? []),
css` css`
:host { :host {
--app-header-background-color: var(--sidebar-background-color); --app-header-background-color: var(--sidebar-background-color);
--app-header-text-color: var(--sidebar-text-color); --app-header-text-color: var(--sidebar-text-color);
--app-header-border-bottom: 1px solid var(--divider-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 { .card-actions {
display: flex; display: flex;
@ -99,26 +193,13 @@ class BrowserModPanel extends LitElement {
margin-right: 7px; margin-right: 7px;
margin-left: 0.5em; margin-left: 0.5em;
} }
ha-icon-button > * {
display: flex;
}
`, `,
]; ];
} }
} }
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");
};
loadDevTools().then(() => {
customElements.define("browser-mod-panel", BrowserModPanel); customElements.define("browser-mod-panel", BrowserModPanel);
}); });

63
js/helpers.ts Normal file
View File

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

View File

@ -8,7 +8,10 @@ export const BrowserModBrowserMixin = (C) =>
document.addEventListener("visibilitychange", () => this.sensor_update()); document.addEventListener("visibilitychange", () => this.sensor_update());
window.addEventListener("location-changed", () => 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() { sensor_update() {
@ -28,7 +31,7 @@ export const BrowserModBrowserMixin = (C) =>
charging: window.fully?.isPlugged() ?? battery?.charging, charging: window.fully?.isPlugged() ?? battery?.charging,
darkMode: this.hass?.themes?.darkMode, darkMode: this.hass?.themes?.darkMode,
userData: this.hass?.user, userData: this.hass?.user,
config: this.config, // config: this.config,
}, },
}); });
}; };

View File

@ -1,42 +1,175 @@
import { deviceID } from "card-tools/src/deviceID"; import { hass, provideHass } from "../helpers";
import { hass, provideHass } from "card-tools/src/hass";
export class BrowserModConnection { const ID_STORAGE_KEY = "browser_mod-device-id";
hass;
connection;
async connect() { export const ConnectionMixin = (SuperClass) => {
const isCast = document.querySelector("hc-main") !== null; class BrowserModConnection extends SuperClass {
if (!isCast) { public hass;
while (!window.hassConnection) public connection;
await new Promise((resolve) => window.setTimeout(resolve, 100)); private _data;
this.connection = (await window.hassConnection).conn; public connected = false;
} else { private _connectionResolve;
this.connection = hass().connection; 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), { private fireEvent(event, detail = undefined) {
this.dispatchEvent(new CustomEvent(event, { detail }));
}
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", type: "browser_mod/connect",
deviceID: deviceID, 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); provideHass(this);
} }
get connected() { get config() {
return this.connection !== undefined; return this._data?.config ?? {};
} }
msg_callback(message) { get devices() {
console.log(message); 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) { sendUpdate(data) {
if (!this.connected) return; if (!this.connected || !this.registered) return;
const dt = new Date();
this.LOG("Send:", data);
this.connection.sendMessage({ this.connection.sendMessage({
type: "browser_mod/update", type: "browser_mod/update",
deviceID, deviceID: this.deviceID,
data, 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
}
}
return BrowserModConnection;
} }

View File

@ -5,8 +5,10 @@ import { fireEvent } from "card-tools/src/event";
import { ha_element, hass_loaded } from "card-tools/src/hass"; import { ha_element, hass_loaded } from "card-tools/src/hass";
import "./browser-player"; import "./browser-player";
import { BrowserModConnection } from "./connection"; // import { BrowserModConnection } from "./connection";
import { BrowserModMediaPlayerMixin } from "./mediaPlayer"; import { ConnectionMixin } from "./connection";
import { ScreenSaverMixin } from "./screensaver";
import { MediaPlayerMixin } from "./mediaPlayer";
import { FullyKioskMixin } from "./fullyKiosk"; import { FullyKioskMixin } from "./fullyKiosk";
import { BrowserModCameraMixin } from "./camera"; import { BrowserModCameraMixin } from "./camera";
import { BrowserModScreensaverMixin } from "./screensaver"; import { BrowserModScreensaverMixin } from "./screensaver";
@ -17,14 +19,15 @@ import pjson from "../../package.json";
const ext = (baseClass, mixins) => const ext = (baseClass, mixins) =>
mixins.reduceRight((base, mixin) => mixin(base), baseClass); mixins.reduceRight((base, mixin) => mixin(base), baseClass);
export class BrowserMod extends ext(BrowserModConnection, [ // export class BrowserMod extends ext(BrowserModConnection, [
BrowserModBrowserMixin, // BrowserModBrowserMixin,
BrowserModPopupsMixin, // BrowserModPopupsMixin,
BrowserModScreensaverMixin, // BrowserModScreensaverMixin,
BrowserModCameraMixin, // BrowserModCameraMixin,
FullyKioskMixin, // FullyKioskMixin,
BrowserModMediaPlayerMixin, // BrowserModMediaPlayerMixin,
]) { // ]) {
export class BrowserMod extends MediaPlayerMixin(ScreenSaverMixin(ConnectionMixin(EventTarget))) {
constructor() { constructor() {
super(); super();
this.entity_id = deviceID.replace("-", "_"); this.entity_id = deviceID.replace("-", "_");
@ -121,21 +124,21 @@ export class BrowserMod extends ext(BrowserModConnection, [
this.hass.callService(domain, service, service_data); this.hass.callService(domain, service, service_data);
} }
update(msg = null) { // update(msg = null) {
if (msg) { // if (msg) {
if (msg.name) { // if (msg.name) {
this.entity_id = msg.name.toLowerCase(); // this.entity_id = msg.name.toLowerCase();
} // }
if (msg.camera && !this.isFully) { // if (msg.camera && !this.isFully) {
this.setup_camera(); // this.setup_camera();
} // }
this.config = { ...this.config, ...msg }; // this.config = { ...this.config, ...msg };
} // }
this.player_update(); // this.player_update();
this.fully_update(); // this.fully_update();
this.screen_update(); // this.screen_update();
this.sensor_update(); // this.sensor_update();
} // }
} }
(async () => { (async () => {

View File

@ -1,59 +1,71 @@
export const BrowserModMediaPlayerMixin = (C) => export const MediaPlayerMixin = (SuperClass) => {
class extends C { return class MediaPlayerMixinClass extends SuperClass {
public player;
private _player_enabled;
constructor() { constructor() {
super(); super();
this.player = new Audio();
for (const event of ["play", "pause", "ended", "volumechange"]) { this.player = new Audio();
this.player.addEventListener(event, () => this.player_update()); this._player_enabled = false;
for (const ev of ["play", "pause", "ended", "volumechange"]) {
this.player.addEventListener(ev, () => this._player_update());
} }
window.addEventListener( window.addEventListener("pointerdown", () => {
"click", this._player_enabled = true;
() => {
if (!this.player.ended) this.player.play(); 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({ this.sendUpdate({
player: { player: {
volume: this.player.volume, volume: this.player.volume,
muted: this.player.muted, muted: this.player.muted,
src: this.player.src, 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);
}
};

View File

@ -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) => export const BrowserModScreensaverMixin = (C) =>
class extends C { class extends C {
constructor() { constructor() {

View File

@ -1,7 +1,7 @@
{ {
"name": "browser_mod", "name": "browser_mod",
"private": true, "private": true,
"version": "1.5.3", "version": "2.0.0b0",
"description": "", "description": "",
"scripts": { "scripts": {
"build": "rollup -c", "build": "rollup -c",

View File

@ -2,16 +2,21 @@ default_config:
demo: demo:
browser_mod: logger:
devices: default: warning
camdevice: logs:
camera: true custom_components.browser_mod: info
testdevice:
alias: test # browser_mod:
fully: # devices:
force_stay_awake: true # camdevice:
fully2: # camera: true
screensaver: true # testdevice:
# alias: test
# fully:
# force_stay_awake: true
# fully2:
# screensaver: true
lovelace: lovelace:
mode: storage mode: storage