Compare commits

...

44 Commits

Author SHA1 Message Date
Ludeeus
a0b11eb357 Move partial backup logic to backend 2021-12-16 12:36:02 +00:00
J. Nick Koston
6f9b2ee569 Add hardware version to the device info card (#10914) 2021-12-16 05:16:23 -06:00
Bram Kragten
4ebdca2a46 Bumped version to 20211215.0 2021-12-15 13:36:34 +01:00
Philip Allgaier
fc700fdaf0 Outline new collapsable area in state dev tools + auto-expand (#10917) 2021-12-15 13:15:50 +01:00
Philip Allgaier
d8e12f4280 Add tooltips and aria-labels to media player buttons (#10881) 2021-12-13 16:33:34 -08:00
krazos
86114758c3 Add group to input row domains to fix mobile focus issue (#10897) 2021-12-13 16:30:21 -08:00
Joakim Sørensen
792278cf17 Hide stop for hassio (#10905)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-12-13 16:29:01 -08:00
Joakim Sørensen
b8832f2121 Change entrypoint for Settings (#10904) 2021-12-13 16:08:19 -08:00
Joakim Sørensen
76339c90f7 Show app configuration in sidebar for non-admin users (#10890) 2021-12-13 16:06:46 -08:00
Bram Kragten
b3d4451035 Not valid config, but we support it in the editor (#10893) 2021-12-13 11:01:41 -08:00
Joakim Sørensen
dc58481918 Fix overriding username suggestion (#10899) 2021-12-13 18:56:44 +01:00
Philip Allgaier
14af735507 Fix tooltip and aria-label for password input field (#10898)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-12-13 16:32:45 +00:00
Joakim Sørensen
a7b558b64a Add no update available message (#10891) 2021-12-13 17:20:38 +01:00
Joakim Sørensen
b7665bef6f Don't backup core for supervisor/os updates (#10886) 2021-12-13 10:53:02 +01:00
Christopher Toth
5ec37a35f1 Fix all instances where HTML ARIA-ROLE should actually just be role (#10888) 2021-12-13 08:35:46 +00:00
Philip Allgaier
91bb2ddcc4 Make energy graph colors brighter in dark mode (#10789) 2021-12-12 14:10:30 +01:00
Bram Kragten
85168b3a35 Bumped version to 20211212.0 2021-12-12 13:37:28 +01:00
Bram Kragten
942150cda2 Remove milliseconds from state trigger when 0 (#10879) 2021-12-12 12:27:14 +00:00
Philip Allgaier
2606d55895 Add tooltips and aria-labels to climate modes (#10875) 2021-12-12 13:25:05 +01:00
Philip Allgaier
1f671198aa Fix tooltip and aria-label for ZWave JS log download (#10876) 2021-12-12 13:24:24 +01:00
Bram Kragten
deb65e7108 Fix button with images (#10872) 2021-12-12 13:19:32 +01:00
Philip Allgaier
cd00f7f874 Fix typo in cover close tilt translation key (#10871) 2021-12-11 20:59:42 +01:00
Bram Kragten
2b0359edba Bumped version to 20211211.0 2021-12-11 17:15:38 +01:00
Philip Allgaier
35e9687170 Replace mwc-icon-button with ha-icon-button in automation picker (#10858) 2021-12-11 17:15:16 +01:00
Bram Kragten
b730676914 Fix translations cover controls (#10868) 2021-12-11 17:13:43 +01:00
Bram Kragten
2890192c05 Fix formfield label touch (#10867) 2021-12-11 17:13:24 +01:00
Bram Kragten
bfb84a834f Still have manual input if camera is not supported (#10849)
* Still have manual input if camera is not supported

* Adjust & fix
2021-12-11 17:12:41 +01:00
Philip Allgaier
ca6fd6c770 Prevent quickbar command entry duplicates (#10861) 2021-12-11 17:01:24 +01:00
Joakim Sørensen
585648ac4c Revert "handle ha-radio and ha-checkbox in ha-formfield" (#10863) 2021-12-10 23:30:35 -08:00
Matthias de Baat
bec5c564b6 Update blueprint description (#10854) 2021-12-10 11:18:05 -08:00
Erik Montnemery
48c66e6349 Tweak some energy related translation strings (#10852) 2021-12-10 09:49:53 -08:00
Bram Kragten
cea40610c0 Add base trigger to struct (#10851) 2021-12-10 14:44:40 +01:00
Bram Kragten
0c3fd8f3ad typo login -> log in (#10850) 2021-12-10 14:41:09 +01:00
Paulus Schoutsen
02bdeebc82 Bumped version to 20211209.0 2021-12-09 13:39:03 -08:00
Bram Kragten
60c7669d8f Put set state in expansion panel (#10845) 2021-12-09 13:38:27 -08:00
Bram Kragten
919bf94a03 Only add milliseconds when enabled or if it has a value (#10842) 2021-12-09 13:38:03 -08:00
Bram Kragten
ead5e288eb Use normal card color in narrow config screen too (#10843) 2021-12-09 13:37:45 -08:00
Bram Kragten
add8a702cc Change select camera UI, remove manual QR input (#10844) 2021-12-09 13:37:30 -08:00
Paulus Schoutsen
39774c0e02 Allow trigger reconnect from external bus (#10819) 2021-12-09 13:30:20 -08:00
Joakim Sørensen
149f381bc3 Make dashboard entries translatable (#10831) 2021-12-09 09:59:27 -08:00
Bram Kragten
faccb12430 Fix keep me logged in (#10835) 2021-12-09 09:57:11 -08:00
Paulus Schoutsen
7039bae9be Disable local only option for system generated users (#10827) 2021-12-09 11:22:32 +01:00
Bram Kragten
0a7b703d57 Clear warnings when yaml changes (#10820) 2021-12-08 09:12:09 +01:00
Joakim Sørensen
24e8028e8f Use _version_latest for change log URL (#10821) 2021-12-07 23:37:06 +01:00
57 changed files with 786 additions and 396 deletions

View File

@@ -206,6 +206,7 @@ const createDeviceRegistryEntries = (
model: "Mock Device", model: "Mock Device",
name: "Tag Reader", name: "Tag Reader",
sw_version: null, sw_version: null,
hw_version: "1.0.0",
id: "mock-device-id", id: "mock-device-id",
identifiers: [], identifiers: [],
via_device_id: null, via_device_id: null,

View File

@@ -29,10 +29,6 @@ import {
HassioAddonDetails, HassioAddonDetails,
updateHassioAddon, updateHassioAddon,
} from "../../../src/data/hassio/addon"; } from "../../../src/data/hassio/addon";
import {
createHassioPartialBackup,
HassioPartialBackupCreateParams,
} from "../../../src/data/hassio/backup";
import { import {
extractApiErrorMessage, extractApiErrorMessage,
ignoreSupervisorError, ignoreSupervisorError,
@@ -103,7 +99,7 @@ class UpdateAvailableCard extends LitElement {
@state() private _addonInfo?: HassioAddonDetails; @state() private _addonInfo?: HassioAddonDetails;
@state() private _action: "backup" | "update" | null = null; @state() private _updating = false;
@state() private _error?: string; @state() private _error?: string;
@@ -120,7 +116,7 @@ class UpdateAvailableCard extends LitElement {
return html``; return html``;
} }
const changelog = changelogUrl(this._updateType, this._version); const changelog = changelogUrl(this._updateType, this._version_latest);
return html` return html`
<ha-card <ha-card
@@ -132,7 +128,13 @@ class UpdateAvailableCard extends LitElement {
${this._error ${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""} : ""}
${this._action === null ${this._version === this._version_latest
? html`<p>
${this.supervisor.localize("update_available.no_update", {
name: this._name,
})}
</p>`
: !this._updating
? html` ? html`
${this._changelogContent ${this._changelogContent
? html` ? html`
@@ -166,18 +168,13 @@ class UpdateAvailableCard extends LitElement {
: html`<ha-circular-progress alt="Updating" size="large" active> : html`<ha-circular-progress alt="Updating" size="large" active>
</ha-circular-progress> </ha-circular-progress>
<p class="progress-text"> <p class="progress-text">
${this._action === "update" ${this.supervisor.localize("update_available.updating", {
? this.supervisor.localize("update_available.updating", { name: this._name,
name: this._name, version: this._version_latest,
version: this._version_latest, })}
})
: this.supervisor.localize(
"update_available.creating_backup",
{ name: this._name }
)}
</p>`} </p>`}
</div> </div>
${this._action === null ${this._version !== this._version_latest && !this._updating
? html` ? html`
<div class="card-actions"> <div class="card-actions">
${changelog ${changelog
@@ -224,6 +221,9 @@ class UpdateAvailableCard extends LitElement {
} }
get _shouldCreateBackup(): boolean { get _shouldCreateBackup(): boolean {
if (this._updateType && !["core", "addon"].includes(this._updateType)) {
return false;
}
const checkbox = this.shadowRoot?.querySelector("ha-checkbox"); const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
if (checkbox) { if (checkbox) {
return checkbox.checked; return checkbox.checked;
@@ -310,37 +310,16 @@ class UpdateAvailableCard extends LitElement {
private async _update() { private async _update() {
this._error = undefined; this._error = undefined;
if (this._shouldCreateBackup) { this._updating = true;
let backupArgs: HassioPartialBackupCreateParams;
if (this._updateType === "addon") {
backupArgs = {
name: `addon_${this.addonSlug}_${this._version}`,
addons: [this.addonSlug!],
homeassistant: false,
};
} else {
backupArgs = {
name: `${this._updateType}_${this._version}`,
folders: ["homeassistant"],
homeassistant: true,
};
}
this._action = "backup";
try {
await createHassioPartialBackup(this.hass, backupArgs);
} catch (err: any) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
}
this._action = "update";
try { try {
if (this._updateType === "addon") { if (this._updateType === "addon") {
await updateHassioAddon(this.hass, this.addonSlug!); await updateHassioAddon(
this.hass,
this.addonSlug!,
this._shouldCreateBackup
);
} else if (this._updateType === "core") { } else if (this._updateType === "core") {
await updateCore(this.hass); await updateCore(this.hass, this._shouldCreateBackup);
} else if (this._updateType === "os") { } else if (this._updateType === "os") {
await updateOS(this.hass); await updateOS(this.hass);
} else if (this._updateType === "supervisor") { } else if (this._updateType === "supervisor") {
@@ -349,7 +328,7 @@ class UpdateAvailableCard extends LitElement {
} catch (err: any) { } catch (err: any) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) { if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
this._error = extractApiErrorMessage(err); this._error = extractApiErrorMessage(err);
this._action = null; this._updating = false;
return; return;
} }
} }

View File

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

View File

@@ -208,6 +208,7 @@ export const DOMAINS_HIDE_DEFAULT_MORE_INFO = [
export const DOMAINS_INPUT_ROW = [ export const DOMAINS_INPUT_ROW = [
"cover", "cover",
"fan", "fan",
"group",
"humidifier", "humidifier",
"input_boolean", "input_boolean",
"input_datetime", "input_datetime",

View File

@@ -46,6 +46,7 @@ class HaAlert extends LitElement {
rtl: this.rtl, rtl: this.rtl,
[this.alertType]: true, [this.alertType]: true,
})}" })}"
role="alert"
> >
<div class="icon ${this.title ? "" : "no-title"}"> <div class="icon ${this.title ? "" : "no-title"}">
<slot name="icon"> <slot name="icon">

View File

@@ -56,6 +56,7 @@ export class HaRelatedFilterButtonMenu extends LitElement {
return html` return html`
<ha-icon-button <ha-icon-button
@click=${this._handleClick} @click=${this._handleClick}
.label=${this.hass.localize("ui.components.related-filter-menu.filter")}
.path=${mdiFilterVariant} .path=${mdiFilterVariant}
></ha-icon-button> ></ha-icon-button>
<mwc-menu-surface <mwc-menu-surface

View File

@@ -18,13 +18,9 @@ export class HaChip extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div class="mdc-chip"> <div class="mdc-chip ${this.noText ? "no-text" : ""}">
${this.hasIcon ${this.hasIcon
? html`<div ? html`<div class="mdc-chip__icon mdc-chip__icon--leading">
class="mdc-chip__icon mdc-chip__icon--leading ${this.noText
? "no-text"
: ""}"
>
<slot name="icon"></slot> <slot name="icon"></slot>
</div>` </div>`
: null} : null}
@@ -49,6 +45,10 @@ export class HaChip extends LitElement {
color: var(--ha-chip-text-color, var(--primary-text-color)); color: var(--ha-chip-text-color, var(--primary-text-color));
} }
.mdc-chip.no-text {
padding: 0 10px;
}
.mdc-chip:hover { .mdc-chip:hover {
color: var(--ha-chip-text-color, var(--primary-text-color)); color: var(--ha-chip-text-color, var(--primary-text-color));
} }
@@ -57,8 +57,8 @@ export class HaChip extends LitElement {
--mdc-icon-size: 20px; --mdc-icon-size: 20px;
color: var(--ha-chip-icon-color, var(--ha-chip-text-color)); color: var(--ha-chip-icon-color, var(--ha-chip-text-color));
} }
.mdc-chip .mdc-chip.no-text
.mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden).no-text { .mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden) {
margin-right: -4px; margin-right: -4px;
} }
`; `;

View File

@@ -35,7 +35,7 @@ class HaCoverControls extends LitElement {
hidden: !supportsOpen(this.stateObj), hidden: !supportsOpen(this.stateObj),
})} })}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.dialogs.more_info_control.open_cover" "ui.dialogs.more_info_control.cover.open_cover"
)} )}
@click=${this._onOpenTap} @click=${this._onOpenTap}
.disabled=${this._computeOpenDisabled()} .disabled=${this._computeOpenDisabled()}
@@ -47,7 +47,7 @@ class HaCoverControls extends LitElement {
hidden: !supportsStop(this.stateObj), hidden: !supportsStop(this.stateObj),
})} })}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.dialogs.more_info_control.stop_cover" "ui.dialogs.more_info_control.cover.stop_cover"
)} )}
.path=${mdiStop} .path=${mdiStop}
@click=${this._onStopTap} @click=${this._onStopTap}
@@ -58,7 +58,7 @@ class HaCoverControls extends LitElement {
hidden: !supportsClose(this.stateObj), hidden: !supportsClose(this.stateObj),
})} })}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.dialogs.more_info_control.close_cover" "ui.dialogs.more_info_control.cover.close_cover"
)} )}
@click=${this._onCloseTap} @click=${this._onCloseTap}
.disabled=${this._computeClosedDisabled()} .disabled=${this._computeClosedDisabled()}

View File

@@ -30,7 +30,7 @@ class HaCoverTiltControls extends LitElement {
invisible: !supportsOpenTilt(this.stateObj), invisible: !supportsOpenTilt(this.stateObj),
})} })}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.dialogs.more_info_control.open_tilt_cover" "ui.dialogs.more_info_control.cover.open_tilt_cover"
)} )}
.path=${mdiArrowTopRight} .path=${mdiArrowTopRight}
@click=${this._onOpenTiltTap} @click=${this._onOpenTiltTap}
@@ -40,7 +40,9 @@ class HaCoverTiltControls extends LitElement {
class=${classMap({ class=${classMap({
invisible: !supportsStopTilt(this.stateObj), invisible: !supportsStopTilt(this.stateObj),
})} })}
.label=${this.hass.localize("ui.dialogs.more_info_control.stop_cover")} .label=${this.hass.localize(
"ui.dialogs.more_info_control.cover.stop_cover"
)}
.path=${mdiStop} .path=${mdiStop}
@click=${this._onStopTiltTap} @click=${this._onStopTiltTap}
.disabled=${this.stateObj.state === UNAVAILABLE} .disabled=${this.stateObj.state === UNAVAILABLE}
@@ -50,7 +52,7 @@ class HaCoverTiltControls extends LitElement {
invisible: !supportsCloseTilt(this.stateObj), invisible: !supportsCloseTilt(this.stateObj),
})} })}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.dialogs.more_info_control.close_tilt_cover" "ui.dialogs.more_info_control.cover.close_tilt_cover"
)} )}
.path=${mdiArrowBottomLeft} .path=${mdiArrowBottomLeft}
@click=${this._onCloseTiltTap} @click=${this._onCloseTiltTap}

View File

@@ -122,14 +122,20 @@ class HaDurationInput extends LitElement {
value %= 60; value %= 60;
} }
const newValue: HaDurationData = {
hours,
minutes,
seconds: this._seconds,
};
if (this.enableMillisecond || this._milliseconds) {
newValue.milliseconds = this._milliseconds;
}
newValue[unit] = value;
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: { value: newValue,
hours,
minutes,
seconds: this._seconds,
milliseconds: this._milliseconds,
...{ [unit]: value },
},
}); });
} }
} }

