From f59cb661cd0ad522e3d686faf775371d80cf3ea6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Feb 2022 14:27:38 -0800 Subject: [PATCH 001/142] Add a docs icon to the config flow dialog --- .../config-flow/dialog-data-entry-flow.ts | 45 ++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index 29bea7491e..dc1edf1005 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -1,5 +1,5 @@ import "@material/mwc-button"; -import { mdiClose } from "@mdi/js"; +import { mdiClose, mdiHelpCircleOutline } from "@mdi/js"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, @@ -33,6 +33,7 @@ import { } from "../../data/device_registry"; import { haStyleDialog } from "../../resources/styles"; import type { HomeAssistant } from "../../types"; +import { documentationUrl } from "../../util/documentation-url"; import { showAlertDialog } from "../generic/show-dialog-box"; import { DataEntryFlowDialogParams, @@ -231,14 +232,33 @@ class DataEntryFlowDialog extends LitElement { // to reset the element. "" : html` - +
+ ${this._step + ? html` + + ` + : ""} + +
${this._step === null ? this._handler ? html` * { + color: var(--secondary-text-color); + } `, ]; } From 35a41b3490aa8accf017e937b5188164162e16f0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 11 Feb 2022 08:35:29 -0800 Subject: [PATCH 002/142] Use same help icon everywhere --- src/dialogs/config-flow/dialog-data-entry-flow.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index dc1edf1005..d41cdad08e 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -1,5 +1,5 @@ import "@material/mwc-button"; -import { mdiClose, mdiHelpCircleOutline } from "@mdi/js"; +import { mdiClose, mdiHelpCircle } from "@mdi/js"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, @@ -244,7 +244,7 @@ class DataEntryFlowDialog extends LitElement { rel="noreferrer noopener" > From add92a559d20daf193c5268d335762979ddd4692 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 2 Mar 2022 17:50:01 +0100 Subject: [PATCH 003/142] Fix quickbar overlaying, fix click handling (#11900) --- src/dialogs/quick-bar/ha-quick-bar.ts | 57 ++++++++++++++------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/src/dialogs/quick-bar/ha-quick-bar.ts b/src/dialogs/quick-bar/ha-quick-bar.ts index 337855d71b..3a7b3aaa96 100644 --- a/src/dialogs/quick-bar/ha-quick-bar.ts +++ b/src/dialogs/quick-bar/ha-quick-bar.ts @@ -86,11 +86,11 @@ export class QuickBar extends LitElement { @state() private _search = ""; - @state() private _opened = false; + @state() private _open = false; @state() private _commandMode = false; - @state() private _done = false; + @state() private _opened = false; @state() private _narrow = false; @@ -109,12 +109,12 @@ export class QuickBar extends LitElement { "all and (max-width: 450px), all and (max-height: 500px)" ).matches; this._initializeItemsIfNeeded(); - this._opened = true; + this._open = true; } public closeDialog() { + this._open = false; this._opened = false; - this._done = false; this._focusSet = false; this._filter = ""; this._search = ""; @@ -133,7 +133,7 @@ export class QuickBar extends LitElement { ); protected render() { - if (!this._opened) { + if (!this._open) { return html``; } @@ -218,24 +218,26 @@ export class QuickBar extends LitElement { ` : html` - - + ${this._opened + ? html` + ` + : ""} `} ${this._hint ? html`
${this._hint}
` : ""} @@ -252,9 +254,7 @@ export class QuickBar extends LitElement { } private _handleOpened() { - this.updateComplete.then(() => { - this._done = true; - }); + this._opened = true; } private async _handleRangeChanged(e) { @@ -454,9 +454,10 @@ export class QuickBar extends LitElement { } private _handleItemClick(ev) { + const listItem = ev.target.closest("mwc-list-item"); this.processItemAndCloseDialog( - (ev.target as any).item, - Number((ev.target as HTMLElement).getAttribute("index")) + listItem.item, + Number(listItem.getAttribute("index")) ); } From 24829bd9038ca270d689fef2ad01cab47be64806 Mon Sep 17 00:00:00 2001 From: Steve Repsher Date: Thu, 3 Mar 2022 04:15:22 -0500 Subject: [PATCH 004/142] Supervisor mobile click accessibility (#11915) --- hassio/src/entrypoint.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hassio/src/entrypoint.ts b/hassio/src/entrypoint.ts index 48c4a33180..09e73ecf4d 100644 --- a/hassio/src/entrypoint.ts +++ b/hassio/src/entrypoint.ts @@ -1,9 +1,12 @@ // Compat needs to be first import import "../../src/resources/compatibility"; +import { setCancelSyntheticClickEvents } from "@polymer/polymer/lib/utils/settings"; import "../../src/resources/roboto"; import "../../src/resources/safari-14-attachshadow-patch"; import "./hassio-main"; +setCancelSyntheticClickEvents(false); + const styleEl = document.createElement("style"); styleEl.innerHTML = ` body { From 797c871137d6123996105f4680e753464ba82d4e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 3 Mar 2022 13:55:40 +0100 Subject: [PATCH 005/142] Convert objects to string in config flow error (#11908) --- src/dialogs/config-flow/dialog-data-entry-flow.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index 29bea7491e..6a128d081a 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -117,13 +117,17 @@ class DataEntryFlowDialog extends LitElement { ); } catch (err: any) { this.closeDialog(); + let message = err.message || err.body || "Unknown error"; + if (typeof message !== "string") { + message = JSON.stringify(message); + } showAlertDialog(this, { title: this.hass.localize( "ui.panel.config.integrations.config_flow.error" ), text: `${this.hass.localize( "ui.panel.config.integrations.config_flow.could_not_load" - )}: ${err.message || err.body}`, + )}: ${message}`, }); return; } From 8c445f6409f71d3bc29890ea663fb11dafefa330 Mon Sep 17 00:00:00 2001 From: Robin Wittebol Date: Thu, 3 Mar 2022 19:45:03 +0100 Subject: [PATCH 006/142] Fix datepicker triangle (#11920) --- src/components/date-range-picker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/date-range-picker.ts b/src/components/date-range-picker.ts index f0c0951a77..e6a5c6479f 100644 --- a/src/components/date-range-picker.ts +++ b/src/components/date-range-picker.ts @@ -115,7 +115,7 @@ class DateRangePickerElement extends WrappedElement { color: var(--primary-text-color); min-width: initial !important; } - .daterangepicker:after { + .daterangepicker:before { display: none; } .daterangepicker:after { From 604b79696e001a0b36c8daef45e99a37f9fab93f Mon Sep 17 00:00:00 2001 From: Robin Wittebol Date: Thu, 3 Mar 2022 19:46:14 +0100 Subject: [PATCH 007/142] Always show tab labels (#11919) --- src/components/ha-tab.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/ha-tab.ts b/src/components/ha-tab.ts index da172e1fab..106ebd6211 100644 --- a/src/components/ha-tab.ts +++ b/src/components/ha-tab.ts @@ -42,9 +42,7 @@ export class HaTab extends LitElement { @keydown=${this._handleKeyDown} > ${this.narrow ? html`` : ""} - ${!this.narrow || this.active - ? html`${this.name}` - : ""} + ${this.name} ${this._shouldRenderRipple ? html`` : ""} `; From 8f8017ecff0b06b1bf60f203cfed9b5ca36544ad Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 4 Mar 2022 23:10:44 +0100 Subject: [PATCH 008/142] Remove zwave and ozw panels (#11911) Remove zwave and ozw panels --- cast/src/receiver/second-load.ts | 1 - package.json | 2 - src/data/ozw.ts | 213 ----- src/data/zwave.ts | 81 -- .../ozw/ha-device-actions-ozw.ts | 84 -- .../ozw/ha-device-info-ozw.ts | 99 --- .../zwave_js/ha-device-info-zwave_js.ts | 8 +- .../config/devices/ha-config-device-page.ts | 16 - src/panels/config/ha-panel-config.ts | 10 - .../integrations/ha-integration-card.ts | 2 - .../ozw/dialog-ozw-refresh-node.ts | 269 ------ .../ozw/ozw-config-dashboard.ts | 260 ------ .../ozw/ozw-config-router.ts | 67 -- .../ozw/ozw-network-dashboard.ts | 245 ------ .../ozw/ozw-network-nodes.ts | 131 --- .../ozw/ozw-network-router.ts | 74 -- .../integration-panels/ozw/ozw-node-config.ts | 265 ------ .../ozw/ozw-node-dashboard.ts | 254 ------ .../integration-panels/ozw/ozw-node-router.ts | 84 -- .../ozw/show-dialog-ozw-refresh-node.ts | 19 - .../zwave/ha-config-zwave.js | 765 ------------------ .../zwave/zwave-config-router.ts | 62 -- .../integration-panels/zwave/zwave-groups.js | 380 --------- .../zwave/zwave-log-dialog.js | 83 -- .../integration-panels/zwave/zwave-log.js | 160 ---- .../zwave/zwave-migration.ts | 573 ------------- .../integration-panels/zwave/zwave-network.ts | 302 ------- .../zwave/zwave-node-config.ts | 388 --------- .../zwave/zwave-node-protection.js | 179 ---- .../zwave/zwave-usercodes.js | 226 ------ .../integration-panels/zwave/zwave-values.ts | 109 --- src/translations/en.json | 204 ----- yarn.lock | 70 +- 33 files changed, 9 insertions(+), 5676 deletions(-) delete mode 100644 src/data/ozw.ts delete mode 100644 src/data/zwave.ts delete mode 100644 src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-actions-ozw.ts delete mode 100644 src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-info-ozw.ts delete mode 100644 src/panels/config/integrations/integration-panels/ozw/dialog-ozw-refresh-node.ts delete mode 100644 src/panels/config/integrations/integration-panels/ozw/ozw-config-dashboard.ts delete mode 100644 src/panels/config/integrations/integration-panels/ozw/ozw-config-router.ts delete mode 100644 src/panels/config/integrations/integration-panels/ozw/ozw-network-dashboard.ts delete mode 100644 src/panels/config/integrations/integration-panels/ozw/ozw-network-nodes.ts delete mode 100644 src/panels/config/integrations/integration-panels/ozw/ozw-network-router.ts delete mode 100644 src/panels/config/integrations/integration-panels/ozw/ozw-node-config.ts delete mode 100644 src/panels/config/integrations/integration-panels/ozw/ozw-node-dashboard.ts delete mode 100644 src/panels/config/integrations/integration-panels/ozw/ozw-node-router.ts delete mode 100644 src/panels/config/integrations/integration-panels/ozw/show-dialog-ozw-refresh-node.ts delete mode 100644 src/panels/config/integrations/integration-panels/zwave/ha-config-zwave.js delete mode 100644 src/panels/config/integrations/integration-panels/zwave/zwave-config-router.ts delete mode 100644 src/panels/config/integrations/integration-panels/zwave/zwave-groups.js delete mode 100644 src/panels/config/integrations/integration-panels/zwave/zwave-log-dialog.js delete mode 100755 src/panels/config/integrations/integration-panels/zwave/zwave-log.js delete mode 100644 src/panels/config/integrations/integration-panels/zwave/zwave-migration.ts delete mode 100644 src/panels/config/integrations/integration-panels/zwave/zwave-network.ts delete mode 100644 src/panels/config/integrations/integration-panels/zwave/zwave-node-config.ts delete mode 100644 src/panels/config/integrations/integration-panels/zwave/zwave-node-protection.js delete mode 100644 src/panels/config/integrations/integration-panels/zwave/zwave-usercodes.js delete mode 100644 src/panels/config/integrations/integration-panels/zwave/zwave-values.ts diff --git a/cast/src/receiver/second-load.ts b/cast/src/receiver/second-load.ts index e3c0885561..97de839a76 100644 --- a/cast/src/receiver/second-load.ts +++ b/cast/src/receiver/second-load.ts @@ -1,4 +1,3 @@ -import "web-animations-js/web-animations-next-lite.min"; import "../../../src/resources/ha-style"; import "../../../src/resources/roboto"; import "./layout/hc-lovelace"; diff --git a/package.json b/package.json index c9e04cedd0..24c7cccf92 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,6 @@ "@polymer/iron-icon": "^3.0.1", "@polymer/iron-input": "^3.0.1", "@polymer/iron-resizable-behavior": "^3.0.1", - "@polymer/paper-dropdown-menu": "^3.2.0", "@polymer/paper-input": "^3.2.1", "@polymer/paper-item": "^3.0.1", "@polymer/paper-listbox": "^3.0.1", @@ -136,7 +135,6 @@ "vis-network": "^8.5.4", "vue": "^2.6.12", "vue2-daterange-picker": "^0.5.1", - "web-animations-js": "^2.3.2", "workbox-cacheable-response": "^6.4.2", "workbox-core": "^6.4.2", "workbox-expiration": "^6.4.2", diff --git a/src/data/ozw.ts b/src/data/ozw.ts deleted file mode 100644 index 011a685fd8..0000000000 --- a/src/data/ozw.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { HomeAssistant } from "../types"; -import { DeviceRegistryEntry } from "./device_registry"; - -export interface OZWNodeIdentifiers { - ozw_instance: number; - node_id: number; -} - -export interface OZWDevice { - node_id: number; - node_query_stage: string; - is_awake: boolean; - is_failed: boolean; - is_zwave_plus: boolean; - ozw_instance: number; - event: string; - node_manufacturer_name: string; - node_product_name: string; -} - -export interface OZWDeviceMetaDataResponse { - node_id: number; - ozw_instance: number; - metadata: OZWDeviceMetaData; -} - -export interface OZWDeviceMetaData { - OZWInfoURL: string; - ZWAProductURL: string; - ProductPic: string; - Description: string; - ProductManualURL: string; - ProductPageURL: string; - InclusionHelp: string; - ExclusionHelp: string; - ResetHelp: string; - WakeupHelp: string; - ProductSupportURL: string; - Frequency: string; - Name: string; - ProductPicBase64: string; -} - -export interface OZWInstance { - ozw_instance: number; - OZWDaemon_Version: string; - OpenZWave_Version: string; - QTOpenZWave_Version: string; - Status: string; - getControllerPath: string; - homeID: string; -} - -export interface OZWNetworkStatistics { - ozw_instance: number; - node_count: number; - readCnt: number; - writeCnt: number; - ACKCnt: number; - CANCnt: number; - NAKCnt: number; - dropped: number; - retries: number; -} - -export interface OZWDeviceConfig { - label: string; - type: string; - value: string | number; - parameter: number; - min: number; - max: number; - help: string; -} - -export const nodeQueryStages = [ - "ProtocolInfo", - "Probe", - "WakeUp", - "ManufacturerSpecific1", - "NodeInfo", - "NodePlusInfo", - "ManufacturerSpecific2", - "Versions", - "Instances", - "Static", - "CacheLoad", - "Associations", - "Neighbors", - "Session", - "Dynamic", - "Configuration", - "Complete", -]; - -export const networkOnlineStatuses = [ - "driverAllNodesQueried", - "driverAllNodesQueriedSomeDead", - "driverAwakeNodesQueried", -]; -export const networkStartingStatuses = [ - "starting", - "started", - "Ready", - "driverReady", -]; -export const networkOfflineStatuses = [ - "Offline", - "stopped", - "driverFailed", - "driverReset", - "driverRemoved", - "driverAllNodesOnFire", -]; - -export const getIdentifiersFromDevice = function ( - device: DeviceRegistryEntry -): OZWNodeIdentifiers | undefined { - if (!device) { - return undefined; - } - - const ozwIdentifier = device.identifiers.find( - (identifier) => identifier[0] === "ozw" - ); - if (!ozwIdentifier) { - return undefined; - } - - const identifiers = ozwIdentifier[1].split("."); - return { - node_id: parseInt(identifiers[1]), - ozw_instance: parseInt(identifiers[0]), - }; -}; - -export const fetchOZWInstances = ( - hass: HomeAssistant -): Promise => - hass.callWS({ - type: "ozw/get_instances", - }); - -export const fetchOZWNetworkStatus = ( - hass: HomeAssistant, - ozw_instance: number -): Promise => - hass.callWS({ - type: "ozw/network_status", - ozw_instance, - }); - -export const fetchOZWNetworkStatistics = ( - hass: HomeAssistant, - ozw_instance: number -): Promise => - hass.callWS({ - type: "ozw/network_statistics", - ozw_instance, - }); - -export const fetchOZWNodes = ( - hass: HomeAssistant, - ozw_instance: number -): Promise => - hass.callWS({ - type: "ozw/get_nodes", - ozw_instance, - }); - -export const fetchOZWNodeStatus = ( - hass: HomeAssistant, - ozw_instance: number, - node_id: number -): Promise => - hass.callWS({ - type: "ozw/node_status", - ozw_instance, - node_id, - }); - -export const fetchOZWNodeMetadata = ( - hass: HomeAssistant, - ozw_instance: number, - node_id: number -): Promise => - hass.callWS({ - type: "ozw/node_metadata", - ozw_instance, - node_id, - }); - -export const fetchOZWNodeConfig = ( - hass: HomeAssistant, - ozw_instance: number, - node_id: number -): Promise => - hass.callWS({ - type: "ozw/get_config_parameters", - ozw_instance, - node_id, - }); - -export const refreshNodeInfo = ( - hass: HomeAssistant, - ozw_instance: number, - node_id: number -): Promise => - hass.callWS({ - type: "ozw/refresh_node_info", - ozw_instance, - node_id, - }); diff --git a/src/data/zwave.ts b/src/data/zwave.ts deleted file mode 100644 index fb6fbe935e..0000000000 --- a/src/data/zwave.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { HomeAssistant } from "../types"; - -export interface ZWaveNetworkStatus { - state: number; -} - -export interface ZWaveValue { - key: number; - value: { - index: number; - instance: number; - label: string; - poll_intensity: number; - }; -} - -export interface ZWaveConfigItem { - key: number; - value: { - data: any; - data_items: any[]; - help: string; - label: string; - max: number; - min: number; - type: string; - }; -} - -export interface ZWaveConfigServiceData { - node_id: number; - parameter: number; - value: number | string; -} - -export interface ZWaveNode { - attributes: ZWaveAttributes; -} - -export interface ZWaveAttributes { - node_id: number; - wake_up_interval?: number; -} - -export interface ZWaveMigrationConfig { - usb_path: string; - network_key: string; -} - -export const ZWAVE_NETWORK_STATE_STOPPED = 0; -export const ZWAVE_NETWORK_STATE_FAILED = 1; -export const ZWAVE_NETWORK_STATE_STARTED = 5; -export const ZWAVE_NETWORK_STATE_AWAKED = 7; -export const ZWAVE_NETWORK_STATE_READY = 10; - -export const fetchNetworkStatus = ( - hass: HomeAssistant -): Promise => - hass.callWS({ - type: "zwave/network_status", - }); - -export const startZwaveJsConfigFlow = ( - hass: HomeAssistant -): Promise<{ flow_id: string }> => - hass.callWS({ - type: "zwave/start_zwave_js_config_flow", - }); - -export const fetchMigrationConfig = ( - hass: HomeAssistant -): Promise => - hass.callWS({ - type: "zwave/get_migration_config", - }); - -export const fetchValues = (hass: HomeAssistant, nodeId: number) => - hass.callApi("GET", `zwave/values/${nodeId}`); - -export const fetchNodeConfig = (hass: HomeAssistant, nodeId: number) => - hass.callApi("GET", `zwave/config/${nodeId}`); diff --git a/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-actions-ozw.ts b/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-actions-ozw.ts deleted file mode 100644 index 8b9f4309a9..0000000000 --- a/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-actions-ozw.ts +++ /dev/null @@ -1,84 +0,0 @@ -import "@material/mwc-button/mwc-button"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property } from "lit/decorators"; -import { navigate } from "../../../../../../common/navigate"; -import { DeviceRegistryEntry } from "../../../../../../data/device_registry"; -import { - getIdentifiersFromDevice, - OZWNodeIdentifiers, -} from "../../../../../../data/ozw"; -import { haStyle } from "../../../../../../resources/styles"; -import { HomeAssistant } from "../../../../../../types"; -import { showOZWRefreshNodeDialog } from "../../../../integrations/integration-panels/ozw/show-dialog-ozw-refresh-node"; - -@customElement("ha-device-actions-ozw") -export class HaDeviceActionsOzw extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property() public device!: DeviceRegistryEntry; - - @property() - private node_id = 0; - - @property() - private ozw_instance = 1; - - protected updated(changedProperties: PropertyValues) { - if (changedProperties.has("device")) { - const identifiers: OZWNodeIdentifiers | undefined = - getIdentifiersFromDevice(this.device); - if (!identifiers) { - return; - } - this.ozw_instance = identifiers.ozw_instance; - this.node_id = identifiers.node_id; - } - } - - protected render(): TemplateResult { - if (!this.ozw_instance || !this.node_id) { - return html``; - } - return html` - - ${this.hass.localize("ui.panel.config.ozw.node.button")} - - - ${this.hass.localize("ui.panel.config.ozw.refresh_node.button")} - - `; - } - - private async _refreshNodeClicked() { - showOZWRefreshNodeDialog(this, { - node_id: this.node_id, - ozw_instance: this.ozw_instance, - }); - } - - private async _nodeDetailsClicked() { - navigate( - `/config/ozw/network/${this.ozw_instance}/node/${this.node_id}/dashboard` - ); - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - :host { - display: flex; - flex-direction: column; - align-items: flex-start; - } - `, - ]; - } -} diff --git a/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-info-ozw.ts b/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-info-ozw.ts deleted file mode 100644 index 4717d235b8..0000000000 --- a/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-info-ozw.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { DeviceRegistryEntry } from "../../../../../../data/device_registry"; -import { - fetchOZWNodeStatus, - getIdentifiersFromDevice, - OZWDevice, - OZWNodeIdentifiers, -} from "../../../../../../data/ozw"; -import { haStyle } from "../../../../../../resources/styles"; -import { HomeAssistant } from "../../../../../../types"; - -@customElement("ha-device-info-ozw") -export class HaDeviceInfoOzw extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property() public device!: DeviceRegistryEntry; - - @property() - private node_id = 0; - - @property() - private ozw_instance = 1; - - @state() private _ozwDevice?: OZWDevice; - - protected updated(changedProperties: PropertyValues) { - if (changedProperties.has("device")) { - const identifiers: OZWNodeIdentifiers | undefined = - getIdentifiersFromDevice(this.device); - if (!identifiers) { - return; - } - this.ozw_instance = identifiers.ozw_instance; - this.node_id = identifiers.node_id; - - this._fetchNodeDetails(); - } - } - - protected async _fetchNodeDetails() { - this._ozwDevice = await fetchOZWNodeStatus( - this.hass, - this.ozw_instance, - this.node_id - ); - } - - protected render(): TemplateResult { - if (!this._ozwDevice) { - return html``; - } - return html` -

- ${this.hass.localize("ui.panel.config.ozw.device_info.zwave_info")} -

-
- ${this.hass.localize("ui.panel.config.ozw.common.node_id")}: - ${this._ozwDevice.node_id} -
-
- ${this.hass.localize("ui.panel.config.ozw.device_info.stage")}: - ${this._ozwDevice.node_query_stage} -
-
- ${this.hass.localize("ui.panel.config.ozw.common.ozw_instance")}: - ${this._ozwDevice.ozw_instance} -
-
- ${this.hass.localize("ui.panel.config.ozw.device_info.node_failed")}: - ${this._ozwDevice.is_failed - ? this.hass.localize("ui.common.yes") - : this.hass.localize("ui.common.no")} -
- `; - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - h4 { - margin-bottom: 4px; - } - div { - word-break: break-all; - margin-top: 2px; - } - `, - ]; - } -} diff --git a/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts b/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts index 32a732662f..99f48c8989 100644 --- a/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts +++ b/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts @@ -27,7 +27,7 @@ import { HomeAssistant } from "../../../../../../types"; export class HaDeviceInfoZWaveJS extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public device!: DeviceRegistryEntry; + @property({ attribute: false }) public device!: DeviceRegistryEntry; @state() private _entryId?: string; @@ -173,3 +173,9 @@ export class HaDeviceInfoZWaveJS extends LitElement { ]; } } + +declare global { + interface HTMLElementTagNameMap { + "ha-device-info-zwave_js": HaDeviceInfoZWaveJS; + } +} diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index 5572834106..7512b3067f 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -902,22 +902,6 @@ export class HaConfigDevicePage extends LitElement { > `); } - if (domains.includes("ozw")) { - import("./device-detail/integration-elements/ozw/ha-device-actions-ozw"); - import("./device-detail/integration-elements/ozw/ha-device-info-ozw"); - deviceInfo.push(html` - - `); - deviceActions.push(html` - - `); - } if (domains.includes("zha")) { import("./device-detail/integration-elements/zha/ha-device-actions-zha"); import("./device-detail/integration-elements/zha/ha-device-info-zha"); diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index c938ca0975..850408f095 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -376,21 +376,11 @@ class HaPanelConfig extends HassRouterPage { "./integrations/integration-panels/zha/zha-config-dashboard-router" ), }, - zwave: { - tag: "zwave-config-router", - load: () => - import("./integrations/integration-panels/zwave/zwave-config-router"), - }, mqtt: { tag: "mqtt-config-panel", load: () => import("./integrations/integration-panels/mqtt/mqtt-config-panel"), }, - ozw: { - tag: "ozw-config-router", - load: () => - import("./integrations/integration-panels/ozw/ozw-config-router"), - }, zwave_js: { tag: "zwave_js-config-router", load: () => diff --git a/src/panels/config/integrations/ha-integration-card.ts b/src/panels/config/integrations/ha-integration-card.ts index 4af887d59a..28154e279c 100644 --- a/src/panels/config/integrations/ha-integration-card.ts +++ b/src/panels/config/integrations/ha-integration-card.ts @@ -55,8 +55,6 @@ const integrationsWithPanel = { hassio: "/hassio/dashboard", mqtt: "/config/mqtt", zha: "/config/zha/dashboard", - ozw: "/config/ozw/dashboard", - zwave: "/config/zwave", zwave_js: "/config/zwave_js/dashboard", }; diff --git a/src/panels/config/integrations/integration-panels/ozw/dialog-ozw-refresh-node.ts b/src/panels/config/integrations/integration-panels/ozw/dialog-ozw-refresh-node.ts deleted file mode 100644 index 54ee3ce9d6..0000000000 --- a/src/panels/config/integrations/integration-panels/ozw/dialog-ozw-refresh-node.ts +++ /dev/null @@ -1,269 +0,0 @@ -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property, state } from "lit/decorators"; -import "../../../../../components/ha-circular-progress"; -import "../../../../../components/ha-code-editor"; -import { createCloseHeading } from "../../../../../components/ha-dialog"; -import { - fetchOZWNodeMetadata, - nodeQueryStages, - OZWDevice, - OZWDeviceMetaData, -} from "../../../../../data/ozw"; -import { haStyleDialog } from "../../../../../resources/styles"; -import { HomeAssistant } from "../../../../../types"; -import { OZWRefreshNodeDialogParams } from "./show-dialog-ozw-refresh-node"; - -@customElement("dialog-ozw-refresh-node") -class DialogOZWRefreshNode extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @state() private _node_id?: number; - - @state() private _ozw_instance = 1; - - @state() private _nodeMetaData?: OZWDeviceMetaData; - - @state() private _node?: OZWDevice; - - @state() private _active = false; - - @state() private _complete = false; - - private _refreshDevicesTimeoutHandle?: number; - - private _subscribed?: Promise<() => Promise>; - - public disconnectedCallback(): void { - super.disconnectedCallback(); - this._unsubscribe(); - } - - protected updated(changedProperties: PropertyValues): void { - super.update(changedProperties); - if (changedProperties.has("node_id")) { - this._fetchData(); - } - } - - private async _fetchData() { - if (!this._node_id) { - return; - } - const metaDataResponse = await fetchOZWNodeMetadata( - this.hass, - this._ozw_instance, - this._node_id - ); - - this._nodeMetaData = metaDataResponse.metadata; - } - - public async showDialog(params: OZWRefreshNodeDialogParams): Promise { - this._node_id = params.node_id; - this._ozw_instance = params.ozw_instance; - this._fetchData(); - } - - protected render(): TemplateResult { - if (!this._node_id) { - return html``; - } - - return html` - - ${this._complete - ? html` -

- ${this.hass.localize( - "ui.panel.config.ozw.refresh_node.complete" - )} -

- - ${this.hass.localize("ui.common.close")} - - ` - : html` - ${this._active - ? html` -
- -
-

- - ${this.hass.localize( - "ui.panel.config.ozw.refresh_node.refreshing_description" - )} - -

- ${this._node - ? html` -

- ${this.hass.localize( - "ui.panel.config.ozw.refresh_node.node_status" - )}: - ${this._node.node_query_stage} - (${this.hass.localize( - "ui.panel.config.ozw.refresh_node.step" - )} - ${nodeQueryStages.indexOf( - this._node.node_query_stage - ) + 1}/17) -

-

- - ${this.hass.localize( - "ui.panel.config.ozw.node_query_stages." + - this._node.node_query_stage.toLowerCase() - )} -

- ` - : ``} -
-
- ` - : html` - ${this.hass.localize( - "ui.panel.config.ozw.refresh_node.description" - )} -

- ${this.hass.localize( - "ui.panel.config.ozw.refresh_node.battery_note" - )} -

- `} - ${this._nodeMetaData?.WakeupHelp !== "" - ? html` - - ${this.hass.localize( - "ui.panel.config.ozw.refresh_node.wakeup_header" - )} - ${this._nodeMetaData!.Name} - -
- ${this._nodeMetaData!.WakeupHelp} -
- - ${this.hass.localize( - "ui.panel.config.ozw.refresh_node.wakeup_instructions_source" - )} - -
- ` - : ""} - ${!this._active - ? html` - - ${this.hass.localize( - "ui.panel.config.ozw.refresh_node.start_refresh_button" - )} - - ` - : html``} - `} -
- `; - } - - private _startRefresh(): void { - this._subscribe(); - } - - private _handleMessage(message: any): void { - if (message.type === "node_updated") { - this._node = message; - if (message.node_query_stage === "Complete") { - this._unsubscribe(); - this._complete = true; - } - } - } - - private _unsubscribe(): void { - this._active = false; - if (this._refreshDevicesTimeoutHandle) { - clearTimeout(this._refreshDevicesTimeoutHandle); - } - if (this._subscribed) { - this._subscribed.then((unsub) => unsub()); - this._subscribed = undefined; - } - } - - private _subscribe(): void { - if (!this.hass) { - return; - } - this._active = true; - this._subscribed = this.hass.connection.subscribeMessage( - (message) => this._handleMessage(message), - { - type: "ozw/refresh_node_info", - node_id: this._node_id, - ozw_instance: this._ozw_instance, - } - ); - this._refreshDevicesTimeoutHandle = window.setTimeout( - () => this._unsubscribe(), - 120000 - ); - } - - private _close(): void { - this._complete = false; - this._node_id = undefined; - this._node = undefined; - } - - static get styles(): CSSResultGroup { - return [ - haStyleDialog, - css` - blockquote { - display: block; - background-color: #ddd; - padding: 8px; - margin: 8px 0; - font-size: 0.9em; - } - - blockquote em { - font-size: 0.9em; - margin-top: 6px; - } - - .flex-container { - display: flex; - align-items: center; - } - - .flex-container ha-circular-progress { - margin-right: 20px; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "dialog-ozw-refresh-node": DialogOZWRefreshNode; - } -} diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-config-dashboard.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-config-dashboard.ts deleted file mode 100644 index dcdb81518d..0000000000 --- a/src/panels/config/integrations/integration-panels/ozw/ozw-config-dashboard.ts +++ /dev/null @@ -1,260 +0,0 @@ -import "@material/mwc-button/mwc-button"; -import { mdiCheckCircle, mdiCircle, mdiCloseCircle, mdiZWave } from "@mdi/js"; -import "@polymer/paper-item/paper-icon-item"; -import "@polymer/paper-item/paper-item-body"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { navigate } from "../../../../../common/navigate"; -import "../../../../../components/ha-card"; -import "../../../../../components/ha-icon-next"; -import { - fetchOZWInstances, - networkOfflineStatuses, - networkOnlineStatuses, - networkStartingStatuses, - OZWInstance, -} from "../../../../../data/ozw"; -import "../../../../../layouts/hass-error-screen"; -import "../../../../../layouts/hass-loading-screen"; -import "../../../../../layouts/hass-tabs-subpage"; -import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage"; -import { haStyle } from "../../../../../resources/styles"; -import type { HomeAssistant, Route } from "../../../../../types"; -import "../../../ha-config-section"; -import "../../../../../components/ha-alert"; - -export const ozwTabs: PageNavigation[] = []; - -@customElement("ozw-config-dashboard") -class OZWConfigDashboard extends LitElement { - @property({ type: Object }) public hass!: HomeAssistant; - - @property({ type: Object }) public route!: Route; - - @property({ type: Boolean }) public narrow!: boolean; - - @property({ type: Boolean }) public isWide!: boolean; - - @property() public configEntryId?: string; - - @state() private _instances?: OZWInstance[]; - - protected firstUpdated() { - this._fetchData(); - } - - protected render(): TemplateResult { - if (!this._instances) { - return html``; - } - - if (this._instances.length === 0) { - return html``; - } - - return html` - - - The OpenZWave integration is deprecated and will no longer receive any - updates. The technical dependencies will render this integration - unusable in the near future. We strongly advise you to migrate to the - new - Z-Wave JS integration. - - learn more - - - - -
- ${this.hass.localize("ui.panel.config.ozw.select_instance.header")} -
- -
- ${this.hass.localize( - "ui.panel.config.ozw.select_instance.introduction" - )} -
- ${this._instances.length > 0 - ? html` - ${this._instances.map((instance) => { - let status = "unknown"; - let icon = mdiCircle; - if (networkOnlineStatuses.includes(instance.Status)) { - status = "online"; - icon = mdiCheckCircle; - } - if (networkStartingStatuses.includes(instance.Status)) { - status = "starting"; - } - if (networkOfflineStatuses.includes(instance.Status)) { - status = "offline"; - icon = mdiCloseCircle; - } - - return html` - - - - - - - ${this.hass.localize( - "ui.panel.config.ozw.common.instance" - )} - ${instance.ozw_instance} -
- - ${this.hass.localize( - "ui.panel.config.ozw.network_status." + status - )} - - - ${this.hass.localize( - "ui.panel.config.ozw.network_status.details." + - instance.Status.toLowerCase() - )}
- ${this.hass.localize( - "ui.panel.config.ozw.common.controller" - )} - : ${instance.getControllerPath}
- OZWDaemon ${instance.OZWDaemon_Version} (OpenZWave - ${instance.OpenZWave_Version}) -
-
- -
-
-
- `; - })} - ` - : ""} -
-
- `; - } - - private async _fetchData() { - this._instances = await fetchOZWInstances(this.hass!); - if (this._instances.length === 1) { - navigate(`/config/ozw/network/${this._instances[0].ozw_instance}`, { - replace: true, - }); - } - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - ha-card:last-child { - margin-bottom: 24px; - } - ha-config-section { - margin-top: -12px; - } - :host([narrow]) ha-config-section { - margin-top: -20px; - } - ha-alert { - display: block; - margin: 16px; - } - ha-alert a { - text-decoration: none; - } - ha-card { - overflow: hidden; - } - ha-card a { - text-decoration: none; - color: var(--primary-text-color); - } - paper-item-body { - margin: 16px 0; - } - a { - text-decoration: none; - color: var(--primary-text-color); - position: relative; - display: block; - outline: 0; - } - ha-svg-icon.network-status-icon { - height: 14px; - width: 14px; - } - .online { - color: green; - } - .starting { - color: orange; - } - .offline { - color: red; - } - ha-svg-icon, - ha-icon-next { - color: var(--secondary-text-color); - } - .iron-selected paper-item::before, - a:not(.iron-selected):focus::before { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - pointer-events: none; - content: ""; - transition: opacity 15ms linear; - will-change: opacity; - } - a:not(.iron-selected):focus::before { - background-color: currentColor; - opacity: var(--dark-divider-opacity); - } - .iron-selected paper-item:focus::before, - .iron-selected:focus paper-item::before { - opacity: 0.2; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "ozw-config-dashboard": OZWConfigDashboard; - } -} diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-config-router.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-config-router.ts deleted file mode 100644 index ad2d2e38fe..0000000000 --- a/src/panels/config/integrations/integration-panels/ozw/ozw-config-router.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { customElement, property } from "lit/decorators"; -import memoizeOne from "memoize-one"; -import { - HassRouterPage, - RouterOptions, -} from "../../../../../layouts/hass-router-page"; -import { HomeAssistant, Route } from "../../../../../types"; - -export const computeTail = memoizeOne((route: Route) => { - const dividerPos = route.path.indexOf("/", 1); - return dividerPos === -1 - ? { - prefix: route.prefix + route.path, - path: "", - } - : { - prefix: route.prefix + route.path.substr(0, dividerPos), - path: route.path.substr(dividerPos), - }; -}); - -@customElement("ozw-config-router") -class OZWConfigRouter extends HassRouterPage { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property() public isWide!: boolean; - - @property() public narrow!: boolean; - - private _configEntry = new URLSearchParams(window.location.search).get( - "config_entry" - ); - - protected routerOptions: RouterOptions = { - defaultPage: "dashboard", - showLoading: true, - routes: { - dashboard: { - tag: "ozw-config-dashboard", - load: () => import("./ozw-config-dashboard"), - }, - network: { - tag: "ozw-network-router", - load: () => import("./ozw-network-router"), - }, - }, - }; - - protected updatePageEl(el): void { - el.route = this.routeTail; - el.hass = this.hass; - el.isWide = this.isWide; - el.narrow = this.narrow; - el.configEntryId = this._configEntry; - if (this._currentPage === "network") { - const path = this.routeTail.path.split("/"); - el.ozwInstance = path[1]; - el.route = computeTail(this.routeTail); - } - } -} - -declare global { - interface HTMLElementTagNameMap { - "ozw-config-router": OZWConfigRouter; - } -} diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-network-dashboard.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-network-dashboard.ts deleted file mode 100644 index 7dc5478939..0000000000 --- a/src/panels/config/integrations/integration-panels/ozw/ozw-network-dashboard.ts +++ /dev/null @@ -1,245 +0,0 @@ -import "@material/mwc-button/mwc-button"; -import { mdiCheckCircle, mdiCircle, mdiCloseCircle } from "@mdi/js"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { classMap } from "lit/directives/class-map"; -import { navigate } from "../../../../../common/navigate"; -import "../../../../../components/buttons/ha-call-service-button"; -import "../../../../../components/ha-card"; -import { - fetchOZWNetworkStatistics, - fetchOZWNetworkStatus, - networkOfflineStatuses, - networkOnlineStatuses, - networkStartingStatuses, - OZWInstance, - OZWNetworkStatistics, -} from "../../../../../data/ozw"; -import "../../../../../layouts/hass-tabs-subpage"; -import { haStyle } from "../../../../../resources/styles"; -import type { HomeAssistant, Route } from "../../../../../types"; -import "../../../ha-config-section"; -import { ozwNetworkTabs } from "./ozw-network-router"; - -@customElement("ozw-network-dashboard") -class OZWNetworkDashboard extends LitElement { - @property({ type: Object }) public hass!: HomeAssistant; - - @property({ type: Object }) public route!: Route; - - @property({ type: Boolean }) public narrow!: boolean; - - @property({ type: Boolean }) public isWide!: boolean; - - @property() public configEntryId?: string; - - @property() public ozwInstance?: number; - - @state() private _network?: OZWInstance; - - @state() private _statistics?: OZWNetworkStatistics; - - @state() private _status = "unknown"; - - @state() private _icon = mdiCircle; - - protected firstUpdated() { - if (!this.ozwInstance) { - navigate("/config/ozw/dashboard", { replace: true }); - } else if (this.hass) { - this._fetchData(); - } - } - - protected render(): TemplateResult { - return html` - - -
- ${this.hass.localize("ui.panel.config.ozw.network.header")} -
- -
- ${this.hass.localize("ui.panel.config.ozw.network.introduction")} -
- ${this._network - ? html` - -
-
- - ${this.hass.localize( - "ui.panel.config.ozw.common.network" - )} - ${this.hass.localize( - `ui.panel.config.ozw.network_status.${this._status}` - )} -
- - ${this.hass.localize( - `ui.panel.config.ozw.network_status.details.${this._network.Status.toLowerCase()}` - )} - -
-
- ${this.hass.localize( - "ui.panel.config.ozw.common.ozw_instance" - )} - ${this._network.ozw_instance} - ${this._statistics - ? html` - • - ${this.hass.localize( - "ui.panel.config.ozw.network.node_count", - "count", - this._statistics.node_count - )} - ` - : ``} -
- ${this.hass.localize( - "ui.panel.config.ozw.common.controller" - )}: - ${this._network.getControllerPath}
- OZWDaemon ${this._network.OZWDaemon_Version} (OpenZWave - ${this._network.OpenZWave_Version}) -
-
-
- ${this._generateServiceButton("add_node")} - ${this._generateServiceButton("remove_node")} - ${this._generateServiceButton("cancel_command")} -
-
- ` - : ``} -
-
- `; - } - - private async _fetchData() { - if (!this.ozwInstance) return; - this._network = await fetchOZWNetworkStatus(this.hass!, this.ozwInstance); - this._statistics = await fetchOZWNetworkStatistics( - this.hass!, - this.ozwInstance - ); - if (networkOnlineStatuses.includes(this._network!.Status)) { - this._status = "online"; - this._icon = mdiCheckCircle; - } - if (networkStartingStatuses.includes(this._network!.Status)) { - this._status = "starting"; - } - if (networkOfflineStatuses.includes(this._network!.Status)) { - this._status = "offline"; - this._icon = mdiCloseCircle; - } - } - - private _generateServiceButton(service: string) { - const serviceData = { instance_id: this.ozwInstance }; - return html` - - ${this.hass!.localize(`ui.panel.config.ozw.services.${service}`)} - - `; - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - .secondary { - color: var(--secondary-text-color); - } - .online { - color: green; - } - .starting { - color: orange; - } - .offline { - color: red; - } - .content { - margin-top: 24px; - } - - .sectionHeader { - position: relative; - padding-right: 40px; - } - - .network-status { - text-align: center; - } - - .network-status div.details { - font-size: 1.5rem; - margin-bottom: 16px; - } - - .network-status ha-svg-icon { - display: block; - margin: 0px auto 16px; - width: 48px; - height: 48px; - } - - .network-status small { - font-size: 1rem; - } - - ha-card { - margin: 0 auto; - max-width: 600px; - } - - .card-actions.warning ha-call-service-button { - color: var(--error-color); - } - - .toggle-help-icon { - position: absolute; - top: -6px; - right: 0; - color: var(--primary-color); - } - - ha-service-description { - display: block; - color: grey; - padding: 0 8px 12px; - } - - [hidden] { - display: none; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "ozw-network-dashboard": OZWNetworkDashboard; - } -} diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-network-nodes.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-network-nodes.ts deleted file mode 100644 index 8547058884..0000000000 --- a/src/panels/config/integrations/integration-panels/ozw/ozw-network-nodes.ts +++ /dev/null @@ -1,131 +0,0 @@ -import "@material/mwc-button/mwc-button"; -import { mdiAlert, mdiCheck } from "@mdi/js"; -import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import memoizeOne from "memoize-one"; -import { HASSDomEvent } from "../../../../../common/dom/fire_event"; -import { navigate } from "../../../../../common/navigate"; -import "../../../../../components/buttons/ha-call-service-button"; -import { - DataTableColumnContainer, - RowClickedEvent, -} from "../../../../../components/data-table/ha-data-table"; -import "../../../../../components/ha-card"; -import { fetchOZWNodes, OZWDevice } from "../../../../../data/ozw"; -import "../../../../../layouts/hass-tabs-subpage"; -import "../../../../../layouts/hass-tabs-subpage-data-table"; -import { haStyle } from "../../../../../resources/styles"; -import type { HomeAssistant, Route } from "../../../../../types"; -import "../../../ha-config-section"; -import { ozwNetworkTabs } from "./ozw-network-router"; - -export interface NodeRowData extends OZWDevice { - node?: NodeRowData; - id?: number; -} - -@customElement("ozw-network-nodes") -class OZWNetworkNodes extends LitElement { - @property({ type: Object }) public hass!: HomeAssistant; - - @property({ type: Object }) public route!: Route; - - @property({ type: Boolean }) public narrow!: boolean; - - @property({ type: Boolean }) public isWide!: boolean; - - @property() public configEntryId?: string; - - @property() public ozwInstance = 0; - - @state() private _nodes: OZWDevice[] = []; - - private _columns = memoizeOne( - (narrow: boolean): DataTableColumnContainer => ({ - node_id: { - title: this.hass.localize("ui.panel.config.ozw.nodes_table.id"), - sortable: true, - type: "numeric", - width: "72px", - filterable: true, - direction: "asc", - }, - node_product_name: { - title: this.hass.localize("ui.panel.config.ozw.nodes_table.model"), - sortable: true, - width: narrow ? "75%" : "25%", - }, - node_manufacturer_name: { - title: this.hass.localize( - "ui.panel.config.ozw.nodes_table.manufacturer" - ), - sortable: true, - hidden: narrow, - width: "25%", - }, - node_query_stage: { - title: this.hass.localize( - "ui.panel.config.ozw.nodes_table.query_stage" - ), - sortable: true, - width: narrow ? "25%" : "15%", - }, - is_zwave_plus: { - title: this.hass.localize("ui.panel.config.ozw.nodes_table.zwave_plus"), - hidden: narrow, - template: (value: boolean) => - value ? html` ` : "", - }, - is_failed: { - title: this.hass.localize("ui.panel.config.ozw.nodes_table.failed"), - hidden: narrow, - template: (value: boolean) => - value ? html` ` : "", - }, - }) - ); - - protected firstUpdated() { - if (!this.ozwInstance) { - navigate("/config/ozw/dashboard", { replace: true }); - } else if (this.hass) { - this._fetchData(); - } - } - - protected render(): TemplateResult { - return html` - - - `; - } - - private async _fetchData() { - this._nodes = await fetchOZWNodes(this.hass!, this.ozwInstance!); - } - - private _handleRowClicked(ev: HASSDomEvent) { - const nodeId = ev.detail.id; - navigate(`/config/ozw/network/${this.ozwInstance}/node/${nodeId}`); - } - - static get styles(): CSSResultGroup { - return haStyle; - } -} - -declare global { - interface HTMLElementTagNameMap { - "ozw-network-nodes": OZWNetworkNodes; - } -} diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-network-router.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-network-router.ts deleted file mode 100644 index 0294ff9b0f..0000000000 --- a/src/panels/config/integrations/integration-panels/ozw/ozw-network-router.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { mdiNetwork, mdiServerNetwork } from "@mdi/js"; -import { customElement, property } from "lit/decorators"; -import { - HassRouterPage, - RouterOptions, -} from "../../../../../layouts/hass-router-page"; -import { PageNavigation } from "../../../../../layouts/hass-tabs-subpage"; -import { HomeAssistant } from "../../../../../types"; -import { computeTail } from "./ozw-config-router"; - -export const ozwNetworkTabs = (instance: number): PageNavigation[] => [ - { - translationKey: "ui.panel.config.ozw.navigation.network", - path: `/config/ozw/network/${instance}/dashboard`, - iconPath: mdiServerNetwork, - }, - { - translationKey: "ui.panel.config.ozw.navigation.nodes", - path: `/config/ozw/network/${instance}/nodes`, - iconPath: mdiNetwork, - }, -]; - -@customElement("ozw-network-router") -class OZWNetworkRouter extends HassRouterPage { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property() public isWide!: boolean; - - @property() public narrow!: boolean; - - @property() public ozwInstance!: number; - - private _configEntry = new URLSearchParams(window.location.search).get( - "config_entry" - ); - - protected routerOptions: RouterOptions = { - defaultPage: "dashboard", - showLoading: true, - routes: { - dashboard: { - tag: "ozw-network-dashboard", - load: () => import("./ozw-network-dashboard"), - }, - nodes: { - tag: "ozw-network-nodes", - load: () => import("./ozw-network-nodes"), - }, - node: { - tag: "ozw-node-router", - load: () => import("./ozw-node-router"), - }, - }, - }; - - protected updatePageEl(el): void { - el.route = computeTail(this.routeTail); - el.hass = this.hass; - el.isWide = this.isWide; - el.narrow = this.narrow; - el.configEntryId = this._configEntry; - el.ozwInstance = this.ozwInstance; - if (this._currentPage === "node") { - el.nodeId = this.routeTail.path.split("/")[1]; - } - } -} - -declare global { - interface HTMLElementTagNameMap { - "ozw-network-router": OZWNetworkRouter; - } -} diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-node-config.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-node-config.ts deleted file mode 100644 index 7c29bf2cbc..0000000000 --- a/src/panels/config/integrations/integration-panels/ozw/ozw-node-config.ts +++ /dev/null @@ -1,265 +0,0 @@ -import "@material/mwc-button/mwc-button"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { navigate } from "../../../../../common/navigate"; -import "../../../../../components/buttons/ha-call-service-button"; -import "../../../../../components/ha-card"; -import { - fetchOZWNodeConfig, - fetchOZWNodeMetadata, - fetchOZWNodeStatus, - OZWDevice, - OZWDeviceConfig, - OZWDeviceMetaDataResponse, -} from "../../../../../data/ozw"; -import { ERR_NOT_FOUND } from "../../../../../data/websocket_api"; -import "../../../../../layouts/hass-tabs-subpage"; -import { haStyle } from "../../../../../resources/styles"; -import type { HomeAssistant, Route } from "../../../../../types"; -import "../../../ha-config-section"; -import { ozwNodeTabs } from "./ozw-node-router"; -import { showOZWRefreshNodeDialog } from "./show-dialog-ozw-refresh-node"; - -@customElement("ozw-node-config") -class OZWNodeConfig extends LitElement { - @property({ type: Object }) public hass!: HomeAssistant; - - @property({ type: Object }) public route!: Route; - - @property({ type: Boolean }) public narrow!: boolean; - - @property({ type: Boolean }) public isWide!: boolean; - - @property() public configEntryId?: string; - - @property() public ozwInstance?; - - @property() public nodeId?; - - @state() private _node?: OZWDevice; - - @state() private _metadata?: OZWDeviceMetaDataResponse; - - @state() private _config?: OZWDeviceConfig[]; - - @state() private _error?: string; - - protected firstUpdated() { - if (!this.ozwInstance) { - navigate("/config/ozw/dashboard", { replace: true }); - } else if (!this.nodeId) { - navigate(`/config/ozw/network/${this.ozwInstance}/nodes`, { - replace: true, - }); - } else { - this._fetchData(); - } - } - - protected render(): TemplateResult { - if (this._error) { - return html` - - `; - } - - return html` - - -
- ${this.hass.localize("ui.panel.config.ozw.node_config.header")} -
- -
- ${this.hass.localize( - "ui.panel.config.ozw.node_config.introduction" - )} -

- - ${this.hass.localize( - "ui.panel.config.ozw.node_config.help_source" - )} - -

-

- Note: This panel is currently read-only. The ability to change - values will come in a later update. -

-
- ${this._node - ? html` - -
- - ${this._node.node_manufacturer_name} - ${this._node.node_product_name}
- ${this.hass.localize("ui.panel.config.ozw.common.node_id")}: - ${this._node.node_id}
- ${this.hass.localize( - "ui.panel.config.ozw.common.query_stage" - )}: - ${this._node.node_query_stage} - ${this._metadata?.metadata.ProductManualURL - ? html` -

- ${this.hass.localize( - "ui.panel.config.ozw.node_metadata.product_manual" - )} -

-
` - : ``} -
-
- - ${this.hass.localize( - "ui.panel.config.ozw.refresh_node.button" - )} - -
-
- - ${this._metadata?.metadata.WakeupHelp - ? html` - -
- - ${this.hass.localize( - "ui.panel.config.ozw.node_config.wakeup_help" - )} - -

${this._metadata.metadata.WakeupHelp}

-
-
- ` - : ``} - ${this._config - ? html` - ${this._config.map( - (item) => html` - -
- ${item.label}
- ${item.help} -

${item.value}

-
-
- ` - )} - ` - : ``} - ` - : ``} -
-
- `; - } - - private async _fetchData() { - if (!this.ozwInstance || !this.nodeId) { - return; - } - - try { - const nodeProm = fetchOZWNodeStatus( - this.hass!, - this.ozwInstance, - this.nodeId - ); - const metadataProm = fetchOZWNodeMetadata( - this.hass!, - this.ozwInstance, - this.nodeId - ); - const configProm = fetchOZWNodeConfig( - this.hass!, - this.ozwInstance, - this.nodeId - ); - [this._node, this._metadata, this._config] = await Promise.all([ - nodeProm, - metadataProm, - configProm, - ]); - } catch (err: any) { - if (err.code === ERR_NOT_FOUND) { - this._error = ERR_NOT_FOUND; - return; - } - throw err; - } - } - - private async _refreshNodeClicked() { - showOZWRefreshNodeDialog(this, { - node_id: this.nodeId, - ozw_instance: this.ozwInstance, - }); - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - .secondary { - color: var(--secondary-text-color); - font-size: 0.9em; - } - - .content { - margin-top: 24px; - } - - .sectionHeader { - position: relative; - padding-right: 40px; - } - - ha-card { - margin: 0 auto; - max-width: 600px; - } - - [hidden] { - display: none; - } - - blockquote { - display: block; - background-color: #ddd; - padding: 8px; - margin: 8px 0; - font-size: 0.9em; - } - - blockquote em { - font-size: 0.9em; - margin-top: 6px; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "ozw-node-config": OZWNodeConfig; - } -} diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-node-dashboard.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-node-dashboard.ts deleted file mode 100644 index ec45071c16..0000000000 --- a/src/panels/config/integrations/integration-panels/ozw/ozw-node-dashboard.ts +++ /dev/null @@ -1,254 +0,0 @@ -import "@material/mwc-button/mwc-button"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { navigate } from "../../../../../common/navigate"; -import "../../../../../components/buttons/ha-call-service-button"; -import "../../../../../components/ha-card"; -import { - fetchOZWNodeMetadata, - fetchOZWNodeStatus, - OZWDevice, - OZWDeviceMetaDataResponse, -} from "../../../../../data/ozw"; -import { ERR_NOT_FOUND } from "../../../../../data/websocket_api"; -import "../../../../../layouts/hass-tabs-subpage"; -import { haStyle } from "../../../../../resources/styles"; -import type { HomeAssistant, Route } from "../../../../../types"; -import "../../../ha-config-section"; -import { ozwNodeTabs } from "./ozw-node-router"; -import { showOZWRefreshNodeDialog } from "./show-dialog-ozw-refresh-node"; - -@customElement("ozw-node-dashboard") -class OZWNodeDashboard extends LitElement { - @property({ type: Object }) public hass!: HomeAssistant; - - @property({ type: Object }) public route!: Route; - - @property({ type: Boolean }) public narrow!: boolean; - - @property({ type: Boolean }) public isWide!: boolean; - - @property() public configEntryId?: string; - - @property() public ozwInstance?; - - @property() public nodeId?; - - @state() private _node?: OZWDevice; - - @state() private _metadata?: OZWDeviceMetaDataResponse; - - @state() private _not_found = false; - - protected firstUpdated() { - if (!this.ozwInstance) { - navigate("/config/ozw/dashboard", { replace: true }); - } else if (!this.nodeId) { - navigate(`/config/ozw/network/${this.ozwInstance}/nodes`, { - replace: true, - }); - } else if (this.hass) { - this._fetchData(); - } - } - - protected render(): TemplateResult { - if (this._not_found) { - return html` - - `; - } - - return html` - - -
Node Management
- -
- View the status of a node and manage its configuration. -
- ${this._node - ? html` - -
-
- - ${this._node.node_manufacturer_name} - ${this._node.node_product_name} - -
- Node ID: ${this._node.node_id}
- Query Stage: ${this._node.node_query_stage} - ${this._metadata?.metadata.ProductManualURL - ? html` -

Product Manual

-
` - : ``} -
- ${this._metadata?.metadata.ProductPicBase64 - ? html`` - : ``} -
-
- - Refresh Node - -
-
- - ${this._metadata - ? html` - -
- ${this._metadata.metadata.Description} -
-
- -
- ${this._metadata.metadata.InclusionHelp} -
-
- -
- ${this._metadata.metadata.ExclusionHelp} -
-
- -
- ${this._metadata.metadata.ResetHelp} -
-
- ${this._metadata.metadata.WakeupHelp - ? html` - -
- ${this._metadata.metadata.WakeupHelp} -
-
- ` - : ``} - ` - : ``} - ` - : ``} -
-
- `; - } - - private async _fetchData() { - if (!this.ozwInstance || !this.nodeId) { - return; - } - - try { - this._node = await fetchOZWNodeStatus( - this.hass!, - this.ozwInstance, - this.nodeId - ); - this._metadata = await fetchOZWNodeMetadata( - this.hass!, - this.ozwInstance, - this.nodeId - ); - } catch (err: any) { - if (err.code === ERR_NOT_FOUND) { - this._not_found = true; - return; - } - throw err; - } - } - - private async _refreshNodeClicked() { - showOZWRefreshNodeDialog(this, { - node_id: this.nodeId, - ozw_instance: this.ozwInstance, - }); - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - .secondary { - color: var(--secondary-text-color); - } - - .content { - margin-top: 24px; - } - - .content:last-child { - margin-bottom: 24px; - } - - .sectionHeader { - position: relative; - padding-right: 40px; - } - - ha-card { - margin: 0 auto; - max-width: 600px; - } - - .flex { - display: flex; - justify-content: space-between; - } - - .card-actions.warning ha-call-service-button { - color: var(--error-color); - } - - .toggle-help-icon { - position: absolute; - top: -6px; - right: 0; - color: var(--primary-color); - } - - ha-service-description { - display: block; - color: grey; - padding: 0 8px 12px; - } - - [hidden] { - display: none; - } - - .product-image { - padding: 12px; - max-height: 140px; - max-width: 140px; - } - .card-actions { - clear: right; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "ozw-node-dashboard": OZWNodeDashboard; - } -} diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-node-router.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-node-router.ts deleted file mode 100644 index f5676bdcaa..0000000000 --- a/src/panels/config/integrations/integration-panels/ozw/ozw-node-router.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { mdiNetwork, mdiWrench } from "@mdi/js"; -import { customElement, property } from "lit/decorators"; -import { navigate } from "../../../../../common/navigate"; -import { - HassRouterPage, - RouterOptions, -} from "../../../../../layouts/hass-router-page"; -import { PageNavigation } from "../../../../../layouts/hass-tabs-subpage"; -import { HomeAssistant } from "../../../../../types"; - -export const ozwNodeTabs = ( - instance: number, - node: number -): PageNavigation[] => [ - { - translationKey: "ui.panel.config.ozw.navigation.node.dashboard", - path: `/config/ozw/network/${instance}/node/${node}/dashboard`, - iconPath: mdiNetwork, - }, - { - translationKey: "ui.panel.config.ozw.navigation.node.config", - path: `/config/ozw/network/${instance}/node/${node}/config`, - iconPath: mdiWrench, - }, -]; - -@customElement("ozw-node-router") -class OZWNodeRouter extends HassRouterPage { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property() public isWide!: boolean; - - @property() public narrow!: boolean; - - @property() public ozwInstance!: number; - - @property() public nodeId!: number; - - private _configEntry = new URLSearchParams(window.location.search).get( - "config_entry" - ); - - protected routerOptions: RouterOptions = { - defaultPage: "dashboard", - showLoading: true, - routes: { - dashboard: { - tag: "ozw-node-dashboard", - load: () => import("./ozw-node-dashboard"), - }, - config: { - tag: "ozw-node-config", - load: () => import("./ozw-node-config"), - }, - }, - }; - - protected updatePageEl(el): void { - el.route = this.routeTail; - el.hass = this.hass; - el.isWide = this.isWide; - el.narrow = this.narrow; - el.configEntryId = this._configEntry; - el.ozwInstance = this.ozwInstance; - el.nodeId = this.nodeId; - - const searchParams = new URLSearchParams(window.location.search); - if (this._configEntry && !searchParams.has("config_entry")) { - searchParams.append("config_entry", this._configEntry); - navigate( - `${this.routeTail.prefix}${ - this.routeTail.path - }?${searchParams.toString()}`, - { replace: true } - ); - } - } -} - -declare global { - interface HTMLElementTagNameMap { - "ozw-node-router": OZWNodeRouter; - } -} diff --git a/src/panels/config/integrations/integration-panels/ozw/show-dialog-ozw-refresh-node.ts b/src/panels/config/integrations/integration-panels/ozw/show-dialog-ozw-refresh-node.ts deleted file mode 100644 index e3ecd75e47..0000000000 --- a/src/panels/config/integrations/integration-panels/ozw/show-dialog-ozw-refresh-node.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { fireEvent } from "../../../../../common/dom/fire_event"; - -export interface OZWRefreshNodeDialogParams { - ozw_instance: number; - node_id: number; -} - -export const loadRefreshNodeDialog = () => import("./dialog-ozw-refresh-node"); - -export const showOZWRefreshNodeDialog = ( - element: HTMLElement, - refreshNodeDialogParams: OZWRefreshNodeDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-ozw-refresh-node", - dialogImport: loadRefreshNodeDialog, - dialogParams: refreshNodeDialogParams, - }); -}; diff --git a/src/panels/config/integrations/integration-panels/zwave/ha-config-zwave.js b/src/panels/config/integrations/integration-panels/zwave/ha-config-zwave.js deleted file mode 100644 index 58caa941af..0000000000 --- a/src/panels/config/integrations/integration-panels/zwave/ha-config-zwave.js +++ /dev/null @@ -1,765 +0,0 @@ -import "@polymer/app-layout/app-header/app-header"; -import "@polymer/app-layout/app-toolbar/app-toolbar"; -import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; -import { setCancelSyntheticClickEvents } from "@polymer/polymer/lib/utils/settings"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { computeStateDomain } from "../../../../../common/entity/compute_state_domain"; -import { computeStateName } from "../../../../../common/entity/compute_state_name"; -import { sortStatesByName } from "../../../../../common/entity/states_sort_by_name"; -import "../../../../../components/buttons/ha-call-service-button"; -import "../../../../../components/ha-card"; -import "../../../../../components/ha-alert"; -import "../../../../../components/ha-icon"; -import "../../../../../components/ha-icon-button"; -import "../../../../../components/ha-icon-button-arrow-prev"; -import "../../../../../components/ha-menu-button"; -import "../../../../../components/ha-service-description"; -import "../../../../../layouts/ha-app-layout"; -import { EventsMixin } from "../../../../../mixins/events-mixin"; -import LocalizeMixin from "../../../../../mixins/localize-mixin"; -import "../../../../../styles/polymer-ha-style"; -import "../../../ha-config-section"; -import "../../../ha-form-style"; -import "./zwave-groups"; -import "./zwave-log"; -import "./zwave-network"; -import "./zwave-node-config"; -import "./zwave-node-protection"; -import "./zwave-usercodes"; -import "./zwave-values"; - -/* - * @appliesMixin LocalizeMixin - * @appliesMixin EventsMixin - */ -class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) { - static get template() { - return html` - - - - - -
[[localize('component.zwave.title')]]
-
-
- - - This Z-Wave integration is deprecated and will no longer receive any - updates. The technical dependencies will render this integration - unusable in the near future. We strongly advise you to migrate to the - new - Z-Wave JS integration. - - learn more - - - - - -
- [[localize('ui.panel.config.zwave.migration.zwave_js.introduction')]] -
- -
-
- - - - - -
- [[localize('ui.panel.config.zwave.node_management.header')]] - - - -
- - [[localize('ui.panel.config.zwave.node_management.introduction')]] - - - -
- - - - - -
- - - -
- - - - - - - - -
- - - -
- `; - } - - static get properties() { - return { - hass: Object, - - isWide: Boolean, - - nodes: { - type: Array, - computed: "computeNodes(hass)", - }, - - selectedNode: { - type: Number, - value: -1, - observer: "selectedNodeChanged", - }, - - nodeFailed: { - type: Boolean, - value: false, - }, - - config: { - type: Array, - value: () => [], - }, - - entities: { - type: Array, - computed: "computeEntities(selectedNode)", - }, - - selectedEntity: { - type: Number, - value: -1, - observer: "selectedEntityChanged", - }, - - values: { - type: Array, - }, - - groups: { - type: Array, - }, - - userCodes: { - type: Array, - value: () => [], - }, - - hasNodeUserCodes: { - type: Boolean, - value: false, - }, - - showHelp: { - type: Boolean, - value: false, - }, - - entityIgnored: Boolean, - - entityPollingIntensity: { - type: Number, - value: 0, - }, - - _protection: { - type: Array, - value: () => [], - }, - - _protectionNode: { - type: Boolean, - value: false, - }, - }; - } - - ready() { - super.ready(); - import("web-animations-js/web-animations-next-lite.min"); - this.addEventListener("hass-service-called", (ev) => - this.serviceCalled(ev) - ); - } - - attached() { - setCancelSyntheticClickEvents(true); - } - - detached() { - setCancelSyntheticClickEvents(false); - } - - serviceCalled(ev) { - if (ev.detail.success && ev.detail.service === "set_poll_intensity") { - this._saveEntity(); - } - } - - computeNodes(hass) { - return Object.keys(hass.states) - .map((key) => hass.states[key]) - .filter((ent) => ent.entity_id.match("zwave[.]")) - .sort(sortStatesByName); - } - - computeEntities(selectedNode) { - if (!this.nodes || selectedNode === -1) { - return -1; - } - const nodeid = this.nodes[this.selectedNode].attributes.node_id; - const hass = this.hass; - return Object.keys(this.hass.states) - .map((key) => hass.states[key]) - .filter((ent) => { - if (ent.attributes.node_id === undefined) { - return false; - } - return ( - "node_id" in ent.attributes && - ent.attributes.node_id === nodeid && - !ent.entity_id.match("zwave[.]") - ); - }) - .sort(sortStatesByName); - } - - selectedNodeChanged(selectedNode) { - if (selectedNode === -1) { - return; - } - this.selectedEntity = -1; - - this.hass - .callApi( - "GET", - `zwave/config/${this.nodes[selectedNode].attributes.node_id}` - ) - .then((configs) => { - this.config = this._objToArray(configs); - }); - - this.hass - .callApi( - "GET", - `zwave/values/${this.nodes[selectedNode].attributes.node_id}` - ) - .then((values) => { - this.values = this._objToArray(values); - }); - - this.hass - .callApi( - "GET", - `zwave/groups/${this.nodes[selectedNode].attributes.node_id}` - ) - .then((groups) => { - this.groups = this._objToArray(groups); - }); - - this.hasNodeUserCodes = false; - this.notifyPath("hasNodeUserCodes"); - this.hass - .callApi( - "GET", - `zwave/usercodes/${this.nodes[selectedNode].attributes.node_id}` - ) - .then((usercodes) => { - this.userCodes = this._objToArray(usercodes); - this.hasNodeUserCodes = this.userCodes.length > 0; - this.notifyPath("hasNodeUserCodes"); - }); - this.hass - .callApi( - "GET", - `zwave/protection/${this.nodes[selectedNode].attributes.node_id}` - ) - .then((protections) => { - this._protection = this._objToArray(protections); - if (this._protection) { - if (this._protection.length === 0) { - return; - } - this._protectionNode = true; - } - }); - - this.nodeFailed = this.nodes[selectedNode].attributes.is_failed; - } - - selectedEntityChanged(selectedEntity) { - if (selectedEntity === -1) { - return; - } - this.hass - .callApi( - "GET", - `zwave/values/${this.nodes[this.selectedNode].attributes.node_id}` - ) - .then((values) => { - this.values = this._objToArray(values); - }); - - const valueId = this.entities[selectedEntity].attributes.value_id; - const valueData = this.values.find((obj) => obj.key === valueId); - const valueIndex = this.values.indexOf(valueData); - this.hass - .callApi( - "GET", - `config/zwave/device_config/${this.entities[selectedEntity].entity_id}` - ) - .then((data) => { - this.setProperties({ - entityIgnored: data.ignored || false, - entityPollingIntensity: this.values[valueIndex].value.poll_intensity, - }); - }) - .catch(() => { - this.setProperties({ - entityIgnored: false, - entityPollingIntensity: this.values[valueIndex].value.poll_intensity, - }); - }); - } - - computeSelectCaption(stateObj) { - return ( - computeStateName(stateObj) + - " (Node:" + - stateObj.attributes.node_id + - " " + - stateObj.attributes.query_stage + - ")" - ); - } - - computeSelectCaptionEnt(stateObj) { - return computeStateDomain(stateObj) + "." + computeStateName(stateObj); - } - - computeIsNodeSelected() { - return this.nodes && this.selectedNode !== -1; - } - - computeIsEntitySelected(selectedEntity) { - return selectedEntity === -1; - } - - computeNodeServiceData(selectedNode) { - return { node_id: this.nodes[selectedNode].attributes.node_id }; - } - - computeHealNodeServiceData(selectedNode) { - return { - node_id: this.nodes[selectedNode].attributes.node_id, - return_routes: true, - }; - } - - computeRefreshEntityServiceData(selectedEntity) { - if (selectedEntity === -1) { - return -1; - } - return { entity_id: this.entities[selectedEntity].entity_id }; - } - - computePollIntensityServiceData(entityPollingIntensity) { - if (this.selectedNode === -1 || this.selectedEntity === -1) { - return -1; - } - return { - node_id: this.nodes[this.selectedNode].attributes.node_id, - value_id: this.entities[this.selectedEntity].attributes.value_id, - poll_intensity: parseInt(entityPollingIntensity), - }; - } - - _nodeMoreInfo() { - this.fire("hass-more-info", { - entityId: this.nodes[this.selectedNode].entity_id, - }); - } - - _entityMoreInfo() { - this.fire("hass-more-info", { - entityId: this.entities[this.selectedEntity].entity_id, - }); - } - - _saveEntity() { - const data = { - ignored: this.entityIgnored, - polling_intensity: parseInt(this.entityPollingIntensity), - }; - return this.hass.callApi( - "POST", - `config/zwave/device_config/${ - this.entities[this.selectedEntity].entity_id - }`, - data - ); - } - - toggleHelp() { - this.showHelp = !this.showHelp; - } - - _objToArray(obj) { - const array = []; - Object.keys(obj).forEach((key) => { - array.push({ - key, - value: obj[key], - }); - }); - return array; - } - - _backTapped() { - history.back(); - } - - entityIgnoredChanged(ev) { - this.entityIgnored = ev.target.checked; - } -} - -customElements.define("ha-config-zwave", HaConfigZwave); diff --git a/src/panels/config/integrations/integration-panels/zwave/zwave-config-router.ts b/src/panels/config/integrations/integration-panels/zwave/zwave-config-router.ts deleted file mode 100644 index 4ebb4b9970..0000000000 --- a/src/panels/config/integrations/integration-panels/zwave/zwave-config-router.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { customElement, property } from "lit/decorators"; -import { navigate } from "../../../../../common/navigate"; -import { - HassRouterPage, - RouterOptions, -} from "../../../../../layouts/hass-router-page"; -import { HomeAssistant } from "../../../../../types"; - -@customElement("zwave-config-router") -class ZWaveConfigRouter extends HassRouterPage { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property() public isWide!: boolean; - - @property() public narrow!: boolean; - - private _configEntry = new URLSearchParams(window.location.search).get( - "config_entry" - ); - - protected routerOptions: RouterOptions = { - defaultPage: "dashboard", - showLoading: true, - routes: { - dashboard: { - tag: "ha-config-zwave", - load: () => - import(/* webpackChunkName: "ha-config-zwave" */ "./ha-config-zwave"), - }, - migration: { - tag: "zwave-migration", - load: () => - import(/* webpackChunkName: "zwave-migration" */ "./zwave-migration"), - }, - }, - }; - - protected updatePageEl(el): void { - el.route = this.routeTail; - el.hass = this.hass; - el.isWide = this.isWide; - el.narrow = this.narrow; - el.configEntryId = this._configEntry; - - const searchParams = new URLSearchParams(window.location.search); - if (this._configEntry && !searchParams.has("config_entry")) { - searchParams.append("config_entry", this._configEntry); - navigate( - `${this.routeTail.prefix}${ - this.routeTail.path - }?${searchParams.toString()}`, - { replace: true } - ); - } - } -} - -declare global { - interface HTMLElementTagNameMap { - "zwave-config-router": ZWaveConfigRouter; - } -} diff --git a/src/panels/config/integrations/integration-panels/zwave/zwave-groups.js b/src/panels/config/integrations/integration-panels/zwave/zwave-groups.js deleted file mode 100644 index 129587fcfb..0000000000 --- a/src/panels/config/integrations/integration-panels/zwave/zwave-groups.js +++ /dev/null @@ -1,380 +0,0 @@ -import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { computeStateName } from "../../../../../common/entity/compute_state_name"; -import "../../../../../components/buttons/ha-call-service-button"; -import "../../../../../components/ha-card"; -import LocalizeMixin from "../../../../../mixins/localize-mixin"; -import "../../../../../styles/polymer-ha-style"; - -class ZwaveGroups extends LocalizeMixin(PolymerElement) { - static get template() { - return html` - - - -
- - - - - -
- - - -
- `; - } - - static get properties() { - return { - hass: Object, - - nodes: Array, - - groups: Array, - - selectedNode: { - type: Number, - observer: "_selectedNodeChanged", - }, - - _selectedTargetNode: { - type: Number, - value: -1, - observer: "_selectedTargetNodeChanged", - }, - - _selectedGroup: { - type: Number, - value: -1, - }, - - _otherGroupNodes: { - type: Array, - value: -1, - computed: "_computeOtherGroupNodes(_selectedGroup)", - }, - - _maxAssociations: { - type: String, - value: "", - computed: "_computeMaxAssociations(_selectedGroup)", - }, - - _noAssociationsLeft: { - type: Boolean, - value: true, - computed: "_computeAssociationsLeft(_selectedGroup)", - }, - - _addAssocServiceData: { - type: String, - value: "", - }, - - _removeAssocServiceData: { - type: String, - value: "", - }, - - _removeBroadcastNodeServiceData: { - type: String, - value: "", - }, - - _isBroadcastNodeInGroup: { - type: Boolean, - value: false, - }, - }; - } - - static get observers() { - return ["_selectedGroupChanged(groups, _selectedGroup)"]; - } - - ready() { - super.ready(); - this.addEventListener("hass-service-called", (ev) => - this.serviceCalled(ev) - ); - } - - serviceCalled(ev) { - if (ev.detail.success) { - setTimeout(() => { - this._refreshGroups(this.selectedNode); - }, 5000); - } - } - - _computeAssociationsLeft(selectedGroup) { - if (selectedGroup === -1) return true; - return this._maxAssociations === this._otherGroupNodes.length; - } - - _computeMaxAssociations(selectedGroup) { - if (selectedGroup === -1) return -1; - const maxAssociations = this.groups[selectedGroup].value.max_associations; - if (!maxAssociations) return "None"; - return maxAssociations; - } - - _computeOtherGroupNodes(selectedGroup) { - if (selectedGroup === -1) return -1; - this.setProperties({ _isBroadcastNodeInGroup: false }); - const associations = Object.values( - this.groups[selectedGroup].value.association_instances - ); - if (!associations.length) return ["None"]; - return associations.map((assoc) => { - if (!assoc.length || assoc.length !== 2) { - return `Unknown Node: ${assoc}`; - } - const id = assoc[0]; - const instance = assoc[1]; - const node = this.nodes.find((n) => n.attributes.node_id === id); - if (id === 255) { - this.setProperties({ - _isBroadcastNodeInGroup: true, - _removeBroadcastNodeServiceData: { - node_id: this.nodes[this.selectedNode].attributes.node_id, - association: "remove", - target_node_id: 255, - group: this.groups[selectedGroup].key, - }, - }); - } - if (!node) { - return `Unknown Node (${id}: (${instance} ? ${id}.${instance} : ${id}))`; - } - let caption = this._computeSelectCaption(node); - if (instance) { - caption += `/ Instance: ${instance}`; - } - return caption; - }); - } - - _computeTargetInGroup(selectedGroup, selectedTargetNode) { - if (selectedGroup === -1 || selectedTargetNode === -1) return false; - const associations = Object.values( - this.groups[selectedGroup].value.associations - ); - if (!associations.length) return false; - return ( - associations.indexOf( - this.nodes[selectedTargetNode].attributes.node_id - ) !== -1 - ); - } - - _computeSelectCaption(stateObj) { - return `${computeStateName(stateObj)} - (Node: ${stateObj.attributes.node_id} - ${stateObj.attributes.query_stage})`; - } - - _computeSelectCaptionGroup(stateObj) { - return `${stateObj.key}: ${stateObj.value.label}`; - } - - _computeIsTargetNodeSelected(selectedTargetNode) { - return this.nodes && selectedTargetNode !== -1; - } - - _computeIsGroupSelected(selectedGroup) { - return this.nodes && this.selectedNode !== -1 && selectedGroup !== -1; - } - - _computeAssocServiceData(selectedGroup, type) { - if ( - !this.groups || - selectedGroup === -1 || - this.selectedNode === -1 || - this._selectedTargetNode === -1 - ) { - return -1; - } - return { - node_id: this.nodes[this.selectedNode].attributes.node_id, - association: type, - target_node_id: this.nodes[this._selectedTargetNode].attributes.node_id, - group: this.groups[selectedGroup].key, - }; - } - - async _refreshGroups(selectedNode) { - const groupData = []; - const groups = await this.hass.callApi( - "GET", - `zwave/groups/${this.nodes[selectedNode].attributes.node_id}` - ); - Object.keys(groups).forEach((key) => { - groupData.push({ - key, - value: groups[key], - }); - }); - this.setProperties({ - groups: groupData, - _maxAssociations: groupData[this._selectedGroup].value.max_associations, - _otherGroupNodes: Object.values( - groupData[this._selectedGroup].value.associations - ), - _isBroadcastNodeInGroup: false, - }); - const oldGroup = this._selectedGroup; - this.setProperties({ _selectedGroup: -1 }); - this.setProperties({ _selectedGroup: oldGroup }); - } - - _selectedGroupChanged() { - if (this._selectedGroup === -1) return; - this.setProperties({ - _maxAssociations: this.groups[this._selectedGroup].value.max_associations, - _otherGroupNodes: Object.values( - this.groups[this._selectedGroup].value.associations - ), - }); - } - - _selectedTargetNodeChanged() { - if (this._selectedGroup === -1) return; - if ( - this._computeTargetInGroup(this._selectedGroup, this._selectedTargetNode) - ) { - this.setProperties({ - _removeAssocServiceData: this._computeAssocServiceData( - this._selectedGroup, - "remove" - ), - }); - } else { - this.setProperties({ - _addAssocServiceData: this._computeAssocServiceData( - this._selectedGroup, - "add" - ), - }); - } - } - - _selectedNodeChanged() { - if (this.selectedNode === -1) return; - this.setProperties({ _selectedTargetNode: -1, _selectedGroup: -1 }); - } -} - -customElements.define("zwave-groups", ZwaveGroups); diff --git a/src/panels/config/integrations/integration-panels/zwave/zwave-log-dialog.js b/src/panels/config/integrations/integration-panels/zwave/zwave-log-dialog.js deleted file mode 100644 index df5acc95d1..0000000000 --- a/src/panels/config/integrations/integration-panels/zwave/zwave-log-dialog.js +++ /dev/null @@ -1,83 +0,0 @@ -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { EventsMixin } from "../../../../../mixins/events-mixin"; -import "../../../../../styles/polymer-ha-style-dialog"; -import "../../../../../components/ha-dialog"; - -class ZwaveLogDialog extends EventsMixin(PolymerElement) { - static get template() { - return html` - - -
-
[[_ozwLog]]
-
- - `; - } - - static get properties() { - return { - hass: Object, - _ozwLog: String, - - _dialogClosedCallback: Function, - - _opened: { - type: Boolean, - value: false, - }, - - _intervalId: String, - - _numLogLines: { - type: Number, - }, - }; - } - - ready() { - super.ready(); - this.addEventListener("iron-overlay-closed", (ev) => - this._dialogClosed(ev) - ); - } - - showDialog({ _ozwLog, hass, _tail, _numLogLines, dialogClosedCallback }) { - this.hass = hass; - this._ozwLog = _ozwLog; - this._opened = true; - this._dialogClosedCallback = dialogClosedCallback; - this._numLogLines = _numLogLines; - if (_tail) { - this.setProperties({ - _intervalId: setInterval(() => { - this._refreshLog(); - }, 1500), - }); - } - } - - closeDialog() { - clearInterval(this._intervalId); - this._opened = false; - const closedEvent = true; - this._dialogClosedCallback({ closedEvent }); - this._dialogClosedCallback = null; - } - - async _refreshLog() { - const info = await this.hass.callApi( - "GET", - "zwave/ozwlog?lines=" + this._numLogLines - ); - this.setProperties({ _ozwLog: info }); - } -} - -customElements.define("zwave-log-dialog", ZwaveLogDialog); diff --git a/src/panels/config/integrations/integration-panels/zwave/zwave-log.js b/src/panels/config/integrations/integration-panels/zwave/zwave-log.js deleted file mode 100755 index c55e153f00..0000000000 --- a/src/panels/config/integrations/integration-panels/zwave/zwave-log.js +++ /dev/null @@ -1,160 +0,0 @@ -import "@material/mwc-button"; -import "@polymer/paper-input/paper-input"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import isPwa from "../../../../../common/config/is_pwa"; -import "../../../../../components/ha-card"; -import { EventsMixin } from "../../../../../mixins/events-mixin"; -import LocalizeMixin from "../../../../../mixins/localize-mixin"; -import "../../../../../styles/polymer-ha-style"; -import "../../../ha-config-section"; - -let registeredDialog = false; - -class OzwLog extends LocalizeMixin(EventsMixin(PolymerElement)) { - static get template() { - return html` - - - - [[localize('ui.panel.config.zwave.ozw_log.header')]] - - - [[localize('ui.panel.config.zwave.ozw_log.introduction')]] - - -
- - -
-
- [[localize('ui.panel.config.zwave.ozw_log.load')]] - [[localize('ui.panel.config.zwave.ozw_log.tail')]] - - -`; - } - - static get properties() { - return { - hass: Object, - - isWide: { - type: Boolean, - value: false, - }, - - _ozwLogs: String, - - _completeLog: { - type: Boolean, - value: true, - }, - - numLogLines: { - type: Number, - value: 0, - observer: "_isCompleteLog", - }, - - _intervalId: String, - - tail: Boolean, - }; - } - - async _tailLog() { - this.setProperties({ tail: true }); - const ozwWindow = await this._openLogWindow(); - if (!isPwa()) { - this.setProperties({ - _intervalId: setInterval(() => { - this._refreshLog(ozwWindow); - }, 1500), - }); - } - } - - async _openLogWindow() { - const info = await this.hass.callApi( - "GET", - "zwave/ozwlog?lines=" + this.numLogLines - ); - this.setProperties({ _ozwLogs: info }); - if (isPwa()) { - this._showOzwlogDialog(); - return -1; - } - const ozwWindow = open("", "ozwLog", "toolbar"); - ozwWindow.document.body.innerHTML = `
${this._ozwLogs}
`; - return ozwWindow; - } - - async _refreshLog(ozwWindow) { - if (ozwWindow.closed === true) { - clearInterval(this._intervalId); - this.setProperties({ _intervalId: null }); - } else { - const info = await this.hass.callApi( - "GET", - "zwave/ozwlog?lines=" + this.numLogLines - ); - this.setProperties({ _ozwLogs: info }); - ozwWindow.document.body.innerHTML = `
${this._ozwLogs}
`; - } - } - - _isCompleteLog() { - if (this.numLogLines !== "0") { - this.setProperties({ _completeLog: false }); - } else { - this.setProperties({ _completeLog: true }); - } - } - - connectedCallback() { - super.connectedCallback(); - if (!registeredDialog) { - registeredDialog = true; - this.fire("register-dialog", { - dialogShowEvent: "show-ozwlog-dialog", - dialogTag: "zwave-log-dialog", - dialogImport: () => import("./zwave-log-dialog"), - }); - } - } - - _showOzwlogDialog() { - this.fire("show-ozwlog-dialog", { - hass: this.hass, - _numLogLines: this.numLogLines, - _ozwLog: this._ozwLogs, - _tail: this.tail, - dialogClosedCallback: () => this._dialogClosed(), - }); - } - - _dialogClosed() { - this.setProperties({ - tail: false, - }); - } -} -customElements.define("ozw-log", OzwLog); diff --git a/src/panels/config/integrations/integration-panels/zwave/zwave-migration.ts b/src/panels/config/integrations/integration-panels/zwave/zwave-migration.ts deleted file mode 100644 index 2272bd884e..0000000000 --- a/src/panels/config/integrations/integration-panels/zwave/zwave-migration.ts +++ /dev/null @@ -1,573 +0,0 @@ -import "@material/mwc-button/mwc-button"; -import "@polymer/app-layout/app-header/app-header"; -import "@polymer/app-layout/app-toolbar/app-toolbar"; -import { UnsubscribeFunc } from "home-assistant-js-websocket"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { isComponentLoaded } from "../../../../../common/config/is_component_loaded"; -import { computeStateDomain } from "../../../../../common/entity/compute_state_domain"; -import { computeStateName } from "../../../../../common/entity/compute_state_name"; -import "../../../../../components/buttons/ha-call-api-button"; -import "../../../../../components/buttons/ha-call-service-button"; -import "../../../../../components/ha-alert"; -import "../../../../../components/ha-card"; -import "../../../../../components/ha-circular-progress"; -import "../../../../../components/ha-icon"; -import "../../../../../components/ha-icon-button"; -import { - computeDeviceName, - DeviceRegistryEntry, - fetchDeviceRegistry, - subscribeDeviceRegistry, -} from "../../../../../data/device_registry"; -import { - fetchMigrationConfig, - fetchNetworkStatus, - startZwaveJsConfigFlow, - ZWaveMigrationConfig, - ZWaveNetworkStatus, - ZWAVE_NETWORK_STATE_STOPPED, -} from "../../../../../data/zwave"; -import { - fetchZwaveNetworkStatus as fetchZwaveJsNetworkStatus, - fetchZwaveNodeStatus, - getZwaveJsIdentifiersFromDevice, - migrateZwave, - subscribeZwaveNodeReady, - ZWaveJsMigrationData, -} from "../../../../../data/zwave_js"; -import { showConfigFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-config-flow"; -import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box"; -import "../../../../../layouts/hass-subpage"; -import { haStyle } from "../../../../../resources/styles"; -import type { HomeAssistant, Route } from "../../../../../types"; -import "../../../ha-config-section"; - -@customElement("zwave-migration") -export class ZwaveMigration extends LitElement { - @property({ type: Object }) public hass!: HomeAssistant; - - @property({ type: Object }) public route!: Route; - - @property({ type: Boolean }) public narrow!: boolean; - - @property({ type: Boolean }) public isWide!: boolean; - - @state() private _networkStatus?: ZWaveNetworkStatus; - - @state() private _step = 0; - - @state() private _stoppingNetwork = false; - - @state() private _migrationConfig?: ZWaveMigrationConfig; - - @state() private _migrationData?: ZWaveJsMigrationData; - - @state() private _migratedZwaveEntities?: string[]; - - @state() private _deviceNameLookup: { [id: string]: string } = {}; - - @state() private _waitingOnDevices?: DeviceRegistryEntry[]; - - private _zwaveJsEntryId?: string; - - private _nodeReadySubscriptions?: Promise[]; - - private _unsub?: Promise; - - private _unsubDevices?: UnsubscribeFunc; - - public disconnectedCallback(): void { - this._unsubscribe(); - if (this._unsubDevices) { - this._unsubDevices(); - this._unsubDevices = undefined; - } - } - - protected render(): TemplateResult { - return html` - - -
- ${this.hass.localize( - "ui.panel.config.zwave.migration.zwave_js.header" - )} -
- -
- ${this.hass.localize( - "ui.panel.config.zwave.migration.zwave_js.introduction" - )} -
- ${html` - ${this._step === 0 - ? html` - -
-

- This wizard will walk through the following steps to - migrate from the legacy Z-Wave integration to Z-Wave JS. -

-
    -
  1. Stop the Z-Wave network
  2. - ${!isComponentLoaded(this.hass, "hassio") - ? html`
  3. Configure and start Z-Wave JS
  4. ` - : ""} -
  5. Set up the Z-Wave JS integration
  6. -
  7. - Migrate entities and devices to the new integration -
  8. -
  9. Remove legacy Z-Wave integration
  10. -
-

- - ${isComponentLoaded(this.hass, "hassio") - ? html`Please - make a backup - before proceeding.` - : "Please make a backup of your installation before proceeding."} - -

-
-
- - Continue - -
-
- ` - : this._step === 1 - ? html` - -
-

- We need to stop the Z-Wave network to perform the - migration. Home Assistant will not be able to control - Z-Wave devices while the network is stopped. -

- ${Object.values(this.hass.states) - .filter( - (entityState) => - computeStateDomain(entityState) === "zwave" && - !["ready", "sleeping"].includes(entityState.state) - ) - .map( - (entityState) => - html` - Device ${computeStateName(entityState)} - (${entityState.entity_id}) is not ready yet! For - the best result, wake the device up if it is - battery powered and wait for this device to become - ready. - ` - )} - ${this._stoppingNetwork - ? html` -
- -

Stopping Z-Wave Network...

-
- ` - : ``} -
-
- - Stop Network - -
-
- ` - : this._step === 2 - ? html` - -
-

Now it's time to set up the Z-Wave JS integration.

- ${isComponentLoaded(this.hass, "hassio") - ? html` -

- Z-Wave JS runs as a Home Assistant add-on that - will be setup next. Make sure to check the - checkbox to use the add-on. -

- ` - : html` -

- You are not running Home Assistant OS (the default - installation type) or Home Assistant Supervised, - so we can not setup Z-Wave JS automaticaly. Follow - the - advanced installation instructions - to install Z-Wave JS. -

-

- Here's the current Z-Wave configuration. You'll - need these values when setting up Z-Wave JS. -

- ${this._migrationConfig - ? html`
- USB Path: ${this._migrationConfig.usb_path}
- Network Key: - ${this._migrationConfig.network_key} -
` - : ``} -

- Once Z-Wave JS is installed and running, click - 'Continue' to set up the Z-Wave JS integration and - migrate your devices and entities. -

- `} -
-
- - Continue - -
-
- ` - : this._step === 3 - ? html` - -
-

- Now it's time to migrate your devices and entities from - the legacy Z-Wave integration to the Z-Wave JS - integration, to make sure all your UI's and automations - keep working. -

- ${this._waitingOnDevices?.map( - (device) => - html` - Device ${computeDeviceName(device, this.hass)} is - not ready yet! For the best result, wake the device - up if it is battery powered and wait for this device - to become ready. - ` - )} - ${this._migrationData - ? html` -

Below is a list of what will be migrated.

- ${this._migratedZwaveEntities!.length !== - this._migrationData.zwave_entity_ids.length - ? html` - The following entities will not be migrated - and might need manual adjustments to your - config: - -
    - ${this._migrationData.zwave_entity_ids.map( - (entity_id) => - !this._migratedZwaveEntities!.includes( - entity_id - ) - ? html`
  • - ${entity_id in this.hass.states - ? computeStateName( - this.hass.states[entity_id] - ) - : ""} - (${entity_id}) -
  • ` - : "" - )} -
` - : ""} - ${Object.keys( - this._migrationData.migration_device_map - ).length - ? html`

Devices that will be migrated:

-
    - ${Object.keys( - this._migrationData.migration_device_map - ).map( - (device_id) => - html`
  • - ${this._deviceNameLookup[device_id] || - device_id} -
  • ` - )} -
` - : ""} - ${Object.keys( - this._migrationData.migration_entity_map - ).length - ? html`

Entities that will be migrated:

-
    - ${Object.keys( - this._migrationData.migration_entity_map - ).map( - (entity_id) => html`
  • - ${entity_id in this.hass.states - ? computeStateName( - this.hass.states[entity_id] - ) - : ""} - (${entity_id}) -
  • ` - )} -
` - : ""} - ` - : html`
-

Loading migration data...

- - -
`} -
-
- - Migrate - -
-
- ` - : this._step === 4 - ? html` -
- That was all! You are now migrated to the new Z-Wave JS - integration, check if all your devices and entities are back - the way they where, if not all entities could be migrated - you might have to change those manually. -

- If you have 'zwave' in your configurtion.yaml file, you - should remove it now. -

-
- -
` - : ""} - `} -
-
- `; - } - - private async _getMigrationConfig(): Promise { - this._migrationConfig = await fetchMigrationConfig(this.hass!); - } - - private async _unsubscribe(): Promise { - if (this._unsub) { - (await this._unsub)(); - this._unsub = undefined; - } - } - - private _continue(): void { - this._step++; - } - - private async _stopNetwork(): Promise { - this._stoppingNetwork = true; - await this._getNetworkStatus(); - if (this._networkStatus?.state === ZWAVE_NETWORK_STATE_STOPPED) { - this._networkStopped(); - return; - } - - this._unsub = this.hass!.connection.subscribeEvents( - () => this._networkStopped(), - "zwave.network_stop" - ); - this.hass!.callService("zwave", "stop_network"); - } - - private async _setupZwaveJs() { - const zwaveJsConfigFlow = await startZwaveJsConfigFlow(this.hass); - showConfigFlowDialog(this, { - continueFlowId: zwaveJsConfigFlow.flow_id, - dialogClosedCallback: (params) => { - if (params.entryId) { - this._zwaveJsEntryId = params.entryId; - this._getZwaveJSNodesStatus(); - this._step = 3; - } - }, - showAdvanced: this.hass.userData?.showAdvanced, - }); - this.hass.loadBackendTranslation("title", "zwave_js", true); - } - - private async _getZwaveJSNodesStatus() { - if (this._nodeReadySubscriptions?.length) { - const unsubs = await Promise.all(this._nodeReadySubscriptions); - unsubs.forEach((unsub) => { - unsub(); - }); - } - this._nodeReadySubscriptions = []; - const networkStatus = await fetchZwaveJsNetworkStatus( - this.hass, - this._zwaveJsEntryId! - ); - const nodeStatePromisses = networkStatus.controller.nodes.map((nodeId) => - fetchZwaveNodeStatus(this.hass, this._zwaveJsEntryId!, nodeId) - ); - const nodesNotReady = (await Promise.all(nodeStatePromisses)).filter( - (node) => !node.ready - ); - - // eslint-disable-next-line no-console - console.log("waiting for nodes to be ready", nodesNotReady); - - this._getMigrationData(); - if (nodesNotReady.length === 0) { - this._waitingOnDevices = []; - return; - } - this._nodeReadySubscriptions = nodesNotReady.map((node) => - subscribeZwaveNodeReady( - this.hass, - this._zwaveJsEntryId!, - node.node_id, - () => { - this._getZwaveJSNodesStatus(); - } - ) - ); - const deviceReg: DeviceRegistryEntry[] = await fetchDeviceRegistry( - this.hass.connection - ); - this._waitingOnDevices = deviceReg.filter((device) => { - const identifiers = getZwaveJsIdentifiersFromDevice(device); - if ( - !identifiers || - Number(identifiers.home_id) !== networkStatus.controller.home_id - ) { - return false; - } - return nodesNotReady.some((node) => identifiers.node_id === node.node_id); - }); - } - - private async _getMigrationData() { - try { - this._migrationData = await migrateZwave( - this.hass, - this._zwaveJsEntryId!, - true - ); - } catch (err: any) { - showAlertDialog(this, { - title: "Failed to get migration data!", - text: - err.code === "unknown_command" - ? "Restart Home Assistant and try again." - : err.message, - }); - return; - } - this._migratedZwaveEntities = Object.keys( - this._migrationData.migration_entity_map - ); - if (Object.keys(this._migrationData.migration_device_map).length) { - this._fetchDevices(); - } - } - - private _fetchDevices() { - this._unsubDevices = subscribeDeviceRegistry( - this.hass.connection, - (devices) => { - if (!this._migrationData) { - return; - } - const migrationDevices = Object.keys( - this._migrationData.migration_device_map - ); - const deviceNameLookup = {}; - devices.forEach((device) => { - if (migrationDevices.includes(device.id)) { - deviceNameLookup[device.id] = computeDeviceName(device, this.hass); - } - }); - this._deviceNameLookup = deviceNameLookup; - } - ); - } - - private async _doMigrate() { - const data = await migrateZwave(this.hass, this._zwaveJsEntryId!, false); - if (!data.migrated) { - showAlertDialog(this, { title: "Migration failed!" }); - return; - } - this._step = 4; - } - - private _networkStopped(): void { - this._unsubscribe(); - this._getMigrationConfig(); - this._stoppingNetwork = false; - this._step = 2; - } - - private async _getNetworkStatus(): Promise { - this._networkStatus = await fetchNetworkStatus(this.hass!); - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - .content { - margin-top: 24px; - } - - .flex-container { - display: flex; - align-items: center; - } - - .flex-container ha-circular-progress { - margin-right: 20px; - } - - blockquote { - display: block; - background-color: var(--secondary-background-color); - color: var(--primary-text-color); - padding: 8px; - margin: 8px 0; - font-size: 0.9em; - font-family: monospace; - } - - ha-card { - margin: 0 auto; - max-width: 600px; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "zwave-migration": ZwaveMigration; - } -} diff --git a/src/panels/config/integrations/integration-panels/zwave/zwave-network.ts b/src/panels/config/integrations/integration-panels/zwave/zwave-network.ts deleted file mode 100644 index f49090c2ad..0000000000 --- a/src/panels/config/integrations/integration-panels/zwave/zwave-network.ts +++ /dev/null @@ -1,302 +0,0 @@ -import { mdiCheckboxMarkedCircle, mdiClose, mdiHelpCircle } from "@mdi/js"; -import { UnsubscribeFunc } from "home-assistant-js-websocket"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import "../../../../../components/buttons/ha-call-api-button"; -import "../../../../../components/buttons/ha-call-service-button"; -import "../../../../../components/ha-card"; -import "../../../../../components/ha-circular-progress"; -import "../../../../../components/ha-svg-icon"; -import "../../../../../components/ha-icon-button"; -import "../../../../../components/ha-service-description"; -import { - fetchNetworkStatus, - ZWaveNetworkStatus, - ZWAVE_NETWORK_STATE_AWAKED, - ZWAVE_NETWORK_STATE_READY, - ZWAVE_NETWORK_STATE_STARTED, - ZWAVE_NETWORK_STATE_STOPPED, -} from "../../../../../data/zwave"; -import { haStyle } from "../../../../../resources/styles"; -import { HomeAssistant } from "../../../../../types"; -import { documentationUrl } from "../../../../../util/documentation-url"; -import "../../../ha-config-section"; - -@customElement("zwave-network") -export class ZwaveNetwork extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property() public isWide!: boolean; - - @state() private _showHelp = false; - - @state() private _networkStatus?: ZWaveNetworkStatus; - - @state() private _unsubs: Array> = []; - - public disconnectedCallback(): void { - this._unsubscribe(); - } - - protected firstUpdated(changedProps): void { - super.firstUpdated(changedProps); - this._getNetworkStatus(); - this._subscribe(); - } - - protected render(): TemplateResult { - return html` - -
- - ${this.hass!.localize( - "ui.panel.config.zwave.network_management.header" - )} - - -
-
- ${this.hass!.localize( - "ui.panel.config.zwave.network_management.introduction" - )} -

- - ${this.hass!.localize("ui.panel.config.zwave.learn_more")} - -

-
- - ${this._networkStatus - ? html` - -
- ${this._networkStatus.state === ZWAVE_NETWORK_STATE_STOPPED - ? html` - - ${this.hass!.localize( - "ui.panel.config.zwave.network_status.network_stopped" - )} - ` - : this._networkStatus.state === ZWAVE_NETWORK_STATE_STARTED - ? html` - - ${this.hass!.localize( - "ui.panel.config.zwave.network_status.network_starting" - )}
- - ${this.hass!.localize( - "ui.panel.config.zwave.network_status.network_starting_note" - )} - - ` - : this._networkStatus.state === ZWAVE_NETWORK_STATE_AWAKED - ? html` - - ${this.hass!.localize( - "ui.panel.config.zwave.network_status.network_started" - )}
- - ${this.hass!.localize( - "ui.panel.config.zwave.network_status.network_started_note_some_queried" - )} - - ` - : this._networkStatus.state === ZWAVE_NETWORK_STATE_READY - ? html` - ${this.hass!.localize( - "ui.panel.config.zwave.network_status.network_started" - )}
- - ${this.hass!.localize( - "ui.panel.config.zwave.network_status.network_started_note_all_queried" - )} - - ` - : ""} -
-
- ${this._networkStatus.state >= ZWAVE_NETWORK_STATE_AWAKED - ? html` - ${this._generateServiceButton("stop_network")} - ${this._generateServiceButton("heal_network")} - ${this._generateServiceButton("test_network")} - ` - : html` ${this._generateServiceButton("start_network")} `} -
- ${this._networkStatus.state >= ZWAVE_NETWORK_STATE_AWAKED - ? html` -
- ${this._generateServiceButton("soft_reset")} - - ${this.hass!.localize( - "ui.panel.config.zwave.services.save_config" - )} - -
- ` - : ""} -
- ${this._networkStatus.state >= ZWAVE_NETWORK_STATE_AWAKED - ? html` - -
- ${this._generateServiceButton("add_node_secure")} - ${this._generateServiceButton("add_node")} - ${this._generateServiceButton("remove_node")} -
-
- ${this._generateServiceButton("cancel_command")} -
-
- ` - : ""} - ` - : ""} -
- `; - } - - private async _getNetworkStatus(): Promise { - this._networkStatus = await fetchNetworkStatus(this.hass!); - } - - private _subscribe(): void { - this._unsubs = [ - "zwave.network_start", - "zwave.network_stop", - "zwave.network_ready", - "zwave.network_complete", - "zwave.network_complete_some_dead", - ].map((e) => - this.hass!.connection.subscribeEvents( - (event) => this._handleEvent(event), - e - ) - ); - } - - private _unsubscribe(): void { - while (this._unsubs.length) { - this._unsubs.pop()!.then((unsub) => unsub()); - } - } - - private _handleEvent(event) { - if (event.event_type === "zwave.network_start") { - // Optimistically set the state, wait 1s and poll the backend - // The backend will still report a state of 0 when the 'network_start' - // event is first fired. - if (this._networkStatus) { - this._networkStatus = { ...this._networkStatus, state: 5 }; - } - setTimeout(() => this._getNetworkStatus, 1000); - } else { - this._getNetworkStatus(); - } - } - - private _onHelpTap(): void { - this._showHelp = !this._showHelp; - } - - private _generateServiceButton(service: string) { - return html` - - ${this.hass!.localize("ui.panel.config.zwave.services." + service)} - - - - `; - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - .content { - margin-top: 24px; - } - - .sectionHeader { - position: relative; - padding-right: 40px; - } - - .network-status { - text-align: center; - } - - .network-status div.details { - font-size: 1.5rem; - padding: 24px; - } - - .network-status ha-svg-icon { - display: block; - margin: 0px auto 16px; - width: 48px; - height: 48px; - } - - .network-status small { - font-size: 1rem; - } - - ha-card { - margin: 0 auto; - max-width: 600px; - } - - .card-actions.warning ha-call-service-button { - color: var(--error-color); - } - - .toggle-help-icon { - position: absolute; - top: -6px; - right: 0; - color: var(--primary-color); - } - - ha-service-description { - display: block; - color: grey; - padding: 0 8px 12px; - } - - [hidden] { - display: none; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "zwave-network": ZwaveNetwork; - } -} diff --git a/src/panels/config/integrations/integration-panels/zwave/zwave-node-config.ts b/src/panels/config/integrations/integration-panels/zwave/zwave-node-config.ts deleted file mode 100644 index 975862c679..0000000000 --- a/src/panels/config/integrations/integration-panels/zwave/zwave-node-config.ts +++ /dev/null @@ -1,388 +0,0 @@ -import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property, state } from "lit/decorators"; -import "../../../../../components/buttons/ha-call-service-button"; -import "../../../../../components/ha-card"; -import { - fetchNodeConfig, - ZWaveConfigItem, - ZWaveConfigServiceData, - ZWaveNode, -} from "../../../../../data/zwave"; -import { haStyle } from "../../../../../resources/styles"; -import { HomeAssistant } from "../../../../../types"; - -@customElement("zwave-node-config") -export class ZwaveNodeConfig extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property() public nodes: ZWaveNode[] = []; - - @property() public config: ZWaveConfigItem[] = []; - - @property() public selectedNode = -1; - - @state() private _configItem?: ZWaveConfigItem; - - @state() private _wakeupInput = -1; - - @state() private _selectedConfigParameter = -1; - - @state() private _selectedConfigValue: number | string = -1; - - protected render(): TemplateResult { - return html` -
- - ${"wake_up_interval" in this.nodes[this.selectedNode].attributes - ? html` -
- -
- ${this.hass!.localize( - "ui.panel.config.zwave.node_config.seconds" - )} -
-
- - ${this.hass!.localize( - "ui.panel.config.zwave.node_config.set_wakeup" - )} - -
- ` - : ""} -
- - - ${this.config.map( - (entityState) => html` - - ${entityState.key}: ${entityState.value.label} - - ` - )} - - -
- ${this._configItem - ? html` - ${this._configItem.value.type === "List" - ? html` -
- - - ${this._configItem.value.data_items.map( - (entityState) => html` - ${entityState} - ` - )} - - -
- ` - : ""} - ${["Byte", "Short", "Int"].includes(this._configItem.value.type) - ? html` -
- - -
- ` - : ""} - ${["Bool", "Button"].includes(this._configItem.value.type) - ? html` -
- - - - ${this.hass!.localize( - "ui.panel.config.zwave.node_config.true" - )} - - - ${this.hass!.localize( - "ui.panel.config.zwave.node_config.false" - )} - - - -
- ` - : ""} -
- ${this._configItem.value.help} -
- ${["Bool", "Button", "Byte", "Short", "Int", "List"].includes( - this._configItem.value.type - ) - ? html` -
- - ${this.hass!.localize( - "ui.panel.config.zwave.node_config.set_config_parameter" - )} - -
- ` - : ""} - ` - : ""} -
-
- `; - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - .content { - margin-top: 24px; - } - - ha-card { - margin: 0 auto; - max-width: 600px; - } - - .device-picker { - @apply --layout-horizontal; - @apply --layout-center-center; - display: -ms-flexbox; - display: -webkit-flex; - display: flex; - -ms-flex-direction: row; - -webkit-flex-direction: row; - flex-direction: row; - -ms-flex-align: center; - -webkit-align-items: center; - align-items: center; - padding-left: 24px; - padding-right: 24px; - padding-bottom: 24px; - } - - .help-text { - padding-left: 24px; - padding-right: 24px; - } - - .flex { - -ms-flex: 1 1 0.000000001px; - -webkit-flex: 1; - flex: 1; - -webkit-flex-basis: 0.000000001px; - flex-basis: 0.000000001px; - } - `, - ]; - } - - protected firstUpdated(changedProps: PropertyValues): void { - super.firstUpdated(changedProps); - this.addEventListener("hass-service-called", (ev) => - this.serviceCalled(ev) - ); - } - - protected updated(changedProps: PropertyValues): void { - super.updated(changedProps); - if (changedProps.has("selectedNode")) { - this._nodesChanged(); - } - } - - private serviceCalled(ev): void { - if (ev.detail.success) { - setTimeout(() => { - this._refreshConfig(this.selectedNode); - }, 5000); - } - } - - private _nodesChanged(): void { - if (!this.nodes) { - return; - } - this._configItem = undefined; - this._wakeupInput = - this.nodes[this.selectedNode].attributes.wake_up_interval || -1; - } - - private _onWakeupIntervalChanged(value: ChangeEvent): void { - this._wakeupInput = value.detail!.value; - } - - private _computeWakeupServiceData(wakeupInput: number) { - return { - node_id: this.nodes[this.selectedNode].attributes.node_id, - value: wakeupInput, - }; - } - - private _computeSetConfigParameterServiceData(): - | ZWaveConfigServiceData - | boolean { - if (this.selectedNode === -1 || typeof this._configItem === "undefined") { - return false; - } - let valueData: number | string = ""; - if (["Short", "Byte", "Int"].includes(this._configItem!.value.type)) { - valueData = - typeof this._selectedConfigValue === "string" - ? parseInt(this._selectedConfigValue, 10) - : this._selectedConfigValue; - } - if (["Bool", "Button", "List"].includes(this._configItem!.value.type)) { - valueData = this._selectedConfigValue; - } - return { - node_id: this.nodes[this.selectedNode].attributes.node_id, - parameter: this._configItem.key, - value: valueData, - }; - } - - private _selectedConfigParameterChanged(event: ItemSelectedEvent): void { - if (event.target!.selected === -1) { - return; - } - this._selectedConfigParameter = event.target!.selected; - this._configItem = this.config[event.target!.selected]; - } - - private _configValueSelectChanged(event: ItemSelectedEvent): void { - if (event.target!.selected === -1) { - return; - } - this._selectedConfigValue = event.target!.selectedItem.textContent; - } - - private _configValueInputChanged(value: ChangeEvent): void { - this._selectedConfigValue = value.detail!.value; - } - - private async _refreshConfig(selectedNode): Promise { - const configData: ZWaveConfigItem[] = []; - const config = await fetchNodeConfig( - this.hass, - this.nodes[selectedNode].attributes.node_id - ); - - Object.keys(config).forEach((key) => { - configData.push({ - key: parseInt(key, 10), - value: config[key], - }); - }); - - this.config = configData; - this._configItem = this.config[this._selectedConfigParameter]; - } -} - -export interface ChangeEvent { - detail?: { - value?: any; - }; - target?: EventTarget; -} - -export interface PickerTarget extends EventTarget { - selected: number; - selectedItem?: any; -} - -export interface ItemSelectedEvent { - target?: PickerTarget; -} - -declare global { - interface HTMLElementTagNameMap { - "zwave-node-config": ZwaveNodeConfig; - } -} diff --git a/src/panels/config/integrations/integration-panels/zwave/zwave-node-protection.js b/src/panels/config/integrations/integration-panels/zwave/zwave-node-protection.js deleted file mode 100644 index 693ed425fb..0000000000 --- a/src/panels/config/integrations/integration-panels/zwave/zwave-node-protection.js +++ /dev/null @@ -1,179 +0,0 @@ -import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "../../../../../components/buttons/ha-call-api-button"; -import "../../../../../components/ha-card"; -import LocalizeMixin from "../../../../../mixins/localize-mixin"; -import "../../../../../styles/polymer-ha-style"; - -class ZwaveNodeProtection extends LocalizeMixin(PolymerElement) { - static get template() { - return html` - -
- -
- - - - - -
-
- - [[localize('ui.panel.config.zwave.node_management.set_protection')]] - -
-
-
-`; - } - - static get properties() { - return { - hass: Object, - - nodes: Array, - - selectedNode: { - type: Number, - value: -1, - }, - - protectionNode: { - type: Boolean, - value: false, - }, - - _protectionValueID: { - type: Number, - value: -1, - }, - - _selectedProtectionParameter: { - type: Number, - value: -1, - observer: "_computeProtectionData", - }, - - _protectionOptions: Array, - - _protection: { - type: Array, - value: () => [], - }, - - _loadedProtectionValue: { - type: String, - value: "", - }, - - _protectionData: { - type: Object, - value: {}, - }, - - _nodePath: String, - }; - } - - static get observers() { - return ["_nodesChanged(nodes, selectedNode)"]; - } - - ready() { - super.ready(); - this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev)); - } - - apiCalled(ev) { - if (ev.detail.success) { - setTimeout(() => { - this._refreshProtection(this.selectedNode); - }, 5000); - } - } - - _nodesChanged() { - if (!this.nodes) return; - if (this.protection) { - if (this.protection.length === 0) { - return; - } - let options = []; - let value_id = -1; - let selected = -1; - this.protection.forEach((item) => { - if (item.key === "options") options = item.value; - else if (item.key === "value_id") value_id = item.value; - else if (item.key === "selected") selected = item.value; - }); - this.setProperties({ - protectionNode: true, - _protectionOptions: options, - _loadedProtectionValue: selected, - _protectionValueID: value_id, - }); - } - } - - async _refreshProtection(selectedNode) { - const protectionValues = []; - const protections = await this.hass.callApi( - "GET", - `zwave/protection/${this.nodes[selectedNode].attributes.node_id}` - ); - Object.keys(protections).forEach((key) => { - protectionValues.push({ - key, - value: protections[key], - }); - }); - this.setProperties({ - _protection: protectionValues, - _selectedProtectionParameter: -1, - _loadedProtectionValue: this.protection[1].value, - }); - } - - _computeProtectionData(selectedProtectionParameter) { - if (this.selectedNode === -1 || selectedProtectionParameter === -1) return; - this._protectionData = { - selection: this._protectionOptions[selectedProtectionParameter], - value_id: this._protectionValueID, - }; - this._nodePath = `zwave/protection/${ - this.nodes[this.selectedNode].attributes.node_id - }`; - } -} - -customElements.define("zwave-node-protection", ZwaveNodeProtection); diff --git a/src/panels/config/integrations/integration-panels/zwave/zwave-usercodes.js b/src/panels/config/integrations/integration-panels/zwave/zwave-usercodes.js deleted file mode 100644 index 7180f8d880..0000000000 --- a/src/panels/config/integrations/integration-panels/zwave/zwave-usercodes.js +++ /dev/null @@ -1,226 +0,0 @@ -import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "../../../../../components/buttons/ha-call-service-button"; -import "../../../../../components/ha-card"; -import "../../../../../styles/polymer-ha-style"; - -class ZwaveUsercodes extends PolymerElement { - static get template() { - return html` - -
- -
- - - - - -
- - -
-
- `; - } - - static get properties() { - return { - hass: Object, - - nodes: Array, - - selectedNode: { - type: Number, - observer: "_selectedNodeChanged", - }, - - userCodes: Object, - - _selectedUserCode: { - type: Number, - value: -1, - observer: "_selectedUserCodeChanged", - }, - - _selectedUserCodeValue: String, - - _computedCodeOutput: { - type: String, - value: "", - }, - }; - } - - ready() { - super.ready(); - this.addEventListener("hass-service-called", (ev) => - this.serviceCalled(ev) - ); - } - - serviceCalled(ev) { - if (ev.detail.success) { - setTimeout(() => { - this._refreshUserCodes(this.selectedNode); - }, 5000); - } - } - - _isUserCodeSelected(selectedUserCode) { - if (selectedUserCode === -1) return false; - return true; - } - - _computeSelectCaptionUserCodes(stateObj) { - return `${stateObj.key}: ${stateObj.value.label}`; - } - - _selectedUserCodeChanged(selectedUserCode) { - if (this._selectedUserCode === -1 || selectedUserCode === -1) return; - const value = this.userCodes[selectedUserCode].value.code; - this.setProperties({ - _selectedUserCodeValue: this._a2hex(value), - _computedCodeOutput: `[${this._hex2a(this._a2hex(value))}]`, - }); - } - - _computeUserCodeServiceData(selectedUserCodeValue, type) { - if (this.selectedNode === -1 || !selectedUserCodeValue) return -1; - let serviceData = null; - let valueData = null; - if (type === "Add") { - valueData = this._hex2a(selectedUserCodeValue); - this._computedCodeOutput = `[${valueData}]`; - serviceData = { - node_id: this.nodes[this.selectedNode].attributes.node_id, - code_slot: this._selectedUserCode, - usercode: valueData, - }; - } - if (type === "Delete") { - serviceData = { - node_id: this.nodes[this.selectedNode].attributes.node_id, - code_slot: this._selectedUserCode, - }; - } - return serviceData; - } - - async _refreshUserCodes(selectedNode) { - this.setProperties({ _selectedUserCodeValue: "" }); - const userCodes = []; - const userCodeData = await this.hass.callApi( - "GET", - `zwave/usercodes/${this.nodes[selectedNode].attributes.node_id}` - ); - Object.keys(userCodeData).forEach((key) => { - userCodes.push({ - key, - value: userCodeData[key], - }); - }); - this.setProperties({ userCodes: userCodes }); - this._selectedUserCodeChanged(this._selectedUserCode); - } - - _a2hex(str) { - const arr = []; - let output = ""; - for (let i = 0, l = str.length; i < l; i++) { - const hex = Number(str.charCodeAt(i)).toString(16); - if (hex === "0") { - output = "00"; - } else { - output = hex; - } - arr.push("\\x" + output); - } - return arr.join(""); - } - - _hex2a(hexx) { - const hex = hexx.toString(); - const hexMod = hex.replace(/\\x/g, ""); - let str = ""; - for (let i = 0; i < hexMod.length; i += 2) { - str += String.fromCharCode(parseInt(hexMod.substr(i, 2), 16)); - } - return str; - } - - _selectedNodeChanged() { - if (this.selectedNode === -1) return; - this.setProperties({ _selecteduserCode: -1 }); - } -} - -customElements.define("zwave-usercodes", ZwaveUsercodes); diff --git a/src/panels/config/integrations/integration-panels/zwave/zwave-values.ts b/src/panels/config/integrations/integration-panels/zwave/zwave-values.ts deleted file mode 100644 index 1e6750c6af..0000000000 --- a/src/panels/config/integrations/integration-panels/zwave/zwave-values.ts +++ /dev/null @@ -1,109 +0,0 @@ -import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import "../../../../../components/buttons/ha-call-service-button"; -import "../../../../../components/ha-card"; -import { ZWaveValue } from "../../../../../data/zwave"; -import { haStyle } from "../../../../../resources/styles"; -import { HomeAssistant } from "../../../../../types"; - -@customElement("zwave-values") -export class ZwaveValues extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property() public values: ZWaveValue[] = []; - - @state() private _selectedValue = -1; - - protected render(): TemplateResult { - return html` -
- -
- - - ${this.values.map( - (item) => html` - ${this._computeCaption(item)} - ` - )} - - -
-
-
- `; - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - .content { - margin-top: 24px; - } - - ha-card { - margin: 0 auto; - max-width: 600px; - } - - .device-picker { - @apply --layout-horizontal; - @apply --layout-center-center; - display: -ms-flexbox; - display: -webkit-flex; - display: flex; - -ms-flex-direction: row; - -webkit-flex-direction: row; - flex-direction: row; - -ms-flex-align: center; - -webkit-align-items: center; - align-items: center; - padding-left: 24px; - padding-right: 24px; - padding-bottom: 24px; - } - - .flex { - -ms-flex: 1 1 0.000000001px; - -webkit-flex: 1; - flex: 1; - -webkit-flex-basis: 0.000000001px; - flex-basis: 0.000000001px; - } - - .help-text { - padding-left: 24px; - padding-right: 24px; - } - `, - ]; - } - - private _computeCaption(item) { - let out = `${item.value.label}`; - out += ` (${this.hass.localize("ui.panel.config.zwave.common.instance")}:`; - out += ` ${item.value.instance},`; - out += ` ${this.hass.localize("ui.panel.config.zwave.common.index")}:`; - out += ` ${item.value.index})`; - return out; - } -} - -declare global { - interface HTMLElementTagNameMap { - "zwave-values": ZwaveValues; - } -} diff --git a/src/translations/en.json b/src/translations/en.json index d1ec5a56b3..f657f3e6dd 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2692,120 +2692,6 @@ "stop_listening": "Stop listening", "message_received": "Message {id} received on {topic} at {time}:" }, - "ozw": { - "common": { - "zwave": "Z-Wave", - "node_id": "Node ID", - "ozw_instance": "OpenZWave Instance", - "instance": "Instance", - "controller": "Controller", - "network": "Network", - "wakeup_instructions": "Wake-up Instructions", - "query_stage": "Query Stage" - }, - "device_info": { - "zwave_info": "Z-Wave Info", - "stage": "Stage", - "node_failed": "Node Failed" - }, - "node_query_stages": { - "protocolinfo": "Obtaining basic Z-Wave capabilities of this node from the controller", - "probe": "Checking if the node is awake/alive", - "wakeup": "Setting up support for wake-up queues and messages", - "manufacturerspecific1": "Obtaining manufacturer and product ID codes from the node", - "nodeinfo": "Obtaining supported command classes from the node", - "nodeplusinfo": "Obtaining Z-Wave+ information from the node", - "manufacturerspecific2": "Obtaining additional manufacturer and product ID codes from the node", - "versions": "Obtaining information about firmware and command class versions", - "instances": "Obtaining details about what instances or channels a device supports", - "static": "Obtaining static values from the device", - "cacheload": "Loading information from the OpenZWave cache file. Battery nodes will stay at this stage until the node wakes up.", - "associations": "Refreshing association groups and memberships", - "neighbors": "Obtaining a list of the node's neighbors", - "session": "Obtaining infrequently changing values from the node", - "dynamic": "Obtaining frequently changing values from the node", - "configuration": "Obtaining configuration values from the node", - "complete": "Interview process is complete" - }, - "refresh_node": { - "button": "Refresh Node", - "title": "Refresh Node Information", - "complete": "Node Refresh Complete", - "description": "This will tell OpenZWave to re-interview a node and update the node's command classes, capabilities, and values.", - "battery_note": "If the node is battery powered, be sure to wake it before proceeding", - "wakeup_header": "Wake-up Instructions for", - "wakeup_instructions_source": "Wake-up instructions are sourced from the OpenZWave community device database.", - "start_refresh_button": "Start Refresh", - "refreshing_description": "Refreshing node information…", - "node_status": "Node Status", - "step": "Step" - }, - "network_status": { - "online": "Online", - "offline": "Offline", - "starting": "Starting", - "unknown": "Unknown", - "details": { - "driverallnodesqueried": "All nodes have been queried", - "driverallnodesqueriedsomedead": "All nodes have been queried. Some nodes were found dead", - "driverawakenodesqueries": "All awake nodes have been queried", - "driverremoved": "The driver has been removed", - "driverreset": "The driver has been reset", - "driverfailed": "Failed to connect to Z-Wave controller", - "driverready": "Initializing the Z-Wave controller", - "ready": "Ready to connect", - "stopped": "OpenZWave stopped", - "started": "Connected to MQTT", - "starting": "Connecting to MQTT", - "offline": "OZWDaemon offline" - } - }, - "navigation": { - "select_instance": "Select Instance", - "network": "Network", - "nodes": "Nodes", - "node": { - "dashboard": "Dashboard", - "config": "Config" - } - }, - "select_instance": { - "header": "Select an OpenZWave Instance", - "introduction": "You have more than one OpenZWave instance running. Which instance would you like to manage?", - "none_found": "We couldn't find an OpenZWave instance. If you believe this is incorrect, check your OpenZWave and MQTT setups and ensure that Home Assistant can communicate with your MQTT broker." - }, - "network": { - "header": "Network Management", - "introduction": "Manage network-wide functions.", - "node_count": "{count} nodes" - }, - "nodes_table": { - "id": "ID", - "manufacturer": "Manufacturer", - "model": "Model", - "query_stage": "Query Stage", - "zwave_plus": "Z-Wave Plus", - "failed": "Failed" - }, - "node": { - "button": "Node Details", - "not_found": "Node not found" - }, - "node_config": { - "header": "Node Configuration", - "introduction": "Manage the different configuration parameters for a Z-Wave node.", - "help_source": "Config parameter descriptions and help text are provided by the OpenZWave project.", - "wakeup_help": "Battery powered nodes must be awake to change their configuration. If the node is not awake, OpenZWave will attempt to update the node's configuration the next time it wakes up, which could be multiple hours (or days) later. Follow these steps to wake up your device:" - }, - "node_metadata": { - "product_manual": "Product Manual" - }, - "services": { - "add_node": "Add Node", - "remove_node": "Remove Node", - "cancel_command": "Cancel Command" - } - }, "zha": { "common": { "clusters": "Clusters", @@ -2899,96 +2785,6 @@ "unbind_button_help": "Unbind the selected group from the selected device clusters." } }, - "zwave": { - "description": "Manage your Z-Wave network", - "learn_more": "Learn more about Z-Wave", - "common": { - "value": "Value", - "instance": "Instance", - "index": "Index", - "unknown": "unknown", - "wakeup_interval": "Wake-up Interval" - }, - "migration": { - "zwave_js": { - "header": "Migrate to Z-Wave JS", - "introduction": "This integration is no longer maintained, and we advise you to move to the new Z-Wave JS integration. This wizard will help you migrate from the legacy Z-Wave integration to the new Z-Wave JS integration." - } - }, - "network_management": { - "header": "Z-Wave Network Management", - "introduction": "Run commands that affect the Z-Wave network. You won't get feedback on whether most commands succeeded, but you can check the OZW Log to try to find out." - }, - "node_management": { - "header": "Z-Wave Node Management", - "introduction": "Run Z-Wave commands that affect a single node. Pick a node to see a list of available commands.", - "nodes": "Nodes", - "nodes_hint": "Select node to view per-node options", - "entities": "Entities of this node", - "entity_info": "Entity Information", - "exclude_entity": "Exclude this entity from Home Assistant", - "pooling_intensity": "Polling intensity", - "node_protection": "Node protection", - "protection": "Protection", - "set_protection": "Set Protection", - "node_group_associations": "Node group associations", - "group": "Group", - "node_to_control": "Node to control", - "nodes_in_group": "Other nodes in this group:", - "max_associations": "Max Associations:", - "add_to_group": "Add to Group", - "remove_from_group": "Remove from Group", - "remove_broadcast": "Remove Broadcast" - }, - "ozw_log": { - "header": "OZW Log", - "introduction": "View the log. 0 is the minimum (loads entire log) and 1000 is the maximum. Load will show a static log and tail will auto update with the last specified number of lines of the log.", - "last_log_lines": "Number of last log lines", - "load": "Load", - "tail": "Tail" - }, - "network_status": { - "network_stopped": "Z-Wave Network Stopped", - "network_starting": "Starting Z-Wave Network…", - "network_starting_note": "This may take a while depending on the size of your network.", - "network_started": "Z-Wave Network Started", - "network_started_note_some_queried": "Awake nodes have been queried. Sleeping nodes will be queried when they wake.", - "network_started_note_all_queried": "All nodes have been queried." - }, - "node_config": { - "header": "Node Configuration Options", - "seconds": "seconds", - "set_wakeup": "Set Wake-up Interval", - "config_parameter": "Configuration Parameter", - "config_value": "Configuration Value", - "true": "True", - "false": "False", - "set_config_parameter": "Set Configuration Parameter" - }, - "values": { - "header": "Node Values" - }, - "services": { - "start_network": "Start Network", - "stop_network": "Stop Network", - "heal_network": "Heal Network", - "test_network": "Test Network", - "soft_reset": "Soft Reset", - "save_config": "Save Configuration", - "add_node_secure": "Add Node Secure", - "add_node": "Add Node", - "remove_node": "Remove Node", - "cancel_command": "Cancel Command", - "refresh_node": "Refresh Node", - "remove_failed_node": "Remove Failed Node", - "replace_failed_node": "Replace Failed Node", - "print_node": "Print Node", - "heal_node": "Heal Node", - "test_node": "Test Node", - "node_info": "Node Information", - "refresh_entity": "Refresh Entity" - } - }, "zwave_js": { "navigation": { "network": "Network", diff --git a/yarn.lock b/yarn.lock index 10cde4b7b8..d20b3377e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3136,19 +3136,7 @@ __metadata: languageName: node linkType: hard -"@polymer/iron-dropdown@npm:^3.0.0-pre.26": - version: 3.0.1 - resolution: "@polymer/iron-dropdown@npm:3.0.1" - dependencies: - "@polymer/iron-behaviors": ^3.0.0-pre.26 - "@polymer/iron-overlay-behavior": ^3.0.0-pre.27 - "@polymer/neon-animation": ^3.0.0-pre.26 - "@polymer/polymer": ^3.0.0 - checksum: 2c1ba429c8f5553f8493f256691efa8a338e8c038c1102f482ecb612b61c079b5019f6c362aefb31b44d3429661152c1b6912408a69c67e9d6fff62914ad801f - languageName: node - linkType: hard - -"@polymer/iron-fit-behavior@npm:^3.0.0-pre.26, @polymer/iron-fit-behavior@npm:^3.1.0": +"@polymer/iron-fit-behavior@npm:^3.0.0-pre.26": version: 3.1.0 resolution: "@polymer/iron-fit-behavior@npm:3.1.0" dependencies: @@ -3295,17 +3283,6 @@ __metadata: languageName: node linkType: hard -"@polymer/neon-animation@npm:^3.0.0-pre.26": - version: 3.0.1 - resolution: "@polymer/neon-animation@npm:3.0.1" - dependencies: - "@polymer/iron-resizable-behavior": ^3.0.0-pre.26 - "@polymer/iron-selector": ^3.0.0-pre.26 - "@polymer/polymer": ^3.0.0 - checksum: c5ea5e1ef9f2017faaa5799ea108b26634dd7d986fe469369e629075efe382a5e5d4f9c537bacc77f9852453a2758c9f67e491d6ea5a1c4457f772bfdf06c707 - languageName: node - linkType: hard - "@polymer/paper-behaviors@npm:^3.0.0-pre.27": version: 3.0.1 resolution: "@polymer/paper-behaviors@npm:3.0.1" @@ -3318,25 +3295,6 @@ __metadata: languageName: node linkType: hard -"@polymer/paper-dropdown-menu@npm:^3.2.0": - version: 3.2.0 - resolution: "@polymer/paper-dropdown-menu@npm:3.2.0" - dependencies: - "@polymer/iron-a11y-keys-behavior": ^3.0.0-pre.26 - "@polymer/iron-form-element-behavior": ^3.0.0-pre.26 - "@polymer/iron-icon": ^3.0.0-pre.26 - "@polymer/iron-iconset-svg": ^3.0.0-pre.26 - "@polymer/iron-validatable-behavior": ^3.0.0-pre.26 - "@polymer/paper-behaviors": ^3.0.0-pre.27 - "@polymer/paper-input": ^3.1.0 - "@polymer/paper-menu-button": ^3.1.0 - "@polymer/paper-ripple": ^3.0.0-pre.26 - "@polymer/paper-styles": ^3.0.0-pre.26 - "@polymer/polymer": ^3.3.1 - checksum: dc7f6a8e3d449f37068ad5ee1d1c6d9037c9abd855ccc1d4e433d743c6378bb25af165ea86174edd1414887cf56a011ad0adda6d24dd55765e9458d49dc3e5e3 - languageName: node - linkType: hard - "@polymer/paper-icon-button@npm:^3.0.0-pre.26": version: 3.0.2 resolution: "@polymer/paper-icon-button@npm:3.0.2" @@ -3349,7 +3307,7 @@ __metadata: languageName: node linkType: hard -"@polymer/paper-input@npm:^3.0.0-pre.26, @polymer/paper-input@npm:^3.1.0, @polymer/paper-input@npm:^3.2.1": +"@polymer/paper-input@npm:^3.0.0-pre.26, @polymer/paper-input@npm:^3.2.1": version: 3.2.1 resolution: "@polymer/paper-input@npm:3.2.1" dependencies: @@ -3388,21 +3346,6 @@ __metadata: languageName: node linkType: hard -"@polymer/paper-menu-button@npm:^3.1.0": - version: 3.1.0 - resolution: "@polymer/paper-menu-button@npm:3.1.0" - dependencies: - "@polymer/iron-a11y-keys-behavior": ^3.0.0-pre.26 - "@polymer/iron-behaviors": ^3.0.0-pre.26 - "@polymer/iron-dropdown": ^3.0.0-pre.26 - "@polymer/iron-fit-behavior": ^3.1.0 - "@polymer/neon-animation": ^3.0.0-pre.26 - "@polymer/paper-styles": ^3.0.0-pre.26 - "@polymer/polymer": ^3.0.0 - checksum: 9243e104bac583189c6221f2df8dffeb331868cbf8084dd488cf2ddaba25987bfb3d4d2a9bd3168e6b49f28ba6b1b07ef7163fbfcf3af97978d34608e91cc605 - languageName: node - linkType: hard - "@polymer/paper-progress@npm:^3.0.0-pre.26": version: 3.0.1 resolution: "@polymer/paper-progress@npm:3.0.1" @@ -9113,7 +9056,6 @@ fsevents@^1.2.7: "@polymer/iron-icon": ^3.0.1 "@polymer/iron-input": ^3.0.1 "@polymer/iron-resizable-behavior": ^3.0.1 - "@polymer/paper-dropdown-menu": ^3.2.0 "@polymer/paper-input": ^3.2.1 "@polymer/paper-item": ^3.0.1 "@polymer/paper-listbox": ^3.0.1 @@ -9240,7 +9182,6 @@ fsevents@^1.2.7: vis-network: ^8.5.4 vue: ^2.6.12 vue2-daterange-picker: ^0.5.1 - web-animations-js: ^2.3.2 webpack: ^5.55.1 webpack-cli: ^4.8.0 webpack-dev-server: ^4.3.0 @@ -15703,13 +15644,6 @@ typescript@^4.4.3: languageName: node linkType: hard -"web-animations-js@npm:^2.3.2": - version: 2.3.2 - resolution: "web-animations-js@npm:2.3.2" - checksum: 194db111bb2f92c15100c33b63af320ccdc26066748e358a945b947c510216c78e0a1e2ae22fefbaacb585c8a0b41b62a1417d8b549636ee32e16f059bb488f2 - languageName: node - linkType: hard - "web-component-analyzer@npm:~1.1.1": version: 1.1.6 resolution: "web-component-analyzer@npm:1.1.6" From adefc7a4e23cda62c3e1607c8480c86b3b75fa60 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 4 Mar 2022 23:15:10 +0100 Subject: [PATCH 009/142] Convert lovelace config dialogs to ha-form (#11910) --- .../dialog-lovelace-dashboard-detail.ts | 290 ++++++++---------- .../dialog-lovelace-resource-detail.ts | 227 +++++++------- 2 files changed, 240 insertions(+), 277 deletions(-) diff --git a/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts b/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts index 39b449d3d7..75db9e031c 100644 --- a/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts +++ b/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts @@ -1,20 +1,18 @@ import "@material/mwc-button/mwc-button"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../../../common/dom/fire_event"; import { slugify } from "../../../../common/string/slugify"; -import { computeRTLDirection } from "../../../../common/util/compute_rtl"; import { createCloseHeading } from "../../../../components/ha-dialog"; -import "../../../../components/ha-formfield"; -import "../../../../components/ha-icon-picker"; -import "../../../../components/ha-switch"; -import type { HaSwitch } from "../../../../components/ha-switch"; +import "../../../../components/ha-form/ha-form"; +import { HaFormSchema } from "../../../../components/ha-form/types"; +import { CoreFrontendUserData } from "../../../../data/frontend"; import { LovelaceDashboard, LovelaceDashboardCreateParams, - LovelaceDashboardMutableParams, } from "../../../../data/lovelace"; import { DEFAULT_PANEL, setDefaultPanel } from "../../../../data/panel"; -import { PolymerChangedEvent } from "../../../../polymer-types"; import { haStyleDialog } from "../../../../resources/styles"; import { HomeAssistant } from "../../../../types"; import { LovelaceDashboardDetailsDialogParams } from "./show-dialog-lovelace-dashboard-detail"; @@ -25,62 +23,54 @@ export class DialogLovelaceDashboardDetail extends LitElement { @state() private _params?: LovelaceDashboardDetailsDialogParams; - @state() private _urlPath!: LovelaceDashboard["url_path"]; + @state() private _urlPathChanged = false; - @state() private _showInSidebar!: boolean; + @state() private _data?: Partial; - @state() private _icon!: string; - - @state() private _title!: string; - - @state() - private _requireAdmin!: LovelaceDashboard["require_admin"]; - - @state() private _error?: string; + @state() private _error?: Record; @state() private _submitting = false; - public async showDialog( - params: LovelaceDashboardDetailsDialogParams - ): Promise { + public showDialog(params: LovelaceDashboardDetailsDialogParams): void { this._params = params; this._error = undefined; - this._urlPath = this._params.urlPath || ""; + this._urlPathChanged = false; if (this._params.dashboard) { - this._showInSidebar = !!this._params.dashboard.show_in_sidebar; - this._icon = this._params.dashboard.icon || ""; - this._title = this._params.dashboard.title || ""; - this._requireAdmin = this._params.dashboard.require_admin || false; + this._data = this._params.dashboard; } else { - this._showInSidebar = true; - this._icon = ""; - this._title = ""; - this._requireAdmin = false; + this._data = { + show_in_sidebar: true, + icon: "", + title: "", + require_admin: false, + mode: "storage", + }; } - await this.updateComplete; + } + + public closeDialog(): void { + this._params = undefined; + this._data = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); } protected render(): TemplateResult { - if (!this._params) { + if (!this._params || !this._data) { return html``; } const defaultPanelUrlPath = this.hass.defaultPanel; - const urlInvalid = - this._params.urlPath !== "lovelace" && - !/^[a-zA-Z0-9_-]+-[a-zA-Z0-9_-]+$/.test(this._urlPath); - const titleInvalid = !this._title.trim(); - const dir = computeRTLDirection(this.hass); + const titleInvalid = !this._data.title || !this._data.title.trim(); return html` ${this._error}
` - : ""} -
- - - ${!this._params.dashboard && this.hass.userData?.showAdvanced - ? html` - - ` - : ""} -
- - - - -
-
- - - - -
-
+ `}
${this._params.urlPath @@ -206,7 +134,9 @@ export class DialogLovelaceDashboardDetail extends LitElement { ${this._params.urlPath @@ -223,41 +153,97 @@ export class DialogLovelaceDashboardDetail extends LitElement { `; } - private _urlChanged(ev: PolymerChangedEvent) { - this._error = undefined; - this._urlPath = ev.detail.value; - } + private _schema = memoizeOne( + ( + params: LovelaceDashboardDetailsDialogParams, + userData: CoreFrontendUserData | null | undefined + ) => + [ + { + name: "title", + required: true, + selector: { + text: {}, + }, + }, + { + name: "icon", + required: true, + selector: { + icon: {}, + }, + }, + !params.dashboard && + userData?.showAdvanced && { + name: "url_path", + required: true, + selector: { text: {} }, + }, + { + name: "require_admin", + required: true, + selector: { + boolean: {}, + }, + }, + { + name: "show_in_sidebar", + required: true, + selector: { + boolean: {}, + }, + }, + ].filter(Boolean) + ); - private _iconChanged(ev: PolymerChangedEvent) { - this._error = undefined; - this._icon = ev.detail.value; - } + private _computeLabel = (entry: HaFormSchema): string => + this.hass.localize( + `ui.panel.config.lovelace.dashboards.detail.${ + entry.name === "show_in_sidebar" + ? "show_sidebar" + : entry.name === "url_path" + ? "url" + : entry.name + }` + ); - private _titleChanged(ev: PolymerChangedEvent) { + private _valueChanged(ev: CustomEvent) { this._error = undefined; - this._title = ev.detail.value; - if (!this.hass.userData?.showAdvanced) { - this._fillUrlPath(); + const value = ev.detail.value; + if (value.url_path !== this._data?.url_path) { + this._urlPathChanged = true; + if ( + !value.url_path || + value.url_path === "lovelace" || + !/^[a-zA-Z0-9_-]+-[a-zA-Z0-9_-]+$/.test(value.url_path) + ) { + this._error = { + url_path: this.hass.localize( + "ui.panel.config.lovelace.dashboards.detail.url_error_msg" + ), + }; + } + } + if (value.title !== this._data?.title) { + this._data = value; + this._fillUrlPath(value.title); + } else { + this._data = value; } } - private _fillUrlPath() { - if ((this.hass.userData?.showAdvanced && this._urlPath) || !this._title) { + private _fillUrlPath(title: string) { + if ((this.hass.userData?.showAdvanced && this._urlPathChanged) || !title) { return; } - const slugifyTitle = slugify(this._title, "-"); - this._urlPath = slugifyTitle.includes("-") - ? slugifyTitle - : `lovelace-${slugifyTitle}`; - } - - private _showSidebarChanged(ev: Event) { - this._showInSidebar = (ev.target as HaSwitch).checked; - } - - private _requireAdminChanged(ev: Event) { - this._requireAdmin = (ev.target as HaSwitch).checked; + const slugifyTitle = slugify(title, "-"); + this._data = { + ...this._data, + url_path: slugifyTitle.includes("-") + ? slugifyTitle + : `lovelace-${slugifyTitle}`, + }; } private _toggleDefault() { @@ -273,29 +259,20 @@ export class DialogLovelaceDashboardDetail extends LitElement { private async _updateDashboard() { if (this._params?.urlPath && !this._params.dashboard?.id) { - this._close(); + this.closeDialog(); } this._submitting = true; try { - const values: Partial = { - require_admin: this._requireAdmin, - show_in_sidebar: this._showInSidebar, - icon: this._icon || undefined, - title: this._title, - }; if (this._params!.dashboard) { - await this._params!.updateDashboard(values); + await this._params!.updateDashboard(this._data as LovelaceDashboard); } else { - (values as LovelaceDashboardCreateParams).url_path = - this._urlPath.trim(); - (values as LovelaceDashboardCreateParams).mode = "storage"; await this._params!.createDashboard( - values as LovelaceDashboardCreateParams + this._data as LovelaceDashboardCreateParams ); } - this._close(); + this.closeDialog(); } catch (err: any) { - this._error = err?.message || "Unknown error"; + this._error = { base: err?.message || "Unknown error" }; } finally { this._submitting = false; } @@ -305,26 +282,15 @@ export class DialogLovelaceDashboardDetail extends LitElement { this._submitting = true; try { if (await this._params!.removeDashboard()) { - this._close(); + this.closeDialog(); } } finally { this._submitting = false; } } - private _close(): void { - this._params = undefined; - } - static get styles(): CSSResultGroup { - return [ - haStyleDialog, - css` - ha-switch { - padding: 16px 0; - } - `, - ]; + return [haStyleDialog, css``]; } } diff --git a/src/panels/config/lovelace/resources/dialog-lovelace-resource-detail.ts b/src/panels/config/lovelace/resources/dialog-lovelace-resource-detail.ts index eb2ebe92d3..283d1f2986 100644 --- a/src/panels/config/lovelace/resources/dialog-lovelace-resource-detail.ts +++ b/src/panels/config/lovelace/resources/dialog-lovelace-resource-detail.ts @@ -1,21 +1,20 @@ import "@material/mwc-button/mwc-button"; -import "@material/mwc-list/mwc-list-item"; -import "@polymer/paper-input/paper-input"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { stopPropagation } from "../../../../common/dom/stop_propagation"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../../../common/dom/fire_event"; import { createCloseHeading } from "../../../../components/ha-dialog"; -import "../../../../components/ha-select"; -import { - LovelaceResource, - LovelaceResourcesMutableParams, -} from "../../../../data/lovelace"; -import { PolymerChangedEvent } from "../../../../polymer-types"; +import "../../../../components/ha-form/ha-form"; +import { HaFormSchema } from "../../../../components/ha-form/types"; +import { LovelaceResourcesMutableParams } from "../../../../data/lovelace"; import { haStyleDialog } from "../../../../resources/styles"; import { HomeAssistant } from "../../../../types"; import { LovelaceResourceDetailsDialogParams } from "./show-dialog-lovelace-resource-detail"; -const detectResourceType = (url: string) => { +const detectResourceType = (url?: string) => { + if (!url) { + return undefined; + } const ext = url.split(".").pop() || ""; if (ext === "css") { @@ -35,38 +34,41 @@ export class DialogLovelaceResourceDetail extends LitElement { @state() private _params?: LovelaceResourceDetailsDialogParams; - @state() private _url!: LovelaceResource["url"]; + @state() private _data?: Partial; - @state() private _type?: LovelaceResource["type"]; - - @state() private _error?: string; + @state() private _error?: Record; @state() private _submitting = false; - public async showDialog( - params: LovelaceResourceDetailsDialogParams - ): Promise { + public showDialog(params: LovelaceResourceDetailsDialogParams): void { this._params = params; this._error = undefined; if (this._params.resource) { - this._url = this._params.resource.url || ""; - this._type = this._params.resource.type || undefined; + this._data = { + url: this._params.resource.url, + res_type: this._params.resource.type, + }; } else { - this._url = ""; - this._type = undefined; + this._data = { + url: "", + }; } - await this.updateComplete; + } + + public closeDialog(): void { + this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); } protected render(): TemplateResult { if (!this._params) { return html``; } - const urlInvalid = this._url.trim() === ""; + const urlInvalid = !this._data?.url || this._data.url.trim() === ""; return html`
- ${this._error ? html`
${this._error}
` : ""} -
-

- ${this.hass!.localize( - "ui.panel.config.lovelace.resources.detail.warning_header" - )} -

+ ${this.hass!.localize( "ui.panel.config.lovelace.resources.detail.warning_text" )} - -
- - - ${this.hass!.localize( - "ui.panel.config.lovelace.resources.types.module" - )} - - ${this._type === "js" - ? html` - - ${this.hass!.localize( - "ui.panel.config.lovelace.resources.types.js" - )} - - ` - : ""} - - ${this.hass!.localize( - "ui.panel.config.lovelace.resources.types.css" - )} - - ${this._type === "html" - ? html` - - ${this.hass!.localize( - "ui.panel.config.lovelace.resources.types.html" - )} - - ` - : ""} - -
+ + +
${this._params.resource ? html` @@ -159,7 +118,7 @@ export class DialogLovelaceResourceDetail extends LitElement { ${this._params.resource ? this.hass!.localize( @@ -173,37 +132,86 @@ export class DialogLovelaceResourceDetail extends LitElement { `; } - private _urlChanged(ev: PolymerChangedEvent) { - this._error = undefined; - this._url = ev.detail.value; - if (!this._type) { - this._type = detectResourceType(this._url); + private _schema = memoizeOne((data) => [ + { + name: "url", + required: true, + selector: { + text: {}, + }, + }, + { + name: "res_type", + required: true, + selector: { + select: { + options: [ + { + value: "module", + label: this.hass!.localize( + "ui.panel.config.lovelace.resources.types.module" + ), + }, + { + value: "css", + label: this.hass!.localize( + "ui.panel.config.lovelace.resources.types.css" + ), + }, + data.type === "js" && { + value: "js", + label: this.hass!.localize( + "ui.panel.config.lovelace.resources.types.js" + ), + }, + data.type === "html" && { + value: "html", + label: this.hass!.localize( + "ui.panel.config.lovelace.resources.types.html" + ), + }, + ].filter(Boolean), + }, + }, + }, + ]); + + private _computeLabel = (entry: HaFormSchema): string => + this.hass.localize( + `ui.panel.config.lovelace.resources.detail.${entry.name}` + ); + + private _valueChanged(ev: CustomEvent) { + this._data = ev.detail.value; + if (!this._data!.res_type) { + const type = detectResourceType(this._data!.url); + if (!type) { + return; + } + this._data = { + ...this._data, + res_type: type, + }; } } - private _typeChanged(ev) { - this._type = ev.target.value; - } - private async _updateResource() { - if (!this._type) { + if (!this._data?.res_type) { return; } this._submitting = true; try { - const values: LovelaceResourcesMutableParams = { - url: this._url.trim(), - res_type: this._type, - }; if (this._params!.resource) { - await this._params!.updateResource(values); + await this._params!.updateResource(this._data!); } else { - await this._params!.createResource(values); + await this._params!.createResource( + this._data! as LovelaceResourcesMutableParams + ); } this._params = undefined; } catch (err: any) { - this._error = err?.message || "Unknown error"; + this._error = { base: err?.message || "Unknown error" }; } finally { this._submitting = false; } @@ -213,26 +221,15 @@ export class DialogLovelaceResourceDetail extends LitElement { this._submitting = true; try { if (await this._params!.removeResource()) { - this._close(); + this.closeDialog(); } } finally { this._submitting = false; } } - private _close(): void { - this._params = undefined; - } - static get styles(): CSSResultGroup { - return [ - haStyleDialog, - css` - .warning { - color: var(--error-color); - } - `, - ]; + return haStyleDialog; } } From 0936fd9ae407105cd4fc3cf98c5dfcb200c401a4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Mar 2022 14:31:11 -0800 Subject: [PATCH 010/142] Guard setting up config flow for an unsupported domain (#11937) --- .../config-flow/dialog-data-entry-flow.ts | 13 ++++++++++--- .../integrations/ha-config-integrations.ts | 19 ++++++++++++++++++- src/translations/en.json | 1 + 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index 6a128d081a..6fbab75c74 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -377,13 +377,20 @@ class DataEntryFlowDialog extends LitElement { step = await this._params!.flowConfig.createFlow(this.hass, handler); } catch (err: any) { this.closeDialog(); + const message = + err?.status_code === 404 + ? this.hass.localize( + "ui.panel.config.integrations.config_flow.no_config_flow" + ) + : `${this.hass.localize( + "ui.panel.config.integrations.config_flow.could_not_load" + )}: ${err?.body?.message || err?.message}`; + showAlertDialog(this, { title: this.hass.localize( "ui.panel.config.integrations.config_flow.error" ), - text: `${this.hass.localize( - "ui.panel.config.integrations.config_flow.could_not_load" - )}: ${err.message || err.body}`, + text: message, }); return; } finally { diff --git a/src/panels/config/integrations/ha-config-integrations.ts b/src/panels/config/integrations/ha-config-integrations.ts index f58a797a36..5255d1197e 100644 --- a/src/panels/config/integrations/ha-config-integrations.ts +++ b/src/panels/config/integrations/ha-config-integrations.ts @@ -30,6 +30,7 @@ import "../../../components/ha-check-list-item"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { ConfigEntry, getConfigEntries } from "../../../data/config_entries"; import { + getConfigFlowHandlers, getConfigFlowInProgressCollection, localizeConfigFlowTitle, subscribeConfigFlowInProgress, @@ -51,7 +52,10 @@ import { } from "../../../data/integration"; import { scanUSBDevices } from "../../../data/usb"; import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; -import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../../dialogs/generic/show-dialog-box"; import "../../../layouts/hass-loading-screen"; import "../../../layouts/hass-tabs-subpage"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; @@ -652,6 +656,19 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { if (!domain) { return; } + const handlers = await getConfigFlowHandlers(this.hass); + + if (!handlers.includes(domain)) { + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.integrations.config_flow.error" + ), + text: this.hass.localize( + "ui.panel.config.integrations.config_flow.no_config_flow" + ), + }); + return; + } const localize = await localizePromise; if ( !(await showConfirmationDialog(this, { diff --git a/src/translations/en.json b/src/translations/en.json index f657f3e6dd..b97fd52f09 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2605,6 +2605,7 @@ "finish": "Finish", "submit": "Submit", "next": "Next", + "no_config_flow": "This integration does not support configuration via the UI. If you followed this link from the Home Assistant website, make sure you run the latest version of Home Assistant.", "not_all_required_fields": "Not all required fields are filled in.", "error_saving_area": "Error saving area: {error}", "created_config": "Created configuration for {name}.", From bd20c15a55a8de24f9f9a6922ac8f2990960fd1d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Mar 2022 23:24:31 -0800 Subject: [PATCH 011/142] Show triggered vars on click (#11924) --- .../trigger/ha-automation-trigger-row.ts | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts index 7844e7ba09..60847b2c43 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts @@ -16,12 +16,16 @@ import "../../../../components/ha-alert"; import "../../../../components/ha-button-menu"; import "../../../../components/ha-card"; import "../../../../components/ha-icon-button"; +import "../../../../components/ha-yaml-editor"; import "../../../../components/ha-select"; import type { HaSelect } from "../../../../components/ha-select"; import "../../../../components/ha-textfield"; import { subscribeTrigger, Trigger } from "../../../../data/automation"; import { validateConfig } from "../../../../data/config"; -import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../../../dialogs/generic/show-dialog-box"; import { haStyle } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; import "./types/ha-automation-trigger-device"; @@ -94,7 +98,7 @@ export default class HaAutomationTriggerRow extends LitElement { @state() private _requestShowId = false; - @state() private _triggered = false; + @state() private _triggered?: Record; @state() private _triggerColor = false; @@ -231,9 +235,10 @@ export default class HaAutomationTriggerRow extends LitElement {
${this.hass.localize( "ui.panel.config.automation.editor.triggers.triggered" @@ -298,16 +303,16 @@ export default class HaAutomationTriggerRow extends LitElement { const triggerUnsub = subscribeTrigger( this.hass, - () => { + (result) => { if (untriggerTimeout !== undefined) { clearTimeout(untriggerTimeout); this._triggerColor = !this._triggerColor; } else { this._triggerColor = false; } - this._triggered = true; + this._triggered = result; untriggerTimeout = window.setTimeout(() => { - this._triggered = false; + this._triggered = undefined; untriggerTimeout = undefined; }, showTriggeredTime); }, @@ -416,6 +421,18 @@ export default class HaAutomationTriggerRow extends LitElement { this._yamlMode = !this._yamlMode; } + private _showTriggeredInfo() { + showAlertDialog(this, { + text: html` + + `, + }); + } + static get styles(): CSSResultGroup { return [ haStyle, @@ -426,12 +443,12 @@ export default class HaAutomationTriggerRow extends LitElement { --mdc-theme-text-primary-on-background: var(--primary-text-color); } .triggered { + cursor: pointer; position: absolute; top: 0px; right: 0px; left: 0px; text-transform: uppercase; - pointer-events: none; font-weight: bold; font-size: 14px; background-color: var(--primary-color); @@ -446,6 +463,9 @@ export default class HaAutomationTriggerRow extends LitElement { .triggered.active { max-height: 100px; } + .triggered:hover { + opacity: 0.8; + } .triggered.accent { background-color: var(--accent-color); color: var(--text-accent-color, var(--text-primary-color)); From ccb91e0b491ff00348670d54d1bdbda8f2ebfdde Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 Mar 2022 03:39:16 -0800 Subject: [PATCH 012/142] Allow marking YAML editor as read only (#11960) --- src/components/ha-yaml-editor.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/ha-yaml-editor.ts b/src/components/ha-yaml-editor.ts index d315e804ef..8f285cb439 100644 --- a/src/components/ha-yaml-editor.ts +++ b/src/components/ha-yaml-editor.ts @@ -31,6 +31,8 @@ export class HaYamlEditor extends LitElement { @property() public label?: string; + @property({ type: Boolean }) public readOnly = false; + @state() private _yaml = ""; public setValue(value): void { @@ -61,6 +63,7 @@ export class HaYamlEditor extends LitElement { Date: Mon, 7 Mar 2022 12:45:39 +0100 Subject: [PATCH 013/142] Convert inputs (#11907) * Convert inputs * Update dialog-thingtalk.ts * imports --- hassio/src/components/hassio-upload-backup.ts | 1 - src/components/ha-date-input.ts | 2 +- src/components/ha-textfield.ts | 11 +++ src/components/search-input.ts | 2 +- src/dialogs/quick-bar/ha-quick-bar.ts | 2 +- .../ha-voice-command-dialog.ts | 29 ++++--- .../automation/blueprint-automation-editor.ts | 84 +++++++++++++------ .../automation/thingtalk/dialog-thingtalk.ts | 16 ++-- .../types/ha-automation-trigger-webhook.ts | 2 +- .../dialog-manage-cloudhook.ts | 40 ++++----- .../dialogs/dialog-energy-gas-settings.ts | 19 ++--- .../dialog-energy-grid-flow-settings.ts | 22 ++--- .../zwave_js/dialog-zwave_js-add-node.ts | 24 +++--- .../zwave_js/zwave_js-node-config.ts | 9 +- .../config/script/blueprint-script-editor.ts | 4 +- .../components/hui-input-list-editor.ts | 29 ++++--- .../config-elements/hui-map-card-editor.ts | 7 +- .../hui-picture-card-editor.ts | 1 - 18 files changed, 168 insertions(+), 136 deletions(-) diff --git a/hassio/src/components/hassio-upload-backup.ts b/hassio/src/components/hassio-upload-backup.ts index ae19a12c81..d40345522e 100644 --- a/hassio/src/components/hassio-upload-backup.ts +++ b/hassio/src/components/hassio-upload-backup.ts @@ -1,5 +1,4 @@ import { mdiFolderUpload } from "@mdi/js"; -import "@polymer/paper-input/paper-input-container"; import { html, LitElement, TemplateResult } from "lit"; import { customElement, state } from "lit/decorators"; import { fireEvent } from "../../../src/common/dom/fire_event"; diff --git a/src/components/ha-date-input.ts b/src/components/ha-date-input.ts index 42308ee6c7..7575e3bcef 100644 --- a/src/components/ha-date-input.ts +++ b/src/components/ha-date-input.ts @@ -41,7 +41,7 @@ export class HaDateInput extends LitElement { return html` diff --git a/src/dialogs/quick-bar/ha-quick-bar.ts b/src/dialogs/quick-bar/ha-quick-bar.ts index 3a7b3aaa96..6135b6f4fd 100644 --- a/src/dialogs/quick-bar/ha-quick-bar.ts +++ b/src/dialogs/quick-bar/ha-quick-bar.ts @@ -162,7 +162,7 @@ export class QuickBar extends LitElement { "ui.dialogs.quick-bar.filter_placeholder" )} .value=${this._commandMode ? `>${this._search}` : this._search} - .icon=${true} + icon .iconTrailing=${this._search !== undefined || this._narrow} @input=${this._handleSearchChange} @keydown=${this._handleInputKeyDown} diff --git a/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts index 4c8fa20780..d05e441781 100644 --- a/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts +++ b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts @@ -1,7 +1,6 @@ /* eslint-disable lit/prefer-static-styles */ +import "@material/mwc-button/mwc-button"; import { mdiMicrophone } from "@mdi/js"; -import "@polymer/paper-input/paper-input"; -import type { PaperInputElement } from "@polymer/paper-input/paper-input"; import { css, CSSResultGroup, @@ -10,12 +9,16 @@ import { PropertyValues, TemplateResult, } 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 { fireEvent } from "../../common/dom/fire_event"; import { SpeechRecognition } from "../../common/dom/speech-recognition"; import { uid } from "../../common/util/uid"; +import "../../components/ha-dialog"; +import type { HaDialog } from "../../components/ha-dialog"; import "../../components/ha-icon-button"; +import "../../components/ha-textfield"; +import type { HaTextField } from "../../components/ha-textfield"; import { AgentInfo, getAgentInfo, @@ -24,9 +27,6 @@ import { } from "../../data/conversation"; import { haStyleDialog } from "../../resources/styles"; import type { HomeAssistant } from "../../types"; -import "../../components/ha-dialog"; -import type { HaDialog } from "../../components/ha-dialog"; -import "@material/mwc-button/mwc-button"; interface Message { who: string; @@ -127,18 +127,19 @@ export class HaVoiceCommandDialog extends LitElement { : ""}
- ${SpeechRecognition ? html` - + ${this.results ? html`
@@ -155,7 +156,7 @@ export class HaVoiceCommandDialog extends LitElement { ` : ""} - + ${this._agentInfo && this._agentInfo.attribution ? html`
- - - + + ${this._showDescription + ? html` + + ` + : html` + + `}
${this.stateObj ? html` @@ -173,15 +199,14 @@ export class HaBlueprintAutomationEditor extends LitElement { value?.default} @value-changed=${this._inputChanged} >` - : html``} + @input=${this._inputChanged} + >`} ` ) : html`

@@ -221,7 +246,7 @@ export class HaBlueprintAutomationEditor extends LitElement { ev.stopPropagation(); const target = ev.target as any; const key = target.key; - const value = ev.detail.value; + const value = ev.detail?.value || target.value; if ( (this.config.use_blueprint.input && this.config.use_blueprint.input[key] === value) || @@ -262,6 +287,10 @@ export class HaBlueprintAutomationEditor extends LitElement { }); } + private _addDescription() { + this._showDescription = true; + } + static get styles(): CSSResultGroup { return [ haStyle, @@ -273,9 +302,16 @@ export class HaBlueprintAutomationEditor extends LitElement { .padding { padding: 16px; } + .link-button-row { + padding: 14px; + } .blueprint-picker-container { padding: 0 16px 16px; } + ha-textarea, + ha-textfield { + display: block; + } h3 { margin: 16px; } @@ -292,9 +328,7 @@ export class HaBlueprintAutomationEditor extends LitElement { --paper-time-input-justify-content: flex-end; border-top: 1px solid var(--divider-color); } - :host(:not([narrow])) ha-settings-row paper-input { - width: 60%; - } + :host(:not([narrow])) ha-settings-row ha-textfield, :host(:not([narrow])) ha-settings-row ha-selector { width: 60%; } diff --git a/src/panels/config/automation/thingtalk/dialog-thingtalk.ts b/src/panels/config/automation/thingtalk/dialog-thingtalk.ts index 46cc842c5a..8eae2ce4b6 100644 --- a/src/panels/config/automation/thingtalk/dialog-thingtalk.ts +++ b/src/panels/config/automation/thingtalk/dialog-thingtalk.ts @@ -1,10 +1,11 @@ import "@material/mwc-button"; -import "@polymer/paper-input/paper-input"; -import type { PaperInputElement } from "@polymer/paper-input/paper-input"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, state, query } from "lit/decorators"; +import { customElement, property, query, state } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/ha-circular-progress"; +import "../../../../components/ha-dialog"; +import "../../../../components/ha-textfield"; +import type { HaTextField } from "../../../../components/ha-textfield"; import type { AutomationConfig } from "../../../../data/automation"; import { convertThingTalk } from "../../../../data/cloud"; import { haStyle, haStyleDialog } from "../../../../resources/styles"; @@ -12,7 +13,6 @@ import type { HomeAssistant } from "../../../../types"; import "./ha-thingtalk-placeholders"; import type { PlaceholderValues } from "./ha-thingtalk-placeholders"; import type { ThingtalkDialogParams } from "./show-dialog-thingtalk"; -import "../../../../components/ha-dialog"; export interface Placeholder { name: string; @@ -38,7 +38,7 @@ class DialogThingtalk extends LitElement { @state() private _placeholders?: PlaceholderContainer; - @query("#input") private _input?: PaperInputElement; + @query("#input") private _input?: HaTextField; private _value?: string; @@ -58,7 +58,7 @@ class DialogThingtalk extends LitElement { this._placeholders = undefined; this._params = undefined; if (this._input) { - this._input.value = null; + this._input.value = ""; } fireEvent(this, "dialog-closed", { dialog: this.localName }); } @@ -127,13 +127,13 @@ class DialogThingtalk extends LitElement { - + > diff --git a/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts b/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts index b018af2959..1dc48f780f 100644 --- a/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts +++ b/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts @@ -1,11 +1,12 @@ import "@material/mwc-button"; -import "@polymer/paper-input/paper-input"; -import type { PaperInputElement } from "@polymer/paper-input/paper-input"; import { css, CSSResultGroup, html, LitElement } from "lit"; -import { state } from "lit/decorators"; +import { query, state } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; +import { copyToClipboard } from "../../../../common/util/copy-clipboard"; +import type { HaTextField } from "../../../../components/ha-textfield"; +import "../../../../components/ha-textfield"; import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; -import { haStyle } from "../../../../resources/styles"; +import { haStyle, haStyleDialog } from "../../../../resources/styles"; import { HomeAssistant } from "../../../../types"; import { documentationUrl } from "../../../../util/documentation-url"; import { WebhookDialogParams } from "./show-dialog-manage-cloudhook"; @@ -17,6 +18,8 @@ export class DialogManageCloudhook extends LitElement { @state() private _params?: WebhookDialogParams; + @query("ha-textfield") _input!: HaTextField; + public showDialog(params: WebhookDialogParams) { this._params = params; } @@ -53,12 +56,12 @@ export class DialogManageCloudhook extends LitElement { "ui.panel.config.cloud.dialog_cloudhook.available_at" )}

- + >

${cloudhook.managed ? html` @@ -98,10 +101,6 @@ export class DialogManageCloudhook extends LitElement { `; } - private get _paperInput(): PaperInputElement { - return this.shadowRoot!.querySelector("paper-input")!; - } - private async _disableWebhook() { showConfirmationDialog(this, { text: this.hass!.localize( @@ -117,14 +116,10 @@ export class DialogManageCloudhook extends LitElement { } private _copyClipboard(ev: FocusEvent) { - // paper-input -> iron-input -> input - const paperInput = ev.currentTarget as PaperInputElement; - const input = (paperInput.inputElement as any) - .inputElement as HTMLInputElement; - input.setSelectionRange(0, input.value.length); + const textField = ev.currentTarget as HaTextField; try { - document.execCommand("copy"); - paperInput.label = this.hass!.localize( + copyToClipboard(textField.value); + textField.label = this.hass!.localize( "ui.panel.config.cloud.dialog_cloudhook.copied_to_clipboard" ); } catch (err: any) { @@ -133,18 +128,19 @@ export class DialogManageCloudhook extends LitElement { } private _restoreLabel() { - this._paperInput.label = inputLabel; + this._input.label = inputLabel; } static get styles(): CSSResultGroup { return [ haStyle, + haStyleDialog, css` ha-dialog { width: 650px; } - paper-input { - margin-top: -8px; + ha-textfield { + display: block; } button.link { color: var(--primary-color); diff --git a/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts index b180c5f151..8a3b57d427 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts @@ -19,6 +19,7 @@ import "../../../../components/entity/ha-statistic-picker"; import "../../../../components/entity/ha-entity-picker"; import "../../../../components/ha-radio"; import "../../../../components/ha-formfield"; +import "../../../../components/ha-textfield"; import type { HaRadio } from "../../../../components/ha-radio"; @customElement("dialog-energy-gas-settings") @@ -188,20 +189,19 @@ export class DialogEnergyGasSettings > ${this._costs === "number" - ? html` - ${this.hass.config.currency}/${unit} - ` + ` : ""} @@ -223,10 +223,10 @@ export class DialogEnergyGasSettings this._costs = input.value as any; } - private _numberPriceChanged(ev: CustomEvent) { + private _numberPriceChanged(ev) { this._source = { ...this._source!, - number_energy_price: Number(ev.detail.value), + number_energy_price: Number(ev.target.value), entity_energy_price: null, stat_cost: null, }; @@ -295,13 +295,10 @@ export class DialogEnergyGasSettings ha-formfield { display: block; } - ha-statistic-picker { - width: 100%; - } .price-options { display: block; padding-left: 52px; - margin-top: -16px; + margin-top: -8px; } `, ]; diff --git a/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts index d32e57f505..2599f61ee3 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts @@ -190,24 +190,21 @@ export class DialogEnergyGridFlowSettings > ${this._costs === "number" - ? html` - ${this.hass.localize( - `ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.cost_number_suffix`, - { currency: this.hass.config.currency } - )} - ` + ` : ""} @@ -302,13 +299,10 @@ export class DialogEnergyGridFlowSettings ha-formfield { display: block; } - ha-statistic-picker { - width: 100%; - } .price-options { display: block; padding-left: 52px; - margin-top: -16px; + margin-top: -8px; } `, ]; diff --git a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-add-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-add-node.ts index 4c9603ba0a..c1a75807c7 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-add-node.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-add-node.ts @@ -1,7 +1,5 @@ import "@material/mwc-button/mwc-button"; import { mdiAlertCircle, mdiCheckCircle, mdiQrcodeScan } from "@mdi/js"; -import "@polymer/paper-input/paper-input"; -import type { PaperInputElement } from "@polymer/paper-input/paper-input"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, query, state } from "lit/decorators"; @@ -11,28 +9,30 @@ import { HaCheckbox } from "../../../../../components/ha-checkbox"; import "../../../../../components/ha-circular-progress"; import { createCloseHeading } from "../../../../../components/ha-dialog"; import "../../../../../components/ha-formfield"; +import "../../../../../components/ha-qr-scanner"; import "../../../../../components/ha-radio"; import "../../../../../components/ha-switch"; +import "../../../../../components/ha-textfield"; +import type { HaTextField } from "../../../../../components/ha-textfield"; import { - zwaveGrantSecurityClasses, InclusionStrategy, MINIMUM_QR_STRING_LENGTH, - zwaveParseQrCode, + PlannedProvisioningEntry, provisionZwaveSmartStartNode, QRProvisioningInformation, RequestedGrant, SecurityClass, stopZwaveInclusion, subscribeAddZwaveNode, + ZWaveFeature, + zwaveGrantSecurityClasses, + zwaveParseQrCode, zwaveSupportsFeature, zwaveValidateDskAndEnterPin, - ZWaveFeature, - PlannedProvisioningEntry, } from "../../../../../data/zwave_js"; import { haStyle, haStyleDialog } from "../../../../../resources/styles"; import { HomeAssistant } from "../../../../../types"; import { ZWaveJSAddNodeDialogParams } from "./show-dialog-zwave_js-add-node"; -import "../../../../../components/ha-qr-scanner"; export interface ZWaveJSAddNodeDevice { id: string; @@ -98,7 +98,7 @@ class DialogZWaveJSAddNode extends LitElement { this._startInclusion(); } - @query("#pin-input") private _pinInput?: PaperInputElement; + @query("#pin-input") private _pinInput?: HaTextField; protected render(): TemplateResult { if (!this._entryId) { @@ -202,12 +202,11 @@ class DialogZWaveJSAddNode extends LitElement { : "" }

- + > ${this._dsk}
${item.metadata.unit ? html`${item.metadata.unit}` : ""} - `; + `; } if (item.configuration_value_type === "enumerated") { @@ -492,7 +493,7 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) { font-size: 1.3em; } - :host(:not([narrow])) ha-settings-row paper-input { + :host(:not([narrow])) ha-settings-row ha-textfield { width: 30%; text-align: right; } diff --git a/src/panels/config/script/blueprint-script-editor.ts b/src/panels/config/script/blueprint-script-editor.ts index 738fdb8419..6edae7d2cc 100644 --- a/src/panels/config/script/blueprint-script-editor.ts +++ b/src/panels/config/script/blueprint-script-editor.ts @@ -186,9 +186,7 @@ export class HaBlueprintScriptEditor extends LitElement { --paper-time-input-justify-content: flex-end; border-top: 1px solid var(--divider-color); } - :host(:not([narrow])) ha-settings-row paper-input { - width: 60%; - } + :host(:not([narrow])) ha-settings-row ha-textfield, :host(:not([narrow])) ha-settings-row ha-selector { width: 60%; } diff --git a/src/panels/lovelace/components/hui-input-list-editor.ts b/src/panels/lovelace/components/hui-input-list-editor.ts index fe53e72c11..a7c6ea75e0 100644 --- a/src/panels/lovelace/components/hui-input-list-editor.ts +++ b/src/panels/lovelace/components/hui-input-list-editor.ts @@ -1,11 +1,11 @@ import { mdiClose } from "@mdi/js"; -import "@polymer/paper-input/paper-input"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-icon-button"; import { HomeAssistant } from "../../../types"; import { EditorTarget } from "../editor/types"; +import "../../../components/ha-textfield"; @customElement("hui-input-list-editor") export class HuiInputListEditor extends LitElement { @@ -23,30 +23,31 @@ export class HuiInputListEditor extends LitElement { return html` ${this.value.map( (listEntry, index) => html` - Clear + > + + ` )} - + > `; } @@ -103,10 +104,12 @@ export class HuiInputListEditor extends LitElement { static get styles(): CSSResultGroup { return css` ha-icon-button { - --mdc-icon-button-size: 24px; - padding: 2px; + margin-right: -24px; color: var(--secondary-text-color); } + ha-textfield { + display: block; + } `; } } diff --git a/src/panels/lovelace/editor/config-elements/hui-map-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-map-card-editor.ts index 50c8fd65d3..ea0741ae33 100644 --- a/src/panels/lovelace/editor/config-elements/hui-map-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-map-card-editor.ts @@ -1,5 +1,4 @@ import "../../../../components/ha-form/ha-form"; -import "@polymer/paper-input/paper-input"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { @@ -24,7 +23,7 @@ import { EntityConfig } from "../../entity-rows/types"; import { LovelaceCardEditor } from "../../types"; import { processEditorEntities } from "../process-editor-entities"; import { entitiesConfigStruct } from "../structs/entities-struct"; -import { EditorTarget, EntitiesEditorEvent } from "../types"; +import { EntitiesEditorEvent } from "../types"; import { configElementStyle } from "./config-elements-style"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { HaFormSchema } from "../../../../components/ha-form/types"; @@ -127,10 +126,6 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor { if (!this._config || !this.hass) { return; } - const target = ev.target! as EditorTarget; - if (!target.configValue) { - return; - } const value = ev.detail.value; diff --git a/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts index 53288a843b..9b1f46f96b 100644 --- a/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts @@ -1,4 +1,3 @@ -import "@polymer/paper-input/paper-input"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { assert, object, optional, string, assign } from "superstruct"; From b4b52d387286363d4c08a9d6cc28c4eb3d3d13a9 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 7 Mar 2022 06:49:51 -0500 Subject: [PATCH 014/142] Remove some additional old zwave code (#11941) --- src/common/entity/domain_icon.ts | 16 ---------------- src/common/style/icon_color_css.ts | 3 --- 2 files changed, 19 deletions(-) diff --git a/src/common/entity/domain_icon.ts b/src/common/entity/domain_icon.ts index af56f1135b..b64d93f8a0 100644 --- a/src/common/entity/domain_icon.ts +++ b/src/common/entity/domain_icon.ts @@ -9,7 +9,6 @@ import { mdiCast, mdiCastConnected, mdiClock, - mdiEmoticonDead, mdiFlash, mdiGestureTapButton, mdiLanConnect, @@ -22,14 +21,11 @@ import { mdiPowerPlug, mdiPowerPlugOff, mdiRestart, - mdiSleep, - mdiTimerSand, mdiToggleSwitch, mdiToggleSwitchOff, mdiCheckCircleOutline, mdiCloseCircleOutline, mdiWeatherNight, - mdiZWave, } from "@mdi/js"; import { HassEntity } from "home-assistant-js-websocket"; /** @@ -115,18 +111,6 @@ export const domainIcon = ( return mdiFlash; } - case "zwave": - switch (compareState) { - case "dead": - return mdiEmoticonDead; - case "sleeping": - return mdiSleep; - case "initializing": - return mdiTimerSand; - default: - return mdiZWave; - } - case "sensor": { const icon = sensorIcon(stateObj); if (icon) { diff --git a/src/common/style/icon_color_css.ts b/src/common/style/icon_color_css.ts index 7e00bf1c77..c4d4c7b9ee 100644 --- a/src/common/style/icon_color_css.ts +++ b/src/common/style/icon_color_css.ts @@ -70,9 +70,6 @@ export const iconColorCSS = css` } ha-state-icon[data-domain="plant"][data-state="problem"], - ha-state-icon[data-domain="zwave"][data-state="dead"] { - color: var(--state-icon-error-color); - } /* Color the icon if unavailable */ ha-state-icon[data-state="unavailable"] { From 2e10eb04b65ae1b88e41e0946f770e956e9a7072 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 7 Mar 2022 13:08:54 +0100 Subject: [PATCH 015/142] Correct media upload error + add file name (#11949) Co-authored-by: Bram Kragten --- src/data/image.ts | 2 +- src/data/media_source.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/image.ts b/src/data/image.ts index 497f2ffa3a..94198de6dc 100644 --- a/src/data/image.ts +++ b/src/data/image.ts @@ -29,7 +29,7 @@ export const createImage = async ( body: fd, }); if (resp.status === 413) { - throw new Error("Uploaded image is too large"); + throw new Error(`Uploaded image is too large (${file.name})`); } else if (resp.status !== 200) { throw new Error("Unknown error"); } diff --git a/src/data/media_source.ts b/src/data/media_source.ts index 61494ee3f8..a6e93d83cf 100644 --- a/src/data/media_source.ts +++ b/src/data/media_source.ts @@ -43,7 +43,7 @@ export const uploadLocalMedia = async ( } ); if (resp.status === 413) { - throw new Error("Uploaded image is too large"); + throw new Error(`Uploaded file is too large (${file.name})`); } else if (resp.status !== 200) { throw new Error("Unknown error"); } From dfcb0f6ba0409a23edf7a6fa893697eb668535c8 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 7 Mar 2022 14:25:38 +0100 Subject: [PATCH 016/142] Fix humidifier more info mode dropdown (#11964) --- .../controls/more-info-humidifier.ts | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/src/dialogs/more-info/controls/more-info-humidifier.ts b/src/dialogs/more-info/controls/more-info-humidifier.ts index bce976b45b..b5618e8619 100644 --- a/src/dialogs/more-info/controls/more-info-humidifier.ts +++ b/src/dialogs/more-info/controls/more-info-humidifier.ts @@ -1,3 +1,4 @@ +import "@material/mwc-list/mwc-list-item"; import { css, CSSResultGroup, @@ -12,6 +13,7 @@ import { fireEvent } from "../../../common/dom/fire_event"; import { stopPropagation } from "../../../common/dom/stop_propagation"; import { supportsFeature } from "../../../common/entity/supports-feature"; import { computeRTLDirection } from "../../../common/util/compute_rtl"; +import "../../../components/ha-select"; import "../../../components/ha-slider"; import "../../../components/ha-switch"; import { @@ -19,8 +21,6 @@ import { HUMIDIFIER_SUPPORT_MODES, } from "../../../data/humidifier"; import { HomeAssistant } from "../../../types"; -import "@material/mwc-list/mwc-list"; -import "@material/mwc-list/mwc-list-item"; class MoreInfoHumidifier extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -67,26 +67,24 @@ class MoreInfoHumidifier extends LitElement { ${supportModes ? html` -
- - ${stateObj.attributes.available_modes!.map( - (mode) => html` - - ${hass.localize( - `state_attributes.humidifier.mode.${mode}` - ) || mode} - - ` - )} - -
+ + ${stateObj.attributes.available_modes!.map( + (mode) => html` + + ${hass.localize( + `state_attributes.humidifier.mode.${mode}` + ) || mode} + + ` + )} + ` : ""}
From 8258641443b576a69a38f01dc76ead47b5364ec6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 7 Mar 2022 14:55:44 +0100 Subject: [PATCH 017/142] Make min width of select configurable (#11965) --- src/components/ha-select.ts | 3 +++ src/panels/lovelace/entity-rows/hui-input-select-entity-row.ts | 1 + 2 files changed, 4 insertions(+) diff --git a/src/components/ha-select.ts b/src/components/ha-select.ts index 59269bde4a..b90133b8eb 100644 --- a/src/components/ha-select.ts +++ b/src/components/ha-select.ts @@ -44,6 +44,9 @@ export class HaSelect extends SelectBase { .mdc-select:not(.mdc-select--disabled) .mdc-select__icon { color: var(--secondary-text-color); } + .mdc-select__anchor { + width: var(--ha-select-min-width, 200px); + } `, ]; } diff --git a/src/panels/lovelace/entity-rows/hui-input-select-entity-row.ts b/src/panels/lovelace/entity-rows/hui-input-select-entity-row.ts index 0b88452129..1db8a8c1b6 100644 --- a/src/panels/lovelace/entity-rows/hui-input-select-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-input-select-entity-row.ts @@ -87,6 +87,7 @@ class HuiInputSelectEntityRow extends LitElement implements LovelaceRow { } ha-select { width: 100%; + --ha-select-min-width: 0; } `; From 9d28df31bd1e5788c98ae3115fb91a2b09b3d3d1 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Mon, 7 Mar 2022 08:29:00 -0600 Subject: [PATCH 018/142] Fix for Statistics Editor (#11942) Co-authored-by: Bram Kragten --- .../config-elements/hui-statistics-graph-card-editor.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts index b68ecaebda..c695e56b54 100644 --- a/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts @@ -153,7 +153,7 @@ export class HuiStatisticsGraphCardEditor .pickedStatisticLabel=${`Statistic`} .value=${this._configEntities} .configValue=${"entities"} - @value-changed=${this._valueChanged} + @value-changed=${this._entitiesChanged} >
`; @@ -163,6 +163,12 @@ export class HuiStatisticsGraphCardEditor fireEvent(this, "config-changed", { config: ev.detail.value }); } + private _entitiesChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { + config: { ...this._config!, entities: ev.detail.value }, + }); + } + private _computeLabelCallback = (schema: HaFormSchema) => this.hass!.localize( `ui.panel.lovelace.editor.card.generic.${schema.name}` From 4b8b14a69dfa677907ab79f0fb0dfcf73df73dea Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 7 Mar 2022 15:40:19 +0100 Subject: [PATCH 019/142] A11y expansion panel (#11967) --- src/components/ha-expansion-panel.ts | 58 +++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/src/components/ha-expansion-panel.ts b/src/components/ha-expansion-panel.ts index 705b5a6b81..b3dc414f1e 100644 --- a/src/components/ha-expansion-panel.ts +++ b/src/components/ha-expansion-panel.ts @@ -1,6 +1,13 @@ import { mdiChevronDown } from "@mdi/js"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, query } from "lit/decorators"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { fireEvent } from "../common/dom/fire_event"; import { nextRender } from "../common/util/render-status"; @@ -16,11 +23,21 @@ class HaExpansionPanel extends LitElement { @property() secondary?: string; + @state() _showContent = this.expanded; + @query(".container") private _container!: HTMLDivElement; protected render(): TemplateResult { return html` -
+
${this.header} ${this.secondary} @@ -33,21 +50,37 @@ class HaExpansionPanel extends LitElement {
- + ${this._showContent ? html`` : ""}
`; } - private _handleTransitionEnd() { - this._container.style.removeProperty("height"); + protected willUpdate(changedProps: PropertyValues) { + if (changedProps.has("expanded") && this.expanded) { + this._showContent = this.expanded; + } } - private async _toggleContainer(): Promise { + private _handleTransitionEnd() { + this._container.style.removeProperty("height"); + this._showContent = this.expanded; + } + + private async _toggleContainer(ev): Promise { + if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") { + return; + } + ev.preventDefault(); const newExpanded = !this.expanded; fireEvent(this, "expanded-will-change", { expanded: newExpanded }); if (newExpanded) { + this._showContent = true; // allow for dynamic content to be rendered await nextRender(); } @@ -80,17 +113,21 @@ class HaExpansionPanel extends LitElement { var(--divider-color, #e0e0e0) ); border-radius: var(--ha-card-border-radius, 4px); - padding: 0 8px; } - .summary { + #summary { display: flex; - padding: var(--expansion-panel-summary-padding, 0); + padding: var(--expansion-panel-summary-padding, 0 8px); min-height: 48px; align-items: center; cursor: pointer; overflow: hidden; font-weight: 500; + outline: none; + } + + #summary:focus { + background: var(--input-fill-color); } .summary-icon { @@ -103,6 +140,7 @@ class HaExpansionPanel extends LitElement { } .container { + padding: var(--expansion-panel-content-padding, 0 8px); overflow: hidden; transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1); height: 0px; From 0dac10aa23a6d6d5ac65d1de430755960af6c357 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 7 Mar 2022 15:42:40 +0100 Subject: [PATCH 020/142] Convert file upload to mdc (#11906) --- src/components/ha-file-upload.ts | 211 +++++++++++------- .../config/person/dialog-person-detail.ts | 3 + 2 files changed, 129 insertions(+), 85 deletions(-) diff --git a/src/components/ha-file-upload.ts b/src/components/ha-file-upload.ts index 31dd853e06..9c81a3f17a 100644 --- a/src/components/ha-file-upload.ts +++ b/src/components/ha-file-upload.ts @@ -1,6 +1,5 @@ +import { styles } from "@material/mwc-textfield/mwc-textfield.css"; import { mdiClose } from "@mdi/js"; -import "@polymer/iron-input/iron-input"; -import "@polymer/paper-input/paper-input-container"; import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; @@ -21,7 +20,7 @@ export class HaFileUpload extends LitElement { @property() public accept!: string; - @property() public icon!: string; + @property() public icon?: string; @property() public label!: string; @@ -39,15 +38,7 @@ export class HaFileUpload extends LitElement { protected firstUpdated(changedProperties: PropertyValues) { super.firstUpdated(changedProperties); if (this.autoOpenFileDialog) { - this._input?.click(); - } - } - - protected updated(changedProperties: PropertyValues) { - if (changedProperties.has("_drag") && !this.uploading) { - ( - this.shadowRoot!.querySelector("paper-input-container") as any - )._setFocused(this._drag); + this._openFilePicker(); } } @@ -60,51 +51,75 @@ export class HaFileUpload extends LitElement { active >` : html` - `} `; } + private _openFilePicker() { + this._input?.click(); + } + private _handleDrop(ev: DragEvent) { ev.preventDefault(); ev.stopPropagation(); @@ -137,40 +152,66 @@ export class HaFileUpload extends LitElement { } static get styles() { - return css` - paper-input-container { - position: relative; - padding: 8px; - margin: 0 -8px; - } - paper-input-container.dragged:before { - position: var(--layout-fit_-_position); - top: var(--layout-fit_-_top); - right: var(--layout-fit_-_right); - bottom: var(--layout-fit_-_bottom); - left: var(--layout-fit_-_left); - background: currentColor; - content: ""; - opacity: var(--dark-divider-opacity); - pointer-events: none; - border-radius: 4px; - } - input.file { - display: none; - } - img { - max-width: 125px; - max-height: 125px; - } - ha-icon-button { - --mdc-icon-button-size: 24px; - --mdc-icon-size: 20px; - } - ha-circular-progress { - display: block; - text-align-last: center; - } - `; + return [ + styles, + css` + :host { + display: block; + } + .mdc-text-field--filled { + height: auto; + padding-top: 16px; + cursor: pointer; + } + .mdc-text-field--filled.mdc-text-field--with-trailing-icon { + padding-top: 28px; + } + .mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon { + color: var(--secondary-text-color); + } + .mdc-text-field--filled.mdc-text-field--with-trailing-icon + .mdc-text-field__icon { + align-self: flex-end; + } + .mdc-text-field__icon--leading { + margin-bottom: 12px; + } + .mdc-text-field--filled .mdc-floating-label--float-above { + transform: scale(0.75); + top: 8px; + } + .dragged:before { + position: var(--layout-fit_-_position); + top: var(--layout-fit_-_top); + right: var(--layout-fit_-_right); + bottom: var(--layout-fit_-_bottom); + left: var(--layout-fit_-_left); + background: currentColor; + content: ""; + opacity: var(--dark-divider-opacity); + pointer-events: none; + border-radius: 4px; + } + .value { + width: 100%; + } + input.file { + display: none; + } + img { + max-width: 100%; + max-height: 125px; + } + ha-icon-button { + --mdc-icon-button-size: 24px; + --mdc-icon-size: 20px; + } + ha-circular-progress { + display: block; + text-align-last: center; + } + `, + ]; } } diff --git a/src/panels/config/person/dialog-person-detail.ts b/src/panels/config/person/dialog-person-detail.ts index 185056404f..9406d469f8 100644 --- a/src/panels/config/person/dialog-person-detail.ts +++ b/src/panels/config/person/dialog-person-detail.ts @@ -464,6 +464,9 @@ class DialogPersonDetail extends LitElement { ha-textfield { display: block; } + ha-picture-upload { + margin-top: 16px; + } ha-formfield { display: block; padding: 16px 0; From 4cdff3faeaa57423c38b87b7e371e4e58eae59aa Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 7 Mar 2022 15:47:20 +0100 Subject: [PATCH 021/142] Add location selector, convert zone editor (#11902) --- gallery/src/pages/components/ha-form.ts | 5 + gallery/src/pages/components/ha-selector.ts | 5 + src/components/ha-form/ha-form-constant.ts | 4 +- src/components/ha-form/ha-form.ts | 4 +- src/components/ha-form/types.ts | 2 +- .../ha-selector/ha-selector-location.ts | 80 +++++ src/components/ha-selector/ha-selector.ts | 1 + src/data/selector.ts | 13 +- src/data/zone.ts | 6 +- src/panels/config/zone/dialog-zone-detail.ts | 310 +++++++----------- 10 files changed, 226 insertions(+), 204 deletions(-) create mode 100644 src/components/ha-selector/ha-selector-location.ts diff --git a/gallery/src/pages/components/ha-form.ts b/gallery/src/pages/components/ha-form.ts index b6ff6d1711..0722040cc0 100644 --- a/gallery/src/pages/components/ha-form.ts +++ b/gallery/src/pages/components/ha-form.ts @@ -38,6 +38,7 @@ const SCHEMAS: { select: "Select", icon: "Icon", media: "Media", + location: "Location", }, schema: [ { name: "addon", selector: { addon: {} } }, @@ -75,6 +76,10 @@ const SCHEMAS: { media: {}, }, }, + { + name: "location", + selector: { location: { radius: true, icon: "mdi:home" } }, + }, ], }, { diff --git a/gallery/src/pages/components/ha-selector.ts b/gallery/src/pages/components/ha-selector.ts index ea4a892015..600176d024 100644 --- a/gallery/src/pages/components/ha-selector.ts +++ b/gallery/src/pages/components/ha-selector.ts @@ -168,6 +168,11 @@ const SCHEMAS: { }, icon: { name: "Icon", selector: { icon: {} } }, media: { name: "Media", selector: { media: {} } }, + location: { name: "Location", selector: { location: {} } }, + location_radius: { + name: "Location with radius", + selector: { location: { radius: true, icon: "mdi:home" } }, + }, }, }, ]; diff --git a/src/components/ha-form/ha-form-constant.ts b/src/components/ha-form/ha-form-constant.ts index f20c057dfc..c1b6c67e9e 100644 --- a/src/components/ha-form/ha-form-constant.ts +++ b/src/components/ha-form/ha-form-constant.ts @@ -9,7 +9,9 @@ export class HaFormConstant extends LitElement implements HaFormElement { @property() public label!: string; protected render(): TemplateResult { - return html`${this.label}: ${this.schema.value}`; + return html`${this.label}${this.schema.value + ? `: ${this.schema.value}` + : ""}`; } static get styles(): CSSResultGroup { diff --git a/src/components/ha-form/ha-form.ts b/src/components/ha-form/ha-form.ts index c824d8fb89..e1e87d86f9 100644 --- a/src/components/ha-form/ha-form.ts +++ b/src/components/ha-form/ha-form.ts @@ -25,6 +25,8 @@ import { HomeAssistant } from "../../types"; const getValue = (obj, item) => obj ? (!item.name ? obj : obj[item.name]) : null; +const getError = (obj, item) => (obj && item.name ? obj[item.name] : null); + let selectorImported = false; @customElement("ha-form") @@ -84,7 +86,7 @@ export class HaForm extends LitElement implements HaFormElement { ` : ""} ${this.schema.map((item) => { - const error = getValue(this.error, item); + const error = getError(this.error, item); return html` ${error diff --git a/src/components/ha-form/types.ts b/src/components/ha-form/types.ts index fc45bc3ef1..2c3a9912d4 100644 --- a/src/components/ha-form/types.ts +++ b/src/components/ha-form/types.ts @@ -40,7 +40,7 @@ export interface HaFormSelector extends HaFormBaseSchema { export interface HaFormConstantSchema extends HaFormBaseSchema { type: "constant"; - value: string; + value?: string; } export interface HaFormIntegerSchema extends HaFormBaseSchema { diff --git a/src/components/ha-selector/ha-selector-location.ts b/src/components/ha-selector/ha-selector-location.ts new file mode 100644 index 0000000000..18372f525d --- /dev/null +++ b/src/components/ha-selector/ha-selector-location.ts @@ -0,0 +1,80 @@ +import { html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../common/dom/fire_event"; +import type { + LocationSelector, + LocationSelectorValue, +} from "../../data/selector"; +import "../../panels/lovelace/components/hui-theme-select-editor"; +import type { HomeAssistant } from "../../types"; +import type { MarkerLocation } from "../map/ha-locations-editor"; +import "../map/ha-locations-editor"; + +@customElement("ha-selector-location") +export class HaLocationSelector extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public selector!: LocationSelector; + + @property() public value?: LocationSelectorValue; + + @property() public label?: string; + + @property({ type: Boolean, reflect: true }) public disabled = false; + + protected render() { + return html` + + `; + } + + private _location = memoizeOne( + ( + selector: LocationSelector, + value?: LocationSelectorValue + ): MarkerLocation[] => { + const computedStyles = getComputedStyle(this); + const zoneRadiusColor = selector.location.radius + ? computedStyles.getPropertyValue("--zone-radius-color") || + computedStyles.getPropertyValue("--accent-color") + : undefined; + return [ + { + id: "location", + latitude: value?.latitude || this.hass.config.latitude, + longitude: value?.longitude || this.hass.config.longitude, + radius: selector.location.radius ? value?.radius || 1000 : undefined, + radius_color: zoneRadiusColor, + icon: selector.location.icon, + location_editable: true, + radius_editable: true, + }, + ]; + } + ); + + private _locationChanged(ev: CustomEvent) { + const [latitude, longitude] = ev.detail.location; + fireEvent(this, "value-changed", { + value: { ...this.value, latitude, longitude }, + }); + } + + private _radiusChanged(ev: CustomEvent) { + const radius = ev.detail.radius; + fireEvent(this, "value-changed", { value: { ...this.value, radius } }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-location": HaLocationSelector; + } +} diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index 5f6a31aebc..61b790cd98 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -20,6 +20,7 @@ import "./ha-selector-time"; import "./ha-selector-icon"; import "./ha-selector-media"; import "./ha-selector-theme"; +import "./ha-selector-location"; @customElement("ha-selector") export class HaSelector extends LitElement { diff --git a/src/data/selector.ts b/src/data/selector.ts index d8cb9a4e63..c8cc87b36e 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -15,7 +15,8 @@ export type Selector = | SelectSelector | IconSelector | MediaSelector - | ThemeSelector; + | ThemeSelector + | LocationSelector; export interface EntitySelector { entity: { @@ -164,6 +165,16 @@ export interface MediaSelector { media: {}; } +export interface LocationSelector { + location: { radius?: boolean; icon?: string }; +} + +export interface LocationSelectorValue { + latitude: number; + longitude: number; + radius?: number; +} + export interface MediaSelectorValue { entity_id?: string; media_content_id?: string; diff --git a/src/data/zone.ts b/src/data/zone.ts index a4e091c30e..52ea733317 100644 --- a/src/data/zone.ts +++ b/src/data/zone.ts @@ -12,12 +12,12 @@ export interface Zone { } export interface ZoneMutableParams { + name: string; icon?: string; latitude: number; longitude: number; - name: string; - passive: boolean; - radius: number; + passive?: boolean; + radius?: number; } export const fetchZones = (hass: HomeAssistant) => diff --git a/src/panels/config/zone/dialog-zone-detail.ts b/src/panels/config/zone/dialog-zone-detail.ts index 81c3e78807..07b9fcb4fa 100644 --- a/src/panels/config/zone/dialog-zone-detail.ts +++ b/src/panels/config/zone/dialog-zone-detail.ts @@ -1,17 +1,12 @@ import "@material/mwc-button"; -import "@polymer/paper-input/paper-input"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../common/dom/fire_event"; import { addDistanceToCoord } from "../../../common/location/add_distance_to_coord"; -import { computeRTLDirection } from "../../../common/util/compute_rtl"; import { createCloseHeading } from "../../../components/ha-dialog"; -import "../../../components/ha-formfield"; -import "../../../components/ha-icon-picker"; -import "../../../components/ha-switch"; -import "../../../components/map/ha-locations-editor"; -import type { MarkerLocation } from "../../../components/map/ha-locations-editor"; +import "../../../components/ha-form/ha-form"; +import { HaFormSchema } from "../../../components/ha-form/types"; import { getZoneEditorInitData, ZoneMutableParams } from "../../../data/zone"; import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; @@ -20,19 +15,9 @@ import { ZoneDetailDialogParams } from "./show-dialog-zone-detail"; class DialogZoneDetail extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @state() private _name!: string; + @state() private _error?: Record; - @state() private _icon!: string; - - @state() private _latitude!: number; - - @state() private _longitude!: number; - - @state() private _passive!: boolean; - - @state() private _radius!: number; - - @state() private _error?: string; + @state() private _data?: ZoneMutableParams; @state() private _params?: ZoneDetailDialogParams; @@ -42,13 +27,7 @@ class DialogZoneDetail extends LitElement { this._params = params; this._error = undefined; if (this._params.entry) { - this._name = this._params.entry.name || ""; - this._icon = this._params.entry.icon || ""; - this._latitude = this._params.entry.latitude || this.hass.config.latitude; - this._longitude = - this._params.entry.longitude || this.hass.config.longitude; - this._passive = this._params.entry.passive || false; - this._radius = this._params.entry.radius || 100; + this._data = this._params.entry; } else { const initConfig = getZoneEditorInitData(); let movedHomeLocation; @@ -59,30 +38,34 @@ class DialogZoneDetail extends LitElement { Math.random() * 500 * (Math.random() < 0.5 ? -1 : 1) ); } - this._latitude = initConfig?.latitude || movedHomeLocation[0]; - this._longitude = initConfig?.longitude || movedHomeLocation[1]; - this._name = initConfig?.name || ""; - this._icon = initConfig?.icon || "mdi:map-marker"; - - this._passive = false; - this._radius = 100; + this._data = { + latitude: initConfig?.latitude || movedHomeLocation[0], + longitude: initConfig?.longitude || movedHomeLocation[1], + name: initConfig?.name || "", + icon: initConfig?.icon || "mdi:map-marker", + passive: false, + radius: 100, + }; } } public closeDialog(): void { this._params = undefined; + this._data = undefined; fireEvent(this, "dialog-closed", { dialog: this.localName }); } protected render(): TemplateResult { - if (!this._params) { + if (!this._params || !this._data) { return html``; } - const nameInvalid = this._name.trim() === ""; - const iconInvalid = Boolean(this._icon && !this._icon.trim().includes(":")); - const latInvalid = String(this._latitude) === ""; - const lngInvalid = String(this._longitude) === ""; - const radiusInvalid = String(this._radius) === ""; + const nameInvalid = this._data.name.trim() === ""; + const iconInvalid = Boolean( + this._data.icon && !this._data.icon.trim().includes(":") + ); + const latInvalid = String(this._data.latitude) === ""; + const lngInvalid = String(this._data.longitude) === ""; + const radiusInvalid = String(this._data.radius) === ""; const valid = !nameInvalid && @@ -105,96 +88,15 @@ class DialogZoneDetail extends LitElement { )} >
- ${this._error ? html`
${this._error}
` : ""} -
- - - -
- - -
- -

- ${this.hass!.localize("ui.panel.config.zone.detail.passive_note")} -

- - - -
+
${this._params.entry ? html` @@ -221,74 +123,94 @@ class DialogZoneDetail extends LitElement { `; } - private _location = memoizeOne( - ( - lat: number, - lng: number, - radius: number, - passive: boolean, - icon: string - ): MarkerLocation[] => { - const computedStyles = getComputedStyle(this); - const zoneRadiusColor = computedStyles.getPropertyValue("--accent-color"); - const passiveRadiusColor = computedStyles.getPropertyValue( - "--secondary-text-color" - ); - return [ + private _schema = memoizeOne((icon?: string): HaFormSchema[] => [ + { + name: "name", + required: true, + selector: { + text: {}, + }, + }, + { + name: "icon", + required: false, + selector: { + icon: {}, + }, + }, + { + name: "location", + required: true, + selector: { location: { radius: true, icon } }, + }, + { + name: "", + type: "grid", + schema: [ { - id: "location", - latitude: Number(lat), - longitude: Number(lng), - radius, - radius_color: passive ? passiveRadiusColor : zoneRadiusColor, - icon, - location_editable: true, - radius_editable: true, + name: "latitude", + required: true, + selector: { text: {} }, }, - ]; - } - ); + { + name: "longitude", + required: true, - private _locationChanged(ev: CustomEvent) { - [this._latitude, this._longitude] = ev.detail.location; - } + selector: { text: {} }, + }, + ], + }, + { name: "passive_note", type: "constant" }, + { name: "passive", selector: { boolean: {} } }, + { + name: "radius", + required: false, + selector: { number: { min: 0, max: 999999, mode: "box" } }, + }, + ]); - private _radiusChanged(ev: CustomEvent) { - this._radius = ev.detail.radius; - } - - private _passiveChanged(ev) { - this._passive = ev.target.checked; - } + private _formData = memoizeOne((data: ZoneMutableParams) => ({ + ...data, + location: { + latitude: data.latitude, + longitude: data.longitude, + radius: data.radius, + }, + })); private _valueChanged(ev: CustomEvent) { - const configValue = (ev.target as any).configValue; - this._error = undefined; - this[`_${configValue}`] = ev.detail.value; + const value = ev.detail.value; + if ( + value.location.latitude !== this._data!.latitude || + value.location.longitude !== this._data!.longitude || + value.location.radius !== this._data!.radius + ) { + value.latitude = value.location.latitude; + value.longitude = value.location.longitude; + value.radius = Math.round(value.location.radius); + } + delete value.location; + if (!value.icon) { + delete value.icon; + } + this._data = value; } + private _computeLabel = (entry: HaFormSchema): string => + this.hass.localize(`ui.panel.config.zone.detail.${entry.name}`); + private async _updateEntry() { this._submitting = true; try { - const values: ZoneMutableParams = { - name: this._name.trim(), - latitude: this._latitude, - longitude: this._longitude, - passive: this._passive, - radius: this._radius, - }; - if (this._icon) { - values.icon = this._icon.trim(); - } if (this._params!.entry) { - await this._params!.updateEntry!(values); + await this._params!.updateEntry!(this._data!); } else { - await this._params!.createEntry(values); + await this._params!.createEntry(this._data!); } - this._params = undefined; + this.closeDialog(); } catch (err: any) { - this._error = err ? err.message : "Unknown error"; + this._error = { base: err ? err.message : "Unknown error" }; } finally { this._submitting = false; } @@ -309,24 +231,18 @@ class DialogZoneDetail extends LitElement { return [ haStyleDialog, css` - .location { - display: flex; + ha-dialog { + --mdc-dialog-min-width: 600px; } - .location > * { - flex-grow: 1; - min-width: 0; + @media all and (max-width: 450px), all and (max-height: 500px) { + ha-dialog { + --mdc-dialog-min-width: calc( + 100vw - env(safe-area-inset-right) - env(safe-area-inset-left) + ); + } } - .location > *:first-child { - margin-right: 4px; - } - .location > *:last-child { - margin-left: 4px; - } - ha-locations-editor { - margin-top: 16px; - } - a { - color: var(--primary-color); + ha-form.passive { + --zone-radius-color: var(--secondary-text-color); } `, ]; From 68e7ce188397eb57eac6500881d03707427fa9c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 7 Mar 2022 17:42:49 +0100 Subject: [PATCH 022/142] Add systemd_resolved unsupported reason (#11971) --- src/translations/en.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/translations/en.json b/src/translations/en.json index b97fd52f09..91badbfe5f 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4323,7 +4323,8 @@ "privileged": "Supervisor is not privileged", "software": "Unsupported software detected", "source_mods": "Source modifications", - "systemd": "Systemd" + "systemd": "Systemd", + "systemd_resolved": "Systemd-Resolved" }, "unhealthy_reason": { "privileged": "Supervisor is not privileged", From 86dbf99ebea48ec16f66ddac81db438ecacbffde Mon Sep 17 00:00:00 2001 From: Marius <33354141+Mariusthvdb@users.noreply.github.com> Date: Tue, 8 Mar 2022 17:41:32 +0100 Subject: [PATCH 023/142] replace default switch icon to make it stand out against a power entity which uses the same mdiFlash https://github.com/home-assistant/core/issues/67620#issuecomment-1061949527 suggest the Outline version, so create a subtle difference with the on/off icons. --- src/common/entity/domain_icon.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/entity/domain_icon.ts b/src/common/entity/domain_icon.ts index b64d93f8a0..a5a594621d 100644 --- a/src/common/entity/domain_icon.ts +++ b/src/common/entity/domain_icon.ts @@ -9,7 +9,6 @@ import { mdiCast, mdiCastConnected, mdiClock, - mdiFlash, mdiGestureTapButton, mdiLanConnect, mdiLanDisconnect, @@ -23,6 +22,7 @@ import { mdiRestart, mdiToggleSwitch, mdiToggleSwitchOff, + mdiToggleSwitchOutline, mdiCheckCircleOutline, mdiCloseCircleOutline, mdiWeatherNight, @@ -108,7 +108,7 @@ export const domainIcon = ( case "switch": return compareState === "on" ? mdiToggleSwitch : mdiToggleSwitchOff; default: - return mdiFlash; + return mdiToggleSwitchOutline; } case "sensor": { From fc6b594a2735f84cea107d105f11da4ea6a4bca0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Mar 2022 10:09:45 -0800 Subject: [PATCH 024/142] Allow selecting multiple entities (#11986) --- gallery/src/pages/components/ha-selector.ts | 6 ++ .../ha-selector/ha-selector-entity.ts | 99 +++++++++++++++++-- src/data/selector.ts | 1 + 3 files changed, 97 insertions(+), 9 deletions(-) diff --git a/gallery/src/pages/components/ha-selector.ts b/gallery/src/pages/components/ha-selector.ts index 600176d024..c3855147ed 100644 --- a/gallery/src/pages/components/ha-selector.ts +++ b/gallery/src/pages/components/ha-selector.ts @@ -175,6 +175,12 @@ const SCHEMAS: { }, }, }, + { + name: "Multiples", + input: { + entity: { name: "Entity", selector: { entity: { multiple: true } } }, + }, + }, ]; @customElement("demo-components-ha-selector") diff --git a/src/components/ha-selector/ha-selector-entity.ts b/src/components/ha-selector/ha-selector-entity.ts index 566dfc0a45..36d0ce57ad 100644 --- a/src/components/ha-selector/ha-selector-entity.ts +++ b/src/components/ha-selector/ha-selector-entity.ts @@ -1,6 +1,8 @@ import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; -import { html, LitElement } from "lit"; +import { css, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { repeat } from "lit/directives/repeat"; +import { fireEvent } from "../../common/dom/fire_event"; import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { subscribeEntityRegistry } from "../../data/entity_registry"; import { EntitySelector } from "../../data/selector"; @@ -23,14 +25,45 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) { @property({ type: Boolean }) public disabled = false; protected render() { - return html``; + if (!this.selector.entity.multiple) { + return html``; + } + + // For multiple, the value is a list. + const value = this._normalizedValue as string[]; + + return html` + ${this.label ? html`
${this.label}
` : ""} + ${repeat( + value, + (val) => val, + (entityId, index) => html` + + ` + )} + + `; } public hassSubscribe(): UnsubscribeFunc[] { @@ -48,6 +81,17 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) { ]; } + private get _normalizedValue() { + if (!this.selector.entity.multiple) { + return this.value; + } + + if (!this.value) { + return []; + } + return Array.isArray(this.value) ? this.value : [this.value]; + } + private _filterEntities = (entity: HassEntity): boolean => { if (this.selector.entity?.domain) { const filterDomain = this.selector.entity.domain; @@ -79,6 +123,43 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) { } return true; }; + + // this method is only used when multiple: true + private _valueChanged(ev: any) { + ev.stopPropagation(); + + // undefined = new value + const index = ev.target.index as number | undefined; + // undefined = remove + const newValue = ev.detail.value as string | undefined; + + let updatedValue: string[] | undefined; + + if (index === undefined) { + if (newValue) { + updatedValue = [...this._normalizedValue, newValue]; + } + ev.target.value = ""; + } else if (newValue) { + updatedValue = [...this._normalizedValue]; + updatedValue[index] = newValue; + } else { + updatedValue = this._normalizedValue.filter((_, i) => i !== index); + } + + if (updatedValue) { + fireEvent(this, "value-changed", { + value: updatedValue, + }); + } + } + + static styles = css` + ha-entity-picker + ha-entity-picker { + display: block; + margin-top: 16px; + } + `; } declare global { diff --git a/src/data/selector.ts b/src/data/selector.ts index c8cc87b36e..76f673b125 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -23,6 +23,7 @@ export interface EntitySelector { integration?: string; domain?: string | string[]; device_class?: string; + multiple?: boolean; }; } From db830e9014ed24985407dcfd90844597d81f4d69 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 8 Mar 2022 19:13:08 +0100 Subject: [PATCH 025/142] Fix theme setting (#11977) --- gallery/src/components/demo-cards.ts | 3 +++ hassio/src/hassio-main.ts | 3 ++- src/auth/ha-authorize.ts | 20 +++++++++++++------- src/common/dom/apply_themes_on_element.ts | 9 +++++---- src/fake_data/provide_hass.ts | 4 +++- src/layouts/supervisor-error-screen.ts | 3 ++- src/onboarding/ha-onboarding.ts | 20 +++++++++++++------- src/state/themes-mixin.ts | 23 +++++++++++++++-------- 8 files changed, 56 insertions(+), 29 deletions(-) diff --git a/gallery/src/components/demo-cards.ts b/gallery/src/components/demo-cards.ts index fa91ca2097..d2c485e76e 100644 --- a/gallery/src/components/demo-cards.ts +++ b/gallery/src/components/demo-cards.ts @@ -78,6 +78,9 @@ class DemoCards extends LitElement { ha-formfield { margin-right: 16px; } + #container { + background-color: var(--primary-background-color); + } `; } diff --git a/hassio/src/hassio-main.ts b/hassio/src/hassio-main.ts index b64e505d37..53df932748 100644 --- a/hassio/src/hassio-main.ts +++ b/hassio/src/hassio-main.ts @@ -121,7 +121,8 @@ export class HassioMain extends SupervisorBaseElement { this.parentElement, this.hass.themes, themeName, - themeSettings + themeSettings, + true ); } } diff --git a/src/auth/ha-authorize.ts b/src/auth/ha-authorize.ts index 8f91bfe8bf..add29139bc 100644 --- a/src/auth/ha-authorize.ts +++ b/src/auth/ha-authorize.ts @@ -101,13 +101,19 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) { this._fetchAuthProviders(); if (matchMedia("(prefers-color-scheme: dark)").matches) { - applyThemesOnElement(document.documentElement, { - default_theme: "default", - default_dark_theme: null, - themes: {}, - darkMode: true, - theme: "default", - }); + applyThemesOnElement( + document.documentElement, + { + default_theme: "default", + default_dark_theme: null, + themes: {}, + darkMode: true, + theme: "default", + }, + undefined, + undefined, + true + ); } if (!this.redirectUri) { diff --git a/src/common/dom/apply_themes_on_element.ts b/src/common/dom/apply_themes_on_element.ts index dd5aebc423..ab80caf54b 100644 --- a/src/common/dom/apply_themes_on_element.ts +++ b/src/common/dom/apply_themes_on_element.ts @@ -31,11 +31,12 @@ export const applyThemesOnElement = ( element, themes: HomeAssistant["themes"], selectedTheme?: string, - themeSettings?: Partial + themeSettings?: Partial, + main?: boolean ) => { - // If there is no explicitly desired theme provided, we automatically + // If there is no explicitly desired theme provided, and the element is the main element we automatically // use the active one from `themes`. - const themeToApply = selectedTheme || themes.theme; + const themeToApply = selectedTheme || (main ? themes.theme : undefined); // If there is no explicitly desired dark mode provided, we automatically // use the active one from `themes`. @@ -47,7 +48,7 @@ export const applyThemesOnElement = ( let cacheKey = themeToApply; let themeRules: Partial = {}; - if (darkMode) { + if (themeToApply && darkMode) { cacheKey = `${cacheKey}__dark`; themeRules = { ...darkStyles }; } diff --git a/src/fake_data/provide_hass.ts b/src/fake_data/provide_hass.ts index dc9e875b32..3fd29bcdbf 100644 --- a/src/fake_data/provide_hass.ts +++ b/src/fake_data/provide_hass.ts @@ -300,7 +300,9 @@ export const provideHass = ( applyThemesOnElement( document.documentElement, themes, - selectedTheme!.theme + selectedTheme!.theme, + undefined, + true ); }, diff --git a/src/layouts/supervisor-error-screen.ts b/src/layouts/supervisor-error-screen.ts index cc9a9be2ad..eabb00e439 100644 --- a/src/layouts/supervisor-error-screen.ts +++ b/src/layouts/supervisor-error-screen.ts @@ -101,7 +101,8 @@ class SupervisorErrorScreen extends LitElement { this.parentElement, this.hass.themes, themeName, - themeSettings + themeSettings, + true ); } diff --git a/src/onboarding/ha-onboarding.ts b/src/onboarding/ha-onboarding.ts index eb01f05877..3c05e5b680 100644 --- a/src/onboarding/ha-onboarding.ts +++ b/src/onboarding/ha-onboarding.ts @@ -133,13 +133,19 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) { import("./particles"); } if (matchMedia("(prefers-color-scheme: dark)").matches) { - applyThemesOnElement(document.documentElement, { - default_theme: "default", - default_dark_theme: null, - themes: {}, - darkMode: true, - theme: "default", - }); + applyThemesOnElement( + document.documentElement, + { + default_theme: "default", + default_dark_theme: null, + themes: {}, + darkMode: true, + theme: "default", + }, + undefined, + undefined, + true + ); } } diff --git a/src/state/themes-mixin.ts b/src/state/themes-mixin.ts index 3595a54a59..d56443be0f 100644 --- a/src/state/themes-mixin.ts +++ b/src/state/themes-mixin.ts @@ -38,13 +38,19 @@ export default >(superClass: T) => }); mql.addListener((ev) => this._applyTheme(ev.matches)); if (!this._themeApplied && mql.matches) { - applyThemesOnElement(document.documentElement, { - default_theme: "default", - default_dark_theme: null, - themes: {}, - darkMode: true, - theme: "default", - }); + applyThemesOnElement( + document.documentElement, + { + default_theme: "default", + default_dark_theme: null, + themes: {}, + darkMode: true, + theme: "default", + }, + undefined, + undefined, + true + ); } } @@ -93,7 +99,8 @@ export default >(superClass: T) => document.documentElement, this.hass.themes, themeName, - themeSettings + themeSettings, + true ); if (darkMode !== this.hass.themes.darkMode) { From d968fe41ee71629fdceb18b966bb223aa9546569 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 8 Mar 2022 12:19:18 -0600 Subject: [PATCH 026/142] Update Style of Design Page (#11982) --- gallery/src/components/page-description.ts | 22 ++++- gallery/src/ha-gallery.ts | 94 +++++++++++-------- .../src/pages/components/ha-alert.markdown | 1 + 3 files changed, 76 insertions(+), 41 deletions(-) diff --git a/gallery/src/components/page-description.ts b/gallery/src/components/page-description.ts index 8e449195a6..e9680e59b3 100644 --- a/gallery/src/components/page-description.ts +++ b/gallery/src/components/page-description.ts @@ -12,7 +12,14 @@ class PageDescription extends HaMarkdown { if (!PAGES[this.page].description) { return html``; } + return html` +
+
+ ${PAGES[this.page].metadata.title || this.page.split("/")[1]} +
+
${PAGES[this.page].metadata.subtitle}
+
${until( PAGES[this.page] .description() @@ -25,9 +32,22 @@ class PageDescription extends HaMarkdown { static styles = [ HaMarkdown.styles, css` + .heading { + padding: 16px; + border-bottom: 1px solid var(--secondary-background-color); + } + .title { + font-size: 42px; + line-height: 56px; + padding-bottom: 8px; + } + .subtitle { + font-size: 18px; + line-height: 24px; + } .root { max-width: 800px; - margin: 0 auto; + margin: 16px auto; } .root > *:first-child { margin-top: 0; diff --git a/gallery/src/ha-gallery.ts b/gallery/src/ha-gallery.ts index 86e64e8d35..e602a1916d 100644 --- a/gallery/src/ha-gallery.ts +++ b/gallery/src/ha-gallery.ts @@ -5,6 +5,7 @@ import { html, css, LitElement, PropertyValues } from "lit"; import { customElement, property, query } from "lit/decorators"; import "../../src/components/ha-icon-button"; import "../../src/managers/notification-manager"; +import "../../src/components/ha-expansion-panel"; import { haStyle } from "../../src/resources/styles"; import { PAGES, SIDEBAR } from "../build/import-pages"; import { dynamicElement } from "../../src/common/dom/dynamic-element-directive"; @@ -53,10 +54,9 @@ class HaGallery extends LitElement { sidebar.push( group.header ? html` -
- ${group.header} + ${links} -
+ ` : links ); @@ -92,27 +92,34 @@ class HaGallery extends LitElement { ${dynamicElement(`demo-${this._page.replace("/", "-")}`)}
@@ -186,27 +193,16 @@ class HaGallery extends LitElement { padding: 4px; } - .sidebar details { - margin-top: 1em; - margin-left: 1em; - } - - .sidebar summary { - cursor: pointer; - font-weight: bold; - margin-bottom: 8px; - } - .sidebar a { color: var(--primary-text-color); display: block; - padding: 4px 12px; + padding: 12px; text-decoration: none; position: relative; } .sidebar a[active]::before { - border-radius: 4px; + border-radius: 12px; position: absolute; top: 0; right: 2px; @@ -237,14 +233,32 @@ class HaGallery extends LitElement { .page-footer { text-align: center; - margin: 16px 0; - padding-top: 16px; - border-top: 1px solid rgba(0, 0, 0, 0.12); + margin: 16px; + padding: 16px; + border-radius: 12px; + background-color: var(--primary-background-color); + } + + .page-footer div { + margin-top: 4px; + } + + .page-footer .header { + font-size: 16px; + font-weight: 500; + line-height: 28px; + text-align: center; + } + + .page-footer .secondary { + line-height: 23px; + text-align: center; } .page-footer a { display: inline-block; margin: 0 8px; + text-decoration: none; } `, ]; diff --git a/gallery/src/pages/components/ha-alert.markdown b/gallery/src/pages/components/ha-alert.markdown index 2488b83090..e37a205481 100644 --- a/gallery/src/pages/components/ha-alert.markdown +++ b/gallery/src/pages/components/ha-alert.markdown @@ -1,5 +1,6 @@ --- title: Alerts +subtitle: An alert displays a short, important message in a way that attracts the user's attention without interrupting the user's task. --- # Alert `` From 8301ae262cdbb1402a123d527fc6aeef422ef6a6 Mon Sep 17 00:00:00 2001 From: Marius <33354141+Mariusthvdb@users.noreply.github.com> Date: Tue, 8 Mar 2022 21:40:42 +0100 Subject: [PATCH 027/142] change icon to mimic physical device and follow comments --- src/common/entity/domain_icon.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/entity/domain_icon.ts b/src/common/entity/domain_icon.ts index a5a594621d..40b0106216 100644 --- a/src/common/entity/domain_icon.ts +++ b/src/common/entity/domain_icon.ts @@ -12,6 +12,7 @@ import { mdiGestureTapButton, mdiLanConnect, mdiLanDisconnect, + mdiLightSwitch, mdiLock, mdiLockAlert, mdiLockClock, @@ -22,7 +23,6 @@ import { mdiRestart, mdiToggleSwitch, mdiToggleSwitchOff, - mdiToggleSwitchOutline, mdiCheckCircleOutline, mdiCloseCircleOutline, mdiWeatherNight, @@ -108,7 +108,7 @@ export const domainIcon = ( case "switch": return compareState === "on" ? mdiToggleSwitch : mdiToggleSwitchOff; default: - return mdiToggleSwitchOutline; + return mdiLightSwitch; } case "sensor": { From 9f1e9b43fef706718628efd5acfca89e1c5c96c5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Mar 2022 19:33:23 -0800 Subject: [PATCH 028/142] Use entities-picker in entity selector (#11990) --- gallery/src/pages/components/ha-form.ts | 102 +++++++++++++++++- src/components/entity/ha-entities-picker.ts | 6 +- .../ha-selector/ha-selector-entity.ts | 81 ++------------ 3 files changed, 111 insertions(+), 78 deletions(-) diff --git a/gallery/src/pages/components/ha-form.ts b/gallery/src/pages/components/ha-form.ts index 0722040cc0..cab77bacfc 100644 --- a/gallery/src/pages/components/ha-form.ts +++ b/gallery/src/pages/components/ha-form.ts @@ -12,6 +12,98 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry"; import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor"; import { provideHass } from "../../../../src/fake_data/provide_hass"; import { HomeAssistant } from "../../../../src/types"; +import { getEntity } from "../../../../src/fake_data/entity"; + +const ENTITIES = [ + getEntity("alarm_control_panel", "alarm", "disarmed", { + friendly_name: "Alarm", + }), + getEntity("media_player", "livingroom", "playing", { + friendly_name: "Livingroom", + }), + getEntity("media_player", "lounge", "idle", { + friendly_name: "Lounge", + supported_features: 444983, + }), + getEntity("light", "bedroom", "on", { + friendly_name: "Bedroom", + }), + getEntity("switch", "coffee", "off", { + friendly_name: "Coffee", + }), +]; + +const DEVICES = [ + { + area_id: "bedroom", + configuration_url: null, + config_entries: ["config_entry_1"], + connections: [], + disabled_by: null, + entry_type: null, + id: "device_1", + identifiers: [["demo", "volume1"] as [string, string]], + manufacturer: null, + model: null, + name_by_user: null, + name: "Dishwasher", + sw_version: null, + hw_version: null, + via_device_id: null, + }, + { + area_id: "backyard", + configuration_url: null, + config_entries: ["config_entry_2"], + connections: [], + disabled_by: null, + entry_type: null, + id: "device_2", + identifiers: [["demo", "pwm1"] as [string, string]], + manufacturer: null, + model: null, + name_by_user: null, + name: "Lamp", + sw_version: null, + hw_version: null, + via_device_id: null, + }, + { + area_id: null, + configuration_url: null, + config_entries: ["config_entry_3"], + connections: [], + disabled_by: null, + entry_type: null, + id: "device_3", + identifiers: [["demo", "pwm1"] as [string, string]], + manufacturer: null, + model: null, + name_by_user: "User name", + name: "Technical name", + sw_version: null, + hw_version: null, + via_device_id: null, + }, +]; + +const AREAS = [ + { + area_id: "backyard", + name: "Backyard", + picture: null, + }, + { + area_id: "bedroom", + name: "Bedroom", + picture: null, + }, + { + area_id: "livingroom", + name: "Livingroom", + picture: null, + }, +]; const SCHEMAS: { title: string; @@ -39,6 +131,7 @@ const SCHEMAS: { icon: "Icon", media: "Media", location: "Location", + entities: "Entities", }, schema: [ { name: "addon", selector: { addon: {} } }, @@ -80,6 +173,10 @@ const SCHEMAS: { name: "location", selector: { location: { radius: true, icon: "mdi:home" } }, }, + { + name: "entities", + selector: { entity: { multiple: true } }, + }, ], }, { @@ -320,9 +417,10 @@ class DemoHaForm extends LitElement { const hass = provideHass(this); hass.updateTranslations(null, "en"); hass.updateTranslations("config", "en"); + hass.addEntities(ENTITIES); mockEntityRegistry(hass); - mockDeviceRegistry(hass); - mockAreaRegistry(hass); + mockDeviceRegistry(hass, DEVICES); + mockAreaRegistry(hass, AREAS); mockHassioSupervisor(hass); } diff --git a/src/components/entity/ha-entities-picker.ts b/src/components/entity/ha-entities-picker.ts index 35e21dc950..062b79eced 100644 --- a/src/components/entity/ha-entities-picker.ts +++ b/src/components/entity/ha-entities-picker.ts @@ -51,6 +51,8 @@ class HaEntitiesPickerLight extends LitElement { @property({ attribute: "pick-entity-label" }) public pickEntityLabel?: string; + @property() public entityFilter?: HaEntityPickerEntityFilterFunc; + protected render(): TemplateResult { if (!this.hass) { return html``; @@ -94,7 +96,9 @@ class HaEntitiesPickerLight extends LitElement { private _entityFilter: HaEntityPickerEntityFilterFunc = ( stateObj: HassEntity - ) => !this.value || !this.value.includes(stateObj.entity_id); + ) => + (!this.value || !this.value.includes(stateObj.entity_id)) && + (!this.entityFilter || this.entityFilter(stateObj)); private get _currentEntities() { return this.value || []; diff --git a/src/components/ha-selector/ha-selector-entity.ts b/src/components/ha-selector/ha-selector-entity.ts index 36d0ce57ad..cbc5366953 100644 --- a/src/components/ha-selector/ha-selector-entity.ts +++ b/src/components/ha-selector/ha-selector-entity.ts @@ -1,14 +1,13 @@ import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; -import { css, html, LitElement } from "lit"; +import { html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { repeat } from "lit/directives/repeat"; -import { fireEvent } from "../../common/dom/fire_event"; import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { subscribeEntityRegistry } from "../../data/entity_registry"; import { EntitySelector } from "../../data/selector"; import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../types"; import "../entity/ha-entity-picker"; +import "../entity/ha-entities-picker"; @customElement("ha-selector-entity") export class HaEntitySelector extends SubscribeMixin(LitElement) { @@ -36,33 +35,13 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) { >`; } - // For multiple, the value is a list. - const value = this._normalizedValue as string[]; - return html` - ${this.label ? html`
${this.label}
` : ""} - ${repeat( - value, - (val) => val, - (entityId, index) => html` - - ` - )} - ${this.label}` : ""} + + > `; } @@ -81,17 +60,6 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) { ]; } - private get _normalizedValue() { - if (!this.selector.entity.multiple) { - return this.value; - } - - if (!this.value) { - return []; - } - return Array.isArray(this.value) ? this.value : [this.value]; - } - private _filterEntities = (entity: HassEntity): boolean => { if (this.selector.entity?.domain) { const filterDomain = this.selector.entity.domain; @@ -123,43 +91,6 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) { } return true; }; - - // this method is only used when multiple: true - private _valueChanged(ev: any) { - ev.stopPropagation(); - - // undefined = new value - const index = ev.target.index as number | undefined; - // undefined = remove - const newValue = ev.detail.value as string | undefined; - - let updatedValue: string[] | undefined; - - if (index === undefined) { - if (newValue) { - updatedValue = [...this._normalizedValue, newValue]; - } - ev.target.value = ""; - } else if (newValue) { - updatedValue = [...this._normalizedValue]; - updatedValue[index] = newValue; - } else { - updatedValue = this._normalizedValue.filter((_, i) => i !== index); - } - - if (updatedValue) { - fireEvent(this, "value-changed", { - value: updatedValue, - }); - } - } - - static styles = css` - ha-entity-picker + ha-entity-picker { - display: block; - margin-top: 16px; - } - `; } declare global { From 246e426182eed40b20f003ba21183c6ad601ef54 Mon Sep 17 00:00:00 2001 From: Emil Stjerneman Date: Thu, 10 Mar 2022 02:54:40 +0100 Subject: [PATCH 029/142] #11971 Change order of alarm panel buttons (#11998) --- src/dialogs/more-info/controls/more-info-alarm_control_panel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dialogs/more-info/controls/more-info-alarm_control_panel.ts b/src/dialogs/more-info/controls/more-info-alarm_control_panel.ts index 1efb0094f7..de41f1c173 100644 --- a/src/dialogs/more-info/controls/more-info-alarm_control_panel.ts +++ b/src/dialogs/more-info/controls/more-info-alarm_control_panel.ts @@ -12,7 +12,7 @@ import { import type { HomeAssistant } from "../../../types"; const BUTTONS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "", "0", "clear"]; -const ARM_ACTIONS = ["arm_away", "arm_home"]; +const ARM_ACTIONS = ["arm_home", "arm_away"]; const DISARM_ACTIONS = ["disarm"]; @customElement("more-info-alarm_control_panel") From 99fd3a1b6f9e9420ca79dad0fd46e61d0f9d85f6 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 10 Mar 2022 08:45:12 -0500 Subject: [PATCH 030/142] Fix zwave_js 'add/remove device' disabled bug (#12000) * Fix zwave_js 'add/remove device' disabled bug * revert extra change --- .../zwave_js/zwave_js-config-dashboard.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts index ac4f75522f..3c75b59df0 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts @@ -237,8 +237,10 @@ class ZWaveJSConfigDashboard extends LitElement { ${this.hass.localize( "ui.panel.config.zwave_js.common.remove_node" @@ -304,7 +306,9 @@ class ZWaveJSConfigDashboard extends LitElement { ?rtl=${computeRTL(this.hass)} @click=${this._addNodeClicked} .disabled=${this._status !== "connected" || - this._network?.controller.inclusion_state !== InclusionState.Idle} + (this._network?.controller.inclusion_state !== InclusionState.Idle && + this._network?.controller.inclusion_state !== + InclusionState.SmartStart)} > From f89b8cffcf7f32032e72805727b572737e8d8798 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Thu, 10 Mar 2022 10:21:04 -0500 Subject: [PATCH 031/142] Fix zwave_js set config dropdown default value (#11974) Co-authored-by: Paulus Schoutsen --- .../integration-panels/zwave_js/zwave_js-node-config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-node-config.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-node-config.ts index 5426b494a3..70e9d26390 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-node-config.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-node-config.ts @@ -289,7 +289,7 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
Date: Fri, 11 Mar 2022 11:37:22 -0600 Subject: [PATCH 032/142] Fix changing cost number in energy settings (#12009) --- .../config/energy/dialogs/dialog-energy-grid-flow-settings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts index 2599f61ee3..c018640263 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts @@ -240,7 +240,7 @@ export class DialogEnergyGridFlowSettings this._costStat = null; this._source = { ...this._source!, - number_energy_price: Number(ev.detail.value), + number_energy_price: Number((ev.target as any).value), entity_energy_price: null, }; } From f4e28da0a3d603d7bb1a4b0c05e8e656f2591eda Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Fri, 11 Mar 2022 11:38:18 -0600 Subject: [PATCH 033/142] Fix Dashboard Editing (#12011) --- .../dashboards/dialog-lovelace-dashboard-detail.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts b/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts index 75db9e031c..f48ef1f439 100644 --- a/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts +++ b/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts @@ -11,6 +11,7 @@ import { CoreFrontendUserData } from "../../../../data/frontend"; import { LovelaceDashboard, LovelaceDashboardCreateParams, + LovelaceDashboardMutableParams, } from "../../../../data/lovelace"; import { DEFAULT_PANEL, setDefaultPanel } from "../../../../data/panel"; import { haStyleDialog } from "../../../../resources/styles"; @@ -40,7 +41,7 @@ export class DialogLovelaceDashboardDetail extends LitElement { } else { this._data = { show_in_sidebar: true, - icon: "", + icon: undefined, title: "", require_admin: false, mode: "storage", @@ -264,7 +265,13 @@ export class DialogLovelaceDashboardDetail extends LitElement { this._submitting = true; try { if (this._params!.dashboard) { - await this._params!.updateDashboard(this._data as LovelaceDashboard); + const values: Partial = { + require_admin: this._data!.require_admin, + show_in_sidebar: this._data!.show_in_sidebar, + icon: this._data!.icon || undefined, + title: this._data!.title, + }; + await this._params!.updateDashboard(values); } else { await this._params!.createDashboard( this._data as LovelaceDashboardCreateParams From 9b6c935ffb19fa15d6e92ed5e2c0c9d7b750ff8e Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Fri, 11 Mar 2022 11:39:04 -0600 Subject: [PATCH 034/142] Fix For Selecting Device Class (#12010) --- src/panels/config/entities/entity-registry-settings.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index 4c59b0d99e..72854d8389 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -11,6 +11,7 @@ import { } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; +import { stopPropagation } from "../../../common/dom/stop_propagation"; import { computeDomain } from "../../../common/entity/compute_domain"; import { domainIcon } from "../../../common/entity/domain_icon"; import "../../../components/ha-alert"; @@ -166,7 +167,10 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { "ui.dialogs.entity_registry.editor.device_class" )} .value=${this._deviceClass} + naturalMenuWidth + fixedMenuPosition @selected=${this._deviceClassChanged} + @closed=${stopPropagation} > ${OVERRIDE_DEVICE_CLASSES[domain].map( (deviceClass: string) => html` @@ -422,6 +426,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { } ha-select { width: 100%; + margin: 8px 0; } ha-switch { margin-right: 16px; From 5dfe17a43a27cb0d41d1281546650349add71e8e Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Fri, 11 Mar 2022 19:07:56 -0600 Subject: [PATCH 035/142] Fix: Allow for deleting Input_select options (#12007) --- src/panels/config/helpers/forms/ha-input_select-form.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panels/config/helpers/forms/ha-input_select-form.ts b/src/panels/config/helpers/forms/ha-input_select-form.ts index cec1eb952e..c7abc8e176 100644 --- a/src/panels/config/helpers/forms/ha-input_select-form.ts +++ b/src/panels/config/helpers/forms/ha-input_select-form.ts @@ -85,7 +85,7 @@ class HaInputSelectForm extends LitElement { ${this._options.length ? this._options.map( (option, index) => html` - + ${option} Date: Fri, 11 Mar 2022 23:25:09 -0600 Subject: [PATCH 036/142] Script ID update with Alias (#12008) --- src/panels/config/script/ha-script-editor.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index 2aeb262d47..611e52abec 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -569,9 +569,13 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { } private _aliasChanged(alias: string) { - if (this.scriptEntityId || this._entityId) { + if ( + this.scriptEntityId || + (this._entityId && this._entityId !== slugify(this._config!.alias)) + ) { return; } + const aliasSlugify = slugify(alias); let id = aliasSlugify; let i = 2; @@ -595,6 +599,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { private _valueChanged(ev: CustomEvent) { ev.stopPropagation(); const values = ev.detail.value as any; + const currentId = this._entityId; for (const key of Object.keys(values)) { if (key === "sequence") { @@ -603,7 +608,10 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { const value = values[key]; - if (value === this._config![key]) { + if ( + value === this._config![key] || + (key === "id" && currentId === value) + ) { continue; } From fad8a27232760988a62dcc6a62b9fdb4823f5a5e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 12 Mar 2022 11:56:25 -0800 Subject: [PATCH 037/142] HAWS 6.1 (#12016) --- package.json | 2 +- src/components/entity/ha-entity-picker.ts | 31 ++++++++++--------- .../media-browser/browser-media-player.ts | 2 +- yarn.lock | 10 +++--- 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 24c7cccf92..0725b0af60 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "fuse.js": "^6.0.0", "google-timezones-json": "^1.0.2", "hls.js": "^1.1.5", - "home-assistant-js-websocket": "^6.0.1", + "home-assistant-js-websocket": "^6.1.1", "idb-keyval": "^5.1.3", "intl-messageformat": "^9.9.1", "js-yaml": "^4.1.0", diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts index d24f6fbac4..a8d17084c6 100644 --- a/src/components/entity/ha-entity-picker.ts +++ b/src/components/entity/ha-entity-picker.ts @@ -15,18 +15,21 @@ import "../ha-icon-button"; import "../ha-svg-icon"; import "./state-badge"; +interface HassEntityWithCachedName extends HassEntity { + friendly_name: string; +} + export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; // eslint-disable-next-line lit/prefer-static-styles -const rowRenderer: ComboBoxLitRenderer = - (item) => - html` - ${item.state - ? html`` - : ""} - ${item.friendly_name} - ${item.entity_id} - `; +const rowRenderer: ComboBoxLitRenderer = (item) => + html` + ${item.state + ? html`` + : ""} + ${item.friendly_name} + ${item.entity_id} + `; @customElement("ha-entity-picker") export class HaEntityPicker extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -96,7 +99,7 @@ export class HaEntityPicker extends LitElement { private _initedStates = false; - private _states: HassEntity[] = []; + private _states: HassEntityWithCachedName[] = []; private _getStates = memoizeOne( ( @@ -107,8 +110,8 @@ export class HaEntityPicker extends LitElement { entityFilter: this["entityFilter"], includeDeviceClasses: this["includeDeviceClasses"], includeUnitOfMeasurement: this["includeUnitOfMeasurement"] - ) => { - let states: HassEntity[] = []; + ): HassEntityWithCachedName[] => { + let states: HassEntityWithCachedName[] = []; if (!hass) { return []; @@ -122,7 +125,7 @@ export class HaEntityPicker extends LitElement { state: "", last_changed: "", last_updated: "", - context: { id: "", user_id: null }, + context: { id: "", user_id: null, parent_id: null }, friendly_name: this.hass!.localize( "ui.components.entity.entity-picker.no_entities" ), @@ -190,7 +193,7 @@ export class HaEntityPicker extends LitElement { state: "", last_changed: "", last_updated: "", - context: { id: "", user_id: null }, + context: { id: "", user_id: null, parent_id: null }, friendly_name: this.hass!.localize( "ui.components.entity.entity-picker.no_match" ), diff --git a/src/panels/media-browser/browser-media-player.ts b/src/panels/media-browser/browser-media-player.ts index b4096f9a7f..c4c4bddc22 100644 --- a/src/panels/media-browser/browser-media-player.ts +++ b/src/panels/media-browser/browser-media-player.ts @@ -84,7 +84,7 @@ export class BrowserMediaPlayer { last_changed: now, last_updated: now, attributes: {}, - context: { id: "", user_id: null }, + context: { id: "", user_id: null, parent_id: null }, }; } diff --git a/yarn.lock b/yarn.lock index d20b3377e8..ab97532484 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9128,7 +9128,7 @@ fsevents@^1.2.7: gulp-rename: ^2.0.0 gulp-zopfli-green: ^3.0.1 hls.js: ^1.1.5 - home-assistant-js-websocket: ^6.0.1 + home-assistant-js-websocket: ^6.1.1 html-minifier: ^4.0.0 husky: ^1.3.1 idb-keyval: ^5.1.3 @@ -9198,10 +9198,10 @@ fsevents@^1.2.7: languageName: unknown linkType: soft -"home-assistant-js-websocket@npm:^6.0.1": - version: 6.0.1 - resolution: "home-assistant-js-websocket@npm:6.0.1" - checksum: 566d6de6a4eb0e05ca434a45433cfe6fdd6b5cb2008e9a165709e08335df1c9b70903564c479ab8d48c6f5468a9784f47697192f9023170d2d86d43a461d6126 +"home-assistant-js-websocket@npm:^6.1.1": + version: 6.1.1 + resolution: "home-assistant-js-websocket@npm:6.1.1" + checksum: b46dd44ac1b393ecfdda152743fe44dcd6534463d1df631c448a71e71166c04d4fea2a0827dbb2e55c85fcd8d9ccdea4f168713bf18e832c7f688b9b2fdc8d72 languageName: node linkType: hard From 4916527e5fd4e3bc583ca0dbcec1f3a062d93e48 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 12 Mar 2022 13:42:42 -0800 Subject: [PATCH 038/142] Bumped version to 20220301.1 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 13b918a742..3b273584c0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = home-assistant-frontend -version = 20220301.0 +version = 20220301.1 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From c4dc6bfb0d633932d95afa309e8d9ccf0771c3c8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 12 Mar 2022 13:45:34 -0800 Subject: [PATCH 039/142] Bumped version to 20220301.2 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 3b273584c0..a73c6937e0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = home-assistant-frontend -version = 20220301.1 +version = 20220301.2 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From cc27ddb3628577b8d0476625c3aa7e276bd665a4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 12 Mar 2022 13:47:05 -0800 Subject: [PATCH 040/142] Bumped version to 20220312.0 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index a73c6937e0..d8c03a5e88 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = home-assistant-frontend -version = 20220301.2 +version = 20220312.0 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 84ffa2369aca55ff4046b498beb9ee2444118105 Mon Sep 17 00:00:00 2001 From: jpearl Date: Mon, 14 Mar 2022 11:19:43 -0400 Subject: [PATCH 041/142] Add shade to device class overrides (#11874) --- src/panels/config/entities/entity-registry-settings.ts | 2 +- src/translations/en.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index 72854d8389..04bbefa81a 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -43,7 +43,7 @@ import type { HomeAssistant } from "../../../types"; import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail"; const OVERRIDE_DEVICE_CLASSES = { - cover: ["window", "door", "garage", "gate"], + cover: ["window", "door", "garage", "gate", "shade"], binary_sensor: ["window", "door", "garage_door", "opening"], }; diff --git a/src/translations/en.json b/src/translations/en.json index 91badbfe5f..9f1ba66f0c 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -773,7 +773,8 @@ "door": "Door", "garage": "Garage door", "gate": "Gate", - "window": "Window" + "window": "Window", + "shade": "Shade" } }, "unavailable": "This entity is unavailable.", From 1fa04baa1688b85c1c9b5a0db85cc255b7e13578 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Mon, 14 Mar 2022 10:33:49 -0500 Subject: [PATCH 042/142] Fix: Changing Blueprint Automation Name (#12036) --- src/panels/config/automation/blueprint-automation-editor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panels/config/automation/blueprint-automation-editor.ts b/src/panels/config/automation/blueprint-automation-editor.ts index e134f3dacd..907ee3bb03 100644 --- a/src/panels/config/automation/blueprint-automation-editor.ts +++ b/src/panels/config/automation/blueprint-automation-editor.ts @@ -278,7 +278,7 @@ export class HaBlueprintAutomationEditor extends LitElement { if (!name) { return; } - const newVal = ev.detail.value; + const newVal = target.value; if ((this.config![name] || "") === newVal) { return; } From dcf50e055b21f59e86bad5e473305ed0d75048fb Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Mon, 14 Mar 2022 11:11:46 -0500 Subject: [PATCH 043/142] Fix @changed where using ev.detail (#12043) --- src/panels/config/users/dialog-user-detail.ts | 5 ++--- .../editor/config-elements/hui-picture-card-editor.ts | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/panels/config/users/dialog-user-detail.ts b/src/panels/config/users/dialog-user-detail.ts index 9729262167..490839b3cd 100644 --- a/src/panels/config/users/dialog-user-detail.ts +++ b/src/panels/config/users/dialog-user-detail.ts @@ -22,7 +22,6 @@ import { showAlertDialog, showPromptDialog, } from "../../../dialogs/generic/show-dialog-box"; -import { PolymerChangedEvent } from "../../../polymer-types"; import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import { UserDetailDialogParams } from "./show-dialog-user-detail"; @@ -212,9 +211,9 @@ class DialogUserDetail extends LitElement { `; } - private _nameChanged(ev: PolymerChangedEvent) { + private _nameChanged(ev) { this._error = undefined; - this._name = ev.detail.value; + this._name = ev.target.value; } private _adminChanged(ev): void { diff --git a/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts index 9b1f46f96b..0df731ba97 100644 --- a/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts @@ -113,9 +113,9 @@ export class HuiPictureCardEditor return; } const target = ev.target! as EditorTarget; - const value = ev.detail.value; + const value = ev.detail?.value ?? target.value; - if (this[`_${target.configValue}`] === target.value) { + if (this[`_${target.configValue}`] === value) { return; } if (target.configValue) { From 3358fc2b185fc0985728d978a02853129cf6eaf5 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Mon, 14 Mar 2022 12:45:12 -0500 Subject: [PATCH 044/142] Add all cover device classes (#12042) --- .../config/entities/entity-registry-settings.ts | 13 ++++++++++++- src/translations/en.json | 7 ++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index 04bbefa81a..542a730d32 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -43,7 +43,18 @@ import type { HomeAssistant } from "../../../types"; import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail"; const OVERRIDE_DEVICE_CLASSES = { - cover: ["window", "door", "garage", "gate", "shade"], + cover: [ + "awning", + "blind", + "curtain", + "damper", + "door", + "garage", + "gate", + "shade", + "shutter", + "window", + ], binary_sensor: ["window", "door", "garage_door", "opening"], }; diff --git a/src/translations/en.json b/src/translations/en.json index 9f1ba66f0c..4eaa79d614 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -774,7 +774,12 @@ "garage": "Garage door", "gate": "Gate", "window": "Window", - "shade": "Shade" + "shade": "Shade", + "awning": "Awning", + "blind": "Blind", + "curtain": "Curtain", + "damper": "Damper", + "shutter": "Shutter" } }, "unavailable": "This entity is unavailable.", From 641003bb2ad28b4caff55d463ef60d5044ab1232 Mon Sep 17 00:00:00 2001 From: Matthias de Baat Date: Mon, 14 Mar 2022 19:05:44 +0100 Subject: [PATCH 045/142] Rename Lovelace Dashboard to just Dashboard (#12044) Co-authored-by: Zack Barett --- src/translations/en.json | 42 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/translations/en.json b/src/translations/en.json index 4eaa79d614..19be33d2e5 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1414,7 +1414,7 @@ "error_from_custom_integration": "This error originated from a custom integration." }, "lovelace": { - "caption": "Lovelace Dashboards", + "caption": "Dashboards", "description": "Create customized sets of cards to control your home", "dashboards": { "default_dashboard": "This is the default dashboard", @@ -1440,7 +1440,7 @@ "confirm_delete_title": "Delete {dashboard_title}?", "confirm_delete_text": "Your dashboard will be permanently deleted.", "cant_edit_yaml": "Dashboards created in YAML cannot be edited from the UI. Change them in configuration.yaml.", - "cant_edit_default": "The default Lovelace dashboard, Overview, cannot be edited from the UI. You can hide it by setting another dashboard as default.", + "cant_edit_default": "The default dashboard, Overview, cannot be edited from the UI. You can hide it by setting another dashboard as default.", "detail": { "edit_dashboard": "Edit dashboard", "new_dashboard": "Add new dashboard", @@ -1478,7 +1478,7 @@ "confirm_delete": "Are you sure you want to delete this resource?", "refresh_header": "Do you want to refresh?", "refresh_body": "You have to refresh the page to complete the removal. Do you want to refresh now?", - "cant_edit_yaml": "You are using Lovelace in YAML mode, therefore you cannot manage your resources through the UI. Manage them in configuration.yaml.", + "cant_edit_yaml": "You are using your dashboard in YAML mode, therefore you cannot manage your resources through the UI. Manage them in configuration.yaml.", "detail": { "new_resource": "Add new resource", "dismiss": "Close", @@ -2362,7 +2362,7 @@ "sensor": "Sensors", "diagnostic": "Diagnostic", "config": "Configuration", - "add_entities_lovelace": "Add to Lovelace", + "add_entities_lovelace": "Add to dashboard", "none": "This device has no entities", "hide_disabled": "Hide disabled", "disabled_entities": "+{count} {count, plural,\n one {disabled entity}\n other {disabled entities}\n}" @@ -2442,7 +2442,7 @@ "button": "Remove selected", "confirm_title": "Do you want to remove {number} {number, plural,\n one {entity}\n other {entities}\n}?", "confirm_partly_title": "Only {number} {number, plural,\n one {selected entity}\n other {selected entities}\n} can be removed.", - "confirm_text": "You should remove them from your Lovelace config and automations if they contain these entities.", + "confirm_text": "You should remove them from your dashboard config and automations if they contain these entities.", "confirm_partly_text": "You can only remove {removable} of the selected {selected} entities. Entities can only be removed when the integration is no longer providing the entities. Sometimes you have to restart Home Assistant before you can remove the entities of a removed integration. Are you sure you want to remove the removable entities?" } } @@ -3083,7 +3083,7 @@ }, "unused_entities": { "title": "Unused entities", - "available_entities": "These are the entities that you have available, but are not in your Lovelace UI yet.", + "available_entities": "These are the entities that you have available, but are not in your dashboard yet.", "select_to_add": "Select the entities you want to add to a card and then click the add card button.", "state_icon": "State", "entity": "Entity", @@ -3094,9 +3094,9 @@ "no_data": "No unused entities found" }, "add_entities": { - "yaml_unsupported": "You cannot use this function when using Lovelace UI in YAML mode.", - "generated_unsupported": "You can only use this function when you have taken control of the Lovelace UI.", - "saving_failed": "Saving Lovelace UI configuration failed." + "yaml_unsupported": "You cannot use this function when using your dashboard in YAML mode.", + "generated_unsupported": "You can only use this function when you have taken control of your dashboard.", + "saving_failed": "Saving dashboard configuration failed." }, "views": { "confirm_delete": "Delete view?", @@ -3119,7 +3119,7 @@ "editor": { "header": "Edit UI", "menu": { - "open": "Open Lovelace UI menu", + "open": "Open dashboard menu", "raw_editor": "Raw configuration editor", "manage_dashboards": "Manage dashboards", "manage_resources": "Manage resources" @@ -3136,20 +3136,20 @@ "unsaved_changes": "Unsaved changes", "saved": "Saved", "reload": "Reload", - "lovelace_changed": "The Lovelace config was updated, do you want to load the updated config in the editor and lose your current changes?", - "confirm_remove_config_title": "Are you sure you want to remove your Lovelace UI configuration?", - "confirm_remove_config_text": "We will automatically generate your Lovelace UI views with your areas and devices if you remove your Lovelace UI configuration.", + "lovelace_changed": "Your dashboard was updated, do you want to load the updated config in the editor and lose your current changes?", + "confirm_remove_config_title": "Are you sure you want to remove your dashboard configuration?", + "confirm_remove_config_text": "We will automatically generate your dashboard views with your areas and devices if you remove your dashboard configuration.", "confirm_unsaved_changes": "You have unsaved changes, are you sure you want to exit?", "confirm_unsaved_comments": "Your configuration might contains comment(s), these will not be saved. Do you want to continue?", "error_parse_yaml": "Unable to parse YAML: {error}", "error_invalid_config": "Your configuration is not valid: {error}", "error_save_yaml": "Unable to save YAML: {error}", "error_remove": "Unable to remove configuration: {error}", - "resources_moved": "Resources should no longer be added to the Lovelace configuration but can be added in the Lovelace config panel." + "resources_moved": "Resources should no longer be added to the dashboard configuration but can be added in the dashboard config panel." }, "edit_lovelace": { - "header": "Title of your Lovelace UI", - "explanation": "This title is shown above all your views in the Lovelace UI.", + "header": "Title of your dashboard", + "explanation": "This title is shown above all your views in the dashboard.", "edit_title": "Edit title", "title": "Title" }, @@ -3211,13 +3211,13 @@ "suggest_card": { "header": "We created a suggestion for you", "create_own": "Pick different card", - "add": "Add to Lovelace UI" + "add": "Add to dashboard" }, "save_config": { - "header": "Take control of your Lovelace UI", - "para": "This dashboard is currently being maintained by Home Assistant. It is automatically updated when new entities or Lovelace UI components become available. If you take control, this dashboard will no longer be automatically updated. You can always create a new dashboard in configuration to play around with.", + "header": "Take control of your dashboard", + "para": "This dashboard is currently being maintained by Home Assistant. It is automatically updated when new entities or dashboard components become available. If you take control, this dashboard will no longer be automatically updated. You can always create a new dashboard in configuration to play around with.", "para_sure": "Are you sure you want to take control of your user interface?", - "yaml_mode": "You are using YAML mode for this dashboard, which means you cannot change your Lovelace config from the UI. If you want to manage this dashboard from the UI, remove 'mode: yaml' from your Lovelace configuration in 'configuration.yaml.'.", + "yaml_mode": "You are using YAML mode for this dashboard, which means you cannot change your dashboard config from the UI. If you want to manage this dashboard from the UI, remove 'mode: yaml' from your dashboard configuration in 'configuration.yaml.'.", "yaml_control": "To take control in YAML mode, create a YAML file with the name you specified in your config for this dashboard, or the default 'ui-lovelace.yaml'.", "yaml_config": "To help you start here is the current config of this dashboard:", "empty_config": "Start with an empty dashboard", @@ -3530,7 +3530,7 @@ "starting": "Home Assistant is starting, not everything may be available yet" }, "changed_toast": { - "message": "The Lovelace UI configuration for this dashboard was updated. Refresh to see changes?" + "message": "Your dashboard was updated. Refresh to see changes?" }, "components": { "timestamp-display": { From b8d3c68a7ad8d5ba870a393893e0d4a628d168af Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Mon, 14 Mar 2022 13:07:15 -0500 Subject: [PATCH 046/142] Add Color Temp Selector (#12041) --- gallery/src/pages/components/ha-selector.ts | 4 ++ .../ha-selector/ha-selector-color-temp.ts | 58 +++++++++++++++++++ src/components/ha-selector/ha-selector.ts | 1 + src/data/selector.ts | 10 +++- 4 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 src/components/ha-selector/ha-selector-color-temp.ts diff --git a/gallery/src/pages/components/ha-selector.ts b/gallery/src/pages/components/ha-selector.ts index c3855147ed..9f585d051a 100644 --- a/gallery/src/pages/components/ha-selector.ts +++ b/gallery/src/pages/components/ha-selector.ts @@ -173,6 +173,10 @@ const SCHEMAS: { name: "Location with radius", selector: { location: { radius: true, icon: "mdi:home" } }, }, + "color-temp": { + name: "Color Temperature", + selector: { color_temp: {} }, + }, }, }, { diff --git a/src/components/ha-selector/ha-selector-color-temp.ts b/src/components/ha-selector/ha-selector-color-temp.ts new file mode 100644 index 0000000000..684a813225 --- /dev/null +++ b/src/components/ha-selector/ha-selector-color-temp.ts @@ -0,0 +1,58 @@ +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import type { HomeAssistant } from "../../types"; +import type { ColorTempSelector } from "../../data/selector"; +import { fireEvent } from "../../common/dom/fire_event"; +import "../ha-labeled-slider"; + +@customElement("ha-selector-color_temp") +export class HaColorTempSelector extends LitElement { + @property() public hass!: HomeAssistant; + + @property() public selector!: ColorTempSelector; + + @property() public value?: string; + + @property() public label?: string; + + @property({ type: Boolean, reflect: true }) public disabled = false; + + protected render() { + return html` + + `; + } + + private _valueChanged(ev: CustomEvent) { + fireEvent(this, "value-changed", { + value: Number((ev.target as any).value), + }); + } + + static styles = css` + ha-labeled-slider { + --ha-slider-background: -webkit-linear-gradient( + right, + rgb(255, 160, 0) 0%, + white 50%, + rgb(166, 209, 255) 100% + ); + /* The color temp minimum value shouldn't be rendered differently. It's not "off". */ + --paper-slider-knob-start-border-color: var(--primary-color); + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-color_temp": HaColorTempSelector; + } +} diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index 61b790cd98..25c570a8e9 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -21,6 +21,7 @@ import "./ha-selector-icon"; import "./ha-selector-media"; import "./ha-selector-theme"; import "./ha-selector-location"; +import "./ha-selector-color-temp"; @customElement("ha-selector") export class HaSelector extends LitElement { diff --git a/src/data/selector.ts b/src/data/selector.ts index 76f673b125..4e75ad8268 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -16,7 +16,8 @@ export type Selector = | IconSelector | MediaSelector | ThemeSelector - | LocationSelector; + | LocationSelector + | ColorTempSelector; export interface EntitySelector { entity: { @@ -97,6 +98,13 @@ export interface NumberSelector { }; } +export interface ColorTempSelector { + color_temp: { + min_mireds?: number; + max_mireds?: number; + }; +} + export interface BooleanSelector { // eslint-disable-next-line @typescript-eslint/ban-types boolean: {}; From e01cb3ca829b5d19cb65e382844da47b6f1b1785 Mon Sep 17 00:00:00 2001 From: Zack Date: Mon, 14 Mar 2022 14:22:45 -0500 Subject: [PATCH 047/142] Utilize Hide Hidden Entities --- src/data/entity_registry.ts | 2 + .../device-detail/ha-device-entities-card.ts | 124 +++++++++--------- .../config/devices/ha-config-device-page.ts | 2 +- .../entities/entity-registry-settings.ts | 111 +++++++++++----- .../config/entities/ha-config-entities.ts | 27 ++++ .../common/generate-lovelace-config.ts | 5 +- src/translations/en.json | 14 +- 7 files changed, 186 insertions(+), 99 deletions(-) diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts index 83fcc601b5..eaefcee6e3 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -14,6 +14,7 @@ export interface EntityRegistryEntry { device_id: string | null; area_id: string | null; disabled_by: string | null; + hidden_by: string | null; entity_category: "config" | "diagnostic" | null; } @@ -38,6 +39,7 @@ export interface EntityRegistryEntryUpdateParams { device_class?: string | null; area_id?: string | null; disabled_by?: string | null; + hidden_by: string | null; new_entity_id?: string; } diff --git a/src/panels/config/devices/device-detail/ha-device-entities-card.ts b/src/panels/config/devices/device-detail/ha-device-entities-card.ts index ebfbd159dd..4c352d3c21 100644 --- a/src/panels/config/devices/device-detail/ha-device-entities-card.ts +++ b/src/panels/config/devices/device-detail/ha-device-entities-card.ts @@ -40,7 +40,7 @@ export class HaDeviceEntitiesCard extends LitElement { @property() public entities!: EntityRegistryStateEntry[]; - @property() public showDisabled = false; + @property() public showHidden = false; @state() private _extDisabledEntityEntries?: Record< string, @@ -60,77 +60,75 @@ export class HaDeviceEntitiesCard extends LitElement { } protected render(): TemplateResult { - const disabledEntities: EntityRegistryStateEntry[] = []; + if (!this.entities.length) { + return html` +
+ ${this.hass.localize("ui.panel.config.devices.entities.none")} +
+ `; + } + + const shownEntities: EntityRegistryStateEntry[] = []; + const hiddenEntities: EntityRegistryStateEntry[] = []; this._entityRows = []; + + this.entities.forEach((entry) => { + if (entry.disabled_by || entry.hidden_by) { + if (this._extDisabledEntityEntries) { + hiddenEntities.push( + this._extDisabledEntityEntries[entry.entity_id] || entry + ); + } else { + hiddenEntities.push(entry); + } + } else { + shownEntities.push(entry); + } + }); + return html` - ${this.entities.length - ? html` -
- ${this.entities.map((entry: EntityRegistryStateEntry) => { - if (entry.disabled_by) { - if (this._extDisabledEntityEntries) { - disabledEntities.push( - this._extDisabledEntityEntries[entry.entity_id] || entry - ); - } else { - disabledEntities.push(entry); - } - return ""; - } - return this.hass.states[entry.entity_id] - ? this._renderEntity(entry) - : this._renderEntry(entry); - })} -
- ${disabledEntities.length - ? !this.showDisabled - ? html` - - ` - : html` - ${disabledEntities.map((entry) => - this._renderEntry(entry) - )} - - ` - : ""} -
- +
+ ${shownEntities.map((entry) => + this.hass.states[entry.entity_id] + ? this._renderEntity(entry) + : this._renderEntry(entry) + )} +
+ ${hiddenEntities.length + ? !this.showHidden + ? html` +
- ` - : html` -
- ${this.hass.localize("ui.panel.config.devices.entities.none")} -
- `} + + ` + : html` + ${hiddenEntities.map((entry) => this._renderEntry(entry))} + + ` + : ""} +
+ + ${this.hass.localize( + "ui.panel.config.devices.entities.add_entities_lovelace" + )} + +
`; } - private _toggleShowDisabled() { - this.showDisabled = !this.showDisabled; - if (!this.showDisabled || this._extDisabledEntityEntries !== undefined) { + private _toggleShowHidden() { + this.showHidden = !this.showHidden; + if (!this.showHidden || this._extDisabledEntityEntries !== undefined) { return; } this._extDisabledEntityEntries = {}; diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index 7512b3067f..54d926b978 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -557,7 +557,7 @@ export class HaConfigDevicePage extends LitElement { )} .deviceName=${deviceName} .entities=${entitiesByCategory[category]} - .showDisabled=${device.disabled_by !== null} + .showHidden=${device.disabled_by !== null} > ` diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index 542a730d32..d66fb83e23 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -76,6 +76,8 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { @state() private _disabledBy!: string | null; + @state() private _hiddenBy!: string | null; + private _deviceLookup?: Record; @state() private _device?: DeviceRegistryEntry; @@ -112,6 +114,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { this._areaId = this.entry.area_id; this._entityId = this.entry.entity_id; this._disabledBy = this.entry.disabled_by; + this._hiddenBy = this.entry.hidden_by; this._device = this.entry.device_id && this._deviceLookup ? this._deviceLookup[this.entry.device_id] @@ -244,41 +247,75 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
- ${this.entry.device_id - ? html` + + +
+
+ ${this.hass.localize( + "ui.dialogs.entity_registry.editor.hidden_label" )} - outlined - > -

- ${this.hass.localize( - "ui.dialogs.entity_registry.editor.area_note" - )} -

- ${this._areaId - ? html`${this.hass.localize( - "ui.dialogs.entity_registry.editor.follow_device_area" - )}` - : this._device - ? html`${this.hass.localize( - "ui.dialogs.entity_registry.editor.change_device_area" - )}` +
+
+ ${this._hiddenBy && this._hiddenBy !== "user" + ? this.hass.localize( + "ui.dialogs.entity_registry.editor.hidden_cause", + "cause", + this.hass.localize( + `config_entry.hidden_by.${this._hiddenBy}` + ) + ) : ""} - ${this.hass.localize( + "ui.dialogs.entity_registry.editor.note" + )} +
+
+ + ${this.entry.device_id + ? html` + ` + outlined + > +

+ ${this.hass.localize( + "ui.dialogs.entity_registry.editor.area_note" + )} +

+ ${this._areaId + ? html`${this.hass.localize( + "ui.dialogs.entity_registry.editor.follow_device_area" + )}` + : this._device + ? html`${this.hass.localize( + "ui.dialogs.entity_registry.editor.change_device_area" + )}` + : ""} + +
+ ` : ""}
@@ -354,6 +391,12 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { ) { params.disabled_by = this._disabledBy; } + if ( + this.entry.hidden_by !== this._hiddenBy && + (this._hiddenBy === null || this._hiddenBy === "user") + ) { + params.hidden_by = this._hiddenBy; + } try { const result = await updateEntityRegistryEntry( this.hass!, @@ -409,6 +452,10 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { this._disabledBy = (ev.target as HaSwitch).checked ? null : "user"; } + private _hiddenByChanged(ev: Event): void { + this._hiddenBy = (ev.target as HaSwitch).checked ? "user" : null; + } + static get styles(): CSSResultGroup { return [ haStyle, diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index f93930d2d3..87c9fb2336 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -101,6 +101,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { @state() private _showDisabled = false; + @state() private _showHidden = false; + @state() private _showUnavailable = true; @state() private _showReadOnly = true; @@ -301,6 +303,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { showDisabled: boolean, showUnavailable: boolean, showReadOnly: boolean, + showHidden: boolean, entries?: ConfigEntry[] ) => { const result: EntityRow[] = []; @@ -362,6 +365,12 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { ); } + if (!showHidden) { + filteredEntities = filteredEntities.filter( + (entity) => !entity.hidden_by + ); + } + for (const entry of filteredEntities) { const entity = this.hass.states[entry.entity_id]; const unavailable = entity?.state === UNAVAILABLE; @@ -465,6 +474,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { this._showDisabled, this._showUnavailable, this._showReadOnly, + this._showHidden, this._entries ); @@ -603,6 +613,15 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { "ui.panel.config.entities.picker.filter.show_disabled" )} + + ${this.hass!.localize( + "ui.panel.config.entities.picker.filter.show_hidden" + )} + ) { + if (ev.detail.source !== "property") { + return; + } + this._showHidden = ev.detail.selected; + } + private _showRestoredChanged(ev: CustomEvent) { if (ev.detail.source !== "property") { return; diff --git a/src/panels/lovelace/common/generate-lovelace-config.ts b/src/panels/lovelace/common/generate-lovelace-config.ts index 0d3fd0cb9a..8ffdd43709 100644 --- a/src/panels/lovelace/common/generate-lovelace-config.ts +++ b/src/panels/lovelace/common/generate-lovelace-config.ts @@ -238,7 +238,10 @@ const computeDefaultViewStates = ( const hiddenEntities = new Set( entityEntries .filter( - (entry) => entry.entity_category || HIDE_PLATFORM.has(entry.platform) + (entry) => + entry.entity_category || + HIDE_PLATFORM.has(entry.platform) || + entry.hidden_by ) .map((entry) => entry.entity_id) ); diff --git a/src/translations/en.json b/src/translations/en.json index 4eaa79d614..7d5e1440ac 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -102,6 +102,12 @@ "integration": "Integration", "config_entry": "Config entry", "device": "Device" + }, + "hidden_by": { + "user": "User", + "integration": "Integration", + "config_entry": "Config entry", + "device": "Device" } }, "ui": { @@ -785,11 +791,14 @@ "unavailable": "This entity is unavailable.", "enabled_label": "Enable entity", "enabled_cause": "Disabled by {cause}.", + "hidden_label": "Hide entity", + "hidden_cause": "Hidden by {cause}.", "device_disabled": "The device of this entity is disabled.", "open_device_settings": "Open device settings", "enabled_description": "Disabled entities will not be added to Home Assistant.", "enabled_delay_confirm": "The enabled entities will be added to Home Assistant in {delay} seconds", "enabled_restart_confirm": "Restart Home Assistant to finish enabling the entities", + "hidden_description": "Hidden entities will not be shown in Home Assistant UI.", "delete": "Delete", "confirm_delete": "Are you sure you want to delete this entity?", "update": "Update", @@ -2364,8 +2373,8 @@ "config": "Configuration", "add_entities_lovelace": "Add to Lovelace", "none": "This device has no entities", - "hide_disabled": "Hide disabled", - "disabled_entities": "+{count} {count, plural,\n one {disabled entity}\n other {disabled entities}\n}" + "hide_disabled": "Hide hidden", + "disabled_entities": "+{count} {count, plural,\n one {hidden entity}\n other {hidden entities}\n}" }, "confirm_rename_entity_ids": "Do you also want to rename the entity IDs of your entities?", "confirm_rename_entity_ids_warning": "This will not change any configuration (like automations, scripts, scenes, dashboards) that is currently using these entities! You will have to update them yourself to use the new entity IDs!", @@ -2405,6 +2414,7 @@ "search": "Search entities", "filter": { "filter": "Filter", + "show_hidden": "Show hidden entities", "show_disabled": "Show disabled entities", "show_unavailable": "Show unavailable entities", "show_readonly": "Show read-only entities", From f1ec479d415ba3d3e1028bfd8488405cfad85f77 Mon Sep 17 00:00:00 2001 From: Zack Date: Mon, 14 Mar 2022 20:37:37 -0500 Subject: [PATCH 048/142] Reviews --- .../device-detail/ha-device-entities-card.ts | 8 +- .../entities/entity-registry-settings.ts | 164 +++++++++--------- src/translations/en.json | 6 +- 3 files changed, 94 insertions(+), 84 deletions(-) diff --git a/src/panels/config/devices/device-detail/ha-device-entities-card.ts b/src/panels/config/devices/device-detail/ha-device-entities-card.ts index 4c352d3c21..488feaf07c 100644 --- a/src/panels/config/devices/device-detail/ha-device-entities-card.ts +++ b/src/panels/config/devices/device-detail/ha-device-entities-card.ts @@ -62,9 +62,11 @@ export class HaDeviceEntitiesCard extends LitElement { protected render(): TemplateResult { if (!this.entities.length) { return html` -
- ${this.hass.localize("ui.panel.config.devices.entities.none")} -
+ +
+ ${this.hass.localize("ui.panel.config.devices.entities.none")} +
+
`; } diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index d66fb83e23..62ab9e3ebb 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -1,3 +1,5 @@ +import "@material/mwc-formfield/mwc-formfield"; +import "../../../components/ha-radio"; import "@material/mwc-button/mwc-button"; import "@material/mwc-list/mwc-list-item"; import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; @@ -20,7 +22,6 @@ import "../../../components/ha-expansion-panel"; import "../../../components/ha-icon-picker"; import "../../../components/ha-select"; import "../../../components/ha-switch"; -import type { HaSwitch } from "../../../components/ha-switch"; import "../../../components/ha-textfield"; import { DeviceRegistryEntry, @@ -214,79 +215,67 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { @value-changed=${this._areaPicked} >` : ""} -
- - -
-
- ${this.hass.localize( + +
+ ${this.hass.localize( + "ui.dialogs.entity_registry.editor.view_status" + )}: +
+
+ -
- ${this._disabledBy && this._disabledBy !== "user" - ? this.hass.localize( - "ui.dialogs.entity_registry.editor.enabled_cause", - "cause", - this.hass.localize( - `config_entry.disabled_by.${this._disabledBy}` - ) - ) - : ""} - ${this.hass.localize( - "ui.dialogs.entity_registry.editor.enabled_description" - )} -
${this.hass.localize( - "ui.dialogs.entity_registry.editor.note" - )} -
-
-
- -
- - -
-
- ${this.hass.localize( + > + + + -
- ${this._hiddenBy && this._hiddenBy !== "user" - ? this.hass.localize( - "ui.dialogs.entity_registry.editor.hidden_cause", - "cause", - this.hass.localize( - `config_entry.hidden_by.${this._hiddenBy}` - ) - ) - : ""} - ${this.hass.localize( - "ui.dialogs.entity_registry.editor.hidden_description" + > + + + ${this.hass.localize( - "ui.dialogs.entity_registry.editor.note" - )} -
+ > + +
-
- ${this.entry.device_id - ? html` - + + ${this.entry.device_id + ? html`

${this.hass.localize( "ui.dialogs.entity_registry.editor.area_note" @@ -314,9 +303,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { )} @value-changed=${this._areaPicked} > - - ` - : ""} + ` + : ""} +

Date: Mon, 14 Mar 2022 20:39:03 -0500 Subject: [PATCH 049/142] add to demo --- gallery/src/pages/misc/integration-card.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/gallery/src/pages/misc/integration-card.ts b/gallery/src/pages/misc/integration-card.ts index 53f16a16b2..aa26eb2c6b 100644 --- a/gallery/src/pages/misc/integration-card.ts +++ b/gallery/src/pages/misc/integration-card.ts @@ -188,6 +188,7 @@ const createEntityRegistryEntries = ( device_id: "mock-device-id", area_id: null, disabled_by: null, + hidden_by: null, entity_category: null, entity_id: "binary_sensor.updater", name: null, From db78b046a233cc60469f11b5581d8df36983be16 Mon Sep 17 00:00:00 2001 From: Matthias de Baat Date: Tue, 15 Mar 2022 15:01:42 +0100 Subject: [PATCH 050/142] Add `Brand` folder and `Our story` page (#11978) Co-authored-by: Zack Barett --- gallery/sidebar.js | 4 +++ gallery/src/pages/brand/our-story.markdown | 41 ++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 gallery/src/pages/brand/our-story.markdown diff --git a/gallery/sidebar.js b/gallery/sidebar.js index 02ffeb2aa8..d98d361224 100644 --- a/gallery/sidebar.js +++ b/gallery/sidebar.js @@ -36,6 +36,10 @@ module.exports = [ category: "misc", header: "Miscelaneous", }, + { + category: "brand", + header: "Brand", + }, { category: "user-test", header: "User Tests", diff --git a/gallery/src/pages/brand/our-story.markdown b/gallery/src/pages/brand/our-story.markdown new file mode 100644 index 0000000000..5bdd3866e8 --- /dev/null +++ b/gallery/src/pages/brand/our-story.markdown @@ -0,0 +1,41 @@ +--- +title: "Our story" +--- + +## Open source home automation that puts local control and privacy first + +Home Assistant is a free and open-source software for home automation that is designed to be the central control system for smart home devices with a focus on local control and privacy. It can be accessed via a web-based user interface, via apps for Android and iOS, or using voice commands via a supported virtual assistant like Google Assistant and Amazon Alexa. + +IoT devices and services are supported by modular support for controlling proprietary ecosystems if they provide public access via an Open API for third-party integrations and protocols like Bluetooth, MQTT, Zigbee, and Z-Wave, After the Home Assistant software application is installed as a computer appliance it will act as a central control system for home automation. Information from all entities it sees can be used and controlled from within scripts trigger automations using scheduling and "blueprint" subroutines, e.g. for controlling lighting, climate, entertainment systems, and appliances. + +# Open Home + +The Open Home is our vision for the smart home. It defines the values that we put at the heart of every decision we make at Home Assistant. It’s woven into our architecture, licensing, community, and everything else. + +The Open Home is about privacy, choice, and durability. + +## Privacy + +Your home should be your safe space. A place where you can be your true self without having to bother about what the world thinks of you. A place where you don’t need to act differently to avoid an algorithm categorizing your behavior. Privacy for the Open Home means that devices need to work locally. No one else needs to know if you turn on a light bulb or change the thermostat. + +It is okay for a product to offer a cloud connection, but it should be extra and opt-in. + +## Choice + +Devices in your home gather data about themselves and their surroundings. Your data. Vendors shouldn’t be able to limit your access to your data or limit the interoperability of your devices with the rest of your smart home. + +Choice for the Open Home means that devices need to make the gathered data available through local APIs. This avoids vendor lock-in and allows users to create their own smart home with devices from different manufacturers. + +## Durability + +If there is one thing that technology firms are very good at, it is launching new products. However, maintaining the products and making sure they keep working is an afterthought for most. The result is that vendors can decide to no longer support your device, crippling its features or even preventing it from working at all. As we install more and more devices in our home, durability is becoming more and more important. We shouldn’t have to buy everything new every couple of years because the manufacturer decided to move on. + +Durability for the Open Home means that devices are designed and built to keep working. Not just this year, but for the next decade. + +# Our history + +The project was started as a Python application by Paulus Schoutsen in September 2013 and first published publicly on GitHub in November 2013. In July 2017, a managed operating system called Hass.io was initially introduced to make it easier use to use Home Assistant on single-board computers like the Raspberry Pi series. Its bundled "supervisor" management system allowed users to manage, backup, and update the local installation and introduced the option to extend the functionality of the software with add-ons. + +An optional subscription service was introduced in December 2017 for $5/month to solve the complexities associated with secured remote access, as well as linking to Amazon Alexa and Google Assistant. Nabu Casa, Inc. was formed in September 2018 to take over the subscription service. The company's funding is based solely on revenue from the subscription service. It is used to finance the project's infrastructure and to pay for full-time employees contributing to the project. + +In January 2020, branding was adjusted to make it easier to refer to different parts of the project. The main piece of software was renamed to Home Assistant Core, while the full suite of software with the embedded operating system and bundled "supervisor" management system was renamed to Home Assistant. From 5f6490e54e09927884400f4997ac39ed903cf804 Mon Sep 17 00:00:00 2001 From: Zack Date: Tue, 15 Mar 2022 14:17:24 -0500 Subject: [PATCH 051/142] Add HA to public folder and show in markdown --- gallery/public/images/logo-with-text.png | Bin 0 -> 68244 bytes .../design.home-assistant.io/editing.markdown | 7 +++---- 2 files changed, 3 insertions(+), 4 deletions(-) create mode 100644 gallery/public/images/logo-with-text.png diff --git a/gallery/public/images/logo-with-text.png b/gallery/public/images/logo-with-text.png new file mode 100644 index 0000000000000000000000000000000000000000..c47eaa7112dbf191fd45230cd6658db11963884d GIT binary patch literal 68244 zcmeFZby$>J*EmcH0)mPl(inuq&>bQoBBgYPGz{G_AcCNzf;0>$CEY`Zq~y>83`0mW zLo;-I!#O_ZocH-1-v7VrddCZ9bML+OUVHUgd*6P1r7U-Yh?)os3+smbi)X4>Sl8&V zu&~VuuVJ2CFtVv*zOY?X<(^^{_0z6m{%AATmbXw+!g`Ep6JlM%zKw-@IRx_$3!54X z|Bp5nmL@jMUu{Ed*56}rFk^VIFu$?LF+Ua|)>wGI$9rSGFF!9ZKbV>R{C;KaX%4hA zadmKJeC6!G$jilz>B0Mi_s0x(Fzu3b?7vq}$NA4}*XYu5|I@}c$Be_`<>uuU<>3+K z6=dY$6UDT7d9cjQsezcT>y9sUT(Gc+F&Gh(V`C+yP+?)==2)w1yJ{;bikdpuahaGo zyfNqUv~#?i2}|5l6w|abcQs-3v;*3^hadUCsmn33jWE6Kcvk+B%_WU{TOy^_7l?`>f=2)OJ4@NjVh{te9B)A|aq%bqL2Ff{sWTmNG*PZP&Kf&L)IpE!zN zVj!xcYVP6ybi3S(q!2GR<^$ll{4M?mg8!rY@9q5ws{Ox%{3XGa|}oRz?J7W$UmR`WBetVME?+FooCi|=Jqa^`{NT35(oTi&%Z)y{|(6_ zASm!p(0_FO0~AAkQ6-EpnwhwoJTq}Mm%P*fP97dkUP1Ls4d55$7t$36{6E9~YYG`> za}4mmO>n6~zbE*|pnn5*a8`G4080M9%KYc@e?b2p^BY_oa4E|F5bZzp=8vZs#gQb! zK>kMwC5alaB*U<Y$<3wZA5*`RPF4Pa&JaaA<@ZvA;ye`< z6=-j&V-5?V*d3~_HJYoYVRX=&Dv~2Z_>k}k7S7GLSa^(B*#Fo3KRW!M5d5Dq{GTQG zKa2SPD|e6pz{_6hc?wGPm*6Wuj8mwhYjU33%gAndCj(JY*%T8HsI-uMy%h7vivhKOgY(X5*#bg}t z!6(}?``ixcdN>P@(%zZx(z(YTaFz3Amc-1J29$7~7}^SW&y=D$jSwq%Q|I-h3fdcF zEyk($<|2^7)Nm|^1e>r|`3VE7+Wt4+fQA>SvacmS?}PT~fiL#yPj})9=ad|zELNe^ z)VWtWxQ@uTzJcV4hDU*k?F5CMhjd|ggQ_NR?%IBab0#=8HHce*`wPV^-kq^vXoh>g zWI)m`ZnB3cOtL`NEa1ar4isWHJf~9Z>DX0wzI1EK-TFygpve_N^y+#*61@2KZYz)X z5}C!Ay?z6FnAkEKTyPIK7_rCzzD}mo93b54xPLw4Y5I#N4B|bUYbAbN>e_JEokDITm55(T2WH{~OJOGF!Hn*ONUeo6M*KnwL;pWeN9&kARYTQd2E&H{1(d0=L5%A*WM0~ptUw=QQQC|B*7$Sa(AZD!D|o}*g~z<~GQ?bM4p%RS zbJiGuN6UQl$=ohR@yVt^LkQY%%*Q(#);)O@_(weE`@c2dhq~~394DVi@Q2oi^f?9A z^tT3lv^UKga?sOYIR(qyeZG>DG4u_g_Dl!d#pX`GrC48c)Hs46y=ORoQWz!z}qj|El}6VVU)6IL;sc45RUK)SU;bOiu5dBWRt8Epw;3laisLXuC- zdnB9N*uN}TugEHTKVGt`rFSG$;ac*LE zG;9)la2T|%7Ki(nxrVT!BAH*d_kK_>oRb|7*PT{gRLbb!ejN7@Uo9>N{CHm&^G7b<%*3 z51Mgxea5pk@aV=ssY^YFjmn4-scc@y;)5k;AI+J~-VnM0#~Z_GoAW|gUvd7`uv}`; zy-Q|05&)$>#-(-@c35dh!D@P8+m@)MvzMH(nQNY74Wt(%>Jz#KF2hf2 zWgpGMTJu6(&RD-(q1{*kewR#;kz)K}M*{B>Vdyx(l1XgnKGXO2N>P4y^NhA>;&^hD z$4~5Oh0Ili&UpDYz4iM>qI^oc*RbU=wdx9eGyzk#tAy-P^t+9|LENjhh4EcPc!Z*N zj7xOrus!w<#9M$l7Y8@RKl~sopXiDulYk~hULiXe(4~6;ABjY(jcN%PT;=Kz>nB~u zi=FtMV)L+h;C{h-?+Dc{Y0;H}8vWE=D7C5ujN7#o_YTP_|JH!>>CkB{Rb$20MA$Cq zbYgYj3KhU6495xhNGQ6{KOSyXgG%q|pVjm(nt2QmtL$;_Hm~Ai=mZ$faU+i(RA?@Q z8^>f*^|I0kwCjF)U|Rt7^;lpwo&6dH=PbQ&33AG)x9v8Eac$;dW>6vhSMkP3T{uHP zDVZq0o4#Nnf0Nq;a`mJKtyfuR7+2?=TdN8D%01HoxJHYYvwGa#oq^XcH4huyWgse; zccHfNkq%aqPn0D}iUi!SJt!5eS^LUuR%Ya9_mvxdI2csi1T0p!H)+fseQKiGQOx;; z^Is(vgP8FMVK`{uE9rMJJu%nmB~aa{gMf1Wv2QvhO^je8C!grHrJj~qEOrdoE_*ys zM$t{+jnbOSiFGS3K_$VnyAG9^*k*mMyMBr2r#ddIMqRIxWgv@cYFrZ50e4ysldRcB+OUB(y$uj&tCK$J@OE@p?cg5ancl0-Ul z!!lc%Uya|JyrsnozZ!ls+Rn@yr;1)NSw#l*cG_Xs%1 zSnR(`se9;%QYgbgPM*D7VJ&i8KCJpK7m<_c7A&NNcM+Ow#0Cq)gTk8M@mb1>bOa^r zIL836VuyYO>);5c@Oq=h&u-hKS#JV$v$cw#1op#HoAC}`M8AaIurcNJOC&JAlCy_+ zYZ5iBX(}*2R#8O-NdqaJ#zLjdWr+B}vF0^>D4b@G=COXcq=!J&at#b-3wv3#EO6ua z21SuhT3r%+cM6HDw0#8IrEY5Z(K853y9>;?>G-PB2r=uWS{Pw-mp^Dm#L7!}y2#lo zRucdwDwYDu9?oWw1?O+hfaS2v%wLl6e89fe$4@MAs6paHQ3 zB*siUPkeh?Ynr-{;F^ehIu*IRj;aAKukXPt>*$~-g)(Z9dq`;8-^;jpjZ%BP zhhx^UrN6M)krirk^tA5R+!lq&CWZS8ONd&L6IzA*XGi$F1*Lu!klP_rLa##g##O3= zi7;hx@aRJM$2XFmZ1g&eswsmM+`JQqrGjy$R*OX?6n^kSg&k!*7lPXhIUof_rB;vY z=RU-m6vt9R62QvR*CJ?_KjG8=GE+71t9%cuETJv#wHkaba8}Sq#G8h zdhTa&$T8Typ|j6oeXK0Zc68dV0nWZ$h^jWs-`7MBS~Jicqp1hM4b|n6?G)-J-b7r_ z(BD6GQvwHe8O3L}z8ln%2_KTE&YEbDh_3_;5Sg`kwRzc~_`Qjq-C1JAecj0!yE8G_ zoyR-+?4^y@pxbV!h<$Z~^K^FsveIS~GQA$VtLd&^tRaTJ%7-)`UOK-(F^({=drkC; zNU|Iaz1Lds^~s*&^=|5ngvkj9*tC7UvyT(duBv>dr~xPyx1MDUlC0L{H19vp#ZWO% zmyuB;NAIznq?N4aUdN-{8CpZFNeVItapy{WdX8|Om?F;)^OjmtityfH>av?bN>54N z3{3$u5?Q&23L5p}b-K&lec_ey>s9Q_xRx~Qs480rA;X%6(Cr>UwyRyP{G|}38rv=3 zv4c%_QI)S&VDJM8!c!|MFA+5fU-8u{W@e-AndI30wzGn&YAn?h{HFb?xx(;GmAW^O z0f$sFgUVCbMrmBFO1I4EP4ko8F6J-qY2L`DBrio<#Ie<69qU7CmF!`=&i=&*^;43t zGB~;(2t`T&s-Tr9cL@M|*Bjci{^d#|xy{54ct<*4F816%d#9#HWJ)w^jOx`V3y$uu z#dZ{`^r;6rI@lqd>$Me?UG#dI6Bw$MHMi2U4^d#!8z!#%eYt`FB^8&RYrFMb;|+N9 zNeN$xt_&rb+io$g7}xRdN|V_wNA?^hS@yBd;AyG8=s(dH5IG<>bE|TO7+2OGzNMJk zPCjOE+?rt9+FHsO)15Uj@otpy?T|^5ONnvNZBQk7YNqFfbw3VibbQbAr{f42ck_@h zga6y__pVx={12UWrijl%7`lL9$z{ot3K-Jmq*Rs99~TLc&a<%8Sr{rbMkcT)7%Mp& zciwRsop`=98W*u=Y*{xLEnzz+OX(==Rb{RdvR0-MM&YSV?hfOlca348f#Whb*{cc4 ztzQ6loWK*O4Mh_uU5WXxiT0(+*%?O#&b0gfPUns5NY>@)>Bkrwy6JW{okn{Vp{VFC zL&@(liC%n&+=<5rl7*9cV|p@6QVtto{h|<*18#nwnpi`ydp=e7AvKKIfL3f53Zt%I z%~vEnxdVIwb$sOpYS=UMZ0_D_!^WdcLE8|E*S8mWj)T0sI|7%!fOkLj^=T%IB`g~5 zyZIQ76O?g^m6~#VzfX}-sIKTv3|G}iQp&MT2Ht(B!&YyRnzYXuzAQ1hDo~-Ipa{bn1XAL_9pE!vG!2(HaKZ>D8!R5) zfU1F`%a#otY6wWag-osc(4(`yh-5SXy<8D;VwLU^FDJR&2m#~%vWJz3e2iC%pRd6o zbC9g1^ep)>Jcvh}ZIz>uLX zYFwU%y7izJF(_=D)uU=)@h(-HUOG+sM)n6~%l0!pU>^|AfuQr1t~v91xc`83q`ZnO zi@6gVuD2+Q_$J};&D(He^Q?ZlXK@PisBjOY5A8W?NQ3#*r1<#c*S~MfGqn+D6kE(% zusqevSqOqXKPwDZzo&;ZdcUU^hz%yH?*et=?+=zl(5Adh@=qlnz47`jt$6h&h%wh7h<#~2uU zyf1+Ciq9gj-R5W6X<}PBoIj24)4U!v1UGDkZjIa`6h3@8#-(#}+Iugl#9C)RoPd}+ zGoNB8vofxYsWBT;tsJYC3V*yb_*~}!l9%hGsTynup&`8CdDhUIDEaT?z4`Wu|7CRk zgD$0gYVZ!S0%86500}|Oe>w32)zwcG!^_}+gWcna-QFYnmy746K=5w;wBXe1&C2aO ztlmQyQV6TYC+`{_kbH5JckjiwOcIFmus%Nv>=NSomO4bZu+{34Z^%BNRkb zvnDFP=!b{{g*!_XJH*GIxaql{EczMFZ~+d~67jBzJ;Dht^O$ggrR_xXy~?U2`>~aP z;G`sxZ=Q8(k!OjeIIx19J~V)UlNtY2S>oL>aPg!3 zo}Tv+wTOJbT^~7`hDOtQjSrhaczmr$lE`NoZkwr$f>caUr z#8czVla^6n){sCI_2MIB zpy#fu2Zubsn?}05#S6fAJ_(T#HTtS7a)$bX+EpsYLnWWc*6PeuRA>m_dT7WE1o#i{s$FO7Y54(0Q z%3$~_cgnEH0k&}ruJglaXk+9|o3){k{gRnhY7W1jb7Q`8B-&a+cv63Ea%`8TgKc&e zIoX9nYB|YAFNo0|T?762v0`*$CO>&LP>)1Ct-KqK&?O@IZUAcVD6AYaEEsmTFRxhl z1|cr&D?Ua2I()xcdPPkMLm2K$KHo{LF}Ji%^tp`dHm1<^1;+zHHi#^ zrdEJa?+ZG3?bz71(g~g;<6h>hmvV?ZT*T&@gd0h0mFz}iV!dBk%`(07ld3w&xtw|> zfnA~P1!Gm(aBscr&>`LOq_|(>;0JW{B9DePJm7k>B;KbZ>-8w0&_^OrY;2PA4 zDKR7!&7`i3B_KOQMMj5RXhN4~6qZL2@$c^{ziR$LmYf=&6{1iKqab@@tQucuDYf$& zaLX>OH&jt}z2Cc{N+*Y>rPIg>tn2d#2_S6|yeebheuWcuO;oo%aFMjv_rVR}M=U_5 z*rW_x?Y2J)Izxo+?Og2d?i=@|`Py*TNO#GC&% zwwL#yl?`6b^pfDE&1ZKTs*&>p>yzFcXU#!ws3uycbEs^G0wx&!CeVOExTns*KUYll zzX+J|%s5bK$t0g46{BU18iDXW5`pht?~yl*eaVE|*aT>#MEeRyCl`cOyt~z!nVgj2^p3*q zIHUj(D`#BN>rV-Z#8bQqaL7DD#HwVU++)ZvJ>en~e`b$~Ss44&H>_1m2&*VuI&F_% zJ7AqOO&sdK`1FQ*m|eRa#=)P& z?!IBL^LY&OnSdjTFoDn?&vslD81wher#H+EYChS)TnnFS39lFd%k{nYXRTJnO@@a` zyy)?0X-Bzuwo15A7234zOiw?)QhUO}ZV5h2$sqY&lwFkcMV)TKwzZvx6XvJUecWyF zdM8*>`sDFwyJ6+s;{7STdj0+eL{z?WU3>%L(&!k>U{=QjGVWFR?zjJ^6b1}FOy}D^ zA6&qR$w^*`S7edbdQv~U5YL>G{OPwRApfI+JV~)p;mVS=NndjH@R1tE ziuPr|BDOUZkS(GT`l5A<>ix0>DYm#xgjHi3-uLRh(|fTjtui4xCUI_;ZALRJPpeq) z4VQiW(yhaQ227S{p6#kn?bjyAJ%B8F8Dvnr+=ZfbM}7P+s7$k;C_V)YpM-WzEgL zR(h%EtuCv?_I2$djc3bVhVddfg%hc3OAV0-hAL$A7B8_de}NKcS$eWsABxFZrcYvg zS|l*Bj&x$_Kx#W<(D#k>;lh~vQauj<2BV*5+^tDVdKHJijQY5=N_N8yWQL&-rHM42 z!4db;vatqj8lCSg$$Ak+Gapq$@C--fm%Q|;(U2OIqv0gUf;=_#OR8T}ZD!Im zx13;cI9BvTb!5#|ktH@^6D&6(citbhW^E2)vNeGp>g(UW;`5Xkqg!(Aoc5XnA?BK@tk_}!#ZsvuU%HJ8R*uF0QN#O`KBKj%q5pxTXob*$}*##-#Jjk@qT zowg_h9s#-VW3!VN{fg9!0PzOUUHA1OxxY)|%paPUh+wK&>qI#b)|k*M;DH?kkZ;^U z+_!YXUJ@q%rxk8~avc)~o1|BC7?)77CfI>@)GgUyeQVLXaB9SA%BYl0AU-Xr9>US0 z0iIZl$?7ciW=#s|xj5tf0`%UnkL6CoZ_iIkt^}n%rD6;J%Cr`EXVA~04ep#g=`Y(w zBMN_EyP|maA4T$j$m<*eeKk`T4TvW!$t`~CUoLBWOV4Ht$Lb#Yca!(-%ey4erTz|7 zWtd-$GITKk!se21f5H`2sF4vF>-tWDHh9Oj{?{Cp z!FGBw(IXcBUs}YZ3jqP)mB1G{YM&>}bWd2>&AbMKQI9E*z%S~F)FZ4Bx(3Od6Zij- zh3&;QJ0w)1L6zDLibjh0z8kO4tiCXEUl6V=Zx6E@?(;o{M0x#5xhGk8TaUCtJsdk0 z^}(ak`i}I=-if%i+L3UcS#DG8kVkW)M8YeU#JP_G5SlCZ$ZT8I4ukHv4AE#gDRO|C z%=*t09?`AJvgz#~2T$D*%Nu6Y36(jdwQ;MVgFX01*^}x{Kp!sE3u%JJb}}q}nO+5) z&hb2tBG1JfO(4VWZ(s8Ak3;jKv58E@vRL~jv~P+Z+|8$}Mxtl3@AbONhuSqNWHg^B zL`TG*GRQ71{cYQ_B{sBTjC+-1hZ&06NDT*%=?$WEsSi7)pqR5;E zBX#YhS?oIv!00~8`I82}ijLT&vw5J+gE#@q`OTs-I(an~$@*8lA%N#mN!QX@1$HmU zpJx6#E>#E&}(88=9A9k`7nDY`4f(b;>yH+^+8DR>t-ezPB>)p%bUQx@^9wt)0LDk zt0JwLN$s_^Y;U(bm@tmOUoPBu;rfaW>GttBcCYI$R);YmVXqtx4MEAu4Z)7_6WeRe zAAfJ+*>#dN;>{0S{`EN&sS|~9jB?IQi?#2)Vc$jCb6U5N7^LZ>o4>v-%I~APOwQ^L zIz2*8AU#J8h>RHjZpiK{>4d?hiuC~P_+V|5*Wl%iI*#cvn^D@C#L22Eaelf+-{ID| z`sAuC?7h5eG<^7}6}cbHed!=^a(ye^M!6tN7HkMn? zldwI;J@XcPBKKS4xv$?8VPHNJ2~Q=sC0gD?@?>?l&7!8f(L>R5n@jC~$rhmd~i z&nsK*em{pYg<-bUTLb9V8S?vSu1mHy=2tRh_W3<_zo?D5=dbKXCPml;H*#LJWG@o8 zC>$73D$n@jLKznYi*ioKf3o{#e7Sr4MVIyI-=z-M=d+$_!ws_pv6>T4p!6fg;kW@_ z&t~JW3($fz8T+kvSh&2;V0c^=^m>m!(bR#j(=lQN+{d0ymZg@N0{=`8Jk4m``>ZM9 z7no7)iq$np2V)R1n^7wZU0I2oF<8STlPfl&^@9~Ta!ZNilHuL)9N?qB^W;OodyJ%O zz96s^e*t7%r;q8}tbK@TC`+oEFWxMDSTHJnnm+K>>C_cC!l05{6rS#7r^6mgoSn@6 ziukYme2FFCE}6-MC5ee*a8Pq5d9?)=F>Mq zcVom?z_4FgZZptuFJs>-C)Q;IN#jf-*ALSNn$FI{-LP}Ur1?#`ZtDI89vUPS(!p>F+Qo2jleR4%e_WVb(D z1ly>}6j^!o;k|ei=l1xA5c%vFyH3NkM0r#(eM1(%pQ%wXU@1a(80j2lT8JYpq zE#WGqJO}q$s!6T~$TK_Ua&82N-aT9lT{orwvo!UJEnwS&FIlECmb_x6q5v+Kxi+sd z-v@t*R{bb%d2zQGYYV&F8&I2Cpw-Yi_N5+yuA`qJ`R>p-yL0IGK2SZArqh+cgEKAC zdr(!_7x8N=IAbiRecu0}@c!rS1Be1$zo@-|_N`(})GgyK_6Xm{_d-z*5gmF2C^dZ) zVLjjBAIv5Rtaa31^R#{r5bNK0&L-d8nRa}aBoiHrQeZ)X51_}f$lY8k+P@=HDhC{u z%2l#obLHkQkC`iL^yTkiE*bCzxGV!r+;P*A8xl2tD}xpJ_wiAYx9xQ~sx>Sg(cT+q zunz;E4h-|zSYLD&17RYO) zq1%p*d9jNp+Orsi@xX9PVxH+5&5#SNMK(oHiRGu&#~*ytGz4y}``jQWW2Vu+{%e2L znwmXtjvEjAcHGSIej9OC%@he8xS8+_2R8Zby#C#Lwogu)`0hNvYa>*wm-+kNJrl7R z>$H%3J{Fb|<(Dwpr0l0MB#&R@%HO;%lDudICJoVUp6ioipKZRbqLiqqUgJO_UO>ja zS2R3&<28_=BcGH=tnA(PaH)xDTXP^mx~smm{-|Z3w_$=z_UBbf>T4AfW-oE66aT)6 z_r!=$e5Qb7H7sj7B4yA=%Nt{`I0;is(MG_*F@;LMW1!$5--8#w{cS1p`2+48`CRdOcoudw z?fsx+{w*9bEXGk)sZ#RF{r0L=$Ll~3TR@ZZ*TlJ%Cz^pNU5QVpUc9eJ6iF&%?1bQu zI+U{0#@%3=EipfFJzVWO2u5ytu>!03PCN`tAJWz!*GjBMhb;1uK8RK^-&0O$I5HkU z8seZ&x367ZuxVP!SM5(C4kYwH4TQG@c0BUlfx9KS_=sM!XnDsS>$QzYEH-^Kn#)Y5 zqNaH(ib<<1uEj#7e7m(`xF50RVRKjFPZh-+0CPXVk(<k;=?X#x+e{!yh#q+#;OVJbdt zuPjQYJwknpZ&@vNExR?qT_MW^k9J}9VmZu##Hbn-w>2P`N~oF^iLg^TT$e+U?_?9zN>qf6&x}=0MUB^^9rqsy1|9frYR;>I zW|`wjgr$B%4PYN|3Z)$_4fdDDIC!FNR(b(QDsivbPzbQv4J>n%{Mv z_37Dz^8FZ8mWofl)!%-I=RrCjc+r|n8N$Nii>TLAVxj4ohU?^uV5 z8(ir~W~NcJph{1Y&aTRr+Dz0c6`x*t%B;#aRIIG*E5);f!NB~Sq4n4yzX8Vy7iyps z2diPb@$l41;-0exkN&+3NqImhM_Rcb}B0{Oo~Q4+!w8<#0i&{FR5QNzeW)N?Ck zMSPr?7qcdMBS(pfzmB!Z|EyJ{a|V3${Zx_iMfUTqv!K&U^Hk@CcnJW~r$!h8X=u~{ z!}$mtUb7U(-w4rc2>|%D5(~~G7B|gaR@opHn;^l zOCSc{{OBqu@sL2v%LTm!1Wqo4N-aCg@c{vP?Y~sj)2$Ruh**OBC zX>!T^24o9D4u*hGxV}QX=ny&ELRKcS8V8u6{ZYg7=&x?9fa^@$`&yzT=2AZ=CR^qv z(Z0QNaQS9w_uaRFM2RZGqMqf9&uY6(M6c0K3%0#&MIQjKS4ky^?DA>m&{s17CEcS+ z>?eMeSp87<+yf(9bX0`XJ9vgqwv`U7++1FIh*xmxdlXBA(tl zRmMR|hi5TlidA>IJI2zzJK7#*QzZ>Ky{W?7$!DeWsJtm{mwzJwr&utF|8I^Ry_~Iq zY=r{M*YB+_fqjN<{gZKYHxqi(1M73>d|rvAj}*SVI}a7@Yq__A4){1?+W}5~RoB8a zx?bvRQr}`S`m2L)w9epp@v+cM(`fA&fd#nQVO}PQ3|_(u)U6zOgS$$pK&_%h(<^{b zrIM12YDT4%%^Y7VKPl9s-#2KwA5pA1Tz7=s2f~RVjXf;xnk-A#kWmoLT+63$w2B`o z)|qiX`^TN47#ZVTLcpSzppe8!+&@p@0$vg-Y7YEJP2CA|pQTxkwPI(mbFHu;r3)`+ zd@&15KQ25kumDNcP`#nAw=BQ^j9D+>o|>W zPP_L`mRS76CMX>oC3ZSrYD?=%Ib1eyiBDP^dQ>HG(>_c~@S()2y>3H=a!&kHS}6uG zujb^4i=H}qBG`0lQTM=jQu(p+n>;%9_w&qp-|1c+ZBwEP9#Xz^lA_#ECr{p)l|dOn zZbd;o69m`fJp|dpm*;bCdHsESXDU70Zpz)m*I^u~_zyucR=g}!eHA%CsoP4;*J@1H zyJX~yN~D3S*5YAMUO(5BB&J+`r7_A*o=%5)P&Avl`Jz`U>RIQ8>o^E=0j$MDNxLpH zYcTtPhDObDlDWxzp1Fmg78z?Ykx|`o|5lY$Ezi{UEaZ=12Xn(dM5<MZw#?wGlaF7xN1bNIG*q+Axmm4i_QIop>? zmb(x05&71{vHxJ~&4dR5vMwm8NraetbnUnLOpu~I#F*D(hjMMLzD6faA8>H`_@{Ad zSs4!L4KLEUU9BnlN2BIguE+<4<}bsNiQvEN zGRh$IeE0&CoO>W^uc;Iwqw__B*K55U-#>Jans^X@NvS@WtX=x}xXqNg^;SG;YWTQS zF+#&=EH%;2p@#yLR=F8~d3RtSt{I%|6!ekaDuYb$uB;{nMdR>-U<^EY;Xs=%_X2M( z#QScH5_6q-4dhY2;)YNSASRikX=GuUnJg*%sjgMb_{;)#%$cGqM`6EN>7qx%is#N$ zRAGLB@t3hvB_sA2JrLg5y*7%ubhMIg|QY$cZM^^xcZCoo5B;5`-xc~^`yPT z%-Nql&&{98q;x)~(ZfziCHi)d8Q;FP@or@^Gk&vdon2|^+*p*47yEh~c?o%)KF&$K zg7L0C=wqS3Q4FrqzTluoS1j`}SAqWe`MrE7?Ayvg4?yX#UVsqv;`n@{NnsD(ChPX5 zU~|X(mj9U{lXUt6oESRS6~cfo*XB^Y@2)8=XKwdxCmk7@K9TlzzHJ$6%@=En9g^!9 zX)rfn+dNmq32(hyRL^%ho9)wDyn(n4fm2}7pdJTgxEtI9d(Q?tev&Y&f$i-D3I;hC zdU#Z$B14>W{3|q`tS7HhDt`Ro)8u6*eByKxG%vX7pmHFO9paJZ?^eB5#3@(=Ofth= zwFy1W1by4jO{Z$NRi0w!V9g)#_^AMQ?+r?u_BN{C*a-O*OD=Y=s?|e>V!te3{JBt; z-iy_Qp*Ta^9yI5oC?)4XUM;>rVZrc9WWdi16*084|4wI-}lV+lfEslQM9MT-~wLEyiz3J1T+(tX}awj z12F%_;Fmf0prqy<+d6l$GlbP~0=lQlw-5HCrp48Bwp!g>%pm5CKgfzJfG(yb_)5iW z>i8volx$@Y>GOzFgSb$rTrywY3u>XIn{nU}ii+ zLEI{c7&%BMS5g)f=~{S2j_+A_CJ8jy$wV8N?X16>N~nq)3-*iOJE3~b-JAlHc;w{& zuFFLcZ1OE;#I?XJW^ZDTE(Vai;&Vo(xJQ@O-R4vNvX%nAv5%GSDT+w;U#(*3rekUx zT>VxHJD|4)%f=Vl)K`i(PStstntT*E)Ab<|_7t)9{3H#v-`bDpp$w7|wzt!6QfHZV zO>6Y-L(Dn&`?`2q!S@f@VmeAT+bIzrb_HToM2uxT+pqfsx2JmlQW&oGK<;l9iDRX! zlza15qAfk&n6*EJQ(_CBWNzS+5eZw>r_F#H)7lectb2dm0iCu7Z=oq2J*N!UFN!ux zMEv%L^jezEPU9ReTHk2t#h~bFP0SDJOx?HoH4Ym>%Qx*$TBRB}^&RJBOl;NjWzHDkq3`*Yb|ICHQx^!2HJc(L6C^}#8=39gj*(0Fc}vr+7JBWl zs7orrx7$p0I<2H4H8D#m4tZuMdrZdnIA@kM^Qa7p2I+22IH_2|ai)dZWLH4jdd#AJ z=o7b_mga`G^S-SM4vJj-_hK`f5Gtr$vpqW9nL#lnB5x~FBL0+J=u>q$ z9Ez<9IRIZMB+KL>_uVeTUr>{~vFG$Hn%2;E`d%zih#q+*E_@4*&)lhNCtv9XRZ$ji=g4LX1KHEBA0TKY*ry8feMin)u6 zu86sS5DzoTp+>i7P1R=sj!RlxWJWb>k^Rh)jD`GCKBF=}!{j?Prf}I*It}fV)|R6W zA2}P)T_i^QDzO&RaX%?vi~!{ZG}k{YY12G=Nsg~qBzVL}E>ewl(lnnm?ntxz2H0uh z7lAI^i8+dP-*Eg&gk{~t`r?o>#*cuu*GwE&rSolndh7#RqZq#|%cQRkNv8t~Br&7R z8kvE8D>}@gaeJ+v@4Fs8T;FUGtZ846QZlqrHE;gW8xp8!X!&h>uYEp)`M6YtnjRaU z_Jq!Hz(z9lBjhBtf2Hcjtul|yzQqU3O}s}l%tR$5_h*e;+n@t#oEJeKX@?p3Ucd6t zC+FyNgQ6uJ_PsVyOM1{nh%r9(f^XxG1zhHwgg+tlsMmbr8#@incAUxktZxq=liscT zxwq%DU~y@34qCaG&y>XyrPoi=l1>Xh7J(V18ChOcX544^o)-~+r^xkt1W$||sjZh} zqX^J8xpMVL^Xp)-@eBIH5qT{^vs;8c>O$m#9I2sN!oU)CI&b;-nWI<2Cp55N>$=!z z_z2bN%Mkux6z0C&EkobEjX5v5{&gXgW*yz&A7Np&hpn%DYd$O8|ZTbjDuzYOl` zllwWH$WSZFEXzmbt}KKS;M2M)WTnk)r=v$DpjD?r-SO)`BrPb-{hF2sTSFIVX?9ZcgjErfvJaJ z-9)A2-h;ZKxom=Y3Q{#S_sT$nxmhhS#nrW7&&oi~pT(X7zkoMFaogW#-(WxaW=R7L zaQ!BhoT>|-amflyNr)=CwTg^uOh_Hj3Jc`fe?jxS{~&Lur+14bpLaS7@hH3asJ_o^ zlj}q?_xuLmIakbX9&IKuJSTC(-|g#6?wYCMwpcl9&-RHHGqwMK{NRyL!is`l@#;^1;9;0O1(dWgX7NGcn&Cr8{6_1xvw>SIzUeQKJOpC*0l7P#t%87Yq+H4U2NC6gjR4%rYV`9J}mHyeENj^LeGiTX9?@DpFR< zTc}pS;09Q>vFnB&B*MR8Z)vL;QUhW!wCTT?7b+xzijQ}QxXK8dJLrW-5SSc&!=Iyz znt|CXWRlY%58$nWeNv5EwT^=vTH={+TLmZ`2P~5}UFgQtwgr_eCEOicPK!gUJ`8Ex zfC^iIJolNzLRdD+A^C4xg^C*3^<9TSgRAs%AK#W8*@%VSpcz)L7hQNT(@+E;>u}FM z>lqGM)Dqr<7U^|Ydw#~;_g;y8%XRA{jae({r*vsm7i(6G9j}p)$jk!lx%C?mxTl{J zwMvpF#XlH-VIqRkf{!0dqc@omhCJ!>NCb0hO4AL7)Kt+%Dbwi2pzrQ`!(hSoCZ3w! z1#@Q1TRtV$ZRIHcnVoz^Kd9Nq>(<hhaw|C)b45qxv4>*$?D*d|2MWgg$H>LTVyuQDCM#5K4CF!8m z9XnuXxiz7%c81^cwgoLZ?H_yMjHS~!q|3BTWA=q3CtSmU8Gy!7<+tqNl_zVD=2}%G zgzMK1C0Z1sCOCb!azlb+4iH_};^&34INdecJnXAG!$45AS(7x!1PL;xjH1%7v%Y?axW= z?Qtakh*unQ6(&Cu{&FtD943ZRt4tc(l8sS@y+Zq1lwZ8Dt#4bPV9sbM7B^a-Z{n_* zTDZ6PygT;h03WW6QTmBcTMluyq{jHh^3swYHBEFOA5b5}g)BFtjebWqBXIQgBduq~ zm;REjX6G;aFVHR~XnhcsQzvM8#oaJH+Iv zC7SIXpY31M+0yOlxu@BSeUs9`^FA<+A`DsTS8C{-Y=Y{F$SwNYF)u!Xjx{YF;sc6s zhTczz*r-a{FxO+#Dzxg$?r(C3M4X_GZfH%TUs*8G=|xR}>^#>W91zi<1YZ-| z_09_xl5Up3**JgJWL1qtq1HaUR`Lta$v{}-hH~`4fp$j_q34CJrz=!8iXUH_fDVyEI(f-r}i zF$;+&5o#A3mtlTxp&q88Ys53?5$2=-t+qmvR>-<^Fx8D0=H--Q6Gtw!Ehexy-)assfcsum zCg5}J+8k7AuQPeXA16v6hG2gNSTFH|6Tj}5dVlq4jZw1bbHgEkDQ<;JW&|1(lSAUz z`#5+K7Vh_Bq4#mPhZT7o-Mb`4f&C-A_1@5U=jsWVN4l1#xbo?2O_1<8a0`%Y$ZJN44QqvPX@>3z%1%v#R7>^Q-0DJB(Fwkc+bG!KJ4{u`n2H> z`)OvWA`#EU+6cE?6<-YqTw46&z-p`~>3J4(DU^AL z? zh|_(7jji7(`wnpq{T}*FX7C;THab9i{@di$Hs)UqaNJ&p+F%`ZQXOd?5*Q(mXdmcj zPWwhnGgc+>)XK@(M$heg?&d6yfr2LWQAPFH_<3L=PRCxXt=Km`Kzcf?HJ8mf7I4^Y zg$D`>gY~$1;7-fUwXA__>;LnQ#zUC0 zL)0DJ#%h~AU*a0OO;kDQ84hRb3WrVBr_X8Q%Ee(pWL6{h^=s~G809((;9TLip|Uya zR!^H&jf!t79&inFP8C^O;W$n8w*Q^ID%4(-q*QzLwpvZLCHuwk{kSSCcB)tIM;QWR zPv8(+fzIqcaO_u7zkuJe;<8JCCh{)x@?C=*Zn*oFcQ{v*Zf zM>*Hobf=8K1UK6p{>h|v3Frt+Y7SF)j`bav;L{wIInb^Ou0F4V5vR?j0-fdbU8}gZ zT-k@R{uVu^4|WD&;shH+S8d6WF=^=?wjh0m4=TZOP^3p^9QOI>?2A#KiT<{I*<{_Q zPqc^mz0>-jMs{eNB9Z!`iTW9|I1W!kVQE!?C5? z4&8L$*6`XdT@gO=!yCiLe{xfJ-Ip#87hHFDHs!=qL4XtYSexnw^cn*XkJ~H6D;}Ji zt{Nyx##;f0w^_j9xLx}naL{sBE?yW8(v=s!e=XN-p8oQCH`wiy9u8YAQu70QcEh{Q zzuKO|p&qs4?fBhv>%$RAnwuYj#VKWVsY-J%B(Zm~bw^d_;A(~T%HDrI7C8hakATiJ zJKtp7&!BF#XLNS`*G+WWrEyIc}`p3PZT z4pjX&qTgsfYzBAkw^8hkHi7XEIwUWsQMUJ+woTlbU(OqMh2Enn7!aNRjS8uz>}1XK zqW$P)PIfvP-yP_2qREiYGt5kf)pmu$@7om)d#tSH_w7rL+%+uFdMxP{KvQ-l!Tt8Y zg#Wtgj_|q5?+CxLAcP#pI!oWv^X*~N3IYr|Y0P(6;NkFi#Y1gA`8;f2FSc7BRT*&b zv7U8^`D*+)$nx{9xjVe-3zvoGedcH356{0Q+-|p|a&FqE8&@s~kJ)n+PYQ^8-j|jg zw(*UW0t|UedQ0W&w)(ogkiFkht+ixjKc35Z!EClEYaPD-8aq=1fk_~6ge~GclZ}(G zm|51akG1`~$++IqMFA80>pXVpG@J2LOg?1th_Rb}n(zUZ{o#=2u7JZwP3r#i4QVb_ z411Uv)elzE+zD)+yNRY2W(zD-b&I9v+e}9Jvs!0&o@+9SU$C3*#5q*{uJwK69QwEG z3d?qcCa;}+K6(4Jsuk%wg8l}LIRb}yX}sE>K^l34H~9*62= z<%M=d#%oX9D=f15(MYvLhT4u|FX^yMt#$7K=~zXS;<2yo?4OROqUhN2 z<4Ysu_Oz`EYmXeWa^9ZLLrNqFOdJ7ypnTS3J~&Ra^B!&Tacz(WN?BVuj_ll{+t!v$WivtGi3H-DF~rkDy_7iwF)@wyJ~R1 zZ%^5KUHI734h~P;rx?Ia>)&tP^6L_HXN69VH-;O(}6XO8`S-#OWhFN8k_(zk)qcE8YV0yf=>YVCb_rH$Ph7|i0*{hqsUwr)yy|RjS`ziHL z){0}7?Uu%GH5~O27!Lxc*fifY9#gGsmY*=oCsbw~!;?th7+ciw16bG}*%&7eV4P7K z;*mBX-NuE~9RYDWth+89Y3tTXpXosJho5~o~(9sJurGcKkG?n zjs8Kkh_Rj#(3K_g@%#3?JOBpo>FzV;W{y(b?q;^+eO%A;a-X6bGzAM>BiVO5UYkV~ z@}m22qh2&-FIm7r&u~z{;cXUh*gp$6q}wjlx|g?#_r+IRJNpQ_XKHo$vK6j;#qc9T{~ z_kDe=$;(ZiVN&(q^))tzJ_G;QWG)GoA@qyoT=v5t`?AmH-#+j4_&UQn9^7l^F8k@H z{v{??nJ9QJ(e|AtJDKQ;gMCa+G12dZ6()m7ueL$-TWuJ@FHpxwyUh_)Z@EY@^#*}h*erA=0tX@wI$p`OtT-Eig-kW?gciP)1+K%nY zQ+c@p4#%tx2drM0+TG+Vc{``{EM1#6xxLmL>btpqeQochZtZnC&rWC5_4xTdwGraB z?|(Vz)1}mx2*Q{Ceuw@=U(|2W?~rMi}gqH4ij;{-(BY@>>xgRbCsl*xB}pdsd`+1OQnPf>v9k4^rM$ySq2 z((R^nZYBp>{=ZFr&7{ht<4yCNOM+#h*AJF+*$;#4%RcXXpWkMx|F_GX^P8q$#pjbg zi~hdLxw!g|eWT0#bzSQBs{R%0c(#eI)@zsanYYB`43oE;>}^tI((fXD-+XtKgACF3 zJvO%fe(AS~zGv#Y%&?Mv2kN_LC*SMm`+IX(`=~GD4&%pFUnX8}r0mr6Rc(Q}ST&xusVf&!;3Qj5 zz7p|Pogep}TWwUeezB@npSqUqdqphR8!Jln_O;kwaGOm3RZd2F{6N$oRM0Gerz9XGKi%2&_O0TuKS&S z!wjC*Z=&G?{yvsJO4>&&RW)9VUW{VtK{gs5U#ZUyc4 z6HQJsQ6NBdgGj4vki$$a>0q$={NC&U&uFXk$Ll@RHR`*$eghYi>bgm;?{++w173E# zT(TJ(ef-QuR+j<{R;OyKOXcaX%wl z{TlSM2G{evm7d#C{=$P&;NjeB^gSjmJq90it0cQ}!BbqGimJ9O1{Ly+qL#LGJFZM8 zHc7FY%q;1Wxb`*EwRt*_sx9R=k+V@ zyrl2rRi=M6<1a92_g#6mIVkv`tH$-a<(%0R=h*fL>%WW1ZF96UjPByA@8|k$@?{fq z&dr+i_wNUpeAIINR(*wuzEi94SUUX3Ex3ENY&7?d; zS;yN`kRUm6Wv`s9E_v&%AwB;f7SoH>sXb-V^MJ!ryTaj2yTajM3pmV{G;UE^=@xa; zTw7_jt9K4bFV46yhgQ8c=A9313vc=AmEki#zs&*;`i!%M%R?Q#)RtTEM>pRW-tgti z!}o8zx0R{7`ey60q0E-Ol%%LzXJnSb#dbBu2flMdc=_iq3#WhT;_$rBTpHf|)vLn4 z{^Is<|HC`#*w4nEq)>wD~P-R7L`F$+4w|8XXtH|h36 zS3hPxWAZ4IFPc=Hj;~+<{kS@~0=q?%IzE2aL_e-N2{;t{`t}cPL+|aUn{@Vt%6oQ~ z%ki+HyDsxidy}pBX%($n0fcVTr@HOXq5e*$SCzxR!zskirz(y|Ij^2=qQFBx(<{xG z{ytQd9Zb|HF|SlS+g?QTkP}t{WBl-dHsqzFFmPq)vh%aOn;F{FW;4$4W{1? zz5LE;W~*79e56lp)-d9A3XfirX+vxOEA+jXG~om$ zAn2gq$1Z7a<<8j5iJIJ}-MnTdbLVO4D2PAF)0oNGYumZQbtX*PY4CM(u(l=UUw!LQ ztHPmc7nO|Fir%MIQ~g#mDxHqH-PZYs7hD^zxqFLs5f3?$g+lAM+=@}GO}Acp--XwP zEALV;wRxIt{V|4-cT4Q^Vuu?K+A|H__pR%~+yDEQ;TzZ86>fj%VY}Us&27hyaQU4N zhQGV`=I}qibVc~?jrVn6(dSUi+jrAdA%AJXhPQt0%JB88?+mv+XmhY`TX#$wn+GlE zaASDI=YAHxag9FjD@wNJ&4;WBr|q>O%;;G4f7)mEPych#6tpvyd0Q-{nwnzj6!*Km z#C;iiPqVSg#BsAV_Sp8i9{Y9L z?EzDiy4$=0Nrd4VmgiqCX?nfgo*&~ydO6+QIp zq|4?`lq%HOzarcJVs^h>f$i(e=9v}Pb(7+uzni@zSk_aU3(I;-qg!I1*YBJ8u+QsP z+*#>hQ|t9}rQHbn-t!t0{kG^=(%;Ir^vhTaJHOtdepIXV_e|fBzuu<_UTJntGSP3I zDicA6D(Q}h?FAz;kDjNYPaNHez9_X=LHSJTa@mKiubIR83( z;PNaS)G?oj+0*eC>t^Y*%>oPWzTleh^=t1jRgI}k&(o-lugV9v?FjF`@aphIyIs|6 zbC_$}%PsKm1dfhx%-{Z0=1~QN`Jp<#GB^mUHh!lS}(ytYG6K zwtZfqT=k~ThI*2TE~)NUQVjifOt$pPU=ut24myazp-sl=SCxKbKiT9z`(-e*^K;Af zp6}!=iC%{OUF#J8lK0Yi9`re*qBVZY?Eb1p+n?&OU%PInnZoYvlqziBqaxeismQL( ze$1q*Adu|$x<6nkdiAMSmi1Ofx5Peg+&7cH&l^|VzUB$-ReIL=ob~)k&%IlBlym(H zt()iEqQ8CP@;&IEZLTMq^!ojw6)c@l^n8=8trYrDf2`S<{WdUt7z7i;C14)naZkn1 zg3J|~dqizR&eD>Lw6@~R5gHw;Nmq{C^!Z|K-Zg&SWaX@LRMWO}JXvMzpXJ(Cz~L=N zt_p{*tKx(xA9xOVT%*0@3zDrO)8@9N{`o8KFxBi3ijH%;mg}$$;qRV#MEJ~ejtXa; zv`<*ORG%SnkZ9;CyVBujx9Oz6?o*tiOnHid0x!&-fBYvmhb!V$7Ig-!Q?}NuS^)rs z(L{nMqlhl_7}Fxj*&Q(-|0IM`NtfU}y^kmP^=;5xgC zq)K1kO!%EcSBEFNlq}`1Git7L^qq_YO9n3b<~qSkblcW zk>h?RecGMXZ=)^jX`sEo<7^SfRJ4#!&%th0K?i-2fAJjVHP^0t&ecGR{@-m;-;Zj2 zp8v2PhFaL@)sYO#Mxed2Y_v3*HSF`o{W9tMym7_tYYuF$(zC|rdhXf2<6BK7dJEdC zcCE3K_0V_DQBRlH1od5TTSXJq$yT4N$hOR`GJ`|Q+KxNb1%ta3pl*#$d%!Ub)P(EuQdBrIFF0d z)(?hv7Aq>)KD`2){Wd8F^3VHiv|#Uh72EfrK5iR7@*XZevD3B07SsxLMAvh$faSo*2;y=)HsXY0Cow#JIvs&JY< zY;zW`D$~Uk8t>I<3d_cw(kshGOQTuCK5yJFlfKUzSKPj4Lwl8;H7@A6XZwzFxL?~| zt!IsmJ@*_;N1e(0T$Ajq*2)I`Xl0g@;aw}XkNF`J82_Mye3hA$eVIAivJcbycn8a8 zq65`>ME_={;LgxkXD`w#NL04KLTavWTV%CwJbXnsYL+V;ine;$&ezxDhL0K7mF^W4 zm!$Q0TdVx$2kiU0VVbgsV@JobWvA@3u3;ueY*-O?S+S(9UO@n*R<>K!Yo2kg**-`P z`a0813+G>dPkhcs-e`0EO$#=>|4I9W7azGxc+K&~Y}*?b}>sTD-iYn8tBtNVg(FQpDC<}ei$r1|qx4~T)9SD&zZ`0!H?4jWf4O+4c~ z7KHELbZ>Z|$-}KRr@1Cymzcil(3Rn&jW)L1jk(51>yP!SGnqDt<7@5d=#Lz-=DKM` zy=10V@65bIp1ZkB!QX$io@_y2+z7}y`uGYbEhn|^OFfA6XEw;S6%O;RNV>(;?^R*A ziOu&lsq078WBOra!ffa>>O~c;>Dy-ahB?}<>S~9>ZC)qNac;eKufjp7{aE#H;3|U- zyY~namW|+gW!Y$HG;7%Bjr(QN_j%)r+t*YXbhx?w@N?3*Votqh?>y>3hgaM5t2`OX z=_p9AD;}y!x~haU!33)Mu{A(sc2>2VmX_kZ@e3n+y;s$4VUudib?O_IM-OT&m-|JB zYtoEP*QA;HCV#1I$2q8O#(u%hgg02g;h6RH{u8%bo~N7cHSg4JYrWcvBBKn;Z1Y(V z9m4i~JzsZiKa~3=Dk5y-Sigk|RLmjO_+|5MvHyLDtL#D7nuG8EHFnFV7ay^UH^+R} zm5amwIAlZk_)l-mc6WOC_4W5{4g0M%XM97_*tK;y+Xl6);;Oske=z;nWhyMUOF>?C z^sWU9cKzP+#o>iV>>57!z3X!`dRX@_?G{T%?xHKtnkOH-#-)2b{qlPCVJqWBKFfp1*u;_ zhi6tWH#hnJ*n1B^xsK{?e4h05^wfK?Y|E0nELoOqz}TjkYD^1+G%!IXq~K7D3t&?I zfg~S^0Sgm)67m^BLXwY=V0!ETw&|{Nx8)*tOIGjFd(!*=elv6KoxOYSwzsqGosrJY z+?g}yocYb&eY@w}nfU?=Psv5(ocGL+Krd;sp65UwzsL?XS#CFr<7av-C&d420x%3Z z{CN}P`!sT2j-zi$*bNv|lC5$10YUFzKW= zo%8b9h=EDDmhP(ID!Uk4!X1R%J&~b!5b|FUNk^=|_suYu_wheBLr{$}egVE!HS*TW zra^~#5$iJkt`9mSj!gg`mhtS_CWaD*nj)+tJFtk=nFw&NcI4i_z(Y*x)i z$?}y+E36mJLg%e8_+0oTFVdIkmeKF*CJ&vzyejd8qdrgWg2^18m+YO&KYnb7SvF^q zuXGvDFxa$*zgH<637zIoo{%fk^#G@rC)d<($x3i@VcWa`!k=d+`F0&RR3dZH)B!Wz zije_0O^c@wI2&xL!ciX_jZgbP-VP^+TA^wzM&B6;;1qC*;DdDjfr7zS+ z8;kU2u}perbtDa1Pz1ULfpa5T`d!HWaYXrfqPeQ#B2cU+pt;3Trv@GJ@Q6_t^GTwe zF!;(CM(4;}i#jg(j*(_GK`5U*IIRhC@vw)@Zg?60_(;=Tnc%q@s!W??Bcip#4phS&OJA!NgUA%}$)l_cvQ;AdgQFogQ3w zLQ%2ogjr~<4Lawo#k*UzwbFUrY>dY8N{41+SxFurCW@72#uBV@I|Bjt$Z}`c(x4Ue zgbEdjkao!(U+M&vdda;UdWh+8^<+J-tn;pE+dpV`0f$NEv^k@T<%mAXi$*2wk{9vS zR!^s}pV{RPF*;)qDi7x@=|8O-x(`B%!|@goYxTA`IH}DmjBTm3>cR_u|H)(bKW9T?Sa~q zO?iE0F`Pzm-h#$88&|tLN+u*P#rRU zTnBGGtY_83+~d4+F}~MD8Y7<|q?5WCDb^NCvteA!)j>sLkF{dx525hbBBecAuUPiz z#8w!V&UpoJw5cZ$;7W&Bi4Uk~atPcL5eQxB5E0x?5Zwbm?&;_ha*&o@u}*3vcQuYX zzg--t)=|oCL9$ZV;2ranhNGg*drq8WPMV^ zr!-S{%uLuo)17D>R6Sz8bmu1CLP>fSD?+|@_p^@82OhW;)zLU?x)n+A=PHnk?Dk*? zqj6OOkh1fV%#Sx32dF1)f+l%vZD_I7uq_yH!jiVTc}g05fxu}}1iB7^vte=N2y5d* zqT~3w#=DQI=QZffC%I#!1rVxE&UitGR(_bq3Y_r1Yevu^<|?GG$9Qv8k;x!Wj6g@M zRZUff(~-@;1*--kS=NlLmBh4W$`YB9Um%4R=CPcn{2ciPXif{q(r#6XWxHpf_4;+r zTaUSFY%KzOSSMB*OMf4#VHop!y#^hsq2J~0qkAO3LkFM!s!mJJROj%7Ulb{If0l6h zHpk&#lRDpr0<3e@<>SpEY)doQJO?WrsyodovvO*NjLI6)*4dN#t&`i0EJfnyIQV!C zhIjFE7TB$b(jIB)yrnbDPoLan9zc-7LBvfJ0OuH7cY%GgvOWFmk+}i0)k+jpv`IAVpyG87d zZZWiiom^EBD~&ejz&|^&-k%HzelOO*tH|ZnIt<)3GilHvMe~BR*whlgw?{N@W<=R4x&sjCft3y^iu}YWCF__0wnvU%Ppw^R~aTAiOfcd~issrZd*ox~miH)!*-rx19!c|+lf z2R;#h`56n%F|!7p1DY}tIdDWn+z5Z#g9fop(Fe~s+$_mGl(~*kb51C?jxIby;@qRt z&obAy4+k{wH~+nEtNGF6JIp=?9IXAcmqz1P$cEkK7fo9>D65t>#(;Ah;!!w4-49vC84i%ja9ZcD!(BsY{kmHp^$?@2$n1?}wC5k|9dS=4e}hJ?L~TCanua+$E)UxuiZMn?aA1&n4_J&G z@Yov4y*F4#8<-?V!OauvGDJQn$d;0qT)uqPq>mO9feuFC#E7A)iriMt|HklTRh!dxN3n75+riH1D=70D{tLca=3JgkuekSME-9dKg(Qr3eMQ0 zOG6y@$j`u`(U(7Op;`&!=5^TA zJW(Z*#`<2nF`w?|H>R(bpxvB{dm-k|=7=hj!fl(LQ&tDsc8__J6}LlNp`$Z#k_G{XBnaUh ztKP{@vP@_EFQ49R*4(nuTypyS!ZRN18UpXY zw~zv>#F0PB3AfH|3N3Nc*C&(*LthO0&s{Rj{O_YX%{`m8d7 z1GVzYF`lF{=X>H4o($yt4D@rK-;cZ-lVaJ8vo<=LbO)MdM#Kd(e>b(s>9-Tye`Lv-Vl% z?-E-MLibg-Z!#CHnq!{7V3G~aB>gjg4($pD1{~}P2jak6h)f7r;o`lJan@mzlkVAq z=#=`Rab)>~U-F`U(q5Ej<%*m^{7L_y4Ls~KkL&^>;}Bf1?-kB7$7wH6#x*8~W3|aP zmTTj^kpw5oo3%y}=w<|BIyE~f=V?nW=(v4uUitI83mPNAnAmsi{B-yoy zjpbk->p_RrwSeCs51oOr5}$7V%SgiC47rzp4n&fl75g|UJ_~wpwxLDWbZRa4^aoxi zR~9h{LHa3(K^Q)1)agmF?DQFGzI{69&Bs(FRz`q_Y4U74elJ<+^A*S$NYJDRC<0>) zfu0IFIG1NSgL3U%pGkw%+X`GbcQCJTVgHlEm!$|NDqXAZIA)@Gkze5;&R8p_i7&Ov zt(C7{(_XfCDnJ=AUs$`DD1cG~fAP#7^GQ7QZvzjuwN$$Et-I}moPBBEQqJP5ym9$y zfH5eEGn=+mi#!yo$!+0QQ6D&cuK5@P4_Ku@y^@BZ>#Ex~n~P4FW9AO>VXM-yk(XyU zeC)Q(=5_`xd^3IVG?`BL_n>d3fCF{aI;L9rD6^haN$z9}`|{-z%}4LvXCB>+umQ&9 zV-4w<6n>8E)Ue&6=)r&?A*Qq|ZE;p++g+``Mnr&5@a>8yKbB~|2|M-6Ys@WzIw|N7 zYi>2@5C_@p$88)foi8<3P>`QVt2=-Abz3-=gAR0@yrFKyYVYv%+F9%7=VqyU177zmvH-N4Kb?S;e#S)V<#*)tY z7@P1m40i@}^6%wugI*4r0MeugC<2NAb_(k9Lo}c?2c3j^}p=MY4(f;>!C>86t2L)!x}uve~{1w$wC#l zkn4SlJ=7nFEiY#DmNPU~Bl>y%!v{{AYfhQ#w^9nm&&L=)b;lO-vuE~NhdZ6_LPK!f ze(Y8T9QdG9(#hUec7+214rfA~&8XDB+wwH42RU8Cvhgg15>^6Ey95|XHHKvoUQ-Tibn}SrnkeET6e2rQ= zRuGtmf%3CXQ|$=Q@jg})o!r5`noWg)TPr=h35w#hkBCE!43?3r`No0l!@w?P4M z&Ne$wwEN&gv*lNlm(~B0Bc_-OSI&j|+t#|FKsTzMA>;ubyt&JOS|YG?*zOvK5*I`vvMxBVY2r{I-Y}Q|2EEq$a>M& z?>t~`#~BT2asZ4+5aT_k%rWe&kQWW9@Lf>Yho(zrZtK{8xi1_@L1{TSc2=6%O9|@($ZLhRqmKaAm)3sSHX=L zMD29UE{JGg%&nEg?3h7^S3$`FQPiUR>(E0-emA7aFp^LG{$J2DpzA>wgXV)eAt{!f zFbl1X|fVkpoB1n@Wpa>`eWe9YC&_SG@92%XIp@UMcvv~po z8l=DNXJ1k|(hvOS9u{wfLt;(`7F^(AwiyTSvJE~6Ua<978?oX6+YJqS*l51xNyY)@ zfIq1oZ6VM%T2YnyGw|?11RhRd;2{`?Ljtb|zF3mR;C>Og>R&BMTTj@}$hMSv*)TWuLQ+$zmF`yV~?s`UTjhjapI!IvN2!O&u%kiY2&C zxjX9QuZLY7!}Xw5JFkA4x&sSqM@%`1z*r4B9FMV|9q|^R12A@42L)Ws7$fnA(OY8( zt|Id-X#Zgqo=$7#)@v^UtpPm_`U>bdpmrz4vfVS#di^@*t;f8VG;#$4SDZZvdJibp zN&RX7j9EY0uWzdi|2ar;exZxB{AAZP;r*$-o0vJV>$!5yRQ;oXX7^ zKgW1qLj4jL)K3|ZwY<(@)Jmca03_Q$1-~lNNs#yJ~bC$UPggv~$-+Nr)z<|T^ z7YtfEt$-%J$hfk3R)w@@>qmP{xkpZRlMzl&a*xRSN)&k}X1V+!pbk{mGc1cN%@u*p zMj&R;VRwfNJ?2UWZd0L25r~06%=cytdpkkqSPeSFbg18iiCy2K$&I<%VRehf5USyl zVm?*-ld!Uj%KZEGyCB{JKL@=U)UKphwhQ_8>D4)JANJj@`8628ouI2gWA%GMx6h&4 zuL!gSfv$gi-}|2}c!_T^DWsUPN_Ztt9G{*)>6Y<|(z&`H2Lw-`BfW-1{=8IFpT|3z zx*|QS-@5Oh`Q9Vj%q<&so2PaSn?o4Vyh#(xig}aFxkpVmOQ+}idd^wo0@+`Eu~9;2<3u)i3>P&5QID)z>OFk8eK@_Ta<-3?4X6Ox!#X4S(%{ z61*fYna@DDv`ZBwX;K8b41t(W>UYXYhnRNg674R#;k(G(nD5Olf}t)`XVk^r8_ayt z=xq^)(u%E>#C+=ds9ZETKN<2)2i?+Sxz)rm_)cg2wV*SqF?VqL^I_X}K;H*l0=gH} z>ZDk<8u9k)(>ZVd9CVWfd|2aaAO;;YDFTXsB2YI1<&W>zEuy_0=#UhuW{L3GJmc}q zI%E@G&v(~09wFS_>ALmX&AYC9#C-k!&1U`f{pJ8xCm@z$p4l^Ou6uNs`QVLDn}57} zYsspGaKsEeT)1K;_VLfUh4Hu!&UpCh1Aey+HiO5yoO5_XncY(6R!i?+HOs7;oBG0% zdjx}ES(07hfPll9SmDs?>2AC)>}ntm6z>)LKFYked=w&ip9*e8*hstNMSNPX2y`$4 zu^gNHL&T@lIw1|ih*-9yAGDn?(%!M5{Ev6cp`sB$ELqXX_(iVdH*6d>Pwqj~iuY6b?IdiP zD*|H?f#5GiQ-{x|(>f_}Z`T+}gAOrJRhdL!vcjMU)PTV18h9FI;gVVI?cd52b1Mw) z5q}xzKr092b@6ZFAAr6K`UlV)P`4)jCVwC3#h^EV9s#v7DVD87yEAm@oOi}NbgC6g zVc7qGj_p(f)hf+IVEQ$ znOyO3*~*!@fd>gVoH2i5MH!XkSJR)4q_UkMU$)!~{nu|8GFLym9|0)0qJ=WE!4eru zUVYXky;cNz3;{kt79+L8sZ%r#K8+dc3F!0#xk$U#D*}puA`k}wu7;u0H&$X$^$oFv zw?Qr){jUXmqYb)6<$s~@3(%KAuaL?v%}<5h+^&jeiZGbc>ZDk<8u9k)(>ZVd9CVrm zC&Qp0fDY?4!_*!{KoRIZ1bQ;)Aa3Z?YauoDO_1#cXR7NxV(!^KY%aNJr&&33yg6)A zpRs=G5j^RQlsIzf1h;*>PPul%&}O-9A71?W@pBW0x5#L=llET_aIoh*OP~STrwxoZ zXCFS*4EB#Rw<5^!#I7N4lgs|=;a%nxM@})5_;`O#df{P%h9mzX0uN$0A9u$(2V2iB z?CDY9Wz{5dY8`()vLD`lzOME}%Hk7jIl_JpP2yBUDe|{u5lCmmdW7@K{if(Og41w?i zyD;7@*2;w2|Mu?92`4-Re_DQ|~s@oZK;K-i_oeSdp{RLfBgo0ydel)HJ^yTD+4dhXuReMf zf(~vP#0f0U=fI?fi(B>$o4cOflQ??maac5U!2H$Oi_G)^w*$zVPcY`QYqyx6JPkf1 zj&;xQA#=eGpLWh>Rwj<(a-W5BQ==D~*g2D`jYq~Uc^*IOQqidmdAu3*=7uMCo6q05 z8Ru7|5`>2}yN3>$|5&%(+`ehQ`OxWe%;Xy16(TLM#?Pt9t~Hl&OJ2*EYLzW~zj$hg zx#4l1xdCf2m((?4^KovDeVysUJuwb-HAkJoHtT~M}sUU(nIO&;Q5vEYj>BsK27 zuywu@GQp2EVN+P2a%)Ku=qdy@N3xcy3-}4e^Ay^hrbIL#7Jd;;R?%I8K&*gZmkeLc ztO0=wYT!|pQUuP!<$Tb_((0C9@hc>QAAEAV+k+4Mk@n9ZetG{MsMSfaY&GKT*Qayd z{yAv31^h|Qb8?lW&WaaIy=pfrA~efvqX z%+ELMH3vr=Z>bhBytG!X+_>c`%~}5}&UwJY@upzGgk`RK{FTi~4M6EmtZew~9h=-r zkCGN%xXppY{`-N|mA7v(fA*X?B~I|NZkY=D+$l@sWSXm9);<2J67v`s6ma)|4G{SI zft<7(Ev`K~a^4PM9q*+~l5w-eLE+yKCA>~crfYI3GLi}{C<5J$KupIbpTOPL0q|(h zp#vsCO{g1zaIeX_J8OrYM_?&NcuvocviSq#?@4Y4w5Iu@8giauUT5TeN(>_VNhYm_H$$5@JHcg$-XJ$_9w}(jEHY>u*NyH_EZ1=)zZfmsWw$0|^RkO{6 zlC5|OyVTsk@#kS6<3EV@FW#}aAm9KsGF_Yqj~Rm$KYntLdCB2b&YEQ3%mnh1_K1EN zh}f8|m-a?;k(=$G)k8j-nLZ_h~+s0l?Tdjq&m8@GB#5KAQDQO=XOVT34f?Q-Tg+BftJK z_&_=u#Q7y*jd$0iNwD;*pp!vvNqT!sS1j9udpou2oVOGG*6Usjm928#(?RtU&@~`# zCASXLXnIbg9krh#pa`@E0Y2oeDT@IA#AcIsN{e&aHS>3gIBi)Yzf+JWPFuDU=6s!1 zSewDO?r|yZP}~~aixYyC;uLpxm*5Vi6lj4KcPLhhI~2F#?(QKJC%B&UfA+pQd!MU3 zPi~TL=9{%<*808gBnUY-;hKv^Qqnn$D&?26iidU`HnvqCPGzGHUmgp)4LV7vzs^gb z-#-|v80m#aYS&dqWIP$~htfZPGCo&!vrxQL`CsmTF^=t< z{vK@eaNiVyDR#i1CwzgjHfgbD%GrBrRZ0VTTC~Fsg&ZDremW`jN^bd=>)@LoJMK3> z+fDV9Pbe?Wrge(1_*gZ@xXft-D!601(q!q1XR@TCRNuoGUe}2Ha3YatB$&+WUCen= zEZA>KpGzjMuV6r8pYnH*i@O#CVqb-ziV|EJO={->q}CG1dG74cDSqjBPg>Ov5@*9m z5!c?m4Z&Lrvt){41`8VFY_|~Xht7w-BXR@zsK9gQ?h6^XVymG@_vTr~}i}6&uy8 zw*>5ze^wkTF>a^ZSKJ_BdQN4>v6NwpEPl%;in6bqof@091F<-#AP8L7T`9>G4em#yT8RM5%*>|wEgqS^^3fmVi#f6S=*EA%Ju3L&ljm|$s zjh07aFA_6-nHw`T(^Kbw&UH9!)7kR5<)o>nW*wUCtNAYW)iSp3q+}a;gZtjBL~>IJ zRjtKFh&S6PnY`MnJ@QljiFNl%uik(FDgdwXjo}TLogRcD&bSk?_xEw9jIlL|!K<+t zMyvw-;Vpr$(xcq34gBjD(6o4HTsl3Sq%w6DK6Rm*wN?nuH+rmL9~Z_-4MdnAi4M$2 z?`Eg1BNud_-CIXdl<0Xceys1Jf=@x{ss+hv&++P@=;_DD4J3C;=6?gv^M88|l`o+;A+E(`I zD5cV$P0~6wBqirkldY_E$|iC9TGq`y-=|CT4yEZgpmcxgpBZ+&Bcl-1KvQsRDafmX7PU(pDly(KPE8n8dRR;H4B*jal z{#8`I@S>EjwC?{==C)>5D-xFTL_f)@AU2Ed#X&-6sGoR;)kn`RVwj&4XCw@6sSQ(0 z5woq>t;`lrS%K`m(*v~PsCHGToO(+QUszWaQR2nX?0e}`n(tTU6L_v!wvII|oC>HN z(~#=;PdoeX5TY@KN+I8yR-653aqGX7wPm2{g~UXu(Vg*pv?j4|oj$?R;BG1NPFv!0 z0`KI(Hq4V!(cQlil32Gn75V0pD{mH^DFi8~X7!ZjQ$La|YWC0?eH7=g)ok$Int=a- z$oQ7dJtulh;l&4XT1155wGv>{!cTcX6v`(>M3c{Er#!H(As$~=E1a|AMx%{b5XXrmoj-dcr3npdZDM>QTTzsApaj=vcWb;)3~DAWwBI#yUIn3ZZq} z??#0ockuD*v&{=}Z3uE9V$^54GCR4vL;~n@o=lFsMq9G}Zts1yGB6+;7O<*wE=N@d z{!@!=;?z(-<#!l?)yTQ0hKx7=b)=_m!lSSAwpqj< zykD&!Uy)dQ;7>($458)ZiC%>PS}ICv+Ya86+jlT`@F<*A{tc=SefCF1d*t%%TIzx_ zA%$gi^HR!MreCdwM~i=1MDfD)^m$o+cG`K$Jj=^sN|GQpuPB2#lL~?H2WuCiIpBK{ zb2sA;}vCg367*pcA!+zhouUKz;x$z zbN2D2NnS_1WE3(V-2pJarMD-^gfd_nZHt@KZleC%YsCKX}D0mdTMift_ z;XQ#m$}m3kYn}`=OID=CYRR-DxhoVqdg9Z`6;F4n%mDjyO^y|HX&}<7SmcldLl`Qi z_$2CX1Oa18>Z7#Dn|m>e%5^k47WN(Cl1K7`{wOuxT>oRcmh&HBvoUwJpx*WPwoJd=<*x8f$qx|4&mHbNiT(ku-=o;+AA^pp+;Gp<-=A2I z7nSYQ^XV%aO?A8sni?FWe=ND2UP$p)U~9<30OKJE$?_$yG>HSg9~t!j5qCPdH~6aF z@tfc*ciQXbumM--9Tk2P{^YDNh8tFPs!!?=wKHd@Z|=qvso+HO0R3NAkkQ?Zj(T|% z4xOli(g%YiqbpUZnxpKNaxW%$%b9^B-c?+-llS{=d%{D}`ÌxVR`Af2&!`w*hr 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 QaDO+h8C^yG_2Ak60mFMH^8f$< literal 0 HcmV?d00001 diff --git a/gallery/src/pages/design.home-assistant.io/editing.markdown b/gallery/src/pages/design.home-assistant.io/editing.markdown index 2a503ced6a..742a79eeea 100644 --- a/gallery/src/pages/design.home-assistant.io/editing.markdown +++ b/gallery/src/pages/design.home-assistant.io/editing.markdown @@ -2,6 +2,8 @@ title: Editing design.home-assistant.io --- +![Home Assistant Logo](/images/logo-with-text.png) + # How to edit design.home-assistant.io All pages are stored in [the pages folder][pages-folder] on GitHub. Pages are grouped in a folder per sidebar section. Each page can contain a `.markdown` description file, a `.ts` demo file or both. If both are defined the description is rendered first. The description can contain metadata to specify the title of the page. @@ -41,15 +43,12 @@ import { html, css, LitElement } from "lit"; import { customElement } from "lit/decorators"; import "../../../../src/components/ha-card"; - @customElement("demo-user-experience-usability") export class DemoUserExperienceUsability extends LitElement { protected render() { return html` -
- Hello world! -
+
Hello world!
`; } From 4e0fc8ee08243ea3e8559b05c4a1d94622b8318b Mon Sep 17 00:00:00 2001 From: Zack Date: Tue, 15 Mar 2022 14:26:21 -0500 Subject: [PATCH 052/142] Update Translations --- .../config/devices/device-detail/ha-device-entities-card.ts | 2 +- src/translations/en.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/panels/config/devices/device-detail/ha-device-entities-card.ts b/src/panels/config/devices/device-detail/ha-device-entities-card.ts index 488feaf07c..19e60da9ee 100644 --- a/src/panels/config/devices/device-detail/ha-device-entities-card.ts +++ b/src/panels/config/devices/device-detail/ha-device-entities-card.ts @@ -102,7 +102,7 @@ export class HaDeviceEntitiesCard extends LitElement { ? html`
+
+ ${(this._disabledBy && this._disabledBy !== "user") || 1 + ? this.hass.localize( + "ui.dialogs.entity_registry.editor.enabled_cause", + "cause", + this.hass.localize( + `config_entry.disabled_by.${this._disabledBy}` + ) + ) + : ""} +
Date: Tue, 15 Mar 2022 14:33:11 -0500 Subject: [PATCH 054/142] remove 1 --- src/panels/config/entities/entity-registry-settings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index 5251e297b5..5a22bec551 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -227,7 +227,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { )}:
- ${(this._disabledBy && this._disabledBy !== "user") || 1 + ${this._disabledBy && this._disabledBy !== "user" ? this.hass.localize( "ui.dialogs.entity_registry.editor.enabled_cause", "cause", From a27dd1e7f12cd9f873ee887a291e21de0a6f2e91 Mon Sep 17 00:00:00 2001 From: Zack Date: Tue, 15 Mar 2022 14:47:15 -0500 Subject: [PATCH 055/142] Add Description of chosen --- .../entities/entity-registry-settings.ts | 41 +++++++++++++------ src/translations/en.json | 1 - 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index 5a22bec551..a11803f6db 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -285,13 +285,39 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
+ ${this._disabledBy !== null + ? html` +
+ ${this.hass.localize( + "ui.dialogs.entity_registry.editor.enabled_description" + )} +
+ ` + : this._hiddenBy !== null + ? html` +
+ ${this.hass.localize( + "ui.dialogs.entity_registry.editor.hidden_description" + )} +
+ ` + : ""} ${this.entry.device_id ? html` -

+ +

${this.hass.localize( "ui.dialogs.entity_registry.editor.area_note" )} -

+
${this._areaId ? html`${this.hass.localize( @@ -305,15 +331,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { )}` : ""} - ` : ""} @@ -515,7 +532,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { .label { margin-top: 8px; } - p { + .secondary { margin: 8px 0; width: 340px; } diff --git a/src/translations/en.json b/src/translations/en.json index 9a3255711b..fe235befe7 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -106,7 +106,6 @@ "hidden_by": { "user": "User", "integration": "Integration", - "config_entry": "Config entry", "device": "Device" } }, From d5a307f8f48fab0e50f146bd88c038700a5baf73 Mon Sep 17 00:00:00 2001 From: Zack Date: Tue, 15 Mar 2022 15:00:35 -0500 Subject: [PATCH 056/142] Add icons and buttons --- .../config/entities/ha-config-entities.ts | 51 ++++++++++++++++++- src/translations/en.json | 6 +++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index 87c9fb2336..4fe64de1a8 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -3,6 +3,7 @@ import { mdiAlertCircle, mdiCancel, mdiDelete, + mdiEyeOff, mdiFilterVariant, mdiPencilOff, mdiPlus, @@ -251,7 +252,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { filterable: true, width: "68px", template: (_status, entity: EntityRow) => - entity.unavailable || entity.disabled_by || entity.readonly + entity.unavailable || + entity.disabled_by || + entity.hidden_by || + entity.readonly ? html`
@@ -282,6 +288,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { ? this.hass.localize( "ui.panel.config.entities.picker.status.disabled" ) + : entity.hidden_by + ? this.hass.localize( + "ui.panel.config.entities.picker.status.hidden" + ) : this.hass.localize( "ui.panel.config.entities.picker.status.readonly" )} @@ -543,6 +553,11 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { "ui.panel.config.entities.picker.disable_selected.button" )} + ${this.hass.localize( + "ui.panel.config.entities.picker.hide_selected.button" + )} ${this.hass.localize( "ui.panel.config.entities.picker.remove_selected.button" @@ -572,6 +587,17 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { "ui.panel.config.entities.picker.disable_selected.button" )} + + + ${this.hass.localize( + "ui.panel.config.entities.picker.hide_selected.button" + )} + { + this._selectedEntities.forEach((entity) => + updateEntityRegistryEntry(this.hass, entity, { + hidden_by: "user", + }) + ); + this._clearSelection(); + }, + }); + } + private _removeSelected() { const removeableEntities = this._selectedEntities.filter((entity) => { const stateObj = this.hass.states[entity]; diff --git a/src/translations/en.json b/src/translations/en.json index fe235befe7..c87066ee6c 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -297,6 +297,7 @@ "remove": "Remove", "enable": "Enable", "disable": "Disable", + "hide": "Hide", "close": "Close", "clear": "Clear", "leave": "Leave", @@ -2455,6 +2456,11 @@ "confirm_partly_title": "Only {number} {number, plural,\n one {selected entity}\n other {selected entities}\n} can be removed.", "confirm_text": "You should remove them from your Lovelace config and automations if they contain these entities.", "confirm_partly_text": "You can only remove {removable} of the selected {selected} entities. Entities can only be removed when the integration is no longer providing the entities. Sometimes you have to restart Home Assistant before you can remove the entities of a removed integration. Are you sure you want to remove the removable entities?" + }, + "hide_selected": { + "button": "Hide selected", + "confirm_title": "Do you want to hide {number} {number, plural,\n one {entity}\n other {entities}\n}?", + "confirm_text": "Hidden entities will not be shown on your dashboard. Their history is still tracked and you can still interact with them with services." } } }, From 5c53bc422599cc483f7fab097767a61095688a66 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 15 Mar 2022 17:34:02 -0500 Subject: [PATCH 057/142] Add Color RGB Selector (#12039) --- gallery/src/pages/components/ha-selector.ts | 3 +- .../ha-selector/ha-selector-color-rgb.ts | 58 +++++++++++++++++++ src/components/ha-selector/ha-selector.ts | 1 + src/data/selector.ts | 8 ++- 4 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 src/components/ha-selector/ha-selector-color-rgb.ts diff --git a/gallery/src/pages/components/ha-selector.ts b/gallery/src/pages/components/ha-selector.ts index 9f585d051a..2ea007d913 100644 --- a/gallery/src/pages/components/ha-selector.ts +++ b/gallery/src/pages/components/ha-selector.ts @@ -173,10 +173,11 @@ const SCHEMAS: { name: "Location with radius", selector: { location: { radius: true, icon: "mdi:home" } }, }, - "color-temp": { + color_temp: { name: "Color Temperature", selector: { color_temp: {} }, }, + color_rgb: { name: "Color", selector: { color_rgb: {} } }, }, }, { diff --git a/src/components/ha-selector/ha-selector-color-rgb.ts b/src/components/ha-selector/ha-selector-color-rgb.ts new file mode 100644 index 0000000000..5357a80db3 --- /dev/null +++ b/src/components/ha-selector/ha-selector-color-rgb.ts @@ -0,0 +1,58 @@ +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import type { HomeAssistant } from "../../types"; +import type { ColorRGBSelector } from "../../data/selector"; +import { fireEvent } from "../../common/dom/fire_event"; +import { hex2rgb, rgb2hex } from "../../common/color/convert-color"; +import "../ha-textfield"; + +@customElement("ha-selector-color_rgb") +export class HaColorRGBSelector extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public selector!: ColorRGBSelector; + + @property() public value?: string; + + @property() public label?: string; + + @property({ type: Boolean, reflect: true }) public disabled = false; + + protected render() { + return html` + + `; + } + + private _valueChanged(ev: CustomEvent) { + const value = (ev.target as any).value; + fireEvent(this, "value-changed", { + value: hex2rgb(value), + }); + } + + static styles = css` + :host { + display: flex; + justify-content: flex-end; + align-items: center; + } + ha-textfield { + --text-field-padding: 8px; + min-width: 75px; + flex-grow: 1; + margin: 0 4px; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-color_rgb": HaColorRGBSelector; + } +} diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index 25c570a8e9..4aa268ee23 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -8,6 +8,7 @@ import "./ha-selector-addon"; import "./ha-selector-area"; import "./ha-selector-attribute"; import "./ha-selector-boolean"; +import "./ha-selector-color-rgb"; import "./ha-selector-device"; import "./ha-selector-duration"; import "./ha-selector-entity"; diff --git a/src/data/selector.ts b/src/data/selector.ts index 4e75ad8268..d4d83c6ccb 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -17,7 +17,8 @@ export type Selector = | MediaSelector | ThemeSelector | LocationSelector - | ColorTempSelector; + | ColorTempSelector + | ColorRGBSelector; export interface EntitySelector { entity: { @@ -34,6 +35,11 @@ export interface AttributeSelector { }; } +export interface ColorRGBSelector { + // eslint-disable-next-line @typescript-eslint/ban-types + color_rgb: {}; +} + export interface DeviceSelector { device: { integration?: string; From 4ac097f32bc1725854b23635df02402a574bab44 Mon Sep 17 00:00:00 2001 From: Zack Date: Wed, 16 Mar 2022 14:20:45 -0500 Subject: [PATCH 058/142] Add Date Selector --- gallery/src/pages/components/ha-selector.ts | 1 + .../ha-selector/ha-selector-date.ts | 42 +++++++++++++++++++ src/components/ha-selector/ha-selector.ts | 1 + src/data/selector.ts | 6 +++ 4 files changed, 50 insertions(+) create mode 100644 src/components/ha-selector/ha-selector-date.ts diff --git a/gallery/src/pages/components/ha-selector.ts b/gallery/src/pages/components/ha-selector.ts index 2ea007d913..b5110c71fb 100644 --- a/gallery/src/pages/components/ha-selector.ts +++ b/gallery/src/pages/components/ha-selector.ts @@ -146,6 +146,7 @@ const SCHEMAS: { }, boolean: { name: "Boolean", selector: { boolean: {} } }, time: { name: "Time", selector: { time: {} } }, + date: { name: "Date", selector: { date: {} } }, action: { name: "Action", selector: { action: {} } }, text: { name: "Text", diff --git a/src/components/ha-selector/ha-selector-date.ts b/src/components/ha-selector/ha-selector-date.ts new file mode 100644 index 0000000000..a19438a1b4 --- /dev/null +++ b/src/components/ha-selector/ha-selector-date.ts @@ -0,0 +1,42 @@ +import { html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import type { HomeAssistant } from "../../types"; +import type { DateSelector } from "../../data/selector"; +import { fireEvent } from "../../common/dom/fire_event"; +import "../ha-date-input"; + +@customElement("ha-selector-date") +export class HaDateSelector extends LitElement { + @property() public hass!: HomeAssistant; + + @property() public selector!: DateSelector; + + @property() public value?: string; + + @property() public label?: string; + + @property({ type: Boolean, reflect: true }) public disabled = false; + + protected render() { + return html` + + + `; + } + + private _valueChanged(ev: CustomEvent) { + fireEvent(this, "value-changed", { value: ev.detail.value }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-date": HaDateSelector; + } +} diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index 4aa268ee23..9986de7ecc 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -9,6 +9,7 @@ import "./ha-selector-area"; import "./ha-selector-attribute"; import "./ha-selector-boolean"; import "./ha-selector-color-rgb"; +import "./ha-selector-date"; import "./ha-selector-device"; import "./ha-selector-duration"; import "./ha-selector-entity"; diff --git a/src/data/selector.ts b/src/data/selector.ts index d4d83c6ccb..ab5878180b 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -2,6 +2,7 @@ export type Selector = | AddonSelector | AttributeSelector | EntitySelector + | DateSelector | DeviceSelector | DurationSelector | AreaSelector @@ -40,6 +41,11 @@ export interface ColorRGBSelector { color_rgb: {}; } +export interface DateSelector { + // eslint-disable-next-line @typescript-eslint/ban-types + date: {}; +} + export interface DeviceSelector { device: { integration?: string; From d5010dda9e8b2164babc35dcf5ad87f249cf7824 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Mar 2022 14:12:10 -0700 Subject: [PATCH 059/142] Add ha-form context (#12062) --- gallery/src/pages/components/ha-form.ts | 1 + src/components/ha-form/ha-form.ts | 16 ++++++ src/components/ha-form/types.ts | 1 + .../ha-selector/ha-selector-attribute.ts | 51 ++++++++++++++++++- src/components/ha-selector/ha-selector.ts | 3 ++ src/data/selector.ts | 2 +- 6 files changed, 71 insertions(+), 3 deletions(-) diff --git a/gallery/src/pages/components/ha-form.ts b/gallery/src/pages/components/ha-form.ts index cab77bacfc..fb2cec39af 100644 --- a/gallery/src/pages/components/ha-form.ts +++ b/gallery/src/pages/components/ha-form.ts @@ -139,6 +139,7 @@ const SCHEMAS: { { name: "Attribute", selector: { attribute: { entity_id: "" } }, + context: { filter_entity: "entity" }, }, { name: "Device", selector: { device: {} } }, { name: "Duration", selector: { duration: {} } }, diff --git a/src/components/ha-form/ha-form.ts b/src/components/ha-form/ha-form.ts index e1e87d86f9..f3db63e2c1 100644 --- a/src/components/ha-form/ha-form.ts +++ b/src/components/ha-form/ha-form.ts @@ -106,6 +106,7 @@ export class HaForm extends LitElement implements HaFormElement { .disabled=${this.disabled} .helper=${this._computeHelper(item)} .required=${item.required || false} + .context=${this._generateContext(item)} >` : dynamicElement(`ha-form-${item.type}`, { schema: item, @@ -115,6 +116,7 @@ export class HaForm extends LitElement implements HaFormElement { hass: this.hass, computeLabel: this.computeLabel, computeHelper: this.computeHelper, + context: this._generateContext(item), })} `; })} @@ -122,6 +124,20 @@ export class HaForm extends LitElement implements HaFormElement { `; } + private _generateContext( + schema: HaFormSchema + ): Record | undefined { + if (!schema.context) { + return undefined; + } + + const context = {}; + for (const [context_key, data_key] of Object.entries(schema.context)) { + context[context_key] = this.data[data_key]; + } + return context; + } + protected createRenderRoot() { const root = super.createRenderRoot(); // attach it as soon as possible to make sure we fetch all events. diff --git a/src/components/ha-form/types.ts b/src/components/ha-form/types.ts index 2c3a9912d4..d8e380cca4 100644 --- a/src/components/ha-form/types.ts +++ b/src/components/ha-form/types.ts @@ -24,6 +24,7 @@ export interface HaFormBaseSchema { // This value will be set initially when form is loaded suggested_value?: HaFormData; }; + context?: Record; } export interface HaFormGridSchema extends HaFormBaseSchema { diff --git a/src/components/ha-selector/ha-selector-attribute.ts b/src/components/ha-selector/ha-selector-attribute.ts index 5739089061..a75f9bb5ec 100644 --- a/src/components/ha-selector/ha-selector-attribute.ts +++ b/src/components/ha-selector/ha-selector-attribute.ts @@ -1,9 +1,10 @@ import "../entity/ha-entity-attribute-picker"; -import { html, LitElement } from "lit"; +import { html, LitElement, PropertyValues } from "lit"; import { customElement, property } from "lit/decorators"; import { AttributeSelector } from "../../data/selector"; import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../types"; +import { fireEvent } from "../../common/dom/fire_event"; @customElement("ha-selector-attribute") export class HaSelectorAttribute extends SubscribeMixin(LitElement) { @@ -17,11 +18,16 @@ export class HaSelectorAttribute extends SubscribeMixin(LitElement) { @property({ type: Boolean }) public disabled = false; + @property() public context?: { + filter_entity?: string; + }; + protected render() { return html` `; } + + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + if ( + // No need to filter value if no value + !this.value || + // Only adjust value if we used the context + this.selector.attribute.entity_id || + // Only check if context has changed + !changedProps.has("context") + ) { + return; + } + + const oldContext = changedProps.get("context") as this["context"]; + + if ( + !this.context || + oldContext?.filter_entity === this.context.filter_entity + ) { + return; + } + + // Validate that that the attribute is still valid for this entity, else unselect. + let invalid = false; + if (this.context.filter_entity) { + const stateObj = this.hass.states[this.context.filter_entity]; + + if (!(stateObj && this.value in stateObj.attributes)) { + invalid = true; + } + } else { + invalid = this.value !== undefined; + } + + if (invalid) { + fireEvent(this, "value-changed", { + value: undefined, + }); + } + } } declare global { diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index 4aa268ee23..74ad6cf623 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -42,6 +42,8 @@ export class HaSelector extends LitElement { @property({ type: Boolean }) public required = true; + @property() public context?: Record; + public focus() { this.shadowRoot?.getElementById("selector")?.focus(); } @@ -61,6 +63,7 @@ export class HaSelector extends LitElement { disabled: this.disabled, required: this.required, helper: this.helper, + context: this.context, id: "selector", })} `; diff --git a/src/data/selector.ts b/src/data/selector.ts index d4d83c6ccb..8d8c062b08 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -31,7 +31,7 @@ export interface EntitySelector { export interface AttributeSelector { attribute: { - entity_id: string; + entity_id?: string; }; } From ab5df0fe6eaa5389de9b0da33f8deac6f3eafd3b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Mar 2022 14:13:13 -0700 Subject: [PATCH 060/142] test condition (#11925) --- src/components/buttons/ha-progress-button.ts | 60 ++++++++++----- .../condition/ha-automation-condition-row.ts | 75 ++++++++++++++++++- .../trigger/ha-automation-trigger-row.ts | 2 +- src/translations/en.json | 3 + 4 files changed, 120 insertions(+), 20 deletions(-) diff --git a/src/components/buttons/ha-progress-button.ts b/src/components/buttons/ha-progress-button.ts index 3272e8be1d..d025cb7ff3 100644 --- a/src/components/buttons/ha-progress-button.ts +++ b/src/components/buttons/ha-progress-button.ts @@ -1,8 +1,9 @@ import "@material/mwc-button"; -import type { Button } from "@material/mwc-button"; +import { mdiAlertOctagram, mdiCheckBold } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, query } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import "../ha-circular-progress"; +import "../ha-svg-icon"; @customElement("ha-progress-button") export class HaProgressButton extends LitElement { @@ -12,38 +13,53 @@ export class HaProgressButton extends LitElement { @property({ type: Boolean }) public raised = false; - @query("mwc-button", true) private _button?: Button; + @state() private _result?: "success" | "error"; public render(): TemplateResult { + const overlay = this._result || this.progress; return html` - ${this.progress - ? html`
- -
` - : ""} + ${!overlay + ? "" + : html` +
+ ${this._result === "success" + ? html`` + : this._result === "error" + ? html`` + : this.progress + ? html` + + ` + : ""} +
+ `} `; } public actionSuccess(): void { - this._tempClass("success"); + this._setResult("success"); } public actionError(): void { - this._tempClass("error"); + this._setResult("error"); } - private _tempClass(className: string): void { - this._button!.classList.add(className); + private _setResult(result: "success" | "error"): void { + this._result = result; setTimeout(() => { - this._button!.classList.remove(className); - }, 1000); + this._result = undefined; + }, 2000); } private _buttonTapped(ev: Event): void { @@ -69,6 +85,7 @@ export class HaProgressButton extends LitElement { background-color: var(--success-color); transition: none; border-radius: 4px; + pointer-events: none; } mwc-button[raised].success { @@ -81,6 +98,7 @@ export class HaProgressButton extends LitElement { background-color: var(--error-color); transition: none; border-radius: 4px; + pointer-events: none; } mwc-button[raised].error { @@ -89,13 +107,21 @@ export class HaProgressButton extends LitElement { } .progress { - bottom: 0; - margin-top: 4px; + bottom: 4px; position: absolute; text-align: center; - top: 0; + top: 4px; width: 100%; } + + ha-svg-icon { + color: white; + } + + mwc-button.success slot, + mwc-button.error slot { + visibility: hidden; + } `; } } diff --git a/src/panels/config/automation/condition/ha-automation-condition-row.ts b/src/panels/config/automation/condition/ha-automation-condition-row.ts index 4bce08a441..8585e232e9 100644 --- a/src/panels/config/automation/condition/ha-automation-condition-row.ts +++ b/src/panels/config/automation/condition/ha-automation-condition-row.ts @@ -7,12 +7,18 @@ import { fireEvent } from "../../../../common/dom/fire_event"; import { handleStructError } from "../../../../common/structs/handle-errors"; import "../../../../components/ha-button-menu"; import "../../../../components/ha-card"; +import "../../../../components/buttons/ha-progress-button"; +import type { HaProgressButton } from "../../../../components/buttons/ha-progress-button"; import "../../../../components/ha-icon-button"; -import { Condition } from "../../../../data/automation"; -import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; +import { Condition, testCondition } from "../../../../data/automation"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../../../dialogs/generic/show-dialog-box"; import { haStyle } from "../../../../resources/styles"; import { HomeAssistant } from "../../../../types"; import "./ha-automation-condition-editor"; +import { validateConfig } from "../../../../data/config"; export interface ConditionElement extends LitElement { condition: Condition; @@ -61,6 +67,11 @@ export default class HaAutomationConditionRow extends LitElement {
+ + ${this.hass.localize( + "ui.panel.config.automation.editor.conditions.test" + )} + Date: Wed, 16 Mar 2022 14:14:25 -0700 Subject: [PATCH 061/142] Revamp URL form (#12060) --- src/common/string/is_ip_address.ts | 4 + src/data/cloud.ts | 2 + src/panels/config/core/ha-config-url-form.ts | 260 ++++++++++++++++--- src/translations/en.json | 15 +- 4 files changed, 240 insertions(+), 41 deletions(-) create mode 100644 src/common/string/is_ip_address.ts diff --git a/src/common/string/is_ip_address.ts b/src/common/string/is_ip_address.ts new file mode 100644 index 0000000000..8f0277176a --- /dev/null +++ b/src/common/string/is_ip_address.ts @@ -0,0 +1,4 @@ +const regexp = + /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + +export const isIPAddress = (input: string): boolean => regexp.test(input); diff --git a/src/data/cloud.ts b/src/data/cloud.ts index e8add4f4ef..49a4244a1f 100644 --- a/src/data/cloud.ts +++ b/src/data/cloud.ts @@ -6,6 +6,7 @@ import { AutomationConfig } from "./automation"; interface CloudStatusNotLoggedIn { logged_in: false; cloud: "disconnected" | "connecting" | "connected"; + http_use_ssl: boolean; } export interface GoogleEntityConfig { @@ -59,6 +60,7 @@ export interface CloudStatusLoggedIn { remote_connected: boolean; remote_certificate: undefined | CertificateInformation; http_use_ssl: boolean; + active_subscription: boolean; } export type CloudStatus = CloudStatusNotLoggedIn | CloudStatusLoggedIn; diff --git a/src/panels/config/core/ha-config-url-form.ts b/src/panels/config/core/ha-config-url-form.ts index df25744fea..e74546ce7b 100644 --- a/src/panels/config/core/ha-config-url-form.ts +++ b/src/panels/config/core/ha-config-url-form.ts @@ -1,12 +1,25 @@ import "@material/mwc-button/mwc-button"; -import "@polymer/paper-input/paper-input"; -import type { PaperInputElement } from "@polymer/paper-input/paper-input"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; import { customElement, property, state } from "lit/decorators"; +import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import "../../../components/ha-card"; +import "../../../components/ha-switch"; +import "../../../components/ha-alert"; +import "../../../components/ha-formfield"; +import "../../../components/ha-textfield"; +import type { HaTextField } from "../../../components/ha-textfield"; +import { CloudStatus, fetchCloudStatus } from "../../../data/cloud"; import { saveCoreConfig } from "../../../data/core"; import type { PolymerChangedEvent } from "../../../polymer-types"; import type { HomeAssistant } from "../../../types"; +import { isIPAddress } from "../../../common/string/is_ip_address"; @customElement("ha-config-url-form") class ConfigUrlForm extends LitElement { @@ -20,18 +33,48 @@ class ConfigUrlForm extends LitElement { @state() private _internal_url?: string; + @state() private _cloudStatus?: CloudStatus | null; + + @state() private _showCustomExternalUrl = false; + + @state() private _showCustomInternalUrl = false; + protected render(): TemplateResult { const canEdit = ["storage", "default"].includes( this.hass.config.config_source ); const disabled = this._working || !canEdit; - if (!this.hass.userData?.showAdvanced) { + if (!this.hass.userData?.showAdvanced || this._cloudStatus === undefined) { return html``; } + const internalUrl = this._internalUrlValue; + const externalUrl = this._externalUrlValue; + let hasCloud: boolean; + let remoteEnabled: boolean; + let httpUseHttps: boolean; + + if (this._cloudStatus === null) { + hasCloud = false; + remoteEnabled = false; + httpUseHttps = false; + } else { + httpUseHttps = this._cloudStatus.http_use_ssl; + + if (this._cloudStatus.logged_in) { + hasCloud = true; + remoteEnabled = + this._cloudStatus.active_subscription && + this._cloudStatus.prefs.remote_enabled; + } else { + hasCloud = false; + remoteEnabled = false; + } + } + return html` - +
${!canEdit ? html` @@ -43,46 +86,147 @@ class ConfigUrlForm extends LitElement { ` : ""} ${this._error ? html`
${this._error}
` : ""} -
-
- ${this.hass.localize( - "ui.panel.config.core.section.core.core_config.external_url" - )} -
- - +
+ ${this.hass.localize("ui.panel.config.url.description")}
+ ${hasCloud + ? html` +
+
+ ${this.hass.localize( + "ui.panel.config.url.external_url_label" + )} +
+ + + +
+ ` + : ""} + ${!this._showCustomExternalUrl + ? "" + : html` +
+
+ ${hasCloud + ? "" + : this.hass.localize( + "ui.panel.config.url.external_url_label" + )} +
+ + +
+ `} + ${hasCloud || !isComponentLoaded(this.hass, "cloud") + ? "" + : html` +
+ `} + ${!this._showCustomExternalUrl && hasCloud + ? html` + ${remoteEnabled + ? "" + : html` + + ${this.hass.localize( + "ui.panel.config.url.ha_cloud_remote_not_enabled" + )} + + + `} + ` + : ""} +
- ${this.hass.localize( - "ui.panel.config.core.section.core.core_config.internal_url" - )} + ${this.hass.localize("ui.panel.config.url.internal_url_label")}
- - + +
+ + ${!this._showCustomInternalUrl + ? "" + : html` +
+
+ + +
+ `} + ${ + // If the user has configured a cert, show an error if + httpUseHttps && // there is no internal url configured + (!internalUrl || + // the internal url does not start with https + !internalUrl.startsWith("https://") || + // the internal url points at an IP address + isIPAddress(new URL(internalUrl).hostname)) + ? html` + + ${this.hass.localize( + "ui.panel.config.url.internal_url_https_error_description" + )} + + ` + : "" + }
@@ -95,6 +239,24 @@ class ConfigUrlForm extends LitElement { `; } + protected override firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + + this._showCustomInternalUrl = this._internalUrlValue !== null; + + if (isComponentLoaded(this.hass, "cloud")) { + fetchCloudStatus(this.hass).then((cloudStatus) => { + if (cloudStatus.logged_in) { + this._cloudStatus = cloudStatus; + this._showCustomExternalUrl = this._externalUrlValue !== null; + } + }); + } else { + this._cloudStatus = null; + this._showCustomExternalUrl = true; + } + } + private get _internalUrlValue() { return this._internal_url !== undefined ? this._internal_url @@ -107,9 +269,17 @@ class ConfigUrlForm extends LitElement { : this.hass.config.external_url; } + private _toggleCloud(ev) { + this._showCustomExternalUrl = !ev.currentTarget.checked; + } + + private _toggleInternalAutomatic(ev) { + this._showCustomInternalUrl = !ev.currentTarget.checked; + } + private _handleChange(ev: PolymerChangedEvent) { - const target = ev.currentTarget as PaperInputElement; - this[`_${target.name}`] = target.value; + const target = ev.currentTarget as HaTextField; + this[`_${target.name}`] = target.value || null; } private async _save() { @@ -117,8 +287,12 @@ class ConfigUrlForm extends LitElement { this._error = undefined; try { await saveCoreConfig(this.hass, { - external_url: this._external_url || null, - internal_url: this._internal_url || null, + external_url: this._showCustomExternalUrl + ? this._external_url || null + : null, + internal_url: this._showCustomInternalUrl + ? this._internal_url || null + : null, }); } catch (err: any) { this._error = err.message || err; @@ -129,11 +303,15 @@ class ConfigUrlForm extends LitElement { static get styles(): CSSResultGroup { return css` + .description { + margin-bottom: 1em; + } .row { display: flex; flex-direction: row; margin: 0 -8px; align-items: center; + padding: 8px 0; } .secondary { @@ -154,6 +332,10 @@ class ConfigUrlForm extends LitElement { .card-actions { text-align: right; } + + a { + color: var(--primary-color); + } `; } } diff --git a/src/translations/en.json b/src/translations/en.json index 6880894b7f..45661e7f8e 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1355,13 +1355,24 @@ "metric_example": "Celsius, kilograms", "find_currency_value": "Find your value", "save_button": "Save", - "external_url": "External URL", - "internal_url": "Internal URL", "currency": "Currency" } } } }, + "url": { + "caption": "Home Assistant URL", + "description": "Configure what website addresses Home Assistant should share with other devices when they need to fetch data from Home Assistant (eg. to play text-to-speech or other hosted media).", + "internal_url_label": "Local Network", + "external_url_label": "Internet", + "external_use_ha_cloud": "Use Home Assistant Cloud", + "external_get_ha_cloud": "Access from anywhere using Home Assistant Cloud", + "ha_cloud_remote_not_enabled": "Your Home Assistant Cloud remote connection is currently not enabled.", + "enable_remote": "[%key:ui::common::enable%]", + "internal_url_automatic": "Automatic", + "internal_url_https_error_title": "Invalid local network URL", + "internal_url_https_error_description": "You have configured an HTTPS certificate in Home Assistant. This means that your internal URL needs to be set to a domain covered by the certficate." + }, "info": { "caption": "Info", "copy_menu": "Copy menu", From 9908162ac296d7fa12c7e69367f795bf90a3c540 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Mar 2022 14:14:38 -0700 Subject: [PATCH 062/142] Add support for menu data entry flow option (#12055) --- src/data/data_entry_flow.ts | 21 +++-- .../config-flow/dialog-data-entry-flow.ts | 11 ++- .../config-flow/show-dialog-config-flow.ts | 15 ++++ .../show-dialog-data-entry-flow.ts | 9 +++ .../config-flow/show-dialog-options-flow.ts | 15 ++++ src/dialogs/config-flow/step-flow-menu.ts | 80 +++++++++++++++++++ 6 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 src/dialogs/config-flow/step-flow-menu.ts diff --git a/src/data/data_entry_flow.ts b/src/data/data_entry_flow.ts index d617b37020..14821f8b77 100644 --- a/src/data/data_entry_flow.ts +++ b/src/data/data_entry_flow.ts @@ -28,7 +28,7 @@ export interface DataEntryFlowStepForm { step_id: string; data_schema: HaFormSchema[]; errors: Record; - description_placeholders: Record; + description_placeholders?: Record; last_step: boolean | null; } @@ -49,7 +49,7 @@ export interface DataEntryFlowStepCreateEntry { title: string; result?: ConfigEntry; description: string; - description_placeholders: Record; + description_placeholders?: Record; } export interface DataEntryFlowStepAbort { @@ -57,7 +57,7 @@ export interface DataEntryFlowStepAbort { flow_id: string; handler: string; reason: string; - description_placeholders: Record; + description_placeholders?: Record; } export interface DataEntryFlowStepProgress { @@ -66,7 +66,17 @@ export interface DataEntryFlowStepProgress { handler: string; step_id: string; progress_action: string; - description_placeholders: Record; + description_placeholders?: Record; +} + +export interface DataEntryFlowStepMenu { + type: "menu"; + flow_id: string; + handler: string; + step_id: string; + /** If array, use value to lookup translations in strings.json */ + menu_options: string[] | Record; + description_placeholders?: Record; } export type DataEntryFlowStep = @@ -74,7 +84,8 @@ export type DataEntryFlowStep = | DataEntryFlowStepExternal | DataEntryFlowStepCreateEntry | DataEntryFlowStepAbort - | DataEntryFlowStepProgress; + | DataEntryFlowStepProgress + | DataEntryFlowStepMenu; export const subscribeDataEntryFlowProgressed = ( conn: Connection, diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index 6fbab75c74..c35216021e 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -46,6 +46,7 @@ import "./step-flow-loading"; import "./step-flow-pick-flow"; import "./step-flow-pick-handler"; import "./step-flow-progress"; +import "./step-flow-menu"; let instance = 0; @@ -292,6 +293,14 @@ class DataEntryFlowDialog extends LitElement { .hass=${this.hass} > ` + : this._step.type === "menu" + ? html` + + ` : this._devices === undefined || this._areas === undefined ? // When it's a create entry result, we will fetch device & area registry html` @@ -421,7 +430,7 @@ class DataEntryFlowDialog extends LitElement { title: this.hass.localize( "ui.panel.config.integrations.config_flow.error" ), - text: err.message || err.body, + text: err?.body?.message, }); return; } finally { diff --git a/src/dialogs/config-flow/show-dialog-config-flow.ts b/src/dialogs/config-flow/show-dialog-config-flow.ts index e516dfc4fe..42337b40ff 100644 --- a/src/dialogs/config-flow/show-dialog-config-flow.ts +++ b/src/dialogs/config-flow/show-dialog-config-flow.ts @@ -181,6 +181,21 @@ export const showConfigFlowDialog = ( : ""; }, + renderMenuHeader(hass, step) { + return ( + hass.localize( + `component.${step.handler}.config.step.${step.step_id}.title` + ) || hass.localize(`component.${step.handler}.title`) + ); + }, + + renderMenuOption(hass, step, option) { + return hass.localize( + `component.${step.handler}.config.step.${step.step_id}.menu_options.${option}`, + step.description_placeholders + ); + }, + renderLoadingDescription(hass, reason, handler, step) { if (!["loading_flow", "loading_step"].includes(reason)) { return ""; diff --git a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts index b0217ec057..be54d09839 100644 --- a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts @@ -7,6 +7,7 @@ import { DataEntryFlowStepCreateEntry, DataEntryFlowStepExternal, DataEntryFlowStepForm, + DataEntryFlowStepMenu, DataEntryFlowStepProgress, } from "../../data/data_entry_flow"; import { HomeAssistant } from "../../types"; @@ -80,6 +81,14 @@ export interface FlowConfig { step: DataEntryFlowStepProgress ): TemplateResult | ""; + renderMenuHeader(hass: HomeAssistant, step: DataEntryFlowStepMenu): string; + + renderMenuOption( + hass: HomeAssistant, + step: DataEntryFlowStepMenu, + option: string + ): string; + renderLoadingDescription( hass: HomeAssistant, loadingReason: LoadingReason, diff --git a/src/dialogs/config-flow/show-dialog-options-flow.ts b/src/dialogs/config-flow/show-dialog-options-flow.ts index 7c7fde94dc..451e601ed4 100644 --- a/src/dialogs/config-flow/show-dialog-options-flow.ts +++ b/src/dialogs/config-flow/show-dialog-options-flow.ts @@ -134,6 +134,21 @@ export const showOptionsFlowDialog = ( : ""; }, + renderMenuHeader(hass, step) { + return ( + hass.localize( + `component.${step.handler}.option.step.${step.step_id}.title` + ) || hass.localize(`component.${step.handler}.title`) + ); + }, + + renderMenuOption(hass, step, option) { + return hass.localize( + `component.${step.handler}.options.step.${step.step_id}.menu_options.${option}`, + step.description_placeholders + ); + }, + renderLoadingDescription(hass, reason) { return ( hass.localize(`component.${configEntry.domain}.options.loading`) || diff --git a/src/dialogs/config-flow/step-flow-menu.ts b/src/dialogs/config-flow/step-flow-menu.ts new file mode 100644 index 0000000000..a49baed97f --- /dev/null +++ b/src/dialogs/config-flow/step-flow-menu.ts @@ -0,0 +1,80 @@ +import "@material/mwc-list/mwc-list-item"; +import { css, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; +import type { DataEntryFlowStepMenu } from "../../data/data_entry_flow"; +import type { HomeAssistant } from "../../types"; +import type { FlowConfig } from "./show-dialog-data-entry-flow"; +import "../../components/ha-icon-next"; +import { configFlowContentStyles } from "./styles"; +import { fireEvent } from "../../common/dom/fire_event"; + +@customElement("step-flow-menu") +class StepFlowMenu extends LitElement { + @property({ attribute: false }) public flowConfig!: FlowConfig; + + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public step!: DataEntryFlowStepMenu; + + protected render(): TemplateResult { + let options: string[]; + let translations: Record; + + if (Array.isArray(this.step.menu_options)) { + options = this.step.menu_options; + translations = {}; + for (const option of options) { + translations[option] = this.flowConfig.renderMenuOption( + this.hass, + this.step, + option + ); + } + } else { + options = Object.keys(this.step.menu_options); + translations = this.step.menu_options; + } + + return html` +

${this.flowConfig.renderMenuHeader(this.hass, this.step)}

+
+ ${options.map( + (option) => html` + + ${translations[option]} + + + ` + )} +
+ `; + } + + private _handleStep(ev) { + fireEvent(this, "flow-update", { + stepPromise: this.flowConfig.handleFlowStep( + this.hass, + this.step.flow_id, + { + next_step_id: ev.currentTarget.step, + } + ), + }); + } + + static styles = [ + configFlowContentStyles, + css` + .options { + margin-top: 20px; + margin-bottom: 8px; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "step-flow-menu": StepFlowMenu; + } +} From 29119db5ce000627107f44a866310ed4d87fb696 Mon Sep 17 00:00:00 2001 From: Zack Date: Wed, 16 Mar 2022 17:05:52 -0500 Subject: [PATCH 063/142] Add translation --- src/translations/en.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/translations/en.json b/src/translations/en.json index c87066ee6c..a24a506548 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2428,6 +2428,7 @@ "unavailable": "Unavailable", "disabled": "Disabled", "readonly": "Read-only", + "hidden": "Hidden", "ok": "Ok" }, "headers": { From 94ebb63589457708ba90c6cb99092a33ae9b4530 Mon Sep 17 00:00:00 2001 From: Zack Date: Wed, 16 Mar 2022 17:25:08 -0500 Subject: [PATCH 064/142] add to basic editor and update advanced style --- .../entities/entity-registry-basic-editor.ts | 142 ++++++++++++++---- .../entities/entity-registry-settings.ts | 29 ++-- src/translations/en.json | 1 + 3 files changed, 132 insertions(+), 40 deletions(-) diff --git a/src/panels/config/entities/entity-registry-basic-editor.ts b/src/panels/config/entities/entity-registry-basic-editor.ts index 52240794b3..fcf2e7883e 100644 --- a/src/panels/config/entities/entity-registry-basic-editor.ts +++ b/src/panels/config/entities/entity-registry-basic-editor.ts @@ -1,3 +1,5 @@ +import "../../../components/ha-expansion-panel"; +import "@material/mwc-formfield/mwc-formfield"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; @@ -5,6 +7,7 @@ import { computeDomain } from "../../../common/entity/compute_domain"; import "../../../components/ha-area-picker"; import "../../../components/ha-switch"; import "../../../components/ha-textfield"; +import "../../../components/ha-radio"; import type { HaSwitch } from "../../../components/ha-switch"; import { DeviceRegistryEntry, @@ -33,6 +36,8 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { @state() private _disabledBy!: string | null; + @state() private _hiddenBy!: string | null; + private _deviceLookup?: Record; @state() private _device?: DeviceRegistryEntry; @@ -51,6 +56,12 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { ) { params.disabled_by = this._disabledBy; } + if ( + this.entry.hidden_by !== this._hiddenBy && + (this._hiddenBy === null || this._hiddenBy === "user") + ) { + params.hidden_by = this._hiddenBy; + } try { const result = await updateEntityRegistryEntry( this.hass!, @@ -101,6 +112,7 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { this._origEntityId = this.entry.entity_id; this._entityId = this.entry.entity_id; this._disabledBy = this.entry.disabled_by; + this._hiddenBy = this.entry.hidden_by; this._areaId = this.entry.area_id; this._device = this.entry.device_id && this._deviceLookup @@ -138,37 +150,95 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { .placeholder=${this._device?.area_id} @value-changed=${this._areaPicked} > -
- - -
-
- ${this.hass.localize( + + +
+ ${this.hass.localize( + "ui.dialogs.entity_registry.editor.view_status" + )}: +
+
+ ${this._disabledBy && this._disabledBy !== "user" + ? this.hass.localize( + "ui.dialogs.entity_registry.editor.enabled_cause", + "cause", + this.hass.localize( + `config_entry.disabled_by.${this._disabledBy}` + ) + ) + : ""} +
+
+ -
- ${this._disabledBy && this._disabledBy !== "user" - ? this.hass.localize( - "ui.dialogs.entity_registry.editor.enabled_cause", - "cause", - this.hass.localize( - `config_entry.disabled_by.${this._disabledBy}` - ) - ) - : ""} - ${this.hass.localize( - "ui.dialogs.entity_registry.editor.enabled_description" + > + + + ${this.hass.localize( - "ui.dialogs.entity_registry.editor.note" + > + + + + > + +
-
+ + ${this._disabledBy !== null + ? html` +
+ ${this.hass.localize( + "ui.dialogs.entity_registry.editor.enabled_description" + )} +
+ ` + : this._hiddenBy !== null + ? html` +
+ ${this.hass.localize( + "ui.dialogs.entity_registry.editor.hidden_description" + )} +
+ ` + : ""} +
`; } @@ -184,6 +254,23 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { this._disabledBy = (ev.target as HaSwitch).checked ? null : "user"; } + private _viewStatusChanged(ev: CustomEvent): void { + switch ((ev.target as any).value) { + case "enabled": + this._disabledBy = null; + this._hiddenBy = null; + break; + case "disabled": + this._disabledBy = "user"; + this._hiddenBy = null; + break; + case "hidden": + this._hiddenBy = "user"; + this._disabledBy = null; + break; + } + } + static get styles() { return css` ha-switch { @@ -202,6 +289,9 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { display: block; margin-bottom: 8px; } + ha-expansion-panel { + margin-top: 8px; + } `; } } diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index a11803f6db..76948c4851 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -304,6 +304,11 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { : ""} ${this.entry.device_id ? html` +
+ ${this.hass.localize( + "ui.dialogs.entity_registry.editor.change_area" + )}: +
+ ${this.hass.localize( + "ui.dialogs.entity_registry.editor.change_device_area" + )} + + ` + : ""}
- ${this._areaId - ? html`${this.hass.localize( - "ui.dialogs.entity_registry.editor.follow_device_area" - )}` - : this._device - ? html`${this.hass.localize( - "ui.dialogs.entity_registry.editor.change_device_area" - )}` - : ""} ` : ""} @@ -530,7 +531,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { align-items: center; } .label { - margin-top: 8px; + margin-top: 16px; } .secondary { margin: 8px 0; diff --git a/src/translations/en.json b/src/translations/en.json index a24a506548..23290720f0 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -790,6 +790,7 @@ }, "unavailable": "This entity is unavailable.", "view_status": "Entity view status", + "change_area": "Change Area", "enabled_label": "Enabled", "disabled_label": "Disabled", "enabled_cause": "Cannot change view status. Disabled by {cause}.", From 01eed225922bcf6c9573bd7293de4bb55d79a59c Mon Sep 17 00:00:00 2001 From: Zack Date: Wed, 16 Mar 2022 17:34:09 -0500 Subject: [PATCH 065/142] clean up --- src/panels/config/entities/entity-registry-basic-editor.ts | 5 ----- src/panels/config/entities/entity-registry-settings.ts | 5 ----- src/translations/en.json | 2 +- 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/panels/config/entities/entity-registry-basic-editor.ts b/src/panels/config/entities/entity-registry-basic-editor.ts index fcf2e7883e..53d98571e9 100644 --- a/src/panels/config/entities/entity-registry-basic-editor.ts +++ b/src/panels/config/entities/entity-registry-basic-editor.ts @@ -8,7 +8,6 @@ import "../../../components/ha-area-picker"; import "../../../components/ha-switch"; import "../../../components/ha-textfield"; import "../../../components/ha-radio"; -import type { HaSwitch } from "../../../components/ha-switch"; import { DeviceRegistryEntry, subscribeDeviceRegistry, @@ -250,10 +249,6 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { this._entityId = ev.target.value; } - private _disabledByChanged(ev: Event): void { - this._disabledBy = (ev.target as HaSwitch).checked ? null : "user"; - } - private _viewStatusChanged(ev: CustomEvent): void { switch ((ev.target as any).value) { case "enabled": diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index 76948c4851..5f5ea8b835 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -397,11 +397,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { } } - private _clearArea() { - this._error = undefined; - this._areaId = null; - } - private _openDeviceSettings() { showDeviceRegistryDetailDialog(this, { device: this._device!, diff --git a/src/translations/en.json b/src/translations/en.json index 23290720f0..9861b3d23b 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -801,7 +801,7 @@ "enabled_description": "Disabled entities will not be added to Home Assistant.", "enabled_delay_confirm": "The enabled entities will be added to Home Assistant in {delay} seconds", "enabled_restart_confirm": "Restart Home Assistant to finish enabling the entities", - "hidden_description": "Hidden entities will not be shown in Home Assistant UI.", + "hidden_description": "Hidden entities will not be shown on your dashboard. Their history is still tracked and you can still interact with them with services.", "delete": "Delete", "confirm_delete": "Are you sure you want to delete this entity?", "update": "Update", From dde1c5e03c1bf18f8045856ad44748aebe65ec0e Mon Sep 17 00:00:00 2001 From: Zack Date: Wed, 16 Mar 2022 17:38:38 -0500 Subject: [PATCH 066/142] Entity Status --- src/panels/config/entities/entity-registry-basic-editor.ts | 5 ++++- src/panels/config/entities/entity-registry-settings.ts | 2 +- src/translations/en.json | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/panels/config/entities/entity-registry-basic-editor.ts b/src/panels/config/entities/entity-registry-basic-editor.ts index 53d98571e9..10d3bdf994 100644 --- a/src/panels/config/entities/entity-registry-basic-editor.ts +++ b/src/panels/config/entities/entity-registry-basic-editor.ts @@ -158,7 +158,7 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { >
${this.hass.localize( - "ui.dialogs.entity_registry.editor.view_status" + "ui.dialogs.entity_registry.editor.entity_status" )}:
@@ -287,6 +287,9 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { ha-expansion-panel { margin-top: 8px; } + .label { + margin-top: 16px; + } `; } } diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index 5f5ea8b835..c4278a7936 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -223,7 +223,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { >
${this.hass.localize( - "ui.dialogs.entity_registry.editor.view_status" + "ui.dialogs.entity_registry.editor.entity_status" )}:
diff --git a/src/translations/en.json b/src/translations/en.json index 9861b3d23b..babe137ca5 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -789,11 +789,11 @@ } }, "unavailable": "This entity is unavailable.", - "view_status": "Entity view status", + "entity_status": "Entity status", "change_area": "Change Area", "enabled_label": "Enabled", "disabled_label": "Disabled", - "enabled_cause": "Cannot change view status. Disabled by {cause}.", + "enabled_cause": "Cannot change status. Disabled by {cause}.", "hidden_label": "Hidden", "hidden_cause": "Hidden by {cause}.", "device_disabled": "The device of this entity is disabled.", From 73ff8e28a8110a6f7d58a1267cef7e9d830373bb Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Wed, 16 Mar 2022 17:40:34 -0500 Subject: [PATCH 067/142] Add Devices Picker (#12056) --- gallery/src/pages/components/ha-selector.ts | 1 + src/components/device/ha-devices-picker.ts | 8 ++- .../ha-selector/ha-selector-device.ts | 51 ++++++++++++------- src/data/selector.ts | 1 + 4 files changed, 43 insertions(+), 18 deletions(-) diff --git a/gallery/src/pages/components/ha-selector.ts b/gallery/src/pages/components/ha-selector.ts index 2ea007d913..4408191b20 100644 --- a/gallery/src/pages/components/ha-selector.ts +++ b/gallery/src/pages/components/ha-selector.ts @@ -184,6 +184,7 @@ const SCHEMAS: { name: "Multiples", input: { entity: { name: "Entity", selector: { entity: { multiple: true } } }, + device: { name: "Device", selector: { device: { multiple: true } } }, }, }, ]; diff --git a/src/components/device/ha-devices-picker.ts b/src/components/device/ha-devices-picker.ts index 1b437da780..ff975abef5 100644 --- a/src/components/device/ha-devices-picker.ts +++ b/src/components/device/ha-devices-picker.ts @@ -1,4 +1,4 @@ -import { html, LitElement, TemplateResult } from "lit"; +import { css, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; import { PolymerChangedEvent } from "../../polymer-types"; @@ -116,6 +116,12 @@ class HaDevicesPicker extends LitElement { this._updateDevices([...currentDevices, toAdd]); } + + static override styles = css` + div { + margin-top: 8px; + } + `; } declare global { diff --git a/src/components/ha-selector/ha-selector-device.ts b/src/components/ha-selector/ha-selector-device.ts index 60da624665..2b0af5ace1 100644 --- a/src/components/ha-selector/ha-selector-device.ts +++ b/src/components/ha-selector/ha-selector-device.ts @@ -1,10 +1,11 @@ import { html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import { ConfigEntry, getConfigEntries } from "../../data/config_entries"; -import { DeviceRegistryEntry } from "../../data/device_registry"; -import { DeviceSelector } from "../../data/selector"; -import { HomeAssistant } from "../../types"; +import type { DeviceRegistryEntry } from "../../data/device_registry"; +import type { DeviceSelector } from "../../data/selector"; +import type { HomeAssistant } from "../../types"; import "../device/ha-device-picker"; +import "../device/ha-devices-picker"; @customElement("ha-selector-device") export class HaDeviceSelector extends LitElement { @@ -30,20 +31,36 @@ export class HaDeviceSelector extends LitElement { } protected render() { - return html``; + if (!this.selector.device.multiple) { + return html` `; + } + + return html` + ${this.label ? html`` : ""} + + `; } private _filterDevices = (device: DeviceRegistryEntry): boolean => { diff --git a/src/data/selector.ts b/src/data/selector.ts index 8d8c062b08..ce84b86fbc 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -49,6 +49,7 @@ export interface DeviceSelector { domain?: EntitySelector["entity"]["domain"]; device_class?: EntitySelector["entity"]["device_class"]; }; + multiple?: boolean; }; } From 4fc2c3ef059827e858192d626ae58c495a21c9ef Mon Sep 17 00:00:00 2001 From: Zack Date: Wed, 16 Mar 2022 17:42:11 -0500 Subject: [PATCH 068/142] Remvoe redunency --- src/components/ha-selector/ha-selector-date.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/components/ha-selector/ha-selector-date.ts b/src/components/ha-selector/ha-selector-date.ts index a19438a1b4..d8b5742f90 100644 --- a/src/components/ha-selector/ha-selector-date.ts +++ b/src/components/ha-selector/ha-selector-date.ts @@ -2,7 +2,6 @@ import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import type { HomeAssistant } from "../../types"; import type { DateSelector } from "../../data/selector"; -import { fireEvent } from "../../common/dom/fire_event"; import "../ha-date-input"; @customElement("ha-selector-date") @@ -24,15 +23,10 @@ export class HaDateSelector extends LitElement { .locale=${this.hass.locale} .disabled=${this.disabled} .value=${this.value} - @value-changed=${this._valueChanged} > `; } - - private _valueChanged(ev: CustomEvent) { - fireEvent(this, "value-changed", { value: ev.detail.value }); - } } declare global { From 1ca9c7838a47954d7b12e779d025d469ae282f97 Mon Sep 17 00:00:00 2001 From: Zack Date: Wed, 16 Mar 2022 17:46:32 -0500 Subject: [PATCH 069/142] Bumped version to 20220316.0 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index d8c03a5e88..83de1db87f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = home-assistant-frontend -version = 20220312.0 +version = 20220316.0 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 5247b2813f01e0e5e6b4b7b5b85d05f2d7d3a41c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Mar 2022 22:54:25 -0700 Subject: [PATCH 070/142] Bump HAWS to 7.0.0 (#12067) --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 0725b0af60..3e6035c3a4 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "fuse.js": "^6.0.0", "google-timezones-json": "^1.0.2", "hls.js": "^1.1.5", - "home-assistant-js-websocket": "^6.1.1", + "home-assistant-js-websocket": "^7.0.0", "idb-keyval": "^5.1.3", "intl-messageformat": "^9.9.1", "js-yaml": "^4.1.0", diff --git a/yarn.lock b/yarn.lock index ab97532484..3e059bda92 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9128,7 +9128,7 @@ fsevents@^1.2.7: gulp-rename: ^2.0.0 gulp-zopfli-green: ^3.0.1 hls.js: ^1.1.5 - home-assistant-js-websocket: ^6.1.1 + home-assistant-js-websocket: ^7.0.0 html-minifier: ^4.0.0 husky: ^1.3.1 idb-keyval: ^5.1.3 @@ -9198,10 +9198,10 @@ fsevents@^1.2.7: languageName: unknown linkType: soft -"home-assistant-js-websocket@npm:^6.1.1": - version: 6.1.1 - resolution: "home-assistant-js-websocket@npm:6.1.1" - checksum: b46dd44ac1b393ecfdda152743fe44dcd6534463d1df631c448a71e71166c04d4fea2a0827dbb2e55c85fcd8d9ccdea4f168713bf18e832c7f688b9b2fdc8d72 +"home-assistant-js-websocket@npm:^7.0.0": + version: 7.0.0 + resolution: "home-assistant-js-websocket@npm:7.0.0" + checksum: b006f00e8218575caafa68a8c1f4a3392c1471416e233eec94c57ff6118173465b21c9ffe22ab49608d3686c19fa9549e92d1c21be3f408afa474cb93c4b55af languageName: node linkType: hard From 0261cea796cc299434b4333e4967a39737283437 Mon Sep 17 00:00:00 2001 From: Matthias de Baat Date: Thu, 17 Mar 2022 14:14:13 +0100 Subject: [PATCH 071/142] Create new Logo page --- gallery/src/pages/brand/logo.markdown | 34 +++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 gallery/src/pages/brand/logo.markdown diff --git a/gallery/src/pages/brand/logo.markdown b/gallery/src/pages/brand/logo.markdown new file mode 100644 index 0000000000..08724a6d71 --- /dev/null +++ b/gallery/src/pages/brand/logo.markdown @@ -0,0 +1,34 @@ +--- +title: "Logo" +--- + +![Using our logo](/images/using-our-logo.png) + +# Using our logo + +As a community we are proud of our logo. Follow these guidelines to ensure it always looks its best. Our logo follows Googles material design spec and uses the blue interface color. + +[Download Logo](https://github.com/home-assistant/assets/tree/master/logo) + +![Logo](/images/logo.png) + + +## Using the icon + +Our icon is a shorter and most used version of our logo. The icon can exist without the wordmark, the wordmark should never exist without the icon. + +![Logo variants](/images/logo-variants.png) + +## Using the right variant + +The pretty blue logo with a background shadow, pictured top left, is our primary logo. It should only be used with black, white, and non-duotoned photography. + +When needed you can use our logo without a shadow, as seen as the second variant. + +The outlined logo should only be used on packaging. + +## Exclusion zone + +The logo needs some personal space. TIt's exclusion zone is equal to a quarter the height of the icon. + +![Clearspace](/images/clearspace.png) From 3e0942b6310cd48ead81e835577284fb0e4489df Mon Sep 17 00:00:00 2001 From: Matthias de Baat Date: Thu, 17 Mar 2022 14:19:28 +0100 Subject: [PATCH 072/142] Add files via upload --- gallery/public/images/clearspace.png | Bin 0 -> 44504 bytes gallery/public/images/logo-variants.png | Bin 0 -> 35697 bytes gallery/public/images/logo.png | Bin 0 -> 27959 bytes gallery/public/images/using-our-logo.png | Bin 0 -> 33246 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 gallery/public/images/clearspace.png create mode 100644 gallery/public/images/logo-variants.png create mode 100644 gallery/public/images/logo.png create mode 100644 gallery/public/images/using-our-logo.png diff --git a/gallery/public/images/clearspace.png b/gallery/public/images/clearspace.png new file mode 100644 index 0000000000000000000000000000000000000000..ffd1aa5f62d570a3e541c55689f1be5f0f772ee6 GIT binary patch literal 44504 zcmZ^L1y~%*vM}y0fyE^d++8-fYjAgWch}$$+=CNbf;+)AKyY_=Tl6RA-kWpYeea+B zW~O(#s=B(Qr>c74vx1yBG6Eg~7#J9`q=d*PFfd4xw{{*J%-gkjy_E9p2JZApTnMak zoZ#T?OOc7Xq^Ybd7|mN74h#ky0}S#v$lC`D91jfouQnK%Dmeb1wk|l;zhNNWV3@() zu3*@2r)ii47}USvz2EM?4~e()jncpF3Ks4r);0#tc8(+pj&>w046F?7Z+(!t;D3*q z3-KQskR-W~|7nBey}^Ms4AINH-QevdG@QV|5HWwB;9%*QxNkIUEtJ)r)n#RPjO=U} z42|d>urLJzk3*&8JHOVFSNJG{^9R`V7VLE|9_F3ElmGEWBu;=7usLK z_!p0Szd7LfOQ9Mf7B(ifPH*!8GIOx-{l(k=>iK(i|G-rL87SVY==lfc+r)T; z9Zh~4kg}bfHSqtkCqBmihWal8vTq7EHgGl&F>p2kvM{}Ai;0zXu`7IJ2^-m^FcGfPx6$#{IVR}0lnSWpT82?xIf2015sbXv4415#X zUnu|e_`k6<{_g+32K;CDf1}DfT9~|<<6pco|9iB5KKnO3ALDO}{D+19%l!X(`ey$? zgg3GL$CrQzD|*ClZZ7~PDI%!s4u0$dt54AP>$Nq{u~>J-SlW@X$tb8BW}3JAE0hoh zNiYTpahw=D`!xm$JgMF=u>=M9WeA!S1gZZ)f4{#&pKL81j$|^uMzQbR>+`LxyjNM- z)C$=U>+Qa4aa$T_Y-#1LsI8{C3iJzZB2kpAZ)eku$U#kQV=8~ebJ++@4I^Ag`&sk# zdsAO8pKar7M~B>U_Mre#aI@ED>OiAK7J2>y>)vX9$In%zcL^$AbW>~5@R-MYJ#i1! zqj*}PU5=#*4)yfblv2>rqpli}p+g#$Fe%3UC9AwkI+uDcCp`^IHmZy)qG&m;kuk!y zS^L3-EBLZZGaXFnRzkg*x14na@P*DuVb?(q6)e7P*Fbv_L#Tlz;xR;i_la% zS?{`WjkHn6HD7-KaE4Q0+7t}0o3K4ACig=3hBGI^DYB72%KFW&a^ZUr+Dzw&JFQ{k zZ;iIuh9h=IK3hhyZeU+N;cqIyo~-2F$unl})GZS0ir?z84WT-Fqo}7aoh9m~zCUU= z@aDwz;2&=79<39j%kKj`iuVGZ^LZgDw;?OeFruz-bNnue%%#wz$*aCFBDa~1<6xL= z-%vdfe3`qs3;oRa-Ix7`#e_qX8{}nlS{ZpDzR_#yW|TUaxI{-B*E+%FsyhBQzZ0>D zA7}Lo&jg+WN2gMhdirM6S#+@b%6XJkDsE?xLq7mC^%Nb2XXa^#yB)eZ;r?_M9f&-ABFM3gKw$4RiJ{b3P_EC0fvXT{$yh}!CZ0$c?y!o8(@<5$c zN>XJ`1BC^F03PBmBtV*rW?$j6MBVJsF{?P;suUykyeQY~e7gQgfXK74y42fLY`EsY zyn2VxkHY56V9%wXjZQ_N6W!iMZzrzEr(u>FvGqZg4hG!q27!vi&kBG}o)w`ANA=GE zn8@Q+SE!43k!YYrvRBopP|Oao(O6XvbxHLFx@?PKUlGSXulPX%uRTNQS;NhIo3ywS?P>ty18$1^|Rg6?3zg_ zkotq;FSITo+V%a#rMLn02Lza-C8xTAlw;(e&XuksyjWfXj+emdla^6@0X5u(l_blCG9m}H63!%`ZJxA!cl)vM68Bo zy)-7VRD%D~&cC>a#180T)|DKo7m2eR2XHGWhO1W<)mor$CE#tiN{aU(QQT2r&tPHD z;_ZfjETv7$-Q|*4dNX=Dlnb!`)--Mp92IMuaFdhD^rz4Mnt@4D&e%5pwt|01i3AF~ zwF@hmBBp3wAATgER+;o4Qzl4-JFcujq+`;Uy5D_H#`-cNlwQ6REQeSkSE&92vayGm3h2JD}4x?V>Z%SNDZ#MM&V`H&q)g$#PChQRdUg zU`5S1lHEguS`uKj;8yoA{330m?n?`XCspg5rK!6Y#7xpZr1BSAB;0WI6k$<$hVT#_ z43VSfdF-i{4q8)GV`L${WLLVg^%6;Wf7?x$fN<9*);4a)iW`TS51cV?;WI=R~&;b6$+;j`#UErt2N655rrH-|Q{+&;6z72tq0r6cJ)NY$rFR3iQ{ z2S}8FDV8b|m*Js?@{$7VM5`4fiKCK(70I$jvP4a&T&6rQtL^Je3>OSfOD`owwZA>& zx9|SeS`^%TVaK?8PK#CQouiGm>CoxJ47-rxKb%mXgnWImW7r@KG`C0y35Ym%vHAv2 zoP9ir59cCu>H5Q?a5d5TGmnotllku`*fHt3Spi0$mBRD(~Ch zVgy5<*R@Fr)k)4Yi<{Qw-DjGp(6Eus;;%^Pa7jyeEEjBjEnH8#P_d~M0e4WVP?4^F zSTl+U{KvfiZBl5A5c9#dS%B5sD6%7t(7s52sp7wf>6#QI`7rUp;vm_&$YvZ6Z0cUI z7>sf;mCPwLLRNt!q~Yc8>CS9Gi3)1 z()>5Fe>eaVGZe|a=4_rI7dV+=7C`sS<@-NBGY$^#!L)=?WYj;zM&$itq59Vvs{n@t z4W%FIR>6Lf>rth`SfrtpN+bIB+7JMV4xaA&eKgbjB(xj0Mn$!n2hSRG2229pgYhs~ zm8VrT3|iN=dWLZ6{K`0~82_X<$hL5b*D9@fF%e>-#E`Q;%~F5R=GN*e)A|v`i)U)( zrBse7-Rg|UkKoP|H%03LDK50)c64!UiYFgn#aj2&zGPA-?PWz&pFmf$l$jb^<5C3e zj8%>&gZcDR4VA1-Nlj12K{MOJ7323Q-e&egt%J^G>xIl-vkn z(QWPQnV%ttQ2Y#t40LL9*u8^!82E0MKfRQ?VLsNA?wU`B5oe?BX!SxB^b~l9xs5Z- zuSsxMemG9Gq(I4%U5m0n`FO>C`^=&y&rjv+#NlIW<%+#9X5-)jWz6No8_i?bJ8TqomiHFI1`;xTiVkr0>4FcrKyv<_EdpSe75X zX6!C?t|e~-J8N=$Z*>{&VGvUUN$6dCD;hI3HhWzBK)?TE*HMS^lTx8 zb83mhMX`9*f0W&>(BxR1TtnJ#PP_hOEIHb2c_S(C!UV-(6es5nH)o6k`SBF-N3yA# zD{nphy=$wyw+=_mMhZ$^jYx!(Q1CSwan{N%@(_-ncH{jq6>9Nhok;N*O!^D9r7Q84 z{uua1hiwLz;0$gtqcQ71%{WJEj-v6&aqjG%Hl%$+m(Q)V`uOjl zY=q3lGWK+Q`leqXvW1pu`5myr}=& z_olV|2?r`&Qg6vNB=y?}R(*?@vZlSq^^uBo-DxNNO%114k!>@jj<^oi_Q(dj8n!FX z%rhAHZU8+q@YTngGwPLe6(4*R@c>dkG3ri?iW0sJKg!#peUvPRfW6M`78=GF6Mq=3fY+I{kLW7W@B{?wY1I+q#e6D{!t zN~7naI4=_CuJ74wU!yyn#TG&xCswEI>aEa~WjNZ-M$17mw`;zna zY~tx3Z0ugB30s(k8VWd-E!NVb+91wWr>-Daz7{vilo%mwu!>L;@V1t@D8~~kQ4s^I z*4%chv{p4QSChOrU)GVV?SYJQ3l=%_z=5A}PP>97c$7f17R=gY9M1Ho=@sn6N2<=x zg8lHrnsBa*(JV@wm-ozb+tk8DdMSdp7tqGU@4qjR90?Lln~b8RP0W&C#edU%S0%j7 zy4|?+oMYBGddMhk1XWd2PSl*}K`A;mL013KiSW%=w|<@aL0mv_qK@soQnn1H2Kw_RK9jq0JQ*{HZp$0ErJtA(HZ zCpnzQI{YUG40|&Tz1|(CBVav%xWGCkt?E1?ScrA?CCZG6$GJ+jf!E=*({!%OlJ|e! zVYu#a+U{CK1hwzgqy}JN?LMgvJ*2+{{f0sFBtKE!-A%TJd;+qP@BBKky+~@i7=39O zs9a{0Z&VdBX`0-<@`2n@Eoie=5wM1`o&qD>HEC^p{5gJg35=_=lTHyW-Ww{B><*Zj z7kNnT(P@8&Ftes9c1`aU*xb^t^C@9vzkJD$3+B}iDd3JtLBfL^vwSBYyA5*me$Tc_ zn^A9U)g?FR>tfZvgMc8kfMT`cNGs<`XRdnm`NRg2_EsgiF@QG0;PfLx-$uOv_y$j0 zzYkd-1xnSOXL-v#-h-jSBC<3!s!#ln=Mi7v52>6o-aDTTAt}mT!_eUbAKrk^c#)c& zjqS^Obvh_JM&MG)!(pP;9~5MGl&4s;T-UH2@prEimHrcHb>#}{ALf=_#8p(YP9SgG zTGJ(dB(_l=9uPP(KZX%V+V!xB(K5MD#GoxR%YxYa;hG;)8KUm$!h)Cjpa@T^NT6x5 z<8IS1Jx$3;$XB0UsG0wFfa9M8SKz`uU{KuWp`K^Du|cldU@wsv;(@kCddsn;*K?SrPid=~989cgrNBT7k$qOTP<)*cN2U)B(Lj(f!J+z6Jz^th@D1)y9Q+clsnKh`M$4PlX{z-KL5N>X871O}cnapF{<(I7u?K;0m=5byk>FEF2s@19VrUjbAqol zCI8U$Us}Qyg437lNtfNnGnIlT5-QJ6|C5^p1cZ0t#m~O8;`oZ%&%dwg0x-+<)`l-$ zeoK<&0PU?y>EAs04ATI*2bRLUixn3C6+dDG)SB5k8UIq%sNLr+cJ0gU<#HSAr2ppB zI?qE#u-#CiBHVz>n20*zE1kZl+p3%;w#rygO79pYs<#$?<1|eD&y4*oTor^GrS(Xh zpWBn`AGfEauh3*6`Dw;vXgY&i74}G=-)4};ak1Ifk!6!=di*)iFt3S)GMgsgk0~ZZ z`LkN3Pb6=!_{M+H4vfzMQFBm7FvD`5gXBKchJB}i{umP#DUvfF09%%o5{jrp`SW2Q)vdN+}7m>*jkmhc=XE@0d&hu1-e2)Uur_V5|ksx zg9FBto6U#aD2qmt&Y|^%IIJ|Yb!v0<_}eOV7MaO0HeqO4L)nB@{uDs;(MXuI(>9_P zrJLW4`J0=%lj0t*D9=bCS4*^yFILGD;Hm&Uf|X8F<4G}{jt#?7IXefxRs@1_b}C*} zm=a;A&yc6U&Bu@!V<2jNY`~#KO4Sx{mr}p)@=m9K)o!)X9rZ34x+^j^$E&WiOf;mP zbwlrOa^7$(_y6$WFe)=0kLKGS*ZNnEKO`WKvT%>6F^ysP;RKdo&hEw<&WERQDf^ED1k8|}&yH}E)(s<7v8x6pASf%z zei{;eFUH!ol7*fRYYg>+eGD2VzSMh^v3|r3Wh~*ceAVby4l0YCQpYwUp4uc0SYrWl z2FZWmAkkVZ`{X)YPN`Z@2Y?5)z!(V|B231dCj%Q2&b8jdye+3J8$GX-*X^UvOVp}c z%u9HqZG}eInR|e!Ohnacuz>B0lf<>%r~vFYy;B7`qfMi`Y|JBpx@TknX`dBP8t_dd z_%{SzP1={V=t~P*Eh{twvENycZ~24QF4Q)}M!xUJVno2q+>m+SUFdU}4W{?UG zx{ztq<^bwUC1T|D=K21q5ygFiG7oL+BV%nF;|`ics#3|AG=R1`AN0|e9QIS-EAh?@ zM&3)@DBA94Z2Sb@Z0;Sp%(+A%sw`A*16xq~m{{xc`q>Gxo%QARwydihgLdrPP#3|6 zDL1RgJ2&(RddryG+Y#`_Pg*8*2%yFcIBJ6*nihA2>S!^G;S2Hza|S^uxu+*VC4Mb8 zN+VUe`(wKMI1pO$Kzu7ngo=1BN6Oo>NeStym-VXXF{}@x#?R~Iv24!%*cPiC`xq${K^jUv2EzQ)I;9Mv`DXyu z5b5f2mflDfSf*3K9XKQjWG>8+%BHkTR$}G%51KdVgI+6aDc+bFb6J5#r|h!0CjVG< zLIe=dQfW4aYbC%X1#iMIBoV$t{yHGQh#P4Nc#0!yUFIC1uS3kj=$}>4rr*epa~huB zS=C^8b)*{)Hjh9-n8D6&qSDA1lop&b_=yA;0|w~IG$1_}?1!Twp6ByrNZlK#t9@X* zGN|Of3tr}0q2G&={DNd-#FNI<6$hCL7&}q;TT;09aOHq|6+Q*a5=0PWZ98}f*d=aK ztOkg~<2a5kEjuFBy2i$=kG3!$b8-5(R*Sc!UV0T-- zV~%f;#!I9}Scfgcvv!z8chBhN%Ggi_i(I$77g8ux`NwM~=Isr~+#H zkVPdjGA@)`vuYM38B;ee8f>;pWIbJOb%XrO(`xLd8ns)G>Qr{ioyw z1B8f-x%g~Vuf|i4YGrxE>nLJ*P#oyFP_L|TBKyVEz11BGGWaE6T|L(SlDW8x!OdWI zD{iwMsMRV}FS(5A2$YlgfX7*Goz$)qC(ONDK{%GHl#hk?L02;Qxs3_O8uMuM90jdH z02bKS`5>H2-+H;>-(|~fqMW`APFYY$LrbV3lLLR@4BB3>(V6<}B;?IkA7>d?1ix?| zgvl*TQ^Z=7ED(8-r!&8bB~iGCJXB!&XUoLip9GAxZOYrfUWJ{N212m8N*sinWAi1f zdhV_|p}s^z3H{*XhxfvKE5K$al?*mlq22sa!wCCy+`W`t=jK?!#0M(GL%+0`v7URE zE>eM!vbDRdIWpR|a#AcbR9`wC#2>MGALAs2X8kX_S-Iu#4id#t#%r}fObvljaM-zW zIpWz>2Z<<*fcRyKgwP=CqTK}(*8pL2HJ(gz$V&6;;cxA_2Q|#ebaF^)c?f?eG6G^? zrtVsZHk*U8J`_pWm;j?wv)!MNl3!xTaj-ck%P&bUJDI+FxtHF&y(YwINZ0UksEOcg zK5ETTx)8EWiZr9LqTN$FEp02%A<~A|;#pcn%CDB5CBH-P#R9Q`TKsBr2-&<>N+Pj~ zzk2a)I;*L{%fFXtKFXu!C{Z1!3NQMGag2K_CjeTnEt?*h+Wx@weWXx9uYRlFSq}Qa zoBs07CTr5NTV%cGLIcIaSgweUF1iEgVDm+7+Zo2Zy${3D4m{bprS()bUDF3vo6W7+N z^73+{z6k8Bpk^IC=amn#>50?7ezDiIwzlF*tMIb0uyAzQyyhJh4il@xDR&{A%?c;a z7tA0)8$;rKbkhe%%jaljKa*4f<>8hlVA>p6SjGkwyWy zeFr+4bv+PspHFqVb z@EYunkB9eC=YFw?v*x}Pojo*coa7ER3Ha#iZEI$>2U6hc2-tJS^2R^?SW;fz<}{K_ ze}oDKr;NEJk}J*H>y=kQF@F6lnLNGBcmK|do&BbG2!1<+X(V8caai$17{q66FUYK# z(KUP^pa68{a-6nyyunIo=-J6uA+qRC`3mNEz(t21S>(;+hu1%jgT=djKqJF?P&@{! zI`IPw?*<{&k6Or8gN2{ zHZUf>%ulAvgu;Pt(VG5{4}FAPyN)CDg8&XICAWm2oom@IN+YhYNj@C{B7KMIh-H%P zwE8gk)1_N5HM9t<-t#SfK#kErOux*M=z3$m038>C|KoFL zvN`(ZzR>nIUC->HOfIFr7yUsv^N27#P%cDm?jS>B@8$l6URQRk`br!m#rE(JL$`a~ za&0YucD4yNI9RuUw`_h1$5*iez_y%4mt8jT$zEdIH0GB^env{!DYn#e69Ye9syQCe7&%7Nsq4_n( zhvKYt&LAT&=u)j6inrojK;M80jauf+IOot23nXl*}k%+fbf6k;p1QL5)lc<$UzdQ z40FbBp+G?2hvE=c62a!!OVr_zoFu!HB?wkxw1G{>ifXym-sM*W+l^9m&m39#b0cstiZVAA4{_IpdWRvgJVGS}FbDm1bq zQh?a8Wv_KAs;9Spb0soQm-kZKewNpDO4*uPjB9@Fny$hgU&$d~pXL#`F%V?v#8)?D z1}o~Eb~=}|w)RM?UJa&G6*5BeF@!AtN~Bdu{e6hMy@*lt-W83qz>Of1&~=st?=bC) zp!OijHpRzvn$Gc?3@5tkTcQx7-C<*6Ya8-l0`G-XQMZrI#*BiSA5$a(iz9MUETz`unsJr|1%NF9eP8W1WnT%|9j z`WoOnM%KxLK~BrH?(W`@U35<3(#6Y{w;Z5evzsZxlDm*;>FtyOl?cqx0J7$ZLw?7d zGOeBVld(l<8qO6`PC8tQIU+s+Q(yAE=>rR?!5?(g5T*`{lkoLlGUnK$xX+ib(tk=0 zJRgbj+Z7|Vf6?_$1T>A}5orQs7X3cikwMzs0dJYHT9$ zr^9%z@-Ej-;OB)&QbX3tP6&Oz^dD2Ez(YU=*}y&I`fX1DrnjAZ-)ADXl) z*YasB>wz^d=iqbngO@N*=;!Go;F--&hX(;|RWQT-7&BKGGpzUEJ-R;DP)pZ9>|Utcu;l-s zA5hy488bsEz;GOIII^fScj++06voH%Q946KqClIWT@xNsGRz1xV<%sds5YKWrp#Y z`aGysR#U6oqd&dI*tD9vjN_{lOoXY9(h1d>`D!PIF$9OhtG8})CL2x7&!=~E?VeQ) zq7f?xj&pfGFMHCyZ08*mPQlblTan#eH58#}G#2X&GY>u$D-8@eNmC9dd=~u#iQjC6 z$K`ooE*ie=;xuxI4uym4u#joF(F^zLMQnU0yR(z zgE}xZOEF!OvKC=3M658(JfFd@t%w6;q#naD`IBTwC+(p0Eh^B4`{GHk*h>7ZfWiGZ zdOT2jPWs5S7b%}Qt+@8#oqG*emPAEGQ+gkMH4_pNn%S+i+{CA+ z92WD0Rk%2#b19LTi;Eq`k3v`5v*isXPp&l<;!FiaTopks3*uj%+Z@rx<+f_eltwU= zfa=0>ilznJ=RFOQ*fO6KJxc7tu<2WcSPAWS-ty_QVm!*B*HxJbld zJ^;y0&cwNZjMjb%r&}%`2~sY-$Y?d6bC+q$T0K|Ln0w3I1pLvtuN?Z#iYjf0~1CE7gWUj4)15*O0>lNL0j)15b9GEr<97>m};?aRA$ zt$};FPQPAkWpzg<><7ctsad~IQe1@@kwB^rHn$B6@Xe877>qmG zMNPq^6`tG2Hs;z{VvS_>BE1~#r(B3t(xbU#DL_!N>3l9%9Il-<8M<#Hf&6k?_G|1i zQvOch>ShSHoY33P4Bwi2Y5Qp?#PQ_V&_;*ED?z1wFNpAR&INfBe1(9($#+AL!f{~~ zW6V-D$5YD>4OD$JXFRA(v*z=yWViqg*Aq@(tQQ`$ReUpuYxu2Z40s?sT|&227Kkt? zx+4hwXe?7!`S0A7L=FmqCC6|57p;<$jU!@WET(vog_$vKDcwrUbL)ufNU;ljddSaD zDZ&hys&uccW~s)wFbSn`nd;fJE?2EPDP^|;p;k$k%g*Y(mML@0B~OFHo}klA3~Dxl zsT%k?!aHB}4`ukg)y}lC4Tie~&0mGz#f&B#?Y^@bj?qo=vswAj*sg>qTBL+Oqsv7j zZmYrcsXsRg$UW>nl>WT01pSaQWHCg&U!IS-H)^4OQ`JM-%r1ye8epR4`b5OKOzO57 z*Zo-mIC=#?;`)6U(+bao9CsK0%asY?tyLRxBs4{}tAMnMG24m*iy>L!m#`RIYByq_ z_SS_Q6(FTem6p-gA_0GOrPQKwAZ5?|k!^J;01(S*S!n8)lF7lLMx@Q9ZS-k2K)PXY z+tVj|VUo(tuUd(Vm2>$du|hV>ae8UVgs1+}sQ*VpV&NR~W|gkO4=5U;H_MlxgsATM z5~I}I36UHGBz%=u5{>oe8@4Pi>8c*Cib84aPYBKqOEl|5g zsQIjOzSrS+pAHZJ$@x-WB$yq%%n8Xd2VaD|39Z1;!JjYD!Cj$1b)`rRVuz`$@-s`} zpzTRwoEv3`*G3|6x&k%6w(2C$dKO?n&V1x`3?^V%jt(mDN%$O1kcM`79YU|g?C!JC z`K_R)LR!qYP?9#;{2BaaJ!<$QEEYjAcvA#oD1lDds5GsqqULZn%@Ha^KcsON>mwL^ zflQG;fiiFsys7BYflIEX;5+TrXqaxVi+V;QvUGOkkJ0}2u-LehN8arwvyXGL$IQzm z{!Fn&eEth8)iLsJ#!M_TBU%|dOI3C$A7b(8HBuXR@AQ7D`|kWojD5_ljv6~sG`((n zI3u2AH_Qpwu>~48dE42R@--QzEF*?&GalW(7YF}|_^Gp0tV{RPr$ET674G`Uz@MiF zXdE7@;CtH5SJpU{b3*gl(QEDMopK}M{m)<*vlEJnit$11qJl*|-`%zcu9$txB4`F2VuPWT01P6;C z>UEiO<9F3G;%RKJ!$Ig*7t-aoJ!-vvQ9N`0dYaV)QEiKQ1SjhQGE@fM@A|(UADBA< zyipDXvh9A{ygOeT2w#FGvgv+7*4%juNnr2kaXf-J*afFy)5miAINom_Gld;P$Srug zLf-5g9D4fP<*sVkzoNU>phOgWQ1vO6%}9=^Z_dRgyN2+J>bPJAApmz&gE+iK6xST^-Uh|wb7uee4zgVlSU}Pic^^< zYNW2xU#Q3A?vA1A%cI&%B!1Aanfz!9qV59anc;a_(62iJ4gv%@SA`8GB_9CfT(knu zyDhHJy$Re8XZqapzql$lnxnmT`h~@U2h(=jR9B4d{O6G_ZKXzBT4758x>;!;#J-^6 zfv;Q#mpE57IlcAA!*C=i!i7OK4ZTvo#AHfUgvo9Cj#yETZbl!uqd-w}?#RL(fFg^= z*B?CHhEc7{ShE)TS*xZhfUkh1CKRyNJP6veOT3gmi^Q1Nrd5ICY3rsf!e4a>j z41xgnJtr|A?H@QFDPaoxA{GYX+H5a#In;gXfDX8_@NQZQ@>fp^pVZaW1;P+twO)Yh zmInnLQPP395$|#W7(RA{*&8uH6@F1vwdXg;^`^%^T>KpSAO*+m=Pz^x0rkx$d=eV& z>n6BCCB_L4^uf6BDt9<4Xz}PeE_8V2sH9hk>XjXCs?}jl4W%JJy4Qa!@b>nWRdM2r zK$!pz+qGWs&#oX;U+zN?XW}Dcy+^OWd~noDGtxY6O(49-nx48DmABX3@xqm!HbVfa zEtK#Rbdw)4a2~elv@QrD9a7sg*G>+a!OVa1y?ce67!oA3`)9($I6trfoPdK53i}WB zOavb~_X2>}{nH!rkao#iy7=F_eX9A}#JE?^%j}=sw!HulN19Z<4kw->4>f~#B>VW- z?Hq|WkXV=2)w#Gh>(AcXX<^2Cfgho-``2}q@S<)+aj9LlH12UbU&Hpc^v1@xk% zAPRj*&7YwxO2Kdm3)j&Kq3()%8O0um{HxQzdO*E!uN2||f1eF=*D15HdMY2nSZsw1ICA|x9 zdyjF?#B?9yLhf|BE-?#0nAP4yPYu`(1|GzGm+z+lk7>pw$|3dW21&V(y!Ub7Nu#o7 z?TSykjKn2+j!P(}%C_MoU3Zd`ULi%UrQVq;MBWv*mQ*IdVc%BL$0C11}@H7YVg>lGGTIOYf=OWfDRPym;>{cX2&%K+F~m|2#av z%=G>7OS1+G`$wJ=?n(bfC@#G|3 z=e&;Z$dY=-!L)ImXCI{V6)QPvBBfAW{=TyeZ)ZBxb!?oC-XzOB9RX=9sVi%IOpz!Q zs-(041`2{aCTxAn9sFZcIEx<|a%oTsfe#S*Kt9m^9xvE`O$h8B6$>mzY`|H8hJdOH z3_dzA8qpvGZMhUmCj#|+;XH&n+yUwfu?qsCIN2G0$%xeQBbA82{YU5xwe=UY`+d!N zI5Q79(KIao>`epmjhNXh%s%-w&(c~X{*R$=0a)4gWr~0DUW-ZQDkXA*u%)ZlK`rPC5k~0j%*EV z<>Eg2=K5-6B;GP6H3wSvf&B0i>hM#@c@tsqH2k0+{G}iEPUx}UXMteG0}TmYNYHo) z%M;hR7efBQceuGVZG*Zzq|7~&Ef$U~r!Ga`aMBCzYsd);(7R7LuoLJH>M=Jvco`b* zt`lR!!=3~aV|o|M$0F(yK3|^(cppMgi#m5qoVAZ<|>f+qoxSuWk}4e20e zY*UvDc5xSRG+!0Ts}U}o2QASK;YlF4_*>KrSlWS@*bD5ZI$!Y!@5HJf=obI7uP#sX z4HkT)Z@sa#%Weeyoh^WD4YL2z9)C98^vo@EPHo-FE)q7SS_Bhe=56Dk)*`M559vbr z{mdwkqDXW92ukh!SK7b`SR&q0Z<*1WA)EOlFF8@?g$h1S2hG>&j_D)@C5C6u=sK#jZk>5O<~y zMBC}`e|+1W=|KYR&UoA0LOJy~-i(r^X%J|ENLH$3M!%em(j-4C1o(B@68nl)rmF;& zcKOQTWB?RfIbr)P(;{Mn!Gcp{=-hI1U*V>-k%(h2(cj~sVa14dL7t}wp3|U+QThrR zSfc}p0O7gD3ax>2;j<&q4%H^-c;**dqClI9LPxo?xbUC2@2?oWMBi zGYil3j+Z=C@m!QU($|}F%ylDb;{H6auQmh^XJK&OXwrK9c;xvy!f4ruh5i#gZi3`Q zJG~MyfrnW4_#}g?2t-lh0H$5}dpw9U@0lyJL3d>Dyd6dKG?YwRi0BN#GPVtb_oR?a z#Hgco(50{&sNy~tNyjDEnFxNwiFaFExq(L=T;((NQP9Yjnd5Fh6H$!--l`g5t&`az zpni!Dm){$gc8|^Fx4FY*XvJjYY&r)}4HM&@%&o@G8}2&6Nr%JKC^s`>mx@}qN2yqe zJ-;e*F=T$q9n-h&hPiMv0KYj`TyW%viARl4GM%=-Ht-wlQxKw(zNGh>ymY5ZLr_zD~#W14%Obszy1$>rNxt@eVkDI$Kt; zsz^7N6f%0-u^2Gw1ywM8=VfQ?aAPw4xeaIjH99rWtN9!BaF9}mivp=kVP7#wQP!ppl9s8?F`8VuSkQ zt&Bx*k46D)(PaB-8CCLW{?&t|!sMg+-MH_|x6&>jG>o;beTI~Fa}@F?ugO(ZdncH- zO7|AUyL`bd$@0glyJDGR#u33X7=)HzA^R{j4vGMu)r0T?4h% zr}cnFQLR@;AoXB(u2@PCyL4RUw?M znv=OxM9#Sb0hlzt505bp-e;%%c5u7vgIOyvY`5`_L4*OhGF!bku>*;xr6NqSD?q8=PX{6-T;*r7+W}W!&NHI?=M;z>i?U1*Z<2m9 z6fV+WTC6CA>)2s;xscg1QrFZVlrhOI7i#QR3dlOEK-oRDvh;dcaYQj=11HZlzz-R% z&SF&{QpQ*H*v(P4p|R?_2bx0*qvu_H%qJ5iClld1wdB)Nl=rZzZ&g2No#hKO%00o- zCKLc=7rq{j&OK8{236 zXZMH0d-Pdxhh~%)?!*gRsNCrJO?T=STFcn zmv{!sB~;1>2yl;P7_-mBfz>!0n66Z>7?XU2fH4T7q;4%iKfpsR;X%uVRnSyqT@8W9 zISzR?L_NCr>>ln9MUpmXU^!wc!ES9Qd3@JBglOEE%gmncQzc7sWQR25S)5)c^aR7>-p7)?_VqIM64o@NMqT_P#PX)k+3Mbq ztNZsq+pwN)Y3@&;`5`=3VWTP5wa6)>I#9E{t&scx2etyuM3it`wn86X?;JBXdRw=> zXZ8q0@RHz9E7YT_VJGcgp~p#`K=@=mmw2M4YrxY({2pv*CUsm-n)=17= z=!mV(fhu9HiJ@U8HX8w6LM@7@_}NQriQR6y$mbv49MsH#F@)vOiK;Cl8olcUVYE^q zfVqX-{tDMSp0pZ)ts{&j1<>;&4VyEgd~eW9Wc}Ql5{&NR$D^k z7+(HPHE%rv=a zYZafMmGlK`d0zK3g+V#g1o%c20#TrQ-JO)%C{>C!H@&!TP314UmACpGeN7F!PeIwo zpY2l8jsmL5>QADOpzvh&1K-9iS; zq_V!g*XGx7$NE{>EC*u5r53ll&>5F$$%m#?b7 zMjpS9rL$Mi8kBirpwu%{ASO#-)J0D1ta^YPPiiSO8eos^qoPVK3=5V8Sq{k{Anxx| z*2#Uj^SCX|oe5672p~pe(K5-RwH=*LpCck7q%XccQCBj%;X5}!naAR>A4Z=nHF81; zW46u*Y;$grzly`XWR}A1y+m;E!GiBjl=Nb}XJUaYe!ZsddZ+y{is$*glvhL0Y-hm0 zSzK~YT)ZmLaBkiW?R|KR^hDgLYa;7#VhMu599S6+nROh6n45rt5B|wBW~DgpC8jhD z8G@<8C^7;iC zN_SLE5LHfu|Apw_Whs<5>##4%raK}wN5M04Wi+cE7Tp99zvuN?gUyndNofk?3V`a4 zN(5KDDjZaYCeVorEI&vZB36jakybcsRQz}uexWf4HRgqW%jf;+vFhtr0~B*5_g^4_ z6itBq*PKdWUMm1ve@zFDvZ`uF&{|F}J}er`PrLH7k#kKS$O+<{J2co#tpFzpTw6`W z+i`b#UDD2mA$(9&+iu_UQg|!D#F3^GxQmO6=Ok@OFKm+sayBGfOMajubM}B1v+zIz z<+cI?D8|JdTU{H!5(PZaTbv#`#`{%qgPF$AmP&4q?AZ1`7x4}#g3lE>a2ZvJ=(FVi zY{nZM;P{&C?7+S!6_%Qm8fProoW{mXZCw6-ip?c}z3(Ry+kG(UJVUhXPhY{jF!M(bdW{nrE6{y|d8Zz?_YK9t5H&^0tW0$v% zo-3ZB`Use`*1j(eS#4TNd4w!J=%b+VFpE_*QF8T=b!b!5wsg_*&WbfiBT4lyQBG1Z z-D&TCCYxa!?2NduUKLu8nMK>vllkeEv(G-QuAQ0BJ?|{K1%nn+zq&)hZk#J*OKbbM%r;e#_KSw-Rl$r*>t< zM)-ahhY;a|@cApWhsy;XMn2>4({K#*J&3SN;I+|y{Ko4V(4+@2)A5IBW_3x7!V!eZ zA3{JJkJ}kx3g8lkCo%H)2tNLilqoG6X+fB{6*tr157Ghye^8%ea7p1Zl5?VruOS_k zIb(m+;{KMRqM{dL3F6t=+3#S6=SI*VmidN)4u*1aJOm*Mg(-LF&j~1G9=!3zPy>Nz zc?6!Iu=FZ;;1uL34OB11cNKr=V9IHd9YvH|ql4 zz7s(d{){|X3_e+muubs29P+WxD@_qwaQ}D|G9`^sDC?CP(FxC4cW+rG}`+DRF)))7ap^VQ&n?DLBPQe9daA56f zHb~Dhk>80r{3wdxA54y!ntgaSzK*_o2Z05_2q=M=nfeOue_;rV_p%cS`K}tak%H;fzK4KZVyxh$O)SKhl_ynam$QMY@F<`@)2M$*V7Wffbub~&@PY8&yoct#_xSx z)WRfU0eT_+=+F0d$fkXrQl6J7&zwG19zA-D96BOfvms5ByyXf$V{a-#gyh9w|#m z_~PMELgr-4^n7$JnNahQ%L~aoKyL7n9|M0Q{Nu^;_)uSi=INSW+%3h&p8^L*?Tn+R z%>M~K<~K$VG#TH^K~eZ40-r=(LKF?$p9jG!M3C>5xpx3ZUEhHM^!r%Khn^L}CPyNl zh7ZRO>rsynRY*j3e72Nf1|#d?z5V&=5i8hfBzwr^P5k zc-Tv}7fXsL$iIXH#Vz zfPlH27jZc9?lbV4%xO^<&JHEh+0aKLK+veDl721v?~`~6BEO+tPa}PmDLn@#j57+R z=;Yk;oDWc%5}-!xG>>+D48`J|cuRrePQ3ZdnB9@)fH>~SUsO6Cgt8k``3TVC490U| zXpe&xKJr`=-I^rQib1M5M*M*uO&KH+r^!@gFL(&zWf*t8W2Q^4m=t*xNZjN}mUOjW ze=Z#Fp(zPi+%JzEJ4SwgQiWWA*@t#Sd;mqHE5uT^u_ObLK=A)<+$Sq`x5{i-T)h|a z+t;w_r4ANXDc3dPoVZAC3*^q9!AjRCEcd?wXM$XXRWJEint%5Pwc5aVqX5r}!ukAU&3pi89!A3rNxE~vCbYwD7s&ha2`m&eV_I%64vyg&9X!pvx6||L zzy1_t+$x-I?t{(e%doke=6{>8;e6?iR@s8^?#O&7pW$GfOl(p*3LB&@gLMQh0sHT^ z{qku&WwiL*(NIDjz-r$ge&msl_vwWN>aQu@+MoD{eKD!{F-rM|J)WjvljcDX=#DU@ z?ZSujHcb2XT5e%V7jg?hrV}H-RH}|b#B@U>_fK$*_{WB@SeKklc{g5#fyAIJwW9o0 zxa2x><5CU^{mTmq3gQyTkd`wl`$3CedFC}f)LB^nU5qfZkc}X*K{(e<=<;53X!~{u zch6wtd#@fA>laH#cA)&%k?urzU72A$W?)IlaKwizKNi`z5K|TF=@dP)GLVJ(~~;p6{keaFQyJ)8B>YVDI;wS{F^A> zsL1f8(Ws{}{qp5O1(S-o(RD)^jxb7NOym z6}#dt3;o4so%J=2-s2NO8>6XpP+B{nD87bv1lLprO62<@^y3qyDtP~NW1xfkkj^(H zn`pX^=a%muo+b2dJc*eGyxoRpma}{wcq&!8<1EZ@+$2wwf9V;!9J0hNrQ`K7Quo`; zaUJ99G<8KKjdIOun1$dgD8&wM1RKn{`Wz9*q(Up8$$n&Mcqkv0S=c-@EmJP+_p&R! z7%or82M{Y$bjKH37WQKZWWP0(zaZ<;LMm5`WJ4CiPl(fQgY^uF5}tNW`H@TZFT~QT zv6xMHrN$%GFm>9h34VMH=>+v(d{43OqysY;<8Y?PRhaSE<>{2ys`ulFhW$`ny2X`k ze$9uL%e#tfhuj7w1X8X1r3&(=b}SVt!v+vib3=NQ(9YEJb>fxgjK10?OxA>h+|^a$ zbbIPi-SUJp_bCHxH;2k9VW?;wi*Wu1M)0Y8Py;@=-+Mm%<#9=r9h-F6u}`dNlZ z$o>+nZgoRe7=igU?w!(lDiJ4LTDfV7Gp(MCuOSNg4e-VD4cvDmJ#uy7CdlKc!Qe`iX)%XFXgTL|v0Ao^0k z_lLd@eODib0H4=H9gS~r0K&|F(deKXd`-y5_l8jc;WLyMEn}i_Wi6|1jqN8U{DMrh z9H!4{uEk4mVqda;VR>?K4oFk4Esd$D4s?79-mw%AewipGYw{F+p?s8sL#TY5TbU7N z2~^^BP*Cc^Wd_p~GD=1Dnk$kDWic04JKa${iop$#7UV-Hd~(xe{g}R<3B4 zP7TFrRSi_v5UgEN?&Dzfb2w{`gU2+r%R>+64NKDw!49UU+=u_V8+7bVG)6nQH-*=nbs(@b0qbhM3%DY^@O5B z%R6QPkeksf^famX<(hnMY(_gK8R+w=+6tleT|-#dOYC!5fi?ZB5II**09riIZ$9qV z!ADhK^gQ_0N0ajnVdFvP!ph3Z4Okju#>S8=cinpGh~>6He7_6 zp*!N4l?y0oRme}DN{8zD*ft8@!IvoNIt9<$1+c($4`!<(DOcjx|Vo3<4GruTQtJ%vmqX+=x;-w`>3v6PAu0&Za#AU$Bz4i z?_Y^=CwvxbF!OTXM-32vz1q^RhmRN`T$z)I8=VE288acAX!I)~f&8(Qk3>;h;il$x$8o(Bu}3aLfDWjFq3PY{0Z!C(P_RW!dg_d3RTv zoc{85dEsRJ+wEsW?mapG)g7`PCu4H6`zhsFnkBArd1$_QKUz6cYjYeEjoZVI3Pnb$;gqU?_n zA{0oBJvDoA3qBUOhOS&xyDB~2?QgOz~TX$d5p@G zSQq9s`Ohl_uy*x>shRSv37Nf2<(hIPbW2I=I2=O#@|P;))aR>k7TkVK2K>q4!(nYS zO)fd8K;DOZ_SA-ba@yEjS%R}dm?xqO!(EggpFL%aJof1xSz7DS2Cl32cgqL6+vFRs z?UWZ!9p$G<*$ch}TL8ZE)^2%bQ!@s_9r7itIqSfTQ3j$f!a>(Ox2`X443v-`&vav1 zSBpGd-34KVN=Tr>(d0hHg&#lcOEseBDJg+4X2t#wR>K)fh$lD!4@Ml9ufUgfuG@nu6pY98EQnbGEQw_s~hyZy_X?XNV|2L1Rp4+@% zZdz6^pVW6~O2~{NV1feHLg~2p^sNYY zQ;S#Io1)c_4}9zf_Ok|k%Dx>!!y_1(eWy>k!(@Umb_#@!;}8|M0sm1D&`mix%qr7`sxT16)sEqKT}x5Ta5Vr2|RE&5(JmRA^$ND;L8!lr7bZX zXA!{HHz40Xkhsv0fsg$xgg;wZS-At(FTm4O>Sy@f3q`O6dw5!K-6w$?>;AW*zy1ge zs4yEP8J;V16vB=|xt}BcMU?pl+PNCP&6pi(fr92iJl?w2IlMrCw73)P@{}+BO`e*+aRbSL8F2_SCQP%?CI|f*D_Ke01#^RR>coy2zYs3#> z7Obs^tNN);bn>lUnBn!pE@>|el3wsgBTFTndTI!kO-l`3GgS=H`0S8#OyAL-WP#5p z<1z5;!uZ(LlQXmp=SiCQ=qUu|r*E zfAAdn6@`%8=9};$0MBK+Zf-HWKmEGK4!QpQdigg7wNHAXR<55@fQs0({lzy!9()^1 zzc|bBgV}|0!<@o6$~D&`{5@wvZoxs_`@1~y?;YJZtdOr=elp>-hWam!f4$fTV{>tA zCiTuUW^Vk#Sn8{T64;*sM!ag0TaF)@mh@sfutB^9S_eBk3yg9Zv|j8FmtTIiPwx3> z52|33bH_s_&{l~g?>kAxUotjZ>-QfmYmk*tLRK|mfDnxEJRSlzeo8@x{Pge=S&YMF zlId9X>1FA%q5>vQ{hd{Q^M1X>L zktu8_-BNI;l`8Ywjf&T`eQ7B03YU#SB_ANNeb@xyGQ}H)jp953`dFkl3jE{?9)h?b zUOHU%Q(0L_#o=p)Jn0Z7zlr2D=CYX)V(Ixc2o=c6W z3@k^U_i?`oz6#1n1Huz}+2N46r!f0eihNhVSHPPcmy}(DdR>k3XfFCwD2yLMVfqll z{bu;81UKB9=_hm#Tz`);IpcUa{OB;5C!&rMQRl0Xj(B_uQ_stQ(=uFdMH{N(S*jI= zZqkFOcwi6Gbkz4a5I-G$20Rt-5Qq4Xdv&7_7;p&i4Xw$1xWMlb!yIz`K|C`fxiy8@ zha(ZN3m4A>_W&P(0p14>A11dz0pJpOo4sD{I~1=6J~P^Lle`K{E=ZSq4lM@R26?f% zMQ&Y=*D%VU49HIu_a9$7BVWEhs~~C0hmPK)T!(ynVuq~V>yfP2skwB*>0jh zYdsb{h^CUQxwSzED^UnRvYSXj?n>?mU3KJDbsVDt!y#^TOmO-w?lJknR}#@;P0FeNAs;sn$}|`&}$A z+l{c4))(}{I`s2syg$xE-7Z1`&Oj*_7J)T5*sIVcnmvrL36dAsoIFz2QR2$gkTI0h z6wSAUwpA$H>qsw>j}m-9?!)nCNp(9~IwAKp$*THx7`n2{T+D!QAMT0e+1lXq9V_-g zG4NoyQk}GMNle&RXaA}+MN>kGP(BBoA#HRi!-Zch8HL}jLW8tc>~}C81avrT4iz` zEK|X>STgu={hl_t?cE*N+p!pqqk94-Asw7=X$H(Mz^tM}#Xc+2wpu^l0fzdkv<# z0{FGj2q;k0Bo-7Xq0g(!v7E>dq^~v8NC53V;i6w zFA50G#~?vDyqUh9k26IFp$=2=dpd;LWl#uyiy4&{aUa?^JxA}~E(qh#LNH%}GLJy? z@xcB_(B!O3Bwo%y{2av3#P1U1TS_ZjnVFey2b7xLr6egNu#x;P!0AIMK1YJ=ai{}- zxnwC~eGzvs(#=NOmf-r$%F4>$a{o;%YiCHCR0o{6B=q~h>^u<84MV&G@!Jr;9exkm z*ot2cmgxOSUZ{YlvN)t&1~J-DC<1KW1_ z>!ubN0r`^3$HkL*+Ic?;j8e~foIUbfb(`$);5ERpKNdsCBNazWwdgN#nincWDWs24 z?&`H%f<wjgVmm+mklmaQ_ z-@l?B7O?hV+RrCBu%LA&HZ)x^y+E68W7b6|^Tg3^O#xXv(j^OqW%e@n7Nv-gD4tjH zz_K0kI!;ZeEo9dNE4c)ph8R&%d|{zgeEY1H*15Tx*Kc4vj*6?2++ z?=MEeKf&Wg7q|0bl)nb4pMjwI8iY?SADjt)BF@AjUuD~zLFgw15M#G?tkUW&pv zdt=<0O1!oJ=VyWYYiP#~cz)E|p;YpH!uN`XDe~LV-Y+5DB7_fV*99}YS(cgMZBF6= zCKh!|6K6oIa1drA$$k1Dj>et-x@?5Oo^QUjM~bl#omNn#8-g<|LSa}w@QZ^9F`)Z{ zdBbJSFymtozAe?w9rE~R4N`_<6dpLTR6CD@%dQ^zv{5!-P?jchd2U+UJ0Zt`h9>**7q3=L{&Qmb)-F|e8|>?1I2*=V(nn|{qq_Lou)wv{ zh5e9PP;%@d$OxbA0rQao`5yI>5&5e#Vz)N4K;gI!`I$HQorZue-)L0B`q5eohbtSJ0yRfBdC`c!N*-vINvk9@L6 zJ7?r;lk&9njpT(KjnjSF^jKOy25mX1v0-n!yoXcP{ftKjgm*wr(wGsy4u3|HzvWva=Xq(%f6Uof1Ibo!Gj8bzcl6iR-&13G5lxGNp6Uf#u?eIWg`YP7j z;{i{O%gMDRDL^x+^%tKuK3XTB}-h{GSQT9)9AGZt42JAqF9Z*I-#zByOV7VxB1j;-D zo@FkE4{o~0H60ak?!EYBWo2az1pB30Xo~*?WYeOi{On?7njlf6Lq;Bo_l#Z5FhP>XorHi4{4NB2z>Df z*dT;xN=OgO@^)iWw`|lD56XT$13_Av4a$1JD<}sdZU8!`r+ADt z)pA1k#YvBGIWk4i12`^BGaIuY|7jb=V^gGdI|9>g&6tgN4NI`b<8;bjuimREgcYzl zrD>-cBL--pxJDcV^WiR!rZ{Ay>NJI$6IN3VZ4(t3+JGkW{l8;J$yX=l%b%f;T=M!( z`2dW03a0$H=_u&*QGj4TA1^cwm`Vom;x9bIv~c3dYXh!*EapJUhL3wqnD!1sIHt_{ z?jYFY!kZW1azoIrzEd6WIbqt|yB~s58)|BtkGJCl?Hrh%4Tp7~8Uzj}8r?8xBCItVK4kts2)b||~qYg&*E}p52p#(J=1JyN7T_1jt zS8~fKCE&xCTGyO2DYXTpfO?ZvR5TBB9u7OXY@c0A;offg>g{HPmBu4&pt(3 ztR1`yr(;qMxN1g`@;>Zp?6jGG%9dGAl64N{7{ zW$;EvA#tlQV5nU~ux&w`s2!9W+U9`i*~z%CF@z1cOKc1;K)-**R584SKAcyW}H^OAO3OBe+2^UO?YQfDG2g7?nl6X8DSs6N01)O zRAb*mP~M4DKY?KXXYc}NSiS@wRDMtH9?|VQE3L4`z5%??wh6oz$0=tP-p` z2ZT>|7ao~#G7c3BP1XZn>%W@7jBv^=x+C9nBRdDMHMx&*y`BX5OmfE99PPlra1;s<|7~nc#|K(|iRDyx;)Kk09vU;NNJ?A**jO(*tz-R{ zPITa5BeHPFO^e)&lQxek%fhMVtypVl!>O8?!S#ML1^hYeLkF3#pM2=of>Ey^zsDS#&A7B61xgkUoknJ8=- zT?=^aGh@IIyynRE7Wi^Q+tPvO5xD=4A#A{1LSQ}tl`@-Uu0h?eLHH;AsW50C1v+bw3H!lP|#gTk^0;c@XLl^g6Ac@TUz8hKAbKAjfN zmzV;jE6-$&%Z74uP-dR7E`f46N)wl(V2|hOW+Wyb%r|8^2KUKQKx~*%;mk_NPGszZ zXvGq-dQ-U(Z=pC<0jFDl(-EJdIyXhN67JTZG|KbGp1j1%%g>`!tz`31hor$ZG#Ht5`~~Px#}26)f)gy=Zb? zFXe;OxD@_`F^bZ5wJi44Kv(m%0_?zbw*?wf*YsvD+X?vUE-2` z*tE^%R9V=76wQ^wb{RL0bA{l;*jV)N$Mw<10 znD+8%*n5IgIJxGJ`&%By>FfW5hFuC1X%*O*6r(E9s_Bo@7?DhedJ$ty;lMAa3wdfempPYaqhqHj7a9Kf*2X!N~j3P!c#>5Udjf-5Vd zJ)xmjnkd3Q@VZQ=&xmlqnKDXH?qeFVFw@89DxehnU`{cX)q7#_3dV3Cw6yz+yg=VE0;$na07WbrvpP=c&!P~1do7Hpf-5pG9v<<03)(!7nou=6oOdF z2h-9~dH4?YfBbN63AADXV>QZyAJq#t|M;C&>AYT zmPYga|218s?$*p`&PGSoPo?gz<5ktas(aq6M=%*IoI;du{C zokblP&J(~ubG*cIQ!tj!f&Y~DNc1r?rb{Z6vc@5AbxROT^-L=B<;{{);HFVWwz@(q)u3|!N+@u zt3VqfQ?f&%Vh2Egqz1pUV#ki!(3szhy45Ru(z`!qT-ICK*qC*axh6<8yxzE8@%2** ziQ^RS$8Lfr52Kl8&HHIy#y-oh(#4?{Cz$+I4JMR_6Fziy@`-*goJ?g*3pdSkm>hjS%74{{DP zpmK7%%6=YL7oN(Mgo%}%FVI0A5Q0R2G+3w}nPv^79*hKB9kj8)j7Kv|;gc{(t~p_n+=68|?(4f3LdcKbZi%St zjdrB7E|hF3h_F57*ayf1NLu)y3SjOv{J$`ONV9!J(<#^9V{4^U?!6@#Wk)>2XGMkx2~|U_(*1#v+ZkLjpM4& zXR%$zKD+En68mxT@6@^&o5ggN@XdhQwO6;)+>jtKr+KoC(bJPw5*ab+& zS3w}ZAeu0V8}wNb%9tD@X)K{6MzZ09AkdTZVSU?OsjE@;*{-nR9*dEu)|H482g7bX zgYXjThc6F`0AdLit|BmRH!wiKNAW$jNUj_%u{{ra3r%yEp}v<6n{b3Czrv%VH-{{0n&)dVIb#A2a02IDX!8 z-?dx`xd1C6|9kFCDJ#sA7aM|dBZQE5o4N34)z#G+=AoKk#&fp|C%ZYf{P3oB`8L%5 z?u3nyMN>=UCyOS_T-b)oblzFPpacxO0?~qUiG7tB_yBVVi2)iFM;4^V0s0rN?oc+KI@-6tmJl~a& zBQUM)TrN7J;t?8--a|#7g}2Z4gD>)(0c@^=5K)h~Vew=d+C~Tj0^dhIZbdpB%=$}p zb#eU@G-?5}|)B+x${G~IhF-a|Osk`*^`;>6`3v=!d|MGAnqT8!0&sqR8BDbAIo zZLaVM!140>`udX)o)m^yIhv2OXTV3da^apTp?71upj#l~wzD1ZZzGM}@m$E2GiS~` z9D&J{E7;1#_lBzxZbtz%TaE$(F@48H5L1+L6~;;(uF2Gw<8wCBUI=e@Xm)s|Js{RS z@^&HI{j)w1ThLgKVIBIK0!V{9A?V&j

B*)CM2FJ@xhVUqt%*kk%n+S~NY_Z{ed#m;BiGFDanzwr)p+2y*yU!pp3UQjhd`p?iCQUMglXXIJfFJPd4&%zn5}f0G?7~^fpx|>5LZ}^U3sx}_RzlU$uM`hGzO_@nwW37{ zA17ls%iA+xaUK8+^mYT2%!LR=`xMEN|6-;3f!+*q2vW~XFZCDOk22dn3$ojIk9 zRKq*$>3V|b_5{uHZa@{N$=8B{$J*v@x$d<+vgCJLB?iO z+Je~sfOjt!9t#HX*O9)z!@fGj}(bm8%`Gkss3+s;a6e;EWd+hM@2u8v8oD`^CEm%+`Hi z`j>E972@%$GsO9bH3YviaE^eueGy(WRkdJ9u1Ef(hU7{!0CgoZf3z!m3(9#jo9be@ zx&WF_pqyTNSqNK%cGe@z@rB_z4~(ZjAe`H$?2aoSKLkE!Q9#4JdrYddM;&!kInpe| zO2ju1utWt8{%Zp57xC!sA1Ny<+k!ehjkwOiWW0mX2yHE+C zXCau$yO7`g;}b-=kJ{*Fe2Y450Fh_lDFkRoEEl29KLG}(vrc$+;AFSfa-fof$pV%hnKDJGMihG77>(0Qc#l3$uO%8(%l9zXPD}#p7`oQc~o zipq%IYLUiZNY26G_CI}WW>gW8J6jj{&i466H5>@eqJri7xz@KMZDrv&DZ0ubBoi$cym<{s6Y1{1J@6 z>li28&rd7F+;3flncm|N#yrexcbuJl8~%E0Yl}T@Oy7ff-c?^;e=8cvZB5788w#V9$}B-Vh00U#ZhQt;_PyQl*Y-ZN@f~|$ruRHog?INcjH%N=>pb{qeR2P{ zXvfjGJ_&qRAPwu=3s0t;_2qdN=DP^q-X~1b5C7j7%kS|akpm5;X@jSrqaDZY|3e?m z09KoD&+|1Et*x!QaQ}i%#4D6tw4ooV4C5TsX}T@00AYWN=VL(UdE{LSS`=2;Ml$3J zL30tVwea@wfyY4ynx$y}1YplPj#vjHmHUCi22`-lRmsm`OckQ-FQdIQY#Qi)|1tl> zQvW-xAHSBXN4a(7-Y{%gZA3*yR>{RB#anX2Jy0{gHm7Q4O_yD{gcdJY(Es%vZ#SkFM zDjE(D4VZHKVB!Ov+t=aYTi1M)*mJd;Pf0K&)jPpcMg=a1z&FiCyantgKr+9s6U ztF&odv7kn-I)1V;WEd;YU>LEmyuyfrnjNdbl*8Q(AfJyX^}TRVeg&yuv^Wb;;!LNn zk*8t*bEv!wBZMnqT<~HT2fXyCiLi3sCAYt|PoCM)E@wkq!Bh-1?I?UaxwTz>2F+J% zVNk}8J$0u{3E)uCBsqI#sr>VTO6kU6q(MUt=y(oRL8SdqSZQB4Yg(}!4GWpC!6=TZ zeslmMzakNq2iCR7qZ?ac9I;p?;n38I#!k8J)m?HLbd688)h}jwdeD)$#)o0XWsCrr z+U<39bw2_jGA0+|YTvTLAN#BD!+!<*KEwy%X?>c^a;(`!GI^`uZ?3GYd>7Yo=fF%X z1)kRdBMKrXxB#Hdjn5;DM&LNp&#f0cz|U<^JU0;QyTz4&AAqs`6|PB49U;{)4qN6Z zc#wg!m)DQycVHuij8Ik`FxgRGU;iSAUT!B03_b;>>K6!8%)E{x!w9D9W;_Rg%_nfR zzwBf>zXVSl_rtd%4Zku0c*d1P9&zV%D-eF0OZ~1R1y+7GVP^dfFngR8`3SuJ#bWt~ z!T&30d>L1k+X+ushOio3i`A3!AGdrx0m0>a2#)_Tb>>O>3GeQD;L3Nm3jPdujT5hP zfbTW9@{svHT=hx`x0SKJ_V*l*%5i-C0b}iU>p9^C=~hEP;CSJ9(pWLiaVXS|vKqbfcV*ZutVdVbf zq|Jk9>p7^Rz5e_zd=cTFM?BRN7y0{o))egDdLzP{M{aXW&4xvQy{v^0N0$$qGn!ys zOOHIiqeEFI|LVe8wKDPlUTu_DU@)52%a_1LoOax%wH9YQ$QQn{J1EekD~C@kme0?h zsLV1@I~G_&C=$YXj?Tm@DAbtBm5(DP6v*OfC9)M0+1udhs&C#pxm-zAI=Tu z$%S(!%33`CV8tFes;WdjHQeMgc|7IW3jJE>t8UrIyygp3VM#T=vpIIug2jDmJSda4 zLr+)8)NO}F%l}xjU)c<~ z1OiJ#S4jSX{eLuINOfyGT=a2huX=b>hxB60l-u#Z`8wM9phL4+?RBkv@|{)SlUNyg zo=v1hOvM*O!nQ;DomO5XH=j9O>apVTwO1PCnQaGU*`7}M1O{eOj)fVQZ3M!coyBbG zjrft<3*ujZ=lqV{ab=_qKY;cy_dOY%7vO&cI&Y4*)ek!u!1Gz)*$(FCs|f!%yc z@N&kNo(vin;Ssz(#Bnx)ycaiMW4+cTa8dgFDUgxWsj2svifAw_#TQ?YOC| z2&8!veRnPDO3yPm?x{U8?lXs@gLn!kzk$C4G%2v;#6h@w4Z_;h!1wDY;P2pL)t3Uo zaIORG{WTKZnXDRarh~BkU&wbV^4V8GbXgP%>3SoVq5o;O1ogigLLVnU{s}3_bf@CD zGIBfeu0f_#;NA5&8DS$`0Kr6{|4D2c{4?5dL5#+;Pb1tUzro}=yl{0dhI6;y2j9mn zP6x0$Vt1X`b@IUbE%Fx#EK6pVOLd@79s-Yd2UP2xfK`yi*ot+*>@xXpC=!YYAokC6 zq^9q-IA7!Do{-$Lt{pQEXa~oo=J}%ckdnKx%LcsV7muxxOXpU}|6(HgC#&|!(>Nk9 ztN@Z3Joe^`A3SL|g!+474ZV3i)bIxRyXA#LM-MnVJFQZk{hc%k%0AP}3S}aMjyqO2 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 S{wo;pBOxX$TJb?Y;Qs*=_DysE literal 0 HcmV?d00001 From ea19740f5a7bb84878856b96fecfd899cd8081d4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 17 Mar 2022 13:44:06 -0700 Subject: [PATCH 073/142] Ignore diagnostics not found exceptions (#12066) --- .../config/devices/ha-config-device-page.ts | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index 54d926b978..b1b8064359 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -33,6 +33,7 @@ import { fetchDiagnosticHandler, getDeviceDiagnosticsDownloadUrl, getConfigEntryDiagnosticsDownloadUrl, + DiagnosticInfo, } from "../../../data/diagnostics"; import { EntityRegistryEntry, @@ -219,22 +220,32 @@ export class HaConfigDevicePage extends LitElement { } let links = await Promise.all( - this._integrations(device, this.entries).map(async (entry) => { - if (entry.state !== "loaded") { - return false; - } - const info = await fetchDiagnosticHandler(this.hass, entry.domain); + this._integrations(device, this.entries).map( + async (entry): Promise => { + if (entry.state !== "loaded") { + return false; + } + let info: DiagnosticInfo; + try { + info = await fetchDiagnosticHandler(this.hass, entry.domain); + } catch (err: any) { + if (err.code === "not_found") { + return false; + } + throw err; + } - if (!info.handlers.device && !info.handlers.config_entry) { - return false; + if (!info.handlers.device && !info.handlers.config_entry) { + return false; + } + return { + link: info.handlers.device + ? getDeviceDiagnosticsDownloadUrl(entry.entry_id, this.deviceId) + : getConfigEntryDiagnosticsDownloadUrl(entry.entry_id), + domain: entry.domain, + }; } - return { - link: info.handlers.device - ? getDeviceDiagnosticsDownloadUrl(entry.entry_id, this.deviceId) - : getConfigEntryDiagnosticsDownloadUrl(entry.entry_id), - domain: entry.domain, - }; - }) + ) ); links = links.filter(Boolean); From 4fcdae842ef60fe038d5f2283d7e6966d9b59375 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 17 Mar 2022 14:50:16 -0700 Subject: [PATCH 074/142] Bump HAWS to 7.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3e6035c3a4..8165ed0379 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "fuse.js": "^6.0.0", "google-timezones-json": "^1.0.2", "hls.js": "^1.1.5", - "home-assistant-js-websocket": "^7.0.0", + "home-assistant-js-websocket": "^7.0.1", "idb-keyval": "^5.1.3", "intl-messageformat": "^9.9.1", "js-yaml": "^4.1.0", From 102568c4bdbe8b22997ca73765236358de70b92f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 17 Mar 2022 14:58:45 -0700 Subject: [PATCH 075/142] Update lock --- yarn.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3e059bda92..e60cdf3d7f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9128,7 +9128,7 @@ fsevents@^1.2.7: gulp-rename: ^2.0.0 gulp-zopfli-green: ^3.0.1 hls.js: ^1.1.5 - home-assistant-js-websocket: ^7.0.0 + home-assistant-js-websocket: ^7.0.1 html-minifier: ^4.0.0 husky: ^1.3.1 idb-keyval: ^5.1.3 @@ -9198,10 +9198,10 @@ fsevents@^1.2.7: languageName: unknown linkType: soft -"home-assistant-js-websocket@npm:^7.0.0": - version: 7.0.0 - resolution: "home-assistant-js-websocket@npm:7.0.0" - checksum: b006f00e8218575caafa68a8c1f4a3392c1471416e233eec94c57ff6118173465b21c9ffe22ab49608d3686c19fa9549e92d1c21be3f408afa474cb93c4b55af +"home-assistant-js-websocket@npm:^7.0.1": + version: 7.0.1 + resolution: "home-assistant-js-websocket@npm:7.0.1" + checksum: c9a87f11222571226adff43f022008d35df1f78799efae43e9a36f768eef10d21aed99886c905086c42c24d85d47c78e328c1be9593c117b397a18ee86b2fe64 languageName: node linkType: hard From 2e7f8fb46f9f94afcbff3aa5010e73bfb0cbe807 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Thu, 17 Mar 2022 17:01:08 -0500 Subject: [PATCH 076/142] Add Date Time Selector (#12070) --- gallery/src/pages/components/ha-selector.ts | 1 + .../ha-selector/ha-selector-datetime.ts | 73 +++++++++++++++++++ src/components/ha-selector/ha-selector.ts | 1 + src/data/selector.ts | 6 ++ 4 files changed, 81 insertions(+) create mode 100644 src/components/ha-selector/ha-selector-datetime.ts diff --git a/gallery/src/pages/components/ha-selector.ts b/gallery/src/pages/components/ha-selector.ts index f38a494cb9..053c95d7da 100644 --- a/gallery/src/pages/components/ha-selector.ts +++ b/gallery/src/pages/components/ha-selector.ts @@ -147,6 +147,7 @@ const SCHEMAS: { boolean: { name: "Boolean", selector: { boolean: {} } }, time: { name: "Time", selector: { time: {} } }, date: { name: "Date", selector: { date: {} } }, + datetime: { name: "Date Time", selector: { datetime: {} } }, action: { name: "Action", selector: { action: {} } }, text: { name: "Text", diff --git a/src/components/ha-selector/ha-selector-datetime.ts b/src/components/ha-selector/ha-selector-datetime.ts new file mode 100644 index 0000000000..80add84bd4 --- /dev/null +++ b/src/components/ha-selector/ha-selector-datetime.ts @@ -0,0 +1,73 @@ +import { css, html, LitElement } from "lit"; +import { customElement, property, query } from "lit/decorators"; +import type { HomeAssistant } from "../../types"; +import type { DateTimeSelector } from "../../data/selector"; +import type { HaDateInput } from "../ha-date-input"; +import type { HaTimeInput } from "../ha-time-input"; +import { fireEvent } from "../../common/dom/fire_event"; +import "../ha-date-input"; +import "../ha-time-input"; + +@customElement("ha-selector-datetime") +export class HaDateTimeSelector extends LitElement { + @property() public hass!: HomeAssistant; + + @property() public selector!: DateTimeSelector; + + @property() public value?: string; + + @property() public label?: string; + + @property({ type: Boolean, reflect: true }) public disabled = false; + + @query("ha-date-input") private _dateInput!: HaDateInput; + + @query("ha-time-input") private _timeInput!: HaTimeInput; + + protected render() { + const values = this.value?.split(" "); + return html` + + + + `; + } + + private _valueChanged(ev: CustomEvent): void { + ev.stopPropagation(); + fireEvent(this, "value-changed", { + value: `${this._dateInput.value} ${this._timeInput.value}`, + }); + } + + static styles = css` + :host { + display: flex; + align-items: center; + flex-direction: row; + } + + ha-date-input { + min-width: 150px; + margin-right: 4px; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-datetime": HaDateTimeSelector; + } +} diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index 365d22f6bf..09a234e55c 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -10,6 +10,7 @@ import "./ha-selector-attribute"; import "./ha-selector-boolean"; import "./ha-selector-color-rgb"; import "./ha-selector-date"; +import "./ha-selector-datetime"; import "./ha-selector-device"; import "./ha-selector-duration"; import "./ha-selector-entity"; diff --git a/src/data/selector.ts b/src/data/selector.ts index 1ecc64b2cf..b48c11b179 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -3,6 +3,7 @@ export type Selector = | AttributeSelector | EntitySelector | DateSelector + | DateTimeSelector | DeviceSelector | DurationSelector | AreaSelector @@ -46,6 +47,11 @@ export interface DateSelector { date: {}; } +export interface DateTimeSelector { + // eslint-disable-next-line @typescript-eslint/ban-types + datetime: {}; +} + export interface DeviceSelector { device: { integration?: string; From 3e2135a485e36629367ed6d6c4c43174cde5d81d Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Thu, 17 Mar 2022 17:04:59 -0500 Subject: [PATCH 077/142] Add radio Form Logic to Select Selector (#12063) --- gallery/src/pages/components/ha-selector.ts | 17 +++- src/components/ha-form/ha-form-select.ts | 94 +++++++------------ .../ha-selector/ha-selector-select.ts | 74 ++++++++++----- 3 files changed, 101 insertions(+), 84 deletions(-) diff --git a/gallery/src/pages/components/ha-selector.ts b/gallery/src/pages/components/ha-selector.ts index 053c95d7da..3a456cae66 100644 --- a/gallery/src/pages/components/ha-selector.ts +++ b/gallery/src/pages/components/ha-selector.ts @@ -164,9 +164,24 @@ const SCHEMAS: { }, }, object: { name: "Object", selector: { object: {} } }, + select_radio: { + name: "Select (Radio)", + selector: { select: { options: ["Option 1", "Option 2"] } }, + }, select: { name: "Select", - selector: { select: { options: ["Option 1", "Option 2"] } }, + selector: { + select: { + options: [ + "Option 1", + "Option 2", + "Option 3", + "Option 4", + "Option 5", + "Option 6", + ], + }, + }, }, icon: { name: "Icon", selector: { icon: {} } }, media: { name: "Media", selector: { media: {} } }, diff --git a/src/components/ha-form/ha-form-select.ts b/src/components/ha-form/ha-form-select.ts index 2e342a39a2..73dc74b95c 100644 --- a/src/components/ha-form/ha-form-select.ts +++ b/src/components/ha-form/ha-form-select.ts @@ -1,16 +1,20 @@ -import "@material/mwc-list/mwc-list-item"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, query } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; -import { stopPropagation } from "../../common/dom/stop_propagation"; -import "../ha-radio"; -import type { HaRadio } from "../ha-radio"; -import "../ha-select"; -import type { HaSelect } from "../ha-select"; -import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./types"; +import type { HomeAssistant } from "../../types"; +import type { + HaFormElement, + HaFormSelectData, + HaFormSelectSchema, +} from "./types"; +import type { SelectSelector } from "../../data/selector"; +import "../ha-selector/ha-selector-select"; @customElement("ha-form-select") export class HaFormSelect extends LitElement implements HaFormElement { + @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public schema!: HaFormSelectSchema; @property() public data!: HaFormSelectData; @@ -19,60 +23,35 @@ export class HaFormSelect extends LitElement implements HaFormElement { @property({ type: Boolean }) public disabled = false; - @query("ha-select", true) private _input?: HTMLElement; - - public focus() { - if (this._input) { - this._input.focus(); - } - } + private _selectSchema = memoizeOne( + (options): SelectSelector => ({ + select: { + options: options.map((option) => ({ + value: option[0], + label: option[1], + })), + }, + }) + ); protected render(): TemplateResult { - if (this.schema.required && this.schema.options!.length < 6) { - return html` -

- `; - } - return html` - - ${!this.schema.required - ? html`` - : ""} - ${this.schema.options!.map( - ([value, label]) => html` - ${label} - ` - )} - + .required=${this.schema.required} + .selector=${this._selectSchema(this.schema.options)} + @value-changed=${this._valueChanged} + > `; } private _valueChanged(ev: CustomEvent) { ev.stopPropagation(); - let value: string | undefined = (ev.target as HaSelect | HaRadio).value; + let value: string | undefined = ev.detail.value; if (value === this.data) { return; @@ -86,15 +65,6 @@ export class HaFormSelect extends LitElement implements HaFormElement { value, }); } - - static get styles(): CSSResultGroup { - return css` - ha-select, - mwc-formfield { - display: block; - } - `; - } } declare global { diff --git a/src/components/ha-selector/ha-selector-select.ts b/src/components/ha-selector/ha-selector-select.ts index ebc2704992..96cc114483 100644 --- a/src/components/ha-selector/ha-selector-select.ts +++ b/src/components/ha-selector/ha-selector-select.ts @@ -1,17 +1,19 @@ +import "@material/mwc-formfield/mwc-formfield"; import "@material/mwc-list/mwc-list-item"; import { css, CSSResultGroup, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; import { stopPropagation } from "../../common/dom/stop_propagation"; -import { SelectOption, SelectSelector } from "../../data/selector"; -import { HomeAssistant } from "../../types"; +import type { SelectOption, SelectSelector } from "../../data/selector"; +import type { HomeAssistant } from "../../types"; import "../ha-select"; +import "../ha-radio"; @customElement("ha-selector-select") export class HaSelectSelector extends LitElement { - @property() public hass!: HomeAssistant; + @property({ attribute: false }) public hass!: HomeAssistant; - @property() public selector!: SelectSelector; + @property({ attribute: false }) public selector!: SelectSelector; @property() public value?: string; @@ -21,24 +23,51 @@ export class HaSelectSelector extends LitElement { @property({ type: Boolean }) public disabled = false; - protected render() { - return html` - ${this.selector.select.options.map((item: string | SelectOption) => { - const value = typeof item === "object" ? item.value : item; - const label = typeof item === "object" ? item.label : item; + @property({ type: Boolean }) public required = true; - return html`${label}`; - })} - `; + protected render() { + if (this.required && this.selector.select.options!.length < 6) { + return html` +
+ ${this.label} + ${this.selector.select.options.map((item: string | SelectOption) => { + const value = typeof item === "object" ? item.value : item; + const label = typeof item === "object" ? item.label : item; + + return html` + + + + `; + })} +
+ `; + } + + return html` + + ${this.selector.select.options.map((item: string | SelectOption) => { + const value = typeof item === "object" ? item.value : item; + const label = typeof item === "object" ? item.label : item; + + return html`${label}`; + })} + + `; } private _valueChanged(ev) { @@ -56,6 +85,9 @@ export class HaSelectSelector extends LitElement { ha-select { width: 100%; } + mwc-formfield { + display: block; + } `; } } From 03677c33f73cf42816728dd31e7414dc560642e5 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Thu, 17 Mar 2022 17:27:27 -0500 Subject: [PATCH 078/142] Bumped version to 20220317.0 (#12074) --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 83de1db87f..2afbb59467 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = home-assistant-frontend -version = 20220316.0 +version = 20220317.0 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From f927fc64a9902be8ac6a7db4c137fce69ad9cde2 Mon Sep 17 00:00:00 2001 From: Matthias de Baat Date: Fri, 18 Mar 2022 11:25:34 +0100 Subject: [PATCH 079/142] Update gallery/src/pages/brand/logo.markdown Co-authored-by: Zack Barett --- gallery/src/pages/brand/logo.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gallery/src/pages/brand/logo.markdown b/gallery/src/pages/brand/logo.markdown index 08724a6d71..f91ecc52d9 100644 --- a/gallery/src/pages/brand/logo.markdown +++ b/gallery/src/pages/brand/logo.markdown @@ -21,7 +21,7 @@ Our icon is a shorter and most used version of our logo. The icon can exist with ## Using the right variant -The pretty blue logo with a background shadow, pictured top left, is our primary logo. It should only be used with black, white, and non-duotoned photography. +The pretty blue logo with a background shadow, pictured top left, is our primary logo. It should only be used with black, white, and non-duotone photography. When needed you can use our logo without a shadow, as seen as the second variant. From ee230b86c1a3fb3804bb6bb862b6aea8a8f94153 Mon Sep 17 00:00:00 2001 From: Matthias de Baat Date: Fri, 18 Mar 2022 11:25:41 +0100 Subject: [PATCH 080/142] Update gallery/src/pages/brand/logo.markdown Co-authored-by: Zack Barett --- gallery/src/pages/brand/logo.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gallery/src/pages/brand/logo.markdown b/gallery/src/pages/brand/logo.markdown index f91ecc52d9..1d75fa4ccc 100644 --- a/gallery/src/pages/brand/logo.markdown +++ b/gallery/src/pages/brand/logo.markdown @@ -6,7 +6,7 @@ title: "Logo" # Using our logo -As a community we are proud of our logo. Follow these guidelines to ensure it always looks its best. Our logo follows Googles material design spec and uses the blue interface color. +As a community, we are proud of our logo. Follow these guidelines to ensure it always looks its best. Our logo follows Google's material design spec and uses the blue interface color. [Download Logo](https://github.com/home-assistant/assets/tree/master/logo) From 470225abde908e95254c9720e86fe2500f69c75e Mon Sep 17 00:00:00 2001 From: Matthias de Baat Date: Fri, 18 Mar 2022 11:26:19 +0100 Subject: [PATCH 081/142] Update logo.markdown --- gallery/src/pages/brand/logo.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gallery/src/pages/brand/logo.markdown b/gallery/src/pages/brand/logo.markdown index 1d75fa4ccc..e931fed8af 100644 --- a/gallery/src/pages/brand/logo.markdown +++ b/gallery/src/pages/brand/logo.markdown @@ -29,6 +29,6 @@ The outlined logo should only be used on packaging. ## Exclusion zone -The logo needs some personal space. TIt's exclusion zone is equal to a quarter the height of the icon. +The logo needs some personal space. It's exclusion zone is equal to a quarter the height of the icon. ![Clearspace](/images/clearspace.png) From ddf1cc0733df07e91821e626909e874817b488b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 20 Mar 2022 15:47:13 -1000 Subject: [PATCH 082/142] Fetch history with `no_attributes` for entities that do not need them (#12082) --- src/data/cached-history.ts | 19 +++++++++++++-- src/data/history.ts | 48 ++++++++++++++++++++++++++++++-------- 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/src/data/cached-history.ts b/src/data/cached-history.ts index dc7c9a9847..a75f9fd2f0 100644 --- a/src/data/cached-history.ts +++ b/src/data/cached-history.ts @@ -7,6 +7,7 @@ import { HistoryResult, LineChartUnit, TimelineEntity, + entityIdHistoryNeedsAttributes, } from "./history"; export interface CacheConfig { @@ -53,7 +54,17 @@ export const getRecent = ( return cache.data; } - const prom = fetchRecent(hass, entityId, startTime, endTime).then( + const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId); + const prom = fetchRecent( + hass, + entityId, + startTime, + endTime, + false, + undefined, + true, + noAttributes + ).then( (stateHistory) => computeHistory(hass, stateHistory, localize), (err) => { delete RECENT_CACHE[entityId]; @@ -120,6 +131,7 @@ export const getRecentWithCache = ( } const curCacheProm = cache.prom; + const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId); const genProm = async () => { let fetchedHistory: HassEntity[][]; @@ -132,7 +144,10 @@ export const getRecentWithCache = ( entityId, toFetchStartTime, endTime, - appendingToCache + appendingToCache, + undefined, + true, + noAttributes ), ]); fetchedHistory = results[1]; diff --git a/src/data/history.ts b/src/data/history.ts index 368dbcd3dc..c81c592637 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -1,4 +1,5 @@ import { HassEntity } from "home-assistant-js-websocket"; +import { computeDomain } from "../common/entity/compute_domain"; import { computeStateDisplay } from "../common/entity/compute_state_display"; import { computeStateDomain } from "../common/entity/compute_state_domain"; import { computeStateName } from "../common/entity/compute_state_name"; @@ -7,6 +8,13 @@ import { HomeAssistant } from "../types"; import { FrontendLocaleData } from "./translation"; const DOMAINS_USE_LAST_UPDATED = ["climate", "humidifier", "water_heater"]; +const NEED_ATTRIBUTE_DOMAINS = [ + "climate", + "humidifier", + "input_datetime", + "thermostat", + "water_heater", +]; const LINE_ATTRIBUTES_TO_KEEP = [ "temperature", "current_temperature", @@ -131,6 +139,13 @@ export interface StatisticsValidationResults { [statisticId: string]: StatisticsValidationResult[]; } +export const entityIdHistoryNeedsAttributes = ( + hass: HomeAssistant, + entityId: string +) => + !hass.states[entityId] || + NEED_ATTRIBUTE_DOMAINS.includes(computeDomain(entityId)); + export const fetchRecent = ( hass: HomeAssistant, entityId: string, @@ -138,7 +153,8 @@ export const fetchRecent = ( endTime: Date, skipInitialState = false, significantChangesOnly?: boolean, - minimalResponse = true + minimalResponse = true, + noAttributes?: boolean ): Promise => { let url = "history/period"; if (startTime) { @@ -157,7 +173,9 @@ export const fetchRecent = ( if (minimalResponse) { url += "&minimal_response"; } - + if (noAttributes) { + url += "&no_attributes"; + } return hass.callApi("GET", url); }; @@ -171,6 +189,10 @@ export const fetchDate = ( "GET", `history/period/${startTime.toISOString()}?end_time=${endTime.toISOString()}&minimal_response${ entityId ? `&filter_entity_id=${entityId}` : `` + }${ + entityId && !entityIdHistoryNeedsAttributes(hass, entityId) + ? `&no_attributes` + : `` }` ); @@ -278,6 +300,10 @@ const processLineChartEntities = ( }; }; +const stateUsesUnits = (state: HassEntity) => + "unit_of_measurement" in state.attributes || + "state_class" in state.attributes; + export const computeHistory = ( hass: HomeAssistant, stateHistory: HassEntity[][], @@ -294,16 +320,18 @@ export const computeHistory = ( return; } - const stateWithUnitorStateClass = stateInfo.find( - (state) => - state.attributes && - ("unit_of_measurement" in state.attributes || - "state_class" in state.attributes) - ); + const entityId = stateInfo[0].entity_id; + const currentState = + entityId in hass.states ? hass.states[entityId] : undefined; + const stateWithUnitorStateClass = + !currentState && + stateInfo.find((state) => state.attributes && stateUsesUnits(state)); let unit: string | undefined; - if (stateWithUnitorStateClass) { + if (currentState && stateUsesUnits(currentState)) { + unit = currentState.attributes.unit_of_measurement || " "; + } else if (stateWithUnitorStateClass) { unit = stateWithUnitorStateClass.attributes.unit_of_measurement || " "; } else { unit = { @@ -313,7 +341,7 @@ export const computeHistory = ( input_number: "#", number: "#", water_heater: hass.config.unit_system.temperature, - }[computeStateDomain(stateInfo[0])]; + }[computeDomain(entityId)]; } if (!unit) { From f5f8be82763a224c73734f5b307554c91f5767c2 Mon Sep 17 00:00:00 2001 From: Michael Irigoyen Date: Sun, 20 Mar 2022 23:32:21 -0500 Subject: [PATCH 083/142] Update required version of MDI to 6.6.95 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 8165ed0379..304c43e2aa 100644 --- a/package.json +++ b/package.json @@ -72,8 +72,8 @@ "@material/mwc-textfield": "0.25.3", "@material/mwc-top-app-bar-fixed": "^0.25.3", "@material/top-app-bar": "14.0.0-canary.261f2db59.0", - "@mdi/js": "6.5.95", - "@mdi/svg": "6.5.95", + "@mdi/js": "6.6.95", + "@mdi/svg": "6.6.95", "@polymer/app-layout": "^3.1.0", "@polymer/iron-flex-layout": "^3.0.1", "@polymer/iron-icon": "^3.0.1", From 6bf2111a3c2cbbc51efe76d44a0f774ab4b6a4e5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 21 Mar 2022 08:23:05 +0100 Subject: [PATCH 084/142] Upload release assets (#11566) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joakim Sørensen --- .github/workflows/release.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 1ae009b033..c7d3d8fa9c 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -10,10 +10,18 @@ env: NODE_VERSION: 14 NODE_OPTIONS: --max_old_space_size=6144 +# Set default workflow permissions +# All scopes not mentioned here are set to no access +# https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token +permissions: + actions: none + jobs: release: name: Release runs-on: ubuntu-latest + permissions: + contents: write # Required to upload release assets steps: - name: Checkout the repository uses: actions/checkout@v2 @@ -47,6 +55,13 @@ jobs: script/release + - name: Upload release assets + uses: softprops/action-gh-release@v0.1.14 + with: + files: | + dist/*.whl + dist/*.tar.gz + wheels-init: name: Init wheels build needs: release From fa537968c41fdcecf8d6b048874e5e9da47ff096 Mon Sep 17 00:00:00 2001 From: Nick Iacullo Date: Mon, 21 Mar 2022 10:28:24 +0000 Subject: [PATCH 085/142] Update styles for hui-editor Update the background-color and text-color of the app-toolbar in hui-editor to match the styles of hui-root while in edit-mode. Previously, these properties were set using undefined css variables that could not be changed via themes (--dark-background-color and --dark-text-color). --- src/panels/lovelace/hui-editor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/panels/lovelace/hui-editor.ts b/src/panels/lovelace/hui-editor.ts index 17f35b3d10..ce6c15e99e 100644 --- a/src/panels/lovelace/hui-editor.ts +++ b/src/panels/lovelace/hui-editor.ts @@ -148,8 +148,8 @@ class LovelaceFullConfigEditor extends LitElement { } app-toolbar { - background-color: var(--dark-background-color, #455a64); - color: var(--dark-text-color); + background-color: var(--app-header-edit-background-color, #455a64); + color: var(--app-header-edit-text-color, #fff); } mwc-button[disabled] { From ccf1fb573a3c2c395f50e5ffcab0528dbb6011c5 Mon Sep 17 00:00:00 2001 From: Pawel Date: Tue, 22 Mar 2022 05:15:28 +0100 Subject: [PATCH 086/142] Fix gas energy graph units if stats added by external source (#11892) --- src/data/energy.ts | 34 ++++++++++++++++++- src/data/history.ts | 9 +++++ .../components/ha-energy-gas-settings.ts | 1 + 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/data/energy.ts b/src/data/energy.ts index d86419e9b2..7fd941b325 100644 --- a/src/data/energy.ts +++ b/src/data/energy.ts @@ -12,7 +12,12 @@ import { subscribeOne } from "../common/util/subscribe-one"; import { HomeAssistant } from "../types"; import { ConfigEntry, getConfigEntries } from "./config_entries"; import { subscribeEntityRegistry } from "./entity_registry"; -import { fetchStatistics, Statistics } from "./history"; +import { + fetchStatistics, + Statistics, + StatisticsMetaData, + getStatisticMetadata, +} from "./history"; const energyCollectionKeys: (string | undefined)[] = []; @@ -136,6 +141,7 @@ export interface GasSourceTypeEnergyPreference { entity_energy_from: string | null; entity_energy_price: string | null; number_energy_price: number | null; + unit_of_measurement?: string | null; } type EnergySource = @@ -271,6 +277,15 @@ const getEnergyData = async ( const consumptionStatIDs: string[] = []; const statIDs: string[] = []; + const gasSources: GasSourceTypeEnergyPreference[] = + prefs.energy_sources.filter( + (source) => source.type === "gas" + ) as GasSourceTypeEnergyPreference[]; + const gasStatisticIdsWithMeta: StatisticsMetaData[] = + await getStatisticMetadata( + hass, + gasSources.map((source) => source.stat_energy_from) + ); for (const source of prefs.energy_sources) { if (source.type === "solar") { @@ -280,6 +295,20 @@ const getEnergyData = async ( if (source.type === "gas") { statIDs.push(source.stat_energy_from); + const entity = hass.states[source.stat_energy_from]; + if (!entity) { + for (const statisticIdWithMeta of gasStatisticIdsWithMeta) { + if ( + statisticIdWithMeta?.statistic_id === source.stat_energy_from && + statisticIdWithMeta?.unit_of_measurement + ) { + source.unit_of_measurement = + statisticIdWithMeta?.unit_of_measurement === "Wh" + ? "kWh" + : statisticIdWithMeta?.unit_of_measurement; + } + } + } if (source.stat_cost) { statIDs.push(source.stat_cost); } @@ -559,6 +588,9 @@ export const getEnergyGasUnit = ( ? "kWh" : entity.attributes.unit_of_measurement; } + if (source.unit_of_measurement) { + return source.unit_of_measurement; + } } return undefined; }; diff --git a/src/data/history.ts b/src/data/history.ts index c81c592637..eae1b3c9b3 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -373,6 +373,15 @@ export const getStatisticIds = ( statistic_type, }); +export const getStatisticMetadata = ( + hass: HomeAssistant, + statistic_ids?: string[] +) => + hass.callWS({ + type: "recorder/get_statistics_metadata", + statistic_ids, + }); + export const fetchStatistics = ( hass: HomeAssistant, startTime: Date, diff --git a/src/panels/config/energy/components/ha-energy-gas-settings.ts b/src/panels/config/energy/components/ha-energy-gas-settings.ts index 3f92d1a023..4e65150f2b 100644 --- a/src/panels/config/energy/components/ha-energy-gas-settings.ts +++ b/src/panels/config/energy/components/ha-energy-gas-settings.ts @@ -121,6 +121,7 @@ export class EnergyGasSettings extends LitElement { showEnergySettingsGasDialog(this, { unit: getEnergyGasUnitCategory(this.hass, this.preferences), saveCallback: async (source) => { + delete source.unit_of_measurement; await this._savePreferences({ ...this.preferences, energy_sources: this.preferences.energy_sources.concat(source), From 6ac51ede52350ca7a88800af0eb9c924d7044b66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 22 Mar 2022 16:00:43 +0100 Subject: [PATCH 087/142] Change Netlify preview URL (#12095) --- gallery/script/netlify_build_gallery | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gallery/script/netlify_build_gallery b/gallery/script/netlify_build_gallery index a5732d4c83..db8be615d1 100755 --- a/gallery/script/netlify_build_gallery +++ b/gallery/script/netlify_build_gallery @@ -23,7 +23,7 @@ if [[ "${PULL_REQUEST}" == "true" ]]; then createStatus "pending" "Building design preview" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID" gulp build-gallery if [ $? -eq 0 ]; then - createStatus "success" "Build complete" "$DEPLOY_URL" + createStatus "success" "Build complete" "$DEPLOY_PRIME_URL" else createStatus "error" "Build failed" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID" fi From 0e0e07437fa4b67beb89f00b99312d398ddfd12a Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 22 Mar 2022 10:08:43 -0500 Subject: [PATCH 088/142] Update src/dialogs/config-flow/dialog-data-entry-flow.ts --- src/dialogs/config-flow/dialog-data-entry-flow.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index d41cdad08e..604d073345 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -243,7 +243,7 @@ class DataEntryFlowDialog extends LitElement { target="_blank" rel="noreferrer noopener" > Date: Tue, 22 Mar 2022 10:57:09 -0500 Subject: [PATCH 089/142] Stack Action Inputs in the Button Editor (#12076) * Stack Action Inputs in the Button Editor * update style * Update for other editors --- .../config-elements/config-elements-style.ts | 5 ++ .../config-elements/hui-button-card-editor.ts | 76 +++++++++---------- .../hui-picture-card-editor.ts | 54 +++++++------ .../hui-picture-entity-card-editor.ts | 50 ++++++------ .../hui-picture-glance-card-editor.ts | 46 ++++++----- 5 files changed, 114 insertions(+), 117 deletions(-) diff --git a/src/panels/lovelace/editor/config-elements/config-elements-style.ts b/src/panels/lovelace/editor/config-elements/config-elements-style.ts index 061d3aff36..2931974935 100644 --- a/src/panels/lovelace/editor/config-elements/config-elements-style.ts +++ b/src/panels/lovelace/editor/config-elements/config-elements-style.ts @@ -1,6 +1,10 @@ import { css } from "lit"; export const configElementStyle = css` + .card-config { + /* Cancels overlapping Margins for HAForm + Card Config options */ + overflow: auto; + } ha-switch { padding: 16px 6px; } @@ -25,5 +29,6 @@ export const configElementStyle = css` ha-textfield, ha-icon-picker { margin-top: 8px; + display: block; } `; diff --git a/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts index e5f8525d99..2445a1e045 100644 --- a/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts @@ -1,22 +1,22 @@ +import type { HassEntity } from "home-assistant-js-websocket"; import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { assert, boolean, object, optional, string, assign } from "superstruct"; -import type { HassEntity } from "home-assistant-js-websocket"; import memoizeOne from "memoize-one"; +import { assert, assign, boolean, object, optional, string } from "superstruct"; import { fireEvent } from "../../../../common/dom/fire_event"; +import { computeDomain } from "../../../../common/entity/compute_domain"; +import { domainIcon } from "../../../../common/entity/domain_icon"; +import "../../../../components/ha-form/ha-form"; +import type { HaFormSchema } from "../../../../components/ha-form/types"; import { ActionConfig } from "../../../../data/lovelace"; import type { HomeAssistant } from "../../../../types"; import type { ButtonCardConfig } from "../../cards/types"; import "../../components/hui-action-editor"; -import "../../../../components/ha-form/ha-form"; import type { LovelaceCardEditor } from "../../types"; import { actionConfigStruct } from "../structs/action-struct"; +import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import type { EditorTarget } from "../types"; import { configElementStyle } from "./config-elements-style"; -import { baseLovelaceCardConfig } from "../structs/base-card-struct"; -import { computeDomain } from "../../../../common/entity/compute_domain"; -import { domainIcon } from "../../../../common/entity/domain_icon"; -import type { HaFormSchema } from "../../../../components/ha-form/types"; const cardConfigStruct = assign( baseLovelaceCardConfig, @@ -149,38 +149,36 @@ export class HuiButtonCardEditor @value-changed=${this._valueChanged} >
-
- - -
+ +
`; } diff --git a/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts index 0df731ba97..a5e156c42d 100644 --- a/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts @@ -1,6 +1,6 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { assert, object, optional, string, assign } from "superstruct"; +import { assert, assign, object, optional, string } from "superstruct"; import { fireEvent } from "../../../../common/dom/fire_event"; import { ActionConfig } from "../../../../data/lovelace"; import { HomeAssistant } from "../../../../types"; @@ -9,9 +9,9 @@ import "../../components/hui-action-editor"; import "../../components/hui-theme-select-editor"; import { LovelaceCardEditor } from "../../types"; import { actionConfigStruct } from "../structs/action-struct"; +import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { EditorTarget } from "../types"; import { configElementStyle } from "./config-elements-style"; -import { baseLovelaceCardConfig } from "../structs/base-card-struct"; const cardConfigStruct = assign( baseLovelaceCardConfig, @@ -78,32 +78,30 @@ export class HuiPictureCardEditor .configValue=${"theme"} @value-changed=${this._valueChanged} > -
- - -
+ +
`; } diff --git a/src/panels/lovelace/editor/config-elements/hui-picture-entity-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-picture-entity-card-editor.ts index 0d315cc889..ea529a2a35 100644 --- a/src/panels/lovelace/editor/config-elements/hui-picture-entity-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-picture-entity-card-editor.ts @@ -108,32 +108,30 @@ export class HuiPictureEntityCardEditor @value-changed=${this._valueChanged} >
-
- - -
+ +
`; } diff --git a/src/panels/lovelace/editor/config-elements/hui-picture-glance-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-picture-glance-card-editor.ts index adcd07a57e..d6c6e1a0ac 100644 --- a/src/panels/lovelace/editor/config-elements/hui-picture-glance-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-picture-glance-card-editor.ts @@ -1,13 +1,13 @@ -import "../../components/hui-action-editor"; -import "../../../../components/ha-form/ha-form"; import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { array, assert, assign, object, optional, string } from "superstruct"; import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-form/ha-form"; import type { HaFormSchema } from "../../../../components/ha-form/types"; import type { ActionConfig } from "../../../../data/lovelace"; import type { HomeAssistant } from "../../../../types"; import type { PictureGlanceCardConfig } from "../../cards/types"; +import "../../components/hui-action-editor"; import "../../components/hui-entity-editor"; import type { EntityConfig } from "../../entity-rows/types"; import type { LovelaceCardEditor } from "../../types"; @@ -96,28 +96,26 @@ export class HuiPictureGlanceCardEditor @value-changed=${this._valueChanged} >
-
- - -
+ + Date: Tue, 22 Mar 2022 18:23:54 +0100 Subject: [PATCH 090/142] Add support for update entities (#12059) * Add support for update entities * Apply suggestions from code review Co-authored-by: Zack Barett * Add to gallery * implement xx% * Adjustments for skipped * Add progress bar * Add UPDATE_SUPPORT_INSTALL * Allow skipping without install support * Add version to service call if supported * Adjust changelog link * Use Installing * adjustments * Use unavailable Co-authored-by: Zack Barett --- gallery/src/pages/more-info/update.markdown | 3 + gallery/src/pages/more-info/update.ts | 140 ++++++++++++ src/common/const.ts | 2 + src/common/entity/compute_state_display.ts | 28 +++ src/common/entity/domain_icon.ts | 10 + src/data/update.ts | 36 +++ .../more-info/controls/more-info-update.ts | 212 ++++++++++++++++++ .../more-info/state_more_info_control.ts | 1 + src/translations/en.json | 13 ++ 9 files changed, 445 insertions(+) create mode 100644 gallery/src/pages/more-info/update.markdown create mode 100644 gallery/src/pages/more-info/update.ts create mode 100644 src/data/update.ts create mode 100644 src/dialogs/more-info/controls/more-info-update.ts diff --git a/gallery/src/pages/more-info/update.markdown b/gallery/src/pages/more-info/update.markdown new file mode 100644 index 0000000000..e7540412e3 --- /dev/null +++ b/gallery/src/pages/more-info/update.markdown @@ -0,0 +1,3 @@ +--- +title: Update +--- diff --git a/gallery/src/pages/more-info/update.ts b/gallery/src/pages/more-info/update.ts new file mode 100644 index 0000000000..1488e318dc --- /dev/null +++ b/gallery/src/pages/more-info/update.ts @@ -0,0 +1,140 @@ +import { html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { customElement, property, query } from "lit/decorators"; +import "../../../../src/components/ha-card"; +import { + UPDATE_SUPPORT_BACKUP, + UPDATE_SUPPORT_PROGRESS, + UPDATE_SUPPORT_INSTALL, +} from "../../../../src/data/update"; +import "../../../../src/dialogs/more-info/more-info-content"; +import { getEntity } from "../../../../src/fake_data/entity"; +import { + MockHomeAssistant, + provideHass, +} from "../../../../src/fake_data/provide_hass"; +import "../../components/demo-more-infos"; + +const base_attributes = { + title: "Awesome", + current_version: "1.2.2", + latest_version: "1.2.3", + release_url: "https://home-assistant.io", + supported_features: UPDATE_SUPPORT_INSTALL, + skipped_version: null, + in_progress: false, + release_summary: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. In nec metus aliquet, porta mi ut, ultrices odio. Etiam egestas orci tellus, non semper metus blandit tincidunt. Praesent elementum turpis vel tempor pharetra. Sed quis cursus diam. Proin sem justo.", +}; + +const ENTITIES = [ + getEntity("update", "update1", "on", { + ...base_attributes, + friendly_name: "Update", + }), + getEntity("update", "update2", "on", { + ...base_attributes, + title: null, + friendly_name: "Update without title", + }), + getEntity("update", "update3", "on", { + ...base_attributes, + release_url: null, + friendly_name: "Update without release_url", + }), + getEntity("update", "update4", "on", { + ...base_attributes, + release_summary: null, + friendly_name: "Update without release_summary", + }), + getEntity("update", "update5", "off", { + ...base_attributes, + current_version: "1.2.3", + friendly_name: "No update", + }), + getEntity("update", "update6", "off", { + ...base_attributes, + skipped_version: "1.2.3", + friendly_name: "Skipped version", + }), + getEntity("update", "update7", "on", { + ...base_attributes, + supported_features: + base_attributes.supported_features + UPDATE_SUPPORT_BACKUP, + friendly_name: "With backup support", + }), + getEntity("update", "update8", "on", { + ...base_attributes, + in_progress: true, + friendly_name: "With true in_progress", + }), + getEntity("update", "update9", "on", { + ...base_attributes, + in_progress: 25, + supported_features: + base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS, + friendly_name: "With 25 in_progress", + }), + getEntity("update", "update10", "on", { + ...base_attributes, + in_progress: 50, + supported_features: + base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS, + friendly_name: "With 50 in_progress", + }), + getEntity("update", "update11", "on", { + ...base_attributes, + in_progress: 75, + supported_features: + base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS, + friendly_name: "With 75 in_progress", + }), + getEntity("update", "update12", "unavailable", { + ...base_attributes, + in_progress: 50, + friendly_name: "Unavailable", + }), + getEntity("update", "update13", "on", { + ...base_attributes, + supported_features: 0, + friendly_name: "No install support", + }), + getEntity("update", "update14", "off", { + ...base_attributes, + current_version: null, + friendly_name: "Update without current_version", + }), + getEntity("update", "update15", "off", { + ...base_attributes, + latest_version: null, + friendly_name: "Update without latest_version", + }), +]; + +@customElement("demo-more-info-update") +class DemoMoreInfoUpdate extends LitElement { + @property() public hass!: MockHomeAssistant; + + @query("demo-more-infos") private _demoRoot!: HTMLElement; + + protected render(): TemplateResult { + return html` + ent.entityId)} + > + `; + } + + protected firstUpdated(changedProperties: PropertyValues) { + super.firstUpdated(changedProperties); + const hass = provideHass(this._demoRoot); + hass.updateTranslations(null, "en"); + hass.addEntities(ENTITIES); + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-more-info-update": DemoMoreInfoUpdate; + } +} diff --git a/src/common/const.ts b/src/common/const.ts index f11e7d7380..ee9f94000c 100644 --- a/src/common/const.ts +++ b/src/common/const.ts @@ -187,6 +187,7 @@ export const DOMAINS_WITH_MORE_INFO = [ "scene", "sun", "timer", + "update", "vacuum", "water_heater", "weather", @@ -200,6 +201,7 @@ export const DOMAINS_HIDE_DEFAULT_MORE_INFO = [ "input_text", "number", "scene", + "update", "select", ]; diff --git a/src/common/entity/compute_state_display.ts b/src/common/entity/compute_state_display.ts index 3838218ea4..e78b889457 100644 --- a/src/common/entity/compute_state_display.ts +++ b/src/common/entity/compute_state_display.ts @@ -1,12 +1,18 @@ import { HassEntity } from "home-assistant-js-websocket"; import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; import { FrontendLocaleData } from "../../data/translation"; +import { + updateIsInstalling, + UpdateEntity, + UPDATE_SUPPORT_PROGRESS, +} from "../../data/update"; import { formatDate } from "../datetime/format_date"; import { formatDateTime } from "../datetime/format_date_time"; import { formatTime } from "../datetime/format_time"; import { formatNumber, isNumericState } from "../number/format_number"; import { LocalizeFunc } from "../translations/localize"; import { computeStateDomain } from "./compute_state_domain"; +import { supportsFeature } from "./supports-feature"; export const computeStateDisplay = ( localize: LocalizeFunc, @@ -130,6 +136,28 @@ export const computeStateDisplay = ( } } + if (domain === "update") { + // When updating, and entity does not support % show "Installing" + // When updating, and entity does support % show "Installing (xx%)" + // When update available, show the version + // When the latest version is skipped, show the latest version + // When update is not available, show "Up-to-date" + // When update is not available and there is no latest_version show "Unavailable" + return compareState === "on" + ? updateIsInstalling(stateObj as UpdateEntity) + ? supportsFeature(stateObj, UPDATE_SUPPORT_PROGRESS) + ? localize("ui.card.update.installing_with_progress", { + progress: stateObj.attributes.in_progress, + }) + : localize("ui.card.update.installing") + : stateObj.attributes.latest_version + : stateObj.attributes.skipped_version === + stateObj.attributes.latest_version + ? stateObj.attributes.latest_version ?? + localize("state.default.unavailable") + : localize("ui.card.update.up_to_date"); + } + return ( // Return device class translation (stateObj.attributes.device_class && diff --git a/src/common/entity/domain_icon.ts b/src/common/entity/domain_icon.ts index 40b0106216..af89f00d16 100644 --- a/src/common/entity/domain_icon.ts +++ b/src/common/entity/domain_icon.ts @@ -26,8 +26,11 @@ import { mdiCheckCircleOutline, mdiCloseCircleOutline, mdiWeatherNight, + mdiPackage, + mdiPackageDown, } from "@mdi/js"; import { HassEntity } from "home-assistant-js-websocket"; +import { updateIsInstalling, UpdateEntity } from "../../data/update"; /** * Return the icon to be used for a domain. * @@ -133,6 +136,13 @@ export const domainIcon = ( return stateObj?.state === "above_horizon" ? FIXED_DOMAIN_ICONS[domain] : mdiWeatherNight; + + case "update": + return compareState === "on" + ? updateIsInstalling(stateObj as UpdateEntity) + ? mdiPackageDown + : mdiPackageUp + : mdiPackage; } if (domain in FIXED_DOMAIN_ICONS) { diff --git a/src/data/update.ts b/src/data/update.ts new file mode 100644 index 0000000000..22891a92ac --- /dev/null +++ b/src/data/update.ts @@ -0,0 +1,36 @@ +import type { + HassEntityAttributeBase, + HassEntityBase, +} from "home-assistant-js-websocket"; +import { supportsFeature } from "../common/entity/supports-feature"; + +export const UPDATE_SUPPORT_INSTALL = 1; +export const UPDATE_SUPPORT_SPECIFIC_VERSION = 2; +export const UPDATE_SUPPORT_PROGRESS = 4; +export const UPDATE_SUPPORT_BACKUP = 8; + +interface UpdateEntityAttributes extends HassEntityAttributeBase { + current_version: string | null; + in_progress: boolean | number; + latest_version: string | null; + release_summary: string | null; + release_url: string | null; + skipped_version: string | null; + title: string | null; +} + +export interface UpdateEntity extends HassEntityBase { + attributes: UpdateEntityAttributes; +} + +export const updateUsesProgress = (entity: UpdateEntity): boolean => + supportsFeature(entity, UPDATE_SUPPORT_PROGRESS) && + typeof entity.attributes.in_progress === "number"; + +export const updateCanInstall = (entity: UpdateEntity): boolean => + supportsFeature(entity, UPDATE_SUPPORT_INSTALL) && + entity.attributes.latest_version !== entity.attributes.current_version && + entity.attributes.latest_version !== entity.attributes.skipped_version; + +export const updateIsInstalling = (entity: UpdateEntity): boolean => + updateUsesProgress(entity) || !!entity.attributes.in_progress; diff --git a/src/dialogs/more-info/controls/more-info-update.ts b/src/dialogs/more-info/controls/more-info-update.ts new file mode 100644 index 0000000000..de1dbe1f10 --- /dev/null +++ b/src/dialogs/more-info/controls/more-info-update.ts @@ -0,0 +1,212 @@ +import "@material/mwc-button/mwc-button"; +import "@material/mwc-linear-progress/mwc-linear-progress"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; +import { supportsFeature } from "../../../common/entity/supports-feature"; +import "../../../components/ha-checkbox"; +import "../../../components/ha-formfield"; +import "../../../components/ha-markdown"; +import { UNAVAILABLE_STATES } from "../../../data/entity"; +import { + updateIsInstalling, + UpdateEntity, + UPDATE_SUPPORT_BACKUP, + UPDATE_SUPPORT_INSTALL, + UPDATE_SUPPORT_PROGRESS, + UPDATE_SUPPORT_SPECIFIC_VERSION, +} from "../../../data/update"; +import type { HomeAssistant } from "../../../types"; + +@customElement("more-info-update") +class MoreInfoUpdate extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj?: UpdateEntity; + + protected render(): TemplateResult { + if ( + !this.hass || + !this.stateObj || + UNAVAILABLE_STATES.includes(this.stateObj.state) + ) { + return html``; + } + + const skippedVersion = + this.stateObj.attributes.latest_version && + this.stateObj.attributes.skipped_version === + this.stateObj.attributes.latest_version; + + return html` + ${this.stateObj.attributes.in_progress + ? supportsFeature(this.stateObj, UPDATE_SUPPORT_PROGRESS) && + typeof this.stateObj.attributes.in_progress === "number" + ? html`` + : html`` + : ""} + ${this.stateObj.attributes.title + ? html`

${this.stateObj.attributes.title}

` + : ""} + +
+
+ ${this.hass.localize( + "ui.dialogs.more_info_control.update.current_version" + )} +
+
+ ${this.stateObj.attributes.current_version ?? + this.hass.localize("state.default.unavailable")} +
+
+
+
+ ${this.hass.localize( + "ui.dialogs.more_info_control.update.latest_version" + )} +
+
+ ${this.stateObj.attributes.latest_version ?? + this.hass.localize("state.default.unavailable")} +
+
+ + ${this.stateObj.attributes.release_url + ? html`
` + : ""} + ${this.stateObj.attributes.release_summary + ? html`
+ ` + : ""} + ${supportsFeature(this.stateObj, UPDATE_SUPPORT_BACKUP) + ? html`
+ + + ` + : ""} +
+
+ + ${this.hass.localize("ui.dialogs.more_info_control.update.skip")} + + ${supportsFeature(this.stateObj, UPDATE_SUPPORT_INSTALL) + ? html` + + ${this.hass.localize( + "ui.dialogs.more_info_control.update.install" + )} + + ` + : ""} +
+ `; + } + + get _shouldCreateBackup(): boolean | null { + if (!supportsFeature(this.stateObj!, UPDATE_SUPPORT_BACKUP)) { + return null; + } + const checkbox = this.shadowRoot?.querySelector("ha-checkbox"); + if (checkbox) { + return checkbox.checked; + } + return true; + } + + private _handleInstall(): void { + const installData: Record = { + entity_id: this.stateObj!.entity_id, + }; + + if (this._shouldCreateBackup) { + installData.backup = true; + } + + if ( + supportsFeature(this.stateObj!, UPDATE_SUPPORT_SPECIFIC_VERSION) && + this.stateObj!.attributes.latest_version + ) { + installData.version = this.stateObj!.attributes.latest_version; + } + + this.hass.callService("update", "install", installData); + } + + private _handleSkip(): void { + this.hass.callService("update", "skip", { + entity_id: this.stateObj!.entity_id, + }); + } + + static get styles(): CSSResultGroup { + return css` + hr { + border-color: var(--divider-color); + border-bottom: none; + margin: 16px 0; + } + ha-expansion-panel { + margin: 16px 0; + } + .row { + margin: 0; + display: flex; + flex-direction: row; + justify-content: space-between; + } + .actions { + margin: 8px 0 0; + display: flex; + flex-wrap: wrap; + justify-content: center; + } + + .actions mwc-button { + margin: 0 4px 4px; + } + a { + color: var(--primary-color); + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "more-info-update": MoreInfoUpdate; + } +} diff --git a/src/dialogs/more-info/state_more_info_control.ts b/src/dialogs/more-info/state_more_info_control.ts index 02abe5e731..3df5e85b4b 100644 --- a/src/dialogs/more-info/state_more_info_control.ts +++ b/src/dialogs/more-info/state_more_info_control.ts @@ -25,6 +25,7 @@ const LAZY_LOADED_MORE_INFO_CONTROL = { script: () => import("./controls/more-info-script"), sun: () => import("./controls/more-info-sun"), timer: () => import("./controls/more-info-timer"), + update: () => import("./controls/more-info-update"), vacuum: () => import("./controls/more-info-vacuum"), water_heater: () => import("./controls/more-info-water_heater"), weather: () => import("./controls/more-info-weather"), diff --git a/src/translations/en.json b/src/translations/en.json index 7b597b64c7..bb75c9224e 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -228,6 +228,11 @@ "service": { "run": "Run" }, + "update": { + "installing": "Installing", + "installing_with_progress": "Installing ({progress}%)", + "up_to_date": "Up-to-date" + }, "timer": { "actions": { "start": "start", @@ -719,6 +724,14 @@ "rising": "Rising", "setting": "Setting" }, + "update": { + "current_version": "Current version", + "latest_version": "Latest version", + "release_announcement": "Read release announcement", + "skip": "Skip", + "install": "Install", + "create_backup": "Create backup before updating" + }, "updater": { "title": "Update Instructions" }, From 73f5580555340302038f327e54822cce61477495 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 22 Mar 2022 12:47:12 -0700 Subject: [PATCH 091/142] Add support for integration type (#12077) --- src/common/entity/compute_state_domain.ts | 2 +- .../ha-selector/ha-selector-area.ts | 12 +- .../ha-selector/ha-selector-device.ts | 12 +- .../ha-selector/ha-selector-target.ts | 5 +- src/data/config_entries.ts | 20 +- src/data/config_flow.ts | 10 +- src/data/energy.ts | 8 +- src/data/helpers_crud.ts | 71 ++++ .../config-flow/show-dialog-config-flow.ts | 2 +- .../config-flow/step-flow-pick-handler.ts | 9 +- src/onboarding/onboarding-integrations.ts | 4 +- .../zwave_js/ha-device-info-zwave_js.ts | 7 +- .../components/ha-energy-grid-settings.ts | 54 ++- .../dialogs/dialog-energy-solar-settings.ts | 14 +- .../settings/entity-settings-helper-tab.ts | 93 +---- .../entities/entity-registry-settings.ts | 49 ++- src/panels/config/helpers/const.ts | 16 +- .../config/helpers/dialog-helper-detail.ts | 208 +++++++----- .../config/helpers/ha-config-helpers.ts | 317 ++++++++++++------ .../helpers/show-dialog-helper-detail.ts | 13 +- .../integrations/ha-config-integrations.ts | 38 ++- .../mqtt/mqtt-config-panel.ts | 4 +- .../zwave_js/zwave_js-config-dashboard.ts | 8 +- src/translations/en.json | 3 +- 24 files changed, 602 insertions(+), 377 deletions(-) create mode 100644 src/data/helpers_crud.ts diff --git a/src/common/entity/compute_state_domain.ts b/src/common/entity/compute_state_domain.ts index b4408257a6..1b972ea22f 100644 --- a/src/common/entity/compute_state_domain.ts +++ b/src/common/entity/compute_state_domain.ts @@ -1,4 +1,4 @@ -import { HassEntity } from "home-assistant-js-websocket"; +import type { HassEntity } from "home-assistant-js-websocket"; import { computeDomain } from "./compute_domain"; export const computeStateDomain = (stateObj: HassEntity) => diff --git a/src/components/ha-selector/ha-selector-area.ts b/src/components/ha-selector/ha-selector-area.ts index 5f307dd013..704c79282a 100644 --- a/src/components/ha-selector/ha-selector-area.ts +++ b/src/components/ha-selector/ha-selector-area.ts @@ -28,7 +28,11 @@ export class HaAreaSelector extends LitElement { oldSelector !== this.selector && this.selector.area.device?.integration ) { - this._loadConfigEntries(); + getConfigEntries(this.hass, { + domain: this.selector.area.device.integration, + }).then((entries) => { + this._configEntries = entries; + }); } } } @@ -85,12 +89,6 @@ export class HaAreaSelector extends LitElement { } return true; }; - - private async _loadConfigEntries() { - this._configEntries = (await getConfigEntries(this.hass)).filter( - (entry) => entry.domain === this.selector.area.device?.integration - ); - } } declare global { diff --git a/src/components/ha-selector/ha-selector-device.ts b/src/components/ha-selector/ha-selector-device.ts index 2b0af5ace1..945c0de795 100644 --- a/src/components/ha-selector/ha-selector-device.ts +++ b/src/components/ha-selector/ha-selector-device.ts @@ -25,7 +25,11 @@ export class HaDeviceSelector extends LitElement { if (changedProperties.has("selector")) { const oldSelector = changedProperties.get("selector"); if (oldSelector !== this.selector && this.selector.device?.integration) { - this._loadConfigEntries(); + getConfigEntries(this.hass, { + domain: this.selector.device.integration, + }).then((entries) => { + this._configEntries = entries; + }); } } } @@ -88,12 +92,6 @@ export class HaDeviceSelector extends LitElement { } return true; }; - - private async _loadConfigEntries() { - this._configEntries = (await getConfigEntries(this.hass)).filter( - (entry) => entry.domain === this.selector.device.integration - ); - } } declare global { diff --git a/src/components/ha-selector/ha-selector-target.ts b/src/components/ha-selector/ha-selector-target.ts index 4d81be67df..cf25317cce 100644 --- a/src/components/ha-selector/ha-selector-target.ts +++ b/src/components/ha-selector/ha-selector-target.ts @@ -134,9 +134,8 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) { private async _loadConfigEntries() { this._configEntries = (await getConfigEntries(this.hass)).filter( (entry) => - entry.domain === - (this.selector.target.device?.integration || - this.selector.target.entity?.integration) + entry.domain === this.selector.target.device?.integration || + entry.domain === this.selector.target.entity?.integration ); } diff --git a/src/data/config_entries.ts b/src/data/config_entries.ts index f2e84ddac0..b9e555998c 100644 --- a/src/data/config_entries.ts +++ b/src/data/config_entries.ts @@ -34,8 +34,24 @@ export const ERROR_STATES: ConfigEntry["state"][] = [ "setup_retry", ]; -export const getConfigEntries = (hass: HomeAssistant) => - hass.callApi("GET", "config/config_entries/entry"); +export const getConfigEntries = ( + hass: HomeAssistant, + filters?: { type?: "helper" | "integration"; domain?: string } +): Promise => { + const params = new URLSearchParams(); + if (filters) { + if (filters.type) { + params.append("type", filters.type); + } + if (filters.domain) { + params.append("domain", filters.domain); + } + } + return hass.callApi( + "GET", + `config/config_entries/entry?${params.toString()}` + ); +}; export const updateConfigEntry = ( hass: HomeAssistant, diff --git a/src/data/config_flow.ts b/src/data/config_flow.ts index 39019393c5..c236a1b05c 100644 --- a/src/data/config_flow.ts +++ b/src/data/config_flow.ts @@ -65,8 +65,14 @@ export const ignoreConfigFlow = ( export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) => hass.callApi("DELETE", `config/config_entries/flow/${flowId}`); -export const getConfigFlowHandlers = (hass: HomeAssistant) => - hass.callApi("GET", "config/config_entries/flow_handlers"); +export const getConfigFlowHandlers = ( + hass: HomeAssistant, + type?: "helper" | "integration" +) => + hass.callApi( + "GET", + `config/config_entries/flow_handlers${type ? `?type=${type}` : ""}` + ); export const fetchConfigFlowInProgress = ( conn: Connection diff --git a/src/data/energy.ts b/src/data/energy.ts index 7fd941b325..d9f4f1c0c2 100644 --- a/src/data/energy.ts +++ b/src/data/energy.ts @@ -247,14 +247,14 @@ const getEnergyData = async ( end?: Date ): Promise => { const [configEntries, entityRegistryEntries, info] = await Promise.all([ - getConfigEntries(hass), + getConfigEntries(hass, { domain: "co2signal" }), subscribeOne(hass.connection, subscribeEntityRegistry), getEnergyInfo(hass), ]); - const co2SignalConfigEntry = configEntries.find( - (entry) => entry.domain === "co2signal" - ); + const co2SignalConfigEntry = configEntries.length + ? configEntries[0] + : undefined; let co2SignalEntity: string | undefined; diff --git a/src/data/helpers_crud.ts b/src/data/helpers_crud.ts new file mode 100644 index 0000000000..c03ce664ac --- /dev/null +++ b/src/data/helpers_crud.ts @@ -0,0 +1,71 @@ +import { fetchCounter, updateCounter, deleteCounter } from "./counter"; +import { + fetchInputBoolean, + updateInputBoolean, + deleteInputBoolean, +} from "./input_boolean"; +import { + fetchInputButton, + updateInputButton, + deleteInputButton, +} from "./input_button"; +import { + fetchInputDateTime, + updateInputDateTime, + deleteInputDateTime, +} from "./input_datetime"; +import { + fetchInputNumber, + updateInputNumber, + deleteInputNumber, +} from "./input_number"; +import { + fetchInputSelect, + updateInputSelect, + deleteInputSelect, +} from "./input_select"; +import { fetchInputText, updateInputText, deleteInputText } from "./input_text"; +import { fetchTimer, updateTimer, deleteTimer } from "./timer"; + +export const HELPERS_CRUD = { + input_boolean: { + fetch: fetchInputBoolean, + update: updateInputBoolean, + delete: deleteInputBoolean, + }, + input_button: { + fetch: fetchInputButton, + update: updateInputButton, + delete: deleteInputButton, + }, + input_text: { + fetch: fetchInputText, + update: updateInputText, + delete: deleteInputText, + }, + input_number: { + fetch: fetchInputNumber, + update: updateInputNumber, + delete: deleteInputNumber, + }, + input_datetime: { + fetch: fetchInputDateTime, + update: updateInputDateTime, + delete: deleteInputDateTime, + }, + input_select: { + fetch: fetchInputSelect, + update: updateInputSelect, + delete: deleteInputSelect, + }, + counter: { + fetch: fetchCounter, + update: updateCounter, + delete: deleteCounter, + }, + timer: { + fetch: fetchTimer, + update: updateTimer, + delete: deleteTimer, + }, +}; diff --git a/src/dialogs/config-flow/show-dialog-config-flow.ts b/src/dialogs/config-flow/show-dialog-config-flow.ts index 42337b40ff..5b7c0722b7 100644 --- a/src/dialogs/config-flow/show-dialog-config-flow.ts +++ b/src/dialogs/config-flow/show-dialog-config-flow.ts @@ -24,7 +24,7 @@ export const showConfigFlowDialog = ( loadDevicesAndAreas: true, getFlowHandlers: async (hass) => { const [handlers] = await Promise.all([ - getConfigFlowHandlers(hass), + getConfigFlowHandlers(hass, "integration"), hass.loadBackendTranslation("title", undefined, true), ]); diff --git a/src/dialogs/config-flow/step-flow-pick-handler.ts b/src/dialogs/config-flow/step-flow-pick-handler.ts index 9f2a7e6740..0beb4ac26f 100644 --- a/src/dialogs/config-flow/step-flow-pick-handler.ts +++ b/src/dialogs/config-flow/step-flow-pick-handler.ts @@ -216,15 +216,16 @@ class StepFlowPickHandler extends LitElement { if (handler.is_add) { if (handler.slug === "zwave_js") { - const entries = await getConfigEntries(this.hass); - const entry = entries.find((ent) => ent.domain === "zwave_js"); + const entries = await getConfigEntries(this.hass, { + domain: "zwave_js", + }); - if (!entry) { + if (!entries.length) { return; } showZWaveJSAddNodeDialog(this, { - entry_id: entry.entry_id, + entry_id: entries[0].entry_id, }); } else if (handler.slug === "zha") { navigate("/config/zha/add"); diff --git a/src/onboarding/onboarding-integrations.ts b/src/onboarding/onboarding-integrations.ts index 6ac3504e5e..9fc15b0c07 100644 --- a/src/onboarding/onboarding-integrations.ts +++ b/src/onboarding/onboarding-integrations.ts @@ -169,8 +169,8 @@ class OnboardingIntegrations extends LitElement { } private async _loadConfigEntries() { - const entries = await getConfigEntries(this.hass!); - // We filter out the config entry for the local weather and rpi_power. + const entries = await getConfigEntries(this.hass!, { type: "integration" }); + // We filter out the config entries that are automatically created during onboarding. // It is one that we create automatically and it will confuse the user // if it starts showing up during onboarding. this._entries = entries.filter( diff --git a/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts b/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts index 99f48c8989..78d56a0b1b 100644 --- a/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts +++ b/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts @@ -58,12 +58,11 @@ export class HaDeviceInfoZWaveJS extends LitElement { return; } - const configEntries = await getConfigEntries(this.hass); + const configEntries = await getConfigEntries(this.hass, { + domain: "zwave_js", + }); let zwaveJsConfEntries = 0; for (const entry of configEntries) { - if (entry.domain !== "zwave_js") { - continue; - } if (zwaveJsConfEntries) { this._multipleConfigEntries = true; } diff --git a/src/panels/config/energy/components/ha-energy-grid-settings.ts b/src/panels/config/energy/components/ha-energy-grid-settings.ts index 700329dd1c..46ac49d885 100644 --- a/src/panels/config/energy/components/ha-energy-grid-settings.ts +++ b/src/panels/config/energy/components/ha-energy-grid-settings.ts @@ -54,7 +54,7 @@ export class EnergyGridSettings extends LitElement { @property({ attribute: false }) public validationResult?: EnergyPreferencesValidation; - @state() private _configEntries?: ConfigEntry[]; + @state() private _co2ConfigEntry?: ConfigEntry; protected firstUpdated() { this._fetchCO2SignalConfigEntries(); @@ -195,28 +195,28 @@ export class EnergyGridSettings extends LitElement { "ui.panel.config.energy.grid.grid_carbon_footprint" )} - ${this._configEntries?.map( - (entry) => html`
- - ${entry.title} - - - - -
` - )} - ${this._configEntries?.length === 0 - ? html` + ${this._co2ConfigEntry + ? html`
+ + ${this._co2ConfigEntry.title} + + + + +
` + : html`
- ` - : ""} + `}
`; } private async _fetchCO2SignalConfigEntries() { - this._configEntries = (await getConfigEntries(this.hass)).filter( - (entry) => entry.domain === "co2signal" - ); + const entries = await getConfigEntries(this.hass, { domain: "co2signal" }); + this._co2ConfigEntry = entries.length ? entries[0] : undefined; } private _addCO2Sensor() { diff --git a/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts index 1f2acaf624..8ec83a64b8 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts @@ -176,9 +176,17 @@ export class DialogEnergySolarSettings private async _fetchSolarForecastConfigEntries() { const domains = this._params!.info.solar_forecast_domains; - this._configEntries = (await getConfigEntries(this.hass)).filter((entry) => - domains.includes(entry.domain) - ); + this._configEntries = + domains.length === 0 + ? [] + : domains.length === 1 + ? await getConfigEntries(this.hass, { + type: "integration", + domain: domains[0], + }) + : (await getConfigEntries(this.hass, { type: "integration" })).filter( + (entry) => domains.includes(entry.domain) + ); } private _handleForecastChanged(ev: CustomEvent) { diff --git a/src/panels/config/entities/editor-tabs/settings/entity-settings-helper-tab.ts b/src/panels/config/entities/editor-tabs/settings/entity-settings-helper-tab.ts index 913bcd8353..210d931a6c 100644 --- a/src/panels/config/entities/editor-tabs/settings/entity-settings-helper-tab.ts +++ b/src/panels/config/entities/editor-tabs/settings/entity-settings-helper-tab.ts @@ -10,50 +10,11 @@ import { customElement, property, state, query } from "lit/decorators"; import { isComponentLoaded } from "../../../../../common/config/is_component_loaded"; import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive"; import { fireEvent } from "../../../../../common/dom/fire_event"; -import { - deleteCounter, - fetchCounter, - updateCounter, -} from "../../../../../data/counter"; import { ExtEntityRegistryEntry, removeEntityRegistryEntry, } from "../../../../../data/entity_registry"; -import { - deleteInputBoolean, - fetchInputBoolean, - updateInputBoolean, -} from "../../../../../data/input_boolean"; -import { - deleteInputButton, - fetchInputButton, - updateInputButton, -} from "../../../../../data/input_button"; -import { - deleteInputDateTime, - fetchInputDateTime, - updateInputDateTime, -} from "../../../../../data/input_datetime"; -import { - deleteInputNumber, - fetchInputNumber, - updateInputNumber, -} from "../../../../../data/input_number"; -import { - deleteInputSelect, - fetchInputSelect, - updateInputSelect, -} from "../../../../../data/input_select"; -import { - deleteInputText, - fetchInputText, - updateInputText, -} from "../../../../../data/input_text"; -import { - deleteTimer, - fetchTimer, - updateTimer, -} from "../../../../../data/timer"; +import { HELPERS_CRUD } from "../../../../../data/helpers_crud"; import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box"; import { haStyle } from "../../../../../resources/styles"; import type { HomeAssistant } from "../../../../../types"; @@ -69,49 +30,6 @@ import "../../../helpers/forms/ha-timer-form"; import "../../entity-registry-basic-editor"; import type { HaEntityRegistryBasicEditor } from "../../entity-registry-basic-editor"; -const HELPERS = { - input_boolean: { - fetch: fetchInputBoolean, - update: updateInputBoolean, - delete: deleteInputBoolean, - }, - input_button: { - fetch: fetchInputButton, - update: updateInputButton, - delete: deleteInputButton, - }, - input_text: { - fetch: fetchInputText, - update: updateInputText, - delete: deleteInputText, - }, - input_number: { - fetch: fetchInputNumber, - update: updateInputNumber, - delete: deleteInputNumber, - }, - input_datetime: { - fetch: fetchInputDateTime, - update: updateInputDateTime, - delete: deleteInputDateTime, - }, - input_select: { - fetch: fetchInputSelect, - update: updateInputSelect, - delete: deleteInputSelect, - }, - counter: { - fetch: fetchCounter, - update: updateCounter, - delete: deleteCounter, - }, - timer: { - fetch: fetchTimer, - update: updateTimer, - delete: deleteTimer, - }, -}; - @customElement("entity-settings-helper-tab") export class EntityRegistrySettingsHelper extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -198,7 +116,7 @@ export class EntityRegistrySettingsHelper extends LitElement { } private async _getItem() { - const items = await HELPERS[this.entry.platform].fetch(this.hass!); + const items = await HELPERS_CRUD[this.entry.platform].fetch(this.hass!); this._item = items.find((item) => item.id === this.entry.unique_id) || null; } @@ -206,7 +124,7 @@ export class EntityRegistrySettingsHelper extends LitElement { this._submitting = true; try { if (this._componentLoaded && this._item) { - await HELPERS[this.entry.platform].update( + await HELPERS_CRUD[this.entry.platform].update( this.hass!, this._item.id, this._item @@ -236,7 +154,10 @@ export class EntityRegistrySettingsHelper extends LitElement { try { if (this._componentLoaded && this._item) { - await HELPERS[this.entry.platform].delete(this.hass!, this._item.id); + await HELPERS_CRUD[this.entry.platform].delete( + this.hass!, + this._item.id + ); } else { const stateObj = this.hass.states[this.entry.entity_id]; if (!stateObj?.attributes.restored) { diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index c4278a7936..34d099db0d 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -42,6 +42,12 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail"; +import { + ConfigEntry, + deleteConfigEntry, + getConfigEntries, +} from "../../../data/config_entries"; +import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow"; const OVERRIDE_DEVICE_CLASSES = { cover: [ @@ -83,6 +89,8 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { @state() private _device?: DeviceRegistryEntry; + @state() private _helperConfigEntry?: ConfigEntry; + @state() private _error?: string; @state() private _submitting?: boolean; @@ -103,6 +111,20 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { ]; } + protected firstUpdated(changedProps: PropertyValues): void { + super.firstUpdated(changedProps); + if (this.entry.config_entry_id) { + getConfigEntries(this.hass, { + type: "helper", + domain: this.entry.platform, + }).then((entries) => { + this._helperConfigEntry = entries.find( + (ent) => ent.entry_id === this.entry.config_entry_id + ); + }); + } + } + protected updated(changedProperties: PropertyValues) { super.updated(changedProperties); if (changedProperties.has("entry")) { @@ -215,6 +237,21 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { @value-changed=${this._areaPicked} >` : ""} + ${this._helperConfigEntry + ? html` +
+ + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.configure_state" + )} + +
+ ` + : ""} + ${this.hass.localize("ui.dialogs.entity_registry.editor.delete")} @@ -471,13 +508,21 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { this._submitting = true; try { - await removeEntityRegistryEntry(this.hass!, this._origEntityId); + if (this._helperConfigEntry) { + await deleteConfigEntry(this.hass, this._helperConfigEntry.entry_id); + } else { + await removeEntityRegistryEntry(this.hass!, this._origEntityId); + } fireEvent(this, "close-dialog"); } finally { this._submitting = false; } } + private async _showOptionsFlow() { + showOptionsFlowDialog(this, this._helperConfigEntry!); + } + static get styles(): CSSResultGroup { return [ haStyle, diff --git a/src/panels/config/helpers/const.ts b/src/panels/config/helpers/const.ts index 2e927f66ad..c103332573 100644 --- a/src/panels/config/helpers/const.ts +++ b/src/panels/config/helpers/const.ts @@ -1,11 +1,11 @@ -import { Counter } from "../../../data/counter"; -import { InputBoolean } from "../../../data/input_boolean"; -import { InputButton } from "../../../data/input_button"; -import { InputDateTime } from "../../../data/input_datetime"; -import { InputNumber } from "../../../data/input_number"; -import { InputSelect } from "../../../data/input_select"; -import { InputText } from "../../../data/input_text"; -import { Timer } from "../../../data/timer"; +import type { Counter } from "../../../data/counter"; +import type { InputBoolean } from "../../../data/input_boolean"; +import type { InputButton } from "../../../data/input_button"; +import type { InputDateTime } from "../../../data/input_datetime"; +import type { InputNumber } from "../../../data/input_number"; +import type { InputSelect } from "../../../data/input_select"; +import type { InputText } from "../../../data/input_text"; +import type { Timer } from "../../../data/timer"; export const HELPER_DOMAINS = [ "input_boolean", diff --git a/src/panels/config/helpers/dialog-helper-detail.ts b/src/panels/config/helpers/dialog-helper-detail.ts index bbd0b645e1..c3adf6a826 100644 --- a/src/panels/config/helpers/dialog-helper-detail.ts +++ b/src/panels/config/helpers/dialog-helper-detail.ts @@ -8,6 +8,8 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { dynamicElement } from "../../../common/dom/dynamic-element-directive"; import { domainIcon } from "../../../common/entity/domain_icon"; import "../../../components/ha-dialog"; +import "../../../components/ha-circular-progress"; +import { getConfigFlowHandlers } from "../../../data/config_flow"; import { createCounter } from "../../../data/counter"; import { createInputBoolean } from "../../../data/input_boolean"; import { createInputButton } from "../../../data/input_button"; @@ -16,6 +18,7 @@ import { createInputNumber } from "../../../data/input_number"; import { createInputSelect } from "../../../data/input_select"; import { createInputText } from "../../../data/input_text"; import { createTimer } from "../../../data/timer"; +import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import { Helper } from "./const"; @@ -27,6 +30,8 @@ import "./forms/ha-input_number-form"; import "./forms/ha-input_select-form"; import "./forms/ha-input_text-form"; import "./forms/ha-timer-form"; +import { domainToName } from "../../../data/integration"; +import type { ShowDialogHelperDetailParams } from "./show-dialog-helper-detail"; const HELPERS = { input_boolean: createInputBoolean, @@ -47,7 +52,7 @@ export class DialogHelperDetail extends LitElement { @state() private _opened = false; - @state() private _platform?: string; + @state() private _domain?: string; @state() private _error?: string; @@ -55,102 +60,135 @@ export class DialogHelperDetail extends LitElement { @query(".form") private _form?: HTMLDivElement; - public async showDialog(): Promise { - this._platform = undefined; + @state() private _helperFlows?: string[]; + + private _params?: ShowDialogHelperDetailParams; + + public async showDialog(params: ShowDialogHelperDetailParams): Promise { + this._params = params; + this._domain = undefined; this._item = undefined; this._opened = true; await this.updateComplete; + Promise.all([ + getConfigFlowHandlers(this.hass, "helper"), + // Ensure the titles are loaded before we render the flows. + this.hass.loadBackendTranslation("title", undefined, true), + ]).then(([flows]) => { + this._helperFlows = flows; + }); } public closeDialog(): void { this._opened = false; this._error = ""; + this._params = undefined; } protected render(): TemplateResult { + let content: TemplateResult; + + if (this._domain) { + content = html` +
+ ${this._error ? html`
${this._error}
` : ""} + ${dynamicElement(`ha-${this._domain}-form`, { + hass: this.hass, + item: this._item, + new: true, + })} +
+ + ${this.hass!.localize("ui.panel.config.helpers.dialog.create")} + + + ${this.hass!.localize("ui.common.back")} + + `; + } else if (this._helperFlows === undefined) { + content = html``; + } else { + const items: [string, string][] = []; + + for (const helper of Object.keys(HELPERS)) { + items.push([ + helper, + this.hass.localize(`ui.panel.config.helpers.types.${helper}`) || + helper, + ]); + } + + for (const domain of this._helperFlows) { + items.push([domain, domainToName(this.hass.localize, domain)]); + } + + items.sort((a, b) => a[1].localeCompare(b[1])); + + content = html` + ${items.map(([domain, label]) => { + // Only OG helpers need to be loaded prior adding one + const isLoaded = + !(domain in HELPERS) || isComponentLoaded(this.hass, domain); + return html` + + + ${label} + + ${!isLoaded + ? html` + ${this.hass.localize( + "ui.dialogs.helper_settings.platform_not_loaded", + "platform", + domain + )} + ` + : ""} + `; + })} + + ${this.hass!.localize("ui.common.cancel")} + + `; + } + return html` - ${this._platform - ? html` -
- ${this._error - ? html`
${this._error}
` - : ""} - ${dynamicElement(`ha-${this._platform}-form`, { - hass: this.hass, - item: this._item, - new: true, - })} -
- - ${this.hass!.localize("ui.panel.config.helpers.dialog.create")} - - - ${this.hass!.localize("ui.common.back")} - - ` - : html` - ${Object.keys(HELPERS).map((platform: string) => { - const isLoaded = isComponentLoaded(this.hass, platform); - return html` - - - - ${this.hass.localize( - `ui.panel.config.helpers.types.${platform}` - ) || platform} - - - ${!isLoaded - ? html` - ${this.hass.localize( - "ui.dialogs.helper_settings.platform_not_loaded", - "platform", - platform - )} - ` - : ""} - `; - })} - - ${this.hass!.localize("ui.common.cancel")} - - `} + ${content}
`; } @@ -160,13 +198,13 @@ export class DialogHelperDetail extends LitElement { } private async _createItem(): Promise { - if (!this._platform || !this._item) { + if (!this._domain || !this._item) { return; } this._submitting = true; this._error = ""; try { - await HELPERS[this._platform](this.hass, this._item); + await HELPERS[this._domain](this.hass, this._item); this.closeDialog(); } catch (err: any) { this._error = err.message || "Unknown error"; @@ -181,12 +219,22 @@ export class DialogHelperDetail extends LitElement { } ev.stopPropagation(); ev.preventDefault(); - this._platformPicked(ev); + this._domainPicked(ev); } - private _platformPicked(ev: Event): void { - this._platform = (ev.currentTarget! as any).platform; - this._focusForm(); + private _domainPicked(ev: Event): void { + const domain = (ev.currentTarget! as any).domain; + + if (domain in HELPERS) { + this._domain = domain; + this._focusForm(); + } else { + showConfigFlowDialog(this, { + startFlowHandler: domain, + dialogClosedCallback: this._params!.dialogClosedCallback, + }); + this.closeDialog(); + } } private async _focusForm(): Promise { @@ -195,7 +243,7 @@ export class DialogHelperDetail extends LitElement { } private _goBack() { - this._platform = undefined; + this._domain = undefined; this._item = undefined; this._error = undefined; } diff --git a/src/panels/config/helpers/ha-config-helpers.ts b/src/panels/config/helpers/ha-config-helpers.ts index ac58f83e44..4ce9d00118 100644 --- a/src/panels/config/helpers/ha-config-helpers.ts +++ b/src/panels/config/helpers/ha-config-helpers.ts @@ -1,28 +1,58 @@ import { mdiPencilOff, mdiPlus } from "@mdi/js"; import "@polymer/paper-tooltip/paper-tooltip"; -import { HassEntity } from "home-assistant-js-websocket"; +import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; -import memoize from "memoize-one"; +import memoizeOne from "memoize-one"; import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { domainIcon } from "../../../common/entity/domain_icon"; +import { LocalizeFunc } from "../../../common/translations/localize"; import { DataTableColumnContainer, RowClickedEvent, } from "../../../components/data-table/ha-data-table"; import "../../../components/ha-fab"; +import "../../../components/ha-icon-overflow-menu"; import "../../../components/ha-icon"; import "../../../components/ha-svg-icon"; +import { ConfigEntry, getConfigEntries } from "../../../data/config_entries"; +import { + EntityRegistryEntry, + subscribeEntityRegistry, +} from "../../../data/entity_registry"; +import { domainToName } from "../../../data/integration"; import "../../../layouts/hass-loading-screen"; import "../../../layouts/hass-tabs-subpage-data-table"; +import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { HomeAssistant, Route } from "../../../types"; import { showEntityEditorDialog } from "../entities/show-dialog-entity-editor"; import { configSections } from "../ha-panel-config"; import { HELPER_DOMAINS } from "./const"; import { showHelperDetailDialog } from "./show-dialog-helper-detail"; +// This groups items by a key but only returns last entry per key. +const groupByOne = ( + items: T[], + keySelector: (item: T) => string +): Record => { + const result: Record = {}; + for (const item of items) { + result[keySelector(item)] = item; + } + return result; +}; + +const getConfigEntry = ( + entityEntries: Record, + configEntries: Record, + entityId: string +) => { + const configEntryId = entityEntries![entityId]?.config_entry_id; + return configEntryId ? configEntries![configEntryId] : undefined; +}; + @customElement("ha-config-helpers") -export class HaConfigHelpers extends LitElement { +export class HaConfigHelpers extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @property() public isWide!: boolean; @@ -33,98 +63,122 @@ export class HaConfigHelpers extends LitElement { @state() private _stateItems: HassEntity[] = []; - private _columns = memoize((narrow, _language): DataTableColumnContainer => { - const columns: DataTableColumnContainer = { - icon: { + @state() private _entityEntries?: Record; + + @state() private _configEntries?: Record; + + private _columns = memoizeOne( + (narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => { + const columns: DataTableColumnContainer = { + icon: { + title: "", + label: localize("ui.panel.config.helpers.picker.headers.icon"), + type: "icon", + template: (icon, helper: any) => + icon + ? html` ` + : html``, + }, + name: { + title: localize("ui.panel.config.helpers.picker.headers.name"), + sortable: true, + filterable: true, + grows: true, + direction: "asc", + template: (name, item: any) => + html` + ${name} + ${narrow + ? html`
${item.entity_id}
` + : ""} + `, + }, + }; + if (!narrow) { + columns.entity_id = { + title: localize("ui.panel.config.helpers.picker.headers.entity_id"), + sortable: true, + filterable: true, + width: "25%", + }; + } + columns.type = { + title: localize("ui.panel.config.helpers.picker.headers.type"), + sortable: true, + width: "25%", + filterable: true, + template: (type, row) => + row.configEntry + ? domainToName(localize, type) + : html` + ${localize(`ui.panel.config.helpers.types.${type}`) || type} + `, + }; + columns.editable = { title: "", label: this.hass.localize( - "ui.panel.config.helpers.picker.headers.icon" + "ui.panel.config.helpers.picker.headers.editable" ), type: "icon", - template: (icon, helper: any) => - icon - ? html` ` - : html``, - }, - name: { - title: this.hass.localize( - "ui.panel.config.helpers.picker.headers.name" - ), - sortable: true, - filterable: true, - grows: true, - direction: "asc", - template: (name, item: any) => - html` - ${name} - ${narrow - ? html`
${item.entity_id}
` - : ""} - `, - }, - }; - if (!narrow) { - columns.entity_id = { - title: this.hass.localize( - "ui.panel.config.helpers.picker.headers.entity_id" - ), - sortable: true, - filterable: true, - width: "25%", - }; - } - columns.type = { - title: this.hass.localize("ui.panel.config.helpers.picker.headers.type"), - sortable: true, - width: "25%", - filterable: true, - template: (type) => - html` - ${this.hass.localize(`ui.panel.config.helpers.types.${type}`) || type} + template: (editable) => html` + ${!editable + ? html` +
+ + + ${this.hass.localize( + "ui.panel.config.entities.picker.status.readonly" + )} + +
+ ` + : ""} `, - }; - columns.editable = { - title: "", - label: this.hass.localize( - "ui.panel.config.helpers.picker.headers.editable" - ), - type: "icon", - template: (editable) => html` - ${!editable - ? html` -
- - - ${this.hass.localize( - "ui.panel.config.entities.picker.status.readonly" - )} - -
- ` - : ""} - `, - }; - return columns; - }); + }; + return columns; + } + ); - private _getItems = memoize((stateItems: HassEntity[]) => - stateItems.map((entityState) => ({ - id: entityState.entity_id, - icon: entityState.attributes.icon, - name: entityState.attributes.friendly_name || "", - entity_id: entityState.entity_id, - editable: entityState.attributes.editable, - type: computeStateDomain(entityState), - })) + private _getItems = memoizeOne( + ( + stateItems: HassEntity[], + entityEntries: Record, + configEntries: Record + ) => + stateItems.map((entityState) => { + const configEntry = getConfigEntry( + entityEntries, + configEntries, + entityState.entity_id + ); + + return { + id: entityState.entity_id, + icon: entityState.attributes.icon, + name: entityState.attributes.friendly_name || "", + entity_id: entityState.entity_id, + editable: + configEntry !== undefined || entityState.attributes.editable, + type: configEntry + ? configEntry.domain + : computeStateDomain(entityState), + configEntry, + }; + }) ); protected render(): TemplateResult { - if (!this.hass || this._stateItems === undefined) { + if ( + !this.hass || + this._stateItems === undefined || + this._entityEntries === undefined || + this._configEntries === undefined + ) { return html` `; } @@ -135,8 +189,12 @@ export class HaConfigHelpers extends LitElement { back-path="/config" .route=${this.route} .tabs=${configSections.automations} - .columns=${this._columns(this.narrow, this.hass.language)} - .data=${this._getItems(this._stateItems)} + .columns=${this._columns(this.narrow, this.hass.localize)} + .data=${this._getItems( + this._stateItems, + this._entityEntries, + this._configEntries + )} @row-click=${this._openEditDialog} hasFab clickable @@ -160,32 +218,67 @@ export class HaConfigHelpers extends LitElement { protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); - this._getStates(); + this._getConfigEntries(); } - protected updated(changedProps: PropertyValues) { - super.updated(changedProps); - const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - if (oldHass && this._stateItems) { - this._getStates(oldHass); + protected willUpdate(changedProps: PropertyValues) { + super.willUpdate(changedProps); + + if (!this._entityEntries || !this._configEntries) { + return; + } + + let changed = + !this._stateItems || + changedProps.has("_entityEntries") || + changedProps.has("_configEntries"); + + if (!changed && changedProps.has("hass")) { + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + changed = !oldHass || oldHass.states !== this.hass.states; + } + if (!changed) { + return; + } + + const extraEntities = new Set(); + + for (const entityEntry of Object.values(this._entityEntries)) { + if ( + entityEntry.config_entry_id && + entityEntry.config_entry_id in this._configEntries + ) { + extraEntities.add(entityEntry.entity_id); + } + } + + const newStates = Object.values(this.hass!.states).filter( + (entity) => + extraEntities.has(entity.entity_id) || + HELPER_DOMAINS.includes(computeStateDomain(entity)) + ); + + if ( + this._stateItems.length !== newStates.length || + !this._stateItems.every((val, idx) => newStates[idx] === val) + ) { + this._stateItems = newStates; } } - private _getStates(oldHass?: HomeAssistant) { - let changed = false; - const tempStates = Object.values(this.hass!.states).filter((entity) => { - if (!HELPER_DOMAINS.includes(computeStateDomain(entity))) { - return false; - } - if (oldHass?.states[entity.entity_id] !== entity) { - changed = true; - } - return true; - }); + public hassSubscribe(): UnsubscribeFunc[] { + return [ + subscribeEntityRegistry(this.hass.connection!, (entries) => { + this._entityEntries = groupByOne(entries, (entry) => entry.entity_id); + }), + ]; + } - if (changed || this._stateItems.length !== tempStates.length) { - this._stateItems = tempStates; - } + private async _getConfigEntries() { + this._configEntries = groupByOne( + await getConfigEntries(this.hass, { type: "helper" }), + (entry) => entry.entry_id + ); } private async _openEditDialog(ev: CustomEvent): Promise { @@ -196,6 +289,12 @@ export class HaConfigHelpers extends LitElement { } private _createHelpler() { - showHelperDetailDialog(this); + showHelperDetailDialog(this, { + dialogClosedCallback: (params) => { + if (params.flowFinished) { + this._getConfigEntries(); + } + }, + }); } } diff --git a/src/panels/config/helpers/show-dialog-helper-detail.ts b/src/panels/config/helpers/show-dialog-helper-detail.ts index 959f92ad75..83fbbce4ee 100644 --- a/src/panels/config/helpers/show-dialog-helper-detail.ts +++ b/src/panels/config/helpers/show-dialog-helper-detail.ts @@ -1,11 +1,20 @@ import { fireEvent } from "../../../common/dom/fire_event"; +import { DataEntryFlowDialogParams } from "../../../dialogs/config-flow/show-dialog-data-entry-flow"; export const loadHelperDetailDialog = () => import("./dialog-helper-detail"); -export const showHelperDetailDialog = (element: HTMLElement) => { +export interface ShowDialogHelperDetailParams { + // Only used for config entries + dialogClosedCallback: DataEntryFlowDialogParams["dialogClosedCallback"]; +} + +export const showHelperDetailDialog = ( + element: HTMLElement, + params: ShowDialogHelperDetailParams +) => { fireEvent(element, "show-dialog", { dialogTag: "dialog-helper-detail", dialogImport: loadHelperDetailDialog, - dialogParams: {}, + dialogParams: params, }); }; diff --git a/src/panels/config/integrations/ha-config-integrations.ts b/src/panels/config/integrations/ha-config-integrations.ts index 5255d1197e..c860953efc 100644 --- a/src/panels/config/integrations/ha-config-integrations.ts +++ b/src/panels/config/integrations/ha-config-integrations.ts @@ -521,24 +521,26 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { } private _loadConfigEntries() { - getConfigEntries(this.hass).then((configEntries) => { - this._configEntries = configEntries - .map( - (entry: ConfigEntry): ConfigEntryExtended => ({ - ...entry, - localized_domain_name: domainToName( - this.hass.localize, - entry.domain - ), - }) - ) - .sort((conf1, conf2) => - caseInsensitiveStringCompare( - conf1.localized_domain_name + conf1.title, - conf2.localized_domain_name + conf2.title + getConfigEntries(this.hass, { type: "integration" }).then( + (configEntries) => { + this._configEntries = configEntries + .map( + (entry: ConfigEntry): ConfigEntryExtended => ({ + ...entry, + localized_domain_name: domainToName( + this.hass.localize, + entry.domain + ), + }) ) - ); - }); + .sort((conf1, conf2) => + caseInsensitiveStringCompare( + conf1.localized_domain_name + conf1.title, + conf2.localized_domain_name + conf2.title + ) + ); + } + ); } private async _scanUSBDevices() { @@ -656,7 +658,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { if (!domain) { return; } - const handlers = await getConfigFlowHandlers(this.hass); + const handlers = await getConfigFlowHandlers(this.hass, "integration"); if (!handlers.includes(domain)) { showAlertDialog(this, { diff --git a/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.ts b/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.ts index 9337854c8e..eaa03dc98f 100644 --- a/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.ts +++ b/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.ts @@ -111,7 +111,9 @@ class HaPanelDevMqtt extends LitElement { return; } const configEntryId = searchParams.get("config_entry") as string; - const configEntries = await getConfigEntries(this.hass); + const configEntries = await getConfigEntries(this.hass, { + domain: "mqtt", + }); const configEntry = configEntries.find( (entry) => entry.entry_id === configEntryId ); diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts index 3c75b59df0..9b5a5c8e37 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts @@ -384,7 +384,9 @@ class ZWaveJSConfigDashboard extends LitElement { if (!this.configEntryId) { return; } - const configEntries = await getConfigEntries(this.hass); + const configEntries = await getConfigEntries(this.hass, { + domain: "zwave_js", + }); this._configEntry = configEntries.find( (entry) => entry.entry_id === this.configEntryId! ); @@ -467,7 +469,9 @@ class ZWaveJSConfigDashboard extends LitElement { if (!this.configEntryId) { return; } - const configEntries = await getConfigEntries(this.hass); + const configEntries = await getConfigEntries(this.hass, { + domain: "zwave_js", + }); const configEntry = configEntries.find( (entry) => entry.entry_id === this.configEntryId ); diff --git a/src/translations/en.json b/src/translations/en.json index bb75c9224e..8218353cf8 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -823,7 +823,8 @@ "area": "Set entity area only", "area_note": "By default the entities of a device are in the same area as the device. If you change the area of this entity, it will no longer follow the area of the device.", "follow_device_area": "Follow device area", - "change_device_area": "Change device area" + "change_device_area": "Change device area", + "configure_state": "Configure State" } }, "helper_settings": { From 49124f6f09dfe0a7f024e3d85d929551dfda8fd0 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 22 Mar 2022 14:53:22 -0500 Subject: [PATCH 092/142] Update When entity can change enabled or hidden (#12096) --- .../entities/entity-registry-basic-editor.ts | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/panels/config/entities/entity-registry-basic-editor.ts b/src/panels/config/entities/entity-registry-basic-editor.ts index 10d3bdf994..de7d374318 100644 --- a/src/panels/config/entities/entity-registry-basic-editor.ts +++ b/src/panels/config/entities/entity-registry-basic-editor.ts @@ -1,13 +1,13 @@ -import "../../../components/ha-expansion-panel"; import "@material/mwc-formfield/mwc-formfield"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import "../../../components/ha-area-picker"; +import "../../../components/ha-expansion-panel"; +import "../../../components/ha-radio"; import "../../../components/ha-switch"; import "../../../components/ha-textfield"; -import "../../../components/ha-radio"; import { DeviceRegistryEntry, subscribeDeviceRegistry, @@ -182,9 +182,12 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { name="hiddendisabled" value="enabled" .checked=${!this._hiddenBy && !this._disabledBy} - .disabled=${(this._hiddenBy && this._hiddenBy !== "user") || - this._device?.disabled_by || - (this._disabledBy && this._disabledBy !== "user")} + .disabled=${this._device?.disabled_by || + (this._disabledBy && + !( + this._disabledBy === "user" || + this._disabledBy === "integration" + ))} @change=${this._viewStatusChanged} > @@ -197,9 +200,12 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { name="hiddendisabled" value="hidden" .checked=${this._hiddenBy !== null} - .disabled=${(this._hiddenBy && this._hiddenBy !== "user") || - Boolean(this._device?.disabled_by) || - (this._disabledBy && this._disabledBy !== "user")} + .disabled=${this._device?.disabled_by || + (this._disabledBy && + !( + this._disabledBy === "user" || + this._disabledBy === "integration" + ))} @change=${this._viewStatusChanged} > @@ -212,9 +218,12 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { name="hiddendisabled" value="disabled" .checked=${this._disabledBy !== null} - .disabled=${(this._hiddenBy && this._hiddenBy !== "user") || - Boolean(this._device?.disabled_by) || - (this._disabledBy && this._disabledBy !== "user")} + .disabled=${this._device?.disabled_by || + (this._disabledBy && + !( + this._disabledBy === "user" || + this._disabledBy === "integration" + ))} @change=${this._viewStatusChanged} > From 88af0aa78873946d485daf9f00fdd871a899807e Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 22 Mar 2022 14:58:03 -0500 Subject: [PATCH 093/142] Add entity include and exclude to selector (#12078) Co-authored-by: Paulus Schoutsen --- src/components/entity/ha-entities-picker.ts | 20 ++++++ src/components/entity/ha-entity-picker.ts | 64 +++++++++++++++++-- .../ha-selector/ha-selector-entity.ts | 6 +- src/data/selector.ts | 2 + 4 files changed, 85 insertions(+), 7 deletions(-) diff --git a/src/components/entity/ha-entities-picker.ts b/src/components/entity/ha-entities-picker.ts index 062b79eced..3f6127e303 100644 --- a/src/components/entity/ha-entities-picker.ts +++ b/src/components/entity/ha-entities-picker.ts @@ -46,6 +46,22 @@ class HaEntitiesPickerLight extends LitElement { @property({ type: Array, attribute: "include-unit-of-measurement" }) public includeUnitOfMeasurement?: string[]; + /** + * List of allowed entities to show. Will ignore all other filters. + * @type {Array} + * @attr include-entities + */ + @property({ type: Array, attribute: "include-entities" }) + public includeEntities?: string[]; + + /** + * List of entities to be excluded. + * @type {Array} + * @attr exclude-entities + */ + @property({ type: Array, attribute: "exclude-entities" }) + public excludeEntities?: string[]; + @property({ attribute: "picked-entity-label" }) public pickedEntityLabel?: string; @@ -69,6 +85,8 @@ class HaEntitiesPickerLight extends LitElement { .hass=${this.hass} .includeDomains=${this.includeDomains} .excludeDomains=${this.excludeDomains} + .includeEntities=${this.includeEntities} + .excludeEntities=${this.excludeEntities} .includeDeviceClasses=${this.includeDeviceClasses} .includeUnitOfMeasurement=${this.includeUnitOfMeasurement} .entityFilter=${this._entityFilter} @@ -84,6 +102,8 @@ class HaEntitiesPickerLight extends LitElement { .hass=${this.hass} .includeDomains=${this.includeDomains} .excludeDomains=${this.excludeDomains} + .includeEntities=${this.includeEntities} + .excludeEntities=${this.excludeEntities} .includeDeviceClasses=${this.includeDeviceClasses} .includeUnitOfMeasurement=${this.includeUnitOfMeasurement} .entityFilter=${this._entityFilter} diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts index a8d17084c6..9b13c75515 100644 --- a/src/components/entity/ha-entity-picker.ts +++ b/src/components/entity/ha-entity-picker.ts @@ -7,6 +7,7 @@ import memoizeOne from "memoize-one"; import { fireEvent } from "../../common/dom/fire_event"; import { computeDomain } from "../../common/entity/compute_domain"; import { computeStateName } from "../../common/entity/compute_state_name"; +import { caseInsensitiveStringCompare } from "../../common/string/compare"; import { PolymerChangedEvent } from "../../polymer-types"; import { HomeAssistant } from "../../types"; import "../ha-combo-box"; @@ -77,6 +78,22 @@ export class HaEntityPicker extends LitElement { @property({ type: Array, attribute: "include-unit-of-measurement" }) public includeUnitOfMeasurement?: string[]; + /** + * List of allowed entities to show. Will ignore all other filters. + * @type {Array} + * @attr include-entities + */ + @property({ type: Array, attribute: "include-entities" }) + public includeEntities?: string[]; + + /** + * List of entities to be excluded. + * @type {Array} + * @attr exclude-entities + */ + @property({ type: Array, attribute: "exclude-entities" }) + public excludeEntities?: string[]; + @property() public entityFilter?: HaEntityPickerEntityFilterFunc; @property({ type: Boolean }) public hideClearIcon = false; @@ -109,7 +126,9 @@ export class HaEntityPicker extends LitElement { excludeDomains: this["excludeDomains"], entityFilter: this["entityFilter"], includeDeviceClasses: this["includeDeviceClasses"], - includeUnitOfMeasurement: this["includeUnitOfMeasurement"] + includeUnitOfMeasurement: this["includeUnitOfMeasurement"], + includeEntities: this["includeEntities"], + excludeEntities: this["excludeEntities"] ): HassEntityWithCachedName[] => { let states: HassEntityWithCachedName[] = []; @@ -139,6 +158,30 @@ export class HaEntityPicker extends LitElement { ]; } + if (includeEntities) { + entityIds = entityIds.filter((entityId) => + this.includeEntities!.includes(entityId) + ); + + return entityIds + .map((key) => ({ + ...hass!.states[key], + friendly_name: computeStateName(hass!.states[key]) || key, + })) + .sort((entityA, entityB) => + caseInsensitiveStringCompare( + entityA.friendly_name, + entityB.friendly_name + ) + ); + } + + if (excludeEntities) { + entityIds = entityIds.filter( + (entityId) => !excludeEntities!.includes(entityId) + ); + } + if (includeDomains) { entityIds = entityIds.filter((eid) => includeDomains.includes(computeDomain(eid)) @@ -151,10 +194,17 @@ export class HaEntityPicker extends LitElement { ); } - states = entityIds.sort().map((key) => ({ - ...hass!.states[key], - friendly_name: computeStateName(hass!.states[key]) || key, - })); + states = entityIds + .map((key) => ({ + ...hass!.states[key], + friendly_name: computeStateName(hass!.states[key]) || key, + })) + .sort((entityA, entityB) => + caseInsensitiveStringCompare( + entityA.friendly_name, + entityB.friendly_name + ) + ); if (includeDeviceClasses) { states = states.filter( @@ -231,7 +281,9 @@ export class HaEntityPicker extends LitElement { this.excludeDomains, this.entityFilter, this.includeDeviceClasses, - this.includeUnitOfMeasurement + this.includeUnitOfMeasurement, + this.includeEntities, + this.excludeEntities ); if (this._initedStates) { (this.comboBox as any).filteredItems = this._states; diff --git a/src/components/ha-selector/ha-selector-entity.ts b/src/components/ha-selector/ha-selector-entity.ts index cbc5366953..8bf085f79f 100644 --- a/src/components/ha-selector/ha-selector-entity.ts +++ b/src/components/ha-selector/ha-selector-entity.ts @@ -6,8 +6,8 @@ import { subscribeEntityRegistry } from "../../data/entity_registry"; import { EntitySelector } from "../../data/selector"; import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../types"; -import "../entity/ha-entity-picker"; import "../entity/ha-entities-picker"; +import "../entity/ha-entity-picker"; @customElement("ha-selector-entity") export class HaEntitySelector extends SubscribeMixin(LitElement) { @@ -29,6 +29,8 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) { .hass=${this.hass} .value=${this.value} .label=${this.label} + .includeEntities=${this.selector.entity.includeEntities} + .excludeEntities=${this.selector.entity.excludeEntities} .entityFilter=${this._filterEntities} .disabled=${this.disabled} allow-custom-entity @@ -41,6 +43,8 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) { .hass=${this.hass} .value=${this.value} .entityFilter=${this._filterEntities} + .includeEntities=${this.selector.entity.includeEntities} + .excludeEntities=${this.selector.entity.excludeEntities} > `; } diff --git a/src/data/selector.ts b/src/data/selector.ts index b48c11b179..aba717b4a7 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -28,6 +28,8 @@ export interface EntitySelector { domain?: string | string[]; device_class?: string; multiple?: boolean; + includeEntities?: string[]; + excludeEntities?: string[]; }; } From afd2e71f6c490c2fdd0e4f0486f5451e5263ff65 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 22 Mar 2022 17:39:22 -0500 Subject: [PATCH 094/142] change from hidden to not shown (#12097) --- src/translations/en.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/translations/en.json b/src/translations/en.json index 8218353cf8..8fe2de52a5 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2405,7 +2405,7 @@ "add_entities_lovelace": "Add to dashboard", "none": "This device has no entities", "show_less": "Show less", - "hidden_entities": "+{count} {count, plural,\n one {hidden entity}\n other {hidden entities}\n}" + "hidden_entities": "+{count} {count, plural,\n one {entity}\n other {entities}\n} not shown" }, "confirm_rename_entity_ids": "Do you also want to rename the entity IDs of your entities?", "confirm_rename_entity_ids_warning": "This will not change any configuration (like automations, scripts, scenes, dashboards) that is currently using these entities! You will have to update them yourself to use the new entity IDs!", @@ -2430,7 +2430,7 @@ "filter": { "filter": "Filter", "show_disabled": "Show disabled devices", - "hidden_devices": "{number} hidden {number, plural,\n one {device}\n other {devices}\n}", + "hidden_devices": "{number} {number, plural,\n one {device}\n other {devices}\n} not shown", "show_all": "Show all" } } @@ -2449,7 +2449,7 @@ "show_disabled": "Show disabled entities", "show_unavailable": "Show unavailable entities", "show_readonly": "Show read-only entities", - "hidden_entities": "{number} hidden {number, plural,\n one {entity}\n other {entities}\n}", + "hidden_entities": "{number} {number, plural,\n one {entity}\n other {entities}\n} not shown", "show_all": "Show all" }, "status": { From 840858b18c2ec05c8d0e5360eb94a38808ceeec9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 22 Mar 2022 15:40:00 -0700 Subject: [PATCH 095/142] Add statistic adjust dialog (#12101) Co-authored-by: Zack Barett --- .../ha-selector/ha-selector-datetime.ts | 3 +- .../ha-selector/ha-selector-number.ts | 2 +- src/data/history.ts | 13 ++ .../statistics/developer-tools-statistics.ts | 27 +++ .../dialog-statistics-adjust-sum.ts | 166 ++++++++++++++++++ .../dialog-statistics-fix-units-changed.ts | 2 +- ...og-statistics-fix-unsupported-unit-meta.ts | 2 +- .../show-dialog-statistics-adjust-sum.ts | 20 +++ 8 files changed, 231 insertions(+), 4 deletions(-) create mode 100644 src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts create mode 100644 src/panels/developer-tools/statistics/show-dialog-statistics-adjust-sum.ts diff --git a/src/components/ha-selector/ha-selector-datetime.ts b/src/components/ha-selector/ha-selector-datetime.ts index 80add84bd4..caee6d8457 100644 --- a/src/components/ha-selector/ha-selector-datetime.ts +++ b/src/components/ha-selector/ha-selector-datetime.ts @@ -26,6 +26,7 @@ export class HaDateTimeSelector extends LitElement { protected render() { const values = this.value?.split(" "); + return html` stats.some((stat) => stat[type] !== null); + +export const adjustStatisticsSum = ( + hass: HomeAssistant, + statistic_id: string, + start_time: string, + adjustment: number +): Promise => + hass.callWS({ + type: "recorder/adjust_sum_statistics", + statistic_id, + start_time, + adjustment, + }); diff --git a/src/panels/developer-tools/statistics/developer-tools-statistics.ts b/src/panels/developer-tools/statistics/developer-tools-statistics.ts index 4b48ebb116..3183133beb 100644 --- a/src/panels/developer-tools/statistics/developer-tools-statistics.ts +++ b/src/panels/developer-tools/statistics/developer-tools-statistics.ts @@ -1,10 +1,12 @@ import "@material/mwc-button/mwc-button"; +import { mdiSlopeUphill } from "@mdi/js"; import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../common/dom/fire_event"; import { computeStateName } from "../../../common/entity/compute_state_name"; +import "../../../components/ha-icon-overflow-menu"; import "../../../components/data-table/ha-data-table"; import type { DataTableColumnContainer } from "../../../components/data-table/ha-data-table"; import { subscribeEntityRegistry } from "../../../data/entity_registry"; @@ -24,6 +26,7 @@ import { haStyle } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import { showFixStatisticsUnitsChangedDialog } from "./show-dialog-statistics-fix-units-changed"; import { showFixStatisticsUnsupportedUnitMetadataDialog } from "./show-dialog-statistics-fix-unsupported-unit-meta"; +import { showStatisticsAdjustSumDialog } from "./show-dialog-statistics-adjust-sum"; const FIX_ISSUES_ORDER = { no_state: 0, @@ -111,6 +114,30 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) { : ""}`, width: "113px", }, + actions: { + title: "", + type: "overflow-menu", + template: ( + _info, + statistic: StatisticsMetaData + ) => html` + showStatisticsAdjustSumDialog(this, { + statistic: statistic, + }), + }, + ]} + style="color: var(--secondary-text-color)" + >`, + }, }) ); diff --git a/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts b/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts new file mode 100644 index 0000000000..9819177799 --- /dev/null +++ b/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts @@ -0,0 +1,166 @@ +import "@material/mwc-button/mwc-button"; +import { LitElement, TemplateResult, html, CSSResultGroup } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import "../../../components/ha-dialog"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { haStyle, haStyleDialog } from "../../../resources/styles"; +import { HomeAssistant } from "../../../types"; +import "../../../components/ha-formfield"; +import "../../../components/ha-radio"; +import "../../../components/ha-form/ha-form"; +import type { DialogStatisticsAdjustSumParams } from "./show-dialog-statistics-adjust-sum"; +import type { + HaFormBaseSchema, + HaFormSchema, +} from "../../../components/ha-form/types"; +import { adjustStatisticsSum } from "../../../data/history"; +import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; +import { showToast } from "../../../util/toast"; + +let lastMoment: string | undefined; + +@customElement("dialog-statistics-adjust-sum") +export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _params?: DialogStatisticsAdjustSumParams; + + @state() private _data?: { + moment: string; + amount: number; + }; + + @state() private _busy = false; + + public showDialog(params: DialogStatisticsAdjustSumParams): void { + this._params = params; + this._busy = false; + const now = new Date(); + this._data = { + moment: + lastMoment || + `${now.getFullYear()}-${ + now.getMonth() + 1 + }-${now.getDate()} ${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`, + amount: 0, + }; + } + + public closeDialog(): void { + this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult | void { + if (!this._params) { + return html``; + } + + return html` + + + + + + + `; + } + + private _getSchema = memoizeOne((statistic): HaFormSchema[] => [ + { + type: "constant", + name: "name", + value: statistic.name || statistic.statistic_id, + }, + { + name: "moment", + required: true, + selector: { + datetime: {}, + }, + }, + { + name: "amount", + required: true, + default: 0, + selector: { + number: { + mode: "box", + step: 0.1, + unit_of_measurement: statistic.unit_of_measurement, + }, + }, + }, + ]); + + private _computeLabel(value: HaFormBaseSchema) { + switch (value.name) { + case "name": + return "Statistic"; + case "moment": + return "Moment to adjust"; + case "amount": + return "Amount"; + default: + return value.name; + } + } + + private _valueChanged(ev) { + this._data = ev.detail.value; + } + + private async _fixIssue(): Promise { + this._busy = true; + try { + await adjustStatisticsSum( + this.hass, + this._params!.statistic.statistic_id, + this._data!.moment, + this._data!.amount + ); + } catch (err: any) { + this._busy = false; + showAlertDialog(this, { + text: `Error adjusting sum: ${err.message || err}`, + }); + return; + } + showToast(this, { + message: "Statistic sum adjusted", + }); + lastMoment = this._data!.moment; + this.closeDialog(); + } + + static get styles(): CSSResultGroup { + return [haStyle, haStyleDialog]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-statistics-adjust-sum": DialogStatisticsFixUnsupportedUnitMetadata; + } +} diff --git a/src/panels/developer-tools/statistics/dialog-statistics-fix-units-changed.ts b/src/panels/developer-tools/statistics/dialog-statistics-fix-units-changed.ts index 1ce0c607ce..3168bffc9f 100644 --- a/src/panels/developer-tools/statistics/dialog-statistics-fix-units-changed.ts +++ b/src/panels/developer-tools/statistics/dialog-statistics-fix-units-changed.ts @@ -11,7 +11,7 @@ import { } from "../../../data/history"; import "../../../components/ha-formfield"; import "../../../components/ha-radio"; -import { DialogStatisticsUnitsChangedParams } from "./show-dialog-statistics-fix-units-changed"; +import type { DialogStatisticsUnitsChangedParams } from "./show-dialog-statistics-fix-units-changed"; @customElement("dialog-statistics-fix-units-changed") export class DialogStatisticsFixUnitsChanged extends LitElement { diff --git a/src/panels/developer-tools/statistics/dialog-statistics-fix-unsupported-unit-meta.ts b/src/panels/developer-tools/statistics/dialog-statistics-fix-unsupported-unit-meta.ts index 93d1e320b1..4bfaebe489 100644 --- a/src/panels/developer-tools/statistics/dialog-statistics-fix-unsupported-unit-meta.ts +++ b/src/panels/developer-tools/statistics/dialog-statistics-fix-unsupported-unit-meta.ts @@ -8,7 +8,7 @@ import { HomeAssistant } from "../../../types"; import { updateStatisticsMetadata } from "../../../data/history"; import "../../../components/ha-formfield"; import "../../../components/ha-radio"; -import { DialogStatisticsUnsupportedUnitMetaParams } from "./show-dialog-statistics-fix-unsupported-unit-meta"; +import type { DialogStatisticsUnsupportedUnitMetaParams } from "./show-dialog-statistics-fix-unsupported-unit-meta"; @customElement("dialog-statistics-fix-unsupported-unit-meta") export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement { diff --git a/src/panels/developer-tools/statistics/show-dialog-statistics-adjust-sum.ts b/src/panels/developer-tools/statistics/show-dialog-statistics-adjust-sum.ts new file mode 100644 index 0000000000..1db2c76307 --- /dev/null +++ b/src/panels/developer-tools/statistics/show-dialog-statistics-adjust-sum.ts @@ -0,0 +1,20 @@ +import { fireEvent } from "../../../common/dom/fire_event"; +import { StatisticsMetaData } from "../../../data/history"; + +export const loadAdjustSumDialog = () => + import("./dialog-statistics-adjust-sum"); + +export interface DialogStatisticsAdjustSumParams { + statistic: StatisticsMetaData; +} + +export const showStatisticsAdjustSumDialog = ( + element: HTMLElement, + detailParams: DialogStatisticsAdjustSumParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-statistics-adjust-sum", + dialogImport: loadAdjustSumDialog, + dialogParams: detailParams, + }); +}; From 2d9b50defc5ee96a8161374e63ce19112c4ccc70 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 22 Mar 2022 18:33:16 -0500 Subject: [PATCH 096/142] Fix Duration Selector Default (#12098) * Fix Duration Default * USe initial form data function --- .../ha-form/compute-initial-ha-form-data.ts | 22 ++++++++++++++++++- .../ha-selector/ha-selector-duration.ts | 6 ++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/components/ha-form/compute-initial-ha-form-data.ts b/src/components/ha-form/compute-initial-ha-form-data.ts index 0e80433c74..7c5d728d91 100644 --- a/src/components/ha-form/compute-initial-ha-form-data.ts +++ b/src/components/ha-form/compute-initial-ha-form-data.ts @@ -1,4 +1,5 @@ -import { HaFormSchema } from "./types"; +import type { Selector } from "../../data/selector"; +import type { HaFormSchema } from "./types"; export const computeInitialHaFormData = ( schema: HaFormSchema[] @@ -31,6 +32,25 @@ export const computeInitialHaFormData = ( minutes: 0, seconds: 0, }; + } else if ("selector" in field) { + const selector: Selector = field.selector; + if ("boolean" in selector) { + data[field.name] = false; + } else if ("text" in selector) { + data[field.name] = ""; + } else if ("number" in selector) { + data[field.name] = "min" in selector.number ? selector.number.min : 0; + } else if ("select" in selector) { + if (selector.select.options.length) { + data[field.name] = selector.select.options[0][0]; + } + } else if ("duration" in selector) { + data[field.name] = { + hours: 0, + minutes: 0, + seconds: 0, + }; + } } }); return data; diff --git a/src/components/ha-selector/ha-selector-duration.ts b/src/components/ha-selector/ha-selector-duration.ts index 1471750d90..1f5e88b146 100644 --- a/src/components/ha-selector/ha-selector-duration.ts +++ b/src/components/ha-selector/ha-selector-duration.ts @@ -1,8 +1,8 @@ -import "../ha-duration-input"; import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; -import { DurationSelector } from "../../data/selector"; -import { HomeAssistant } from "../../types"; +import type { DurationSelector } from "../../data/selector"; +import type { HomeAssistant } from "../../types"; +import "../ha-duration-input"; @customElement("ha-selector-duration") export class HaTimeDuration extends LitElement { From a7a347ed05ba75a7260d02fce4f9847f133c9537 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 22 Mar 2022 19:08:30 -0500 Subject: [PATCH 097/142] Bumped version to 20220322.0 (#12102) --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 2afbb59467..e8f04faf57 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = home-assistant-frontend -version = 20220317.0 +version = 20220322.0 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From c0dce08e19cb2f965ba6700627db7cf4203df511 Mon Sep 17 00:00:00 2001 From: Matthias de Baat Date: Wed, 23 Mar 2022 04:51:35 +0100 Subject: [PATCH 098/142] Create user types page and rename the category (#12089) Co-authored-by: Zack Barett Co-authored-by: Paulus Schoutsen --- gallery/sidebar.js | 5 +++-- gallery/src/ha-gallery.ts | 4 ++++ gallery/src/pages/user-test/user-types.markdown | 17 +++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 gallery/src/pages/user-test/user-types.markdown diff --git a/gallery/sidebar.js b/gallery/sidebar.js index d98d361224..14a8d38fe8 100644 --- a/gallery/sidebar.js +++ b/gallery/sidebar.js @@ -42,10 +42,11 @@ module.exports = [ }, { category: "user-test", - header: "User Tests", + header: "Users", + pages: ["user-types", "configuration-menu"], }, { category: "design.home-assistant.io", - header: "Design Documentation", + header: "About", }, ]; diff --git a/gallery/src/ha-gallery.ts b/gallery/src/ha-gallery.ts index e602a1916d..4f22bac519 100644 --- a/gallery/src/ha-gallery.ts +++ b/gallery/src/ha-gallery.ts @@ -45,6 +45,10 @@ class HaGallery extends LitElement { for (const page of group.pages!) { const key = `${group.category}/${page}`; const active = this._page === key; + if (!(key in PAGES)) { + console.error("Undefined page referenced in sidebar.js:", key); + continue; + } const title = PAGES[key].metadata.title || page; links.push(html` ${title} diff --git a/gallery/src/pages/user-test/user-types.markdown b/gallery/src/pages/user-test/user-types.markdown new file mode 100644 index 0000000000..eacc108cd4 --- /dev/null +++ b/gallery/src/pages/user-test/user-types.markdown @@ -0,0 +1,17 @@ +--- +title: "User types" +--- + +We have defined three user types for Home Assistant. They are a lean segmentation of users that helps us make decisions throughout the product. User types differ from traditional personas in that the segmentation criteria aren’t demographic and don’t personify a group into a single character with a fictitious background story. + +# Outgrowers + +Users that outgrow big tech smart home solutions. It just needs to work with easy setup via an app. + +# Tinkerers + +Technoid users in home networking and development that know how to code. + +# Questioner + +Users who want more advanced home automation, but need support to make it work. From d6a1d5af79a15e4e21782ba929b9a0027a9a7672 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 23 Mar 2022 09:22:12 +0100 Subject: [PATCH 099/142] Remove `setup.py` (#11593) --- setup.py | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 setup.py diff --git a/setup.py b/setup.py deleted file mode 100644 index 69bf65dd8a..0000000000 --- a/setup.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Entry point for setuptools. Required for editable installs. -TODO: Remove file after updating to pip 21.3 -""" -from setuptools import setup - -setup() From 079cc39a6ed81af35403a1a3742d1def08a6e8f1 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 23 Mar 2022 14:24:55 +0100 Subject: [PATCH 100/142] Fix selecting 0 with number selector --- src/components/ha-selector/ha-selector-number.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ha-selector/ha-selector-number.ts b/src/components/ha-selector/ha-selector-number.ts index 0094192f1b..8a9dfb1c3a 100644 --- a/src/components/ha-selector/ha-selector-number.ts +++ b/src/components/ha-selector/ha-selector-number.ts @@ -46,7 +46,7 @@ export class HaNumberSelector extends LitElement { class=${classMap({ single: this.selector.number.mode === "box" })} .min=${this.selector.number.min} .max=${this.selector.number.max} - .value=${this.value || ""} + .value=${this.value ?? ""} .step=${this.selector.number.step ?? 1} .disabled=${this.disabled} .required=${this.required} From 504b043159f3b0103f6412263e6a382b2df3447b Mon Sep 17 00:00:00 2001 From: Michael Irigoyen Date: Wed, 23 Mar 2022 08:46:22 -0500 Subject: [PATCH 101/142] Update lock file with MDI updates --- yarn.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/yarn.lock b/yarn.lock index e60cdf3d7f..7701804f4f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2975,17 +2975,17 @@ __metadata: languageName: node linkType: hard -"@mdi/js@npm:6.5.95": - version: 6.5.95 - resolution: "@mdi/js@npm:6.5.95" - checksum: b1db7713d216c119f584bf973514a2f9d8f2e671e91bf19ce8e56cfa7a9843c0a060328e794507ac31f2bded1032123294f39ff8e987ea5acb2719ab522ef146 +"@mdi/js@npm:6.6.95": + version: 6.6.95 + resolution: "@mdi/js@npm:6.6.95" + checksum: 4cf8c48156f0e9ff67e4394cd428158bd164b1a6b7ca1aa70fc6a6aee91cfede9eba56720eb7d13fa57315ac636e9519a62dedd3cd2a9708aa11f2e3624ddbff languageName: node linkType: hard -"@mdi/svg@npm:6.5.95": - version: 6.5.95 - resolution: "@mdi/svg@npm:6.5.95" - checksum: 2d45221d042d52d54c85eaf672a5f3697ed5201607fa38a6e235ee2e60d1c3c25d456a284f19ce47b5f06418cacfee29e8fecf6580b8c28538fd26044becaf1a +"@mdi/svg@npm:6.6.95": + version: 6.6.95 + resolution: "@mdi/svg@npm:6.6.95" + checksum: 59b79db945847a3d981351418e0e7a457b831e09846fa751d44e80df8fb4cd19ef12bc889538ed2945d2638e522aa7ea5b1f97997e19dd68345f5d7bf5cad5e6 languageName: node linkType: hard @@ -9048,8 +9048,8 @@ fsevents@^1.2.7: "@material/mwc-textfield": 0.25.3 "@material/mwc-top-app-bar-fixed": ^0.25.3 "@material/top-app-bar": 14.0.0-canary.261f2db59.0 - "@mdi/js": 6.5.95 - "@mdi/svg": 6.5.95 + "@mdi/js": 6.6.95 + "@mdi/svg": 6.6.95 "@open-wc/dev-server-hmr": ^0.0.2 "@polymer/app-layout": ^3.1.0 "@polymer/iron-flex-layout": ^3.0.1 From 7b6d3c0e36989e8c293b1d07334821c4313e8ddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 23 Mar 2022 21:04:29 +0100 Subject: [PATCH 102/142] Use update entities for showing updates on configuration panel (#12100) Co-authored-by: Paulus Schoutsen --- .../update-available/update-available-card.ts | 7 +- src/data/supervisor/root.ts | 58 ------ .../config/dashboard/ha-config-dashboard.ts | 165 +++++++++++------- .../config/dashboard/ha-config-updates.ts | 108 +++++------- src/panels/config/ha-panel-config.ts | 32 +--- src/translations/en.json | 4 +- 6 files changed, 158 insertions(+), 216 deletions(-) delete mode 100644 src/data/supervisor/root.ts diff --git a/hassio/src/update-available/update-available-card.ts b/hassio/src/update-available/update-available-card.ts index a5a2bc81a2..27c3533b8e 100644 --- a/hassio/src/update-available/update-available-card.ts +++ b/hassio/src/update-available/update-available-card.ts @@ -45,7 +45,6 @@ import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; import "../../../src/layouts/hass-loading-screen"; import "../../../src/layouts/hass-subpage"; import "../../../src/layouts/hass-tabs-subpage"; -import { SUPERVISOR_UPDATE_NAMES } from "../../../src/panels/config/dashboard/ha-config-updates"; import { HomeAssistant, Route } from "../../../src/types"; import { addonArchIsSupported, extractChangelog } from "../util/addon"; @@ -55,6 +54,12 @@ declare global { } } +const SUPERVISOR_UPDATE_NAMES = { + core: "Home Assistant Core", + os: "Home Assistant Operating System", + supervisor: "Home Assistant Supervisor", +}; + type updateType = "os" | "supervisor" | "core" | "addon"; const changelogUrl = ( diff --git a/src/data/supervisor/root.ts b/src/data/supervisor/root.ts deleted file mode 100644 index 51fe449ecd..0000000000 --- a/src/data/supervisor/root.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { HomeAssistant } from "../../types"; - -interface SupervisorBaseAvailableUpdates { - panel_path?: string; - update_type?: string; - version_latest?: string; -} - -interface SupervisorAddonAvailableUpdates - extends SupervisorBaseAvailableUpdates { - update_type?: "addon"; - icon?: string; - name?: string; -} - -interface SupervisorCoreAvailableUpdates - extends SupervisorBaseAvailableUpdates { - update_type?: "core"; -} - -interface SupervisorOsAvailableUpdates extends SupervisorBaseAvailableUpdates { - update_type?: "os"; -} - -interface SupervisorSupervisorAvailableUpdates - extends SupervisorBaseAvailableUpdates { - update_type?: "supervisor"; -} - -export type SupervisorAvailableUpdates = - | SupervisorAddonAvailableUpdates - | SupervisorCoreAvailableUpdates - | SupervisorOsAvailableUpdates - | SupervisorSupervisorAvailableUpdates; - -export interface SupervisorAvailableUpdatesResponse { - available_updates: SupervisorAvailableUpdates[]; -} - -export const fetchSupervisorAvailableUpdates = async ( - hass: HomeAssistant -): Promise => - ( - await hass.callWS({ - type: "supervisor/api", - endpoint: "/available_updates", - method: "get", - }) - ).available_updates; - -export const refreshSupervisorAvailableUpdates = async ( - hass: HomeAssistant -): Promise => - hass.callWS({ - type: "supervisor/api", - endpoint: "/refresh_updates", - method: "post", - }); diff --git a/src/panels/config/dashboard/ha-config-dashboard.ts b/src/panels/config/dashboard/ha-config-dashboard.ts index 731a5c7079..3f0e3e4849 100644 --- a/src/panels/config/dashboard/ha-config-dashboard.ts +++ b/src/panels/config/dashboard/ha-config-dashboard.ts @@ -1,3 +1,5 @@ +import type { ActionDetail } from "@material/mwc-list"; +import "@material/mwc-list/mwc-list-item"; import { mdiCloudLock, mdiDotsVertical, @@ -5,10 +7,9 @@ import { mdiMagnify, mdiNewBox, } from "@mdi/js"; -import "@material/mwc-list/mwc-list-item"; -import type { ActionDetail } from "@material/mwc-list"; import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-toolbar/app-toolbar"; +import type { HassEntities } from "home-assistant-js-websocket"; import { css, CSSResultGroup, @@ -18,30 +19,29 @@ import { TemplateResult, } from "lit"; import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; -import "../../../components/ha-card"; -import "../../../components/ha-icon-next"; -import "../../../components/ha-icon-button"; -import "../../../components/ha-menu-button"; +import { computeStateDomain } from "../../../common/entity/compute_state_domain"; +import { caseInsensitiveStringCompare } from "../../../common/string/compare"; import "../../../components/ha-button-menu"; +import "../../../components/ha-card"; +import "../../../components/ha-icon-button"; +import "../../../components/ha-icon-next"; +import "../../../components/ha-menu-button"; import "../../../components/ha-svg-icon"; import { CloudStatus } from "../../../data/cloud"; -import { - refreshSupervisorAvailableUpdates, - SupervisorAvailableUpdates, -} from "../../../data/supervisor/root"; +import { updateCanInstall, UpdateEntity } from "../../../data/update"; +import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar"; import "../../../layouts/ha-app-layout"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; +import { documentationUrl } from "../../../util/documentation-url"; +import { showToast } from "../../../util/toast"; import "../ha-config-section"; import { configSections } from "../ha-panel-config"; import "./ha-config-navigation"; import "./ha-config-updates"; -import { fireEvent } from "../../../common/dom/fire_event"; -import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; -import { showToast } from "../../../util/toast"; -import { documentationUrl } from "../../../util/documentation-url"; const randomTip = (hass: HomeAssistant) => { const weighted: string[] = []; @@ -113,9 +113,6 @@ class HaConfigDashboard extends LitElement { @property() public cloudStatus?: CloudStatus; - // null means not available - @property() public supervisorUpdates?: SupervisorAvailableUpdates[] | null; - @property() public showAdvanced!: boolean; @state() private _tip?: string; @@ -123,6 +120,9 @@ class HaConfigDashboard extends LitElement { private _notifyUpdates = false; protected render(): TemplateResult { + const canInstallUpdates = this._filterUpdateEntitiesWithInstall( + this.hass.states + ); return html` @@ -160,50 +160,47 @@ class HaConfigDashboard extends LitElement { .isWide=${this.isWide} full-width > - ${this.supervisorUpdates === undefined - ? // Hide everything until updates loaded - html`` - : html`${this.supervisorUpdates?.length - ? html` - - ` - : ""} - - ${this.narrow && this.supervisorUpdates?.length - ? html`
- ${this.hass.localize("panel.config")} -
` - : ""} - ${this.cloudStatus && isComponentLoaded(this.hass, "cloud") - ? html` - - ` - : ""} + ${canInstallUpdates.length + ? html` + + ` + : ""} + + ${this.narrow && canInstallUpdates.length + ? html`
+ ${this.hass.localize("panel.config")} +
` + : ""} + ${this.cloudStatus && isComponentLoaded(this.hass, "cloud") + ? html` -
`} + ` + : ""} + +
Tip! @@ -221,11 +218,11 @@ class HaConfigDashboard extends LitElement { this._tip = randomTip(this.hass); } - if (!changedProps.has("supervisorUpdates") || !this._notifyUpdates) { + if (!changedProps.has("hass") || !this._notifyUpdates) { return; } this._notifyUpdates = false; - if (this.supervisorUpdates?.length) { + if (this._filterUpdateEntitiesWithInstall(this.hass.states).length) { showToast(this, { message: this.hass.localize( "ui.panel.config.updates.updates_refreshed" @@ -238,6 +235,44 @@ class HaConfigDashboard extends LitElement { } } + private _filterUpdateEntities = memoizeOne((entities: HassEntities) => + ( + Object.values(entities).filter( + (entity) => computeStateDomain(entity) === "update" + ) as UpdateEntity[] + ).sort((a, b) => { + if (a.attributes.title === "Home Assistant Core") { + return -3; + } + if (b.attributes.title === "Home Assistant Core") { + return 3; + } + if (a.attributes.title === "Home Assistant Operating System") { + return -2; + } + if (b.attributes.title === "Home Assistant Operating System") { + return 2; + } + if (a.attributes.title === "Home Assistant Supervisor") { + return -1; + } + if (b.attributes.title === "Home Assistant Supervisor") { + return 1; + } + return caseInsensitiveStringCompare( + a.attributes.title || a.attributes.friendly_name || "", + b.attributes.title || b.attributes.friendly_name || "" + ); + }) + ); + + private _filterUpdateEntitiesWithInstall = memoizeOne( + (entities: HassEntities) => + this._filterUpdateEntities(entities).filter((entity) => + updateCanInstall(entity) + ) + ); + private _showQuickBar(): void { showQuickBar(this, { commandMode: true, @@ -246,20 +281,24 @@ class HaConfigDashboard extends LitElement { } private async _handleMenuAction(ev: CustomEvent) { + const _entities = this._filterUpdateEntities(this.hass.states).map( + (entity) => entity.entity_id + ); switch (ev.detail.index) { case 0: - if (isComponentLoaded(this.hass, "hassio")) { + if (_entities.length) { this._notifyUpdates = true; - await refreshSupervisorAvailableUpdates(this.hass); - fireEvent(this, "ha-refresh-supervisor"); + await this.hass.callService("homeassistant", "update_entity", { + entity_id: _entities, + }); return; } showAlertDialog(this, { title: this.hass.localize( - "ui.panel.config.updates.check_unavailable.title" + "ui.panel.config.updates.no_update_entities.title" ), text: this.hass.localize( - "ui.panel.config.updates.check_unavailable.description" + "ui.panel.config.updates.no_update_entities.description" ), warning: true, }); diff --git a/src/panels/config/dashboard/ha-config-updates.ts b/src/panels/config/dashboard/ha-config-updates.ts index 8c11e4325e..cae7397e58 100644 --- a/src/panels/config/dashboard/ha-config-updates.ts +++ b/src/panels/config/dashboard/ha-config-updates.ts @@ -1,21 +1,14 @@ import "@material/mwc-button/mwc-button"; -import { mdiPackageVariant } from "@mdi/js"; import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-item-body"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../common/dom/fire_event"; +import "../../../components/entity/state-badge"; import "../../../components/ha-alert"; -import "../../../components/ha-logo-svg"; -import "../../../components/ha-svg-icon"; -import { SupervisorAvailableUpdates } from "../../../data/supervisor/root"; -import { HomeAssistant } from "../../../types"; import "../../../components/ha-icon-next"; - -export const SUPERVISOR_UPDATE_NAMES = { - core: "Home Assistant Core", - os: "Home Assistant Operating System", - supervisor: "Home Assistant Supervisor", -}; +import type { UpdateEntity } from "../../../data/update"; +import { HomeAssistant } from "../../../types"; @customElement("ha-config-updates") class HaConfigUpdates extends LitElement { @@ -24,62 +17,60 @@ class HaConfigUpdates extends LitElement { @property({ type: Boolean }) public narrow!: boolean; @property({ attribute: false }) - public supervisorUpdates?: SupervisorAvailableUpdates[] | null; + public updateEntities?: UpdateEntity[]; @state() private _showAll = false; protected render(): TemplateResult { - if (!this.supervisorUpdates?.length) { + if (!this.updateEntities?.length) { return html``; } const updates = - this._showAll || this.supervisorUpdates.length <= 3 - ? this.supervisorUpdates - : this.supervisorUpdates.slice(0, 2); + this._showAll || this.updateEntities.length <= 3 + ? this.updateEntities + : this.updateEntities.slice(0, 2); return html`
${this.hass.localize("ui.panel.config.updates.title", { - count: this.supervisorUpdates.length, + count: this.updateEntities.length, })}
${updates.map( - (update) => html` - - - - ${update.update_type === "addon" - ? update.icon - ? html`` - : html`` - : html``} - - - ${update.update_type === "addon" - ? update.name - : SUPERVISOR_UPDATE_NAMES[update.update_type!]} -
- ${this.hass.localize( - "ui.panel.config.updates.version_available", - { - version_available: update.version_latest, - } - )} -
-
- ${!this.narrow ? html`` : ""} -
-
+ (entity) => html` + + + + + + ${entity.attributes.title || entity.attributes.friendly_name} +
+ ${this.hass.localize( + "ui.panel.config.updates.version_available", + { + version_available: entity.attributes.latest_version, + } + )} +
+
+ ${!this.narrow ? html`` : ""} +
` )} - ${!this._showAll && this.supervisorUpdates.length >= 4 + ${!this._showAll && this.updateEntities.length >= 4 ? html` ` @@ -87,6 +78,12 @@ class HaConfigUpdates extends LitElement { `; } + private _openMoreInfo(ev: MouseEvent): void { + fireEvent(this, "hass-more-info", { + entityId: (ev.currentTarget as any).entity_id, + }); + } + private _showAllClicked() { this._showAll = true; } @@ -99,25 +96,11 @@ class HaConfigUpdates extends LitElement { padding: 16px; padding-bottom: 0; } - a { - text-decoration: none; - color: var(--primary-text-color); - } .icon { display: inline-flex; height: 100%; align-items: center; } - img, - ha-svg-icon, - ha-logo-svg { - --mdc-icon-size: 32px; - max-height: 32px; - width: 32px; - } - ha-logo-svg { - color: var(--secondary-text-color); - } ha-icon-next { color: var(--secondary-text-color); height: 24px; @@ -139,6 +122,9 @@ class HaConfigUpdates extends LitElement { outline: none; text-decoration: underline; } + paper-icon-item { + cursor: pointer; + } `, ]; } diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index 850408f095..0ca0544fe7 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -27,10 +27,6 @@ import { customElement, property, state } from "lit/decorators"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { listenMediaQuery } from "../../common/dom/media_query"; import { CloudStatus, fetchCloudStatus } from "../../data/cloud"; -import { - fetchSupervisorAvailableUpdates, - SupervisorAvailableUpdates, -} from "../../data/supervisor/root"; import "../../layouts/hass-loading-screen"; import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page"; import { PageNavigation } from "../../layouts/hass-tabs-subpage"; @@ -397,8 +393,6 @@ class HaPanelConfig extends HassRouterPage { @state() private _cloudStatus?: CloudStatus; - @state() private _supervisorUpdates?: SupervisorAvailableUpdates[] | null; - private _listeners: Array<() => void> = []; public connectedCallback() { @@ -433,19 +427,7 @@ class HaPanelConfig extends HassRouterPage { } }); } - if (isComponentLoaded(this.hass, "hassio")) { - this._loadSupervisorUpdates(); - this.addEventListener("ha-refresh-supervisor", () => { - this._loadSupervisorUpdates(); - }); - this.addEventListener("connection-status", (ev) => { - if (ev.detail === "connected") { - this._loadSupervisorUpdates(); - } - }); - } else { - this._supervisorUpdates = null; - } + this.addEventListener("ha-refresh-cloud-status", () => this._updateCloudStatus() ); @@ -476,7 +458,6 @@ class HaPanelConfig extends HassRouterPage { isWide, narrow: this.narrow, cloudStatus: this._cloudStatus, - supervisorUpdates: this._supervisorUpdates, }); } else { el.route = this.routeTail; @@ -485,7 +466,6 @@ class HaPanelConfig extends HassRouterPage { el.isWide = isWide; el.narrow = this.narrow; el.cloudStatus = this._cloudStatus; - el.supervisorUpdates = this._supervisorUpdates; } } @@ -503,16 +483,6 @@ class HaPanelConfig extends HassRouterPage { setTimeout(() => this._updateCloudStatus(), 5000); } } - - private async _loadSupervisorUpdates(): Promise { - try { - this._supervisorUpdates = await fetchSupervisorAvailableUpdates( - this.hass - ); - } catch (err) { - this._supervisorUpdates = null; - } - } } declare global { diff --git a/src/translations/en.json b/src/translations/en.json index 8fe2de52a5..3ef90c50bd 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1079,9 +1079,9 @@ "learn_more": "Learn more" }, "updates": { - "check_unavailable": { + "no_update_entities": { "title": "Unable to check for updates", - "description": "You need to run the Home Assistant operating system to be able to check and install updates from the Home Assistant user interface." + "description": "You do not have any integrations that provide updates." }, "check_updates": "Check for updates", "no_new_updates": "No new updates found", From c522670815fc5a89685fd51b436c73e5e63124e9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 23 Mar 2022 21:11:46 +0100 Subject: [PATCH 103/142] Fix loading traces for automation with custom id (#12112) --- src/panels/config/automation/ha-config-automation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panels/config/automation/ha-config-automation.ts b/src/panels/config/automation/ha-config-automation.ts index a4fa5d38ab..70f2b16add 100644 --- a/src/panels/config/automation/ha-config-automation.ts +++ b/src/panels/config/automation/ha-config-automation.ts @@ -87,7 +87,7 @@ class HaConfigAutomation extends HassRouterPage { (!changedProps || changedProps.has("route")) && this._currentPage !== "dashboard" ) { - const automationId = this.routeTail.path.substr(1); + const automationId = decodeURIComponent(this.routeTail.path.substr(1)); pageEl.automationId = automationId === "new" ? null : automationId; } } From cbd030a37951b5ffaf186d059866f185de251c48 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 23 Mar 2022 14:51:01 -0700 Subject: [PATCH 104/142] Only show docs link when showing a form --- src/dialogs/config-flow/dialog-data-entry-flow.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index d52c9edd7f..b190f605b5 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -238,12 +238,14 @@ class DataEntryFlowDialog extends LitElement { "" : html`
- ${this._step + ${["form", "menu", "external"].includes( + this._step?.type as any + ) ? html` Date: Wed, 23 Mar 2022 23:05:41 +0100 Subject: [PATCH 105/142] Exclude restored automations from dashboard (#12113) --- src/panels/config/automation/ha-config-automation.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/panels/config/automation/ha-config-automation.ts b/src/panels/config/automation/ha-config-automation.ts index 70f2b16add..d1a117e616 100644 --- a/src/panels/config/automation/ha-config-automation.ts +++ b/src/panels/config/automation/ha-config-automation.ts @@ -59,7 +59,9 @@ class HaConfigAutomation extends HassRouterPage { private _getAutomations = memoizeOne( (states: HassEntities): AutomationEntity[] => Object.values(states).filter( - (entity) => computeStateDomain(entity) === "automation" + (entity) => + computeStateDomain(entity) === "automation" && + !entity.attributes.restored ) as AutomationEntity[] ); From df9619943343aa5086ceb90f41e8ef234b446962 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 23 Mar 2022 23:14:57 +0100 Subject: [PATCH 106/142] Support descriptions in flow menu steps (#12108) Co-authored-by: Paulus Schoutsen --- .../config-flow/show-dialog-config-flow.ts | 12 ++++++++++++ .../config-flow/show-dialog-data-entry-flow.ts | 5 +++++ .../config-flow/show-dialog-options-flow.ts | 16 ++++++++++++++++ src/dialogs/config-flow/step-flow-menu.ts | 16 ++++++++++++++++ 4 files changed, 49 insertions(+) diff --git a/src/dialogs/config-flow/show-dialog-config-flow.ts b/src/dialogs/config-flow/show-dialog-config-flow.ts index 5b7c0722b7..7f413fee8d 100644 --- a/src/dialogs/config-flow/show-dialog-config-flow.ts +++ b/src/dialogs/config-flow/show-dialog-config-flow.ts @@ -189,6 +189,18 @@ export const showConfigFlowDialog = ( ); }, + renderMenuDescription(hass, step) { + const description = hass.localize( + `component.${step.handler}.config.step.${step.step_id}.description`, + step.description_placeholders + ); + return description + ? html` + + ` + : ""; + }, + renderMenuOption(hass, step, option) { return hass.localize( `component.${step.handler}.config.step.${step.step_id}.menu_options.${option}`, diff --git a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts index be54d09839..2736b98fe3 100644 --- a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts @@ -83,6 +83,11 @@ export interface FlowConfig { renderMenuHeader(hass: HomeAssistant, step: DataEntryFlowStepMenu): string; + renderMenuDescription( + hass: HomeAssistant, + step: DataEntryFlowStepMenu + ): TemplateResult | ""; + renderMenuOption( hass: HomeAssistant, step: DataEntryFlowStepMenu, diff --git a/src/dialogs/config-flow/show-dialog-options-flow.ts b/src/dialogs/config-flow/show-dialog-options-flow.ts index 451e601ed4..fbc9205ef3 100644 --- a/src/dialogs/config-flow/show-dialog-options-flow.ts +++ b/src/dialogs/config-flow/show-dialog-options-flow.ts @@ -142,6 +142,22 @@ export const showOptionsFlowDialog = ( ); }, + renderMenuDescription(hass, step) { + const description = hass.localize( + `component.${step.handler}.option.step.${step.step_id}.description`, + step.description_placeholders + ); + return description + ? html` + + ` + : ""; + }, + renderMenuOption(hass, step, option) { return hass.localize( `component.${step.handler}.options.step.${step.step_id}.menu_options.${option}`, diff --git a/src/dialogs/config-flow/step-flow-menu.ts b/src/dialogs/config-flow/step-flow-menu.ts index a49baed97f..66d0b8d0c6 100644 --- a/src/dialogs/config-flow/step-flow-menu.ts +++ b/src/dialogs/config-flow/step-flow-menu.ts @@ -35,8 +35,14 @@ class StepFlowMenu extends LitElement { translations = this.step.menu_options; } + const description = this.flowConfig.renderMenuDescription( + this.hass, + this.step + ); + return html`

${this.flowConfig.renderMenuHeader(this.hass, this.step)}

+ ${description ? html`
${description}
` : ""}
${options.map( (option) => html` @@ -69,6 +75,16 @@ class StepFlowMenu extends LitElement { margin-top: 20px; margin-bottom: 8px; } + .content { + padding-bottom: 16px; + border-bottom: 1px solid var(--divider-color); + } + .content + .options { + margin-top: 8px; + } + mwc-list-item { + --mdc-list-side-padding: 24px; + } `, ]; } From 40d878689fb4c4b0998ba0bbc6fd0103fe23c687 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 24 Mar 2022 11:53:32 +0100 Subject: [PATCH 107/142] Sort selectors (#12120) --- src/data/selector.ts | 226 +++++++++++++++++++++---------------------- 1 file changed, 112 insertions(+), 114 deletions(-) diff --git a/src/data/selector.ts b/src/data/selector.ts index aba717b4a7..20132ec8b1 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -1,35 +1,51 @@ export type Selector = + | ActionSelector | AddonSelector + | AreaSelector | AttributeSelector - | EntitySelector + | BooleanSelector + | ColorRGBSelector + | ColorTempSelector | DateSelector | DateTimeSelector | DeviceSelector | DurationSelector - | AreaSelector - | TargetSelector + | EntitySelector + | IconSelector + | LocationSelector + | MediaSelector | NumberSelector - | BooleanSelector - | TimeSelector - | ActionSelector - | StringSelector | ObjectSelector | SelectSelector - | IconSelector - | MediaSelector + | StringSelector + | TargetSelector | ThemeSelector - | LocationSelector - | ColorTempSelector - | ColorRGBSelector; + | TimeSelector; -export interface EntitySelector { - entity: { - integration?: string; - domain?: string | string[]; - device_class?: string; - multiple?: boolean; - includeEntities?: string[]; - excludeEntities?: string[]; +export interface ActionSelector { + // eslint-disable-next-line @typescript-eslint/ban-types + action: {}; +} + +export interface AddonSelector { + addon: { + name?: string; + slug?: string; + }; +} + +export interface AreaSelector { + area: { + entity?: { + integration?: EntitySelector["entity"]["integration"]; + domain?: EntitySelector["entity"]["domain"]; + device_class?: EntitySelector["entity"]["device_class"]; + }; + device?: { + integration?: DeviceSelector["device"]["integration"]; + manufacturer?: DeviceSelector["device"]["manufacturer"]; + model?: DeviceSelector["device"]["model"]; + }; }; } @@ -39,11 +55,23 @@ export interface AttributeSelector { }; } +export interface BooleanSelector { + // eslint-disable-next-line @typescript-eslint/ban-types + boolean: {}; +} + export interface ColorRGBSelector { // eslint-disable-next-line @typescript-eslint/ban-types color_rgb: {}; } +export interface ColorTempSelector { + color_temp: { + min_mireds?: number; + max_mireds?: number; + }; +} + export interface DateSelector { // eslint-disable-next-line @typescript-eslint/ban-types date: {}; @@ -72,40 +100,49 @@ export interface DurationSelector { duration: {}; } -export interface AddonSelector { - addon: { - name?: string; - slug?: string; +export interface EntitySelector { + entity: { + integration?: string; + domain?: string | string[]; + device_class?: string; + multiple?: boolean; + includeEntities?: string[]; + excludeEntities?: string[]; }; } -export interface AreaSelector { - area: { - entity?: { - integration?: EntitySelector["entity"]["integration"]; - domain?: EntitySelector["entity"]["domain"]; - device_class?: EntitySelector["entity"]["device_class"]; - }; - device?: { - integration?: DeviceSelector["device"]["integration"]; - manufacturer?: DeviceSelector["device"]["manufacturer"]; - model?: DeviceSelector["device"]["model"]; - }; +export interface IconSelector { + icon: { + placeholder?: string; + fallbackPath?: string; }; } -export interface TargetSelector { - target: { - entity?: { - integration?: EntitySelector["entity"]["integration"]; - domain?: EntitySelector["entity"]["domain"]; - device_class?: EntitySelector["entity"]["device_class"]; - }; - device?: { - integration?: DeviceSelector["device"]["integration"]; - manufacturer?: DeviceSelector["device"]["manufacturer"]; - model?: DeviceSelector["device"]["model"]; - }; +export interface LocationSelector { + location: { radius?: boolean; icon?: string }; +} + +export interface LocationSelectorValue { + latitude: number; + longitude: number; + radius?: number; +} + +export interface MediaSelector { + // eslint-disable-next-line @typescript-eslint/ban-types + media: {}; +} + +export interface MediaSelectorValue { + entity_id?: string; + media_content_id?: string; + media_content_type?: string; + metadata?: { + title?: string; + thumbnail?: string | null; + media_class?: string; + children_media_class?: string | null; + navigateIds?: { media_content_type: string; media_content_id: string }[]; }; } @@ -119,28 +156,22 @@ export interface NumberSelector { }; } -export interface ColorTempSelector { - color_temp: { - min_mireds?: number; - max_mireds?: number; +export interface ObjectSelector { + // eslint-disable-next-line @typescript-eslint/ban-types + object: {}; +} + +export interface SelectOption { + value: string; + label: string; +} + +export interface SelectSelector { + select: { + options: string[] | SelectOption[]; }; } -export interface BooleanSelector { - // eslint-disable-next-line @typescript-eslint/ban-types - boolean: {}; -} - -export interface TimeSelector { - // eslint-disable-next-line @typescript-eslint/ban-types - time: {}; -} - -export interface ActionSelector { - // eslint-disable-next-line @typescript-eslint/ban-types - action: {}; -} - export interface StringSelector { text: { multiline?: boolean; @@ -162,58 +193,25 @@ export interface StringSelector { }; } -export interface ObjectSelector { - // eslint-disable-next-line @typescript-eslint/ban-types - object: {}; -} - -export interface SelectOption { - value: string; - label: string; -} - -export interface SelectSelector { - select: { - options: string[] | SelectOption[]; +export interface TargetSelector { + target: { + entity?: { + integration?: EntitySelector["entity"]["integration"]; + domain?: EntitySelector["entity"]["domain"]; + device_class?: EntitySelector["entity"]["device_class"]; + }; + device?: { + integration?: DeviceSelector["device"]["integration"]; + manufacturer?: DeviceSelector["device"]["manufacturer"]; + model?: DeviceSelector["device"]["model"]; + }; }; } - -export interface IconSelector { - icon: { - placeholder?: string; - fallbackPath?: string; - }; -} - export interface ThemeSelector { // eslint-disable-next-line @typescript-eslint/ban-types theme: {}; } - -export interface MediaSelector { +export interface TimeSelector { // eslint-disable-next-line @typescript-eslint/ban-types - media: {}; -} - -export interface LocationSelector { - location: { radius?: boolean; icon?: string }; -} - -export interface LocationSelectorValue { - latitude: number; - longitude: number; - radius?: number; -} - -export interface MediaSelectorValue { - entity_id?: string; - media_content_id?: string; - media_content_type?: string; - metadata?: { - title?: string; - thumbnail?: string | null; - media_class?: string; - children_media_class?: string | null; - navigateIds?: { media_content_type: string; media_content_id: string }[]; - }; + time: {}; } From 859f49f3ebfb3250c74ff4c8b147a4d2e6f8d234 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Thu, 24 Mar 2022 08:47:07 -0500 Subject: [PATCH 108/142] Update type for backend (#12122) --- src/components/ha-selector/ha-selector-entity.ts | 8 ++++---- src/data/selector.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/ha-selector/ha-selector-entity.ts b/src/components/ha-selector/ha-selector-entity.ts index 8bf085f79f..566560c7a9 100644 --- a/src/components/ha-selector/ha-selector-entity.ts +++ b/src/components/ha-selector/ha-selector-entity.ts @@ -29,8 +29,8 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) { .hass=${this.hass} .value=${this.value} .label=${this.label} - .includeEntities=${this.selector.entity.includeEntities} - .excludeEntities=${this.selector.entity.excludeEntities} + .includeEntities=${this.selector.entity.include_entities} + .excludeEntities=${this.selector.entity.exclude_entities} .entityFilter=${this._filterEntities} .disabled=${this.disabled} allow-custom-entity @@ -43,8 +43,8 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) { .hass=${this.hass} .value=${this.value} .entityFilter=${this._filterEntities} - .includeEntities=${this.selector.entity.includeEntities} - .excludeEntities=${this.selector.entity.excludeEntities} + .includeEntities=${this.selector.entity.include_entities} + .excludeEntities=${this.selector.entity.exclude_entities} > `; } diff --git a/src/data/selector.ts b/src/data/selector.ts index 20132ec8b1..427697d3cf 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -106,8 +106,8 @@ export interface EntitySelector { domain?: string | string[]; device_class?: string; multiple?: boolean; - includeEntities?: string[]; - excludeEntities?: string[]; + include_entities?: string[]; + exclude_entities?: string[]; }; } From 27ca61ec85a2bfe11c5d1bc8ea60d9d94a685813 Mon Sep 17 00:00:00 2001 From: Brynley McDonald Date: Fri, 25 Mar 2022 04:31:55 +1300 Subject: [PATCH 109/142] Fix issue where theme select does not appear when user's theme is deleted (#12104) --- src/panels/profile/ha-pick-theme-row.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/panels/profile/ha-pick-theme-row.ts b/src/panels/profile/ha-pick-theme-row.ts index b0147b6842..59d9774712 100644 --- a/src/panels/profile/ha-pick-theme-row.ts +++ b/src/panels/profile/ha-pick-theme-row.ts @@ -173,6 +173,9 @@ export class HaPickThemeRow extends LitElement { } private _supportsModeSelection(themeName: string): boolean { + if (!(themeName in this.hass.themes.themes)) { + return false; // User's theme no longer exists + } return "modes" in this.hass.themes.themes[themeName]; } From a58b4fb262c189d68ce0d244ff8d58ac59d67efe Mon Sep 17 00:00:00 2001 From: Pawel Date: Thu, 24 Mar 2022 20:10:49 +0100 Subject: [PATCH 110/142] Fix possibility to enable entity disabled by integration (#12121) Co-authored-by: Zack Barett --- .../entities/entity-registry-settings.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index 34d099db0d..792c2f147a 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -264,7 +264,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { )}:
- ${this._disabledBy && this._disabledBy !== "user" + ${this._disabledBy && + this._disabledBy !== "user" && + this._disabledBy !== "integration" ? this.hass.localize( "ui.dialogs.entity_registry.editor.enabled_cause", "cause", @@ -286,7 +288,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { .checked=${!this._hiddenBy && !this._disabledBy} .disabled=${(this._hiddenBy && this._hiddenBy !== "user") || this._device?.disabled_by || - (this._disabledBy && this._disabledBy !== "user")} + (this._disabledBy && + this._disabledBy !== "user" && + this._disabledBy !== "integration")} @change=${this._viewStatusChanged} > @@ -301,7 +305,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { .checked=${this._hiddenBy !== null} .disabled=${(this._hiddenBy && this._hiddenBy !== "user") || Boolean(this._device?.disabled_by) || - (this._disabledBy && this._disabledBy !== "user")} + (this._disabledBy && + this._disabledBy !== "user" && + this._disabledBy !== "integration")} @change=${this._viewStatusChanged} > @@ -316,7 +322,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { .checked=${this._disabledBy !== null} .disabled=${(this._hiddenBy && this._hiddenBy !== "user") || Boolean(this._device?.disabled_by) || - (this._disabledBy && this._disabledBy !== "user")} + (this._disabledBy && + this._disabledBy !== "user" && + this._disabledBy !== "integration")} @change=${this._viewStatusChanged} > @@ -378,7 +386,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { class="warning" @click=${this._confirmDeleteEntry} .disabled=${this._submitting || - (!this._helperConfigEntry && !stateObj.attributes.restored)} + (!this._helperConfigEntry && !stateObj?.attributes.restored)} > ${this.hass.localize("ui.dialogs.entity_registry.editor.delete")} From 224df896a153e6d8c27787522c9d0f8ac0656a8c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 Mar 2022 17:50:38 -0700 Subject: [PATCH 111/142] Allow rendering helper text from strings.json (#12119) * Allow rendering helper text from strings.json * Persistent helpers * Update src/components/ha-base-time-input.ts Co-authored-by: Zack Barett * Update src/components/ha-base-time-input.ts Co-authored-by: Zack Barett --- src/components/ha-base-time-input.ts | 13 +++++++++++++ src/components/ha-duration-input.ts | 3 +++ src/components/ha-form/ha-form-string.ts | 4 ++++ src/components/ha-selector/ha-selector-duration.ts | 3 +++ src/components/ha-selector/ha-selector-number.ts | 4 ++++ src/components/ha-selector/ha-selector-text.ts | 6 ++++++ src/dialogs/config-flow/dialog-data-entry-flow.ts | 2 -- src/dialogs/config-flow/show-dialog-config-flow.ts | 6 ++++++ .../config-flow/show-dialog-data-entry-flow.ts | 6 ++++++ src/dialogs/config-flow/show-dialog-options-flow.ts | 6 ++++++ src/dialogs/config-flow/step-flow-form.ts | 4 ++++ 11 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/components/ha-base-time-input.ts b/src/components/ha-base-time-input.ts index 64b12f18e2..4c1f1d7ff4 100644 --- a/src/components/ha-base-time-input.ts +++ b/src/components/ha-base-time-input.ts @@ -21,6 +21,11 @@ export class HaBaseTimeInput extends LitElement { */ @property() label?: string; + /** + * Helper for the input + */ + @property() helper?: string; + /** * auto validate time inputs */ @@ -207,6 +212,7 @@ export class HaBaseTimeInput extends LitElement { PM `}
+ ${this.helper ? html`
${this.helper}
` : ""} `; } @@ -303,6 +309,13 @@ export class HaBaseTimeInput extends LitElement { color: var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, 0.87)); padding-left: 4px; } + + .helper { + color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6)); + font-size: 0.75rem; + padding-left: 16px; + padding-right: 16px; + } `; } diff --git a/src/components/ha-duration-input.ts b/src/components/ha-duration-input.ts index 9ac4e72e2f..b862059b35 100644 --- a/src/components/ha-duration-input.ts +++ b/src/components/ha-duration-input.ts @@ -17,6 +17,8 @@ class HaDurationInput extends LitElement { @property() public label?: string; + @property() public helper?: string; + @property({ type: Boolean }) public required?: boolean; @property({ type: Boolean }) public enableMillisecond?: boolean; @@ -35,6 +37,7 @@ class HaDurationInput extends LitElement { return html`
@@ -166,6 +167,9 @@ class StepFlowForm extends LitElement { private _labelCallback = (field: HaFormSchema): string => this.flowConfig.renderShowFormStepFieldLabel(this.hass, this.step, field); + private _helperCallback = (field: HaFormSchema): string => + this.flowConfig.renderShowFormStepFieldHelper(this.hass, this.step, field); + private _errorCallback = (error: string) => this.flowConfig.renderShowFormStepFieldError(this.hass, this.step, error); From dd963be7230f6abd4ccd8a35c8c9460785a3ab42 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Thu, 24 Mar 2022 19:57:20 -0500 Subject: [PATCH 112/142] Add Day to duration selector (#12125) --- src/components/ha-base-time-input.ts | 47 +++++++++++++++++-- src/components/ha-duration-input.ts | 15 ++++++ .../ha-selector/ha-selector-duration.ts | 1 + src/data/selector.ts | 5 +- 4 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/components/ha-base-time-input.ts b/src/components/ha-base-time-input.ts index 4c1f1d7ff4..9b102faee5 100644 --- a/src/components/ha-base-time-input.ts +++ b/src/components/ha-base-time-input.ts @@ -1,12 +1,13 @@ -import { LitElement, html, TemplateResult, css } from "lit"; -import { customElement, property } from "lit/decorators"; -import "./ha-select"; import "@material/mwc-list/mwc-list-item"; -import "./ha-textfield"; +import { css, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; import { stopPropagation } from "../common/dom/stop_propagation"; +import "./ha-select"; +import "./ha-textfield"; export interface TimeChangedEvent { + days?: number; hours: number; minutes: number; seconds: number; @@ -46,6 +47,11 @@ export class HaBaseTimeInput extends LitElement { */ @property({ type: Boolean }) disabled = false; + /** + * day + */ + @property({ type: Number }) days = 0; + /** * hour */ @@ -66,6 +72,11 @@ export class HaBaseTimeInput extends LitElement { */ @property({ type: Number }) milliseconds = 0; + /** + * Label for the day input + */ + @property() dayLabel = ""; + /** * Label for the hour input */ @@ -96,6 +107,11 @@ export class HaBaseTimeInput extends LitElement { */ @property({ type: Boolean }) enableMillisecond = false; + /** + * show the day field + */ + @property({ type: Boolean }) enableDay = false; + /** * limit hours input */ @@ -115,6 +131,29 @@ export class HaBaseTimeInput extends LitElement { return html` ${this.label ? html`` : ""}
+ ${this.enableDay + ? html` + + + ` + : ""} + 24) { + value.days = (value.days ?? 0) + Math.floor(value.hours / 24); + value.hours %= 24; + } + fireEvent(this, "value-changed", { value, }); diff --git a/src/components/ha-selector/ha-selector-duration.ts b/src/components/ha-selector/ha-selector-duration.ts index 73266aaed5..61bde6fa21 100644 --- a/src/components/ha-selector/ha-selector-duration.ts +++ b/src/components/ha-selector/ha-selector-duration.ts @@ -28,6 +28,7 @@ export class HaTimeDuration extends LitElement { .data=${this.value} .disabled=${this.disabled} .required=${this.required} + .enableDay=${this.selector.duration.enable_day} > `; } diff --git a/src/data/selector.ts b/src/data/selector.ts index 427697d3cf..ae985f1343 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -96,8 +96,9 @@ export interface DeviceSelector { } export interface DurationSelector { - // eslint-disable-next-line @typescript-eslint/ban-types - duration: {}; + duration: { + enable_day?: boolean; + }; } export interface EntitySelector { From b3fa0cccb452233683c3f957064ebd1b4f9ba497 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Mar 2022 20:33:22 -0700 Subject: [PATCH 113/142] Add variables to automation trigger type --- src/data/automation.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/data/automation.ts b/src/data/automation.ts index f85a387210..87b0f9c828 100644 --- a/src/data/automation.ts +++ b/src/data/automation.ts @@ -62,6 +62,7 @@ export interface ContextConstraint { export interface BaseTrigger { platform: string; id?: string; + variables?: Record; } export interface StateTrigger extends BaseTrigger { From 637e4203e5f8dff218b239139002dcb998820f12 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 28 Mar 2022 15:14:24 +0200 Subject: [PATCH 114/142] Fix z-index map, always set icon for location selector (#12137) --- src/components/ha-selector/ha-selector-location.ts | 5 ++++- src/components/map/ha-map.ts | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/ha-selector/ha-selector-location.ts b/src/components/ha-selector/ha-selector-location.ts index 18372f525d..26831a793d 100644 --- a/src/components/ha-selector/ha-selector-location.ts +++ b/src/components/ha-selector/ha-selector-location.ts @@ -52,7 +52,10 @@ export class HaLocationSelector extends LitElement { longitude: value?.longitude || this.hass.config.longitude, radius: selector.location.radius ? value?.radius || 1000 : undefined, radius_color: zoneRadiusColor, - icon: selector.location.icon, + icon: + selector.location.icon || selector.location.radius + ? "mdi:map-marker-radius" + : "mdi:map-marker", location_editable: true, radius_editable: true, }, diff --git a/src/components/map/ha-map.ts b/src/components/map/ha-map.ts index 9e56fa7379..f422f053d6 100644 --- a/src/components/map/ha-map.ts +++ b/src/components/map/ha-map.ts @@ -488,6 +488,14 @@ export class HaMap extends ReactiveElement { text-align: center; color: var(--primary-text-color); } + .leaflet-pane { + z-index: 0 !important; + } + .leaflet-control, + .leaflet-top, + .leaflet-bottom { + z-index: 1 !important; + } `; } } From d30e8ee9d843de6526f0aff35568bcd71e4f0077 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 28 Mar 2022 15:50:07 +0200 Subject: [PATCH 115/142] Make padding on settings row content consistent (#12139) --- src/components/ha-service-control.ts | 4 +--- src/components/ha-settings-row.ts | 14 +++++++++++++- .../automation/blueprint-automation-editor.ts | 5 +---- src/panels/lovelace/components/hui-card-options.ts | 2 +- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/components/ha-service-control.ts b/src/components/ha-service-control.ts index ee1692e567..438adb66d4 100644 --- a/src/components/ha-service-control.ts +++ b/src/components/ha-service-control.ts @@ -471,6 +471,7 @@ export class HaServiceControl extends LitElement { } ha-settings-row { --paper-time-input-justify-content: flex-end; + --settings-row-content-width: 100%; border-top: var( --service-control-items-border-top, 1px solid var(--divider-color) @@ -489,9 +490,6 @@ export class HaServiceControl extends LitElement { margin: var(--service-control-padding, 0 16px); padding: 16px 0; } - :host(:not([narrow])) ha-settings-row ha-selector { - width: 60%; - } .checkbox-spacer { width: 32px; } diff --git a/src/components/ha-settings-row.ts b/src/components/ha-settings-row.ts index a28738b265..252bb80dc8 100644 --- a/src/components/ha-settings-row.ts +++ b/src/components/ha-settings-row.ts @@ -21,7 +21,7 @@ export class HaSettingsRow extends LitElement {
- +
`; } @@ -43,6 +43,18 @@ export class HaSettingsRow extends LitElement { ); flex: 1; } + .content { + display: contents; + } + :host(:not([narrow])) .content { + display: flex; + justify-content: flex-end; + flex: 1; + padding: 16px 0; + } + .content ::slotted(*) { + width: var(--settings-row-content-width); + } :host([narrow]) { align-items: normal; flex-direction: column; diff --git a/src/panels/config/automation/blueprint-automation-editor.ts b/src/panels/config/automation/blueprint-automation-editor.ts index 907ee3bb03..37b08264de 100644 --- a/src/panels/config/automation/blueprint-automation-editor.ts +++ b/src/panels/config/automation/blueprint-automation-editor.ts @@ -326,12 +326,9 @@ export class HaBlueprintAutomationEditor extends LitElement { } ha-settings-row { --paper-time-input-justify-content: flex-end; + --settings-row-content-width: 100%; border-top: 1px solid var(--divider-color); } - :host(:not([narrow])) ha-settings-row ha-textfield, - :host(:not([narrow])) ha-settings-row ha-selector { - width: 60%; - } `, ]; } diff --git a/src/panels/lovelace/components/hui-card-options.ts b/src/panels/lovelace/components/hui-card-options.ts index aef797e278..7e34b16c70 100644 --- a/src/panels/lovelace/components/hui-card-options.ts +++ b/src/panels/lovelace/components/hui-card-options.ts @@ -116,7 +116,7 @@ export class HuiCardOptions extends LitElement { outline: 2px solid var(--primary-color); } - :host:not(.panel) ::slotted(*) { + :host(:not(.panel)) ::slotted(*) { display: block; } From b080bca9ce823021b7eac5c8268f4de8994c2348 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Mon, 28 Mar 2022 09:07:00 -0500 Subject: [PATCH 116/142] Add Area Multiple Selector option (#12138) --- gallery/src/pages/components/ha-selector.ts | 19 ++- src/components/ha-areas-picker.ts | 160 ++++++++++++++++++ .../ha-selector/ha-selector-area.ts | 53 ++++-- src/data/selector.ts | 1 + 4 files changed, 209 insertions(+), 24 deletions(-) create mode 100644 src/components/ha-areas-picker.ts diff --git a/gallery/src/pages/components/ha-selector.ts b/gallery/src/pages/components/ha-selector.ts index 3a456cae66..af52d5c640 100644 --- a/gallery/src/pages/components/ha-selector.ts +++ b/gallery/src/pages/components/ha-selector.ts @@ -1,20 +1,20 @@ /* eslint-disable lit/no-template-arrow */ import "@material/mwc-button"; -import { LitElement, TemplateResult, css, html } from "lit"; +import { css, html, LitElement, TemplateResult } from "lit"; import { customElement, state } from "lit/decorators"; +import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry"; +import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry"; +import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry"; +import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor"; import "../../../../src/components/ha-selector/ha-selector"; import "../../../../src/components/ha-settings-row"; +import { BlueprintInput } from "../../../../src/data/blueprint"; +import { showDialog } from "../../../../src/dialogs/make-dialog-manager"; +import { getEntity } from "../../../../src/fake_data/entity"; import { provideHass } from "../../../../src/fake_data/provide_hass"; +import { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin"; import type { HomeAssistant } from "../../../../src/types"; import "../../components/demo-black-white-row"; -import { BlueprintInput } from "../../../../src/data/blueprint"; -import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry"; -import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry"; -import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry"; -import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor"; -import { getEntity } from "../../../../src/fake_data/entity"; -import { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin"; -import { showDialog } from "../../../../src/dialogs/make-dialog-manager"; const ENTITIES = [ getEntity("alarm_control_panel", "alarm", "disarmed", { @@ -202,6 +202,7 @@ const SCHEMAS: { input: { entity: { name: "Entity", selector: { entity: { multiple: true } } }, device: { name: "Device", selector: { device: { multiple: true } } }, + area: { name: "Area", selector: { area: { multiple: true } } }, }, }, ]; diff --git a/src/components/ha-areas-picker.ts b/src/components/ha-areas-picker.ts new file mode 100644 index 0000000000..fbf4a9287f --- /dev/null +++ b/src/components/ha-areas-picker.ts @@ -0,0 +1,160 @@ +import { css, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../common/dom/fire_event"; +import type { EntityRegistryEntry } from "../data/entity_registry"; +import { SubscribeMixin } from "../mixins/subscribe-mixin"; +import type { HomeAssistant } from "../types"; +import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; +import "./ha-area-picker"; + +@customElement("ha-areas-picker") +export class HaAreasPicker extends SubscribeMixin(LitElement) { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public label?: string; + + @property() public value?: string[]; + + @property() public placeholder?: string; + + @property({ type: Boolean, attribute: "no-add" }) + public noAdd?: boolean; + + /** + * Show only areas with entities from specific domains. + * @type {Array} + * @attr include-domains + */ + @property({ type: Array, attribute: "include-domains" }) + public includeDomains?: string[]; + + /** + * Show no areas with entities of these domains. + * @type {Array} + * @attr exclude-domains + */ + @property({ type: Array, attribute: "exclude-domains" }) + public excludeDomains?: string[]; + + /** + * Show only areas with entities of these device classes. + * @type {Array} + * @attr include-device-classes + */ + @property({ type: Array, attribute: "include-device-classes" }) + public includeDeviceClasses?: string[]; + + @property() public deviceFilter?: HaDevicePickerDeviceFilterFunc; + + @property() public entityFilter?: (entity: EntityRegistryEntry) => boolean; + + @property({ attribute: "picked-area-label" }) + public pickedAreaLabel?: string; + + @property({ attribute: "pick-area-label" }) + public pickAreaLabel?: string; + + @property({ type: Boolean }) public disabled?: boolean; + + protected render(): TemplateResult { + if (!this.hass) { + return html``; + } + + const currentAreas = this._currentAreas; + return html` + ${currentAreas.map( + (area) => html` +
+ +
+ ` + )} +
+ +
+ `; + } + + private get _currentAreas(): string[] { + return this.value || []; + } + + private async _updateAreas(areas) { + this.value = areas; + + fireEvent(this, "value-changed", { + value: areas, + }); + } + + private _areaChanged(ev: CustomEvent) { + ev.stopPropagation(); + const curValue = (ev.currentTarget as any).curValue; + const newValue = ev.detail.value; + if (newValue === curValue) { + return; + } + const currentAreas = this._currentAreas; + if (!newValue || currentAreas.includes(newValue)) { + this._updateAreas(currentAreas.filter((ent) => ent !== curValue)); + return; + } + this._updateAreas( + currentAreas.map((ent) => (ent === curValue ? newValue : ent)) + ); + } + + private _addArea(ev: CustomEvent) { + ev.stopPropagation(); + + const toAdd = ev.detail.value; + if (!toAdd) { + return; + } + (ev.currentTarget as any).value = ""; + const currentAreas = this._currentAreas; + if (currentAreas.includes(toAdd)) { + return; + } + + this._updateAreas([...currentAreas, toAdd]); + } + + static override styles = css` + div { + margin-top: 8px; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-areas-picker": HaAreasPicker; + } +} diff --git a/src/components/ha-selector/ha-selector-area.ts b/src/components/ha-selector/ha-selector-area.ts index 704c79282a..e6926c9701 100644 --- a/src/components/ha-selector/ha-selector-area.ts +++ b/src/components/ha-selector/ha-selector-area.ts @@ -6,6 +6,7 @@ import { EntityRegistryEntry } from "../../data/entity_registry"; import { AreaSelector } from "../../data/selector"; import { HomeAssistant } from "../../types"; import "../ha-area-picker"; +import "../ha-areas-picker"; @customElement("ha-selector-area") export class HaAreaSelector extends LitElement { @@ -38,21 +39,43 @@ export class HaAreaSelector extends LitElement { } protected render() { - return html``; + if (!this.selector.area.multiple) { + return html` + + `; + } + + return html` + + `; } private _filterEntities = (entity: EntityRegistryEntry): boolean => { diff --git a/src/data/selector.ts b/src/data/selector.ts index ae985f1343..076fa78aa3 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -46,6 +46,7 @@ export interface AreaSelector { manufacturer?: DeviceSelector["device"]["manufacturer"]; model?: DeviceSelector["device"]["model"]; }; + multiple?: boolean; }; } From a44b8981e1f53906fba8504e1acb5738b9c35b05 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 28 Mar 2022 17:21:16 +0200 Subject: [PATCH 117/142] break theme picker out of lovelace (#12140) --- .../ha-selector/ha-selector-location.ts | 1 - .../ha-selector/ha-selector-theme.ts | 6 ++--- .../ha-theme-picker.ts} | 22 ++++++++---------- .../config-elements/config-elements-style.ts | 1 - .../hui-alarm-panel-card-editor.ts | 8 +++++++ .../config-elements/hui-area-card-editor.ts | 6 +++++ .../config-elements/hui-button-card-editor.ts | 8 +++++++ .../hui-calendar-card-editor.ts | 8 +++++++ .../hui-entities-card-editor.ts | 11 ++++++--- .../config-elements/hui-entity-card-editor.ts | 8 +++++++ .../config-elements/hui-gauge-card-editor.ts | 6 +++++ .../config-elements/hui-glance-card-editor.ts | 22 ++++++++++++++---- .../hui-humidifier-card-editor.ts | 8 +++++++ .../config-elements/hui-light-card-editor.ts | 8 +++++++ .../hui-logbook-card-editor.ts | 23 +++++++++++++++---- .../hui-markdown-card-editor.ts | 22 +++++++++++++----- .../hui-media-control-card-editor.ts | 11 ++++++--- .../hui-picture-card-editor.ts | 11 ++++++--- .../hui-picture-entity-card-editor.ts | 8 +++++++ .../hui-picture-glance-card-editor.ts | 8 +++++++ .../hui-plant-status-card-editor.ts | 8 ++++++- .../config-elements/hui-sensor-card-editor.ts | 8 +++++++ .../hui-shopping-list-editor.ts | 11 ++++++--- .../hui-thermostat-card-editor.ts | 8 +++++++ .../hui-weather-forecast-card-editor.ts | 8 +++++++ src/translations/en.json | 5 +++- 26 files changed, 206 insertions(+), 48 deletions(-) rename src/{panels/lovelace/components/hui-theme-select-editor.ts => components/ha-theme-picker.ts} (68%) diff --git a/src/components/ha-selector/ha-selector-location.ts b/src/components/ha-selector/ha-selector-location.ts index 26831a793d..a7e030122e 100644 --- a/src/components/ha-selector/ha-selector-location.ts +++ b/src/components/ha-selector/ha-selector-location.ts @@ -6,7 +6,6 @@ import type { LocationSelector, LocationSelectorValue, } from "../../data/selector"; -import "../../panels/lovelace/components/hui-theme-select-editor"; import type { HomeAssistant } from "../../types"; import type { MarkerLocation } from "../map/ha-locations-editor"; import "../map/ha-locations-editor"; diff --git a/src/components/ha-selector/ha-selector-theme.ts b/src/components/ha-selector/ha-selector-theme.ts index d25539908f..f90362b552 100644 --- a/src/components/ha-selector/ha-selector-theme.ts +++ b/src/components/ha-selector/ha-selector-theme.ts @@ -1,4 +1,4 @@ -import "../../panels/lovelace/components/hui-theme-select-editor"; +import "../ha-theme-picker"; import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import type { HomeAssistant } from "../../types"; @@ -18,11 +18,11 @@ export class HaThemeSelector extends LitElement { protected render() { return html` - + > `; } } diff --git a/src/panels/lovelace/components/hui-theme-select-editor.ts b/src/components/ha-theme-picker.ts similarity index 68% rename from src/panels/lovelace/components/hui-theme-select-editor.ts rename to src/components/ha-theme-picker.ts index 659b5af4e9..093865d3a7 100644 --- a/src/panels/lovelace/components/hui-theme-select-editor.ts +++ b/src/components/ha-theme-picker.ts @@ -2,13 +2,13 @@ import "@material/mwc-button"; import "@material/mwc-list/mwc-list-item"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; -import { fireEvent } from "../../../common/dom/fire_event"; -import { stopPropagation } from "../../../common/dom/stop_propagation"; -import "../../../components/ha-select"; -import { HomeAssistant } from "../../../types"; +import { fireEvent } from "../common/dom/fire_event"; +import { stopPropagation } from "../common/dom/stop_propagation"; +import "./ha-select"; +import { HomeAssistant } from "../types"; -@customElement("hui-theme-select-editor") -export class HuiThemeSelectEditor extends LitElement { +@customElement("ha-theme-picker") +export class HaThemePicker extends LitElement { @property() public value?: string; @property() public label?: string; @@ -19,11 +19,7 @@ export class HuiThemeSelectEditor extends LitElement { return html` ${this.hass!.localize( - "ui.panel.lovelace.editor.card.generic.no_theme" + "ui.components.theme_picker.no_theme" )} ${Object.keys(this.hass!.themes.themes) @@ -64,6 +60,6 @@ export class HuiThemeSelectEditor extends LitElement { declare global { interface HTMLElementTagNameMap { - "hui-theme-select-editor": HuiThemeSelectEditor; + "ha-theme-picker": HaThemePicker; } } diff --git a/src/panels/lovelace/editor/config-elements/config-elements-style.ts b/src/panels/lovelace/editor/config-elements/config-elements-style.ts index 2931974935..2f068a1f91 100644 --- a/src/panels/lovelace/editor/config-elements/config-elements-style.ts +++ b/src/panels/lovelace/editor/config-elements/config-elements-style.ts @@ -23,7 +23,6 @@ export const configElementStyle = css` .suffix { margin: 0 8px; } - hui-theme-select-editor, hui-action-editor, ha-select, ha-textfield, diff --git a/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts index ae8f79bd27..96f98fbbc2 100644 --- a/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts @@ -99,6 +99,14 @@ export class HuiAlarmPanelCardEditor return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.name`); } + if (schema.name === "theme") { + return `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.theme" + )} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.optional" + )})`; + } + return this.hass!.localize( `ui.panel.lovelace.editor.card.alarm-panel.${ schema.name === "states" ? "available_states" : schema.name diff --git a/src/panels/lovelace/editor/config-elements/hui-area-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-area-card-editor.ts index 447c1ccd2e..ed4114bda5 100644 --- a/src/panels/lovelace/editor/config-elements/hui-area-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-area-card-editor.ts @@ -69,6 +69,12 @@ export class HuiAreaCardEditor private _computeLabelCallback = (schema: HaFormSchema) => { switch (schema.name) { + case "theme": + return `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.theme" + )} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.optional" + )})`; case "area": return this.hass!.localize("ui.panel.lovelace.editor.card.area.name"); case "navigation_path": diff --git a/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts index 2445a1e045..90bc7e51d3 100644 --- a/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts @@ -200,6 +200,14 @@ export class HuiButtonCardEditor )}`; } + if (schema.name === "theme") { + return `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.theme" + )} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.optional" + )})`; + } + return this.hass!.localize( `ui.panel.lovelace.editor.card.generic.${schema.name}` ); diff --git a/src/panels/lovelace/editor/config-elements/hui-calendar-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-calendar-card-editor.ts index 30c81af52c..b1e6baa3e5 100644 --- a/src/panels/lovelace/editor/config-elements/hui-calendar-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-calendar-card-editor.ts @@ -121,6 +121,14 @@ export class HuiCalendarCardEditor return this.hass!.localize("ui.panel.lovelace.editor.card.generic.title"); } + if (schema.name === "theme") { + return `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.theme" + )} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.optional" + )})`; + } + return this.hass!.localize( `ui.panel.lovelace.editor.card.calendar.${schema.name}` ); diff --git a/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts index b0c418718e..536eede67c 100644 --- a/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts @@ -31,7 +31,7 @@ import "../../../../components/ha-icon"; import "../../../../components/ha-switch"; import type { HomeAssistant } from "../../../../types"; import type { EntitiesCardConfig } from "../../cards/types"; -import "../../components/hui-theme-select-editor"; +import "../../../../components/ha-theme-picker"; import { TIMESTAMP_RENDERING_FORMATS } from "../../components/types"; import type { LovelaceRowConfig } from "../../entity-rows/types"; import { headerFooterConfigStructs } from "../../header-footer/structs"; @@ -265,12 +265,17 @@ export class HuiEntitiesCardEditor .configValue=${"title"} @input=${this._valueChanged} > - + >
- this.hass!.localize( - `ui.panel.lovelace.editor.card.glance.${schema.name}` - ) || - this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); + private _computeLabelCallback = (schema: HaFormSchema) => { + if (schema.name === "theme") { + return `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.theme" + )} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.optional" + )})`; + } + return ( + this.hass!.localize( + `ui.panel.lovelace.editor.card.glance.${schema.name}` + ) || + this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ) + ); + }; } declare global { diff --git a/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts index 7f472d39a1..b5e83f7c81 100644 --- a/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts @@ -75,6 +75,14 @@ export class HuiHumidifierCardEditor ); } + if (schema.name === "theme") { + return `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.theme" + )} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.optional" + )})`; + } + return this.hass!.localize( `ui.panel.lovelace.editor.card.generic.${schema.name}` ); diff --git a/src/panels/lovelace/editor/config-elements/hui-light-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-light-card-editor.ts index 8995c7a6ff..42e47ef81a 100644 --- a/src/panels/lovelace/editor/config-elements/hui-light-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-light-card-editor.ts @@ -179,6 +179,14 @@ export class HuiLightCardEditor ); } + if (schema.name === "theme") { + return `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.theme" + )} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.optional" + )})`; + } + return this.hass!.localize( `ui.panel.lovelace.editor.card.generic.${schema.name}` ); diff --git a/src/panels/lovelace/editor/config-elements/hui-logbook-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-logbook-card-editor.ts index 427535e412..f789c09bbe 100644 --- a/src/panels/lovelace/editor/config-elements/hui-logbook-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-logbook-card-editor.ts @@ -96,11 +96,24 @@ export class HuiLogbookCardEditor fireEvent(this, "config-changed", { config: ev.detail.value }); } - private _computeLabelCallback = (schema: HaFormSchema) => - this.hass!.localize( - `ui.panel.lovelace.editor.card.generic.${schema.name}` - ) || - this.hass!.localize(`ui.panel.lovelace.editor.card.logbook.${schema.name}`); + private _computeLabelCallback = (schema: HaFormSchema) => { + if (schema.name === "theme") { + return `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.theme" + )} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.optional" + )})`; + } + + return ( + this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ) || + this.hass!.localize( + `ui.panel.lovelace.editor.card.logbook.${schema.name}` + ) + ); + }; } declare global { diff --git a/src/panels/lovelace/editor/config-elements/hui-markdown-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-markdown-card-editor.ts index fe1b3360c4..54dc74bcd7 100644 --- a/src/panels/lovelace/editor/config-elements/hui-markdown-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-markdown-card-editor.ts @@ -58,13 +58,23 @@ export class HuiMarkdownCardEditor fireEvent(this, "config-changed", { config: ev.detail.value }); } - private _computeLabelCallback = (schema: HaFormSchema) => - this.hass!.localize( - `ui.panel.lovelace.editor.card.generic.${schema.name}` - ) || - this.hass!.localize( - `ui.panel.lovelace.editor.card.markdown.${schema.name}` + private _computeLabelCallback = (schema: HaFormSchema) => { + if (schema.name === "theme") { + return `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.theme" + )} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.optional" + )})`; + } + return ( + this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ) || + this.hass!.localize( + `ui.panel.lovelace.editor.card.markdown.${schema.name}` + ) ); + }; } declare global { diff --git a/src/panels/lovelace/editor/config-elements/hui-media-control-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-media-control-card-editor.ts index 012dac5a35..9732c5b7a4 100644 --- a/src/panels/lovelace/editor/config-elements/hui-media-control-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-media-control-card-editor.ts @@ -5,7 +5,7 @@ import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/entity/ha-entity-picker"; import { HomeAssistant } from "../../../../types"; import { MediaControlCardConfig } from "../../cards/types"; -import "../../components/hui-theme-select-editor"; +import "../../../../components/ha-theme-picker"; import { LovelaceCardEditor } from "../../types"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { EditorTarget, EntitiesEditorEvent } from "../types"; @@ -62,12 +62,17 @@ export class HuiMediaControlCardEditor @change=${this._valueChanged} allow-custom-entity > - + >
`; } diff --git a/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts index a5e156c42d..8174a049e2 100644 --- a/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts @@ -6,7 +6,7 @@ import { ActionConfig } from "../../../../data/lovelace"; import { HomeAssistant } from "../../../../types"; import { PictureCardConfig } from "../../cards/types"; import "../../components/hui-action-editor"; -import "../../components/hui-theme-select-editor"; +import "../../../../components/ha-theme-picker"; import { LovelaceCardEditor } from "../../types"; import { actionConfigStruct } from "../structs/action-struct"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; @@ -72,12 +72,17 @@ export class HuiPictureCardEditor .configValue=${"image"} @input=${this._valueChanged} > - + > - + >
`; } diff --git a/src/panels/lovelace/editor/config-elements/hui-thermostat-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-thermostat-card-editor.ts index ed4ab2a61b..78d6dc3a97 100644 --- a/src/panels/lovelace/editor/config-elements/hui-thermostat-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-thermostat-card-editor.ts @@ -71,6 +71,14 @@ export class HuiThermostatCardEditor ); } + if (schema.name === "theme") { + return `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.theme" + )} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.optional" + )})`; + } + return this.hass!.localize( `ui.panel.lovelace.editor.card.generic.${schema.name}` ); diff --git a/src/panels/lovelace/editor/config-elements/hui-weather-forecast-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-weather-forecast-card-editor.ts index cd6c2ab227..70a310b5da 100644 --- a/src/panels/lovelace/editor/config-elements/hui-weather-forecast-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-weather-forecast-card-editor.ts @@ -184,6 +184,14 @@ export class HuiWeatherForecastCardEditor )})`; } + if (schema.name === "theme") { + return `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.theme" + )} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.optional" + )})`; + } + return ( this.hass!.localize( `ui.panel.lovelace.editor.card.generic.${schema.name}` diff --git a/src/translations/en.json b/src/translations/en.json index 3ef90c50bd..4075052bd6 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -401,6 +401,10 @@ "add_device_id": "Choose device", "add_entity_id": "Choose entity" }, + "theme-picker": { + "theme": "Theme", + "no_theme": "No theme" + }, "user-picker": { "no_user": "No user", "add_user": "Add user", @@ -3458,7 +3462,6 @@ "tap_action": "Tap Action", "title": "Title", "theme": "Theme", - "no_theme": "No theme", "unit": "Unit", "url": "URL", "state": "State", From 61f6e8855b1a3dee277bf98cf393012bf2bd9e30 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Mon, 28 Mar 2022 10:44:21 -0500 Subject: [PATCH 118/142] Allow binary sensor device class updates (#12124) --- .../entities/entity-registry-settings.ts | 172 ++++++++++++------ src/translations/en.json | 26 ++- 2 files changed, 137 insertions(+), 61 deletions(-) diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index 792c2f147a..da5b40359f 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -1,6 +1,5 @@ -import "@material/mwc-formfield/mwc-formfield"; -import "../../../components/ha-radio"; import "@material/mwc-button/mwc-button"; +import "@material/mwc-formfield/mwc-formfield"; import "@material/mwc-list/mwc-list-item"; import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { @@ -20,9 +19,15 @@ import "../../../components/ha-alert"; import "../../../components/ha-area-picker"; import "../../../components/ha-expansion-panel"; import "../../../components/ha-icon-picker"; +import "../../../components/ha-radio"; import "../../../components/ha-select"; import "../../../components/ha-switch"; import "../../../components/ha-textfield"; +import { + ConfigEntry, + deleteConfigEntry, + getConfigEntries, +} from "../../../data/config_entries"; import { DeviceRegistryEntry, subscribeDeviceRegistry, @@ -34,6 +39,7 @@ import { removeEntityRegistryEntry, updateEntityRegistryEntry, } from "../../../data/entity_registry"; +import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow"; import { showAlertDialog, showConfirmationDialog, @@ -42,27 +48,39 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail"; -import { - ConfigEntry, - deleteConfigEntry, - getConfigEntries, -} from "../../../data/config_entries"; -import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow"; const OVERRIDE_DEVICE_CLASSES = { cover: [ - "awning", - "blind", - "curtain", - "damper", - "door", - "garage", - "gate", - "shade", - "shutter", - "window", + [ + "awning", + "blind", + "curtain", + "damper", + "door", + "garage", + "gate", + "shade", + "shutter", + "window", + ], + ], + binary_sensor: [ + ["lock"], // Lock + ["window", "door", "garage_door", "opening"], // Door + ["battery", "battery_charging"], // Battery + ["cold", "gas", "heat"], // Climate + ["running", "motion", "moving", "occupancy", "presence", "vibration"], // Presence + ["power", "plug", "light"], // Power + [ + "smoke", + "safety", + "sound", + "problem", + "tamper", + "carbon_monoxide", + "moisture", + ], // Alarm ], - binary_sensor: ["window", "door", "garage_door", "opening"], }; @customElement("entity-registry-settings") @@ -85,8 +103,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { @state() private _hiddenBy!: string | null; - private _deviceLookup?: Record; - @state() private _device?: DeviceRegistryEntry; @state() private _helperConfigEntry?: ConfigEntry; @@ -97,6 +113,10 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { private _origEntityId!: string; + private _deviceLookup?: Record; + + private _deviceClassOptions?: string[][]; + public hassSubscribe(): UnsubscribeFunc[] { return [ subscribeDeviceRegistry(this.hass.connection!, (devices) => { @@ -125,23 +145,41 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { } } - protected updated(changedProperties: PropertyValues) { - super.updated(changedProperties); - if (changedProperties.has("entry")) { - this._error = undefined; - this._name = this.entry.name || ""; - this._icon = this.entry.icon || ""; - this._deviceClass = - this.entry.device_class || this.entry.original_device_class; - this._origEntityId = this.entry.entity_id; - this._areaId = this.entry.area_id; - this._entityId = this.entry.entity_id; - this._disabledBy = this.entry.disabled_by; - this._hiddenBy = this.entry.hidden_by; - this._device = - this.entry.device_id && this._deviceLookup - ? this._deviceLookup[this.entry.device_id] - : undefined; + protected willUpdate(changedProperties: PropertyValues) { + super.willUpdate(changedProperties); + if (!changedProperties.has("entry")) { + return; + } + + this._error = undefined; + this._name = this.entry.name || ""; + this._icon = this.entry.icon || ""; + this._deviceClass = + this.entry.device_class || this.entry.original_device_class; + this._origEntityId = this.entry.entity_id; + this._areaId = this.entry.area_id; + this._entityId = this.entry.entity_id; + this._disabledBy = this.entry.disabled_by; + this._hiddenBy = this.entry.hidden_by; + this._device = + this.entry.device_id && this._deviceLookup + ? this._deviceLookup[this.entry.device_id] + : undefined; + + const domain = computeDomain(this.entry.entity_id); + const deviceClasses: string[][] = OVERRIDE_DEVICE_CLASSES[domain]; + + if (!deviceClasses) { + return; + } + + this._deviceClassOptions = [[], []]; + for (const deviceClass of deviceClasses) { + if (deviceClass.includes(this.entry.original_device_class!)) { + this._deviceClassOptions[0] = deviceClass; + } else { + this._deviceClassOptions[1].push(...deviceClass); + } } } @@ -197,28 +235,39 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { : undefined} .disabled=${this._submitting} > - ${OVERRIDE_DEVICE_CLASSES[domain]?.includes(this._deviceClass) || - (domain === "cover" && this.entry.original_device_class === null) - ? html` - ${OVERRIDE_DEVICE_CLASSES[domain].map( - (deviceClass: string) => html` - - ${this.hass.localize( - `ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}` - )} - - ` - )} - ` + ${this._deviceClassOptions + ? html` + + ${this._deviceClassOptions[0].map( + (deviceClass: string) => html` + + ${this.hass.localize( + `ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}` + )} + + ` + )} +
  • + ${this._deviceClassOptions[1].map( + (deviceClass: string) => html` + + ${this.hass.localize( + `ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}` + )} + + ` + )} +
    + ` : ""} Date: Mon, 28 Mar 2022 12:38:58 -0500 Subject: [PATCH 119/142] Add selector initial values (#12142) --- gallery/src/pages/components/ha-form.ts | 16 +++++---- .../ha-form/compute-initial-ha-form-data.ts | 36 +++++++++++++++++-- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/gallery/src/pages/components/ha-form.ts b/gallery/src/pages/components/ha-form.ts index fb2cec39af..4d8ad42b73 100644 --- a/gallery/src/pages/components/ha-form.ts +++ b/gallery/src/pages/components/ha-form.ts @@ -1,18 +1,18 @@ /* eslint-disable lit/no-template-arrow */ import "@material/mwc-button"; -import { LitElement, TemplateResult, html } from "lit"; +import { html, LitElement, TemplateResult } from "lit"; import { customElement, state } from "lit/decorators"; -import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data"; -import type { HaFormSchema } from "../../../../src/components/ha-form/types"; -import "../../../../src/components/ha-form/ha-form"; -import "../../components/demo-black-white-row"; import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry"; import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry"; import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry"; import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor"; +import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data"; +import "../../../../src/components/ha-form/ha-form"; +import type { HaFormSchema } from "../../../../src/components/ha-form/types"; +import { getEntity } from "../../../../src/fake_data/entity"; import { provideHass } from "../../../../src/fake_data/provide_hass"; import { HomeAssistant } from "../../../../src/types"; -import { getEntity } from "../../../../src/fake_data/entity"; +import "../../components/demo-black-white-row"; const ENTITIES = [ getEntity("alarm_control_panel", "alarm", "disarmed", { @@ -147,7 +147,9 @@ const SCHEMAS: { { name: "target", selector: { target: {} } }, { name: "number", selector: { number: { min: 0, max: 10 } } }, { name: "boolean", selector: { boolean: {} } }, - { name: "time", selector: { time: {} } }, + { name: "time", required: true, selector: { time: {} } }, + { name: "datetime", required: true, selector: { datetime: {} } }, + { name: "date", required: true, selector: { date: {} } }, { name: "action", selector: { action: {} } }, { name: "text", selector: { text: { multiline: false } } }, { name: "text_multiline", selector: { text: { multiline: true } } }, diff --git a/src/components/ha-form/compute-initial-ha-form-data.ts b/src/components/ha-form/compute-initial-ha-form-data.ts index 7c5d728d91..b70148fa86 100644 --- a/src/components/ha-form/compute-initial-ha-form-data.ts +++ b/src/components/ha-form/compute-initial-ha-form-data.ts @@ -34,12 +34,25 @@ export const computeInitialHaFormData = ( }; } else if ("selector" in field) { const selector: Selector = field.selector; - if ("boolean" in selector) { + + if ("device" in selector) { + data[field.name] = selector.device.multiple ? [] : ""; + } else if ("entity" in selector) { + data[field.name] = selector.entity.multiple ? [] : ""; + } else if ("area" in selector) { + data[field.name] = selector.area.multiple ? [] : ""; + } else if ("boolean" in selector) { data[field.name] = false; - } else if ("text" in selector) { + } else if ( + "text" in selector || + "addon" in selector || + "attribute" in selector || + "icon" in selector || + "theme" in selector + ) { data[field.name] = ""; } else if ("number" in selector) { - data[field.name] = "min" in selector.number ? selector.number.min : 0; + data[field.name] = selector.number.min ?? 0; } else if ("select" in selector) { if (selector.select.options.length) { data[field.name] = selector.select.options[0][0]; @@ -50,6 +63,23 @@ export const computeInitialHaFormData = ( minutes: 0, seconds: 0, }; + } else if ("time" in selector) { + data[field.name] = "00:00:00"; + } else if ("date" in selector || "datetime" in selector) { + const now = new Date().toISOString().slice(0, 10); + data[field.name] = `${now} 00:00:00`; + } else if ("color_rgb" in selector) { + data[field.name] = [0, 0, 0]; + } else if ("color_temp" in selector) { + data[field.name] = selector.color_temp.min_mireds ?? 153; + } else if ( + "action" in selector || + "media" in selector || + "target" in selector + ) { + data[field.name] = {}; + } else { + throw new Error("Selector not supported in initial form data"); } } }); From 85d3011625fdc195ffcd9eee6f24b279a2cf14b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 29 Mar 2022 14:35:05 +0200 Subject: [PATCH 120/142] Add badge to configuration sidebar to indicate pending updates (#12146) --- src/components/ha-sidebar.ts | 119 ++++++++++++++++++++++++----------- 1 file changed, 84 insertions(+), 35 deletions(-) diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 70ee6ed471..b5e1ba55f5 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -37,6 +37,7 @@ import { LocalStorage } from "../common/decorators/local-storage"; import { fireEvent } from "../common/dom/fire_event"; import { toggleAttribute } from "../common/dom/toggle_attribute"; import { computeDomain } from "../common/entity/compute_domain"; +import { computeStateDomain } from "../common/entity/compute_state_domain"; import { stringCompare } from "../common/string/compare"; import { computeRTL } from "../common/util/compute_rtl"; import { ActionHandlerDetail } from "../data/lovelace"; @@ -44,6 +45,7 @@ import { PersistentNotification, subscribeNotifications, } from "../data/persistent_notification"; +import { updateCanInstall, UpdateEntity } from "../data/update"; import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; import { haStyleScrollbar } from "../resources/styles"; import type { HomeAssistant, PanelInfo, Route } from "../types"; @@ -68,7 +70,6 @@ const SORT_VALUE_URL_PATHS = { const PANEL_ICONS = { calendar: mdiCalendar, - config: mdiCog, "developer-tools": mdiHammer, energy: mdiLightningBolt, history: mdiChartBox, @@ -190,6 +191,8 @@ class HaSidebar extends LitElement { @state() private _notifications?: PersistentNotification[]; + @state() private _updatesCount = 0; + @state() private _renderEmptySortable = false; private _mouseLeaveTimeout?: number; @@ -235,6 +238,7 @@ class HaSidebar extends LitElement { changedProps.has("narrow") || changedProps.has("alwaysExpand") || changedProps.has("_externalConfig") || + changedProps.has("_updatesCount") || changedProps.has("_notifications") || changedProps.has("editMode") || changedProps.has("_renderEmptySortable") || @@ -290,6 +294,12 @@ class HaSidebar extends LitElement { toggleAttribute(this, "rtl", computeRTL(this.hass)); } + this._updatesCount = Object.values(this.hass.states).filter( + (entity) => + computeStateDomain(entity) === "update" && + updateCanInstall(entity as UpdateEntity) + ).length; + if (!SUPPORT_SCROLL_IF_NEEDED) { return; } @@ -387,35 +397,37 @@ class HaSidebar extends LitElement { icon?: string | null, iconPath?: string | null ) { - return html` -
    - - ${iconPath - ? html`` - : html``} - ${title} - - ${this.editMode - ? html`` - : ""} - - `; + return urlPath === "config" + ? this._renderConfiguration(title) + : html` + + + ${iconPath + ? html`` + : html``} + ${title} + + ${this.editMode + ? html`` + : ""} + + `; } private _renderPanelsEdit(beforeSpacer: PanelInfo[]) { @@ -477,6 +489,35 @@ class HaSidebar extends LitElement { return html`
    `; } + private _renderConfiguration(title: string | null) { + return html` + + + ${!this.alwaysExpand && this._updatesCount > 0 + ? html` + + ${this._updatesCount} + + ` + : ""} + ${title} + ${this.alwaysExpand && this._updatesCount > 0 + ? html` + ${this._updatesCount} + ` + : ""} + + `; + } + private _renderNotifications() { let notificationCount = this._notifications ? this._notifications.length @@ -953,18 +994,21 @@ class HaSidebar extends LitElement { height: 1px; background-color: var(--divider-color); } - .notifications-container { + .notifications-container, + .configuration-container { display: flex; margin-left: env(safe-area-inset-left); } - :host([rtl]) .notifications-container { + :host([rtl]) .notifications-container, + :host([rtl]) .configuration-container { margin-left: initial; margin-right: env(safe-area-inset-right); } .notifications { cursor: pointer; } - .notifications .item-text { + .notifications .item-text, + .configuration .item-text { flex: 1; } .profile { @@ -988,7 +1032,8 @@ class HaSidebar extends LitElement { margin-right: 8px; } - .notification-badge { + .notification-badge, + .configuration-badge { min-width: 20px; box-sizing: border-box; border-radius: 50%; @@ -999,7 +1044,11 @@ class HaSidebar extends LitElement { padding: 0px 6px; color: var(--text-accent-color, var(--text-primary-color)); } - ha-svg-icon + .notification-badge { + .configuration-badge { + background-color: var(--primary-color); + } + ha-svg-icon + .notification-badge, + ha-svg-icon + .configuration-badge { position: absolute; bottom: 14px; left: 26px; From 2a12172eebfc32263dbf7db84ea980a39795d1a4 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 29 Mar 2022 17:25:56 -0500 Subject: [PATCH 121/142] Bumped version to 20220329.0 (#12152) --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index e8f04faf57..ce991f5820 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = home-assistant-frontend -version = 20220322.0 +version = 20220329.0 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 00cbd1d9e62daf6618485d69307a890bad466f36 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 29 Mar 2022 17:09:51 -0700 Subject: [PATCH 122/142] Add entity source API (#12149) --- .../util/time-cache-entity-promise-func.ts | 53 +++++++++++++ .../util/time-cache-function-promise.ts | 73 ++++++++++++----- .../ha-selector/ha-selector-entity.ts | 78 +++++++++---------- src/data/camera.ts | 4 +- src/data/entity_sources.ts | 46 +++++++++++ 5 files changed, 195 insertions(+), 59 deletions(-) create mode 100644 src/common/util/time-cache-entity-promise-func.ts create mode 100644 src/data/entity_sources.ts diff --git a/src/common/util/time-cache-entity-promise-func.ts b/src/common/util/time-cache-entity-promise-func.ts new file mode 100644 index 0000000000..0b3cc5a293 --- /dev/null +++ b/src/common/util/time-cache-entity-promise-func.ts @@ -0,0 +1,53 @@ +import { HomeAssistant } from "../../types"; + +interface ResultCache { + [entityId: string]: Promise | undefined; +} + +/** + * Call a function with result caching per entity. + * @param cacheKey key to store the cache on hass object + * @param cacheTime time to cache the results + * @param func function to fetch the data + * @param hass Home Assistant object + * @param entityId entity to fetch data for + * @param args extra arguments to pass to the function to fetch the data + * @returns + */ +export const timeCacheEntityPromiseFunc = async ( + cacheKey: string, + cacheTime: number, + func: (hass: HomeAssistant, entityId: string, ...args: any[]) => Promise, + hass: HomeAssistant, + entityId: string, + ...args: any[] +): Promise => { + let cache: ResultCache | undefined = (hass as any)[cacheKey]; + + if (!cache) { + cache = hass[cacheKey] = {}; + } + + const lastResult = cache[entityId]; + + if (lastResult) { + return lastResult; + } + + const result = func(hass, entityId, ...args); + cache[entityId] = result; + + result.then( + // When successful, set timer to clear cache + () => + setTimeout(() => { + cache![entityId] = undefined; + }, cacheTime), + // On failure, clear cache right away + () => { + cache![entityId] = undefined; + } + ); + + return result; +}; diff --git a/src/common/util/time-cache-function-promise.ts b/src/common/util/time-cache-function-promise.ts index d841ea256b..daa4b730b5 100644 --- a/src/common/util/time-cache-function-promise.ts +++ b/src/common/util/time-cache-function-promise.ts @@ -1,43 +1,80 @@ import { HomeAssistant } from "../../types"; -interface ResultCache { - [entityId: string]: Promise | undefined; +interface CacheResult { + result: T; + cacheKey: any; } +/** + * Caches a result of a promise for X time. Allows optional extra validation + * check to invalidate the cache. + * @param cacheKey the key to store the cache + * @param cacheTime the time to cache the result + * @param func the function to fetch the data + * @param generateCacheKey optional function to generate a cache key based on current hass + cached result. Cache is invalid if generates a different cache key. + * @param hass Home Assistant object + * @param args extra arguments to pass to the function to fetch the data + * @returns + */ export const timeCachePromiseFunc = async ( cacheKey: string, cacheTime: number, - func: (hass: HomeAssistant, entityId: string, ...args: any[]) => Promise, + func: (hass: HomeAssistant, ...args: any[]) => Promise, + generateCacheKey: + | ((hass: HomeAssistant, lastResult: T) => unknown) + | undefined, hass: HomeAssistant, - entityId: string, ...args: any[] ): Promise => { - let cache: ResultCache | undefined = (hass as any)[cacheKey]; + const anyHass = hass as any; + const lastResult: Promise> | CacheResult | undefined = + anyHass[cacheKey]; - if (!cache) { - cache = hass[cacheKey] = {}; - } + const checkCachedResult = (result: CacheResult): T | Promise => { + if ( + !generateCacheKey || + generateCacheKey(hass, result.result) === result.cacheKey + ) { + return result.result; + } - const lastResult = cache[entityId]; + anyHass[cacheKey] = undefined; + return timeCachePromiseFunc( + cacheKey, + cacheTime, + func, + generateCacheKey, + hass, + ...args + ); + }; + // If we have a cached result, return it if it's still valid if (lastResult) { - return lastResult; + return lastResult instanceof Promise + ? lastResult.then(checkCachedResult) + : checkCachedResult(lastResult); } - const result = func(hass, entityId, ...args); - cache[entityId] = result; + const resultPromise = func(hass, ...args); + anyHass[cacheKey] = resultPromise; - result.then( + resultPromise.then( // When successful, set timer to clear cache - () => + (result) => { + anyHass[cacheKey] = { + result, + cacheKey: generateCacheKey?.(hass, result), + }; setTimeout(() => { - cache![entityId] = undefined; - }, cacheTime), + anyHass[cacheKey] = undefined; + }, cacheTime); + }, // On failure, clear cache right away () => { - cache![entityId] = undefined; + anyHass[cacheKey] = undefined; } ); - return result; + return resultPromise; }; diff --git a/src/components/ha-selector/ha-selector-entity.ts b/src/components/ha-selector/ha-selector-entity.ts index 566560c7a9..220550cf34 100644 --- a/src/components/ha-selector/ha-selector-entity.ts +++ b/src/components/ha-selector/ha-selector-entity.ts @@ -1,21 +1,23 @@ -import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; -import { html, LitElement } from "lit"; +import { HassEntity } from "home-assistant-js-websocket"; +import { html, LitElement, PropertyValues } from "lit"; import { customElement, property, state } from "lit/decorators"; import { computeStateDomain } from "../../common/entity/compute_state_domain"; -import { subscribeEntityRegistry } from "../../data/entity_registry"; +import { + EntitySources, + fetchEntitySourcesWithCache, +} from "../../data/entity_sources"; import { EntitySelector } from "../../data/selector"; -import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../types"; import "../entity/ha-entities-picker"; import "../entity/ha-entity-picker"; @customElement("ha-selector-entity") -export class HaEntitySelector extends SubscribeMixin(LitElement) { +export class HaEntitySelector extends LitElement { @property() public hass!: HomeAssistant; @property() public selector!: EntitySelector; - @state() private _entityPlaformLookup?: Record; + @state() private _entitySources?: EntitySources; @property() public value?: any; @@ -49,49 +51,47 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) { `; } - public hassSubscribe(): UnsubscribeFunc[] { - return [ - subscribeEntityRegistry(this.hass.connection!, (entities) => { - const entityLookup = {}; - for (const confEnt of entities) { - if (!confEnt.platform) { - continue; - } - entityLookup[confEnt.entity_id] = confEnt.platform; - } - this._entityPlaformLookup = entityLookup; - }), - ]; + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + if ( + changedProps.has("selector") && + this.selector.entity.integration && + !this._entitySources + ) { + fetchEntitySourcesWithCache(this.hass).then((sources) => { + this._entitySources = sources; + }); + } } private _filterEntities = (entity: HassEntity): boolean => { - if (this.selector.entity?.domain) { - const filterDomain = this.selector.entity.domain; - const filterDomainIsArray = Array.isArray(filterDomain); + const { + domain: filterDomain, + device_class: filterDeviceClass, + integration: filterIntegration, + } = this.selector.entity; + + if (filterDomain) { const entityDomain = computeStateDomain(entity); if ( - (filterDomainIsArray && !filterDomain.includes(entityDomain)) || - (!filterDomainIsArray && entityDomain !== filterDomain) + Array.isArray(filterDomain) + ? !filterDomain.includes(entityDomain) + : entityDomain !== filterDomain ) { return false; } } - if (this.selector.entity?.device_class) { - if ( - !entity.attributes.device_class || - entity.attributes.device_class !== this.selector.entity.device_class - ) { - return false; - } + if ( + filterDeviceClass && + entity.attributes.device_class !== filterDeviceClass + ) { + return false; } - if (this.selector.entity?.integration) { - if ( - !this._entityPlaformLookup || - this._entityPlaformLookup[entity.entity_id] !== - this.selector.entity.integration - ) { - return false; - } + if ( + filterIntegration && + this._entitySources?.[entity.entity_id]?.domain !== filterIntegration + ) { + return false; } return true; }; diff --git a/src/data/camera.ts b/src/data/camera.ts index d556578ae2..48bbe4bc73 100644 --- a/src/data/camera.ts +++ b/src/data/camera.ts @@ -2,7 +2,7 @@ import { HassEntityAttributeBase, HassEntityBase, } from "home-assistant-js-websocket"; -import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise"; +import { timeCacheEntityPromiseFunc } from "../common/util/time-cache-entity-promise-func"; import { HomeAssistant } from "../types"; import { getSignedPath } from "./auth"; @@ -50,7 +50,7 @@ export const fetchThumbnailUrlWithCache = async ( width: number, height: number ) => { - const base_url = await timeCachePromiseFunc( + const base_url = await timeCacheEntityPromiseFunc( "_cameraTmbUrl", 9000, fetchThumbnailUrl, diff --git a/src/data/entity_sources.ts b/src/data/entity_sources.ts new file mode 100644 index 0000000000..67455c1d22 --- /dev/null +++ b/src/data/entity_sources.ts @@ -0,0 +1,46 @@ +import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise"; +import { HomeAssistant } from "../types"; + +interface EntitySourceConfigEntry { + source: "config_entry"; + domain: string; + custom_component: boolean; + config_entry: string; +} + +interface EntitySourcePlatformConfig { + source: "platform_config"; + domain: string; + custom_component: boolean; +} + +export type EntitySources = Record< + string, + EntitySourceConfigEntry | EntitySourcePlatformConfig +>; + +const fetchEntitySources = ( + hass: HomeAssistant, + entity_id?: string +): Promise => + hass.callWS({ + type: "entity/source", + entity_id, + }); + +export const fetchEntitySourcesWithCache = ( + hass: HomeAssistant, + entity_id?: string +): Promise => + entity_id + ? fetchEntitySources(hass, entity_id) + : timeCachePromiseFunc( + "_entitySources", + // cache for 30 seconds + 30000, + fetchEntitySources, + // We base the cache on number of states. If number of states + // changes we force a refresh + (hass2) => Object.keys(hass2.states).length, + hass + ); From c7050e4676e5469aa7713247bba03e316dcbe6b2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 29 Mar 2022 20:40:17 -0700 Subject: [PATCH 123/142] Update adjust statistic dialog (#12118) * Update text for adjust statistic dialog * Change everything * Import type * Max show 5 * Revert back the API change * Hide adjust button if no sum * Adjustments * Update src/panels/developer-tools/statistics/developer-tools-statistics.ts * Render optional Co-authored-by: Zack --- src/data/history.ts | 2 + .../statistics/developer-tools-statistics.ts | 48 +-- .../dialog-statistics-adjust-sum.ts | 388 ++++++++++++++---- 3 files changed, 325 insertions(+), 113 deletions(-) diff --git a/src/data/history.ts b/src/data/history.ts index 8b8e708f5c..a631c3423a 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -84,6 +84,8 @@ export interface StatisticsMetaData { statistic_id: string; source: string; name?: string | null; + has_sum: boolean; + has_mean: boolean; } export type StatisticsValidationResult = diff --git a/src/panels/developer-tools/statistics/developer-tools-statistics.ts b/src/panels/developer-tools/statistics/developer-tools-statistics.ts index 3183133beb..b9b2359dcf 100644 --- a/src/panels/developer-tools/statistics/developer-tools-statistics.ts +++ b/src/panels/developer-tools/statistics/developer-tools-statistics.ts @@ -6,7 +6,6 @@ import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../common/dom/fire_event"; import { computeStateName } from "../../../common/entity/compute_state_name"; -import "../../../components/ha-icon-overflow-menu"; import "../../../components/data-table/ha-data-table"; import type { DataTableColumnContainer } from "../../../components/data-table/ha-data-table"; import { subscribeEntityRegistry } from "../../../data/entity_registry"; @@ -24,9 +23,9 @@ import { import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; +import { showStatisticsAdjustSumDialog } from "./show-dialog-statistics-adjust-sum"; import { showFixStatisticsUnitsChangedDialog } from "./show-dialog-statistics-fix-units-changed"; import { showFixStatisticsUnsupportedUnitMetadataDialog } from "./show-dialog-statistics-fix-unsupported-unit-meta"; -import { showStatisticsAdjustSumDialog } from "./show-dialog-statistics-adjust-sum"; const FIX_ISSUES_ORDER = { no_state: 0, @@ -116,27 +115,21 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) { }, actions: { title: "", - type: "overflow-menu", - template: ( - _info, - statistic: StatisticsMetaData - ) => html` - showStatisticsAdjustSumDialog(this, { - statistic: statistic, - }), - }, - ]} - style="color: var(--secondary-text-color)" - >`, + label: localize("ui.panel.developer-tools.tabs.statistics.adjust_sum"), + type: "icon-button", + template: (_info, statistic: StatisticsMetaData) => + statistic.has_sum + ? html` + + ` + : "", }, }) ); @@ -154,6 +147,13 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) { `; } + private _showStatisticsAdjustSumDialog(ev) { + ev.stopPropagation(); + showStatisticsAdjustSumDialog(this, { + statistic: ev.currentTarget.statistic, + }); + } + private _rowClicked(ev) { const id = ev.detail.id; if (id in this.hass.states) { @@ -212,6 +212,8 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) { source: "", state: this.hass.states[statisticId], issues: issues[statisticId], + has_mean: false, + has_sum: false, }); } }); diff --git a/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts b/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts index 9819177799..bae0576f9e 100644 --- a/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts +++ b/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts @@ -1,24 +1,37 @@ import "@material/mwc-button/mwc-button"; -import { LitElement, TemplateResult, html, CSSResultGroup } from "lit"; +import "@material/mwc-list/mwc-list-item"; +import { mdiChevronRight } from "@mdi/js"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; -import "../../../components/ha-dialog"; +import { formatDateTime } from "../../../common/datetime/format_date_time"; import { fireEvent } from "../../../common/dom/fire_event"; +import "../../../components/ha-circular-progress"; +import "../../../components/ha-dialog"; +import "../../../components/ha-form/ha-form"; +import "../../../components/ha-selector/ha-selector-datetime"; +import "../../../components/ha-selector/ha-selector-number"; +import "../../../components/ha-svg-icon"; +import { + adjustStatisticsSum, + fetchStatistics, + StatisticValue, +} from "../../../data/history"; +import type { DateTimeSelector, NumberSelector } from "../../../data/selector"; +import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import { haStyle, haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; -import "../../../components/ha-formfield"; -import "../../../components/ha-radio"; -import "../../../components/ha-form/ha-form"; -import type { DialogStatisticsAdjustSumParams } from "./show-dialog-statistics-adjust-sum"; -import type { - HaFormBaseSchema, - HaFormSchema, -} from "../../../components/ha-form/types"; -import { adjustStatisticsSum } from "../../../data/history"; -import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import { showToast } from "../../../util/toast"; +import type { DialogStatisticsAdjustSumParams } from "./show-dialog-statistics-adjust-sum"; -let lastMoment: string | undefined; +/* eslint-disable lit/no-template-arrow */ @customElement("dialog-statistics-adjust-sum") export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement { @@ -26,29 +39,54 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement { @state() private _params?: DialogStatisticsAdjustSumParams; - @state() private _data?: { - moment: string; - amount: number; + @state() private _busy = false; + + @state() private _moment?: string; + + @state() private _stats5min?: StatisticValue[]; + + @state() private _statsHour?: StatisticValue[]; + + @state() private _chosenStat?: StatisticValue; + + private _origAmount?: number; + + @state() private _amount?: number; + + private _dateTimeSelector: DateTimeSelector = { + datetime: {}, }; - @state() private _busy = false; + private _amountSelector = memoizeOne( + (unit_of_measurement: string): NumberSelector => ({ + number: { + step: 0.01, + unit_of_measurement, + mode: "box", + }, + }) + ); public showDialog(params: DialogStatisticsAdjustSumParams): void { this._params = params; - this._busy = false; const now = new Date(); - this._data = { - moment: - lastMoment || - `${now.getFullYear()}-${ - now.getMonth() + 1 - }-${now.getDate()} ${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`, - amount: 0, - }; + this._moment = `${now.getFullYear()}-${ + now.getMonth() + 1 + }-${now.getDate()} ${now.getHours()}:${ + now.getMinutes() - (now.getMinutes() % 5) + }:00`; + this._fetchStats(); } public closeDialog(): void { this._params = undefined; + this._moment = undefined; + this._stats5min = undefined; + this._statsHour = undefined; + this._origAmount = undefined; + this._amount = undefined; + this._chosenStat = undefined; + this._busy = false; fireEvent(this, "dialog-closed", { dialog: this.localName }); } @@ -57,78 +95,201 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement { return html``; } + let content: TemplateResult; + + if (!this._chosenStat) { + content = this._renderPickStatistic(); + } else { + content = this._renderAdjustStat(); + } + return html` - - - - + ${content} `; } - private _getSchema = memoizeOne((statistic): HaFormSchema[] => [ - { - type: "constant", - name: "name", - value: statistic.name || statistic.statistic_id, - }, - { - name: "moment", - required: true, - selector: { - datetime: {}, - }, - }, - { - name: "amount", - required: true, - default: 0, - selector: { - number: { - mode: "box", - step: 0.1, - unit_of_measurement: statistic.unit_of_measurement, - }, - }, - }, - ]); - - private _computeLabel(value: HaFormBaseSchema) { - switch (value.name) { - case "name": - return "Statistic"; - case "moment": - return "Moment to adjust"; - case "amount": - return "Amount"; - default: - return value.name; + protected shouldUpdate(changedProps: PropertyValues): boolean { + if (changedProps.size !== 1 || !changedProps.has("hass")) { + return true; } + // We only respond to hass changes if the translations changed + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + return !oldHass || oldHass.localize !== this.hass.localize; } - private _valueChanged(ev) { - this._data = ev.detail.value; + private _renderPickStatistic() { + let stats: TemplateResult; + + if (!this._stats5min || !this._statsHour) { + stats = html``; + } else if (this._statsHour.length < 2 && this._stats5min.length < 2) { + stats = html`

    No statistics found for this period.

    `; + } else { + const data = + this._stats5min.length >= 2 ? this._stats5min : this._statsHour; + const unit = this._params!.statistic.unit_of_measurement; + const rows: TemplateResult[] = []; + for (let i = 1; i < data.length; i++) { + const stat = data[i]; + const growth = Math.round((stat.sum! - data[i - 1].sum!) * 100) / 100; + rows.push(html` + { + this._chosenStat = stat; + this._origAmount = growth; + this._amount = growth; + }} + > + ${growth} ${unit} + + ${formatDateTime(new Date(stat.start), this.hass.locale)} + + + + `); + } + stats = html`${rows}`; + } + + return html` +
    + Sometimes the statistics end up being incorrect for a specific point in + time. This can mess up your beautiful graphs! Select a time below to + find the bad moment and adjust the data. +
    + +
    ${stats}
    + + `; + } + + private _dateTimeSelectorChanged(ev) { + this._moment = ev.detail.value; + this._fetchStats(); + } + + private _renderAdjustStat() { + return html` +
    + ${this._params!.statistic.name || this._params!.statistic.statistic_id} +
    + +
    + Start + ${formatDateTime( + new Date(this._chosenStat!.start), + this.hass.locale + )} +
    + +
    + End + ${formatDateTime( + new Date(this._chosenStat!.end), + this.hass.locale + )} +
    + + { + this._amount = ev.detail.value; + }} + > + + { + this._fixIssue(); + }} + > + { + this._chosenStat = undefined; + }} + > + `; + } + + private async _fetchStats(): Promise { + this._stats5min = undefined; + this._statsHour = undefined; + const statId = this._params!.statistic.statistic_id; + const moment = new Date(this._moment!); + + // Search 3 hours before and 3 hours after chosen time + const hourStatStart = new Date(moment.getTime()); + hourStatStart.setTime(hourStatStart.getTime() - 3 * 3600 * 1000); + const hourStatEnd = new Date(moment.getTime()); + hourStatEnd.setTime(hourStatEnd.getTime() + 3 * 3600 * 1000); + + const statsHourData = await fetchStatistics( + this.hass, + hourStatStart, + hourStatEnd, + [statId], + "hour" + ); + this._statsHour = + statId in statsHourData ? statsHourData[statId].slice(0, 6) : []; + + // Can't have 5 min data if no hourly data + if (this._statsHour.length === 0) { + this._stats5min = []; + return; + } + + // Search 15 minutes before and 15 minutes after chosen time + const minStatStart = new Date(moment.getTime()); + minStatStart.setTime(minStatStart.getTime() - 15 * 60 * 1000); + const minStatEnd = new Date(moment.getTime()); + minStatEnd.setTime(minStatEnd.getTime() + 15 * 60 * 1000); + + const stats5MinData = await fetchStatistics( + this.hass, + minStatStart, + minStatEnd, + [statId], + "5minute" + ); + + this._stats5min = + statId in stats5MinData ? stats5MinData[statId].slice(0, 6) : []; } private async _fixIssue(): Promise { @@ -137,8 +298,8 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement { await adjustStatisticsSum( this.hass, this._params!.statistic.statistic_id, - this._data!.moment, - this._data!.amount + this._chosenStat!.start, + this._amount! - this._origAmount! ); } catch (err: any) { this._busy = false; @@ -150,12 +311,59 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement { showToast(this, { message: "Statistic sum adjusted", }); - lastMoment = this._data!.moment; this.closeDialog(); } static get styles(): CSSResultGroup { - return [haStyle, haStyleDialog]; + return [ + haStyle, + haStyleDialog, + css` + @media all and (max-width: 450px), all and (max-height: 500px) { + /* overrule the ha-style-dialog max-height on small screens */ + ha-dialog { + --mdc-dialog-max-height: 100%; + height: 100%; + } + } + + @media all and (min-width: 850px) { + ha-dialog { + --mdc-dialog-max-height: 80%; + --mdc-dialog-max-height: 80%; + } + } + + @media all and (min-width: 451px) and (min-height: 501px) { + ha-dialog { + --mdc-dialog-max-width: 480px; + } + } + + .text-content, + ha-selector-datetime, + ha-selector-number { + margin-bottom: 20px; + } + mwc-list-item { + margin: 0 -24px; + --mdc-list-side-padding: 24px; + } + .table-row { + display: flex; + justify-content: space-between; + margin-bottom: 20px; + } + .stat-list { + min-height: 360px; + display: flex; + flex-direction: column; + } + .stat-list ha-circular-progress { + margin: 0 auto; + } + `, + ]; } } From e263b57296314e6304ba18dd906af5471bdc9fb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 30 Mar 2022 07:22:12 +0200 Subject: [PATCH 124/142] Fetch release notes for update entities that provides it (#12148) * Fetch release notes for update entities that provides it * lint --- gallery/src/data/text.ts | 11 +++++ gallery/src/pages/components/ha-faded.ts | 13 +----- gallery/src/pages/more-info/update.ts | 38 ++++++++++++++++ src/data/update.ts | 8 ++++ .../more-info/controls/more-info-update.ts | 44 +++++++++++++++++-- 5 files changed, 98 insertions(+), 16 deletions(-) create mode 100644 gallery/src/data/text.ts diff --git a/gallery/src/data/text.ts b/gallery/src/data/text.ts new file mode 100644 index 0000000000..f25d4933cd --- /dev/null +++ b/gallery/src/data/text.ts @@ -0,0 +1,11 @@ +export const LONG_TEXT = ` +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc laoreet velit ut elit volutpat, eget ultrices odio lacinia. In imperdiet malesuada est, nec sagittis metus ultricies quis. Sed nisl ex, convallis porttitor ante quis, hendrerit tristique justo. Mauris pharetra venenatis augue, eu maximus sem cursus in. Quisque sed consequat risus. Suspendisse facilisis ligula a odio consectetur condimentum. Curabitur vehicula elit nec augue mollis, et volutpat massa dictum. + +Nam pellentesque auctor rutrum. Suspendisse elit est, sodales vel diam nec, porttitor faucibus massa. Ut pretium ac orci eu pharetra. Praesent in nibh at magna viverra rutrum eu vitae tortor. Etiam eget sem ex. Fusce tristique odio nec lacus mattis, vitae tempor nunc malesuada. Maecenas faucibus magna vel libero maximus egestas. Vestibulum luctus semper velit, in lobortis risus tempus non. Curabitur bibendum ornare commodo. Quisque commodo neque sit amet tincidunt lacinia. Proin elementum ante velit, eu congue nulla semper quis. Pellentesque consequat vel nunc at scelerisque. Mauris sit amet venenatis diam, blandit viverra leo. Integer commodo laoreet orci. + +Curabitur ipsum tortor, sodales ut augue sed, commodo porttitor libero. Pellentesque molestie vitae mi consectetur tempor. In sed lectus consequat, lobortis neque non, semper ipsum. Etiam eget ex et nibh sagittis pulvinar lacinia ac mauris. Aenean ligula eros, viverra ac nibh at, venenatis semper quam. Sed interdum ligula sit amet massa tincidunt tincidunt. Suspendisse potenti. Aliquam egestas facilisis est, sed faucibus erat scelerisque id. Duis dolor quam, viverra vitae orci euismod, laoreet pellentesque justo. Nunc malesuada non erat at ullamcorper. Mauris eget posuere odio. Vestibulum turpis nunc, pharetra eget ante in, feugiat mollis justo. Proin porttitor, diam nec vulputate pretium, tellus arcu rhoncus turpis, a blandit nisi nulla quis arcu. Nunc ac ullamcorper ligula, nec facilisis leo. + +In vitae eros sollicitudin, iaculis ex eget, egestas orci. Etiam sed pretium lorem. Nam nisi enim, consectetur sit amet semper ac, semper pharetra diam. In pulvinar neque sapien, ac ullamcorper est lacinia a. Etiam tincidunt velit sed diam malesuada, eu ornare ex consectetur. Phasellus in imperdiet tellus. Sed bibendum, dui sit amet fringilla aliquet, enim odio sollicitudin lorem, vel semper turpis mauris vel mauris. Aenean congue magna ac massa cursus, in dictum orci commodo. Pellentesque mollis velit in sollicitudin tincidunt. Vestibulum et efficitur nulla. + +Quisque posuere, velit sed porttitor dapibus, neque augue fringilla felis, eu luctus nisi nisl nec ipsum. Curabitur pellentesque ac lectus eget ultricies. Vestibulum est dolor, lacinia pharetra vulputate a, facilisis a magna. Nam vitae arcu nibh. Praesent finibus blandit ante, ac gravida ex mollis eget. Donec quam est, pulvinar vitae neque ut, bibendum aliquam erat. Nullam mollis arcu at sem tincidunt, in tristique lectus facilisis. Aenean ut lacus vel nisl finibus iaculis non a turpis. Integer eget ipsum ante. Donec nunc neque, vestibulum ac magna ac, posuere scelerisque dui. Pellentesque massa nibh, rhoncus id dolor quis, placerat posuere turpis. Donec aliquet augue nisi, eu finibus dui auctor et. Vestibulum eu varius lorem. Quisque lectus ante, malesuada pretium risus eget, interdum mattis enim. +`; diff --git a/gallery/src/pages/components/ha-faded.ts b/gallery/src/pages/components/ha-faded.ts index 1f962ef101..ad9970fadf 100644 --- a/gallery/src/pages/components/ha-faded.ts +++ b/gallery/src/pages/components/ha-faded.ts @@ -3,18 +3,7 @@ import { customElement } from "lit/decorators"; import "../../../../src/components/ha-card"; import "../../../../src/components/ha-faded"; import "../../../../src/components/ha-markdown"; - -const LONG_TEXT = ` -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc laoreet velit ut elit volutpat, eget ultrices odio lacinia. In imperdiet malesuada est, nec sagittis metus ultricies quis. Sed nisl ex, convallis porttitor ante quis, hendrerit tristique justo. Mauris pharetra venenatis augue, eu maximus sem cursus in. Quisque sed consequat risus. Suspendisse facilisis ligula a odio consectetur condimentum. Curabitur vehicula elit nec augue mollis, et volutpat massa dictum. - -Nam pellentesque auctor rutrum. Suspendisse elit est, sodales vel diam nec, porttitor faucibus massa. Ut pretium ac orci eu pharetra. Praesent in nibh at magna viverra rutrum eu vitae tortor. Etiam eget sem ex. Fusce tristique odio nec lacus mattis, vitae tempor nunc malesuada. Maecenas faucibus magna vel libero maximus egestas. Vestibulum luctus semper velit, in lobortis risus tempus non. Curabitur bibendum ornare commodo. Quisque commodo neque sit amet tincidunt lacinia. Proin elementum ante velit, eu congue nulla semper quis. Pellentesque consequat vel nunc at scelerisque. Mauris sit amet venenatis diam, blandit viverra leo. Integer commodo laoreet orci. - -Curabitur ipsum tortor, sodales ut augue sed, commodo porttitor libero. Pellentesque molestie vitae mi consectetur tempor. In sed lectus consequat, lobortis neque non, semper ipsum. Etiam eget ex et nibh sagittis pulvinar lacinia ac mauris. Aenean ligula eros, viverra ac nibh at, venenatis semper quam. Sed interdum ligula sit amet massa tincidunt tincidunt. Suspendisse potenti. Aliquam egestas facilisis est, sed faucibus erat scelerisque id. Duis dolor quam, viverra vitae orci euismod, laoreet pellentesque justo. Nunc malesuada non erat at ullamcorper. Mauris eget posuere odio. Vestibulum turpis nunc, pharetra eget ante in, feugiat mollis justo. Proin porttitor, diam nec vulputate pretium, tellus arcu rhoncus turpis, a blandit nisi nulla quis arcu. Nunc ac ullamcorper ligula, nec facilisis leo. - -In vitae eros sollicitudin, iaculis ex eget, egestas orci. Etiam sed pretium lorem. Nam nisi enim, consectetur sit amet semper ac, semper pharetra diam. In pulvinar neque sapien, ac ullamcorper est lacinia a. Etiam tincidunt velit sed diam malesuada, eu ornare ex consectetur. Phasellus in imperdiet tellus. Sed bibendum, dui sit amet fringilla aliquet, enim odio sollicitudin lorem, vel semper turpis mauris vel mauris. Aenean congue magna ac massa cursus, in dictum orci commodo. Pellentesque mollis velit in sollicitudin tincidunt. Vestibulum et efficitur nulla. - -Quisque posuere, velit sed porttitor dapibus, neque augue fringilla felis, eu luctus nisi nisl nec ipsum. Curabitur pellentesque ac lectus eget ultricies. Vestibulum est dolor, lacinia pharetra vulputate a, facilisis a magna. Nam vitae arcu nibh. Praesent finibus blandit ante, ac gravida ex mollis eget. Donec quam est, pulvinar vitae neque ut, bibendum aliquam erat. Nullam mollis arcu at sem tincidunt, in tristique lectus facilisis. Aenean ut lacus vel nisl finibus iaculis non a turpis. Integer eget ipsum ante. Donec nunc neque, vestibulum ac magna ac, posuere scelerisque dui. Pellentesque massa nibh, rhoncus id dolor quis, placerat posuere turpis. Donec aliquet augue nisi, eu finibus dui auctor et. Vestibulum eu varius lorem. Quisque lectus ante, malesuada pretium risus eget, interdum mattis enim. -`; +import { LONG_TEXT } from "../../data/text"; const SMALL_TEXT = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."; diff --git a/gallery/src/pages/more-info/update.ts b/gallery/src/pages/more-info/update.ts index 1488e318dc..f23ae25780 100644 --- a/gallery/src/pages/more-info/update.ts +++ b/gallery/src/pages/more-info/update.ts @@ -5,6 +5,7 @@ import { UPDATE_SUPPORT_BACKUP, UPDATE_SUPPORT_PROGRESS, UPDATE_SUPPORT_INSTALL, + UPDATE_SUPPORT_RELEASE_NOTES, } from "../../../../src/data/update"; import "../../../../src/dialogs/more-info/more-info-content"; import { getEntity } from "../../../../src/fake_data/entity"; @@ -13,6 +14,7 @@ import { provideHass, } from "../../../../src/fake_data/provide_hass"; import "../../components/demo-more-infos"; +import { LONG_TEXT } from "../../data/text"; const base_attributes = { title: "Awesome", @@ -108,6 +110,24 @@ const ENTITIES = [ latest_version: null, friendly_name: "Update without latest_version", }), + getEntity("update", "update16", "off", { + ...base_attributes, + friendly_name: "Update with release notes", + supported_features: + base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES, + }), + getEntity("update", "update17", "off", { + ...base_attributes, + friendly_name: "Update with release notes error", + supported_features: + base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES, + }), + getEntity("update", "update18", "off", { + ...base_attributes, + friendly_name: "Update with release notes loading", + supported_features: + base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES, + }), ]; @customElement("demo-more-info-update") @@ -130,6 +150,24 @@ class DemoMoreInfoUpdate extends LitElement { const hass = provideHass(this._demoRoot); hass.updateTranslations(null, "en"); hass.addEntities(ENTITIES); + hass.mockWS( + "update/release_notes", + (msg: { type: string; entity_id: string }) => { + if (msg.entity_id === "update.update16") { + return LONG_TEXT; + } + if (msg.entity_id === "update.update17") { + return Promise.reject({ + code: "error", + message: "Could not fetch release notes", + }); + } + if (msg.entity_id === "update.update18") { + return undefined; + } + return null; + } + ); } } diff --git a/src/data/update.ts b/src/data/update.ts index 22891a92ac..7482412343 100644 --- a/src/data/update.ts +++ b/src/data/update.ts @@ -3,11 +3,13 @@ import type { HassEntityBase, } from "home-assistant-js-websocket"; import { supportsFeature } from "../common/entity/supports-feature"; +import { HomeAssistant } from "../types"; export const UPDATE_SUPPORT_INSTALL = 1; export const UPDATE_SUPPORT_SPECIFIC_VERSION = 2; export const UPDATE_SUPPORT_PROGRESS = 4; export const UPDATE_SUPPORT_BACKUP = 8; +export const UPDATE_SUPPORT_RELEASE_NOTES = 16; interface UpdateEntityAttributes extends HassEntityAttributeBase { current_version: string | null; @@ -34,3 +36,9 @@ export const updateCanInstall = (entity: UpdateEntity): boolean => export const updateIsInstalling = (entity: UpdateEntity): boolean => updateUsesProgress(entity) || !!entity.attributes.in_progress; + +export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) => + hass.callWS({ + type: "update/release_notes", + entity_id: entityId, + }); diff --git a/src/dialogs/more-info/controls/more-info-update.ts b/src/dialogs/more-info/controls/more-info-update.ts index de1dbe1f10..44d3d84840 100644 --- a/src/dialogs/more-info/controls/more-info-update.ts +++ b/src/dialogs/more-info/controls/more-info-update.ts @@ -1,18 +1,23 @@ +import "../../../components/ha-alert"; +import "../../../components/ha-faded"; import "@material/mwc-button/mwc-button"; import "@material/mwc-linear-progress/mwc-linear-progress"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-checkbox"; +import "../../../components/ha-circular-progress"; import "../../../components/ha-formfield"; import "../../../components/ha-markdown"; import { UNAVAILABLE_STATES } from "../../../data/entity"; import { - updateIsInstalling, UpdateEntity, + updateIsInstalling, + updateReleaseNotes, UPDATE_SUPPORT_BACKUP, UPDATE_SUPPORT_INSTALL, UPDATE_SUPPORT_PROGRESS, + UPDATE_SUPPORT_RELEASE_NOTES, UPDATE_SUPPORT_SPECIFIC_VERSION, } from "../../../data/update"; import type { HomeAssistant } from "../../../types"; @@ -23,6 +28,10 @@ class MoreInfoUpdate extends LitElement { @property({ attribute: false }) public stateObj?: UpdateEntity; + @state() private _releaseNotes?: string | null; + + @state() private _error?: string; + protected render(): TemplateResult { if ( !this.hass || @@ -50,6 +59,9 @@ class MoreInfoUpdate extends LitElement { ${this.stateObj.attributes.title ? html`

    ${this.stateObj.attributes.title}

    ` : ""} + ${this._error + ? html`${this._error}` + : ""}
    @@ -89,11 +101,19 @@ class MoreInfoUpdate extends LitElement {
    ` : ""} - ${this.stateObj.attributes.release_summary + ${supportsFeature(this.stateObj!, UPDATE_SUPPORT_RELEASE_NOTES) && + !this._error + ? this._releaseNotes === undefined + ? html`` + : html`
    + + + ` + : this.stateObj.attributes.release_summary ? html`
    ` + >` : ""} ${supportsFeature(this.stateObj, UPDATE_SUPPORT_BACKUP) ? html`
    @@ -136,6 +156,18 @@ class MoreInfoUpdate extends LitElement { `; } + protected firstUpdated(): void { + if (supportsFeature(this.stateObj!, UPDATE_SUPPORT_RELEASE_NOTES)) { + updateReleaseNotes(this.hass, this.stateObj!.entity_id) + .then((result) => { + this._releaseNotes = result; + }) + .catch((err) => { + this._error = err.message; + }); + } + } + get _shouldCreateBackup(): boolean | null { if (!supportsFeature(this.stateObj!, UPDATE_SUPPORT_BACKUP)) { return null; @@ -201,6 +233,10 @@ class MoreInfoUpdate extends LitElement { a { color: var(--primary-color); } + ha-circular-progress { + width: 100%; + justify-content: center; + } `; } } From ac670614b464dcd905cd7f4038a8670683efc3ac Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 30 Mar 2022 03:00:36 -0400 Subject: [PATCH 125/142] Add support for new timer properties (#11940) --- src/data/timer.ts | 3 +++ .../more-info/controls/more-info-timer.ts | 2 +- .../config/helpers/forms/ha-timer-form.ts | 23 +++++++++++++++++++ src/translations/en.json | 3 ++- 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/data/timer.ts b/src/data/timer.ts index cff601b60a..4631454e12 100644 --- a/src/data/timer.ts +++ b/src/data/timer.ts @@ -12,6 +12,7 @@ export type TimerEntity = HassEntityBase & { attributes: HassEntityAttributeBase & { duration: string; remaining: string; + restore: boolean; }; }; @@ -26,12 +27,14 @@ export interface Timer { name: string; icon?: string; duration?: string | number | DurationDict; + restore?: boolean; } export interface TimerMutableParams { name: string; icon: string; duration: string | number | DurationDict; + restore: boolean; } export const fetchTimer = (hass: HomeAssistant) => diff --git a/src/dialogs/more-info/controls/more-info-timer.ts b/src/dialogs/more-info/controls/more-info-timer.ts index 55dbe3168a..63be3032f9 100644 --- a/src/dialogs/more-info/controls/more-info-timer.ts +++ b/src/dialogs/more-info/controls/more-info-timer.ts @@ -46,7 +46,7 @@ class MoreInfoTimer extends LitElement { `; } diff --git a/src/panels/config/helpers/forms/ha-timer-form.ts b/src/panels/config/helpers/forms/ha-timer-form.ts index 5d7b13cfad..5aef7ca212 100644 --- a/src/panels/config/helpers/forms/ha-timer-form.ts +++ b/src/panels/config/helpers/forms/ha-timer-form.ts @@ -21,16 +21,20 @@ class HaTimerForm extends LitElement { @state() private _duration!: string | number | DurationDict; + @state() private _restore!: boolean; + set item(item: Timer) { this._item = item; if (item) { this._name = item.name || ""; this._icon = item.icon || ""; this._duration = item.duration || "00:00:00"; + this._restore = item.restore || false; } else { this._name = ""; this._icon = ""; this._duration = "00:00:00"; + this._restore = false; } } @@ -79,6 +83,18 @@ class HaTimerForm extends LitElement { "ui.dialogs.helper_settings.timer.duration" )} > + + + +
    `; } @@ -104,6 +120,13 @@ class HaTimerForm extends LitElement { }); } + private _toggleRestore() { + this._restore = !this._restore; + fireEvent(this, "value-changed", { + value: { ...this._item, restore: this._restore }, + }); + } + static get styles(): CSSResultGroup { return [ haStyle, diff --git a/src/translations/en.json b/src/translations/en.json index 3d48b16f2c..2d05c998e8 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -900,7 +900,8 @@ "step": "Step size" }, "timer": { - "duration": "Duration" + "duration": "Duration", + "restore": "Restore?" } }, "options_flow": { From 7ca28469b752219f8bd34a298e46dd8100d28064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 30 Mar 2022 11:21:00 +0200 Subject: [PATCH 126/142] Fix theme settings on design page (#12154) --- .../src/components/demo-black-white-row.ts | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/gallery/src/components/demo-black-white-row.ts b/gallery/src/components/demo-black-white-row.ts index 549f10554c..43a58ea59e 100644 --- a/gallery/src/components/demo-black-white-row.ts +++ b/gallery/src/components/demo-black-white-row.ts @@ -53,13 +53,19 @@ class DemoBlackWhiteRow extends LitElement { firstUpdated(changedProps) { super.firstUpdated(changedProps); - applyThemesOnElement(this.shadowRoot!.querySelector(".dark"), { - default_theme: "default", - default_dark_theme: "default", - themes: {}, - darkMode: true, - theme: "default", - }); + applyThemesOnElement( + this.shadowRoot!.querySelector(".dark"), + { + default_theme: "default", + default_dark_theme: "default", + themes: {}, + darkMode: true, + theme: "default", + }, + undefined, + undefined, + true + ); } handleSubmit(ev) { From cfc1999a28c05f8c1595e7f9196c6fa943a49544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 30 Mar 2022 11:22:36 +0200 Subject: [PATCH 127/142] Allow ha-alert to be used in our markdown render (#12153) --- gallery/src/pages/lovelace/markdown-card.ts | 13 ++++++++++++- src/resources/markdown_worker.ts | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/gallery/src/pages/lovelace/markdown-card.ts b/gallery/src/pages/lovelace/markdown-card.ts index 2b6257cd23..1abca4bdcc 100644 --- a/gallery/src/pages/lovelace/markdown-card.ts +++ b/gallery/src/pages/lovelace/markdown-card.ts @@ -9,7 +9,7 @@ const CONFIGS = [ heading: "markdown-it demo", config: ` - type: markdown - content: > + content: >- # h1 Heading 8-) ## h2 Heading @@ -249,6 +249,17 @@ const CONFIGS = [ ::: warning *here be dragons* ::: + + ### ha-alert + + You can use our [\`ha-alert\`](https://design.home-assistant.io/#components/ha-alert) component in markdown content rendered in the Home Assistant Frontend. + + This is an error alert — check it out! + This is a warning alert — check it out! + This is an info alert — check it out! + This is a success alert — check it out! + This is an alert with a title + `, }, ]; diff --git a/src/resources/markdown_worker.ts b/src/resources/markdown_worker.ts index 38a069c1d7..a69bd9c02e 100644 --- a/src/resources/markdown_worker.ts +++ b/src/resources/markdown_worker.ts @@ -47,6 +47,7 @@ const renderMarkdown = ( input: ["type", "disabled", "checked"], "ha-icon": ["icon"], "ha-svg-icon": ["path"], + "ha-alert": ["alert-type", "title"], }; } From ff80ab34ee477eaa5e95b6f19f22115da94e9c84 Mon Sep 17 00:00:00 2001 From: Marius <33354141+Mariusthvdb@users.noreply.github.com> Date: Wed, 30 Mar 2022 11:35:37 +0200 Subject: [PATCH 128/142] Allow device_tracker entities to use state_color (#12127) --- src/common/style/icon_color_css.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/style/icon_color_css.ts b/src/common/style/icon_color_css.ts index c4d4c7b9ee..3f4faeed99 100644 --- a/src/common/style/icon_color_css.ts +++ b/src/common/style/icon_color_css.ts @@ -7,6 +7,7 @@ export const iconColorCSS = css` ha-state-icon[data-domain="calendar"][data-state="on"], ha-state-icon[data-domain="camera"][data-state="streaming"], ha-state-icon[data-domain="cover"][data-state="open"], + ha-state-icon[data-domain="device_tracker"][data-state="home"], ha-state-icon[data-domain="fan"][data-state="on"], ha-state-icon[data-domain="humidifier"][data-state="on"], ha-state-icon[data-domain="light"][data-state="on"], From f5af63a50ec4f0bfec88f8c60cf3c1f335af12f9 Mon Sep 17 00:00:00 2001 From: blair <1585872+blairun@users.noreply.github.com> Date: Wed, 30 Mar 2022 03:22:26 -0700 Subject: [PATCH 129/142] Automation description text overflow (#12040) Co-authored-by: Bram Kragten --- src/components/ha-textarea.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/ha-textarea.ts b/src/components/ha-textarea.ts index 2a3bf1a040..ce7c1539eb 100644 --- a/src/components/ha-textarea.ts +++ b/src/components/ha-textarea.ts @@ -19,13 +19,11 @@ export class HaTextArea extends TextAreaBase { textfieldStyles, textareaStyles, css` - :host([autogrow]) { - max-height: 200px; - } :host([autogrow]) .mdc-text-field { position: relative; min-height: 74px; min-width: 178px; + max-height: 200px; } :host([autogrow]) .mdc-text-field:after { content: attr(data-value); From 7ab54ee5ce39befca62e929cd33e9cf74664ccbc Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Wed, 30 Mar 2022 06:48:56 -0500 Subject: [PATCH 130/142] Update Pickers and selectors with required (#12151) * Update Pickers and selectors with required * Use native * for device and entity --- gallery/src/pages/components/ha-selector.ts | 2 ++ src/components/device/ha-device-picker.ts | 3 ++ src/components/device/ha-devices-picker.ts | 3 ++ src/components/entity/ha-entities-picker.ts | 3 ++ .../entity/ha-entity-attribute-picker.ts | 3 ++ src/components/entity/ha-entity-picker.ts | 3 ++ src/components/ha-addon-picker.ts | 4 +++ src/components/ha-area-picker.ts | 5 ++- src/components/ha-areas-picker.ts | 3 ++ src/components/ha-base-time-input.ts | 4 ++- src/components/ha-combo-box.ts | 4 +++ src/components/ha-date-input.ts | 3 ++ src/components/ha-icon-picker.ts | 3 ++ src/components/ha-labeled-slider.js | 7 +++- .../ha-selector/ha-selector-addon.ts | 6 ++++ .../ha-selector/ha-selector-area.ts | 4 +++ .../ha-selector/ha-selector-attribute.ts | 7 ++-- .../ha-selector/ha-selector-color-rgb.ts | 10 ++++-- .../ha-selector/ha-selector-color-temp.ts | 8 +++-- .../ha-selector/ha-selector-date.ts | 5 ++- .../ha-selector/ha-selector-datetime.ts | 12 ++++--- .../ha-selector/ha-selector-device.ts | 34 +++++++++++-------- .../ha-selector/ha-selector-entity.ts | 4 +++ .../ha-selector/ha-selector-icon.ts | 10 ++++-- .../ha-selector/ha-selector-media.ts | 3 ++ .../ha-selector/ha-selector-number.ts | 2 +- .../ha-selector/ha-selector-object.ts | 5 ++- .../ha-selector/ha-selector-select.ts | 3 +- .../ha-selector/ha-selector-theme.ts | 8 +++-- .../ha-selector/ha-selector-time.ts | 3 ++ src/components/ha-theme-picker.ts | 8 ++++- src/components/ha-time-input.ts | 5 ++- src/components/ha-yaml-editor.ts | 4 ++- .../hui-graph-footer-editor.ts | 9 +++-- .../hui-media-control-card-editor.ts | 9 +++-- 35 files changed, 159 insertions(+), 50 deletions(-) diff --git a/gallery/src/pages/components/ha-selector.ts b/gallery/src/pages/components/ha-selector.ts index af52d5c640..1995fb8208 100644 --- a/gallery/src/pages/components/ha-selector.ts +++ b/gallery/src/pages/components/ha-selector.ts @@ -363,9 +363,11 @@ class DemoHaSelector extends LitElement implements ProvideHassElement { ${value?.name || key} ${value?.description} diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts index d1a7d2b55a..ea62482637 100644 --- a/src/components/device/ha-device-picker.ts +++ b/src/components/device/ha-device-picker.ts @@ -86,6 +86,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { @property({ type: Boolean }) public disabled?: boolean; + @property({ type: Boolean }) public required?: boolean; + @state() private _opened?: boolean; @query("ha-combo-box", true) public comboBox!: HaComboBox; @@ -269,6 +271,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { .value=${this._value} .renderer=${rowRenderer} .disabled=${this.disabled} + .required=${this.required} item-value-path="id" item-label-path="name" @opened-changed=${this._openedChanged} diff --git a/src/components/device/ha-devices-picker.ts b/src/components/device/ha-devices-picker.ts index ff975abef5..9ca65e20a5 100644 --- a/src/components/device/ha-devices-picker.ts +++ b/src/components/device/ha-devices-picker.ts @@ -11,6 +11,8 @@ class HaDevicesPicker extends LitElement { @property() public value?: string[]; + @property({ type: Boolean }) public required?: boolean; + /** * Show entities from specific domains. * @type {string} @@ -66,6 +68,7 @@ class HaDevicesPicker extends LitElement { .excludeDomains=${this.excludeDomains} .includeDeviceClasses=${this.includeDeviceClasses} .label=${this.pickDeviceLabel} + .required=${this.required} @value-changed=${this._addDevice} >
    diff --git a/src/components/entity/ha-entities-picker.ts b/src/components/entity/ha-entities-picker.ts index 3f6127e303..75002c51d4 100644 --- a/src/components/entity/ha-entities-picker.ts +++ b/src/components/entity/ha-entities-picker.ts @@ -14,6 +14,8 @@ class HaEntitiesPickerLight extends LitElement { @property({ type: Array }) public value?: string[]; + @property({ type: Boolean }) public required?: boolean; + /** * Show entities from specific domains. * @type {string} @@ -108,6 +110,7 @@ class HaEntitiesPickerLight extends LitElement { .includeUnitOfMeasurement=${this.includeUnitOfMeasurement} .entityFilter=${this._entityFilter} .label=${this.pickEntityLabel} + .required=${this.required} @value-changed=${this._addEntity} >
    diff --git a/src/components/entity/ha-entity-attribute-picker.ts b/src/components/entity/ha-entity-attribute-picker.ts index 291d30da5e..49bfd1db33 100644 --- a/src/components/entity/ha-entity-attribute-picker.ts +++ b/src/components/entity/ha-entity-attribute-picker.ts @@ -19,6 +19,8 @@ class HaEntityAttributePicker extends LitElement { @property({ type: Boolean }) public disabled = false; + @property({ type: Boolean }) public required = false; + @property({ type: Boolean, attribute: "allow-custom-value" }) public allowCustomValue; @@ -61,6 +63,7 @@ class HaEntityAttributePicker extends LitElement { "ui.components.entity.entity-attribute-picker.attribute" )} .disabled=${this.disabled || !this.entityId} + .required=${this.required} .allowCustomValue=${this.allowCustomValue} item-value-path="value" item-label-path="label" diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts index 9b13c75515..c8f288ad70 100644 --- a/src/components/entity/ha-entity-picker.ts +++ b/src/components/entity/ha-entity-picker.ts @@ -39,6 +39,8 @@ export class HaEntityPicker extends LitElement { @property({ type: Boolean }) public disabled?: boolean; + @property({ type: Boolean }) public required?: boolean; + @property({ type: Boolean, attribute: "allow-custom-entity" }) public allowCustomEntity; @@ -305,6 +307,7 @@ export class HaEntityPicker extends LitElement { .allowCustomValue=${this.allowCustomEntity} .filteredItems=${this._states} .renderer=${rowRenderer} + .required=${this.required} @opened-changed=${this._openedChanged} @value-changed=${this._valueChanged} @filter-changed=${this._filterChanged} diff --git a/src/components/ha-addon-picker.ts b/src/components/ha-addon-picker.ts index a28f188de9..1dd6a11cea 100644 --- a/src/components/ha-addon-picker.ts +++ b/src/components/ha-addon-picker.ts @@ -30,6 +30,8 @@ class HaAddonPicker extends LitElement { @property({ type: Boolean }) public disabled = false; + @property({ type: Boolean }) public required = false; + @query("ha-combo-box") private _comboBox!: HaComboBox; public open() { @@ -55,6 +57,8 @@ class HaAddonPicker extends LitElement { ? this.hass.localize("ui.components.addon-picker.addon") : this.label} .value=${this._value} + .required=${this.required} + .disabled=${this.disabled} .renderer=${rowRenderer} .items=${this._addons} item-value-path="slug" diff --git a/src/components/ha-area-picker.ts b/src/components/ha-area-picker.ts index a671893737..b841575b67 100644 --- a/src/components/ha-area-picker.ts +++ b/src/components/ha-area-picker.ts @@ -28,8 +28,8 @@ import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { PolymerChangedEvent } from "../polymer-types"; import { HomeAssistant } from "../types"; import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; -import type { HaComboBox } from "./ha-combo-box"; import "./ha-combo-box"; +import type { HaComboBox } from "./ha-combo-box"; import "./ha-icon-button"; import "./ha-svg-icon"; @@ -84,6 +84,8 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { @property({ type: Boolean }) public disabled?: boolean; + @property({ type: Boolean }) public required?: boolean; + @state() private _areas?: AreaRegistryEntry[]; @state() private _devices?: DeviceRegistryEntry[]; @@ -315,6 +317,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { item-label-path="name" .value=${this.value} .disabled=${this.disabled} + .required=${this.required} .label=${this.label === undefined && this.hass ? this.hass.localize("ui.components.area-picker.area") : this.label} diff --git a/src/components/ha-areas-picker.ts b/src/components/ha-areas-picker.ts index fbf4a9287f..24b4e4c2b0 100644 --- a/src/components/ha-areas-picker.ts +++ b/src/components/ha-areas-picker.ts @@ -56,6 +56,8 @@ export class HaAreasPicker extends SubscribeMixin(LitElement) { @property({ type: Boolean }) public disabled?: boolean; + @property({ type: Boolean }) public required?: boolean; + protected render(): TemplateResult { if (!this.hass) { return html``; @@ -95,6 +97,7 @@ export class HaAreasPicker extends SubscribeMixin(LitElement) { .entityFilter=${this.entityFilter} .disabled=${this.disabled} .placeholder=${this.placeholder} + .required=${this.required} @value-changed=${this._addArea} >
    diff --git a/src/components/ha-base-time-input.ts b/src/components/ha-base-time-input.ts index 9b102faee5..c54739d65d 100644 --- a/src/components/ha-base-time-input.ts +++ b/src/components/ha-base-time-input.ts @@ -129,7 +129,9 @@ export class HaBaseTimeInput extends LitElement { protected render(): TemplateResult { return html` - ${this.label ? html`` : ""} + ${this.label + ? html`` + : ""}
    ${this.enableDay ? html` diff --git a/src/components/ha-combo-box.ts b/src/components/ha-combo-box.ts index f8f1bd5cde..7c44541bcc 100644 --- a/src/components/ha-combo-box.ts +++ b/src/components/ha-combo-box.ts @@ -87,6 +87,8 @@ export class HaComboBox extends LitElement { @property({ type: Boolean }) public disabled?: boolean; + @property({ type: Boolean }) public required?: boolean; + @property({ type: Boolean, reflect: true, attribute: "opened" }) private _opened?: boolean; @@ -119,6 +121,7 @@ export class HaComboBox extends LitElement { .filteredItems=${this.filteredItems} .allowCustomValue=${this.allowCustomValue} .disabled=${this.disabled} + .required=${this.required} ${comboBoxRenderer(this.renderer || this._defaultRowRenderer)} @opened-changed=${this._openedChanged} @filter-changed=${this._filterChanged} @@ -129,6 +132,7 @@ export class HaComboBox extends LitElement { .label=${this.label} .placeholder=${this.placeholder} .disabled=${this.disabled} + .required=${this.required} .validationMessage=${this.validationMessage} .errorMessage=${this.errorMessage} class="input" diff --git a/src/components/ha-date-input.ts b/src/components/ha-date-input.ts index 7575e3bcef..0963b1fd02 100644 --- a/src/components/ha-date-input.ts +++ b/src/components/ha-date-input.ts @@ -35,6 +35,8 @@ export class HaDateInput extends LitElement { @property({ type: Boolean }) public disabled = false; + @property({ type: Boolean }) public required = false; + @property() public label?: string; render() { @@ -46,6 +48,7 @@ export class HaDateInput extends LitElement { .value=${this.value ? formatDateNumeric(new Date(this.value), this.locale) : ""} + .required=${this.required} > `; diff --git a/src/components/ha-icon-picker.ts b/src/components/ha-icon-picker.ts index 57ec368cdc..719976bde2 100644 --- a/src/components/ha-icon-picker.ts +++ b/src/components/ha-icon-picker.ts @@ -39,6 +39,8 @@ export class HaIconPicker extends LitElement { @property({ type: Boolean }) public disabled = false; + @property({ type: Boolean }) public required = false; + @property({ type: Boolean }) public invalid = false; @state() private _opened = false; @@ -56,6 +58,7 @@ export class HaIconPicker extends LitElement { .filteredItems=${iconItems} .label=${this.label} .disabled=${this.disabled} + .required=${this.required} .placeholder=${this.placeholder} .errorMessage=${this.errorMessage} .invalid=${this.invalid} diff --git a/src/components/ha-labeled-slider.js b/src/components/ha-labeled-slider.js index f029b2f21f..af7134ab13 100644 --- a/src/components/ha-labeled-slider.js +++ b/src/components/ha-labeled-slider.js @@ -33,7 +33,7 @@ class HaLabeledSlider extends PolymerElement { } -
    [[caption]]
    +
    [[_getTitle()]]
    @@ -49,10 +49,15 @@ class HaLabeledSlider extends PolymerElement { `; } + _getTitle() { + return `${this.caption}${this.required ? "*" : ""}`; + } + static get properties() { return { caption: String, disabled: Boolean, + required: Boolean, min: Number, max: Number, pin: Boolean, diff --git a/src/components/ha-selector/ha-selector-addon.ts b/src/components/ha-selector/ha-selector-addon.ts index 47bb9c045b..acabb24cf5 100644 --- a/src/components/ha-selector/ha-selector-addon.ts +++ b/src/components/ha-selector/ha-selector-addon.ts @@ -14,11 +14,17 @@ export class HaAddonSelector extends LitElement { @property() public label?: string; + @property({ type: Boolean }) public disabled = false; + + @property({ type: Boolean }) public required = true; + protected render() { return html``; } diff --git a/src/components/ha-selector/ha-selector-area.ts b/src/components/ha-selector/ha-selector-area.ts index e6926c9701..8aaada7e23 100644 --- a/src/components/ha-selector/ha-selector-area.ts +++ b/src/components/ha-selector/ha-selector-area.ts @@ -22,6 +22,8 @@ export class HaAreaSelector extends LitElement { @property({ type: Boolean }) public disabled = false; + @property({ type: Boolean }) public required = true; + protected updated(changedProperties) { if (changedProperties.has("selector")) { const oldSelector = changedProperties.get("selector"); @@ -55,6 +57,7 @@ export class HaAreaSelector extends LitElement { ? [this.selector.area.entity.domain] : undefined} .disabled=${this.disabled} + .required=${this.required} > `; } @@ -74,6 +77,7 @@ export class HaAreaSelector extends LitElement { ? [this.selector.area.entity.domain] : undefined} .disabled=${this.disabled} + .required=${this.required} > `; } diff --git a/src/components/ha-selector/ha-selector-attribute.ts b/src/components/ha-selector/ha-selector-attribute.ts index a75f9bb5ec..06baa1e6b6 100644 --- a/src/components/ha-selector/ha-selector-attribute.ts +++ b/src/components/ha-selector/ha-selector-attribute.ts @@ -1,10 +1,10 @@ -import "../entity/ha-entity-attribute-picker"; import { html, LitElement, PropertyValues } from "lit"; import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../../common/dom/fire_event"; import { AttributeSelector } from "../../data/selector"; import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../types"; -import { fireEvent } from "../../common/dom/fire_event"; +import "../entity/ha-entity-attribute-picker"; @customElement("ha-selector-attribute") export class HaSelectorAttribute extends SubscribeMixin(LitElement) { @@ -18,6 +18,8 @@ export class HaSelectorAttribute extends SubscribeMixin(LitElement) { @property({ type: Boolean }) public disabled = false; + @property({ type: Boolean }) public required = true; + @property() public context?: { filter_entity?: string; }; @@ -31,6 +33,7 @@ export class HaSelectorAttribute extends SubscribeMixin(LitElement) { .value=${this.value} .label=${this.label} .disabled=${this.disabled} + .required=${this.required} allow-custom-value > `; diff --git a/src/components/ha-selector/ha-selector-color-rgb.ts b/src/components/ha-selector/ha-selector-color-rgb.ts index 5357a80db3..780d83c832 100644 --- a/src/components/ha-selector/ha-selector-color-rgb.ts +++ b/src/components/ha-selector/ha-selector-color-rgb.ts @@ -1,9 +1,9 @@ import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; -import type { HomeAssistant } from "../../types"; -import type { ColorRGBSelector } from "../../data/selector"; -import { fireEvent } from "../../common/dom/fire_event"; import { hex2rgb, rgb2hex } from "../../common/color/convert-color"; +import { fireEvent } from "../../common/dom/fire_event"; +import type { ColorRGBSelector } from "../../data/selector"; +import type { HomeAssistant } from "../../types"; import "../ha-textfield"; @customElement("ha-selector-color_rgb") @@ -18,12 +18,16 @@ export class HaColorRGBSelector extends LitElement { @property({ type: Boolean, reflect: true }) public disabled = false; + @property({ type: Boolean }) public required = true; + protected render() { return html` `; diff --git a/src/components/ha-selector/ha-selector-color-temp.ts b/src/components/ha-selector/ha-selector-color-temp.ts index 684a813225..67ac8abcc7 100644 --- a/src/components/ha-selector/ha-selector-color-temp.ts +++ b/src/components/ha-selector/ha-selector-color-temp.ts @@ -1,8 +1,8 @@ import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; -import type { HomeAssistant } from "../../types"; -import type { ColorTempSelector } from "../../data/selector"; import { fireEvent } from "../../common/dom/fire_event"; +import type { ColorTempSelector } from "../../data/selector"; +import type { HomeAssistant } from "../../types"; import "../ha-labeled-slider"; @customElement("ha-selector-color_temp") @@ -17,6 +17,8 @@ export class HaColorTempSelector extends LitElement { @property({ type: Boolean, reflect: true }) public disabled = false; + @property({ type: Boolean }) public required = true; + protected render() { return html` `; diff --git a/src/components/ha-selector/ha-selector-date.ts b/src/components/ha-selector/ha-selector-date.ts index d8b5742f90..47fa5b7d91 100644 --- a/src/components/ha-selector/ha-selector-date.ts +++ b/src/components/ha-selector/ha-selector-date.ts @@ -1,7 +1,7 @@ import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; -import type { HomeAssistant } from "../../types"; import type { DateSelector } from "../../data/selector"; +import type { HomeAssistant } from "../../types"; import "../ha-date-input"; @customElement("ha-selector-date") @@ -16,6 +16,8 @@ export class HaDateSelector extends LitElement { @property({ type: Boolean, reflect: true }) public disabled = false; + @property({ type: Boolean }) public required = true; + protected render() { return html` `; diff --git a/src/components/ha-selector/ha-selector-datetime.ts b/src/components/ha-selector/ha-selector-datetime.ts index caee6d8457..c8d1dab2d3 100644 --- a/src/components/ha-selector/ha-selector-datetime.ts +++ b/src/components/ha-selector/ha-selector-datetime.ts @@ -1,12 +1,12 @@ import { css, html, LitElement } from "lit"; import { customElement, property, query } from "lit/decorators"; -import type { HomeAssistant } from "../../types"; -import type { DateTimeSelector } from "../../data/selector"; -import type { HaDateInput } from "../ha-date-input"; -import type { HaTimeInput } from "../ha-time-input"; import { fireEvent } from "../../common/dom/fire_event"; +import type { DateTimeSelector } from "../../data/selector"; +import type { HomeAssistant } from "../../types"; import "../ha-date-input"; +import type { HaDateInput } from "../ha-date-input"; import "../ha-time-input"; +import type { HaTimeInput } from "../ha-time-input"; @customElement("ha-selector-datetime") export class HaDateTimeSelector extends LitElement { @@ -20,6 +20,8 @@ export class HaDateTimeSelector extends LitElement { @property({ type: Boolean, reflect: true }) public disabled = false; + @property({ type: Boolean }) public required = true; + @query("ha-date-input") private _dateInput!: HaDateInput; @query("ha-time-input") private _timeInput!: HaTimeInput; @@ -32,6 +34,7 @@ export class HaDateTimeSelector extends LitElement { .label=${this.label} .locale=${this.hass.locale} .disabled=${this.disabled} + .required=${this.required} .value=${values?.[0]} @value-changed=${this._valueChanged} > @@ -41,6 +44,7 @@ export class HaDateTimeSelector extends LitElement { .value=${values?.[1] || "0:00:00"} .locale=${this.hass.locale} .disabled=${this.disabled} + .required=${this.required} @value-changed=${this._valueChanged} > `; diff --git a/src/components/ha-selector/ha-selector-device.ts b/src/components/ha-selector/ha-selector-device.ts index 945c0de795..efb5dee013 100644 --- a/src/components/ha-selector/ha-selector-device.ts +++ b/src/components/ha-selector/ha-selector-device.ts @@ -21,6 +21,8 @@ export class HaDeviceSelector extends LitElement { @property({ type: Boolean }) public disabled = false; + @property({ type: Boolean }) public required = true; + protected updated(changedProperties) { if (changedProperties.has("selector")) { const oldSelector = changedProperties.get("selector"); @@ -36,20 +38,23 @@ export class HaDeviceSelector extends LitElement { protected render() { if (!this.selector.device.multiple) { - return html` `; + return html` + + `; } return html` @@ -63,6 +68,7 @@ export class HaDeviceSelector extends LitElement { .includeDomains=${this.selector.device.entity?.domain ? [this.selector.device.entity.domain] : undefined} + .required=${this.required} > `; } diff --git a/src/components/ha-selector/ha-selector-entity.ts b/src/components/ha-selector/ha-selector-entity.ts index 220550cf34..84f060ea0d 100644 --- a/src/components/ha-selector/ha-selector-entity.ts +++ b/src/components/ha-selector/ha-selector-entity.ts @@ -25,6 +25,8 @@ export class HaEntitySelector extends LitElement { @property({ type: Boolean }) public disabled = false; + @property({ type: Boolean }) public required = true; + protected render() { if (!this.selector.entity.multiple) { return html``; } @@ -47,6 +50,7 @@ export class HaEntitySelector extends LitElement { .entityFilter=${this._filterEntities} .includeEntities=${this.selector.entity.include_entities} .excludeEntities=${this.selector.entity.exclude_entities} + .required=${this.required} > `; } diff --git a/src/components/ha-selector/ha-selector-icon.ts b/src/components/ha-selector/ha-selector-icon.ts index 0e4a712588..69e11ed0ad 100644 --- a/src/components/ha-selector/ha-selector-icon.ts +++ b/src/components/ha-selector/ha-selector-icon.ts @@ -1,9 +1,9 @@ -import "../ha-icon-picker"; import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; -import { HomeAssistant } from "../../types"; -import { IconSelector } from "../../data/selector"; import { fireEvent } from "../../common/dom/fire_event"; +import { IconSelector } from "../../data/selector"; +import { HomeAssistant } from "../../types"; +import "../ha-icon-picker"; @customElement("ha-selector-icon") export class HaIconSelector extends LitElement { @@ -17,11 +17,15 @@ export class HaIconSelector extends LitElement { @property({ type: Boolean, reflect: true }) public disabled = false; + @property({ type: Boolean }) public required = true; + protected render() { return html` ) { @@ -84,6 +86,7 @@ export class HaMediaSelector extends LitElement { .label=${this.label || this.hass.localize("ui.components.selectors.media.pick_media_player")} .disabled=${this.disabled} + .required=${this.required} include-domains='["media_player"]' allow-custom-entity @value-changed=${this._entityChanged} diff --git a/src/components/ha-selector/ha-selector-number.ts b/src/components/ha-selector/ha-selector-number.ts index ef3bc63fc0..84e8f0e12f 100644 --- a/src/components/ha-selector/ha-selector-number.ts +++ b/src/components/ha-selector/ha-selector-number.ts @@ -27,7 +27,7 @@ export class HaNumberSelector extends LitElement { protected render() { return html`${this.selector.number.mode !== "box" - ? html`${this.label} diff --git a/src/components/ha-selector/ha-selector-theme.ts b/src/components/ha-selector/ha-selector-theme.ts index f90362b552..eccf3c5b23 100644 --- a/src/components/ha-selector/ha-selector-theme.ts +++ b/src/components/ha-selector/ha-selector-theme.ts @@ -1,8 +1,8 @@ -import "../ha-theme-picker"; import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; -import type { HomeAssistant } from "../../types"; import type { ThemeSelector } from "../../data/selector"; +import type { HomeAssistant } from "../../types"; +import "../ha-theme-picker"; @customElement("ha-selector-theme") export class HaThemeSelector extends LitElement { @@ -16,12 +16,16 @@ export class HaThemeSelector extends LitElement { @property({ type: Boolean, reflect: true }) public disabled = false; + @property({ type: Boolean }) public required = true; + protected render() { return html` `; } diff --git a/src/components/ha-selector/ha-selector-time.ts b/src/components/ha-selector/ha-selector-time.ts index ff829ff12b..caf2ee0d4d 100644 --- a/src/components/ha-selector/ha-selector-time.ts +++ b/src/components/ha-selector/ha-selector-time.ts @@ -16,12 +16,15 @@ export class HaTimeSelector extends LitElement { @property({ type: Boolean }) public disabled = false; + @property({ type: Boolean }) public required = false; + protected render() { return html` diff --git a/src/components/ha-theme-picker.ts b/src/components/ha-theme-picker.ts index 093865d3a7..af064958b5 100644 --- a/src/components/ha-theme-picker.ts +++ b/src/components/ha-theme-picker.ts @@ -4,8 +4,8 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; import { stopPropagation } from "../common/dom/stop_propagation"; -import "./ha-select"; import { HomeAssistant } from "../types"; +import "./ha-select"; @customElement("ha-theme-picker") export class HaThemePicker extends LitElement { @@ -15,12 +15,18 @@ export class HaThemePicker extends LitElement { @property({ attribute: false }) public hass?: HomeAssistant; + @property({ type: Boolean, reflect: true }) public disabled = false; + + @property({ type: Boolean }) public required = false; + protected render(): TemplateResult { return html` `; } diff --git a/src/components/ha-yaml-editor.ts b/src/components/ha-yaml-editor.ts index 8f285cb439..d4a30cc158 100644 --- a/src/components/ha-yaml-editor.ts +++ b/src/components/ha-yaml-editor.ts @@ -33,6 +33,8 @@ export class HaYamlEditor extends LitElement { @property({ type: Boolean }) public readOnly = false; + @property({ type: Boolean }) public required = false; + @state() private _yaml = ""; public setValue(value): void { @@ -59,7 +61,7 @@ export class HaYamlEditor extends LitElement { return html``; } return html` - ${this.label ? html`

    ${this.label}

    ` : ""} + ${this.label ? html`

    ${this.label}${this.required ? "*" : ""}

    ` : ""}
    diff --git a/src/panels/lovelace/editor/config-elements/hui-media-control-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-media-control-card-editor.ts index 9732c5b7a4..9470be0a78 100644 --- a/src/panels/lovelace/editor/config-elements/hui-media-control-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-media-control-card-editor.ts @@ -3,9 +3,9 @@ import { customElement, property, state } from "lit/decorators"; import { assert, assign, object, optional, string } from "superstruct"; import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/entity/ha-entity-picker"; +import "../../../../components/ha-theme-picker"; import { HomeAssistant } from "../../../../types"; import { MediaControlCardConfig } from "../../cards/types"; -import "../../../../components/ha-theme-picker"; import { LovelaceCardEditor } from "../../types"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { EditorTarget, EntitiesEditorEvent } from "../types"; @@ -50,15 +50,14 @@ export class HuiMediaControlCardEditor return html`
    From 624cb48f7887b871203fcf7f9962be9db77627e5 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 30 Mar 2022 13:53:28 +0200 Subject: [PATCH 131/142] Add support for my links to create a helper config entry (#12155) --- .../config/helpers/dialog-helper-detail.ts | 2 +- .../config/helpers/ha-config-helpers.ts | 69 +++++++++++++++++++ .../helpers/show-dialog-helper-detail.ts | 3 +- .../integrations/ha-config-integrations.ts | 14 ++++ 4 files changed, 86 insertions(+), 2 deletions(-) diff --git a/src/panels/config/helpers/dialog-helper-detail.ts b/src/panels/config/helpers/dialog-helper-detail.ts index c3adf6a826..1a2833636e 100644 --- a/src/panels/config/helpers/dialog-helper-detail.ts +++ b/src/panels/config/helpers/dialog-helper-detail.ts @@ -66,7 +66,7 @@ export class DialogHelperDetail extends LitElement { public async showDialog(params: ShowDialogHelperDetailParams): Promise { this._params = params; - this._domain = undefined; + this._domain = params.domain; this._item = undefined; this._opened = true; await this.updateComplete; diff --git a/src/panels/config/helpers/ha-config-helpers.ts b/src/panels/config/helpers/ha-config-helpers.ts index 4ce9d00118..4173fd69ef 100644 --- a/src/panels/config/helpers/ha-config-helpers.ts +++ b/src/panels/config/helpers/ha-config-helpers.ts @@ -29,6 +29,14 @@ import { showEntityEditorDialog } from "../entities/show-dialog-entity-editor"; import { configSections } from "../ha-panel-config"; import { HELPER_DOMAINS } from "./const"; import { showHelperDetailDialog } from "./show-dialog-helper-detail"; +import { navigate } from "../../../common/navigate"; +import { extractSearchParam } from "../../../common/url/search-params"; +import { getConfigFlowHandlers } from "../../../data/config_flow"; +import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../../dialogs/generic/show-dialog-box"; // This groups items by a key but only returns last entry per key. const groupByOne = ( @@ -219,6 +227,67 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); this._getConfigEntries(); + if (this.route.path === "/add") { + this._handleAdd(); + } + } + + private async _handleAdd() { + const domain = extractSearchParam("domain"); + navigate("/config/helpers", { replace: true }); + if (!domain) { + return; + } + if (HELPER_DOMAINS.includes(domain)) { + showHelperDetailDialog(this, { + domain, + }); + return; + } + const handlers = await getConfigFlowHandlers(this.hass, "helper"); + + if (!handlers.includes(domain)) { + const integrations = await getConfigFlowHandlers( + this.hass, + "integration" + ); + if (integrations.includes(domain)) { + navigate(`/config/integrations/add?domain=${domain}`, { + replace: true, + }); + return; + } + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.integrations.config_flow.error" + ), + text: this.hass.localize( + "ui.panel.config.integrations.config_flow.no_config_flow" + ), + }); + return; + } + const localize = await this.hass.loadBackendTranslation( + "title", + domain, + true + ); + if ( + !(await showConfirmationDialog(this, { + title: this.hass.localize("ui.panel.config.integrations.confirm_new", { + integration: domainToName(localize, domain), + }), + })) + ) { + return; + } + showConfigFlowDialog(this, { + dialogClosedCallback: () => { + this._getConfigEntries(); + }, + startFlowHandler: domain, + showAdvanced: this.hass.userData?.showAdvanced, + }); } protected willUpdate(changedProps: PropertyValues) { diff --git a/src/panels/config/helpers/show-dialog-helper-detail.ts b/src/panels/config/helpers/show-dialog-helper-detail.ts index 83fbbce4ee..bbee0bc619 100644 --- a/src/panels/config/helpers/show-dialog-helper-detail.ts +++ b/src/panels/config/helpers/show-dialog-helper-detail.ts @@ -4,8 +4,9 @@ import { DataEntryFlowDialogParams } from "../../../dialogs/config-flow/show-dia export const loadHelperDetailDialog = () => import("./dialog-helper-detail"); export interface ShowDialogHelperDetailParams { + domain?: string; // Only used for config entries - dialogClosedCallback: DataEntryFlowDialogParams["dialogClosedCallback"]; + dialogClosedCallback?: DataEntryFlowDialogParams["dialogClosedCallback"]; } export const showHelperDetailDialog = ( diff --git a/src/panels/config/integrations/ha-config-integrations.ts b/src/panels/config/integrations/ha-config-integrations.ts index c860953efc..a78eb0e94b 100644 --- a/src/panels/config/integrations/ha-config-integrations.ts +++ b/src/panels/config/integrations/ha-config-integrations.ts @@ -67,6 +67,7 @@ import "./ha-ignored-config-entry-card"; import "./ha-integration-card"; import type { HaIntegrationCard } from "./ha-integration-card"; import { fetchDiagnosticHandlers } from "../../../data/diagnostics"; +import { HELPER_DOMAINS } from "../helpers/const"; export interface ConfigEntryUpdatedEvent { entry: ConfigEntry; @@ -661,6 +662,19 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { const handlers = await getConfigFlowHandlers(this.hass, "integration"); if (!handlers.includes(domain)) { + if (HELPER_DOMAINS.includes(domain)) { + navigate(`/config/helpers/add?domain=${domain}`, { + replace: true, + }); + return; + } + const helpers = await getConfigFlowHandlers(this.hass, "helper"); + if (helpers.includes(domain)) { + navigate(`/config/helpers/add?domain=${domain}`, { + replace: true, + }); + return; + } showAlertDialog(this, { title: this.hass.localize( "ui.panel.config.integrations.config_flow.error" From 505c22248bc8c163c697ee05deb78236de0725e2 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 30 Mar 2022 13:53:37 +0200 Subject: [PATCH 132/142] Use brand icon instead of domain icon for helpers (#12157) --- src/panels/config/helpers/dialog-helper-detail.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/panels/config/helpers/dialog-helper-detail.ts b/src/panels/config/helpers/dialog-helper-detail.ts index 1a2833636e..a1e303c06b 100644 --- a/src/panels/config/helpers/dialog-helper-detail.ts +++ b/src/panels/config/helpers/dialog-helper-detail.ts @@ -6,7 +6,6 @@ import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { dynamicElement } from "../../../common/dom/dynamic-element-directive"; -import { domainIcon } from "../../../common/entity/domain_icon"; import "../../../components/ha-dialog"; import "../../../components/ha-circular-progress"; import { getConfigFlowHandlers } from "../../../data/config_flow"; @@ -32,6 +31,7 @@ import "./forms/ha-input_text-form"; import "./forms/ha-timer-form"; import { domainToName } from "../../../data/integration"; import type { ShowDialogHelperDetailParams } from "./show-dialog-helper-detail"; +import { brandsUrl } from "../../../util/brands-url"; const HELPERS = { input_boolean: createInputBoolean, @@ -146,10 +146,17 @@ export class DialogHelperDetail extends LitElement { dialogInitialFocus graphic="icon" > - + loading="lazy" + src=${brandsUrl({ + domain, + type: "icon", + useFallback: true, + darkOptimized: this.hass.themes?.darkMode, + })} + referrerpolicy="no-referrer" + /> ${label} ${!isLoaded From ba235ac797c5a80a88d136506e69034827ca2b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 30 Mar 2022 14:22:24 +0200 Subject: [PATCH 133/142] Import components that are allowed to be defined in markdown (#12158) --- src/components/ha-markdown.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/ha-markdown.ts b/src/components/ha-markdown.ts index 2b628c1e04..4183fba672 100644 --- a/src/components/ha-markdown.ts +++ b/src/components/ha-markdown.ts @@ -2,6 +2,11 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import "./ha-markdown-element"; +// Import components that are allwoed to be defined. +import "./ha-alert"; +import "./ha-icon"; +import "./ha-svg-icon"; + @customElement("ha-markdown") export class HaMarkdown extends LitElement { @property() public content?; From 062f21aa916e2a6c9f8e47f3482c51bff1f0d71b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 30 Mar 2022 14:34:37 +0200 Subject: [PATCH 134/142] Add options to selectors gallery (#12156) --- gallery/src/pages/components/ha-selector.ts | 44 ++++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/gallery/src/pages/components/ha-selector.ts b/gallery/src/pages/components/ha-selector.ts index 1995fb8208..f4a5500d93 100644 --- a/gallery/src/pages/components/ha-selector.ts +++ b/gallery/src/pages/components/ha-selector.ts @@ -211,6 +211,12 @@ const SCHEMAS: { class DemoHaSelector extends LitElement implements ProvideHassElement { @state() public hass!: HomeAssistant; + @state() private _disabled = false; + + @state() private _required = false; + + @state() private _label = true; + private data = SCHEMAS.map(() => ({})); constructor() { @@ -344,6 +350,29 @@ class DemoHaSelector extends LitElement implements ProvideHassElement { protected render(): TemplateResult { return html` +
    + + + + + + + + + +
    ${SCHEMAS.map((info, idx) => { const data = this.data[idx]; const valueChanged = (ev) => { @@ -363,12 +392,13 @@ class DemoHaSelector extends LitElement implements ProvideHassElement { ${value?.name || key} ${value?.description} @@ -381,10 +411,20 @@ class DemoHaSelector extends LitElement implements ProvideHassElement { `; } + private _handleOptionChange(ev) { + this[`_${ev.target.name}`] = ev.target.checked; + } + static styles = css` ha-selector { width: 60; } + .options { + padding: 16px 48px; + } + .options ha-formfield { + margin-right: 16px; + } `; } From 86afd883a5e7738dbac1a8a922b8e99e7e1267e6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 30 Mar 2022 15:55:54 +0200 Subject: [PATCH 135/142] Add helpers to list when searching in add integration (#12159) --- .../config-flow/dialog-data-entry-flow.ts | 3 +- .../config-flow/show-dialog-config-flow.ts | 12 +--- .../show-dialog-data-entry-flow.ts | 6 +- .../config-flow/step-flow-pick-handler.ts | 59 +++++++++++++++---- 4 files changed, 56 insertions(+), 24 deletions(-) diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index 836e699ff6..1d84b6efed 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -35,6 +35,7 @@ import { documentationUrl } from "../../util/documentation-url"; import { showAlertDialog } from "../generic/show-dialog-box"; import { DataEntryFlowDialogParams, + FlowHandlers, LoadingReason, } from "./show-dialog-data-entry-flow"; import "./step-flow-abort"; @@ -85,7 +86,7 @@ class DataEntryFlowDialog extends LitElement { @state() private _areas?: AreaRegistryEntry[]; - @state() private _handlers?: string[]; + @state() private _handlers?: FlowHandlers; @state() private _handler?: string; diff --git a/src/dialogs/config-flow/show-dialog-config-flow.ts b/src/dialogs/config-flow/show-dialog-config-flow.ts index 937dabdac2..53c92fd214 100644 --- a/src/dialogs/config-flow/show-dialog-config-flow.ts +++ b/src/dialogs/config-flow/show-dialog-config-flow.ts @@ -1,5 +1,4 @@ import { html } from "lit"; -import { caseInsensitiveStringCompare } from "../../common/string/compare"; import { createConfigFlow, deleteConfigFlow, @@ -23,17 +22,12 @@ export const showConfigFlowDialog = ( showFlowDialog(element, dialogParams, { loadDevicesAndAreas: true, getFlowHandlers: async (hass) => { - const [handlers] = await Promise.all([ + const [integrations, helpers] = await Promise.all([ getConfigFlowHandlers(hass, "integration"), + getConfigFlowHandlers(hass, "helper"), hass.loadBackendTranslation("title", undefined, true), ]); - - return handlers.sort((handlerA, handlerB) => - caseInsensitiveStringCompare( - domainToName(hass.localize, handlerA), - domainToName(hass.localize, handlerB) - ) - ); + return { integrations, helpers }; }, createFlow: async (hass, handler) => { const [step] = await Promise.all([ diff --git a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts index 78346468e5..35f6630216 100644 --- a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts @@ -12,10 +12,14 @@ import { } from "../../data/data_entry_flow"; import { HomeAssistant } from "../../types"; +export interface FlowHandlers { + integrations: string[]; + helpers: string[]; +} export interface FlowConfig { loadDevicesAndAreas: boolean; - getFlowHandlers?: (hass: HomeAssistant) => Promise; + getFlowHandlers?: (hass: HomeAssistant) => Promise; createFlow(hass: HomeAssistant, handler: string): Promise; diff --git a/src/dialogs/config-flow/step-flow-pick-handler.ts b/src/dialogs/config-flow/step-flow-pick-handler.ts index 0beb4ac26f..e40158dd97 100644 --- a/src/dialogs/config-flow/step-flow-pick-handler.ts +++ b/src/dialogs/config-flow/step-flow-pick-handler.ts @@ -26,11 +26,13 @@ import { HomeAssistant } from "../../types"; import { brandsUrl } from "../../util/brands-url"; import { documentationUrl } from "../../util/documentation-url"; import { configFlowContentStyles } from "./styles"; +import { FlowHandlers } from "./show-dialog-data-entry-flow"; interface HandlerObj { name: string; slug: string; is_add?: boolean; + is_helper?: boolean; } declare global { @@ -46,7 +48,7 @@ declare global { class StepFlowPickHandler extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public handlers!: string[]; + @property({ attribute: false }) public handlers!: FlowHandlers; @property() public initialFilter?: string; @@ -57,8 +59,12 @@ class StepFlowPickHandler extends LitElement { private _height?: number; private _filterHandlers = memoizeOne( - (h: string[], filter?: string, _localize?: LocalizeFunc) => { - const handlers: HandlerObj[] = h.map((handler) => ({ + ( + h: FlowHandlers, + filter?: string, + _localize?: LocalizeFunc + ): [HandlerObj[], HandlerObj[]] => { + const integrations: HandlerObj[] = h.integrations.map((handler) => ({ name: domainToName(this.hass.localize, handler), slug: handler, })); @@ -70,17 +76,31 @@ class StepFlowPickHandler extends LitElement { minMatchCharLength: 2, threshold: 0.2, }; - const fuse = new Fuse(handlers, options); - return fuse.search(filter).map((result) => result.item); + const helpers: HandlerObj[] = h.helpers.map((handler) => ({ + name: domainToName(this.hass.localize, handler), + slug: handler, + is_helper: true, + })); + return [ + new Fuse(integrations, options) + .search(filter) + .map((result) => result.item), + new Fuse(helpers, options) + .search(filter) + .map((result) => result.item), + ]; } - return handlers.sort((a, b) => - caseInsensitiveStringCompare(a.name, b.name) - ); + return [ + integrations.sort((a, b) => + caseInsensitiveStringCompare(a.name, b.name) + ), + [], + ]; } ); protected render(): TemplateResult { - const handlers = this._getHandlers(); + const [integrations, helpers] = this._getHandlers(); const addDeviceRows: HandlerObj[] = ["zha", "zwave_js"] .filter((domain) => isComponentLoaded(this.hass, domain)) @@ -115,8 +135,8 @@ class StepFlowPickHandler extends LitElement { ` : ""} - ${handlers.length - ? handlers.map((handler) => this._renderRow(handler)) + ${integrations.length + ? integrations.map((handler) => this._renderRow(handler)) : html`

    ${this.hass.localize( @@ -139,6 +159,12 @@ class StepFlowPickHandler extends LitElement { >.

    `} + ${helpers.length + ? html` + + ${helpers.map((handler) => this._renderRow(handler))} + ` + : ""} `; } @@ -162,7 +188,7 @@ class StepFlowPickHandler extends LitElement { })} referrerpolicy="no-referrer" /> - ${handler.name} + ${handler.name} ${handler.is_helper ? " (helper)" : ""} ${handler.is_add ? "" : html``} `; @@ -236,6 +262,13 @@ class StepFlowPickHandler extends LitElement { return; } + if (handler.is_helper) { + navigate(`/config/helpers/add?domain=${handler.slug}`); + // This closes dialog. + fireEvent(this, "flow-update"); + return; + } + fireEvent(this, "handler-picked", { handler: handler.slug, }); @@ -250,7 +283,7 @@ class StepFlowPickHandler extends LitElement { if (handlers.length > 0) { fireEvent(this, "handler-picked", { - handler: handlers[0].slug, + handler: handlers[0][0].slug, }); } } From 944422890726a775749cc722f33b3093ecea3d3e Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Wed, 30 Mar 2022 11:14:39 -0500 Subject: [PATCH 136/142] List Selector (#12099) Co-authored-by: Bram Kragten --- gallery/src/pages/components/ha-selector.ts | 50 +++- src/components/ha-chip-set.ts | 8 +- src/components/ha-chip.ts | 17 +- src/components/ha-combo-box.ts | 6 +- .../ha-selector/ha-selector-select.ts | 256 +++++++++++++++--- src/data/selector.ts | 4 + 6 files changed, 294 insertions(+), 47 deletions(-) diff --git a/gallery/src/pages/components/ha-selector.ts b/gallery/src/pages/components/ha-selector.ts index f4a5500d93..15482b68d0 100644 --- a/gallery/src/pages/components/ha-selector.ts +++ b/gallery/src/pages/components/ha-selector.ts @@ -109,7 +109,7 @@ const AREAS = [ const SCHEMAS: { name: string; - input: Record; + input: Record; }[] = [ { name: "One of each", @@ -166,7 +166,9 @@ const SCHEMAS: { object: { name: "Object", selector: { object: {} } }, select_radio: { name: "Select (Radio)", - selector: { select: { options: ["Option 1", "Option 2"] } }, + selector: { + select: { options: ["Option 1", "Option 2"], mode: "list" }, + }, }, select: { name: "Select", @@ -183,6 +185,22 @@ const SCHEMAS: { }, }, }, + select_custom: { + name: "Select (Custom)", + selector: { + select: { + custom_value: true, + options: [ + "Option 1", + "Option 2", + "Option 3", + "Option 4", + "Option 5", + "Option 6", + ], + }, + }, + }, icon: { name: "Icon", selector: { icon: {} } }, media: { name: "Media", selector: { media: {} } }, location: { name: "Location", selector: { location: {} } }, @@ -203,6 +221,34 @@ const SCHEMAS: { entity: { name: "Entity", selector: { entity: { multiple: true } } }, device: { name: "Device", selector: { device: { multiple: true } } }, area: { name: "Area", selector: { area: { multiple: true } } }, + select: { + name: "Select Multiple", + selector: { + select: { + multiple: true, + custom_value: true, + options: [ + "Option 1", + "Option 2", + "Option 3", + "Option 4", + "Option 5", + "Option 6", + ], + }, + }, + }, + select_checkbox: { + name: "Select Multiple (Checkbox)", + required: false, + selector: { + select: { + mode: "list", + multiple: true, + options: ["Option 1", "Option 2", "Option 3", "Option 4"], + }, + }, + }, }, }, ]; diff --git a/src/components/ha-chip-set.ts b/src/components/ha-chip-set.ts index 2e659c8353..699f1b0a73 100644 --- a/src/components/ha-chip-set.ts +++ b/src/components/ha-chip-set.ts @@ -25,13 +25,7 @@ export class HaChipSet extends LitElement { ${unsafeCSS(chipStyles)} slot::slotted(ha-chip) { - margin: 4px; - } - slot::slotted(ha-chip:first-of-type) { - margin-left: -4px; - } - slot::slotted(ha-chip:last-of-type) { - margin-right: -4px; + margin: 4px 4px 4px 0; } `; } diff --git a/src/components/ha-chip.ts b/src/components/ha-chip.ts index 2a41ffc760..f22e0e29ad 100644 --- a/src/components/ha-chip.ts +++ b/src/components/ha-chip.ts @@ -14,6 +14,8 @@ import { customElement, property } from "lit/decorators"; export class HaChip extends LitElement { @property({ type: Boolean }) public hasIcon = false; + @property({ type: Boolean }) public hasTrailingIcon = false; + @property({ type: Boolean }) public noText = false; protected render(): TemplateResult { @@ -30,6 +32,11 @@ export class HaChip extends LitElement { + ${this.hasTrailingIcon + ? html`
    + +
    ` + : null}
    `; } @@ -53,14 +60,20 @@ export class HaChip extends LitElement { color: var(--ha-chip-text-color, var(--primary-text-color)); } - .mdc-chip__icon--leading { - --mdc-icon-size: 20px; + .mdc-chip__icon--leading, + .mdc-chip__icon--trailing { + --mdc-icon-size: 18px; + line-height: 14px; color: var(--ha-chip-icon-color, var(--ha-chip-text-color)); } .mdc-chip.no-text .mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden) { margin-right: -4px; } + + span[role="gridcell"] { + line-height: 14px; + } `; } } diff --git a/src/components/ha-combo-box.ts b/src/components/ha-combo-box.ts index 7c44541bcc..c1bc266ed9 100644 --- a/src/components/ha-combo-box.ts +++ b/src/components/ha-combo-box.ts @@ -110,14 +110,18 @@ export class HaComboBox extends LitElement { return this._comboBox.selectedItem; } + public setInputValue(value: string) { + this._comboBox.value = value; + } + protected render(): TemplateResult { return html` + typeof option === "object" ? option : { value: option, label: option } + ); + + if (!this.selector.select.custom_value && this._mode === "list") { + if (!this.selector.select.multiple || this.required) { + return html` +
    + ${this.label} + ${options.map( + (item: SelectOption) => html` + + + + ` + )} +
    + `; + } + return html`
    - ${this.label} - ${this.selector.select.options.map((item: string | SelectOption) => { - const value = typeof item === "object" ? item.value : item; - const label = typeof item === "object" ? item.label : item; - - return html` - - html` + + - - `; - })} + @change=${this._checkboxChanged} + > + + ` + )}
    `; } + if (this.selector.select.multiple) { + const value = + !this.value || this.value === "" ? [] : (this.value as string[]); + + return html` + + ${value?.map( + (item, idx) => + html` + + ${options.find((option) => option.value === item)?.label || + item} + + + ` + )} + + + !this.value?.includes(item.value))} + @filter-changed=${this._filterChanged} + @value-changed=${this._comboBoxValueChanged} + > + `; + } + + if (this.selector.select.custom_value) { + if ( + this.value !== undefined && + !options.find((option) => option.value === this.value) + ) { + options.unshift({ value: this.value, label: this.value }); + } + + return html` + + `; + } + return html` - ${this.selector.select.options.map((item: string | SelectOption) => { - const value = typeof item === "object" ? item.value : item; - const label = typeof item === "object" ? item.label : item; - - return html`${label}`; - })} + ${options.map( + (item: SelectOption) => html` + ${item.label} + ` + )} `; } + private get _mode(): "list" | "dropdown" { + return ( + this.selector.select.mode || + (this.selector.select.options.length < 6 ? "list" : "dropdown") + ); + } + private _valueChanged(ev) { ev.stopPropagation(); - if (this.disabled || !ev.target.value) { + const value = ev.detail?.value || ev.target.value; + if (this.disabled || !value) { return; } fireEvent(this, "value-changed", { - value: ev.target.value, + value: value, }); } - static get styles(): CSSResultGroup { - return css` - ha-select { - width: 100%; + private _checkboxChanged(ev) { + ev.stopPropagation(); + if (this.disabled) { + return; + } + + let newValue: string[]; + const value: string = ev.target.value; + const checked = ev.target.checked; + + if (checked) { + if (!this.value) { + newValue = [value]; + } else if (this.value.includes(value)) { + return; + } else { + newValue = [...this.value, value]; } - mwc-formfield { - display: block; + } else { + if (!this.value?.includes(value)) { + return; } - `; + newValue = (this.value as string[]).filter((v) => v !== value); + } + + fireEvent(this, "value-changed", { + value: newValue, + }); } + + private async _removeItem(ev) { + const value: string[] = [...(this.value! as string[])]; + value.splice(ev.target.idx, 1); + + fireEvent(this, "value-changed", { + value, + }); + await this.updateComplete; + this._filterChanged(); + } + + private _comboBoxValueChanged(ev: CustomEvent): void { + ev.stopPropagation(); + const newValue = ev.detail.value; + + if (this.disabled || newValue === "") { + return; + } + + if (!this.selector.select.multiple) { + fireEvent(this, "value-changed", { + value: newValue, + }); + return; + } + + if (newValue !== undefined && this.value?.includes(newValue)) { + return; + } + + setTimeout(() => { + this._filterChanged(); + this.comboBox.setInputValue(""); + }, 0); + + const currentValue = + !this.value || this.value === "" ? [] : (this.value as string[]); + + fireEvent(this, "value-changed", { + value: [...currentValue, newValue], + }); + } + + private _filterChanged(ev?: CustomEvent): void { + this._filter = ev?.detail.value || ""; + + const filteredItems = this.comboBox.items?.filter((item) => { + if (this.selector.select.multiple && this.value?.includes(item.value)) { + return false; + } + const label = item.label || item.value; + return label.toLowerCase().includes(this._filter?.toLowerCase()); + }); + + if (this._filter && this.selector.select.custom_value) { + filteredItems?.unshift({ label: this._filter, value: this._filter }); + } + + this.comboBox.filteredItems = filteredItems; + } + + static styles = css` + ha-select, + mwc-formfield, + ha-formfield { + display: block; + } + `; } declare global { diff --git a/src/data/selector.ts b/src/data/selector.ts index 076fa78aa3..10cda864b7 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -170,6 +170,9 @@ export interface SelectOption { export interface SelectSelector { select: { + multiple?: boolean; + custom_value?: boolean; + mode?: "list" | "dropdown"; options: string[] | SelectOption[]; }; } @@ -209,6 +212,7 @@ export interface TargetSelector { }; }; } + export interface ThemeSelector { // eslint-disable-next-line @typescript-eslint/ban-types theme: {}; From b5861869e39290fd2e15737e89571dfc543b3ad3 Mon Sep 17 00:00:00 2001 From: NachtaktiverHalbaffe <57433516+NachtaktiverHalbaffe@users.noreply.github.com> Date: Wed, 30 Mar 2022 19:16:27 +0200 Subject: [PATCH 137/142] Add shuffle and repeat-mode of media_player to UI (#12052) Co-authored-by: Paulus Schoutsen --- src/data/media-player.ts | 68 ++++++++++++++++++- .../controls/more-info-media_player.ts | 14 ++-- .../lovelace/cards/hui-media-control-card.ts | 12 ++-- .../media-browser/ha-bar-media-player.ts | 11 +-- src/translations/en.json | 2 + 5 files changed, 90 insertions(+), 17 deletions(-) diff --git a/src/data/media-player.ts b/src/data/media-player.ts index 14b1a19c80..199cbbb386 100644 --- a/src/data/media-player.ts +++ b/src/data/media-player.ts @@ -16,6 +16,11 @@ import { mdiPlayPause, mdiPodcast, mdiPower, + mdiRepeat, + mdiRepeatOff, + mdiRepeatOnce, + mdiShuffle, + mdiShuffleDisabled, mdiSkipNext, mdiSkipPrevious, mdiStop, @@ -49,6 +54,8 @@ interface MediaPlayerEntityAttributes extends HassEntityAttributeBase { entity_picture_local?: string; is_volume_muted?: boolean; volume_level?: number; + repeat?: string; + shuffle?: boolean; source?: string; source_list?: string[]; sound_mode?: string; @@ -80,7 +87,9 @@ export const SUPPORT_VOLUME_BUTTONS = 1024; export const SUPPORT_SELECT_SOURCE = 2048; export const SUPPORT_STOP = 4096; export const SUPPORT_PLAY = 16384; +export const SUPPORT_REPEAT_SET = 262144; export const SUPPORT_SELECT_SOUND_MODE = 65536; +export const SUPPORT_SHUFFLE_SET = 32768; export const SUPPORT_BROWSE_MEDIA = 131072; export type MediaPlayerBrowseAction = "pick" | "play"; @@ -233,7 +242,8 @@ export const computeMediaDescription = ( }; export const computeMediaControls = ( - stateObj: MediaPlayerEntity + stateObj: MediaPlayerEntity, + useExtendedControls = false ): ControlButton[] | undefined => { if (!stateObj) { return undefined; @@ -266,6 +276,18 @@ export const computeMediaControls = ( } const assumedState = stateObj.attributes.assumed_state === true; + const stateAttr = stateObj.attributes; + + if ( + (state === "playing" || state === "paused" || assumedState) && + supportsFeature(stateObj, SUPPORT_SHUFFLE_SET) && + useExtendedControls + ) { + buttons.push({ + icon: stateAttr.shuffle === true ? mdiShuffle : mdiShuffleDisabled, + action: "shuffle_set", + }); + } if ( (state === "playing" || state === "paused" || assumedState) && @@ -337,6 +359,22 @@ export const computeMediaControls = ( }); } + if ( + (state === "playing" || state === "paused" || assumedState) && + supportsFeature(stateObj, SUPPORT_REPEAT_SET) && + useExtendedControls + ) { + buttons.push({ + icon: + stateAttr.repeat === "all" + ? mdiRepeat + : stateAttr.repeat === "one" + ? mdiRepeatOnce + : mdiRepeatOff, + action: "repeat_set", + }); + } + return buttons.length > 0 ? buttons : undefined; }; @@ -375,3 +413,31 @@ export const setMediaPlayerVolume = ( volume_level: number ) => hass.callService("media_player", "volume_set", { entity_id, volume_level }); + +export const handleMediaControlClick = ( + hass: HomeAssistant, + stateObj: MediaPlayerEntity, + action: string +) => + hass!.callService( + "media_player", + action, + action === "shuffle_set" + ? { + entity_id: stateObj!.entity_id, + shuffle: !stateObj!.attributes.shuffle, + } + : action === "repeat_set" + ? { + entity_id: stateObj!.entity_id, + repeat: + stateObj!.attributes.repeat === "all" + ? "one" + : stateObj!.attributes.repeat === "off" + ? "all" + : "off", + } + : { + entity_id: stateObj!.entity_id, + } + ); diff --git a/src/dialogs/more-info/controls/more-info-media_player.ts b/src/dialogs/more-info/controls/more-info-media_player.ts index 916257695a..6f4b88926e 100644 --- a/src/dialogs/more-info/controls/more-info-media_player.ts +++ b/src/dialogs/more-info/controls/more-info-media_player.ts @@ -23,6 +23,7 @@ import { showMediaBrowserDialog } from "../../../components/media-player/show-me import { UNAVAILABLE, UNKNOWN } from "../../../data/entity"; import { computeMediaControls, + handleMediaControlClick, MediaPickedEvent, MediaPlayerEntity, SUPPORT_BROWSE_MEDIA, @@ -47,7 +48,7 @@ class MoreInfoMediaPlayer extends LitElement { } const stateObj = this.stateObj; - const controls = computeMediaControls(stateObj); + const controls = computeMediaControls(stateObj, true); return html`
    @@ -202,6 +203,7 @@ class MoreInfoMediaPlayer extends LitElement { } .basic-controls { + display: inline-flex; flex-grow: 1; } @@ -231,12 +233,10 @@ class MoreInfoMediaPlayer extends LitElement { } private _handleClick(e: MouseEvent): void { - this.hass!.callService( - "media_player", - (e.currentTarget! as HTMLElement).getAttribute("action")!, - { - entity_id: this.stateObj!.entity_id, - } + handleMediaControlClick( + this.hass!, + this.stateObj!, + (e.currentTarget as HTMLElement).getAttribute("action")! ); } diff --git a/src/panels/lovelace/cards/hui-media-control-card.ts b/src/panels/lovelace/cards/hui-media-control-card.ts index 50e26074f2..f1d2793ae0 100644 --- a/src/panels/lovelace/cards/hui-media-control-card.ts +++ b/src/panels/lovelace/cards/hui-media-control-card.ts @@ -28,6 +28,7 @@ import { computeMediaControls, computeMediaDescription, getCurrentProgress, + handleMediaControlClick, MediaPickedEvent, MediaPlayerEntity, SUPPORT_BROWSE_MEDIA, @@ -174,7 +175,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { UNAVAILABLE_STATES.includes(entityState) || (entityState === "off" && !supportsFeature(stateObj, SUPPORT_TURN_ON)); const hasNoImage = !this._image; - const controls = computeMediaControls(stateObj); + const controls = computeMediaControls(stateObj, false); const showControls = controls && (!this._veryNarrow || @@ -504,10 +505,11 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { } private _handleClick(e: MouseEvent): void { - const action = (e.currentTarget! as HTMLElement).getAttribute("action")!; - this.hass!.callService("media_player", action, { - entity_id: this._config!.entity, - }); + handleMediaControlClick( + this.hass!, + this._stateObj!, + (e.currentTarget as HTMLElement).getAttribute("action")! + ); } private _updateProgressBar(): void { diff --git a/src/panels/media-browser/ha-bar-media-player.ts b/src/panels/media-browser/ha-bar-media-player.ts index 3e913c6e43..bfc2e63656 100644 --- a/src/panels/media-browser/ha-bar-media-player.ts +++ b/src/panels/media-browser/ha-bar-media-player.ts @@ -39,6 +39,7 @@ import { computeMediaDescription, formatMediaTime, getCurrentProgress, + handleMediaControlClick, MediaPlayerEntity, MediaPlayerItem, setMediaPlayerVolume, @@ -173,7 +174,7 @@ export class BarMediaPlayer extends LitElement { } const controls = !this.narrow - ? computeMediaControls(stateObj) + ? computeMediaControls(stateObj, true) : (stateObj.state === "playing" && (supportsFeature(stateObj, SUPPORT_PAUSE) || supportsFeature(stateObj, SUPPORT_STOP))) || @@ -490,9 +491,11 @@ export class BarMediaPlayer extends LitElement { const action = (e.currentTarget! as HTMLElement).getAttribute("action")!; if (!this._browserPlayer) { - this.hass!.callService("media_player", action, { - entity_id: this.entityId, - }); + handleMediaControlClick( + this.hass!, + this._stateObj!, + (e.currentTarget as HTMLElement).getAttribute("action")! + ); return; } if (action === "media_pause") { diff --git a/src/translations/en.json b/src/translations/en.json index 2d05c998e8..051fa237c0 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -211,6 +211,8 @@ "media_volume_down": "Volume down", "media_volume_mute": "Volume mute", "media_volume_unmute": "Volume unmute", + "repeat_set": "Repeat mode", + "shuffle_set": "Shuffle", "text_to_speak": "Text to speak", "nothing_playing": "Nothing Playing" }, From 2b1457e1cdae79e0e4d4605ac944a240bb9d00db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 30 Mar 2022 19:22:15 +0200 Subject: [PATCH 138/142] Add panel to Backup integration (#11671) Co-authored-by: Zack Barett Co-authored-by: Paulus Schoutsen --- src/data/backup.ts | 36 +++ src/panels/config/backup/ha-config-backup.ts | 224 +++++++++++++++++++ src/panels/config/ha-panel-config.ts | 21 ++ src/translations/en.json | 25 +++ 4 files changed, 306 insertions(+) create mode 100644 src/data/backup.ts create mode 100644 src/panels/config/backup/ha-config-backup.ts diff --git a/src/data/backup.ts b/src/data/backup.ts new file mode 100644 index 0000000000..129b76726d --- /dev/null +++ b/src/data/backup.ts @@ -0,0 +1,36 @@ +import { HomeAssistant } from "../types"; + +export interface BackupContent { + slug: string; + date: string; + name: string; + size: number; + path: string; +} + +export interface BackupData { + backing_up: boolean; + backups: BackupContent[]; +} + +export const getBackupDownloadUrl = (slug: string) => + `/api/backup/download/${slug}`; + +export const fetchBackupInfo = (hass: HomeAssistant): Promise => + hass.callWS({ + type: "backup/info", + }); + +export const removeBackup = ( + hass: HomeAssistant, + slug: string +): Promise => + hass.callWS({ + type: "backup/remove", + slug, + }); + +export const generateBackup = (hass: HomeAssistant): Promise => + hass.callWS({ + type: "backup/generate", + }); diff --git a/src/panels/config/backup/ha-config-backup.ts b/src/panels/config/backup/ha-config-backup.ts new file mode 100644 index 0000000000..514f9f23e3 --- /dev/null +++ b/src/panels/config/backup/ha-config-backup.ts @@ -0,0 +1,224 @@ +import { mdiDelete, mdiDownload, mdiPlus } from "@mdi/js"; +import "@polymer/paper-tooltip/paper-tooltip"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoize from "memoize-one"; +import { relativeTime } from "../../../common/datetime/relative_time"; +import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table"; +import "../../../components/ha-circular-progress"; +import "../../../components/ha-fab"; +import "../../../components/ha-icon"; +import "../../../components/ha-icon-overflow-menu"; +import "../../../components/ha-svg-icon"; +import { getSignedPath } from "../../../data/auth"; +import { + BackupContent, + BackupData, + fetchBackupInfo, + generateBackup, + getBackupDownloadUrl, + removeBackup, +} from "../../../data/backup"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../../dialogs/generic/show-dialog-box"; +import "../../../layouts/hass-loading-screen"; +import "../../../layouts/hass-tabs-subpage-data-table"; +import { HomeAssistant, Route } from "../../../types"; +import { fileDownload } from "../../../util/file_download"; +import { configSections } from "../ha-panel-config"; + +@customElement("ha-config-backup") +class HaConfigBackup extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public isWide!: boolean; + + @property({ type: Boolean }) public narrow!: boolean; + + @property({ attribute: false }) public route!: Route; + + @state() private _backupData?: BackupData; + + private _columns = memoize( + (narrow, _language): DataTableColumnContainer => ({ + name: { + title: this.hass.localize("ui.panel.config.backup.name"), + sortable: true, + filterable: true, + grows: true, + template: (entry: string, backup: BackupContent) => + html`${entry} +
    ${backup.path}
    `, + }, + size: { + title: this.hass.localize("ui.panel.config.backup.size"), + width: "15%", + hidden: narrow, + filterable: true, + sortable: true, + template: (entry: number) => Math.ceil(entry * 10) / 10 + " MB", + }, + date: { + title: this.hass.localize("ui.panel.config.backup.created"), + width: "15%", + direction: "desc", + hidden: narrow, + filterable: true, + sortable: true, + template: (entry: string) => + relativeTime(new Date(entry), this.hass.locale), + }, + + actions: { + title: "", + width: "15%", + template: (_: string, backup: BackupContent) => + html` this._downloadBackup(backup), + }, + // Delete button + { + path: mdiDelete, + label: this.hass.localize( + "ui.panel.config.backup.remove_backup" + ), + action: () => this._removeBackup(backup), + }, + ]} + style="color: var(--secondary-text-color)" + > + `, + }, + }) + ); + + private _getItems = memoize((backupItems: BackupContent[]) => + backupItems.map((backup) => ({ + name: backup.name, + slug: backup.slug, + date: backup.date, + size: backup.size, + path: backup.path, + })) + ); + + protected render(): TemplateResult { + if (!this.hass || this._backupData === undefined) { + return html``; + } + + return html` + + + ${this._backupData.backing_up + ? html`` + : html``} + + + `; + } + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + this._getBackups(); + } + + private async _getBackups(): Promise { + this._backupData = await fetchBackupInfo(this.hass); + } + + private async _downloadBackup(backup: BackupContent): Promise { + const signedUrl = await getSignedPath( + this.hass, + getBackupDownloadUrl(backup.slug) + ); + fileDownload(signedUrl.path); + } + + private async _generateBackup(): Promise { + const confirm = await showConfirmationDialog(this, { + title: this.hass.localize("ui.panel.config.backup.create.title"), + text: this.hass.localize("ui.panel.config.backup.create.description"), + confirmText: this.hass.localize("ui.panel.config.backup.create.confirm"), + }); + if (!confirm) { + return; + } + + generateBackup(this.hass) + .then(() => this._getBackups()) + .catch((err) => showAlertDialog(this, { text: (err as Error).message })); + + await this._getBackups(); + } + + private async _removeBackup(backup: BackupContent): Promise { + const confirm = await showConfirmationDialog(this, { + title: this.hass.localize("ui.panel.config.backup.remove.title"), + text: this.hass.localize("ui.panel.config.backup.remove.description", { + name: backup.name, + }), + confirmText: this.hass.localize("ui.panel.config.backup.remove.confirm"), + }); + if (!confirm) { + return; + } + + await removeBackup(this.hass, backup.slug); + await this._getBackups(); + } + + static get styles(): CSSResultGroup { + return [ + css` + ha-fab[disabled] { + --mdc-theme-secondary: var(--disabled-text-color) !important; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-config-backup": HaConfigBackup; + } +} diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index 0ca0544fe7..7483960acd 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -1,5 +1,6 @@ import { mdiAccount, + mdiBackupRestore, mdiBadgeAccountHorizontal, mdiCellphoneCog, mdiCog, @@ -63,6 +64,13 @@ export const configSections: { [name: string]: PageNavigation[] } = { iconColor: "#64B5F6", component: "blueprint", }, + { + path: "/config/backup", + translationKey: "backup", + iconPath: mdiBackupRestore, + iconColor: "#4084CD", + component: "backup", + }, { path: "/hassio", translationKey: "supervisor", @@ -105,6 +113,15 @@ export const configSections: { [name: string]: PageNavigation[] } = { core: true, }, ], + backup: [ + { + path: "/config/backup", + translationKey: "ui.panel.config.backup.caption", + iconPath: mdiBackupRestore, + iconColor: "#4084CD", + component: "backup", + }, + ], devices: [ { component: "integrations", @@ -287,6 +304,10 @@ class HaPanelConfig extends HassRouterPage { tag: "ha-config-automation", load: () => import("./automation/ha-config-automation"), }, + backup: { + tag: "ha-config-backup", + load: () => import("./backup/ha-config-backup"), + }, blueprint: { tag: "ha-config-blueprint", load: () => import("./blueprint/ha-config-blueprint"), diff --git a/src/translations/en.json b/src/translations/en.json index 051fa237c0..2a3869adbd 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1070,6 +1070,10 @@ "title": "Automations & Scenes", "description": "Manage automations, scenes, scripts and helpers" }, + "backup": { + "title": "Backup", + "description": "Generate backups of your Home Assistant configuration" + }, "blueprints": { "title": "Blueprints", "description": "Pre-made automations and scripts by the community" @@ -1160,6 +1164,27 @@ "confirmation_text": "All devices in this area will become unassigned." } }, + "backup": { + "caption": "[%key:ui::panel::config::dashboard::backup::title%]", + "create_backup": "[%key:supervisor::backup::create_backup%]", + "creating_backup": "Backup is currently being created", + "download_backup": "[%key:supervisor::backup::download_backup%]", + "remove_backup": "[%key:supervisor::backup::delete_backup_title%]", + "name": "[%key:supervisor::backup::name%]", + "size": "[%key:supervisor::backup::size%]", + "created": "[%key:supervisor::backup::created%]", + "no_backups": "[%key:supervisor::backup::no_backups%]", + "create": { + "title": "Create backup", + "description": "Create a backup of your current configuration directory, this will take some time.", + "confirm": "create" + }, + "remove": { + "title": "Remove backup", + "description": "Are you sure you want to remove the backup with the name {name}?", + "confirm": "[%key:ui::common::remove%]" + } + }, "tag": { "caption": "Tags", "description": "Trigger automations when an NFC tag, QR code, etc. is scanned", From 396791b80518b308e45da93cbf60c9ab3858c422 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Wed, 30 Mar 2022 12:52:05 -0500 Subject: [PATCH 139/142] Fix for Mobile View of Entities Table (#12160) --- src/layouts/hass-tabs-subpage-data-table.ts | 34 +- .../config/entities/ha-config-entities.ts | 296 +++++++++--------- 2 files changed, 177 insertions(+), 153 deletions(-) diff --git a/src/layouts/hass-tabs-subpage-data-table.ts b/src/layouts/hass-tabs-subpage-data-table.ts index 1e44c0fafd..5085aba824 100644 --- a/src/layouts/hass-tabs-subpage-data-table.ts +++ b/src/layouts/hass-tabs-subpage-data-table.ts @@ -132,6 +132,12 @@ export class HaTabsSubpageDataTable extends LitElement { */ @property() public tabs!: PageNavigation[]; + /** + * Force hides the filter menu. + * @type {Boolean} + */ + @property({ type: Boolean }) public hideFilterMenu = false; + @query("ha-data-table", true) private _dataTable!: HaDataTable; public clearSelection() { @@ -195,16 +201,24 @@ export class HaTabsSubpageDataTable extends LitElement { .mainPage=${this.mainPage} .supervisor=${this.supervisor} > -
    - ${this.narrow - ? html`
    - ${this.numHidden || this.activeFilters - ? html`${this.numHidden || "!"}` - : ""} - -
    ` - : ""} -
    + ${!this.hideFilterMenu + ? html` +
    + ${this.narrow + ? html` +
    + ${this.numHidden || this.activeFilters + ? html`${this.numHidden || "!"}` + : ""} + +
    + ` + : ""} +
    + ` + : ""} ${this.narrow ? html`
    diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index 4fe64de1a8..9cfb164d8f 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -29,9 +29,9 @@ import type { SelectionChangedEvent, } from "../../../components/data-table/ha-data-table"; import "../../../components/ha-button-menu"; +import "../../../components/ha-check-list-item"; import "../../../components/ha-icon-button"; import "../../../components/ha-svg-icon"; -import "../../../components/ha-check-list-item"; import { AreaRegistryEntry, subscribeAreaRegistry, @@ -507,6 +507,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { .data=${filteredEntities} .activeFilters=${activeFilters} .numHidden=${this._numHiddenEntities} + .hideFilterMenu=${this._selectedEntities.length > 0} .searchLabel=${this.hass.localize( "ui.panel.config.entities.picker.search" )} @@ -526,149 +527,155 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { .hasFab=${includeZHAFab} > ${this._selectedEntities.length - ? html`
    -

    - ${this.hass.localize( - "ui.panel.config.entities.picker.selected", - "number", - this._selectedEntities.length - )} -

    -
    - ${!this.narrow - ? html` - ${this.hass.localize( - "ui.panel.config.entities.picker.enable_selected.button" - )} - ${this.hass.localize( - "ui.panel.config.entities.picker.disable_selected.button" - )} - ${this.hass.localize( - "ui.panel.config.entities.picker.hide_selected.button" - )} - ${this.hass.localize( - "ui.panel.config.entities.picker.remove_selected.button" - )} - ` - : html` - - - ${this.hass.localize( - "ui.panel.config.entities.picker.enable_selected.button" - )} - - - - ${this.hass.localize( - "ui.panel.config.entities.picker.disable_selected.button" - )} - - - - ${this.hass.localize( - "ui.panel.config.entities.picker.hide_selected.button" - )} - - - - ${this.hass.localize( - "ui.panel.config.entities.picker.remove_selected.button" - )} - - `} + ? html` +
    +

    + ${this.hass.localize( + "ui.panel.config.entities.picker.selected", + "number", + this._selectedEntities.length + )} +

    +
    + ${!this.narrow + ? html` + ${this.hass.localize( + "ui.panel.config.entities.picker.enable_selected.button" + )} + ${this.hass.localize( + "ui.panel.config.entities.picker.disable_selected.button" + )} + ${this.hass.localize( + "ui.panel.config.entities.picker.hide_selected.button" + )} + ${this.hass.localize( + "ui.panel.config.entities.picker.remove_selected.button" + )} + ` + : html` + + + ${this.hass.localize( + "ui.panel.config.entities.picker.enable_selected.button" + )} + + + + ${this.hass.localize( + "ui.panel.config.entities.picker.disable_selected.button" + )} + + + + ${this.hass.localize( + "ui.panel.config.entities.picker.hide_selected.button" + )} + + + + ${this.hass.localize( + "ui.panel.config.entities.picker.remove_selected.button" + )} + + `} +
    -
    ` - : html` - - ${this.narrow && activeFilters?.length - ? html`${this.hass.localize( - "ui.components.data-table.filtering_by" - )} - ${activeFilters.join(", ")} - Clear` - : ""} - - ${this.hass!.localize( - "ui.panel.config.entities.picker.filter.show_disabled" - )} - - - ${this.hass!.localize( - "ui.panel.config.entities.picker.filter.show_hidden" - )} - - - ${this.hass!.localize( - "ui.panel.config.entities.picker.filter.show_unavailable" - )} - - - ${this.hass!.localize( - "ui.panel.config.entities.picker.filter.show_readonly" - )} - - `} + ` + : html` + + + ${this.narrow && activeFilters?.length + ? html`${this.hass.localize( + "ui.components.data-table.filtering_by" + )} + ${activeFilters.join(", ")} + Clear` + : ""} + + ${this.hass!.localize( + "ui.panel.config.entities.picker.filter.show_disabled" + )} + + + ${this.hass!.localize( + "ui.panel.config.entities.picker.filter.show_hidden" + )} + + + ${this.hass!.localize( + "ui.panel.config.entities.picker.filter.show_unavailable" + )} + + + ${this.hass!.localize( + "ui.panel.config.entities.picker.filter.show_readonly" + )} + + + `} ${includeZHAFab ? html` mwc-button, .header-btns > ha-icon-button { margin: 8px; From bad776b979acb32188d98c7549e473ce50bbf770 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Wed, 30 Mar 2022 13:03:19 -0500 Subject: [PATCH 140/142] Allow Sensor Units to be updated via Entity Registry (#12143) --- src/data/entity_registry.ts | 4 ++ .../entities/entity-registry-settings.ts | 55 +++++++++++++++++++ src/translations/en.json | 1 + 3 files changed, 60 insertions(+) diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts index eaefcee6e3..e77d75c2b5 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -41,6 +41,10 @@ export interface EntityRegistryEntryUpdateParams { disabled_by?: string | null; hidden_by: string | null; new_entity_id?: string; + options_domain?: string; + options?: { + unit_of_measurement?: string | null; + }; } export const findBatteryEntity = ( diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index da5b40359f..a5c215bbd6 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -83,6 +83,11 @@ const OVERRIDE_DEVICE_CLASSES = { ], }; +const OVERRIDE_SENSOR_UNITS = { + temperature: ["°C", "°F", "K"], + pressure: ["hPa", "Pa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"], +}; + @customElement("entity-registry-settings") export class EntityRegistrySettings extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @@ -107,6 +112,8 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { @state() private _helperConfigEntry?: ConfigEntry; + @state() private _unit_of_measurement?: string | null; + @state() private _error?: string; @state() private _submitting?: boolean; @@ -167,6 +174,13 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { : undefined; const domain = computeDomain(this.entry.entity_id); + + if (domain === "sensor") { + const stateObj: HassEntity | undefined = + this.hass.states[this.entry.entity_id]; + this._unit_of_measurement = stateObj?.attributes?.unit_of_measurement; + } + const deviceClasses: string[][] = OVERRIDE_DEVICE_CLASSES[domain]; if (!deviceClasses) { @@ -269,6 +283,30 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { ` : ""} + ${this._deviceClass && + stateObj.attributes.unit_of_measurement && + OVERRIDE_SENSOR_UNITS[this._deviceClass]?.includes( + stateObj.attributes.unit_of_measurement + ) + ? html` + + ${OVERRIDE_SENSOR_UNITS[this._deviceClass].map( + (unit: string) => html` + ${unit} + ` + )} + + ` + : ""} Date: Wed, 30 Mar 2022 20:19:26 +0200 Subject: [PATCH 141/142] Add switch as x to entity settings (#12161) Co-authored-by: Zack --- .../entities/entity-registry-settings.ts | 88 ++++++++++++++++++- src/translations/en.json | 1 + 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index a5c215bbd6..48ebb5df49 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -28,6 +28,11 @@ import { deleteConfigEntry, getConfigEntries, } from "../../../data/config_entries"; +import { + createConfigFlow, + handleConfigFlowStep, +} from "../../../data/config_flow"; +import { DataEntryFlowStepCreateEntry } from "../../../data/data_entry_flow"; import { DeviceRegistryEntry, subscribeDeviceRegistry, @@ -36,9 +41,11 @@ import { import { EntityRegistryEntryUpdateParams, ExtEntityRegistryEntry, + fetchEntityRegistry, removeEntityRegistryEntry, updateEntityRegistryEntry, } from "../../../data/entity_registry"; +import { domainToName } from "../../../data/integration"; import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow"; import { showAlertDialog, @@ -48,6 +55,7 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail"; +import { showEntityEditorDialog } from "./show-dialog-entity-editor"; const OVERRIDE_DEVICE_CLASSES = { cover: [ @@ -88,6 +96,8 @@ const OVERRIDE_SENSOR_UNITS = { pressure: ["hPa", "Pa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"], }; +const SWITCH_AS_DOMAINS = ["light", "lock", "cover", "fan", "siren"]; + @customElement("entity-registry-settings") export class EntityRegistrySettings extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @@ -102,6 +112,8 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { @state() private _deviceClass?: string; + @state() private _switchAs = "switch"; + @state() private _areaId?: string | null; @state() private _disabledBy!: string | null; @@ -263,7 +275,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { > ${this._deviceClassOptions[0].map( (deviceClass: string) => html` - + ${this.hass.localize( `ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}` )} @@ -273,7 +285,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
  • ${this._deviceClassOptions[1].map( (deviceClass: string) => html` - + ${this.hass.localize( `ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}` )} @@ -307,6 +319,28 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { ` : ""} + ${domain === "switch" + ? html` + + ${domainToName(this.hass.localize, "switch")} + ${SWITCH_AS_DOMAINS.map( + (as_domain) => html` + + ${domainToName(this.hass.localize, as_domain)} + + ` + )} + ` + : ""} { this._submitting = true; + + const parent = (this.getRootNode() as ShadowRoot).host as HTMLElement; + const params: Partial = { name: this._name.trim() || null, icon: this._icon.trim() || null, @@ -604,6 +648,46 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { } finally { this._submitting = false; } + + if (this._switchAs !== "switch") { + if ( + !(await showConfirmationDialog(this, { + text: this.hass!.localize( + "ui.dialogs.entity_registry.editor.switch_as_x_confirm", + "domain", + this._switchAs + ), + })) + ) { + return; + } + const configFlow = await createConfigFlow(this.hass, "switch_as_x"); + const result = (await handleConfigFlowStep( + this.hass, + configFlow.flow_id, + { + entity_id: this._entityId.trim(), + target_domain: this._switchAs, + } + )) as DataEntryFlowStepCreateEntry; + if (!result.result?.entry_id) { + return; + } + const unsub = await this.hass.connection.subscribeEvents(() => { + unsub(); + fetchEntityRegistry(this.hass.connection).then((entityRegistry) => { + const entity = entityRegistry.find( + (reg) => reg.config_entry_id === result.result!.entry_id + ); + if (!entity) { + return; + } + showEntityEditorDialog(parent, { + entity_id: entity.entity_id, + }); + }); + }, "entity_registry_updated"); + } } private async _confirmDeleteEntry(): Promise { diff --git a/src/translations/en.json b/src/translations/en.json index ce6aa85021..86bc60cbfc 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -842,6 +842,7 @@ "hidden_cause": "Hidden by {cause}.", "device_disabled": "The device of this entity is disabled.", "open_device_settings": "Open device settings", + "switch_as_x_confirm": "This switch will be hidden and a new {domain} will be added. Your existing configurations using the switch will continue to work.", "enabled_description": "Disabled entities will not be added to Home Assistant.", "enabled_delay_confirm": "The enabled entities will be added to Home Assistant in {delay} seconds", "enabled_restart_confirm": "Restart Home Assistant to finish enabling the entities", From 9e4bee123f910e95ba1514390da7be9dadb46789 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Wed, 30 Mar 2022 13:33:59 -0500 Subject: [PATCH 142/142] Bumped version to 20220330.0 (#12164) --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index ce991f5820..eee0210ac8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = home-assistant-frontend -version = 20220329.0 +version = 20220330.0 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0