Merge pull request #10818 from home-assistant/dev

This commit is contained in:
Paulus Schoutsen 2021-12-06 15:21:37 -08:00 committed by GitHub
commit 8f729e2a95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 250 additions and 116 deletions

View File

@ -48,7 +48,6 @@ import "../../../src/layouts/hass-subpage";
import "../../../src/layouts/hass-tabs-subpage"; import "../../../src/layouts/hass-tabs-subpage";
import { SUPERVISOR_UPDATE_NAMES } from "../../../src/panels/config/dashboard/ha-config-updates"; import { SUPERVISOR_UPDATE_NAMES } from "../../../src/panels/config/dashboard/ha-config-updates";
import { HomeAssistant, Route } from "../../../src/types"; import { HomeAssistant, Route } from "../../../src/types";
import { documentationUrl } from "../../../src/util/documentation-url";
import { addonArchIsSupported, extractChangelog } from "../util/addon"; import { addonArchIsSupported, extractChangelog } from "../util/addon";
declare global { declare global {
@ -60,7 +59,6 @@ declare global {
type updateType = "os" | "supervisor" | "core" | "addon"; type updateType = "os" | "supervisor" | "core" | "addon";
const changelogUrl = ( const changelogUrl = (
hass: HomeAssistant,
entry: updateType, entry: updateType,
version: string version: string
): string | undefined => { ): string | undefined => {
@ -68,17 +66,19 @@ const changelogUrl = (
return undefined; return undefined;
} }
if (entry === "core") { if (entry === "core") {
return version?.includes("dev") return version.includes("dev")
? "https://github.com/home-assistant/core/commits/dev" ? "https://github.com/home-assistant/core/commits/dev"
: documentationUrl(hass, "/latest-release-notes/"); : version.includes("b")
? "https://next.home-assistant.io/latest-release-notes/"
: "https://www.home-assistant.io/latest-release-notes/";
} }
if (entry === "os") { if (entry === "os") {
return version?.includes("dev") return version.includes("dev")
? "https://github.com/home-assistant/operating-system/commits/dev" ? "https://github.com/home-assistant/operating-system/commits/dev"
: `https://github.com/home-assistant/operating-system/releases/tag/${version}`; : `https://github.com/home-assistant/operating-system/releases/tag/${version}`;
} }
if (entry === "supervisor") { if (entry === "supervisor") {
return version?.includes("dev") return version.includes("dev")
? "https://github.com/home-assistant/supervisor/commits/main" ? "https://github.com/home-assistant/supervisor/commits/main"
: `https://github.com/home-assistant/supervisor/releases/tag/${version}`; : `https://github.com/home-assistant/supervisor/releases/tag/${version}`;
} }
@ -120,7 +120,7 @@ class UpdateAvailableCard extends LitElement {
return html``; return html``;
} }
const changelog = changelogUrl(this.hass, this._updateType, this._version); const changelog = changelogUrl(this._updateType, this._version);
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="20211203.0", version="20211206.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

@ -221,6 +221,7 @@ export const DOMAINS_INPUT_ROW = [
"scene", "scene",
"script", "script",
"select", "select",
"switch",
]; ];
/** Domains that should have the history hidden in the more info dialog. */ /** Domains that should have the history hidden in the more info dialog. */

View File

@ -96,7 +96,11 @@ export class HaDateInput extends LitElement {
attr-for-value="value" attr-for-value="value"
.i18n=${i18n} .i18n=${i18n}
> >
<paper-input .label=${this.label} no-label-float> <paper-input
.label=${this.label}
.disabled=${this.disabled}
no-label-float
>
<ha-svg-icon slot="suffix" .path=${mdiCalendar}></ha-svg-icon> <ha-svg-icon slot="suffix" .path=${mdiCalendar}></ha-svg-icon>
</paper-input> </paper-input>
</vaadin-date-picker-light>`; </vaadin-date-picker-light>`;

View File

@ -39,6 +39,7 @@ export class HaPictureUpload extends LitElement {
.uploading=${this._uploading} .uploading=${this._uploading}
.value=${this.value ? html`<img .src=${this.value} />` : ""} .value=${this.value ? html`<img .src=${this.value} />` : ""}
@file-picked=${this._handleFilePicked} @file-picked=${this._handleFilePicked}
@change=${this._handleFileCleared}
accept="image/png, image/jpeg, image/gif" accept="image/png, image/jpeg, image/gif"
></ha-file-upload> ></ha-file-upload>
`; `;
@ -53,6 +54,10 @@ export class HaPictureUpload extends LitElement {
} }
} }
private async _handleFileCleared() {
this.value = null;
}
private async _cropFile(file: File) { private async _cropFile(file: File) {
if (!["image/png", "image/jpeg", "image/gif"].includes(file.type)) { if (!["image/png", "image/jpeg", "image/gif"].includes(file.type)) {
showAlertDialog(this, { showAlertDialog(this, {

View File

@ -208,11 +208,11 @@ export const enum NodeStatus {
export interface ZwaveJSProvisioningEntry { export interface ZwaveJSProvisioningEntry {
/** The device specific key (DSK) in the form aaaaa-bbbbb-ccccc-ddddd-eeeee-fffff-11111-22222 */ /** The device specific key (DSK) in the form aaaaa-bbbbb-ccccc-ddddd-eeeee-fffff-11111-22222 */
dsk: string; dsk: string;
securityClasses: SecurityClass[]; security_classes: SecurityClass[];
/** additional_properties: {
* Additional properties to be stored in this provisioning entry, e.g. the device ID from a scanned QR code nodeId?: number;
*/ [prop: string]: any;
[prop: string]: any; };
} }
export interface RequestedGrant { export interface RequestedGrant {
@ -278,7 +278,7 @@ export const setZwaveDataCollectionPreference = (
export const fetchZwaveProvisioningEntries = ( export const fetchZwaveProvisioningEntries = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string entry_id: string
): Promise<any> => ): Promise<ZwaveJSProvisioningEntry[]> =>
hass.callWS({ hass.callWS({
type: "zwave_js/get_provisioning_entries", type: "zwave_js/get_provisioning_entries",
entry_id, entry_id,

View File

@ -1,17 +1,27 @@
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 { createDurationData } from "../../../../../common/datetime/create_duration_data"; import { createDurationData } from "../../../../../common/datetime/create_duration_data";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/entity/ha-entity-attribute-picker"; import "../../../../../components/entity/ha-entity-attribute-picker";
import "../../../../../components/entity/ha-entity-picker"; import "../../../../../components/entity/ha-entity-picker";
import "../../../../../components/ha-duration-input";
import { StateCondition } from "../../../../../data/automation"; import { StateCondition } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import { forDictStruct } from "../../structs";
import { import {
ConditionElement, ConditionElement,
handleChangeEvent, handleChangeEvent,
} from "../ha-automation-condition-row"; } from "../ha-automation-condition-row";
import "../../../../../components/ha-duration-input";
import { fireEvent } from "../../../../../common/dom/fire_event"; const stateConditionStruct = object({
condition: literal("state"),
entity_id: string(),
attribute: optional(string()),
state: string(),
for: optional(union([string(), forDictStruct])),
});
@customElement("ha-automation-condition-state") @customElement("ha-automation-condition-state")
export class HaStateCondition extends LitElement implements ConditionElement { export class HaStateCondition extends LitElement implements ConditionElement {
@ -23,19 +33,14 @@ export class HaStateCondition extends LitElement implements ConditionElement {
return { entity_id: "", state: "" }; return { entity_id: "", state: "" };
} }
public willUpdate(changedProperties: PropertyValues): boolean { public shouldUpdate(changedProperties: PropertyValues) {
if ( if (changedProperties.has("condition")) {
changedProperties.has("condition") && try {
Array.isArray(this.condition?.state) assert(this.condition, stateConditionStruct);
) { } catch (e: any) {
fireEvent( fireEvent(this, "ui-mode-not-available", e);
this, return false;
"ui-mode-not-available", }
Error(this.hass.localize("ui.errors.config.no_state_array_support"))
);
// We have to stop the update if state is an array.
// Otherwise the state will be changed to a comma-separated string by the input element.
return false;
} }
return true; return true;
} }

View File

@ -0,0 +1,8 @@
import { object, optional, number } from "superstruct";
export const forDictStruct = object({
days: optional(number()),
hours: optional(number()),
minutes: optional(number()),
seconds: optional(number()),
});

View File

@ -68,7 +68,7 @@ export const handleChangeEvent = (element: TriggerElement, ev: CustomEvent) => {
} }
let newTrigger: Trigger; let newTrigger: Trigger;
if (!newVal) { if (newVal === undefined || newVal === "") {
newTrigger = { ...element.trigger }; newTrigger = { ...element.trigger };
delete newTrigger[name]; delete newTrigger[name];
} else { } else {

View File

@ -1,19 +1,30 @@
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 { 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";
import "../../../../../components/entity/ha-entity-attribute-picker"; import "../../../../../components/entity/ha-entity-attribute-picker";
import "../../../../../components/entity/ha-entity-picker"; import "../../../../../components/entity/ha-entity-picker";
import "../../../../../components/ha-duration-input";
import { StateTrigger } from "../../../../../data/automation"; import { StateTrigger } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-duration-input"; import { forDictStruct } from "../../structs";
import { import {
handleChangeEvent, handleChangeEvent,
TriggerElement, TriggerElement,
} from "../ha-automation-trigger-row"; } from "../ha-automation-trigger-row";
const stateTriggerStruct = object({
platform: literal("state"),
entity_id: string(),
attribute: optional(string()),
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 {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -24,9 +35,9 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
return { entity_id: "" }; return { entity_id: "" };
} }
public willUpdate(changedProperties: PropertyValues) { public shouldUpdate(changedProperties: PropertyValues) {
if (!changedProperties.has("trigger")) { if (!changedProperties.has("trigger")) {
return; return true;
} }
// 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)) {
@ -35,7 +46,15 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
"ui-mode-not-available", "ui-mode-not-available",
Error(this.hass.localize("ui.errors.config.no_template_editor_support")) Error(this.hass.localize("ui.errors.config.no_template_editor_support"))
); );
return false;
} }
try {
assert(this.trigger, stateTriggerStruct);
} catch (e: any) {
fireEvent(this, "ui-mode-not-available", e);
return false;
}
return true;
} }
protected render() { protected render() {

View File

@ -224,7 +224,7 @@ class HaBlueprintOverview extends LitElement {
.narrow=${this.narrow} .narrow=${this.narrow}
back-path="/config" back-path="/config"
.route=${this.route} .route=${this.route}
.tabs=${configSections.automations} .tabs=${configSections.blueprints}
.columns=${this._columns(this.narrow, this.hass.language)} .columns=${this._columns(this.narrow, this.hass.language)}
.data=${this._processedBlueprints(this.blueprints)} .data=${this._processedBlueprints(this.blueprints)}
id="entity_id" id="entity_id"

View File

@ -1,4 +1,4 @@
import { mdiCellphoneCog, mdiCloudLock } from "@mdi/js"; import { mdiCloudLock } from "@mdi/js";
import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/app-layout/app-toolbar/app-toolbar";
import { import {
@ -110,29 +110,10 @@ class HaConfigDashboard extends LitElement {
></ha-config-navigation> ></ha-config-navigation>
` `
: ""} : ""}
${this._externalConfig?.hasSettingsScreen
? html`
<ha-config-navigation
.hass=${this.hass}
.narrow=${this.narrow}
.showAdvanced=${this.showAdvanced}
.pages=${[
{
path: "#external-app-configuration",
name: "Companion App",
description: "Location and notifications",
iconPath: mdiCellphoneCog,
iconColor: "#37474F",
core: true,
},
]}
@click=${this._handleExternalAppConfiguration}
></ha-config-navigation>
`
: ""}
<ha-config-navigation <ha-config-navigation
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.externalConfig=${this._externalConfig}
.showAdvanced=${this.showAdvanced} .showAdvanced=${this.showAdvanced}
.pages=${configSections.dashboard} .pages=${configSections.dashboard}
></ha-config-navigation> ></ha-config-navigation>
@ -142,13 +123,6 @@ class HaConfigDashboard extends LitElement {
`; `;
} }
private _handleExternalAppConfiguration(ev: Event) {
ev.preventDefault();
this.hass.auth.external!.fireMessage({
type: "config_screen/show",
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,

View File

@ -6,6 +6,7 @@ import { canShowPage } from "../../../common/config/can_show_page";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-icon-next"; import "../../../components/ha-icon-next";
import { CloudStatus, CloudStatusLoggedIn } from "../../../data/cloud"; import { CloudStatus, CloudStatusLoggedIn } from "../../../data/cloud";
import { ExternalConfig } from "../../../external_app/external_config";
import { PageNavigation } from "../../../layouts/hass-tabs-subpage"; import { PageNavigation } from "../../../layouts/hass-tabs-subpage";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
@ -19,10 +20,16 @@ class HaConfigNavigation extends LitElement {
@property() public pages!: PageNavigation[]; @property() public pages!: PageNavigation[];
@property() public externalConfig?: ExternalConfig;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
${this.pages.map((page) => ${this.pages.map((page) =>
canShowPage(this.hass, page) (
page.path === "#external-app-configuration"
? this.externalConfig?.hasSettingsScreen
: canShowPage(this.hass, page)
)
? html` ? html`
<a href=${page.path} aria-role="option" tabindex="-1"> <a href=${page.path} aria-role="option" tabindex="-1">
<paper-icon-item @click=${this._entryClicked}> <paper-icon-item @click=${this._entryClicked}>
@ -77,6 +84,16 @@ class HaConfigNavigation extends LitElement {
private _entryClicked(ev) { private _entryClicked(ev) {
ev.currentTarget.blur(); ev.currentTarget.blur();
if (
ev.currentTarget.parentElement.href.endsWith(
"#external-app-configuration"
)
) {
ev.preventDefault();
this.hass.auth.external!.fireMessage({
type: "config_screen/show",
});
}
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@ -1,6 +1,7 @@
import { import {
mdiAccount, mdiAccount,
mdiBadgeAccountHorizontal, mdiBadgeAccountHorizontal,
mdiCellphoneCog,
mdiCog, mdiCog,
mdiDevices, mdiDevices,
mdiHomeAssistant, mdiHomeAssistant,
@ -57,22 +58,22 @@ export const configSections: { [name: string]: PageNavigation[] } = {
{ {
path: "/config/automation", path: "/config/automation",
name: "Automations & Scenes", name: "Automations & Scenes",
description: "Automations, blueprints, scenes and scripts", description: "Manage automations, scenes, scripts and helpers",
iconPath: mdiRobot, iconPath: mdiRobot,
iconColor: "#518C43", iconColor: "#518C43",
components: ["automation", "blueprint", "scene", "script"],
},
{
path: "/config/helpers",
name: "Automation Helpers",
description: "Elements that help build automations",
iconPath: mdiTools,
iconColor: "#4D2EA4",
core: true, core: true,
}, },
{
path: "/config/blueprint",
name: "Blueprints",
description: "Manage blueprints",
iconPath: mdiPaletteSwatch,
iconColor: "#64B5F6",
component: "blueprint",
},
{ {
path: "/hassio", path: "/hassio",
name: "Add-ons & Backups (Supervisor)", name: "Add-ons, Backups & Supervisor",
description: "Create backups, check logs or reboot your system", description: "Create backups, check logs or reboot your system",
iconPath: mdiHomeAssistant, iconPath: mdiHomeAssistant,
iconColor: "#4084CD", iconColor: "#4084CD",
@ -111,6 +112,13 @@ export const configSections: { [name: string]: PageNavigation[] } = {
iconColor: "#E48629", iconColor: "#E48629",
components: ["person", "zone", "users"], components: ["person", "zone", "users"],
}, },
{
path: "#external-app-configuration",
name: "Companion App",
description: "Location and notifications",
iconPath: mdiCellphoneCog,
iconColor: "#8E24AA",
},
{ {
path: "/config/core", path: "/config/core",
name: "Settings", name: "Settings",
@ -155,13 +163,6 @@ export const configSections: { [name: string]: PageNavigation[] } = {
}, },
], ],
automations: [ automations: [
{
component: "blueprint",
path: "/config/blueprint",
translationKey: "ui.panel.config.blueprint.caption",
iconPath: mdiPaletteSwatch,
iconColor: "#518C43",
},
{ {
component: "automation", component: "automation",
path: "/config/automation", path: "/config/automation",
@ -183,8 +184,6 @@ export const configSections: { [name: string]: PageNavigation[] } = {
iconPath: mdiScriptText, iconPath: mdiScriptText,
iconColor: "#518C43", iconColor: "#518C43",
}, },
],
helpers: [
{ {
component: "helpers", component: "helpers",
path: "/config/helpers", path: "/config/helpers",
@ -194,6 +193,15 @@ export const configSections: { [name: string]: PageNavigation[] } = {
core: true, core: true,
}, },
], ],
blueprints: [
{
component: "blueprint",
path: "/config/blueprint",
translationKey: "ui.panel.config.blueprint.caption",
iconPath: mdiPaletteSwatch,
iconColor: "#518C43",
},
],
tags: [ tags: [
{ {
component: "tag", component: "tag",
@ -447,9 +455,19 @@ class HaPanelConfig extends HassRouterPage {
this.hass.loadBackendTranslation("title"); this.hass.loadBackendTranslation("title");
if (isComponentLoaded(this.hass, "cloud")) { if (isComponentLoaded(this.hass, "cloud")) {
this._updateCloudStatus(); this._updateCloudStatus();
this.addEventListener("connection-status", (ev) => {
if (ev.detail === "connected") {
this._updateCloudStatus();
}
});
} }
if (isComponentLoaded(this.hass, "hassio")) { if (isComponentLoaded(this.hass, "hassio")) {
this._loadSupervisorUpdates(); this._loadSupervisorUpdates();
this.addEventListener("connection-status", (ev) => {
if (ev.detail === "connected") {
this._loadSupervisorUpdates();
}
});
} else { } else {
this._supervisorUpdates = null; this._supervisorUpdates = null;
} }

View File

@ -132,7 +132,7 @@ export class HaConfigHelpers extends LitElement {
.narrow=${this.narrow} .narrow=${this.narrow}
back-path="/config" back-path="/config"
.route=${this.route} .route=${this.route}
.tabs=${configSections.helpers} .tabs=${configSections.automations}
.columns=${this._columns(this.narrow, this.hass.language)} .columns=${this._columns(this.narrow, this.hass.language)}
.data=${this._getItems(this._stateItems)} .data=${this._getItems(this._stateItems)}
@row-click=${this._openEditDialog} @row-click=${this._openEditDialog}

View File

@ -1,6 +1,7 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property } from "lit/decorators"; import { property } from "lit/decorators";
import "../../../layouts/hass-tabs-subpage"; import "../../../layouts/hass-tabs-subpage";
import "../../../components/ha-logo-svg";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url"; import { documentationUrl } from "../../../util/documentation-url";
@ -40,13 +41,14 @@ class HaConfigInfo extends LitElement {
href=${documentationUrl(this.hass, "")} href=${documentationUrl(this.hass, "")}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
><img >
src="/static/icons/favicon-192x192.png" <ha-logo-svg
height="192" title=${this.hass.localize(
alt=${this.hass.localize(
"ui.panel.config.info.home_assistant_logo" "ui.panel.config.info.home_assistant_logo"
)} )}
/></a> >
</ha-logo-svg>
</a>
<br /> <br />
<h2>Home Assistant ${hass.connection.haVersion}</h2> <h2>Home Assistant ${hass.connection.haVersion}</h2>
<p> <p>
@ -193,6 +195,11 @@ class HaConfigInfo extends LitElement {
margin: 0 auto; margin: 0 auto;
padding-bottom: 16px; padding-bottom: 16px;
} }
ha-logo-svg {
padding: 12px;
height: 180px;
width: 180px;
}
`, `,
]; ];
} }

View File

@ -45,6 +45,8 @@ export interface ZWaveJSAddNodeDevice {
class DialogZWaveJSAddNode extends LitElement { class DialogZWaveJSAddNode extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: ZWaveJSAddNodeDialogParams;
@state() private _entryId?: string; @state() private _entryId?: string;
@state() private _status?: @state() private _status?:
@ -91,6 +93,7 @@ class DialogZWaveJSAddNode extends LitElement {
} }
public async showDialog(params: ZWaveJSAddNodeDialogParams): Promise<void> { public async showDialog(params: ZWaveJSAddNodeDialogParams): Promise<void> {
this._params = params;
this._entryId = params.entry_id; this._entryId = params.entry_id;
this._status = "loading"; this._status = "loading";
this._checkSmartStartSupport(); this._checkSmartStartSupport();
@ -562,6 +565,9 @@ class DialogZWaveJSAddNode extends LitElement {
provisioningInfo provisioningInfo
); );
this._status = "provisioned"; this._status = "provisioned";
if (this._params?.addedCallback) {
this._params.addedCallback();
}
} catch (err: any) { } catch (err: any) {
this._error = err.message; this._error = err.message;
this._status = "failed"; this._status = "failed";
@ -693,6 +699,9 @@ class DialogZWaveJSAddNode extends LitElement {
if (message.event === "interview completed") { if (message.event === "interview completed") {
this._unsubscribe(); this._unsubscribe();
this._status = "finished"; this._status = "finished";
if (this._params?.addedCallback) {
this._params.addedCallback();
}
} }
if (message.event === "interview stage completed") { if (message.event === "interview stage completed") {

View File

@ -2,6 +2,7 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
export interface ZWaveJSAddNodeDialogParams { export interface ZWaveJSAddNodeDialogParams {
entry_id: string; entry_id: string;
addedCallback?: () => void;
} }
export const loadAddNodeDialog = () => import("./dialog-zwave_js-add-node"); export const loadAddNodeDialog = () => import("./dialog-zwave_js-add-node");

View File

@ -411,6 +411,7 @@ class ZWaveJSConfigDashboard extends LitElement {
private async _addNodeClicked() { private async _addNodeClicked() {
showZWaveJSAddNodeDialog(this, { showZWaveJSAddNodeDialog(this, {
entry_id: this.configEntryId!, entry_id: this.configEntryId!,
addedCallback: () => this._fetchData(),
}); });
} }

View File

@ -327,6 +327,9 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
if (!("states" in item.metadata)) { if (!("states" in item.metadata)) {
return false; return false;
} }
if (Object.keys(item.metadata.states).length !== 2) {
return false;
}
if (!(0 in item.metadata.states) || !(1 in item.metadata.states)) { if (!(0 in item.metadata.states) || !(1 in item.metadata.states)) {
return false; return false;
} }

View File

@ -1,4 +1,4 @@
import { mdiDelete } from "@mdi/js"; import { mdiCheckCircle, mdiCloseCircleOutline, mdiDelete } from "@mdi/js";
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@ -42,17 +42,42 @@ class ZWaveJSProvisioned extends LitElement {
private _columns = memoizeOne( private _columns = memoizeOne(
(narrow: boolean): DataTableColumnContainer => ({ (narrow: boolean): DataTableColumnContainer => ({
included: {
title: this.hass.localize(
"ui.panel.config.zwave_js.provisioned.included"
),
type: "icon",
width: "100px",
template: (_info, provisioningEntry: any) =>
provisioningEntry.additional_properties.nodeId
? html`
<ha-svg-icon
.label=${this.hass.localize(
"ui.panel.config.zwave_js.provisioned.included"
)}
.path=${mdiCheckCircle}
></ha-svg-icon>
`
: html`
<ha-svg-icon
.label=${this.hass.localize(
"ui.panel.config.zwave_js.provisioned.not_included"
)}
.path=${mdiCloseCircleOutline}
></ha-svg-icon>
`,
},
dsk: { dsk: {
title: this.hass.localize("ui.panel.config.zwave_js.provisioned.dsk"), title: this.hass.localize("ui.panel.config.zwave_js.provisioned.dsk"),
sortable: true, sortable: true,
filterable: true, filterable: true,
grows: true, grows: true,
}, },
securityClasses: { security_classes: {
title: this.hass.localize( title: this.hass.localize(
"ui.panel.config.zwave_js.provisioned.security_classes" "ui.panel.config.zwave_js.provisioned.security_classes"
), ),
width: "15%", width: "30%",
hidden: narrow, hidden: narrow,
filterable: true, filterable: true,
sortable: true, sortable: true,
@ -60,7 +85,7 @@ class ZWaveJSProvisioned extends LitElement {
securityClasses securityClasses
.map((secClass) => .map((secClass) =>
this.hass.localize( this.hass.localize(
`ui.panel.config.zwave_js.security_classes.${SecurityClass[secClass]}` `ui.panel.config.zwave_js.security_classes.${SecurityClass[secClass]}.title`
) )
) )
.join(", "), .join(", "),
@ -70,6 +95,7 @@ class ZWaveJSProvisioned extends LitElement {
"ui.panel.config.zwave_js.provisioned.unprovison" "ui.panel.config.zwave_js.provisioned.unprovison"
), ),
type: "icon-button", type: "icon-button",
width: "100px",
template: (_info, provisioningEntry: any) => html` template: (_info, provisioningEntry: any) => html`
<ha-icon-button <ha-icon-button
.label=${this.hass.localize( .label=${this.hass.localize(
@ -97,6 +123,8 @@ class ZWaveJSProvisioned extends LitElement {
} }
private _unprovision = async (ev) => { private _unprovision = async (ev) => {
const dsk = ev.currentTarget.provisioningEntry.dsk;
const confirm = await showConfirmationDialog(this, { const confirm = await showConfirmationDialog(this, {
title: this.hass.localize( title: this.hass.localize(
"ui.panel.config.zwave_js.provisioned.confirm_unprovision_title" "ui.panel.config.zwave_js.provisioned.confirm_unprovision_title"
@ -113,11 +141,8 @@ class ZWaveJSProvisioned extends LitElement {
return; return;
} }
await unprovisionZwaveSmartStartNode( await unprovisionZwaveSmartStartNode(this.hass, this.configEntryId, dsk);
this.hass, this._fetchData();
this.configEntryId,
ev.currentTarget.provisioningEntry.dsk
);
}; };
} }

View File

@ -25,6 +25,7 @@ import { computeDomain } from "../../../common/entity/compute_domain";
import { domainIcon } from "../../../common/entity/domain_icon"; import { domainIcon } from "../../../common/entity/domain_icon";
import { navigate } from "../../../common/navigate"; import { navigate } from "../../../common/navigate";
import { formatNumber } from "../../../common/number/format_number"; import { formatNumber } from "../../../common/number/format_number";
import { subscribeOne } from "../../../common/util/subscribe-one";
import "../../../components/entity/state-badge"; import "../../../components/entity/state-badge";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
@ -83,8 +84,11 @@ export class HuiAreaCard
return document.createElement("hui-area-card-editor"); return document.createElement("hui-area-card-editor");
} }
public static getStubConfig(): AreaCardConfig { public static async getStubConfig(
return { type: "area", area: "" }; hass: HomeAssistant
): Promise<AreaCardConfig> {
const areas = await subscribeOne(hass.connection, subscribeAreaRegistry);
return { type: "area", area: areas[0]?.area_id || "" };
} }
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -358,12 +362,12 @@ export class HuiAreaCard
}); });
let cameraEntityId: string | undefined; let cameraEntityId: string | undefined;
if ("camera" in entitiesByDomain) { if (this._config.show_camera && "camera" in entitiesByDomain) {
cameraEntityId = entitiesByDomain.camera[0].entity_id; cameraEntityId = entitiesByDomain.camera[0].entity_id;
} }
return html` return html`
<ha-card class=${area.picture ? "image" : ""}> <ha-card class=${area.picture || cameraEntityId ? "image" : ""}>
${area.picture || cameraEntityId ${area.picture || cameraEntityId
? html`<hui-image ? html`<hui-image
.config=${this._config} .config=${this._config}

View File

@ -79,6 +79,7 @@ export interface EntitiesCardConfig extends LovelaceCardConfig {
export interface AreaCardConfig extends LovelaceCardConfig { export interface AreaCardConfig extends LovelaceCardConfig {
area: string; area: string;
navigation_path?: string; navigation_path?: string;
show_camera?: boolean;
} }
export interface ButtonCardConfig extends LovelaceCardConfig { export interface ButtonCardConfig extends LovelaceCardConfig {

View File

@ -1,7 +1,7 @@
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { assert, assign, object, optional, string } from "superstruct"; import { assert, assign, boolean, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-area-picker"; import "../../../../components/ha-area-picker";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
@ -11,6 +11,8 @@ import { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EditorTarget } from "../types"; import { EditorTarget } from "../types";
import { configElementStyle } from "./config-elements-style"; import { configElementStyle } from "./config-elements-style";
import "../../../../components/ha-formfield";
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
const cardConfigStruct = assign( const cardConfigStruct = assign(
baseLovelaceCardConfig, baseLovelaceCardConfig,
@ -18,6 +20,7 @@ const cardConfigStruct = assign(
area: optional(string()), area: optional(string()),
navigation_path: optional(string()), navigation_path: optional(string()),
theme: optional(string()), theme: optional(string()),
show_camera: optional(boolean()),
}) })
); );
@ -47,6 +50,10 @@ export class HuiAreaCardEditor
return this._config!.theme || ""; return this._config!.theme || "";
} }
get _show_camera(): boolean {
return this._config!.show_camera || false;
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.hass || !this._config) { if (!this.hass || !this._config) {
return html``; return html``;
@ -59,9 +66,23 @@ export class HuiAreaCardEditor
.value=${this._area} .value=${this._area}
.placeholder=${this._area} .placeholder=${this._area}
.configValue=${"area"} .configValue=${"area"}
.label=${this.hass.localize("ui.dialogs.entity_registry.editor.area")} .label=${this.hass.localize(
"ui.panel.lovelace.editor.card.area.name"
)}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-area-picker> ></ha-area-picker>
<ha-formfield
.label=${this.hass.localize(
"ui.panel.lovelace.editor.card.area.show_camera"
)}
.dir=${computeRTLDirection(this.hass)}
>
<ha-switch
.checked=${this._show_camera}
.configValue=${"show_camera"}
@change=${this._valueChanged}
></ha-switch>
</ha-formfield>
<paper-input <paper-input
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.panel.lovelace.editor.action-editor.navigation_path" "ui.panel.lovelace.editor.action-editor.navigation_path"
@ -86,7 +107,8 @@ export class HuiAreaCardEditor
return; return;
} }
const target = ev.target! as EditorTarget; const target = ev.target! as EditorTarget;
const value = ev.detail.value; const value =
target.checked !== undefined ? target.checked : ev.detail.value;
if (this[`_${target.configValue}`] === value) { if (this[`_${target.configValue}`] === value) {
return; return;

View File

@ -13,7 +13,7 @@ export const getCardStubConfig = async (
const elClass = await getCardElementClass(type); const elClass = await getCardElementClass(type);
if (elClass && elClass.getStubConfig) { if (elClass && elClass.getStubConfig) {
const classStubConfig = elClass.getStubConfig( const classStubConfig = await elClass.getStubConfig(
hass, hass,
entities, entities,
entitiesFallback entitiesFallback

View File

@ -13,7 +13,7 @@ export const getHeaderFooterStubConfig = async (
const elClass = await getHeaderFooterElementClass(type); const elClass = await getHeaderFooterElementClass(type);
if (elClass && elClass.getStubConfig) { if (elClass && elClass.getStubConfig) {
const classStubConfig = elClass.getStubConfig( const classStubConfig = await elClass.getStubConfig(
hass, hass,
entities, entities,
entitiesFallback entitiesFallback

View File

@ -91,6 +91,7 @@ export const coreCards: Card[] = [
}, },
{ {
type: "area", type: "area",
showElement: true,
}, },
{ {
type: "conditional", type: "conditional",

View File

@ -1,7 +1,7 @@
import { PaperInputElement } from "@polymer/paper-input/paper-input"; import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { UNAVAILABLE } from "../../../data/entity"; import { UNAVAILABLE, UNAVAILABLE_STATES } from "../../../data/entity";
import { setValue } from "../../../data/input_text"; import { setValue } from "../../../data/input_text";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { hasConfigOrEntityChanged } from "../common/has-changed"; import { hasConfigOrEntityChanged } from "../common/has-changed";
@ -67,6 +67,12 @@ class HuiInputTextEntityRow extends LitElement implements LovelaceRow {
const element = this._inputEl; const element = this._inputEl;
const stateObj = this.hass!.states[this._config!.entity]; const stateObj = this.hass!.states[this._config!.entity];
// Filter out invalid text states
if (element.value && UNAVAILABLE_STATES.includes(element.value)) {
element.value = stateObj.state;
return;
}
if (element.value !== stateObj.state) { if (element.value !== stateObj.state) {
setValue(this.hass!, stateObj.entity_id, element.value!); setValue(this.hass!, stateObj.entity_id, element.value!);
} }

View File

@ -95,6 +95,7 @@ export const derivedStyles = {
"mdc-theme-text-disabled-on-light": "var(--disabled-text-color)", "mdc-theme-text-disabled-on-light": "var(--disabled-text-color)",
"mdc-theme-text-primary-on-background": "var(--primary-text-color)", "mdc-theme-text-primary-on-background": "var(--primary-text-color)",
"mdc-theme-text-secondary-on-background": "var(--secondary-text-color)", "mdc-theme-text-secondary-on-background": "var(--secondary-text-color)",
"mdc-theme-text-hint-on-background": "var(--secondary-text-color)",
"mdc-theme-text-icon-on-background": "var(--secondary-text-color)", "mdc-theme-text-icon-on-background": "var(--secondary-text-color)",
"mdc-theme-error": "var(--error-color)", "mdc-theme-error": "var(--error-color)",
"app-header-text-color": "var(--text-primary-color)", "app-header-text-color": "var(--text-primary-color)",

View File

@ -878,8 +878,7 @@
"key_missing": "Required key ''{key}'' is missing.", "key_missing": "Required key ''{key}'' is missing.",
"key_not_expected": "Key ''{key}'' is not expected or not supported by the visual editor.", "key_not_expected": "Key ''{key}'' is not expected or not supported by the visual editor.",
"key_wrong_type": "The provided value for ''{key}'' is not supported by the visual editor. We support ({type_correct}) but received ({type_wrong}).", "key_wrong_type": "The provided value for ''{key}'' is not supported by the visual editor. We support ({type_correct}) but received ({type_wrong}).",
"no_template_editor_support": "Templates not supported in visual editor", "no_template_editor_support": "Templates not supported in visual editor"
"no_state_array_support": "Multiple state values not supported in visual editor"
}, },
"supervisor": { "supervisor": {
"title": "Could not load the Supervisor panel!", "title": "Could not load the Supervisor panel!",
@ -1514,7 +1513,7 @@
"extra_fields": { "extra_fields": {
"above": "Above", "above": "Above",
"below": "Below", "below": "Below",
"for": "Duration", "for": "Duration (optional)",
"zone": "[%key:ui::panel::config::automation::editor::triggers::type::zone::label%]" "zone": "[%key:ui::panel::config::automation::editor::triggers::type::zone::label%]"
} }
}, },
@ -1537,9 +1536,9 @@
"state": { "state": {
"label": "State", "label": "State",
"attribute": "Attribute (optional)", "attribute": "Attribute (optional)",
"from": "From", "from": "From (optional)",
"for": "For", "for": "For (optional)",
"to": "To" "to": "To (optional)"
}, },
"homeassistant": { "homeassistant": {
"label": "Home Assistant", "label": "Home Assistant",
@ -2897,6 +2896,8 @@
"dsk": "DSK", "dsk": "DSK",
"security_classes": "Security classes", "security_classes": "Security classes",
"unprovison": "Unprovison", "unprovison": "Unprovison",
"included": "Included",
"not_included": "Not Included",
"confirm_unprovision_title": "Are you sure you want to unprovision the device?", "confirm_unprovision_title": "Are you sure you want to unprovision the device?",
"confirm_unprovision_text": "If you unprovision the device it will not be added to Home Assistant when it is powered on. If it is already added to Home Assistant, removing the provisioned device will not remove it from Home Assistant." "confirm_unprovision_text": "If you unprovision the device it will not be added to Home Assistant when it is powered on. If it is already added to Home Assistant, removing the provisioned device will not remove it from Home Assistant."
}, },
@ -3251,11 +3252,12 @@
"alarm-panel": { "alarm-panel": {
"name": "Alarm Panel", "name": "Alarm Panel",
"available_states": "Available States", "available_states": "Available States",
"description": "The Alarm Panel card allows you to Arm and Disarm your alarm control panel integrations." "description": "The Alarm Panel card allows you to arm and disarm your alarm control panel integrations."
}, },
"area": { "area": {
"name": "Area", "name": "Area",
"description": "The Area card automatically displays entities of a specific area." "description": "The Area card automatically displays entities of a specific area.",
"show_camera": "Show camera feed instead of area picture"
}, },
"calendar": { "calendar": {
"name": "Calendar", "name": "Calendar",