View File

@@ -66,7 +66,7 @@ export class HaFormString extends LitElement implements HaFormElement {
${isPassword ${isPassword
? html`<ha-icon-button ? html`<ha-icon-button
toggles toggles
.label="Click to toggle between masked and clear password" .label=${`${this._unmaskedPassword ? "Hide" : "Show"} password`}
@click=${this._toggleUnmaskedPassword} @click=${this._toggleUnmaskedPassword}
tabindex="-1" tabindex="-1"
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye} .path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}

View File

@@ -1,6 +1,7 @@
import { Formfield } from "@material/mwc-formfield"; import { Formfield } from "@material/mwc-formfield";
import { css, CSSResultGroup } from "lit"; import { css, CSSResultGroup } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
@customElement("ha-formfield") @customElement("ha-formfield")
// @ts-expect-error // @ts-expect-error
@@ -13,6 +14,7 @@ export class HaFormfield extends Formfield {
case "HA-CHECKBOX": case "HA-CHECKBOX":
case "HA-RADIO": case "HA-RADIO":
(input as any).checked = !(input as any).checked; (input as any).checked = !(input as any).checked;
fireEvent(input, "change");
break; break;
default: default:
input.click(); input.click();

View File

@@ -29,7 +29,7 @@ export class HaIconOverflowMenu extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
${this.narrow ${this.narrow
? html` <!-- Collapsed Representation for Small Screens --> ? html` <!-- Collapsed representation for small screens -->
<ha-button-menu <ha-button-menu
@click=${this._handleIconOverflowMenuOpened} @click=${this._handleIconOverflowMenuOpened}
@closed=${this._handleIconOverflowMenuClosed} @closed=${this._handleIconOverflowMenuClosed}
@@ -59,8 +59,7 @@ export class HaIconOverflowMenu extends LitElement {
)} )}
</ha-button-menu>` </ha-button-menu>`
: html` : html`
<!-- Icon Representation for Big Screens --> <!-- Icon representation for big screens -->
${this.items.map((item) => ${this.items.map((item) =>
item.narrowOnly item.narrowOnly
? "" ? ""
@@ -70,13 +69,12 @@ export class HaIconOverflowMenu extends LitElement {
${item.tooltip} ${item.tooltip}
</paper-tooltip>` </paper-tooltip>`
: ""} : ""}
<mwc-icon-button <ha-icon-button
@click=${item.action} @click=${item.action}
.label=${item.label} .label=${item.label}
.path=${item.path}
.disabled=${item.disabled} .disabled=${item.disabled}
> ></ha-icon-button>
<ha-svg-icon .path=${item.path}></ha-svg-icon>
</mwc-icon-button>
</div> ` </div> `
)} )}
`} `}

View File

