Compare commits

..

1 Commits

Author SHA1 Message Date
Zack Arnett
bd316a36a0 Password manager? 2020-08-10 15:15:21 -05:00
146 changed files with 2723 additions and 12981 deletions

View File

@@ -9,6 +9,7 @@ import {
mdiExclamationThick,
mdiFlask,
mdiHomeAssistant,
mdiInformation,
mdiKey,
mdiNetwork,
mdiPound,
@@ -52,7 +53,6 @@ import { HomeAssistant } from "../../../../src/types";
import "../../components/hassio-card-content";
import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown";
import { hassioStyle } from "../../resources/hassio-style";
import "../../../../src/components/ha-settings-row";
const STAGE_ICON = {
stable: mdiCheckCircle,
@@ -386,94 +386,67 @@ class HassioAddonInfo extends LitElement {
${this.addon.version
? html`
<div class="addon-options">
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
Start on boot
</span>
<span slot="description">
Make the add-on start during a system boot
</span>
<ha-switch
@change=${this._startOnBootToggled}
.checked=${this.addon.boot === "auto"}
haptic
></ha-switch>
</ha-settings-row>
${this.hass.userData?.showAdvanced
? html`
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
Watchdog
</span>
<span slot="description">
This will start the add-on if it crashes
</span>
<ha-switch
@change=${this._watchdogToggled}
.checked=${this.addon.watchdog}
haptic
></ha-switch>
</ha-settings-row>
`
: ""}
${this.addon.auto_update || this.hass.userData?.showAdvanced
? html`
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
Auto update
</span>
<span slot="description">
Auto update the add-on when there is a new version
available
</span>
<ha-switch
@change=${this._autoUpdateToggled}
.checked=${this.addon.auto_update}
haptic
></ha-switch>
</ha-settings-row>
`
: ""}
${this.addon.ingress
? html`
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
Show in sidebar
</span>
<span slot="description">
${this._computeCannotIngressSidebar
? "This option requires Home Assistant 0.92 or later."
: "Add this add-on to your sidebar"}
</span>
<ha-switch
@change=${this._panelToggled}
.checked=${this.addon.ingress_panel}
.disabled=${this._computeCannotIngressSidebar}
haptic
></ha-switch>
</ha-settings-row>
`
: ""}
${this._computeUsesProtectedOptions
? html`
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
Protection mode
</span>
<span slot="description">
Blocks elevated system access from the add-on
</span>
<ha-switch
@change=${this._protectionToggled}
.checked=${this.addon.protected}
haptic
></ha-switch>
</ha-settings-row>
`
: ""}
<div class="state">
<div>Start on boot</div>
<ha-switch
@change=${this._startOnBootToggled}
.checked=${this.addon.boot === "auto"}
haptic
></ha-switch>
</div>
${this.addon.auto_update || this.hass.userData?.showAdvanced
? html`
<div class="state">
<div>Auto update</div>
<ha-switch
@change=${this._autoUpdateToggled}
.checked=${this.addon.auto_update}
haptic
></ha-switch>
</div>
`
: ""}
${this.addon.ingress
? html`
<div class="state">
<div>Show in sidebar</div>
<ha-switch
@change=${this._panelToggled}
.checked=${this.addon.ingress_panel}
.disabled=${this._computeCannotIngressSidebar}
haptic
></ha-switch>
${this._computeCannotIngressSidebar
? html`
<span>
This option requires Home Assistant 0.92 or
later.
</span>
`
: ""}
</div>
`
: ""}
${this._computeUsesProtectedOptions
? html`
<div class="state">
<div>
Protection mode
<span>
<ha-svg-icon path=${mdiInformation}></ha-svg-icon>
<paper-tooltip>
Grant the add-on elevated system access.
</paper-tooltip>
</span>
</div>
<ha-switch
@change=${this._protectionToggled}
.checked=${this.addon.protected}
haptic
></ha-switch>
</div>
`
: ""}
`
: ""}
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
@@ -579,6 +552,137 @@ class HassioAddonInfo extends LitElement {
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
:host {
display: block;
}
ha-card {
display: block;
margin-bottom: 16px;
}
ha-card.warning {
background-color: var(--error-color);
color: white;
}
ha-card.warning .card-header {
color: white;
}
ha-card.warning .card-content {
color: white;
}
ha-card.warning mwc-button {
--mdc-theme-primary: white !important;
}
.warning {
color: var(--error-color);
--mdc-theme-primary: var(--error-color);
}
.light-color {
color: var(--secondary-text-color);
}
.addon-header {
padding-left: 8px;
font-size: 24px;
color: var(--ha-card-header-color, --primary-text-color);
}
.addon-version {
float: right;
font-size: 15px;
vertical-align: middle;
}
.errors {
color: var(--error-color);
margin-bottom: 16px;
}
.description {
margin-bottom: 16px;
}
img.logo {
max-height: 60px;
margin: 16px 0;
display: block;
}
.state {
display: flex;
margin: 33px 0;
}
.state div {
width: 180px;
display: inline-block;
}
.state ha-svg-icon {
width: 16px;
height: 16px;
color: var(--secondary-text-color);
}
ha-switch {
display: flex;
}
ha-svg-icon.running {
color: var(--paper-green-400);
}
ha-svg-icon.stopped {
color: var(--google-red-300);
}
ha-call-api-button {
font-weight: 500;
color: var(--primary-color);
}
.right {
float: right;
}
protection-enable mwc-button {
--mdc-theme-primary: white;
}
.description a {
color: var(--primary-color);
}
.red {
--ha-label-badge-color: var(--label-badge-red, #df4c1e);
}
.blue {
--ha-label-badge-color: var(--label-badge-blue, #039be5);
}
.green {
--ha-label-badge-color: var(--label-badge-green, #0da035);
}
.yellow {
--ha-label-badge-color: var(--label-badge-yellow, #f4b400);
}
.security {
margin-bottom: 16px;
}
.card-actions {
display: flow-root;
}
.security h3 {
margin-bottom: 8px;
font-weight: normal;
}
.security ha-label-badge {
cursor: pointer;
margin-right: 4px;
--ha-label-badge-padding: 8px 0 0 0;
}
.changelog {
display: contents;
}
.changelog-link {
color: var(--primary-color);
text-decoration: underline;
cursor: pointer;
}
ha-markdown {
padding: 16px;
}
`,
];
}
private get _computeHassioApi(): boolean {
return (
this.addon.hassio_api &&
@@ -667,24 +771,6 @@ class HassioAddonInfo extends LitElement {
}
}
private async _watchdogToggled(): Promise<void> {
this._error = undefined;
const data: HassioAddonSetOptionParams = {
watchdog: !this.addon.watchdog,
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
const eventdata = {
success: true,
response: undefined,
path: "option",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon option, ${err.body?.message || err}`;
}
}
private async _autoUpdateToggled(): Promise<void> {
this._error = undefined;
const data: HassioAddonSetOptionParams = {
@@ -801,146 +887,6 @@ class HassioAddonInfo extends LitElement {
this._error = `Failed to uninstall addon, ${err.body?.message || err}`;
}
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
:host {
display: block;
}
ha-card {
display: block;
margin-bottom: 16px;
}
ha-card.warning {
background-color: var(--error-color);
color: white;
}
ha-card.warning .card-header {
color: white;
}
ha-card.warning .card-content {
color: white;
}
ha-card.warning mwc-button {
--mdc-theme-primary: white !important;
}
.warning {
color: var(--error-color);
--mdc-theme-primary: var(--error-color);
}
.light-color {
color: var(--secondary-text-color);
}
.addon-header {
padding-left: 8px;
font-size: 24px;
color: var(--ha-card-header-color, --primary-text-color);
}
.addon-version {
float: right;
font-size: 15px;
vertical-align: middle;
}
.errors {
color: var(--error-color);
margin-bottom: 16px;
}
.description {
margin-bottom: 16px;
}
img.logo {
max-height: 60px;
margin: 16px 0;
display: block;
}
ha-switch {
display: flex;
}
ha-svg-icon.running {
color: var(--paper-green-400);
}
ha-svg-icon.stopped {
color: var(--google-red-300);
}
ha-call-api-button {
font-weight: 500;
color: var(--primary-color);
}
.right {
float: right;
}
protection-enable mwc-button {
--mdc-theme-primary: white;
}
.description a {
color: var(--primary-color);
}
.red {
--ha-label-badge-color: var(--label-badge-red, #df4c1e);
}
.blue {
--ha-label-badge-color: var(--label-badge-blue, #039be5);
}
.green {
--ha-label-badge-color: var(--label-badge-green, #0da035);
}
.yellow {
--ha-label-badge-color: var(--label-badge-yellow, #f4b400);
}
.security {
margin-bottom: 16px;
}
.card-actions {
display: flow-root;
}
.security h3 {
margin-bottom: 8px;
font-weight: normal;
}
.security ha-label-badge {
cursor: pointer;
margin-right: 4px;
--ha-label-badge-padding: 8px 0 0 0;
}
.changelog {
display: contents;
}
.changelog-link {
color: var(--primary-color);
text-decoration: underline;
cursor: pointer;
}
ha-markdown {
padding: 16px;
}
ha-settings-row {
padding: 0;
height: 54px;
width: 100%;
}
ha-settings-row > span[slot="description"] {
white-space: normal;
color: var(--secondary-text-color);
}
ha-settings-row[three-line] {
height: 74px;
}
.addon-options {
max-width: 50%;
}
@media (max-width: 720px) {
.addon-options {
max-width: 100%;
}
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {

View File

@@ -10,7 +10,7 @@ import {
internalProperty,
TemplateResult,
} from "lit-element";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/buttons/ha-call-api-button";
import "../../../src/components/ha-card";
import "../../../src/components/ha-svg-icon";
import { HassioHassOSInfo } from "../../../src/data/hassio/host";
@@ -21,11 +21,6 @@ import {
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { hassioStyle } from "../resources/hassio-style";
import {
showConfirmationDialog,
showAlertDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import { HassioResponse } from "../../../src/data/hassio/common";
@customElement("hassio-update")
export class HassioUpdate extends LitElement {
@@ -131,43 +126,31 @@ export class HassioUpdate extends LitElement {
<a href="${releaseNotesUrl}" target="_blank" rel="noreferrer">
<mwc-button>Release notes</mwc-button>
</a>
<ha-progress-button
.apiPath=${apiPath}
.name=${name}
.version=${lastVersion}
@click=${this._confirmUpdate}
<ha-call-api-button
.hass=${this.hass}
.path=${apiPath}
@hass-api-called=${this._apiCalled}
>
Update
</ha-progress-button>
</ha-call-api-button>
</div>
</ha-card>
`;
}
private async _confirmUpdate(ev): Promise<void> {
const item = ev.target;
item.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: `Update ${item.name}`,
text: `Are you sure you want to upgrade ${item.name} to version ${item.version}?`,
confirmText: "update",
dismissText: "cancel",
});
if (!confirmed) {
item.progress = false;
private _apiCalled(ev): void {
if (ev.detail.success) {
this._error = "";
return;
}
try {
await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath);
} catch (err) {
showAlertDialog(this, {
title: "Update failed",
text:
typeof err === "object" ? err.body?.message || "Unkown error" : err,
});
const response = ev.detail.response;
if (typeof response.body === "object") {
this._error = response.body.message || "Unknown error";
} else {
this._error = response.body;
}
item.progress = false;
}
static get styles(): CSSResult[] {

View File

@@ -1,328 +0,0 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-icon-button";
import "@material/mwc-tab-bar";
import "@material/mwc-tab";
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { mdiClose } from "@mdi/js";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
internalProperty,
TemplateResult,
} from "lit-element";
import { cache } from "lit-html/directives/cache";
import {
updateNetworkInterface,
NetworkInterface,
} from "../../../../src/data/hassio/network";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { HassioNetworkDialogParams } from "./show-dialog-network";
import { haStyleDialog } from "../../../../src/resources/styles";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../src/dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../../src/types";
import type { HaRadio } from "../../../../src/components/ha-radio";
import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-radio";
import "../../../../src/components/ha-related-items";
import "../../../../src/components/ha-svg-icon";
@customElement("dialog-hassio-network")
export class DialogHassioNetwork extends LitElement implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant;
@internalProperty() private _prosessing = false;
@internalProperty() private _params?: HassioNetworkDialogParams;
@internalProperty() private _network!: {
interface: string;
data: NetworkInterface;
}[];
@internalProperty() private _curTabIndex = 0;
@internalProperty() private _device?: {
interface: string;
data: NetworkInterface;
};
@internalProperty() private _dirty = false;
public async showDialog(params: HassioNetworkDialogParams): Promise<void> {
this._params = params;
this._dirty = false;
this._curTabIndex = 0;
this._network = Object.keys(params.network?.interfaces)
.map((device) => ({
interface: device,
data: params.network.interfaces[device],
}))
.sort((a, b) => {
return a.data.primary > b.data.primary ? -1 : 1;
});
this._device = this._network[this._curTabIndex];
this._device.data.nameservers = String(this._device.data.nameservers);
await this.updateComplete;
}
public closeDialog(): void {
this._params = undefined;
this._prosessing = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._params || !this._network) {
return html``;
}
return html`
<ha-dialog open .heading=${true} hideActions @closed=${this.closeDialog}>
<div slot="heading">
<ha-header-bar>
<span slot="title">
Network settings
</span>
<mwc-icon-button slot="actionItems" dialogAction="cancel">
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
</ha-header-bar>
${this._network.length > 1
? html` <mwc-tab-bar
.activeIndex=${this._curTabIndex}
@MDCTabBar:activated=${this._handleTabActivated}
>${this._network.map(
(device) =>
html`<mwc-tab
.id=${device.interface}
.label=${device.interface}
>
</mwc-tab>`
)}
</mwc-tab-bar>`
: ""}
</div>
${cache(this._renderTab())}
</ha-dialog>
`;
}
private _renderTab() {
return html` <div class="form container">
<ha-formfield label="DHCP">
<ha-radio
@change=${this._handleRadioValueChanged}
value="dhcp"
name="method"
?checked=${this._device!.data.method === "dhcp"}
>
</ha-radio>
</ha-formfield>
<ha-formfield label="Static">
<ha-radio
@change=${this._handleRadioValueChanged}
value="static"
name="method"
?checked=${this._device!.data.method === "static"}
>
</ha-radio>
</ha-formfield>
${this._device!.data.method !== "dhcp"
? html` <paper-input
class="flex-auto"
id="ip_address"
label="IP address/Netmask"
.value="${this._device!.data.ip_address}"
@value-changed=${this._handleInputValueChanged}
></paper-input>
<paper-input
class="flex-auto"
id="gateway"
label="Gateway address"
.value="${this._device!.data.gateway}"
@value-changed=${this._handleInputValueChanged}
></paper-input>
<paper-input
class="flex-auto"
id="nameservers"
label="DNS servers"
.value="${this._device!.data.nameservers as string}"
@value-changed=${this._handleInputValueChanged}
></paper-input>
NB!: If you are changing IP or gateway addresses, you might lose
the connection.`
: ""}
</div>
<div class="buttons">
<mwc-button label="close" @click=${this.closeDialog}> </mwc-button>
<mwc-button @click=${this._updateNetwork} ?disabled=${!this._dirty}>
${this._prosessing
? html`<ha-circular-progress active></ha-circular-progress>`
: "Update"}
</mwc-button>
</div>`;
}
private async _updateNetwork() {
this._prosessing = true;
let options: Partial<NetworkInterface> = {
method: this._device!.data.method,
};
if (options.method !== "dhcp") {
options = {
...options,
address: this._device!.data.ip_address,
gateway: this._device!.data.gateway,
dns: String(this._device!.data.nameservers).split(","),
};
}
try {
await updateNetworkInterface(this.hass, this._device!.interface, options);
} catch (err) {
showAlertDialog(this, {
title: "Failed to change network settings",
text:
typeof err === "object" ? err.body.message || "Unkown error" : err,
});
this._prosessing = false;
return;
}
this._params?.loadData();
this.closeDialog();
}
private async _handleTabActivated(ev: CustomEvent): Promise<void> {
if (this._dirty) {
const confirm = await showConfirmationDialog(this, {
text:
"You have unsaved changes, these will get lost if you change tabs, do you want to continue?",
confirmText: "yes",
dismissText: "no",
});
if (!confirm) {
this.requestUpdate("_device");
return;
}
}
this._curTabIndex = ev.detail.index;
this._device = this._network[ev.detail.index];
this._device.data.nameservers = String(this._device.data.nameservers);
}
private _handleRadioValueChanged(ev: CustomEvent): void {
const value = (ev.target as HaRadio).value as "dhcp" | "static";
if (!value || !this._device || this._device!.data.method === value) {
return;
}
this._dirty = true;
this._device!.data.method = value;
this.requestUpdate("_device");
}
private _handleInputValueChanged(ev: CustomEvent): void {
const value: string | null | undefined = (ev.target as PaperInputElement)
.value;
const id = (ev.target as PaperInputElement).id;
if (!value || !this._device || this._device.data[id] === value) {
return;
}
this._dirty = true;
this._device.data[id] = value;
}
static get styles(): CSSResult[] {
return [
haStyleDialog,
css`
ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0;
}
mwc-tab-bar {
border-bottom: 1px solid
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
}
ha-dialog {
--dialog-content-position: static;
--dialog-content-padding: 0;
--dialog-z-index: 6;
}
@media all and (min-width: 451px) and (min-height: 501px) {
.container {
width: 400px;
}
}
.content {
display: block;
padding: 20px 24px;
}
/* overrule the ha-style-dialog max-height on small screens */
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-header-bar {
--mdc-theme-primary: var(--app-header-background-color);
--mdc-theme-on-primary: var(--app-header-text-color, white);
}
}
mwc-button.warning {
--mdc-theme-primary: var(--error-color);
}
:host([rtl]) app-toolbar {
direction: rtl;
text-align: right;
}
.container {
padding: 20px 24px;
}
.form {
margin-bottom: 53px;
}
.buttons {
position: absolute;
bottom: 0;
width: 100%;
box-sizing: border-box;
border-top: 1px solid
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
display: flex;
justify-content: space-between;
padding: 8px;
padding-bottom: max(env(safe-area-inset-bottom), 8px);
background-color: var(--mdc-theme-surface, #fff);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-network": DialogHassioNetwork;
}
}

View File

@@ -1,22 +0,0 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { NetworkInfo } from "../../../../src/data/hassio/network";
import "./dialog-hassio-network";
export interface HassioNetworkDialogParams {
network: NetworkInfo;
loadData: () => Promise<void>;
}
export const showNetworkDialog = (
element: HTMLElement,
dialogParams: HassioNetworkDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-network",
dialogImport: () =>
import(
/* webpackChunkName: "dialog-hassio-network" */ "./dialog-hassio-network"
),
dialogParams,
});
};

View File

@@ -7,9 +7,9 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
internalProperty,
TemplateResult,
} from "lit-element";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
@@ -19,7 +19,6 @@ import {
fetchHassioSnapshotInfo,
HassioSnapshotDetail,
} from "../../../../src/data/hassio/snapshot";
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { PolymerChangedEvent } from "../../../../src/polymer-types";
import { haStyleDialog } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
@@ -267,12 +266,8 @@ class HassioSnapshotDialog extends LitElement {
this._snapshotPassword = ev.detail.value;
}
private async _partialRestoreClicked() {
if (
!(await showConfirmationDialog(this, {
title: "Are you sure you want partially to restore this snapshot?",
}))
) {
private _partialRestoreClicked() {
if (!confirm("Are you sure you want to restore this snapshot?")) {
return;
}
@@ -317,13 +312,8 @@ class HassioSnapshotDialog extends LitElement {
);
}
private async _fullRestoreClicked() {
if (
!(await showConfirmationDialog(this, {
title:
"Are you sure you want to wipe your system and restore this snapshot?",
}))
) {
private _fullRestoreClicked() {
if (!confirm("Are you sure you want to restore this snapshot?")) {
return;
}
@@ -348,12 +338,8 @@ class HassioSnapshotDialog extends LitElement {
);
}
private async _deleteClicked() {
if (
!(await showConfirmationDialog(this, {
title: "Are you sure you want to delete this snapshot?",
}))
) {
private _deleteClicked() {
if (!confirm("Are you sure you want to delete this snapshot?")) {
return;
}

View File

@@ -106,9 +106,7 @@ export class HassioMain extends urlSyncMixin(ProvideHassLitMixin(LitElement)) {
};
}
} else {
themeName =
((this.hass.selectedTheme as unknown) as string) ||
this.hass.themes.default_theme;
themeName = (this.hass.selectedTheme as unknown) as string;
}
applyThemesOnElement(

View File

@@ -1,23 +1,18 @@
import "@material/mwc-button";
import "@material/mwc-list/mwc-list-item";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import { mdiDotsVertical } from "@mdi/js";
import { safeDump } from "js-yaml";
import memoizeOne from "memoize-one";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
internalProperty,
TemplateResult,
} from "lit-element";
import "../../../src/components/buttons/ha-call-api-button";
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
import {
changeHostOptions,
configSyncOS,
fetchHassioHostInfo,
HassioHassOSInfo,
HassioHostInfo as HassioHostInfoType,
@@ -25,26 +20,16 @@ import {
shutdownHost,
updateOS,
} from "../../../src/data/hassio/host";
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
import {
fetchNetworkInfo,
NetworkInfo,
} from "../../../src/data/hassio/network";
import { HassioInfo } from "../../../src/data/hassio/supervisor";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import {
showAlertDialog,
showConfirmationDialog,
showPromptDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
import { showNetworkDialog } from "../dialogs/network/show-dialog-network";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row";
import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-host-info")
class HassioHostInfo extends LitElement {
@@ -56,130 +41,86 @@ class HassioHostInfo extends LitElement {
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
@internalProperty() public _networkInfo?: NetworkInfo;
@internalProperty() private _errors?: string;
public render(): TemplateResult | void {
const primaryIpAddress = this.hostInfo.features.includes("network")
? this._primaryIpAddress(this._networkInfo!)
: "";
return html`
<ha-card header="Host System">
<ha-card>
<div class="card-content">
<h2>Host system</h2>
<table class="info">
<tbody>
<tr>
<td>Hostname</td>
<td>${this.hostInfo.hostname}</td>
</tr>
<tr>
<td>System</td>
<td>${this.hostInfo.operating_system}</td>
</tr>
${!this.hostInfo.features.includes("hassos")
? html`<tr>
<td>Docker version</td>
<td>${this.hassioInfo.docker}</td>
</tr>`
: ""}
${this.hostInfo.deployment
? html`
<tr>
<td>Deployment</td>
<td>${this.hostInfo.deployment}</td>
</tr>
`
: ""}
</tbody>
</table>
<mwc-button raised @click=${this._showHardware} class="info">
Hardware
</mwc-button>
${this.hostInfo.features.includes("hostname")
? html`<ha-settings-row>
<span slot="heading">
Hostname
</span>
<span slot="description">
${this.hostInfo.hostname}
</span>
? html`
<mwc-button
title="Change the hostname"
label="Change"
raised
@click=${this._changeHostnameClicked}
class="info"
>
Change hostname
</mwc-button>
</ha-settings-row>`
`
: ""}
${this.hostInfo.features.includes("network")
? html` <ha-settings-row>
<span slot="heading">
IP address
</span>
<span slot="description">
${primaryIpAddress}
</span>
<mwc-button
title="Change the network"
label="Change"
@click=${this._changeNetworkClicked}
>
</mwc-button>
</ha-settings-row>`
: ""}
<ha-settings-row>
<span slot="heading">
Operating system
</span>
<span slot="description">
${this.hostInfo.operating_system}
</span>
${this.hostInfo.version !== this.hostInfo.version_latest &&
this.hostInfo.features.includes("hassos")
? html`
<mwc-button
title="Update the host OS"
label="Update"
@click=${this._osUpdate}
>
</mwc-button>
`
: ""}
</ha-settings-row>
${!this.hostInfo.features.includes("hassos")
? html`<ha-settings-row>
<span slot="heading">
Docker version
</span>
<span slot="description">
${this.hassioInfo.docker}
</span>
</ha-settings-row>`
: ""}
${this.hostInfo.deployment
? html`<ha-settings-row>
<span slot="heading">
Deployment
</span>
<span slot="description">
${this.hostInfo.deployment}
</span>
</ha-settings-row>`
${this._errors
? html` <div class="errors">Error: ${this._errors}</div> `
: ""}
</div>
<div class="card-actions">
${this.hostInfo.features.includes("reboot")
? html`
<mwc-button
title="Reboot the host OS"
label="Reboot"
class="warning"
@click=${this._hostReboot}
<mwc-button class="warning" @click=${this._rebootHost}
>Reboot</mwc-button
>
</mwc-button>
`
: ""}
${this.hostInfo.features.includes("shutdown")
? html`
<mwc-button
title="Shutdown the host OS"
label="Shutdown"
class="warning"
@click=${this._hostShutdown}
<mwc-button class="warning" @click=${this._shutdownHost}
>Shutdown</mwc-button
>
</mwc-button>
`
: ""}
<ha-button-menu
corner="BOTTOM_START"
@action=${this._handleMenuAction}
>
<mwc-icon-button slot="trigger">
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item title="Show a list of hardware">
Hardware
</mwc-list-item>
${this.hostInfo.features.includes("hassos")
? html`<mwc-list-item
${this.hostInfo.features.includes("hassos")
? html`
<ha-call-api-button
class="warning"
.hass=${this.hass}
path="hassio/os/config/sync"
title="Load HassOS configs or updates from USB"
>Import from USB</ha-call-api-button
>
Import from USB
</mwc-list-item>`
: ""}
</ha-button-menu>
`
: ""}
${this.hostInfo.version !== this.hostInfo.version_latest
? html` <mwc-button @click=${this._updateOS}>Update</mwc-button> `
: ""}
</div>
</ha-card>
`;
@@ -192,96 +133,72 @@ class HassioHostInfo extends LitElement {
css`
ha-card {
height: 100%;
justify-content: space-between;
flex-direction: column;
display: flex;
}
.card-actions {
height: 48px;
border-top: none;
display: flex;
justify-content: space-between;
align-items: center;
}
ha-settings-row {
padding: 0;
height: 54px;
width: 100%;
}
ha-settings-row[three-line] {
height: 74px;
.card-content {
color: var(--primary-text-color);
box-sizing: border-box;
height: calc(100% - 47px);
}
ha-settings-row > span[slot="description"] {
white-space: normal;
color: var(--secondary-text-color);
.info {
width: 100%;
}
.info td:nth-child(2) {
text-align: right;
}
.errors {
color: var(--error-color);
margin-top: 16px;
}
mwc-button.info {
max-width: calc(50% - 12px);
}
table.info {
margin-bottom: 10px;
}
.warning {
--mdc-theme-primary: var(--error-color);
}
ha-button-menu {
color: var(--secondary-text-color);
--mdc-menu-min-width: 200px;
}
@media (min-width: 563px) {
paper-listbox {
max-height: 150px;
overflow: auto;
}
}
paper-item {
cursor: pointer;
min-height: 35px;
}
mwc-list-item ha-svg-icon {
color: var(--secondary-text-color);
}
`,
];
}
protected firstUpdated(): void {
this._loadData();
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
}
private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => {
if (!network_info) {
return "";
private _apiCalled(ev): void {
if (ev.detail.success) {
this._errors = undefined;
return;
}
return Object.keys(network_info?.interfaces)
.map((device) => network_info.interfaces[device])
.find((device) => device.primary)?.ip_address;
});
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
await this._showHardware();
break;
case 1:
await this._importFromUSB();
break;
}
const response = ev.detail.response;
this._errors =
typeof response.body === "object"
? response.body.message || "Unknown error"
: response.body;
}
private async _showHardware(): Promise<void> {
try {
const content = await fetchHassioHardwareInfo(this.hass);
const content = this._objectToMarkdown(
await fetchHassioHardwareInfo(this.hass)
);
showHassioMarkdownDialog(this, {
title: "Hardware",
content: `<pre>${safeDump(content, { indent: 2 })}</pre>`,
content,
});
} catch (err) {
showAlertDialog(this, {
title: "Failed to get Hardware list",
text:
typeof err === "object" ? err.body?.message || "Unkown error" : err,
showHassioMarkdownDialog(this, {
title: "Hardware",
content: "Error getting hardware info",
});
}
}
private async _hostReboot(): Promise<void> {
private async _rebootHost(): Promise<void> {
const confirmed = await showConfirmationDialog(this, {
title: "Reboot",
text: "Are you sure you want to reboot the host?",
@@ -298,13 +215,12 @@ class HassioHostInfo extends LitElement {
} catch (err) {
showAlertDialog(this, {
title: "Failed to reboot",
text:
typeof err === "object" ? err.body?.message || "Unkown error" : err,
text: err.body.message,
});
}
}
private async _hostShutdown(): Promise<void> {
private async _shutdownHost(): Promise<void> {
const confirmed = await showConfirmationDialog(this, {
title: "Shutdown",
text: "Are you sure you want to shutdown the host?",
@@ -321,13 +237,12 @@ class HassioHostInfo extends LitElement {
} catch (err) {
showAlertDialog(this, {
title: "Failed to shutdown",
text:
typeof err === "object" ? err.body?.message || "Unkown error" : err,
text: err.body.message,
});
}
}
private async _osUpdate(): Promise<void> {
private async _updateOS(): Promise<void> {
const confirmed = await showConfirmationDialog(this, {
title: "Update",
text: "Are you sure you want to update the OS?",
@@ -344,17 +259,30 @@ class HassioHostInfo extends LitElement {
} catch (err) {
showAlertDialog(this, {
title: "Failed to update",
text:
typeof err === "object" ? err.body?.message || "Unkown error" : err,
text: err.body.message,
});
}
}
private async _changeNetworkClicked(): Promise<void> {
showNetworkDialog(this, {
network: this._networkInfo!,
loadData: () => this._loadData(),
private _objectToMarkdown(obj, indent = ""): string {
let data = "";
Object.keys(obj).forEach((key) => {
if (typeof obj[key] !== "object") {
data += `${indent}- ${key}: ${obj[key]}\n`;
} else {
data += `${indent}- ${key}:\n`;
if (Array.isArray(obj[key])) {
if (obj[key].length) {
data +=
`${indent} - ` + obj[key].join(`\n${indent} - `) + "\n";
}
} else {
data += this._objectToMarkdown(obj[key], ` ${indent}`);
}
}
});
return data;
}
private async _changeHostnameClicked(): Promise<void> {
@@ -373,29 +301,11 @@ class HassioHostInfo extends LitElement {
} catch (err) {
showAlertDialog(this, {
title: "Setting hostname failed",
text:
typeof err === "object" ? err.body?.message || "Unkown error" : err,
text: err.body.message,
});
}
}
}
private async _importFromUSB(): Promise<void> {
try {
await configSyncOS(this.hass);
this.hostInfo = await fetchHassioHostInfo(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to import from USB",
text:
typeof err === "object" ? err.body?.message || "Unkown error" : err,
});
}
}
private async _loadData(): Promise<void> {
this._networkInfo = await fetchNetworkInfo(this.hass);
}
}
declare global {

View File

@@ -6,28 +6,26 @@ import {
html,
LitElement,
property,
internalProperty,
TemplateResult,
} from "lit-element";
import { HassioHostInfo as HassioHostInfoType } from "../../../src/data/hassio/host";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-call-api-button";
import "../../../src/components/ha-card";
import {
HassioSupervisorInfo as HassioSupervisorInfoType,
reloadSupervisor,
setSupervisorOption,
SupervisorOptions,
updateSupervisor,
} from "../../../src/data/hassio/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row";
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";
@customElement("hassio-supervisor-info")
class HassioSupervisorInfo extends LitElement {
@@ -35,110 +33,90 @@ class HassioSupervisorInfo extends LitElement {
@property() public supervisorInfo!: HassioSupervisorInfoType;
@property() public hostInfo!: HassioHostInfoType;
@internalProperty() private _errors?: string;
public render(): TemplateResult | void {
return html`
<ha-card header="Supervisor">
<ha-card>
<div class="card-content">
<ha-settings-row>
<span slot="heading">
Version
</span>
<span slot="description">
${this.supervisorInfo.version}
</span>
</ha-settings-row>
<ha-settings-row>
<span slot="heading">
Newest version
</span>
<span slot="description">
${this.supervisorInfo.version_latest}
</span>
${this.supervisorInfo.version !== this.supervisorInfo.version_latest
? html`
<mwc-button
title="Update the supervisor"
label="Update"
@click=${this._supervisorUpdate}
>
</mwc-button>
`
: ""}
</ha-settings-row>
<ha-settings-row>
<span slot="heading">
Channel
</span>
<span slot="description">
${this.supervisorInfo.channel}
</span>
${this.supervisorInfo.channel === "beta"
? html`
<mwc-button
@click=${this._toggleBeta}
label="Leave beta channel"
title="Get stable updates for Home Assistant, supervisor and host"
>
</mwc-button>
`
: this.supervisorInfo.channel === "stable"
? html`
<mwc-button
@click=${this._toggleBeta}
label="Join beta channel"
title="Get beta updates for Home Assistant (RCs), supervisor and host"
>
</mwc-button>
`
: ""}
</ha-settings-row>
${this.supervisorInfo?.supported
? html` <ha-settings-row three-line>
<span slot="heading">
Share diagnostics
</span>
<div slot="description" class="diagnostics-description">
Share crash reports and diagnostic information.
<button
class="link"
title="Show more information about this"
@click=${this._diagnosticsInformationDialog}
>
Learn more
</button>
</div>
<ha-switch
haptic
.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"
title="Learn more about how you can make your system compliant"
<h2>Supervisor</h2>
<table class="info">
<tbody>
<tr>
<td>Version</td>
<td>${this.supervisorInfo.version}</td>
</tr>
<tr>
<td>Latest version</td>
<td>${this.supervisorInfo.version_latest}</td>
</tr>
${this.supervisorInfo.channel !== "stable"
? html`
<tr>
<td>Channel</td>
<td>${this.supervisorInfo.channel}</td>
</tr>
`
: ""}
</tbody>
</table>
<div class="options">
<ha-settings-row>
<span slot="heading">
Share Diagnostics
</span>
<span slot="description">
Share crash reports and diagnostic information.
<button
class="link"
@click=${this._diagnosticsInformationDialog}
>
Learn More
</a>
</div>`}
Learn more
</button>
</span>
<ha-switch
.checked=${this.supervisorInfo.diagnostics}
@change=${this._toggleDiagnostics}
></ha-switch>
</ha-settings-row>
</div>
${this._errors
? html` <div class="errors">Error: ${this._errors}</div> `
: ""}
</div>
<div class="card-actions">
<mwc-button
@click=${this._supervisorReload}
title="Reload parts of the supervisor."
label="Reload"
<ha-call-api-button .hass=${this.hass} path="hassio/supervisor/reload"
>Reload</ha-call-api-button
>
</mwc-button>
${this.supervisorInfo.version !== this.supervisorInfo.version_latest
? html`
<ha-call-api-button
.hass=${this.hass}
path="hassio/supervisor/update"
>Update</ha-call-api-button
>
`
: ""}
${this.supervisorInfo.channel === "beta"
? html`
<ha-call-api-button
.hass=${this.hass}
path="hassio/supervisor/options"
.data=${{ channel: "stable" }}
>Leave beta channel</ha-call-api-button
>
`
: ""}
${this.supervisorInfo.channel === "stable"
? html`
<mwc-button
@click=${this._joinBeta}
class="warning"
title="Get beta updates for Home Assistant (RCs), supervisor and host"
>Join beta channel</mwc-button
>
`
: ""}
</div>
</ha-card>
`;
@@ -151,103 +129,92 @@ class HassioSupervisorInfo extends LitElement {
css`
ha-card {
height: 100%;
justify-content: space-between;
flex-direction: column;
display: flex;
width: 100%;
}
.card-actions {
height: 48px;
border-top: none;
display: flex;
justify-content: space-between;
align-items: center;
.card-content {
color: var(--primary-text-color);
box-sizing: border-box;
height: calc(100% - 47px);
}
button.link {
color: var(--primary-color);
.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;
height: 54px;
width: 100%;
}
ha-settings-row[three-line] {
height: 74px;
}
ha-settings-row > span[slot="description"] {
white-space: normal;
color: var(--secondary-text-color);
button.link {
color: var(--primary-color);
}
`,
];
}
private async _toggleBeta(): Promise<void> {
if (this.supervisorInfo.channel === "stable") {
const confirmed = await showConfirmationDialog(this, {
title: "WARNING",
text: html` Beta releases are for testers and early adopters and can
contain unstable code changes.
<br />
<b>
Make sure you have backups of your data before you activate this
feature.
</b>
<br /><br />
This includes beta releases for:
<li>Home Assistant Core</li>
<li>Home Assistant Supervisor</li>
<li>Home Assistant Operating System</li>
<br />
Do you want to join the beta channel?`,
confirmText: "join beta",
dismissText: "no",
});
protected firstUpdated(): void {
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
}
if (!confirmed) {
return;
}
private _apiCalled(ev): void {
if (ev.detail.success) {
this._errors = undefined;
return;
}
const response = ev.detail.response;
this._errors =
typeof response.body === "object"
? response.body.message || "Unknown error"
: response.body;
}
private async _joinBeta() {
const confirmed = await showConfirmationDialog(this, {
title: "WARNING",
text: html` Beta releases are for testers and early adopters and can
contain unstable code changes.
<br />
<b>
Make sure you have backups of your data before you activate this
feature.
</b>
<br /><br />
This includes beta releases for:
<li>Home Assistant Core</li>
<li>Home Assistant Supervisor</li>
<li>Home Assistant Operating System</li>
<br />
Do you want to join the beta channel?`,
confirmText: "join beta",
dismissText: "no",
});
if (!confirmed) {
return;
}
try {
const data: Partial<SupervisorOptions> = {
channel: this.supervisorInfo.channel !== "stable" ? "beta" : "stable",
};
const data: SupervisorOptions = { channel: "beta" };
await setSupervisorOption(this.hass, data);
await reloadSupervisor(this.hass);
const eventdata = {
success: true,
response: undefined,
path: "option",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
showAlertDialog(this, {
title: "Failed to set supervisor option",
text:
typeof err === "object" ? err.body?.message || "Unkown error" : err,
});
this._errors = `Error joining beta channel, ${err.body?.message || err}`;
}
}
private async _supervisorReload(): Promise<void> {
try {
await reloadSupervisor(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to reload the supervisor",
text:
typeof err === "object" ? err.body?.message || "Unkown error" : err,
});
}
}
private async _supervisorUpdate(): Promise<void> {
try {
await updateSupervisor(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to update the supervisor",
text:
typeof err === "object" ? err.body.message || "Unkown error" : err,
});
}
}
private async _diagnosticsInformationDialog(): Promise<void> {
private async _diagnosticsInformationDialog() {
await showAlertDialog(this, {
title: "Help Improve Home Assistant",
text: html`Would you want to automatically share crash reports and
@@ -257,23 +224,27 @@ class HassioSupervisorInfo extends LitElement {
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
The data does not include any private/sensetive information and you can
disable this in settings at any time you want.`,
});
}
private async _toggleDiagnostics(): Promise<void> {
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) {
showAlertDialog(this, {
title: "Failed to set supervisor option",
text:
typeof err === "object" ? err.body.message || "Unkown error" : err,
});
this._errors = `Error changing supervisor setting, ${
err.body?.message || err
}`;
}
}
}

View File

@@ -7,20 +7,18 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
internalProperty,
TemplateResult,
} from "lit-element";
import "../../../src/components/ha-card";
import { fetchHassioLogs } from "../../../src/data/hassio/supervisor";
import { hassioStyle } from "../resources/hassio-style";
import "../../../src/layouts/hass-loading-screen";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import "../../../src/components/ha-card";
import "../../../src/layouts/hass-loading-screen";
import "../components/hassio-ansi-to-html";
import { hassioStyle } from "../resources/hassio-style";
interface LogProvider {
key: string;
@@ -104,7 +102,7 @@ class HassioSupervisorLog extends LitElement {
: html`<hass-loading-screen no-toolbar></hass-loading-screen>`}
</div>
<div class="card-actions">
<mwc-button @click=${this._loadData}>Refresh</mwc-button>
<mwc-button @click=${this._refresh}>Refresh</mwc-button>
</div>
</ha-card>
`;
@@ -116,7 +114,6 @@ class HassioSupervisorLog extends LitElement {
hassioStyle,
css`
ha-card {
margin-top: 8px;
width: 100%;
}
pre {
@@ -130,6 +127,9 @@ class HassioSupervisorLog extends LitElement {
color: var(--error-color);
margin-bottom: 16px;
}
.card-content {
padding-top: 0px;
}
`,
];
}
@@ -142,6 +142,7 @@ class HassioSupervisorLog extends LitElement {
private async _loadData(): Promise<void> {
this._error = undefined;
this._content = undefined;
try {
this._content = await fetchHassioLogs(
@@ -150,10 +151,14 @@ class HassioSupervisorLog extends LitElement {
);
} catch (err) {
this._error = `Failed to get supervisor logs, ${
typeof err === "object" ? err.body?.message || "Unkown error" : err
err.body?.message || err
}`;
}
}
private async _refresh(): Promise<void> {
await this._loadData();
}
}
declare global {

View File

@@ -52,10 +52,10 @@ class HassioSystem extends LitElement {
>
<span slot="header">System</span>
<div class="content">
<h1>Information</h1>
<div class="card-group">
<hassio-supervisor-info
.hass=${this.hass}
.hostInfo=${this.hostInfo}
.supervisorInfo=${this.supervisorInfo}
></hassio-supervisor-info>
<hassio-host-info
@@ -65,6 +65,7 @@ class HassioSystem extends LitElement {
.hassOsInfo=${this.hassOsInfo}
></hassio-host-info>
</div>
<h1>System log</h1>
<hassio-supervisor-log .hass=${this.hass}></hassio-supervisor-log>
</div>
</hass-tabs-subpage>

View File

@@ -23,11 +23,8 @@
"license": "Apache-2.0",
"dependencies": {
"@formatjs/intl-pluralrules": "^1.5.8",
"@fullcalendar/common": "5.1.0",
"@fullcalendar/core": "5.1.0",
"@fullcalendar/daygrid": "5.1.0",
"@fullcalendar/interaction": "5.1.0",
"@fullcalendar/list": "5.1.0",
"@fullcalendar/core": "^5.0.0-beta.2",
"@fullcalendar/daygrid": "^5.0.0-beta.2",
"@material/chips": "=8.0.0-canary.096a7a066.0",
"@material/circular-progress": "=8.0.0-canary.a78ceb112.0",
"@material/mwc-button": "^0.18.0",
@@ -44,8 +41,8 @@
"@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.5.55",
"@mdi/svg": "5.5.55",
"@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",
@@ -88,7 +85,6 @@
"codemirror": "^5.49.0",
"comlink": "^4.3.0",
"cpx": "^1.5.0",
"cropperjs": "^1.5.7",
"deep-clone-simple": "^1.1.1",
"deep-freeze": "^0.0.1",
"es6-object-assign": "^1.1.0",
@@ -149,7 +145,7 @@
"@types/leaflet-draw": "^1.0.1",
"@types/marked": "^1.1.0",
"@types/memoize-one": "4.1.0",
"@types/mocha": "^7.0.2",
"@types/mocha": "^5.2.6",
"@types/resize-observer-browser": "^0.1.3",
"@types/webspeechapi": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^2.28.0",
@@ -160,7 +156,7 @@
"eslint": "^6.8.0",
"eslint-config-airbnb-typescript": "^7.2.1",
"eslint-config-prettier": "^6.10.1",
"eslint-import-resolver-webpack": "^0.12.2",
"eslint-import-resolver-webpack": "^0.12.1",
"eslint-plugin-disable": "^2.0.1",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-lit": "^1.2.0",
@@ -183,7 +179,7 @@
"magic-string": "^0.25.7",
"map-stream": "^0.0.7",
"merge-stream": "^1.0.1",
"mocha": "^7.2.0",
"mocha": "^6.0.2",
"object-hash": "^2.0.3",
"open": "^7.0.4",
"prettier": "^2.0.4",
@@ -200,7 +196,7 @@
"systemjs": "^6.3.2",
"terser-webpack-plugin": "^3.0.6",
"ts-lit-plugin": "^1.2.0",
"ts-mocha": "^7.0.0",
"ts-mocha": "^6.0.0",
"typescript": "^3.8.3",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20200824.0",
version="20200807.1",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",

View File

@@ -1,9 +0,0 @@
import { HomeAssistant } from "../../types";
/** Return if a service is loaded. */
export const isServiceLoaded = (
hass: HomeAssistant,
domain: string,
service: string
): boolean =>
hass && domain in hass.services && service in hass.services[domain];

View File

@@ -21,11 +21,6 @@ export default function relativeTime(
const tense = delta >= 0 ? "past" : "future";
delta = Math.abs(delta);
let roundedDelta = Math.round(delta);
if (roundedDelta === 0) {
return localize("ui.components.relative_time.just_now");
}
let unit = "week";
for (let i = 0; i < tests.length; i++) {

View File

@@ -5,16 +5,12 @@ import { domainIcon } from "./domain_icon";
import { batteryIcon } from "./battery_icon";
const fixedDeviceClassIcons = {
current: "hass:current-ac",
energy: "hass:flash",
humidity: "hass:water-percent",
illuminance: "hass:brightness-5",
temperature: "hass:thermometer",
pressure: "hass:gauge",
power: "hass:flash",
power_factor: "hass:angle-acute",
signal_strength: "hass:wifi",
voltage: "hass:sine-wave",
};
export const sensorIcon = (state: HassEntity) => {

View File

@@ -3,21 +3,19 @@ import {
css,
CSSResult,
customElement,
eventOptions,
html,
internalProperty,
LitElement,
property,
internalProperty,
PropertyValues,
query,
TemplateResult,
eventOptions,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { ifDefined } from "lit-html/directives/if-defined";
import { styleMap } from "lit-html/directives/style-map";
import { scroll } from "lit-virtualizer";
import memoizeOne from "memoize-one";
import { restoreScroll } from "../../common/decorators/restore-scroll";
import { fireEvent } from "../../common/dom/fire_event";
import "../../common/search/search-input";
import { debounce } from "../../common/util/debounce";
@@ -26,6 +24,8 @@ import "../ha-checkbox";
import type { HaCheckbox } from "../ha-checkbox";
import "../ha-icon";
import { filterData, sortData } from "./sort-filter";
import memoizeOne from "memoize-one";
import { restoreScroll } from "../../common/decorators/restore-scroll";
declare global {
// for fire event
@@ -70,7 +70,6 @@ export interface DataTableColumnData extends DataTableSortColumnData {
maxWidth?: string;
grows?: boolean;
forceLTR?: boolean;
hidden?: boolean;
}
export interface DataTableRowData {
@@ -215,15 +214,13 @@ export class HaDataTable extends LitElement {
class="mdc-data-table__table ${classMap({
"auto-height": this.autoHeight,
})}"
role="table"
aria-rowcount=${this._filteredData.length}
style=${styleMap({
height: this.autoHeight
? `${(this._filteredData.length || 1) * 53 + 57}px`
: `calc(100% - ${this._header?.clientHeight}px)`,
})}
>
<div class="mdc-data-table__header-row" role="row">
<div class="mdc-data-table__header-row">
${this.selectable
? html`
<div
@@ -243,10 +240,8 @@ export class HaDataTable extends LitElement {
</div>
`
: ""}
${Object.entries(this.columns).map(([key, column]) => {
if (column.hidden) {
return "";
}
${Object.entries(this.columns).map((columnEntry) => {
const [key, column] = columnEntry;
const sorted = key === this._sortColumn;
const classes = {
"mdc-data-table__header-cell--numeric": Boolean(
@@ -293,8 +288,8 @@ export class HaDataTable extends LitElement {
${!this._filteredData.length
? html`
<div class="mdc-data-table__content">
<div class="mdc-data-table__row" role="row">
<div class="mdc-data-table__cell grows center" role="cell">
<div class="mdc-data-table__row">
<div class="mdc-data-table__cell grows center">
${this.noDataText || "No data"}
</div>
</div>
@@ -309,14 +304,12 @@ export class HaDataTable extends LitElement {
items: !this.hasFab
? this._filteredData
: [...this._filteredData, ...[{ empty: true }]],
renderItem: (row: DataTableRowData, index) => {
renderItem: (row: DataTableRowData) => {
if (row.empty) {
return html` <div class="mdc-data-table__row"></div> `;
}
return html`
<div
aria-rowindex=${index}
role="row"
.rowId="${row[this.id]}"
@click=${this._handleRowClick}
class="mdc-data-table__row ${classMap({
@@ -335,7 +328,6 @@ export class HaDataTable extends LitElement {
? html`
<div
class="mdc-data-table__cell mdc-data-table__cell--checkbox"
role="cell"
>
<ha-checkbox
class="mdc-data-table__row-checkbox"
@@ -349,45 +341,40 @@ export class HaDataTable extends LitElement {
</div>
`
: ""}
${Object.entries(this.columns).map(
([key, column]) => {
if (column.hidden) {
return "";
}
return html`
<div
role="cell"
class="mdc-data-table__cell ${classMap({
"mdc-data-table__cell--numeric": Boolean(
column.type === "numeric"
),
"mdc-data-table__cell--icon": Boolean(
column.type === "icon"
),
"mdc-data-table__cell--icon-button": Boolean(
column.type === "icon-button"
),
grows: Boolean(column.grows),
forceLTR: Boolean(column.forceLTR),
})}"
style=${column.width
? styleMap({
[column.grows
? "minWidth"
: "width"]: column.width,
maxWidth: column.maxWidth
? column.maxWidth
: "",
})
: ""}
>
${column.template
? column.template(row[key], row)
: row[key]}
</div>
`;
}
)}
${Object.entries(this.columns).map((columnEntry) => {
const [key, column] = columnEntry;
return html`
<div
class="mdc-data-table__cell ${classMap({
"mdc-data-table__cell--numeric": Boolean(
column.type === "numeric"
),
"mdc-data-table__cell--icon": Boolean(
column.type === "icon"
),
"mdc-data-table__cell--icon-button": Boolean(
column.type === "icon-button"
),
grows: Boolean(column.grows),
forceLTR: Boolean(column.forceLTR),
})}"
style=${column.width
? styleMap({
[column.grows
? "minWidth"
: "width"]: column.width,
maxWidth: column.maxWidth
? column.maxWidth
: "",
})
: ""}
>
${column.template
? column.template(row[key], row)
: row[key]}
</div>
`;
})}
</div>
`;
},

View File

@@ -1,11 +1,11 @@
// To use comlink under ES5
import { expose } from "comlink";
import "proxy-polyfill";
import { expose } from "comlink";
import type {
DataTableRowData,
DataTableSortColumnData,
SortableColumnContainer,
DataTableRowData,
SortingDirection,
SortableColumnContainer,
} from "./ha-data-table";
const filterData = (
@@ -19,7 +19,7 @@ const filterData = (
const [key, column] = columnEntry;
if (column.filterable) {
if (
String(column.filterKey ? row[key][column.filterKey] : row[key])
(column.filterKey ? row[key][column.filterKey] : row[key])
.toUpperCase()
.includes(filter)
) {

View File

@@ -1,12 +1,12 @@
/* eslint-plugin-disable lit */
import { IronResizableBehavior } from "@polymer/iron-resizable-behavior/iron-resizable-behavior";
import "../ha-icon-button";
import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class";
import { timeOut } from "@polymer/polymer/lib/utils/async";
import { Debouncer } from "@polymer/polymer/lib/utils/debounce";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { formatTime } from "../../common/datetime/format_time";
import "../ha-icon-button";
// eslint-disable-next-line no-unused-vars
/* global Chart moment Color */
@@ -355,7 +355,7 @@ class HaChartBase extends mixinBehaviors(
return value;
}
const date = new Date(values[index].value);
return formatTime(date, this.hass.language);
return formatTime(date);
}
drawChart() {
@@ -420,7 +420,7 @@ class HaChartBase extends mixinBehaviors(
},
};
options = Chart.helpers.merge(options, this.data.options);
options.scales.xAxes[0].ticks.callback = this._formatTickValue.bind(this);
options.scales.xAxes[0].ticks.callback = this._formatTickValue;
if (this.data.type === "timeline") {
this.set("isTimeline", true);
if (this.data.colors !== undefined) {

View File

@@ -1,67 +0,0 @@
import {
css,
CSSResult,
customElement,
LitElement,
property,
svg,
TemplateResult,
} from "lit-element";
import {
getValueInPercentage,
normalize,
roundWithOneDecimal,
} from "../util/calculate";
@customElement("ha-bar")
export class HaBar extends LitElement {
@property({ type: Number }) public min = 0;
@property({ type: Number }) public max = 100;
@property({ type: Number }) public value!: number;
protected render(): TemplateResult {
const valuePrecentage = roundWithOneDecimal(
getValueInPercentage(
normalize(this.value, this.min, this.max),
this.min,
this.max
)
);
return svg`
<svg>
<g>
<rect></rect>
<rect width="${valuePrecentage}%"></rect>
</g>
</svg>
`;
}
static get styles(): CSSResult {
return css`
rect:first-child {
width: 100%;
fill: var(--ha-bar-background-color, var(--secondary-background-color));
}
rect:last-child {
fill: var(--ha-bar-primary-color, var(--primary-color));
rx: var(--ha-bar-border-radius, 4px);
}
svg {
border-radius: var(--ha-bar-border-radius, 4px);
height: 12px;
width: 100%;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-bar": HaBar;
}
}

View File

@@ -1,20 +1,21 @@
import "@material/mwc-icon-button/mwc-icon-button";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
property,
LitElement,
CSSResult,
css,
} from "lit-element";
import "./ha-icon-button";
import { fireEvent } from "../common/dom/fire_event";
import type { ToggleButton } from "../types";
import "./ha-svg-icon";
@customElement("ha-button-toggle-group")
export class HaButtonToggleGroup extends LitElement {
@property({ attribute: false }) public buttons!: ToggleButton[];
@property() public buttons!: ToggleButton[];
@property() public active?: string;
@@ -22,23 +23,21 @@ export class HaButtonToggleGroup extends LitElement {
return html`
<div>
${this.buttons.map(
(button) => html`
<mwc-icon-button
.label=${button.label}
.value=${button.value}
?active=${this.active === button.value}
@click=${this._handleClick}
>
<ha-svg-icon .path=${button.iconPath}></ha-svg-icon>
</mwc-icon-button>
`
(button) => html` <ha-icon-button
.label=${button.label}
.icon=${button.icon}
.value=${button.value}
?active=${this.active === button.value}
@click=${this._handleClick}
>
</ha-icon-button>`
)}
</div>
`;
}
private _handleClick(ev): void {
this.active = ev.currentTarget.value;
this.active = ev.target.value;
fireEvent(this, "value-changed", { value: this.active });
}
@@ -49,13 +48,12 @@ export class HaButtonToggleGroup extends LitElement {
--mdc-icon-button-size: var(--button-toggle-size, 36px);
--mdc-icon-size: var(--button-toggle-icon-size, 20px);
}
mwc-icon-button {
ha-icon-button {
border: 1px solid var(--primary-color);
border-right-width: 0px;
position: relative;
cursor: pointer;
}
mwc-icon-button::before {
ha-icon-button::before {
top: 0;
left: 0;
width: 100%;
@@ -67,26 +65,22 @@ export class HaButtonToggleGroup extends LitElement {
content: "";
transition: opacity 15ms linear, background-color 15ms linear;
}
mwc-icon-button[active]::before {
ha-icon-button[active]::before {
opacity: var(--mdc-icon-button-ripple-opacity, 0.12);
}
mwc-icon-button:first-child {
ha-icon-button:first-child {
border-radius: 4px 0 0 4px;
}
mwc-icon-button:last-child {
ha-icon-button:last-child {
border-radius: 0 4px 4px 0;
border-right-width: 1px;
}
mwc-icon-button:only-child {
border-radius: 4px;
border-right-width: 1px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-button-toggle-group": HaButtonToggleGroup;
"ha-button-toggle-button": HaButtonToggleGroup;
}
}

View File

@@ -12,8 +12,6 @@ import {
import { fireEvent } from "../common/dom/fire_event";
import { computeStateName } from "../common/entity/compute_state_name";
import { supportsFeature } from "../common/entity/supports-feature";
import { nextRender } from "../common/util/render-status";
import { getExternalConfig } from "../external_app/external_config";
import {
CAMERA_SUPPORT_STREAM,
computeMJPEGStreamUrl,
@@ -39,8 +37,6 @@ class HaCameraStream extends LitElement {
private _hlsPolyfillInstance?: Hls;
private _useExoPlayer = false;
public connectedCallback() {
super.connectedCallback();
this._attached = true;
@@ -129,33 +125,22 @@ class HaCameraStream extends LitElement {
return this.shadowRoot!.querySelector("video")!;
}
private async _getUseExoPlayer(): Promise<boolean> {
if (!this.hass!.auth.external) {
return false;
}
const externalConfig = await getExternalConfig(this.hass!.auth.external);
return externalConfig && externalConfig.hasExoPlayer;
}
private async _startHls(): Promise<void> {
// eslint-disable-next-line
let hls;
const Hls = ((await import(
/* webpackChunkName: "hls.js" */ "hls.js"
)) as any).default as HLSModule;
let hlsSupported = Hls.isSupported();
const videoEl = this._videoEl;
this._useExoPlayer = await this._getUseExoPlayer();
if (!this._useExoPlayer) {
hls = ((await import(/* webpackChunkName: "hls.js" */ "hls.js")) as any)
.default as HLSModule;
let hlsSupported = hls.isSupported();
if (!hlsSupported) {
hlsSupported =
videoEl.canPlayType("application/vnd.apple.mpegurl") !== "";
}
if (!hlsSupported) {
hlsSupported =
videoEl.canPlayType("application/vnd.apple.mpegurl") !== "";
}
if (!hlsSupported) {
this._forceMJPEG = this.stateObj!.entity_id;
return;
}
if (!hlsSupported) {
this._forceMJPEG = this.stateObj!.entity_id;
return;
}
try {
@@ -164,10 +149,8 @@ class HaCameraStream extends LitElement {
this.stateObj!.entity_id
);
if (this._useExoPlayer) {
this._renderHLSExoPlayer(url);
} else if (hls.isSupported()) {
this._renderHLSPolyfill(videoEl, hls, url);
if (Hls.isSupported()) {
this._renderHLSPolyfill(videoEl, Hls, url);
} else {
this._renderHLSNative(videoEl, url);
}
@@ -180,29 +163,6 @@ class HaCameraStream extends LitElement {
}
}
private async _renderHLSExoPlayer(url: string) {
window.addEventListener("resize", this._resizeExoPlayer);
this.updateComplete.then(() => nextRender()).then(this._resizeExoPlayer);
this._videoEl.style.visibility = "hidden";
await this.hass!.auth.external!.sendMessage({
type: "exoplayer/play_hls",
payload: new URL(url, window.location.href).toString(),
});
}
private _resizeExoPlayer = () => {
const rect = this._videoEl.getBoundingClientRect();
this.hass!.auth.external!.fireMessage({
type: "exoplayer/resize",
payload: {
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
},
});
};
private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) {
videoEl.src = url;
await new Promise((resolve) =>
@@ -234,15 +194,11 @@ class HaCameraStream extends LitElement {
fireEvent(this, "iron-resize");
}
private _destroyPolyfill() {
private _destroyPolyfill(): void {
if (this._hlsPolyfillInstance) {
this._hlsPolyfillInstance.destroy();
this._hlsPolyfillInstance = undefined;
}
if (this._useExoPlayer) {
window.removeEventListener("resize", this._resizeExoPlayer);
this.hass!.auth.external!.fireMessage({ type: "exoplayer/stop" });
}
}
static get styles(): CSSResult {

View File

@@ -1,8 +1,8 @@
import { Editor } from "codemirror";
import {
customElement,
internalProperty,
property,
internalProperty,
PropertyValues,
UpdatingElement,
} from "lit-element";
@@ -101,6 +101,11 @@ export class HaCodeEditor extends UpdatingElement {
.CodeMirror-scroll {
max-height: var(--code-mirror-max-height, --code-mirror-height);
}
.CodeMirror-gutters {
border-right: 1px solid var(--paper-input-container-color, var(--secondary-text-color));
background-color: var(--paper-dialog-background-color, var(--primary-background-color));
transition: 0.2s ease border-right;
}
:host(.error-state) .CodeMirror-gutters {
border-color: var(--error-state-color, red);
}
@@ -108,7 +113,7 @@ export class HaCodeEditor extends UpdatingElement {
border-right: 2px solid var(--paper-input-container-focus-color, var(--primary-color));
}
.CodeMirror-linenumber {
color: var(--paper-dialog-color, var(--secondary-text-color));
color: var(--paper-dialog-color, var(--primary-text-color));
}
.rtl .CodeMirror-vscrollbar {
right: auto;
@@ -117,100 +122,6 @@ export class HaCodeEditor extends UpdatingElement {
.rtl-gutter {
width: 20px;
}
.CodeMirror-gutters {
border-right: 1px solid var(--paper-input-container-color, var(--secondary-text-color));
background-color: var(--paper-dialog-background-color, var(--primary-background-color));
transition: 0.2s ease border-right;
}
.cm-s-default.CodeMirror {
background-color: var(--code-editor-background-color, var(--card-background-color));
color: var(--primary-text-color);
}
.cm-s-default .CodeMirror-cursor {
border-left: 1px solid var(--secondary-text-color);
}
.cm-s-default div.CodeMirror-selected, .cm-s-default.CodeMirror-focused div.CodeMirror-selected {
background: rgba(var(--rgb-primary-color), 0.2);
}
.cm-s-default .CodeMirror-line::selection,
.cm-s-default .CodeMirror-line>span::selection,
.cm-s-default .CodeMirror-line>span>span::selection {
background: rgba(var(--rgb-primary-color), 0.2);
}
.cm-s-default .cm-keyword {
color: var(--codemirror-keyword, #6262FF);
}
.cm-s-default .cm-operator {
color: var(--codemirror-operator, #cda869);
}
.cm-s-default .cm-variable-2 {
color: var(--codemirror-variable-2, #690);
}
.cm-s-default .cm-builtin {
color: var(--codemirror-builtin, #9B7536);
}
.cm-s-default .cm-atom {
color: var(--codemirror-atom, #F90);
}
.cm-s-default .cm-number {
color: var(--codemirror-number, #ca7841);
}
.cm-s-default .cm-def {
color: var(--codemirror-def, #8DA6CE);
}
.cm-s-default .cm-string {
color: var(--codemirror-string, #07a);
}
.cm-s-default .cm-string-2 {
color: var(--codemirror-string-2, #bd6b18);
}
.cm-s-default .cm-comment {
color: var(--codemirror-comment, #777);
}
.cm-s-default .cm-variable {
color: var(--codemirror-variable, #07a);
}
.cm-s-default .cm-tag {
color: var(--codemirror-tag, #997643);
}
.cm-s-default .cm-meta {
color: var(--codemirror-meta, #000);
}
.cm-s-default .cm-attribute {
color: var(--codemirror-attribute, #d6bb6d);
}
.cm-s-default .cm-property {
color: var(--codemirror-property, #905);
}
.cm-s-default .cm-qualifier {
color: var(--codemirror-qualifier, #690);
}
.cm-s-default .cm-variable-3 {
color: var(--codemirror-variable-3, #07a);
}
.cm-s-default .cm-type {
color: var(--codemirror-type, #07a);
}
</style>`;
this.codemirror = codeMirror(shadowRoot, {

View File

@@ -37,6 +37,24 @@ export class HaFormString extends LitElement implements HaFormElement {
}
}
protected firstUpdated(): void {
if (this.schema.name.includes("password")) {
const stepInput = document.createElement("input");
stepInput.setAttribute("type", "password");
stepInput.setAttribute("name", "password");
stepInput.setAttribute("autocomplete", "on");
stepInput.onkeyup = (ev) => this._externalValueChanged(ev, this);
document.documentElement.appendChild(stepInput);
} else if (this.schema.name.includes("username")) {
const stepInput = document.createElement("input");
stepInput.setAttribute("type", "text");
stepInput.setAttribute("name", "username");
stepInput.setAttribute("autocomplete", "on");
stepInput.onkeyup = (ev) => this._externalValueChanged(ev, this);
document.documentElement.appendChild(stepInput);
}
}
protected render(): TemplateResult {
return this.schema.name.includes("password")
? html`
@@ -81,11 +99,21 @@ export class HaFormString extends LitElement implements HaFormElement {
if (this.data === value) {
return;
}
fireEvent(this, "value-changed", {
value,
});
}
private _externalValueChanged(ev: Event, el): void {
const value = (ev.target as PaperInputElement).value;
if (this.data === value) {
return;
}
el.shadowRoot!.querySelector("paper-input").value = value;
}
private get _stringType(): string {
if (this.schema.format) {
if (["email", "url"].includes(this.schema.format)) {

View File

@@ -11,13 +11,23 @@ import { styleMap } from "lit-html/directives/style-map";
import { afterNextRender } from "../common/util/render-status";
import { ifDefined } from "lit-html/directives/if-defined";
import { getValueInPercentage, normalize } from "../util/calculate";
const getAngle = (value: number, min: number, max: number) => {
const percentage = getValueInPercentage(normalize(value, min, max), min, max);
return (percentage * 180) / 100;
};
const normalize = (value: number, min: number, max: number) => {
if (value > max) return max;
if (value < min) return min;
return value;
};
const getValueInPercentage = (value: number, min: number, max: number) => {
const newMax = max - min;
const newVal = value - min;
return (100 * newVal) / newMax;
};
// Workaround for https://github.com/home-assistant/frontend/issues/6467
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);

View File

@@ -106,7 +106,6 @@ const mdiRenameMapping = {
pot: "pot-steam",
ruby: "language-ruby",
sailing: "sail-boat",
scooter: "human-scooter",
settings: "cog",
"settings-box": "cog-box",
"settings-outline": "cog-outline",

View File

@@ -1,226 +0,0 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiClose, mdiImagePlus } from "@mdi/js";
import "@polymer/iron-input/iron-input";
import "@polymer/paper-input/paper-input-container";
import {
css,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { fireEvent } from "../common/dom/fire_event";
import { createImage, generateImageThumbnailUrl } from "../data/image";
import { HomeAssistant } from "../types";
import "./ha-circular-progress";
import "./ha-svg-icon";
import {
showImageCropperDialog,
CropOptions,
} from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
@customElement("ha-picture-upload")
export class HaPictureUpload extends LitElement {
public hass!: HomeAssistant;
@property() public value: string | null = null;
@property() public label?: string;
@property({ type: Boolean }) public crop = false;
@property({ attribute: false }) public cropOptions?: CropOptions;
@property({ type: Number }) public size = 512;
@internalProperty() private _error = "";
@internalProperty() private _uploading = false;
@internalProperty() private _drag = false;
protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("_drag")) {
(this.shadowRoot!.querySelector(
"paper-input-container"
) as any)._setFocused(this._drag);
}
}
public render(): TemplateResult {
return html`
${this._uploading
? html`<ha-circular-progress
alt="Uploading"
size="large"
active
></ha-circular-progress>`
: html`
${this._error ? html`<div class="error">${this._error}</div>` : ""}
<label for="input">
<paper-input-container
.alwaysFloatLabel=${Boolean(this.value)}
@drop=${this._handleDrop}
@dragenter=${this._handleDragStart}
@dragover=${this._handleDragStart}
@dragleave=${this._handleDragEnd}
@dragend=${this._handleDragEnd}
class=${classMap({
dragged: this._drag,
})}
>
<label for="input" slot="label">
${this.label ||
this.hass.localize("ui.components.picture-upload.label")}
</label>
<iron-input slot="input">
<input
id="input"
type="file"
class="file"
accept="image/png, image/jpeg, image/gif"
@change=${this._handleFilePicked}
/>
${this.value ? html`<img .src=${this.value} />` : ""}
</iron-input>
${this.value
? html`<mwc-icon-button
slot="suffix"
@click=${this._clearPicture}
>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>`
: html`<mwc-icon-button slot="suffix">
<ha-svg-icon .path=${mdiImagePlus}></ha-svg-icon>
</mwc-icon-button>`}
</paper-input-container>
</label>
`}
`;
}
private _handleDrop(ev: DragEvent) {
ev.preventDefault();
ev.stopPropagation();
if (ev.dataTransfer?.files) {
if (this.crop) {
this._cropFile(ev.dataTransfer.files[0]);
} else {
this._uploadFile(ev.dataTransfer.files[0]);
}
}
this._drag = false;
}
private _handleDragStart(ev: DragEvent) {
ev.preventDefault();
ev.stopPropagation();
this._drag = true;
}
private _handleDragEnd(ev: DragEvent) {
ev.preventDefault();
ev.stopPropagation();
this._drag = false;
}
private async _handleFilePicked(ev) {
if (this.crop) {
this._cropFile(ev.target.files[0]);
} else {
this._uploadFile(ev.target.files[0]);
}
}
private async _cropFile(file: File) {
if (!["image/png", "image/jpeg", "image/gif"].includes(file.type)) {
this._error = this.hass.localize(
"ui.components.picture-upload.unsupported_format"
);
return;
}
showImageCropperDialog(this, {
file,
options: this.cropOptions || {
round: false,
aspectRatio: NaN,
},
croppedCallback: (croppedFile) => {
this._uploadFile(croppedFile);
},
});
}
private async _uploadFile(file: File) {
if (!["image/png", "image/jpeg", "image/gif"].includes(file.type)) {
this._error = this.hass.localize(
"ui.components.picture-upload.unsupported_format"
);
return;
}
this._uploading = true;
this._error = "";
try {
const media = await createImage(this.hass, file);
this.value = generateImageThumbnailUrl(media.id, this.size);
fireEvent(this, "change");
} catch (err) {
this._error = err.toString();
} finally {
this._uploading = false;
}
}
private _clearPicture(ev: Event) {
ev.preventDefault();
this.value = null;
this._error = "";
fireEvent(this, "change");
}
static get styles() {
return css`
.error {
color: var(--error-color);
}
paper-input-container {
position: relative;
padding: 8px;
margin: 0 -8px;
}
paper-input-container.dragged:before {
position: var(--layout-fit_-_position);
top: var(--layout-fit_-_top);
right: var(--layout-fit_-_right);
bottom: var(--layout-fit_-_bottom);
left: var(--layout-fit_-_left);
background: currentColor;
content: "";
opacity: var(--dark-divider-opacity);
pointer-events: none;
border-radius: 4px;
}
img {
max-width: 125px;
max-height: 125px;
}
input.file {
display: none;
}
mwc-icon-button {
--mdc-icon-button-size: 24px;
--mdc-icon-size: 20px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-picture-upload": HaPictureUpload;
}
}

View File

@@ -25,7 +25,7 @@ export class HaSettingsRow extends LitElement {
</style>
<paper-item-body
?two-line=${!this.threeLine}
?three-line=${this.threeLine}
?three-line=${!this.threeLine}
>
<slot name="heading"></slot>
<div secondary><slot name="description"></slot></div>

View File

@@ -21,8 +21,8 @@ import {
import { fireEvent } from "../../common/dom/fire_event";
import {
LeafletModuleType,
replaceTileLayer,
setupLeafletMap,
replaceTileLayer,
} from "../../common/dom/setup-leaflet-map";
import { nextRender } from "../../common/util/render-status";
import { defaultRadiusColor } from "../../data/zone";
@@ -40,8 +40,6 @@ class LocationEditor extends LitElement {
@property() public icon?: string;
@property({ type: Boolean }) public darkMode?: boolean;
public fitZoom = 16;
private _iconEl?: DivIcon;
@@ -131,7 +129,7 @@ class LocationEditor extends LitElement {
private async _initMap(): Promise<void> {
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
this._mapEl,
this.darkMode ?? this.hass.themes?.darkMode,
this.hass.themes?.darkMode,
Boolean(this.radius)
);
this._leafletMap.addEventListener(

View File

@@ -1,115 +0,0 @@
import {
css,
CSSResultArray,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { HASSDomEvent } from "../../common/dom/fire_event";
import type {
MediaPickedEvent,
MediaPlayerBrowseAction,
} from "../../data/media-player";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { createCloseHeading } from "../ha-dialog";
import "./ha-media-player-browse";
import { MediaPlayerBrowseDialogParams } from "./show-media-browser-dialog";
@customElement("dialog-media-player-browse")
class DialogMediaPlayerBrowse extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@internalProperty() private _entityId!: string;
@internalProperty() private _mediaContentId?: string;
@internalProperty() private _mediaContentType?: string;
@internalProperty() private _action?: MediaPlayerBrowseAction;
@internalProperty() private _params?: MediaPlayerBrowseDialogParams;
public async showDialog(
params: MediaPlayerBrowseDialogParams
): Promise<void> {
this._params = params;
this._entityId = this._params.entityId;
this._mediaContentId = this._params.mediaContentId;
this._mediaContentType = this._params.mediaContentType;
this._action = this._params.action || "play";
await this.updateComplete;
}
protected render(): TemplateResult {
if (!this._params) {
return html``;
}
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
hideActions
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.components.media-browser.media-player-browser")
)}
@closed=${this._closeDialog}
>
<ha-media-player-browse
.hass=${this.hass}
.entityId=${this._entityId}
.action=${this._action!}
.mediaContentId=${this._mediaContentId}
.mediaContentType=${this._mediaContentType}
@media-picked=${this._mediaPicked}
></ha-media-player-browse>
</ha-dialog>
`;
}
private _closeDialog() {
this._params = undefined;
}
private _mediaPicked(ev: HASSDomEvent<MediaPickedEvent>): void {
this._params!.mediaPickedCallback(ev.detail);
if (this._action !== "play") {
this._closeDialog();
}
}
static get styles(): CSSResultArray {
return [
haStyleDialog,
css`
ha-dialog {
--dialog-z-index: 8;
--dialog-content-padding: 0;
}
@media (min-width: 800px) {
ha-dialog {
--mdc-dialog-max-width: 800px;
}
ha-media-player-browse {
width: 700px;
padding: 20px 24px;
}
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-media-player-browse": DialogMediaPlayerBrowse;
}
}

View File

@@ -1,591 +0,0 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-fab/mwc-fab";
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiArrowLeft, mdiFolder, mdiPlay, mdiPlus } from "@mdi/js";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
css,
CSSResultArray,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { debounce } from "../../common/util/debounce";
import { browseMediaPlayer, MediaPickedEvent } from "../../data/media-player";
import type { MediaPlayerItem } from "../../data/media-player";
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../entity/ha-entity-picker";
import "../ha-button-menu";
import "../ha-card";
import "../ha-circular-progress";
import "../ha-paper-dropdown-menu";
import "../ha-svg-icon";
declare global {
interface HASSDomEvents {
"media-picked": MediaPickedEvent;
}
}
@customElement("ha-media-player-browse")
export class HaMediaPlayerBrowse extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public entityId!: string;
@property() public mediaContentId?: string;
@property() public mediaContentType?: string;
@property() public action: "pick" | "play" = "play";
@property({ type: Boolean, attribute: "narrow", reflect: true })
private _narrow = false;
@internalProperty() private _loading = false;
@internalProperty() private _mediaPlayerItems: MediaPlayerItem[] = [];
private _resizeObserver?: ResizeObserver;
public connectedCallback(): void {
super.connectedCallback();
this.updateComplete.then(() => this._attachObserver());
}
public disconnectedCallback(): void {
if (this._resizeObserver) {
this._resizeObserver.disconnect();
}
}
protected render(): TemplateResult {
if (!this._mediaPlayerItems.length) {
return html``;
}
if (this._loading) {
return html`<ha-circular-progress active></ha-circular-progress>`;
}
const mostRecentItem = this._mediaPlayerItems[
this._mediaPlayerItems.length - 1
];
const previousItem =
this._mediaPlayerItems.length > 1
? this._mediaPlayerItems[this._mediaPlayerItems.length - 2]
: undefined;
const hasExpandableChildren:
| MediaPlayerItem
| undefined = this._hasExpandableChildren(mostRecentItem.children);
return html`
<div class="header">
<div class="header-content">
${mostRecentItem.thumbnail
? html`
<div
class="img"
style="background-image: url(${mostRecentItem.thumbnail})"
>
${this._narrow && mostRecentItem?.can_play
? html`
<mwc-fab
mini
.item=${mostRecentItem}
@click=${this._actionClicked}
>
<ha-svg-icon
slot="icon"
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play" ? mdiPlay : mdiPlus}
></ha-svg-icon>
${this.hass.localize(
`ui.components.media-browser.${this.action}`
)}
</mwc-fab>
`
: ""}
</div>
`
: html``}
<div class="header-info">
<div class="breadcrumb-overflow">
<div class="breadcrumb">
${previousItem
? html`
<div
class="previous-title"
.previous=${true}
.item=${previousItem}
@click=${this._navigate}
>
<ha-svg-icon .path=${mdiArrowLeft}></ha-svg-icon>
${previousItem.title}
</div>
`
: ""}
<h1 class="title">${mostRecentItem.title}</h1>
<h2 class="subtitle">
${this.hass.localize(
`ui.components.media-browser.content-type.${mostRecentItem.media_content_type}`
)}
</h2>
</div>
</div>
${mostRecentItem?.can_play &&
(!this._narrow || (this._narrow && !mostRecentItem.thumbnail))
? html`
<div class="actions">
<mwc-button
raised
.item=${mostRecentItem}
@click=${this._actionClicked}
>
<ha-svg-icon
slot="icon"
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play" ? mdiPlay : mdiPlus}
></ha-svg-icon>
${this.hass.localize(
`ui.components.media-browser.${this.action}`
)}
</mwc-button>
</div>
`
: ""}
</div>
</div>
</div>
<div class="divider"></div>
${mostRecentItem.children?.length
? hasExpandableChildren
? html`
<div class="children">
${mostRecentItem.children?.length
? html`
${mostRecentItem.children.map(
(child) => html`
<div
class="child"
.item=${child}
@click=${this._navigate}
>
<div class="ha-card-parent">
<ha-card
outlined
style="background-image: url(${child.thumbnail})"
>
${child.can_expand && !child.thumbnail
? html`
<ha-svg-icon
class="folder"
.path=${mdiFolder}
></ha-svg-icon>
`
: ""}
</ha-card>
${child.can_play
? html`
<mwc-icon-button
class="play"
.item=${child}
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
@click=${this._actionClicked}
>
<ha-svg-icon
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
></ha-svg-icon>
</mwc-icon-button>
`
: ""}
</div>
<div class="title">${child.title}</div>
<div class="type">
${this.hass.localize(
`ui.components.media-browser.content-type.${child.media_content_type}`
)}
</div>
</div>
`
)}
`
: ""}
</div>
`
: html`
<mwc-list>
${mostRecentItem.children.map(
(child) => html`<mwc-list-item
@click=${this._actionClicked}
.item=${child}
graphic="icon"
>
<span>${child.title}</span>
<ha-svg-icon
slot="graphic"
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play" ? mdiPlay : mdiPlus}
></ha-svg-icon
></mwc-list-item>
<li divider role="separator"></li>`
)}
</mwc-list>
`
: this.hass.localize("ui.components.media-browser.no_items")}
`;
}
protected firstUpdated(): void {
this._measureCard();
this._attachObserver();
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (
!changedProps.has("entityId") &&
!changedProps.has("mediaContentId") &&
!changedProps.has("mediaContentType") &&
!changedProps.has("action")
) {
return;
}
this._fetchData(this.mediaContentId, this.mediaContentType).then(
(itemData) => {
this._mediaPlayerItems = [itemData];
}
);
}
private _actionClicked(ev: MouseEvent): void {
ev.stopPropagation();
const item = (ev.currentTarget as any).item;
this._runAction(item);
}
private _runAction(item: MediaPlayerItem): void {
fireEvent(this, "media-picked", {
media_content_id: item.media_content_id,
media_content_type: item.media_content_type,
});
}
private async _navigate(ev: MouseEvent): Promise<void> {
const target = ev.currentTarget as any;
let item: MediaPlayerItem | undefined;
if (target.previous) {
this._mediaPlayerItems!.pop();
item = this._mediaPlayerItems!.pop();
}
item = target.item;
if (!item) {
return;
}
const itemData = await this._fetchData(
item.media_content_id,
item.media_content_type
);
this._mediaPlayerItems = [...this._mediaPlayerItems, itemData];
}
private async _fetchData(
mediaContentId?: string,
mediaContentType?: string
): Promise<MediaPlayerItem> {
const itemData = await browseMediaPlayer(
this.hass,
this.entityId,
!mediaContentId ? undefined : mediaContentId,
mediaContentType
);
return itemData;
}
private _measureCard(): void {
this._narrow = this.offsetWidth < 500;
}
private async _attachObserver(): Promise<void> {
if (!this._resizeObserver) {
await installResizeObserver();
this._resizeObserver = new ResizeObserver(
debounce(() => this._measureCard(), 250, false)
);
}
this._resizeObserver.observe(this);
}
private _hasExpandableChildren = memoizeOne((children) =>
children.find((item: MediaPlayerItem) => item.can_expand)
);
static get styles(): CSSResultArray {
return [
haStyle,
css`
:host {
display: block;
}
.header {
display: flex;
justify-content: space-between;
}
.breadcrumb-overflow {
display: flex;
justify-content: space-between;
}
.header-content {
display: flex;
flex-wrap: wrap;
flex-grow: 1;
align-items: flex-start;
}
.header-content .img {
height: 200px;
width: 200px;
margin-right: 16px;
background-size: cover;
}
.header-info {
display: flex;
flex-direction: column;
justify-content: space-between;
align-self: stretch;
min-width: 0;
flex: 1;
}
.header-info .actions {
padding-top: 24px;
--mdc-theme-primary: var(--primary-color);
}
.breadcrumb {
display: flex;
flex-direction: column;
overflow: hidden;
flex-grow: 1;
}
.breadcrumb .title {
font-size: 48px;
line-height: 1.2;
font-weight: bold;
margin: 0;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.breadcrumb .previous-title {
font-size: 14px;
padding-bottom: 8px;
color: var(--secondary-text-color);
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
--mdc-icon-size: 14px;
}
.breadcrumb .subtitle {
font-size: 16px;
overflow: hidden;
text-overflow: ellipsis;
}
.divider {
padding: 10px 0;
}
.divider::before {
height: 1px;
display: block;
background-color: var(--divider-color);
content: " ";
}
/* ============= CHILDREN ============= */
mwc-list {
--mdc-list-vertical-padding: 0;
--mdc-theme-text-icon-on-background: var(--secondary-text-color);
border: 1px solid var(--divider-color);
border-radius: 4px;
}
mwc-list li:last-child {
display: none;
}
mwc-list li[divider] {
border-bottom-color: var(--divider-color);
}
.children {
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(var(--media-browse-item-size, 175px), 0.33fr)
);
grid-gap: 16px;
margin: 8px 0px;
}
.child {
display: flex;
flex-direction: column;
cursor: pointer;
}
.ha-card-parent {
position: relative;
width: 100%;
}
ha-card {
width: 100%;
padding-bottom: 100%;
position: relative;
box-sizing: border-box;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
}
.child .folder,
.child .play {
position: absolute;
}
.child .folder {
color: var(--secondary-text-color);
top: calc(50% - (var(--mdc-icon-size) / 2));
left: calc(50% - (var(--mdc-icon-size) / 2));
--mdc-icon-size: calc(var(--media-browse-item-size, 175px) * 0.4);
}
.child .play {
bottom: 4px;
right: 4px;
transition: all 0.5s;
background-color: rgba(255, 255, 255, 0.5);
border-radius: 50%;
}
.child .play:hover {
color: var(--primary-color);
}
ha-card:hover {
opacity: 0.5;
}
.child .title {
font-size: 16px;
padding-top: 8px;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.child .type {
font-size: 12px;
color: var(--secondary-text-color);
}
/* ============= Narrow ============= */
:host([narrow]) {
padding: 0;
}
:host([narrow]) mwc-list {
border: 0;
}
:host([narrow]) .breadcrumb .title {
font-size: 38px;
}
:host([narrow]) .breadcrumb-overflow {
align-items: flex-end;
}
:host([narrow]) .header-content {
flex-direction: column;
flex-wrap: nowrap;
}
:host([narrow]) .header-content .img {
height: auto;
width: 100%;
margin-right: 0;
padding-bottom: 100%;
margin-bottom: 8px;
position: relative;
}
:host([narrow]) .header-content .img mwc-fab {
position: absolute;
--mdc-theme-secondary: var(--primary-color);
bottom: -20px;
right: 20px;
}
:host([narrow]) .header-info,
:host([narrow]) .media-source,
:host([narrow]) .children {
padding: 0 24px;
}
:host([narrow]) .children {
grid-template-columns: 1fr 1fr !important;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-media-player-browse": HaMediaPlayerBrowse;
}
}

View File

@@ -1,27 +0,0 @@
import { fireEvent } from "../../common/dom/fire_event";
import {
MediaPickedEvent,
MediaPlayerBrowseAction,
} from "../../data/media-player";
export interface MediaPlayerBrowseDialogParams {
action: MediaPlayerBrowseAction;
entityId: string;
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => void;
mediaContentId?: string;
mediaContentType?: string;
}
export const showMediaBrowserDialog = (
element: HTMLElement,
dialogParams: MediaPlayerBrowseDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-media-player-browse",
dialogImport: () =>
import(
/* webpackChunkName: "dialog-media-player-browse" */ "./dialog-media-player-browse"
),
dialogParams,
});
};

View File

@@ -19,7 +19,6 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
</style>
<ha-chart-base
id="chart"
hass="[[hass]]"
data="[[chartData]]"
identifier="[[identifier]]"
rendered="{{rendered}}"
@@ -29,9 +28,6 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
static get properties() {
return {
hass: {
type: Object,
},
chartData: Object,
data: Object,
names: Object,

View File

@@ -25,7 +25,6 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
}
</style>
<ha-chart-base
hass="[[hass]]"
data="[[chartData]]"
rendered="{{rendered}}"
rtl="{{rtl}}"

View File

@@ -44,14 +44,3 @@ export const createAuthForUser = async (
username,
password,
});
export const adminChangePassword = async (
hass: HomeAssistant,
userId: string,
password: string
) =>
hass.callWS<void>({
type: "config/auth_provider/homeassistant/admin_change_password",
user_id: userId,
password,
});

View File

@@ -3,7 +3,7 @@ import {
HassEntityBase,
} from "home-assistant-js-websocket";
import { navigate } from "../common/navigate";
import { HomeAssistant, Context } from "../types";
import { HomeAssistant } from "../types";
import { DeviceCondition, DeviceTrigger } from "./device_automation";
import { Action } from "./script";
@@ -90,12 +90,6 @@ export interface ZoneTrigger {
event: "enter" | "leave";
}
export interface TagTrigger {
platform: "tag";
tag_id: string;
device_id?: string;
}
export interface TimeTrigger {
platform: "time";
at: string;
@@ -122,7 +116,6 @@ export type Trigger =
| TimePatternTrigger
| WebhookTrigger
| ZoneTrigger
| TagTrigger
| TimeTrigger
| TemplateTrigger
| EventTrigger
@@ -206,31 +199,3 @@ export const getAutomationEditorInitData = () => {
inititialAutomationEditorData = undefined;
return data;
};
export const subscribeTrigger = (
hass: HomeAssistant,
onChange: (result: {
variables: {
trigger: {};
};
context: Context;
}) => void,
trigger: Trigger | Trigger[],
variables?: {}
) =>
hass.connection.subscribeMessage(onChange, {
type: "subscribe_trigger",
trigger,
variables,
});
export const testCondition = (
hass: HomeAssistant,
condition: Condition | Condition[],
variables?: {}
) =>
hass.callWS<{ result: boolean }>({
type: "test_condition",
condition,
variables,
});

View File

@@ -8,7 +8,6 @@ export interface ConfigEntry {
state: string;
connection_class: string;
supports_options: boolean;
supports_unload: boolean;
}
export interface ConfigEntryMutableParams {
@@ -38,11 +37,6 @@ export const deleteConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
require_restart: boolean;
}>("DELETE", `config/config_entries/entry/${configEntryId}`);
export const reloadConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
hass.callApi<{
require_restart: boolean;
}>("POST", `config/config_entries/entry/${configEntryId}/reload`);
export const getConfigEntrySystemOptions = (
hass: HomeAssistant,
configEntryId: string

View File

@@ -72,7 +72,6 @@ export interface HassioAddonDetails extends HassioAddonInfo {
ingress_panel: boolean;
ingress_entry: null | string;
ingress_url: null | string;
watchdog: null | boolean;
}
export interface HassioAddonsInfo {
@@ -100,7 +99,6 @@ export interface HassioAddonSetOptionParams {
auto_update?: boolean;
ingress_panel?: boolean;
network?: object | null;
watchdog?: boolean;
}
export const reloadHassioAddons = async (hass: HomeAssistant) => {

View File

@@ -40,10 +40,6 @@ export const updateOS = async (hass: HomeAssistant) => {
return hass.callApi<HassioResponse<void>>("POST", "hassio/os/update");
};
export const configSyncOS = async (hass: HomeAssistant) => {
return hass.callApi<HassioResponse<void>>("POST", "hassio/os/config/sync");
};
export const changeHostOptions = async (hass: HomeAssistant, options: any) => {
return hass.callApi<HassioResponse<void>>(
"POST",

View File

@@ -1,43 +0,0 @@
import { HomeAssistant } from "../../types";
import { hassioApiResultExtractor, HassioResponse } from "./common";
export interface NetworkInterface {
gateway: string;
id: string;
ip_address: string;
address?: string;
method: "static" | "dhcp";
nameservers: string[] | string;
dns?: string[];
primary: boolean;
type: string;
}
export interface NetworkInterfaces {
[key: string]: NetworkInterface;
}
export interface NetworkInfo {
interfaces: NetworkInterfaces;
}
export const fetchNetworkInfo = async (hass: HomeAssistant) => {
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<NetworkInfo>>(
"GET",
"hassio/network/info"
)
);
};
export const updateNetworkInterface = async (
hass: HomeAssistant,
network_interface: string,
options: Partial<NetworkInterface>
) => {
await hass.callApi<HassioResponse<NetworkInfo>>(
"POST",
`hassio/network/interface/${network_interface}/update`,
options
);
};

View File

@@ -35,14 +35,6 @@ export interface SupervisorOptions {
addons_repositories?: string[];
}
export const reloadSupervisor = async (hass: HomeAssistant) => {
await hass.callApi<HassioResponse<void>>("POST", `hassio/supervisor/reload`);
};
export const updateSupervisor = async (hass: HomeAssistant) => {
await hass.callApi<HassioResponse<void>>("POST", `hassio/supervisor/update`);
};
export const fetchHassioHomeAssistantInfo = async (hass: HomeAssistant) => {
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioHomeAssistantInfo>>(
@@ -79,11 +71,7 @@ export const createHassioSession = async (hass: HomeAssistant) => {
"POST",
"hassio/ingress/session"
);
document.cookie = `ingress_session=${
response.data.session
};path=/api/hassio_ingress/;SameSite=Strict${
location.protocol === "https:" ? ";Secure" : ""
}`;
document.cookie = `ingress_session=${response.data.session};path=/api/hassio_ingress/`;
};
export const setSupervisorOption = async (

View File

@@ -1,54 +0,0 @@
import { HomeAssistant } from "../types";
interface Image {
filesize: number;
name: string;
uploaded_at: string; // isoformat date
content_type: string;
id: string;
}
export interface ImageMutableParams {
name: string;
}
export const generateImageThumbnailUrl = (mediaId: string, size: number) =>
`/api/image/serve/${mediaId}/${size}x${size}`;
export const fetchImages = (hass: HomeAssistant) =>
hass.callWS<Image[]>({ type: "image/list" });
export const createImage = async (
hass: HomeAssistant,
file: File
): Promise<Image> => {
const fd = new FormData();
fd.append("file", file);
const resp = await hass.fetchWithAuth("/api/image/upload", {
method: "POST",
body: fd,
});
if (resp.status === 413) {
throw new Error("Uploaded image is too large");
} else if (resp.status !== 200) {
throw new Error("Unknown error");
}
return await resp.json();
};
export const updateImage = (
hass: HomeAssistant,
id: string,
updates: Partial<ImageMutableParams>
) =>
hass.callWS<Image>({
type: "image/update",
media_id: id,
...updates,
});
export const deleteImage = (hass: HomeAssistant, id: string) =>
hass.callWS({
type: "image/delete",
media_id: id,
});

View File

@@ -7,12 +7,6 @@ export interface LogbookEntry {
entity_id?: string;
domain: string;
context_user_id?: string;
context_event_type?: string;
context_domain?: string;
context_service?: string;
context_entity_id?: string;
context_entity_id_name?: string;
context_name?: string;
}
const DATA_CACHE: {

View File

@@ -1,5 +1,4 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../types";
import { HassEntity } from "home-assistant-js-websocket";
export const SUPPORT_PAUSE = 1;
export const SUPPORT_SEEK = 2;
@@ -15,49 +14,13 @@ export const SUPPORT_SELECT_SOURCE = 2048;
export const SUPPORT_STOP = 4096;
export const SUPPORTS_PLAY = 16384;
export const SUPPORT_SELECT_SOUND_MODE = 65536;
export const SUPPORT_BROWSE_MEDIA = 131072;
export const CONTRAST_RATIO = 4.5;
export type MediaPlayerBrowseAction = "pick" | "play";
export interface MediaPickedEvent {
media_content_id: string;
media_content_type: string;
}
export interface MediaPlayerThumbnail {
content_type: string;
content: string;
}
export interface ControlButton {
icon: string;
action: string;
}
export interface MediaPlayerItem {
title: string;
media_content_type: string;
media_content_id: string;
can_play: boolean;
can_expand: boolean;
thumbnail?: string;
children?: MediaPlayerItem[];
}
export const browseMediaPlayer = (
hass: HomeAssistant,
entityId: string,
mediaContentId?: string,
mediaContentType?: string
): Promise<MediaPlayerItem> =>
hass.callWS<MediaPlayerItem>({
type: "media_player/browse_media",
entity_id: entityId,
media_content_id: mediaContentId,
media_content_type: mediaContentType,
});
export const getCurrentProgress = (stateObj: HassEntity): number => {
let progress = stateObj.attributes.media_position;

View File

@@ -1,10 +1,4 @@
import { HomeAssistant } from "../types";
import { DeviceRegistryEntry } from "./device_registry";
export interface OZWNodeIdentifiers {
ozw_instance: number;
node_id: number;
}
export interface OZWDevice {
node_id: number;
@@ -13,169 +7,15 @@ export interface OZWDevice {
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 interface OZWInstance {
ozw_instance: number;
OZWDaemon_Version: string;
OpenZWave_Version: string;
QTOpenZWave_Version: string;
Status: string;
getControllerPath: string;
homeID: string;
}
export interface OZWNetworkStatistics {
ozw_instance: number;
node_count: number;
readCnt: number;
writeCnt: number;
ACKCnt: number;
CANCnt: number;
NAKCnt: number;
dropped: number;
retries: number;
}
export const nodeQueryStages = [
"ProtocolInfo",
"Probe",
"WakeUp",
"ManufacturerSpecific1",
"NodeInfo",
"NodePlusInfo",
"ManufacturerSpecific2",
"Versions",
"Instances",
"Static",
"CacheLoad",
"Associations",
"Neighbors",
"Session",
"Dynamic",
"Configuration",
"Complete",
];
export const networkOnlineStatuses = [
"driverAllNodesQueried",
"driverAllNodesQueriedSomeDead",
"driverAwakeNodesQueried",
];
export const networkStartingStatuses = [
"starting",
"started",
"Ready",
"driverReady",
];
export const networkOfflineStatuses = [
"Offline",
"stopped",
"driverFailed",
"driverReset",
"driverRemoved",
"driverAllNodesOnFire",
];
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 fetchOZWInstances = (
hass: HomeAssistant
): Promise<OZWInstance[]> =>
hass.callWS({
type: "ozw/get_instances",
});
export const fetchOZWNetworkStatus = (
hass: HomeAssistant,
ozw_instance: number
): Promise<OZWInstance> =>
hass.callWS({
type: "ozw/network_status",
ozw_instance: ozw_instance,
});
export const fetchOZWNetworkStatistics = (
hass: HomeAssistant,
ozw_instance: number
): Promise<OZWNetworkStatistics> =>
hass.callWS({
type: "ozw/network_statistics",
ozw_instance: ozw_instance,
});
export const fetchOZWNodeStatus = (
hass: HomeAssistant,
ozw_instance: number,
node_id: number
ozw_instance: string,
node_id: string
): 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,
});

View File

@@ -17,9 +17,7 @@ export const setDefaultPanel = (
};
export const getDefaultPanel = (hass: HomeAssistant): PanelInfo =>
hass.panels[hass.defaultPanel]
? hass.panels[hass.defaultPanel]
: hass.panels[DEFAULT_PANEL];
hass.panels[hass.defaultPanel];
export const getPanelTitle = (hass: HomeAssistant): string | undefined => {
if (!hass.panels) {

View File

@@ -5,14 +5,12 @@ export interface Person {
name: string;
user_id?: string;
device_trackers?: string[];
picture?: string;
}
export interface PersonMutableParams {
name: string;
user_id: string | null;
device_trackers: string[];
picture: string | null;
}
export const fetchPersons = (hass: HomeAssistant) =>

View File

@@ -1,57 +0,0 @@
import { HomeAssistant } from "../types";
import { HassEventBase } from "home-assistant-js-websocket";
export const EVENT_TAG_SCANNED = "tag_scanned";
export interface TagScannedEvent extends HassEventBase {
event_type: "tag_scanned";
data: {
tag_id: string;
device_id?: string;
};
}
export interface Tag {
id: string;
name?: string;
description?: string;
last_scanned?: string;
}
export interface UpdateTagParams {
name?: Tag["name"];
description?: Tag["description"];
}
export const fetchTags = async (hass: HomeAssistant) =>
hass.callWS<Tag[]>({
type: "tag/list",
});
export const createTag = async (
hass: HomeAssistant,
params: UpdateTagParams,
tagId?: string
) =>
hass.callWS<Tag>({
type: "tag/create",
tag_id: tagId,
...params,
});
export const updateTag = async (
hass: HomeAssistant,
tagId: string,
params: UpdateTagParams
) =>
hass.callWS<Tag>({
...params,
type: "tag/update",
tag_id: tagId,
});
export const deleteTag = async (hass: HomeAssistant, tagId: string) =>
hass.callWS<void>({
type: "tag/delete",
tag_id: tagId,
});

View File

@@ -2,7 +2,6 @@ import { SVGTemplateResult, svg, html, TemplateResult, css } from "lit-element";
import { styleMap } from "lit-html/directives/style-map";
import type { HomeAssistant, WeatherEntity } from "../types";
import { roundWithOneDecimal } from "../util/calculate";
export const weatherSVGs = new Set<string>([
"clear-night",
@@ -136,7 +135,7 @@ export const getSecondaryWeatherAttribute = (
return `
${hass!.localize(
`ui.card.weather.attributes.${attribute}`
)} ${roundWithOneDecimal(value)} ${getWeatherUnit(hass!, attribute)}
)} ${value} ${getWeatherUnit(hass!, attribute)}
`;
};

View File

@@ -1,136 +0,0 @@
import "@material/mwc-button/mwc-button";
import Cropper from "cropperjs";
// @ts-ignore
import cropperCss from "cropperjs/dist/cropper.css";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
query,
TemplateResult,
unsafeCSS,
} from "lit-element";
import "../../components/ha-dialog";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { HaImageCropperDialogParams } from "./show-image-cropper-dialog";
import { classMap } from "lit-html/directives/class-map";
@customElement("image-cropper-dialog")
export class HaImagecropperDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@internalProperty() private _params?: HaImageCropperDialogParams;
@internalProperty() private _open = false;
@query("img") private _image!: HTMLImageElement;
private _cropper?: Cropper;
public showDialog(params: HaImageCropperDialogParams): void {
this._params = params;
this._open = true;
}
public closeDialog() {
this._open = false;
this._params = undefined;
this._cropper?.destroy();
}
protected updated(changedProperties: PropertyValues) {
if (!changedProperties.has("_params") || !this._params) {
return;
}
if (!this._cropper) {
this._image.src = URL.createObjectURL(this._params.file);
this._cropper = new Cropper(this._image, {
aspectRatio: this._params.options.aspectRatio,
viewMode: 1,
dragMode: "move",
minCropBoxWidth: 50,
ready: () => {
URL.revokeObjectURL(this._image!.src);
},
});
} else {
this._cropper.replace(URL.createObjectURL(this._params.file));
}
}
protected render(): TemplateResult {
return html`<ha-dialog
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.open=${this._open}
>
<div
class="container ${classMap({
round: Boolean(this._params?.options.round),
})}"
>
<img />
</div>
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.cancel")}
</mwc-button>
<mwc-button slot="primaryAction" @click=${this._cropImage}>
${this.hass.localize("ui.dialogs.image_cropper.crop")}
</mwc-button>
</ha-dialog>`;
}
private _cropImage() {
this._cropper!.getCroppedCanvas().toBlob(
(blob) => {
if (!blob) {
return;
}
const file = new File([blob], this._params!.file.name, {
type: this._params!.options.type || this._params!.file.type,
});
this._params!.croppedCallback(file);
this.closeDialog();
},
this._params!.options.type || this._params!.file.type,
this._params!.options.quality
);
}
static get styles(): CSSResult[] {
return [
haStyleDialog,
css`
${unsafeCSS(cropperCss)}
.container {
max-width: 640px;
}
img {
max-width: 100%;
}
.container.round .cropper-view-box,
.container.round .cropper-face {
border-radius: 50%;
}
.cropper-line,
.cropper-point,
.cropper-point.point-se::before {
background-color: var(--primary-color);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"image-cropper-dialog": HaImagecropperDialog;
}
}

View File

@@ -1,30 +0,0 @@
import { fireEvent } from "../../common/dom/fire_event";
export interface CropOptions {
round: boolean;
type?: "image/jpeg" | "image/png";
quality?: number;
aspectRatio: number;
}
export interface HaImageCropperDialogParams {
file: File;
options: CropOptions;
croppedCallback: (file: File) => void;
}
const loadImageCropperDialog = () =>
import(
/* webpackChunkName: "image-cropper-dialog" */ "./image-cropper-dialog"
);
export const showImageCropperDialog = (
element: HTMLElement,
dialogParams: HaImageCropperDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "image-cropper-dialog",
dialogImport: loadImageCropperDialog,
dialogParams,
});
};

View File

@@ -0,0 +1,361 @@
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) {
const oldVal = this.stateObj.attributes.effect;
const 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) {
const 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) {
const 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) {
const 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);

View File

@@ -1,305 +0,0 @@
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-attributes";
import "../../../components/ha-color-picker";
import "../../../components/ha-icon-button";
import "../../../components/ha-labeled-slider";
import "../../../components/ha-paper-dropdown-menu";
import {
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
SUPPORT_COLOR_TEMP,
SUPPORT_EFFECT,
SUPPORT_WHITE_VALUE,
} from "../../../data/light";
import type { HomeAssistant, LightEntity } from "../../../types";
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;
protected render(): TemplateResult {
if (!this.hass || !this.stateObj) {
return html``;
}
return html`
<div
class="content ${classMap({
"is-on": this.stateObj.state === "on",
})}"
>
${this.stateObj.state === "on"
? html`
${supportsFeature(this.stateObj!, SUPPORT_BRIGHTNESS)
? html`
<ha-labeled-slider
caption=${this.hass.localize("ui.card.light.brightness")}
icon="hass:brightness-5"
min="1"
max="255"
value=${this._brightnessSliderValue}
@change=${this._brightnessSliderChanged}
></ha-labeled-slider>
`
: ""}
${supportsFeature(this.stateObj, SUPPORT_COLOR_TEMP)
? html`
<ha-labeled-slider
class="color_temp"
caption=${this.hass.localize(
"ui.card.light.color_temperature"
)}
icon="hass:thermometer"
.min=${this.stateObj.attributes.min_mireds}
.max=${this.stateObj.attributes.max_mireds}
.value=${this._ctSliderValue}
@change=${this._ctSliderChanged}
></ha-labeled-slider>
`
: ""}
${supportsFeature(this.stateObj, SUPPORT_WHITE_VALUE)
? html`
<ha-labeled-slider
caption=${this.hass.localize("ui.card.light.white_value")}
icon="hass:file-word-box"
max="255"
.value=${this._wvSliderValue}
@change=${this._wvSliderChanged}
></ha-labeled-slider>
`
: ""}
${supportsFeature(this.stateObj, SUPPORT_COLOR)
? html`
<div class="segmentationContainer">
<ha-color-picker
class="color"
@colorselected=${this._colorPicked}
.desiredHsColor=${this._colorPickerColor}
throttle="500"
.hueSegments=${this._hueSegments}
.saturationSegments=${this._saturationSegments}
>
</ha-color-picker>
<ha-icon-button
icon="hass:palette"
@click=${this._segmentClick}
class="segmentationButton"
></ha-icon-button>
</div>
`
: ""}
${supportsFeature(this.stateObj, SUPPORT_EFFECT) &&
this.stateObj!.attributes.effect_list?.length
? html`
<ha-paper-dropdown-menu
.label=${this.hass.localize("ui.card.light.effect")}
>
<paper-listbox
slot="dropdown-content"
.selected=${this.stateObj.attributes.effect || ""}
@iron-select=${this._effectChanged}
attr-for-selected="item-name"
>${this.stateObj.attributes.effect_list.map(
(effect: string) => html`
<paper-item itemName=${effect}
>${effect}</paper-item
>
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>
`
: ""}
`
: ""}
<ha-attributes
.stateObj=${this.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>
`;
}
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;
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 _brightnessSliderChanged(ev: CustomEvent) {
const bri = parseInt((ev.target as any).value, 10);
if (isNaN(bri)) {
return;
}
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
brightness: bri,
});
}
private _ctSliderChanged(ev: CustomEvent) {
const ct = parseInt((ev.target as any).value, 10);
if (isNaN(ct)) {
return;
}
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
color_temp: ct,
});
}
private _wvSliderChanged(ev: CustomEvent) {
const wv = parseInt((ev.target as any).value, 10);
if (isNaN(wv)) {
return;
}
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
white_value: wv,
});
}
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`
.content {
display: flex;
flex-direction: column;
align-items: center;
}
.content.is-on {
margin-top: -16px;
}
.content > * {
width: 100%;
max-height: 84px;
overflow: hidden;
padding-top: 16px;
}
.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;
max-height: 500px;
}
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;
}
.segmentationButton {
position: absolute;
top: 5%;
color: var(--secondary-text-color);
}
paper-item {
cursor: pointer;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"more-info-light": MoreInfoLight;
}
}

View File

@@ -0,0 +1,421 @@
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;
const oldVal = this.playerObj.source;
const newVal = ev.detail.value;
if (!newVal || oldVal === newVal) return;
this.playerObj.selectSource(newVal);
}
handleSoundModeChanged(ev) {
if (!this.playerObj) return;
const oldVal = this.playerObj.soundMode;
const 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);

View File

@@ -1,431 +0,0 @@
import "@material/mwc-button/mwc-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
query,
TemplateResult,
} from "lit-element";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { computeRTLDirection } from "../../../common/util/compute_rtl";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-paper-dropdown-menu";
import "../../../components/ha-slider";
import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog";
import { UNAVAILABLE, UNAVAILABLE_STATES, UNKNOWN } from "../../../data/entity";
import {
ControlButton,
MediaPickedEvent,
SUPPORTS_PLAY,
SUPPORT_BROWSE_MEDIA,
SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE,
SUPPORT_PLAY_MEDIA,
SUPPORT_PREVIOUS_TRACK,
SUPPORT_SELECT_SOUND_MODE,
SUPPORT_SELECT_SOURCE,
SUPPORT_STOP,
SUPPORT_TURN_OFF,
SUPPORT_TURN_ON,
SUPPORT_VOLUME_BUTTONS,
SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_SET,
} from "../../../data/media-player";
import { HomeAssistant, MediaEntity } from "../../../types";
@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">
<div class="basic-controls">
${controls!.map(
(control) => html`
<ha-icon-button
action=${control.action}
.icon=${control.icon}
@click=${this._handleClick}
></ha-icon-button>
`
)}
</div>
${supportsFeature(stateObj, SUPPORT_BROWSE_MEDIA)
? html`
<ha-icon-button
icon="hass:folder-multiple"
.title=${this.hass.localize(
"ui.card.media_player.browse_media"
)}
@click=${this._showBrowseMedia}
>
</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;
}
.controls {
display: flex;
align-items: center;
}
.basic-controls {
flex-grow: 1;
}
.volume,
.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.item.itemName;
if (!newVal || this.stateObj!.attributes.source === newVal) {
return;
}
this.hass.callService("media_player", "select_source", {
entity_id: this.stateObj!.entity_id,
source: newVal,
});
}
private _handleSoundModeChanged(e: CustomEvent) {
const newVal = e.detail.item.itemName;
if (!newVal || this.stateObj?.attributes.sound_mode === newVal) {
return;
}
this.hass.callService("media_player", "select_sound_mode", {
entity_id: this.stateObj!.entity_id,
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 = "";
}
private _showBrowseMedia(): void {
showMediaBrowserDialog(this, {
action: "play",
entityId: this.stateObj!.entity_id,
mediaPickedCallback: (pickedMedia: MediaPickedEvent) =>
this._playMedia(
pickedMedia.media_content_id,
pickedMedia.media_content_type
),
});
}
private _playMedia(media_content_id: string, media_content_type: string) {
this.hass!.callService("media_player", "play_media", {
entity_id: this.stateObj!.entity_id,
media_content_id,
media_content_type,
});
}
}
declare global {
interface HTMLElementTagNameMap {
"more-info-media_player": MoreInfoMediaPlayer;
}
}

View File

@@ -2,8 +2,6 @@ import { ExternalMessaging } from "./external_messaging";
export interface ExternalConfig {
hasSettingsScreen: boolean;
canWriteTag: boolean;
hasExoPlayer: boolean;
}
export const getExternalConfig = (

View File

@@ -22,14 +22,6 @@
.header img {
margin-right: 16px;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #111111;
color: #e1e1e1;
--primary-text-color: #e1e1e1;
--secondary-text-color: #9b9b9b;
}
}
</style>
</head>
<body>

View File

@@ -48,7 +48,7 @@
}
@media (prefers-color-scheme: dark) {
html {
background-color: var(--primary-background-color, #111111);
background-color: #111111;
}
#ha-init-skeleton::before {
background-color: #1c1c1c;

View File

@@ -23,14 +23,6 @@
.header img {
margin-right: 16px;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #111111;
color: #e1e1e1;
--primary-text-color: #e1e1e1;
--secondary-text-color: #9b9b9b;
}
}
</style>
</head>
<body>

View File

@@ -5,15 +5,15 @@ import type { AppDrawerElement } from "@polymer/app-layout/app-drawer/app-drawer
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
customElement,
PropertyValues,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../common/dom/fire_event";
import { listenMediaQuery } from "../common/dom/media_query";
import { fireEvent } from "../common/dom/fire_event";
import { toggleAttribute } from "../common/dom/toggle_attribute";
import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer";
import type { HomeAssistant, Route } from "../types";
@@ -49,15 +49,7 @@ class HomeAssistantMain extends LitElement {
const disableSwipe =
!sidebarNarrow || NON_SWIPABLE_PANELS.indexOf(hass.panelUrl) !== -1;
// Style block in render because of the mixin that is not supported
return html`
<style>
app-drawer {
--app-drawer-content-container: {
background-color: var(--primary-background-color, #fff);
}
}
</style>
<app-drawer-layout
fullbleed
.forceNarrow=${sidebarNarrow}

View File

@@ -8,9 +8,9 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
internalProperty,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../common/dom/fire_event";
@@ -27,7 +27,6 @@ import type { PolymerChangedEvent } from "../polymer-types";
import type { HomeAssistant } from "../types";
const amsterdam = [52.3731339, 4.8903147];
const mql = matchMedia("(prefers-color-scheme: dark)");
@customElement("onboarding-core-config")
class OnboardingCoreConfig extends LitElement {
@@ -94,7 +93,6 @@ class OnboardingCoreConfig extends LitElement {
.hass=${this.hass}
.location=${this._locationValue}
.fitZoom=${14}
.darkMode=${mql.matches}
@change=${this._locationChanged}
></ha-location-editor>
</div>

View File

@@ -1,85 +1,67 @@
// @ts-ignore
import fullcalendarStyle from "@fullcalendar/common/main.css";
import { Calendar } from "@fullcalendar/core";
import type { CalendarOptions } from "@fullcalendar/core";
import dayGridPlugin from "@fullcalendar/daygrid";
// @ts-ignore
import daygridStyle from "@fullcalendar/daygrid/main.css";
import interactionPlugin from "@fullcalendar/interaction";
import listPlugin from "@fullcalendar/list";
// @ts-ignore
import listStyle from "@fullcalendar/list/main.css";
import "@material/mwc-button";
import { mdiViewAgenda, mdiViewDay, mdiViewModule, mdiViewWeek } from "@mdi/js";
import {
css,
property,
internalProperty,
PropertyValues,
LitElement,
CSSResult,
html,
internalProperty,
LitElement,
property,
PropertyValues,
TemplateResult,
css,
unsafeCSS,
TemplateResult,
} from "lit-element";
import memoize from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-button-toggle-group";
import { Calendar } from "@fullcalendar/core";
import dayGridPlugin from "@fullcalendar/daygrid";
// @ts-ignore
import fullcalendarStyle from "@fullcalendar/core/main.css";
// @ts-ignore
import daygridStyle from "@fullcalendar/daygrid/main.css";
import "@material/mwc-button";
import "../../components/ha-icon-button";
import { haStyle } from "../../resources/styles";
import "../../components/ha-button-toggle-group";
import type {
CalendarEvent,
CalendarViewChanged,
FullCalendarView,
HomeAssistant,
CalendarEvent,
ToggleButton,
HomeAssistant,
} from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
import { haStyle } from "../../resources/styles";
declare global {
interface HTMLElementTagNameMap {
"ha-full-calendar": HAFullCalendar;
}
interface HASSDomEvents {
"view-changed": CalendarViewChanged;
}
}
const defaultFullCalendarConfig: CalendarOptions = {
const fullCalendarConfig = {
headerToolbar: false,
plugins: [dayGridPlugin, listPlugin, interactionPlugin],
plugins: [dayGridPlugin],
initialView: "dayGridMonth",
dayMaxEventRows: true,
height: "parent",
eventDisplay: "list-item",
};
const viewButtons: ToggleButton[] = [
{ label: "Month View", value: "dayGridMonth", iconPath: mdiViewModule },
{ label: "Week View", value: "dayGridWeek", iconPath: mdiViewWeek },
{ label: "Day View", value: "dayGridDay", iconPath: mdiViewDay },
{ label: "List View", value: "listWeek", iconPath: mdiViewAgenda },
{ label: "Month View", value: "dayGridMonth", icon: "hass:view-module" },
{ label: "Week View", value: "dayGridWeek", icon: "hass:view-week" },
{ label: "Day View", value: "dayGridDay", icon: "hass:view-day" },
];
class HAFullCalendar extends LitElement {
public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow = false;
@property() public events: CalendarEvent[] = [];
@property({ attribute: false }) public events: CalendarEvent[] = [];
@property({ attribute: false }) public views: FullCalendarView[] = [
"dayGridMonth",
"dayGridWeek",
"dayGridDay",
];
@property({ type: Boolean, reflect: true })
public narrow!: boolean;
@internalProperty() private calendar?: Calendar;
@internalProperty() private _activeView: FullCalendarView = "dayGridMonth";
@internalProperty() private _activeView = "dayGridMonth";
protected render(): TemplateResult {
const viewToggleButtons = this._viewToggleButtons(this.views);
return html`
${this.calendar
? html`
@@ -114,12 +96,27 @@ class HAFullCalendar extends LitElement {
${this.calendar.view.title}
</h1>
<ha-button-toggle-group
.buttons=${viewToggleButtons}
.buttons=${viewButtons}
.active=${this._activeView}
@value-changed=${this._handleView}
></ha-button-toggle-group>
`
: html`
<div class="controls">
<mwc-button
outlined
class="today"
@click=${this._handleToday}
>${this.hass.localize(
"ui.panel.calendar.today"
)}</mwc-button
>
<ha-button-toggle-group
.buttons=${viewButtons}
.active=${this._activeView}
@value-changed=${this._handleView}
></ha-button-toggle-group>
</div>
<div class="controls">
<h1>
${this.calendar.view.title}
@@ -141,21 +138,6 @@ class HAFullCalendar extends LitElement {
</ha-icon-button>
</div>
</div>
<div class="controls">
<mwc-button
outlined
class="today"
@click=${this._handleToday}
>${this.hass.localize(
"ui.panel.calendar.today"
)}</mwc-button
>
<ha-button-toggle-group
.buttons=${viewToggleButtons}
.active=${this._activeView}
@value-changed=${this._handleView}
></ha-button-toggle-group>
</div>
`}
</div>
`
@@ -175,25 +157,14 @@ class HAFullCalendar extends LitElement {
this.calendar.removeAllEventSources();
this.calendar.addEventSource(this.events);
}
if (changedProps.has("views") && !this.views.includes(this._activeView)) {
this._activeView = this.views[0];
this.calendar!.changeView(this._activeView);
this._fireViewChanged();
}
}
protected firstUpdated(): void {
const config: CalendarOptions = {
...defaultFullCalendarConfig,
locale: this.hass.language,
};
config.dateClick = (info) => this._handleDateClick(info);
config.eventClick = (info) => this._handleEventClick(info);
const config = { ...fullCalendarConfig, locale: this.hass.language };
this.calendar = new Calendar(
this.shadowRoot!.getElementById("calendar")!,
// @ts-ignore
config
);
@@ -201,25 +172,6 @@ class HAFullCalendar extends LitElement {
this._fireViewChanged();
}
private _handleEventClick(info): void {
if (info.view.type !== "dayGridMonth") {
return;
}
this._activeView = "dayGridDay";
this.calendar!.changeView("dayGridDay");
this.calendar!.gotoDate(info.event.startStr);
}
private _handleDateClick(info): void {
if (info.view.type !== "dayGridMonth") {
return;
}
this._activeView = "dayGridDay";
this.calendar!.changeView("dayGridDay");
this.calendar!.gotoDate(info.dateStr);
}
private _handleNext(): void {
this.calendar!.next();
this._fireViewChanged();
@@ -249,21 +201,14 @@ class HAFullCalendar extends LitElement {
});
}
private _viewToggleButtons = memoize((views) =>
viewButtons.filter((button) =>
views.includes(button.value as FullCalendarView)
)
);
static get styles(): CSSResult[] {
return [
haStyle,
css`
${unsafeCSS(fullcalendarStyle)}
${unsafeCSS(daygridStyle)}
${unsafeCSS(listStyle)}
:host {
:host {
display: flex;
flex-direction: column;
--fc-theme-standard-border-color: var(--divider-color);
@@ -317,15 +262,6 @@ class HAFullCalendar extends LitElement {
#calendar {
flex-grow: 1;
background-color: var(--card-background-color);
min-height: 400px;
--fc-neutral-bg-color: var(--card-background-color);
--fc-list-event-hover-bg-color: var(--card-background-color);
--fc-theme-standard-border-color: var(--divider-color);
--fc-border-color: var(--divider-color);
}
a {
color: inherit !important;
}
.fc-theme-standard .fc-scrollgrid {
@@ -337,20 +273,15 @@ class HAFullCalendar extends LitElement {
}
th.fc-col-header-cell.fc-day {
color: var(--secondary-text-color);
color: #70757a;
font-size: 11px;
font-weight: 400;
text-transform: uppercase;
}
.fc-daygrid-dot-event:hover {
background-color: inherit
}
.fc-daygrid-day-top {
text-align: center;
padding-top: 5px;
justify-content: center;
padding-top: 8px;
}
table.fc-scrollgrid-sync-table
@@ -365,21 +296,13 @@ class HAFullCalendar extends LitElement {
font-size: 12px;
}
.fc .fc-daygrid-day-number {
padding: 3px !important;
}
.fc .fc-daygrid-day.fc-day-today {
td.fc-day-today {
background: inherit;
}
td.fc-day-today .fc-daygrid-day-top {
padding-top: 4px;
}
td.fc-day-today .fc-daygrid-day-number {
height: 24px;
color: var(--text-primary-color) !important;
color: var(--text-primary-color);
background-color: var(--primary-color);
border-radius: 50%;
display: inline-block;
@@ -419,66 +342,6 @@ class HAFullCalendar extends LitElement {
.fc-popover-header {
background-color: var(--secondary-background-color) !important;
}
.fc-theme-standard .fc-list-day-frame {
background-color: transparent;
}
.fc-list.fc-view,
.fc-list-event.fc-event td {
border: none;
}
.fc-list-day.fc-day th {
border-bottom: none;
border-top: 1px solid var(--fc-theme-standard-border-color, #ddd) !important;
}
.fc-list-day-text {
font-size: 16px;
font-weight: 400;
}
.fc-list-day-side-text {
font-weight: 400;
font-size: 16px;
color: var(--primary-color);
}
.fc-list-table td,
.fc-list-day-frame {
padding-top: 12px;
padding-bottom: 12px;
}
:host([narrow]) .fc-dayGridMonth-view
.fc-daygrid-dot-event
.fc-event-time,
:host([narrow]) .fc-dayGridMonth-view
.fc-daygrid-dot-event
.fc-event-title,
:host([narrow]) .fc-dayGridMonth-view .fc-daygrid-day-bottom {
display: none;
}
:host([narrow]) .fc .fc-dayGridMonth-view .fc-daygrid-event-harness-abs {
visibility: visible !important;
position: static;
}
:host([narrow]) .fc-dayGridMonth-view .fc-daygrid-day-events {
display: flex;
min-height: 2em !important;
justify-content: center;
flex-wrap: wrap;
max-height: 2em;
height: 2em;
overflow: hidden;
}
:host([narrow]) .fc-dayGridMonth-view .fc-scrollgrid-sync-table {
overflow: hidden;
}
`,
];
}

View File

@@ -53,6 +53,12 @@ class PanelCalendar extends LitElement {
selected: true,
calendar,
}));
if (!this._start || !this._end) {
return;
}
this._fetchEvents(this._start, this._end, this._selectedCalendars);
}
protected render(): TemplateResult {
@@ -82,8 +88,8 @@ class PanelCalendar extends LitElement {
<mwc-formfield .label=${selCal.calendar.name}>
<mwc-checkbox
style=${styleMap({
"--mdc-theme-secondary": selCal.calendar
.backgroundColor!,
"--mdc-theme-secondary":
selCal.calendar.backgroundColor,
})}
.value=${selCal.calendar.entity_id}
.checked=${selCal.selected}

View File

@@ -1,5 +1,3 @@
import "@material/mwc-fab";
import { mdiPlus } from "@mdi/js";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import {
@@ -18,8 +16,7 @@ import {
DataTableColumnContainer,
RowClickedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
import "@material/mwc-fab";
import {
AreaRegistryEntry,
createAreaRegistryEntry,
@@ -29,6 +26,8 @@ import {
devicesInArea,
} from "../../../data/device_registry";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage-data-table";
import { HomeAssistant, Route } from "../../../types";
@@ -38,6 +37,7 @@ import {
loadAreaRegistryDetailDialog,
showAreaRegistryDetailDialog,
} from "./show-dialog-area-registry-detail";
import { mdiPlus } from "@mdi/js";
@customElement("ha-config-areas-dashboard")
export class HaConfigAreasDashboard extends LitElement {

View File

@@ -34,7 +34,6 @@ import "./types/ha-automation-trigger-time";
import "./types/ha-automation-trigger-time_pattern";
import "./types/ha-automation-trigger-webhook";
import "./types/ha-automation-trigger-zone";
import "./types/ha-automation-trigger-tag";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import { haStyle } from "../../../../resources/styles";
@@ -47,7 +46,6 @@ const OPTIONS = [
"mqtt",
"numeric_state",
"sun",
"tag",
"template",
"time",
"time_pattern",

View File

@@ -1,72 +0,0 @@
import "@polymer/paper-input/paper-input";
import {
customElement,
html,
LitElement,
property,
internalProperty,
PropertyValues,
} from "lit-element";
import { TagTrigger } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types";
import { TriggerElement } from "../ha-automation-trigger-row";
import { Tag, fetchTags } from "../../../../../data/tag";
import { fireEvent } from "../../../../../common/dom/fire_event";
@customElement("ha-automation-trigger-tag")
export class HaTagTrigger extends LitElement implements TriggerElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public trigger!: TagTrigger;
@internalProperty() private _tags: Tag[] = [];
public static get defaultConfig() {
return { tag_id: "" };
}
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
this._fetchTags();
}
protected render() {
const { tag_id } = this.trigger;
return html`
<ha-paper-dropdown-menu
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.tag.label"
)}
?disabled=${this._tags.length === 0}
>
<paper-listbox
slot="dropdown-content"
.selected=${tag_id}
attr-for-selected="tag_id"
@iron-select=${this._tagChanged}
>
${this._tags.map(
(tag) => html`
<paper-item tag_id=${tag.id} .tag=${tag}>
${tag.name || tag.id}
</paper-item>
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>
`;
}
private async _fetchTags() {
this._tags = await fetchTags(this.hass);
}
private _tagChanged(ev) {
fireEvent(this, "value-changed", {
value: {
...this.trigger,
tag_id: ev.detail.item.tag.id,
},
});
}
}

View File

@@ -12,13 +12,7 @@ import {
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import { haStyle } from "../../../../../../resources/styles";
import { HomeAssistant } from "../../../../../../types";
import {
OZWDevice,
fetchOZWNodeStatus,
getIdentifiersFromDevice,
OZWNodeIdentifiers,
} from "../../../../../../data/ozw";
import { showOZWRefreshNodeDialog } from "../../../../integrations/integration-panels/ozw/show-dialog-ozw-refresh-node";
import { OZWDevice, fetchOZWNodeStatus } from "../../../../../../data/ozw";
@customElement("ha-device-info-ozw")
export class HaDeviceInfoOzw extends LitElement {
@@ -26,34 +20,26 @@ export class HaDeviceInfoOzw extends LitElement {
@property() public device!: DeviceRegistryEntry;
@property()
private node_id = 0;
@property()
private ozw_instance = 1;
@internalProperty() private _ozwDevice?: OZWDevice;
protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("device")) {
const identifiers:
| OZWNodeIdentifiers
| undefined = getIdentifiersFromDevice(this.device);
if (!identifiers) {
return;
}
this.ozw_instance = identifiers.ozw_instance;
this.node_id = identifiers.node_id;
this._fetchNodeDetails();
this._fetchNodeDetails(this.device);
}
}
protected async _fetchNodeDetails() {
protected async _fetchNodeDetails(device) {
const ozwIdentifier = device.identifiers.find(
(identifier) => identifier[0] === "ozw"
);
if (!ozwIdentifier) {
return;
}
const identifiers = ozwIdentifier[1].split(".");
this._ozwDevice = await fetchOZWNodeStatus(
this.hass,
this.ozw_instance,
this.node_id
identifiers[0],
identifiers[1]
);
}
@@ -83,19 +69,9 @@ export class HaDeviceInfoOzw extends LitElement {
? this.hass.localize("ui.common.yes")
: this.hass.localize("ui.common.no")}
</div>
<mwc-button @click=${this._refreshNodeClicked}>
Refresh Node
</mwc-button>
`;
}
private async _refreshNodeClicked() {
showOZWRefreshNodeDialog(this, {
node_id: this.node_id,
ozw_instance: this.ozw_instance,
});
}
static get styles(): CSSResult[] {
return [
haStyle,

View File

@@ -119,11 +119,9 @@ export class HaDeviceActionsZha extends LitElement {
return;
}
await this.hass.callService("zha", "remove", {
this.hass.callService("zha", "remove", {
ieee_address: this._zhaDevice!.ieee,
});
history.back();
}
static get styles(): CSSResult[] {

View File

@@ -44,7 +44,6 @@ import "./device-detail/ha-device-entities-card";
import "./device-detail/ha-device-info-card";
import { showDeviceAutomationDialog } from "./device-detail/show-dialog-device-automation";
import { slugify } from "../../../common/string/slugify";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
export interface EntityRegistryStateEntry extends EntityRegistryEntry {
stateName?: string | null;
@@ -308,15 +307,11 @@ export class HaConfigDevicePage extends LitElement {
: "";
})
: html`
<paper-item class="no-link">
${this.hass.localize(
"ui.panel.config.devices.add_prompt",
"name",
this.hass.localize(
"ui.panel.config.devices.automation.automations"
)
)}
</paper-item>
<paper-item class="no-link"
>${this.hass.localize(
"ui.panel.config.devices.automation.no_automations"
)}</paper-item
>
`}
</ha-card>
`
@@ -380,15 +375,11 @@ export class HaConfigDevicePage extends LitElement {
: "";
})
: html`
<paper-item class="no-link">
${this.hass.localize(
"ui.panel.config.devices.add_prompt",
"name",
this.hass.localize(
"ui.panel.config.devices.scene.scenes"
)
)}
</paper-item>
<paper-item class="no-link"
>${this.hass.localize(
"ui.panel.config.devices.scene.no_scenes"
)}</paper-item
>
`
}
</ha-card>
@@ -437,13 +428,9 @@ export class HaConfigDevicePage extends LitElement {
: html`
<paper-item class="no-link">
${this.hass.localize(
"ui.panel.config.devices.add_prompt",
"name",
this.hass.localize(
"ui.panel.config.devices.script.scripts"
)
)}
</paper-item>
"ui.panel.config.devices.script.no_scripts"
)}</paper-item
>
`}
</ha-card>
`
@@ -562,14 +549,11 @@ export class HaConfigDevicePage extends LitElement {
const renameEntityid =
this.showAdvanced &&
(await showConfirmationDialog(this, {
title: this.hass.localize(
confirm(
this.hass.localize(
"ui.panel.config.devices.confirm_rename_entity_ids"
),
text: this.hass.localize(
"ui.panel.config.devices.confirm_rename_entity_ids_warning"
),
}));
)
);
const updateProms = entities.map((entity) => {
const name = entity.name || entity.stateName;
@@ -718,10 +702,6 @@ export class HaConfigDevicePage extends LitElement {
color: var(--primary-color);
}
ha-card {
padding-bottom: 8px;
}
ha-card a {
color: var(--primary-text-color);
}

View File

@@ -1,9 +1,9 @@
import {
customElement,
html,
internalProperty,
LitElement,
property,
internalProperty,
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
@@ -25,8 +25,8 @@ import {
} from "../../../data/device_registry";
import {
EntityRegistryEntry,
findBatteryChargingEntity,
findBatteryEntity,
findBatteryChargingEntity,
} from "../../../data/entity_registry";
import { domainToName } from "../../../data/integration";
import "../../../layouts/hass-tabs-subpage-data-table";
@@ -181,8 +181,8 @@ export class HaConfigDeviceDashboard extends LitElement {
);
private _columns = memoizeOne(
(narrow: boolean): DataTableColumnContainer => {
const columns: DataTableColumnContainer = narrow
(narrow: boolean): DataTableColumnContainer =>
narrow
? {
name: {
title: "Device",
@@ -199,6 +199,36 @@ export class HaConfigDeviceDashboard extends LitElement {
`;
},
},
battery_entity: {
title: this.hass.localize(
"ui.panel.config.devices.data_table.battery"
),
sortable: true,
type: "numeric",
width: "90px",
template: (
batteryEntityPair: DeviceRowData["battery_entity"]
) => {
const battery =
batteryEntityPair && batteryEntityPair[0]
? this.hass.states[batteryEntityPair[0]]
: undefined;
const batteryCharging =
batteryEntityPair && batteryEntityPair[1]
? this.hass.states[batteryEntityPair[1]]
: undefined;
return battery
? html`
${isNaN(battery.state as any) ? "-" : battery.state}%
<ha-battery-icon
.hass=${this.hass!}
.batteryStateObj=${battery}
.batteryChargingStateObj=${batteryCharging}
></ha-battery-icon>
`
: html` - `;
},
},
}
: {
name: {
@@ -210,69 +240,70 @@ export class HaConfigDeviceDashboard extends LitElement {
grows: true,
direction: "asc",
},
};
columns.manufacturer = {
title: this.hass.localize(
"ui.panel.config.devices.data_table.manufacturer"
),
sortable: true,
hidden: narrow,
filterable: true,
width: "15%",
};
columns.model = {
title: this.hass.localize("ui.panel.config.devices.data_table.model"),
sortable: true,
hidden: narrow,
filterable: true,
width: "15%",
};
columns.area = {
title: this.hass.localize("ui.panel.config.devices.data_table.area"),
sortable: true,
hidden: narrow,
filterable: true,
width: "15%",
};
columns.integration = {
title: this.hass.localize(
"ui.panel.config.devices.data_table.integration"
),
sortable: true,
hidden: narrow,
filterable: true,
width: "15%",
};
columns.battery_entity = {
title: this.hass.localize("ui.panel.config.devices.data_table.battery"),
sortable: true,
type: "numeric",
width: narrow ? "90px" : "15%",
maxWidth: "90px",
template: (batteryEntityPair: DeviceRowData["battery_entity"]) => {
const battery =
batteryEntityPair && batteryEntityPair[0]
? this.hass.states[batteryEntityPair[0]]
: undefined;
const batteryCharging =
batteryEntityPair && batteryEntityPair[1]
? this.hass.states[batteryEntityPair[1]]
: undefined;
return battery && !isNaN(battery.state as any)
? html`
${battery.state}%
<ha-battery-icon
.hass=${this.hass!}
.batteryStateObj=${battery}
.batteryChargingStateObj=${batteryCharging}
></ha-battery-icon>
`
: html` - `;
},
};
return columns;
}
manufacturer: {
title: this.hass.localize(
"ui.panel.config.devices.data_table.manufacturer"
),
sortable: true,
filterable: true,
width: "15%",
},
model: {
title: this.hass.localize(
"ui.panel.config.devices.data_table.model"
),
sortable: true,
filterable: true,
width: "15%",
},
area: {
title: this.hass.localize(
"ui.panel.config.devices.data_table.area"
),
sortable: true,
filterable: true,
width: "15%",
},
integration: {
title: this.hass.localize(
"ui.panel.config.devices.data_table.integration"
),
sortable: true,
filterable: true,
width: "15%",
},
battery_entity: {
title: this.hass.localize(
"ui.panel.config.devices.data_table.battery"
),
sortable: true,
type: "numeric",
width: "15%",
maxWidth: "90px",
template: (
batteryEntityPair: DeviceRowData["battery_entity"]
) => {
const battery =
batteryEntityPair && batteryEntityPair[0]
? this.hass.states[batteryEntityPair[0]]
: undefined;
const batteryCharging =
batteryEntityPair && batteryEntityPair[1]
? this.hass.states[batteryEntityPair[1]]
: undefined;
return battery && !isNaN(battery.state as any)
? html`
${battery.state}%
<ha-battery-icon
.hass=${this.hass!}
.batteryStateObj=${battery}
.batteryChargingStateObj=${batteryCharging}
></ha-battery-icon>
`
: html` - `;
},
},
}
);
public constructor() {

View File

@@ -32,7 +32,6 @@ import {
mdiInformation,
mdiMathLog,
mdiPencil,
mdiNfcVariant,
} from "@mdi/js";
declare global {
@@ -100,15 +99,6 @@ export const configSections: { [name: string]: PageNavigation[] } = {
core: true,
},
],
experimental: [
{
component: "tags",
path: "/config/tags",
translationKey: "ui.panel.config.tags.caption",
iconPath: mdiNfcVariant,
core: true,
},
],
lovelace: [
{
component: "lovelace",
@@ -205,13 +195,6 @@ class HaPanelConfig extends HassRouterPage {
/* webpackChunkName: "panel-config-automation" */ "./automation/ha-config-automation"
),
},
tags: {
tag: "ha-config-tags",
load: () =>
import(
/* webpackChunkName: "panel-config-tags" */ "./tags/ha-config-tags"
),
},
cloud: {
tag: "ha-config-cloud",
load: () =>
@@ -352,13 +335,6 @@ class HaPanelConfig extends HassRouterPage {
/* webpackChunkName: "panel-config-mqtt" */ "./integrations/integration-panels/mqtt/mqtt-config-panel"
),
},
ozw: {
tag: "ozw-config-router",
load: () =>
import(
/* webpackChunkName: "panel-config-ozw" */ "./integrations/integration-panels/ozw/ozw-config-router"
),
},
},
};

View File

@@ -14,7 +14,6 @@ import {
ConfigEntry,
updateConfigEntry,
deleteConfigEntry,
reloadConfigEntry,
} from "../../../data/config_entries";
import { EntityRegistryEntry } from "../../../data/entity_registry";
import { DeviceRegistryEntry } from "../../../data/device_registry";
@@ -29,8 +28,7 @@ import { haStyle } from "../../../resources/styles";
import "../../../components/ha-icon-next";
import { fireEvent } from "../../../common/dom/fire_event";
import { mdiDotsVertical, mdiOpenInNew } from "@mdi/js";
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
export interface ConfigEntryUpdatedEvent {
entry: ConfigEntry;
@@ -57,10 +55,6 @@ const integrationsWithPanel = {
buttonLocalizeKey: "ui.panel.config.zha.button",
path: "/config/zha/dashboard",
},
ozw: {
buttonLocalizeKey: "ui.panel.config.ozw.button",
path: "/config/ozw/dashboard",
},
zwave: {
buttonLocalizeKey: "ui.panel.config.zwave.button",
path: "/config/zwave",
@@ -230,7 +224,7 @@ export class HaIntegrationCard extends LitElement {
`
: ""}
</div>
<ha-button-menu corner="BOTTOM_START">
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
<mwc-icon-button
.title=${this.hass.localize("ui.common.menu")}
.label=${this.hass.localize("ui.common.overflow_menu")}
@@ -238,7 +232,7 @@ export class HaIntegrationCard extends LitElement {
>
<ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item @request-selected="${this._handleSystemOptions}">
<mwc-list-item>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.system_options"
)}
@@ -261,17 +255,7 @@ export class HaIntegrationCard extends LitElement {
</mwc-list-item>
</a>
`}
${item.state === "loaded" && item.supports_unload
? html`<mwc-list-item @request-selected="${this._handleReload}">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.reload"
)}
</mwc-list-item>`
: ""}
<mwc-list-item
class="warning"
@request-selected="${this._handleDelete}"
>
<mwc-list-item class="warning">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.delete"
)}
@@ -321,31 +305,17 @@ export class HaIntegrationCard extends LitElement {
showOptionsFlowDialog(this, ev.target.closest("ha-card").configEntry);
}
private _handleReload(ev: CustomEvent<RequestSelectedDetail>): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
private _handleAction(ev: CustomEvent<ActionDetail>) {
const configEntry = ((ev.target as HTMLElement).closest("ha-card") as any)
.configEntry;
switch (ev.detail.index) {
case 0:
this._showSystemOptions(configEntry);
break;
case 1:
this._removeIntegration(configEntry);
break;
}
this._reloadIntegration(
((ev.target as HTMLElement).closest("ha-card") as any).configEntry
);
}
private _handleDelete(ev: CustomEvent<RequestSelectedDetail>): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
this._removeIntegration(
((ev.target as HTMLElement).closest("ha-card") as any).configEntry
);
}
private _handleSystemOptions(ev: CustomEvent<RequestSelectedDetail>): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
this._showSystemOptions(
((ev.target as HTMLElement).closest("ha-card") as any).configEntry
);
}
private _showSystemOptions(configEntry: ConfigEntry) {
@@ -379,21 +349,6 @@ export class HaIntegrationCard extends LitElement {
});
}
private async _reloadIntegration(configEntry: ConfigEntry) {
const entryId = configEntry.entry_id;
reloadConfigEntry(this.hass, entryId).then((result) => {
const locale_key = result.require_restart
? "reload_restart_confirm"
: "reload_confirm";
showAlertDialog(this, {
text: this.hass.localize(
`ui.panel.config.integrations.config_entry.${locale_key}`
),
});
});
}
private async _editEntryName(ev) {
const configEntry = ev.target.closest("ha-card").configEntry;
const newName = await showPromptDialog(this, {

View File

@@ -1,272 +0,0 @@
import {
CSSResult,
customElement,
html,
LitElement,
property,
internalProperty,
TemplateResult,
PropertyValues,
css,
} from "lit-element";
import "../../../../../components/ha-code-editor";
import "../../../../../components/ha-circular-progress";
import { createCloseHeading } from "../../../../../components/ha-dialog";
import { haStyleDialog } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import { OZWRefreshNodeDialogParams } from "./show-dialog-ozw-refresh-node";
import {
fetchOZWNodeMetadata,
OZWDeviceMetaData,
OZWDevice,
nodeQueryStages,
} from "../../../../../data/ozw";
@customElement("dialog-ozw-refresh-node")
class DialogOZWRefreshNode extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@internalProperty() private _node_id?: number;
@internalProperty() private _ozw_instance = 1;
@internalProperty() private _nodeMetaData?: OZWDeviceMetaData;
@internalProperty() private _node?: OZWDevice;
@internalProperty() private _active = false;
@internalProperty() private _complete = false;
private _refreshDevicesTimeoutHandle?: number;
private _subscribed?: Promise<() => Promise<void>>;
public disconnectedCallback(): void {
super.disconnectedCallback();
this._unsubscribe();
}
protected updated(changedProperties: PropertyValues): void {
super.update(changedProperties);
if (changedProperties.has("node_id")) {
this._fetchData();
}
}
private async _fetchData() {
if (!this._node_id) {
return;
}
const metaDataResponse = await fetchOZWNodeMetadata(
this.hass,
this._ozw_instance,
this._node_id
);
this._nodeMetaData = metaDataResponse.metadata;
}
public async showDialog(params: OZWRefreshNodeDialogParams): Promise<void> {
this._node_id = params.node_id;
this._ozw_instance = params.ozw_instance;
this._fetchData();
}
protected render(): TemplateResult {
if (!this._node_id) {
return html``;
}
return html`
<ha-dialog
open
@closing="${this._close}"
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.ozw.refresh_node.title")
)}
>
${this._complete
? html`
<p>
${this.hass.localize(
"ui.panel.config.ozw.refresh_node.complete"
)}
</p>
<mwc-button slot="primaryAction" @click=${this._close}>
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: html`
${this._active
? html`
<div class="flex-container">
<ha-circular-progress active></ha-circular-progress>
<div>
<p>
<b>
${this.hass.localize(
"ui.panel.config.ozw.refresh_node.refreshing_description"
)}
</b>
</p>
${this._node
? html`
<p>
${this.hass.localize(
"ui.panel.config.ozw.refresh_node.node_status"
)}:
${this._node.node_query_stage}
(${this.hass.localize(
"ui.panel.config.ozw.refresh_node.step"
)}
${nodeQueryStages.indexOf(
this._node.node_query_stage
) + 1}/17)
</p>
<p>
<em>
${this.hass.localize(
"ui.panel.config.ozw.node_query_stages." +
this._node.node_query_stage.toLowerCase()
)}</em
>
</p>
`
: ``}
</div>
</div>
`
: html`
${this.hass.localize(
"ui.panel.config.ozw.refresh_node.description"
)}
<p>
${this.hass.localize(
"ui.panel.config.ozw.refresh_node.battery_note"
)}
</p>
`}
${this._nodeMetaData?.WakeupHelp !== ""
? html`
<b>
${this.hass.localize(
"ui.panel.config.ozw.refresh_node.wakeup_header"
)}
${this._nodeMetaData!.Name}
</b>
<blockquote>
${this._nodeMetaData!.WakeupHelp}
<br />
<em>
${this.hass.localize(
"ui.panel.config.ozw.refresh_node.wakeup_instructions_source"
)}
</em>
</blockquote>
`
: ""}
${!this._active
? html`
<mwc-button
slot="primaryAction"
@click=${this._startRefresh}
>
${this.hass.localize(
"ui.panel.config.ozw.refresh_node.start_refresh_button"
)}
</mwc-button>
`
: html``}
`}
</ha-dialog>
`;
}
private _startRefresh(): void {
this._subscribe();
}
private _handleMessage(message: any): void {
if (message.type === "node_updated") {
this._node = message;
if (message.node_query_stage === "Complete") {
this._unsubscribe();
this._complete = true;
}
}
}
private _unsubscribe(): void {
this._active = false;
if (this._refreshDevicesTimeoutHandle) {
clearTimeout(this._refreshDevicesTimeoutHandle);
}
if (this._subscribed) {
this._subscribed.then((unsub) => unsub());
this._subscribed = undefined;
}
}
private _subscribe(): void {
if (!this.hass) {
return;
}
this._active = true;
this._subscribed = this.hass.connection.subscribeMessage(
(message) => this._handleMessage(message),
{
type: "ozw/refresh_node_info",
node_id: this._node_id,
ozw_instance: this._ozw_instance,
}
);
this._refreshDevicesTimeoutHandle = window.setTimeout(
() => this._unsubscribe(),
120000
);
}
private _close(): void {
this._complete = false;
this._node_id = undefined;
this._node = undefined;
}
static get styles(): CSSResult[] {
return [
haStyleDialog,
css`
blockquote {
display: block;
background-color: #ddd;
padding: 8px;
margin: 8px 0;
font-size: 0.9em;
}
blockquote em {
font-size: 0.9em;
margin-top: 6px;
}
.flex-container {
display: flex;
align-items: center;
}
.flex-container ha-circular-progress {
margin-right: 20px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-ozw-refresh-node": DialogOZWRefreshNode;
}
}

View File

@@ -1,227 +0,0 @@
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import "@material/mwc-fab";
import {
css,
CSSResultArray,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { navigate } from "../../../../../common/navigate";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-next";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import "../../../ha-config-section";
import { mdiCircle, mdiCheckCircle, mdiCloseCircle, mdiZWave } from "@mdi/js";
import "../../../../../layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
import "@material/mwc-button/mwc-button";
import {
OZWInstance,
fetchOZWInstances,
networkOnlineStatuses,
networkOfflineStatuses,
networkStartingStatuses,
} from "../../../../../data/ozw";
export const ozwTabs: PageNavigation[] = [];
@customElement("ozw-config-dashboard")
class OZWConfigDashboard extends LitElement {
@property({ type: Object }) public hass!: HomeAssistant;
@property({ type: Object }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
@property({ type: Boolean }) public isWide!: boolean;
@property() public configEntryId?: string;
@internalProperty() private _instances: OZWInstance[] = [];
public connectedCallback(): void {
super.connectedCallback();
if (this.hass) {
this._fetchData();
}
}
private async _fetchData() {
this._instances = await fetchOZWInstances(this.hass!);
if (this._instances.length === 1) {
navigate(
this,
`/config/ozw/network/${this._instances[0].ozw_instance}`,
true
);
}
}
protected render(): TemplateResult {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${ozwTabs}
back-path="/config/integrations"
>
<ha-config-section .narrow=${this.narrow} .isWide=${this.isWide}>
<div slot="header">
${this.hass.localize("ui.panel.config.ozw.select_instance.header")}
</div>
<div slot="introduction">
${this.hass.localize(
"ui.panel.config.ozw.select_instance.introduction"
)}
</div>
${this._instances.length > 0
? html`
${this._instances.map((instance) => {
let status = "unknown";
let icon = mdiCircle;
if (networkOnlineStatuses.includes(instance.Status)) {
status = "online";
icon = mdiCheckCircle;
}
if (networkStartingStatuses.includes(instance.Status)) {
status = "starting";
}
if (networkOfflineStatuses.includes(instance.Status)) {
status = "offline";
icon = mdiCloseCircle;
}
return html`
<ha-card>
<a
href="/config/ozw/network/${instance.ozw_instance}"
aria-role="option"
tabindex="-1"
>
<paper-icon-item>
<ha-svg-icon .path=${mdiZWave} slot="item-icon">
</ha-svg-icon>
<paper-item-body>
${this.hass.localize(
"ui.panel.config.ozw.common.instance"
)}
${instance.ozw_instance}
<div secondary>
<ha-svg-icon
.path=${icon}
class="network-status-icon ${status}"
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.ozw.network_status." + status
)}
-
${this.hass.localize(
"ui.panel.config.ozw.network_status.details." +
instance.Status.toLowerCase()
)}<br />
${this.hass.localize(
"ui.panel.config.ozw.common.controller"
)}
: ${instance.getControllerPath}<br />
OZWDaemon ${instance.OZWDaemon_Version} (OpenZWave
${instance.OpenZWave_Version})
</div>
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-icon-item>
</a>
</ha-card>
`;
})}
`
: ``}
</ha-config-section>
</hass-tabs-subpage>
`;
}
static get styles(): CSSResultArray {
return [
haStyle,
css`
ha-card:last-child {
margin-bottom: 24px;
}
ha-config-section {
margin-top: -12px;
}
:host([narrow]) ha-config-section {
margin-top: -20px;
}
ha-card {
overflow: hidden;
}
ha-card a {
text-decoration: none;
color: var(--primary-text-color);
}
paper-item-body {
margin: 16px 0;
}
a {
text-decoration: none;
color: var(--primary-text-color);
position: relative;
display: block;
outline: 0;
}
ha-svg-icon.network-status-icon {
height: 14px;
width: 14px;
}
.online {
color: green;
}
.starting {
color: orange;
}
.offline {
color: red;
}
ha-svg-icon,
ha-icon-next {
color: var(--secondary-text-color);
}
.iron-selected paper-item::before,
a:not(.iron-selected):focus::before {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
pointer-events: none;
content: "";
transition: opacity 15ms linear;
will-change: opacity;
}
a:not(.iron-selected):focus::before {
background-color: currentColor;
opacity: var(--dark-divider-opacity);
}
.iron-selected paper-item:focus::before,
.iron-selected:focus paper-item::before {
opacity: 0.2;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ozw-config-dashboard": OZWConfigDashboard;
}
}

View File

@@ -1,253 +0,0 @@
import "@material/mwc-fab";
import {
css,
CSSResultArray,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { navigate } from "../../../../../common/navigate";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-next";
import "../../../../../components/buttons/ha-call-service-button";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import "../../../ha-config-section";
import { mdiCircle, mdiCheckCircle, mdiCloseCircle } from "@mdi/js";
import "../../../../../layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
import "@material/mwc-button/mwc-button";
import {
OZWInstance,
fetchOZWNetworkStatus,
fetchOZWNetworkStatistics,
networkOnlineStatuses,
networkOfflineStatuses,
networkStartingStatuses,
OZWNetworkStatistics,
} from "../../../../../data/ozw";
export const ozwTabs: PageNavigation[] = [];
@customElement("ozw-config-network")
class OZWConfigNetwork extends LitElement {
@property({ type: Object }) public hass!: HomeAssistant;
@property({ type: Object }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
@property({ type: Boolean }) public isWide!: boolean;
@property() public configEntryId?: string;
@property() public ozw_instance = 0;
@internalProperty() private _network?: OZWInstance;
@internalProperty() private _statistics?: OZWNetworkStatistics;
@internalProperty() private _status = "unknown";
@internalProperty() private _icon = mdiCircle;
public connectedCallback(): void {
super.connectedCallback();
if (this.ozw_instance <= 0) {
navigate(this, "/config/ozw/dashboard", true);
}
if (this.hass) {
this._fetchData();
}
}
private async _fetchData() {
this._network = await fetchOZWNetworkStatus(this.hass!, this.ozw_instance);
this._statistics = await fetchOZWNetworkStatistics(
this.hass!,
this.ozw_instance
);
if (networkOnlineStatuses.includes(this._network.Status)) {
this._status = "online";
this._icon = mdiCheckCircle;
}
if (networkStartingStatuses.includes(this._network.Status)) {
this._status = "starting";
}
if (networkOfflineStatuses.includes(this._network.Status)) {
this._status = "offline";
this._icon = mdiCloseCircle;
}
}
private _generateServiceButton(service: string) {
return html`
<ha-call-service-button
.hass=${this.hass}
domain="ozw"
service="${service}"
>
${this.hass!.localize("ui.panel.config.ozw.services." + service)}
</ha-call-service-button>
`;
}
protected render(): TemplateResult {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${ozwTabs}
>
<ha-config-section .narrow=${this.narrow} .isWide=${this.isWide}>
<div slot="header">
${this.hass.localize("ui.panel.config.ozw.network.header")}
</div>
<div slot="introduction">
${this.hass.localize("ui.panel.config.ozw.network.introduction")}
</div>
${this._network
? html`
<ha-card class="content network-status">
<div class="card-content">
<div class="details">
<ha-svg-icon
.path=${this._icon}
class="network-status-icon ${this._status}"
slot="item-icon"
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.ozw.common.network"
)}
${this.hass.localize(
"ui.panel.config.ozw.network_status." + this._status
)}
<br />
<small>
${this.hass.localize(
"ui.panel.config.ozw.network_status.details." +
this._network.Status.toLowerCase()
)}
</small>
</div>
<div class="secondary">
${this.hass.localize(
"ui.panel.config.ozw.common.ozw_instance"
)}
${this._network.ozw_instance}
${this._statistics
? html`
&bull;
${this.hass.localize(
"ui.panel.config.ozw.network.node_count",
"count",
this._statistics.node_count
)}
`
: ``}
<br />
${this.hass.localize(
"ui.panel.config.ozw.common.controller"
)}:
${this._network.getControllerPath}<br />
OZWDaemon ${this._network.OZWDaemon_Version} (OpenZWave
${this._network.OpenZWave_Version})
</div>
</div>
<div class="card-actions">
${this._generateServiceButton("add_node")}
${this._generateServiceButton("remove_node")}
</div>
</ha-card>
`
: ``}
</ha-config-section>
</hass-tabs-subpage>
`;
}
static get styles(): CSSResultArray {
return [
haStyle,
css`
.secondary {
color: var(--secondary-text-color);
}
.online {
color: green;
}
.starting {
color: orange;
}
.offline {
color: red;
}
.content {
margin-top: 24px;
}
.sectionHeader {
position: relative;
padding-right: 40px;
}
.network-status {
text-align: center;
}
.network-status div.details {
font-size: 1.5rem;
margin-bottom: 16px;
}
.network-status ha-svg-icon {
display: block;
margin: 0px auto 16px;
width: 48px;
height: 48px;
}
.network-status small {
font-size: 1rem;
}
ha-card {
margin: 0 auto;
max-width: 600px;
}
.card-actions.warning ha-call-service-button {
color: var(--error-color);
}
.toggle-help-icon {
position: absolute;
top: -6px;
right: 0;
color: var(--primary-color);
}
ha-service-description {
display: block;
color: grey;
padding: 0 8px 12px;
}
[hidden] {
display: none;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ozw-config-network": OZWConfigNetwork;
}
}

View File

@@ -1,70 +0,0 @@
import { customElement, property } from "lit-element";
import {
HassRouterPage,
RouterOptions,
} from "../../../../../layouts/hass-router-page";
import { HomeAssistant } from "../../../../../types";
import { navigate } from "../../../../../common/navigate";
@customElement("ozw-config-router")
class OZWConfigRouter extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public isWide!: boolean;
@property() public narrow!: boolean;
private _configEntry = new URLSearchParams(window.location.search).get(
"config_entry"
);
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
showLoading: true,
routes: {
dashboard: {
tag: "ozw-config-dashboard",
load: () =>
import(
/* webpackChunkName: "ozw-config-dashboard" */ "./ozw-config-dashboard"
),
},
network: {
tag: "ozw-config-network",
load: () =>
import(
/* webpackChunkName: "ozw-config-network" */ "./ozw-config-network"
),
},
},
};
protected updatePageEl(el): void {
el.route = this.routeTail;
el.hass = this.hass;
el.isWide = this.isWide;
el.narrow = this.narrow;
el.configEntryId = this._configEntry;
if (this._currentPage === "network") {
el.ozw_instance = this.routeTail.path.substr(1);
}
const searchParams = new URLSearchParams(window.location.search);
if (this._configEntry && !searchParams.has("config_entry")) {
searchParams.append("config_entry", this._configEntry);
navigate(
this,
`${this.routeTail.prefix}${
this.routeTail.path
}?${searchParams.toString()}`,
true
);
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ozw-config-router": OZWConfigRouter;
}
}

View File

@@ -1,22 +0,0 @@
import { fireEvent } from "../../../../../common/dom/fire_event";
export interface OZWRefreshNodeDialogParams {
ozw_instance: number;
node_id: number;
}
export const loadRefreshNodeDialog = () =>
import(
/* webpackChunkName: "dialog-ozw-refresh-node" */ "./dialog-ozw-refresh-node"
);
export const showOZWRefreshNodeDialog = (
element: HTMLElement,
refreshNodeDialogParams: OZWRefreshNodeDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-ozw-refresh-node",
dialogImport: loadRefreshNodeDialog,
dialogParams: refreshNodeDialogParams,
});
};

View File

@@ -10,8 +10,6 @@ import {
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import "../../../components/ha-picture-upload";
import type { HaPictureUpload } from "../../../components/ha-picture-upload";
import "../../../components/entity/ha-entities-picker";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/user/ha-user-picker";
@@ -20,17 +18,9 @@ import { PolymerChangedEvent } from "../../../polymer-types";
import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { PersonDetailDialogParams } from "./show-dialog-person-detail";
import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog";
const includeDomains = ["device_tracker"];
const cropOptions: CropOptions = {
round: true,
type: "image/jpeg",
quality: 0.75,
aspectRatio: 1,
};
class DialogPersonDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -40,8 +30,6 @@ class DialogPersonDetail extends LitElement {
@internalProperty() private _deviceTrackers!: string[];
@internalProperty() private _picture!: string | null;
@internalProperty() private _error?: string;
@internalProperty() private _params?: PersonDetailDialogParams;
@@ -62,12 +50,10 @@ class DialogPersonDetail extends LitElement {
this._name = this._params.entry.name || "";
this._userId = this._params.entry.user_id || undefined;
this._deviceTrackers = this._params.entry.device_trackers || [];
this._picture = this._params.entry.picture || null;
} else {
this._name = "";
this._userId = undefined;
this._deviceTrackers = [];
this._picture = null;
}
await this.updateComplete;
}
@@ -80,7 +66,7 @@ class DialogPersonDetail extends LitElement {
return html`
<ha-dialog
open
@closed=${this._close}
@closing=${this._close}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
@@ -106,14 +92,6 @@ class DialogPersonDetail extends LitElement {
required
auto-validate
></paper-input>
<ha-picture-upload
.hass=${this.hass}
.value=${this._picture}
crop
.cropOptions=${cropOptions}
@change=${this._pictureChanged}
></ha-picture-upload>
<ha-user-picker
label="${this.hass!.localize(
"ui.panel.config.person.detail.linked_user"
@@ -219,11 +197,6 @@ class DialogPersonDetail extends LitElement {
this._deviceTrackers = ev.detail.value;
}
private _pictureChanged(ev: PolymerChangedEvent<string | null>) {
this._error = undefined;
this._picture = (ev.target as HaPictureUpload).value;
}
private async _updateEntry() {
this._submitting = true;
try {
@@ -231,7 +204,6 @@ class DialogPersonDetail extends LitElement {
name: this._name.trim(),
device_trackers: this._deviceTrackers,
user_id: this._userId || null,
picture: this._picture,
};
if (this._params!.entry) {
await this._params!.updateEntry(values);
@@ -268,9 +240,6 @@ class DialogPersonDetail extends LitElement {
.form {
padding-bottom: 24px;
}
ha-picture-upload {
display: block;
}
ha-user-picker {
margin-top: 16px;
}

View File

@@ -1,4 +1,4 @@
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import {
css,
@@ -32,7 +32,6 @@ import {
} from "./show-dialog-person-detail";
import "../../../components/ha-svg-icon";
import { mdiPlus } from "@mdi/js";
import { styleMap } from "lit-html/directives/style-map";
class HaConfigPerson extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@@ -85,20 +84,11 @@ class HaConfigPerson extends LitElement {
<ha-card class="storage">
${this._storageItems.map((entry) => {
return html`
<paper-icon-item @click=${this._openEditEntry} .entry=${entry}>
${entry.picture
? html`<div
style=${styleMap({
backgroundImage: `url(${entry.picture})`,
})}
class="picture"
slot="item-icon"
></div>`
: ""}
<paper-item @click=${this._openEditEntry} .entry=${entry}>
<paper-item-body>
${entry.name}
</paper-item-body>
</paper-icon-item>
</paper-item>
`;
})}
${this._storageItems.length === 0
@@ -121,20 +111,11 @@ class HaConfigPerson extends LitElement {
<ha-card header="Configuration.yaml persons">
${this._configItems.map((entry) => {
return html`
<paper-icon-item>
${entry.picture
? html`<div
style=${styleMap({
backgroundImage: `url(${entry.picture})`,
})}
class="picture"
slot="item-icon"
></div>`
: ""}
<paper-item>
<paper-item-body>
${entry.name}
</paper-item-body>
</paper-icon-item>
</paper-item>
`;
})}
</ha-card>
@@ -247,21 +228,15 @@ class HaConfigPerson extends LitElement {
margin: 16px auto;
overflow: hidden;
}
.picture {
width: 40px;
height: 40px;
background-size: cover;
border-radius: 50%;
}
.empty {
text-align: center;
padding: 8px;
}
paper-icon-item {
paper-item {
padding-top: 4px;
padding-bottom: 4px;
}
ha-card.storage paper-icon-item {
ha-card.storage paper-item {
cursor: pointer;
}
`;

View File

@@ -16,7 +16,6 @@ import { HomeAssistant, Route } from "../../../types";
import "@material/mwc-button";
import "@polymer/paper-input/paper-input";
import { isServiceLoaded } from "../../../common/config/is_service_loaded";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-card";
@@ -36,20 +35,6 @@ const reloadableDomains = [
"input_number",
"input_datetime",
"input_select",
"template",
"universal",
"rest",
"command_line",
"filter",
"statistics",
"generic",
"generic_thermostat",
"homekit",
"min_max",
"history_stats",
"trend",
"ping",
"filesize",
];
@customElement("ha-config-server-control")
@@ -179,20 +164,18 @@ export class HaConfigServerControl extends LitElement {
"ui.panel.config.server_control.section.server_management.restart"
)}
</ha-call-service-button>
${!isComponentLoaded(this.hass, "hassio")
? html` <ha-call-service-button
class="warning"
.hass=${this.hass}
domain="homeassistant"
service="stop"
confirmation=${this.hass.localize(
"ui.panel.config.server_control.section.server_management.confirm_stop"
)}
>${this.hass.localize(
"ui.panel.config.server_control.section.server_management.stop"
)}
</ha-call-service-button>`
: ""}
<ha-call-service-button
class="warning"
.hass=${this.hass}
domain="homeassistant"
service="stop"
confirmation=${this.hass.localize(
"ui.panel.config.server_control.section.server_management.confirm_stop"
)}
>${this.hass.localize(
"ui.panel.config.server_control.section.server_management.stop"
)}
</ha-call-service-button>
</div>
</ha-card>
@@ -219,7 +202,7 @@ export class HaConfigServerControl extends LitElement {
</ha-call-service-button>
</div>
${reloadableDomains.map((domain) =>
isServiceLoaded(this.hass, domain, "reload")
isComponentLoaded(this.hass, domain)
? html`<div class="card-actions">
<ha-call-service-button
.hass=${this.hass}

View File

@@ -1,210 +0,0 @@
import "@material/mwc-button";
import "@polymer/paper-input/paper-input";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../common/dom/fire_event";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-formfield";
import "../../../components/ha-switch";
import "../../../components/map/ha-location-editor";
import { Tag, UpdateTagParams } from "../../../data/tag";
import { HassDialog } from "../../../dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { TagDetailDialogParams } from "./show-dialog-tag-detail";
@customElement("dialog-tag-detail")
class DialogTagDetail extends LitElement implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant;
@internalProperty() private _id?: string;
@internalProperty() private _name!: string;
@internalProperty() private _error?: string;
@internalProperty() private _params?: TagDetailDialogParams;
@internalProperty() private _submitting = false;
public showDialog(params: TagDetailDialogParams): void {
this._params = params;
this._error = undefined;
if (this._params.entry) {
this._name = this._params.entry.name || "";
} else {
this._id = "";
this._name = "";
}
}
public closeDialog(): void {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._params) {
return html``;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this._params.entry
? this._params.entry.name || this._params.entry.id
: this.hass!.localize("ui.panel.config.tags.detail.new_tag")
)}
>
<div>
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
<div class="form">
${this._params.entry
? html`${this.hass!.localize(
"ui.panel.config.tags.detail.tag_id"
)}:
${this._params.entry.id}`
: ""}
<paper-input
dialogInitialFocus
.value=${this._name}
.configValue=${"name"}
@value-changed=${this._valueChanged}
.label="${this.hass!.localize(
"ui.panel.config.tags.detail.name"
)}"
.errorMessage="${this.hass!.localize(
"ui.panel.config.tags.detail.required_error_msg"
)}"
required
auto-validate
></paper-input>
${!this._params.entry
? html` <paper-input
.value=${this._id}
.configValue=${"id"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.panel.config.tags.detail.tag_id"
)}
.placeholder=${this.hass!.localize(
"ui.panel.config.tags.detail.tag_id_placeholder"
)}
></paper-input>`
: ""}
</div>
</div>
${this._params.entry
? html`
<mwc-button
slot="secondaryAction"
class="warning"
@click="${this._deleteEntry}"
.disabled=${this._submitting}
>
${this.hass!.localize("ui.panel.config.tags.detail.delete")}
</mwc-button>
`
: html``}
<mwc-button
slot="primaryAction"
@click="${this._updateEntry}"
.disabled=${this._submitting}
>
${this._params.entry
? this.hass!.localize("ui.panel.config.tags.detail.update")
: this.hass!.localize("ui.panel.config.tags.detail.create")}
</mwc-button>
${this._params.openWrite && !this._params.entry
? html` <mwc-button
slot="primaryAction"
@click="${this._updateWriteEntry}"
.disabled=${this._submitting}
>
${this.hass!.localize(
"ui.panel.config.tags.detail.create_and_write"
)}
</mwc-button>`
: ""}
</ha-dialog>
`;
}
private _valueChanged(ev: CustomEvent) {
const configValue = (ev.target as any).configValue;
this._error = undefined;
this[`_${configValue}`] = ev.detail.value;
}
private async _updateEntry() {
this._submitting = true;
let newValue: Tag | undefined;
try {
const values: UpdateTagParams = {
name: this._name.trim(),
};
if (this._params!.entry) {
newValue = await this._params!.updateEntry!(values);
} else {
newValue = await this._params!.createEntry(values, this._id);
}
this.closeDialog();
} catch (err) {
this._error = err ? err.message : "Unknown error";
} finally {
this._submitting = false;
}
return newValue;
}
private async _updateWriteEntry() {
const openWrite = this._params?.openWrite;
const tag = await this._updateEntry();
if (!tag || !openWrite) {
return;
}
openWrite(tag);
}
private async _deleteEntry() {
this._submitting = true;
try {
if (await this._params!.removeEntry!()) {
this._params = undefined;
}
} finally {
this._submitting = false;
}
}
static get styles(): CSSResult[] {
return [
haStyleDialog,
css`
a {
color: var(--primary-color);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-tag-detail": DialogTagDetail;
}
}

View File

@@ -1,293 +0,0 @@
import "@material/mwc-fab";
import { mdiCog, mdiContentDuplicate, mdiPlus, mdiRobot } from "@mdi/js";
import {
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
} from "lit-element";
import memoizeOne from "memoize-one";
import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
import "../../../components/ha-card";
import "../../../components/ha-relative-time";
import { showAutomationEditor, TagTrigger } from "../../../data/automation";
import {
createTag,
deleteTag,
EVENT_TAG_SCANNED,
fetchTags,
Tag,
TagScannedEvent,
updateTag,
UpdateTagParams,
} from "../../../data/tag";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import { getExternalConfig } from "../../../external_app/external_config";
import "../../../layouts/hass-tabs-subpage-data-table";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config";
import { showTagDetailDialog } from "./show-dialog-tag-detail";
import "./tag-image";
export interface TagRowData extends Tag {
last_scanned_datetime: Date | null;
}
@customElement("ha-config-tags")
export class HaConfigTags extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public isWide!: boolean;
@property() public narrow!: boolean;
@property() public route!: Route;
@internalProperty() private _tags: Tag[] = [];
@internalProperty() private _canWriteTags = false;
private _columns = memoizeOne(
(
narrow: boolean,
canWriteTags: boolean,
_language
): DataTableColumnContainer => {
const columns: DataTableColumnContainer = {
icon: {
title: "",
type: "icon",
template: (_icon, tag) => html`<tag-image .tag=${tag}></tag-image>`,
},
display_name: {
title: this.hass.localize("ui.panel.config.tags.headers.name"),
sortable: true,
filterable: true,
grows: true,
template: (name, tag: any) => html`${name}
${narrow
? html`<div class="secondary">
${tag.last_scanned_datetime
? html`<ha-relative-time
.hass=${this.hass}
.datetimeObj=${tag.last_scanned_datetime}
></ha-relative-time>`
: this.hass.localize("ui.panel.config.tags.never_scanned")}
</div>`
: ""}`,
},
};
if (!narrow) {
columns.last_scanned_datetime = {
title: this.hass.localize(
"ui.panel.config.tags.headers.last_scanned"
),
sortable: true,
direction: "desc",
width: "20%",
template: (last_scanned_datetime) => html`
${last_scanned_datetime
? html`<ha-relative-time
.hass=${this.hass}
.datetimeObj=${last_scanned_datetime}
></ha-relative-time>`
: this.hass.localize("ui.panel.config.tags.never_scanned")}
`,
};
}
if (canWriteTags) {
columns.write = {
title: "",
type: "icon-button",
template: (_write, tag: any) => html` <mwc-icon-button
.tag=${tag}
@click=${(ev: Event) =>
this._openWrite((ev.currentTarget as any).tag)}
title=${this.hass.localize("ui.panel.config.tags.write")}
>
<ha-svg-icon .path=${mdiContentDuplicate}></ha-svg-icon>
</mwc-icon-button>`,
};
}
columns.automation = {
title: "",
type: "icon-button",
template: (_automation, tag: any) => html` <mwc-icon-button
.tag=${tag}
@click=${(ev: Event) =>
this._createAutomation((ev.currentTarget as any).tag)}
title=${this.hass.localize("ui.panel.config.tags.create_automation")}
>
<ha-svg-icon .path=${mdiRobot}></ha-svg-icon>
</mwc-icon-button>`,
};
columns.edit = {
title: "",
type: "icon-button",
template: (_settings, tag: any) => html` <mwc-icon-button
.tag=${tag}
@click=${(ev: Event) =>
this._openDialog((ev.currentTarget as any).tag)}
title=${this.hass.localize("ui.panel.config.tags.edit")}
>
<ha-svg-icon .path=${mdiCog}></ha-svg-icon>
</mwc-icon-button>`,
};
return columns;
}
);
private _data = memoizeOne((tags: Tag[]): TagRowData[] => {
return tags.map((tag) => {
return {
...tag,
display_name: tag.name || tag.id,
last_scanned_datetime: tag.last_scanned
? new Date(tag.last_scanned)
: null,
};
});
});
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
this._fetchTags();
if (this.hass && this.hass.auth.external) {
getExternalConfig(this.hass.auth.external).then((conf) => {
this._canWriteTags = conf.canWriteTag;
});
}
}
protected hassSubscribe() {
return [
this.hass.connection.subscribeEvents<TagScannedEvent>((ev) => {
const foundTag = this._tags.find((tag) => tag.id === ev.data.tag_id);
if (!foundTag) {
this._fetchTags();
return;
}
foundTag.last_scanned = ev.time_fired;
this._tags = [...this._tags];
}, EVENT_TAG_SCANNED),
];
}
protected render() {
return html`
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.route=${this.route}
.tabs=${configSections.experimental}
.columns=${this._columns(
this.narrow,
this._canWriteTags,
this.hass.language
)}
.data=${this._data(this._tags)}
.noDataText=${this.hass.localize("ui.panel.config.tags.no_tags")}
hasFab
>
<mwc-fab
slot="fab"
title=${this.hass.localize("ui.panel.config.tags.add_tag")}
@click=${this._addTag}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
</hass-tabs-subpage-data-table>
`;
}
private async _fetchTags() {
this._tags = await fetchTags(this.hass);
}
private _openWrite(tag: Tag) {
this.hass.auth.external!.fireMessage({
type: "tag/write",
payload: { name: tag.name || null, tag: tag.id },
});
}
private _createAutomation(tag: Tag) {
const data = {
alias: this.hass.localize(
"ui.panel.config.tags.automation_title",
"name",
tag.name || tag.id
),
trigger: [{ platform: "tag", tag_id: tag.id } as TagTrigger],
};
showAutomationEditor(this, data);
}
private _addTag() {
this._openDialog();
}
private _openDialog(entry?: Tag) {
showTagDetailDialog(this, {
entry,
openWrite: this._canWriteTags ? (tag) => this._openWrite(tag) : undefined,
createEntry: (values, tagId) => this._createTag(values, tagId),
updateEntry: entry
? (values) => this._updateTag(entry, values)
: undefined,
removeEntry: entry ? () => this._removeTag(entry) : undefined,
});
}
private async _createTag(
values: Partial<UpdateTagParams>,
tagId?: string
): Promise<Tag> {
const newTag = await createTag(this.hass, values, tagId);
this._tags = [...this._tags, newTag];
return newTag;
}
private async _updateTag(
selectedTag: Tag,
values: Partial<UpdateTagParams>
): Promise<Tag> {
const updated = await updateTag(this.hass, selectedTag.id, values);
this._tags = this._tags.map((tag) =>
tag.id === selectedTag.id ? updated : tag
);
return updated;
}
private async _removeTag(selectedTag: Tag) {
if (
!(await showConfirmationDialog(this, {
title: "Remove tag?",
text: `Are you sure you want to remove tag ${
selectedTag.name || selectedTag.id
}?`,
dismissText: this.hass!.localize("ui.common.no"),
confirmText: this.hass!.localize("ui.common.yes"),
}))
) {
return false;
}
try {
await deleteTag(this.hass, selectedTag.id);
this._tags = this._tags.filter((tag) => tag.id !== selectedTag.id);
return true;
} catch (err) {
return false;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-tags": HaConfigTags;
}
}

View File

@@ -1,27 +0,0 @@
import { fireEvent } from "../../../common/dom/fire_event";
import { Tag, UpdateTagParams } from "../../../data/tag";
export interface TagDetailDialogParams {
entry?: Tag;
openWrite?: (tag: Tag) => void;
createEntry: (
values: Partial<UpdateTagParams>,
tagId?: string
) => Promise<Tag>;
updateEntry?: (updates: Partial<UpdateTagParams>) => Promise<Tag>;
removeEntry?: () => Promise<boolean>;
}
export const loadTagDetailDialog = () =>
import(/* webpackChunkName: "dialog-tag-detail" */ "./dialog-tag-detail");
export const showTagDetailDialog = (
element: HTMLElement,
systemLogDetailParams: TagDetailDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-tag-detail",
dialogImport: loadTagDetailDialog,
dialogParams: systemLogDetailParams,
});
};

View File

@@ -1,93 +0,0 @@
import {
property,
customElement,
LitElement,
html,
CSSResult,
css,
} from "lit-element";
import "../../../components/ha-svg-icon";
import { mdiNfcVariant } from "@mdi/js";
import { TagRowData } from "./ha-config-tags";
@customElement("tag-image")
export class HaTagImage extends LitElement {
@property() public tag?: TagRowData;
private _timeout?: number;
protected updated() {
const msSinceLastScaned = this.tag?.last_scanned_datetime
? new Date().getTime() - this.tag.last_scanned_datetime.getTime()
: undefined;
if (msSinceLastScaned && msSinceLastScaned < 1000) {
if (this._timeout) {
clearTimeout(this._timeout);
this._timeout = undefined;
this.classList.remove("just-scanned");
requestAnimationFrame(() => this.classList.add("just-scanned"));
} else {
this.classList.add("just-scanned");
}
this._timeout = window.setTimeout(() => {
this.classList.remove("just-scanned");
this._timeout = undefined;
}, 10000);
} else if (!msSinceLastScaned || msSinceLastScaned > 10000) {
clearTimeout(this._timeout);
this._timeout = undefined;
this.classList.remove("just-scanned");
}
}
protected render() {
if (!this.tag) {
return html``;
}
return html`<div class="container">
<div class="image">
<ha-svg-icon .path=${mdiNfcVariant}></ha-svg-icon>
</div>
</div>`;
}
static get styles(): CSSResult {
return css`
.image {
height: 100%;
width: 100%;
background-size: cover;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
}
.container {
height: 40px;
width: 40px;
border-radius: 50%;
}
:host(.just-scanned) .container {
animation: glow 10s;
}
@keyframes glow {
0% {
box-shadow: 0px 0px 24px 0px rgba(var(--rgb-primary-color), 0);
}
10% {
box-shadow: 0px 0px 24px 0px rgba(var(--rgb-primary-color), 1);
}
100% {
box-shadow: 0px 0px 24px 0px rgba(var(--rgb-primary-color), 0);
}
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"tag-image": HaTagImage;
}
}

View File

@@ -1,21 +1,20 @@
import "@material/mwc-button";
import "@polymer/paper-input/paper-input";
import "../../../components/ha-circular-progress";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
internalProperty,
PropertyValues,
TemplateResult,
} from "lit-element";
import { computeRTLDirection } from "../../../common/util/compute_rtl";
import "../../../components/ha-circular-progress";
import "../../../components/ha-dialog";
import "../../../components/ha-formfield";
import "../../../components/ha-switch";
import "../../../components/ha-formfield";
import { createAuthForUser } from "../../../data/auth";
import {
createUser,
@@ -28,6 +27,7 @@ import { PolymerChangedEvent } from "../../../polymer-types";
import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { AddUserDialogParams } from "./show-dialog-add-user";
import { computeRTLDirection } from "../../../common/util/compute_rtl";
@customElement("dialog-add-user")
export class DialogAddUser extends LitElement {
@@ -46,8 +46,6 @@ export class DialogAddUser extends LitElement {
@internalProperty() private _password?: string;
@internalProperty() private _passwordConfirm?: string;
@internalProperty() private _isAdmin?: boolean;
public showDialog(params: AddUserDialogParams) {
@@ -55,7 +53,6 @@ export class DialogAddUser extends LitElement {
this._name = "";
this._username = "";
this._password = "";
this._passwordConfirm = "";
this._isAdmin = false;
this._error = undefined;
this._loading = false;
@@ -86,20 +83,17 @@ export class DialogAddUser extends LitElement {
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
<paper-input
class="name"
name="name"
.label=${this.hass.localize("ui.panel.config.users.add_user.name")}
.value=${this._name}
required
auto-validate
autocapitalize="on"
.errorMessage=${this.hass.localize("ui.common.error_required")}
@value-changed=${this._handleValueChanged}
@value-changed=${this._nameChanged}
@blur=${this._maybePopulateUsername}
></paper-input>
<paper-input
class="username"
name="username"
.label=${this.hass.localize(
"ui.panel.config.users.add_user.username"
)}
@@ -107,40 +101,20 @@ export class DialogAddUser extends LitElement {
required
auto-validate
autocapitalize="none"
@value-changed=${this._handleValueChanged}
@value-changed=${this._usernameChanged}
.errorMessage=${this.hass.localize("ui.common.error_required")}
></paper-input>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.users.add_user.password"
)}
type="password"
name="password"
.value=${this._password}
required
auto-validate
@value-changed=${this._handleValueChanged}
@value-changed=${this._passwordChanged}
.errorMessage=${this.hass.localize("ui.common.error_required")}
></paper-input>
<paper-input
label="${this.hass.localize(
"ui.panel.config.users.add_user.password_confirm"
)}"
name="passwordConfirm"
.value=${this._passwordConfirm}
@value-changed=${this._handleValueChanged}
required
type="password"
.invalid=${this._password !== "" &&
this._passwordConfirm !== "" &&
this._passwordConfirm !== this._password}
.errorMessage="${this.hass.localize(
"ui.panel.config.users.add_user.password_not_match"
)}"
></paper-input>
<ha-formfield
.label=${this.hass.localize("ui.panel.config.users.editor.admin")}
.dir=${computeRTLDirection(this.hass)}
@@ -173,10 +147,7 @@ export class DialogAddUser extends LitElement {
: html`
<mwc-button
slot="primaryAction"
.disabled=${!this._name ||
!this._username ||
!this._password ||
this._password !== this._passwordConfirm}
.disabled=${!this._name || !this._username || !this._password}
@click=${this._createUser}
>
${this.hass.localize("ui.panel.config.users.add_user.create")}
@@ -202,10 +173,19 @@ export class DialogAddUser extends LitElement {
}
}
private _handleValueChanged(ev: PolymerChangedEvent<string>): void {
private _nameChanged(ev: PolymerChangedEvent<string>) {
this._error = undefined;
const name = (ev.target as any).name;
this[`_${name}`] = ev.detail.value;
this._name = ev.detail.value;
}
private _usernameChanged(ev: PolymerChangedEvent<string>) {
this._error = undefined;
this._username = ev.detail.value;
}
private _passwordChanged(ev: PolymerChangedEvent<string>) {
this._error = undefined;
this._password = ev.detail.value;
}
private async _adminChanged(ev): Promise<void> {

View File

@@ -6,28 +6,23 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
internalProperty,
TemplateResult,
} from "lit-element";
import { computeRTLDirection } from "../../../common/util/compute_rtl";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-formfield";
import "../../../components/ha-switch";
import { adminChangePassword } from "../../../data/auth";
import "../../../components/ha-formfield";
import {
SYSTEM_GROUP_ID_ADMIN,
SYSTEM_GROUP_ID_USER,
} from "../../../data/user";
import {
showAlertDialog,
showPromptDialog,
} from "../../../dialogs/generic/show-dialog-box";
import { PolymerChangedEvent } from "../../../polymer-types";
import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { UserDetailDialogParams } from "./show-dialog-user-detail";
import { computeRTLDirection } from "../../../common/util/compute_rtl";
@customElement("dialog-user-detail")
class DialogUserDetail extends LitElement {
@@ -146,15 +141,7 @@ class DialogUserDetail extends LitElement {
</paper-tooltip>
`
: ""}
${!user.system_generated && this.hass.user?.is_owner
? html`<mwc-button @click=${this._changePassword}>
${this.hass.localize(
"ui.panel.config.users.editor.change_password"
)}
</mwc-button>`
: ""}
</div>
<div slot="primaryAction">
<mwc-button
@click=${this._updateEntry}
@@ -215,52 +202,6 @@ class DialogUserDetail extends LitElement {
}
}
private async _changePassword() {
const credential = this._params?.entry.credentials.find(
(cred) => cred.type === "homeassistant"
);
if (!credential) {
showAlertDialog(this, {
title: "No Home Assistant credentials found.",
});
return;
}
const newPassword = await showPromptDialog(this, {
title: this.hass.localize("ui.panel.config.users.editor.change_password"),
inputType: "password",
inputLabel: this.hass.localize(
"ui.panel.config.users.editor.new_password"
),
});
if (!newPassword) {
return;
}
const confirmPassword = await showPromptDialog(this, {
title: this.hass.localize("ui.panel.config.users.editor.change_password"),
inputType: "password",
inputLabel: this.hass.localize(
"ui.panel.config.users.add_user.password_confirm"
),
});
if (!confirmPassword) {
return;
}
if (newPassword !== confirmPassword) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.users.add_user.password_not_match"
),
});
return;
}
await adminChangePassword(this.hass, this._params!.entry.id, newPassword);
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.users.add_user.password_changed"
),
});
}
private _close(): void {
this._params = undefined;
}

View File

@@ -0,0 +1,204 @@
import "../../../components/ha-circular-progress";
import { timeOut } from "@polymer/polymer/lib/utils/async";
import { Debouncer } from "@polymer/polymer/lib/utils/debounce";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-code-editor";
import LocalizeMixin from "../../../mixins/localize-mixin";
import "../../../styles/polymer-ha-style";
class HaPanelDevTemplate extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include="ha-style iron-flex iron-positioning"></style>
<style>
:host {
-ms-user-select: initial;
-webkit-user-select: initial;
-moz-user-select: initial;
}
.content {
padding: 16px;
direction: ltr;
}
.edit-pane {
margin-right: 16px;
}
.edit-pane a {
color: var(--primary-color);
}
.horizontal .edit-pane {
max-width: 50%;
}
.render-pane {
position: relative;
max-width: 50%;
}
.render-spinner {
position: absolute;
top: 8px;
right: 8px;
}
.rendered {
@apply --paper-font-code1;
clear: both;
white-space: pre-wrap;
}
.rendered.error {
color: red;
}
</style>
<div class$="[[computeFormClasses(narrow)]]">
<div class="edit-pane">
<p>
[[localize('ui.panel.developer-tools.tabs.templates.description')]]
</p>
<ul>
<li>
<a
href="http://jinja.pocoo.org/docs/dev/templates/"
target="_blank"
rel="noreferrer"
>[[localize('ui.panel.developer-tools.tabs.templates.jinja_documentation')]]</a
>
</li>
<li>
<a
href="https://home-assistant.io/docs/configuration/templating/"
target="_blank"
rel="noreferrer"
>[[localize('ui.panel.developer-tools.tabs.templates.template_extensions')]]</a
>
</li>
</ul>
<p>[[localize('ui.panel.developer-tools.tabs.templates.editor')]]</p>
<ha-code-editor
mode="jinja2"
value="[[template]]"
error="[[error]]"
autofocus
on-value-changed="templateChanged"
></ha-code-editor>
</div>
<div class="render-pane">
<ha-circular-progress
class="render-spinner"
active="[[rendering]]"
size="small"
></ha-circular-progress>
<pre class$="[[computeRenderedClasses(error)]]">[[processed]]</pre>
</div>
</div>
`;
}
static get properties() {
return {
hass: {
type: Object,
},
error: {
type: Boolean,
value: false,
},
rendering: {
type: Boolean,
value: false,
},
template: {
type: String,
/* eslint-disable max-len */
value: `Imitate available variables:
{% set my_test_json = {
"temperature": 25,
"unit": "°C"
} %}
The temperature is {{ my_test_json.temperature }} {{ my_test_json.unit }}.
{% if is_state("device_tracker.paulus", "home") and
is_state("device_tracker.anne_therese", "home") -%}
You are both home, you silly
{%- else -%}
Anne Therese is at {{ states("device_tracker.anne_therese") }}
Paulus is at {{ states("device_tracker.paulus") }}
{%- endif %}
For loop example:
{% for state in states.sensor -%}
{%- if loop.first %}The {% elif loop.last %} and the {% else %}, the {% endif -%}
{{ state.name | lower }} is {{state.state_with_unit}}
{%- endfor %}.`,
/* eslint-enable max-len */
},
processed: {
type: String,
value: "",
},
};
}
ready() {
super.ready();
this.renderTemplate();
}
computeFormClasses(narrow) {
return narrow ? "content" : "content layout horizontal";
}
computeRenderedClasses(error) {
return error ? "error rendered" : "rendered";
}
templateChanged(ev) {
this.template = ev.detail.value;
if (this.error) {
this.error = false;
}
this._debouncer = Debouncer.debounce(
this._debouncer,
timeOut.after(500),
() => {
this.renderTemplate();
}
);
}
renderTemplate() {
this.rendering = true;
this.hass.callApi("POST", "template", { template: this.template }).then(
function (processed) {
this.processed = processed;
this.rendering = false;
}.bind(this),
function (error) {
this.processed =
(error && error.body && error.body.message) ||
this.hass.localize(
"ui.panel.developer-tools.tabs.templates.unknown_error_template"
);
this.error = true;
this.rendering = false;
}.bind(this)
);
}
}
customElements.define("developer-tools-template", HaPanelDevTemplate);

View File

@@ -1,280 +0,0 @@
import "@material/mwc-button/mwc-button";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResultArray,
customElement,
html,
internalProperty,
LitElement,
property,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { debounce } from "../../../common/util/debounce";
import "../../../components/ha-circular-progress";
import "../../../components/ha-code-editor";
import { subscribeRenderTemplate } from "../../../data/ws-templates";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
const DEMO_TEMPLATE = `{## Imitate available variables: ##}
{% set my_test_json = {
"temperature": 25,
"unit": "°C"
} %}
The temperature is {{ my_test_json.temperature }} {{ my_test_json.unit }}.
{% if is_state("sun.sun", "above_horizon") -%}
The sun rose {{ relative_time(states.sun.sun.last_changed) }} ago.
{%- else -%}
The sun will rise at {{ as_timestamp(strptime(state_attr("sun.sun", "next_rising"), "")) | timestamp_local }}.
{%- endif %}
For loop example getting 3 entity values:
{% for states in states | slice(3) -%}
{% set state = states | first %}
{%- if loop.first %}The {% elif loop.last %} and the {% else %}, the {% endif -%}
{{ state.name | lower }} is {{state.state_with_unit}}
{%- endfor %}.`;
@customElement("developer-tools-template")
class HaPanelDevTemplate extends LitElement {
@property() public hass!: HomeAssistant;
@property() public narrow!: boolean;
@internalProperty() private _error = false;
@internalProperty() private _rendering = false;
@internalProperty() private _processed = "";
@internalProperty() private _unsubRenderTemplate?: Promise<UnsubscribeFunc>;
private _template = "";
private _inited = false;
public connectedCallback() {
super.connectedCallback();
if (this._template && !this._unsubRenderTemplate) {
this._subscribeTemplate();
}
}
public disconnectedCallback() {
this._unsubscribeTemplate();
}
protected firstUpdated() {
if (localStorage && localStorage["panel-dev-template-template"]) {
this._template = localStorage["panel-dev-template-template"];
} else {
this._template = DEMO_TEMPLATE;
}
this._subscribeTemplate();
this._inited = true;
}
protected render() {
return html`
<div
class="content ${classMap({
layout: !this.narrow,
horizontal: !this.narrow,
})}"
>
<div class="edit-pane">
<p>
${this.hass.localize(
"ui.panel.developer-tools.tabs.templates.description"
)}
</p>
<ul>
<li>
<a
href="http://jinja.pocoo.org/docs/dev/templates/"
target="_blank"
rel="noreferrer"
>${this.hass.localize(
"ui.panel.developer-tools.tabs.templates.jinja_documentation"
)}
</a>
</li>
<li>
<a
href="https://home-assistant.io/docs/configuration/templating/"
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.developer-tools.tabs.templates.template_extensions"
)}</a
>
</li>
</ul>
<p>
${this.hass.localize(
"ui.panel.developer-tools.tabs.templates.editor"
)}
</p>
<ha-code-editor
mode="jinja2"
.value=${this._template}
.error=${this._error}
autofocus
@value-changed=${this._templateChanged}
></ha-code-editor>
<mwc-button @click=${this._restoreDemo}>
${this.hass.localize(
"ui.panel.developer-tools.tabs.templates.reset"
)}
</mwc-button>
</div>
<div class="render-pane">
<ha-circular-progress
class="render-spinner"
.active=${this._rendering}
size="small"
></ha-circular-progress>
<pre class="rendered ${classMap({ error: this._error })}">
${this._processed}</pre
>
</div>
</div>
`;
}
static get styles(): CSSResultArray {
return [
haStyle,
css`
:host {
-ms-user-select: initial;
-webkit-user-select: initial;
-moz-user-select: initial;
}
.content {
padding: 16px;
direction: ltr;
}
.edit-pane {
margin-right: 16px;
}
.edit-pane a {
color: var(--primary-color);
}
.horizontal .edit-pane {
max-width: 50%;
}
.render-pane {
position: relative;
max-width: 50%;
}
.render-spinner {
position: absolute;
top: 8px;
right: 8px;
}
.rendered {
@apply --paper-font-code1;
clear: both;
white-space: pre-wrap;
}
.rendered.error {
color: var(--error-color);
}
`,
];
}
private _debounceRender = debounce(
() => {
this._subscribeTemplate();
this._storeTemplate();
},
500,
false
);
private _templateChanged(ev) {
this._template = ev.detail.value;
if (this._error) {
this._error = false;
}
this._debounceRender();
}
private async _subscribeTemplate() {
this._rendering = true;
await this._unsubscribeTemplate();
try {
this._unsubRenderTemplate = subscribeRenderTemplate(
this.hass.connection,
(result) => {
this._processed = result;
},
{
template: this._template,
}
);
await this._unsubRenderTemplate;
} catch (err) {
this._error = true;
if (err.message) {
this._processed = err.message;
}
this._unsubRenderTemplate = undefined;
} finally {
this._rendering = false;
}
}
private async _unsubscribeTemplate(): Promise<void> {
if (!this._unsubRenderTemplate) {
return;
}
try {
const unsub = await this._unsubRenderTemplate;
unsub();
this._unsubRenderTemplate = undefined;
} catch (e) {
if (e.code === "not_found") {
// If we get here, the connection was probably already closed. Ignore.
} else {
throw e;
}
}
}
private _storeTemplate() {
if (!this._inited) {
return;
}
localStorage["panel-dev-template-template"] = this._template;
}
private _restoreDemo() {
this._template = DEMO_TEMPLATE;
this._subscribeTemplate();
delete localStorage["panel-dev-template-template"];
}
}
declare global {
interface HTMLElementTagNameMap {
"developer-tools-template": HaPanelDevTemplate;
}
}

View File

@@ -118,26 +118,6 @@ class HaLogbook extends LitElement {
? ` (${item_username})`
: ``}</span
>
${!item.context_event_type
? ""
: item.context_event_type === "call_service"
? // Service Call
html` by service ${item.context_domain}.${item.context_service}`
: item.context_entity_id === item.entity_id
? // HomeKit or something that self references
html` by
${item.context_name
? item.context_name
: item.context_event_type}`
: // Another entity such as an automation or script
html` by
<a
href="#"
@click=${this._entityClicked}
.entityId=${item.context_entity_id}
class="name"
>${item.context_entity_id_name}</a
>`}
</div>
</div>
</div>

View File

@@ -1,213 +0,0 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
PropertyValues,
TemplateResult,
internalProperty,
} from "lit-element";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import "../../calendar/ha-full-calendar";
import type {
HomeAssistant,
CalendarEvent,
Calendar,
CalendarViewChanged,
FullCalendarView,
} from "../../../types";
import type { LovelaceCard, LovelaceCardEditor } from "../types";
import type { CalendarCardConfig } from "./types";
import { findEntities } from "../common/find-entites";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import "../components/hui-warning";
import { fetchCalendarEvents } from "../../../data/calendar";
import { HASSDomEvent } from "../../../common/dom/fire_event";
import { HA_COLOR_PALETTE } from "../../../common/const";
import { debounce } from "../../../common/util/debounce";
import { installResizeObserver } from "../common/install-resize-observer";
@customElement("hui-calendar-card")
export class HuiCalendarCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(
/* webpackChunkName: "hui-calendar-card-editor" */ "../editor/config-elements/hui-calendar-card-editor"
);
return document.createElement("hui-calendar-card-editor");
}
public static getStubConfig(
hass: HomeAssistant,
entities: string[],
entitiesFill: string[]
) {
const includeDomains = ["calendar"];
const maxEntities = 2;
const foundEntities = findEntities(
hass,
maxEntities,
entities,
entitiesFill,
includeDomains
);
return {
entities: foundEntities,
};
}
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public _events: CalendarEvent[] = [];
@internalProperty() private _config?: CalendarCardConfig;
@internalProperty() private _calendars: Calendar[] = [];
@internalProperty() private _narrow = false;
@internalProperty() private _veryNarrow = false;
private _resizeObserver?: ResizeObserver;
public setConfig(config: CalendarCardConfig): void {
if (!config.entities) {
throw new Error("Entities must be defined");
}
if (config.entities && !Array.isArray(config.entities)) {
throw new Error("Entities need to be an array");
}
this._calendars = config!.entities.map((entity, idx) => {
return {
entity_id: entity,
backgroundColor: `#${HA_COLOR_PALETTE[idx % HA_COLOR_PALETTE.length]}`,
};
});
this._config = config;
}
public getCardSize(): number {
return 4;
}
public connectedCallback(): void {
super.connectedCallback();
this.updateComplete.then(() => this._attachObserver());
}
public disconnectedCallback(): void {
if (this._resizeObserver) {
this._resizeObserver.disconnect();
}
}
protected render(): TemplateResult {
if (!this._config || !this.hass || !this._calendars.length) {
return html``;
}
const views: FullCalendarView[] = this._veryNarrow
? ["listWeek"]
: ["listWeek", "dayGridMonth", "dayGridDay"];
return html`
<ha-card>
<div class="header">${this._config.title}</div>
<ha-full-calendar
.narrow=${this._narrow}
.events=${this._events}
.hass=${this.hass}
.views=${views}
@view-changed=${this._handleViewChanged}
></ha-full-calendar>
</ha-card>
`;
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (!this._config || !this.hass) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const oldConfig = changedProps.get("_config") as
| CalendarCardConfig
| undefined;
if (
!oldHass ||
!oldConfig ||
(changedProps.has("hass") && oldHass.themes !== this.hass.themes) ||
(changedProps.has("_config") && oldConfig.theme !== this._config.theme)
) {
applyThemesOnElement(this, this.hass.themes, this._config!.theme);
}
}
private async _handleViewChanged(
ev: HASSDomEvent<CalendarViewChanged>
): Promise<void> {
this._events = await fetchCalendarEvents(
this.hass!,
ev.detail.start,
ev.detail.end,
this._calendars
);
}
private _measureCard() {
const card = this.shadowRoot!.querySelector("ha-card");
if (!card) {
return;
}
this._narrow = card.offsetWidth < 870;
this._veryNarrow = card.offsetWidth < 350;
}
private async _attachObserver(): Promise<void> {
if (!this._resizeObserver) {
await installResizeObserver();
this._resizeObserver = new ResizeObserver(
debounce(() => this._measureCard(), 250, false)
);
}
const card = this.shadowRoot!.querySelector("ha-card");
// If we show an error or warning there is no ha-card
if (!card) {
return;
}
this._resizeObserver.observe(card);
}
static get styles(): CSSResult {
return css`
ha-card {
position: relative;
padding: 0 8px 8px;
}
.header {
color: var(--ha-card-header-color, --primary-text-color);
font-size: var(--ha-card-header-font-size, 24px);
line-height: 1.2;
padding-top: 16px;
padding-left: 8px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-calendar-card": HuiCalendarCard;
}
}

View File

@@ -19,8 +19,6 @@ import { processConfigEntities } from "../common/process-config-entities";
import { EntityConfig } from "../entity-rows/types";
import { LovelaceCard } from "../types";
import { HistoryGraphCardConfig } from "./types";
import { HistoryResult } from "../../../data/history";
import { hasConfigOrEntitiesChanged } from "../common/has-changed";
@customElement("hui-history-graph-card")
export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
@@ -51,7 +49,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
@property({ attribute: false }) public hass?: HomeAssistant;
@internalProperty() private _stateHistory?: HistoryResult;
@internalProperty() private _stateHistory?: any;
@internalProperty() private _config?: HistoryGraphCardConfig;
@@ -61,9 +59,9 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
private _cacheConfig?: CacheConfig;
private _fetching = false;
private _interval?: number;
private _date?: Date;
private _fetching = false;
public getCardSize(): number {
return 4;
@@ -99,8 +97,9 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
};
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
return hasConfigOrEntitiesChanged(this, changedProps);
public disconnectedCallback(): void {
super.disconnectedCallback();
this._clearInterval();
}
protected updated(changedProps: PropertyValues) {
@@ -109,19 +108,21 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
return;
}
if (!changedProps.has("_config") && !changedProps.has("hass")) {
if (!changedProps.has("_config")) {
return;
}
const oldConfig = changedProps.get("_config") as HistoryGraphCardConfig;
if (changedProps.has("_config") && oldConfig !== this._config) {
this._getStateHistory();
} else if (
this._cacheConfig.refresh &&
Date.now() - this._date!.getTime() >= this._cacheConfig.refresh * 100
) {
if (oldConfig !== this._config) {
this._getStateHistory();
this._clearInterval();
if (!this._interval && this._cacheConfig.refresh) {
this._interval = window.setInterval(() => {
this._getStateHistory();
}, this._cacheConfig.refresh * 1000);
}
}
}
@@ -154,23 +155,27 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
if (this._fetching) {
return;
}
this._date = new Date();
this._fetching = true;
try {
this._stateHistory = {
...(await getRecentWithCache(
this.hass!,
this._cacheConfig!.cacheKey,
this._cacheConfig!,
this.hass!.localize,
this.hass!.language
)),
};
this._stateHistory = await getRecentWithCache(
this.hass!,
this._cacheConfig!.cacheKey,
this._cacheConfig!,
this.hass!.localize,
this.hass!.language
);
} finally {
this._fetching = false;
}
}
private _clearInterval(): void {
if (this._interval) {
window.clearInterval(this._interval);
this._interval = undefined;
}
}
static get styles(): CSSResult {
return css`
.content {

View File

@@ -1,3 +1,4 @@
import "../../../components/ha-icon-button";
import "@polymer/paper-progress/paper-progress";
import type { PaperProgressElement } from "@polymer/paper-progress/paper-progress";
import {
@@ -5,9 +6,9 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
internalProperty,
PropertyValues,
query,
TemplateResult,
@@ -24,17 +25,12 @@ import { supportsFeature } from "../../../common/entity/supports-feature";
import { debounce } from "../../../common/util/debounce";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog";
import { UNAVAILABLE_STATES } from "../../../data/entity";
import {
computeMediaDescription,
CONTRAST_RATIO,
ControlButton,
getCurrentProgress,
MediaPickedEvent,
SUPPORTS_PLAY,
SUPPORT_BROWSE_MEDIA,
SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE,
SUPPORT_PREVIOUS_TRACK,
@@ -47,11 +43,11 @@ import type { HomeAssistant, MediaEntity } from "../../../types";
import { contrast } from "../common/color/contrast";
import { findEntities } from "../common/find-entites";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { installResizeObserver } from "../common/install-resize-observer";
import "../components/hui-marquee";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import type { LovelaceCard, LovelaceCardEditor } from "../types";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import { MediaControlCardConfig } from "./types";
import { installResizeObserver } from "../common/install-resize-observer";
function getContrastRatio(
rgb1: [number, number, number],
@@ -161,6 +157,11 @@ const customGenerator = (colors: Swatch[]) => {
return [foregroundColor, backgroundColor.hex];
};
interface ControlButton {
icon: string;
action: string;
}
@customElement("hui-media-control-card")
export class HuiMediaControlCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
@@ -188,7 +189,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
return { type: "media-control", entity: foundEntities[0] || "" };
}
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public hass?: HomeAssistant;
@internalProperty() private _config?: MediaControlCardConfig;
@@ -392,27 +393,12 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
${controls!.map(
(control) => html`
<ha-icon-button
.title=${this.hass.localize(
`ui.card.media_player.${control.action}`
)}
.icon=${control.icon}
action=${control.action}
@click=${this._handleClick}
></ha-icon-button>
`
)}
${supportsFeature(stateObj, SUPPORT_BROWSE_MEDIA)
? html`
<ha-icon-button
class="browse-media"
icon="hass:folder-multiple"
.title=${this.hass.localize(
"ui.card.media_player.browse_media"
)}
@click=${this._handleBrowseMedia}
></ha-icon-button>
`
: ""}
</div>
`}
</div>
@@ -661,31 +647,14 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
});
}
private _handleBrowseMedia(): void {
showMediaBrowserDialog(this, {
action: "play",
entityId: this._config!.entity,
mediaPickedCallback: (pickedMedia: MediaPickedEvent) =>
this._playMedia(
pickedMedia.media_content_id,
pickedMedia.media_content_type
),
});
}
private _playMedia(media_content_id: string, media_content_type: string) {
this.hass!.callService("media_player", "play_media", {
entity_id: this._config!.entity,
media_content_id,
media_content_type,
});
}
private _handleClick(e: MouseEvent): void {
const action = (e.currentTarget! as HTMLElement).getAttribute("action")!;
this.hass!.callService("media_player", action, {
entity_id: this._config!.entity,
});
this.hass!.callService(
"media_player",
(e.currentTarget! as HTMLElement).getAttribute("action")!,
{
entity_id: this._config!.entity,
}
);
}
private _updateProgressBar(): void {
@@ -866,12 +835,6 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
--mdc-icon-size: 40px;
}
ha-icon-button.browse-media {
position: absolute;
right: 0;
--mdc-icon-size: 24px;
}
.top-info {
display: flex;
justify-content: space-between;
@@ -941,10 +904,6 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
--mdc-icon-size: 36px;
}
.narrow ha-icon-button.browse-media {
--mdc-icon-size: 24px;
}
.no-progress.player:not(.no-controls) {
padding-bottom: 0px;
}

View File

@@ -436,7 +436,6 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
.forecast-image-icon > * {
width: 40px;
height: 40px;
--mdc-icon-size: 40px;
}
.forecast-icon {
@@ -470,7 +469,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
width: 52px;
}
:host([narrow]) .icon-image .weather-icon {
:host([narrow]) .weather-icon {
--mdc-icon-size: 52px;
}

View File

@@ -12,12 +12,6 @@ export interface AlarmPanelCardConfig extends LovelaceCardConfig {
theme?: string;
}
export interface CalendarCardConfig extends LovelaceCardConfig {
entities: string[];
title?: string;
theme?: string;
}
export interface ConditionalCardConfig extends LovelaceCardConfig {
card: LovelaceCardConfig;
conditions: Condition[];

View File

@@ -1,8 +1,11 @@
import { PropertyValues } from "lit-element";
import { HomeAssistant } from "../../../types";
import { processConfigEntities } from "./process-config-entities";
function hasConfigChanged(element: any, changedProps: PropertyValues): boolean {
// Check if config or Entity changed
export function hasConfigOrEntityChanged(
element: any,
changedProps: PropertyValues
): boolean {
if (changedProps.has("_config")) {
return true;
}
@@ -20,41 +23,9 @@ function hasConfigChanged(element: any, changedProps: PropertyValues): boolean {
) {
return true;
}
return false;
}
// Check if config or Entity changed
export function hasConfigOrEntityChanged(
element: any,
changedProps: PropertyValues
): boolean {
if (hasConfigChanged(element, changedProps)) {
return true;
}
const oldHass = changedProps.get("hass") as HomeAssistant;
return (
oldHass.states[element._config!.entity] !==
element.hass!.states[element._config!.entity]
);
}
// Check if config or Entities changed
export function hasConfigOrEntitiesChanged(
element: any,
changedProps: PropertyValues
): boolean {
if (hasConfigChanged(element, changedProps)) {
return true;
}
const oldHass = changedProps.get("hass") as HomeAssistant;
const entities = processConfigEntities(element._config!.entities);
return entities.some(
(entity) =>
oldHass.states[entity.entity] !== element.hass!.states[entity.entity]
);
}

View File

@@ -1,6 +1,5 @@
import { LovelaceCardConfig } from "../../../data/lovelace";
import "../cards/hui-button-card";
import "../cards/hui-calendar-card";
import "../cards/hui-entities-card";
import "../cards/hui-entity-button-card";
import "../cards/hui-entity-card";
@@ -53,7 +52,6 @@ const LAZY_LOAD_TYPES = {
map: () => import("../cards/hui-map-card"),
markdown: () => import("../cards/hui-markdown-card"),
picture: () => import("../cards/hui-picture-card"),
calendar: () => import("../cards/hui-calendar-card"),
};
// This will not return an error card but will throw the error

View File

@@ -1,133 +0,0 @@
import {
customElement,
html,
LitElement,
property,
TemplateResult,
internalProperty,
} from "lit-element";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { HomeAssistant } from "../../../../types";
import type { CalendarCardConfig } from "../../cards/types";
import "../../components/hui-entity-editor";
import "../../../../components/entity/ha-entities-picker";
import "../../components/hui-theme-select-editor";
import type { LovelaceCardEditor } from "../../types";
import type { EditorTarget, EntitiesEditorEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
import {
string,
optional,
object,
boolean,
array,
union,
assert,
} from "superstruct";
const cardConfigStruct = object({
type: string(),
title: optional(union([string(), boolean()])),
theme: optional(string()),
entities: array(string()),
});
@customElement("hui-calendar-card-editor")
export class HuiCalendarCardEditor extends LitElement
implements LovelaceCardEditor {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) private _config?: CalendarCardConfig;
@internalProperty() private _configEntities?: string[];
public setConfig(config: CalendarCardConfig): void {
assert(config, cardConfigStruct);
this._config = config;
this._configEntities = config.entities;
}
get _title(): string {
return this._config!.title || "";
}
get _theme(): string {
return this._config!.theme || "";
}
protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
}
return html`
${configElementStyle}
<div class="card-config">
<div class="side-by-side">
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.title"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._title}
.configValue=${"title"}
@value-changed=${this._valueChanged}
></paper-input>
<hui-theme-select-editor
.hass=${this.hass}
.value=${this._theme}
.configValue=${"theme"}
@value-changed=${this._valueChanged}
></hui-theme-select-editor>
</div>
</div>
<h3>
${"Calendar Entities" +
" (" +
this.hass!.localize("ui.panel.lovelace.editor.card.config.required") +
")"}
</h3>
<ha-entities-picker
.hass=${this.hass!}
.value=${this._configEntities}
.includeDomains=${["calendar"]}
@value-changed=${this._valueChanged}
>
</ha-entities-picker>
`;
}
private _valueChanged(ev: EntitiesEditorEvent | CustomEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (this[`_${target.configValue}`] === target.value) {
return;
}
if (ev.detail && ev.detail.value && Array.isArray(ev.detail.value)) {
this._config = { ...this._config, entities: ev.detail.value };
} else if (target.configValue) {
if (target.value === "") {
delete this._config[target.configValue!];
} else {
this._config = {
...this._config,
[target.configValue]: target.value,
};
}
}
fireEvent(this, "config-changed", { config: this._config });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-calendar-card-editor": HuiCalendarCardEditor;
}
}

View File

@@ -1,5 +1,6 @@
import "@material/mwc-button";
import "@material/mwc-icon-button/mwc-icon-button";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "../../../components/ha-circular-progress";
import {
css,
CSSResult,
@@ -8,28 +9,25 @@ import {
LitElement,
property,
internalProperty,
query,
TemplateResult,
} from "lit-element";
import { mdiHelpCircle } from "@mdi/js";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/dialog/ha-paper-dialog";
import type { HaPaperDialog } from "../../../components/dialog/ha-paper-dialog";
import "../../../components/ha-switch";
import "../../../components/ha-formfield";
import "../../../components/ha-yaml-editor";
import type { PolymerChangedEvent } from "../../../polymer-types";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import type { SaveDialogParams } from "./show-save-config-dialog";
import { computeRTLDirection } from "../../../common/util/compute_rtl";
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
import "../../../components/ha-switch";
import "../../../components/ha-formfield";
import "../../../components/ha-yaml-editor";
import "../../../components/ha-svg-icon";
import "../../../components/ha-dialog";
import "../../../components/ha-circular-progress";
const EMPTY_CONFIG = { views: [] };
const coreDocumentationURLBase = "https://www.home-assistant.io/lovelace/";
@customElement("hui-dialog-save-config")
export class HuiSaveConfig extends LitElement implements HassDialog {
export class HuiSaveConfig extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@internalProperty() private _params?: SaveDialogParams;
@@ -38,20 +36,18 @@ export class HuiSaveConfig extends LitElement implements HassDialog {
@internalProperty() private _saving: boolean;
@query("ha-paper-dialog") private _dialog?: HaPaperDialog;
public constructor() {
super();
this._saving = false;
}
public showDialog(params: SaveDialogParams): void {
public async showDialog(params: SaveDialogParams): Promise<void> {
this._params = params;
this._emptyConfig = false;
}
public closeDialog(): boolean {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
return true;
await this.updateComplete;
this._dialog!.open();
}
protected render(): TemplateResult {
@@ -59,27 +55,15 @@ export class HuiSaveConfig extends LitElement implements HassDialog {
return html``;
}
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
@closed=${this._close}
.heading=${html`${this.hass!.localize(
"ui.panel.lovelace.editor.save_config.header"
)}<a
class="header_button"
href=${coreDocumentationURLBase}
title=${this.hass!.localize("ui.panel.lovelace.menu.help")}
target="_blank"
rel="noreferrer"
dir=${computeRTLDirection(this.hass!)}
>
<mwc-icon-button>
<ha-svg-icon path=${mdiHelpCircle}></ha-svg-icon>
</mwc-icon-button>
</a>`}
<ha-paper-dialog
with-backdrop
opened
@opened-changed=${this._openedChanged}
>
<div>
<h2>
${this.hass!.localize("ui.panel.lovelace.editor.save_config.header")}
</h2>
<paper-dialog-scrollable>
<p>
${this.hass!.localize("ui.panel.lovelace.editor.save_config.para")}
</p>
@@ -121,46 +105,55 @@ export class HuiSaveConfig extends LitElement implements HassDialog {
</p>
<ha-yaml-editor
.defaultValue=${this._params!.lovelace.config}
@editor-refreshed=${this._editorRefreshed}
></ha-yaml-editor>
`}
</paper-dialog-scrollable>
<div class="paper-dialog-buttons">
${this._params.mode === "storage"
? html`
<mwc-button @click="${this._closeDialog}"
>${this.hass!.localize(
"ui.panel.lovelace.editor.save_config.cancel"
)}
</mwc-button>
<mwc-button
?disabled="${this._saving}"
@click="${this._saveConfig}"
>
<ha-circular-progress
?active="${this._saving}"
alt="Saving"
></ha-circular-progress>
${this.hass!.localize(
"ui.panel.lovelace.editor.save_config.save"
)}
</mwc-button>
`
: html`
<mwc-button @click=${this._closeDialog}
>${this.hass!.localize(
"ui.panel.lovelace.editor.save_config.close"
)}
</mwc-button>
`}
</div>
${this._params.mode === "storage"
? html`
<mwc-button slot="primaryAction" @click=${this.closeDialog}
>${this.hass!.localize(
"ui.panel.lovelace.editor.save_config.cancel"
)}
</mwc-button>
<mwc-button
slot="primaryAction"
?disabled=${this._saving}
@click=${this._saveConfig}
>
<ha-circular-progress
?active=${this._saving}
alt="Saving"
></ha-circular-progress>
${this.hass!.localize(
"ui.panel.lovelace.editor.save_config.save"
)}
</mwc-button>
`
: html`
<mwc-button slot="primaryAction" @click=${this.closeDialog}
>${this.hass!.localize(
"ui.panel.lovelace.editor.save_config.close"
)}
</mwc-button>
`}
</ha-dialog>
</ha-paper-dialog>
`;
}
private _close(ev?: Event) {
if (ev) {
ev.stopPropagation();
private _closeDialog(): void {
this._dialog!.close();
}
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
if (!ev.detail.value) {
this._params = undefined;
}
this.closeDialog();
}
private _editorRefreshed() {
fireEvent(this._dialog! as HTMLElement, "iron-resize");
}
private _emptyConfigChanged(ev) {
@@ -179,7 +172,7 @@ export class HuiSaveConfig extends LitElement implements HassDialog {
);
lovelace.setEditMode(true);
this._saving = false;
this.closeDialog();
this._closeDialog();
} catch (err) {
alert(`Saving failed: ${err.message}`);
this._saving = false;

View File

@@ -9,10 +9,6 @@ export const coreCards: Card[] = [
type: "button",
showElement: true,
},
{
type: "calendar",
showElement: true,
},
{
type: "entities",
showElement: true,

View File

@@ -17,7 +17,7 @@ export interface EntityFilterEntityConfig extends EntityConfig {
}
export interface DividerConfig {
type: "divider";
style: { [key: string]: string };
style: string;
}
export interface SectionConfig {
type: "section";

View File

@@ -40,7 +40,7 @@ export const buttonsHeaderFooterConfigStruct = object({
export const graphHeaderFooterConfigStruct = object({
type: string(),
entity: string(),
detail: optional(number()),
detail: optional(string()),
hours_to_show: optional(number()),
});

Some files were not shown because too many files have changed in this diff Show More