mirror of
https://github.com/home-assistant/frontend.git
synced 2025-12-05 07:37:20 +00:00
Compare commits
79 Commits
fix-long-t
...
light-info
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6af277a71e | ||
|
|
c4d8aba5c8 | ||
|
|
39f24c41ad | ||
|
|
21644ec889 | ||
|
|
613470b44d | ||
|
|
6c918e346b | ||
|
|
bce8539572 | ||
|
|
aab86e00ec | ||
|
|
2a58726caf | ||
|
|
4163b35b32 | ||
|
|
9c6dac8180 | ||
|
|
80fc37724b | ||
|
|
77b25f5132 | ||
|
|
684f098450 | ||
|
|
d09f74d30f | ||
|
|
3d973b112e | ||
|
|
96986164a4 | ||
|
|
2bb64e9e2f | ||
|
|
746844dfc8 | ||
|
|
41b613a2d7 | ||
|
|
283b134d84 | ||
|
|
271eb614cd | ||
|
|
16167bef07 | ||
|
|
1eac9fa1cd | ||
|
|
7f819f0020 | ||
|
|
dec1f99a5f | ||
|
|
c705e74fc8 | ||
|
|
01df10f93e | ||
|
|
9877f08cf4 | ||
|
|
02791c51ae | ||
|
|
49683326e6 | ||
|
|
947773a82e | ||
|
|
2a229df624 | ||
|
|
0d4f43472b | ||
|
|
b30e467685 | ||
|
|
a56c0b52d5 | ||
|
|
c17ebfd279 | ||
|
|
5400b1da96 | ||
|
|
69f4a618b2 | ||
|
|
16b8b6698c | ||
|
|
b29a700d40 | ||
|
|
bbb1468439 | ||
|
|
72f9d6a8d3 | ||
|
|
3ec8da1f17 | ||
|
|
dbea3848df | ||
|
|
33871435e1 | ||
|
|
2fb9a56e0b | ||
|
|
14e8f66ed7 | ||
|
|
e6f5072462 | ||
|
|
a64f50fa72 | ||
|
|
bb5f6e88d0 | ||
|
|
6991403203 | ||
|
|
410bd22f8a | ||
|
|
b81d823602 | ||
|
|
7bcbed80d7 | ||
|
|
8fb62ebf5f | ||
|
|
209dd9923f | ||
|
|
c75207e391 | ||
|
|
d957f36927 | ||
|
|
9ac459b6d9 | ||
|
|
e08b2817ba | ||
|
|
4ca13c409b | ||
|
|
0d515e2303 | ||
|
|
a2153bc6aa | ||
|
|
ca171afe6f | ||
|
|
bf4e97bd48 | ||
|
|
8c59a12a03 | ||
|
|
89569355be | ||
|
|
3a41b3bdcf | ||
|
|
12bd7037b3 | ||
|
|
ca4f573be0 | ||
|
|
07fceeab5a | ||
|
|
3aa376e912 | ||
|
|
92d30a8896 | ||
|
|
83876fb9da | ||
|
|
29bdf7877c | ||
|
|
29199e2782 | ||
|
|
68e1378615 | ||
|
|
cf7efb5bfc |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"extends": [
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"airbnb-typescript/base",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:wc/recommended",
|
||||
"plugin:lit/recommended",
|
||||
"prettier",
|
||||
@@ -45,16 +45,16 @@
|
||||
"func-names": 0,
|
||||
"prefer-arrow-callback": 0,
|
||||
"no-underscore-dangle": 0,
|
||||
"no-var": 0,
|
||||
"strict": 0,
|
||||
"prefer-spread": 0,
|
||||
"no-plusplus": 0,
|
||||
"no-bitwise": 0,
|
||||
"no-bitwise": 2,
|
||||
"comma-dangle": 0,
|
||||
"vars-on-top": 0,
|
||||
"no-continue": 0,
|
||||
"no-param-reassign": 0,
|
||||
"no-multi-assign": 0,
|
||||
"no-console": 2,
|
||||
"radix": 0,
|
||||
"no-alert": 0,
|
||||
"no-return-await": 0,
|
||||
|
||||
@@ -147,6 +147,10 @@
|
||||
"path": "M21.11,18.5C20.97,18.5 20.83,18.44 20.71,18.36C20.37,18.13 20.28,17.68 20.5,17.34C21.18,16.34 21.54,15.16 21.54,13.93C21.54,12.71 21.18,11.53 20.5,10.5C20.28,10.18 20.37,9.73 20.71,9.5C21.04,9.28 21.5,9.37 21.72,9.7C22.56,10.95 23,12.41 23,13.93C23,15.45 22.56,16.91 21.72,18.16C21.58,18.37 21.35,18.5 21.11,18.5M19,17.29C18.88,17.29 18.74,17.25 18.61,17.17C18.28,16.94 18.19,16.5 18.42,16.15C18.86,15.5 19.1,14.73 19.1,13.93C19.1,13.14 18.86,12.37 18.42,11.71C18.19,11.37 18.28,10.92 18.61,10.69C18.95,10.47 19.4,10.55 19.63,10.89C20.24,11.79 20.56,12.84 20.56,13.93C20.56,15 20.24,16.07 19.63,16.97C19.5,17.18 19.25,17.29 19,17.29M14.9,15.73C15.89,15.73 16.7,14.92 16.7,13.93C16.7,13.17 16.22,12.5 15.55,12.25C15.5,12.55 15.43,12.85 15.34,13.14C15.23,13.44 14.95,13.64 14.64,13.64C14.57,13.64 14.5,13.62 14.41,13.6C14.03,13.47 13.82,13.06 13.95,12.67C14.09,12.24 14.17,11.78 14.17,11.32C14.17,8.93 12.22,7 9.82,7C8.1,7 6.56,8 5.87,9.5C6.54,9.7 7.16,10.04 7.66,10.54C7.95,10.83 7.95,11.29 7.66,11.58C7.38,11.86 6.91,11.86 6.63,11.58C6.17,11.12 5.56,10.86 4.9,10.86C3.56,10.86 2.46,11.96 2.46,13.3C2.46,14.64 3.56,15.73 4.9,15.73H14.9M15.6,10.75C17.06,11.07 18.17,12.37 18.17,13.93C18.17,15.73 16.7,17.19 14.9,17.19H4.9C2.75,17.19 1,15.45 1,13.3C1,11.34 2.45,9.73 4.33,9.45C5.12,7.12 7.33,5.5 9.82,5.5C12.83,5.5 15.31,7.82 15.6,10.75Z",
|
||||
"name": "mixcloud"
|
||||
},
|
||||
{
|
||||
"path": "M5.68,3.96L11.41,11.65C11.55,11.84 11.55,12.1 11.41,12.29L5.65,20L5.5,20.18C4.76,21 3.47,21.07 2.64,20.31C1.85,19.59 1.79,18.37 2.43,17.5L6.56,11.97L2.46,6.47C1.83,5.62 1.88,4.39 2.67,3.67L2.82,3.54C3.73,2.87 5,3.05 5.68,3.96M18.32,3.96C19,3.05 20.27,2.87 21.18,3.54L21.33,3.67C22.12,4.39 22.17,5.61 21.54,6.47L17.44,11.97L21.57,17.5C22.21,18.36 22.15,19.59 21.36,20.31C20.53,21.07 19.24,21 18.5,20.18L18.35,20L12.59,12.29C12.45,12.1 12.45,11.84 12.59,11.65L18.32,3.96Z",
|
||||
"name": "mixer"
|
||||
},
|
||||
{
|
||||
"path": "M3.25,4.03L19.95,20.73L18.7,22L14.86,18.13C14.77,18.12 14.68,18.09 14.59,18.05C14.26,17.89 14.14,17.62 14.11,17.38L12.18,15.45C12.14,15.53 12.09,15.6 12.05,15.66C11.62,16.26 11.19,16.26 10.86,16.04C10.54,15.83 5.5,12 5.23,11.87C4.95,11.76 4.85,12.03 5.12,13.5C5.39,15 4.95,15.39 4.57,15.45C4.2,15.5 3.06,15.18 3,12.14C2.95,9.11 3.76,8.62 4.14,8.62C4.6,8.62 7.08,10.69 8.84,12.12L2,5.28L3.25,4.03M18.38,16.56C18.75,15.4 19.12,13.8 19.1,12.03V12C19.14,8.5 17.66,5.58 17.66,5.58C17.66,5.58 17.42,4.72 18.12,4.39C18.83,4.06 19.3,4.61 19.3,4.61C21.12,8.22 21,11.64 21,12C21,12.27 21.09,14.96 19.88,18.05L18.38,16.56M15.14,13.31C15.19,12.92 15.22,12.5 15.24,12.03V12C15.14,8.5 14.13,7.21 14.13,7.21C14.13,7.21 13.89,6.34 14.59,6C15.3,5.69 15.77,6.23 15.77,6.23C17.26,8.94 17.16,11.64 17.14,12C17.15,12.2 17.2,13.38 16.82,15L15.14,13.31M10.2,8.38C10.23,7.77 10.59,7.64 10.59,7.64C10.59,7.64 11.19,7.37 11.57,7.8C11.91,8.19 12.72,9.57 12.89,11.07L10.2,8.38Z",
|
||||
"name": "nfc-off"
|
||||
|
||||
@@ -22,6 +22,8 @@ class HcLovelace extends LitElement {
|
||||
|
||||
@property() public viewPath?: string | number;
|
||||
|
||||
public urlPath?: string | null;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const index = this._viewIndex;
|
||||
if (index === undefined) {
|
||||
@@ -35,6 +37,7 @@ class HcLovelace extends LitElement {
|
||||
const lovelace: Lovelace = {
|
||||
config: this.lovelaceConfig,
|
||||
editMode: false,
|
||||
urlPath: this.urlPath!,
|
||||
enableFullEditMode: () => undefined,
|
||||
mode: "storage",
|
||||
language: "en",
|
||||
|
||||
@@ -87,6 +87,7 @@ export class HcMain extends HassElement {
|
||||
.hass=${this.hass}
|
||||
.lovelaceConfig=${this._lovelaceConfig}
|
||||
.viewPath=${this._lovelacePath}
|
||||
.urlPath=${this._urlPath}
|
||||
@config-refresh=${this._generateLovelaceConfig}
|
||||
></hc-lovelace>
|
||||
`;
|
||||
|
||||
@@ -242,19 +242,23 @@ class HassioAddonInfo extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
<div class="security">
|
||||
<ha-label-badge
|
||||
class=${classMap({
|
||||
green: this.addon.stage === "stable",
|
||||
yellow: this.addon.stage === "experimental",
|
||||
red: this.addon.stage === "deprecated",
|
||||
})}
|
||||
@click=${this._showMoreInfo}
|
||||
id="stage"
|
||||
label="stage"
|
||||
description=""
|
||||
>
|
||||
<ha-svg-icon .path=${STAGE_ICON[this.addon.stage]}></ha-svg-icon>
|
||||
</ha-label-badge>
|
||||
${this.addon.stage !== "stable"
|
||||
? html` <ha-label-badge
|
||||
class=${classMap({
|
||||
yellow: this.addon.stage === "experimental",
|
||||
red: this.addon.stage === "deprecated",
|
||||
})}
|
||||
@click=${this._showMoreInfo}
|
||||
id="stage"
|
||||
label="stage"
|
||||
description=""
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${STAGE_ICON[this.addon.stage]}
|
||||
></ha-svg-icon>
|
||||
</ha-label-badge>`
|
||||
: ""}
|
||||
|
||||
<ha-label-badge
|
||||
class=${classMap({
|
||||
green: [5, 6].includes(Number(this.addon.rating)),
|
||||
|
||||
@@ -31,6 +31,10 @@ class HassioMarkdownDialog extends LitElement {
|
||||
this._opened = true;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._opened = false;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._opened) {
|
||||
return html``;
|
||||
@@ -38,7 +42,7 @@ class HassioMarkdownDialog extends LitElement {
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closing=${this._closeDialog}
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(this.hass, this.title)}
|
||||
>
|
||||
<ha-markdown .content=${this.content || ""}></ha-markdown>
|
||||
@@ -46,10 +50,6 @@ class HassioMarkdownDialog extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _closeDialog(): void {
|
||||
this._opened = false;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleDialog,
|
||||
|
||||
@@ -1,116 +1,35 @@
|
||||
import { PolymerElement } from "@polymer/polymer";
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
internalProperty,
|
||||
html,
|
||||
PropertyValues,
|
||||
customElement,
|
||||
LitElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import "./hassio-router";
|
||||
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
|
||||
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
|
||||
import { HomeAssistant, Route } from "../../src/types";
|
||||
import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
|
||||
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../src/common/dom/fire_event";
|
||||
import { navigate } from "../../src/common/navigate";
|
||||
import { fetchHassioAddonInfo } from "../../src/data/hassio/addon";
|
||||
import {
|
||||
fetchHassioHassOsInfo,
|
||||
fetchHassioHostInfo,
|
||||
HassioHassOSInfo,
|
||||
HassioHostInfo,
|
||||
} from "../../src/data/hassio/host";
|
||||
import {
|
||||
createHassioSession,
|
||||
fetchHassioHomeAssistantInfo,
|
||||
fetchHassioSupervisorInfo,
|
||||
fetchHassioInfo,
|
||||
HassioHomeAssistantInfo,
|
||||
HassioInfo,
|
||||
HassioPanelInfo,
|
||||
HassioSupervisorInfo,
|
||||
} from "../../src/data/hassio/supervisor";
|
||||
import {
|
||||
AlertDialogParams,
|
||||
showAlertDialog,
|
||||
} from "../../src/dialogs/generic/show-dialog-box";
|
||||
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
|
||||
import {
|
||||
HassRouterPage,
|
||||
RouterOptions,
|
||||
} from "../../src/layouts/hass-router-page";
|
||||
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
|
||||
import "../../src/resources/ha-style";
|
||||
import { HomeAssistant } from "../../src/types";
|
||||
// Don't codesplit it, that way the dashboard always loads fast.
|
||||
import "./hassio-panel";
|
||||
import { atLeastVersion } from "../../src/common/config/version";
|
||||
|
||||
@customElement("hassio-main")
|
||||
class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
||||
export class HassioMain extends urlSyncMixin(ProvideHassLitMixin(LitElement)) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public panel!: HassioPanelInfo;
|
||||
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
// Hass.io has a page with tabs, so we route all non-matching routes to it.
|
||||
defaultPage: "dashboard",
|
||||
initialLoad: () => this._fetchData(),
|
||||
showLoading: true,
|
||||
routes: {
|
||||
dashboard: {
|
||||
tag: "hassio-panel",
|
||||
cache: true,
|
||||
},
|
||||
snapshots: "dashboard",
|
||||
store: "dashboard",
|
||||
system: "dashboard",
|
||||
addon: {
|
||||
tag: "hassio-addon-dashboard",
|
||||
load: () =>
|
||||
import(
|
||||
/* webpackChunkName: "hassio-addon-dashboard" */ "./addon-view/hassio-addon-dashboard"
|
||||
),
|
||||
},
|
||||
ingress: {
|
||||
tag: "hassio-ingress-view",
|
||||
load: () =>
|
||||
import(
|
||||
/* webpackChunkName: "hassio-ingress-view" */ "./ingress-view/hassio-ingress-view"
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@internalProperty() private _supervisorInfo: HassioSupervisorInfo;
|
||||
|
||||
@internalProperty() private _hostInfo: HassioHostInfo;
|
||||
|
||||
@internalProperty() private _hassioInfo?: HassioInfo;
|
||||
|
||||
@internalProperty() private _hassOsInfo?: HassioHassOSInfo;
|
||||
|
||||
@internalProperty() private _hassInfo: HassioHomeAssistantInfo;
|
||||
@property() public route?: Route;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
applyThemesOnElement(
|
||||
this.parentElement,
|
||||
this.hass.themes,
|
||||
this.hass.selectedTheme || this.hass.themes.default_theme
|
||||
);
|
||||
this._applyTheme();
|
||||
|
||||
this.style.setProperty(
|
||||
"--app-header-background-color",
|
||||
"var(--sidebar-background-color)"
|
||||
);
|
||||
this.style.setProperty(
|
||||
"--app-header-text-color",
|
||||
"var(--sidebar-text-color)"
|
||||
);
|
||||
this.style.setProperty(
|
||||
"--app-header-border-bottom",
|
||||
"1px solid var(--divider-color)"
|
||||
);
|
||||
|
||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
||||
// Paulus - March 17, 2019
|
||||
// We went to a single hass-toggle-menu event in HA 0.90. However, the
|
||||
// supervisor UI can also run under older versions of Home Assistant.
|
||||
@@ -143,152 +62,61 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
||||
});
|
||||
});
|
||||
|
||||
makeDialogManager(this, document.body);
|
||||
makeDialogManager(this, this.shadowRoot!);
|
||||
}
|
||||
|
||||
protected updatePageEl(el) {
|
||||
// the tabs page does its own routing so needs full route.
|
||||
const route = el.nodeName === "HASSIO-PANEL" ? this.route : this.routeTail;
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass) {
|
||||
return;
|
||||
}
|
||||
if (oldHass.themes !== this.hass.themes) {
|
||||
this._applyTheme();
|
||||
}
|
||||
}
|
||||
|
||||
if ("setProperties" in el) {
|
||||
// As long as we have Polymer pages
|
||||
(el as PolymerElement).setProperties({
|
||||
hass: this.hass,
|
||||
narrow: this.narrow,
|
||||
supervisorInfo: this._supervisorInfo,
|
||||
hassioInfo: this._hassioInfo,
|
||||
hostInfo: this._hostInfo,
|
||||
hassInfo: this._hassInfo,
|
||||
hassOsInfo: this._hassOsInfo,
|
||||
route,
|
||||
});
|
||||
protected render() {
|
||||
return html`
|
||||
<hassio-router
|
||||
.hass=${this.hass}
|
||||
.route=${this.route}
|
||||
.panel=${this.panel}
|
||||
.narrow=${this.narrow}
|
||||
></hassio-router>
|
||||
`;
|
||||
}
|
||||
|
||||
private _applyTheme() {
|
||||
let themeName: string;
|
||||
let options: Partial<HomeAssistant["selectedTheme"]> | undefined;
|
||||
|
||||
if (atLeastVersion(this.hass.config.version, 0, 114)) {
|
||||
themeName =
|
||||
this.hass.selectedTheme?.theme ||
|
||||
(this.hass.themes.darkMode && this.hass.themes.default_dark_theme
|
||||
? this.hass.themes.default_dark_theme!
|
||||
: this.hass.themes.default_theme);
|
||||
|
||||
options = this.hass.selectedTheme;
|
||||
if (themeName === "default" && options?.dark === undefined) {
|
||||
options = {
|
||||
...this.hass.selectedTheme,
|
||||
dark: this.hass.themes.darkMode,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
el.hass = this.hass;
|
||||
el.narrow = this.narrow;
|
||||
el.supervisorInfo = this._supervisorInfo;
|
||||
el.hassioInfo = this._hassioInfo;
|
||||
el.hostInfo = this._hostInfo;
|
||||
el.hassInfo = this._hassInfo;
|
||||
el.hassOsInfo = this._hassOsInfo;
|
||||
el.route = route;
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchData() {
|
||||
if (this.panel.config && this.panel.config.ingress) {
|
||||
await this._redirectIngress(this.panel.config.ingress);
|
||||
return;
|
||||
themeName =
|
||||
((this.hass.selectedTheme as unknown) as string) ||
|
||||
this.hass.themes.default_theme;
|
||||
}
|
||||
|
||||
const [supervisorInfo, hostInfo, hassInfo, hassioInfo] = await Promise.all([
|
||||
fetchHassioSupervisorInfo(this.hass),
|
||||
fetchHassioHostInfo(this.hass),
|
||||
fetchHassioHomeAssistantInfo(this.hass),
|
||||
fetchHassioInfo(this.hass),
|
||||
]);
|
||||
this._supervisorInfo = supervisorInfo;
|
||||
this._hassioInfo = hassioInfo;
|
||||
this._hostInfo = hostInfo;
|
||||
this._hassInfo = hassInfo;
|
||||
|
||||
if (this._hostInfo.features && this._hostInfo.features.includes("hassos")) {
|
||||
this._hassOsInfo = await fetchHassioHassOsInfo(this.hass);
|
||||
}
|
||||
}
|
||||
|
||||
private async _redirectIngress(addonSlug: string) {
|
||||
// When we trigger a navigation, we sleep to make sure we don't
|
||||
// show the hassio dashboard before navigating away.
|
||||
const awaitAlert = async (
|
||||
alertParams: AlertDialogParams,
|
||||
action: () => void
|
||||
) => {
|
||||
await new Promise((resolve) => {
|
||||
alertParams.confirm = resolve;
|
||||
showAlertDialog(this, alertParams);
|
||||
});
|
||||
action();
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
};
|
||||
|
||||
const createSessionPromise = createHassioSession(this.hass).then(
|
||||
() => true,
|
||||
() => false
|
||||
applyThemesOnElement(
|
||||
this.parentElement,
|
||||
this.hass.themes,
|
||||
themeName,
|
||||
options
|
||||
);
|
||||
|
||||
let addon;
|
||||
|
||||
try {
|
||||
addon = await fetchHassioAddonInfo(this.hass, addonSlug);
|
||||
} catch (err) {
|
||||
await awaitAlert(
|
||||
{
|
||||
text: "Unable to fetch add-on info to start Ingress",
|
||||
title: "Supervisor",
|
||||
},
|
||||
() => history.back()
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!addon.ingress_url) {
|
||||
await awaitAlert(
|
||||
{
|
||||
text: "Add-on does not support Ingress",
|
||||
title: addon.name,
|
||||
},
|
||||
() => history.back()
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (addon.state !== "started") {
|
||||
await awaitAlert(
|
||||
{
|
||||
text: "Add-on is not running. Please start it first",
|
||||
title: addon.name,
|
||||
},
|
||||
() => navigate(this, `/hassio/addon/${addon.slug}/info`, true)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await createSessionPromise)) {
|
||||
await awaitAlert(
|
||||
{
|
||||
text: "Unable to create an Ingress session",
|
||||
title: addon.name,
|
||||
},
|
||||
() => history.back()
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
location.assign(addon.ingress_url);
|
||||
// await a promise that doesn't resolve, so we show the loading screen
|
||||
// while we load the next page.
|
||||
await new Promise(() => undefined);
|
||||
}
|
||||
|
||||
private _apiCalled(ev) {
|
||||
if (!ev.detail.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
let tries = 1;
|
||||
|
||||
const tryUpdate = () => {
|
||||
this._fetchData().catch(() => {
|
||||
tries += 1;
|
||||
setTimeout(tryUpdate, Math.min(tries, 5) * 1000);
|
||||
});
|
||||
};
|
||||
|
||||
tryUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
css,
|
||||
CSSResult,
|
||||
} from "lit-element";
|
||||
import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host";
|
||||
import {
|
||||
@@ -33,6 +35,9 @@ class HassioPanel extends LitElement {
|
||||
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.supervisorInfo) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<hassio-panel-router
|
||||
.route=${this.route}
|
||||
@@ -46,6 +51,16 @@ class HassioPanel extends LitElement {
|
||||
></hassio-panel-router>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
--app-header-background-color: var(--sidebar-background-color);
|
||||
--app-header-text-color: var(--sidebar-text-color);
|
||||
--app-header-border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
150
hassio/src/hassio-router.ts
Normal file
150
hassio/src/hassio-router.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import {
|
||||
fetchHassioHassOsInfo,
|
||||
fetchHassioHostInfo,
|
||||
HassioHassOSInfo,
|
||||
HassioHostInfo,
|
||||
} from "../../src/data/hassio/host";
|
||||
import {
|
||||
fetchHassioHomeAssistantInfo,
|
||||
fetchHassioSupervisorInfo,
|
||||
fetchHassioInfo,
|
||||
HassioHomeAssistantInfo,
|
||||
HassioInfo,
|
||||
HassioPanelInfo,
|
||||
HassioSupervisorInfo,
|
||||
} from "../../src/data/hassio/supervisor";
|
||||
import {
|
||||
HassRouterPage,
|
||||
RouterOptions,
|
||||
} from "../../src/layouts/hass-router-page";
|
||||
import "../../src/resources/ha-style";
|
||||
import { HomeAssistant } from "../../src/types";
|
||||
// Don't codesplit it, that way the dashboard always loads fast.
|
||||
import "./hassio-panel";
|
||||
|
||||
@customElement("hassio-router")
|
||||
class HassioRouter extends HassRouterPage {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public panel!: HassioPanelInfo;
|
||||
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
// Hass.io has a page with tabs, so we route all non-matching routes to it.
|
||||
defaultPage: "dashboard",
|
||||
initialLoad: () => this._fetchData(),
|
||||
showLoading: true,
|
||||
routes: {
|
||||
dashboard: {
|
||||
tag: "hassio-panel",
|
||||
cache: true,
|
||||
},
|
||||
snapshots: "dashboard",
|
||||
store: "dashboard",
|
||||
system: "dashboard",
|
||||
addon: {
|
||||
tag: "hassio-addon-dashboard",
|
||||
load: () =>
|
||||
import(
|
||||
/* webpackChunkName: "hassio-addon-dashboard" */ "./addon-view/hassio-addon-dashboard"
|
||||
),
|
||||
},
|
||||
ingress: {
|
||||
tag: "hassio-ingress-view",
|
||||
load: () =>
|
||||
import(
|
||||
/* webpackChunkName: "hassio-ingress-view" */ "./ingress-view/hassio-ingress-view"
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@internalProperty() private _supervisorInfo: HassioSupervisorInfo;
|
||||
|
||||
@internalProperty() private _hostInfo: HassioHostInfo;
|
||||
|
||||
@internalProperty() private _hassioInfo?: HassioInfo;
|
||||
|
||||
@internalProperty() private _hassOsInfo?: HassioHassOSInfo;
|
||||
|
||||
@internalProperty() private _hassInfo: HassioHomeAssistantInfo;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
||||
}
|
||||
|
||||
protected updatePageEl(el) {
|
||||
// the tabs page does its own routing so needs full route.
|
||||
const route = el.nodeName === "HASSIO-PANEL" ? this.route : this.routeTail;
|
||||
|
||||
el.hass = this.hass;
|
||||
el.narrow = this.narrow;
|
||||
el.supervisorInfo = this._supervisorInfo;
|
||||
el.hassioInfo = this._hassioInfo;
|
||||
el.hostInfo = this._hostInfo;
|
||||
el.hassInfo = this._hassInfo;
|
||||
el.hassOsInfo = this._hassOsInfo;
|
||||
el.route = route;
|
||||
|
||||
if (el.localName === "hassio-ingress-view") {
|
||||
el.ingressPanel = this.panel.config && this.panel.config.ingress;
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchData() {
|
||||
if (this.panel.config && this.panel.config.ingress) {
|
||||
this._redirectIngress(this.panel.config.ingress);
|
||||
return;
|
||||
}
|
||||
|
||||
const [supervisorInfo, hostInfo, hassInfo, hassioInfo] = await Promise.all([
|
||||
fetchHassioSupervisorInfo(this.hass),
|
||||
fetchHassioHostInfo(this.hass),
|
||||
fetchHassioHomeAssistantInfo(this.hass),
|
||||
fetchHassioInfo(this.hass),
|
||||
]);
|
||||
this._supervisorInfo = supervisorInfo;
|
||||
this._hassioInfo = hassioInfo;
|
||||
this._hostInfo = hostInfo;
|
||||
this._hassInfo = hassInfo;
|
||||
|
||||
if (this._hostInfo.features && this._hostInfo.features.includes("hassos")) {
|
||||
this._hassOsInfo = await fetchHassioHassOsInfo(this.hass);
|
||||
}
|
||||
}
|
||||
|
||||
private _redirectIngress(addonSlug: string) {
|
||||
this.route = { prefix: "/hassio", path: `/ingress/${addonSlug}` };
|
||||
}
|
||||
|
||||
private _apiCalled(ev) {
|
||||
if (!ev.detail.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
let tries = 1;
|
||||
|
||||
const tryUpdate = () => {
|
||||
this._fetchData().catch(() => {
|
||||
tries += 1;
|
||||
setTimeout(tryUpdate, Math.min(tries, 5) * 1000);
|
||||
});
|
||||
};
|
||||
|
||||
tryUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-router": HassioRouter;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,10 @@ import { createHassioSession } from "../../../src/data/hassio/supervisor";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import "../../../src/layouts/hass-subpage";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
import { mdiMenu } from "@mdi/js";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
|
||||
@customElement("hassio-ingress-view")
|
||||
class HassioIngressView extends LitElement {
|
||||
@@ -24,22 +28,45 @@ class HassioIngressView extends LitElement {
|
||||
|
||||
@property() public route!: Route;
|
||||
|
||||
@property() public ingressPanel = false;
|
||||
|
||||
@internalProperty() private _addon?: HassioAddonDetails;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public narrow = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._addon) {
|
||||
return html` <hass-loading-screen></hass-loading-screen> `;
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-subpage .header=${this._addon.name} hassio>
|
||||
<iframe src=${this._addon.ingress_url}></iframe>
|
||||
</hass-subpage>
|
||||
`;
|
||||
const iframe = html`<iframe src=${this._addon.ingress_url!}></iframe>`;
|
||||
|
||||
if (!this.ingressPanel) {
|
||||
return html`<hass-subpage
|
||||
.header=${this._addon.name}
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
${iframe}
|
||||
</hass-subpage>`;
|
||||
}
|
||||
|
||||
return html`${this.narrow || this.hass.dockedSidebar === "always_hidden"
|
||||
? html`<div class="header">
|
||||
<mwc-icon-button
|
||||
aria-label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
|
||||
@click=${this._toggleMenu}
|
||||
>
|
||||
<ha-svg-icon path=${mdiMenu}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
<div class="main-title">${this._addon.name}</div>
|
||||
</div>
|
||||
${iframe}`
|
||||
: iframe}`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
super.updated(changedProps);
|
||||
|
||||
if (!changedProps.has("route")) {
|
||||
return;
|
||||
@@ -56,27 +83,56 @@ class HassioIngressView extends LitElement {
|
||||
}
|
||||
|
||||
private async _fetchData(addonSlug: string) {
|
||||
const createSessionPromise = createHassioSession(this.hass).then(
|
||||
() => true,
|
||||
() => false
|
||||
);
|
||||
|
||||
let addon;
|
||||
|
||||
try {
|
||||
const [addon] = await Promise.all([
|
||||
fetchHassioAddonInfo(this.hass, addonSlug).catch(() => {
|
||||
throw new Error("Failed to fetch add-on info");
|
||||
}),
|
||||
createHassioSession(this.hass).catch(() => {
|
||||
throw new Error("Failed to create an ingress session");
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!addon.ingress) {
|
||||
throw new Error("This add-on does not support ingress");
|
||||
}
|
||||
|
||||
this._addon = addon;
|
||||
addon = await fetchHassioAddonInfo(this.hass, addonSlug);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line
|
||||
console.error(err);
|
||||
alert(err.message || "Unknown error starting ingress.");
|
||||
await showAlertDialog(this, {
|
||||
text: "Unable to fetch add-on info to start Ingress",
|
||||
title: "Supervisor",
|
||||
});
|
||||
history.back();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!addon.ingress_url) {
|
||||
await showAlertDialog(this, {
|
||||
text: "Add-on does not support Ingress",
|
||||
title: addon.name,
|
||||
});
|
||||
history.back();
|
||||
return;
|
||||
}
|
||||
|
||||
if (addon.state !== "started") {
|
||||
await showAlertDialog(this, {
|
||||
text: "Add-on is not running. Please start it first",
|
||||
title: addon.name,
|
||||
});
|
||||
navigate(this, `/hassio/addon/${addon.slug}/info`, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await createSessionPromise)) {
|
||||
await showAlertDialog(this, {
|
||||
text: "Unable to create an Ingress session",
|
||||
title: addon.name,
|
||||
});
|
||||
history.back();
|
||||
return;
|
||||
}
|
||||
|
||||
this._addon = addon;
|
||||
}
|
||||
|
||||
private _toggleMenu(): void {
|
||||
fireEvent(this, "hass-toggle-menu");
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
@@ -87,6 +143,41 @@ class HassioIngressView extends LitElement {
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.header + iframe {
|
||||
height: calc(100% - 40px);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
pointer-events: none;
|
||||
background-color: var(--app-header-background-color);
|
||||
font-weight: 400;
|
||||
color: var(--app-header-text-color, white);
|
||||
border-bottom: var(--app-header-border-bottom, none);
|
||||
box-sizing: border-box;
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
margin: 0 0 0 24px;
|
||||
line-height: 20px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
mwc-icon-button {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
hass-subpage {
|
||||
--app-header-background-color: var(--sidebar-background-color);
|
||||
--app-header-text-color: var(--sidebar-text-color);
|
||||
--app-header-border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,18 @@ import {
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/buttons/ha-call-api-button";
|
||||
import "../../../src/components/ha-card";
|
||||
import { HassioHostInfo as HassioHostInfoType } from "../../../src/data/hassio/host";
|
||||
import {
|
||||
HassioSupervisorInfo as HassioSupervisorInfoType,
|
||||
setSupervisorOption,
|
||||
SupervisorOptions,
|
||||
} from "../../../src/data/hassio/supervisor";
|
||||
import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import "../../../src/components/ha-switch";
|
||||
import {
|
||||
showConfirmationDialog,
|
||||
showAlertDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
@@ -28,6 +34,8 @@ class HassioSupervisorInfo extends LitElement {
|
||||
|
||||
@property() public supervisorInfo!: HassioSupervisorInfoType;
|
||||
|
||||
@property() public hostInfo!: HassioHostInfoType;
|
||||
|
||||
@internalProperty() private _errors?: string;
|
||||
|
||||
public render(): TemplateResult | void {
|
||||
@@ -55,8 +63,42 @@ class HassioSupervisorInfo extends LitElement {
|
||||
: ""}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="options">
|
||||
${this.supervisorInfo?.supported
|
||||
? html` <ha-settings-row>
|
||||
<span slot="heading">
|
||||
Share Diagnostics
|
||||
</span>
|
||||
<div slot="description" class="diagnostics-description">
|
||||
Share crash reports and diagnostic information.
|
||||
<button
|
||||
class="link"
|
||||
@click=${this._diagnosticsInformationDialog}
|
||||
>
|
||||
Learn more
|
||||
</button>
|
||||
</div>
|
||||
<ha-switch
|
||||
.checked=${this.supervisorInfo.diagnostics}
|
||||
@change=${this._toggleDiagnostics}
|
||||
></ha-switch>
|
||||
</ha-settings-row>`
|
||||
: html`<div class="error">
|
||||
You are running an unsupported installation.
|
||||
<a
|
||||
href="https://github.com/home-assistant/architecture/blob/master/adr/${this.hostInfo.features.includes(
|
||||
"hassos"
|
||||
)
|
||||
? "0015-home-assistant-os.md"
|
||||
: "0014-home-assistant-supervised.md"}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>Learn More</a
|
||||
>
|
||||
</div>`}
|
||||
</div>
|
||||
${this._errors
|
||||
? html` <div class="errors">Error: ${this._errors}</div> `
|
||||
? html` <div class="error">Error: ${this._errors}</div> `
|
||||
: ""}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
@@ -111,15 +153,23 @@ class HassioSupervisorInfo extends LitElement {
|
||||
box-sizing: border-box;
|
||||
height: calc(100% - 47px);
|
||||
}
|
||||
.info {
|
||||
.info,
|
||||
.options {
|
||||
width: 100%;
|
||||
}
|
||||
.info td:nth-child(2) {
|
||||
text-align: right;
|
||||
}
|
||||
.errors {
|
||||
color: var(--error-color);
|
||||
margin-top: 16px;
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
}
|
||||
button.link {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.diagnostics-description {
|
||||
white-space: normal;
|
||||
padding: 0;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
@@ -181,6 +231,40 @@ class HassioSupervisorInfo extends LitElement {
|
||||
this._errors = `Error joining beta channel, ${err.body?.message || err}`;
|
||||
}
|
||||
}
|
||||
|
||||
private async _diagnosticsInformationDialog() {
|
||||
await showAlertDialog(this, {
|
||||
title: "Help Improve Home Assistant",
|
||||
text: html`Would you want to automatically share crash reports and
|
||||
diagnostic information when the supervisor encounters unexpected errors?
|
||||
<br /><br />
|
||||
This will allow us to fix the problems, the information is only
|
||||
accessible to the Home Assistant Core team and will not be shared with
|
||||
others.
|
||||
<br /><br />
|
||||
The data does not include any private/sensitive information and you can
|
||||
disable this in settings at any time you want.`,
|
||||
});
|
||||
}
|
||||
|
||||
private async _toggleDiagnostics() {
|
||||
try {
|
||||
const data: SupervisorOptions = {
|
||||
diagnostics: !this.supervisorInfo?.diagnostics,
|
||||
};
|
||||
await setSupervisorOption(this.hass, data);
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
path: "option",
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err) {
|
||||
this._errors = `Error changing supervisor setting, ${
|
||||
err.body?.message || err
|
||||
}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -56,6 +56,7 @@ class HassioSystem extends LitElement {
|
||||
<div class="card-group">
|
||||
<hassio-supervisor-info
|
||||
.hass=${this.hass}
|
||||
.hostInfo=${this.hostInfo}
|
||||
.supervisorInfo=${this.supervisorInfo}
|
||||
></hassio-supervisor-info>
|
||||
<hassio-host-info
|
||||
|
||||
45
package.json
45
package.json
@@ -25,23 +25,24 @@
|
||||
"@formatjs/intl-pluralrules": "^1.5.8",
|
||||
"@fullcalendar/core": "^5.0.0-beta.2",
|
||||
"@fullcalendar/daygrid": "^5.0.0-beta.2",
|
||||
"@material/chips": "=8.0.0-canary.a78ceb112.0",
|
||||
"@material/chips": "=8.0.0-canary.096a7a066.0",
|
||||
"@material/circular-progress": "=8.0.0-canary.a78ceb112.0",
|
||||
"@material/mwc-button": "^0.17.2",
|
||||
"@material/mwc-checkbox": "^0.17.2",
|
||||
"@material/mwc-dialog": "^0.17.2",
|
||||
"@material/mwc-fab": "^0.17.2",
|
||||
"@material/mwc-formfield": "^0.17.2",
|
||||
"@material/mwc-icon-button": "^0.17.2",
|
||||
"@material/mwc-list": "^0.17.2",
|
||||
"@material/mwc-menu": "^0.17.2",
|
||||
"@material/mwc-ripple": "^0.17.2",
|
||||
"@material/mwc-switch": "^0.17.2",
|
||||
"@material/mwc-tab": "^0.17.2",
|
||||
"@material/mwc-tab-bar": "^0.17.2",
|
||||
"@material/top-app-bar": "=8.0.0-canary.a78ceb112.0",
|
||||
"@mdi/js": "5.3.45",
|
||||
"@mdi/svg": "5.3.45",
|
||||
"@material/mwc-button": "^0.18.0",
|
||||
"@material/mwc-checkbox": "^0.18.0",
|
||||
"@material/mwc-dialog": "^0.18.0",
|
||||
"@material/mwc-fab": "^0.18.0",
|
||||
"@material/mwc-formfield": "^0.18.0",
|
||||
"@material/mwc-icon-button": "^0.18.0",
|
||||
"@material/mwc-list": "^0.18.0",
|
||||
"@material/mwc-menu": "^0.18.0",
|
||||
"@material/mwc-radio": "^0.18.0",
|
||||
"@material/mwc-ripple": "^0.18.0",
|
||||
"@material/mwc-switch": "^0.18.0",
|
||||
"@material/mwc-tab": "^0.18.0",
|
||||
"@material/mwc-tab-bar": "^0.18.0",
|
||||
"@material/top-app-bar": "=8.0.0-canary.096a7a066.0",
|
||||
"@mdi/js": "5.4.55",
|
||||
"@mdi/svg": "5.4.55",
|
||||
"@polymer/app-layout": "^3.0.2",
|
||||
"@polymer/app-route": "^3.0.2",
|
||||
"@polymer/app-storage": "^3.0.2",
|
||||
@@ -100,11 +101,12 @@
|
||||
"lit-element": "^2.3.1",
|
||||
"lit-html": "^1.2.1",
|
||||
"lit-virtualizer": "^0.4.2",
|
||||
"marked": "^0.6.1",
|
||||
"marked": "^1.1.1",
|
||||
"mdn-polyfills": "^5.16.0",
|
||||
"memoize-one": "^5.0.2",
|
||||
"node-vibrant": "^3.1.5",
|
||||
"proxy-polyfill": "^0.3.1",
|
||||
"punycode": "^2.1.1",
|
||||
"regenerator-runtime": "^0.13.2",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
@@ -136,11 +138,12 @@
|
||||
"@rollup/plugin-replace": "^2.3.2",
|
||||
"@types/chai": "^4.1.7",
|
||||
"@types/chromecast-caf-receiver": "^3.0.12",
|
||||
"@types/codemirror": "^0.0.78",
|
||||
"@types/codemirror": "^0.0.97",
|
||||
"@types/hls.js": "^0.12.3",
|
||||
"@types/js-yaml": "^3.12.1",
|
||||
"@types/leaflet": "^1.4.3",
|
||||
"@types/leaflet-draw": "^1.0.1",
|
||||
"@types/marked": "^1.1.0",
|
||||
"@types/memoize-one": "4.1.0",
|
||||
"@types/mocha": "^5.2.6",
|
||||
"@types/resize-observer-browser": "^0.1.3",
|
||||
@@ -210,7 +213,11 @@
|
||||
"@webcomponents/webcomponentsjs": "^2.2.10",
|
||||
"@polymer/polymer": "3.1.0",
|
||||
"lit-html": "1.2.1",
|
||||
"lit-element": "2.3.1"
|
||||
"lit-element": "2.3.1",
|
||||
"@material/animation": "8.0.0-canary.096a7a066.0",
|
||||
"@material/base": "8.0.0-canary.096a7a066.0",
|
||||
"@material/feature-targeting": "8.0.0-canary.096a7a066.0",
|
||||
"@material/theme": "8.0.0-canary.096a7a066.0"
|
||||
},
|
||||
"main": "src/home-assistant.js",
|
||||
"husky": {
|
||||
|
||||
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20200716.0",
|
||||
version="20200811.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||
author="The Home Assistant Authors",
|
||||
|
||||
@@ -16,6 +16,7 @@ import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
|
||||
import { registerServiceWorker } from "../util/register-service-worker";
|
||||
import "./ha-auth-flow";
|
||||
import { extractSearchParamsObject } from "../common/url/search-params";
|
||||
import punycode from "punycode";
|
||||
|
||||
import(/* webpackChunkName: "pick-auth-provider" */ "./ha-pick-auth-provider");
|
||||
|
||||
@@ -75,7 +76,7 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
${this.localize(
|
||||
"ui.panel.page-authorize.authorizing_client",
|
||||
"clientId",
|
||||
this.clientId
|
||||
this.clientId ? punycode.toASCII(this.clientId) : this.clientId
|
||||
)}
|
||||
</p>
|
||||
${loggingInWith}
|
||||
|
||||
113
src/common/color/convert-color.ts
Normal file
113
src/common/color/convert-color.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
const expand_hex = (hex: string): string => {
|
||||
let result = "";
|
||||
for (const val of hex) {
|
||||
result += val + val;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const rgb_hex = (component: number): string => {
|
||||
const hex = Math.round(Math.min(Math.max(component, 0), 255)).toString(16);
|
||||
return hex.length === 1 ? `0${hex}` : hex;
|
||||
};
|
||||
|
||||
// Conversion between HEX and RGB
|
||||
|
||||
export const hex2rgb = (hex: string): [number, number, number] => {
|
||||
hex = hex.replace("#", "");
|
||||
if (hex.length === 3 || hex.length === 4) {
|
||||
hex = expand_hex(hex);
|
||||
}
|
||||
|
||||
return [
|
||||
parseInt(hex.substring(0, 2), 16),
|
||||
parseInt(hex.substring(2, 4), 16),
|
||||
parseInt(hex.substring(4, 6), 16),
|
||||
];
|
||||
};
|
||||
|
||||
export const rgb2hex = (rgb: [number, number, number]): string => {
|
||||
return `#${rgb_hex(rgb[0])}${rgb_hex(rgb[1])}${rgb_hex(rgb[2])}`;
|
||||
};
|
||||
|
||||
// Conversion between LAB, XYZ and RGB from https://github.com/gka/chroma.js
|
||||
// Copyright (c) 2011-2019, Gregor Aisch
|
||||
|
||||
// Constants for XYZ and LAB conversion
|
||||
const Xn = 0.95047;
|
||||
const Yn = 1;
|
||||
const Zn = 1.08883;
|
||||
|
||||
const t0 = 0.137931034; // 4 / 29
|
||||
const t1 = 0.206896552; // 6 / 29
|
||||
const t2 = 0.12841855; // 3 * t1 * t1
|
||||
const t3 = 0.008856452; // t1 * t1 * t1
|
||||
|
||||
const rgb_xyz = (r: number) => {
|
||||
r /= 255;
|
||||
if (r <= 0.04045) {
|
||||
return r / 12.92;
|
||||
}
|
||||
return ((r + 0.055) / 1.055) ** 2.4;
|
||||
};
|
||||
|
||||
const xyz_lab = (t: number) => {
|
||||
if (t > t3) {
|
||||
return t ** (1 / 3);
|
||||
}
|
||||
return t / t2 + t0;
|
||||
};
|
||||
|
||||
const xyz_rgb = (r: number) => {
|
||||
return 255 * (r <= 0.00304 ? 12.92 * r : 1.055 * r ** (1 / 2.4) - 0.055);
|
||||
};
|
||||
|
||||
const lab_xyz = (t: number) => {
|
||||
return t > t1 ? t * t * t : t2 * (t - t0);
|
||||
};
|
||||
|
||||
// Conversions between RGB and LAB
|
||||
|
||||
const rgb2xyz = (rgb: [number, number, number]): [number, number, number] => {
|
||||
let [r, g, b] = rgb;
|
||||
r = rgb_xyz(r);
|
||||
g = rgb_xyz(g);
|
||||
b = rgb_xyz(b);
|
||||
const x = xyz_lab((0.4124564 * r + 0.3575761 * g + 0.1804375 * b) / Xn);
|
||||
const y = xyz_lab((0.2126729 * r + 0.7151522 * g + 0.072175 * b) / Yn);
|
||||
const z = xyz_lab((0.0193339 * r + 0.119192 * g + 0.9503041 * b) / Zn);
|
||||
return [x, y, z];
|
||||
};
|
||||
|
||||
export const rgb2lab = (
|
||||
rgb: [number, number, number]
|
||||
): [number, number, number] => {
|
||||
const [x, y, z] = rgb2xyz(rgb);
|
||||
const l = 116 * y - 16;
|
||||
return [l < 0 ? 0 : l, 500 * (x - y), 200 * (y - z)];
|
||||
};
|
||||
|
||||
export const lab2rgb = (
|
||||
lab: [number, number, number]
|
||||
): [number, number, number] => {
|
||||
const [l, a, b] = lab;
|
||||
|
||||
let y = (l + 16) / 116;
|
||||
let x = isNaN(a) ? y : y + a / 500;
|
||||
let z = isNaN(b) ? y : y - b / 200;
|
||||
|
||||
y = Yn * lab_xyz(y);
|
||||
x = Xn * lab_xyz(x);
|
||||
z = Zn * lab_xyz(z);
|
||||
|
||||
const r = xyz_rgb(3.2404542 * x - 1.5371385 * y - 0.4985314 * z); // D65 -> sRGB
|
||||
const g = xyz_rgb(-0.969266 * x + 1.8760108 * y + 0.041556 * z);
|
||||
const b_ = xyz_rgb(0.0556434 * x - 0.2040259 * y + 1.0572252 * z);
|
||||
|
||||
return [r, g, b_];
|
||||
};
|
||||
|
||||
export const lab2hex = (lab: [number, number, number]): string => {
|
||||
const rgb = lab2rgb(lab);
|
||||
return rgb2hex(rgb);
|
||||
};
|
||||
16
src/common/color/lab.ts
Normal file
16
src/common/color/lab.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// From https://github.com/gka/chroma.js
|
||||
// Copyright (c) 2011-2019, Gregor Aisch
|
||||
|
||||
export const labDarken = (
|
||||
lab: [number, number, number],
|
||||
amount = 1
|
||||
): [number, number, number] => {
|
||||
return [lab[0] - 18 * amount, lab[1], lab[2]];
|
||||
};
|
||||
|
||||
export const labBrighten = (
|
||||
lab: [number, number, number],
|
||||
amount = 1
|
||||
): [number, number, number] => {
|
||||
return labDarken(lab, -amount);
|
||||
};
|
||||
24
src/common/color/rgb.ts
Normal file
24
src/common/color/rgb.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
const luminosity = (rgb: [number, number, number]): number => {
|
||||
// http://www.w3.org/TR/WCAG20/#relativeluminancedef
|
||||
const lum: [number, number, number] = [0, 0, 0];
|
||||
for (let i = 0; i < rgb.length; i++) {
|
||||
const chan = rgb[i] / 255;
|
||||
lum[i] = chan <= 0.03928 ? chan / 12.92 : ((chan + 0.055) / 1.055) ** 2.4;
|
||||
}
|
||||
|
||||
return 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2];
|
||||
};
|
||||
|
||||
export const rgbContrast = (
|
||||
color1: [number, number, number],
|
||||
color2: [number, number, number]
|
||||
) => {
|
||||
const lum1 = luminosity(color1);
|
||||
const lum2 = luminosity(color2);
|
||||
|
||||
if (lum1 > lum2) {
|
||||
return (lum1 + 0.05) / (lum2 + 0.05);
|
||||
}
|
||||
|
||||
return (lum2 + 0.05) / (lum1 + 0.05);
|
||||
};
|
||||
@@ -1,26 +1,20 @@
|
||||
import { derivedStyles } from "../../resources/styles";
|
||||
import { derivedStyles, darkStyles } from "../../resources/styles";
|
||||
import { HomeAssistant, Theme } from "../../types";
|
||||
import {
|
||||
hex2rgb,
|
||||
rgb2hex,
|
||||
rgb2lab,
|
||||
lab2rgb,
|
||||
lab2hex,
|
||||
} from "../color/convert-color";
|
||||
import { rgbContrast } from "../color/rgb";
|
||||
import { labDarken, labBrighten } from "../color/lab";
|
||||
|
||||
interface ProcessedTheme {
|
||||
keys: { [key: string]: "" };
|
||||
styles: { [key: string]: string };
|
||||
}
|
||||
|
||||
const hexToRgb = (hex: string): string | null => {
|
||||
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
|
||||
const checkHex = hex.replace(shorthandRegex, (_m, r, g, b) => {
|
||||
return r + r + g + g + b + b;
|
||||
});
|
||||
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(checkHex);
|
||||
return result
|
||||
? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(
|
||||
result[3],
|
||||
16
|
||||
)}`
|
||||
: null;
|
||||
};
|
||||
|
||||
let PROCESSED_THEMES: { [key: string]: ProcessedTheme } = {};
|
||||
|
||||
/**
|
||||
@@ -33,17 +27,56 @@ let PROCESSED_THEMES: { [key: string]: ProcessedTheme } = {};
|
||||
export const applyThemesOnElement = (
|
||||
element,
|
||||
themes: HomeAssistant["themes"],
|
||||
selectedTheme?: string
|
||||
selectedTheme?: string,
|
||||
themeOptions?: Partial<HomeAssistant["selectedTheme"]>
|
||||
) => {
|
||||
const newTheme = selectedTheme
|
||||
? PROCESSED_THEMES[selectedTheme] || processTheme(selectedTheme, themes)
|
||||
: undefined;
|
||||
let cacheKey = selectedTheme;
|
||||
let themeRules: Partial<Theme> = {};
|
||||
|
||||
if (!element._themes && !newTheme) {
|
||||
if (selectedTheme === "default" && themeOptions) {
|
||||
if (themeOptions.dark) {
|
||||
cacheKey = `${cacheKey}__dark`;
|
||||
themeRules = darkStyles;
|
||||
}
|
||||
if (themeOptions.primaryColor) {
|
||||
cacheKey = `${cacheKey}__primary_${themeOptions.primaryColor}`;
|
||||
const rgbPrimaryColor = hex2rgb(themeOptions.primaryColor);
|
||||
const labPrimaryColor = rgb2lab(rgbPrimaryColor);
|
||||
themeRules["primary-color"] = themeOptions.primaryColor;
|
||||
const rgbLigthPrimaryColor = lab2rgb(labBrighten(labPrimaryColor));
|
||||
themeRules["light-primary-color"] = rgb2hex(rgbLigthPrimaryColor);
|
||||
themeRules["dark-primary-color"] = lab2hex(labDarken(labPrimaryColor));
|
||||
themeRules["text-primary-color"] =
|
||||
rgbContrast(rgbPrimaryColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
|
||||
themeRules["text-light-primary-color"] =
|
||||
rgbContrast(rgbLigthPrimaryColor, [33, 33, 33]) < 6
|
||||
? "#fff"
|
||||
: "#212121";
|
||||
themeRules["state-icon-color"] = themeRules["dark-primary-color"];
|
||||
}
|
||||
if (themeOptions.accentColor) {
|
||||
cacheKey = `${cacheKey}__accent_${themeOptions.accentColor}`;
|
||||
themeRules["accent-color"] = themeOptions.accentColor;
|
||||
const rgbAccentColor = hex2rgb(themeOptions.accentColor);
|
||||
themeRules["text-accent-color"] =
|
||||
rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedTheme && themes.themes[selectedTheme]) {
|
||||
themeRules = themes.themes[selectedTheme];
|
||||
}
|
||||
|
||||
if (!element._themes && !Object.keys(themeRules).length) {
|
||||
// No styles to reset, and no styles to set
|
||||
return;
|
||||
}
|
||||
|
||||
const newTheme =
|
||||
themeRules && cacheKey
|
||||
? PROCESSED_THEMES[cacheKey] || processTheme(cacheKey, themeRules)
|
||||
: undefined;
|
||||
|
||||
// Add previous set keys to reset them, and new theme
|
||||
const styles = { ...element._themes, ...newTheme?.styles };
|
||||
element._themes = newTheme?.keys;
|
||||
@@ -58,42 +91,45 @@ export const applyThemesOnElement = (
|
||||
};
|
||||
|
||||
const processTheme = (
|
||||
themeName: string,
|
||||
themes: HomeAssistant["themes"]
|
||||
cacheKey: string,
|
||||
theme: Partial<Theme>
|
||||
): ProcessedTheme | undefined => {
|
||||
if (!themes.themes[themeName]) {
|
||||
if (!theme || !Object.keys(theme).length) {
|
||||
return undefined;
|
||||
}
|
||||
const theme: Theme = {
|
||||
const combinedTheme: Partial<Theme> = {
|
||||
...derivedStyles,
|
||||
...themes.themes[themeName],
|
||||
...theme,
|
||||
};
|
||||
const styles = {};
|
||||
const keys = {};
|
||||
for (const key of Object.keys(theme)) {
|
||||
for (const key of Object.keys(combinedTheme)) {
|
||||
const prefixedKey = `--${key}`;
|
||||
const value = theme[key];
|
||||
const value = combinedTheme[key]!;
|
||||
styles[prefixedKey] = value;
|
||||
keys[prefixedKey] = "";
|
||||
|
||||
// Try to create a rgb value for this key if it is a hex color
|
||||
// Try to create a rgb value for this key if it is not a var
|
||||
if (!value.startsWith("#")) {
|
||||
// Not a hex color
|
||||
// Can't convert non hex value
|
||||
continue;
|
||||
}
|
||||
|
||||
const rgbKey = `rgb-${key}`;
|
||||
if (theme[rgbKey] !== undefined) {
|
||||
if (combinedTheme[rgbKey] !== undefined) {
|
||||
// Theme has it's own rgb value
|
||||
continue;
|
||||
}
|
||||
const rgbValue = hexToRgb(value);
|
||||
if (rgbValue !== null) {
|
||||
try {
|
||||
const rgbValue = hex2rgb(value).join(",");
|
||||
const prefixedRgbKey = `--${rgbKey}`;
|
||||
styles[prefixedRgbKey] = rgbValue;
|
||||
keys[prefixedRgbKey] = "";
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
PROCESSED_THEMES[themeName] = { styles, keys };
|
||||
PROCESSED_THEMES[cacheKey] = { styles, keys };
|
||||
return { styles, keys };
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Map } from "leaflet";
|
||||
import type { Map, TileLayer } from "leaflet";
|
||||
|
||||
// Sets up a Leaflet map on the provided DOM element
|
||||
export type LeafletModuleType = typeof import("leaflet");
|
||||
@@ -6,9 +6,9 @@ export type LeafletDrawModuleType = typeof import("leaflet-draw");
|
||||
|
||||
export const setupLeafletMap = async (
|
||||
mapElement: HTMLElement,
|
||||
darkMode = false,
|
||||
darkMode?: boolean,
|
||||
draw = false
|
||||
): Promise<[Map, LeafletModuleType]> => {
|
||||
): Promise<[Map, LeafletModuleType, TileLayer]> => {
|
||||
if (!mapElement.parentNode) {
|
||||
throw new Error("Cannot setup Leaflet map on disconnected element");
|
||||
}
|
||||
@@ -28,15 +28,28 @@ export const setupLeafletMap = async (
|
||||
style.setAttribute("rel", "stylesheet");
|
||||
mapElement.parentNode.appendChild(style);
|
||||
map.setView([52.3731339, 4.8903147], 13);
|
||||
createTileLayer(Leaflet, darkMode).addTo(map);
|
||||
|
||||
return [map, Leaflet];
|
||||
const tileLayer = createTileLayer(Leaflet, Boolean(darkMode)).addTo(map);
|
||||
|
||||
return [map, Leaflet, tileLayer];
|
||||
};
|
||||
|
||||
export const createTileLayer = (
|
||||
export const replaceTileLayer = (
|
||||
leaflet: LeafletModuleType,
|
||||
map: Map,
|
||||
tileLayer: TileLayer,
|
||||
darkMode: boolean
|
||||
): TileLayer => {
|
||||
map.removeLayer(tileLayer);
|
||||
tileLayer = createTileLayer(leaflet, darkMode);
|
||||
tileLayer.addTo(map);
|
||||
return tileLayer;
|
||||
};
|
||||
|
||||
const createTileLayer = (
|
||||
leaflet: LeafletModuleType,
|
||||
darkMode: boolean
|
||||
) => {
|
||||
): TileLayer => {
|
||||
return leaflet.tileLayer(
|
||||
`https://{s}.basemaps.cartocdn.com/${
|
||||
darkMode ? "dark_all" : "light_all"
|
||||
|
||||
@@ -13,7 +13,7 @@ export const batteryIcon = (
|
||||
return "hass:battery-unknown";
|
||||
}
|
||||
|
||||
var icon = "hass:battery";
|
||||
let icon = "hass:battery";
|
||||
const batteryRound = Math.round(battery / 10) * 10;
|
||||
if (battery_charging && battery > 10) {
|
||||
icon += `-charging-${batteryRound}`;
|
||||
|
||||
@@ -4,6 +4,6 @@ export const supportsFeature = (
|
||||
stateObj: HassEntity,
|
||||
feature: number
|
||||
): boolean => {
|
||||
// eslint-disable-next-line:no-bitwise
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return (stateObj.attributes.supported_features! & feature) !== 0;
|
||||
};
|
||||
|
||||
@@ -2,11 +2,3 @@ const validEntityId = /^(\w+)\.(\w+)$/;
|
||||
|
||||
export const isValidEntityId = (entityId: string) =>
|
||||
validEntityId.test(entityId);
|
||||
|
||||
export const createValidEntityId = (input: string) =>
|
||||
input
|
||||
.toLowerCase()
|
||||
.replace(/\s|'|\./g, "_") // replace spaces, points and quotes with underscore
|
||||
.replace(/\W/g, "") // remove not allowed chars
|
||||
.replace(/_{2,}/g, "_") // replace multiple underscores with 1
|
||||
.replace(/_$/, ""); // remove underscores at the end
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// https://gist.github.com/hagemann/382adfc57adbd5af078dc93feef01fe1
|
||||
export const slugify = (value: string, delimiter = "-") => {
|
||||
export const slugify = (value: string, delimiter = "_") => {
|
||||
const a =
|
||||
"àáäâãåăæąçćčđďèéěėëêęğǵḧìíïîįłḿǹńňñòóöôœøṕŕřßşśšșťțùúüûǘůűūųẃẍÿýźžż·/_,:;";
|
||||
const b = `aaaaaaaaacccddeeeeeeegghiiiiilmnnnnooooooprrsssssttuuuuuuuuuwxyyzzz${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}`;
|
||||
|
||||
@@ -54,8 +54,8 @@ class HaCallServiceButton extends EventsMixin(PolymerElement) {
|
||||
callService() {
|
||||
this.progress = true;
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
var el = this;
|
||||
var eventData = {
|
||||
const el = this;
|
||||
const eventData = {
|
||||
domain: this.domain,
|
||||
service: this.service,
|
||||
serviceData: this.serviceData,
|
||||
|
||||
@@ -78,7 +78,7 @@ class HaProgressButton extends PolymerElement {
|
||||
}
|
||||
|
||||
tempClass(className) {
|
||||
var classList = this.$.container.classList;
|
||||
const classList = this.$.container.classList;
|
||||
classList.add(className);
|
||||
setTimeout(() => {
|
||||
classList.remove(className);
|
||||
|
||||
@@ -541,7 +541,7 @@ export class HaDataTable extends LitElement {
|
||||
border-radius: 4px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: rgba(var(--rgb-primary-text-color), 0.12);
|
||||
border-color: var(--divider-color);
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
@@ -559,7 +559,7 @@ export class HaDataTable extends LitElement {
|
||||
}
|
||||
|
||||
.mdc-data-table__row ~ .mdc-data-table__row {
|
||||
border-top: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.mdc-data-table__row:not(.mdc-data-table__row--selected):hover {
|
||||
@@ -578,7 +578,7 @@ export class HaDataTable extends LitElement {
|
||||
height: 56px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@@ -831,7 +831,7 @@ export class HaDataTable extends LitElement {
|
||||
right: 12px;
|
||||
}
|
||||
.table-header {
|
||||
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
padding: 0 16px;
|
||||
}
|
||||
search-input {
|
||||
|
||||
@@ -135,7 +135,7 @@ class DateRangePickerElement extends WrappedElement {
|
||||
}
|
||||
.daterangepicker td.in-range {
|
||||
background-color: var(--light-primary-color);
|
||||
color: var(--primary-text-color);
|
||||
color: var(--text-light-primary-color, var(--primary-text-color));
|
||||
}
|
||||
.daterangepicker td.active,
|
||||
.daterangepicker td.active:hover {
|
||||
|
||||
@@ -23,10 +23,10 @@ export const HaIronFocusablesHelper = {
|
||||
* @return {!Array<!HTMLElement>}
|
||||
*/
|
||||
getTabbableNodes: function (node) {
|
||||
var result = [];
|
||||
const result = [];
|
||||
// If there is at least one element with tabindex > 0, we need to sort
|
||||
// the final array by tabindex.
|
||||
var needsSortByTabIndex = this._collectTabbableNodes(node, result);
|
||||
const needsSortByTabIndex = this._collectTabbableNodes(node, result);
|
||||
if (needsSortByTabIndex) {
|
||||
return IronFocusablesHelper._sortByTabIndex(result);
|
||||
}
|
||||
@@ -50,9 +50,9 @@ export const HaIronFocusablesHelper = {
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
var element = /** @type {!HTMLElement} */ (node);
|
||||
var tabIndex = IronFocusablesHelper._normalizedTabIndex(element);
|
||||
var needsSort = tabIndex > 0;
|
||||
const element = /** @type {!HTMLElement} */ (node);
|
||||
const tabIndex = IronFocusablesHelper._normalizedTabIndex(element);
|
||||
let needsSort = tabIndex > 0;
|
||||
if (tabIndex >= 0) {
|
||||
result.push(element);
|
||||
}
|
||||
@@ -70,7 +70,7 @@ export const HaIronFocusablesHelper = {
|
||||
// <input id="B" slot="b" tabindex="1">
|
||||
// </div>
|
||||
// TODO(valdrin) support ShadowDOM v1 when upgrading to Polymer v2.0.
|
||||
var children;
|
||||
let children;
|
||||
if (element.localName === "content" || element.localName === "slot") {
|
||||
children = dom(element).getDistributedNodes();
|
||||
} else {
|
||||
@@ -80,7 +80,7 @@ export const HaIronFocusablesHelper = {
|
||||
children = dom(element.shadowRoot || element.root || element).children;
|
||||
// /////////////////////////
|
||||
}
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
// Ensure method is always invoked to collect tabbable children.
|
||||
needsSort = this._collectTabbableNodes(children[i], result) || needsSort;
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
? ""
|
||||
: this.image
|
||||
? this.image
|
||||
: state.attributes.entity_picture}"
|
||||
: state.attributes.entity_picture_local || state.attributes.entity_picture}"
|
||||
.label="${this._computeLabel(domain, state, this._timerTimeRemaining)}"
|
||||
.description="${this.name ? this.name : computeStateName(state)}"
|
||||
></ha-label-badge>
|
||||
|
||||
@@ -73,10 +73,10 @@ export class StateBadge extends LitElement {
|
||||
if (stateObj) {
|
||||
// hide icon if we have entity picture
|
||||
if (
|
||||
(stateObj.attributes.entity_picture && !this.overrideIcon) ||
|
||||
((stateObj.attributes.entity_picture_local || stateObj.attributes.entity_picture) && !this.overrideIcon) ||
|
||||
this.overrideImage
|
||||
) {
|
||||
let imageUrl = this.overrideImage || stateObj.attributes.entity_picture;
|
||||
let imageUrl = this.overrideImage || stateObj.attributes.entity_picture_local || stateObj.attributes.entity_picture;
|
||||
if (this.hass) {
|
||||
imageUrl = this.hass.hassUrl(imageUrl);
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ export class HaCard extends LitElement {
|
||||
}
|
||||
|
||||
:host ::slotted(.card-actions) {
|
||||
border-top: 1px solid #e8e8e8;
|
||||
border-top: 1px solid var(--divider-color, #e8e8e8);
|
||||
padding: 5px 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -188,10 +188,10 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
// origin is wheel center
|
||||
// returns {x: X, y: Y} object
|
||||
convertToCanvasCoordinates(clientX, clientY) {
|
||||
var svgPoint = this.interactionLayer.createSVGPoint();
|
||||
const svgPoint = this.interactionLayer.createSVGPoint();
|
||||
svgPoint.x = clientX;
|
||||
svgPoint.y = clientY;
|
||||
var cc = svgPoint.matrixTransform(
|
||||
const cc = svgPoint.matrixTransform(
|
||||
this.interactionLayer.getScreenCTM().inverse()
|
||||
);
|
||||
return { x: cc.x, y: cc.y };
|
||||
@@ -225,7 +225,7 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
// Touch events
|
||||
|
||||
onTouchStart(ev) {
|
||||
var touch = ev.changedTouches[0];
|
||||
const touch = ev.changedTouches[0];
|
||||
const cc = this.convertToCanvasCoordinates(touch.clientX, touch.clientY);
|
||||
// return if we're not on the wheel
|
||||
if (!this.isInWheel(cc.x, cc.y)) {
|
||||
@@ -275,8 +275,8 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
|
||||
// Process user input to color
|
||||
processUserSelect(ev) {
|
||||
var canvasXY = this.convertToCanvasCoordinates(ev.clientX, ev.clientY);
|
||||
var hs = this.getColor(canvasXY.x, canvasXY.y);
|
||||
const canvasXY = this.convertToCanvasCoordinates(ev.clientX, ev.clientY);
|
||||
const hs = this.getColor(canvasXY.x, canvasXY.y);
|
||||
this.onColorSelect(hs);
|
||||
}
|
||||
|
||||
@@ -319,11 +319,11 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
|
||||
// set marker position to the given color
|
||||
setMarkerOnColor(hs) {
|
||||
var dist = hs.s * this.radius;
|
||||
var theta = ((hs.h - 180) / 180) * Math.PI;
|
||||
var markerdX = -dist * Math.cos(theta);
|
||||
var markerdY = -dist * Math.sin(theta);
|
||||
var translateString = `translate(${markerdX},${markerdY})`;
|
||||
const dist = hs.s * this.radius;
|
||||
const theta = ((hs.h - 180) / 180) * Math.PI;
|
||||
const markerdX = -dist * Math.cos(theta);
|
||||
const markerdY = -dist * Math.sin(theta);
|
||||
const translateString = `translate(${markerdX},${markerdY})`;
|
||||
this.marker.setAttribute("transform", translateString);
|
||||
this.tooltip.setAttribute("transform", translateString);
|
||||
}
|
||||
@@ -358,8 +358,8 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
|
||||
// get angle (degrees)
|
||||
getAngle(dX, dY) {
|
||||
var theta = Math.atan2(-dY, -dX); // radians from the left edge, clockwise = positive
|
||||
var angle = (theta / Math.PI) * 180 + 180; // degrees, clockwise from right
|
||||
const theta = Math.atan2(-dY, -dX); // radians from the left edge, clockwise = positive
|
||||
const angle = (theta / Math.PI) * 180 + 180; // degrees, clockwise from right
|
||||
return angle;
|
||||
}
|
||||
|
||||
@@ -378,9 +378,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
*/
|
||||
|
||||
getColor(x, y) {
|
||||
var hue = this.getAngle(x, y); // degrees, clockwise from right
|
||||
var relativeDistance = this.getDistance(x, y); // edge of radius = 1
|
||||
var sat = Math.min(relativeDistance, 1); // Distance from center
|
||||
const hue = this.getAngle(x, y); // degrees, clockwise from right
|
||||
const relativeDistance = this.getDistance(x, y); // edge of radius = 1
|
||||
const sat = Math.min(relativeDistance, 1); // Distance from center
|
||||
return { h: hue, s: sat };
|
||||
}
|
||||
|
||||
@@ -402,9 +402,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
if (this.saturationSegments === 1) {
|
||||
hs.s = 1;
|
||||
} else {
|
||||
var segmentSize = 1 / this.saturationSegments;
|
||||
var saturationStep = 1 / (this.saturationSegments - 1);
|
||||
var calculatedSat = Math.floor(hs.s / segmentSize) * saturationStep;
|
||||
const segmentSize = 1 / this.saturationSegments;
|
||||
const saturationStep = 1 / (this.saturationSegments - 1);
|
||||
const calculatedSat = Math.floor(hs.s / segmentSize) * saturationStep;
|
||||
hs.s = Math.min(calculatedSat, 1);
|
||||
}
|
||||
}
|
||||
@@ -477,9 +477,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
hueSegments = hueSegments || 360; // reset 0 segments to 360
|
||||
const angleStep = 360 / hueSegments;
|
||||
const halfAngleStep = angleStep / 2; // center segments on color
|
||||
for (var angle = 0; angle <= 360; angle += angleStep) {
|
||||
var startAngle = (angle - halfAngleStep) * (Math.PI / 180);
|
||||
var endAngle = (angle + halfAngleStep + 1) * (Math.PI / 180);
|
||||
for (let angle = 0; angle <= 360; angle += angleStep) {
|
||||
const startAngle = (angle - halfAngleStep) * (Math.PI / 180);
|
||||
const endAngle = (angle + halfAngleStep + 1) * (Math.PI / 180);
|
||||
context.beginPath();
|
||||
context.moveTo(cX, cY);
|
||||
context.arc(
|
||||
@@ -492,7 +492,7 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
);
|
||||
context.closePath();
|
||||
// gradient
|
||||
var gradient = context.createRadialGradient(
|
||||
const gradient = context.createRadialGradient(
|
||||
cX,
|
||||
cY,
|
||||
0,
|
||||
@@ -507,8 +507,8 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
|
||||
if (saturationSegments > 0) {
|
||||
const ratioStep = 1 / saturationSegments;
|
||||
let ratio = 0;
|
||||
for (var stop = 1; stop < saturationSegments; stop += 1) {
|
||||
var prevLighness = lightness;
|
||||
for (let stop = 1; stop < saturationSegments; stop += 1) {
|
||||
const prevLighness = lightness;
|
||||
ratio = stop * ratioStep;
|
||||
lightness = 100 - 50 * ratio;
|
||||
gradient.addColorStop(
|
||||
|
||||
@@ -95,7 +95,7 @@ class HaCoverControls extends PolymerElement {
|
||||
if (stateObj.state === UNAVAILABLE) {
|
||||
return true;
|
||||
}
|
||||
var assumedState = stateObj.attributes.assumed_state === true;
|
||||
const assumedState = stateObj.attributes.assumed_state === true;
|
||||
return (entityObj.isFullyOpen || entityObj.isOpening) && !assumedState;
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ class HaCoverControls extends PolymerElement {
|
||||
if (stateObj.state === UNAVAILABLE) {
|
||||
return true;
|
||||
}
|
||||
var assumedState = stateObj.attributes.assumed_state === true;
|
||||
const assumedState = stateObj.attributes.assumed_state === true;
|
||||
return (entityObj.isFullyClosed || entityObj.isClosing) && !assumedState;
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ class HaCoverTiltControls extends PolymerElement {
|
||||
if (stateObj.state === UNAVAILABLE) {
|
||||
return true;
|
||||
}
|
||||
var assumedState = stateObj.attributes.assumed_state === true;
|
||||
const assumedState = stateObj.attributes.assumed_state === true;
|
||||
return entityObj.isFullyOpenTilt && !assumedState;
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ class HaCoverTiltControls extends PolymerElement {
|
||||
if (stateObj.state === UNAVAILABLE) {
|
||||
return true;
|
||||
}
|
||||
var assumedState = stateObj.attributes.assumed_state === true;
|
||||
const assumedState = stateObj.attributes.assumed_state === true;
|
||||
return entityObj.isFullyClosedTilt && !assumedState;
|
||||
}
|
||||
|
||||
|
||||
@@ -163,13 +163,6 @@ export class HaDateRangePicker extends LitElement {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
.date-range-ranges {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
}
|
||||
|
||||
.date-range-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@@ -179,12 +172,30 @@ export class HaDateRangePicker extends LitElement {
|
||||
|
||||
paper-input {
|
||||
display: inline-block;
|
||||
max-width: 200px;
|
||||
max-width: 250px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
paper-input:last-child {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
.date-range-ranges {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
paper-input {
|
||||
min-width: inherit;
|
||||
}
|
||||
|
||||
ha-svg-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,10 +34,12 @@ export class HaDialog extends MwcDialog {
|
||||
style,
|
||||
css`
|
||||
.mdc-dialog {
|
||||
--mdc-dialog-scroll-divider-color: var(--divider-color);
|
||||
z-index: var(--dialog-z-index, 7);
|
||||
}
|
||||
.mdc-dialog__actions {
|
||||
justify-content: var(--justify-action-buttons, flex-end);
|
||||
padding-bottom: max(env(safe-area-inset-bottom), 8px);
|
||||
}
|
||||
.mdc-dialog__container {
|
||||
align-items: var(--vertial-align-dialog, center);
|
||||
@@ -50,6 +52,12 @@ export class HaDialog extends MwcDialog {
|
||||
position: var(--dialog-content-position, relative);
|
||||
padding: var(--dialog-content-padding, 20px 24px);
|
||||
}
|
||||
:host([hideactions]) .mdc-dialog .mdc-dialog__content {
|
||||
padding-bottom: max(
|
||||
var(--dialog-content-padding, 20px),
|
||||
env(safe-area-inset-bottom)
|
||||
);
|
||||
}
|
||||
.mdc-dialog .mdc-dialog__surface {
|
||||
position: var(--dialog-surface-position, relative);
|
||||
min-height: var(--mdc-dialog-min-height, auto);
|
||||
|
||||
@@ -100,7 +100,7 @@ export interface HaFormTimeData {
|
||||
}
|
||||
|
||||
export interface HaFormElement extends LitElement {
|
||||
schema: HaFormSchema;
|
||||
schema: HaFormSchema | HaFormSchema[];
|
||||
data?: HaFormDataContainer | HaFormData;
|
||||
label?: string;
|
||||
suffix?: string;
|
||||
@@ -110,7 +110,7 @@ export interface HaFormElement extends LitElement {
|
||||
export class HaForm extends LitElement implements HaFormElement {
|
||||
@property() public data!: HaFormDataContainer | HaFormData;
|
||||
|
||||
@property() public schema!: HaFormSchema;
|
||||
@property() public schema!: HaFormSchema | HaFormSchema[];
|
||||
|
||||
@property() public error;
|
||||
|
||||
@@ -190,7 +190,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
: "";
|
||||
}
|
||||
|
||||
private _computeError(error, schema: HaFormSchema) {
|
||||
private _computeError(error, schema: HaFormSchema | HaFormSchema[]) {
|
||||
return this.computeError ? this.computeError(error, schema) : error;
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const schema = (ev.target as HaFormElement).schema;
|
||||
const schema = (ev.target as HaFormElement).schema as HaFormSchema;
|
||||
const data = this.data as HaFormDataContainer;
|
||||
data[schema.name] = ev.detail.value;
|
||||
fireEvent(this, "value-changed", {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "lit-element";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
import { afterNextRender } from "../common/util/render-status";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
|
||||
const getAngle = (value: number, min: number, max: number) => {
|
||||
const percentage = getValueInPercentage(normalize(value, min, max), min, max);
|
||||
@@ -27,6 +28,9 @@ const getValueInPercentage = (value: number, min: number, max: number) => {
|
||||
return (100 * newVal) / newMax;
|
||||
};
|
||||
|
||||
// Workaround for https://github.com/home-assistant/frontend/issues/6467
|
||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||
|
||||
@customElement("ha-gauge")
|
||||
export class Gauge extends LitElement {
|
||||
@property({ type: Number }) public min = 0;
|
||||
@@ -69,9 +73,28 @@ export class Gauge extends LitElement {
|
||||
></path>
|
||||
<path
|
||||
class="value"
|
||||
style=${styleMap({ transform: `rotate(${this._angle}deg)` })}
|
||||
d="M 90 50.001 A 40 40 0 0 1 10 50"
|
||||
></path>
|
||||
style=${ifDefined(
|
||||
!isSafari
|
||||
? styleMap({ transform: `rotate(${this._angle}deg)` })
|
||||
: undefined
|
||||
)}
|
||||
transform=${ifDefined(
|
||||
isSafari ? `rotate(${this._angle} 50 50)` : undefined
|
||||
)}
|
||||
>
|
||||
${
|
||||
isSafari
|
||||
? svg`<animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
from="0 50 50"
|
||||
to="${this._angle} 50 50"
|
||||
dur="1s"
|
||||
/>`
|
||||
: ""
|
||||
}
|
||||
</path>
|
||||
</svg>
|
||||
<svg class="text">
|
||||
<text class="value-text">
|
||||
@@ -106,8 +129,8 @@ export class Gauge extends LitElement {
|
||||
fill: none;
|
||||
stroke-width: 15;
|
||||
stroke: var(--gauge-color);
|
||||
transition: all 1000ms ease 0s;
|
||||
transform-origin: 50% 100%;
|
||||
transition: all 1s ease 0s;
|
||||
}
|
||||
.gauge {
|
||||
display: block;
|
||||
|
||||
@@ -193,6 +193,7 @@ const mdiRemovedIcons = new Set([
|
||||
"medium",
|
||||
"meetup",
|
||||
"mixcloud",
|
||||
"mixer",
|
||||
"nfc-off",
|
||||
"npm-variant",
|
||||
"npm-variant-outline",
|
||||
|
||||
@@ -30,6 +30,7 @@ class HaLabeledSlider extends PolymerElement {
|
||||
ha-paper-slider {
|
||||
flex-grow: 1;
|
||||
background-image: var(--ha-slider-background);
|
||||
border-radius: var(--ha-slider-border-radius);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ class HaMarkdownElement extends UpdatingElement {
|
||||
{
|
||||
breaks: this.breaks,
|
||||
gfm: true,
|
||||
tables: true,
|
||||
},
|
||||
{
|
||||
allowSvg: this.allowSvg,
|
||||
|
||||
@@ -57,6 +57,10 @@ class HaMarkdown extends LitElement {
|
||||
background-color: var(--markdown-code-background-color, none);
|
||||
border-radius: 3px;
|
||||
}
|
||||
ha-markdown-element svg {
|
||||
background-color: var(--markdown-svg-background-color, none);
|
||||
color: var(--markdown-svg-color, none);
|
||||
}
|
||||
ha-markdown-element code {
|
||||
font-size: 85%;
|
||||
padding: 0.2em 0.4em;
|
||||
@@ -70,8 +74,8 @@ class HaMarkdown extends LitElement {
|
||||
line-height: 1.45;
|
||||
}
|
||||
ha-markdown-element h2 {
|
||||
font-size: 1.5em !important;
|
||||
font-weight: bold !important;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
20
src/components/ha-radio.ts
Normal file
20
src/components/ha-radio.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import "@material/mwc-radio";
|
||||
import type { Radio } from "@material/mwc-radio";
|
||||
import { customElement } from "lit-element";
|
||||
import type { Constructor } from "../types";
|
||||
|
||||
const MwcRadio = customElements.get("mwc-radio") as Constructor<Radio>;
|
||||
|
||||
@customElement("ha-radio")
|
||||
export class HaRadio extends MwcRadio {
|
||||
public firstUpdated() {
|
||||
super.firstUpdated();
|
||||
this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-radio": HaRadio;
|
||||
}
|
||||
}
|
||||
@@ -97,6 +97,7 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
|
||||
</h3>
|
||||
<a
|
||||
href=${`/config/integrations#config_entry=${relatedConfigEntryId}`}
|
||||
@click=${this._navigateAwayClose}
|
||||
>
|
||||
${this.hass.localize(`component.${entry.domain}.title`)}:
|
||||
${entry.title}
|
||||
@@ -116,7 +117,10 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
|
||||
<h3>
|
||||
${this.hass.localize("ui.components.related-items.device")}:
|
||||
</h3>
|
||||
<a href="/config/devices/device/${relatedDeviceId}">
|
||||
<a
|
||||
href="/config/devices/device/${relatedDeviceId}"
|
||||
@click=${this._navigateAwayClose}
|
||||
>
|
||||
${device.name_by_user || device.name}
|
||||
</a>
|
||||
`;
|
||||
@@ -134,7 +138,10 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
|
||||
<h3>
|
||||
${this.hass.localize("ui.components.related-items.area")}:
|
||||
</h3>
|
||||
<a href="/config/areas/area/${relatedAreaId}">
|
||||
<a
|
||||
href="/config/areas/area/${relatedAreaId}"
|
||||
@click=${this._navigateAwayClose}
|
||||
>
|
||||
${area.name}
|
||||
</a>
|
||||
`;
|
||||
@@ -278,6 +285,12 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _navigateAwayClose() {
|
||||
// allow new page to open before closing dialog
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
fireEvent(this, "close-dialog");
|
||||
}
|
||||
|
||||
private async _findRelated() {
|
||||
this._related = await findRelated(this.hass, this.itemType, this.itemId);
|
||||
await this.updateComplete;
|
||||
|
||||
@@ -16,9 +16,9 @@ class HaServiceDescription extends PolymerElement {
|
||||
}
|
||||
|
||||
_getDescription(hass, domain, service) {
|
||||
var domainServices = hass.services[domain];
|
||||
const domainServices = hass.services[domain];
|
||||
if (!domainServices) return "";
|
||||
var serviceObject = domainServices[service];
|
||||
const serviceObject = domainServices[service];
|
||||
if (!serviceObject) return "";
|
||||
return serviceObject.description;
|
||||
}
|
||||
|
||||
59
src/components/ha-settings-row.ts
Normal file
59
src/components/ha-settings-row.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
SVGTemplateResult,
|
||||
} from "lit-element";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
|
||||
@customElement("ha-settings-row")
|
||||
export class HaSettingsRow extends LitElement {
|
||||
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
|
||||
|
||||
@property({ type: Boolean, attribute: "three-line" })
|
||||
public threeLine = false;
|
||||
|
||||
protected render(): SVGTemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
paper-item-body {
|
||||
padding-right: 16px;
|
||||
}
|
||||
</style>
|
||||
<paper-item-body
|
||||
?two-line=${!this.threeLine}
|
||||
?three-line=${!this.threeLine}
|
||||
>
|
||||
<slot name="heading"></slot>
|
||||
<div secondary><slot name="description"></slot></div>
|
||||
</paper-item-body>
|
||||
<slot></slot>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
display: flex;
|
||||
padding: 0 16px;
|
||||
align-content: normal;
|
||||
align-self: auto;
|
||||
align-items: center;
|
||||
}
|
||||
:host([narrow]) {
|
||||
align-items: normal;
|
||||
flex-direction: column;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-settings-row": HaSettingsRow;
|
||||
}
|
||||
}
|
||||
@@ -126,7 +126,7 @@ class HaSidebar extends LitElement {
|
||||
|
||||
// property used only in css
|
||||
// @ts-ignore
|
||||
@property({ type: Boolean, reflect: true }) private _rtl = false;
|
||||
@property({ type: Boolean, reflect: true }) public rtl = false;
|
||||
|
||||
private _mouseLeaveTimeout?: number;
|
||||
|
||||
@@ -312,6 +312,7 @@ class HaSidebar extends LitElement {
|
||||
hass.panelUrl !== oldHass.panelUrl ||
|
||||
hass.user !== oldHass.user ||
|
||||
hass.localize !== oldHass.localize ||
|
||||
hass.language !== oldHass.language ||
|
||||
hass.states !== oldHass.states ||
|
||||
hass.defaultPanel !== oldHass.defaultPanel
|
||||
);
|
||||
@@ -339,12 +340,14 @@ class HaSidebar extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this._rtl = computeRTL(this.hass);
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || oldHass.language !== this.hass.language) {
|
||||
this.rtl = computeRTL(this.hass);
|
||||
}
|
||||
|
||||
if (!SUPPORT_SCROLL_IF_NEEDED) {
|
||||
return;
|
||||
}
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || oldHass.panelUrl !== this.hass.panelUrl) {
|
||||
const selectedEl = this.shadowRoot!.querySelector(".iron-selected");
|
||||
if (selectedEl) {
|
||||
@@ -496,9 +499,12 @@ class HaSidebar extends LitElement {
|
||||
width: 64px;
|
||||
}
|
||||
:host([expanded]) {
|
||||
width: 256px;
|
||||
width: calc(256px + env(safe-area-inset-left));
|
||||
}
|
||||
:host([rtl]) {
|
||||
border-right: 0;
|
||||
border-left: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.menu {
|
||||
box-sizing: border-box;
|
||||
height: 65px;
|
||||
@@ -512,18 +518,25 @@ class HaSidebar extends LitElement {
|
||||
background-color: var(--primary-background-color);
|
||||
font-size: 20px;
|
||||
align-items: center;
|
||||
padding-left: calc(8.5px + env(safe-area-inset-left));
|
||||
}
|
||||
:host([rtl]) .menu {
|
||||
padding-left: 8.5px;
|
||||
padding-right: calc(8.5px + env(safe-area-inset-right));
|
||||
}
|
||||
:host([expanded]) .menu {
|
||||
width: 256px;
|
||||
width: calc(256px + env(safe-area-inset-left));
|
||||
}
|
||||
:host([rtl][expanded]) .menu {
|
||||
width: calc(256px + env(safe-area-inset-right));
|
||||
}
|
||||
|
||||
.menu mwc-icon-button {
|
||||
color: var(--sidebar-icon-color);
|
||||
}
|
||||
:host([expanded]) .menu mwc-icon-button {
|
||||
margin-right: 23px;
|
||||
}
|
||||
:host([expanded][_rtl]) .menu mwc-icon-button {
|
||||
:host([expanded][rtl]) .menu mwc-icon-button {
|
||||
margin-right: 0px;
|
||||
margin-left: 23px;
|
||||
}
|
||||
@@ -551,12 +564,18 @@ class HaSidebar extends LitElement {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
height: calc(100% - 196px);
|
||||
height: calc(100% - 196px - env(safe-area-inset-bottom));
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-color: var(--scrollbar-thumb-color) transparent;
|
||||
scrollbar-width: thin;
|
||||
background: none;
|
||||
margin-left: env(safe-area-inset-left);
|
||||
}
|
||||
|
||||
:host([rtl]) paper-listbox {
|
||||
margin-left: initial;
|
||||
margin-right: env(safe-area-inset-right);
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -580,7 +599,7 @@ class HaSidebar extends LitElement {
|
||||
:host([expanded]) paper-icon-item {
|
||||
width: 240px;
|
||||
}
|
||||
:host([_rtl]) paper-icon-item {
|
||||
:host([rtl]) paper-icon-item {
|
||||
padding-left: auto;
|
||||
padding-right: 12px;
|
||||
}
|
||||
@@ -656,6 +675,11 @@ class HaSidebar extends LitElement {
|
||||
}
|
||||
.notifications-container {
|
||||
display: flex;
|
||||
margin-left: env(safe-area-inset-left);
|
||||
}
|
||||
:host([rtl]) .notifications-container {
|
||||
margin-left: initial;
|
||||
margin-right: env(safe-area-inset-right);
|
||||
}
|
||||
.notifications {
|
||||
cursor: pointer;
|
||||
@@ -664,18 +688,23 @@ class HaSidebar extends LitElement {
|
||||
flex: 1;
|
||||
}
|
||||
.profile {
|
||||
margin-left: env(safe-area-inset-left);
|
||||
}
|
||||
:host([rtl]) .profile {
|
||||
margin-left: initial;
|
||||
margin-right: env(safe-area-inset-right);
|
||||
}
|
||||
.profile paper-icon-item {
|
||||
padding-left: 4px;
|
||||
}
|
||||
:host([_rtl]) .profile paper-icon-item {
|
||||
:host([rtl]) .profile paper-icon-item {
|
||||
padding-left: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
.profile .item-text {
|
||||
margin-left: 8px;
|
||||
}
|
||||
:host([_rtl]) .profile .item-text {
|
||||
:host([rtl]) .profile .item-text {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
@@ -688,7 +717,7 @@ class HaSidebar extends LitElement {
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
padding: 0px 6px;
|
||||
color: var(--text-primary-color);
|
||||
color: var(--text-accent-color, var(--text-primary-color));
|
||||
}
|
||||
ha-svg-icon + .notification-badge {
|
||||
position: absolute;
|
||||
@@ -735,7 +764,7 @@ class HaSidebar extends LitElement {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:host([_rtl]) .menu mwc-icon-button {
|
||||
:host([rtl]) .menu mwc-icon-button {
|
||||
-webkit-transform: scaleX(-1);
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
155
src/components/ha-vertical-range-input.ts
Normal file
155
src/components/ha-vertical-range-input.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import {
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
html,
|
||||
CSSResult,
|
||||
css,
|
||||
customElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
@customElement("ha-vertical-range-input")
|
||||
class HaVerticalRangeInput extends LitElement {
|
||||
@property({ type: Number }) public value!: number;
|
||||
|
||||
@property({ type: Number }) public max = 100;
|
||||
|
||||
@property({ type: Number }) public min = 1;
|
||||
|
||||
@property({ type: Number }) public step = 1;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.value) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<input
|
||||
type="range"
|
||||
.max=${this.max.toString()}
|
||||
.min=${this.min.toString()}
|
||||
.step=${this.step.toString()}
|
||||
.value=${this.value.toString()}
|
||||
@change=${this._valueChanged}
|
||||
/>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: (ev.currentTarget as HTMLInputElement).value,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
height: calc(var(--vertical-range-height, 300px) + 5px);
|
||||
width: var(--vertical-range-width, 100px);
|
||||
position: relative;
|
||||
display: block;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
:host input {
|
||||
width: var(--vertical-range-height, 300px);
|
||||
height: var(--vertical-range-width, 100px);
|
||||
margin: 0;
|
||||
outline: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--divider-color);
|
||||
/* background: var(--vertical-range-track-color, #fafafa); */
|
||||
border-radius: 8px;
|
||||
position: absolute;
|
||||
top: calc(50% - var(--vertical-range-width, 100px) / 2);
|
||||
right: calc(50% - var(--vertical-range-height, 300px) / 2);
|
||||
-webkit-transform: rotate(270deg);
|
||||
-moz-transform: rotate(270deg);
|
||||
-o-transform: rotate(270deg);
|
||||
-ms-transform: rotate(270deg);
|
||||
transform: rotate(270deg);
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
:host input::-webkit-slider-runnable-track {
|
||||
height: 100%;
|
||||
background: var(--vertical-range-track-color, #fafafa);
|
||||
}
|
||||
|
||||
:host input::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
position: relative;
|
||||
cursor: grab;
|
||||
width: var(--vertical-range-thumb-height, 25px);
|
||||
height: var(--vertical-range-thumb-width, 100%);
|
||||
background: var(--vertical-range-thumb-color, #fafafa);
|
||||
box-shadow: calc(var(--vertical-range-height, 300px) * -1) 0 0
|
||||
var(--vertical-range-height, 300px)
|
||||
var(--vertical-range-color, var(--state-icon-active-color));
|
||||
border-right: 10px solid
|
||||
var(
|
||||
--vertical-range-thumb-padding-color,
|
||||
var(--state-icon-active-color)
|
||||
);
|
||||
border-left: 10px solid
|
||||
var(
|
||||
--vertical-range-thumb-padding-color,
|
||||
var(--state-icon-active-color)
|
||||
);
|
||||
border-top: 20px solid
|
||||
var(
|
||||
--vertical-range-thumb-padding-color,
|
||||
var(--state-icon-active-color)
|
||||
);
|
||||
border-bottom: 20px solid
|
||||
var(
|
||||
--vertical-range-thumb-padding-color,
|
||||
var(--state-icon-active-color)
|
||||
);
|
||||
transition: box-shadow 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
:host input::-webkit-slider-thumb:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
:host input::-moz-thumb-track {
|
||||
height: 100%;
|
||||
background-color: var(--vertical-range-track-color, #fafafa);
|
||||
}
|
||||
|
||||
:host input::-moz-range-thumb {
|
||||
width: 5px;
|
||||
height: calc(var(--vertical-range-width, 100px) * 0.4);
|
||||
position: relative;
|
||||
top: 0px;
|
||||
cursor: grab;
|
||||
background: var(--vertical-range-track-color, #fafafa);
|
||||
box-shadow: -350px 0 0 350px
|
||||
var(--vertical-range-color, var(--state-icon-active-color)),
|
||||
inset 0 0 0 80px var(--vertical-range-track-color, #fafafa);
|
||||
border-right: 9px solid
|
||||
var(--vertical-range-color, var(--state-icon-active-color));
|
||||
border-left: 9px solid
|
||||
var(--vertical-range-color, var(--state-icon-active-color));
|
||||
border-top: 22px solid
|
||||
var(--vertical-range-color, var(--state-icon-active-color));
|
||||
border-bottom: 22px solid
|
||||
var(--vertical-range-color, var(--state-icon-active-color));
|
||||
border-radius: 0;
|
||||
transition: box-shadow 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
:host input::-moz-range-thumb:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-vertical-range-input": HaVerticalRangeInput;
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,8 @@ export class HaYamlEditor extends LitElement {
|
||||
try {
|
||||
this._yaml = value && !isEmpty(value) ? safeDump(value) : "";
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
alert(`There was an error converting to YAML: ${err}`);
|
||||
}
|
||||
afterNextRender(() => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
LeafletMouseEvent,
|
||||
Map,
|
||||
Marker,
|
||||
TileLayer,
|
||||
} from "leaflet";
|
||||
import {
|
||||
css,
|
||||
@@ -21,15 +22,19 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
LeafletModuleType,
|
||||
setupLeafletMap,
|
||||
replaceTileLayer,
|
||||
} from "../../common/dom/setup-leaflet-map";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
import { defaultRadiusColor } from "../../data/zone";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("ha-location-editor")
|
||||
class LocationEditor extends LitElement {
|
||||
@property() public location?: [number, number];
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public radius?: number;
|
||||
@property({ type: Array }) public location?: [number, number];
|
||||
|
||||
@property({ type: Number }) public radius?: number;
|
||||
|
||||
@property() public radiusColor?: string;
|
||||
|
||||
@@ -46,6 +51,8 @@ class LocationEditor extends LitElement {
|
||||
|
||||
private _leafletMap?: Map;
|
||||
|
||||
private _tileLayer?: TileLayer;
|
||||
|
||||
private _locationMarker?: Marker | Circle;
|
||||
|
||||
public fitMap(): void {
|
||||
@@ -97,6 +104,22 @@ class LocationEditor extends LitElement {
|
||||
if (changedProps.has("icon")) {
|
||||
this._updateIcon();
|
||||
}
|
||||
|
||||
if (changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || oldHass.themes?.darkMode === this.hass.themes?.darkMode) {
|
||||
return;
|
||||
}
|
||||
if (!this._leafletMap || !this._tileLayer) {
|
||||
return;
|
||||
}
|
||||
this._tileLayer = replaceTileLayer(
|
||||
this.Leaflet,
|
||||
this._leafletMap,
|
||||
this._tileLayer,
|
||||
this.hass.themes?.darkMode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private get _mapEl(): HTMLDivElement {
|
||||
@@ -104,9 +127,9 @@ class LocationEditor extends LitElement {
|
||||
}
|
||||
|
||||
private async _initMap(): Promise<void> {
|
||||
[this._leafletMap, this.Leaflet] = await setupLeafletMap(
|
||||
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
|
||||
this._mapEl,
|
||||
false,
|
||||
this.hass.themes?.darkMode,
|
||||
Boolean(this.radius)
|
||||
);
|
||||
this._leafletMap.addEventListener(
|
||||
@@ -255,9 +278,6 @@ class LocationEditor extends LitElement {
|
||||
#map {
|
||||
height: 100%;
|
||||
}
|
||||
.light {
|
||||
color: #000000;
|
||||
}
|
||||
.leaflet-edit-move {
|
||||
border-radius: 50%;
|
||||
cursor: move !important;
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Map,
|
||||
Marker,
|
||||
MarkerOptions,
|
||||
TileLayer,
|
||||
} from "leaflet";
|
||||
import {
|
||||
css,
|
||||
@@ -21,8 +22,10 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
LeafletModuleType,
|
||||
setupLeafletMap,
|
||||
replaceTileLayer,
|
||||
} from "../../common/dom/setup-leaflet-map";
|
||||
import { defaultRadiusColor } from "../../data/zone";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
@@ -47,6 +50,8 @@ export interface MarkerLocation {
|
||||
|
||||
@customElement("ha-locations-editor")
|
||||
export class HaLocationsEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public locations?: MarkerLocation[];
|
||||
|
||||
public fitZoom = 16;
|
||||
@@ -57,6 +62,8 @@ export class HaLocationsEditor extends LitElement {
|
||||
// eslint-disable-next-line
|
||||
private _leafletMap?: Map;
|
||||
|
||||
private _tileLayer?: TileLayer;
|
||||
|
||||
private _locationMarkers?: { [key: string]: Marker | Circle };
|
||||
|
||||
private _circles: { [key: string]: Circle } = {};
|
||||
@@ -116,6 +123,22 @@ export class HaLocationsEditor extends LitElement {
|
||||
if (changedProps.has("locations")) {
|
||||
this._updateMarkers();
|
||||
}
|
||||
|
||||
if (changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || oldHass.themes.darkMode === this.hass.themes.darkMode) {
|
||||
return;
|
||||
}
|
||||
if (!this._leafletMap || !this._tileLayer) {
|
||||
return;
|
||||
}
|
||||
this._tileLayer = replaceTileLayer(
|
||||
this.Leaflet,
|
||||
this._leafletMap,
|
||||
this._tileLayer,
|
||||
this.hass.themes.darkMode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private get _mapEl(): HTMLDivElement {
|
||||
@@ -123,9 +146,9 @@ export class HaLocationsEditor extends LitElement {
|
||||
}
|
||||
|
||||
private async _initMap(): Promise<void> {
|
||||
[this._leafletMap, this.Leaflet] = await setupLeafletMap(
|
||||
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
|
||||
this._mapEl,
|
||||
false,
|
||||
this.hass.themes.darkMode,
|
||||
true
|
||||
);
|
||||
this._updateMarkers();
|
||||
@@ -290,9 +313,6 @@ export class HaLocationsEditor extends LitElement {
|
||||
#map {
|
||||
height: 100%;
|
||||
}
|
||||
.light {
|
||||
color: #000000;
|
||||
}
|
||||
.leaflet-marker-draggable {
|
||||
cursor: move !important;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import "../ha-icon-button";
|
||||
import { Circle, Layer, Map, Marker } from "leaflet";
|
||||
import { Circle, Layer, Map, Marker, TileLayer } from "leaflet";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import {
|
||||
LeafletModuleType,
|
||||
setupLeafletMap,
|
||||
replaceTileLayer,
|
||||
} from "../../common/dom/setup-leaflet-map";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
@@ -22,11 +23,11 @@ import { HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("ha-map")
|
||||
class HaMap extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public entities?: string[];
|
||||
|
||||
@property() public darkMode = false;
|
||||
@property() public darkMode?: boolean;
|
||||
|
||||
@property() public zoom?: number;
|
||||
|
||||
@@ -35,6 +36,8 @@ class HaMap extends LitElement {
|
||||
|
||||
private _leafletMap?: Map;
|
||||
|
||||
private _tileLayer?: TileLayer;
|
||||
|
||||
// @ts-ignore
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
|
||||
@@ -122,6 +125,20 @@ class HaMap extends LitElement {
|
||||
if (changedProps.has("hass")) {
|
||||
this._drawEntities();
|
||||
this._fitMap();
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || oldHass.themes.darkMode === this.hass.themes.darkMode) {
|
||||
return;
|
||||
}
|
||||
if (!this.Leaflet || !this._leafletMap || !this._tileLayer) {
|
||||
return;
|
||||
}
|
||||
this._tileLayer = replaceTileLayer(
|
||||
this.Leaflet,
|
||||
this._leafletMap,
|
||||
this._tileLayer,
|
||||
this.hass.themes.darkMode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,9 +147,9 @@ class HaMap extends LitElement {
|
||||
}
|
||||
|
||||
private async loadMap(): Promise<void> {
|
||||
[this._leafletMap, this.Leaflet] = await setupLeafletMap(
|
||||
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
|
||||
this._mapEl,
|
||||
this.darkMode
|
||||
this.darkMode ?? this.hass.themes.darkMode
|
||||
);
|
||||
this._drawEntities();
|
||||
this._leafletMap.invalidateSize();
|
||||
@@ -229,7 +246,8 @@ class HaMap extends LitElement {
|
||||
icon: Leaflet.divIcon({
|
||||
html: iconHTML,
|
||||
iconSize: [24, 24],
|
||||
className: this.darkMode ? "dark" : "light",
|
||||
className:
|
||||
this.darkMode ?? this.hass.themes.darkMode ? "dark" : "light",
|
||||
}),
|
||||
interactive: false,
|
||||
title,
|
||||
|
||||
@@ -322,9 +322,9 @@ export class PaperTimeInput extends PolymerElement {
|
||||
* @return {boolean}
|
||||
*/
|
||||
validate() {
|
||||
var valid = true;
|
||||
let valid = true;
|
||||
// Validate hour & min fields
|
||||
if (!this.$.hour.validate() | !this.$.min.validate()) {
|
||||
if (!this.$.hour.validate() || !this.$.min.validate()) {
|
||||
valid = false;
|
||||
}
|
||||
// Validate second field
|
||||
|
||||
@@ -57,7 +57,7 @@ class StateBadge extends LitElement {
|
||||
text-align: center;
|
||||
background-color: var(--light-primary-color);
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
color: var(--text-light-primary-color, var(--primary-text-color));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { HaFormSchema } from "../components/ha-form/ha-form";
|
||||
|
||||
export interface DeviceAutomation {
|
||||
device_id: string;
|
||||
@@ -20,6 +21,10 @@ export interface DeviceTrigger extends DeviceAutomation {
|
||||
platform: "device";
|
||||
}
|
||||
|
||||
export interface DeviceCapabilities {
|
||||
extra_fields: HaFormSchema[];
|
||||
}
|
||||
|
||||
export const fetchDeviceActions = (hass: HomeAssistant, deviceId: string) =>
|
||||
hass.callWS<DeviceAction[]>({
|
||||
type: "device_automation/action/list",
|
||||
@@ -42,7 +47,7 @@ export const fetchDeviceActionCapabilities = (
|
||||
hass: HomeAssistant,
|
||||
action: DeviceAction
|
||||
) =>
|
||||
hass.callWS<DeviceAction[]>({
|
||||
hass.callWS<DeviceCapabilities>({
|
||||
type: "device_automation/action/capabilities",
|
||||
action,
|
||||
});
|
||||
@@ -51,7 +56,7 @@ export const fetchDeviceConditionCapabilities = (
|
||||
hass: HomeAssistant,
|
||||
condition: DeviceCondition
|
||||
) =>
|
||||
hass.callWS<DeviceCondition[]>({
|
||||
hass.callWS<DeviceCapabilities>({
|
||||
type: "device_automation/condition/capabilities",
|
||||
condition,
|
||||
});
|
||||
@@ -60,7 +65,7 @@ export const fetchDeviceTriggerCapabilities = (
|
||||
hass: HomeAssistant,
|
||||
trigger: DeviceTrigger
|
||||
) =>
|
||||
hass.callWS<DeviceTrigger[]>({
|
||||
hass.callWS<DeviceCapabilities>({
|
||||
type: "device_automation/trigger/capabilities",
|
||||
trigger,
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface DeviceRegistryEntry {
|
||||
id: string;
|
||||
config_entries: string[];
|
||||
connections: Array<[string, string]>;
|
||||
identifiers: Array<[string, string]>;
|
||||
manufacturer: string;
|
||||
model?: string;
|
||||
name?: string;
|
||||
|
||||
@@ -31,6 +31,7 @@ export interface CreateSessionResponse {
|
||||
|
||||
export interface SupervisorOptions {
|
||||
channel?: "beta" | "dev" | "stable";
|
||||
diagnostics?: boolean;
|
||||
addons_repositories?: string[];
|
||||
}
|
||||
|
||||
@@ -70,7 +71,7 @@ export const createHassioSession = async (hass: HomeAssistant) => {
|
||||
"POST",
|
||||
"hassio/ingress/session"
|
||||
);
|
||||
document.cookie = `ingress_session=${response.data.session};path=/api/hassio_ingress/`;
|
||||
document.cookie = `ingress_session=${response.data.session};path=/api/hassio_ingress/;SameSite=Strict`;
|
||||
};
|
||||
|
||||
export const setSupervisorOption = async (
|
||||
|
||||
@@ -21,6 +21,11 @@ export interface MediaPlayerThumbnail {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ControlButton {
|
||||
icon: string;
|
||||
action: string;
|
||||
}
|
||||
|
||||
export const getCurrentProgress = (stateObj: HassEntity): number => {
|
||||
let progress = stateObj.attributes.media_position;
|
||||
|
||||
|
||||
114
src/data/ozw.ts
Normal file
114
src/data/ozw.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
import { DeviceRegistryEntry } from "./device_registry";
|
||||
|
||||
export interface OZWNodeIdentifiers {
|
||||
ozw_instance: number;
|
||||
node_id: number;
|
||||
}
|
||||
|
||||
export interface OZWDevice {
|
||||
node_id: number;
|
||||
node_query_stage: string;
|
||||
is_awake: boolean;
|
||||
is_failed: boolean;
|
||||
is_zwave_plus: boolean;
|
||||
ozw_instance: number;
|
||||
event: string;
|
||||
}
|
||||
|
||||
export interface OZWDeviceMetaDataResponse {
|
||||
node_id: number;
|
||||
ozw_instance: number;
|
||||
metadata: OZWDeviceMetaData;
|
||||
}
|
||||
|
||||
export interface OZWDeviceMetaData {
|
||||
OZWInfoURL: string;
|
||||
ZWAProductURL: string;
|
||||
ProductPic: string;
|
||||
Description: string;
|
||||
ProductManualURL: string;
|
||||
ProductPageURL: string;
|
||||
InclusionHelp: string;
|
||||
ExclusionHelp: string;
|
||||
ResetHelp: string;
|
||||
WakeupHelp: string;
|
||||
ProductSupportURL: string;
|
||||
Frequency: string;
|
||||
Name: string;
|
||||
ProductPicBase64: string;
|
||||
}
|
||||
|
||||
export const nodeQueryStages = [
|
||||
"ProtocolInfo",
|
||||
"Probe",
|
||||
"WakeUp",
|
||||
"ManufacturerSpecific1",
|
||||
"NodeInfo",
|
||||
"NodePlusInfo",
|
||||
"ManufacturerSpecific2",
|
||||
"Versions",
|
||||
"Instances",
|
||||
"Static",
|
||||
"CacheLoad",
|
||||
"Associations",
|
||||
"Neighbors",
|
||||
"Session",
|
||||
"Dynamic",
|
||||
"Configuration",
|
||||
"Complete",
|
||||
];
|
||||
|
||||
export const getIdentifiersFromDevice = function (
|
||||
device: DeviceRegistryEntry
|
||||
): OZWNodeIdentifiers | undefined {
|
||||
if (!device) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const ozwIdentifier = device.identifiers.find(
|
||||
(identifier) => identifier[0] === "ozw"
|
||||
);
|
||||
if (!ozwIdentifier) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const identifiers = ozwIdentifier[1].split(".");
|
||||
return {
|
||||
node_id: parseInt(identifiers[1]),
|
||||
ozw_instance: parseInt(identifiers[0]),
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchOZWNodeStatus = (
|
||||
hass: HomeAssistant,
|
||||
ozw_instance: number,
|
||||
node_id: number
|
||||
): Promise<OZWDevice> =>
|
||||
hass.callWS({
|
||||
type: "ozw/node_status",
|
||||
ozw_instance: ozw_instance,
|
||||
node_id: node_id,
|
||||
});
|
||||
|
||||
export const fetchOZWNodeMetadata = (
|
||||
hass: HomeAssistant,
|
||||
ozw_instance: number,
|
||||
node_id: number
|
||||
): Promise<OZWDeviceMetaDataResponse> =>
|
||||
hass.callWS({
|
||||
type: "ozw/node_metadata",
|
||||
ozw_instance: ozw_instance,
|
||||
node_id: node_id,
|
||||
});
|
||||
|
||||
export const refreshNodeInfo = (
|
||||
hass: HomeAssistant,
|
||||
ozw_instance: number,
|
||||
node_id: number
|
||||
): Promise<OZWDevice> =>
|
||||
hass.callWS({
|
||||
type: "ozw/refresh_node_info",
|
||||
ozw_instance: ozw_instance,
|
||||
node_id: node_id,
|
||||
});
|
||||
@@ -58,6 +58,31 @@ export interface WaitAction {
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export interface RepeatAction {
|
||||
repeat: CountRepeat | WhileRepeat | UntilRepeat;
|
||||
}
|
||||
|
||||
interface BaseRepeat {
|
||||
sequence: Action[];
|
||||
}
|
||||
|
||||
export interface CountRepeat extends BaseRepeat {
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface WhileRepeat extends BaseRepeat {
|
||||
while: Condition[];
|
||||
}
|
||||
|
||||
export interface UntilRepeat extends BaseRepeat {
|
||||
until: Condition[];
|
||||
}
|
||||
|
||||
export interface ChooseAction {
|
||||
choose: [{ conditions: Condition[]; sequence: Action[] }];
|
||||
default?: Action[];
|
||||
}
|
||||
|
||||
export type Action =
|
||||
| EventAction
|
||||
| DeviceAction
|
||||
@@ -65,7 +90,9 @@ export type Action =
|
||||
| Condition
|
||||
| DelayAction
|
||||
| SceneAction
|
||||
| WaitAction;
|
||||
| WaitAction
|
||||
| RepeatAction
|
||||
| ChooseAction;
|
||||
|
||||
export const triggerScript = (
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -165,7 +165,6 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-paper-dialog {
|
||||
min-width: 400px;
|
||||
max-width: 500px;
|
||||
}
|
||||
.init-spinner {
|
||||
|
||||
@@ -123,12 +123,12 @@ class MoreInfoConfigurator extends PolymerElement {
|
||||
}
|
||||
|
||||
fieldChanged(ev) {
|
||||
var el = ev.target;
|
||||
const el = ev.target;
|
||||
this.fieldInput[el.name] = el.value;
|
||||
}
|
||||
|
||||
submitClicked() {
|
||||
var data = {
|
||||
const data = {
|
||||
configure_id: this.stateObj.attributes.configure_id,
|
||||
fields: this.fieldInput,
|
||||
};
|
||||
|
||||
@@ -97,7 +97,7 @@ class MoreInfoCover extends LocalizeMixin(PolymerElement) {
|
||||
}
|
||||
|
||||
computeClassNames(stateObj) {
|
||||
var classes = [
|
||||
const classes = [
|
||||
attributeClassNames(stateObj, [
|
||||
"current_position",
|
||||
"current_tilt_position",
|
||||
|
||||
@@ -140,8 +140,8 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
}
|
||||
|
||||
speedChanged(ev) {
|
||||
var oldVal = this.stateObj.attributes.speed;
|
||||
var newVal = ev.detail.value;
|
||||
const oldVal = this.stateObj.attributes.speed;
|
||||
const newVal = ev.detail.value;
|
||||
|
||||
if (!newVal || oldVal === newVal) return;
|
||||
|
||||
@@ -152,8 +152,8 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
}
|
||||
|
||||
oscillationToggleChanged(ev) {
|
||||
var oldVal = this.stateObj.attributes.oscillating;
|
||||
var newVal = ev.target.checked;
|
||||
const oldVal = this.stateObj.attributes.oscillating;
|
||||
const newVal = ev.target.checked;
|
||||
|
||||
if (oldVal === newVal) return;
|
||||
|
||||
|
||||
@@ -53,11 +53,11 @@ class MoreInfoGroup extends PolymerElement {
|
||||
}
|
||||
|
||||
computeStates(stateObj, hass) {
|
||||
var states = [];
|
||||
var entIds = stateObj.attributes.entity_id || [];
|
||||
const states = [];
|
||||
const entIds = stateObj.attributes.entity_id || [];
|
||||
|
||||
for (var i = 0; i < entIds.length; i++) {
|
||||
var state = hass.states[entIds[i]];
|
||||
for (let i = 0; i < entIds.length; i++) {
|
||||
const state = hass.states[entIds[i]];
|
||||
|
||||
if (state) {
|
||||
states.push(state);
|
||||
|
||||
@@ -78,14 +78,14 @@ class DatetimeInput extends PolymerElement {
|
||||
if (stateObj.state === "unknown") {
|
||||
return "";
|
||||
}
|
||||
var monthFiller;
|
||||
let monthFiller;
|
||||
if (stateObj.attributes.month < 10) {
|
||||
monthFiller = "0";
|
||||
} else {
|
||||
monthFiller = "";
|
||||
}
|
||||
|
||||
var dayFiller;
|
||||
let dayFiller;
|
||||
if (stateObj.attributes.day < 10) {
|
||||
dayFiller = "0";
|
||||
} else {
|
||||
@@ -119,15 +119,18 @@ class DatetimeInput extends PolymerElement {
|
||||
};
|
||||
|
||||
if (this.stateObj.attributes.has_time) {
|
||||
changed |=
|
||||
changed =
|
||||
changed ||
|
||||
parseInt(this.selectedMinute) !== this.stateObj.attributes.minute;
|
||||
changed |= parseInt(this.selectedHour) !== this.stateObj.attributes.hour;
|
||||
changed =
|
||||
changed ||
|
||||
parseInt(this.selectedHour) !== this.stateObj.attributes.hour;
|
||||
if (this.selectedMinute < 10) {
|
||||
minuteFiller = "0";
|
||||
} else {
|
||||
minuteFiller = "";
|
||||
}
|
||||
var timeStr =
|
||||
const timeStr =
|
||||
this.selectedHour + ":" + minuteFiller + this.selectedMinute;
|
||||
serviceData.time = timeStr;
|
||||
}
|
||||
@@ -144,7 +147,7 @@ class DatetimeInput extends PolymerElement {
|
||||
this.stateObj.attributes.day
|
||||
);
|
||||
|
||||
changed |= dateValState !== dateValInput;
|
||||
changed = changed || dateValState !== dateValInput;
|
||||
|
||||
serviceData.date = this.selectedDate;
|
||||
}
|
||||
|
||||
@@ -1,361 +0,0 @@
|
||||
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { featureClassNames } from "../../../common/entity/feature_class_names";
|
||||
import "../../../components/ha-attributes";
|
||||
import "../../../components/ha-color-picker";
|
||||
import "../../../components/ha-labeled-slider";
|
||||
import "../../../components/ha-paper-dropdown-menu";
|
||||
import { EventsMixin } from "../../../mixins/events-mixin";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
import "../../../components/ha-icon-button";
|
||||
|
||||
const FEATURE_CLASS_NAMES = {
|
||||
1: "has-brightness",
|
||||
2: "has-color_temp",
|
||||
4: "has-effect_list",
|
||||
16: "has-color",
|
||||
128: "has-white_value",
|
||||
};
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex"></style>
|
||||
<style>
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.effect_list,
|
||||
.brightness,
|
||||
.color_temp,
|
||||
.white_value {
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.5s ease-in;
|
||||
}
|
||||
|
||||
.color_temp {
|
||||
--ha-slider-background: -webkit-linear-gradient(
|
||||
right,
|
||||
rgb(255, 160, 0) 0%,
|
||||
white 50%,
|
||||
rgb(166, 209, 255) 100%
|
||||
);
|
||||
/* The color temp minimum value shouldn't be rendered differently. It's not "off". */
|
||||
--paper-slider-knob-start-border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.segmentationContainer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
ha-color-picker {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.5s ease-in;
|
||||
}
|
||||
|
||||
.segmentationButton {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 5%;
|
||||
transform: translate(0%, 0%);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.has-color.is-on .segmentationButton {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.has-effect_list.is-on .effect_list,
|
||||
.has-brightness .brightness,
|
||||
.has-color_temp.is-on .color_temp,
|
||||
.has-white_value.is-on .white_value {
|
||||
max-height: 84px;
|
||||
}
|
||||
|
||||
.has-brightness .has-color_temp.is-on,
|
||||
.has-white_value.is-on {
|
||||
margin-top: -16px;
|
||||
}
|
||||
|
||||
.has-brightness .brightness,
|
||||
.has-color_temp.is-on .color_temp,
|
||||
.has-white_value.is-on .white_value {
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.has-color.is-on ha-color-picker {
|
||||
max-height: 500px;
|
||||
overflow: visible;
|
||||
--ha-color-picker-wheel-borderwidth: 5;
|
||||
--ha-color-picker-wheel-bordercolor: white;
|
||||
--ha-color-picker-wheel-shadow: none;
|
||||
--ha-color-picker-marker-borderwidth: 2;
|
||||
--ha-color-picker-marker-bordercolor: white;
|
||||
}
|
||||
|
||||
.control {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.is-unavailable .control {
|
||||
max-height: 0px;
|
||||
}
|
||||
|
||||
ha-attributes {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ha-paper-dropdown-menu {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class$="[[computeClassNames(stateObj)]]">
|
||||
<div class="control brightness">
|
||||
<ha-labeled-slider
|
||||
caption="[[localize('ui.card.light.brightness')]]"
|
||||
icon="hass:brightness-5"
|
||||
min="1"
|
||||
max="255"
|
||||
value="{{brightnessSliderValue}}"
|
||||
on-change="brightnessSliderChanged"
|
||||
></ha-labeled-slider>
|
||||
</div>
|
||||
|
||||
<div class="control color_temp">
|
||||
<ha-labeled-slider
|
||||
caption="[[localize('ui.card.light.color_temperature')]]"
|
||||
icon="hass:thermometer"
|
||||
min="[[stateObj.attributes.min_mireds]]"
|
||||
max="[[stateObj.attributes.max_mireds]]"
|
||||
value="{{ctSliderValue}}"
|
||||
on-change="ctSliderChanged"
|
||||
></ha-labeled-slider>
|
||||
</div>
|
||||
|
||||
<div class="control white_value">
|
||||
<ha-labeled-slider
|
||||
caption="[[localize('ui.card.light.white_value')]]"
|
||||
icon="hass:file-word-box"
|
||||
max="255"
|
||||
value="{{wvSliderValue}}"
|
||||
on-change="wvSliderChanged"
|
||||
></ha-labeled-slider>
|
||||
</div>
|
||||
<div class="segmentationContainer">
|
||||
<ha-color-picker
|
||||
class="control color"
|
||||
on-colorselected="colorPicked"
|
||||
desired-hs-color="{{colorPickerColor}}"
|
||||
throttle="500"
|
||||
hue-segments="{{hueSegments}}"
|
||||
saturation-segments="{{saturationSegments}}"
|
||||
>
|
||||
</ha-color-picker>
|
||||
<ha-icon-button
|
||||
icon="mdi:palette"
|
||||
on-click="segmentClick"
|
||||
class="segmentationButton"
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
|
||||
<div class="control effect_list">
|
||||
<ha-paper-dropdown-menu
|
||||
label-float=""
|
||||
dynamic-align=""
|
||||
label="[[localize('ui.card.light.effect')]]"
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
selected="[[stateObj.attributes.effect]]"
|
||||
on-selected-changed="effectChanged"
|
||||
attr-for-selected="item-name"
|
||||
>
|
||||
<template
|
||||
is="dom-repeat"
|
||||
items="[[stateObj.attributes.effect_list]]"
|
||||
>
|
||||
<paper-item item-name$="[[item]]">[[item]]</paper-item>
|
||||
</template>
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
</div>
|
||||
|
||||
<ha-attributes
|
||||
state-obj="[[stateObj]]"
|
||||
extra-filters="brightness,color_temp,white_value,effect_list,effect,hs_color,rgb_color,xy_color,min_mireds,max_mireds,entity_id"
|
||||
></ha-attributes>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
stateObj: {
|
||||
type: Object,
|
||||
observer: "stateObjChanged",
|
||||
},
|
||||
|
||||
brightnessSliderValue: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
},
|
||||
|
||||
ctSliderValue: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
},
|
||||
|
||||
wvSliderValue: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
},
|
||||
|
||||
hueSegments: {
|
||||
type: Number,
|
||||
value: 24,
|
||||
},
|
||||
|
||||
saturationSegments: {
|
||||
type: Number,
|
||||
value: 8,
|
||||
},
|
||||
|
||||
colorPickerColor: {
|
||||
type: Object,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
stateObjChanged(newVal, oldVal) {
|
||||
const props = {
|
||||
brightnessSliderValue: 0,
|
||||
};
|
||||
|
||||
if (newVal && newVal.state === "on") {
|
||||
props.brightnessSliderValue = newVal.attributes.brightness;
|
||||
props.ctSliderValue = newVal.attributes.color_temp;
|
||||
props.wvSliderValue = newVal.attributes.white_value;
|
||||
if (newVal.attributes.hs_color) {
|
||||
props.colorPickerColor = {
|
||||
h: newVal.attributes.hs_color[0],
|
||||
s: newVal.attributes.hs_color[1] / 100,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
this.setProperties(props);
|
||||
|
||||
if (oldVal) {
|
||||
setTimeout(() => {
|
||||
this.fire("iron-resize");
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
computeClassNames(stateObj) {
|
||||
const classes = [
|
||||
"content",
|
||||
featureClassNames(stateObj, FEATURE_CLASS_NAMES),
|
||||
];
|
||||
if (stateObj && stateObj.state === "on") {
|
||||
classes.push("is-on");
|
||||
}
|
||||
if (stateObj && stateObj.state === "unavailable") {
|
||||
classes.push("is-unavailable");
|
||||
}
|
||||
return classes.join(" ");
|
||||
}
|
||||
|
||||
effectChanged(ev) {
|
||||
var oldVal = this.stateObj.attributes.effect;
|
||||
var newVal = ev.detail.value;
|
||||
|
||||
if (!newVal || oldVal === newVal) return;
|
||||
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
effect: newVal,
|
||||
});
|
||||
}
|
||||
|
||||
brightnessSliderChanged(ev) {
|
||||
var bri = parseInt(ev.target.value, 10);
|
||||
|
||||
if (isNaN(bri)) return;
|
||||
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
brightness: bri,
|
||||
});
|
||||
}
|
||||
|
||||
ctSliderChanged(ev) {
|
||||
var ct = parseInt(ev.target.value, 10);
|
||||
|
||||
if (isNaN(ct)) return;
|
||||
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
color_temp: ct,
|
||||
});
|
||||
}
|
||||
|
||||
wvSliderChanged(ev) {
|
||||
var wv = parseInt(ev.target.value, 10);
|
||||
|
||||
if (isNaN(wv)) return;
|
||||
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
white_value: wv,
|
||||
});
|
||||
}
|
||||
|
||||
segmentClick() {
|
||||
if (this.hueSegments === 24 && this.saturationSegments === 8) {
|
||||
this.setProperties({ hueSegments: 0, saturationSegments: 0 });
|
||||
} else {
|
||||
this.setProperties({ hueSegments: 24, saturationSegments: 8 });
|
||||
}
|
||||
}
|
||||
|
||||
serviceChangeColor(hass, entityId, color) {
|
||||
hass.callService("light", "turn_on", {
|
||||
entity_id: entityId,
|
||||
hs_color: [color.h, color.s * 100],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a new color has been picked.
|
||||
* should be throttled with the 'throttle=' attribute of the color picker
|
||||
*/
|
||||
colorPicked(ev) {
|
||||
this.serviceChangeColor(this.hass, this.stateObj.entity_id, ev.detail.hs);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("more-info-light", MoreInfoLight);
|
||||
337
src/dialogs/more-info/controls/more-info-light.ts
Normal file
337
src/dialogs/more-info/controls/more-info-light.ts
Normal file
@@ -0,0 +1,337 @@
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import "@material/mwc-tab-bar";
|
||||
import "@material/mwc-tab";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
|
||||
import {
|
||||
SUPPORT_BRIGHTNESS,
|
||||
SUPPORT_COLOR_TEMP,
|
||||
SUPPORT_WHITE_VALUE,
|
||||
SUPPORT_COLOR,
|
||||
SUPPORT_EFFECT,
|
||||
} from "../../../data/light";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import type { HomeAssistant, LightEntity } from "../../../types";
|
||||
|
||||
import "../../../components/ha-attributes";
|
||||
import "../../../components/ha-color-picker";
|
||||
import "../../../components/ha-labeled-slider";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-paper-dropdown-menu";
|
||||
import "../../../components/ha-vertical-range-input";
|
||||
|
||||
interface HueSatColor {
|
||||
h: number;
|
||||
s: number;
|
||||
}
|
||||
|
||||
@customElement("more-info-light")
|
||||
class MoreInfoLight extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: LightEntity;
|
||||
|
||||
@internalProperty() private _brightnessSliderValue = 0;
|
||||
|
||||
@internalProperty() private _ctSliderValue = 0;
|
||||
|
||||
@internalProperty() private _wvSliderValue = 0;
|
||||
|
||||
@internalProperty() private _hueSegments = 24;
|
||||
|
||||
@internalProperty() private _saturationSegments = 8;
|
||||
|
||||
@internalProperty() private _colorPickerColor?: HueSatColor;
|
||||
|
||||
@internalProperty() private _tabIndex = 0;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const supportsBrightness = supportsFeature(
|
||||
this.stateObj!,
|
||||
SUPPORT_BRIGHTNESS
|
||||
);
|
||||
const supportsColorTemp = supportsFeature(
|
||||
this.stateObj,
|
||||
SUPPORT_COLOR_TEMP
|
||||
);
|
||||
const supportsWhiteValue = supportsFeature(
|
||||
this.stateObj,
|
||||
SUPPORT_WHITE_VALUE
|
||||
);
|
||||
const supportsColor = supportsFeature(this.stateObj, SUPPORT_COLOR);
|
||||
const supportsEffect =
|
||||
supportsFeature(this.stateObj, SUPPORT_EFFECT) &&
|
||||
this.stateObj!.attributes.effect_list?.length;
|
||||
|
||||
if (!supportsBrightness && !this._tabIndex) {
|
||||
this._tabIndex = 1;
|
||||
}
|
||||
|
||||
return html`
|
||||
<mwc-tab-bar
|
||||
.activeIndex=${this._tabIndex}
|
||||
@MDCTabBar:activated=${(ev: CustomEvent) => {
|
||||
this._tabIndex = ev.detail.index;
|
||||
}}
|
||||
>
|
||||
${supportsBrightness
|
||||
? html`<mwc-tab label="Brightness"></mwc-tab>`
|
||||
: ""}
|
||||
${supportsColor ||
|
||||
supportsColorTemp ||
|
||||
supportsEffect ||
|
||||
supportsWhiteValue
|
||||
? html`<mwc-tab label="Color"></mwc-tab>`
|
||||
: ""}
|
||||
</mwc-tab-bar>
|
||||
${this._tabIndex === 0
|
||||
? html`
|
||||
${supportsBrightness
|
||||
? html`
|
||||
<div class="brightness">
|
||||
<div>
|
||||
${Math.round((this._brightnessSliderValue / 255) * 100)}%
|
||||
</div>
|
||||
<ha-vertical-range-input
|
||||
max="255"
|
||||
.caption=${this.hass.localize("ui.card.light.brightness")}
|
||||
.value=${this._brightnessSliderValue}
|
||||
@value-changed=${this._brightnessChanged}
|
||||
>
|
||||
</ha-vertical-range-input>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: html`
|
||||
${supportsColorTemp
|
||||
? html`
|
||||
<ha-labeled-slider
|
||||
class="color_temp"
|
||||
icon="hass:thermometer"
|
||||
.caption=${this.hass.localize(
|
||||
"ui.card.light.color_temperature"
|
||||
)}
|
||||
.min=${this.stateObj.attributes.min_mireds}
|
||||
.max=${this.stateObj.attributes.max_mireds}
|
||||
.value=${this._ctSliderValue}
|
||||
@change=${this._colorTempChanged}
|
||||
></ha-labeled-slider>
|
||||
`
|
||||
: ""}
|
||||
${supportsWhiteValue
|
||||
? html`
|
||||
<ha-labeled-slider
|
||||
icon="hass:file-word-box"
|
||||
max="255"
|
||||
.caption=${this.hass.localize("ui.card.light.white_value")}
|
||||
.value=${this._wvSliderValue}
|
||||
@change=${this._whiteValueChanged}
|
||||
></ha-labeled-slider>
|
||||
`
|
||||
: ""}
|
||||
${supportsColor
|
||||
? html`
|
||||
<div class="color-picker">
|
||||
<ha-icon-button
|
||||
icon="hass:palette"
|
||||
@click=${this._segmentClick}
|
||||
></ha-icon-button>
|
||||
<ha-color-picker
|
||||
throttle="500"
|
||||
.desiredHsColor=${this._colorPickerColor}
|
||||
.hueSegments=${this._hueSegments}
|
||||
.saturationSegments=${this._saturationSegments}
|
||||
@colorselected=${this._colorPicked}
|
||||
>
|
||||
</ha-color-picker>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${supportsEffect
|
||||
? html`
|
||||
<ha-paper-dropdown-menu
|
||||
.label=${this.hass.localize("ui.card.light.effect")}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
attr-for-selected="item-name"
|
||||
.selected=${this.stateObj.attributes.effect!}
|
||||
@iron-select=${this._effectChanged}
|
||||
>${this.stateObj.attributes.effect_list!.map(
|
||||
(effect: string) => html`
|
||||
<paper-item .itemName=${effect}>${effect}</paper-item>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
`
|
||||
: ""}
|
||||
`}
|
||||
<div class="padding"></div>
|
||||
${this.hass.user?.is_admin
|
||||
? html`
|
||||
<ha-attributes
|
||||
.stateObj=${this.stateObj}
|
||||
extraFilters="brightness,color_temp,white_value,effect_list,effect,hs_color,rgb_color,xy_color,min_mireds,max_mireds,entity_id"
|
||||
></ha-attributes>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
const stateObj = this.stateObj! as LightEntity;
|
||||
if (changedProps.has("stateObj") && stateObj.state === "on") {
|
||||
this._brightnessSliderValue = stateObj.attributes.brightness;
|
||||
this._ctSliderValue = stateObj.attributes.color_temp || 326;
|
||||
this._wvSliderValue = stateObj.attributes.white_value;
|
||||
|
||||
if (stateObj.attributes.hs_color) {
|
||||
this._colorPickerColor = {
|
||||
h: stateObj.attributes.hs_color[0],
|
||||
s: stateObj.attributes.hs_color[1] / 100,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _effectChanged(ev: CustomEvent) {
|
||||
const newVal = ev.detail.value;
|
||||
|
||||
if (!newVal || this.stateObj!.attributes.effect === newVal) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
effect: newVal,
|
||||
});
|
||||
}
|
||||
|
||||
private _brightnessChanged(ev: CustomEvent) {
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
brightness: parseInt(ev.detail.value, 10),
|
||||
});
|
||||
}
|
||||
|
||||
private _colorTempChanged(ev: CustomEvent) {
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
color_temp: parseInt((ev.currentTarget as any).value, 10),
|
||||
});
|
||||
}
|
||||
|
||||
private _whiteValueChanged(ev: CustomEvent) {
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
white_value: parseInt((ev.target as any).value, 10),
|
||||
});
|
||||
}
|
||||
|
||||
private _segmentClick() {
|
||||
if (this._hueSegments === 24 && this._saturationSegments === 8) {
|
||||
this._hueSegments = 0;
|
||||
this._saturationSegments = 0;
|
||||
} else {
|
||||
this._hueSegments = 24;
|
||||
this._saturationSegments = 8;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a new color has been picked.
|
||||
* should be throttled with the 'throttle=' attribute of the color picker
|
||||
*/
|
||||
private _colorPicked(ev: CustomEvent) {
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
hs_color: [ev.detail.hs.h, ev.detail.hs.s * 100],
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-labeled-slider,
|
||||
ha-paper-dropdown-menu,
|
||||
.padding {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.brightness {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.brightness div {
|
||||
font-size: 20px;
|
||||
line-height: 1.2;
|
||||
padding: 4px 0;
|
||||
font-weight: 500;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.color_temp {
|
||||
--ha-slider-background: -webkit-linear-gradient(
|
||||
right,
|
||||
rgb(255, 160, 0) 0%,
|
||||
white 50%,
|
||||
rgb(166, 209, 255) 100%
|
||||
);
|
||||
/* The color temp minimum value shouldn't be rendered differently. It's not "off". */
|
||||
--paper-slider-knob-start-border-color: var(--primary-color);
|
||||
--ha-slider-border-radius: 8px;
|
||||
}
|
||||
|
||||
.color-picker {
|
||||
position: relative;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.color-picker ha-icon-button {
|
||||
position: absolute;
|
||||
top: 5%;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-color-picker {
|
||||
--ha-color-picker-wheel-borderwidth: 5;
|
||||
--ha-color-picker-wheel-bordercolor: white;
|
||||
--ha-color-picker-wheel-shadow: none;
|
||||
--ha-color-picker-marker-borderwidth: 2;
|
||||
--ha-color-picker-marker-bordercolor: white;
|
||||
}
|
||||
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"more-info-light": MoreInfoLight;
|
||||
}
|
||||
}
|
||||
@@ -1,421 +0,0 @@
|
||||
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { attributeClassNames } from "../../../common/entity/attribute_class_names";
|
||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
import "../../../components/ha-paper-dropdown-menu";
|
||||
import "../../../components/ha-paper-slider";
|
||||
import "../../../components/ha-icon";
|
||||
import { EventsMixin } from "../../../mixins/events-mixin";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
import HassMediaPlayerEntity from "../../../util/hass-media-player-model";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex iron-flex-alignment"></style>
|
||||
<style>
|
||||
.media-state {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
ha-icon-button[highlight] {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.volume {
|
||||
margin-bottom: 8px;
|
||||
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.5s ease-in;
|
||||
}
|
||||
|
||||
.has-volume_level .volume {
|
||||
max-height: 40px;
|
||||
}
|
||||
|
||||
ha-icon.source-input {
|
||||
padding: 7px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
ha-paper-dropdown-menu.source-input {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class$="[[computeClassNames(stateObj)]]">
|
||||
<div class="layout horizontal">
|
||||
<div class="flex">
|
||||
<ha-icon-button
|
||||
icon="hass:power"
|
||||
highlight$="[[playerObj.isOff]]"
|
||||
on-click="handleTogglePower"
|
||||
hidden$="[[computeHidePowerButton(playerObj)]]"
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<div>
|
||||
<template
|
||||
is="dom-if"
|
||||
if="[[computeShowPlaybackControls(playerObj)]]"
|
||||
>
|
||||
<ha-icon-button
|
||||
icon="hass:skip-previous"
|
||||
on-click="handlePrevious"
|
||||
hidden$="[[!playerObj.supportsPreviousTrack]]"
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
icon="[[computePlaybackControlIcon(playerObj)]]"
|
||||
on-click="handlePlaybackControl"
|
||||
hidden$="[[!computePlaybackControlIcon(playerObj)]]"
|
||||
highlight=""
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
icon="hass:skip-next"
|
||||
on-click="handleNext"
|
||||
hidden$="[[!playerObj.supportsNextTrack]]"
|
||||
></ha-icon-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<!-- VOLUME -->
|
||||
<div
|
||||
class="volume_buttons center horizontal layout"
|
||||
hidden$="[[computeHideVolumeButtons(playerObj)]]"
|
||||
>
|
||||
<ha-icon-button
|
||||
on-click="handleVolumeTap"
|
||||
icon="hass:volume-off"
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
id="volumeDown"
|
||||
disabled$="[[playerObj.isMuted]]"
|
||||
on-mousedown="handleVolumeDown"
|
||||
on-touchstart="handleVolumeDown"
|
||||
on-touchend="handleVolumeTouchEnd"
|
||||
icon="hass:volume-medium"
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
id="volumeUp"
|
||||
disabled$="[[playerObj.isMuted]]"
|
||||
on-mousedown="handleVolumeUp"
|
||||
on-touchstart="handleVolumeUp"
|
||||
on-touchend="handleVolumeTouchEnd"
|
||||
icon="hass:volume-high"
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<div
|
||||
class="volume center horizontal layout"
|
||||
hidden$="[[!playerObj.supportsVolumeSet]]"
|
||||
>
|
||||
<ha-icon-button
|
||||
on-click="handleVolumeTap"
|
||||
hidden$="[[playerObj.supportsVolumeButtons]]"
|
||||
icon="[[computeMuteVolumeIcon(playerObj)]]"
|
||||
></ha-icon-button>
|
||||
<ha-paper-slider
|
||||
disabled$="[[playerObj.isMuted]]"
|
||||
min="0"
|
||||
max="100"
|
||||
value="[[playerObj.volumeSliderValue]]"
|
||||
on-change="volumeSliderChanged"
|
||||
class="flex"
|
||||
ignore-bar-touch=""
|
||||
dir="{{rtl}}"
|
||||
>
|
||||
</ha-paper-slider>
|
||||
</div>
|
||||
<!-- SOURCE PICKER -->
|
||||
<div
|
||||
class="controls layout horizontal justified"
|
||||
hidden$="[[computeHideSelectSource(playerObj)]]"
|
||||
>
|
||||
<ha-icon class="source-input" icon="hass:login-variant"></ha-icon>
|
||||
<ha-paper-dropdown-menu
|
||||
class="flex source-input"
|
||||
dynamic-align=""
|
||||
label-float=""
|
||||
label="[[localize('ui.card.media_player.source')]]"
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
attr-for-selected="item-name"
|
||||
selected="[[playerObj.source]]"
|
||||
on-selected-changed="handleSourceChanged"
|
||||
>
|
||||
<template is="dom-repeat" items="[[playerObj.sourceList]]">
|
||||
<paper-item item-name$="[[item]]">[[item]]</paper-item>
|
||||
</template>
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
</div>
|
||||
<!-- SOUND MODE PICKER -->
|
||||
<template is="dom-if" if="[[!computeHideSelectSoundMode(playerObj)]]">
|
||||
<div class="controls layout horizontal justified">
|
||||
<ha-icon class="source-input" icon="hass:music-note"></ha-icon>
|
||||
<ha-paper-dropdown-menu
|
||||
class="flex source-input"
|
||||
dynamic-align
|
||||
label-float
|
||||
label="[[localize('ui.card.media_player.sound_mode')]]"
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
attr-for-selected="item-name"
|
||||
selected="[[playerObj.soundMode]]"
|
||||
on-selected-changed="handleSoundModeChanged"
|
||||
>
|
||||
<template is="dom-repeat" items="[[playerObj.soundModeList]]">
|
||||
<paper-item item-name$="[[item]]">[[item]]</paper-item>
|
||||
</template>
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
</div>
|
||||
</template>
|
||||
<!-- TTS -->
|
||||
<div
|
||||
hidden$="[[computeHideTTS(ttsLoaded, playerObj)]]"
|
||||
class="layout horizontal end"
|
||||
>
|
||||
<paper-input
|
||||
id="ttsInput"
|
||||
label="[[localize('ui.card.media_player.text_to_speak')]]"
|
||||
class="flex"
|
||||
value="{{ttsMessage}}"
|
||||
on-keydown="ttsCheckForEnter"
|
||||
></paper-input>
|
||||
<ha-icon-button icon="hass:send" on-click="sendTTS"></ha-icon-button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
stateObj: Object,
|
||||
playerObj: {
|
||||
type: Object,
|
||||
computed: "computePlayerObj(hass, stateObj)",
|
||||
observer: "playerObjChanged",
|
||||
},
|
||||
|
||||
ttsLoaded: {
|
||||
type: Boolean,
|
||||
computed: "computeTTSLoaded(hass)",
|
||||
},
|
||||
|
||||
ttsMessage: {
|
||||
type: String,
|
||||
value: "",
|
||||
},
|
||||
|
||||
rtl: {
|
||||
type: String,
|
||||
computed: "_computeRTLDirection(hass)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
computePlayerObj(hass, stateObj) {
|
||||
return new HassMediaPlayerEntity(hass, stateObj);
|
||||
}
|
||||
|
||||
playerObjChanged(newVal, oldVal) {
|
||||
if (oldVal) {
|
||||
setTimeout(() => {
|
||||
this.fire("iron-resize");
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
computeClassNames(stateObj) {
|
||||
return attributeClassNames(stateObj, ["volume_level"]);
|
||||
}
|
||||
|
||||
computeMuteVolumeIcon(playerObj) {
|
||||
return playerObj.isMuted ? "hass:volume-off" : "hass:volume-high";
|
||||
}
|
||||
|
||||
computeHideVolumeButtons(playerObj) {
|
||||
return !playerObj.supportsVolumeButtons || playerObj.isOff;
|
||||
}
|
||||
|
||||
computeShowPlaybackControls(playerObj) {
|
||||
return !playerObj.isOff && playerObj.hasMediaControl;
|
||||
}
|
||||
|
||||
computePlaybackControlIcon(playerObj) {
|
||||
if (playerObj.isPlaying) {
|
||||
return playerObj.supportsPause ? "hass:pause" : "hass:stop";
|
||||
}
|
||||
if (playerObj.hasMediaControl || playerObj.isOff || playerObj.isIdle) {
|
||||
if (
|
||||
playerObj.hasMediaControl &&
|
||||
playerObj.supportsPause &&
|
||||
!playerObj.isPaused
|
||||
) {
|
||||
return "hass:play-pause";
|
||||
}
|
||||
return playerObj.supportsPlay ? "hass:play" : null;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
computeHidePowerButton(playerObj) {
|
||||
return playerObj.isOff
|
||||
? !playerObj.supportsTurnOn
|
||||
: !playerObj.supportsTurnOff;
|
||||
}
|
||||
|
||||
computeHideSelectSource(playerObj) {
|
||||
return (
|
||||
playerObj.isOff ||
|
||||
!playerObj.supportsSelectSource ||
|
||||
!playerObj.sourceList
|
||||
);
|
||||
}
|
||||
|
||||
computeHideSelectSoundMode(playerObj) {
|
||||
return (
|
||||
playerObj.isOff ||
|
||||
!playerObj.supportsSelectSoundMode ||
|
||||
!playerObj.soundModeList
|
||||
);
|
||||
}
|
||||
|
||||
computeHideTTS(ttsLoaded, playerObj) {
|
||||
return !ttsLoaded || !playerObj.supportsPlayMedia;
|
||||
}
|
||||
|
||||
computeTTSLoaded(hass) {
|
||||
return isComponentLoaded(hass, "tts");
|
||||
}
|
||||
|
||||
handleTogglePower() {
|
||||
this.playerObj.togglePower();
|
||||
}
|
||||
|
||||
handlePrevious() {
|
||||
this.playerObj.previousTrack();
|
||||
}
|
||||
|
||||
handlePlaybackControl() {
|
||||
this.playerObj.mediaPlayPause();
|
||||
}
|
||||
|
||||
handleNext() {
|
||||
this.playerObj.nextTrack();
|
||||
}
|
||||
|
||||
handleSourceChanged(ev) {
|
||||
if (!this.playerObj) return;
|
||||
|
||||
var oldVal = this.playerObj.source;
|
||||
var newVal = ev.detail.value;
|
||||
|
||||
if (!newVal || oldVal === newVal) return;
|
||||
|
||||
this.playerObj.selectSource(newVal);
|
||||
}
|
||||
|
||||
handleSoundModeChanged(ev) {
|
||||
if (!this.playerObj) return;
|
||||
|
||||
var oldVal = this.playerObj.soundMode;
|
||||
var newVal = ev.detail.value;
|
||||
|
||||
if (!newVal || oldVal === newVal) return;
|
||||
|
||||
this.playerObj.selectSoundMode(newVal);
|
||||
}
|
||||
|
||||
handleVolumeTap() {
|
||||
if (!this.playerObj.supportsVolumeMute) {
|
||||
return;
|
||||
}
|
||||
this.playerObj.volumeMute(!this.playerObj.isMuted);
|
||||
}
|
||||
|
||||
handleVolumeTouchEnd(ev) {
|
||||
/* when touch ends, we must prevent this from
|
||||
* becoming a mousedown, up, click by emulation */
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
handleVolumeUp() {
|
||||
const obj = this.$.volumeUp;
|
||||
this.handleVolumeWorker("volume_up", obj, true);
|
||||
}
|
||||
|
||||
handleVolumeDown() {
|
||||
const obj = this.$.volumeDown;
|
||||
this.handleVolumeWorker("volume_down", obj, true);
|
||||
}
|
||||
|
||||
handleVolumeWorker(service, obj, force) {
|
||||
if (force || (obj !== undefined && obj.pointerDown)) {
|
||||
this.playerObj.callService(service);
|
||||
setTimeout(() => this.handleVolumeWorker(service, obj, false), 500);
|
||||
}
|
||||
}
|
||||
|
||||
volumeSliderChanged(ev) {
|
||||
const volPercentage = parseFloat(ev.target.value);
|
||||
const volume = volPercentage > 0 ? volPercentage / 100 : 0;
|
||||
this.playerObj.setVolume(volume);
|
||||
}
|
||||
|
||||
ttsCheckForEnter(ev) {
|
||||
if (ev.keyCode === 13) this.sendTTS();
|
||||
}
|
||||
|
||||
sendTTS() {
|
||||
const services = this.hass.services.tts;
|
||||
const serviceKeys = Object.keys(services).sort();
|
||||
let service;
|
||||
let i;
|
||||
|
||||
for (i = 0; i < serviceKeys.length; i++) {
|
||||
if (serviceKeys[i].indexOf("_say") !== -1) {
|
||||
service = serviceKeys[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!service) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass.callService("tts", service, {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
message: this.ttsMessage,
|
||||
});
|
||||
this.ttsMessage = "";
|
||||
this.$.ttsInput.focus();
|
||||
}
|
||||
|
||||
_computeRTLDirection(hass) {
|
||||
return computeRTLDirection(hass);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("more-info-media_player", MoreInfoMediaPlayer);
|
||||
381
src/dialogs/more-info/controls/more-info-media_player.ts
Normal file
381
src/dialogs/more-info/controls/more-info-media_player.ts
Normal file
@@ -0,0 +1,381 @@
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
customElement,
|
||||
query,
|
||||
} from "lit-element";
|
||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
import { HomeAssistant, MediaEntity } from "../../../types";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import { UNAVAILABLE_STATES, UNAVAILABLE, UNKNOWN } from "../../../data/entity";
|
||||
import {
|
||||
SUPPORT_TURN_ON,
|
||||
SUPPORT_TURN_OFF,
|
||||
SUPPORTS_PLAY,
|
||||
SUPPORT_PREVIOUS_TRACK,
|
||||
SUPPORT_PAUSE,
|
||||
SUPPORT_STOP,
|
||||
SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_VOLUME_MUTE,
|
||||
SUPPORT_VOLUME_SET,
|
||||
SUPPORT_VOLUME_BUTTONS,
|
||||
SUPPORT_SELECT_SOURCE,
|
||||
SUPPORT_SELECT_SOUND_MODE,
|
||||
SUPPORT_PLAY_MEDIA,
|
||||
ControlButton,
|
||||
} from "../../../data/media-player";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
|
||||
import "../../../components/ha-paper-dropdown-menu";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-slider";
|
||||
import "../../../components/ha-icon";
|
||||
|
||||
@customElement("more-info-media_player")
|
||||
class MoreInfoMediaPlayer extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: MediaEntity;
|
||||
|
||||
@query("#ttsInput") private _ttsInput?: HTMLInputElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.stateObj) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const controls = this._getControls();
|
||||
const stateObj = this.stateObj;
|
||||
|
||||
return html`
|
||||
${!controls
|
||||
? ""
|
||||
: html`
|
||||
<div class="controls">
|
||||
${controls!.map(
|
||||
(control) => html`
|
||||
<ha-icon-button
|
||||
action=${control.action}
|
||||
.icon=${control.icon}
|
||||
@click=${this._handleClick}
|
||||
></ha-icon-button>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
${(supportsFeature(stateObj, SUPPORT_VOLUME_SET) ||
|
||||
supportsFeature(stateObj, SUPPORT_VOLUME_BUTTONS)) &&
|
||||
![UNAVAILABLE, UNKNOWN, "off"].includes(stateObj.state)
|
||||
? html`
|
||||
<div class="volume">
|
||||
${supportsFeature(stateObj, SUPPORT_VOLUME_MUTE)
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.icon=${stateObj.attributes.is_volume_muted
|
||||
? "hass:volume-off"
|
||||
: "hass:volume-high"}
|
||||
@click=${this._toggleMute}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
${supportsFeature(stateObj, SUPPORT_VOLUME_SET)
|
||||
? html`
|
||||
<ha-slider
|
||||
id="input"
|
||||
pin
|
||||
ignore-bar-touch
|
||||
.dir=${computeRTLDirection(this.hass!)}
|
||||
.value=${Number(stateObj.attributes.volume_level) * 100}
|
||||
@change=${this._selectedValueChanged}
|
||||
></ha-slider>
|
||||
`
|
||||
: supportsFeature(stateObj, SUPPORT_VOLUME_BUTTONS)
|
||||
? html`
|
||||
<ha-icon-button
|
||||
action="volume_down"
|
||||
icon="hass:volume-minus"
|
||||
@click=${this._handleClick}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
action="volume_up"
|
||||
icon="hass:volume-plus"
|
||||
@click=${this._handleClick}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${stateObj.state !== "off" &&
|
||||
supportsFeature(stateObj, SUPPORT_SELECT_SOURCE) &&
|
||||
stateObj.attributes.source_list?.length
|
||||
? html`
|
||||
<div class="source-input">
|
||||
<ha-icon class="source-input" icon="hass:login-variant"></ha-icon>
|
||||
<ha-paper-dropdown-menu
|
||||
.label=${this.hass.localize("ui.card.media_player.source")}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
attr-for-selected="item-name"
|
||||
.selected=${stateObj.attributes.source!}
|
||||
@iron-select=${this._handleSourceChanged}
|
||||
>
|
||||
${stateObj.attributes.source_list!.map(
|
||||
(source) =>
|
||||
html`
|
||||
<paper-item .itemName=${source}>${source}</paper-item>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${supportsFeature(stateObj, SUPPORT_SELECT_SOUND_MODE) &&
|
||||
stateObj.attributes.sound_mode_list?.length
|
||||
? html`
|
||||
<div class="sound-input">
|
||||
<ha-icon icon="hass:music-note"></ha-icon>
|
||||
<ha-paper-dropdown-menu
|
||||
dynamic-align
|
||||
label-float
|
||||
.label=${this.hass.localize("ui.card.media_player.sound_mode")}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
attr-for-selected="item-name"
|
||||
.selected=${stateObj.attributes.sound_mode!}
|
||||
@iron-select=${this._handleSoundModeChanged}
|
||||
>
|
||||
${stateObj.attributes.sound_mode_list.map(
|
||||
(mode) => html`
|
||||
<paper-item itemName=${mode}>${mode}</paper-item>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${isComponentLoaded(this.hass, "tts") &&
|
||||
supportsFeature(stateObj, SUPPORT_PLAY_MEDIA)
|
||||
? html`
|
||||
<div class="tts">
|
||||
<paper-input
|
||||
id="ttsInput"
|
||||
.label=${this.hass.localize(
|
||||
"ui.card.media_player.text_to_speak"
|
||||
)}
|
||||
@keydown=${this._ttsCheckForEnter}
|
||||
></paper-input>
|
||||
<ha-icon-button icon="hass:send" @click=${
|
||||
this._sendTTS
|
||||
}></ha-icon-button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-icon-button[action="turn_off"],
|
||||
ha-icon-button[action="turn_on"],
|
||||
ha-slider,
|
||||
#ttsInput {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.volume,
|
||||
.controls,
|
||||
.source-input,
|
||||
.sound-input,
|
||||
.tts {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.source-input ha-icon,
|
||||
.sound-input ha-icon {
|
||||
padding: 7px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.source-input ha-paper-dropdown-menu,
|
||||
.sound-input ha-paper-dropdown-menu {
|
||||
margin-left: 10px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _getControls(): ControlButton[] | undefined {
|
||||
const stateObj = this.stateObj;
|
||||
|
||||
if (!stateObj) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const state = stateObj.state;
|
||||
|
||||
if (UNAVAILABLE_STATES.includes(state)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (state === "off") {
|
||||
return supportsFeature(stateObj, SUPPORT_TURN_ON)
|
||||
? [
|
||||
{
|
||||
icon: "hass:power",
|
||||
action: "turn_on",
|
||||
},
|
||||
]
|
||||
: undefined;
|
||||
}
|
||||
|
||||
if (state === "idle") {
|
||||
return supportsFeature(stateObj, SUPPORTS_PLAY)
|
||||
? [
|
||||
{
|
||||
icon: "hass:play",
|
||||
action: "media_play",
|
||||
},
|
||||
]
|
||||
: undefined;
|
||||
}
|
||||
|
||||
const buttons: ControlButton[] = [];
|
||||
|
||||
if (supportsFeature(stateObj, SUPPORT_TURN_OFF)) {
|
||||
buttons.push({
|
||||
icon: "hass:power",
|
||||
action: "turn_off",
|
||||
});
|
||||
}
|
||||
|
||||
if (supportsFeature(stateObj, SUPPORT_PREVIOUS_TRACK)) {
|
||||
buttons.push({
|
||||
icon: "hass:skip-previous",
|
||||
action: "media_previous_track",
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
(state === "playing" &&
|
||||
(supportsFeature(stateObj, SUPPORT_PAUSE) ||
|
||||
supportsFeature(stateObj, SUPPORT_STOP))) ||
|
||||
(state === "paused" && supportsFeature(stateObj, SUPPORTS_PLAY))
|
||||
) {
|
||||
buttons.push({
|
||||
icon:
|
||||
state !== "playing"
|
||||
? "hass:play"
|
||||
: supportsFeature(stateObj, SUPPORT_PAUSE)
|
||||
? "hass:pause"
|
||||
: "hass:stop",
|
||||
action: "media_play_pause",
|
||||
});
|
||||
}
|
||||
|
||||
if (supportsFeature(stateObj, SUPPORT_NEXT_TRACK)) {
|
||||
buttons.push({
|
||||
icon: "hass:skip-next",
|
||||
action: "media_next_track",
|
||||
});
|
||||
}
|
||||
|
||||
return buttons.length > 0 ? buttons : undefined;
|
||||
}
|
||||
|
||||
private _handleClick(e: MouseEvent): void {
|
||||
this.hass!.callService(
|
||||
"media_player",
|
||||
(e.currentTarget! as HTMLElement).getAttribute("action")!,
|
||||
{
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _toggleMute() {
|
||||
this.hass!.callService("media_player", "volume_mute", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
is_volume_muted: !this.stateObj!.attributes.is_volume_muted,
|
||||
});
|
||||
}
|
||||
|
||||
private _selectedValueChanged(e: Event): void {
|
||||
this.hass!.callService("media_player", "volume_set", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
volume_level:
|
||||
Number((e.currentTarget! as HTMLElement).getAttribute("value")!) / 100,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSourceChanged(e: CustomEvent) {
|
||||
const newVal = e.detail.value;
|
||||
|
||||
if (!newVal || this.stateObj!.attributes.source === newVal) return;
|
||||
|
||||
this.hass.callService("media_player", "select_source", {
|
||||
source: newVal,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSoundModeChanged(e: CustomEvent) {
|
||||
const newVal = e.detail.value;
|
||||
|
||||
if (!newVal || this.stateObj?.attributes.sound_mode === newVal) return;
|
||||
|
||||
this.hass.callService("media_player", "select_sound_mode", {
|
||||
sound_mode: newVal,
|
||||
});
|
||||
}
|
||||
|
||||
private _ttsCheckForEnter(e: KeyboardEvent) {
|
||||
if (e.keyCode === 13) this._sendTTS();
|
||||
}
|
||||
|
||||
private _sendTTS() {
|
||||
const ttsInput = this._ttsInput;
|
||||
if (!ttsInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
const services = this.hass.services.tts;
|
||||
const serviceKeys = Object.keys(services).sort();
|
||||
|
||||
const service = serviceKeys.find((key) => key.indexOf("_say") !== -1);
|
||||
|
||||
if (!service) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass.callService("tts", service, {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
message: ttsInput.value,
|
||||
});
|
||||
ttsInput.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"more-info-media_player": MoreInfoMediaPlayer;
|
||||
}
|
||||
}
|
||||
@@ -193,7 +193,7 @@ class MoreInfoWaterHeater extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
4: "has-away_mode",
|
||||
};
|
||||
|
||||
var classes = [featureClassNames(stateObj, _featureClassNames)];
|
||||
const classes = [featureClassNames(stateObj, _featureClassNames)];
|
||||
|
||||
classes.push("more-info-water_heater");
|
||||
|
||||
|
||||
@@ -254,7 +254,7 @@ export class MoreInfoDialog extends LitElement {
|
||||
|
||||
ha-header-bar {
|
||||
--mdc-theme-on-primary: var(--primary-text-color);
|
||||
--mdc-theme-primary: var(--card-background-color);
|
||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid
|
||||
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
|
||||
|
||||
@@ -29,9 +29,17 @@ export class HuiNotificationDrawer extends EventsMixin(
|
||||
width: calc(100% - 32px);
|
||||
}
|
||||
|
||||
div[main-title] {
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
}
|
||||
|
||||
.notifications {
|
||||
overflow-y: auto;
|
||||
padding-top: 16px;
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
height: calc(100% - 65px);
|
||||
box-sizing: border-box;
|
||||
background-color: var(--primary-background-color);
|
||||
|
||||
@@ -426,7 +426,7 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
text-align: right;
|
||||
border-bottom-right-radius: 0px;
|
||||
background-color: var(--light-primary-color);
|
||||
color: var(--primary-text-color);
|
||||
color: var(--text-light-primary-color, var(--primary-text-color));
|
||||
}
|
||||
|
||||
.message.hass {
|
||||
|
||||
@@ -84,7 +84,7 @@ function initPushNotifications() {
|
||||
}
|
||||
|
||||
self.addEventListener("push", function (event) {
|
||||
var data;
|
||||
let data;
|
||||
if (event.data) {
|
||||
data = event.data.json();
|
||||
if (data.dismiss) {
|
||||
@@ -113,8 +113,6 @@ function initPushNotifications() {
|
||||
});
|
||||
|
||||
self.addEventListener("notificationclick", function (event) {
|
||||
var url;
|
||||
|
||||
notificationEventCallback("clicked", event);
|
||||
|
||||
event.notification.close();
|
||||
@@ -127,7 +125,7 @@ function initPushNotifications() {
|
||||
return;
|
||||
}
|
||||
|
||||
url = event.notification.data.url;
|
||||
const url = event.notification.data.url;
|
||||
|
||||
if (!url) return;
|
||||
|
||||
@@ -137,8 +135,8 @@ function initPushNotifications() {
|
||||
type: "window",
|
||||
})
|
||||
.then(function (windowClients) {
|
||||
var i;
|
||||
var client;
|
||||
let i;
|
||||
let client;
|
||||
for (i = 0; i < windowClients.length; i++) {
|
||||
client = windowClients[i];
|
||||
if (client.url === url && "focus" in client) {
|
||||
|
||||
@@ -180,7 +180,9 @@ export const provideHass = (
|
||||
config: demoConfig,
|
||||
themes: {
|
||||
default_theme: "default",
|
||||
default_dark_theme: null,
|
||||
themes: {},
|
||||
darkMode: false,
|
||||
},
|
||||
panels: demoPanels,
|
||||
services: demoServices,
|
||||
@@ -253,7 +255,7 @@ export const provideHass = (
|
||||
mockTheme(theme) {
|
||||
invalidateThemeCache();
|
||||
hass().updateHass({
|
||||
selectedTheme: theme ? "mock" : "default",
|
||||
selectedTheme: { theme: theme ? "mock" : "default" },
|
||||
themes: {
|
||||
...hass().themes,
|
||||
themes: {
|
||||
@@ -265,7 +267,7 @@ export const provideHass = (
|
||||
applyThemesOnElement(
|
||||
document.documentElement,
|
||||
themes,
|
||||
selectedTheme as string
|
||||
selectedTheme!.theme
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<meta name='viewport' content='width=device-width, user-scalable=no'>
|
||||
<meta name='viewport' content='width=device-width, user-scalable=no, viewport-fit=cover'>
|
||||
<style>
|
||||
body {
|
||||
font-family: Roboto, sans-serif;
|
||||
@@ -7,6 +7,6 @@
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,14 +5,16 @@
|
||||
<link rel="preload" href="<%= latestAppJS %>" as="script" crossorigin="use-credentials" />
|
||||
<%= renderTemplate('_header') %>
|
||||
<title>Home Assistant</title>
|
||||
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#03a9f4" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/static/icons/favicon-apple-180x180.png"
|
||||
/>
|
||||
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#03a9f4" />
|
||||
<meta name="apple-itunes-app" content="app-id=1099568401" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||
<meta name="apple-mobile-web-app-title" content="Home Assistant">
|
||||
<meta
|
||||
name="msapplication-square70x70logo"
|
||||
content="/static/icons/tile-win-70x70.png"
|
||||
@@ -33,6 +35,7 @@
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="referrer" content="same-origin" />
|
||||
<meta name="theme-color" content="#THEMEC" />
|
||||
<meta name="color-scheme" content="dark light" />
|
||||
<style>
|
||||
#ha-init-skeleton::before {
|
||||
display: block;
|
||||
@@ -41,7 +44,15 @@
|
||||
background-color: #THEMEC;
|
||||
}
|
||||
html {
|
||||
background-color: var(--primary-background-color, #fafafa);
|
||||
background-color: var(--primary-background-color);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background-color: var(--primary-background-color, #111111);
|
||||
}
|
||||
#ha-init-skeleton::before {
|
||||
background-color: #1c1c1c;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -77,6 +77,8 @@ class HaAppLayout extends customElements.get("app-header-layout") {
|
||||
/* Using 'transform' will cause 'position: fixed' elements to behave like
|
||||
'position: absolute' relative to this element. */
|
||||
transform: translate(0);
|
||||
margin-left: env(safe-area-inset-left);
|
||||
margin-right: env(safe-area-inset-right);
|
||||
}
|
||||
|
||||
@media print {
|
||||
|
||||
@@ -60,6 +60,7 @@ class HaInitPage extends LitElement {
|
||||
}
|
||||
p {
|
||||
max-width: 350px;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -60,6 +60,11 @@ class HassSubpage extends LitElement {
|
||||
background-color: var(--primary-background-color);
|
||||
}
|
||||
|
||||
:host([narrow]) {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -26,9 +26,9 @@ import { computeRTLDirection } from "../common/util/compute_rtl";
|
||||
export class HaTabsSubpageDataTable extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public isWide!: boolean;
|
||||
@property({ type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
/**
|
||||
* Object with the columns.
|
||||
@@ -110,6 +110,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
.backPath=${this.backPath}
|
||||
.backCallback=${this.backCallback}
|
||||
.route=${this.route}
|
||||
@@ -168,38 +169,37 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
? html`
|
||||
<div slot="header">
|
||||
<slot name="header">
|
||||
<slot name="header">
|
||||
<div class="table-header">
|
||||
<search-input
|
||||
.filter=${this.filter}
|
||||
no-label-float
|
||||
no-underline
|
||||
@value-changed=${this._handleSearchChange}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.data-table.search"
|
||||
)}
|
||||
>
|
||||
</search-input>
|
||||
${this.activeFilters
|
||||
? html`<div class="active-filters">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.filtering.filtering_by"
|
||||
)}
|
||||
${this.activeFilters.join(", ")}
|
||||
<mwc-button @click=${this._clearFilter}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.filtering.clear"
|
||||
)}</mwc-button
|
||||
>
|
||||
</div>`
|
||||
: ""}
|
||||
</div></slot
|
||||
></slot
|
||||
>
|
||||
<div class="table-header">
|
||||
<search-input
|
||||
.filter=${this.filter}
|
||||
no-label-float
|
||||
no-underline
|
||||
@value-changed=${this._handleSearchChange}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.data-table.search"
|
||||
)}
|
||||
>
|
||||
</search-input>
|
||||
${this.activeFilters
|
||||
? html`<div class="active-filters">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.filtering.filtering_by"
|
||||
)}
|
||||
${this.activeFilters.join(", ")}
|
||||
<mwc-button @click=${this._clearFilter}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.filtering.clear"
|
||||
)}</mwc-button
|
||||
>
|
||||
</div>`
|
||||
: ""}
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
`
|
||||
: html` <div slot="header"></div> `}
|
||||
</ha-data-table>
|
||||
<div slot="fab"><slot name="fab"></slot></div>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import "../components/ha-svg-icon";
|
||||
import "../components/ha-icon";
|
||||
import "../components/ha-tab";
|
||||
import { restoreScroll } from "../common/decorators/restore-scroll";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
|
||||
export interface PageNavigation {
|
||||
path: string;
|
||||
@@ -53,6 +54,11 @@ class HassTabsSubpage extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "is-wide" })
|
||||
public isWide = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public rtl = false;
|
||||
|
||||
@internalProperty() private _activeTab?: PageNavigation;
|
||||
|
||||
// @ts-ignore
|
||||
@@ -107,6 +113,14 @@ class HassTabsSubpage extends LitElement {
|
||||
`${this.route.prefix}${this.route.path}`.includes(tab.path)
|
||||
);
|
||||
}
|
||||
if (changedProperties.has("hass")) {
|
||||
const oldHass = changedProperties.get("hass") as
|
||||
| HomeAssistant
|
||||
| undefined;
|
||||
if (!oldHass || oldHass.language !== this.hass.language) {
|
||||
this.rtl = computeRTL(this.hass);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -152,6 +166,7 @@ class HassTabsSubpage extends LitElement {
|
||||
<div class="content" @scroll=${this._saveScrollPos}>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div id="fab"><slot name="fab"></slot></div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -184,6 +199,11 @@ class HassTabsSubpage extends LitElement {
|
||||
background-color: var(--primary-background-color);
|
||||
}
|
||||
|
||||
:host([narrow]) {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
ha-menu-button {
|
||||
margin-right: 24px;
|
||||
}
|
||||
@@ -215,9 +235,10 @@ class HassTabsSubpage extends LitElement {
|
||||
background-color: var(--sidebar-background-color);
|
||||
border-top: 1px solid var(--divider-color);
|
||||
justify-content: space-between;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
#tabbar:not(.bottom-bar) {
|
||||
@@ -247,7 +268,11 @@ class HassTabsSubpage extends LitElement {
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
width: calc(
|
||||
100% - env(safe-area-inset-left) - env(safe-area-inset-right)
|
||||
);
|
||||
margin-left: env(safe-area-inset-left);
|
||||
margin-right: env(safe-area-inset-right);
|
||||
height: calc(100% - 65px);
|
||||
overflow-y: auto;
|
||||
overflow: auto;
|
||||
@@ -256,6 +281,30 @@ class HassTabsSubpage extends LitElement {
|
||||
|
||||
:host([narrow]) .content {
|
||||
height: calc(100% - 128px);
|
||||
height: calc(100% - 128px - env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
#fab {
|
||||
position: fixed;
|
||||
right: calc(16px + env(safe-area-inset-right));
|
||||
bottom: calc(16px + env(safe-area-inset-bottom));
|
||||
z-index: 1;
|
||||
}
|
||||
:host([narrow]) #fab {
|
||||
bottom: calc(84px + env(safe-area-inset-bottom));
|
||||
}
|
||||
#fab[is-wide] {
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
:host([rtl]) #fab {
|
||||
right: auto;
|
||||
left: calc(16px + env(safe-area-inset-left));
|
||||
}
|
||||
:host([rtl][is-wide]) #fab {
|
||||
bottom: 24px;
|
||||
left: 24px;
|
||||
right: auto;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -152,16 +152,13 @@ class HomeAssistantMain extends LitElement {
|
||||
--app-drawer-width: 64px;
|
||||
}
|
||||
:host([expanded]) {
|
||||
--app-drawer-width: 256px;
|
||||
--app-drawer-width: calc(256px + env(safe-area-inset-left));
|
||||
}
|
||||
partial-panel-resolver,
|
||||
ha-sidebar {
|
||||
/* allow a light tap highlight on the actual interface elements */
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
partial-panel-resolver {
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,9 +62,6 @@ class IntegrationBadge extends LitElement {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:host([clickable]) .icon {
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
color: white;
|
||||
@@ -72,9 +69,8 @@ class IntegrationBadge extends LitElement {
|
||||
right: -10px;
|
||||
background-color: var(--label-badge-green);
|
||||
border-radius: 50%;
|
||||
width: 18px;
|
||||
display: block;
|
||||
height: 18px;
|
||||
--mdc-icon-size: 18px;
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ class OnboardingCoreConfig extends LitElement {
|
||||
<div class="row">
|
||||
<ha-location-editor
|
||||
class="flex"
|
||||
.hass=${this.hass}
|
||||
.location=${this._locationValue}
|
||||
.fitZoom=${14}
|
||||
@change=${this._locationChanged}
|
||||
|
||||
@@ -211,6 +211,7 @@ class HAFullCalendar extends LitElement {
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
--fc-theme-standard-border-color: var(--divider-color);
|
||||
}
|
||||
|
||||
.header {
|
||||
@@ -234,6 +235,10 @@ class HAFullCalendar extends LitElement {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -259,6 +264,10 @@ class HAFullCalendar extends LitElement {
|
||||
background-color: var(--card-background-color);
|
||||
}
|
||||
|
||||
.fc-theme-standard .fc-scrollgrid {
|
||||
border: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.fc-scrollgrid-section-header td {
|
||||
border: none;
|
||||
}
|
||||
@@ -293,14 +302,15 @@ class HAFullCalendar extends LitElement {
|
||||
|
||||
td.fc-day-today .fc-daygrid-day-number {
|
||||
height: 24px;
|
||||
color: #fff;
|
||||
background-color: #1a73e8;
|
||||
color: var(--text-primary-color);
|
||||
background-color: var(--primary-color);
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
width: max-content;
|
||||
min-width: 24px;
|
||||
line-height: 140%;
|
||||
}
|
||||
|
||||
.fc-daygrid-day-events {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "lit-element";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
|
||||
import "@polymer/app-layout/app-header-layout/app-header-layout";
|
||||
import "../../layouts/ha-app-layout";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import "@material/mwc-checkbox";
|
||||
@@ -63,7 +63,7 @@ class PanelCalendar extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<app-header-layout>
|
||||
<ha-app-layout>
|
||||
<app-header fixed slot="header">
|
||||
<app-toolbar>
|
||||
<ha-menu-button
|
||||
@@ -106,7 +106,7 @@ class PanelCalendar extends LitElement {
|
||||
@view-changed=${this._handleViewChanged}
|
||||
></ha-full-calendar>
|
||||
</div>
|
||||
</app-header-layout>
|
||||
</ha-app-layout>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
showAreaRegistryDetailDialog,
|
||||
} from "./show-dialog-area-registry-detail";
|
||||
import { mdiPlus } from "@mdi/js";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
|
||||
@customElement("ha-config-areas-dashboard")
|
||||
export class HaConfigAreasDashboard extends LitElement {
|
||||
@@ -106,6 +105,7 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
back-path="/config"
|
||||
.tabs=${configSections.integrations}
|
||||
.route=${this.route}
|
||||
@@ -123,18 +123,16 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
icon="hass:help-circle"
|
||||
@click=${this._showHelp}
|
||||
></ha-icon-button>
|
||||
<mwc-fab
|
||||
slot="fab"
|
||||
title="${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.create_area"
|
||||
)}"
|
||||
@click=${this._createArea}
|
||||
>
|
||||
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
|
||||
</mwc-fab>
|
||||
</hass-tabs-subpage-data-table>
|
||||
<mwc-fab
|
||||
?is-wide=${this.isWide}
|
||||
?narrow=${this.narrow}
|
||||
?rtl=${computeRTL(this.hass!)}
|
||||
title="${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.create_area"
|
||||
)}"
|
||||
@click=${this._createArea}
|
||||
>
|
||||
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
|
||||
</mwc-fab>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -183,28 +181,6 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
--app-header-background-color: var(--sidebar-background-color);
|
||||
--app-header-text-color: var(--sidebar-text-color);
|
||||
}
|
||||
mwc-fab {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
mwc-fab[is-wide] {
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
mwc-fab[narrow] {
|
||||
bottom: 84px;
|
||||
}
|
||||
mwc-fab[rtl] {
|
||||
right: auto;
|
||||
left: 16px;
|
||||
}
|
||||
mwc-fab[is-wide][rtl] {
|
||||
bottom: 24px;
|
||||
left: 24px;
|
||||
right: auto;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
@@ -29,6 +30,8 @@ import "./types/ha-automation-action-event";
|
||||
import "./types/ha-automation-action-scene";
|
||||
import "./types/ha-automation-action-service";
|
||||
import "./types/ha-automation-action-wait_template";
|
||||
import "./types/ha-automation-action-repeat";
|
||||
import "./types/ha-automation-action-choose";
|
||||
import { handleStructError } from "../../../lovelace/common/structs/handle-errors";
|
||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
@@ -41,6 +44,8 @@ const OPTIONS = [
|
||||
"scene",
|
||||
"service",
|
||||
"wait_template",
|
||||
"repeat",
|
||||
"choose",
|
||||
];
|
||||
|
||||
const getType = (action: Action) => {
|
||||
@@ -96,6 +101,16 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
|
||||
@internalProperty() private _yamlMode = false;
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
if (!changedProperties.has("action")) {
|
||||
return;
|
||||
}
|
||||
this._uiModeAvailable = Boolean(getType(this.action));
|
||||
if (!this._uiModeAvailable && !this._yamlMode) {
|
||||
this._yamlMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const type = getType(this.action);
|
||||
const selected = type ? OPTIONS.indexOf(type) : -1;
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
customElement,
|
||||
LitElement,
|
||||
property,
|
||||
CSSResult,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import { html } from "lit-html";
|
||||
import { Action, ChooseAction } from "../../../../../data/script";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import { ActionElement } from "../ha-automation-action-row";
|
||||
import "../../condition/ha-automation-condition-editor";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../ha-automation-action";
|
||||
import { Condition } from "../../../../../data/automation";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import { mdiDelete } from "@mdi/js";
|
||||
|
||||
@customElement("ha-automation-action-choose")
|
||||
export class HaChooseAction extends LitElement implements ActionElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public action!: ChooseAction;
|
||||
|
||||
public static get defaultConfig() {
|
||||
return { choose: [{ conditions: [], sequence: [] }], default: [] };
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const action = this.action;
|
||||
|
||||
return html`
|
||||
${action.choose.map(
|
||||
(option, idx) => html`<ha-card>
|
||||
<mwc-icon-button
|
||||
.idx=${idx}
|
||||
@click=${this._removeOption}
|
||||
title=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon path=${mdiDelete}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
<div class="card-content">
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.option",
|
||||
"number",
|
||||
idx + 1
|
||||
)}:
|
||||
</h2>
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.conditions"
|
||||
)}:
|
||||
</h3>
|
||||
<ha-automation-condition
|
||||
.conditions=${option.conditions}
|
||||
.hass=${this.hass}
|
||||
.idx=${idx}
|
||||
@value-changed=${this._conditionChanged}
|
||||
></ha-automation-condition>
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.sequence"
|
||||
)}:
|
||||
</h3>
|
||||
<ha-automation-action
|
||||
.actions=${option.sequence}
|
||||
.hass=${this.hass}
|
||||
.idx=${idx}
|
||||
@value-changed=${this._actionChanged}
|
||||
></ha-automation-action>
|
||||
</div>
|
||||
</ha-card>`
|
||||
)}
|
||||
<ha-card>
|
||||
<div class="card-actions add-card">
|
||||
<mwc-button @click=${this._addOption}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.add_option"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.default"
|
||||
)}:
|
||||
</h2>
|
||||
<ha-automation-action
|
||||
.actions=${action.default || []}
|
||||
@value-changed=${this._defaultChanged}
|
||||
.hass=${this.hass}
|
||||
></ha-automation-action>
|
||||
`;
|
||||
}
|
||||
|
||||
private _conditionChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value as Condition[];
|
||||
const index = (ev.target as any).idx;
|
||||
const choose = [...this.action.choose];
|
||||
choose[index].conditions = value;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.action, choose },
|
||||
});
|
||||
}
|
||||
|
||||
private _actionChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value as Action[];
|
||||
const index = (ev.target as any).idx;
|
||||
const choose = [...this.action.choose];
|
||||
choose[index].sequence = value;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.action, choose },
|
||||
});
|
||||
}
|
||||
|
||||
private _addOption() {
|
||||
const choose = [...this.action.choose];
|
||||
choose.push({ conditions: [], sequence: [] });
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.action, choose },
|
||||
});
|
||||
}
|
||||
|
||||
private _removeOption(ev: CustomEvent) {
|
||||
const index = (ev.currentTarget as any).idx;
|
||||
const choose = [...this.action.choose];
|
||||
choose.splice(index, 1);
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.action, choose },
|
||||
});
|
||||
}
|
||||
|
||||
private _defaultChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value as Action[];
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.action,
|
||||
default: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
ha-card {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.add-card mwc-button {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
mwc-icon-button {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
padding: 4px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-action-choose": HaChooseAction;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
DeviceAction,
|
||||
deviceAutomationsEqual,
|
||||
fetchDeviceActionCapabilities,
|
||||
DeviceCapabilities,
|
||||
} from "../../../../../data/device_automation";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
|
||||
@@ -21,11 +22,11 @@ import { HomeAssistant } from "../../../../../types";
|
||||
export class HaDeviceAction extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public action!: DeviceAction;
|
||||
@property({ type: Object }) public action!: DeviceAction;
|
||||
|
||||
@internalProperty() private _deviceId?: string;
|
||||
|
||||
@internalProperty() private _capabilities?;
|
||||
@internalProperty() private _capabilities?: DeviceCapabilities;
|
||||
|
||||
private _origAction?: DeviceAction;
|
||||
|
||||
@@ -37,20 +38,20 @@ export class HaDeviceAction extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private _extraFieldsData = memoizeOne((capabilities, action: DeviceAction) =>
|
||||
capabilities && capabilities.extra_fields
|
||||
? capabilities.extra_fields.map((item) => {
|
||||
return { [item.name]: action[item.name] };
|
||||
})
|
||||
: undefined
|
||||
private _extraFieldsData = memoizeOne(
|
||||
(action: DeviceAction, capabilities: DeviceCapabilities) => {
|
||||
const extraFieldsData: { [key: string]: any } = {};
|
||||
capabilities.extra_fields.forEach((item) => {
|
||||
if (action[item.name] !== undefined) {
|
||||
extraFieldsData![item.name] = action[item.name];
|
||||
}
|
||||
});
|
||||
return extraFieldsData;
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const deviceId = this._deviceId || this.action.device_id;
|
||||
const extraFieldsData = this._extraFieldsData(
|
||||
this._capabilities,
|
||||
this.action
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-device-picker
|
||||
@@ -70,10 +71,10 @@ export class HaDeviceAction extends LitElement {
|
||||
"ui.panel.config.automation.editor.actions.type.device_id.action"
|
||||
)}
|
||||
></ha-device-action-picker>
|
||||
${extraFieldsData
|
||||
${this._capabilities?.extra_fields
|
||||
? html`
|
||||
<ha-form
|
||||
.data=${Object.assign({}, ...extraFieldsData)}
|
||||
.data=${this._extraFieldsData(this.action, this._capabilities)}
|
||||
.schema=${this._capabilities.extra_fields}
|
||||
.computeLabel=${this._extraFieldsComputeLabelCallback(
|
||||
this.hass.localize
|
||||
@@ -105,7 +106,7 @@ export class HaDeviceAction extends LitElement {
|
||||
private async _getCapabilities() {
|
||||
this._capabilities = this.action.domain
|
||||
? await fetchDeviceActionCapabilities(this.hass, this.action)
|
||||
: null;
|
||||
: undefined;
|
||||
}
|
||||
|
||||
private _devicePicked(ev) {
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import { customElement, LitElement, property, CSSResult } from "lit-element";
|
||||
import { html } from "lit-html";
|
||||
import {
|
||||
RepeatAction,
|
||||
Action,
|
||||
CountRepeat,
|
||||
WhileRepeat,
|
||||
UntilRepeat,
|
||||
} from "../../../../../data/script";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import { ActionElement } from "../ha-automation-action-row";
|
||||
import "../../condition/ha-automation-condition-editor";
|
||||
import type { PaperListboxElement } from "@polymer/paper-listbox";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../ha-automation-action";
|
||||
import { Condition } from "../../../../lovelace/common/validate-condition";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
|
||||
const OPTIONS = ["count", "while", "until"];
|
||||
|
||||
const getType = (action) => {
|
||||
return OPTIONS.find((option) => option in action);
|
||||
};
|
||||
|
||||
@customElement("ha-automation-action-repeat")
|
||||
export class HaRepeatAction extends LitElement implements ActionElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public action!: RepeatAction;
|
||||
|
||||
public static get defaultConfig() {
|
||||
return { repeat: { count: 2, sequence: [] } };
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const action = this.action.repeat;
|
||||
|
||||
const type = getType(action);
|
||||
const selected = type ? OPTIONS.indexOf(type) : -1;
|
||||
|
||||
return html`
|
||||
<paper-dropdown-menu-light
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.repeat.type_select"
|
||||
)}
|
||||
no-animations
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
.selected=${selected}
|
||||
@iron-select=${this._typeChanged}
|
||||
>
|
||||
${OPTIONS.map(
|
||||
(opt) => html`
|
||||
<paper-item .action=${opt}>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.actions.type.repeat.type.${opt}.label`
|
||||
)}
|
||||
</paper-item>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu-light>
|
||||
${type === "count"
|
||||
? html`<paper-input
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.repeat.type.count.label"
|
||||
)}
|
||||
name="count"
|
||||
.value=${(action as CountRepeat).count || "0"}
|
||||
@value-changed=${this._countChanged}
|
||||
></paper-input>`
|
||||
: ""}
|
||||
${type === "while"
|
||||
? html` <h3>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.actions.type.repeat.type.while.conditions`
|
||||
)}:
|
||||
</h3>
|
||||
<ha-automation-condition
|
||||
.conditions=${(action as WhileRepeat).while || []}
|
||||
.hass=${this.hass}
|
||||
@value-changed=${this._conditionChanged}
|
||||
></ha-automation-condition>`
|
||||
: ""}
|
||||
${type === "until"
|
||||
? html` <h3>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.actions.type.repeat.type.until.conditions`
|
||||
)}:
|
||||
</h3>
|
||||
<ha-automation-condition
|
||||
.conditions=${(action as UntilRepeat).until || []}
|
||||
.hass=${this.hass}
|
||||
@value-changed=${this._conditionChanged}
|
||||
></ha-automation-condition>`
|
||||
: ""}
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.repeat.sequence"
|
||||
)}:
|
||||
</h3>
|
||||
<ha-automation-action
|
||||
.actions=${action.sequence}
|
||||
@value-changed=${this._actionChanged}
|
||||
.hass=${this.hass}
|
||||
></ha-automation-action>
|
||||
`;
|
||||
}
|
||||
|
||||
private _typeChanged(ev: CustomEvent) {
|
||||
const type = ((ev.target as PaperListboxElement)?.selectedItem as any)
|
||||
?.action;
|
||||
|
||||
if (!type || type === getType(this.action.repeat)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = type === "count" ? 2 : [];
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
repeat: { [type]: value, sequence: this.action.repeat.sequence },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _conditionChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value as Condition[];
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
repeat: {
|
||||
...this.action.repeat,
|
||||
[getType(this.action.repeat)!]: value,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _actionChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value as Action[];
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
repeat: {
|
||||
...this.action.repeat,
|
||||
sequence: value,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _countChanged(ev: CustomEvent): void {
|
||||
const newVal = ev.detail.value;
|
||||
if ((this.action.repeat as CountRepeat).count === newVal) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
repeat: {
|
||||
...this.action.repeat,
|
||||
count: newVal,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return haStyle;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-action-repeat": HaRepeatAction;
|
||||
}
|
||||
}
|
||||
@@ -13,18 +13,20 @@ import {
|
||||
deviceAutomationsEqual,
|
||||
DeviceCondition,
|
||||
fetchDeviceConditionCapabilities,
|
||||
DeviceCapabilities,
|
||||
} from "../../../../../data/device_automation";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import memoizeOne from "memoize-one";
|
||||
|
||||
@customElement("ha-automation-condition-device")
|
||||
export class HaDeviceCondition extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public condition!: DeviceCondition;
|
||||
@property({ type: Object }) public condition!: DeviceCondition;
|
||||
|
||||
@internalProperty() private _deviceId?: string;
|
||||
|
||||
@internalProperty() private _capabilities?;
|
||||
@internalProperty() private _capabilities?: DeviceCapabilities;
|
||||
|
||||
private _origCondition?: DeviceCondition;
|
||||
|
||||
@@ -36,16 +38,21 @@ export class HaDeviceCondition extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private _extraFieldsData = memoizeOne(
|
||||
(condition: DeviceCondition, capabilities: DeviceCapabilities) => {
|
||||
const extraFieldsData: { [key: string]: any } = {};
|
||||
capabilities.extra_fields.forEach((item) => {
|
||||
if (condition[item.name] !== undefined) {
|
||||
extraFieldsData![item.name] = condition[item.name];
|
||||
}
|
||||
});
|
||||
return extraFieldsData;
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const deviceId = this._deviceId || this.condition.device_id;
|
||||
|
||||
const extraFieldsData =
|
||||
this._capabilities && this._capabilities.extra_fields
|
||||
? this._capabilities.extra_fields.map((item) => {
|
||||
return { [item.name]: this.condition[item.name] };
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<ha-device-picker
|
||||
.value=${deviceId}
|
||||
@@ -64,10 +71,10 @@ export class HaDeviceCondition extends LitElement {
|
||||
"ui.panel.config.automation.editor.conditions.type.device.condition"
|
||||
)}
|
||||
></ha-device-condition-picker>
|
||||
${extraFieldsData
|
||||
${this._capabilities?.extra_fields
|
||||
? html`
|
||||
<ha-form
|
||||
.data=${Object.assign({}, ...extraFieldsData)}
|
||||
.data=${this._extraFieldsData(this.condition, this._capabilities)}
|
||||
.schema=${this._capabilities.extra_fields}
|
||||
.computeLabel=${this._extraFieldsComputeLabelCallback(
|
||||
this.hass.localize
|
||||
@@ -103,7 +110,7 @@ export class HaDeviceCondition extends LitElement {
|
||||
|
||||
this._capabilities = condition.domain
|
||||
? await fetchDeviceConditionCapabilities(this.hass, condition)
|
||||
: null;
|
||||
: undefined;
|
||||
}
|
||||
|
||||
private _devicePicked(ev) {
|
||||
@@ -141,3 +148,9 @@ export class HaDeviceCondition extends LitElement {
|
||||
) || schema.name;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-condition-device": HaDeviceCondition;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,7 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "@material/mwc-fab";
|
||||
@@ -46,6 +44,7 @@ import "./trigger/ha-automation-trigger";
|
||||
import { HaDeviceTrigger } from "./trigger/types/ha-automation-trigger-device";
|
||||
import { mdiContentSave } from "@mdi/js";
|
||||
import { PaperListboxElement } from "@polymer/paper-listbox";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
|
||||
const MODES = ["single", "restart", "queued", "parallel"];
|
||||
const MODES_MAX = ["queued", "parallel"];
|
||||
@@ -72,7 +71,7 @@ export class HaAutomationEditor extends LitElement {
|
||||
|
||||
@internalProperty() private _config?: AutomationConfig;
|
||||
|
||||
@internalProperty() private _dirty?: boolean;
|
||||
@internalProperty() private _dirty = false;
|
||||
|
||||
@internalProperty() private _errors?: string;
|
||||
|
||||
@@ -312,16 +311,10 @@ export class HaAutomationEditor extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
<mwc-fab
|
||||
?is-wide="${this.isWide}"
|
||||
?narrow="${this.narrow}"
|
||||
?dirty="${this._dirty}"
|
||||
.title="${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.save"
|
||||
)}"
|
||||
slot="fab"
|
||||
class=${classMap({ dirty: this._dirty })}
|
||||
.title=${this.hass.localize("ui.panel.config.automation.editor.save")}
|
||||
@click=${this._saveAutomation}
|
||||
class="${classMap({
|
||||
rtl: computeRTL(this.hass),
|
||||
})}"
|
||||
>
|
||||
<ha-svg-icon slot="icon" path=${mdiContentSave}></ha-svg-icon>
|
||||
</mwc-fab>
|
||||
@@ -411,6 +404,10 @@ export class HaAutomationEditor extends LitElement {
|
||||
const mode = ((ev.target as PaperListboxElement)?.selectedItem as any)
|
||||
?.mode;
|
||||
|
||||
if (mode === this._config!.mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._config = { ...this._config!, mode };
|
||||
if (!MODES_MAX.includes(mode)) {
|
||||
delete this._config.max;
|
||||
@@ -538,35 +535,12 @@ export class HaAutomationEditor extends LitElement {
|
||||
margin-right: 8px;
|
||||
}
|
||||
mwc-fab {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
z-index: 3;
|
||||
margin-bottom: -80px;
|
||||
transition: margin-bottom 0.3s;
|
||||
position: relative;
|
||||
bottom: calc(-80px - env(safe-area-inset-bottom));
|
||||
transition: bottom 0.3s;
|
||||
}
|
||||
|
||||
mwc-fab[is-wide] {
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
mwc-fab[narrow] {
|
||||
bottom: 84px;
|
||||
margin-bottom: -140px;
|
||||
}
|
||||
mwc-fab[dirty] {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
mwc-fab.rtl {
|
||||
right: auto;
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
mwc-fab[is-wide].rtl {
|
||||
bottom: 24px;
|
||||
right: auto;
|
||||
left: 24px;
|
||||
mwc-fab.dirty {
|
||||
bottom: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import "../../../components/ha-icon-button";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import {
|
||||
css,
|
||||
CSSResultArray,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
CSSResult,
|
||||
} from "lit-element";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -15,7 +14,6 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { formatDateTime } from "../../../common/datetime/format_date_time";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/entity/ha-entity-toggle";
|
||||
import "@material/mwc-fab";
|
||||
@@ -169,19 +167,16 @@ class HaAutomationPicker extends LitElement {
|
||||
)}
|
||||
hasFab
|
||||
>
|
||||
<mwc-fab
|
||||
slot="fab"
|
||||
title=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.add_automation"
|
||||
)}
|
||||
@click=${this._createNew}
|
||||
>
|
||||
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
|
||||
</mwc-fab>
|
||||
</hass-tabs-subpage-data-table>
|
||||
<mwc-fab
|
||||
slot="fab"
|
||||
?is-wide=${this.isWide}
|
||||
?narrow=${this.narrow}
|
||||
title=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.add_automation"
|
||||
)}
|
||||
?rtl=${computeRTL(this.hass)}
|
||||
@click=${this._createNew}
|
||||
>
|
||||
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
|
||||
</mwc-fab>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -207,37 +202,8 @@ class HaAutomationPicker extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
mwc-fab {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
mwc-fab[is-wide] {
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
mwc-fab[narrow] {
|
||||
bottom: 84px;
|
||||
}
|
||||
mwc-fab[rtl] {
|
||||
right: auto;
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
mwc-fab[rtl][is-wide] {
|
||||
bottom: 24px;
|
||||
right: auto;
|
||||
left: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
static get styles(): CSSResult {
|
||||
return haStyle;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,18 +13,20 @@ import {
|
||||
deviceAutomationsEqual,
|
||||
DeviceTrigger,
|
||||
fetchDeviceTriggerCapabilities,
|
||||
DeviceCapabilities,
|
||||
} from "../../../../../data/device_automation";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import memoizeOne from "memoize-one";
|
||||
|
||||
@customElement("ha-automation-trigger-device")
|
||||
export class HaDeviceTrigger extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public trigger!: DeviceTrigger;
|
||||
@property({ type: Object }) public trigger!: DeviceTrigger;
|
||||
|
||||
@internalProperty() private _deviceId?: string;
|
||||
|
||||
@internalProperty() private _capabilities?;
|
||||
@internalProperty() private _capabilities?: DeviceCapabilities;
|
||||
|
||||
private _origTrigger?: DeviceTrigger;
|
||||
|
||||
@@ -36,16 +38,21 @@ export class HaDeviceTrigger extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private _extraFieldsData = memoizeOne(
|
||||
(trigger: DeviceTrigger, capabilities: DeviceCapabilities) => {
|
||||
const extraFieldsData: { [key: string]: any } = {};
|
||||
capabilities.extra_fields.forEach((item) => {
|
||||
if (trigger[item.name] !== undefined) {
|
||||
extraFieldsData![item.name] = trigger[item.name];
|
||||
}
|
||||
});
|
||||
return extraFieldsData;
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const deviceId = this._deviceId || this.trigger.device_id;
|
||||
|
||||
const extraFieldsData =
|
||||
this._capabilities && this._capabilities.extra_fields
|
||||
? this._capabilities.extra_fields.map((item) => {
|
||||
return { [item.name]: this.trigger[item.name] };
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<ha-device-picker
|
||||
.value=${deviceId}
|
||||
@@ -64,10 +71,10 @@ export class HaDeviceTrigger extends LitElement {
|
||||
"ui.panel.config.automation.editor.triggers.type.device.trigger"
|
||||
)}
|
||||
></ha-device-trigger-picker>
|
||||
${extraFieldsData
|
||||
${this._capabilities?.extra_fields
|
||||
? html`
|
||||
<ha-form
|
||||
.data=${Object.assign({}, ...extraFieldsData)}
|
||||
.data=${this._extraFieldsData(this.trigger, this._capabilities)}
|
||||
.schema=${this._capabilities.extra_fields}
|
||||
.computeLabel=${this._extraFieldsComputeLabelCallback(
|
||||
this.hass.localize
|
||||
@@ -100,7 +107,7 @@ export class HaDeviceTrigger extends LitElement {
|
||||
|
||||
this._capabilities = trigger.domain
|
||||
? await fetchDeviceTriggerCapabilities(this.hass, trigger)
|
||||
: null;
|
||||
: undefined;
|
||||
}
|
||||
|
||||
private _devicePicked(ev) {
|
||||
@@ -120,7 +127,7 @@ export class HaDeviceTrigger extends LitElement {
|
||||
fireEvent(this, "value-changed", { value: trigger });
|
||||
}
|
||||
|
||||
private _extraFieldsChanged(ev) {
|
||||
private _extraFieldsChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
@@ -138,3 +145,9 @@ export class HaDeviceTrigger extends LitElement {
|
||||
) || schema.name;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-trigger-device": HaDeviceTrigger;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ class ConfigCoreForm extends LitElement {
|
||||
<div class="row">
|
||||
<ha-location-editor
|
||||
class="flex"
|
||||
.hass=${this.hass}
|
||||
.location=${this._locationValue}
|
||||
@change=${this._locationChanged}
|
||||
></ha-location-editor>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import "@polymer/app-layout/app-header-layout/app-header-layout";
|
||||
import "../../../layouts/ha-app-layout";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import {
|
||||
@@ -127,7 +127,7 @@ class HaConfigDashboard extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<app-header-layout>
|
||||
<ha-app-layout>
|
||||
<app-header fixed slot="header">
|
||||
<app-toolbar>
|
||||
<ha-menu-button
|
||||
@@ -138,7 +138,7 @@ class HaConfigDashboard extends LitElement {
|
||||
</app-header>
|
||||
|
||||
${content}
|
||||
</app-header-layout>
|
||||
</ha-app-layout>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user