@@ -1,6 +1,8 @@
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select"; import "@material/mwc-select/mwc-select";
import type { Select } from "@material/mwc-select/mwc-select"; import "@material/mwc-textfield/mwc-textfield";
import type { TextField } from "@material/mwc-textfield/mwc-textfield";
import { mdiCamera } from "@mdi/js";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import type QrScanner from "qr-scanner"; import type QrScanner from "qr-scanner";
@@ -8,6 +10,8 @@ import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation"; import { stopPropagation } from "../common/dom/stop_propagation";
import { LocalizeFunc } from "../common/translations/localize"; import { LocalizeFunc } from "../common/translations/localize";
import "./ha-alert"; import "./ha-alert";
import "./ha-button-menu";
import "@material/mwc-button/mwc-button";
@customElement("ha-qr-scanner") @customElement("ha-qr-scanner")
class HaQrScanner extends LitElement { class HaQrScanner extends LitElement {
@@ -25,6 +29,8 @@ class HaQrScanner extends LitElement {
@query("#canvas-container", true) private _canvasContainer!: HTMLDivElement; @query("#canvas-container", true) private _canvasContainer!: HTMLDivElement;
@query("mwc-textfield") private _manualInput?: TextField;
public disconnectedCallback(): void { public disconnectedCallback(): void {
super.disconnectedCallback(); super.disconnectedCallback();
this._qrNotFoundCount = 0; this._qrNotFoundCount = 0;
@@ -58,34 +64,53 @@ class HaQrScanner extends LitElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
return html`${this._cameras && this._cameras.length > 1 return html`${this._error
? html`<mwc-select
.label=${this.localize(
"ui.panel.config.zwave_js.add_node.select_camera"
)}
fixedMenuPosition
naturalMenuWidth
@closed=${stopPropagation}
@selected=${this._cameraChanged}
>
${this._cameras!.map(
(camera) => html`
<mwc-list-item .value=${camera.id}>${camera.label}</mwc-list-item>
`
)}
</mwc-select>`
: ""}
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""} : ""}
${navigator.mediaDevices ${navigator.mediaDevices
? html`<video></video> ? html`<video></video>
<div id="canvas-container"></div>` <div id="canvas-container">
: html`<ha-alert alert-type="warning" ${this._cameras && this._cameras.length > 1
>${!window.isSecureContext ? html`<ha-button-menu
? "You can only use your camera to scan a QR core when using HTTPS." corner="BOTTOM_START"
: "Your browser doesn't support QR scanning."}</ha-alert fixed
>`}`; @closed=${stopPropagation}
>
<ha-icon-button
slot="trigger"
.label=${this.localize(
"ui.components.qr-scanner.select_camera"
)}
.path=${mdiCamera}
></ha-icon-button>
${this._cameras!.map(
(camera) => html`
<mwc-list-item
.value=${camera.id}
@click=${this._cameraChanged}
>${camera.label}</mwc-list-item
>
`
)}
</ha-button-menu>`
: ""}
</div>`
: html`<ha-alert alert-type="warning">
${!window.isSecureContext
? this.localize("ui.components.qr-scanner.only_https_supported")
: this.localize("ui.components.qr-scanner.not_supported")}
</ha-alert>
<p>${this.localize("ui.components.qr-scanner.manual_input")}</p>
<div class="row">
<mwc-textfield
.label=${this.localize("ui.components.qr-scanner.enter_qr_code")}
@keyup=${this._manualKeyup}
@paste=${this._manualPaste}
></mwc-textfield>
<mwc-button @click=${this._manualSubmit}
>${this.localize("ui.common.submit")}</mwc-button
>
</div>`}`;
} }
private async _loadQrScanner() { private async _loadQrScanner() {
@@ -134,17 +159,49 @@ class HaQrScanner extends LitElement {
fireEvent(this, "qr-code-scanned", { value: qrCodeString }); fireEvent(this, "qr-code-scanned", { value: qrCodeString });
}; };
private _manualKeyup(ev: KeyboardEvent) {
if (ev.key === "Enter") {
this._qrCodeScanned((ev.target as TextField).value);
}
}
private _manualPaste(ev: ClipboardEvent) {
this._qrCodeScanned(
// @ts-ignore
(ev.clipboardData || window.clipboardData).getData("text")
);
}
private _manualSubmit() {
this._qrCodeScanned(this._manualInput!.value);
}
private _cameraChanged(ev: CustomEvent): void { private _cameraChanged(ev: CustomEvent): void {
this._qrScanner?.setCamera((ev.target as Select).value); this._qrScanner?.setCamera((ev.target as any).value);
} }
static styles = css` static styles = css`
canvas { canvas {
width: 100%; width: 100%;
} }
mwc-select { #canvas-container {
width: 100%; position: relative;
margin-bottom: 16px; }
ha-button-menu {
position: absolute;
bottom: 8px;
right: 8px;
background: #727272b2;
color: white;
border-radius: 50%;
}
.row {
display: flex;
align-items: center;
}
mwc-textfield {
flex: 1;
margin-right: 8px;
} }
`; `;
} }

View File

@@ -3,6 +3,7 @@ import {
mdiBell, mdiBell,
mdiCalendar, mdiCalendar,
mdiCart, mdiCart,
mdiCellphoneCog,
mdiChartBox, mdiChartBox,
mdiClose, mdiClose,
mdiCog, mdiCog,
@@ -43,6 +44,10 @@ import {
PersistentNotification, PersistentNotification,
subscribeNotifications, subscribeNotifications,
} from "../data/persistent_notification"; } from "../data/persistent_notification";
import {
ExternalConfig,
getExternalConfig,
} from "../external_app/external_config";
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, PanelInfo, Route } from "../types"; import type { HomeAssistant, PanelInfo, Route } from "../types";
@@ -187,6 +192,8 @@ class HaSidebar extends LitElement {
@property({ type: Boolean }) public editMode = false; @property({ type: Boolean }) public editMode = false;
@state() private _externalConfig?: ExternalConfig;
@state() private _notifications?: PersistentNotification[]; @state() private _notifications?: PersistentNotification[];
@state() private _renderEmptySortable = false; @state() private _renderEmptySortable = false;
@@ -233,6 +240,7 @@ class HaSidebar extends LitElement {
changedProps.has("expanded") || changedProps.has("expanded") ||
changedProps.has("narrow") || changedProps.has("narrow") ||
changedProps.has("alwaysExpand") || changedProps.has("alwaysExpand") ||
changedProps.has("_externalConfig") ||
changedProps.has("_notifications") || changedProps.has("_notifications") ||
changedProps.has("editMode") || changedProps.has("editMode") ||
changedProps.has("_renderEmptySortable") || changedProps.has("_renderEmptySortable") ||
@@ -263,6 +271,12 @@ class HaSidebar extends LitElement {
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
if (this.hass && this.hass.auth.external) {
getExternalConfig(this.hass.auth.external).then((conf) => {
this._externalConfig = conf;
});
}
subscribeNotifications(this.hass.connection, (notifications) => { subscribeNotifications(this.hass.connection, (notifications) => {
this._notifications = notifications; this._notifications = notifications;
}); });
@@ -358,6 +372,7 @@ class HaSidebar extends LitElement {
: this._renderPanels(beforeSpacer)} : this._renderPanels(beforeSpacer)}
${this._renderSpacer()} ${this._renderSpacer()}
${this._renderPanels(afterSpacer)} ${this._renderPanels(afterSpacer)}
${this._renderExternalConfiguration()}
</paper-listbox> </paper-listbox>
`; `;
} }
@@ -387,7 +402,7 @@ class HaSidebar extends LitElement {
) { ) {
return html` return html`
<a <a
aria-role="option" role="option"
href=${`/${urlPath}`} href=${`/${urlPath}`}
data-panel=${urlPath} data-panel=${urlPath}
tabindex="-1" tabindex="-1"
@@ -492,7 +507,7 @@ class HaSidebar extends LitElement {
> >
<paper-icon-item <paper-icon-item
class="notifications" class="notifications"
aria-role="option" role="option"
@click=${this._handleShowNotificationDrawer} @click=${this._handleShowNotificationDrawer}
> >
<ha-svg-icon slot="item-icon" .path=${mdiBell}></ha-svg-icon> <ha-svg-icon slot="item-icon" .path=${mdiBell}></ha-svg-icon>
@@ -523,7 +538,7 @@ class HaSidebar extends LitElement {
href="/profile" href="/profile"
data-panel="panel" data-panel="panel"
tabindex="-1" tabindex="-1"
aria-role="option" role="option"
aria-label=${this.hass.localize("panel.profile")} aria-label=${this.hass.localize("panel.profile")}
@mouseenter=${this._itemMouseEnter} @mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave} @mouseleave=${this._itemMouseLeave}
@@ -542,6 +557,43 @@ class HaSidebar extends LitElement {
</a>`; </a>`;
} }
private _renderExternalConfiguration() {
return html`${!this.hass.user?.is_admin &&
this._externalConfig &&
this._externalConfig.hasSettingsScreen
? html`
<a
role="option"
aria-label=${this.hass.localize(
"ui.sidebar.external_app_configuration"
)}
href="#external-app-configuration"
tabindex="-1"
@click=${this._handleExternalAppConfiguration}
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item>
<ha-svg-icon
slot="item-icon"
.path=${mdiCellphoneCog}
></ha-svg-icon>
<span class="item-text">
${this.hass.localize("ui.sidebar.external_app_configuration")}
</span>
</paper-icon-item>
</a>
`
: ""}`;
}
private _handleExternalAppConfiguration(ev: Event) {
ev.preventDefault();
this.hass.auth.external!.fireMessage({
type: "config_screen/show",
});
}
private get _tooltip() { private get _tooltip() {
return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement; return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement;
} }

View File

@@ -13,6 +13,7 @@ export interface DeviceRegistryEntry {
model: string | null; model: string | null;
name: string | null; name: string | null;
sw_version: string | null; sw_version: string | null;
hw_version: string | null;
via_device_id: string | null; via_device_id: string | null;
area_id: string | null; area_id: string | null;
name_by_user: string | null; name_by_user: string | null;

View File

@@ -302,7 +302,8 @@ export const installHassioAddon = async (
export const updateHassioAddon = async ( export const updateHassioAddon = async (
hass: HomeAssistant, hass: HomeAssistant,
slug: string slug: string,
backup: boolean
): Promise<void> => { ): Promise<void> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) { if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({ await hass.callWS({
@@ -310,11 +311,13 @@ export const updateHassioAddon = async (
endpoint: `/store/addons/${slug}/update`, endpoint: `/store/addons/${slug}/update`,
method: "post", method: "post",
timeout: null, timeout: null,
data: { backup: backup },
}); });
} else { } else {
await hass.callApi<HassioResponse<void>>( await hass.callApi<HassioResponse<void>>(
"POST", "POST",
`hassio/addons/${slug}/update` `hassio/addons/${slug}/update`,
{ backup: backup }
); );
} }
}; };

View File

@@ -156,6 +156,7 @@ export interface MediaPlayerThumbnail {
export interface ControlButton { export interface ControlButton {
icon: string; icon: string;
// Used as key for action as well as tooltip and aria-label translation key
action: string; action: string;
} }

View File

@@ -6,15 +6,18 @@ export const restartCore = async (hass: HomeAssistant) => {
await hass.callService("homeassistant", "restart"); await hass.callService("homeassistant", "restart");
}; };
export const updateCore = async (hass: HomeAssistant) => { export const updateCore = async (hass: HomeAssistant, backup: boolean) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) { if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({ await hass.callWS({
type: "supervisor/api", type: "supervisor/api",
endpoint: "/core/update", endpoint: "/core/update",
method: "post", method: "post",
timeout: null, timeout: null,
data: { backup: backup },
}); });
} else { } else {
await hass.callApi<HassioResponse<void>>("POST", `hassio/core/update`); await hass.callApi<HassioResponse<void>>("POST", `hassio/core/update`, {
backup: backup,
});
} }
}; };

View File

