Lots of changes and modernization. WIP
This commit is contained in:
parent
69e9642b4b
commit
466a5eb5e7
@ -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)
|
||||||
|
@ -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,
|
|
||||||
}
|
|
||||||
|
@ -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
@ -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={})
|
||||||
|
@ -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")
|
|
||||||
|
@ -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",
|
||||||
|
15
custom_components/browser_mod/coordinator.py
Normal file
15
custom_components/browser_mod/coordinator.py
Normal 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
|
108
custom_components/browser_mod/device.py
Normal file
108
custom_components/browser_mod/device.py
Normal 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]
|
@ -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):
|
||||||
|
@ -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")
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
|
@ -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"),
|
||||||
|
@ -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
|
92
custom_components/browser_mod/store.py
Normal file
92
custom_components/browser_mod/store.py
Normal 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()
|
16
js/config_panel/helpers.ts
Normal file
16
js/config_panel/helpers.ts
Normal 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");
|
||||||
|
};
|
@ -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
63
js/helpers.ts
Normal 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);
|
||||||
|
}
|
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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 () => {
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
};
|
|
@ -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() {
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user