Merge pull request #10846 from home-assistant/dev

This commit is contained in:
Paulus Schoutsen 2021-12-09 14:05:30 -08:00 committed by GitHub
commit cdc3d11181
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 320 additions and 190 deletions

View File

@ -120,7 +120,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

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="20211209.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

@ -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

@ -1,6 +1,6 @@
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 { 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 +8,7 @@ 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";
@customElement("ha-qr-scanner") @customElement("ha-qr-scanner")
class HaQrScanner extends LitElement { class HaQrScanner extends LitElement {
@ -58,29 +59,37 @@ 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">
${this._cameras && this._cameras.length > 1
? html`<ha-button-menu
corner="BOTTOM_START"
fixed
@closed=${stopPropagation}
>
<ha-icon-button
slot="trigger"
.label=${this.localize(
"ui.panel.config.zwave_js.add_node.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" : html`<ha-alert alert-type="warning"
>${!window.isSecureContext >${!window.isSecureContext
? "You can only use your camera to scan a QR core when using HTTPS." ? "You can only use your camera to scan a QR core when using HTTPS."
@ -135,16 +144,23 @@ class HaQrScanner extends LitElement {
}; };
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%;
} }
`; `;
} }

View File

@ -43,6 +43,7 @@ import {
PersistentNotification, PersistentNotification,
subscribeNotifications, subscribeNotifications,
} from "../data/persistent_notification"; } from "../data/persistent_notification";
import { 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";
@ -266,6 +267,11 @@ class HaSidebar extends LitElement {
subscribeNotifications(this.hass.connection, (notifications) => { subscribeNotifications(this.hass.connection, (notifications) => {
this._notifications = notifications; this._notifications = notifications;
}); });
// Temporary workaround for a bug in Android. Can be removed in Home Assistant 2022.2
if (this.hass.auth.external) {
getExternalConfig(this.hass.auth.external);
}
} }
protected updated(changedProps) { protected updated(changedProps) {

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

@ -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

@ -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

@ -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

@ -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

@ -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/core",
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

@ -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";
@ -183,17 +182,9 @@ class DialogZWaveJSAddNode extends LitElement {
.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>
@ -274,7 +265,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 +364,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 +501,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 +556,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";
@ -636,6 +614,11 @@ class DialogZWaveJSAddNode extends LitElement {
ZWaveFeature.SmartStart ZWaveFeature.SmartStart
) )
).supported; ).supported;
this._supportsSmartStart = true;
}
private _startOver(_ev: Event) {
this._startInclusion();
} }
private _startInclusion( private _startInclusion(

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,72 @@ 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">
<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>

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

@ -928,9 +928,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": "Manage blueprints"
},
"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": {
@ -2813,7 +2851,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",

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);
} }
} }