@@ -65,6 +65,9 @@ class MoreInfoMediaPlayer extends LitElement {
action=${control.action} action=${control.action}
@click=${this._handleClick} @click=${this._handleClick}
.path=${control.icon} .path=${control.icon}
.label=${this.hass.localize(
`ui.card.media_player.${control.action}`
)}
> >
</ha-icon-button> </ha-icon-button>
` `

View File

@@ -99,6 +99,8 @@ export class QuickBar extends LitElement {
private _focusSet = false; private _focusSet = false;
private _focusListElement?: ListItem | null;
public async showDialog(params: QuickBarParams) { public async showDialog(params: QuickBarParams) {
this._commandMode = params.commandMode || this._toggleIfAlreadyOpened(); this._commandMode = params.commandMode || this._toggleIfAlreadyOpened();
this._initializeItemsIfNeeded(); this._initializeItemsIfNeeded();
@@ -317,7 +319,8 @@ export class QuickBar extends LitElement {
} else if (ev.code === "ArrowDown") { } else if (ev.code === "ArrowDown") {
ev.preventDefault(); ev.preventDefault();
this._getItemAtIndex(0)?.focus(); this._getItemAtIndex(0)?.focus();
this._getItemAtIndex(1)?.focus(); this._focusSet = true;
this._focusListElement = this._getItemAtIndex(0);
} }
} }
@@ -350,6 +353,11 @@ export class QuickBar extends LitElement {
this._initializeItemsIfNeeded(); this._initializeItemsIfNeeded();
this._filter = this._search; this._filter = this._search;
} else { } else {
if (this._focusSet && this._focusListElement) {
this._focusSet = false;
// @ts-ignore
this._focusListElement.rippleHandlers.endFocus();
}
this._debouncedSetFilter(this._search); this._debouncedSetFilter(this._search);
} }
} }
@@ -366,12 +374,14 @@ export class QuickBar extends LitElement {
private _setFocusFirstListItem() { private _setFocusFirstListItem() {
// @ts-ignore // @ts-ignore
this._getItemAtIndex(0)?.rippleHandlers.startFocus(); this._getItemAtIndex(0)?.rippleHandlers.startFocus();
this._focusListElement = this._getItemAtIndex(0);
} }
private _handleListItemKeyDown(ev: KeyboardEvent) { private _handleListItemKeyDown(ev: KeyboardEvent) {
const isSingleCharacter = ev.key.length === 1; const isSingleCharacter = ev.key.length === 1;
const isFirstListItem = const isFirstListItem =
(ev.target as HTMLElement).getAttribute("index") === "0"; (ev.target as HTMLElement).getAttribute("index") === "0";
this._focusListElement = ev.target as ListItem;
if (ev.key === "ArrowUp") { if (ev.key === "ArrowUp") {
if (isFirstListItem) { if (isFirstListItem) {
this._filterInputField?.focus(); this._filterInputField?.focus();
@@ -511,7 +521,13 @@ export class QuickBar extends LitElement {
if (page.component) { if (page.component) {
const info = this._getNavigationInfoFromConfig(page); const info = this._getNavigationInfoFromConfig(page);
if (info) { // Add to list, but only if we do not already have an entry for the same path and component
if (
info &&
!items.some(
(e) => e.path === info.path && e.component === info.component
)
) {
items.push(info); items.push(info);
} }
} }

View File

@@ -37,6 +37,25 @@ declare global {
} }
} }
const clearUrlParams = () => {
// Clear auth data from url if we have been able to establish a connection
if (location.search.includes("auth_callback=1")) {
const searchParams = new URLSearchParams(location.search);
// https://github.com/home-assistant/home-assistant-js-websocket/blob/master/lib/auth.ts
// Remove all data from QueryCallbackData type
searchParams.delete("auth_callback");
searchParams.delete("code");
searchParams.delete("state");
searchParams.delete("storeToken");
const search = searchParams.toString();
history.replaceState(
null,
"",
`${location.pathname}${search ? `?${search}` : ""}`
);
}
};
const authProm = isExternal const authProm = isExternal
? () => ? () =>
import("../external_app/external_auth").then(({ createExternalAuth }) => import("../external_app/external_auth").then(({ createExternalAuth }) =>
@@ -52,23 +71,7 @@ const authProm = isExternal
const connProm = async (auth) => { const connProm = async (auth) => {
try { try {
const conn = await createConnection({ auth }); const conn = await createConnection({ auth });
// Clear auth data from url if we have been able to establish a connection clearUrlParams();
if (location.search.includes("auth_callback=1")) {
const searchParams = new URLSearchParams(location.search);
// https://github.com/home-assistant/home-assistant-js-websocket/blob/master/lib/auth.ts
// Remove all data from QueryCallbackData type
searchParams.delete("auth_callback");
searchParams.delete("code");
searchParams.delete("state");
searchParams.delete("storeToken");
const search = searchParams.toString();
history.replaceState(
null,
"",
`${location.pathname}${search ? `?${search}` : ""}`
);
}
return { auth, conn }; return { auth, conn };
} catch (err: any) { } catch (err: any) {
if (err !== ERR_INVALID_AUTH) { if (err !== ERR_INVALID_AUTH) {
@@ -85,6 +88,7 @@ const connProm = async (auth) => {
} }
auth = await authProm(); auth = await authProm();
const conn = await createConnection({ auth }); const conn = await createConnection({ auth });
clearUrlParams();
return { auth, conn }; return { auth, conn };
} }
}; };

View File

@@ -2,7 +2,7 @@
* Auth class that connects to a native app for authentication. * Auth class that connects to a native app for authentication.
*/ */
import { Auth } from "home-assistant-js-websocket"; import { Auth } from "home-assistant-js-websocket";
import { ExternalMessaging, InternalMessage } from "./external_messaging"; import { ExternalMessaging, EMMessage } from "./external_messaging";
const CALLBACK_SET_TOKEN = "externalAuthSetToken"; const CALLBACK_SET_TOKEN = "externalAuthSetToken";
const CALLBACK_REVOKE_TOKEN = "externalAuthRevokeToken"; const CALLBACK_REVOKE_TOKEN = "externalAuthRevokeToken";
@@ -36,7 +36,7 @@ declare global {
postMessage(payload: BasePayload); postMessage(payload: BasePayload);
}; };
externalBus: { externalBus: {
postMessage(payload: InternalMessage); postMessage(payload: EMMessage);
}; };
}; };
}; };

View File

@@ -1,3 +1,4 @@
import { Connection } from "home-assistant-js-websocket";
import { import {
externalForwardConnectionEvents, externalForwardConnectionEvents,
externalForwardHaptics, externalForwardHaptics,
@@ -7,39 +8,50 @@ const CALLBACK_EXTERNAL_BUS = "externalBus";
interface CommandInFlight { interface CommandInFlight {
resolve: (data: any) => void; resolve: (data: any) => void;
reject: (err: ExternalError) => void; reject: (err: EMError) => void;
} }
export interface InternalMessage { export interface EMMessage {
id?: number; id?: number;
type: string; type: string;
payload?: unknown; payload?: unknown;
} }
interface ExternalError { interface EMError {
code: string; code: string;
message: string; message: string;
} }
interface ExternalMessageResult { interface EMMessageResultSuccess {
id: number; id: number;
type: "result"; type: "result";
success: true; success: true;
result: unknown; result: unknown;
} }
interface ExternalMessageResultError { interface EMMessageResultError {
id: number; id: number;
type: "result"; type: "result";
success: false; success: false;
error: ExternalError; error: EMError;
} }
type ExternalMessage = ExternalMessageResult | ExternalMessageResultError; interface EMExternalMessageRestart {
id: number;
type: "command";
command: "restart";
}
type ExternalMessage =
| EMMessageResultSuccess
| EMMessageResultError
| EMExternalMessageRestart;
export class ExternalMessaging { export class ExternalMessaging {
public commands: { [msgId: number]: CommandInFlight } = {}; public commands: { [msgId: number]: CommandInFlight } = {};
public connection?: Connection;
public cache: Record<string, any> = {}; public cache: Record<string, any> = {};
public msgId = 0; public msgId = 0;
@@ -54,7 +66,7 @@ export class ExternalMessaging {
* Send message to external app that expects a response. * Send message to external app that expects a response.
* @param msg message to send * @param msg message to send
*/ */
public sendMessage<T>(msg: InternalMessage): Promise<T> { public sendMessage<T>(msg: EMMessage): Promise<T> {
const msgId = ++this.msgId; const msgId = ++this.msgId;
msg.id = msgId; msg.id = msgId;
@@ -69,7 +81,9 @@ export class ExternalMessaging {
* Send message to external app without expecting a response. * Send message to external app without expecting a response.
* @param msg message to send * @param msg message to send
*/ */
public fireMessage(msg: InternalMessage) { public fireMessage(
msg: EMMessage | EMMessageResultSuccess | EMMessageResultError
) {
if (!msg.id) { if (!msg.id) {
msg.id = ++this.msgId; msg.id = ++this.msgId;
} }
@@ -82,6 +96,43 @@ export class ExternalMessaging {
console.log("Receiving message from external app", msg); console.log("Receiving message from external app", msg);
} }
if (msg.type === "command") {
if (!this.connection) {
// eslint-disable-next-line no-console
console.warn("Received command without having connection set", msg);
this.fireMessage({
id: msg.id,
type: "result",
success: false,
error: {
code: "commands_not_init",
message: `Commands connection not set`,
},
});
} else if (msg.command === "restart") {
this.connection.socket.close();
this.fireMessage({
id: msg.id,
type: "result",
success: true,
result: null,
});
} else {
// eslint-disable-next-line no-console
console.warn("Received unknown command", msg.command, msg);
this.fireMessage({
id: msg.id,
type: "result",
success: false,
error: {
code: "unknown_command",
message: `Unknown command ${msg.command}`,
},
});
}
return;
}
const pendingCmd = this.commands[msg.id]; const pendingCmd = this.commands[msg.id];
if (!pendingCmd) { if (!pendingCmd) {
@@ -99,7 +150,7 @@ export class ExternalMessaging {
} }
} }
protected _sendExternal(msg: InternalMessage) { protected _sendExternal(msg: EMMessage) {
if (__DEV__) { if (__DEV__) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log("Sending message to external app", msg); console.log("Sending message to external app", msg);

View File

@@ -127,7 +127,9 @@ export class HassRouterPage extends ReactiveElement {
// Update the url if we know where we're mounted. // Update the url if we know where we're mounted.
if (route) { if (route) {
navigate(`${route.prefix}/${result}`, { replace: true }); navigate(`${route.prefix}/${result}${location.search}`, {
replace: true,
});
} }
} }
} }

View File

@@ -51,7 +51,9 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
const path = curPath(); const path = curPath();
if (["", "/"].includes(path)) { if (["", "/"].includes(path)) {
navigate(`/${getStorageDefaultPanelUrlPath()}`, { replace: true }); navigate(`/${getStorageDefaultPanelUrlPath()}${location.search}`, {
replace: true,
});
} }
this._route = { this._route = {
prefix: "", prefix: "",

View File

@@ -95,8 +95,11 @@ class OnboardingCreateUser extends LitElement {
private _handleValueChanged( private _handleValueChanged(
ev: PolymerChangedEvent<HaFormDataContainer> ev: PolymerChangedEvent<HaFormDataContainer>
): void { ): void {
const nameChanged = ev.detail.value.name !== this._newUser.name;
this._newUser = ev.detail.value; this._newUser = ev.detail.value;
this._maybePopulateUsername(); if (nameChanged) {
this._maybePopulateUsername();
}
this._formError.password_confirm = this._formError.password_confirm =
this._newUser.password !== this._newUser.password_confirm this._newUser.password !== this._newUser.password_confirm
? this.localize( ? this.localize(

View File

@@ -138,7 +138,8 @@ export default class HaAutomationConditionEditor extends LitElement {
if (!ev.detail.isValid) { if (!ev.detail.isValid) {
return; return;
} }
fireEvent(this, "value-changed", { value: ev.detail.value }); // @ts-ignore
fireEvent(this, "value-changed", { value: ev.detail.value, yaml: true });
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@@ -109,6 +109,7 @@ export default class HaAutomationConditionRow extends LitElement {
: ""} : ""}
<ha-automation-condition-editor <ha-automation-condition-editor
@ui-mode-not-available=${this._handleUiModeNotAvailable} @ui-mode-not-available=${this._handleUiModeNotAvailable}
@value-changed=${this._handleChangeEvent}
.yamlMode=${this._yamlMode} .yamlMode=${this._yamlMode}
.hass=${this.hass} .hass=${this.hass}
.condition=${this.condition} .condition=${this.condition}
@@ -127,6 +128,12 @@ export default class HaAutomationConditionRow extends LitElement {
} }
} }
private _handleChangeEvent(ev: CustomEvent) {
if (ev.detail.yaml) {
this._warnings = undefined;
}
}
private _handleAction(ev: CustomEvent<ActionDetail>) { private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) { switch (ev.detail.index) {
case 0: case 0:

View File

@@ -17,9 +17,9 @@ import {
const stateConditionStruct = object({ const stateConditionStruct = object({
condition: literal("state"), condition: literal("state"),
entity_id: string(), entity_id: optional(string()),
attribute: optional(string()), attribute: optional(string()),
state: string(), state: optional(string()),
for: optional(union([string(), forDictStruct])), for: optional(union([string(), forDictStruct])),
}); });

View File

@@ -1,4 +1,9 @@
import { object, optional, number } from "superstruct"; import { object, optional, number, string } from "superstruct";
export const baseTriggerStruct = object({
platform: string(),
id: optional(string()),
});
export const forDictStruct = object({ export const forDictStruct = object({
days: optional(number()), days: optional(number()),

View File

@@ -291,6 +291,7 @@ export default class HaAutomationTriggerRow extends LitElement {
if (!ev.detail.isValid) { if (!ev.detail.isValid) {
return; return;
} }
this._warnings = undefined;
fireEvent(this, "value-changed", { value: ev.detail.value }); fireEvent(this, "value-changed", { value: ev.detail.value });
} }

View File

@@ -1,7 +1,15 @@
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import { html, LitElement, PropertyValues } from "lit"; import { html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { assert, literal, object, optional, string, union } from "superstruct"; import {
assert,
assign,
literal,
object,
optional,
string,
union,
} from "superstruct";
import { createDurationData } from "../../../../../common/datetime/create_duration_data"; import { createDurationData } from "../../../../../common/datetime/create_duration_data";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import { hasTemplate } from "../../../../../common/string/has-template"; import { hasTemplate } from "../../../../../common/string/has-template";
@@ -10,20 +18,23 @@ import "../../../../../components/entity/ha-entity-picker";
import "../../../../../components/ha-duration-input"; import "../../../../../components/ha-duration-input";
import { StateTrigger } from "../../../../../data/automation"; import { StateTrigger } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import { forDictStruct } from "../../structs"; import { baseTriggerStruct, forDictStruct } from "../../structs";
import { import {
handleChangeEvent, handleChangeEvent,
TriggerElement, TriggerElement,
} from "../ha-automation-trigger-row"; } from "../ha-automation-trigger-row";
const stateTriggerStruct = object({ const stateTriggerStruct = assign(
platform: literal("state"), baseTriggerStruct,
entity_id: string(), object({
attribute: optional(string()), platform: literal("state"),
from: optional(string()), entity_id: optional(string()),
to: optional(string()), attribute: optional(string()),
for: optional(union([string(), forDictStruct])), from: optional(string()),
}); to: optional(string()),
for: optional(union([string(), forDictStruct])),
})
);
@customElement("ha-automation-trigger-state") @customElement("ha-automation-trigger-state")
export class HaStateTrigger extends LitElement implements TriggerElement { export class HaStateTrigger extends LitElement implements TriggerElement {
@@ -39,6 +50,13 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
if (!changedProperties.has("trigger")) { if (!changedProperties.has("trigger")) {
return true; return true;
} }
if (
this.trigger.for &&
typeof this.trigger.for === "object" &&
this.trigger.for.milliseconds === 0
) {
delete this.trigger.for.milliseconds;
}
// Check for templates in trigger. If found, revert to YAML mode. // Check for templates in trigger. If found, revert to YAML mode.
if (this.trigger && hasTemplate(this.trigger)) { if (this.trigger && hasTemplate(this.trigger)) {
fireEvent( fireEvent(

View File

@@ -131,7 +131,7 @@ class HaConfigDashboard extends LitElement {
border-bottom: var(--app-header-border-bottom); border-bottom: var(--app-header-border-bottom);
--header-height: 55px; --header-height: 55px;
} }
ha-card:last-child { :host(:not([narrow])) ha-card:last-child {
margin-bottom: 24px; margin-bottom: 24px;
} }
ha-config-section { ha-config-section {
@@ -152,7 +152,7 @@ class HaConfigDashboard extends LitElement {
padding-bottom: 0; padding-bottom: 0;
} }
:host([narrow]) ha-card { :host([narrow]) ha-card {
background-color: var(--primary-background-color); border-radius: 0;
box-shadow: unset; box-shadow: unset;
} }

View File

@@ -31,7 +31,7 @@ class HaConfigNavigation extends LitElement {
: canShowPage(this.hass, page) : canShowPage(this.hass, page)
) )
? html` ? html`
<a href=${page.path} aria-role="option" tabindex="-1"> <a href=${page.path} role="option" tabindex="-1">
<paper-icon-item @click=${this._entryClicked}> <paper-icon-item @click=${this._entryClicked}>
<div <div
class=${page.iconColor ? "icon-background" : ""} class=${page.iconColor ? "icon-background" : ""}
@@ -43,8 +43,7 @@ class HaConfigNavigation extends LitElement {
<paper-item-body two-line> <paper-item-body two-line>
${page.name || ${page.name ||
this.hass.localize( this.hass.localize(
page.translationKey || `ui.panel.config.dashboard.${page.translationKey}.title`
`ui.panel.config.${page.component}.caption`
)} )}
${page.component === "cloud" && (page.info as CloudStatus) ${page.component === "cloud" && (page.info as CloudStatus)
? page.info.logged_in ? page.info.logged_in
@@ -68,7 +67,7 @@ class HaConfigNavigation extends LitElement {
<div secondary> <div secondary>
${page.description || ${page.description ||
this.hass.localize( this.hass.localize(
`ui.panel.config.${page.component}.description` `ui.panel.config.dashboard.${page.translationKey}.description`
)} )}
</div> </div>
`} `}

View File

@@ -66,6 +66,17 @@ export class HaDeviceCard extends LitElement {
</div> </div>
` `
: ""} : ""}
${this.device.hw_version
? html`
<div class="extra-info">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.hardware",
"version",
this.device.hw_version
)}
</div>
`
: ""}
<slot></slot> <slot></slot>
</div> </div>
<slot name="actions"></slot> <slot name="actions"></slot>

View File

@@ -49,80 +49,69 @@ export const configSections: { [name: string]: PageNavigation[] } = {
dashboard: [ dashboard: [
{ {
path: "/config/integrations", path: "/config/integrations",
name: "Devices & Services", translationKey: "devices",
description: "Integrations, devices, entities and areas",
iconPath: mdiDevices, iconPath: mdiDevices,
iconColor: "#0D47A1", iconColor: "#0D47A1",
core: true, core: true,
}, },
{ {
path: "/config/automation", path: "/config/automation",
name: "Automations & Scenes", translationKey: "automations",
description: "Manage automations, scenes, scripts and helpers",
iconPath: mdiRobot, iconPath: mdiRobot,
iconColor: "#518C43", iconColor: "#518C43",
core: true, core: true,
}, },
{ {
path: "/config/blueprint", path: "/config/blueprint",
name: "Blueprints", translationKey: "blueprints",
description: "Manage blueprints",
iconPath: mdiPaletteSwatch, iconPath: mdiPaletteSwatch,
iconColor: "#64B5F6", iconColor: "#64B5F6",
component: "blueprint", component: "blueprint",
}, },
{ {
path: "/hassio", path: "/hassio",
name: "Add-ons, Backups & Supervisor", translationKey: "supervisor",
description: "Create backups, check logs or reboot your system",
iconPath: mdiHomeAssistant, iconPath: mdiHomeAssistant,
iconColor: "#4084CD", iconColor: "#4084CD",
component: "hassio", component: "hassio",
}, },
{ {
path: "/config/lovelace/dashboards", path: "/config/lovelace/dashboards",
name: "Dashboards", translationKey: "dashboards",
description: "Create customized sets of cards to control your home",
iconPath: mdiViewDashboard, iconPath: mdiViewDashboard,
iconColor: "#B1345C", iconColor: "#B1345C",
component: "lovelace", component: "lovelace",
}, },
{ {
path: "/config/energy", path: "/config/energy",
name: "Energy", translationKey: "energy",
description: "Monitor your energy production and consumption",
iconPath: mdiLightningBolt, iconPath: mdiLightningBolt,
iconColor: "#F1C447", iconColor: "#F1C447",
component: "energy", component: "energy",
}, },
{ {
path: "/config/tags", path: "/config/tags",
name: "Tags", translationKey: "tags",
description:
"Trigger automations when a NFC tag, QR code, etc. is scanned",
iconPath: mdiNfcVariant, iconPath: mdiNfcVariant,
iconColor: "#616161", iconColor: "#616161",
component: "tag", component: "tag",
}, },
{ {
path: "/config/person", path: "/config/person",
name: "People & Zones", translationKey: "people",
description: "Manage the people and zones that Home Assistant tracks",
iconPath: mdiAccount, iconPath: mdiAccount,
iconColor: "#E48629", iconColor: "#E48629",
components: ["person", "zone", "users"], components: ["person", "zone", "users"],
}, },
{ {
path: "#external-app-configuration", path: "#external-app-configuration",
name: "Companion App", translationKey: "companion",
description: "Location and notifications",
iconPath: mdiCellphoneCog, iconPath: mdiCellphoneCog,
iconColor: "#8E24AA", iconColor: "#8E24AA",
}, },
{ {
path: "/config/core", path: "/config/server_control",
name: "Settings", translationKey: "settings",
description: "Basic settings, server controls, logs and info",
iconPath: mdiCog, iconPath: mdiCog,
iconColor: "#4A5963", iconColor: "#4A5963",
core: true, core: true,

View File

@@ -95,7 +95,7 @@ class OZWConfigDashboard extends LitElement {
<ha-card> <ha-card>
<a <a
href="/config/ozw/network/${instance.ozw_instance}" href="/config/ozw/network/${instance.ozw_instance}"
aria-role="option" role="option"
tabindex="-1" tabindex="-1"
> >
<paper-icon-item> <paper-icon-item>

View File

@@ -129,7 +129,11 @@ class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) {
<span <span
>[[localize('ui.panel.config.zwave.node_management.header')]]</span >[[localize('ui.panel.config.zwave.node_management.header')]]</span
> >
<ha-icon-button class="toggle-help-icon" on-click="toggleHelp"> <ha-icon-button
class="toggle-help-icon"
on-click="toggleHelp"
label="[[localize('ui.common.help')]]"
>
<ha-icon icon="hass:help-circle"></ha-icon> <ha-icon icon="hass:help-circle"></ha-icon>
</ha-icon-button> </ha-icon-button>
</div> </div>

View File

@@ -1,5 +1,4 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import type { TextField } from "@material/mwc-textfield/mwc-textfield";
import "@material/mwc-textfield/mwc-textfield"; import "@material/mwc-textfield/mwc-textfield";
import { mdiAlertCircle, mdiCheckCircle, mdiQrcodeScan } from "@mdi/js"; import { mdiAlertCircle, mdiCheckCircle, mdiQrcodeScan } from "@mdi/js";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
@@ -179,21 +178,16 @@ class DialogZWaveJSAddNode extends LitElement {
Search device Search device
</mwc-button>` </mwc-button>`
: this._status === "qr_scan" : this._status === "qr_scan"
? html`<ha-qr-scanner ? html`${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<ha-qr-scanner
.localize=${this.hass.localize} .localize=${this.hass.localize}
@qr-code-scanned=${this._qrCodeScanned} @qr-code-scanned=${this._qrCodeScanned}
></ha-qr-scanner> ></ha-qr-scanner>
<p> <mwc-button slot="secondaryAction" @click=${this._startOver}>
If scanning doesn't work, you can enter the QR code value ${this.hass.localize("ui.panel.config.zwave_js.common.back")}
manually: </mwc-button>`
</p>
<mwc-textfield
.label=${this.hass.localize(
"ui.panel.config.zwave_js.add_node.enter_qr_code"
)}
.disabled=${this._qrProcessing}
@keydown=${this._qrKeyDown}
></mwc-textfield>`
: this._status === "validate_dsk_enter_pin" : this._status === "validate_dsk_enter_pin"
? html` ? html`
<p> <p>
@@ -203,9 +197,9 @@ class DialogZWaveJSAddNode extends LitElement {
</p> </p>
${ ${
this._error this._error
? html`<ha-alert alert-type="error" ? html`<ha-alert alert-type="error">
>${this._error}</ha-alert ${this._error}
>` </ha-alert>`
: "" : ""
} }
<div class="flex-container"> <div class="flex-container">
@@ -274,7 +268,7 @@ class DialogZWaveJSAddNode extends LitElement {
We have not found any device in inclusion mode. Make sure the We have not found any device in inclusion mode. Make sure the
device is active and in inclusion mode. device is active and in inclusion mode.
</p> </p>
<mwc-button slot="primaryAction" @click=${this._startInclusion}> <mwc-button slot="primaryAction" @click=${this._startOver}>
Retry Retry
</mwc-button> </mwc-button>
` `
@@ -373,7 +367,7 @@ class DialogZWaveJSAddNode extends LitElement {
</div> </div>
</div> </div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}> <mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.panel.config.zwave_js.common.close")} ${this.hass.localize("ui.common.close")}
</mwc-button> </mwc-button>
` `
: this._status === "failed" : this._status === "failed"
@@ -510,15 +504,6 @@ class DialogZWaveJSAddNode extends LitElement {
this._status = "qr_scan"; this._status = "qr_scan";
} }
private _qrKeyDown(ev: KeyboardEvent) {
if (this._qrProcessing) {
return;
}
if (ev.key === "Enter") {
this._handleQrCodeScanned((ev.target as TextField).value);
}
}
private _qrCodeScanned(ev: CustomEvent): void { private _qrCodeScanned(ev: CustomEvent): void {
if (this._qrProcessing) { if (this._qrProcessing) {
return; return;
@@ -574,11 +559,7 @@ class DialogZWaveJSAddNode extends LitElement {
} }
} else if (provisioningInfo.version === 0) { } else if (provisioningInfo.version === 0) {
this._inclusionStrategy = InclusionStrategy.Security_S2; this._inclusionStrategy = InclusionStrategy.Security_S2;
// this._startInclusion(provisioningInfo); this._startInclusion(provisioningInfo);
this._startInclusion(undefined, undefined, {
dsk: "34673-15546-46480-39591-32400-22155-07715-45994",
security_classes: [0, 1, 7],
});
} else { } else {
this._error = "This QR code is not supported"; this._error = "This QR code is not supported";
this._status = "failed"; this._status = "failed";
@@ -638,6 +619,10 @@ class DialogZWaveJSAddNode extends LitElement {
).supported; ).supported;
} }
private _startOver(_ev: Event) {
this._startInclusion();
}
private _startInclusion( private _startInclusion(
qrProvisioningInformation?: QRProvisioningInformation, qrProvisioningInformation?: QRProvisioningInformation,
qrCodeString?: string, qrCodeString?: string,

View File

@@ -5,6 +5,7 @@ import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { componentsWithService } from "../../../common/config/components_with_service"; import { componentsWithService } from "../../../common/config/components_with_service";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/buttons/ha-call-service-button"; import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-card"; import "../../../components/ha-card";
import { checkCoreConfig } from "../../../data/core"; import { checkCoreConfig } from "../../../data/core";
@@ -157,18 +158,20 @@ export class HaConfigServerControl extends LitElement {
"ui.panel.config.server_control.section.server_management.restart" "ui.panel.config.server_control.section.server_management.restart"
)} )}
</ha-call-service-button> </ha-call-service-button>
<ha-call-service-button ${!isComponentLoaded(this.hass, "hassio")
class="warning" ? html`<ha-call-service-button
.hass=${this.hass} class="warning"
domain="homeassistant" .hass=${this.hass}
service="stop" domain="homeassistant"
confirmation=${this.hass.localize( service="stop"
"ui.panel.config.server_control.section.server_management.confirm_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" >${this.hass.localize(
)} "ui.panel.config.server_control.section.server_management.stop"
</ha-call-service-button> )}
</ha-call-service-button>`
: ""}
</div> </div>
</ha-card> </ha-card>

View File

@@ -106,6 +106,7 @@ class DialogUserDetail extends LitElement {
.dir=${computeRTLDirection(this.hass)} .dir=${computeRTLDirection(this.hass)}
> >
<ha-switch <ha-switch
.disabled=${user.system_generated}
.checked=${this._localOnly} .checked=${this._localOnly}
@change=${this._localOnlyChanged} @change=${this._localOnlyChanged}
> >

View File

@@ -18,6 +18,7 @@ import "../../../components/ha-code-editor";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import "../../../components/ha-checkbox"; import "../../../components/ha-checkbox";
import "../../../components/ha-expansion-panel";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { EventsMixin } from "../../../mixins/events-mixin"; import { EventsMixin } from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin"; import LocalizeMixin from "../../../mixins/localize-mixin";
@@ -40,6 +41,10 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
padding: 16px; padding: 16px;
} }
ha-expansion-panel {
margin: 0 8px 16px;
}
.inputs { .inputs {
width: 100%; width: 100%;
max-width: 400px; max-width: 400px;
@@ -135,72 +140,77 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
padding: 0; padding: 0;
} }
</style> </style>
<p>
[[localize('ui.panel.developer-tools.tabs.states.description1')]]<br />
[[localize('ui.panel.developer-tools.tabs.states.description2')]]
</p>
<div class="state-wrapper flex layout horizontal">
<div class="inputs">
<ha-entity-picker
autofocus
hass="[[hass]]"
value="{{_entityId}}"
on-change="entityIdChanged"
allow-custom-entity
></ha-entity-picker>
<paper-input
label="[[localize('ui.panel.developer-tools.tabs.states.state')]]"
required
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
value="{{_state}}"
class="state-input"
></paper-input>
<p>
[[localize('ui.panel.developer-tools.tabs.states.state_attributes')]]
</p>
<ha-code-editor
mode="yaml"
value="[[_stateAttributes]]"
error="[[!validJSON]]"
on-value-changed="_yamlChanged"
></ha-code-editor>
<div class="button-row">
<mwc-button
on-click="handleSetState"
disabled="[[!validJSON]]"
raised
>[[localize('ui.panel.developer-tools.tabs.states.set_state')]]</mwc-button
>
<ha-icon-button
on-click="entityIdChanged"
label="[[localize('ui.common.refresh')]]"
path="[[refreshIcon()]]"
></ha-icon-button>
</div>
</div>
<div class="info">
<template is="dom-if" if="[[_entity]]">
<p>
<b
>[[localize('ui.panel.developer-tools.tabs.states.last_changed')]]:</b
><br />[[lastChangedString(_entity)]]
</p>
<p>
<b
>[[localize('ui.panel.developer-tools.tabs.states.last_updated')]]:</b
><br />[[lastUpdatedString(_entity)]]
</p>
</template>
</div>
</div>
<h1> <h1>
[[localize('ui.panel.developer-tools.tabs.states.current_entities')]] [[localize('ui.panel.developer-tools.tabs.states.current_entities')]]
</h1> </h1>
<ha-expansion-panel
header="Set state"
outlined
expanded="[[_expanded]]"
on-expanded-changed="expandedChanged"
>
<p>
[[localize('ui.panel.developer-tools.tabs.states.description1')]]<br />
[[localize('ui.panel.developer-tools.tabs.states.description2')]]
</p>
<div class="state-wrapper flex layout horizontal">
<div class="inputs">
<ha-entity-picker
autofocus
hass="[[hass]]"
value="{{_entityId}}"
on-change="entityIdChanged"
allow-custom-entity
></ha-entity-picker>
<paper-input
label="[[localize('ui.panel.developer-tools.tabs.states.state')]]"
required
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
value="{{_state}}"
class="state-input"
></paper-input>
<p>
[[localize('ui.panel.developer-tools.tabs.states.state_attributes')]]
</p>
<ha-code-editor
mode="yaml"
value="[[_stateAttributes]]"
error="[[!validJSON]]"
on-value-changed="_yamlChanged"
></ha-code-editor>
<div class="button-row">
<mwc-button
on-click="handleSetState"
disabled="[[!validJSON]]"
raised
>[[localize('ui.panel.developer-tools.tabs.states.set_state')]]</mwc-button
>
<ha-icon-button
on-click="entityIdChanged"
label="[[localize('ui.common.refresh')]]"
path="[[refreshIcon()]]"
></ha-icon-button>
</div>
</div>
<div class="info">
<template is="dom-if" if="[[_entity]]">
<p>
<b
>[[localize('ui.panel.developer-tools.tabs.states.last_changed')]]:</b
><br />[[lastChangedString(_entity)]]
</p>
<p>
<b
>[[localize('ui.panel.developer-tools.tabs.states.last_updated')]]:</b
><br />[[lastUpdatedString(_entity)]]
</p>
</template>
</div>
</div>
</ha-expansion-panel>
<div class="table-wrapper"> <div class="table-wrapper">
<table class="entities"> <table class="entities">
<tr> <tr>
@@ -348,6 +358,11 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
"computeEntities(hass, _entityFilter, _stateFilter, _attributeFilter)", "computeEntities(hass, _entityFilter, _stateFilter, _attributeFilter)",
}, },
_expanded: {
type: Boolean,
value: false,
},
narrow: { narrow: {
type: Boolean, type: Boolean,
reflectToAttribute: true, reflectToAttribute: true,
@@ -371,6 +386,7 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
this._entity = state; this._entity = state;
this._state = state.state; this._state = state.state;
this._stateAttributes = dump(state.attributes); this._stateAttributes = dump(state.attributes);
this._expanded = true;
ev.preventDefault(); ev.preventDefault();
} }
@@ -388,6 +404,11 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
this._entity = state; this._entity = state;
this._state = state.state; this._state = state.state;
this._stateAttributes = dump(state.attributes); this._stateAttributes = dump(state.attributes);
this._expanded = true;
}
expandedChanged(ev) {
this._expanded = ev.detail.expanded;
} }
entityMoreInfo(ev) { entityMoreInfo(ev) {

View File

@@ -26,7 +26,7 @@ import {
rgb2hex, rgb2hex,
rgb2lab, rgb2lab,
} from "../../../../common/color/convert-color"; } from "../../../../common/color/convert-color";
import { labDarken } from "../../../../common/color/lab"; import { labBrighten, labDarken } from "../../../../common/color/lab";
import { import {
EnergyData, EnergyData,
getEnergyDataCollection, getEnergyDataCollection,
@@ -247,10 +247,15 @@ export class HuiEnergyGasGraphCard
const data: ChartDataset<"bar" | "line">[] = []; const data: ChartDataset<"bar" | "line">[] = [];
const entity = this.hass.states[source.stat_energy_from]; const entity = this.hass.states[source.stat_energy_from];
const borderColor = const modifiedColor =
idx > 0 idx > 0
? rgb2hex(lab2rgb(labDarken(rgb2lab(hex2rgb(gasColor)), idx))) ? this.hass.themes.darkMode
: gasColor; ? labBrighten(rgb2lab(hex2rgb(gasColor)), idx)
: labDarken(rgb2lab(hex2rgb(gasColor)), idx)
: undefined;
const borderColor = modifiedColor
? rgb2hex(lab2rgb(modifiedColor))
: gasColor;
let prevValue: number | null = null; let prevValue: number | null = null;
let prevStart: string | null = null; let prevStart: string | null = null;

View File

@@ -1,9 +1,3 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
import { classMap } from "lit/directives/class-map";
import "../../../../components/ha-card";
import { import {
ChartData, ChartData,
ChartDataset, ChartDataset,
@@ -17,16 +11,26 @@ import {
isToday, isToday,
startOfToday, startOfToday,
} from "date-fns"; } from "date-fns";
import { HomeAssistant } from "../../../../types"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { LovelaceCard } from "../../types"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { EnergySolarGraphCardConfig } from "../types"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { import {
hex2rgb, hex2rgb,
lab2rgb, lab2rgb,
rgb2hex, rgb2hex,
rgb2lab, rgb2lab,
} from "../../../../common/color/convert-color"; } from "../../../../common/color/convert-color";
import { labDarken } from "../../../../common/color/lab"; import { labBrighten, labDarken } from "../../../../common/color/lab";
import { formatTime } from "../../../../common/datetime/format_time";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import {
formatNumber,
numberFormatToLocale,
} from "../../../../common/number/format_number";
import "../../../../components/chart/ha-chart-base";
import "../../../../components/ha-card";
import { import {
EnergyData, EnergyData,
EnergySolarForecasts, EnergySolarForecasts,
@@ -34,15 +38,11 @@ import {
getEnergySolarForecasts, getEnergySolarForecasts,
SolarSourceTypeEnergyPreference, SolarSourceTypeEnergyPreference,
} from "../../../../data/energy"; } from "../../../../data/energy";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import "../../../../components/chart/ha-chart-base";
import {
formatNumber,
numberFormatToLocale,
} from "../../../../common/number/format_number";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { FrontendLocaleData } from "../../../../data/translation"; import { FrontendLocaleData } from "../../../../data/translation";
import { formatTime } from "../../../../common/datetime/format_time"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types";
import { EnergySolarGraphCardConfig } from "../types";
@customElement("hui-energy-solar-graph-card") @customElement("hui-energy-solar-graph-card")
export class HuiEnergySolarGraphCard export class HuiEnergySolarGraphCard
@@ -258,10 +258,15 @@ export class HuiEnergySolarGraphCard
const data: ChartDataset<"bar" | "line">[] = []; const data: ChartDataset<"bar" | "line">[] = [];
const entity = this.hass.states[source.stat_energy_from]; const entity = this.hass.states[source.stat_energy_from];
const borderColor = const modifiedColor =
idx > 0 idx > 0
? rgb2hex(lab2rgb(labDarken(rgb2lab(hex2rgb(solarColor)), idx))) ? this.hass.themes.darkMode
: solarColor; ? labBrighten(rgb2lab(hex2rgb(solarColor)), idx)
: labDarken(rgb2lab(hex2rgb(solarColor)), idx)
: undefined;
const borderColor = modifiedColor
? rgb2hex(lab2rgb(modifiedColor))
: solarColor;
let prevValue: number | null = null; let prevValue: number | null = null;
let prevStart: string | null = null; let prevStart: string | null = null;

View File

@@ -17,7 +17,7 @@ import {
rgb2lab, rgb2lab,
hex2rgb, hex2rgb,
} from "../../../../common/color/convert-color"; } from "../../../../common/color/convert-color";
import { labDarken } from "../../../../common/color/lab"; import { labBrighten, labDarken } from "../../../../common/color/lab";
import { computeStateName } from "../../../../common/entity/compute_state_name"; import { computeStateName } from "../../../../common/entity/compute_state_name";
import { formatNumber } from "../../../../common/number/format_number"; import { formatNumber } from "../../../../common/number/format_number";
import "../../../../components/chart/statistics-chart"; import "../../../../components/chart/statistics-chart";
@@ -170,12 +170,17 @@ export class HuiEnergySourcesTableCard
this._data!.stats[source.stat_energy_from] this._data!.stats[source.stat_energy_from]
) || 0; ) || 0;
totalSolar += energy; totalSolar += energy;
const color =
const modifiedColor =
idx > 0 idx > 0
? rgb2hex( ? this.hass.themes.darkMode
lab2rgb(labDarken(rgb2lab(hex2rgb(solarColor)), idx)) ? labBrighten(rgb2lab(hex2rgb(solarColor)), idx)
) : labDarken(rgb2lab(hex2rgb(solarColor)), idx)
: solarColor; : undefined;
const color = modifiedColor
? rgb2hex(lab2rgb(modifiedColor))
: solarColor;
return html`<tr class="mdc-data-table__row"> return html`<tr class="mdc-data-table__row">
<td class="mdc-data-table__cell cell-bullet"> <td class="mdc-data-table__cell cell-bullet">
<div <div
@@ -229,22 +234,26 @@ export class HuiEnergySourcesTableCard
this._data!.stats[source.stat_energy_to] this._data!.stats[source.stat_energy_to]
) || 0; ) || 0;
totalBattery += energyFrom - energyTo; totalBattery += energyFrom - energyTo;
const fromColor =
const modifiedFromColor =
idx > 0 idx > 0
? rgb2hex( ? this.hass.themes.darkMode
lab2rgb( ? labBrighten(rgb2lab(hex2rgb(batteryFromColor)), idx)
labDarken(rgb2lab(hex2rgb(batteryFromColor)), idx) : labDarken(rgb2lab(hex2rgb(batteryFromColor)), idx)
) : undefined;
) const fromColor = modifiedFromColor
: batteryFromColor; ? rgb2hex(lab2rgb(modifiedFromColor))
const toColor = : batteryFromColor;
const modifiedToColor =
idx > 0 idx > 0
? rgb2hex( ? this.hass.themes.darkMode
lab2rgb( ? labBrighten(rgb2lab(hex2rgb(batteryToColor)), idx)
labDarken(rgb2lab(hex2rgb(batteryToColor)), idx) : labDarken(rgb2lab(hex2rgb(batteryToColor)), idx)
) : undefined;
) const toColor = modifiedToColor
: batteryToColor; ? rgb2hex(lab2rgb(modifiedToColor))
: batteryToColor;
return html`<tr class="mdc-data-table__row"> return html`<tr class="mdc-data-table__row">
<td class="mdc-data-table__cell cell-bullet"> <td class="mdc-data-table__cell cell-bullet">
<div <div
@@ -331,14 +340,17 @@ export class HuiEnergySourcesTableCard
if (cost !== null) { if (cost !== null) {
totalGridCost += cost; totalGridCost += cost;
} }
const color =
const modifiedColor =
idx > 0 idx > 0
? rgb2hex( ? this.hass.themes.darkMode
lab2rgb( ? labBrighten(rgb2lab(hex2rgb(consumptionColor)), idx)
labDarken(rgb2lab(hex2rgb(consumptionColor)), idx) : labDarken(rgb2lab(hex2rgb(consumptionColor)), idx)
) : undefined;
) const color = modifiedColor
: consumptionColor; ? rgb2hex(lab2rgb(modifiedColor))
: consumptionColor;
return html`<tr class="mdc-data-table__row"> return html`<tr class="mdc-data-table__row">
<td class="mdc-data-table__cell cell-bullet"> <td class="mdc-data-table__cell cell-bullet">
<div <div
@@ -391,12 +403,17 @@ export class HuiEnergySourcesTableCard
if (cost !== null) { if (cost !== null) {
totalGridCost += cost; totalGridCost += cost;
} }
const color =
const modifiedColor =
idx > 0 idx > 0
? rgb2hex( ? this.hass.themes.darkMode
lab2rgb(labDarken(rgb2lab(hex2rgb(returnColor)), idx)) ? labBrighten(rgb2lab(hex2rgb(returnColor)), idx)
) : labDarken(rgb2lab(hex2rgb(returnColor)), idx)
: returnColor; : undefined;
const color = modifiedColor
? rgb2hex(lab2rgb(modifiedColor))
: returnColor;
return html`<tr class="mdc-data-table__row"> return html`<tr class="mdc-data-table__row">
<td class="mdc-data-table__cell cell-bullet"> <td class="mdc-data-table__cell cell-bullet">
<div <div
@@ -473,12 +490,17 @@ export class HuiEnergySourcesTableCard
if (cost !== null) { if (cost !== null) {
totalGasCost += cost; totalGasCost += cost;
} }
const color =
const modifiedColor =
idx > 0 idx > 0
? rgb2hex( ? this.hass.themes.darkMode
lab2rgb(labDarken(rgb2lab(hex2rgb(gasColor)), idx)) ? labBrighten(rgb2lab(hex2rgb(gasColor)), idx)
) : labDarken(rgb2lab(hex2rgb(gasColor)), idx)
: gasColor; : undefined;
const color = modifiedColor
? rgb2hex(lab2rgb(modifiedColor))
: gasColor;
return html`<tr class="mdc-data-table__row"> return html`<tr class="mdc-data-table__row">
<td class="mdc-data-table__cell cell-bullet"> <td class="mdc-data-table__cell cell-bullet">
<div <div

View File

@@ -1,10 +1,10 @@
import { ChartData, ChartDataset, ChartOptions } from "chart.js"; import { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { import {
startOfToday, addHours,
differenceInDays,
endOfToday, endOfToday,
isToday, isToday,
differenceInDays, startOfToday,
addHours,
} from "date-fns"; } from "date-fns";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
@@ -17,7 +17,7 @@ import {
rgb2hex, rgb2hex,
rgb2lab, rgb2lab,
} from "../../../../common/color/convert-color"; } from "../../../../common/color/convert-color";
import { labDarken } from "../../../../common/color/lab"; import { labBrighten, labDarken } from "../../../../common/color/lab";
import { formatTime } from "../../../../common/datetime/format_time"; import { formatTime } from "../../../../common/datetime/format_time";
import { computeStateName } from "../../../../common/entity/compute_state_name"; import { computeStateName } from "../../../../common/entity/compute_state_name";
import { import {
@@ -477,10 +477,16 @@ export class HuiEnergyUsageGraphCard
Object.entries(sources).forEach(([statId, source], idx) => { Object.entries(sources).forEach(([statId, source], idx) => {
const data: ChartDataset<"bar">[] = []; const data: ChartDataset<"bar">[] = [];
const entity = this.hass.states[statId]; const entity = this.hass.states[statId];
const borderColor =
const modifiedColor =
idx > 0 idx > 0
? rgb2hex(lab2rgb(labDarken(rgb2lab(hex2rgb(colors[type])), idx))) ? this.hass.themes.darkMode
: colors[type]; ? labBrighten(rgb2lab(hex2rgb(colors[type])), idx)
: labDarken(rgb2lab(hex2rgb(colors[type])), idx)
: undefined;
const borderColor = modifiedColor
? rgb2hex(lab2rgb(modifiedColor))
: colors[type];
data.push({ data.push({
label: label:

View File

@@ -235,6 +235,9 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
<div> <div>
<ha-icon-button <ha-icon-button
.path=${mdiDotsVertical} .path=${mdiDotsVertical}
.label=${this.hass.localize(
"ui.panel.lovelace.cards.show_more_info"
)}
class="more-info" class="more-info"
@click=${this._handleMoreInfo} @click=${this._handleMoreInfo}
></ha-icon-button> ></ha-icon-button>

View File

@@ -19,7 +19,7 @@ import {
svg, svg,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, state, query } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { UNIT_F } from "../../../common/const"; import { UNIT_F } from "../../../common/const";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
@@ -427,6 +427,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
@click=${this._handleAction} @click=${this._handleAction}
tabindex="0" tabindex="0"
.path=${modeIcons[mode]} .path=${modeIcons[mode]}
.label=${this.hass!.localize(`component.climate.state._.${mode}`)}
> >
</ha-icon-button> </ha-icon-button>
`; `;

View File

@@ -50,6 +50,7 @@ export class HuiButtonsBase extends LitElement {
.stateObj=${stateObj} .stateObj=${stateObj}
.overrideIcon=${entityConf.icon} .overrideIcon=${entityConf.icon}
.overrideImage=${entityConf.image} .overrideImage=${entityConf.image}
class=${name ? "" : "no-text"}
stateColor stateColor
slot="icon" slot="icon"
></state-badge> ></state-badge>
@@ -85,9 +86,21 @@ export class HuiButtonsBase extends LitElement {
flex-wrap: wrap; flex-wrap: wrap;
} }
state-badge { state-badge {
display: inline-flex;
line-height: inherit; line-height: inherit;
text-align: start;
color: var(--secondary-text-color); color: var(--secondary-text-color);
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
margin-left: -4px;
margin-top: -2px;
}
state-badge.no-text {
width: 26px;
height: 26px;
margin-left: -3px;
margin-top: -3px;
} }
ha-chip { ha-chip {
padding: 4px; padding: 4px;

View File

@@ -30,6 +30,7 @@ import "../../../components/ha-slider";
import { UNAVAILABLE, UNAVAILABLE_STATES, UNKNOWN } from "../../../data/entity"; import { UNAVAILABLE, UNAVAILABLE_STATES, UNKNOWN } from "../../../data/entity";
import { import {
computeMediaDescription, computeMediaDescription,
ControlButton,
MediaPlayerEntity, MediaPlayerEntity,
SUPPORT_NEXT_TRACK, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PAUSE,
@@ -108,6 +109,7 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
} }
const entityState = stateObj.state; const entityState = stateObj.state;
const controlButton = this._computeControlButton(stateObj);
const buttons = html` const buttons = html`
${!this._narrow && ${!this._narrow &&
@@ -116,6 +118,9 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
? html` ? html`
<ha-icon-button <ha-icon-button
.path=${mdiSkipPrevious} .path=${mdiSkipPrevious}
.label=${this.hass.localize(
"ui.card.media_player.media_previous_track"
)}
@click=${this._previousTrack} @click=${this._previousTrack}
></ha-icon-button> ></ha-icon-button>
` `
@@ -130,7 +135,10 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
supportsFeature(stateObj, SUPPORT_PAUSE))) supportsFeature(stateObj, SUPPORT_PAUSE)))
? html` ? html`
<ha-icon-button <ha-icon-button
.path=${this._computeControlIcon(stateObj)} .path=${controlButton.icon}
.label=${this.hass.localize(
`ui.card.media_player.${controlButton.action}`
)}
@click=${this._playPauseStop} @click=${this._playPauseStop}
></ha-icon-button> ></ha-icon-button>
` `
@@ -140,6 +148,9 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
? html` ? html`
<ha-icon-button <ha-icon-button
.path=${mdiSkipNext} .path=${mdiSkipNext}
.label=${this.hass.localize(
"ui.card.media_player.media_next_track"
)}
@click=${this._nextTrack} @click=${this._nextTrack}
></ha-icon-button> ></ha-icon-button>
` `
@@ -162,6 +173,7 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
? html` ? html`
<ha-icon-button <ha-icon-button
.path=${mdiPower} .path=${mdiPower}
.label=${this.hass.localize("ui.card.media_player.turn_on")}
@click=${this._togglePower} @click=${this._togglePower}
></ha-icon-button> ></ha-icon-button>
` `
@@ -175,6 +187,7 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
? html` ? html`
<ha-icon-button <ha-icon-button
.path=${mdiPower} .path=${mdiPower}
.label=${this.hass.localize("ui.card.media_player.turn_off")}
@click=${this._togglePower} @click=${this._togglePower}
></ha-icon-button> ></ha-icon-button>
` `
@@ -193,6 +206,13 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
.path=${stateObj.attributes.is_volume_muted .path=${stateObj.attributes.is_volume_muted
? mdiVolumeOff ? mdiVolumeOff
: mdiVolumeHigh} : mdiVolumeHigh}
.label=${this.hass.localize(
`ui.card.media_player.${
stateObj.attributes.is_volume_muted
? "media_volume_mute"
: "media_volume_unmute"
}`
)}
@click=${this._toggleMute} @click=${this._toggleMute}
></ha-icon-button> ></ha-icon-button>
` `
@@ -214,10 +234,16 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
? html` ? html`
<ha-icon-button <ha-icon-button
.path=${mdiVolumeMinus} .path=${mdiVolumeMinus}
.label=${this.hass.localize(
"ui.card.media_player.media_volume_down"
)}
@click=${this._volumeDown} @click=${this._volumeDown}
></ha-icon-button> ></ha-icon-button>
<ha-icon-button <ha-icon-button
.path=${mdiVolumePlus} .path=${mdiVolumePlus}
.label=${this.hass.localize(
"ui.card.media_player.media_volume_up"
)}
@click=${this._volumeUp} @click=${this._volumeUp}
></ha-icon-button> ></ha-icon-button>
` `
@@ -249,14 +275,14 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
this._veryNarrow = (this.clientWidth || 0) < 225; this._veryNarrow = (this.clientWidth || 0) < 225;
} }
private _computeControlIcon(stateObj: HassEntity): string { private _computeControlButton(stateObj: HassEntity): ControlButton {
return stateObj.state === "on" return stateObj.state === "on"
? mdiPlayPause ? { icon: mdiPlayPause, action: "media_play_pause" }
: stateObj.state !== "playing" : stateObj.state !== "playing"
? mdiPlay ? { icon: mdiPlay, action: "media_play" }
: supportsFeature(stateObj, SUPPORT_PAUSE) : supportsFeature(stateObj, SUPPORT_PAUSE)
? mdiPause ? { icon: mdiPause, action: "media_pause" }
: mdiStop; : { icon: mdiStop, action: "media_stop" };
} }
private _togglePower(): void { private _togglePower(): void {

View File

@@ -698,7 +698,7 @@ class HUIRoot extends LitElement {
private _navigateToView(path: string | number, replace?: boolean) { private _navigateToView(path: string | number, replace?: boolean) {
if (!this.lovelace!.editMode) { if (!this.lovelace!.editMode) {
navigate(`${this.route!.prefix}/${path}`, { replace }); navigate(`${this.route!.prefix}/${path}${location.search}`, { replace });
return; return;
} }
navigate(`${this.route!.prefix}/${path}?${addSearchParam({ edit: "1" })}`, { navigate(`${this.route!.prefix}/${path}?${addSearchParam({ edit: "1" })}`, {

View File

@@ -0,0 +1,15 @@
import { Constructor } from "../types";
import { HassBaseEl } from "./hass-base-mixin";
export const ExternalMixin = <T extends Constructor<HassBaseEl>>(
superClass: T
) =>
class extends superClass {
protected hassConnected() {
super.hassConnected();
if (this.hass!.auth.external) {
this.hass!.auth.external.connection = this.hass!.connection;
}
}
};

View File

@@ -6,6 +6,7 @@ import DisconnectToastMixin from "./disconnect-toast-mixin";
import { hapticMixin } from "./haptic-mixin"; import { hapticMixin } from "./haptic-mixin";
import { HassBaseEl } from "./hass-base-mixin"; import { HassBaseEl } from "./hass-base-mixin";
import { loggingMixin } from "./logging-mixin"; import { loggingMixin } from "./logging-mixin";
import { ExternalMixin } from "./external-mixin";
import MoreInfoMixin from "./more-info-mixin"; import MoreInfoMixin from "./more-info-mixin";
import NotificationMixin from "./notification-mixin"; import NotificationMixin from "./notification-mixin";
import { panelTitleMixin } from "./panel-title-mixin"; import { panelTitleMixin } from "./panel-title-mixin";
@@ -31,4 +32,5 @@ export class HassElement extends ext(HassBaseEl, [
hapticMixin, hapticMixin,
panelTitleMixin, panelTitleMixin,
loggingMixin, loggingMixin,
ExternalMixin,
]) {} ]) {}

View File

@@ -131,5 +131,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
(themeMeta.getAttribute("default-content") as string); (themeMeta.getAttribute("default-content") as string);
themeMeta.setAttribute("content", themeColor); themeMeta.setAttribute("content", themeColor);
} }
this.hass!.auth.external?.fireMessage({ type: "theme-update" });
} }
}; };

View File

@@ -195,8 +195,14 @@
"turn_off": "Turn off", "turn_off": "Turn off",
"media_play": "Play", "media_play": "Play",
"media_play_pause": "Play/pause", "media_play_pause": "Play/pause",
"media_next_track": "Next", "media_pause": "Pause",
"media_previous_track": "Previous", "media_stop": "Stop",
"media_next_track": "Next track",
"media_previous_track": "Previous track",
"media_volume_up": "Volume up",
"media_volume_down": "Volume down",
"media_volume_mute": "Volume mute",
"media_volume_unmute": "Volume unmute",
"text_to_speak": "Text to speak" "text_to_speak": "Text to speak"
}, },
"persistent_notification": { "persistent_notification": {
@@ -291,12 +297,14 @@
"undo": "Undo", "undo": "Undo",
"move": "Move", "move": "Move",
"save": "Save", "save": "Save",
"submit": "Submit",
"rename": "Rename", "rename": "Rename",
"yes": "Yes", "yes": "Yes",
"no": "No", "no": "No",
"not_now": "Not now", "not_now": "Not now",
"skip": "Skip", "skip": "Skip",
"menu": "Menu", "menu": "Menu",
"overflow_menu": "Overflow menu",
"help": "Help", "help": "Help",
"successfully_saved": "Successfully saved", "successfully_saved": "Successfully saved",
"successfully_deleted": "Successfully deleted", "successfully_deleted": "Successfully deleted",
@@ -421,6 +429,7 @@
} }
}, },
"related-filter-menu": { "related-filter-menu": {
"filter": "Filter",
"filter_by_entity": "Filter by entity", "filter_by_entity": "Filter by entity",
"filter_by_device": "Filter by device", "filter_by_device": "Filter by device",
"filter_by_area": "Filter by area", "filter_by_area": "Filter by area",
@@ -538,6 +547,13 @@
}, },
"attributes": { "attributes": {
"expansion_header": "Attributes" "expansion_header": "Attributes"
},
"qr-scanner": {
"select_camera": "Select camera",
"only_https_supported": "You can only use your camera to scan a QR code when using HTTPS.",
"not_supported": "Your browser doesn't support QR scanning.",
"manual_input": "You can scan the QR code with another QR scanner and paste the code in the input below",
"enter_qr_code": "Enter QR code value"
} }
}, },
"dialogs": { "dialogs": {
@@ -678,8 +694,8 @@
"open_cover": "Open cover", "open_cover": "Open cover",
"close_cover": "Close cover", "close_cover": "Close cover",
"open_tilt_cover": "Open cover tilt", "open_tilt_cover": "Open cover tilt",
"close_tile_cover": "Close cover tilt", "close_tilt_cover": "Close cover tilt",
"stop_cover": "Stop cover from moving" "stop_cover": "Stop cover"
} }
}, },
"entity_registry": { "entity_registry": {
@@ -912,6 +928,7 @@
"dismiss": "Dismiss" "dismiss": "Dismiss"
}, },
"sidebar": { "sidebar": {
"external_app_configuration": "App Configuration",
"sidebar_toggle": "Sidebar Toggle", "sidebar_toggle": "Sidebar Toggle",
"done": "Done", "done": "Done",
"hide_panel": "Hide panel", "hide_panel": "Hide panel",
@@ -928,9 +945,47 @@
}, },
"config": { "config": {
"header": "Configure Home Assistant", "header": "Configure Home Assistant",
"advanced_mode": { "dashboard": {
"hint_enable": "Missing config options? Enable advanced mode on", "devices": {
"link_profile_page": "your profile page" "title": "Devices & Services",
"description": "Integrations, devices, entities and areas"
},
"automations": {
"title": "Automations & Scenes",
"description": "Manage automations, scenes, scripts and helpers"
},
"blueprints": {
"title": "Blueprints",
"description": "Pre-made automations and scripts by the community"
},
"supervisor": {
"title": "Add-ons, Backups & Supervisor",
"description": "Create backups, check logs or reboot your system"
},
"dashboards": {
"title": "Dashboards",
"description": "Create customized sets of cards to control your home"
},
"energy": {
"title": "Energy",
"description": "Monitor your energy production and consumption"
},
"tags": {
"title": "Tags",
"description": "Trigger automations when a NFC tag, QR code, etc. is scanned"
},
"people": {
"title": "People & Zones",
"description": "Manage the people and zones that Home Assistant tracks"
},
"companion": {
"title": "Companion App",
"description": "Location and notifications"
},
"settings": {
"title": "Settings",
"description": "Basic settings, server controls, logs and info"
}
}, },
"common": { "common": {
"editor": { "editor": {
@@ -1108,7 +1163,7 @@
"cost_number": "Use a static price", "cost_number": "Use a static price",
"cost_number_input": "Price per {unit}", "cost_number_input": "Price per {unit}",
"gas_usage": "Gas usage", "gas_usage": "Gas usage",
"m3_or_kWh": " or kWh" "m3_or_kWh": "ft³, m³, Wh, kWh or MWh"
} }
}, },
"device_consumption": { "device_consumption": {
@@ -1149,19 +1204,19 @@
}, },
"entity_unexpected_unit_energy": { "entity_unexpected_unit_energy": {
"title": "Unexpected unit of measurement", "title": "Unexpected unit of measurement",
"description": "The following entities do not have the expected units of measurement 'kWh' or 'Wh':" "description": "The following entities do not have the expected units of measurement 'Wh', 'kWh' or 'MWh':"
}, },
"entity_unexpected_unit_gas": { "entity_unexpected_unit_gas": {
"title": "Unexpected unit of measurement", "title": "Unexpected unit of measurement",
"description": "The following entities do not have the expected units of measurement 'kWh', 'm³' or 'ft³':" "description": "The following entities do not have the expected units of measurement 'Wh', 'kWh' or 'MWh' for an energy sensor or 'm³' or 'ft³' for a gas sensor:"
}, },
"entity_unexpected_unit_energy_price": { "entity_unexpected_unit_energy_price": {
"title": "Unexpected unit of measurement", "title": "Unexpected unit of measurement",
"description": "The following entities do not have the expected units of measurement ''{currency}/kWh'' or ''{currency}/Wh'':" "description": "The following entities do not have the expected units of measurement ''{currency}/kWh'', ''{currency}/Wh'' or ''{currency}/MWh'':"
}, },
"entity_unexpected_unit_gas_price": { "entity_unexpected_unit_gas_price": {
"title": "Unexpected unit of measurement", "title": "Unexpected unit of measurement",
"description": "The following entities do not have the expected units of measurement ''{currency}/kWh'', ''{currency}/Wh'', ''{currency}/m³'' or ''{currency}/ft³'':" "description": "The following entities do not have the expected units of measurement ''{currency}/kWh'', ''{currency}/Wh'', ''{currency}/MWh'', ''{currency}/m³'' or ''{currency}/ft³'':"
}, },
"entity_unexpected_state_class": { "entity_unexpected_state_class": {
"title": "Unexpected state class", "title": "Unexpected state class",
@@ -2389,6 +2444,7 @@
"manuf": "by {manufacturer}", "manuf": "by {manufacturer}",
"via": "Connected via", "via": "Connected via",
"firmware": "Firmware: {version}", "firmware": "Firmware: {version}",
"hardware": "Hardware: {version}",
"unnamed_entry": "Unnamed entry", "unnamed_entry": "Unnamed entry",
"unknown_via_device": "Unknown device", "unknown_via_device": "Unknown device",
"area": "In {area}", "area": "In {area}",
@@ -2477,7 +2533,7 @@
"admin": "Administrator", "admin": "Administrator",
"group": "Group", "group": "Group",
"active": "Active", "active": "Active",
"local_only": "Can only login from the local network", "local_only": "Can only log in from the local network",
"system_generated": "System generated", "system_generated": "System generated",
"system_generated_users_not_removable": "Unable to remove system generated users.", "system_generated_users_not_removable": "Unable to remove system generated users.",
"system_generated_users_not_editable": "Unable to update system generated users.", "system_generated_users_not_editable": "Unable to update system generated users.",
@@ -2813,7 +2869,7 @@
"node_id": "Device ID", "node_id": "Device ID",
"home_id": "Home ID", "home_id": "Home ID",
"source": "Source", "source": "Source",
"close": "Close", "back": "Back",
"add_node": "Add device", "add_node": "Add device",
"remove_node": "Remove device", "remove_node": "Remove device",
"reconfigure_server": "Re-configure Server", "reconfigure_server": "Re-configure Server",
@@ -2882,8 +2938,6 @@
"qr_code": "QR Code", "qr_code": "QR Code",
"qr_code_paragraph": "If your device supports SmartStart you can scan the QR code for easy pairing.", "qr_code_paragraph": "If your device supports SmartStart you can scan the QR code for easy pairing.",
"scan_qr_code": "Scan QR code", "scan_qr_code": "Scan QR code",
"enter_qr_code": "Enter QR code value",
"select_camera": "Select camera",
"inclusion_failed": "The device could not be added.", "inclusion_failed": "The device could not be added.",
"check_logs": "Please check the logs for more information.", "check_logs": "Please check the logs for more information.",
"inclusion_finished": "The device has been added.", "inclusion_finished": "The device has been added.",
@@ -2977,7 +3031,8 @@
"title": "Z-Wave JS Logs", "title": "Z-Wave JS Logs",
"log_level": "Log Level", "log_level": "Log Level",
"subscribed_to_logs": "Subscribed to Z-Wave JS Log Messages…", "subscribed_to_logs": "Subscribed to Z-Wave JS Log Messages…",
"log_level_changed": "Log Level changed to: {level}" "log_level_changed": "Log Level changed to: {level}",
"download_logs": "Download logs"
} }
} }
}, },
@@ -4222,7 +4277,8 @@
"create_backup": "Create backup before updating", "create_backup": "Create backup before updating",
"description": "You have {version} installed. Click update to update to version {newest_version}", "description": "You have {version} installed. Click update to update to version {newest_version}",
"updating": "Updating {name} to version {version}", "updating": "Updating {name} to version {version}",
"creating_backup": "Creating backup of {name}" "creating_backup": "Creating backup of {name}",
"no_update": "No update available for {name}"
}, },
"confirm": { "confirm": {
"restart": { "restart": {

View File

@@ -2,16 +2,16 @@ import { assert } from "chai";
import { import {
ExternalMessaging, ExternalMessaging,
InternalMessage, EMMessage,
} from "../../src/external_app/external_messaging"; } from "../../src/external_app/external_messaging";
// @ts-ignore // @ts-ignore
global.__DEV__ = true; global.__DEV__ = true;
class MockExternalMessaging extends ExternalMessaging { class MockExternalMessaging extends ExternalMessaging {
public mockSent: InternalMessage[] = []; public mockSent: EMMessage[] = [];
protected _sendExternal(msg: InternalMessage) { protected _sendExternal(msg: EMMessage) {
this.mockSent.push(msg); this.mockSent.push(msg);
} }
} }