Compare commits

...

3 Commits

22 changed files with 891 additions and 503 deletions

View File

@ -3,6 +3,7 @@ import logging
from homeassistant.components.websocket_api import event_message
from homeassistant.helpers import device_registry, entity_registry
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.core import callback
from .const import DATA_BROWSERS, DOMAIN, DATA_ADDERS
from .sensor import BrowserSensor
@ -130,11 +131,14 @@ class BrowserModBrowser:
er.async_remove(self.entities["camera"].entity_id)
del self.entities["camera"]
hass.create_task(
self.send(
None, browserEntities={k: v.entity_id for k, v in self.entities.items()}
)
)
def send(self, command, **kwargs):
@callback
async def send(self, command, **kwargs):
"""Send a command to this browser."""
if self.connection is None:
return

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -9,6 +9,8 @@ from homeassistant.components.websocket_api import (
from homeassistant.components import websocket_api
from homeassistant.core import callback
from .const import (
BROWSER_ID,
DATA_STORE,
@ -40,6 +42,7 @@ async def async_setup_connection(hass):
browserID = msg[BROWSER_ID]
store = hass.data[DOMAIN][DATA_STORE]
@callback
def send_update(data):
connection.send_message(event_message(msg["id"], {"result": data}))

View File

@ -5,7 +5,7 @@
"dependencies": ["panel_custom", "websocket_api", "http", "frontend", "lovelace"],
"codeowners": [],
"requirements": [],
"version": "2.0.0b4",
"version": "2.0.0b5",
"iot_class": "local_push",
"config_flow": true
}

View File

@ -27,7 +27,7 @@ async def async_setup_services(hass):
if target not in browsers:
continue
browser = browsers[target]
browser.send(service, **data)
hass.create_task(browser.send(service, **data))
def handle_service(call):
service = call.service

View File

@ -16,6 +16,7 @@ class SettingsStoreData:
defaultPanel = attr.ib(type=str, default=None)
sidebarPanelOrder = attr.ib(type=list, default=None)
sidebarHiddenPanels = attr.ib(type=list, default=None)
sidebarTitle = attr.ib(type=str, default=None)
faviconTemplate = attr.ib(type=str, default=None)
titleTemplate = attr.ib(type=str, default=None)

View File

@ -0,0 +1,294 @@
import { LitElement, html, css } from "lit";
import { property } from "lit/decorators.js";
import { selectTree } from "../helpers";
class BrowserModSettingsTable extends LitElement {
@property() settingKey;
@property() settingSelector = {
template: {},
};
@property() hass;
@property() default;
@property() tableData = [];
_users = undefined;
firstUpdated() {
window.browser_mod.addEventListener("browser-mod-config-update", () =>
this.updateTable()
);
}
updated(changedProperties) {
if (changedProperties.has("settingKey")) this.updateTable();
if (
changedProperties.has("hass") &&
changedProperties.get("hass") === undefined
)
this.updateTable();
}
async fetchUsers(): Promise<any[]> {
if (this._users === undefined)
this._users = await this.hass.callWS({ type: "config/auth/list" });
return this._users;
}
clearSetting(type, target) {
const clearSettingCallback = async () => {
if (this.settingKey === "sidebarPanelOrder") {
const sideBar: any = await selectTree(
document,
"home-assistant $ home-assistant-main $ app-drawer-layout app-drawer ha-sidebar"
);
window.browser_mod.setSetting(type, target, {
sidebarHiddenPanels: "[]",
sidebarPanelOrder: "[]",
});
window.browser_mod.setSetting(type, target, {
sidebarHiddenPanels: undefined,
sidebarPanelOrder: undefined,
});
return;
}
if (this.default)
window.browser_mod.setSetting(type, target, {
[this.settingKey]: this.default,
});
window.browser_mod.setSetting(type, target, {
[this.settingKey]: undefined,
});
};
window.browser_mod?.showPopup(
"Are you sure",
"Do you wish to clear this setting?",
{
right_button: "Yes",
right_button_action: clearSettingCallback,
left_button: "No",
}
);
}
changeSetting(type, target) {
const changeSettingCallback = async (newValue) => {
if (this.settingKey === "sidebarPanelOrder") {
const sideBar: any = await selectTree(
document,
"home-assistant $ home-assistant-main $ app-drawer-layout app-drawer ha-sidebar"
);
window.browser_mod.setSetting(type, target, {
sidebarHiddenPanels: JSON.stringify(sideBar._hiddenPanels),
sidebarPanelOrder: JSON.stringify(sideBar._panelOrder),
});
console.log(sideBar._hiddenPanels, sideBar._panelOrder);
return;
}
let value = newValue.value;
window.browser_mod.setSetting(type, target, { [this.settingKey]: value });
};
const settings = window.browser_mod?.getSetting?.(this.settingKey);
const def =
(type === "global" ? settings.global : settings[type][target]) ??
this.default;
window.browser_mod?.showPopup(
"Change value",
(this.settingSelector as any).plaintext ?? [
{
name: "value",
label: (this.settingSelector as any).label ?? "",
default: def,
selector: this.settingSelector,
},
],
{
right_button: "OK",
right_button_action: changeSettingCallback,
left_button: "Cancel",
}
);
}
addBrowserSetting() {
const settings = window.browser_mod?.getSetting?.(this.settingKey);
const allBrowsers = window.browser_mod._data.browsers;
const browsers = [];
for (const target of Object.keys(allBrowsers)) {
if (settings.browser[target] == null) browsers.push(target);
}
if (browsers.length === 0) {
window.browser_mod.showPopup(
"No browsers to configure",
"All registered browsers have already been configured.",
{ right_button: "OK" }
);
return;
}
window.browser_mod.showPopup(
"Select browser to configure",
[
{
name: "browser",
label: "",
selector: {
select: { options: browsers },
},
},
],
{
right_button: "Next",
right_button_action: (value) =>
this.changeSetting("browser", value.browser),
left_button: "Cancel",
}
);
}
async addUserSetting() {
const settings = window.browser_mod?.getSetting?.(this.settingKey);
const allUsers = await this.fetchUsers();
const users = [];
for (const target of allUsers) {
if (target.username && settings.user[target.id] == null)
users.push({ label: target.name, value: target.id });
}
if (users.length === 0) {
window.browser_mod.showPopup(
"No users to configure",
"All users have already been configured.",
{ right_button: "OK" }
);
return;
}
window.browser_mod.showPopup(
"Select user to configure",
[
{
name: "user",
label: "",
selector: {
select: { options: users },
},
},
],
{
right_button: "Next",
right_button_action: (value) => this.changeSetting("user", value.user),
left_button: "Cancel",
}
);
}
async updateTable() {
if (this.hass === undefined) return;
const users = await this.fetchUsers();
const settings = window.browser_mod?.getSetting?.(this.settingKey);
const data = [];
for (const [k, v] of Object.entries(settings.user)) {
const user = users.find((usr) => usr.id === k);
data.push({
name: `User: ${user.name}`,
value: String(v),
controls: html`
<ha-icon-button @click=${() => this.changeSetting("user", k)}>
<ha-icon .icon=${"mdi:pencil"} style="display:flex;"></ha-icon>
</ha-icon-button>
<ha-icon-button @click=${() => this.clearSetting("user", k)}>
<ha-icon .icon=${"mdi:delete"} style="display:flex;"></ha-icon>
</ha-icon-button>
`,
});
}
data.push({
name: "",
value: html`
<mwc-button @click=${() => this.addUserSetting()}>
<ha-icon .icon=${"mdi:plus"}></ha-icon>
Add user setting
</mwc-button>
`,
});
for (const [k, v] of Object.entries(settings.browser)) {
data.push({
name: `Browser: ${k}`,
value: String(v),
controls: html`
<ha-icon-button @click=${() => this.changeSetting("browser", k)}>
<ha-icon .icon=${"mdi:pencil"} style="display:flex;"></ha-icon>
</ha-icon-button>
<ha-icon-button @click=${() => this.clearSetting("browser", k)}>
<ha-icon .icon=${"mdi:delete"} style="display:flex;"></ha-icon>
</ha-icon-button>
`,
});
}
data.push({
name: "",
value: html`
<mwc-button @click=${() => this.addBrowserSetting()}>
<ha-icon .icon=${"mdi:plus"}></ha-icon>
Add browser setting
</mwc-button>
`,
});
data.push({
name: "GLOBAL",
value:
settings.global != null
? String(settings.global)
: html`<span style="color: var(--warning-color);">DEFAULT</span>`,
controls: html`
<ha-icon-button @click=${() => this.changeSetting("global", null)}>
<ha-icon .icon=${"mdi:pencil"} style="display:flex;"></ha-icon>
</ha-icon-button>
<ha-icon-button @click=${() => this.clearSetting("global", null)}>
<ha-icon .icon=${"mdi:delete"} style="display:flex;"></ha-icon>
</ha-icon-button>
`,
});
this.tableData = data;
}
render() {
const global = window.browser_mod?.global_settings?.[this.settingKey];
const columns = {
name: {
title: "Name",
grows: true,
},
value: {
title: "Value",
grows: true,
},
controls: {},
};
return html`
<ha-data-table .columns=${columns} .data=${this.tableData} auto-height>
</ha-data-table>
`;
}
static get styles() {
return css`
:host {
display: block;
}
`;
}
}
customElements.define("browser-mod-settings-table", BrowserModSettingsTable);

View File

@ -136,7 +136,7 @@ class BrowserModRegisteredBrowsersCard extends LitElement {
private _renderInteractionAlert() {
return html`
<ha-alert title="Interaction requirement">
For security reasons many browsers require the user to interact with a
For privacy reasons many browsers require the user to interact with a
webpage before allowing audio playback or video capture. This may affect
the
<code>media_player</code> and <code>camera</code> components of Browser

View File

@ -1,38 +1,73 @@
import { LitElement, html, css } from "lit";
import { property, state } from "lit/decorators.js";
import { loadDeveloperToolsTemplate } from "../helpers";
import { loadDeveloperToolsTemplate, selectTree } from "../helpers";
import "./browser-mod-settings-table";
loadDeveloperToolsTemplate();
class BrowserModFrontendSettingsCard extends LitElement {
@property() hass;
@state() _selectedTab = 0;
@state() _dashboards = [];
@state() _editSidebar = false;
_savedSidebar = { panelOrder: [], hiddenPanels: [] };
firstUpdated() {
window.browser_mod.addEventListener("browser-mod-config-update", () =>
this.requestUpdate()
);
window.browser_mod.addEventListener("browser-mod-favicon-update", () =>
this.requestUpdate()
);
}
_handleSwitchTab(ev: CustomEvent) {
this._selectedTab = parseInt(ev.detail.index, 10);
updated(changedProperties) {
if (
changedProperties.has("hass") &&
changedProperties.get("hass") === undefined
) {
(async () =>
(this._dashboards = await this.hass.callWS({
type: "lovelace/dashboards/list",
})))();
}
}
async toggleEditSidebar() {
const sideBar: any = await selectTree(
document,
"home-assistant $ home-assistant-main $ app-drawer-layout app-drawer ha-sidebar"
);
sideBar.editMode = !sideBar.editMode;
this._editSidebar = sideBar.editMode;
if (this._editSidebar) {
this._savedSidebar = {
panelOrder: sideBar._panelOrder,
hiddenPanels: sideBar._hiddenPanels,
};
} else {
sideBar._panelOrder = this._savedSidebar.panelOrder ?? [];
sideBar._hiddenPanels = this._savedSidebar.hiddenPanels ?? [];
this._savedSidebar = { panelOrder: [], hiddenPanels: [] };
}
}
render() {
const level = ["user", "browser", "global"][this._selectedTab];
const db = this._dashboards.map((d) => {
return { value: d.url_path, label: d.title };
});
const dashboardSelector = {
select: {
options: [{ value: "lovelace", label: "lovelace (default)" }, ...db],
custom_value: true,
},
};
return html`
<ha-card header="Frontend Settings" outlined>
<div class="card-content">
<ha-alert alert-type="warning">
<p>
Please note: The settings in this section severely change the way the Home
<ha-alert alert-type="warning" title="Please note:">
The settings in this section severely change the way the Home
Assistant frontend works and looks. It is very easy to forget that
you made a setting here when you switch devices or user.
</p>
<p>
Do not report any issues to Home Assistant before clearing
<b>EVERY</b> setting here and thouroghly clearing all your browser
@ -41,234 +76,119 @@ class BrowserModFrontendSettingsCard extends LitElement {
</p>
</ha-alert>
<p>
Global settings are applied for all users and browsers.</br>
User settings are applied to the current user and overrides any Global settings.</br>
Browser settings are applied for the current browser and overrides any User or Global settings.
Settings below are applied by first match. I.e. if a matching User
setting exists, it will be applied. Otherwise any matching Browser
setting and otherwise the GLOBAL setting if that differs from
DEFAULT.
</p>
<mwc-tab-bar
.activeIndex=${this._selectedTab}
@MDCTabBar:activated=${this._handleSwitchTab}
>
<mwc-tab .label=${"User (" + this.hass.user.name + ")"}></mwc-tab>
<ha-icon .icon=${"mdi:chevron-double-right"}></ha-icon>
<mwc-tab .label=${"Browser"}></mwc-tab>
<ha-icon .icon=${"mdi:chevron-double-right"}></ha-icon>
<mwc-tab .label=${"Global"}></mwc-tab>
</mwc-tab-bar>
${this._render_settings(level)}
</div>
</ha-card>
`;
}
_render_settings(level) {
const global = window.browser_mod.global_settings;
const browser = window.browser_mod.browser_settings;
const user = window.browser_mod.user_settings;
const current = { global, browser, user }[level];
const DESC_BOOLEAN = (val) =>
({ true: "Enabled", false: "Disabled", undefined: "Unset" }[String(val)]);
const DESC_SET_UNSET = (val) => (val === undefined ? "Unset" : "Set");
const OVERRIDDEN = (key) => {
if (level !== "browser" && browser[key] !== undefined)
return html`<br />Overridden by browser setting`;
if (level === "global" && user[key] !== undefined)
return html`<br />Overridden by user setting`;
};
return html`
<div class="box">
<ha-settings-row>
<span slot="heading">Favicon template</span>
${OVERRIDDEN("faviconTemplate")}
<img src="${window.browser_mod._currentFavicon}" class="favicon" />
</ha-settings-row>
<ha-code-editor
.hass=${this.hass}
.value=${current.faviconTemplate}
@value-changed=${(ev) => {
const tpl = ev.detail.value || undefined;
window.browser_mod.set_setting("faviconTemplate", tpl, level);
}}
></ha-code-editor>
<ha-settings-row>
<mwc-button
@click=${() =>
window.browser_mod.set_setting(
"faviconTemplate",
undefined,
level
)}
>
Clear
</mwc-button>
</ha-settings-row>
<div class="separator"></div>
<ha-settings-row>
<span slot="heading">Title template</span>
${OVERRIDDEN("titleTemplate")}
<span slot="description">
Jinja template for the browser window/tab title
</span>
</ha-settings-row>
<ha-code-editor
<browser-mod-settings-table
.hass=${this.hass}
.value=${current.titleTemplate}
@value-changed=${(ev) => {
const tpl = ev.detail.value || undefined;
window.browser_mod.set_setting("titleTemplate", tpl, level);
}}
></ha-code-editor>
<ha-settings-row>
<mwc-button
@click=${() =>
window.browser_mod.set_setting("titleTemplate", undefined, level)}
>
Clear
</mwc-button>
</ha-settings-row>
.settingKey=${"titleTemplate"}
></browser-mod-settings-table>
<div class="separator"></div>
<ha-settings-row>
<span slot="heading">Hide Sidebar</span>
<span slot="description">Hide the sidebar and hamburger menu</span>
Currently: ${DESC_BOOLEAN(current.hideSidebar)}
${OVERRIDDEN("hideSidebar")}
</ha-settings-row>
<ha-settings-row>
<mwc-button
@click=${() =>
window.browser_mod.set_setting("hideSidebar", true, level)}
>
Enable
</mwc-button>
<mwc-button
@click=${() =>
window.browser_mod.set_setting("hideSidebar", false, level)}
>
Disable
</mwc-button>
<mwc-button
@click=${() =>
window.browser_mod.set_setting("hideSidebar", undefined, level)}
>
Clear
</mwc-button>
<span slot="heading">Favicon template</span>
<span slot="description">
Jinja template for the browser favicon
</span>
</ha-settings-row>
<browser-mod-settings-table
.hass=${this.hass}
.settingKey=${"faviconTemplate"}
></browser-mod-settings-table>
<div class="separator"></div>
<ha-settings-row>
<span slot="heading">Hide Header</span>
<span slot="description">Hide the header on all pages</span>
Currently: ${DESC_BOOLEAN(current.hideHeader)}
${OVERRIDDEN("hideHeader")}
<span slot="heading">Hide sidebar</span>
<span slot="description">
Completely remove the sidebar from all panels
</span>
</ha-settings-row>
<browser-mod-settings-table
.hass=${this.hass}
.settingKey=${"hideSidebar"}
.settingSelector=${{ boolean: {}, label: "Hide sidebar" }}
></browser-mod-settings-table>
<div class="separator"></div>
<ha-settings-row>
<mwc-button
@click=${() =>
window.browser_mod.set_setting("hideHeader", true, level)}
>
Enable
</mwc-button>
<mwc-button
@click=${() =>
window.browser_mod.set_setting("hideHeader", false, level)}
>
Disable
</mwc-button>
<mwc-button
@click=${() =>
window.browser_mod.set_setting("hideHeader", undefined, level)}
>
Clear
</mwc-button>
<span slot="heading">Hide header</span>
<span slot="description">
Completely remove the header from all panels
</span>
</ha-settings-row>
<browser-mod-settings-table
.hass=${this.hass}
.settingKey=${"hideHeader"}
.settingSelector=${{ boolean: {}, label: "Hide header" }}
></browser-mod-settings-table>
<div class="separator"></div>
<ha-settings-row>
<span slot="heading">Default dashboard</span>
<span slot="description">
The dashboard that is showed when navigating to
${location.origin}/
</span>
</ha-settings-row>
<browser-mod-settings-table
.hass=${this.hass}
.settingKey=${"defaultPanel"}
.settingSelector=${dashboardSelector}
.default=${"lovelace"}
></browser-mod-settings-table>
<div class="separator"></div>
<ha-settings-row>
<span slot="heading">Sidebar order</span>
<span slot="description">
Order and visibility of sidebar buttons
Order and visibility of sidebar items. <br />Click EDIT and set
the sidebar up as you want. Then save the settings and finally
click RESTORE.
</span>
Currently: ${DESC_SET_UNSET(current.sidebarPanelOrder)}
${OVERRIDDEN("sidebarPanelOrder")}
</ha-settings-row>
<ha-settings-row>
<span slot="description">
Clearing this does NOT restore the original default order.
</span>
<mwc-button
@click=${() => {
window.browser_mod.set_setting(
"sidebarPanelOrder",
localStorage.getItem("sidebarPanelOrder"),
level
);
window.browser_mod.set_setting(
"sidebarHiddenPanels",
localStorage.getItem("sidebarHiddenPanels"),
level
);
}}
>
Set
</mwc-button>
<mwc-button
@click=${() => {
window.browser_mod.set_setting(
"sidebarPanelOrder",
undefined,
level
);
window.browser_mod.set_setting(
"sidebarHiddenPanels",
undefined,
level
);
}}
>
Clear
<mwc-button @click=${() => this.toggleEditSidebar()}>
${this._editSidebar ? "Restore" : "Edit"}
</mwc-button>
</ha-settings-row>
<browser-mod-settings-table
.hass=${this.hass}
.settingKey=${"sidebarPanelOrder"}
.settingSelector=${{
plaintext: "Press OK to store the current sidebar order",
}}
.default=${"lovelace"}
></browser-mod-settings-table>
<div class="separator"></div>
<ha-settings-row>
<span slot="heading">Default dashboard</span>
<span slot="description"
>The dashboard that's displayed by default</span
>
Currently: ${DESC_SET_UNSET(current.defaultPanel)}
${OVERRIDDEN("defaultPanel")}
</ha-settings-row>
<ha-settings-row>
<span slot="heading">Sidebar title</span>
<span slot="description">
Clearing this does NOT restore the original default dashboard.
The title at the top of the sidebar
</span>
<mwc-button
@click=${() => {
window.browser_mod.set_setting(
"defaultPanel",
localStorage.getItem("defaultPanel"),
level
);
}}
>
Set
</mwc-button>
<mwc-button
@click=${() => {
window.browser_mod.set_setting("defaultPanel", undefined, level);
}}
>
Clear
</mwc-button>
</ha-settings-row>
<browser-mod-settings-table
.hass=${this.hass}
.settingKey=${"sidebarTitle"}
.settingSelector=${{ text: {} }}
></browser-mod-settings-table>
</div>
</ha-card>
`;
}
@ -280,7 +200,7 @@ class BrowserModFrontendSettingsCard extends LitElement {
}
.separator {
border-bottom: 1px solid var(--divider-color);
margin: 0 -8px;
margin: 16px -16px 0px;
}
img.favicon {
width: 64px;

View File

@ -15,12 +15,13 @@ loadConfigDashboard().then(() => {
@property() connection;
firstUpdated() {
window.browser_mod.addEventListener("browser-mod-config-update", () =>
window.addEventListener("browser-mod-config-update", () =>
this.requestUpdate()
);
}
render() {
if (!window.browser_mod) return html``;
return html`
<ha-app-layout>
<app-header slot="header" fixed>

View File

@ -1,9 +1,17 @@
const TIMEOUT_ERROR = "SELECTTREE-TIMEOUT";
async function _await_el(el) {
export async function await_element(el, hard = false) {
if (el.localName?.includes("-"))
await customElements.whenDefined(el.localName);
if (el.updateComplete) await el.updateComplete;
if (hard) {
if (el.pageRendered) await el.pageRendered;
if (el._panelState) {
let rounds = 0;
while (el._panelState !== "loaded" && rounds++ < 5)
await new Promise((r) => setTimeout(r, 100));
}
}
}
async function _selectTree(root, path, all = false) {
@ -18,7 +26,7 @@ async function _selectTree(root, path, all = false) {
if (!p.trim().length) continue;
_await_el(e);
await_element(e);
el = p === "$" ? [e.shadowRoot] : e.querySelectorAll(p);
}
return all ? el : el[0];
@ -102,6 +110,7 @@ export const loadConfigDashboard = async () => {
const configRouter: any = document.createElement("ha-panel-config");
await configRouter?.routerOptions?.routes?.dashboard?.load?.(); // Load ha-config-dashboard
await configRouter?.routerOptions?.routes?.cloud?.load?.(); // Load ha-settings-row
await configRouter?.routerOptions?.routes?.entities?.load?.(); // Load ha-data-table
await customElements.whenDefined("ha-config-dashboard");
};
@ -132,3 +141,32 @@ export function throttle(timeout) {
};
};
}
export function runOnce(restart = false) {
return function (target, propertyKey, descriptor) {
const fn = descriptor.value;
let running = undefined;
const newfn = function (...rest) {
if (restart && running === false) running = true;
if (running !== undefined) return;
running = false;
const retval = fn.bind(this)(...rest);
if (running) {
running = undefined;
return newfn.bind(this)(...rest);
} else {
running = undefined;
return retval;
}
};
descriptor.value = newfn;
};
}
export async function waitRepeat(fn, times, delay) {
while (times--) {
fn();
await new Promise((r) => setTimeout(r, delay));
}
}

View File

@ -13,7 +13,7 @@ export const ConnectionMixin = (SuperClass) => {
public browserEntities = {};
LOG(...args) {
return;
if (window.browser_mod_log === undefined) return;
const dt = new Date();
console.log(`${dt.toLocaleTimeString()}`, ...args);
@ -24,7 +24,7 @@ export const ConnectionMixin = (SuperClass) => {
}
private fireEvent(event, detail = undefined) {
this.dispatchEvent(new CustomEvent(event, { detail }));
this.dispatchEvent(new CustomEvent(event, { detail, bubbles: true }));
}
private incoming_message(msg) {
@ -37,6 +37,7 @@ export const ConnectionMixin = (SuperClass) => {
this.update_config(msg.result);
}
this._connectionResolve?.();
this._connectionResolve = undefined;
}
private update_config(cfg) {

View File

@ -1,25 +1,37 @@
import { selectTree } from "../helpers";
import { await_element, waitRepeat, runOnce, selectTree } from "../helpers";
export const AutoSettingsMixin = (SuperClass) => {
return class AutoSettingsMixinClass extends SuperClass {
class AutoSettingsMixinClass extends SuperClass {
_faviconTemplateSubscription;
_titleTemplateSubscription;
__currentTitle = undefined;
@runOnce()
async runHideHeader() {
while (!(await this._hideHeader()))
await new Promise((r) => setTimeout(r, 500));
}
@runOnce(true)
async runUpdateTitle() {
await waitRepeat(() => this._updateTitle(), 3, 500);
}
constructor() {
super();
this._auto_settings_setup();
this.addEventListener("browser-mod-config-update", () =>
this._auto_settings_setup()
);
const runUpdates = async () => {
this.runUpdateTitle();
this.runHideHeader();
};
window.addEventListener("location-changed", () => {
this._updateTitle();
setTimeout(() => this._updateTitle(), 500);
setTimeout(() => this._updateTitle(), 1000);
setTimeout(() => this._updateTitle(), 5000);
this._auto_settings_setup();
this.addEventListener("browser-mod-config-update", () => {
this._auto_settings_setup();
runUpdates();
});
window.addEventListener("location-changed", runUpdates);
}
async _auto_settings_setup() {
@ -40,7 +52,7 @@ export const AutoSettingsMixin = (SuperClass) => {
// Default panel
if (settings.defaultPanel) {
localStorage.setItem("defaultPanel", settings.defaultPanel);
localStorage.setItem("defaultPanel", `"${settings.defaultPanel}"`);
}
// Hide sidebar
@ -55,18 +67,18 @@ export const AutoSettingsMixin = (SuperClass) => {
).then((el) => el?.remove?.());
}
// Hide header
if (settings.hideHeader === true) {
customElements.whenDefined("app-header-layout").then(() => {
const appHeader = customElements.get("app-header").prototype;
const _attached = appHeader.attached;
appHeader.attached = function () {
_attached.bind(this)();
this.style.setProperty("display", "none");
};
// Sidebar title
if (settings.sidebarTitle) {
selectTree(
document,
"home-assistant $ home-assistant-main $ app-drawer-layout app-drawer ha-sidebar $ .title"
).then((el) => {
if (el) (el as HTMLElement).innerHTML = settings.sidebarTitle;
});
}
// Hide header
// Favicon template
if (settings.faviconTemplate !== undefined) {
(async () => {
@ -111,7 +123,6 @@ export const AutoSettingsMixin = (SuperClass) => {
_updateFavicon({ result }) {
const link: any = document.head.querySelector("link[rel~='icon']");
link.href = result;
window.browser_mod.fireEvent("browser-mod-favicon-update");
}
get _currentTitle() {
@ -121,7 +132,77 @@ export const AutoSettingsMixin = (SuperClass) => {
_updateTitle(data = undefined) {
if (data) this.__currentTitle = data.result;
if (this.__currentTitle) document.title = this.__currentTitle;
window.browser_mod.fireEvent("browser-mod-favicon-update");
}
};
async _hideHeader() {
if (this.settings.hideHeader !== true) return true;
let el = await selectTree(
document,
"home-assistant $ home-assistant-main $ app-drawer-layout partial-panel-resolver"
);
if (!el) return false;
let steps = 0;
while (el && el.localName !== "ha-app-layout" && steps++ < 5) {
await await_element(el, true);
const next =
el.querySelector("ha-app-layout") ??
el.firstElementChild ??
el.shadowRoot;
el = next;
}
if (el?.localName !== "ha-app-layout") return false;
if (el.header) {
el.header.style.setProperty("display", "none");
setTimeout(() => el._updateLayoutStates(), 0);
return true;
}
return false;
}
getSetting(key) {
const retval = { global: undefined, browser: {}, user: {} };
retval.global = this._data.settings?.[key];
for (const [k, v] of Object.entries(this._data.browsers ?? {})) {
if ((v as any).settings?.[key] != null)
retval.browser[k] = (v as any).settings[key];
}
for (const [k, v] of Object.entries(this._data.user_settings ?? {})) {
if (v[key] != null) retval.user[k] = v[key];
}
return retval;
}
setSetting(type, target, settings) {
if (type === "global") {
for (const [key, value] of Object.entries(settings))
this.connection.sendMessage({
type: "browser_mod/settings",
key,
value,
});
} else if (type === "browser") {
const browser = this._data.browsers[target];
const newsettings = { ...browser.settings, ...settings };
console.log(newsettings);
this.connection.sendMessage({
type: "browser_mod/register",
browserID: target,
data: {
...browser,
settings: newsettings,
},
});
} else if (type === "user") {
const user = target;
for (const [key, value] of Object.entries(settings))
this.connection.sendMessage({
type: "browser_mod/settings",
user,
key,
value,
});
}
}
}
return AutoSettingsMixinClass;
};

View File

@ -223,7 +223,6 @@ class BrowserModPopup extends LitElement {
static get styles() {
return css`
ha-dialog {
--dialog-backdrop-filter: blur(5px);
z-index: 10;
--mdc-dialog-min-width: var(--popup-min-width, 400px);
--mdc-dialog-max-width: var(--popup-max-width, 600px);

View File

@ -32,6 +32,7 @@ interface FullyKiosk {
declare global {
interface Window {
browser_mod?: BrowserMod;
browser_mod_log?: any;
fully?: FullyKiosk;
hassConnection?: Promise<any>;
customCards?: [{}?];

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "browser_mod",
"version": "2.0.0b2",
"version": "2.0.0b4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

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

25
test/automations.yaml Normal file
View File

@ -0,0 +1,25 @@
- id: "1660669793583"
alias: Toggle bed light
description: ""
trigger:
- platform: time_pattern
seconds: /3
condition: []
action:
- type: toggle
device_id: 98861bdf58b3c79183c03be06da14f27
entity_id: light.bed_light
domain: light
mode: single
- alias: Popup when kitchen light togggled
trigger:
- platform: state
entity_id: light.kitchen_lights
action:
- service: browser_mod.popup
data:
title: automation
content:
type: markdown
content: "{%raw%}{{states('light.bed_light')}}{%endraw%}"

View File

@ -1,5 +1,7 @@
default_config:
automation: !include test/automations.yaml
demo:
http:
@ -12,6 +14,8 @@ logger:
logs:
custom_components.browser_mod: info
# debugpy:
# browser_mod:
# devices:
# camdevice:

View File

@ -32,6 +32,7 @@ views:
action: more-info
- !include views/popup.yaml
- !include views/frontend-backend.yaml
- title: Popup card
popup_cards:

View File

@ -0,0 +1,30 @@
title: frontend vs backend
cards:
- type: entities
entities:
- light.bed_light
- light.kitchen_lights
- type: button
name: fire-dom-event
tap_action:
action: fire-dom-event
browser_mod:
service: browser_mod.popup
data:
title: fire-dom-event
content:
type: markdown
content: "{{states('light.bed_light')}}"
- type: button
name: call-service
tap_action:
action: call-service
service: browser_mod.popup
data:
title: call-service
content:
type: markdown
content: "{{states('light.bed_light')}}"