From 6623e5f017f633774d40623d175d19e68378ebd6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 10 Nov 2021 20:36:18 +0100 Subject: [PATCH 001/112] Fix thingktalk dialog (#10600) --- .../config/automation/thingtalk/dialog-thingtalk.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/panels/config/automation/thingtalk/dialog-thingtalk.ts b/src/panels/config/automation/thingtalk/dialog-thingtalk.ts index a19ccbed10..46cc842c5a 100644 --- a/src/panels/config/automation/thingtalk/dialog-thingtalk.ts +++ b/src/panels/config/automation/thingtalk/dialog-thingtalk.ts @@ -63,6 +63,13 @@ class DialogThingtalk extends LitElement { fireEvent(this, "dialog-closed", { dialog: this.localName }); } + public closeInitDialog() { + if (this._placeholders) { + return; + } + this.closeDialog(); + } + protected render(): TemplateResult { if (!this._params) { return html``; @@ -82,7 +89,7 @@ class DialogThingtalk extends LitElement { return html` Date: Wed, 10 Nov 2021 21:42:43 +0100 Subject: [PATCH 002/112] Add picture uploader to area (#10544) --- src/components/ha-area-picker.ts | 16 +- src/data/area_registry.ts | 2 + src/data/entity_registry.ts | 2 +- .../show-image-cropper-dialog.ts | 2 +- .../areas/dialog-area-registry-detail.ts | 41 +++- .../config/areas/ha-config-area-page.ts | 101 ++++++++-- .../config/areas/ha-config-areas-dashboard.ts | 189 +++++++++++------- .../config/entities/ha-config-entities.ts | 4 +- src/translations/en.json | 1 + 9 files changed, 254 insertions(+), 104 deletions(-) diff --git a/src/components/ha-area-picker.ts b/src/components/ha-area-picker.ts index b79abb66a4..4a22d3775e 100644 --- a/src/components/ha-area-picker.ts +++ b/src/components/ha-area-picker.ts @@ -340,7 +340,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { item-value-path="area_id" item-id-path="area_id" item-label-path="name" - .value=${this._value} + .value=${this.value} .disabled=${this.disabled} ${comboBoxRenderer(rowRenderer)} @opened-changed=${this._openedChanged} @@ -431,12 +431,24 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { name, }); this._areas = [...this._areas!, area]; + (this.comboBox as any).items = this._getAreas( + this._areas!, + this._devices!, + this._entities!, + this.includeDomains, + this.excludeDomains, + this.includeDeviceClasses, + this.deviceFilter, + this.entityFilter, + this.noAdd + ); this._setValue(area.area_id); } catch (err: any) { showAlertDialog(this, { - text: this.hass.localize( + title: this.hass.localize( "ui.components.area-picker.add_dialog.failed_create_area" ), + text: err.message, }); } }, diff --git a/src/data/area_registry.ts b/src/data/area_registry.ts index bb820261a9..3e432ce626 100644 --- a/src/data/area_registry.ts +++ b/src/data/area_registry.ts @@ -7,10 +7,12 @@ import { HomeAssistant } from "../types"; export interface AreaRegistryEntry { area_id: string; name: string; + picture?: string; } export interface AreaRegistryEntryMutableParams { name: string; + picture?: string | null; } export const createAreaRegistryEntry = ( diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts index 7072c44227..becf455fc0 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -66,7 +66,7 @@ export const computeEntityRegistryName = ( return entry.name; } const state = hass.states[entry.entity_id]; - return state ? computeStateName(state) : null; + return state ? computeStateName(state) : entry.entity_id; }; export const getExtendedEntityRegistryEntry = ( diff --git a/src/dialogs/image-cropper-dialog/show-image-cropper-dialog.ts b/src/dialogs/image-cropper-dialog/show-image-cropper-dialog.ts index 6ce2090070..4f7ff03d14 100644 --- a/src/dialogs/image-cropper-dialog/show-image-cropper-dialog.ts +++ b/src/dialogs/image-cropper-dialog/show-image-cropper-dialog.ts @@ -4,7 +4,7 @@ export interface CropOptions { round: boolean; type?: "image/jpeg" | "image/png"; quality?: number; - aspectRatio: number; + aspectRatio?: number; } export interface HaImageCropperDialogParams { diff --git a/src/panels/config/areas/dialog-area-registry-detail.ts b/src/panels/config/areas/dialog-area-registry-detail.ts index f2fc039470..9bbb35de18 100644 --- a/src/panels/config/areas/dialog-area-registry-detail.ts +++ b/src/panels/config/areas/dialog-area-registry-detail.ts @@ -3,19 +3,31 @@ import "@polymer/paper-input/paper-input"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { property, state } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; -import { navigate } from "../../../common/navigate"; import { createCloseHeading } from "../../../components/ha-dialog"; +import "../../../components/ha-alert"; +import "../../../components/ha-picture-upload"; +import type { HaPictureUpload } from "../../../components/ha-picture-upload"; import { AreaRegistryEntryMutableParams } from "../../../data/area_registry"; +import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog"; import { PolymerChangedEvent } from "../../../polymer-types"; import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import { AreaRegistryDetailDialogParams } from "./show-dialog-area-registry-detail"; +const cropOptions: CropOptions = { + round: false, + type: "image/jpeg", + quality: 0.75, + aspectRatio: 1.78, +}; + class DialogAreaDetail extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @state() private _name!: string; + @state() private _picture!: string | null; + @state() private _error?: string; @state() private _params?: AreaRegistryDetailDialogParams; @@ -28,6 +40,7 @@ class DialogAreaDetail extends LitElement { this._params = params; this._error = undefined; this._name = this._params.entry ? this._params.entry.name : ""; + this._picture = this._params.entry?.picture || null; await this.updateComplete; } @@ -55,7 +68,9 @@ class DialogAreaDetail extends LitElement { )} >
- ${this._error ? html`
${this._error}
` : ""} + ${this._error + ? html` ${this._error} ` + : ""}
${entry ? html` @@ -78,6 +93,13 @@ class DialogAreaDetail extends LitElement { )} .invalid=${nameInvalid} > +
${entry @@ -120,18 +142,24 @@ class DialogAreaDetail extends LitElement { this._name = ev.detail.value; } + private _pictureChanged(ev: PolymerChangedEvent) { + this._error = undefined; + this._picture = (ev.target as HaPictureUpload).value; + } + private async _updateEntry() { this._submitting = true; try { const values: AreaRegistryEntryMutableParams = { name: this._name.trim(), + picture: this._picture, }; if (this._params!.entry) { await this._params!.updateEntry!(values); } else { await this._params!.createEntry!(values); } - this._params = undefined; + this.closeDialog(); } catch (err: any) { this._error = err.message || @@ -145,13 +173,11 @@ class DialogAreaDetail extends LitElement { this._submitting = true; try { if (await this._params!.removeEntry!()) { - this._params = undefined; + this.closeDialog(); } } finally { this._submitting = false; } - - navigate("/config/areas/dashboard"); } static get styles(): CSSResultGroup { @@ -161,9 +187,6 @@ class DialogAreaDetail extends LitElement { .form { padding-bottom: 24px; } - .error { - color: var(--error-color); - } `, ]; } diff --git a/src/panels/config/areas/ha-config-area-page.ts b/src/panels/config/areas/ha-config-area-page.ts index ece1334bce..2de91d403b 100644 --- a/src/panels/config/areas/ha-config-area-page.ts +++ b/src/panels/config/areas/ha-config-area-page.ts @@ -1,13 +1,17 @@ import "@material/mwc-button"; -import { mdiCog } from "@mdi/js"; +import "@polymer/paper-item/paper-item"; +import "@polymer/paper-item/paper-item-body"; +import { mdiImagePlus, mdiPencil } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { computeStateName } from "../../../common/entity/compute_state_name"; +import { afterNextRender } from "../../../common/util/render-status"; import "../../../components/ha-card"; import "../../../components/ha-icon-button"; +import "../../../components/ha-icon-next"; import { AreaRegistryEntry, deleteAreaRegistryEntry, @@ -134,25 +138,59 @@ class HaConfigAreaPage extends LitElement { .tabs=${configSections.integrations} .route=${this.route} > - ${this.narrow ? html` ${area.name} ` : ""} - - + ${this.narrow + ? html` ${area.name} + ` + : ""}
${!this.narrow ? html`
-

${area.name}

+

+ ${area.name} + +

` : ""}
+ ${area.picture + ? html`
+ +
` + : html` + + `} ${devices.length @@ -181,7 +219,8 @@ class HaConfigAreaPage extends LitElement { .header=${this.hass.localize( "ui.panel.config.areas.editor.linked_entities_caption" )} - >${entities.length + > + ${entities.length ? entities.map( (entity) => html` @@ -390,6 +429,7 @@ class HaConfigAreaPage extends LitElement { try { await deleteAreaRegistryEntry(this.hass!, entry!.area_id); + afterNextRender(() => history.back()); return true; } catch (err: any) { return false; @@ -403,7 +443,7 @@ class HaConfigAreaPage extends LitElement { haStyle, css` h1 { - margin-top: 0; + margin: 0; font-family: var(--paper-font-headline_-_font-family); -webkit-font-smoothing: var( --paper-font-headline_-_-webkit-font-smoothing @@ -413,6 +453,13 @@ class HaConfigAreaPage extends LitElement { letter-spacing: var(--paper-font-headline_-_letter-spacing); line-height: var(--paper-font-headline_-_line-height); opacity: var(--dark-primary-opacity); + display: flex; + align-items: center; + } + + img { + border-radius: var(--ha-card-border-radius, 4px); + width: 100%; } .container { @@ -458,6 +505,34 @@ class HaConfigAreaPage extends LitElement { paper-item.no-link { cursor: default; } + + ha-card > a:first-child { + display: block; + } + ha-card > *:first-child { + margin-top: -16px; + } + .img-container { + position: relative; + } + .img-edit-btn { + position: absolute; + top: 4px; + right: 4px; + display: none; + } + .img-container:hover .img-edit-btn { + display: block; + } + .img-edit-btn::before { + content: ""; + position: absolute; + width: 100%; + height: 100%; + background-color: var(--card-background-color); + opacity: 0.5; + border-radius: 50%; + } `, ]; } diff --git a/src/panels/config/areas/ha-config-areas-dashboard.ts b/src/panels/config/areas/ha-config-areas-dashboard.ts index a2d27e63b7..d694234ea6 100644 --- a/src/panels/config/areas/ha-config-areas-dashboard.ts +++ b/src/panels/config/areas/ha-config-areas-dashboard.ts @@ -1,15 +1,8 @@ import { mdiHelpCircle, mdiPlus } from "@mdi/js"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-item/paper-item-body"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; -import { HASSDomEvent } from "../../../common/dom/fire_event"; -import { navigate } from "../../../common/navigate"; -import { - DataTableColumnContainer, - RowClickedEvent, -} from "../../../components/data-table/ha-data-table"; import "../../../components/ha-fab"; import "../../../components/ha-icon-button"; import "../../../components/ha-svg-icon"; @@ -21,7 +14,7 @@ import type { DeviceRegistryEntry } from "../../../data/device_registry"; import type { EntityRegistryEntry } from "../../../data/entity_registry"; import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import "../../../layouts/hass-loading-screen"; -import "../../../layouts/hass-tabs-subpage-data-table"; +import "../../../layouts/hass-tabs-subpage"; import { HomeAssistant, Route } from "../../../types"; import "../ha-config-section"; import { configSections } from "../ha-panel-config"; @@ -53,97 +46,51 @@ export class HaConfigAreasDashboard extends LitElement { entities: EntityRegistryEntry[] ) => areas.map((area) => { + let noDevicesInArea = 0; + let noServicesInArea = 0; + let noEntitiesInArea = 0; + const devicesInArea = new Set(); for (const device of devices) { if (device.area_id === area.area_id) { devicesInArea.add(device.id); + if (device.entry_type === "service") { + noServicesInArea++; + } else { + noDevicesInArea++; + } } } - let entitiesInArea = 0; - for (const entity of entities) { if ( entity.area_id ? entity.area_id === area.area_id : devicesInArea.has(entity.device_id) ) { - entitiesInArea++; + noEntitiesInArea++; } } return { ...area, - devices: devicesInArea.size, - entities: entitiesInArea, + devices: noDevicesInArea, + services: noServicesInArea, + entities: noEntitiesInArea, }; }) ); - private _columns = memoizeOne( - (narrow: boolean): DataTableColumnContainer => - narrow - ? { - name: { - title: this.hass.localize( - "ui.panel.config.areas.data_table.area" - ), - sortable: true, - filterable: true, - grows: true, - direction: "asc", - }, - } - : { - name: { - title: this.hass.localize( - "ui.panel.config.areas.data_table.area" - ), - sortable: true, - filterable: true, - grows: true, - direction: "asc", - }, - devices: { - title: this.hass.localize( - "ui.panel.config.areas.data_table.devices" - ), - sortable: true, - type: "numeric", - width: "20%", - direction: "asc", - }, - entities: { - title: this.hass.localize( - "ui.panel.config.areas.data_table.entities" - ), - sortable: true, - type: "numeric", - width: "20%", - direction: "asc", - }, - } - ); - protected render(): TemplateResult { return html` - + - + `; } @@ -191,11 +190,6 @@ export class HaConfigAreasDashboard extends LitElement { }); } - private _handleRowClicked(ev: HASSDomEvent) { - const areaId = ev.detail.id; - navigate(`/config/areas/area/${areaId}`); - } - private _openDialog(entry?: AreaRegistryEntry) { showAreaRegistryDetailDialog(this, { entry, @@ -210,6 +204,51 @@ export class HaConfigAreasDashboard extends LitElement { --app-header-background-color: var(--sidebar-background-color); --app-header-text-color: var(--sidebar-text-color); } + .container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + grid-gap: 16px 16px; + padding: 8px 16px 16px; + margin: 0 auto 64px auto; + max-width: 1000px; + } + .container > * { + max-width: 500px; + } + ha-card { + overflow: hidden; + } + a { + text-decoration: none; + } + h1 { + padding-bottom: 0; + } + .picture { + height: 150px; + width: 100%; + background-size: cover; + background-position: center; + position: relative; + } + .picture.placeholder::before { + position: absolute; + content: ""; + width: 100%; + height: 100%; + background-color: var(--sidebar-selected-icon-color); + opacity: 0.12; + } + .card-content { + min-height: 16px; + color: var(--secondary-text-color); + } `; } } + +declare global { + interface HTMLElementTagNameMap { + "ha-config-areas-dashboard": HaConfigAreasDashboard; + } +} diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index 055b770147..6f9569e6cf 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -376,9 +376,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { result.push({ ...entry, entity, - name: - computeEntityRegistryName(this.hass!, entry) || - this.hass.localize("state.default.unavailable"), + name: computeEntityRegistryName(this.hass!, entry), unavailable, restored, area: area ? area.name : undefined, diff --git a/src/translations/en.json b/src/translations/en.json index c5df32dc8a..9a565b0875 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -930,6 +930,7 @@ "caption": "Areas", "description": "Group devices and entities into areas", "edit_settings": "Area settings", + "add_picture": "Add a picture", "data_table": { "area": "Area", "devices": "Devices", From d04823b4c595606c98278e41a506bb6ee937614d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 10 Nov 2021 22:55:05 +0100 Subject: [PATCH 003/112] Update image-cropper-dialog.ts --- src/dialogs/image-cropper-dialog/image-cropper-dialog.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dialogs/image-cropper-dialog/image-cropper-dialog.ts b/src/dialogs/image-cropper-dialog/image-cropper-dialog.ts index f8709344e3..74e3711235 100644 --- a/src/dialogs/image-cropper-dialog/image-cropper-dialog.ts +++ b/src/dialogs/image-cropper-dialog/image-cropper-dialog.ts @@ -39,6 +39,7 @@ export class HaImagecropperDialog extends LitElement { this._open = false; this._params = undefined; this._cropper?.destroy(); + this._cropper = undefined; } protected updated(changedProperties: PropertyValues) { From c238c7dbbc579906476b089e8629a2df932cbab9 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 11 Nov 2021 01:48:56 -0800 Subject: [PATCH 004/112] WebRTC fix for Safari (#10602) --- src/components/ha-web-rtc-player.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/ha-web-rtc-player.ts b/src/components/ha-web-rtc-player.ts index 665e5ab456..b2f64a0645 100644 --- a/src/components/ha-web-rtc-player.ts +++ b/src/components/ha-web-rtc-player.ts @@ -80,6 +80,9 @@ class HaWebRtcPlayer extends LitElement { // Some cameras (such as nest) require a data channel to establish a stream // however, not used by any integrations. peerConnection.createDataChannel("dataSendChannel"); + peerConnection.addTransceiver("audio", { direction: "recvonly" }); + peerConnection.addTransceiver("video", { direction: "recvonly" }); + const offerOptions: RTCOfferOptions = { offerToReceiveAudio: true, offerToReceiveVideo: true, From db6ef22ebb923f3d7cb1dc567adb35b84b4760d1 Mon Sep 17 00:00:00 2001 From: Michael Irigoyen Date: Mon, 15 Nov 2021 02:49:53 -0600 Subject: [PATCH 005/112] Update MDI to v6.5.95 (#10618) --- package.json | 4 ++-- yarn.lock | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index de5f22d346..0d9726b250 100644 --- a/package.json +++ b/package.json @@ -67,8 +67,8 @@ "@material/mwc-tab-bar": "0.25.3", "@material/mwc-textfield": "0.25.3", "@material/top-app-bar": "14.0.0-canary.261f2db59.0", - "@mdi/js": "6.4.95", - "@mdi/svg": "6.4.95", + "@mdi/js": "6.5.95", + "@mdi/svg": "6.5.95", "@polymer/app-layout": "^3.1.0", "@polymer/iron-flex-layout": "^3.0.1", "@polymer/iron-icon": "^3.0.1", diff --git a/yarn.lock b/yarn.lock index 6f27e25906..0bfb5c39ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2909,17 +2909,17 @@ __metadata: languageName: node linkType: hard -"@mdi/js@npm:6.4.95": - version: 6.4.95 - resolution: "@mdi/js@npm:6.4.95" - checksum: 76055821b0b611090f1fadff3cb93b0d2c05f7c52778a0e8966abe1c85d3aae8312c497e91882bb8cb02bf9235edda901da944099553afb2dbacafb11ed40b18 +"@mdi/js@npm:6.5.95": + version: 6.5.95 + resolution: "@mdi/js@npm:6.5.95" + checksum: b1db7713d216c119f584bf973514a2f9d8f2e671e91bf19ce8e56cfa7a9843c0a060328e794507ac31f2bded1032123294f39ff8e987ea5acb2719ab522ef146 languageName: node linkType: hard -"@mdi/svg@npm:6.4.95": - version: 6.4.95 - resolution: "@mdi/svg@npm:6.4.95" - checksum: 118945d58484dfa686b3127fabbbbf8e20c20ce1989d158cd270e3da53e22fd30d3c285e3f5d0472f602a9842f2adb8321d853f85ff397ccf8bfefa846bd729e +"@mdi/svg@npm:6.5.95": + version: 6.5.95 + resolution: "@mdi/svg@npm:6.5.95" + checksum: 2d45221d042d52d54c85eaf672a5f3697ed5201607fa38a6e235ee2e60d1c3c25d456a284f19ce47b5f06418cacfee29e8fecf6580b8c28538fd26044becaf1a languageName: node linkType: hard @@ -9037,8 +9037,8 @@ fsevents@^1.2.7: "@material/mwc-tab-bar": 0.25.3 "@material/mwc-textfield": 0.25.3 "@material/top-app-bar": 14.0.0-canary.261f2db59.0 - "@mdi/js": 6.4.95 - "@mdi/svg": 6.4.95 + "@mdi/js": 6.5.95 + "@mdi/svg": 6.5.95 "@open-wc/dev-server-hmr": ^0.0.2 "@polymer/app-layout": ^3.1.0 "@polymer/iron-flex-layout": ^3.0.1 From 4e68383cf7a6d46b6e4f4eb86176feb9e14c1335 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 15 Nov 2021 11:54:59 +0100 Subject: [PATCH 006/112] Remove deprecated icons (#10622) --- build-scripts/removedIcons.json | 103 +---------- src/components/ha-icon.ts | 310 +------------------------------- 2 files changed, 2 insertions(+), 411 deletions(-) diff --git a/build-scripts/removedIcons.json b/build-scripts/removedIcons.json index 3949b1f13e..fe51488c70 100644 --- a/build-scripts/removedIcons.json +++ b/build-scripts/removedIcons.json @@ -1,102 +1 @@ -[ - { - "path": "M21.8 14.5C21.3 13.7 20.1 13.4 18.1 13.4C17.4 13.4 16.7 13.4 16 13.5C15.5 13.2 15 12.9 14.6 12.6C13.6 11.8 12.7 10.3 12 8.5C12 8.5 12 8.4 12.1 8.3C12.6 6.2 13.1 3.6 12.1 2.5C11.8 2.2 11.5 2.1 11.1 2.1H10.7C10.1 2.1 9.6 2.7 9.4 3.3C8.8 5.4 9.2 6.6 9.8 8.5C9.4 10 8.9 11.6 8 13.3C7.5 14.4 6.9 15.4 6.5 16.2C5.9 16.5 5.4 16.8 5.1 17C3.2 18.2 2.2 19.6 2.1 20.4C2 20.7 2 21 2.1 21.2V21.3L2.9 21.8C3.1 21.9 3.4 22 3.6 22C4.9 22 6.4 20.5 8.4 17C8.5 17 8.6 16.9 8.7 16.9C10.4 16.4 12.4 16 15.2 15.7C16.8 16.5 18.8 16.9 20 16.9C20.7 16.9 21.2 16.7 21.5 16.4C21.8 16.1 21.9 15.7 22 15.3C22 15 22 14.7 21.8 14.5M3.4 20.9C3.5 20.3 4.2 19.2 5.4 18.2C5.6 18.1 5.8 17.9 6.2 17.7C5 19.6 4.1 20.6 3.4 20.9M10.8 3.2C10.9 3.1 10.9 3 11 3L11.2 3.1C11.5 3.5 11.5 4 11.3 4.9V5.2C11.2 5.6 11.2 6 11 6.5C10.6 5 10.6 3.9 10.8 3.2M8.8 15.8L8.6 15.9C8.7 15.4 9.1 14.8 9.4 14.2C10.1 12.8 10.7 11.5 11 10.3C11.7 11.8 12.5 12.9 13.5 13.8C13.7 14 13.9 14.2 14.2 14.3C12.8 14.5 10.9 15 8.8 15.8M20.9 15.7H20.5C19.8 15.7 18.6 15.4 17.4 14.9C17.5 14.7 17.7 14.7 17.8 14.7C20.1 14.7 20.7 15.1 20.9 15.3C21 15.4 21 15.4 21 15.5C21 15.6 21 15.6 20.9 15.7Z", - "name": "adobe-acrobat" - }, - { - "path": "M14.58,3H22V19.67L14.58,3M9.42,3H2V19.67L9.42,3M12,9.17L16.67,19.67H13.5L12.17,16.33H8.75L12,9.17Z", - "name": "adobe" - }, - { - "path": "M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M10.43,21.87V19.91C10.43,19.22 10,18.57 9.35,18.3C6.91,17.26 5.17,14.83 5.17,12C5.17,8.26 8.22,5.17 12,5.17C15.78,5.17 18.83,8.26 18.83,12C18.83,16.43 15.39,20.61 10.43,21.87Z", - "name": "amazon-alexa" - }, - { - "path": "M15.93,17.09C15.75,17.25 15.5,17.26 15.3,17.15C14.41,16.41 14.25,16.07 13.76,15.36C12.29,16.86 11.25,17.31 9.34,17.31C7.09,17.31 5.33,15.92 5.33,13.14C5.33,10.96 6.5,9.5 8.19,8.76C9.65,8.12 11.68,8 13.23,7.83V7.5C13.23,6.84 13.28,6.09 12.9,5.54C12.58,5.05 11.95,4.84 11.4,4.84C10.38,4.84 9.47,5.37 9.25,6.45C9.2,6.69 9,6.93 8.78,6.94L6.18,6.66C5.96,6.61 5.72,6.44 5.78,6.1C6.38,2.95 9.23,2 11.78,2C13.08,2 14.78,2.35 15.81,3.33C17.11,4.55 17,6.18 17,7.95V12.12C17,13.37 17.5,13.93 18,14.6C18.17,14.85 18.21,15.14 18,15.31L15.94,17.09H15.93M13.23,10.56V10C11.29,10 9.24,10.39 9.24,12.67C9.24,13.83 9.85,14.62 10.87,14.62C11.63,14.62 12.3,14.15 12.73,13.4C13.25,12.47 13.23,11.6 13.23,10.56M20.16,19.54C18,21.14 14.82,22 12.1,22C8.29,22 4.85,20.59 2.25,18.24C2.05,18.06 2.23,17.81 2.5,17.95C5.28,19.58 8.75,20.56 12.33,20.56C14.74,20.56 17.4,20.06 19.84,19.03C20.21,18.87 20.5,19.27 20.16,19.54M21.07,18.5C20.79,18.14 19.22,18.33 18.5,18.42C18.31,18.44 18.28,18.26 18.47,18.12C19.71,17.24 21.76,17.5 22,17.79C22.24,18.09 21.93,20.14 20.76,21.11C20.58,21.27 20.41,21.18 20.5,21C20.76,20.33 21.35,18.86 21.07,18.5Z", - "name": "amazon" - }, - { - "path": "M22.78,17.91C22.94,18.16 23,18.42 23,18.7C23,19.08 22.87,19.39 22.57,19.64C22.27,19.89 21.94,20 21.56,20H19.08L12.42,8H11.58L4.92,20H2.39C1.92,20 1.53,19.8 1.22,19.38C0.91,18.96 0.89,18.5 1.17,18L10.78,1.69C11.09,1.22 11.5,1 12,1C12.53,1 12.92,1.22 13.17,1.69L22.78,17.91M4.78,22.31L12,9.38L19.22,22.31L18.5,23L12,20.34L5.44,23L4.78,22.31Z", - "name": "android-auto" - }, - { - "path": "M15,9A1,1 0 0,1 14,8A1,1 0 0,1 15,7A1,1 0 0,1 16,8A1,1 0 0,1 15,9M9,9A1,1 0 0,1 8,8A1,1 0 0,1 9,7A1,1 0 0,1 10,8A1,1 0 0,1 9,9M16.12,4.37L18.22,2.27L17.4,1.44L15.09,3.75C14.16,3.28 13.11,3 12,3C10.88,3 9.84,3.28 8.91,3.75L6.6,1.44L5.78,2.27L7.88,4.37C6.14,5.64 5,7.68 5,10V11H19V10C19,7.68 17.86,5.64 16.12,4.37M5,16C5,19.86 8.13,23 12,23A7,7 0 0,0 19,16V12H5V16Z", - "name": "android-debug-bridge" - }, - { - "path": "M22,6L15.5,18H2L8.5,6H22Z", - "name": "bandcamp" - }, - { - "path": "M19.92,10.76C19.92,10.76 22.5,12.24 22.5,13.89C22.5,15.5 19.5,16.06 16.18,15.9C16.18,15.9 14.77,17.87 13.42,18.7C14.88,21.44 16,22.5 15.97,22.5C15.97,22.5 15.23,22.69 13,19.04C11.66,19.89 10.17,20.23 9.56,19.7C8.94,19.17 9.42,18.28 9.68,17.85C9.41,18 8,18.83 6.75,18.83C5.26,18.83 5.05,17.72 5.05,17.15C5.05,15 7.12,12 7.12,12C7.12,12 6.16,9.88 6.05,8.22C4.17,8.06 2,8.39 1.53,8.54C1.4,8.54 1.84,8.22 2,8.18C2.15,8.13 3.91,7.67 6,7.67C6,5.93 6.35,4.33 7.41,4.33C8.13,4.33 8.71,5.45 8.71,5.45C8.71,5.45 8.7,1.5 10.74,1.5C12.8,1.5 15,6.11 15,6.11C15,6.11 17.22,6.32 18.85,7.09C19.5,5.73 20.09,5.11 20.81,3C21,3.7 20.2,5.5 19.35,7.3V7.3H19.35C19.35,7.3 21.65,8.5 21.65,9.83C21.65,10.84 19.92,10.76 19.92,10.76M10.68,18.58C11.36,18.69 12.41,18.1 12.4,18.1L11.58,16.57L10.4,17.4C10.39,17.41 9.64,18.38 10.68,18.58M20.15,9.76C20.15,9.1 18.95,8.35 18.81,8.27L17.89,9.75L19.17,10.37C19.59,10.34 20.15,10.35 20.15,9.76M8,5.63C7.7,5.63 7.09,6.07 7.09,7.64L8.83,7.7L8.72,6.3C8.6,6 8.3,5.63 8,5.63M10.18,15.78C8.92,15.13 8.16,14.06 7.54,12.9C7.54,12.9 5.96,15.55 6.97,16.22C8,16.89 9.64,16.16 10.18,15.78M12.97,17.76C14.11,16.89 17.19,14.73 17.45,11.08C14.57,9.44 10.62,8.71 10.62,8.71C10.62,8.71 10.61,8.21 10.7,7.86C11.64,7.97 14.59,8.47 17.03,9.43C16.35,8.28 15.84,7.85 15.37,7.5C16.53,7.76 17.36,9.26 17.36,9.26L18.28,7.96C18.28,7.96 13.91,5.61 10.19,7.42C10.11,10.3 11.59,14.56 11.59,14.56L10.82,14.89C10.3,13.84 9.63,12.09 9,8.67C8.7,9.08 8.17,9.55 8.16,11.09C7.7,9.8 8.66,8.43 8.67,8.42L7.07,8.26C7.17,9.92 8.05,14.2 10.68,15.53C13,14.21 15.5,11.54 16.13,10.77L16.82,11.28L12.35,15.97C13.59,16 14.32,15.72 14.82,15.5C14.1,16.25 12.86,16.32 12.27,16.32C12.28,16.34 12.57,17.07 12.97,17.76M14.03,6.05C14,5.97 12.66,3.69 11.47,3.86C10.69,4.11 10.24,5.43 10.23,6.87C10.76,6.56 12,6 14.03,6.05M16.71,15.07C16.71,15.07 20,15 19.9,13.76C19.9,12.56 17.92,11.33 17.92,11.35C17.93,13.47 16.71,15.07 16.71,15.07Z", - "name": "battlenet" - }, - { - "path": "M12.5 10H10C9.45 10 9 9.55 9 9C9 8.45 9.45 8 10 8H12.5C13.05 8 13.5 8.45 13.5 9C13.5 9.55 13.05 10 12.5 10M15 14C15 13.45 14.55 13 14 13H10C9.45 13 9 13.45 9 14C9 14.55 9.45 15 10 15H14C14.55 15 15 14.55 15 14M22 4V20C22 21.11 21.11 22 20 22H4C2.89 22 2 21.11 2 20V4C2 2.89 2.89 2 4 2H20C21.11 2 22 2.89 22 4M18 12C18 12 18 11 17 11C16.05 11.03 16 10 16 10L16 8C16 6.34 14.66 5 13 5H9C7.34 5 6 6.34 6 8V15C6 16.66 7.34 18 9 18H15C16.66 18 18 16.66 18 15L18 12Z", - "name": "blogger" - }, - { - "path": "M12.6,2.86C15.27,4.1 18,5.39 20.66,6.63C20.81,6.7 21,6.75 21,6.95C21,7.15 20.81,7.19 20.66,7.26C18,8.5 15.3,9.77 12.62,11C12.21,11.21 11.79,11.21 11.38,11C8.69,9.76 6,8.5 3.32,7.25C3.18,7.19 3,7.14 3,6.94C3,6.76 3.18,6.71 3.31,6.65C6,5.39 8.74,4.1 11.44,2.85C11.73,2.72 12.3,2.73 12.6,2.86M12,21.15C11.8,21.15 11.66,21.07 11.38,20.97C8.69,19.73 6,18.47 3.33,17.22C3.19,17.15 3,17.11 3,16.9C3,16.7 3.19,16.66 3.34,16.59C3.78,16.38 4.23,16.17 4.67,15.96C5.12,15.76 5.56,15.76 6,15.97C7.79,16.8 9.57,17.63 11.35,18.46C11.79,18.67 12.23,18.66 12.67,18.46C14.45,17.62 16.23,16.79 18,15.96C18.44,15.76 18.87,15.75 19.29,15.95C19.77,16.16 20.24,16.39 20.71,16.61C20.78,16.64 20.85,16.68 20.91,16.73C21.04,16.83 21.04,17 20.91,17.08C20.83,17.14 20.74,17.19 20.65,17.23C18,18.5 15.33,19.72 12.66,20.95C12.46,21.05 12.19,21.15 12,21.15M12,16.17C11.9,16.17 11.55,16.07 11.36,16C8.68,14.74 6,13.5 3.34,12.24C3.2,12.18 3,12.13 3,11.93C3,11.72 3.2,11.68 3.35,11.61C3.8,11.39 4.25,11.18 4.7,10.97C5.13,10.78 5.56,10.78 6,11C7.78,11.82 9.58,12.66 11.38,13.5C11.79,13.69 12.21,13.69 12.63,13.5C14.43,12.65 16.23,11.81 18.04,10.97C18.45,10.78 18.87,10.78 19.29,10.97C19.76,11.19 20.24,11.41 20.71,11.63C20.77,11.66 20.84,11.69 20.9,11.74C21.04,11.85 21.04,12 20.89,12.12C20.84,12.16 20.77,12.19 20.71,12.22C18,13.5 15.31,14.75 12.61,16C12.42,16.09 12.08,16.17 12,16.17Z", - "name": "buffer" - }, - { - "path": "M20,18H4V6H20M20,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6C22,4.89 21.1,4 20,4M11,17H13V16H14A1,1 0 0,0 15,15V12A1,1 0 0,0 14,11H11V10H15V8H13V7H11V8H10A1,1 0 0,0 9,9V12A1,1 0 0,0 10,13H13V14H9V16H11V17Z", - "name": "cash-usd-outline" - }, - { - "path": "M20 4H4C2.89 4 2 4.89 2 6V18C2 19.11 2.9 20 4 20H20C21.11 20 22 19.11 22 18V6C22 4.89 21.1 4 20 4M15 10H11V11H14C14.55 11 15 11.45 15 12V15C15 15.55 14.55 16 14 16H13V17H11V16H9V14H13V13H10C9.45 13 9 12.55 9 12V9C9 8.45 9.45 8 10 8H11V7H13V8H15V10Z", - "name": "cash-usd" - }, - { - "path": "M10.94 12.09A1.06 1.06 0 1 0 11.91 10.95A1.06 1.06 0 0 0 10.94 12.09M13.54 13.21A2.62 2.62 0 0 1 12.58 13.88L12.57 13.89A11.17 11.17 0 0 0 13.87 16.92A4.83 4.83 0 0 1 12.92 17.3A4.9 4.9 0 0 1 12.24 17.44L12.16 17.45L12.06 17.46L11.87 17.47C11.75 17.47 11.64 17.5 11.5 17.47A5 5 0 0 1 10.79 17.4A5.13 5.13 0 0 1 10.09 17.23A4.78 4.78 0 0 1 9.46 17A5 5 0 0 1 8.93 16.68A4.45 4.45 0 0 1 8.5 16.38C8.38 16.29 8.28 16.19 8.2 16.12L7.95 15.87C7.95 15.87 8 15.97 8.16 16.15C8.24 16.23 8.32 16.34 8.44 16.45A4.93 4.93 0 0 0 8.82 16.82A5.21 5.21 0 0 0 9.33 17.2A5.08 5.08 0 0 0 9.96 17.56A5.43 5.43 0 0 0 10.68 17.85A5.5 5.5 0 0 0 11.46 18.03C11.6 18.06 11.74 18.07 11.88 18.08L12.07 18.1H12.27A5.5 5.5 0 0 0 13.07 18.05A5.61 5.61 0 0 0 14.39 17.7Q14.63 18.03 14.9 18.34L14.87 18.36L14.66 18.5H14.62C14.59 18.53 14.67 18.5 14.66 18.5H14.64L14.58 18.54L14.44 18.61L14.37 18.64L14.33 18.66L14.32 18.67H14.31C14.3 18.68 14.36 18.66 14.33 18.67H14.32L14.17 18.74Q14.09 18.77 14 18.81V18.82L13.93 18.84L13.84 18.87A6.5 6.5 0 0 1 12.25 19.26A6.4 6.4 0 0 1 11.31 19.3H11.19L11.07 19.29L10.81 19.27C10.65 19.24 10.5 19.23 10.33 19.2A6.59 6.59 0 0 1 8.5 18.57C8.35 18.5 8.21 18.43 8.08 18.35C7.95 18.27 7.83 18.19 7.71 18.11A6.64 6.64 0 0 1 7.07 17.6A6.35 6.35 0 0 1 6.57 17.11C6.43 16.96 6.32 16.82 6.22 16.7C6.13 16.58 6.06 16.5 6 16.42L5.94 16.32L6 16.43C6.04 16.5 6.1 16.6 6.18 16.73C6.26 16.86 6.36 17 6.5 17.18A6.5 6.5 0 0 0 6.94 17.73A6.86 6.86 0 0 0 7.55 18.31C7.67 18.41 7.78 18.5 7.91 18.6C8.04 18.7 8.17 18.79 8.31 18.88A7.12 7.12 0 0 0 9.21 19.37A7.2 7.2 0 0 0 10.2 19.74C10.37 19.8 10.55 19.83 10.73 19.87L11 19.92L11.11 19.94L11.25 19.96A7.27 7.27 0 0 0 12.29 20.03A7.38 7.38 0 0 0 14.14 19.77L14.23 19.74L14.28 19.73L14.34 19.71C14.4 19.69 14.46 19.68 14.5 19.66L14.68 19.6L14.85 19.54L15 19.5L15.06 19.45H15.08L15.13 19.43L15.14 19.42L15.17 19.41L15.39 19.3L15.67 19.16C15.86 19.34 16.06 19.5 16.26 19.69C16.26 19.69 17.23 20.69 17.76 20.28C18.26 19.89 17.68 18.68 17.68 18.68A11.2 11.2 0 0 0 13.54 13.21M10.16 11.57L10.15 11.56A11.18 11.18 0 0 0 6.91 11.11A4.72 4.72 0 0 1 7.34 9.39L7.38 9.31L7.42 9.23L7.5 9.06C7.57 8.96 7.62 8.85 7.69 8.75A4.97 4.97 0 0 1 8.14 8.17A5.05 5.05 0 0 1 8.66 7.67A4.77 4.77 0 0 1 9.2 7.27A5.06 5.06 0 0 1 9.74 7A4.88 4.88 0 0 1 10.22 6.78C10.37 6.72 10.5 6.69 10.61 6.66C10.82 6.6 10.95 6.58 10.95 6.58S10.82 6.59 10.6 6.61C10.5 6.63 10.35 6.64 10.19 6.68A4.94 4.94 0 0 0 9.67 6.82A5.34 5.34 0 0 0 9.08 7.05A5.08 5.08 0 0 0 8.45 7.39A5.47 5.47 0 0 0 7.82 7.84A5.55 5.55 0 0 0 7.25 8.41C7.16 8.5 7.08 8.63 7 8.74L6.88 8.89L6.82 8.97L6.76 9.06A5.5 5.5 0 0 0 6.38 9.77A5.61 5.61 0 0 0 5.97 11.14L5.96 11.16C5.7 11.18 5.43 11.21 5.16 11.26V11.12L5.17 10.87V10.82C5.17 10.78 5.16 10.88 5.16 10.87V10.84L5.17 10.77L5.18 10.62L5.19 10.54V10.5H5.2V10.47C5.2 10.46 5.19 10.5 5.19 10.5L5.22 10.32L5.24 10.14L5.25 10.12V10.11L5.24 10.13L5.25 10.12V10.11L5.26 10.07L5.27 9.97A6.5 6.5 0 0 1 6.26 7.59L6.32 7.5L6.39 7.4L6.55 7.19C6.65 7.07 6.74 6.94 6.86 6.82A6.61 6.61 0 0 1 8.37 5.59C8.5 5.5 8.64 5.43 8.77 5.37C8.9 5.29 9.04 5.24 9.17 5.18A6.76 6.76 0 0 1 9.94 4.9A6.5 6.5 0 0 1 10.62 4.74C10.82 4.69 11 4.68 11.15 4.66C11.3 4.64 11.42 4.64 11.5 4.63L11.62 4.62H11.5C11.42 4.62 11.3 4.61 11.15 4.61C11 4.61 10.82 4.61 10.61 4.63A6.41 6.41 0 0 0 9.9 4.73A7.03 7.03 0 0 0 9.08 4.93C8.94 5 8.79 5.03 8.65 5.09C8.5 5.14 8.35 5.21 8.2 5.28A7.26 7.26 0 0 0 7.31 5.78A7.33 7.33 0 0 0 6.47 6.42C6.33 6.54 6.2 6.68 6.07 6.8L5.9 7L5.82 7.09L5.72 7.19A7.25 7.25 0 0 0 5.12 8.04A7.38 7.38 0 0 0 4.36 9.75L4.33 9.84L4.32 9.89L4.3 9.95L4.25 10.13L4.21 10.29L4.18 10.5L4.15 10.63C4.14 10.65 4.14 10.67 4.14 10.7L4.13 10.72V10.78L4.12 10.81L4.09 11.06L4.05 11.5C3.79 11.57 3.53 11.65 3.28 11.74C3.28 11.74 1.93 12.05 2 12.72C2.08 13.35 3.41 13.5 3.41 13.5A11.21 11.21 0 0 0 10.24 12.74A2.62 2.62 0 0 1 10.16 11.57M19.7 10.84A7.19 7.19 0 0 0 19.53 9.79C19.5 9.62 19.43 9.45 19.38 9.27L19.3 9.03L19.26 8.91L19.21 8.77A7.23 7.23 0 0 0 18.75 7.83A7.35 7.35 0 0 0 17.62 6.35L17.55 6.28L17.5 6.25L17.47 6.2L17.33 6.08L17.21 5.97L17.06 5.85L16.94 5.75L16.89 5.7L16.88 5.69H16.87L16.83 5.66L16.8 5.64L16.59 5.5L16.32 5.31Q16.42 4.88 16.5 4.45S16.88 3.11 16.25 2.85C15.67 2.61 14.91 3.72 14.91 3.72A11.21 11.21 0 0 0 12.25 10.05A2.63 2.63 0 0 1 13.32 10.55A11.2 11.2 0 0 0 15.25 8A4.73 4.73 0 0 1 16.08 8.66A4.81 4.81 0 0 1 16.53 9.19L16.58 9.25L16.63 9.33L16.74 9.5C16.8 9.59 16.86 9.69 16.92 9.8A4.89 4.89 0 0 1 17.4 11.16A4.78 4.78 0 0 1 17.5 11.83A4.88 4.88 0 0 1 17.5 12.44A4.76 4.76 0 0 1 17.44 12.96C17.42 13.11 17.39 13.25 17.36 13.36C17.31 13.57 17.27 13.7 17.27 13.7L17.41 13.37C17.45 13.26 17.5 13.14 17.54 13A5.06 5.06 0 0 0 17.67 12.46A5.34 5.34 0 0 0 17.75 11.83A5.04 5.04 0 0 0 17.76 11.11A5.38 5.38 0 0 0 17.43 9.57C17.38 9.44 17.32 9.31 17.27 9.19L17.18 9L17.14 8.93L17.09 8.83A5.53 5.53 0 0 0 15.67 7.16C15.79 6.9 15.89 6.65 16 6.38L16.03 6.41L16.25 6.53L16.28 6.54V6.55H16.29C16.32 6.57 16.24 6.5 16.25 6.53H16.26L16.27 6.54L16.33 6.58L16.45 6.66L16.5 6.71L16.56 6.73L16.57 6.74H16.58L16.56 6.73H16.57L16.71 6.84L16.85 6.94L16.87 6.96L16.86 6.95L16.87 6.96L16.91 7L17 7.05A6.46 6.46 0 0 1 18.6 9.05L18.65 9.15L18.71 9.27L18.82 9.5C18.87 9.65 18.94 9.79 19 9.95A6.69 6.69 0 0 1 19.24 10.9A6.78 6.78 0 0 1 19.35 11.86C19.36 12 19.36 12.17 19.35 12.32C19.35 12.5 19.34 12.62 19.33 12.77A6.79 6.79 0 0 1 19.2 13.58A6.4 6.4 0 0 1 19 14.25C18.96 14.45 18.89 14.62 18.84 14.76C18.78 14.9 18.73 15 18.7 15.07L18.64 15.19L18.71 15.08C18.75 15 18.81 14.91 18.88 14.78C18.95 14.64 19.04 14.5 19.12 14.29A6.5 6.5 0 0 0 19.37 13.62A6.93 6.93 0 0 0 19.59 12.81C19.61 12.66 19.64 12.5 19.66 12.35C19.68 12.19 19.7 12.03 19.7 11.87A7.1 7.1 0 0 0 19.69 10.84", - "name": "concourse-ci" - }, - { - "path": "M12 2A10 10 0 1 0 22 12A10 10 0 0 0 12 2M15 10H11V11H14A1 1 0 0 1 15 12V15A1 1 0 0 1 14 16H13V17H11V16H9V14H13V13H10A1 1 0 0 1 9 12V9A1 1 0 0 1 10 8H11V7H13V8H15Z", - "name": "currency-usd-circle" - }, - { - "path": "M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4M11,17V16H9V14H13V13H10A1,1 0 0,1 9,12V9A1,1 0 0,1 10,8H11V7H13V8H15V10H11V11H14A1,1 0 0,1 15,12V15A1,1 0 0,1 14,16H13V17H11Z", - "name": "currency-usd-circle-outline" - }, - { - "path": "M20,6H4V4H20V6M20,18V20H4V18H7.33L6.26,14H5V8H19V14H17.74L16.67,18H20M7,12H17V10H7V12M9.4,18H14.6L15.67,14H8.33L9.4,18Z", - "name": "douban" - }, - { - "path": "M10,13C10.55,13 11,13.18 11.41,13.57C11.8,13.96 12,14.44 12,15V22C12,22.17 11.91,22.27 11.72,22.27C11.66,22.27 11.58,22.22 11.5,22.13L7,17.67V13H10M12.5,1.88L17,6.33V11H14C13.45,11 13,10.82 12.59,10.43C12.2,10.04 12,9.56 12,9V2C12,1.83 12.09,1.73 12.28,1.73C12.34,1.73 12.42,1.78 12.5,1.88M22,12C22.17,12 22.27,12.09 22.27,12.28C22.27,12.34 22.22,12.42 22.13,12.5L17.67,17H13V14C13,13.45 13.18,13 13.57,12.59C13.96,12.2 14.44,12 15,12H22M6.33,7H11V10C11,10.55 10.82,11 10.43,11.41C10.04,11.8 9.56,12 9,12H2C1.83,12 1.73,11.91 1.73,11.72C1.73,11.66 1.78,11.58 1.88,11.5L6.33,7Z", - "name": "google-photos" - }, - { - "path": "M12,3L22,12H19V20H5V12H2L12,3M9.22,8.93C8.75,9.4 8.5,10.03 8.5,10.75C8.5,12.43 10.54,13.07 11.76,13.46C13.26,13.93 13.47,14.21 13.5,14.25C13.5,15 12.15,15 12,15V15C11.37,15 11.03,14.88 10.86,14.78C10.67,14.67 10.5,14.5 10.5,14H8.5C8.5,15.43 9.24,16.16 9.85,16.5C10.18,16.7 10.57,16.84 11,16.92V18H13V16.91C14.53,16.61 15.5,15.62 15.5,14.25C15.5,12.67 13.88,12.03 12.36,11.55C10.8,11.06 10.53,10.77 10.5,10.75C10.5,10.5 10.57,10.41 10.64,10.34C10.85,10.13 11.36,10 12,10V10C12.68,10 13.5,10.13 13.5,10.75H15.5C15.5,9.34 14.56,8.37 13,8.09V7H11V8.08C10.26,8.21 9.65,8.5 9.22,8.93Z", - "name": "home-currency-usd" - }, - { - "path": "M 2.73675,10.8077C 3.8293,-1.36109 22.5157,-1.36109 21.1971,13.5579L 8.61392,13.5579C 8.61392,17.8527 14.4157,19.209 19.5394,16.3081L 19.5394,20.5276C 13.2478,23.8806 4.9972,21.4318 4.9972,14.0853C 4.9972,8.58476 9.97019,6.8142 9.97019,6.8142C 9.97019,6.8142 8.57624,8.58489 8.53857,10.0542L 15.6967,10.0542C 15.6967,2.93376 5.90137,5.57095 2.73675,10.8077 Z", - "name": "microsoft-edge-legacy" - }, - { - "path": "M22 12Q22 12.43 21.97 12.94 21.95 13.45 21.89 13.97 21.84 14.5 21.76 15 21.68 15.5 21.56 15.89 21.5 16.07 21.38 16.19 21.24 16.3 21.04 16.3 20.95 16.3 20.66 16.23 20.37 16.16 20.03 16.07L19.39 15.88Q19.09 15.79 18.96 15.76 18.75 16.54 18.41 17.38 18.08 18.21 17.65 19 17.22 19.8 16.7 20.5 16.18 21.25 15.61 21.82L15.43 21.95Q15.33 22 15.21 22 15 22 14.84 21.84L10.1 17.11H2.85Q2.5 17.11 2.25 16.86 2 16.61 2 16.26V7.74Q2 7.39 2.25 7.14 2.5 6.89 2.85 6.89H10.1L14.83 2.16Q15 2 15.21 2 15.33 2 15.42 2.05 15.5 2.09 15.59 2.18 15.85 2.44 16.08 2.71 16.3 3 16.5 3.28 18.23 5.55 18.96 8.28 19.14 8.23 19.44 8.14 19.74 8.05 20.06 7.96 20.37 7.87 20.65 7.8 20.92 7.74 21.04 7.74 21.24 7.74 21.38 7.85 21.5 7.97 21.56 8.15 21.68 8.56 21.77 9.05 21.85 9.55 21.91 10.06 21.96 10.57 22 11.08V12M9.82 9.37Q9.82 9.06 9.62 8.85 9.4 8.64 9.1 8.64 8.9 8.64 8.72 8.74 8.55 8.85 8.45 9.03L7.15 11.47L5.89 9.03Q5.77 8.8 5.57 8.72 5.37 8.64 5.13 8.64 4.82 8.64 4.61 8.85 4.4 9.06 4.4 9.36 4.4 9.57 4.5 9.73L6.25 12.87Q6.27 12.91 6.29 12.97 6.3 13.03 6.3 13.08V14.63Q6.3 15 6.56 15.19 6.81 15.36 7.15 15.36 7.39 15.36 7.54 15.27 7.68 15.18 7.76 15.03 7.84 14.88 7.87 14.69 7.9 14.5 7.9 14.28 7.9 14 7.88 13.76 7.86 13.5 7.86 13.28 7.86 13.14 7.87 13.03 7.88 12.93 7.93 12.85L9.73 9.73Q9.83 9.55 9.83 9.37M15.17 3.63L11.8 7Q12 7.12 12.1 7.31 12.22 7.5 12.22 7.74V10.07L17.72 8.61Q17.34 7.19 16.71 6 16.08 4.77 15.17 3.63M17.73 15.42L12.22 13.95V16.26Q12.22 16.5 12.1 16.69 12 16.88 11.8 17L15.18 20.37Q16.07 19.29 16.72 18.04 17.37 16.79 17.73 15.43V15.42M20.47 14.84Q20.6 14.14 20.66 13.43 20.72 12.73 20.72 12 20.72 11.29 20.66 10.59 20.6 9.89 20.47 9.19 18.4 9.74 16.35 10.29 14.3 10.83 12.22 11.39 12.21 11.55 12.21 11.7V12.32L12.22 12.63Q14.3 13.19 16.35 13.73 18.4 14.27 20.47 14.84Z", - "name": "microsoft-yammer" - }, - { - "path": "M9.78,18.65L10.06,14.42L17.74,7.5C18.08,7.19 17.67,7.04 17.22,7.31L7.74,13.3L3.64,12C2.76,11.75 2.75,11.14 3.84,10.7L19.81,4.54C20.54,4.21 21.24,4.72 20.96,5.84L18.24,18.65C18.05,19.56 17.5,19.78 16.74,19.36L12.6,16.3L10.61,18.23C10.38,18.46 10.19,18.65 9.78,18.65Z", - "name": "telegram" - }, - { - "path": "M14.41,4C14.41,4 14.94,4.39 14.97,4.71C14.97,4.81 14.73,4.85 14.68,4.93C14.62,5 14.7,5.15 14.65,5.21C14.59,5.26 14.5,5.26 14.41,5.41C14.33,5.56 12.07,10.09 11.73,10.63C11.59,11.03 11.47,12.46 11.37,12.66C11.26,12.85 6.34,19.84 6.16,20.05C5.67,20.63 4.31,20.3 3.28,19.56C2.3,18.86 1.74,17.7 2.11,17.16C2.27,16.93 7.15,9.92 7.29,9.75C7.44,9.58 8.75,9 9.07,8.71C9.47,8.22 12.96,4.54 13.07,4.42C13.18,4.3 13.15,4.2 13.18,4.13C13.22,4.06 13.38,4.08 13.43,4C13.5,3.93 13.39,3.71 13.5,3.68C13.59,3.64 13.96,3.67 14.41,4M10.85,4.44L11.74,5.37L10.26,6.94L9.46,5.37C9.38,5.22 9.28,5.22 9.22,5.17C9.17,5.11 9.24,4.97 9.19,4.89C9.13,4.81 8.9,4.83 8.9,4.73C8.9,4.62 9.05,4.28 9.5,3.96C9.5,3.96 10.06,3.6 10.37,3.68C10.47,3.71 10.43,3.95 10.5,4C10.54,4.1 10.7,4.08 10.73,4.15C10.77,4.21 10.73,4.32 10.85,4.44M21.92,17.15C22.29,17.81 21.53,19 20.5,19.7C19.5,20.39 18.21,20.54 17.83,20C17.66,19.78 12.67,12.82 12.56,12.62C12.45,12.43 12.32,11 12.18,10.59L12.15,10.55C12.45,10 13.07,8.77 13.73,7.47C14.3,8.06 14.75,8.56 14.88,8.72C15.21,9 16.53,9.58 16.68,9.75C16.82,9.92 21.78,16.91 21.92,17.15Z", - "name": "untappd" - }, - { - "path": "M15.07 2H8.93C3.33 2 2 3.33 2 8.93V15.07C2 20.67 3.33 22 8.93 22H15.07C20.67 22 22 20.67 22 15.07V8.93C22 3.33 20.67 2 15.07 2M18.15 16.27H16.69C16.14 16.27 15.97 15.82 15 14.83C14.12 14 13.74 13.88 13.53 13.88C13.24 13.88 13.15 13.96 13.15 14.38V15.69C13.15 16.04 13.04 16.26 12.11 16.26C10.57 16.26 8.86 15.32 7.66 13.59C5.85 11.05 5.36 9.13 5.36 8.75C5.36 8.54 5.43 8.34 5.85 8.34H7.32C7.69 8.34 7.83 8.5 7.97 8.9C8.69 11 9.89 12.8 10.38 12.8C10.57 12.8 10.65 12.71 10.65 12.25V10.1C10.6 9.12 10.07 9.03 10.07 8.68C10.07 8.5 10.21 8.34 10.44 8.34H12.73C13.04 8.34 13.15 8.5 13.15 8.88V11.77C13.15 12.08 13.28 12.19 13.38 12.19C13.56 12.19 13.72 12.08 14.05 11.74C15.1 10.57 15.85 8.76 15.85 8.76C15.95 8.55 16.11 8.35 16.5 8.35H17.93C18.37 8.35 18.47 8.58 18.37 8.89C18.19 9.74 16.41 12.25 16.43 12.25C16.27 12.5 16.21 12.61 16.43 12.9C16.58 13.11 17.09 13.55 17.43 13.94C18.05 14.65 18.53 15.24 18.66 15.65C18.77 16.06 18.57 16.27 18.15 16.27Z", - "name": "vk" - }, - { - "path": "M4.8,3C3.8,3 3,3.8 3,4.8V19.2C3,20.2 3.8,21 4.8,21H19.2C20.2,21 21,20.2 21,19.2V4.8C21,3.8 20.2,3 19.2,3M16.07,5H18.11C18.23,5 18.33,5.04 18.37,5.13C18.43,5.22 18.43,5.33 18.37,5.44L13.9,13.36L16.75,18.56C16.81,18.67 16.81,18.78 16.75,18.87C16.7,18.95 16.61,19 16.5,19H14.47C14.16,19 14,18.79 13.91,18.61L11.04,13.35C11.18,13.1 15.53,5.39 15.53,5.39C15.64,5.19 15.77,5 16.07,5M7.09,7.76H9.1C9.41,7.76 9.57,7.96 9.67,8.15L11.06,10.57C10.97,10.71 8.88,14.42 8.88,14.42C8.77,14.61 8.63,14.81 8.32,14.81H6.3C6.18,14.81 6.09,14.76 6.04,14.67C6,14.59 6,14.47 6.04,14.36L8.18,10.57L6.82,8.2C6.77,8.09 6.75,8 6.81,7.89C6.86,7.81 6.96,7.76 7.09,7.76Z", - "name": "xing" - }, - { - "path": "M2,2H22V22H2V2M11.25,17.5H12.75V13.06L16,7H14.5L12,11.66L9.5,7H8L11.25,13.06V17.5Z", - "name": "y-combinator" - } -] +[] diff --git a/src/components/ha-icon.ts b/src/components/ha-icon.ts index b6279d6ac0..a35808ba92 100644 --- a/src/components/ha-icon.ts +++ b/src/components/ha-icon.ts @@ -29,315 +29,7 @@ interface DeprecatedIcon { }; } -const mdiDeprecatedIcons: DeprecatedIcon = { - "adobe-acrobat": { - removeIn: "2021.12", - }, - adobe: { - removeIn: "2021.12", - }, - "amazon-alexa": { - removeIn: "2021.12", - }, - amazon: { - removeIn: "2021.12", - }, - "android-auto": { - removeIn: "2021.12", - }, - "android-debug-bridge": { - removeIn: "2021.12", - }, - "apple-airplay": { - newName: "cast-variant", - removeIn: "2021.12", - }, - bandcamp: { - removeIn: "2021.12", - }, - battlenet: { - removeIn: "2021.12", - }, - blogger: { - removeIn: "2021.12", - }, - "bolnisi-cross": { - newName: "cross-bolnisi", - removeIn: "2021.12", - }, - "boom-gate-down": { - newName: "boom-gate-arrow-down", - removeIn: "2021.12", - }, - "boom-gate-down-outline": { - newName: "boom-gate-arrow-down-outline", - removeIn: "2021.12", - }, - buddhism: { - newName: "dharmachakra", - removeIn: "2021.12", - }, - buffer: { - removeIn: "2021.12", - }, - "cash-usd-outline": { - removeIn: "2021.12", - }, - "cash-usd": { - removeIn: "2021.12", - }, - "cellphone-android": { - newName: "cellphone", - removeIn: "2021.12", - }, - "cellphone-erase": { - newName: "cellphone-remove", - removeIn: "2021.12", - }, - "cellphone-iphone": { - newName: "cellphone", - removeIn: "2021.12", - }, - "celtic-cross": { - newName: "cross-celtic", - removeIn: "2021.12", - }, - christianity: { - newName: "cross", - removeIn: "2021.12", - }, - "christianity-outline": { - newName: "cross-outline", - removeIn: "2021.12", - }, - "concourse-ci": { - removeIn: "2021.12", - }, - "currency-usd-circle": { - removeIn: "2021.12", - }, - "currency-usd-circle-outline": { - removeIn: "2021.12", - }, - "do-not-disturb-off": { - newName: "minus-circle-off", - removeIn: "2021.12", - }, - "do-not-disturb": { - newName: "minus-circle", - removeIn: "2021.12", - }, - douban: { - removeIn: "2021.12", - }, - face: { - newName: "face-man", - removeIn: "2021.12", - }, - "face-outline": { - newName: "face-man-outline", - removeIn: "2021.12", - }, - "face-profile-woman": { - newName: "face-woman-profile", - removeIn: "2021.12", - }, - "face-shimmer": { - newName: "face-man-shimmer", - removeIn: "2021.12", - }, - "face-shimmer-outline": { - newName: "face-man-shimmer-outline", - removeIn: "2021.12", - }, - "file-pdf": { - newName: "file-pdf-box", - removeIn: "2021.12", - }, - "file-pdf-outline": { - newName: "file-pdf-box", - removeIn: "2021.12", - }, - "file-pdf-box-outline": { - newName: "file-pdf-box", - removeIn: "2021.12", - }, - "flash-circle": { - newName: "lightning-bolt-circle", - removeIn: "2021.12", - }, - "floor-lamp-variant": { - newName: "floor-lamp-torchiere-variant", - removeIn: "2021.12", - }, - gif: { - newName: "file-gif-box", - removeIn: "2021.12", - }, - "google-photos": { - removeIn: "2021.12", - }, - gradient: { - newName: "gradient-vertical", - removeIn: "2021.12", - }, - hand: { - newName: "hand-front-right", - removeIn: "2021.12", - }, - "hand-left": { - newName: "hand-back-left", - removeIn: "2021.12", - }, - "hand-right": { - newName: "hand-back-right", - removeIn: "2021.12", - }, - hinduism: { - newName: "om", - removeIn: "2021.12", - }, - "home-currency-usd": { - removeIn: "2021.12", - }, - iframe: { - newName: "application-brackets", - removeIn: "2021.12", - }, - "iframe-outline": { - newName: "application-brackets-outline", - removeIn: "2021.12", - }, - "iframe-array": { - newName: "application-array", - removeIn: "2021.12", - }, - "iframe-array-outline": { - newName: "application-array-outline", - removeIn: "2021.12", - }, - "iframe-braces": { - newName: "application-braces", - removeIn: "2021.12", - }, - "iframe-braces-outline": { - newName: "application-braces-outline", - removeIn: "2021.12", - }, - "iframe-parentheses": { - newName: "application-parentheses", - removeIn: "2021.12", - }, - "iframe-parentheses-outline": { - newName: "application-parentheses-outline", - removeIn: "2021.12", - }, - "iframe-variable": { - newName: "application-variable", - removeIn: "2021.12", - }, - "iframe-variable-outline": { - newName: "application-variable-outline", - removeIn: "2021.12", - }, - islam: { - newName: "star-crescent", - removeIn: "2021.12", - }, - judaism: { - newName: "star-david", - removeIn: "2021.12", - }, - "laptop-chromebook": { - newName: "laptop", - removeIn: "2021.12", - }, - "laptop-mac": { - newName: "laptop", - removeIn: "2021.12", - }, - "laptop-windows": { - newName: "laptop", - removeIn: "2021.12", - }, - "microsoft-edge-legacy": { - removeIn: "2021.12", - }, - "microsoft-yammer": { - removeIn: "2021.12", - }, - "monitor-clean": { - newName: "monitor-shimmer", - removeIn: "2021.12", - }, - "pdf-box": { - newName: "file-pdf-box", - removeIn: "2021.12", - }, - pharmacy: { - newName: "mortar-pestle-plus", - removeIn: "2021.12", - }, - "plus-one": { - newName: "numeric-positive-1", - removeIn: "2021.12", - }, - "poll-box": { - newName: "chart-box", - removeIn: "2021.12", - }, - "poll-box-outline": { - newName: "chart-box-outline", - removeIn: "2021.12", - }, - sparkles: { - newName: "shimmer", - removeIn: "2021.12", - }, - "tablet-ipad": { - newName: "tablet", - removeIn: "2021.12", - }, - teach: { - newName: "human-male-board", - removeIn: "2021.12", - }, - telegram: { - removeIn: "2021.12", - }, - "television-clean": { - newName: "television-shimmer", - removeIn: "2021.12", - }, - "text-subject": { - newName: "text-long", - removeIn: "2021.12", - }, - "twitter-retweet": { - newName: "repeat-variant", - removeIn: "2021.12", - }, - untappd: { - removeIn: "2021.12", - }, - vk: { - removeIn: "2021.12", - }, - "voice-off": { - newName: "account-voice-off", - removeIn: "2021.12", - }, - "xamarian-outline": { - newName: "xamarian", - removeIn: "2021.12", - }, - xing: { - removeIn: "2021.12", - }, - "y-combinator": { - removeIn: "2021.12", - }, -}; +const mdiDeprecatedIcons: DeprecatedIcon = {}; const chunks: Chunks = {}; From 3154011c650f3cf400c603bcca845eb1241165fb Mon Sep 17 00:00:00 2001 From: Lasse Rosenow <10547444+LasseRosenow@users.noreply.github.com> Date: Mon, 15 Nov 2021 16:54:59 +0100 Subject: [PATCH 007/112] Improve startup experience by removing AppBar skeleton (#10569) --- demo/src/html/index.html.template | 51 +++++++++++++-- src/html/index.html.template | 63 ++++++++++++++----- src/layouts/ha-init-page.ts | 90 ++++++++++++++++----------- src/layouts/home-assistant.ts | 49 ++++++++++++--- src/layouts/partial-panel-resolver.ts | 4 +- src/util/init-skeleton.ts | 6 -- src/util/launch-screen.ts | 15 +++++ 7 files changed, 205 insertions(+), 73 deletions(-) delete mode 100644 src/util/init-skeleton.ts create mode 100644 src/util/launch-screen.ts diff --git a/demo/src/html/index.html.template b/demo/src/html/index.html.template index 05cabfe6e6..85e5388388 100644 --- a/demo/src/html/index.html.template +++ b/demo/src/html/index.html.template @@ -63,6 +63,16 @@ /> Home Assistant Demo -
+
+
+ + + + + + + + + + + + + + + + +
+
+ + <%= renderTemplate('_js_base') %> <%= renderTemplate('_preload_roboto') %> diff --git a/src/html/index.html.template b/src/html/index.html.template index f758aa5e33..d5ba727af1 100644 --- a/src/html/index.html.template +++ b/src/html/index.html.template @@ -39,29 +39,64 @@ -
- +
+
+ + + + + + + + + + + + + + + + +
+
+ + <%= renderTemplate('_js_base') %> <%= renderTemplate('_preload_roboto') %> diff --git a/src/layouts/ha-init-page.ts b/src/layouts/ha-init-page.ts index 0a4e730fde..fcd7e66f14 100644 --- a/src/layouts/ha-init-page.ts +++ b/src/layouts/ha-init-page.ts @@ -1,43 +1,52 @@ import "@material/mwc-button"; import { css, CSSResultGroup, html, LitElement } from "lit"; -import { property } from "lit/decorators"; -import "../components/ha-circular-progress"; -import { removeInitSkeleton } from "../util/init-skeleton"; +import { property, state } from "lit/decorators"; class HaInitPage extends LitElement { @property({ type: Boolean }) public error = false; + @state() showProgressIndicator = false; + + private _showProgressIndicatorTimeout; + protected render() { - return html` -
- - ${this.error - ? html` -

Unable to connect to Home Assistant.

- Retry - ${location.host.includes("ui.nabu.casa") - ? html` -

- It is possible that you are seeing this screen because - your Home Assistant is not currently connected. You can - ask it to come online from your - Naba Casa account page. -

- ` - : ""} - ` - : html` - -

Loading data

- `} -
- `; + return this.error + ? html` +

Unable to connect to Home Assistant.

+ Retry + ${location.host.includes("ui.nabu.casa") + ? html` +

+ It is possible that you are seeing this screen because your + Home Assistant is not currently connected. You can ask it to + come online from your + Naba Casa account page. +

+ ` + : ""} + ` + : html` +
+ ${this.showProgressIndicator + ? html`` + : ""} +
+
Loading data
+ `; + } + + disconnectedCallback() { + super.disconnectedCallback(); + clearTimeout(this._showProgressIndicatorTimeout); } protected firstUpdated() { - removeInitSkeleton(); + this._showProgressIndicatorTimeout = setTimeout(async () => { + await import("../components/ha-circular-progress"); + this.showProgressIndicator = true; + }, 5000); } private _retry() { @@ -46,20 +55,23 @@ class HaInitPage extends LitElement { static get styles(): CSSResultGroup { return css` - div { - height: 100%; + :host { + flex: 0; display: flex; flex-direction: column; - justify-content: center; align-items: center; } - ha-circular-progress { - margin-top: 9px; + #progress-indicator-wrapper { + display: flex; + align-items: center; + margin: 25px 0; + height: 50px; } a { color: var(--primary-color); } - p { + p, + #loading-text { max-width: 350px; color: var(--primary-text-color); } @@ -68,3 +80,9 @@ class HaInitPage extends LitElement { } customElements.define("ha-init-page", HaInitPage); + +declare global { + interface HTMLElementTagNameMap { + "ha-init-page": HaInitPage; + } +} diff --git a/src/layouts/home-assistant.ts b/src/layouts/home-assistant.ts index 4c31f66f72..bb452d1781 100644 --- a/src/layouts/home-assistant.ts +++ b/src/layouts/home-assistant.ts @@ -8,6 +8,10 @@ import { HassElement } from "../state/hass-element"; import QuickBarMixin from "../state/quick-bar-mixin"; import { HomeAssistant, Route } from "../types"; import { storeState } from "../util/ha-pref-storage"; +import { + renderLaunchScreenInfoBox, + removeLaunchScreen, +} from "../util/launch-screen"; import { registerServiceWorker, supportsServiceWorker, @@ -40,6 +44,8 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) { private _visiblePromiseResolve?: () => void; + private _visibleLaunchScreen = true; + constructor() { super(); const path = curPath(); @@ -55,16 +61,26 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) { } protected render() { - const hass = this.hass; + if (this._isHassComplete() && this.hass) { + return html` + + `; + } - return hass && hass.states && hass.config && hass.services - ? html` - - ` - : html``; + return ""; + } + + update(changedProps) { + super.update(changedProps); + + // Remove launch screen if main gui is loaded + if (this._isHassComplete() && this._visibleLaunchScreen) { + this._visibleLaunchScreen = false; + removeLaunchScreen(); + } } protected firstUpdated(changedProps) { @@ -109,6 +125,13 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) { navigate(href); } }); + + // Render launch screen info box (loading data / error message) + if (!this._isHassComplete() && this._visibleLaunchScreen) { + renderLaunchScreenInfoBox( + html`` + ); + } } protected updated(changedProps: PropertyValues): void { @@ -229,6 +252,14 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) { this._visiblePromiseResolve = undefined; } } + + private _isHassComplete(): boolean { + if (this.hass?.states && this.hass.config && this.hass.services) { + return true; + } + + return false; + } } declare global { diff --git a/src/layouts/partial-panel-resolver.ts b/src/layouts/partial-panel-resolver.ts index b3d1a72b6a..48b44a0b28 100644 --- a/src/layouts/partial-panel-resolver.ts +++ b/src/layouts/partial-panel-resolver.ts @@ -11,7 +11,7 @@ import { deepEqual } from "../common/util/deep-equal"; import { getDefaultPanel } from "../data/panel"; import { CustomPanelInfo } from "../data/panel_custom"; import { HomeAssistant, Panels } from "../types"; -import { removeInitSkeleton } from "../util/init-skeleton"; +import { removeLaunchScreen } from "../util/launch-screen"; import { HassRouterPage, RouteOptions, @@ -226,7 +226,7 @@ class PartialPanelResolver extends HassRouterPage { ) { await this.rebuild(); await this.pageRendered; - removeInitSkeleton(); + removeLaunchScreen(); } } } diff --git a/src/util/init-skeleton.ts b/src/util/init-skeleton.ts deleted file mode 100644 index f5912e70a0..0000000000 --- a/src/util/init-skeleton.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const removeInitSkeleton = () => { - const initEl = document.getElementById("ha-init-skeleton"); - if (initEl) { - initEl.parentElement!.removeChild(initEl); - } -}; diff --git a/src/util/launch-screen.ts b/src/util/launch-screen.ts new file mode 100644 index 0000000000..eb873a65db --- /dev/null +++ b/src/util/launch-screen.ts @@ -0,0 +1,15 @@ +import { render, TemplateResult } from "lit"; + +export const removeLaunchScreen = () => { + const launchScreenElement = document.getElementById("ha-launch-screen"); + if (launchScreenElement) { + launchScreenElement.parentElement!.removeChild(launchScreenElement); + } +}; + +export const renderLaunchScreenInfoBox = (element: TemplateResult) => { + const infoBoxElement = document.getElementById("ha-launch-screen-info-box"); + if (infoBoxElement) { + render(element, infoBoxElement); + } +}; From 4b992fb0c46482769e019af706102aedf8ca9f45 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 15 Nov 2021 12:11:31 -0500 Subject: [PATCH 008/112] Correct ZHA LQI sort in device children dialog (#10616) --- src/data/zha.ts | 2 +- .../integration-panels/zha/dialog-zha-device-children.ts | 4 ++-- .../integration-panels/zha/zha-network-visualization-page.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/data/zha.ts b/src/data/zha.ts index b8c99e0e24..d1b6f2ab97 100644 --- a/src/data/zha.ts +++ b/src/data/zha.ts @@ -34,7 +34,7 @@ export interface ZHADevice { export interface Neighbor { ieee: string; nwk: string; - lqi: number; + lqi: string; } export interface ZHADeviceEndpoint { diff --git a/src/panels/config/integrations/integration-panels/zha/dialog-zha-device-children.ts b/src/panels/config/integrations/integration-panels/zha/dialog-zha-device-children.ts index 032ee83778..91bd86246c 100644 --- a/src/panels/config/integrations/integration-panels/zha/dialog-zha-device-children.ts +++ b/src/panels/config/integrations/integration-panels/zha/dialog-zha-device-children.ts @@ -43,7 +43,7 @@ class DialogZHADeviceChildren extends LitElement { outputDevices.push({ name: zhaDevice.user_given_name || zhaDevice.name, id: zhaDevice.device_reg_id, - lqi: child.lqi, + lqi: parseInt(child.lqi), }); } }); @@ -64,7 +64,7 @@ class DialogZHADeviceChildren extends LitElement { title: "LQI", sortable: true, filterable: true, - direction: "asc", + type: "numeric", width: "75px", }, }; diff --git a/src/panels/config/integrations/integration-panels/zha/zha-network-visualization-page.ts b/src/panels/config/integrations/integration-panels/zha/zha-network-visualization-page.ts index 8403cac13b..f635d6232a 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-network-visualization-page.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-network-visualization-page.ts @@ -233,11 +233,11 @@ export class ZHANetworkVisualizationPage extends LitElement { from: device.ieee, to: neighbor.ieee, label: neighbor.lqi + "", - color: this._getLQI(neighbor.lqi), + color: this._getLQI(parseInt(neighbor.lqi)), }); } else { edges[idx].color = this._getLQI( - (parseInt(edges[idx].label!) + neighbor.lqi) / 2 + (parseInt(edges[idx].label!) + parseInt(neighbor.lqi)) / 2 ); edges[idx].label += "/" + neighbor.lqi; } From 87c2046ab574ae0d57cb06038e802deaeb83b2a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 15 Nov 2021 18:15:20 +0100 Subject: [PATCH 009/112] Remove add-on store tab (#10624) --- hassio/src/addon-store/hassio-addon-store.ts | 13 ++++--------- hassio/src/addon-view/hassio-addon-dashboard.ts | 1 - hassio/src/dashboard/hassio-dashboard.ts | 11 +++++++++++ hassio/src/hassio-tabs.ts | 7 +------ 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/hassio/src/addon-store/hassio-addon-store.ts b/hassio/src/addon-store/hassio-addon-store.ts index 4cc55898a1..16548cd40c 100644 --- a/hassio/src/addon-store/hassio-addon-store.ts +++ b/hassio/src/addon-store/hassio-addon-store.ts @@ -25,11 +25,10 @@ import { } from "../../../src/data/hassio/addon"; import { Supervisor } from "../../../src/data/supervisor/supervisor"; import "../../../src/layouts/hass-loading-screen"; -import "../../../src/layouts/hass-tabs-subpage"; +import "../../../src/layouts/hass-subpage"; import { HomeAssistant, Route } from "../../../src/types"; import { showRegistriesDialog } from "../dialogs/registries/show-dialog-registries"; import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories"; -import { supervisorTabs } from "../hassio-tabs"; import "./hassio-addon-repository"; const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => { @@ -76,16 +75,12 @@ class HassioAddonStore extends LitElement { } return html` - - ${this.supervisor.localize("panel.store")} ` : ""} - + `; } diff --git a/hassio/src/addon-view/hassio-addon-dashboard.ts b/hassio/src/addon-view/hassio-addon-dashboard.ts index 5de3ac1c5a..4455982ecc 100644 --- a/hassio/src/addon-view/hassio-addon-dashboard.ts +++ b/hassio/src/addon-view/hassio-addon-dashboard.ts @@ -108,7 +108,6 @@ class HassioAddonDashboard extends LitElement { .hass=${this.hass} .localizeFunc=${this.supervisor.localize} .narrow=${this.narrow} - .backPath=${this.addon.version ? "/hassio/dashboard" : "/hassio/store"} .route=${route} .tabs=${addonTabs} supervisor diff --git a/hassio/src/dashboard/hassio-dashboard.ts b/hassio/src/dashboard/hassio-dashboard.ts index 6b98c759ce..fb2916f1e3 100644 --- a/hassio/src/dashboard/hassio-dashboard.ts +++ b/hassio/src/dashboard/hassio-dashboard.ts @@ -1,5 +1,7 @@ +import { mdiStorePlus } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; +import "../../../src/components/ha-fab"; import { Supervisor } from "../../../src/data/supervisor/supervisor"; import "../../../src/layouts/hass-tabs-subpage"; import { haStyle } from "../../../src/resources/styles"; @@ -28,6 +30,7 @@ class HassioDashboard extends LitElement { .tabs=${supervisorTabs} main-page supervisor + hasFab > ${this.supervisor.localize("panel.dashboard")} @@ -42,6 +45,14 @@ class HassioDashboard extends LitElement { .supervisor=${this.supervisor} >
+ + + + `; } diff --git a/hassio/src/hassio-tabs.ts b/hassio/src/hassio-tabs.ts index 5131b56f0d..6abec75296 100644 --- a/hassio/src/hassio-tabs.ts +++ b/hassio/src/hassio-tabs.ts @@ -1,4 +1,4 @@ -import { mdiBackupRestore, mdiCogs, mdiStore, mdiViewDashboard } from "@mdi/js"; +import { mdiBackupRestore, mdiCogs, mdiViewDashboard } from "@mdi/js"; import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage"; export const supervisorTabs: PageNavigation[] = [ @@ -7,11 +7,6 @@ export const supervisorTabs: PageNavigation[] = [ path: `/hassio/dashboard`, iconPath: mdiViewDashboard, }, - { - translationKey: "panel.store", - path: `/hassio/store`, - iconPath: mdiStore, - }, { translationKey: "panel.backups", path: `/hassio/backups`, From a6b98fc3c3be15b136eca9ea3d957462fcce7857 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 15 Nov 2021 23:11:42 +0100 Subject: [PATCH 010/112] Add markers-updated to ha-locations-editor (#10601) --- src/components/map/ha-locations-editor.ts | 2 ++ src/onboarding/onboarding-core-config.ts | 21 ++++++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/components/map/ha-locations-editor.ts b/src/components/map/ha-locations-editor.ts index 930e8efd3b..d21a7185ac 100644 --- a/src/components/map/ha-locations-editor.ts +++ b/src/components/map/ha-locations-editor.ts @@ -26,6 +26,7 @@ declare global { // for fire event interface HASSDomEvents { "location-updated": { id: string; location: [number, number] }; + "markers-updated": undefined; "radius-updated": { id: string; radius: number }; "marker-clicked": { id: string }; } @@ -281,6 +282,7 @@ export class HaLocationsEditor extends LitElement { }); this._circles = circles; this._locationMarkers = locationMarkers; + fireEvent(this, "markers-updated"); } static get styles(): CSSResultGroup { diff --git a/src/onboarding/onboarding-core-config.ts b/src/onboarding/onboarding-core-config.ts index d0f61cfbbc..b18f14653f 100644 --- a/src/onboarding/onboarding-core-config.ts +++ b/src/onboarding/onboarding-core-config.ts @@ -2,13 +2,16 @@ 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 { customElement, property, state } from "lit/decorators"; +import { customElement, property, query, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../common/dom/fire_event"; import type { LocalizeFunc } from "../common/translations/localize"; import { createCurrencyListEl } from "../components/currency-datalist"; import "../components/map/ha-locations-editor"; -import type { MarkerLocation } from "../components/map/ha-locations-editor"; +import type { + HaLocationsEditor, + MarkerLocation, +} from "../components/map/ha-locations-editor"; import { createTimezoneListEl } from "../components/timezone-datalist"; import { ConfigUpdateValues, @@ -25,6 +28,7 @@ import type { HaRadio } from "../components/ha-radio"; const amsterdam: [number, number] = [52.3731339, 4.8903147]; const mql = matchMedia("(prefers-color-scheme: dark)"); +const locationMarkerId = "location"; @customElement("onboarding-core-config") class OnboardingCoreConfig extends LitElement { @@ -46,6 +50,8 @@ class OnboardingCoreConfig extends LitElement { @state() private _timeZone?: string; + @query("ha-locations-editor", true) private map!: HaLocationsEditor; + protected render(): TemplateResult { return html`

@@ -268,7 +274,7 @@ class OnboardingCoreConfig extends LitElement { private _markerLocation = memoizeOne( (location: [number, number]): MarkerLocation[] => [ { - id: "location", + id: locationMarkerId, latitude: location[0], longitude: location[1], location_editable: true, @@ -304,6 +310,15 @@ class OnboardingCoreConfig extends LitElement { const values = await detectCoreConfig(this.hass); if (values.latitude && values.longitude) { + this.map.addEventListener( + "markers-updated", + () => { + this.map.fitMarker(locationMarkerId); + }, + { + once: true, + } + ); this._location = [Number(values.latitude), Number(values.longitude)]; } if (values.elevation) { From b969db0c0fcaf08e917b8e33cc25d8dd88ae4eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 15 Nov 2021 23:21:29 +0100 Subject: [PATCH 011/112] Use ha-form for onboarding-create-user (#10604) --- src/onboarding/onboarding-create-user.ts | 205 +++++++++-------------- src/translations/en.json | 1 - 2 files changed, 75 insertions(+), 131 deletions(-) diff --git a/src/onboarding/onboarding-create-user.ts b/src/onboarding/onboarding-create-user.ts index d9f1eca9fe..bec0c77479 100644 --- a/src/onboarding/onboarding-create-user.ts +++ b/src/onboarding/onboarding-create-user.ts @@ -1,5 +1,4 @@ import "@material/mwc-button"; -import "@polymer/paper-input/paper-input"; import { genClientId } from "home-assistant-js-websocket"; import { css, @@ -9,168 +8,116 @@ import { PropertyValues, TemplateResult, } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { customElement, property, query, state } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; import { LocalizeFunc } from "../common/translations/localize"; +import "../components/ha-form/ha-form"; +import type { HaForm } from "../components/ha-form/ha-form"; +import { HaFormDataContainer, HaFormSchema } from "../components/ha-form/types"; import { onboardUserStep } from "../data/onboarding"; import { PolymerChangedEvent } from "../polymer-types"; +const CREATE_USER_SCHEMA: HaFormSchema[] = [ + { type: "string", name: "name", required: true }, + { type: "string", name: "username", required: true }, + { type: "string", name: "password", required: true }, + { type: "string", name: "password_confirm", required: true }, +]; + @customElement("onboarding-create-user") class OnboardingCreateUser extends LitElement { @property() public localize!: LocalizeFunc; @property() public language!: string; - @state() private _name = ""; - - @state() private _username = ""; - - @state() private _password = ""; - - @state() private _passwordConfirm = ""; - @state() private _loading = false; - @state() private _errorMsg?: string = undefined; + @state() private _errorMsg?: string; + + @state() private _formError: Record = {}; + + @state() private _newUser: HaFormDataContainer = {}; + + @query("ha-form", true) private _form?: HaForm; protected render(): TemplateResult { return html` -

- ${this.localize("ui.panel.page-onboarding.intro")} -

+

${this.localize("ui.panel.page-onboarding.intro")}

+

${this.localize("ui.panel.page-onboarding.user.intro")}

-

- ${this.localize("ui.panel.page-onboarding.user.intro")} -

+ ${this._errorMsg + ? html`${this._errorMsg}` + : ""} - ${ - this._errorMsg - ? html` -

- ${this.localize( - `ui.panel.page-onboarding.user.error.${this._errorMsg}` - ) || this._errorMsg} -

- ` - : "" - } - -
- + > - - - - - - -

- - ${this.localize("ui.panel.page-onboarding.user.create_account")} - -

-
- -`; + + ${this.localize("ui.panel.page-onboarding.user.create_account")} + + `; } protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); - setTimeout( - () => this.shadowRoot!.querySelector("paper-input")!.focus(), - 100 - ); + setTimeout(() => this._form?.focus(), 100); this.addEventListener("keypress", (ev) => { - if (ev.keyCode === 13) { + if ( + ev.keyCode === 13 && + this._newUser.name && + this._newUser.username && + this._newUser.password && + this._newUser.password_confirm + ) { this._submitForm(ev); } }); } - private _handleValueChanged(ev: PolymerChangedEvent): void { - const name = (ev.target as any).name; - this[`_${name}`] = ev.detail.value; + private _computeLabel(localize) { + return (schema: HaFormSchema) => + localize(`ui.panel.page-onboarding.user.data.${schema.name}`); + } + + private _handleValueChanged( + ev: PolymerChangedEvent + ): void { + this._newUser = ev.detail.value; + this._maybePopulateUsername(); + this._formError.password_confirm = + this._newUser.password !== this._newUser.password_confirm + ? this.localize( + "ui.panel.page-onboarding.user.error.password_not_match" + ) + : ""; } private _maybePopulateUsername(): void { - if (this._username) { + if (!this._newUser.name || this._newUser.name === this._newUser.username) { return; } - const parts = this._name.split(" "); - + const parts = String(this._newUser.name).split(" "); if (parts.length) { - this._username = parts[0].toLowerCase(); + this._newUser.username = parts[0].toLowerCase(); } } private async _submitForm(ev): Promise { ev.preventDefault(); - if (!this._name || !this._username || !this._password) { - this._errorMsg = "required_fields"; - return; - } - - if (this._password !== this._passwordConfirm) { - this._errorMsg = "password_not_match"; - return; - } - this._loading = true; this._errorMsg = ""; @@ -179,9 +126,9 @@ class OnboardingCreateUser extends LitElement { const result = await onboardUserStep({ client_id: clientId, - name: this._name, - username: this._username, - password: this._password, + name: String(this._newUser.name), + username: String(this._newUser.username), + password: String(this._newUser.password), language: this.language, }); @@ -199,13 +146,11 @@ class OnboardingCreateUser extends LitElement { static get styles(): CSSResultGroup { return css` - .error { - color: red; - } - - .action { - margin: 32px 0 16px; + mwc-button { + margin: 32px 0 0; text-align: center; + display: block; + text-align: right; } `; } diff --git a/src/translations/en.json b/src/translations/en.json index 9a565b0875..178930041a 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3924,7 +3924,6 @@ }, "create_account": "Create Account", "error": { - "required_fields": "Fill in all required fields", "password_not_match": "Passwords don't match" } }, From 481da19c743f7d88f9a8602dc5eb1993dd420b2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 16 Nov 2021 19:46:41 +0100 Subject: [PATCH 012/112] Fix datatable checkbox width (#10631) --- src/components/data-table/ha-data-table.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index 11e0a50a30..a9c3f300b5 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -678,7 +678,7 @@ export class HaDataTable extends LitElement { padding-left: 16px; /* @noflip */ padding-right: 0; - width: 56px; + width: 60px; } :host([dir="rtl"]) .mdc-data-table__header-cell--checkbox, :host([dir="rtl"]) .mdc-data-table__cell--checkbox { From e9f0967578101bec703645565e8c54116d9d8b71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 17 Nov 2021 19:21:27 +0100 Subject: [PATCH 013/112] Move updates (#10626) --- gallery/src/demos/demo-ha-alert.ts | 26 +- .../src/addon-view/info/hassio-addon-info.ts | 135 ++---- hassio/src/backups/hassio-backups.ts | 2 +- hassio/src/dashboard/hassio-addons.ts | 4 +- hassio/src/dashboard/hassio-dashboard.ts | 15 +- hassio/src/dashboard/hassio-update.ts | 121 +----- .../update/dialog-supervisor-update.ts | 203 --------- .../src/dialogs/update/show-dialog-update.ts | 21 - hassio/src/hassio-my-redirect.ts | 3 + hassio/src/hassio-router.ts | 4 + hassio/src/hassio-tabs.ts | 19 +- hassio/src/system/hassio-core-info.ts | 44 +- hassio/src/system/hassio-host-info.ts | 60 +-- hassio/src/system/hassio-supervisor-info.ts | 66 +-- hassio/src/system/hassio-system.ts | 2 +- .../update-available-dashboard.ts | 391 ++++++++++++++++++ hassio/src/util/addon.ts | 23 ++ src/components/ha-alert.ts | 46 ++- src/components/ha-logo-svg.ts | 51 +++ src/components/ha-sidebar.ts | 13 +- src/data/supervisor/supervisor.ts | 47 +++ src/layouts/home-assistant-main.ts | 1 + .../config/dashboard/ha-config-dashboard.ts | 12 +- .../config/dashboard/ha-config-updates.ts | 120 ++++++ src/translations/en.json | 26 +- 25 files changed, 829 insertions(+), 626 deletions(-) delete mode 100644 hassio/src/dialogs/update/dialog-supervisor-update.ts delete mode 100644 hassio/src/dialogs/update/show-dialog-update.ts create mode 100644 hassio/src/update-available/update-available-dashboard.ts create mode 100644 src/components/ha-logo-svg.ts create mode 100644 src/panels/config/dashboard/ha-config-updates.ts diff --git a/gallery/src/demos/demo-ha-alert.ts b/gallery/src/demos/demo-ha-alert.ts index 677c772127..bb833bed54 100644 --- a/gallery/src/demos/demo-ha-alert.ts +++ b/gallery/src/demos/demo-ha-alert.ts @@ -1,3 +1,4 @@ +import "../../../src/components/ha-logo-svg"; import { html, css, LitElement, TemplateResult } from "lit"; import { customElement } from "lit/decorators"; import "../../../src/components/ha-alert"; @@ -10,6 +11,8 @@ const alerts: { dismissable?: boolean; action?: string; rtl?: boolean; + iconSlot?: TemplateResult; + actionSlot?: TemplateResult; }[] = [ { title: "Test info alert", @@ -81,6 +84,18 @@ const alerts: { type: "warning", action: "save", }, + { + title: "Slotted icon", + description: "Alert with slotted icon", + type: "warning", + iconSlot: html``, + }, + { + title: "Slotted action", + description: "Alert with slotted action", + type: "info", + actionSlot: html``, + }, { description: "Dismissable information (RTL)", type: "info", @@ -117,7 +132,7 @@ export class DemoHaAlert extends LitElement { .actionText=${alert.action || ""} .rtl=${alert.rtl || false} > - ${alert.description} + ${alert.iconSlot} ${alert.description} ${alert.actionSlot} ` )} @@ -145,6 +160,15 @@ export class DemoHaAlert extends LitElement { span { margin-right: 16px; } + ha-logo-svg { + width: 28px; + height: 28px; + padding-right: 8px; + place-self: center; + } + mwc-button { + --mdc-theme-primary: var(--primary-text-color); + } `; } } diff --git a/hassio/src/addon-view/info/hassio-addon-info.ts b/hassio/src/addon-view/info/hassio-addon-info.ts index 48cd99b8ec..be50bc59c5 100644 --- a/hassio/src/addon-view/info/hassio-addon-info.ts +++ b/hassio/src/addon-view/info/hassio-addon-info.ts @@ -1,6 +1,5 @@ import "@material/mwc-button"; import { - mdiArrowUpBoldCircle, mdiCheckCircle, mdiChip, mdiCircle, @@ -49,7 +48,6 @@ import { startHassioAddon, stopHassioAddon, uninstallHassioAddon, - updateHassioAddon, validateHassioAddonOption, } from "../../../../src/data/hassio/addon"; import { @@ -69,9 +67,8 @@ import { bytesToString } from "../../../../src/util/bytes-to-string"; import "../../components/hassio-card-content"; import "../../components/supervisor-metric"; import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown"; -import { showDialogSupervisorUpdate } from "../../dialogs/update/show-dialog-update"; import { hassioStyle } from "../../resources/hassio-style"; -import { addonArchIsSupported } from "../../util/addon"; +import { addonArchIsSupported, extractChangelog } from "../../util/addon"; const STAGE_ICON = { stable: mdiCheckCircle, @@ -128,69 +125,23 @@ class HassioAddonInfo extends LitElement { return html` ${this.addon.update_available ? html` - -
- - ${!this.addon.available && addonStoreInfo - ? !addonArchIsSupported( - this.supervisor.info.supported_arch, - this.addon.arch - ) - ? html` - - ${this.supervisor.localize( - "addon.dashboard.not_available_arch" - )} - - ` - : html` - - ${this.supervisor.localize( - "addon.dashboard.not_available_arch", - "core_version_installed", - this.supervisor.core.version, - "core_version_needed", - addonStoreInfo.homeassistant - )} - - ` - : ""} -
-
- ${this.addon.changelog - ? html` - - ${this.supervisor.localize("addon.dashboard.changelog")} - - ` - : html``} - - ${this.supervisor.localize("common.update")} + ${this.supervisor.localize( + "addon.dashboard.new_update_available", + { name: this.addon.name, version: this.addon.version_latest } + )} + + -
-
+ + ` : ""} ${!this.addon.protected @@ -899,22 +850,14 @@ class HassioAddonInfo extends LitElement { private async _openChangelog(): Promise { try { - let content = await fetchHassioAddonChangelog(this.hass, this.addon.slug); - if ( - content.includes(`# ${this.addon.version}`) && - content.includes(`# ${this.addon.version_latest}`) - ) { - const newcontent = content.split(`# ${this.addon.version}`)[0]; - if (newcontent.includes(`# ${this.addon.version_latest}`)) { - // Only change the content if the new version still exist - // if the changelog does not have the newests version on top - // this will not be true, and we don't modify the content - content = newcontent; - } - } + const content = await fetchHassioAddonChangelog( + this.hass, + this.addon.slug + ); + showHassioMarkdownDialog(this, { title: this.supervisor.localize("addon.dashboard.changelog"), - content, + content: extractChangelog(this.addon, content), }); } catch (err: any) { showAlertDialog(this, { @@ -989,33 +932,6 @@ class HassioAddonInfo extends LitElement { button.progress = false; } - private async _updateClicked(): Promise { - showDialogSupervisorUpdate(this, { - supervisor: this.supervisor, - name: this.addon.name, - version: this.addon.version_latest, - backupParams: { - name: `addon_${this.addon.slug}_${this.addon.version}`, - addons: [this.addon.slug], - homeassistant: false, - }, - updateHandler: async () => this._updateAddon(), - }); - } - - private async _updateAddon(): Promise { - await updateHassioAddon(this.hass, this.addon.slug); - fireEvent(this, "supervisor-collection-refresh", { - collection: "addon", - }); - const eventdata = { - success: true, - response: undefined, - path: "update", - }; - fireEvent(this, "hass-api-called", eventdata); - } - private async _startClicked(ev: CustomEvent): Promise { const button = ev.currentTarget as any; button.progress = true; @@ -1244,6 +1160,13 @@ class HassioAddonInfo extends LitElement { align-self: end; } + ha-alert mwc-button { + --mdc-theme-primary: var(--primary-text-color); + } + a { + text-decoration: none; + } + @media (max-width: 720px) { ha-chip { line-height: 36px; diff --git a/hassio/src/backups/hassio-backups.ts b/hassio/src/backups/hassio-backups.ts index e75dc96d2a..31b136ed04 100644 --- a/hassio/src/backups/hassio-backups.ts +++ b/hassio/src/backups/hassio-backups.ts @@ -158,7 +158,7 @@ export class HassioBackups extends LitElement { } return html` -

${this.supervisor.localize("dashboard.addons")}

+ ${!atLeastVersion(this.hass.config.version, 2021, 12) + ? html`

${this.supervisor.localize("dashboard.addons")}

` + : ""}
${!this.supervisor.supervisor.addons?.length ? html` diff --git a/hassio/src/dashboard/hassio-dashboard.ts b/hassio/src/dashboard/hassio-dashboard.ts index fb2916f1e3..ed74bb920d 100644 --- a/hassio/src/dashboard/hassio-dashboard.ts +++ b/hassio/src/dashboard/hassio-dashboard.ts @@ -1,6 +1,7 @@ import { mdiStorePlus } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; +import { atLeastVersion } from "../../../src/common/config/version"; import "../../../src/components/ha-fab"; import { Supervisor } from "../../../src/data/supervisor/supervisor"; import "../../../src/layouts/hass-tabs-subpage"; @@ -27,7 +28,7 @@ class HassioDashboard extends LitElement { .localizeFunc=${this.supervisor.localize} .narrow=${this.narrow} .route=${this.route} - .tabs=${supervisorTabs} + .tabs=${supervisorTabs(this.hass)} main-page supervisor hasFab @@ -36,10 +37,14 @@ class HassioDashboard extends LitElement { ${this.supervisor.localize("panel.dashboard")}
- + ${!atLeastVersion(this.hass.config.version, 2021, 12) + ? html` + + ` + : ""} @@ -73,26 +57,18 @@ export class HassioUpdate extends LitElement { ${this._renderUpdateCard( "Home Assistant Core", "core", - this.supervisor.core, - "hassio/homeassistant/update", - `https://${ - this.supervisor.core.version_latest.includes("b") ? "rc" : "www" - }.home-assistant.io/latest-release-notes/` + this.supervisor.core )} ${this._renderUpdateCard( "Supervisor", "supervisor", - this.supervisor.supervisor, - "hassio/supervisor/update", - `https://github.com//home-assistant/hassio/releases/tag/${this.supervisor.supervisor.version_latest}` + this.supervisor.supervisor )} ${this.supervisor.host.features.includes("haos") ? this._renderUpdateCard( "Operating System", "os", - this.supervisor.os, - "hassio/os/update", - `https://github.com//home-assistant/hassos/releases/tag/${this.supervisor.os.version_latest}` + this.supervisor.os ) : ""}
@@ -103,9 +79,7 @@ export class HassioUpdate extends LitElement { private _renderUpdateCard( name: string, key: string, - object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo, - apiPath: string, - releaseNotesUrl: string + object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo ): TemplateResult { if (!object.update_available) { return html``; @@ -136,96 +110,15 @@ export class HassioUpdate extends LitElement {
- - - ${this.supervisor.localize("common.release_notes")} + + - - ${this.supervisor.localize("common.update")} -
`; } - private async _confirmUpdate(ev): Promise { - const item = ev.currentTarget; - if (item.key === "core") { - showDialogSupervisorUpdate(this, { - supervisor: this.supervisor, - name: "Home Assistant Core", - version: this.supervisor.core.version_latest, - backupParams: { - name: `core_${this.supervisor.core.version}`, - folders: ["homeassistant"], - homeassistant: true, - }, - updateHandler: async () => this._updateCore(), - }); - return; - } - item.progress = true; - const confirmed = await showConfirmationDialog(this, { - title: this.supervisor.localize( - "confirm.update.title", - "name", - item.name - ), - text: this.supervisor.localize( - "confirm.update.text", - "name", - item.name, - "version", - computeVersion(item.key, item.version) - ), - confirmText: this.supervisor.localize("common.update"), - dismissText: this.supervisor.localize("common.cancel"), - }); - - if (!confirmed) { - item.progress = false; - return; - } - try { - if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) { - await supervisorApiWsRequest(this.hass.connection, { - method: "post", - endpoint: item.apiPath.replace("hassio", ""), - timeout: null, - }); - } else { - await this.hass.callApi>("POST", item.apiPath); - } - fireEvent(this, "supervisor-collection-refresh", { - collection: item.key, - }); - } catch (err: any) { - // Only show an error if the status code was not expected (user behind proxy) - // or no status at all(connection terminated) - if (this.hass.connection.connected && !ignoreSupervisorError(err)) { - showAlertDialog(this, { - title: this.supervisor.localize("common.error.update_failed"), - text: extractApiErrorMessage(err), - }); - } - } - item.progress = false; - } - - private async _updateCore(): Promise { - await updateCore(this.hass); - fireEvent(this, "supervisor-collection-refresh", { - collection: "core", - }); - } - static get styles(): CSSResultGroup { return [ haStyle, diff --git a/hassio/src/dialogs/update/dialog-supervisor-update.ts b/hassio/src/dialogs/update/dialog-supervisor-update.ts deleted file mode 100644 index cc4f80bb09..0000000000 --- a/hassio/src/dialogs/update/dialog-supervisor-update.ts +++ /dev/null @@ -1,203 +0,0 @@ -import "@material/mwc-button/mwc-button"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, state } from "lit/decorators"; -import { fireEvent } from "../../../../src/common/dom/fire_event"; -import "../../../../src/components/ha-alert"; -import "../../../../src/components/ha-circular-progress"; -import "../../../../src/components/ha-dialog"; -import "../../../../src/components/ha-settings-row"; -import "../../../../src/components/ha-switch"; -import { - extractApiErrorMessage, - ignoreSupervisorError, -} from "../../../../src/data/hassio/common"; -import { createHassioPartialBackup } from "../../../../src/data/hassio/backup"; -import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; -import type { HomeAssistant } from "../../../../src/types"; -import { SupervisorDialogSupervisorUpdateParams } from "./show-dialog-update"; - -@customElement("dialog-supervisor-update") -class DialogSupervisorUpdate extends LitElement { - public hass!: HomeAssistant; - - @state() private _opened = false; - - @state() private _createBackup = true; - - @state() private _action: "backup" | "update" | null = null; - - @state() private _error?: string; - - @state() - private _dialogParams?: SupervisorDialogSupervisorUpdateParams; - - public async showDialog( - params: SupervisorDialogSupervisorUpdateParams - ): Promise { - this._opened = true; - this._dialogParams = params; - await this.updateComplete; - } - - public closeDialog(): void { - this._action = null; - this._createBackup = true; - this._error = undefined; - this._dialogParams = undefined; - fireEvent(this, "dialog-closed", { dialog: this.localName }); - } - - public focus(): void { - this.updateComplete.then(() => - ( - this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement - )?.focus() - ); - } - - protected render(): TemplateResult { - if (!this._dialogParams) { - return html``; - } - return html` - - ${this._action === null - ? html` -

- ${this._dialogParams.supervisor.localize( - "confirm.update.title", - "name", - this._dialogParams.name - )} -

-
-
- ${this._dialogParams.supervisor.localize( - "confirm.update.text", - "name", - this._dialogParams.name, - "version", - this._dialogParams.version - )} -
- - - - ${this._dialogParams.supervisor.localize( - "dialog.update.backup" - )} - - - ${this._dialogParams.supervisor.localize( - "dialog.update.create_backup", - "name", - this._dialogParams.name - )} - - - - - - ${this._dialogParams.supervisor.localize("common.cancel")} - - - ${this._dialogParams.supervisor.localize("common.update")} - ` - : html` - -

- ${this._action === "update" - ? this._dialogParams.supervisor.localize( - "dialog.update.updating", - "name", - this._dialogParams.name, - "version", - this._dialogParams.version - ) - : this._dialogParams.supervisor.localize( - "dialog.update.creating_backup", - "name", - this._dialogParams.name - )} -

`} - ${this._error - ? html`${this._error}` - : ""} -
- `; - } - - private _toggleBackup() { - this._createBackup = !this._createBackup; - } - - private async _update() { - if (this._createBackup) { - this._action = "backup"; - try { - await createHassioPartialBackup( - this.hass, - this._dialogParams!.backupParams - ); - } catch (err: any) { - this._error = extractApiErrorMessage(err); - this._action = null; - return; - } - } - - this._action = "update"; - try { - await this._dialogParams!.updateHandler!(); - } catch (err: any) { - if (this.hass.connection.connected && !ignoreSupervisorError(err)) { - this._error = extractApiErrorMessage(err); - this._action = null; - } - return; - } - - this.closeDialog(); - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - haStyleDialog, - css` - .form { - color: var(--primary-text-color); - } - - ha-settings-row { - margin-top: 32px; - padding: 0; - } - - ha-circular-progress { - display: block; - margin: 32px; - text-align: center; - } - - .progress-text { - text-align: center; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "dialog-supervisor-update": DialogSupervisorUpdate; - } -} diff --git a/hassio/src/dialogs/update/show-dialog-update.ts b/hassio/src/dialogs/update/show-dialog-update.ts deleted file mode 100644 index 6c3d15cc05..0000000000 --- a/hassio/src/dialogs/update/show-dialog-update.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { fireEvent } from "../../../../src/common/dom/fire_event"; -import { Supervisor } from "../../../../src/data/supervisor/supervisor"; - -export interface SupervisorDialogSupervisorUpdateParams { - supervisor: Supervisor; - name: string; - version: string; - backupParams: any; - updateHandler: () => Promise; -} - -export const showDialogSupervisorUpdate = ( - element: HTMLElement, - dialogParams: SupervisorDialogSupervisorUpdateParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-supervisor-update", - dialogImport: () => import("./dialog-supervisor-update"), - dialogParams, - }); -}; diff --git a/hassio/src/hassio-my-redirect.ts b/hassio/src/hassio-my-redirect.ts index e2bc3d85ac..777672870a 100644 --- a/hassio/src/hassio-my-redirect.ts +++ b/hassio/src/hassio-my-redirect.ts @@ -34,6 +34,9 @@ const REDIRECTS: Redirects = { supervisor_store: { redirect: "/hassio/store", }, + supervisor_addons: { + redirect: "/hassio/dashboard", + }, supervisor_addon: { redirect: "/hassio/addon", params: { diff --git a/hassio/src/hassio-router.ts b/hassio/src/hassio-router.ts index 3768d26e92..b40c075524 100644 --- a/hassio/src/hassio-router.ts +++ b/hassio/src/hassio-router.ts @@ -35,6 +35,10 @@ class HassioRouter extends HassRouterPage { backups: "dashboard", store: "dashboard", system: "dashboard", + "update-available": { + tag: "update-available-dashboard", + load: () => import("./update-available/update-available-dashboard"), + }, addon: { tag: "hassio-addon-dashboard", load: () => import("./addon-view/hassio-addon-dashboard"), diff --git a/hassio/src/hassio-tabs.ts b/hassio/src/hassio-tabs.ts index 6abec75296..c4c9ae0f0c 100644 --- a/hassio/src/hassio-tabs.ts +++ b/hassio/src/hassio-tabs.ts @@ -1,11 +1,22 @@ -import { mdiBackupRestore, mdiCogs, mdiViewDashboard } from "@mdi/js"; +import { + mdiBackupRestore, + mdiCogs, + mdiPuzzle, + mdiViewDashboard, +} from "@mdi/js"; +import { atLeastVersion } from "../../src/common/config/version"; import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage"; +import { HomeAssistant } from "../../src/types"; -export const supervisorTabs: PageNavigation[] = [ +export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] => [ { - translationKey: "panel.dashboard", + translationKey: atLeastVersion(hass.config.version, 2021, 12) + ? "panel.addons" + : "panel.dashboard", path: `/hassio/dashboard`, - iconPath: mdiViewDashboard, + iconPath: atLeastVersion(hass.config.version, 2021, 12) + ? mdiPuzzle + : mdiViewDashboard, }, { translationKey: "panel.backups", diff --git a/hassio/src/system/hassio-core-info.ts b/hassio/src/system/hassio-core-info.ts index 351b4b53e5..3912e2de3d 100644 --- a/hassio/src/system/hassio-core-info.ts +++ b/hassio/src/system/hassio-core-info.ts @@ -2,7 +2,7 @@ import "@material/mwc-button"; import "@material/mwc-list/mwc-list-item"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { fireEvent } from "../../../src/common/dom/fire_event"; +import { atLeastVersion } from "../../../src/common/config/version"; import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-card"; @@ -12,7 +12,7 @@ import { fetchHassioStats, HassioStats, } from "../../../src/data/hassio/common"; -import { restartCore, updateCore } from "../../../src/data/supervisor/core"; +import { restartCore } from "../../../src/data/supervisor/core"; import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { showAlertDialog, @@ -22,7 +22,6 @@ import { haStyle } from "../../../src/resources/styles"; import { HomeAssistant } from "../../../src/types"; import { bytesToString } from "../../../src/util/bytes-to-string"; import "../components/supervisor-metric"; -import { showDialogSupervisorUpdate } from "../dialogs/update/show-dialog-update"; import { hassioStyle } from "../resources/hassio-style"; @customElement("hassio-core-info") @@ -67,14 +66,15 @@ class HassioCoreInfo extends LitElement { core-${this.supervisor.core.version_latest} - ${this.supervisor.core.update_available + ${!atLeastVersion(this.hass.config.version, 2021, 12) && + this.supervisor.core.update_available ? html` - - ${this.supervisor.localize("common.update")} - + + + + ` : ""} @@ -160,27 +160,6 @@ class HassioCoreInfo extends LitElement { } } - private async _coreUpdate(): Promise { - showDialogSupervisorUpdate(this, { - supervisor: this.supervisor, - name: "Home Assistant Core", - version: this.supervisor.core.version_latest, - backupParams: { - name: `core_${this.supervisor.core.version}`, - folders: ["homeassistant"], - homeassistant: true, - }, - updateHandler: async () => this._updateCore(), - }); - } - - private async _updateCore(): Promise { - await updateCore(this.hass); - fireEvent(this, "supervisor-collection-refresh", { - collection: "core", - }); - } - static get styles(): CSSResultGroup { return [ haStyle, @@ -239,6 +218,9 @@ class HassioCoreInfo extends LitElement { mwc-list-item ha-svg-icon { color: var(--secondary-text-color); } + a { + text-decoration: none; + } `, ]; } diff --git a/hassio/src/system/hassio-host-info.ts b/hassio/src/system/hassio-host-info.ts index 64b7f90e8f..79c63c370b 100644 --- a/hassio/src/system/hassio-host-info.ts +++ b/hassio/src/system/hassio-host-info.ts @@ -21,7 +21,6 @@ import { configSyncOS, rebootHost, shutdownHost, - updateOS, } from "../../../src/data/hassio/host"; import { fetchNetworkInfo, @@ -106,11 +105,15 @@ class HassioHostInfo extends LitElement { ${this.supervisor.host.operating_system} - ${this.supervisor.os.update_available + ${!atLeastVersion(this.hass.config.version, 2021, 12) && + this.supervisor.os.update_available ? html` - - ${this.supervisor.localize("commmon.update")} - + + + + ` : ""} @@ -333,50 +336,6 @@ class HassioHostInfo extends LitElement { button.progress = false; } - private async _osUpdate(ev: CustomEvent): Promise { - const button = ev.currentTarget as any; - button.progress = true; - - const confirmed = await showConfirmationDialog(this, { - title: this.supervisor.localize( - "confirm.update.title", - "name", - "Home Assistant Operating System" - ), - text: this.supervisor.localize( - "confirm.update.text", - "name", - "Home Assistant Operating System", - "version", - this.supervisor.os.version_latest - ), - confirmText: this.supervisor.localize("common.update"), - dismissText: "no", - }); - - if (!confirmed) { - button.progress = false; - return; - } - - try { - await updateOS(this.hass); - fireEvent(this, "supervisor-collection-refresh", { collection: "os" }); - } catch (err: any) { - if (this.hass.connection.connected) { - showAlertDialog(this, { - title: this.supervisor.localize( - "common.failed_to_update_name", - "name", - "Home Assistant Operating System" - ), - text: extractApiErrorMessage(err), - }); - } - } - button.progress = false; - } - private async _changeNetworkClicked(): Promise { showNetworkDialog(this, { supervisor: this.supervisor, @@ -494,6 +453,9 @@ class HassioHostInfo extends LitElement { mwc-list-item ha-svg-icon { color: var(--secondary-text-color); } + a { + text-decoration: none; + } `, ]; } diff --git a/hassio/src/system/hassio-supervisor-info.ts b/hassio/src/system/hassio-supervisor-info.ts index 2a2ef93de9..887ddd105d 100644 --- a/hassio/src/system/hassio-supervisor-info.ts +++ b/hassio/src/system/hassio-supervisor-info.ts @@ -17,7 +17,6 @@ import { restartSupervisor, setSupervisorOption, SupervisorOptions, - updateSupervisor, } from "../../../src/data/hassio/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { @@ -77,16 +76,15 @@ class HassioSupervisorInfo extends LitElement { supervisor-${this.supervisor.supervisor.version_latest} - ${this.supervisor.supervisor.update_available + ${!atLeastVersion(this.hass.config.version, 2021, 12) && + this.supervisor.supervisor.update_available ? html` - - ${this.supervisor.localize("common.update")} - + + + + ` : ""} @@ -337,51 +335,6 @@ class HassioSupervisorInfo extends LitElement { } } - private async _supervisorUpdate(ev: CustomEvent): Promise { - const button = ev.currentTarget as any; - button.progress = true; - - const confirmed = await showConfirmationDialog(this, { - title: this.supervisor.localize( - "confirm.update.title", - "name", - "Supervisor" - ), - text: this.supervisor.localize( - "confirm.update.text", - "name", - "Supervisor", - "version", - this.supervisor.supervisor.version_latest - ), - confirmText: this.supervisor.localize("common.update"), - dismissText: this.supervisor.localize("common.cancel"), - }); - - if (!confirmed) { - button.progress = false; - return; - } - - try { - await updateSupervisor(this.hass); - fireEvent(this, "supervisor-collection-refresh", { - collection: "supervisor", - }); - } catch (err: any) { - showAlertDialog(this, { - title: this.supervisor.localize( - "common.failed_to_update_name", - "name", - "Supervisor" - ), - text: extractApiErrorMessage(err), - }); - } finally { - button.progress = false; - } - } - private async _diagnosticsInformationDialog(): Promise { await showAlertDialog(this, { title: this.supervisor.localize( @@ -513,6 +466,9 @@ class HassioSupervisorInfo extends LitElement { white-space: normal; color: var(--secondary-text-color); } + a { + text-decoration: none; + } `, ]; } diff --git a/hassio/src/system/hassio-system.ts b/hassio/src/system/hassio-system.ts index 22704fcc4a..60b9b4e218 100644 --- a/hassio/src/system/hassio-system.ts +++ b/hassio/src/system/hassio-system.ts @@ -28,7 +28,7 @@ class HassioSystem extends LitElement { .localizeFunc=${this.supervisor.localize} .narrow=${this.narrow} .route=${this.route} - .tabs=${supervisorTabs} + .tabs=${supervisorTabs(this.hass)} main-page supervisor > diff --git a/hassio/src/update-available/update-available-dashboard.ts b/hassio/src/update-available/update-available-dashboard.ts new file mode 100644 index 0000000000..c03c644eb4 --- /dev/null +++ b/hassio/src/update-available/update-available-dashboard.ts @@ -0,0 +1,391 @@ +import "@material/mwc-list/mwc-list-item"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import "../../../src/common/search/search-input"; +import "../../../src/components/buttons/ha-progress-button"; +import "../../../src/components/ha-alert"; +import "../../../src/components/ha-button-menu"; +import "../../../src/components/ha-card"; +import "../../../src/components/ha-checkbox"; +import "../../../src/components/ha-expansion-panel"; +import "../../../src/components/ha-icon-button"; +import "../../../src/components/ha-markdown"; +import "../../../src/components/ha-settings-row"; +import "../../../src/components/ha-svg-icon"; +import "../../../src/components/ha-switch"; +import { + fetchHassioAddonChangelog, + fetchHassioAddonInfo, + HassioAddonDetails, + updateHassioAddon, +} from "../../../src/data/hassio/addon"; +import { + createHassioPartialBackup, + HassioPartialBackupCreateParams, +} from "../../../src/data/hassio/backup"; +import { + extractApiErrorMessage, + ignoreSupervisorError, +} from "../../../src/data/hassio/common"; +import { updateOS } from "../../../src/data/hassio/host"; +import { updateSupervisor } from "../../../src/data/hassio/supervisor"; +import { updateCore } from "../../../src/data/supervisor/core"; +import { StoreAddon } from "../../../src/data/supervisor/store"; +import { Supervisor } from "../../../src/data/supervisor/supervisor"; +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 { documentationUrl } from "../../../src/util/documentation-url"; +import { addonArchIsSupported, extractChangelog } from "../util/addon"; + +const changelogUrl = ( + hass: HomeAssistant, + entry: string, + version: string +): string | undefined => { + if (entry === "core") { + return version?.includes("dev") + ? "https://github.com/home-assistant/core/commits/dev" + : documentationUrl(hass, "/latest-release-notes/"); + } + if (entry === "os") { + return version?.includes("dev") + ? "https://github.com/home-assistant/operating-system/commits/dev" + : `https://github.com/home-assistant/operating-system/releases/tag/${version}`; + } + if (entry === "supervisor") { + return version?.includes("dev") + ? "https://github.com/home-assistant/supervisor/commits/main" + : `https://github.com/home-assistant/supervisor/releases/tag/${version}`; + } + return undefined; +}; + +class UpdateAvailableDashboard extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public supervisor!: Supervisor; + + @property({ type: Boolean }) public narrow!: boolean; + + @property({ attribute: false }) public route!: Route; + + @state() private _updateEntry?: string; + + @state() private _changelogContent?: string; + + @state() private _addonInfo?: HassioAddonDetails; + + @state() private _createBackup = true; + + @state() private _action: "backup" | "update" | null = null; + + @state() private _error?: string; + + private _isAddon = false; + + private _addonStoreInfo = memoizeOne( + (slug: string, storeAddons: StoreAddon[]) => + storeAddons.find((addon) => addon.slug === slug) + ); + + protected render(): TemplateResult { + if (!this._updateEntry) { + return html``; + } + const name = + // @ts-ignore + this._addonInfo?.name || SUPERVISOR_UPDATE_NAMES[this._updateEntry]; + const changelog = !this._isAddon + ? changelogUrl( + this.hass, + this._updateEntry, + this.supervisor[this._updateEntry]?.version + ) + : undefined; + return html` + + +
+ ${this._error + ? html`${this._error}` + : ""} + ${this._action === null + ? html` + ${this._changelogContent + ? html` + + + + + ` + : ""} +
+

+ ${this.supervisor.localize( + "update_available.description", + { + name, + version: + this._addonInfo?.version || + this.supervisor[this._updateEntry]?.version, + newest_version: + this._addonInfo?.version_latest || + this.supervisor[this._updateEntry]?.version_latest, + } + )} +

+ ${this._updateEntry === "core" + ? html` + + ${this.supervisor.localize( + "update_available.core_note", + { + version: + this._addonInfo?.version || + this.supervisor[this._updateEntry]?.version, + } + )} + + ` + : ""} +
+ ${!["os", "supervisor"].includes(this._updateEntry) + ? html` + + + + + ${this.supervisor.localize( + "update_available.create_backup" + )} + + + ` + : ""} + ` + : html` + +

+ ${this._action === "update" + ? this.supervisor.localize("update_available.updating", { + name, + version: + this._addonInfo?.version_latest || + this.supervisor[this._updateEntry]?.version_latest, + }) + : this.supervisor.localize( + "update_available.creating_backup", + { name } + )} +

`} +
+ ${this._action === null + ? html` +
+ ${changelog + ? html` + + + ` + : ""} + + + ${this.supervisor.localize("common.update")} + +
+ ` + : ""} +
+
+ `; + } + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + this._updateEntry = this.route.path.substring(1, this.route.path.length); + this._isAddon = !["core", "os", "supervisor"].includes(this._updateEntry); + if (this._isAddon) { + this._loadAddonData(); + } + } + + private async _loadAddonData() { + try { + this._addonInfo = await fetchHassioAddonInfo( + this.hass, + this._updateEntry! + ); + } catch (err) { + showAlertDialog(this, { + title: this._updateEntry, + text: extractApiErrorMessage(err), + confirm: () => history.back(), + }); + return; + } + const addonStoreInfo = + !this._addonInfo.detached && !this._addonInfo.available + ? this._addonStoreInfo( + this._addonInfo.slug, + this.supervisor.store.addons + ) + : undefined; + + if (this._addonInfo.changelog) { + try { + const content = await fetchHassioAddonChangelog( + this.hass, + this._updateEntry! + ); + this._changelogContent = extractChangelog(this._addonInfo, content); + } catch (err) { + this._error = extractApiErrorMessage(err); + return; + } + } + + if (!this._addonInfo.available && addonStoreInfo) { + if ( + !addonArchIsSupported( + this.supervisor.info.supported_arch, + this._addonInfo.arch + ) + ) { + this._error = this.supervisor.localize( + "addon.dashboard.not_available_arch" + ); + } else { + this._error = this.supervisor.localize( + "addon.dashboard.not_available_arch", + { + core_version_installed: this.supervisor.core.version, + core_version_needed: addonStoreInfo.homeassistant, + } + ); + } + } + } + + private _toggleBackup() { + this._createBackup = !this._createBackup; + } + + private async _update() { + if (this._createBackup) { + let backupArgs: HassioPartialBackupCreateParams; + if (this._isAddon) { + backupArgs = { + name: `addon_${this._updateEntry}_${this._addonInfo?.version}`, + addons: [this._updateEntry!], + homeassistant: false, + }; + } else { + backupArgs = { + name: `${this._updateEntry}_${this._addonInfo?.version}`, + folders: ["homeassistant"], + homeassistant: true, + }; + } + this._action = "backup"; + try { + await createHassioPartialBackup(this.hass, backupArgs); + } catch (err: any) { + this._error = extractApiErrorMessage(err); + this._action = null; + return; + } + } + + this._action = "update"; + try { + if (this._isAddon) { + await updateHassioAddon(this.hass, this._updateEntry!); + } else if (this._updateEntry === "core") { + await updateCore(this.hass); + } else if (this._updateEntry === "os") { + await updateOS(this.hass); + } else if (this._updateEntry === "supervisor") { + await updateSupervisor(this.hass); + } + } catch (err: any) { + if (this.hass.connection.connected && !ignoreSupervisorError(err)) { + this._error = extractApiErrorMessage(err); + this._action = null; + return; + } + } + history.back(); + } + + static get styles(): CSSResultGroup { + return css` + hass-subpage { + --app-header-background-color: background-color: var(--primary-background-color); + } + ha-card { + margin: auto; + margin-top: 16px; + max-width: 600px; + } + a { + text-decoration: none; + color: var(--primary-text-color); + } + ha-settings-row { + padding: 0; + } + .card-actions { + display: flex; + justify-content: space-between; + } + + ha-circular-progress { + display: block; + margin: 32px; + text-align: center; + } + + .progress-text { + text-align: center; + } + `; + } +} + +customElements.define("update-available-dashboard", UpdateAvailableDashboard); diff --git a/hassio/src/util/addon.ts b/hassio/src/util/addon.ts index 49fed477cc..ec15a50c49 100644 --- a/hassio/src/util/addon.ts +++ b/hassio/src/util/addon.ts @@ -1,7 +1,30 @@ import memoizeOne from "memoize-one"; +import { HassioAddonDetails } from "../../../src/data/hassio/addon"; import { SupervisorArch } from "../../../src/data/supervisor/supervisor"; export const addonArchIsSupported = memoizeOne( (supported_archs: SupervisorArch[], addon_archs: SupervisorArch[]) => addon_archs.some((arch) => supported_archs.includes(arch)) ); + +export const extractChangelog = ( + addon: HassioAddonDetails, + content: string +): string => { + if (content.startsWith("# Changelog")) { + content = content.substr(12, content.length); + } + if ( + content.includes(`# ${addon.version}`) && + content.includes(`# ${addon.version_latest}`) + ) { + const newcontent = content.split(`# ${addon.version}`)[0]; + if (newcontent.includes(`# ${addon.version_latest}`)) { + // Only change the content if the new version still exist + // if the changelog does not have the newests version on top + // this will not be true, and we don't modify the content + content = newcontent; + } + } + return content; +}; diff --git a/src/components/ha-alert.ts b/src/components/ha-alert.ts index 25c3975ac6..236ae8c7c5 100644 --- a/src/components/ha-alert.ts +++ b/src/components/ha-alert.ts @@ -51,27 +51,31 @@ class HaAlert extends LitElement { [this.alertType]: true, })}" > -
- -
+ +
+ +
+
${this.title ? html`
${this.title}
` : ""}
- ${this.actionText - ? html`` - : this.dismissable - ? html`` - : ""} + + ${this.actionText + ? html`` + : this.dismissable + ? html`` + : ""} +
@@ -107,14 +111,14 @@ class HaAlert extends LitElement { content: ""; border-radius: 4px; } - .icon { + slot > .icon { margin-right: 8px; width: 24px; } .icon.no-title { align-self: center; } - .issue-type.rtl > .icon { + .issue-type.rtl > slot > .icon { margin-right: 0px; margin-left: 8px; width: 24px; @@ -142,28 +146,28 @@ class HaAlert extends LitElement { ha-icon-button { --mdc-icon-button-size: 36px; } - .issue-type.info > .icon { + .issue-type.info > slot > .icon { color: var(--info-color); } .issue-type.info::before { background-color: var(--info-color); } - .issue-type.warning > .icon { + .issue-type.warning > slot > .icon { color: var(--warning-color); } .issue-type.warning::before { background-color: var(--warning-color); } - .issue-type.error > .icon { + .issue-type.error > slot > .icon { color: var(--error-color); } .issue-type.error::before { background-color: var(--error-color); } - .issue-type.success > .icon { + .issue-type.success > slot > .icon { color: var(--success-color); } .issue-type.success::before { diff --git a/src/components/ha-logo-svg.ts b/src/components/ha-logo-svg.ts new file mode 100644 index 0000000000..e8a78196aa --- /dev/null +++ b/src/components/ha-logo-svg.ts @@ -0,0 +1,51 @@ +import { css, CSSResultGroup, LitElement, svg, SVGTemplateResult } from "lit"; +import { customElement } from "lit/decorators"; + +@customElement("ha-logo-svg") +export class HaLogoSvg extends LitElement { + protected render(): SVGTemplateResult { + return svg` + + + + + + + + + + + + + + + + `; + } + + static get styles(): CSSResultGroup { + return css` + :host { + display: var(--ha-icon-display, inline-flex); + align-items: center; + justify-content: center; + position: relative; + vertical-align: middle; + fill: currentcolor; + width: var(--mdc-icon-size, 24px); + height: var(--mdc-icon-size, 24px); + } + svg { + width: 100%; + height: 100%; + pointer-events: none; + display: block; + } + `; + } +} +declare global { + interface HTMLElementTagNameMap { + "ha-logo-svg": HaLogoSvg; + } +} diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 502c0e7fe7..6fe026809c 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -51,7 +51,7 @@ import { } from "../external_app/external_config"; import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; import { haStyleScrollbar } from "../resources/styles"; -import type { HomeAssistant, PanelInfo } from "../types"; +import type { HomeAssistant, PanelInfo, Route } from "../types"; import "./ha-icon"; import "./ha-icon-button"; import "./ha-menu-button"; @@ -189,6 +189,8 @@ class HaSidebar extends LitElement { @property({ type: Boolean, reflect: true }) public narrow!: boolean; + @property() public route!: Route; + @property({ type: Boolean }) public alwaysExpand = false; @property({ type: Boolean }) public editMode = false; @@ -351,12 +353,19 @@ class HaSidebar extends LitElement { this._hiddenPanels ); + // Show the update-available as beeing part of configuration + const selectedPanel = this.route.path?.startsWith( + "/hassio/update-available" + ) + ? "config" + : this.hass.panelUrl; + // prettier-ignore return html` ( conn: Connection, request: supervisorApiRequest @@ -139,3 +175,14 @@ export const subscribeSupervisorEvents = ( getSupervisorEventCollection(hass.connection, key, endpoint).subscribe( onChange ); + +export const fetchSupervisorAvailableUpdates = async ( + hass: HomeAssistant +): Promise => + ( + await hass.callWS({ + type: "supervisor/api", + endpoint: "/supervisor/available_updates", + method: "get", + }) + ).available_updates; diff --git a/src/layouts/home-assistant-main.ts b/src/layouts/home-assistant-main.ts index 42a43d4acd..39339c4486 100644 --- a/src/layouts/home-assistant-main.ts +++ b/src/layouts/home-assistant-main.ts @@ -88,6 +88,7 @@ class HomeAssistantMain extends LitElement {
${this.hass.localize("ui.panel.config.header")}
-
+
${this.hass.localize("ui.panel.config.introduction")}
+ ${isComponentLoaded(this.hass, "hassio") + ? html`` + : ""} ${this.cloudStatus && isComponentLoaded(this.hass, "cloud") ? html` @@ -134,6 +141,9 @@ class HaConfigDashboard extends LitElement { .promo-advanced a { color: var(--secondary-text-color); } + .intro { + margin-bottom: 24px; + } `, ]; } diff --git a/src/panels/config/dashboard/ha-config-updates.ts b/src/panels/config/dashboard/ha-config-updates.ts new file mode 100644 index 0000000000..29d89f0be7 --- /dev/null +++ b/src/panels/config/dashboard/ha-config-updates.ts @@ -0,0 +1,120 @@ +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, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import "../../../components/ha-alert"; +import "../../../components/ha-logo-svg"; +import "../../../components/ha-svg-icon"; +import { extractApiErrorMessage } from "../../../data/hassio/common"; +import { + fetchSupervisorAvailableUpdates, + SupervisorAvailableUpdates, +} from "../../../data/supervisor/supervisor"; +import { HomeAssistant } from "../../../types"; + +export const SUPERVISOR_UPDATE_NAMES = { + core: "Home Assistant Core", + os: "Home Assistant Operating System", + supervisor: "Home Assistant Supervisor", +}; + +@customElement("ha-config-updates") +class HaConfigUpdates extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _supervisorUpdates?: SupervisorAvailableUpdates[]; + + @state() private _error?: string; + + protected firstUpdated(changedProps: PropertyValues): void { + super.firstUpdated(changedProps); + this._loadSupervisorUpdates(); + } + + protected render(): TemplateResult { + return html` + ${this._error + ? html` + ${this._error} + ` + : ""} + ${this._supervisorUpdates?.map( + (update) => html` + + + ${update.update_type === "addon" + ? update.icon + ? html`` + : html`` + : html``} + + ${this.hass.localize("ui.panel.config.updates.version_available", { + version_available: update.version_latest, + })} + + + + + + ` + )} + `; + } + + private async _loadSupervisorUpdates(): Promise { + try { + this._supervisorUpdates = await fetchSupervisorAvailableUpdates( + this.hass + ); + } catch (err) { + this._error = extractApiErrorMessage(err); + } + } + + static get styles(): CSSResultGroup { + return css` + a { + text-decoration: none; + color: var(--primary-text-color); + } + .icon { + place-self: center; + } + img, + ha-svg-icon, + ha-logo-svg { + width: var(--mdc-icon-size, 32px); + height: var(--mdc-icon-size, 32px); + padding-right: 12px; + display: block; + color: var(--secondary-text-color); + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-config-updates": HaConfigUpdates; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index 178930041a..90cadb1735 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -926,6 +926,11 @@ }, "learn_more": "Learn more" }, + "updates": { + "unable_to_fetch": "Unable to fetch available updates", + "version_available": "Version {version_available} is available", + "review": "review" + }, "areas": { "caption": "Areas", "description": "Group devices and entities into areas", @@ -4178,6 +4183,7 @@ "save": "[%key:ui::common::save%]", "close": "[%key:ui::common::close%]", "menu": "[%key:ui::common::menu%]", + "review": "[%key:ui::panel::config::updates::review%]", "show_more": "Show more information about this", "update_available": "{count, plural,\n one {Update}\n other {{count} updates}\n} pending", "update": "Update", @@ -4187,11 +4193,16 @@ "update_failed": "Update failed" } }, + "update_available": { + "update_name": "Update {name}", + "open_release_notes": "Open release notes", + "create_backup": "Create backup before updating", + "description": "There is an update available for the {name}. You have {version} installed. Click update to update to version {newest_version}", + "core_note": "The supervisor will roll back to version {version} if your instance does not come up after the update.", + "updating": "Updating {name} to version {version}", + "creating_backup": "Creating backup of {name}" + }, "confirm": { - "update": { - "title": "Update {name}", - "text": "Are you sure you want to update {name} to version {version}?" - }, "restart": { "title": "[%key:supervisor::common::restart_name%]", "text": "Are you sure you want to restart {name}?" @@ -4215,6 +4226,7 @@ "repositories": "Repositories" }, "panel": { + "addons": "Add-ons", "dashboard": "Dashboard", "backups": "Backups", "store": "Add-on Store", @@ -4382,12 +4394,6 @@ "confirm_text": "Restart add-on", "text": "Do you want to restart the add-on with your changes?" }, - "update": { - "backup": "Backup", - "create_backup": "Create a backup of {name} before updating", - "updating": "Updating {name} to version {version}", - "creating_backup": "Creating backup of {name}" - }, "hardware": { "title": "Hardware", "search": "Search hardware", From 822590ec8af78306eab01c43b8dc4012d6ae0474 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Wed, 17 Nov 2021 19:22:34 +0100 Subject: [PATCH 014/112] Add correct button label to "no_state" statistics fix dialog (#10628) --- .../developer-tools/statistics/developer-tools-statistics.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/panels/developer-tools/statistics/developer-tools-statistics.ts b/src/panels/developer-tools/statistics/developer-tools-statistics.ts index 9c056b0fa5..a7573d7201 100644 --- a/src/panels/developer-tools/statistics/developer-tools-statistics.ts +++ b/src/panels/developer-tools/statistics/developer-tools-statistics.ts @@ -176,6 +176,7 @@ class HaPanelDevStatistics extends LitElement { it from your database.

Do you want to permanently remove the long term statistics of ${issue.data.statistic_id} from your database?`, + confirmText: this.hass.localize("ui.common.remove"), confirm: async () => { await clearStatistics(this.hass, [issue.data.statistic_id]); this._validateStatistics(); From 582fab7ea1a586573d7b51540294abfb5f7dcdd2 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 17 Nov 2021 19:32:15 +0100 Subject: [PATCH 015/112] Update Lovelace Cast app ID (#10592) --- src/cast/const.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cast/const.ts b/src/cast/const.ts index fe578e8ad1..3d98f7de17 100644 --- a/src/cast/const.ts +++ b/src/cast/const.ts @@ -3,5 +3,5 @@ import { CAST_DEV_APP_ID } from "./dev_const"; // Guard dev mode with `__dev__` so it can only ever be enabled in dev mode. export const CAST_DEV = __DEV__ && true; -export const CAST_APP_ID = CAST_DEV ? CAST_DEV_APP_ID : "B12CE3CA"; +export const CAST_APP_ID = CAST_DEV ? CAST_DEV_APP_ID : "A078F6B0"; export const CAST_NS = "urn:x-cast:com.nabucasa.hast"; From 7d94615f47d9d8ecffe5a72ee61a1768036caeaf Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 17 Nov 2021 19:33:15 +0100 Subject: [PATCH 016/112] Cast fixes (#10598) --- cast/src/receiver/layout/hc-lovelace.ts | 4 +-- cast/src/receiver/layout/hc-main.ts | 31 ++++++++++++------- src/components/map/ha-entity-marker.ts | 4 ++- .../lovelace/cards/hui-media-control-card.ts | 4 ++- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/cast/src/receiver/layout/hc-lovelace.ts b/cast/src/receiver/layout/hc-lovelace.ts index 89ec9ef2e4..122ce510fd 100644 --- a/cast/src/receiver/layout/hc-lovelace.ts +++ b/cast/src/receiver/layout/hc-lovelace.ts @@ -15,7 +15,7 @@ class HcLovelace extends LitElement { @property() public viewPath?: string | number; - public urlPath?: string | null; + @property() public urlPath: string | null = null; protected render(): TemplateResult { const index = this._viewIndex; @@ -31,7 +31,7 @@ class HcLovelace extends LitElement { config: this.lovelaceConfig, rawConfig: this.lovelaceConfig, editMode: false, - urlPath: this.urlPath!, + urlPath: this.urlPath, enableFullEditMode: () => undefined, mode: "storage", locale: this.hass.locale, diff --git a/cast/src/receiver/layout/hc-main.ts b/cast/src/receiver/layout/hc-main.ts index 207d46bb4a..f74d9089cb 100644 --- a/cast/src/receiver/layout/hc-main.ts +++ b/cast/src/receiver/layout/hc-main.ts @@ -40,9 +40,9 @@ export class HcMain extends HassElement { @state() private _error?: string; - private _unsubLovelace?: UnsubscribeFunc; + @state() private _urlPath?: string | null; - private _urlPath?: string | null; + private _unsubLovelace?: UnsubscribeFunc; public processIncomingMessage(msg: HassMessage) { if (msg.type === "connect") { @@ -68,8 +68,10 @@ export class HcMain extends HassElement { !this._lovelaceConfig || this._lovelacePath === null || // Guard against part of HA not being loaded yet. - (this.hass && - (!this.hass.states || !this.hass.config || !this.hass.services)) + !this.hass || + !this.hass.states || + !this.hass.config || + !this.hass.services ) { return html` undefined; await this._generateLovelaceConfig(); @@ -215,8 +224,6 @@ export class HcMain extends HassElement { loadLovelaceResources(resources, this.hass!.auth.data.hassUrl); } } - this._showDemo = false; - this._lovelacePath = msg.viewPath; this._sendStatus(); } @@ -237,7 +244,7 @@ export class HcMain extends HassElement { } private _handleNewLovelaceConfig(lovelaceConfig: LovelaceConfig) { - castContext.setApplicationState(lovelaceConfig.title!); + castContext.setApplicationState(lovelaceConfig.title || ""); this._lovelaceConfig = lovelaceConfig; } diff --git a/src/components/map/ha-entity-marker.ts b/src/components/map/ha-entity-marker.ts index 946c9597c7..b917c29259 100644 --- a/src/components/map/ha-entity-marker.ts +++ b/src/components/map/ha-entity-marker.ts @@ -26,7 +26,9 @@ class HaEntityMarker extends LitElement { ? html`
` : this.entityName} diff --git a/src/panels/lovelace/cards/hui-media-control-card.ts b/src/panels/lovelace/cards/hui-media-control-card.ts index 28f279019d..f579172e9a 100644 --- a/src/panels/lovelace/cards/hui-media-control-card.ts +++ b/src/panels/lovelace/cards/hui-media-control-card.ts @@ -544,7 +544,9 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { } try { - const { foreground, background } = await extractColors(this._image); + const { foreground, background } = await extractColors( + this.hass.hassUrl(this._image) + ); this._backgroundColor = background.hex; this._foregroundColor = foreground.hex; } catch (err: any) { From 1e851e0e8c10bc94a3174a638e31bef1a7989c72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 17 Nov 2021 19:34:20 +0100 Subject: [PATCH 017/112] Remove customize UI (#10632) --- .../config/customize/ha-config-customize.ts | 88 ----- .../customize/ha-customize-attribute.js | 84 ---- .../customize/ha-form-customize-attributes.js | 34 -- .../config/customize/ha-form-customize.js | 362 ------------------ .../customize/types/ha-customize-array.js | 63 --- .../customize/types/ha-customize-boolean.js | 34 -- .../customize/types/ha-customize-icon.js | 40 -- .../customize/types/ha-customize-key-value.js | 45 --- .../customize/types/ha-customize-string.js | 35 -- .../config/entities/dialog-entity-editor.ts | 15 - src/panels/config/ha-entity-config.ts | 122 ------ src/panels/config/ha-panel-config.ts | 17 +- src/panels/my/ha-panel-my.ts | 3 +- src/translations/en.json | 24 -- 14 files changed, 4 insertions(+), 962 deletions(-) delete mode 100644 src/panels/config/customize/ha-config-customize.ts delete mode 100644 src/panels/config/customize/ha-customize-attribute.js delete mode 100644 src/panels/config/customize/ha-form-customize-attributes.js delete mode 100644 src/panels/config/customize/ha-form-customize.js delete mode 100644 src/panels/config/customize/types/ha-customize-array.js delete mode 100644 src/panels/config/customize/types/ha-customize-boolean.js delete mode 100644 src/panels/config/customize/types/ha-customize-icon.js delete mode 100644 src/panels/config/customize/types/ha-customize-key-value.js delete mode 100644 src/panels/config/customize/types/ha-customize-string.js delete mode 100644 src/panels/config/ha-entity-config.ts diff --git a/src/panels/config/customize/ha-config-customize.ts b/src/panels/config/customize/ha-config-customize.ts deleted file mode 100644 index 03fd6f17e9..0000000000 --- a/src/panels/config/customize/ha-config-customize.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { property } from "lit/decorators"; -import "../../../components/ha-card"; -import "../../../layouts/hass-loading-screen"; -import "../../../layouts/hass-tabs-subpage"; -import { haStyle } from "../../../resources/styles"; -import { HomeAssistant, Route } from "../../../types"; -import { documentationUrl } from "../../../util/documentation-url"; -import "../ha-config-section"; -import "../ha-entity-config"; -import { configSections } from "../ha-panel-config"; -import "./ha-form-customize"; - -class HaConfigCustomize extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property() public isWide?: boolean; - - @property() public narrow?: boolean; - - @property() public route!: Route; - - @property() private _selectedEntityId = ""; - - protected render(): TemplateResult { - return html` - - - - ${this.hass.localize("ui.panel.config.customize.picker.header")} - - - ${this.hass.localize( - "ui.panel.config.customize.picker.introduction" - )} -
- - ${this.hass.localize( - "ui.panel.config.customize.picker.documentation" - )} - -
- - -
-
- - `; - } - - protected firstUpdated(changedProps) { - super.firstUpdated(changedProps); - - if (!this.route.path.includes("/edit/")) { - return; - } - const routeSegments = this.route.path.split("/edit/"); - this._selectedEntityId = routeSegments.length > 1 ? routeSegments[1] : ""; - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - a { - color: var(--primary-color); - } - `, - ]; - } -} -customElements.define("ha-config-customize", HaConfigCustomize); diff --git a/src/panels/config/customize/ha-customize-attribute.js b/src/panels/config/customize/ha-customize-attribute.js deleted file mode 100644 index 6d53b06686..0000000000 --- a/src/panels/config/customize/ha-customize-attribute.js +++ /dev/null @@ -1,84 +0,0 @@ -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "../../../components/ha-icon"; -import "../../../components/ha-icon-button"; -import hassAttributeUtil from "../../../util/hass-attributes-util"; -import "../ha-form-style"; -import "./types/ha-customize-array"; -import "./types/ha-customize-boolean"; -import "./types/ha-customize-icon"; -import "./types/ha-customize-key-value"; -import "./types/ha-customize-string"; - -class HaCustomizeAttribute extends PolymerElement { - static get template() { - return html` - -
- - - - `; - } - - static get properties() { - return { - item: { - type: Object, - notify: true, - observer: "itemObserver", - }, - }; - } - - tapButton() { - if (this.item.secondary) { - this.item = { ...this.item, secondary: false }; - } else { - this.item = { ...this.item, closed: true }; - } - } - - getIcon(secondary) { - return secondary ? "hass:pencil" : "hass:close"; - } - - itemObserver(item) { - const wrapper = this.$.wrapper; - const tag = hassAttributeUtil.TYPE_TO_TAG[item.type].toUpperCase(); - let child; - if (wrapper.lastChild && wrapper.lastChild.tagName === tag) { - child = wrapper.lastChild; - } else { - if (wrapper.lastChild) { - wrapper.removeChild(wrapper.lastChild); - } - // Creating an element with upper case works fine in Chrome, but in FF it doesn't immediately - // become a defined Custom Element. Polymer does that in some later pass. - this.$.child = child = document.createElement(tag.toLowerCase()); - child.className = "form-control"; - child.addEventListener("item-changed", () => { - this.item = { ...child.item }; - }); - } - child.setProperties({ item: this.item }); - if (child.parentNode === null) { - wrapper.appendChild(child); - } - } -} -customElements.define("ha-customize-attribute", HaCustomizeAttribute); diff --git a/src/panels/config/customize/ha-form-customize-attributes.js b/src/panels/config/customize/ha-form-customize-attributes.js deleted file mode 100644 index 2c0def4524..0000000000 --- a/src/panels/config/customize/ha-form-customize-attributes.js +++ /dev/null @@ -1,34 +0,0 @@ -import { MutableData } from "@polymer/polymer/lib/mixins/mutable-data"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "./ha-customize-attribute"; - -class HaFormCustomizeAttributes extends MutableData(PolymerElement) { - static get template() { - return html` - - - `; - } - - static get properties() { - return { - attributes: { - type: Array, - notify: true, - }, - }; - } -} -customElements.define( - "ha-form-customize-attributes", - HaFormCustomizeAttributes -); diff --git a/src/panels/config/customize/ha-form-customize.js b/src/panels/config/customize/ha-form-customize.js deleted file mode 100644 index cd3278abeb..0000000000 --- a/src/panels/config/customize/ha-form-customize.js +++ /dev/null @@ -1,362 +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 { computeStateDomain } from "../../../common/entity/compute_state_domain"; -import LocalizeMixin from "../../../mixins/localize-mixin"; -import "../../../styles/polymer-ha-style"; -import { documentationUrl } from "../../../util/documentation-url"; -import hassAttributeUtil from "../../../util/hass-attributes-util"; -import "../ha-form-style"; -import "./ha-form-customize-attributes"; - -export class HaFormCustomize extends LocalizeMixin(PolymerElement) { - static get template() { - return html` - - - - - - - - `; - } - - static get properties() { - return { - hass: { - type: Object, - }, - - entity: Object, - - localAttributes: { - type: Array, - computed: "computeLocalAttributes(localConfig)", - }, - hasLocalAttributes: Boolean, - - globalAttributes: { - type: Array, - computed: "computeGlobalAttributes(localConfig, globalConfig)", - }, - hasGlobalAttributes: Boolean, - - existingAttributes: { - type: Array, - computed: - "computeExistingAttributes(localConfig, globalConfig, entity)", - }, - hasExistingAttributes: Boolean, - - newAttributes: { - type: Array, - value: [], - }, - hasNewAttributes: Boolean, - - newAttributesOptions: Array, - selectedNewAttribute: { - type: Number, - value: -1, - observer: "selectedNewAttributeObserver", - }, - - localConfig: Object, - globalConfig: Object, - }; - } - - static get observers() { - return [ - "attributesObserver(localAttributes.*, globalAttributes.*, existingAttributes.*, newAttributes.*)", - ]; - } - - _initOpenObject(key, value, secondary, config) { - return { - attribute: key, - value: value, - closed: false, - domain: computeStateDomain(this.entity), - secondary: secondary, - description: key, - ...config, - }; - } - - loadEntity(entity) { - this.entity = entity; - return this.hass - .callApi("GET", "config/customize/config/" + entity.entity_id) - .then((data) => { - this.localConfig = data.local; - this.globalConfig = data.global; - this.newAttributes = []; - }); - } - - saveEntity() { - const data = {}; - const attrs = this.localAttributes.concat( - this.globalAttributes, - this.existingAttributes, - this.newAttributes - ); - attrs.forEach((attr) => { - if ( - attr.closed || - attr.secondary || - !attr.attribute || - attr.value === null || - attr.value === undefined - ) - return; - const value = attr.type === "json" ? JSON.parse(attr.value) : attr.value; - if (value === null || value === undefined) return; - data[attr.attribute] = value; - }); - - const objectId = this.entity.entity_id; - return this.hass.callApi( - "POST", - "config/customize/config/" + objectId, - data - ); - } - - _computeSingleAttribute(key, value, secondary) { - const config = hassAttributeUtil.LOGIC_STATE_ATTRIBUTES[key] || { - type: hassAttributeUtil.UNKNOWN_TYPE, - }; - return this._initOpenObject( - key, - config.type === "json" ? JSON.stringify(value) : value, - secondary, - config - ); - } - - _computeAttributes(config, keys, secondary) { - return keys.map((key) => - this._computeSingleAttribute(key, config[key], secondary) - ); - } - - _computeDocumentationUrl(hass) { - return documentationUrl( - hass, - "/docs/configuration/customizing-devices/#customization-using-the-ui" - ); - } - - computeLocalAttributes(localConfig) { - if (!localConfig) return []; - const localKeys = Object.keys(localConfig); - const result = this._computeAttributes(localConfig, localKeys, false); - return result; - } - - computeGlobalAttributes(localConfig, globalConfig) { - if (!localConfig || !globalConfig) return []; - const localKeys = Object.keys(localConfig); - const globalKeys = Object.keys(globalConfig).filter( - (key) => !localKeys.includes(key) - ); - return this._computeAttributes(globalConfig, globalKeys, true); - } - - computeExistingAttributes(localConfig, globalConfig, entity) { - if (!localConfig || !globalConfig || !entity) return []; - const localKeys = Object.keys(localConfig); - const globalKeys = Object.keys(globalConfig); - const entityKeys = Object.keys(entity.attributes).filter( - (key) => !localKeys.includes(key) && !globalKeys.includes(key) - ); - return this._computeAttributes(entity.attributes, entityKeys, true); - } - - computeShowWarning(localConfig, globalConfig) { - if (!localConfig || !globalConfig) return false; - return Object.keys(localConfig).some( - (key) => - JSON.stringify(globalConfig[key]) !== JSON.stringify(localConfig[key]) - ); - } - - filterFromAttributes(attributes) { - return (key) => - !attributes || - attributes.every((attr) => attr.attribute !== key || attr.closed); - } - - getNewAttributesOptions( - localAttributes, - globalAttributes, - existingAttributes, - newAttributes - ) { - const knownKeys = Object.keys(hassAttributeUtil.LOGIC_STATE_ATTRIBUTES) - .filter((key) => { - const conf = hassAttributeUtil.LOGIC_STATE_ATTRIBUTES[key]; - return ( - conf && - (!conf.domains || - !this.entity || - conf.domains.includes(computeStateDomain(this.entity))) - ); - }) - .filter(this.filterFromAttributes(localAttributes)) - .filter(this.filterFromAttributes(globalAttributes)) - .filter(this.filterFromAttributes(existingAttributes)) - .filter(this.filterFromAttributes(newAttributes)); - return knownKeys.sort().concat("Other"); - } - - selectedNewAttributeObserver(selected) { - if (selected < 0) return; - const option = this.newAttributesOptions[selected]; - if (selected === this.newAttributesOptions.length - 1) { - // The "Other" option. - const attr = this._initOpenObject("", "", false /* secondary */, { - type: hassAttributeUtil.ADD_TYPE, - }); - this.push("newAttributes", attr); - this.selectedNewAttribute = -1; - return; - } - let result = this.localAttributes.findIndex( - (attr) => attr.attribute === option - ); - if (result >= 0) { - this.set("localAttributes." + result + ".closed", false); - this.selectedNewAttribute = -1; - return; - } - result = this.globalAttributes.findIndex( - (attr) => attr.attribute === option - ); - if (result >= 0) { - this.set("globalAttributes." + result + ".closed", false); - this.selectedNewAttribute = -1; - return; - } - result = this.existingAttributes.findIndex( - (attr) => attr.attribute === option - ); - if (result >= 0) { - this.set("existingAttributes." + result + ".closed", false); - this.selectedNewAttribute = -1; - return; - } - result = this.newAttributes.findIndex((attr) => attr.attribute === option); - if (result >= 0) { - this.set("newAttributes." + result + ".closed", false); - this.selectedNewAttribute = -1; - return; - } - const attr = this._computeSingleAttribute( - option, - "", - false /* secondary */ - ); - this.push("newAttributes", attr); - this.selectedNewAttribute = -1; - } - - attributesObserver() { - this.hasLocalAttributes = - this.localAttributes && this.localAttributes.some((attr) => !attr.closed); - this.hasGlobalAttributes = - this.globalAttributes && - this.globalAttributes.some((attr) => !attr.closed); - this.hasExistingAttributes = - this.existingAttributes && - this.existingAttributes.some((attr) => !attr.closed); - this.hasNewAttributes = - this.newAttributes && this.newAttributes.some((attr) => !attr.closed); - this.newAttributesOptions = this.getNewAttributesOptions( - this.localAttributes, - this.globalAttributes, - this.existingAttributes, - this.newAttributes - ); - } -} -customElements.define("ha-form-customize", HaFormCustomize); diff --git a/src/panels/config/customize/types/ha-customize-array.js b/src/panels/config/customize/types/ha-customize-array.js deleted file mode 100644 index fe7147d0e9..0000000000 --- a/src/panels/config/customize/types/ha-customize-array.js +++ /dev/null @@ -1,63 +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 { EventsMixin } from "../../../../mixins/events-mixin"; - -/* - * @appliesMixin EventsMixin - */ -class HaCustomizeArray extends EventsMixin(PolymerElement) { - static get template() { - return html` - - - - - - - `; - } - - static get properties() { - return { - item: { - type: Object, - notifies: true, - }, - }; - } - - getOptions(item) { - const domain = item.domain || "*"; - const options = item.options[domain] || item.options["*"]; - if (!options) { - this.item.type = "string"; - this.fire("item-changed"); - return []; - } - return options.sort(); - } - - computeSelected(item) { - const options = this.getOptions(item); - return options.indexOf(item.value); - } -} -customElements.define("ha-customize-array", HaCustomizeArray); diff --git a/src/panels/config/customize/types/ha-customize-boolean.js b/src/panels/config/customize/types/ha-customize-boolean.js deleted file mode 100644 index 803e6d8d21..0000000000 --- a/src/panels/config/customize/types/ha-customize-boolean.js +++ /dev/null @@ -1,34 +0,0 @@ -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "../../../../components/ha-checkbox"; -import "../../../../components/ha-formfield"; - -class HaCustomizeBoolean extends PolymerElement { - static get template() { - return html` - - - - `; - } - - static get properties() { - return { - item: { - type: Object, - notifies: true, - }, - }; - } - - checkedChanged(ev) { - this.item.value = ev.target.checked; - } -} -customElements.define("ha-customize-boolean", HaCustomizeBoolean); diff --git a/src/panels/config/customize/types/ha-customize-icon.js b/src/panels/config/customize/types/ha-customize-icon.js deleted file mode 100644 index 86b32ed5bb..0000000000 --- a/src/panels/config/customize/types/ha-customize-icon.js +++ /dev/null @@ -1,40 +0,0 @@ -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 "../../../../components/ha-icon"; - -class HaCustomizeIcon extends PolymerElement { - static get template() { - return html` - - - - - `; - } - - static get properties() { - return { - item: { - type: Object, - notifies: true, - }, - }; - } -} -customElements.define("ha-customize-icon", HaCustomizeIcon); diff --git a/src/panels/config/customize/types/ha-customize-key-value.js b/src/panels/config/customize/types/ha-customize-key-value.js deleted file mode 100644 index 528ee13694..0000000000 --- a/src/panels/config/customize/types/ha-customize-key-value.js +++ /dev/null @@ -1,45 +0,0 @@ -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"; - -class HaCustomizeKeyValue extends PolymerElement { - static get template() { - return html` - - - - - - `; - } - - static get properties() { - return { - item: { - type: Object, - notifies: true, - }, - }; - } -} -customElements.define("ha-customize-key-value", HaCustomizeKeyValue); diff --git a/src/panels/config/customize/types/ha-customize-string.js b/src/panels/config/customize/types/ha-customize-string.js deleted file mode 100644 index 72fb4a685c..0000000000 --- a/src/panels/config/customize/types/ha-customize-string.js +++ /dev/null @@ -1,35 +0,0 @@ -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 { formatAttributeName } from "../../../../util/hass-attributes-util"; - -class HaCustomizeString extends PolymerElement { - static get template() { - return html` - - - `; - } - - static get properties() { - return { - item: { - type: Object, - notifies: true, - }, - }; - } - - getLabel(item) { - return ( - formatAttributeName(item.description) + - (item.type === "json" ? " (JSON formatted)" : "") - ); - } -} -customElements.define("ha-customize-string", HaCustomizeString); diff --git a/src/panels/config/entities/dialog-entity-editor.ts b/src/panels/config/entities/dialog-entity-editor.ts index 74bae4b7cb..afe29c75cd 100644 --- a/src/panels/config/entities/dialog-entity-editor.ts +++ b/src/panels/config/entities/dialog-entity-editor.ts @@ -165,21 +165,6 @@ export class DialogEntityEditor extends LitElement { >${this.hass.localize("ui.dialogs.entity_registry.faq")}` )} - ${this.hass.userData?.showAdvanced - ? html`

- ${this.hass.localize( - "ui.dialogs.entity_registry.info_customize", - "customize_link", - html`${this.hass.localize( - "ui.dialogs.entity_registry.customize_link" - )}` - )}` - : ""} `; case "tab-related": diff --git a/src/panels/config/ha-entity-config.ts b/src/panels/config/ha-entity-config.ts deleted file mode 100644 index 87a3fa0687..0000000000 --- a/src/panels/config/ha-entity-config.ts +++ /dev/null @@ -1,122 +0,0 @@ -import "@material/mwc-button"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property, query } from "lit/decorators"; -import "../../components/buttons/ha-progress-button"; -import "../../components/entity/ha-entity-picker"; -import "../../components/ha-card"; -import "../../components/ha-circular-progress"; -import { haStyle } from "../../resources/styles"; -import "../../styles/polymer-ha-style"; -import type { HomeAssistant } from "../../types"; -import { HaFormCustomize } from "./customize/ha-form-customize"; - -@customElement("ha-entity-config") -export class HaEntityConfig extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property() public selectedEntityId!: string; - - // False if no entity is selected or currently saving or loading - @property() private _formEditState = false; - - @query("#form") private _form!: HaFormCustomize; - - protected render(): TemplateResult { - return html` - -
- - - -
- - -
-
-
- - ${this.hass.localize("ui.common.save")} - -
-
- `; - } - - protected updated(changedProps: PropertyValues) { - super.updated(changedProps); - if ( - changedProps.has("selectedEntityId") && - changedProps.get("selectedEntityId") !== this.selectedEntityId - ) { - this._selectEntity(this.selectedEntityId); - this.requestUpdate(); - } - } - - private _selectedEntityChanged(ev) { - this._selectEntity(ev.target.value); - } - - private async _selectEntity(entityId?: string) { - if (!this._form || !entityId) return; - const entity = this.hass.states[entityId]; - if (!entity) return; - - this._formEditState = false; - await this._form.loadEntity(entity); - this._formEditState = true; - } - - private async _saveEntity(ev) { - if (!this._formEditState) return; - this._formEditState = false; - const button = ev.target; - button.progress = true; - - try { - await this._form.saveEntity(); - this._formEditState = true; - button.actionSuccess(); - } catch { - button.actionError(); - } finally { - button.progress = false; - } - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - ha-card { - direction: ltr; - } - - .form-placeholder { - height: 96px; - } - - .hidden { - display: none; - } - `, - ]; - } -} diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index a6a6b46126..54ef2ac080 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -10,7 +10,6 @@ import { mdiNfcVariant, mdiPalette, mdiPaletteSwatch, - mdiPencil, mdiPuzzle, mdiRobot, mdiScriptText, @@ -180,16 +179,6 @@ export const configSections: { [name: string]: PageNavigation[] } = { core: true, }, ], - advanced: [ - { - component: "customize", - path: "/config/customize", - translationKey: "ui.panel.config.customize.caption", - iconPath: mdiPencil, - core: true, - advancedOnly: true, - }, - ], }; @customElement("ha-panel-config") @@ -243,10 +232,8 @@ class HaPanelConfig extends HassRouterPage { tag: "ha-config-info", load: () => import("./info/ha-config-info"), }, - customize: { - tag: "ha-config-customize", - load: () => import("./customize/ha-config-customize"), - }, + // customize was removed in 2021.12, fallback to dashboard + customize: "dashboard", dashboard: { tag: "ha-config-dashboard", load: () => import("./dashboard/ha-config-dashboard"), diff --git a/src/panels/my/ha-panel-my.ts b/src/panels/my/ha-panel-my.ts index 07addd5bb5..d7be560e7b 100644 --- a/src/panels/my/ha-panel-my.ts +++ b/src/panels/my/ha-panel-my.ts @@ -141,7 +141,8 @@ const REDIRECTS: Redirects = { redirect: "/config/info", }, customize: { - redirect: "/config/customize", + // customize was removed in 2021.12, fallback to dashboard + redirect: "/config/dashboard", }, profile: { redirect: "/profile/dashboard", diff --git a/src/translations/en.json b/src/translations/en.json index 90cadb1735..bf077bdc02 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -601,7 +601,6 @@ "zone": "[%key:ui::panel::config::zone::caption%]", "users": "[%key:ui::panel::config::users::caption%]", "info": "[%key:ui::panel::config::info::caption%]", - "customize": "[%key:ui::panel::config::customize::caption%]", "blueprint": "[%key:ui::panel::config::blueprint::caption%]", "server_control": "[%key:ui::panel::config::server_control::caption%]" } @@ -687,8 +686,6 @@ "dismiss": "Dismiss", "no_unique_id": "This entity (''{entity_id}'') does not have a unique ID, therefore its settings cannot be managed from the UI. See the {faq_link} for more detail.", "faq": "documentation", - "info_customize": "You can overwrite some attributes in the {customize_link} section.", - "customize_link": "entity customizations", "editor": { "name": "Name", "icon": "Icon", @@ -1404,27 +1401,6 @@ } } }, - "customize": { - "caption": "Customizations", - "description": "Customize your entities", - "picker": { - "header": "Customizations", - "introduction": "Tweak per-entity attributes. Added/edited customizations will take effect immediately. Removed customizations will take effect when the entity is updated.", - "documentation": "Customization documentation" - }, - "warning": { - "include_sentence": "It seems that your configuration.yaml doesn't properly", - "include_link": "include customize.yaml", - "not_applied": "Changes made here are written in it, but will not be applied after a configuration reload unless the include is in place." - }, - "attributes_customize": "The following attributes are already set in customize.yaml", - "attributes_outside": "The following attributes are customized from outside of customize.yaml", - "different_include": "Possibly via a domain, a glob or a different include.", - "attributes_set": "The following attributes of the entity are set programmatically.", - "attributes_override": "You can override them if you like.", - "attributes_not_set": "The following attributes weren't set. Set them if you like.", - "pick_attribute": "Pick an attribute to override" - }, "automation": { "caption": "Automations", "description": "Create custom behavior rules for your home", From a567312bdbda4df91bb2a6854dcbe058508f553f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 17 Nov 2021 19:39:16 +0100 Subject: [PATCH 018/112] Show updates on dashboard for dev (#10637) --- hassio/src/dashboard/hassio-dashboard.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hassio/src/dashboard/hassio-dashboard.ts b/hassio/src/dashboard/hassio-dashboard.ts index ed74bb920d..c33be8e875 100644 --- a/hassio/src/dashboard/hassio-dashboard.ts +++ b/hassio/src/dashboard/hassio-dashboard.ts @@ -37,7 +37,8 @@ class HassioDashboard extends LitElement { ${this.supervisor.localize("panel.dashboard")}
- ${!atLeastVersion(this.hass.config.version, 2021, 12) + ${this.hass.config.version.includes("dev") || + !atLeastVersion(this.hass.config.version, 2021, 12) ? html` Date: Wed, 17 Nov 2021 12:43:41 -0600 Subject: [PATCH 019/112] Area Card (#10141) Co-authored-by: Philip Allgaier Co-authored-by: Bram Kragten Co-authored-by: Paulus Schoutsen --- gallery/public/images/office.jpg | Bin 0 -> 150181 bytes gallery/src/demos/demo-hui-area-card.ts | 156 +++++++ src/components/ha-area-picker.ts | 3 + src/data/area_registry.ts | 2 +- src/panels/lovelace/cards/hui-area-card.ts | 431 ++++++++++++++++++ src/panels/lovelace/cards/types.ts | 5 + .../create-element/create-card-element.ts | 1 + .../config-elements/hui-area-card-editor.ts | 119 +++++ src/panels/lovelace/editor/lovelace-cards.ts | 3 + src/translations/en.json | 7 + 10 files changed, 726 insertions(+), 1 deletion(-) create mode 100644 gallery/public/images/office.jpg create mode 100644 gallery/src/demos/demo-hui-area-card.ts create mode 100644 src/panels/lovelace/cards/hui-area-card.ts create mode 100644 src/panels/lovelace/editor/config-elements/hui-area-card-editor.ts diff --git a/gallery/public/images/office.jpg b/gallery/public/images/office.jpg new file mode 100644 index 0000000000000000000000000000000000000000..128a77368ec4d86ac51f6d53ad29a093e01498ca GIT binary patch literal 150181 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 literal 0 HcmV?d00001 diff --git a/gallery/src/demos/demo-hui-area-card.ts b/gallery/src/demos/demo-hui-area-card.ts new file mode 100644 index 0000000000..bbb6dedfd2 --- /dev/null +++ b/gallery/src/demos/demo-hui-area-card.ts @@ -0,0 +1,156 @@ +import { html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { customElement, query } from "lit/decorators"; +import { getEntity } from "../../../src/fake_data/entity"; +import { provideHass } from "../../../src/fake_data/provide_hass"; +import "../components/demo-cards"; + +const ENTITIES = [ + getEntity("light", "bed_light", "on", { + friendly_name: "Bed Light", + }), + getEntity("switch", "bed_ac", "on", { + friendly_name: "Ecobee", + }), + getEntity("sensor", "bed_temp", "72", { + friendly_name: "Bedroom Temp", + device_class: "temperature", + unit_of_measurement: "°F", + }), + getEntity("light", "living_room_light", "off", { + friendly_name: "Living Room Light", + }), + getEntity("fan", "living_room", "on", { + friendly_name: "Living Room Fan", + }), + getEntity("sensor", "office_humidity", "73", { + friendly_name: "Office Humidity", + device_class: "humidity", + unit_of_measurement: "%", + }), + getEntity("light", "office", "on", { + friendly_name: "Office Light", + }), + getEntity("fan", "kitchen", "on", { + friendly_name: "Second Office Fan", + }), + getEntity("binary_sensor", "kitchen_door", "on", { + friendly_name: "Office Door", + device_class: "door", + }), +]; + +// TODO: Update image here +const CONFIGS = [ + { + heading: "Bedroom", + config: ` +- type: area + area: bedroom + image: "/images/bed.png" + `, + }, + { + heading: "Living Room", + config: ` +- type: area + area: living_room + image: "/images/living_room.png" + `, + }, + { + heading: "Office", + config: ` +- type: area + area: office + image: "/images/office.jpg" + `, + }, + { + heading: "Kitchen", + config: ` +- type: area + area: kitchen + image: "/images/kitchen.png" + `, + }, +]; + +@customElement("demo-hui-area-card") +class DemoArea extends LitElement { + @query("#demos") private _demoRoot!: HTMLElement; + + protected render(): TemplateResult { + return html``; + } + + protected firstUpdated(changedProperties: PropertyValues) { + super.firstUpdated(changedProperties); + const hass = provideHass(this._demoRoot); + hass.updateTranslations(null, "en"); + hass.updateTranslations("lovelace", "en"); + hass.addEntities(ENTITIES); + hass.mockWS("config/area_registry/list", () => [ + { + name: "Bedroom", + area_id: "bedroom", + }, + { + name: "Living Room", + area_id: "living_room", + }, + { + name: "Office", + area_id: "office", + }, + { + name: "Second Office", + area_id: "kitchen", + }, + ]); + hass.mockWS("config/device_registry/list", () => []); + hass.mockWS("config/entity_registry/list", () => [ + { + area_id: "bedroom", + entity_id: "light.bed_light", + }, + { + area_id: "bedroom", + entity_id: "switch.bed_ac", + }, + { + area_id: "bedroom", + entity_id: "sensor.bed_temp", + }, + { + area_id: "living_room", + entity_id: "light.living_room_light", + }, + { + area_id: "living_room", + entity_id: "fan.living_room", + }, + { + area_id: "office", + entity_id: "light.office", + }, + { + area_id: "office", + entity_id: "sensor.office_humidity", + }, + { + area_id: "kitchen", + entity_id: "fan.kitchen", + }, + { + area_id: "kitchen", + entity_id: "binary_sensor.kitchen_door", + }, + ]); + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-hui-area-card": DemoArea; + } +} diff --git a/src/components/ha-area-picker.ts b/src/components/ha-area-picker.ts index 4a22d3775e..ed658188df 100644 --- a/src/components/ha-area-picker.ts +++ b/src/components/ha-area-picker.ts @@ -172,6 +172,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { { area_id: "", name: this.hass.localize("ui.components.area-picker.no_areas"), + picture: null, }, ]; } @@ -295,6 +296,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { { area_id: "", name: this.hass.localize("ui.components.area-picker.no_match"), + picture: null, }, ]; } @@ -306,6 +308,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { { area_id: "add_new", name: this.hass.localize("ui.components.area-picker.add_new"), + picture: null, }, ]; } diff --git a/src/data/area_registry.ts b/src/data/area_registry.ts index 3e432ce626..ffb3d4c153 100644 --- a/src/data/area_registry.ts +++ b/src/data/area_registry.ts @@ -7,7 +7,7 @@ import { HomeAssistant } from "../types"; export interface AreaRegistryEntry { area_id: string; name: string; - picture?: string; + picture: string | null; } export interface AreaRegistryEntryMutableParams { diff --git a/src/panels/lovelace/cards/hui-area-card.ts b/src/panels/lovelace/cards/hui-area-card.ts new file mode 100644 index 0000000000..e9a72c7d16 --- /dev/null +++ b/src/panels/lovelace/cards/hui-area-card.ts @@ -0,0 +1,431 @@ +import "@material/mwc-ripple"; +import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; +import { styleMap } from "lit/directives/style-map"; +import memoizeOne from "memoize-one"; +import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { computeDomain } from "../../../common/entity/compute_domain"; +import { computeStateDisplay } from "../../../common/entity/compute_state_display"; +import { navigate } from "../../../common/navigate"; +import "../../../components/entity/state-badge"; +import "../../../components/ha-card"; +import "../../../components/ha-icon-button"; +import "../../../components/ha-state-icon"; +import "../../../components/ha-svg-icon"; +import { + AreaRegistryEntry, + subscribeAreaRegistry, +} from "../../../data/area_registry"; +import { + DeviceRegistryEntry, + subscribeDeviceRegistry, +} from "../../../data/device_registry"; +import { + EntityRegistryEntry, + subscribeEntityRegistry, +} from "../../../data/entity_registry"; +import { forwardHaptic } from "../../../data/haptics"; +import { ActionHandlerEvent } from "../../../data/lovelace"; +import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; +import { HomeAssistant } from "../../../types"; +import { actionHandler } from "../common/directives/action-handler-directive"; +import { toggleEntity } from "../common/entity/toggle-entity"; +import "../components/hui-warning"; +import { LovelaceCard, LovelaceCardEditor } from "../types"; +import { AreaCardConfig } from "./types"; + +const SENSOR_DOMAINS = new Set(["sensor", "binary_sensor"]); + +const SENSOR_DEVICE_CLASSES = new Set([ + "temperature", + "humidity", + "motion", + "door", + "aqi", +]); + +const TOGGLE_DOMAINS = new Set(["light", "fan", "switch"]); + +@customElement("hui-area-card") +export class HuiAreaCard + extends SubscribeMixin(LitElement) + implements LovelaceCard +{ + public static async getConfigElement(): Promise { + await import("../editor/config-elements/hui-area-card-editor"); + return document.createElement("hui-area-card-editor"); + } + + public static getStubConfig(): AreaCardConfig { + return { type: "area", area: "" }; + } + + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _config?: AreaCardConfig; + + @state() private _entities?: EntityRegistryEntry[]; + + @state() private _devices?: DeviceRegistryEntry[]; + + @state() private _areas?: AreaRegistryEntry[]; + + private _memberships = memoizeOne( + ( + areaId: string, + devicesInArea: Set, + registryEntities: EntityRegistryEntry[], + states: HomeAssistant["states"] + ) => { + const entitiesInArea = registryEntities + .filter( + (entry) => + !entry.entity_category && + (entry.area_id + ? entry.area_id === areaId + : entry.device_id && devicesInArea.has(entry.device_id)) + ) + .map((entry) => entry.entity_id); + + const sensorEntities: HassEntity[] = []; + const entitiesToggle: HassEntity[] = []; + + for (const entity of entitiesInArea) { + const domain = computeDomain(entity); + if (!TOGGLE_DOMAINS.has(domain) && !SENSOR_DOMAINS.has(domain)) { + continue; + } + + const stateObj: HassEntity | undefined = states[entity]; + + if (!stateObj) { + continue; + } + + if (entitiesToggle.length < 3 && TOGGLE_DOMAINS.has(domain)) { + entitiesToggle.push(stateObj); + continue; + } + + if ( + sensorEntities.length < 3 && + SENSOR_DOMAINS.has(domain) && + stateObj.attributes.device_class && + SENSOR_DEVICE_CLASSES.has(stateObj.attributes.device_class) + ) { + sensorEntities.push(stateObj); + } + + if (sensorEntities.length === 3 && entitiesToggle.length === 3) { + break; + } + } + + return { sensorEntities, entitiesToggle }; + } + ); + + private _area = memoizeOne( + (areaId: string | undefined, areas: AreaRegistryEntry[]) => + areas.find((area) => area.area_id === areaId) || null + ); + + private _devicesInArea = memoizeOne( + (areaId: string | undefined, devices: DeviceRegistryEntry[]) => + new Set( + areaId + ? devices + .filter((device) => device.area_id === areaId) + .map((device) => device.id) + : [] + ) + ); + + public hassSubscribe(): UnsubscribeFunc[] { + return [ + subscribeAreaRegistry(this.hass!.connection, (areas) => { + this._areas = areas; + }), + subscribeDeviceRegistry(this.hass!.connection, (devices) => { + this._devices = devices; + }), + subscribeEntityRegistry(this.hass!.connection, (entries) => { + this._entities = entries; + }), + ]; + } + + public getCardSize(): number { + return 3; + } + + public setConfig(config: AreaCardConfig): void { + if (!config.area) { + throw new Error("Area Required"); + } + + this._config = config; + } + + protected shouldUpdate(changedProps: PropertyValues): boolean { + if (changedProps.has("_config") || !this._config) { + return true; + } + + if ( + changedProps.has("_devicesInArea") || + changedProps.has("_area") || + changedProps.has("_entities") + ) { + return true; + } + + if (!changedProps.has("hass")) { + return false; + } + + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + + if ( + !oldHass || + oldHass.themes !== this.hass!.themes || + oldHass.locale !== this.hass!.locale + ) { + return true; + } + + if ( + !this._devices || + !this._devicesInArea(this._config.area, this._devices) || + !this._entities + ) { + return false; + } + + const { sensorEntities, entitiesToggle } = this._memberships( + this._config.area, + this._devicesInArea(this._config.area, this._devices), + this._entities, + this.hass.states + ); + + for (const stateObj of sensorEntities) { + if (oldHass!.states[stateObj.entity_id] !== stateObj) { + return true; + } + } + + for (const stateObj of entitiesToggle) { + if (oldHass!.states[stateObj.entity_id] !== stateObj) { + return true; + } + } + + return false; + } + + protected render(): TemplateResult { + if ( + !this._config || + !this.hass || + !this._areas || + !this._devices || + !this._entities + ) { + return html``; + } + + const { sensorEntities, entitiesToggle } = this._memberships( + this._config.area, + this._devicesInArea(this._config.area, this._devices), + this._entities, + this.hass.states + ); + + const area = this._area(this._config.area, this._areas); + + if (area === null) { + return html` + + ${this.hass.localize("ui.card.area.area_not_found")} + + `; + } + + return html` + +

+
+ ${sensorEntities.map( + (stateObj) => html` + + + ${computeDomain(stateObj.entity_id) === "binary_sensor" + ? "" + : html` + ${computeStateDisplay( + this.hass!.localize, + stateObj, + this.hass!.locale + )} + `} + + ` + )} +
+
+
+ ${area.name} +
+
+ ${entitiesToggle.map( + (stateObj) => html` + + + + ` + )} +
+
+
+ + `; + } + + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + if (!this._config || !this.hass) { + return; + } + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + const oldConfig = changedProps.get("_config") as AreaCardConfig | undefined; + + if ( + (changedProps.has("hass") && + (!oldHass || oldHass.themes !== this.hass.themes)) || + (changedProps.has("_config") && + (!oldConfig || oldConfig.theme !== this._config.theme)) + ) { + applyThemesOnElement(this, this.hass.themes, this._config.theme); + } + } + + private _handleMoreInfo(ev) { + const entity = (ev.currentTarget as any).entity; + fireEvent(this, "hass-more-info", { entityId: entity }); + } + + private _handleNavigation() { + if (this._config!.navigation_path) { + navigate(this._config!.navigation_path); + } + } + + private _handleAction(ev: ActionHandlerEvent) { + const entity = (ev.currentTarget as any).entity as string; + if (ev.detail.action === "hold") { + fireEvent(this, "hass-more-info", { entityId: entity }); + } else if (ev.detail.action === "tap") { + toggleEntity(this.hass, entity); + forwardHaptic("light"); + } + } + + static get styles(): CSSResultGroup { + return css` + ha-card { + overflow: hidden; + position: relative; + padding-bottom: 56.25%; + background-size: cover; + } + + .container { + display: flex; + flex-direction: column; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-color: rgba(0, 0, 0, 0.4); + } + + .sensors { + color: white; + font-size: 18px; + flex: 1; + padding: 16px; + --mdc-icon-size: 28px; + cursor: pointer; + } + + .name { + color: white; + font-size: 24px; + } + + .bottom { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 8px 8px 16px; + } + + .name.navigate { + cursor: pointer; + } + + state-badge { + --ha-icon-display: inline; + } + + ha-icon-button { + color: white; + background-color: var(--area-button-color, rgb(175, 175, 175, 0.5)); + border-radius: 50%; + margin-left: 8px; + --mdc-icon-button-size: 44px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-area-card": HuiAreaCard; + } +} diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index 80a70b4378..cf5ecb5198 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -76,6 +76,11 @@ export interface EntitiesCardConfig extends LovelaceCardConfig { state_color?: boolean; } +export interface AreaCardConfig extends LovelaceCardConfig { + area: string; + navigation_path?: string; +} + export interface ButtonCardConfig extends LovelaceCardConfig { entity?: string; name?: string; diff --git a/src/panels/lovelace/create-element/create-card-element.ts b/src/panels/lovelace/create-element/create-card-element.ts index 6c8c470e35..3d081cadaf 100644 --- a/src/panels/lovelace/create-element/create-card-element.ts +++ b/src/panels/lovelace/create-element/create-card-element.ts @@ -33,6 +33,7 @@ const ALWAYS_LOADED_TYPES = new Set([ const LAZY_LOAD_TYPES = { "alarm-panel": () => import("../cards/hui-alarm-panel-card"), + area: () => import("../cards/hui-area-card"), error: () => import("../cards/hui-error-card"), "empty-state": () => import("../cards/hui-empty-state-card"), "energy-usage-graph": () => 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 new file mode 100644 index 0000000000..e23c463746 --- /dev/null +++ b/src/panels/lovelace/editor/config-elements/hui-area-card-editor.ts @@ -0,0 +1,119 @@ +import "@polymer/paper-input/paper-input"; +import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { assert, assign, object, optional, string } from "superstruct"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-area-picker"; +import { HomeAssistant } from "../../../../types"; +import { AreaCardConfig } from "../../cards/types"; +import "../../components/hui-theme-select-editor"; +import { LovelaceCardEditor } from "../../types"; +import { baseLovelaceCardConfig } from "../structs/base-card-struct"; +import { EditorTarget } from "../types"; +import { configElementStyle } from "./config-elements-style"; + +const cardConfigStruct = assign( + baseLovelaceCardConfig, + object({ + area: optional(string()), + navigation_path: optional(string()), + theme: optional(string()), + }) +); + +@customElement("hui-area-card-editor") +export class HuiAreaCardEditor + extends LitElement + implements LovelaceCardEditor +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: AreaCardConfig; + + public setConfig(config: AreaCardConfig): void { + assert(config, cardConfigStruct); + this._config = config; + } + + get _area(): string { + return this._config!.area || ""; + } + + get _navigation_path(): string { + return this._config!.navigation_path || ""; + } + + get _theme(): string { + return this._config!.theme || ""; + } + + protected render(): TemplateResult { + if (!this.hass || !this._config) { + return html``; + } + + return html` +
+ + + + +
+ `; + } + + private _valueChanged(ev: CustomEvent): void { + if (!this._config || !this.hass) { + return; + } + const target = ev.target! as EditorTarget; + const value = ev.detail.value; + + if (this[`_${target.configValue}`] === value) { + return; + } + + let newConfig; + if (target.configValue) { + if (!value) { + newConfig = { ...this._config }; + delete newConfig[target.configValue!]; + } else { + newConfig = { + ...this._config, + [target.configValue!]: value, + }; + } + } + fireEvent(this, "config-changed", { config: newConfig }); + } + + static get styles(): CSSResultGroup { + return configElementStyle; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-area-card-editor": HuiAreaCardEditor; + } +} diff --git a/src/panels/lovelace/editor/lovelace-cards.ts b/src/panels/lovelace/editor/lovelace-cards.ts index 2a9beb0371..424cf8a4eb 100644 --- a/src/panels/lovelace/editor/lovelace-cards.ts +++ b/src/panels/lovelace/editor/lovelace-cards.ts @@ -89,6 +89,9 @@ export const coreCards: Card[] = [ type: "weather-forecast", showElement: true, }, + { + type: "area", + }, { type: "conditional", }, diff --git a/src/translations/en.json b/src/translations/en.json index bf077bdc02..744ef87c5b 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -116,6 +116,9 @@ "arm_vacation": "Arm vacation", "arm_custom_bypass": "Custom bypass" }, + "area": { + "area_not_found": "Area not found." + }, "automation": { "last_triggered": "Last triggered", "trigger": "Run Actions" @@ -3212,6 +3215,10 @@ "available_states": "Available States", "description": "The Alarm Panel card allows you to Arm and Disarm your alarm control panel integrations." }, + "area": { + "name": "Area", + "description": "The Area card automatically displays entities of a specific area." + }, "calendar": { "name": "Calendar", "description": "The Calendar card displays a calendar including day, week and list views", From 1ebd2fb9f1669baa5d6368e3b3dfeaa5fd3beaf5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 17 Nov 2021 10:54:08 -0800 Subject: [PATCH 020/112] Bumped version to 20211117.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c4511c73a4..615db03d55 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20211109.0", + version="20211117.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/frontend", author="The Home Assistant Authors", From 91b009af7997ca456a08a113862e5ba7448ae67c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 18 Nov 2021 18:57:15 +0100 Subject: [PATCH 021/112] Fix back button color (#10650) --- hassio/src/update-available/update-available-dashboard.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/hassio/src/update-available/update-available-dashboard.ts b/hassio/src/update-available/update-available-dashboard.ts index c03c644eb4..201dca02a3 100644 --- a/hassio/src/update-available/update-available-dashboard.ts +++ b/hassio/src/update-available/update-available-dashboard.ts @@ -355,9 +355,10 @@ class UpdateAvailableDashboard extends LitElement { static get styles(): CSSResultGroup { return css` - hass-subpage { - --app-header-background-color: background-color: var(--primary-background-color); - } + hass-subpage { + --app-header-background-color: var(--primary-background-color); + --app-header-text-color: var(--sidebar-text-color); + } ha-card { margin: auto; margin-top: 16px; From 0d19f4792fd4608abbbe8cdbdb3f3a072a3cd52f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 18 Nov 2021 20:21:19 +0100 Subject: [PATCH 022/112] Fix active tab (#10654) --- src/layouts/hass-tabs-subpage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layouts/hass-tabs-subpage.ts b/src/layouts/hass-tabs-subpage.ts index 9b009aefc9..562374fd71 100644 --- a/src/layouts/hass-tabs-subpage.ts +++ b/src/layouts/hass-tabs-subpage.ts @@ -85,7 +85,7 @@ class HassTabsSubpage extends LitElement { Date: Fri, 19 Nov 2021 01:09:13 +0100 Subject: [PATCH 023/112] Remove ha-alert actionText (#10646) --- gallery/src/demos/demo-ha-alert.ts | 8 +++--- gallery/src/ha-gallery.js | 5 ---- .../src/addon-view/info/hassio-addon-info.ts | 12 ++++++--- hassio/src/system/hassio-supervisor-info.ts | 27 ++++++++++++------- src/components/ha-alert.ts | 22 ++++----------- 5 files changed, 33 insertions(+), 41 deletions(-) diff --git a/gallery/src/demos/demo-ha-alert.ts b/gallery/src/demos/demo-ha-alert.ts index bb833bed54..57b80e47ec 100644 --- a/gallery/src/demos/demo-ha-alert.ts +++ b/gallery/src/demos/demo-ha-alert.ts @@ -9,7 +9,6 @@ const alerts: { description: string | TemplateResult; type: "info" | "warning" | "error" | "success"; dismissable?: boolean; - action?: string; rtl?: boolean; iconSlot?: TemplateResult; actionSlot?: TemplateResult; @@ -76,13 +75,13 @@ const alerts: { title: "Error with action", description: "This is a test error alert with action", type: "error", - action: "restart", + actionSlot: html``, }, { title: "Unsaved data", description: "You have unsaved data", type: "warning", - action: "save", + actionSlot: html``, }, { title: "Slotted icon", @@ -106,7 +105,7 @@ const alerts: { title: "Error with action", description: "This is a test error alert with action (RTL)", type: "error", - action: "restart", + actionSlot: html``, rtl: true, }, { @@ -129,7 +128,6 @@ export class DemoHaAlert extends LitElement { .title=${alert.title || ""} .alertType=${alert.type} .dismissable=${alert.dismissable || false} - .actionText=${alert.action || ""} .rtl=${alert.rtl || false} > ${alert.iconSlot} ${alert.description} ${alert.actionSlot} diff --git a/gallery/src/ha-gallery.js b/gallery/src/ha-gallery.js index 7f53dd717a..671cbe9077 100644 --- a/gallery/src/ha-gallery.js +++ b/gallery/src/ha-gallery.js @@ -176,11 +176,6 @@ class HaGallery extends PolymerElement { this.addEventListener("alert-dismissed-clicked", () => this.$.notifications.showDialog({ message: "Alert dismissed clicked" }) ); - - this.addEventListener("alert-action-clicked", () => - this.$.notifications.showDialog({ message: "Alert action clicked" }) - ); - this.addEventListener("hass-more-info", (ev) => { if (ev.detail.entityId) { this.$.notifications.showDialog({ diff --git a/hassio/src/addon-view/info/hassio-addon-info.ts b/hassio/src/addon-view/info/hassio-addon-info.ts index be50bc59c5..756a471359 100644 --- a/hassio/src/addon-view/info/hassio-addon-info.ts +++ b/hassio/src/addon-view/info/hassio-addon-info.ts @@ -151,14 +151,18 @@ class HassioAddonInfo extends LitElement { .title=${this.supervisor.localize( "addon.dashboard.protection_mode.title" )} - .actionText=${this.supervisor.localize( - "addon.dashboard.protection_mode.enable" - )} - @alert-action-clicked=${this._protectionToggled} > ${this.supervisor.localize( "addon.dashboard.protection_mode.content" )} + + ` : ""} diff --git a/hassio/src/system/hassio-supervisor-info.ts b/hassio/src/system/hassio-supervisor-info.ts index 887ddd105d..4c3a50efa4 100644 --- a/hassio/src/system/hassio-supervisor-info.ts +++ b/hassio/src/system/hassio-supervisor-info.ts @@ -151,24 +151,28 @@ class HassioSupervisorInfo extends LitElement { > ` : "" - : html` + : html` ${this.supervisor.localize( "system.supervisor.unsupported_title" )} + + `} ${!this.supervisor.supervisor.healthy - ? html` + ? html` ${this.supervisor.localize( "system.supervisor.unhealthy_title" )} + + ` : ""}
@@ -466,6 +470,9 @@ class HassioSupervisorInfo extends LitElement { white-space: normal; color: var(--secondary-text-color); } + ha-alert mwc-button { + --mdc-theme-primary: var(--primary-text-color); + } a { text-decoration: none; } diff --git a/src/components/ha-alert.ts b/src/components/ha-alert.ts index 236ae8c7c5..93df6b7e24 100644 --- a/src/components/ha-alert.ts +++ b/src/components/ha-alert.ts @@ -1,4 +1,3 @@ -import "@material/mwc-button/mwc-button"; import { mdiAlertCircleOutline, mdiAlertOutline, @@ -23,7 +22,6 @@ const ALERT_ICONS = { declare global { interface HASSDomEvents { "alert-dismissed-clicked": undefined; - "alert-action-clicked": undefined; } } @@ -37,8 +35,6 @@ class HaAlert extends LitElement { | "error" | "success" = "info"; - @property({ attribute: "action-text" }) public actionText = ""; - @property({ type: Boolean }) public dismissable = false; @property({ type: Boolean }) public rtl = false; @@ -63,12 +59,7 @@ class HaAlert extends LitElement {
- ${this.actionText - ? html`` - : this.dismissable + ${this.dismissable ? html` Date: Fri, 19 Nov 2021 01:09:39 +0100 Subject: [PATCH 024/112] Use ha-formfield around backup checkbox (#10653) --- .../update-available-dashboard.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/hassio/src/update-available/update-available-dashboard.ts b/hassio/src/update-available/update-available-dashboard.ts index 201dca02a3..50aefd54da 100644 --- a/hassio/src/update-available/update-available-dashboard.ts +++ b/hassio/src/update-available/update-available-dashboard.ts @@ -16,6 +16,7 @@ import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-card"; import "../../../src/components/ha-checkbox"; import "../../../src/components/ha-expansion-panel"; +import "../../../src/components/ha-formfield"; import "../../../src/components/ha-icon-button"; import "../../../src/components/ha-markdown"; import "../../../src/components/ha-settings-row"; @@ -171,19 +172,17 @@ class UpdateAvailableDashboard extends LitElement {
${!["os", "supervisor"].includes(this._updateEntry) ? html` - + - - ${this.supervisor.localize( - "update_available.create_backup" - )} - - + ` : ""} ` From 390e5b38815f7b688a00b14710de5ba86128d7f2 Mon Sep 17 00:00:00 2001 From: Lasse Rosenow <10547444+LasseRosenow@users.noreply.github.com> Date: Fri, 19 Nov 2021 01:20:45 +0100 Subject: [PATCH 025/112] Simplify launch screen svg (#10643) --- demo/src/html/index.html.template | 18 +++--------------- src/components/ha-logo-svg.ts | 18 +++--------------- src/html/index.html.template | 18 +++--------------- 3 files changed, 9 insertions(+), 45 deletions(-) diff --git a/demo/src/html/index.html.template b/demo/src/html/index.html.template index 85e5388388..67c8a0bca9 100644 --- a/demo/src/html/index.html.template +++ b/demo/src/html/index.html.template @@ -101,21 +101,9 @@
- - - - - - - - - - - - - - - + + +
diff --git a/src/components/ha-logo-svg.ts b/src/components/ha-logo-svg.ts index e8a78196aa..644f35e93e 100644 --- a/src/components/ha-logo-svg.ts +++ b/src/components/ha-logo-svg.ts @@ -5,21 +5,9 @@ import { customElement } from "lit/decorators"; export class HaLogoSvg extends LitElement { protected render(): SVGTemplateResult { return svg` - - - - - - - - - - - - - - - + + + `; } diff --git a/src/html/index.html.template b/src/html/index.html.template index d5ba727af1..3568135740 100644 --- a/src/html/index.html.template +++ b/src/html/index.html.template @@ -77,21 +77,9 @@
- - - - - - - - - - - - - - - + + +
From 5304e5a67028f31e8b317333266d84ca3a95c4ea Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 19 Nov 2021 13:16:43 -0800 Subject: [PATCH 026/112] Always render groups/areas in a single column (#10655) --- .../common/generate-lovelace-config.ts | 13 +++- .../create-element/create-card-element.ts | 61 +++++++++---------- 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/src/panels/lovelace/common/generate-lovelace-config.ts b/src/panels/lovelace/common/generate-lovelace-config.ts index 0561b1c9db..bd8dce5416 100644 --- a/src/panels/lovelace/common/generate-lovelace-config.ts +++ b/src/panels/lovelace/common/generate-lovelace-config.ts @@ -176,7 +176,18 @@ export const computeCards = ( }); } - return cards; + if (cards.length < 2) { + return cards; + } + + return [ + { + type: "grid", + square: false, + columns: 1, + cards, + }, + ]; }; const computeDefaultViewStates = ( diff --git a/src/panels/lovelace/create-element/create-card-element.ts b/src/panels/lovelace/create-element/create-card-element.ts index 3d081cadaf..df2eb3b45d 100644 --- a/src/panels/lovelace/create-element/create-card-element.ts +++ b/src/panels/lovelace/create-element/create-card-element.ts @@ -5,11 +5,10 @@ import "../cards/hui-entities-card"; import "../cards/hui-entity-button-card"; import "../cards/hui-entity-card"; import "../cards/hui-glance-card"; -import "../cards/hui-horizontal-stack-card"; +import "../cards/hui-grid-card"; import "../cards/hui-light-card"; import "../cards/hui-sensor-card"; import "../cards/hui-thermostat-card"; -import "../cards/hui-vertical-stack-card"; import "../cards/hui-weather-forecast-card"; import { createLovelaceElement, @@ -23,59 +22,59 @@ const ALWAYS_LOADED_TYPES = new Set([ "button", "entity-button", "glance", - "horizontal-stack", + "grid", "light", "sensor", "thermostat", - "vertical-stack", "weather-forecast", ]); const LAZY_LOAD_TYPES = { "alarm-panel": () => import("../cards/hui-alarm-panel-card"), area: () => import("../cards/hui-area-card"), - error: () => import("../cards/hui-error-card"), + calendar: () => import("../cards/hui-calendar-card"), + conditional: () => import("../cards/hui-conditional-card"), "empty-state": () => import("../cards/hui-empty-state-card"), - "energy-usage-graph": () => - import("../cards/energy/hui-energy-usage-graph-card"), - "energy-solar-graph": () => - import("../cards/energy/hui-energy-solar-graph-card"), - "energy-gas-graph": () => import("../cards/energy/hui-energy-gas-graph-card"), - "energy-devices-graph": () => - import("../cards/energy/hui-energy-devices-graph-card"), - "energy-sources-table": () => - import("../cards/energy/hui-energy-sources-table-card"), - "energy-distribution": () => - import("../cards/energy/hui-energy-distribution-card"), - "energy-solar-consumed-gauge": () => - import("../cards/energy/hui-energy-solar-consumed-gauge-card"), - "energy-grid-neutrality-gauge": () => - import("../cards/energy/hui-energy-grid-neutrality-gauge-card"), "energy-carbon-consumed-gauge": () => import("../cards/energy/hui-energy-carbon-consumed-gauge-card"), "energy-date-selection": () => import("../cards/energy/hui-energy-date-selection-card"), - grid: () => import("../cards/hui-grid-card"), - starting: () => import("../cards/hui-starting-card"), + "energy-devices-graph": () => + import("../cards/energy/hui-energy-devices-graph-card"), + "energy-distribution": () => + import("../cards/energy/hui-energy-distribution-card"), + "energy-gas-graph": () => import("../cards/energy/hui-energy-gas-graph-card"), + "energy-grid-neutrality-gauge": () => + import("../cards/energy/hui-energy-grid-neutrality-gauge-card"), + "energy-solar-consumed-gauge": () => + import("../cards/energy/hui-energy-solar-consumed-gauge-card"), + "energy-solar-graph": () => + import("../cards/energy/hui-energy-solar-graph-card"), + "energy-sources-table": () => + import("../cards/energy/hui-energy-sources-table-card"), + "energy-usage-graph": () => + import("../cards/energy/hui-energy-usage-graph-card"), "entity-filter": () => import("../cards/hui-entity-filter-card"), + error: () => import("../cards/hui-error-card"), + gauge: () => import("../cards/hui-gauge-card"), + "history-graph": () => import("../cards/hui-history-graph-card"), + "horizontal-stack": () => import("../cards/hui-horizontal-stack-card"), humidifier: () => import("../cards/hui-humidifier-card"), + iframe: () => import("../cards/hui-iframe-card"), + logbook: () => import("../cards/hui-logbook-card"), + map: () => import("../cards/hui-map-card"), + markdown: () => import("../cards/hui-markdown-card"), "media-control": () => import("../cards/hui-media-control-card"), "picture-elements": () => import("../cards/hui-picture-elements-card"), "picture-entity": () => import("../cards/hui-picture-entity-card"), "picture-glance": () => import("../cards/hui-picture-glance-card"), + picture: () => import("../cards/hui-picture-card"), "plant-status": () => import("../cards/hui-plant-status-card"), "safe-mode": () => import("../cards/hui-safe-mode-card"), "shopping-list": () => import("../cards/hui-shopping-list-card"), - conditional: () => import("../cards/hui-conditional-card"), - gauge: () => import("../cards/hui-gauge-card"), - "history-graph": () => import("../cards/hui-history-graph-card"), + starting: () => import("../cards/hui-starting-card"), "statistics-graph": () => import("../cards/hui-statistics-graph-card"), - iframe: () => import("../cards/hui-iframe-card"), - map: () => import("../cards/hui-map-card"), - markdown: () => import("../cards/hui-markdown-card"), - picture: () => import("../cards/hui-picture-card"), - calendar: () => import("../cards/hui-calendar-card"), - logbook: () => import("../cards/hui-logbook-card"), + "vertical-stack": () => import("../cards/hui-vertical-stack-card"), }; // This will not return an error card but will throw the error From 96f103644a68bf034614629e97bb9024c10c8330 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 19 Nov 2021 22:22:49 +0100 Subject: [PATCH 027/112] Send error message to sender (#10660) --- cast/src/receiver/layout/hc-main.ts | 42 ++++++++++++++++++++++++++--- src/cast/sender_messages.ts | 16 +++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/cast/src/receiver/layout/hc-main.ts b/cast/src/receiver/layout/hc-main.ts index f74d9089cb..935456a98f 100644 --- a/cast/src/receiver/layout/hc-main.ts +++ b/cast/src/receiver/layout/hc-main.ts @@ -13,7 +13,11 @@ import { ShowDemoMessage, ShowLovelaceViewMessage, } from "../../../../src/cast/receiver_messages"; -import { ReceiverStatusMessage } from "../../../../src/cast/sender_messages"; +import { + ReceiverErrorCode, + ReceiverErrorMessage, + ReceiverStatusMessage, +} from "../../../../src/cast/sender_messages"; import { atLeastVersion } from "../../../../src/common/config/version"; import { isNavigationClick } from "../../../../src/common/dom/is-navigation-click"; import { @@ -134,6 +138,26 @@ export class HcMain extends HassElement { } } + private _sendError( + error_code: number, + error_message: string, + senderId?: string + ) { + const error: ReceiverErrorMessage = { + type: "receiver_error", + error_code, + error_message, + }; + + if (senderId) { + this.sendMessage(senderId, error); + } else { + for (const sender of castContext.getSenders()) { + this.sendMessage(sender.id, error); + } + } + } + private _dialogClosed = () => { document.body.setAttribute("style", "overflow-y: auto !important"); }; @@ -156,14 +180,18 @@ export class HcMain extends HassElement { }), }); } catch (err: any) { - this._error = this._getErrorMessage(err); + const errorMessage = this._getErrorMessage(err); + this._error = errorMessage; + this._sendError(err, errorMessage); return; } let connection; try { connection = await createConnection({ auth }); } catch (err: any) { - this._error = this._getErrorMessage(err); + const errorMessage = this._getErrorMessage(err); + this._error = errorMessage; + this._sendError(err, errorMessage); return; } if (this.hass) { @@ -181,8 +209,10 @@ export class HcMain extends HassElement { if (!this.hass) { this._sendStatus(msg.senderId!); this._error = "Cannot show Lovelace because we're not connected."; + this._sendError(ReceiverErrorCode.NOT_CONNECTED, this._error); return; } + this._error = undefined; if (msg.urlPath === "lovelace") { msg.urlPath = null; } @@ -204,10 +234,14 @@ export class HcMain extends HassElement { this._handleNewLovelaceConfig(lovelaceConfig) ); } catch (err: any) { - if (err.code !== "config_not_found") { + if ( + atLeastVersion(this.hass.connection.haVersion, 0, 107) && + err.code !== "config_not_found" + ) { // eslint-disable-next-line console.log("Error fetching Lovelace configuration", err, msg); this._error = `Error fetching Lovelace configuration: ${err.message}`; + this._sendError(ReceiverErrorCode.FETCH_CONFIG_FAILED, this._error); return; } // Generate a Lovelace config. diff --git a/src/cast/sender_messages.ts b/src/cast/sender_messages.ts index e9a7f074a0..1a6eda58f0 100644 --- a/src/cast/sender_messages.ts +++ b/src/cast/sender_messages.ts @@ -11,4 +11,20 @@ export interface ReceiverStatusMessage extends BaseCastMessage { urlPath?: string | null; } +export interface ReceiverErrorMessage extends BaseCastMessage { + type: "receiver_error"; + error_code: ReceiverErrorCode; + error_message: string; +} + +export const enum ReceiverErrorCode { + CONNECTION_FAILED = 1, + AUTHENTICATION_FAILED = 2, + CONNECTION_LOST = 3, + HASS_URL_MISSING = 4, + NO_HTTPS = 5, + NOT_CONNECTED = 21, + FETCH_CONFIG_FAILED = 22, +} + export type SenderMessage = ReceiverStatusMessage; From 91dbfca899da296f6dfac300a4a8bce31147b511 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 21 Nov 2021 05:05:32 +0100 Subject: [PATCH 028/112] Add frequency device class for sensor (#10621) --- src/common/const.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/const.ts b/src/common/const.ts index d75aa3f3dc..fb5ee730f9 100644 --- a/src/common/const.ts +++ b/src/common/const.ts @@ -119,6 +119,7 @@ export const FIXED_DEVICE_CLASS_ICONS = { current: mdiCurrentAc, date: mdiCalendar, energy: mdiLightningBolt, + frequency: mdiSineWave, gas: mdiGasCylinder, humidity: mdiWaterPercent, illuminance: mdiBrightness5, From 1a5c43d72acd6ca6886e9f7cc6aff5d78501a7f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 21 Nov 2021 18:13:48 +0100 Subject: [PATCH 029/112] Fix color over slotted image in ha-alert (#10652) --- gallery/src/demos/demo-ha-alert.ts | 26 +++++++---- src/components/ha-alert.ts | 46 ++++++++++--------- .../config/dashboard/ha-config-updates.ts | 13 ++++-- 3 files changed, 50 insertions(+), 35 deletions(-) diff --git a/gallery/src/demos/demo-ha-alert.ts b/gallery/src/demos/demo-ha-alert.ts index 57b80e47ec..2e87b7beb7 100644 --- a/gallery/src/demos/demo-ha-alert.ts +++ b/gallery/src/demos/demo-ha-alert.ts @@ -87,7 +87,17 @@ const alerts: { title: "Slotted icon", description: "Alert with slotted icon", type: "warning", - iconSlot: html``, + iconSlot: html` + + `, + }, + { + title: "Slotted image", + description: "Alert with slotted image", + type: "warning", + iconSlot: html``, }, { title: "Slotted action", @@ -155,14 +165,14 @@ export class DemoHaAlert extends LitElement { align-items: center; justify-content: space-between; } - span { - margin-right: 16px; + .image { + display: inline-flex; + height: 100%; + align-items: center; } - ha-logo-svg { - width: 28px; - height: 28px; - padding-right: 8px; - place-self: center; + img { + max-height: 24px; + width: 24px; } mwc-button { --mdc-theme-primary: var(--primary-text-color); diff --git a/src/components/ha-alert.ts b/src/components/ha-alert.ts index 93df6b7e24..e4ba0e2752 100644 --- a/src/components/ha-alert.ts +++ b/src/components/ha-alert.ts @@ -47,11 +47,11 @@ class HaAlert extends LitElement { [this.alertType]: true, })}" > - -
+
+ -
- + +
${this.title ? html`
${this.title}
` : ""} @@ -87,7 +87,7 @@ class HaAlert extends LitElement { .issue-type.rtl { flex-direction: row-reverse; } - .issue-type::before { + .issue-type::after { position: absolute; top: 0; right: 0; @@ -98,18 +98,12 @@ class HaAlert extends LitElement { content: ""; border-radius: 4px; } - slot > .icon { - margin-right: 8px; - width: 24px; + .icon { + z-index: 1; } .icon.no-title { align-self: center; } - .issue-type.rtl > slot > .icon { - margin-right: 0px; - margin-left: 8px; - width: 24px; - } .issue-type.rtl > .content { flex-direction: row-reverse; text-align: right; @@ -126,39 +120,47 @@ class HaAlert extends LitElement { } .main-content { overflow-wrap: anywhere; + margin-left: 8px; + margin-right: 0; + } + .issue-type.rtl > .content > .main-content { + margin-left: 0; + margin-right: 8px; } .title { margin-top: 2px; font-weight: bold; } - ha-icon-button { + .action mwc-button, + .action ha-icon-button { + --mdc-theme-primary: var(--primary-text-color); --mdc-icon-button-size: 36px; } - .issue-type.info > slot > .icon { + .issue-type.info > .icon { color: var(--info-color); } - .issue-type.info::before { + .issue-type.info::after { background-color: var(--info-color); } - .issue-type.warning > slot > .icon { + .issue-type.warning > .icon { color: var(--warning-color); } - .issue-type.warning::before { + .issue-type.warning::after { background-color: var(--warning-color); } - .issue-type.error > slot > .icon { + .issue-type.error > .icon { color: var(--error-color); } - .issue-type.error::before { + .issue-type.error::after { background-color: var(--error-color); } - .issue-type.success > slot > .icon { + .issue-type.success > .icon { color: var(--success-color); } - .issue-type.success::before { + .issue-type.success::after { background-color: var(--success-color); } `; diff --git a/src/panels/config/dashboard/ha-config-updates.ts b/src/panels/config/dashboard/ha-config-updates.ts index 29d89f0be7..313e0e34f5 100644 --- a/src/panels/config/dashboard/ha-config-updates.ts +++ b/src/panels/config/dashboard/ha-config-updates.ts @@ -98,15 +98,18 @@ class HaConfigUpdates extends LitElement { color: var(--primary-text-color); } .icon { - place-self: center; + display: inline-flex; + height: 100%; + align-items: center; } img, ha-svg-icon, ha-logo-svg { - width: var(--mdc-icon-size, 32px); - height: var(--mdc-icon-size, 32px); - padding-right: 12px; - display: block; + --mdc-icon-size: 32px; + max-height: 32px; + width: 32px; + } + ha-logo-svg { color: var(--secondary-text-color); } `; From 6c4e987a2462a943b8f19a95a91419396e0d7d24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 21 Nov 2021 18:15:38 +0100 Subject: [PATCH 030/112] Make ha-chip-set slot-able (#10647) --- .../{demo-ha-chip.ts => demo-ha-chips.ts} | 31 +++++++++++++-- src/components/ha-chip-set.ts | 39 +++++-------------- src/components/ha-chip.ts | 11 +----- .../ha-device-automation-card.ts | 25 +++++++++--- .../ha-device-automation-dialog.ts | 2 +- 5 files changed, 58 insertions(+), 50 deletions(-) rename gallery/src/demos/{demo-ha-chip.ts => demo-ha-chips.ts} (59%) diff --git a/gallery/src/demos/demo-ha-chip.ts b/gallery/src/demos/demo-ha-chips.ts similarity index 59% rename from gallery/src/demos/demo-ha-chip.ts rename to gallery/src/demos/demo-ha-chips.ts index 8344d4eb64..d1dcc2ae7d 100644 --- a/gallery/src/demos/demo-ha-chip.ts +++ b/gallery/src/demos/demo-ha-chips.ts @@ -3,6 +3,7 @@ import { css, html, LitElement, TemplateResult } from "lit"; import { customElement } from "lit/decorators"; import "../../../src/components/ha-card"; import "../../../src/components/ha-chip"; +import "../../../src/components/ha-chip-set"; import "../../../src/components/ha-svg-icon"; const chips: { @@ -22,8 +23,8 @@ const chips: { }, ]; -@customElement("demo-ha-chip") -export class DemoHaChip extends LitElement { +@customElement("demo-ha-chips") +export class DemoHaChips extends LitElement { protected render(): TemplateResult { return html` @@ -41,6 +42,23 @@ export class DemoHaChip extends LitElement { )}
+ +
+ + ${chips.map( + (chip) => html` + + ${chip.icon + ? html` + ` + : ""} + ${chip.content} + + ` + )} + +
+
`; } @@ -50,12 +68,19 @@ export class DemoHaChip extends LitElement { max-width: 600px; margin: 24px auto; } + ha-chip { + margin-bottom: 4px; + } + .card-content { + display: flex; + flex-direction: column; + } `; } } declare global { interface HTMLElementTagNameMap { - "demo-ha-chip": DemoHaChip; + "demo-ha-chips": DemoHaChips; } } diff --git a/src/components/ha-chip-set.ts b/src/components/ha-chip-set.ts index 2b2bb6a9e9..2e659c8353 100644 --- a/src/components/ha-chip-set.ts +++ b/src/components/ha-chip-set.ts @@ -8,52 +8,31 @@ import { TemplateResult, unsafeCSS, } from "lit"; -import { customElement, property } from "lit/decorators"; -import { fireEvent } from "../common/dom/fire_event"; -import "./ha-chip"; - -declare global { - // for fire event - interface HASSDomEvents { - "chip-clicked": { index: string }; - } -} +import { customElement } from "lit/decorators"; @customElement("ha-chip-set") export class HaChipSet extends LitElement { - @property() public items = []; - protected render(): TemplateResult { - if (this.items.length === 0) { - return html``; - } return html`
- ${this.items.map( - (item, idx) => - html` - - ${item} - - ` - )} +
`; } - private _handleClick(ev): void { - fireEvent(this, "chip-clicked", { - index: ev.currentTarget.index, - }); - } - static get styles(): CSSResultGroup { return css` ${unsafeCSS(chipStyles)} - ha-chip { + 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; + } `; } } diff --git a/src/components/ha-chip.ts b/src/components/ha-chip.ts index bbb934b981..9f58e3c53c 100644 --- a/src/components/ha-chip.ts +++ b/src/components/ha-chip.ts @@ -10,22 +10,13 @@ import { } from "lit"; import { customElement, property } from "lit/decorators"; -declare global { - // for fire event - interface HASSDomEvents { - "chip-clicked": { index: string }; - } -} - @customElement("ha-chip") export class HaChip extends LitElement { - @property() public index = 0; - @property({ type: Boolean }) public hasIcon = false; protected render(): TemplateResult { return html` -
+
${this.hasIcon ? html`
diff --git a/src/panels/config/devices/device-detail/ha-device-automation-card.ts b/src/panels/config/devices/device-detail/ha-device-automation-card.ts index 741c426f3d..47577805c3 100644 --- a/src/panels/config/devices/device-detail/ha-device-automation-card.ts +++ b/src/panels/config/devices/device-detail/ha-device-automation-card.ts @@ -1,6 +1,8 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { property } from "lit/decorators"; +import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/ha-card"; +import "../../../../components/ha-chip"; import "../../../../components/ha-chip-set"; import { showAutomationEditor } from "../../../../data/automation"; import { @@ -10,6 +12,12 @@ import { import { showScriptEditor } from "../../../../data/script"; import { HomeAssistant } from "../../../../types"; +declare global { + interface HASSDomEvents { + "entry-selected": undefined; + } +} + export abstract class HaDeviceAutomationCard< T extends DeviceAutomation > extends LitElement { @@ -55,29 +63,34 @@ export abstract class HaDeviceAutomationCard< return html`

${this.hass.localize(this.headerKey)}

- - this._localizeDeviceAutomation(this.hass, automation) + + ${this.automations.map( + (automation, idx) => + html` + + ${this._localizeDeviceAutomation(this.hass, automation)} + + ` )} - >
`; } private _handleAutomationClicked(ev: CustomEvent) { - const automation = this.automations[ev.detail.index]; + const automation = this.automations[(ev.currentTarget as any).index]; if (!automation) { return; } if (this.script) { showScriptEditor({ sequence: [automation as DeviceAction] }); + fireEvent(this, "entry-selected"); return; } const data = {}; data[this.type] = [automation]; showAutomationEditor(data); + fireEvent(this, "entry-selected"); } static get styles(): CSSResultGroup { diff --git a/src/panels/config/devices/device-detail/ha-device-automation-dialog.ts b/src/panels/config/devices/device-detail/ha-device-automation-dialog.ts index eb18201a25..b42d2f069b 100644 --- a/src/panels/config/devices/device-detail/ha-device-automation-dialog.ts +++ b/src/panels/config/devices/device-detail/ha-device-automation-dialog.ts @@ -91,7 +91,7 @@ export class DialogDeviceAutomation extends LitElement { }.create` )} > -
+
${this._triggers.length || this._conditions.length || this._actions.length From 6335b13c5e61459d009b6dc7630535b7dfb38d39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 21 Nov 2021 18:16:06 +0100 Subject: [PATCH 031/112] Remove core note on update page (#10661) --- .../update-available/update-available-dashboard.ts | 14 -------------- src/translations/en.json | 1 - 2 files changed, 15 deletions(-) diff --git a/hassio/src/update-available/update-available-dashboard.ts b/hassio/src/update-available/update-available-dashboard.ts index 50aefd54da..fd7c9865ee 100644 --- a/hassio/src/update-available/update-available-dashboard.ts +++ b/hassio/src/update-available/update-available-dashboard.ts @@ -155,20 +155,6 @@ class UpdateAvailableDashboard extends LitElement { } )}

- ${this._updateEntry === "core" - ? html` - - ${this.supervisor.localize( - "update_available.core_note", - { - version: - this._addonInfo?.version || - this.supervisor[this._updateEntry]?.version, - } - )} - - ` - : ""}
${!["os", "supervisor"].includes(this._updateEntry) ? html` diff --git a/src/translations/en.json b/src/translations/en.json index 744ef87c5b..c3e6b2ddf1 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4181,7 +4181,6 @@ "open_release_notes": "Open release notes", "create_backup": "Create backup before updating", "description": "There is an update available for the {name}. You have {version} installed. Click update to update to version {newest_version}", - "core_note": "The supervisor will roll back to version {version} if your instance does not come up after the update.", "updating": "Updating {name} to version {version}", "creating_backup": "Creating backup of {name}" }, From a430142296a16f1020dcecaeb5f0d2b589a395f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 21 Nov 2021 18:52:58 +0100 Subject: [PATCH 032/112] Add iconColor to ha-config-navigation entries (#10658) --- src/layouts/hass-tabs-subpage.ts | 2 ++ .../config/dashboard/ha-config-dashboard.ts | 1 + .../config/dashboard/ha-config-navigation.ts | 23 +++++++++++++++---- src/panels/config/ha-panel-config.ts | 19 +++++++++++++++ 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/layouts/hass-tabs-subpage.ts b/src/layouts/hass-tabs-subpage.ts index 562374fd71..d325e2d044 100644 --- a/src/layouts/hass-tabs-subpage.ts +++ b/src/layouts/hass-tabs-subpage.ts @@ -28,6 +28,8 @@ export interface PageNavigation { core?: boolean; advancedOnly?: boolean; iconPath?: string; + description?: string; + iconColor?: string; info?: any; } diff --git a/src/panels/config/dashboard/ha-config-dashboard.ts b/src/panels/config/dashboard/ha-config-dashboard.ts index 2fb5e219aa..5f0138dc48 100644 --- a/src/panels/config/dashboard/ha-config-dashboard.ts +++ b/src/panels/config/dashboard/ha-config-dashboard.ts @@ -59,6 +59,7 @@ class HaConfigDashboard extends LitElement { name: "Home Assistant Cloud", info: this.cloudStatus, iconPath: mdiCloudLock, + iconColor: "#174B62", }, ]} > diff --git a/src/panels/config/dashboard/ha-config-navigation.ts b/src/panels/config/dashboard/ha-config-navigation.ts index ee08290b06..0e71eeebd0 100644 --- a/src/panels/config/dashboard/ha-config-navigation.ts +++ b/src/panels/config/dashboard/ha-config-navigation.ts @@ -24,10 +24,13 @@ class HaConfigNavigation extends LitElement { ? html`
- + .style="background-color: ${page.iconColor || "undefined"}" + > + +
${page.name || this.hass.localize( @@ -54,7 +57,8 @@ class HaConfigNavigation extends LitElement { ` : html`
- ${this.hass.localize( + ${page.description || + this.hass.localize( `ui.panel.config.${page.component}.description` )}
@@ -81,6 +85,11 @@ class HaConfigNavigation extends LitElement { ha-svg-icon, ha-icon-next { color: var(--secondary-text-color); + height: 24px; + width: 24px; + } + ha-svg-icon { + padding: 8px; } .iron-selected paper-item::before, a:not(.iron-selected):focus::before { @@ -102,6 +111,12 @@ class HaConfigNavigation extends LitElement { .iron-selected:focus paper-item::before { opacity: 0.2; } + .icon-background { + border-radius: 50%; + } + .icon-background ha-svg-icon { + color: var(--card-background-color); + } `; } } diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index 54ef2ac080..1a7b21f334 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -46,6 +46,7 @@ export const configSections: { [name: string]: PageNavigation[] } = { path: "/config/integrations", translationKey: "ui.panel.config.integrations.caption", iconPath: mdiPuzzle, + iconColor: "#004E98", core: true, }, { @@ -53,6 +54,7 @@ export const configSections: { [name: string]: PageNavigation[] } = { path: "/config/devices", translationKey: "ui.panel.config.devices.caption", iconPath: mdiDevices, + iconColor: "#004E98", core: true, }, { @@ -60,6 +62,7 @@ export const configSections: { [name: string]: PageNavigation[] } = { path: "/config/entities", translationKey: "ui.panel.config.entities.caption", iconPath: mdiShape, + iconColor: "#004E98", core: true, }, { @@ -67,6 +70,7 @@ export const configSections: { [name: string]: PageNavigation[] } = { path: "/config/areas", translationKey: "ui.panel.config.areas.caption", iconPath: mdiSofa, + iconColor: "#004E98", core: true, }, ], @@ -76,24 +80,28 @@ export const configSections: { [name: string]: PageNavigation[] } = { path: "/config/blueprint", translationKey: "ui.panel.config.blueprint.caption", iconPath: mdiPaletteSwatch, + iconColor: "#2A850E", }, { component: "automation", path: "/config/automation", translationKey: "ui.panel.config.automation.caption", iconPath: mdiRobot, + iconColor: "#2A850E", }, { component: "scene", path: "/config/scene", translationKey: "ui.panel.config.scene.caption", iconPath: mdiPalette, + iconColor: "#2A850E", }, { component: "script", path: "/config/script", translationKey: "ui.panel.config.script.caption", iconPath: mdiScriptText, + iconColor: "#2A850E", }, ], helpers: [ @@ -102,6 +110,7 @@ export const configSections: { [name: string]: PageNavigation[] } = { path: "/config/helpers", translationKey: "ui.panel.config.helpers.caption", iconPath: mdiTools, + iconColor: "#7209EB", core: true, }, ], @@ -111,12 +120,14 @@ export const configSections: { [name: string]: PageNavigation[] } = { path: "/config/tags", translationKey: "ui.panel.config.tag.caption", iconPath: mdiNfcVariant, + iconColor: "#DF8E44", }, { component: "energy", path: "/config/energy", translationKey: "ui.panel.config.energy.caption", iconPath: mdiLightningBolt, + iconColor: "#578681", }, ], lovelace: [ @@ -125,6 +136,7 @@ export const configSections: { [name: string]: PageNavigation[] } = { path: "/config/lovelace/dashboards", translationKey: "ui.panel.config.lovelace.caption", iconPath: mdiViewDashboard, + iconColor: "#D81159", }, ], persons: [ @@ -133,18 +145,21 @@ export const configSections: { [name: string]: PageNavigation[] } = { path: "/config/person", translationKey: "ui.panel.config.person.caption", iconPath: mdiAccount, + iconColor: "#0A4BF0", }, { component: "zone", path: "/config/zone", translationKey: "ui.panel.config.zone.caption", iconPath: mdiMapMarkerRadius, + iconColor: "#0A4BF0", }, { component: "users", path: "/config/users", translationKey: "ui.panel.config.users.caption", iconPath: mdiBadgeAccountHorizontal, + iconColor: "#0A4BF0", core: true, advancedOnly: true, }, @@ -155,6 +170,7 @@ export const configSections: { [name: string]: PageNavigation[] } = { path: "/config/core", translationKey: "ui.panel.config.core.caption", iconPath: mdiHomeAssistant, + iconColor: "#8F2D56", core: true, }, { @@ -162,6 +178,7 @@ export const configSections: { [name: string]: PageNavigation[] } = { path: "/config/server_control", translationKey: "ui.panel.config.server_control.caption", iconPath: mdiServer, + iconColor: "#8F2D56", core: true, }, { @@ -169,6 +186,7 @@ export const configSections: { [name: string]: PageNavigation[] } = { path: "/config/logs", translationKey: "ui.panel.config.logs.caption", iconPath: mdiMathLog, + iconColor: "#8F2D56", core: true, }, { @@ -176,6 +194,7 @@ export const configSections: { [name: string]: PageNavigation[] } = { path: "/config/info", translationKey: "ui.panel.config.info.caption", iconPath: mdiInformation, + iconColor: "#8F2D56", core: true, }, ], From 2e81f843ce146b43587cdf9baf922b437cd8bad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 21 Nov 2021 19:07:55 +0100 Subject: [PATCH 033/112] Use white for icons with backgound (#10672) --- src/panels/config/dashboard/ha-config-navigation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panels/config/dashboard/ha-config-navigation.ts b/src/panels/config/dashboard/ha-config-navigation.ts index 0e71eeebd0..a46422fdca 100644 --- a/src/panels/config/dashboard/ha-config-navigation.ts +++ b/src/panels/config/dashboard/ha-config-navigation.ts @@ -115,7 +115,7 @@ class HaConfigNavigation extends LitElement { border-radius: 50%; } .icon-background ha-svg-icon { - color: var(--card-background-color); + color: #fff; } `; } From 3bcf2253806954c2f3200fb14df85aa84600643b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 21 Nov 2021 20:16:19 +0100 Subject: [PATCH 034/112] Fix color overlay in ha-alert content (#10674) --- src/components/ha-alert.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/ha-alert.ts b/src/components/ha-alert.ts index e4ba0e2752..a1b5fa0a32 100644 --- a/src/components/ha-alert.ts +++ b/src/components/ha-alert.ts @@ -113,6 +113,7 @@ class HaAlert extends LitElement { justify-content: space-between; align-items: center; width: 100%; + z-index: 1; } .action { width: min-content; From 45efee28b836b1bfaf2b540cf7764eeb0548f90b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 21 Nov 2021 20:59:56 -0800 Subject: [PATCH 035/112] Add scenes and scripts as buttons in footer of area cards (#10673) * Add scenes and scripts as chips in footer of area cards * Remove unused chips config type * Update src/panels/lovelace/common/generate-lovelace-config.ts Co-authored-by: Zack Barett * Fix typing Co-authored-by: Zack Barett --- .../common/generate-lovelace-config.ts | 22 ++++++++++++++++--- src/panels/lovelace/header-footer/types.ts | 7 ++++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/panels/lovelace/common/generate-lovelace-config.ts b/src/panels/lovelace/common/generate-lovelace-config.ts index bd8dce5416..59b5a6b6fa 100644 --- a/src/panels/lovelace/common/generate-lovelace-config.ts +++ b/src/panels/lovelace/common/generate-lovelace-config.ts @@ -25,6 +25,7 @@ import { ThermostatCardConfig, } from "../cards/types"; import { LovelaceRowConfig } from "../entity-rows/types"; +import { ButtonsHeaderFooterConfig } from "../header-footer/types"; const HIDE_DOMAIN = new Set([ "automation", @@ -97,6 +98,8 @@ export const computeCards = ( ? `${entityCardOptions.title} `.toLowerCase() : undefined; + const footerEntities: ButtonsHeaderFooterConfig["entities"] = []; + for (const [entityId, stateObj] of states) { const domain = computeDomain(entityId); @@ -143,6 +146,12 @@ export const computeCards = ( show_forecast: false, }; cards.push(cardConfig); + } else if (domain === "scene" || domain === "script") { + footerEntities.push({ + entity: entityId, + show_icon: true, + show_name: true, + }); } else if ( domain === "sensor" && stateObj?.attributes.device_class === SENSOR_DEVICE_CLASS_BATTERY @@ -168,12 +177,19 @@ export const computeCards = ( } } - if (entities.length > 0) { - cards.unshift({ + if (entities.length > 0 || footerEntities.length > 0) { + const card: EntitiesCardConfig = { type: "entities", entities, ...entityCardOptions, - }); + }; + if (footerEntities.length > 0) { + card.footer = { + type: "buttons", + entities: footerEntities, + } as ButtonsHeaderFooterConfig; + } + cards.unshift(card); } if (cards.length < 2) { diff --git a/src/panels/lovelace/header-footer/types.ts b/src/panels/lovelace/header-footer/types.ts index ddbe7c79c8..a13cfc2401 100644 --- a/src/panels/lovelace/header-footer/types.ts +++ b/src/panels/lovelace/header-footer/types.ts @@ -1,15 +1,17 @@ import { ActionConfig } from "../../../data/lovelace"; -import { EntityConfig } from "../entity-rows/types"; +import { EntitiesCardEntityConfig } from "../cards/types"; export interface LovelaceHeaderFooterConfig { type: string; } export interface ButtonsHeaderFooterConfig extends LovelaceHeaderFooterConfig { - entities: Array; + type: "buttons"; + entities: Array; } export interface GraphHeaderFooterConfig extends LovelaceHeaderFooterConfig { + type: "graph"; entity: string; detail?: number; hours_to_show?: number; @@ -20,6 +22,7 @@ export interface GraphHeaderFooterConfig extends LovelaceHeaderFooterConfig { } export interface PictureHeaderFooterConfig extends LovelaceHeaderFooterConfig { + type: "picture"; image: string; tap_action?: ActionConfig; hold_action?: ActionConfig; From 4719636176a9dc944081037e6065518e97458950 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 22 Nov 2021 06:01:51 +0100 Subject: [PATCH 036/112] Fix dark main-content and split gallery demo (#10675) --- gallery/src/demos/demo-ha-alert.ts | 71 ++++++++++++++++++++++-------- src/components/ha-alert.ts | 2 +- 2 files changed, 53 insertions(+), 20 deletions(-) diff --git a/gallery/src/demos/demo-ha-alert.ts b/gallery/src/demos/demo-ha-alert.ts index 2e87b7beb7..91286a1045 100644 --- a/gallery/src/demos/demo-ha-alert.ts +++ b/gallery/src/demos/demo-ha-alert.ts @@ -1,8 +1,10 @@ -import "../../../src/components/ha-logo-svg"; -import { html, css, LitElement, TemplateResult } from "lit"; +import "@material/mwc-button/mwc-button"; +import { css, html, LitElement, TemplateResult } from "lit"; import { customElement } from "lit/decorators"; +import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element"; import "../../../src/components/ha-alert"; import "../../../src/components/ha-card"; +import "../../../src/components/ha-logo-svg"; const alerts: { title?: string; @@ -130,29 +132,60 @@ const alerts: { export class DemoHaAlert extends LitElement { protected render(): TemplateResult { return html` - -
- ${alerts.map( - (alert) => html` - - ${alert.iconSlot} ${alert.description} ${alert.actionSlot} - - ` - )} -
-
+ ${["light", "dark"].map( + (mode) => html` +
+ +
+ ${alerts.map( + (alert) => html` + + ${alert.iconSlot} ${alert.description} ${alert.actionSlot} + + ` + )} +
+
+
+ ` + )} `; } + firstUpdated(changedProps) { + super.firstUpdated(changedProps); + applyThemesOnElement( + this.shadowRoot!.querySelector(".dark"), + { + default_theme: "default", + default_dark_theme: "default", + themes: {}, + darkMode: false, + }, + "default", + { dark: true } + ); + } + static get styles() { return css` + :host { + display: flex; + flex-direction: row; + justify-content: space-between; + } + .dark, + .light { + display: block; + background-color: var(--primary-background-color); + padding: 0 50px; + } ha-card { - max-width: 600px; margin: 24px auto; } ha-alert { diff --git a/src/components/ha-alert.ts b/src/components/ha-alert.ts index a1b5fa0a32..90c31c3cc6 100644 --- a/src/components/ha-alert.ts +++ b/src/components/ha-alert.ts @@ -113,9 +113,9 @@ class HaAlert extends LitElement { justify-content: space-between; align-items: center; width: 100%; - z-index: 1; } .action { + z-index: 1; width: min-content; --mdc-theme-primary: var(--primary-text-color); } From 3c67fc96b1b2ebb8c3b69294d1471b462e1a5f55 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 22 Nov 2021 10:56:40 +0100 Subject: [PATCH 037/112] Make "Show more" show everything starting from yesterday (#10533) --- src/dialogs/more-info/ha-more-info-history.ts | 11 ++++++++--- src/dialogs/more-info/ha-more-info-logbook.ts | 11 ++++++++--- src/panels/history/ha-panel-history.ts | 5 +++++ src/panels/logbook/ha-panel-logbook.ts | 5 +++++ 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/dialogs/more-info/ha-more-info-history.ts b/src/dialogs/more-info/ha-more-info-history.ts index b00a4ba695..9cbab7e1ca 100644 --- a/src/dialogs/more-info/ha-more-info-history.ts +++ b/src/dialogs/more-info/ha-more-info-history.ts @@ -1,3 +1,4 @@ +import { startOfYesterday } from "date-fns"; import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; @@ -22,6 +23,8 @@ export class MoreInfoHistory extends LitElement { @state() private _stateHistory?: HistoryResult; + private _showMoreHref = ""; + private _throttleGetStateHistory = throttle(() => { this._getStateHistory(); }, 10000); @@ -31,14 +34,12 @@ export class MoreInfoHistory extends LitElement { return html``; } - const href = "/history?entity_id=" + this.entityId; - return html`${isComponentLoaded(this.hass, "history") ? html`
${this.hass.localize("ui.dialogs.more_info_control.history")}
-
${this.hass.localize( "ui.dialogs.more_info_control.show_more" )} { this._getLogBookData(); }, 10000); @@ -44,8 +47,6 @@ export class MoreInfoLogbook extends LitElement { return html``; } - const href = "/logbook?entity_id=" + this.entityId; - return html` ${isComponentLoaded(this.hass, "logbook") ? this._error @@ -67,7 +68,7 @@ export class MoreInfoLogbook extends LitElement {
${this.hass.localize("ui.dialogs.more_info_control.logbook")}
- ${this.hass.localize( "ui.dialogs.more_info_control.show_more" )}) { From d28ad1713591f49d089b6599457fee42d2a2e8f3 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 22 Nov 2021 11:12:04 +0100 Subject: [PATCH 038/112] Use component to ensure relative-time in Glance card gets updated (#10666) --- src/panels/lovelace/cards/hui-glance-card.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/panels/lovelace/cards/hui-glance-card.ts b/src/panels/lovelace/cards/hui-glance-card.ts index 445bd5ff7a..b640284bfd 100644 --- a/src/panels/lovelace/cards/hui-glance-card.ts +++ b/src/panels/lovelace/cards/hui-glance-card.ts @@ -9,7 +9,6 @@ import { import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { ifDefined } from "lit/directives/if-defined"; -import { relativeTime } from "../../../common/datetime/relative_time"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { computeDomain } from "../../../common/entity/compute_domain"; import { computeStateDisplay } from "../../../common/entity/compute_state_display"; @@ -17,6 +16,7 @@ import { computeStateName } from "../../../common/entity/compute_state_name"; import "../../../components/entity/state-badge"; import "../../../components/ha-card"; import "../../../components/ha-icon"; +import "../../../components/ha-relative-time"; import { UNAVAILABLE_STATES } from "../../../data/entity"; import { ActionHandlerEvent, @@ -325,10 +325,13 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard { > ` : entityConf.show_last_changed - ? relativeTime( - new Date(stateObj.last_changed), - this.hass!.locale - ) + ? html` + + ` : computeStateDisplay( this.hass!.localize, stateObj, From 3d99b92c074c83846914cae3f5fbf5ee5d7085e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 22 Nov 2021 17:59:28 +0100 Subject: [PATCH 039/112] Limit setting up supervisor subscriptions to the supervisor panel (#10680) --- hassio/src/hassio-main.ts | 4 +--- hassio/src/supervisor-base-element.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/hassio/src/hassio-main.ts b/hassio/src/hassio-main.ts index 8ded32bbf0..b64e505d37 100644 --- a/hassio/src/hassio-main.ts +++ b/hassio/src/hassio-main.ts @@ -10,7 +10,7 @@ import { HassioPanelInfo } from "../../src/data/hassio/supervisor"; import { Supervisor } from "../../src/data/supervisor/supervisor"; import { makeDialogManager } from "../../src/dialogs/make-dialog-manager"; import "../../src/layouts/hass-loading-screen"; -import { HomeAssistant, Route } from "../../src/types"; +import { HomeAssistant } from "../../src/types"; import "./hassio-router"; import { SupervisorBaseElement } from "./supervisor-base-element"; @@ -24,8 +24,6 @@ export class HassioMain extends SupervisorBaseElement { @property({ type: Boolean }) public narrow!: boolean; - @property({ attribute: false }) public route?: Route; - protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); diff --git a/hassio/src/supervisor-base-element.ts b/hassio/src/supervisor-base-element.ts index 7bd0fbb8ca..a5d3fbb7f6 100644 --- a/hassio/src/supervisor-base-element.ts +++ b/hassio/src/supervisor-base-element.ts @@ -25,7 +25,7 @@ import { } from "../../src/data/supervisor/supervisor"; import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin"; import { urlSyncMixin } from "../../src/state/url-sync-mixin"; -import { HomeAssistant } from "../../src/types"; +import { HomeAssistant, Route } from "../../src/types"; import { getTranslation } from "../../src/util/common-translation"; declare global { @@ -38,6 +38,8 @@ declare global { export class SupervisorBaseElement extends urlSyncMixin( ProvideHassLitMixin(LitElement) ) { + @property({ attribute: false }) public route?: Route; + @property({ attribute: false }) public supervisor: Partial = { localize: () => "", }; @@ -108,7 +110,9 @@ export class SupervisorBaseElement extends urlSyncMixin( this._language = this.hass.language; } this._initializeLocalize(); - this._initSupervisor(); + if (this.route?.prefix === "/hassio") { + this._initSupervisor(); + } } private async _initializeLocalize() { From a991640f52292a4295eb4398de6abe8ae89569f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 22 Nov 2021 18:09:23 +0100 Subject: [PATCH 040/112] Remove first part of the update description (#10669) --- hassio/src/update-available/update-available-dashboard.ts | 1 - src/translations/en.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/hassio/src/update-available/update-available-dashboard.ts b/hassio/src/update-available/update-available-dashboard.ts index fd7c9865ee..9ca85400de 100644 --- a/hassio/src/update-available/update-available-dashboard.ts +++ b/hassio/src/update-available/update-available-dashboard.ts @@ -145,7 +145,6 @@ class UpdateAvailableDashboard extends LitElement { ${this.supervisor.localize( "update_available.description", { - name, version: this._addonInfo?.version || this.supervisor[this._updateEntry]?.version, diff --git a/src/translations/en.json b/src/translations/en.json index c3e6b2ddf1..93fa75033b 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4180,7 +4180,7 @@ "update_name": "Update {name}", "open_release_notes": "Open release notes", "create_backup": "Create backup before updating", - "description": "There is an update available for the {name}. You have {version} installed. Click update to update to version {newest_version}", + "description": "You have {version} installed. Click update to update to version {newest_version}", "updating": "Updating {name} to version {version}", "creating_backup": "Creating backup of {name}" }, From c95a54c6f3f051f9ae036936ac0ea9f8153238fe Mon Sep 17 00:00:00 2001 From: Laszlo Magyar Date: Mon, 22 Nov 2021 18:59:35 +0100 Subject: [PATCH 041/112] Fixing typo in #10626 (#10686) --- hassio/src/update-available/update-available-dashboard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/src/update-available/update-available-dashboard.ts b/hassio/src/update-available/update-available-dashboard.ts index 9ca85400de..ccebe007cd 100644 --- a/hassio/src/update-available/update-available-dashboard.ts +++ b/hassio/src/update-available/update-available-dashboard.ts @@ -276,7 +276,7 @@ class UpdateAvailableDashboard extends LitElement { ); } else { this._error = this.supervisor.localize( - "addon.dashboard.not_available_arch", + "addon.dashboard.not_available_version", { core_version_installed: this.supervisor.core.version, core_version_needed: addonStoreInfo.homeassistant, From 8533b909571cbb37b5ff9169c5a4548fedab4dee Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 22 Nov 2021 17:28:13 -0800 Subject: [PATCH 042/112] Bumped version to 20211123.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 615db03d55..d2623c8494 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20211117.0", + version="20211123.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/frontend", author="The Home Assistant Authors", From f833701e7cb0ff420b3c7be0b71c99cd489e2187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 23 Nov 2021 14:36:11 +0100 Subject: [PATCH 043/112] Update background colors of navigation icons (#10691) --- .../config/dashboard/ha-config-dashboard.ts | 2 +- src/panels/config/ha-panel-config.ts | 38 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/panels/config/dashboard/ha-config-dashboard.ts b/src/panels/config/dashboard/ha-config-dashboard.ts index 5f0138dc48..27bdca32a7 100644 --- a/src/panels/config/dashboard/ha-config-dashboard.ts +++ b/src/panels/config/dashboard/ha-config-dashboard.ts @@ -59,7 +59,7 @@ class HaConfigDashboard extends LitElement { name: "Home Assistant Cloud", info: this.cloudStatus, iconPath: mdiCloudLock, - iconColor: "#174B62", + iconColor: "#3B808E", }, ]} > diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index 1a7b21f334..043f9d84ec 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -46,7 +46,7 @@ export const configSections: { [name: string]: PageNavigation[] } = { path: "/config/integrations", translationKey: "ui.panel.config.integrations.caption", iconPath: mdiPuzzle, - iconColor: "#004E98", + iconColor: "#2D338F", core: true, }, { @@ -54,7 +54,7 @@ export const configSections: { [name: string]: PageNavigation[] } = { path: "/config/devices", translationKey: "ui.panel.config.devices.caption", iconPath: mdiDevices, - iconColor: "#004E98", + iconColor: "#2D338F", core: true, }, { @@ -62,7 +62,7 @@ export const configSections: { [name: string]: PageNavigation[] } = { path: "/config/entities", translationKey: "ui.panel.config.entities.caption", iconPath: mdiShape, - iconColor: "#004E98", + iconColor: "#2D338F", core: true, }, { @@ -70,7 +70,7 @@ export const configSections: { [name: string]: PageNavigation[] } = { path: "/config/areas", translationKey: "ui.panel.config.areas.caption", iconPath: mdiSofa, - iconColor: "#004E98", + iconColor: "#2D338F", core: true, }, ], @@ -80,28 +80,28 @@ export const configSections: { [name: string]: PageNavigation[] } = { path: "/config/blueprint", translationKey: "ui.panel.config.blueprint.caption", iconPath: mdiPaletteSwatch, - iconColor: "#2A850E", + iconColor: "#518C43", }, { component: "automation", path: "/config/automation", translationKey: "ui.panel.config.automation.caption", iconPath: mdiRobot, - iconColor: "#2A850E", + iconColor: "#518C43", }, { component: "scene", path: "/config/scene", translationKey: "ui.panel.config.scene.caption", iconPath: mdiPalette, - iconColor: "#2A850E", + iconColor: "#518C43", }, { component: "script", path: "/config/script", translationKey: "ui.panel.config.script.caption", iconPath: mdiScriptText, - iconColor: "#2A850E", + iconColor: "#518C43", }, ], helpers: [ @@ -110,7 +110,7 @@ export const configSections: { [name: string]: PageNavigation[] } = { path: "/config/helpers", translationKey: "ui.panel.config.helpers.caption", iconPath: mdiTools, - iconColor: "#7209EB", + iconColor: "#4D2EA4", core: true, }, ], @@ -120,14 +120,14 @@ export const configSections: { [name: string]: PageNavigation[] } = { path: "/config/tags", translationKey: "ui.panel.config.tag.caption", iconPath: mdiNfcVariant, - iconColor: "#DF8E44", + iconColor: "#616161", }, { component: "energy", path: "/config/energy", translationKey: "ui.panel.config.energy.caption", iconPath: mdiLightningBolt, - iconColor: "#578681", + iconColor: "#F1C447", }, ], lovelace: [ @@ -136,7 +136,7 @@ export const configSections: { [name: string]: PageNavigation[] } = { path: "/config/lovelace/dashboards", translationKey: "ui.panel.config.lovelace.caption", iconPath: mdiViewDashboard, - iconColor: "#D81159", + iconColor: "#B1345C", }, ], persons: [ @@ -145,21 +145,21 @@ export const configSections: { [name: string]: PageNavigation[] } = { path: "/config/person", translationKey: "ui.panel.config.person.caption", iconPath: mdiAccount, - iconColor: "#0A4BF0", + iconColor: "#E48629", }, { component: "zone", path: "/config/zone", translationKey: "ui.panel.config.zone.caption", iconPath: mdiMapMarkerRadius, - iconColor: "#0A4BF0", + iconColor: "#E48629", }, { component: "users", path: "/config/users", translationKey: "ui.panel.config.users.caption", iconPath: mdiBadgeAccountHorizontal, - iconColor: "#0A4BF0", + iconColor: "#E48629", core: true, advancedOnly: true, }, @@ -170,7 +170,7 @@ export const configSections: { [name: string]: PageNavigation[] } = { path: "/config/core", translationKey: "ui.panel.config.core.caption", iconPath: mdiHomeAssistant, - iconColor: "#8F2D56", + iconColor: "#4A5963", core: true, }, { @@ -178,7 +178,7 @@ export const configSections: { [name: string]: PageNavigation[] } = { path: "/config/server_control", translationKey: "ui.panel.config.server_control.caption", iconPath: mdiServer, - iconColor: "#8F2D56", + iconColor: "#4A5963", core: true, }, { @@ -186,7 +186,7 @@ export const configSections: { [name: string]: PageNavigation[] } = { path: "/config/logs", translationKey: "ui.panel.config.logs.caption", iconPath: mdiMathLog, - iconColor: "#8F2D56", + iconColor: "#4A5963", core: true, }, { @@ -194,7 +194,7 @@ export const configSections: { [name: string]: PageNavigation[] } = { path: "/config/info", translationKey: "ui.panel.config.info.caption", iconPath: mdiInformation, - iconColor: "#8F2D56", + iconColor: "#4A5963", core: true, }, ], From ed291b57d09de8d17633d15f794b12c87d70c12c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 23 Nov 2021 17:18:40 +0100 Subject: [PATCH 044/112] Render update card on add-on page (#10681) --- .../addon-view/info/hassio-addon-info-tab.ts | 5 +- .../src/addon-view/info/hassio-addon-info.ts | 32 +- .../update-available/update-available-card.ts | 398 ++++++++++++++++++ .../update-available-dashboard.ts | 353 +--------------- 4 files changed, 434 insertions(+), 354 deletions(-) create mode 100644 hassio/src/update-available/update-available-card.ts diff --git a/hassio/src/addon-view/info/hassio-addon-info-tab.ts b/hassio/src/addon-view/info/hassio-addon-info-tab.ts index 6b31e546b7..eee42577a9 100644 --- a/hassio/src/addon-view/info/hassio-addon-info-tab.ts +++ b/hassio/src/addon-view/info/hassio-addon-info-tab.ts @@ -4,7 +4,7 @@ import "../../../../src/components/ha-circular-progress"; import { HassioAddonDetails } from "../../../../src/data/hassio/addon"; import { Supervisor } from "../../../../src/data/supervisor/supervisor"; import { haStyle } from "../../../../src/resources/styles"; -import { HomeAssistant } from "../../../../src/types"; +import { HomeAssistant, Route } from "../../../../src/types"; import { hassioStyle } from "../../resources/hassio-style"; import "./hassio-addon-info"; @@ -12,6 +12,8 @@ import "./hassio-addon-info"; class HassioAddonInfoDashboard extends LitElement { @property({ type: Boolean }) public narrow!: boolean; + @property({ attribute: false }) public route!: Route; + @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public supervisor!: Supervisor; @@ -27,6 +29,7 @@ class HassioAddonInfoDashboard extends LitElement {
- ${this.supervisor.localize( - "addon.dashboard.new_update_available", - { name: this.addon.name, version: this.addon.version_latest } - )} - - - - - + ` : ""} ${!this.addon.protected @@ -1171,6 +1163,10 @@ class HassioAddonInfo extends LitElement { text-decoration: none; } + update-available-card { + padding-bottom: 16px; + } + @media (max-width: 720px) { ha-chip { line-height: 36px; diff --git a/hassio/src/update-available/update-available-card.ts b/hassio/src/update-available/update-available-card.ts new file mode 100644 index 0000000000..93d096e07f --- /dev/null +++ b/hassio/src/update-available/update-available-card.ts @@ -0,0 +1,398 @@ +import "@material/mwc-list/mwc-list-item"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../../src/common/dom/fire_event"; +import "../../../src/common/search/search-input"; +import "../../../src/components/buttons/ha-progress-button"; +import "../../../src/components/ha-alert"; +import "../../../src/components/ha-button-menu"; +import "../../../src/components/ha-card"; +import "../../../src/components/ha-checkbox"; +import "../../../src/components/ha-expansion-panel"; +import "../../../src/components/ha-formfield"; +import "../../../src/components/ha-icon-button"; +import "../../../src/components/ha-markdown"; +import "../../../src/components/ha-settings-row"; +import "../../../src/components/ha-svg-icon"; +import "../../../src/components/ha-switch"; +import { + fetchHassioAddonChangelog, + fetchHassioAddonInfo, + HassioAddonDetails, + updateHassioAddon, +} from "../../../src/data/hassio/addon"; +import { + createHassioPartialBackup, + HassioPartialBackupCreateParams, +} from "../../../src/data/hassio/backup"; +import { + extractApiErrorMessage, + ignoreSupervisorError, +} from "../../../src/data/hassio/common"; +import { updateOS } from "../../../src/data/hassio/host"; +import { updateSupervisor } from "../../../src/data/hassio/supervisor"; +import { updateCore } from "../../../src/data/supervisor/core"; +import { StoreAddon } from "../../../src/data/supervisor/store"; +import { Supervisor } from "../../../src/data/supervisor/supervisor"; +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 { documentationUrl } from "../../../src/util/documentation-url"; +import { addonArchIsSupported, extractChangelog } from "../util/addon"; + +declare global { + interface HASSDomEvents { + "update-complete": undefined; + } +} + +type updateType = "os" | "supervisor" | "core" | "addon"; + +const changelogUrl = ( + hass: HomeAssistant, + entry: updateType, + version: string +): string | undefined => { + if (entry === "addon") { + return undefined; + } + if (entry === "core") { + return version?.includes("dev") + ? "https://github.com/home-assistant/core/commits/dev" + : documentationUrl(hass, "/latest-release-notes/"); + } + if (entry === "os") { + return version?.includes("dev") + ? "https://github.com/home-assistant/operating-system/commits/dev" + : `https://github.com/home-assistant/operating-system/releases/tag/${version}`; + } + if (entry === "supervisor") { + return version?.includes("dev") + ? "https://github.com/home-assistant/supervisor/commits/main" + : `https://github.com/home-assistant/supervisor/releases/tag/${version}`; + } + return undefined; +}; + +@customElement("update-available-card") +class UpdateAvailableCard extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public supervisor!: Supervisor; + + @property({ attribute: false }) public route!: Route; + + @property({ type: Boolean }) public narrow!: boolean; + + @property({ attribute: false }) public addonSlug?: string; + + @state() private _updateType?: updateType; + + @state() private _changelogContent?: string; + + @state() private _addonInfo?: HassioAddonDetails; + + @state() private _action: "backup" | "update" | null = null; + + @state() private _error?: string; + + private _addonStoreInfo = memoizeOne( + (slug: string, storeAddons: StoreAddon[]) => + storeAddons.find((addon) => addon.slug === slug) + ); + + protected render(): TemplateResult { + if ( + !this._updateType || + (this._updateType === "addon" && !this._addonInfo) + ) { + return html``; + } + + const changelog = changelogUrl(this.hass, this._updateType, this._version); + + return html` + +
+ ${this._error + ? html`${this._error}` + : ""} + ${this._action === null + ? html` + ${this._changelogContent + ? html` + + + + + ` + : ""} +
+

+ ${this.supervisor.localize("update_available.description", { + name: this._name, + version: this._version, + newest_version: this._version_latest, + })} +

+
+ ${["core", "addon"].includes(this._updateType) + ? html` + + + + ` + : ""} + ` + : html` + +

+ ${this._action === "update" + ? this.supervisor.localize("update_available.updating", { + name: this._name, + version: this._version_latest, + }) + : this.supervisor.localize( + "update_available.creating_backup", + { name: this._name } + )} +

`} +
+ ${this._action === null + ? html` +
+ ${changelog + ? html` + + + ` + : ""} + + + ${this.supervisor.localize("common.update")} + +
+ ` + : ""} +
+ `; + } + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + const pathPart = this.route?.path.substring(1, this.route.path.length); + const updateType = ["core", "os", "supervisor"].includes(pathPart) + ? pathPart + : "addon"; + this._updateType = updateType as updateType; + + if (updateType === "addon") { + if (!this.addonSlug) { + this.addonSlug = pathPart; + } + this._loadAddonData(); + } + } + + get _shouldCreateBackup(): boolean { + return this.shadowRoot?.querySelector("ha-checkbox")?.checked || true; + } + + get _version(): string { + return this._updateType + ? this._updateType === "addon" + ? this._addonInfo!.version + : this.supervisor[this._updateType]?.version || "" + : ""; + } + + get _version_latest(): string { + return this._updateType + ? this._updateType === "addon" + ? this._addonInfo!.version_latest + : this.supervisor[this._updateType]?.version_latest || "" + : ""; + } + + get _name(): string { + return this._updateType + ? this._updateType === "addon" + ? this._addonInfo!.name + : SUPERVISOR_UPDATE_NAMES[this._updateType] + : ""; + } + + private async _loadAddonData() { + try { + this._addonInfo = await fetchHassioAddonInfo(this.hass, this.addonSlug!); + } catch (err) { + showAlertDialog(this, { + title: this._updateType, + text: extractApiErrorMessage(err), + }); + return; + } + const addonStoreInfo = + !this._addonInfo.detached && !this._addonInfo.available + ? this._addonStoreInfo( + this._addonInfo.slug, + this.supervisor.store.addons + ) + : undefined; + + if (this._addonInfo.changelog) { + try { + const content = await fetchHassioAddonChangelog( + this.hass, + this._updateType! + ); + this._changelogContent = extractChangelog(this._addonInfo, content); + } catch (err) { + this._error = extractApiErrorMessage(err); + return; + } + } + + if (!this._addonInfo.available && addonStoreInfo) { + if ( + !addonArchIsSupported( + this.supervisor.info.supported_arch, + this._addonInfo.arch + ) + ) { + this._error = this.supervisor.localize( + "addon.dashboard.not_available_arch" + ); + } else { + this._error = this.supervisor.localize( + "addon.dashboard.not_available_version", + { + core_version_installed: this.supervisor.core.version, + core_version_needed: addonStoreInfo.homeassistant, + } + ); + } + } + } + + private async _update() { + if (this._shouldCreateBackup) { + let backupArgs: HassioPartialBackupCreateParams; + if (this._updateType === "addon") { + backupArgs = { + name: `addon_${this._updateType}_${this._addonInfo?.version}`, + addons: [this._updateType!], + homeassistant: false, + }; + } else { + backupArgs = { + name: `${this._updateType}_${this._addonInfo?.version}`, + folders: ["homeassistant"], + homeassistant: true, + }; + } + this._action = "backup"; + try { + await createHassioPartialBackup(this.hass, backupArgs); + } catch (err: any) { + this._error = extractApiErrorMessage(err); + this._action = null; + return; + } + } + + this._action = "update"; + try { + if (this._updateType === "addon") { + await updateHassioAddon(this.hass, this._updateType!); + } else if (this._updateType === "core") { + await updateCore(this.hass); + } else if (this._updateType === "os") { + await updateOS(this.hass); + } else if (this._updateType === "supervisor") { + await updateSupervisor(this.hass); + } + } catch (err: any) { + if (this.hass.connection.connected && !ignoreSupervisorError(err)) { + this._error = extractApiErrorMessage(err); + this._action = null; + return; + } + } + fireEvent(this, "update-complete"); + } + + static get styles(): CSSResultGroup { + return css` + :host { + display: block; + } + ha-card { + margin: auto; + } + a { + text-decoration: none; + color: var(--primary-text-color); + } + ha-settings-row { + padding: 0; + } + .card-actions { + display: flex; + justify-content: space-between; + border-top: none; + padding: 0 8px 8px; + } + + ha-circular-progress { + display: block; + margin: 32px; + text-align: center; + } + + .progress-text { + text-align: center; + } + + ha-markdown { + padding-bottom: 8px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "update-available-card": UpdateAvailableCard; + } +} diff --git a/hassio/src/update-available/update-available-dashboard.ts b/hassio/src/update-available/update-available-dashboard.ts index ccebe007cd..481808a126 100644 --- a/hassio/src/update-available/update-available-dashboard.ts +++ b/hassio/src/update-available/update-available-dashboard.ts @@ -1,78 +1,11 @@ -import "@material/mwc-list/mwc-list-item"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { property, state } from "lit/decorators"; -import memoizeOne from "memoize-one"; -import "../../../src/common/search/search-input"; -import "../../../src/components/buttons/ha-progress-button"; -import "../../../src/components/ha-alert"; -import "../../../src/components/ha-button-menu"; -import "../../../src/components/ha-card"; -import "../../../src/components/ha-checkbox"; -import "../../../src/components/ha-expansion-panel"; -import "../../../src/components/ha-formfield"; -import "../../../src/components/ha-icon-button"; -import "../../../src/components/ha-markdown"; -import "../../../src/components/ha-settings-row"; -import "../../../src/components/ha-svg-icon"; -import "../../../src/components/ha-switch"; -import { - fetchHassioAddonChangelog, - fetchHassioAddonInfo, - HassioAddonDetails, - updateHassioAddon, -} from "../../../src/data/hassio/addon"; -import { - createHassioPartialBackup, - HassioPartialBackupCreateParams, -} from "../../../src/data/hassio/backup"; -import { - extractApiErrorMessage, - ignoreSupervisorError, -} from "../../../src/data/hassio/common"; -import { updateOS } from "../../../src/data/hassio/host"; -import { updateSupervisor } from "../../../src/data/hassio/supervisor"; -import { updateCore } from "../../../src/data/supervisor/core"; -import { StoreAddon } from "../../../src/data/supervisor/store"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; import { Supervisor } from "../../../src/data/supervisor/supervisor"; -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 { documentationUrl } from "../../../src/util/documentation-url"; -import { addonArchIsSupported, extractChangelog } from "../util/addon"; - -const changelogUrl = ( - hass: HomeAssistant, - entry: string, - version: string -): string | undefined => { - if (entry === "core") { - return version?.includes("dev") - ? "https://github.com/home-assistant/core/commits/dev" - : documentationUrl(hass, "/latest-release-notes/"); - } - if (entry === "os") { - return version?.includes("dev") - ? "https://github.com/home-assistant/operating-system/commits/dev" - : `https://github.com/home-assistant/operating-system/releases/tag/${version}`; - } - if (entry === "supervisor") { - return version?.includes("dev") - ? "https://github.com/home-assistant/supervisor/commits/main" - : `https://github.com/home-assistant/supervisor/releases/tag/${version}`; - } - return undefined; -}; +import "./update-available-card"; +@customElement("update-available-dashboard") class UpdateAvailableDashboard extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -82,258 +15,25 @@ class UpdateAvailableDashboard extends LitElement { @property({ attribute: false }) public route!: Route; - @state() private _updateEntry?: string; - - @state() private _changelogContent?: string; - - @state() private _addonInfo?: HassioAddonDetails; - - @state() private _createBackup = true; - - @state() private _action: "backup" | "update" | null = null; - - @state() private _error?: string; - - private _isAddon = false; - - private _addonStoreInfo = memoizeOne( - (slug: string, storeAddons: StoreAddon[]) => - storeAddons.find((addon) => addon.slug === slug) - ); - protected render(): TemplateResult { - if (!this._updateEntry) { - return html``; - } - const name = - // @ts-ignore - this._addonInfo?.name || SUPERVISOR_UPDATE_NAMES[this._updateEntry]; - const changelog = !this._isAddon - ? changelogUrl( - this.hass, - this._updateEntry, - this.supervisor[this._updateEntry]?.version - ) - : undefined; return html` - -
- ${this._error - ? html`${this._error}` - : ""} - ${this._action === null - ? html` - ${this._changelogContent - ? html` - - - - - ` - : ""} -
-

- ${this.supervisor.localize( - "update_available.description", - { - version: - this._addonInfo?.version || - this.supervisor[this._updateEntry]?.version, - newest_version: - this._addonInfo?.version_latest || - this.supervisor[this._updateEntry]?.version_latest, - } - )} -

-
- ${!["os", "supervisor"].includes(this._updateEntry) - ? html` - - - - - ` - : ""} - ` - : html` - -

- ${this._action === "update" - ? this.supervisor.localize("update_available.updating", { - name, - version: - this._addonInfo?.version_latest || - this.supervisor[this._updateEntry]?.version_latest, - }) - : this.supervisor.localize( - "update_available.creating_backup", - { name } - )} -

`} -
- ${this._action === null - ? html` -
- ${changelog - ? html` - - - ` - : ""} - - - ${this.supervisor.localize("common.update")} - -
- ` - : ""} -
+
`; } - protected firstUpdated(changedProps: PropertyValues) { - super.firstUpdated(changedProps); - this._updateEntry = this.route.path.substring(1, this.route.path.length); - this._isAddon = !["core", "os", "supervisor"].includes(this._updateEntry); - if (this._isAddon) { - this._loadAddonData(); - } - } - - private async _loadAddonData() { - try { - this._addonInfo = await fetchHassioAddonInfo( - this.hass, - this._updateEntry! - ); - } catch (err) { - showAlertDialog(this, { - title: this._updateEntry, - text: extractApiErrorMessage(err), - confirm: () => history.back(), - }); - return; - } - const addonStoreInfo = - !this._addonInfo.detached && !this._addonInfo.available - ? this._addonStoreInfo( - this._addonInfo.slug, - this.supervisor.store.addons - ) - : undefined; - - if (this._addonInfo.changelog) { - try { - const content = await fetchHassioAddonChangelog( - this.hass, - this._updateEntry! - ); - this._changelogContent = extractChangelog(this._addonInfo, content); - } catch (err) { - this._error = extractApiErrorMessage(err); - return; - } - } - - if (!this._addonInfo.available && addonStoreInfo) { - if ( - !addonArchIsSupported( - this.supervisor.info.supported_arch, - this._addonInfo.arch - ) - ) { - this._error = this.supervisor.localize( - "addon.dashboard.not_available_arch" - ); - } else { - this._error = this.supervisor.localize( - "addon.dashboard.not_available_version", - { - core_version_installed: this.supervisor.core.version, - core_version_needed: addonStoreInfo.homeassistant, - } - ); - } - } - } - - private _toggleBackup() { - this._createBackup = !this._createBackup; - } - - private async _update() { - if (this._createBackup) { - let backupArgs: HassioPartialBackupCreateParams; - if (this._isAddon) { - backupArgs = { - name: `addon_${this._updateEntry}_${this._addonInfo?.version}`, - addons: [this._updateEntry!], - homeassistant: false, - }; - } else { - backupArgs = { - name: `${this._updateEntry}_${this._addonInfo?.version}`, - folders: ["homeassistant"], - homeassistant: true, - }; - } - this._action = "backup"; - try { - await createHassioPartialBackup(this.hass, backupArgs); - } catch (err: any) { - this._error = extractApiErrorMessage(err); - this._action = null; - return; - } - } - - this._action = "update"; - try { - if (this._isAddon) { - await updateHassioAddon(this.hass, this._updateEntry!); - } else if (this._updateEntry === "core") { - await updateCore(this.hass); - } else if (this._updateEntry === "os") { - await updateOS(this.hass); - } else if (this._updateEntry === "supervisor") { - await updateSupervisor(this.hass); - } - } catch (err: any) { - if (this.hass.connection.connected && !ignoreSupervisorError(err)) { - this._error = extractApiErrorMessage(err); - this._action = null; - return; - } - } + private _updateComplete() { history.back(); } @@ -343,34 +43,17 @@ class UpdateAvailableDashboard extends LitElement { --app-header-background-color: var(--primary-background-color); --app-header-text-color: var(--sidebar-text-color); } - ha-card { + update-available-card { margin: auto; margin-top: 16px; max-width: 600px; } - a { - text-decoration: none; - color: var(--primary-text-color); - } - ha-settings-row { - padding: 0; - } - .card-actions { - display: flex; - justify-content: space-between; - } - - ha-circular-progress { - display: block; - margin: 32px; - text-align: center; - } - - .progress-text { - text-align: center; - } `; } } -customElements.define("update-available-dashboard", UpdateAvailableDashboard); +declare global { + interface HTMLElementTagNameMap { + "update-available-dashboard": UpdateAvailableDashboard; + } +} From 5fd43157899f9ea7554fe87a24f734e4da3f0549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 23 Nov 2021 17:53:17 +0100 Subject: [PATCH 045/112] Fix addon slug (#10693) --- hassio/src/update-available/update-available-card.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/src/update-available/update-available-card.ts b/hassio/src/update-available/update-available-card.ts index 93d096e07f..7f4518db0f 100644 --- a/hassio/src/update-available/update-available-card.ts +++ b/hassio/src/update-available/update-available-card.ts @@ -274,7 +274,7 @@ class UpdateAvailableCard extends LitElement { try { const content = await fetchHassioAddonChangelog( this.hass, - this._updateType! + this.addonSlug! ); this._changelogContent = extractChangelog(this._addonInfo, content); } catch (err) { From 921763b5f1f478eeb5e536b04db96a64b1130af6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 24 Nov 2021 09:09:21 +0100 Subject: [PATCH 046/112] Improve device information when via device is unknown (#10685) --- .../config/devices/device-detail/ha-device-info-card.ts | 6 +++--- src/translations/en.json | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/panels/config/devices/device-detail/ha-device-info-card.ts b/src/panels/config/devices/device-detail/ha-device-info-card.ts index f442246d0f..3e8b56810b 100644 --- a/src/panels/config/devices/device-detail/ha-device-info-card.ts +++ b/src/panels/config/devices/device-detail/ha-device-info-card.ts @@ -82,9 +82,9 @@ export class HaDeviceCard extends LitElement { const device = devices.find((dev) => dev.id === deviceId); return device ? computeDeviceName(device, this.hass) - : `(${this.hass.localize( - "ui.panel.config.integrations.config_entry.device_unavailable" - )})`; + : `<${this.hass.localize( + "ui.panel.config.integrations.config_entry.unknown_via_device" + )}>`; } static get styles(): CSSResultGroup { diff --git a/src/translations/en.json b/src/translations/en.json index 93fa75033b..f288340b02 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2370,11 +2370,10 @@ "enable_restart_confirm": "Restart Home Assistant to finish enabling this integration", "disable_error": "Enabling or disabling of the integration failed", "manuf": "by {manufacturer}", - "hub": "Connected via", + "via": "Connected via", "firmware": "Firmware: {version}", "unnamed_entry": "Unnamed entry", - "device_unavailable": "Device unavailable", - "entity_unavailable": "Entity unavailable", + "unknown_via_device": "Unknown device", "area": "In {area}", "no_area": "No Area", "not_loaded": "Not loaded", From f70485bc497564c9da5df629f66e8d84e6775a9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 25 Nov 2021 16:56:57 +0100 Subject: [PATCH 047/112] Don't make button disabled on error (#10699) --- hassio/src/update-available/update-available-card.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/src/update-available/update-available-card.ts b/hassio/src/update-available/update-available-card.ts index 7f4518db0f..67430e4092 100644 --- a/hassio/src/update-available/update-available-card.ts +++ b/hassio/src/update-available/update-available-card.ts @@ -193,7 +193,6 @@ class UpdateAvailableCard extends LitElement { Date: Fri, 26 Nov 2021 17:11:06 +0100 Subject: [PATCH 048/112] Use app-header-text-color (#10711) --- src/layouts/hass-tabs-subpage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layouts/hass-tabs-subpage.ts b/src/layouts/hass-tabs-subpage.ts index d325e2d044..d88722d8eb 100644 --- a/src/layouts/hass-tabs-subpage.ts +++ b/src/layouts/hass-tabs-subpage.ts @@ -226,7 +226,7 @@ class HassTabsSubpage extends LitElement { box-sizing: border-box; } .toolbar a { - color: var(--sidebar-text-color); + color: var(--app-header-text-color); text-decoration: none; } .bottom-bar a { From 43011179ebb650f0fd4b2b010dedace1d94a8325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 26 Nov 2021 17:24:30 +0100 Subject: [PATCH 049/112] Finish up config changes (#10710) Co-authored-by: Bram Kragten --- hassio/src/dashboard/hassio-update.ts | 2 +- hassio/src/system/hassio-core-info.ts | 2 +- hassio/src/system/hassio-host-info.ts | 2 +- hassio/src/system/hassio-supervisor-info.ts | 2 +- .../update-available/update-available-card.ts | 8 +- src/common/config/can_show_page.ts | 8 +- src/layouts/hass-subpage.ts | 14 ++ src/layouts/hass-tabs-subpage.ts | 1 + .../config/areas/ha-config-area-page.ts | 2 +- .../config/areas/ha-config-areas-dashboard.ts | 2 +- .../config/automation/ha-automation-editor.ts | 2 +- .../config/automation/ha-automation-picker.ts | 2 +- .../config/automation/ha-automation-trace.ts | 2 +- .../config/blueprint/ha-blueprint-overview.ts | 2 +- src/panels/config/cloud/login/cloud-login.ts | 2 +- .../config/dashboard/ha-config-dashboard.ts | 156 +++++++++-------- .../config/dashboard/ha-config-updates.ts | 163 ++++++++++-------- .../config/devices/ha-config-device-page.ts | 2 +- .../devices/ha-config-devices-dashboard.ts | 2 +- src/panels/config/energy/ha-config-energy.ts | 11 +- .../config/entities/ha-config-entities.ts | 2 +- src/panels/config/ha-config-section.ts | 12 ++ src/panels/config/ha-panel-config.ts | 107 +++++++++++- .../integrations/ha-config-integrations.ts | 2 +- src/panels/config/scene/ha-scene-dashboard.ts | 2 +- src/panels/config/scene/ha-scene-editor.ts | 2 +- src/panels/config/script/ha-script-editor.ts | 2 +- src/panels/config/script/ha-script-picker.ts | 2 +- src/panels/config/script/ha-script-trace.ts | 2 +- src/panels/config/tags/ha-config-tags.ts | 2 +- src/translations/en.json | 7 +- 31 files changed, 346 insertions(+), 183 deletions(-) diff --git a/hassio/src/dashboard/hassio-update.ts b/hassio/src/dashboard/hassio-update.ts index 40cfbc453a..2f8321344d 100644 --- a/hassio/src/dashboard/hassio-update.ts +++ b/hassio/src/dashboard/hassio-update.ts @@ -111,7 +111,7 @@ export class HassioUpdate extends LitElement {
diff --git a/hassio/src/system/hassio-core-info.ts b/hassio/src/system/hassio-core-info.ts index 3912e2de3d..dc3589246e 100644 --- a/hassio/src/system/hassio-core-info.ts +++ b/hassio/src/system/hassio-core-info.ts @@ -71,7 +71,7 @@ class HassioCoreInfo extends LitElement { ? html` diff --git a/hassio/src/system/hassio-host-info.ts b/hassio/src/system/hassio-host-info.ts index 79c63c370b..bb108fb516 100644 --- a/hassio/src/system/hassio-host-info.ts +++ b/hassio/src/system/hassio-host-info.ts @@ -110,7 +110,7 @@ class HassioHostInfo extends LitElement { ? html` diff --git a/hassio/src/system/hassio-supervisor-info.ts b/hassio/src/system/hassio-supervisor-info.ts index 4c3a50efa4..ed2e0de246 100644 --- a/hassio/src/system/hassio-supervisor-info.ts +++ b/hassio/src/system/hassio-supervisor-info.ts @@ -81,7 +81,7 @@ class HassioSupervisorInfo extends LitElement { ? html` diff --git a/hassio/src/update-available/update-available-card.ts b/hassio/src/update-available/update-available-card.ts index 67430e4092..590cbb087f 100644 --- a/hassio/src/update-available/update-available-card.ts +++ b/hassio/src/update-available/update-available-card.ts @@ -310,13 +310,13 @@ class UpdateAvailableCard extends LitElement { let backupArgs: HassioPartialBackupCreateParams; if (this._updateType === "addon") { backupArgs = { - name: `addon_${this._updateType}_${this._addonInfo?.version}`, - addons: [this._updateType!], + name: `addon_${this.addonSlug}_${this._version}`, + addons: [this.addonSlug!], homeassistant: false, }; } else { backupArgs = { - name: `${this._updateType}_${this._addonInfo?.version}`, + name: `${this._updateType}_${this._version}`, folders: ["homeassistant"], homeassistant: true, }; @@ -334,7 +334,7 @@ class UpdateAvailableCard extends LitElement { this._action = "update"; try { if (this._updateType === "addon") { - await updateHassioAddon(this.hass, this._updateType!); + await updateHassioAddon(this.hass, this.addonSlug!); } else if (this._updateType === "core") { await updateCore(this.hass); } else if (this._updateType === "os") { diff --git a/src/common/config/can_show_page.ts b/src/common/config/can_show_page.ts index 62628f4d79..6cf9999340 100644 --- a/src/common/config/can_show_page.ts +++ b/src/common/config/can_show_page.ts @@ -7,7 +7,13 @@ export const canShowPage = (hass: HomeAssistant, page: PageNavigation) => !hideAdvancedPage(hass, page); const isLoadedIntegration = (hass: HomeAssistant, page: PageNavigation) => - !page.component || isComponentLoaded(hass, page.component); + page.component + ? isComponentLoaded(hass, page.component) + : page.components + ? page.components.some((integration) => + isComponentLoaded(hass, integration) + ) + : true; const isCore = (page: PageNavigation) => page.core; const isAdvancedPage = (page: PageNavigation) => page.advancedOnly; const userWantsAdvanced = (hass: HomeAssistant) => hass.userData?.showAdvanced; diff --git a/src/layouts/hass-subpage.ts b/src/layouts/hass-subpage.ts index 58c4c376aa..336d48c23d 100644 --- a/src/layouts/hass-subpage.ts +++ b/src/layouts/hass-subpage.ts @@ -13,6 +13,8 @@ class HassSubpage extends LitElement { @property({ type: Boolean, attribute: "main-page" }) public mainPage = false; + @property({ type: String, attribute: "back-path" }) public backPath?: string; + @property({ type: Boolean, reflect: true }) public narrow = false; @property({ type: Boolean }) public supervisor = false; @@ -31,6 +33,14 @@ class HassSubpage extends LitElement { .narrow=${this.narrow} > ` + : this.backPath + ? html` + + + + ` : html` ${this.narrow diff --git a/src/panels/config/areas/ha-config-areas-dashboard.ts b/src/panels/config/areas/ha-config-areas-dashboard.ts index d694234ea6..6f47226bf9 100644 --- a/src/panels/config/areas/ha-config-areas-dashboard.ts +++ b/src/panels/config/areas/ha-config-areas-dashboard.ts @@ -89,7 +89,7 @@ export class HaConfigAreasDashboard extends LitElement { .narrow=${this.narrow} .isWide=${this.isWide} back-path="/config" - .tabs=${configSections.integrations} + .tabs=${configSections.devices} .route=${this.route} > ${this.narrow ? html`${title} diff --git a/src/panels/config/blueprint/ha-blueprint-overview.ts b/src/panels/config/blueprint/ha-blueprint-overview.ts index 35123dceb0..0e65094222 100644 --- a/src/panels/config/blueprint/ha-blueprint-overview.ts +++ b/src/panels/config/blueprint/ha-blueprint-overview.ts @@ -224,7 +224,7 @@ class HaBlueprintOverview extends LitElement { .narrow=${this.narrow} back-path="/config" .route=${this.route} - .tabs=${configSections.automation} + .tabs=${configSections.automations} .columns=${this._columns(this.narrow, this.hass.language)} .data=${this._processedBlueprints(this.blueprints)} id="entity_id" diff --git a/src/panels/config/cloud/login/cloud-login.ts b/src/panels/config/cloud/login/cloud-login.ts index c058985a60..12f8e3d024 100644 --- a/src/panels/config/cloud/login/cloud-login.ts +++ b/src/panels/config/cloud/login/cloud-login.ts @@ -50,7 +50,7 @@ export class CloudLogin extends LitElement { header="Home Assistant Cloud" >
- + Home Assistant Cloud

diff --git a/src/panels/config/dashboard/ha-config-dashboard.ts b/src/panels/config/dashboard/ha-config-dashboard.ts index 27bdca32a7..39fd28c5be 100644 --- a/src/panels/config/dashboard/ha-config-dashboard.ts +++ b/src/panels/config/dashboard/ha-config-dashboard.ts @@ -15,6 +15,7 @@ import { HomeAssistant } from "../../../types"; import "../ha-config-section"; import { configSections } from "../ha-panel-config"; import "./ha-config-navigation"; +import { SupervisorAvailableUpdates } from "../../../data/supervisor/supervisor"; @customElement("ha-config-dashboard") class HaConfigDashboard extends LitElement { @@ -27,74 +28,11 @@ class HaConfigDashboard extends LitElement { @property() public cloudStatus?: CloudStatus; + @property() public supervisorUpdates?: SupervisorAvailableUpdates[] | null; + @property() public showAdvanced!: boolean; protected render(): TemplateResult { - const content = html` -

${this.hass.localize("ui.panel.config.header")}
- -
- ${this.hass.localize("ui.panel.config.introduction")} -
- - ${isComponentLoaded(this.hass, "hassio") - ? html`` - : ""} - ${this.cloudStatus && isComponentLoaded(this.hass, "cloud") - ? html` - - - - ` - : ""} - ${Object.values(configSections).map( - (section) => html` - - - - ` - )} - ${!this.showAdvanced - ? html` -
- ${this.hass.localize("ui.panel.config.advanced_mode.hint_enable")} - ${this.hass.localize( - "ui.panel.config.advanced_mode.link_profile_page" - )}. -
- ` - : ""} - `; - - if (!this.narrow && this.hass.dockedSidebar !== "always_hidden") { - return content; - } - return html` @@ -103,10 +41,72 @@ class HaConfigDashboard extends LitElement { .hass=${this.hass} .narrow=${this.narrow} > +
${this.hass.localize("panel.config")}
- ${content} + + ${isComponentLoaded(this.hass, "hassio") && + this.supervisorUpdates === undefined + ? html`` + : html`${this.supervisorUpdates !== null + ? html` + + ` + : ""} + + ${this.narrow && this.supervisorUpdates !== null + ? html`
+ ${this.hass.localize("panel.config")} +
` + : ""} + ${this.cloudStatus && isComponentLoaded(this.hass, "cloud") + ? html` + + ` + : ""} + +
+ ${!this.showAdvanced + ? html` +
+ ${this.hass.localize( + "ui.panel.config.advanced_mode.hint_enable" + )} + ${this.hass.localize( + "ui.panel.config.advanced_mode.link_profile_page" + )}. +
+ ` + : ""}`} +
`; } @@ -116,16 +116,16 @@ class HaConfigDashboard extends LitElement { haStyle, css` app-header { - --app-header-background-color: var(--primary-background-color); + border-bottom: var(--app-header-border-bottom); + --header-height: 55px; } ha-card:last-child { margin-bottom: 24px; } ha-config-section { - margin-top: -12px; - } - :host([narrow]) ha-config-section { - margin-top: -20px; + margin: auto; + margin-top: -32px; + max-width: 600px; } ha-card { overflow: hidden; @@ -134,6 +134,11 @@ class HaConfigDashboard extends LitElement { text-decoration: none; color: var(--primary-text-color); } + .title { + font-size: 16px; + padding: 16px; + padding-bottom: 0; + } .promo-advanced { text-align: center; color: var(--secondary-text-color); @@ -142,8 +147,13 @@ class HaConfigDashboard extends LitElement { .promo-advanced a { color: var(--secondary-text-color); } - .intro { - margin-bottom: 24px; + :host([narrow]) ha-card { + background-color: var(--primary-background-color); + box-shadow: unset; + } + + :host([narrow]) ha-config-section { + margin-top: -42px; } `, ]; diff --git a/src/panels/config/dashboard/ha-config-updates.ts b/src/panels/config/dashboard/ha-config-updates.ts index 313e0e34f5..e5cb58ebc9 100644 --- a/src/panels/config/dashboard/ha-config-updates.ts +++ b/src/panels/config/dashboard/ha-config-updates.ts @@ -2,23 +2,13 @@ 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, - PropertyValues, - TemplateResult, -} from "lit"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import "../../../components/ha-alert"; import "../../../components/ha-logo-svg"; import "../../../components/ha-svg-icon"; -import { extractApiErrorMessage } from "../../../data/hassio/common"; -import { - fetchSupervisorAvailableUpdates, - SupervisorAvailableUpdates, -} from "../../../data/supervisor/supervisor"; +import { SupervisorAvailableUpdates } from "../../../data/supervisor/supervisor"; +import { buttonLinkStyle } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; export const SUPERVISOR_UPDATE_NAMES = { @@ -31,88 +21,117 @@ export const SUPERVISOR_UPDATE_NAMES = { class HaConfigUpdates extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @state() private _supervisorUpdates?: SupervisorAvailableUpdates[]; + @property({ type: Boolean }) public narrow!: boolean; - @state() private _error?: string; + @property({ attribute: false }) + public supervisorUpdates?: SupervisorAvailableUpdates[] | null; - protected firstUpdated(changedProps: PropertyValues): void { - super.firstUpdated(changedProps); - this._loadSupervisorUpdates(); - } + @state() private _showAll = false; protected render(): TemplateResult { + if (!this.supervisorUpdates) { + return html``; + } + + const updates = + this._showAll || this.supervisorUpdates.length <= 3 + ? this.supervisorUpdates + : this.supervisorUpdates.slice(0, 2); + return html` - ${this._error - ? html` - ${this._error} - ` - : ""} - ${this._supervisorUpdates?.map( +
+ ${this.hass.localize("ui.panel.config.updates.title", { + count: this.supervisorUpdates.length, + })} +
+ ${updates.map( (update) => html` - - + + ${update.update_type === "addon" ? update.icon ? html`` : html`` : html``} - ${this.hass.localize("ui.panel.config.updates.version_available", { - version_available: update.version_latest, - })} - + + ${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._showAll && !this.narrow ? html`
` : ""} + ${!this._showAll && this.supervisorUpdates.length >= 4 + ? html` + + ` + : ""} `; } - private async _loadSupervisorUpdates(): Promise { - try { - this._supervisorUpdates = await fetchSupervisorAvailableUpdates( - this.hass - ); - } catch (err) { - this._error = extractApiErrorMessage(err); - } + private _showAllClicked() { + this._showAll = true; } - static get styles(): CSSResultGroup { - return css` - 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); - } - `; + static get styles(): CSSResultGroup[] { + return [ + buttonLinkStyle, + css` + .title { + font-size: 16px; + 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); + } + button.show-all { + color: var(--primary-color); + text-decoration: none; + margin: 8px 16px; + } + .divider::before { + content: " "; + display: block; + height: 1px; + background-color: var(--divider-color); + } + `, + ]; } } diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index 9b4de45b9b..5bc90abded 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -218,7 +218,7 @@ export class HaConfigDevicePage extends LitElement { ${ diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts index c8f12b685f..b29d070131 100644 --- a/src/panels/config/devices/ha-config-devices-dashboard.ts +++ b/src/panels/config/devices/ha-config-devices-dashboard.ts @@ -375,7 +375,7 @@ export class HaConfigDeviceDashboard extends LitElement { .backPath=${this._searchParms.has("historyBack") ? undefined : "/config"} - .tabs=${configSections.integrations} + .tabs=${configSections.devices} .route=${this.route} .activeFilters=${activeFilters} .numHidden=${this._numHiddenDevices} diff --git a/src/panels/config/energy/ha-config-energy.ts b/src/panels/config/energy/ha-config-energy.ts index b93c93c749..970573cbe3 100644 --- a/src/panels/config/energy/ha-config-energy.ts +++ b/src/panels/config/energy/ha-config-energy.ts @@ -1,3 +1,4 @@ +import "../../../layouts/hass-error-screen"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { @@ -9,11 +10,10 @@ import { getEnergyPreferences, } from "../../../data/energy"; import "../../../layouts/hass-loading-screen"; -import "../../../layouts/hass-tabs-subpage"; +import "../../../layouts/hass-subpage"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant, Route } from "../../../types"; import "../../../components/ha-alert"; -import { configSections } from "../ha-panel-config"; import "./components/ha-energy-device-settings"; import "./components/ha-energy-grid-settings"; import "./components/ha-energy-solar-settings"; @@ -68,14 +68,13 @@ class HaConfigEnergy extends LitElement { } return html` - ${this.hass.localize("ui.panel.config.energy.new_device_info")} @@ -113,7 +112,7 @@ class HaConfigEnergy extends LitElement { @value-changed=${this._prefsChanged} >
- + `; } diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index 6f9569e6cf..c1b5073c6a 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -478,7 +478,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { ? undefined : "/config"} .route=${this.route} - .tabs=${configSections.integrations} + .tabs=${configSections.devices} .columns=${this._columns( this.narrow, this.hass.language, diff --git a/src/panels/config/ha-config-section.ts b/src/panels/config/ha-config-section.ts index 01211a9b7a..16dc7bc389 100644 --- a/src/panels/config/ha-config-section.ts +++ b/src/panels/config/ha-config-section.ts @@ -8,11 +8,15 @@ export class HaConfigSection extends LitElement { @property({ type: Boolean }) public vertical = false; + @property({ type: Boolean, attribute: "full-width" }) + public fullWidth = false; + protected render() { return html`
@@ -111,6 +115,14 @@ export class HaConfigSection extends LitElement { margin-right: 0; max-width: 500px; } + + .full-width { + padding: 0; + } + + .full-width .layout { + flex-direction: column; + } `; } } diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index 043f9d84ec..30abfa3b6d 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -1,6 +1,7 @@ import { mdiAccount, mdiBadgeAccountHorizontal, + mdiCog, mdiDevices, mdiHomeAssistant, mdiInformation, @@ -27,6 +28,10 @@ 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/supervisor"; import "../../layouts/hass-loading-screen"; import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page"; import { PageNavigation } from "../../layouts/hass-tabs-subpage"; @@ -40,7 +45,82 @@ declare global { } export const configSections: { [name: string]: PageNavigation[] } = { - integrations: [ + dashboard: [ + { + path: "/config/integrations", + name: "Devices & Services", + description: "Integrations, devices, entities and areas", + iconPath: mdiDevices, + iconColor: "#2D338F", + core: true, + }, + { + path: "/config/automation", + name: "Automations", + description: "Automations, bludprints, scenes and scripts", + iconPath: mdiRobot, + iconColor: "#518C43", + components: ["automation", "blueprint", "scene", "script"], + }, + { + path: "/config/helpers", + name: "Helpers", + description: "Elements that help build automations", + iconPath: mdiTools, + iconColor: "#4D2EA4", + core: true, + }, + { + path: "/hassio", + name: "Add-ons & Backups", + description: "Create backups, check logs or reboot your system", + iconPath: mdiHomeAssistant, + iconColor: "#4084CD", + component: "hassio", + }, + { + path: "/config/lovelace/dashboards", + name: "Dashboards", + description: "Create customized sets of cards to control your home", + iconPath: mdiViewDashboard, + iconColor: "#B1345C", + component: "lovelace", + }, + { + path: "/config/energy", + name: "Energy", + description: "Monitor your energy production and consumption", + iconPath: mdiLightningBolt, + iconColor: "#F1C447", + component: "energy", + }, + { + path: "/config/tags", + name: "Tags", + description: + "Trigger automations when a NFC tag, QR code, etc. is scanned", + iconPath: mdiNfcVariant, + iconColor: "#616161", + component: "tag", + }, + { + path: "/config/person", + name: "People & Zones", + description: "Manage the people and zones that Home Assistant tracks", + iconPath: mdiAccount, + iconColor: "#E48629", + components: ["person", "zone", "users"], + }, + { + path: "/config/core", + name: "Settings", + description: "Basic settings, server controls, logs and info", + iconPath: mdiCog, + iconColor: "#4A5963", + core: true, + }, + ], + devices: [ { component: "integrations", path: "/config/integrations", @@ -74,7 +154,7 @@ export const configSections: { [name: string]: PageNavigation[] } = { core: true, }, ], - automation: [ + automations: [ { component: "blueprint", path: "/config/blueprint", @@ -114,7 +194,7 @@ export const configSections: { [name: string]: PageNavigation[] } = { core: true, }, ], - experiences: [ + tags: [ { component: "tag", path: "/config/tags", @@ -122,6 +202,8 @@ export const configSections: { [name: string]: PageNavigation[] } = { iconPath: mdiNfcVariant, iconColor: "#616161", }, + ], + energy: [ { component: "energy", path: "/config/energy", @@ -335,6 +417,8 @@ class HaPanelConfig extends HassRouterPage { @state() private _cloudStatus?: CloudStatus; + @state() private _supervisorUpdates?: SupervisorAvailableUpdates[] | null; + private _listeners: Array<() => void> = []; public connectedCallback() { @@ -364,6 +448,11 @@ class HaPanelConfig extends HassRouterPage { if (isComponentLoaded(this.hass, "cloud")) { this._updateCloudStatus(); } + if (isComponentLoaded(this.hass, "hassio")) { + this._loadSupervisorUpdates(); + } else { + this._supervisorUpdates = null; + } this.addEventListener("ha-refresh-cloud-status", () => this._updateCloudStatus() ); @@ -394,6 +483,7 @@ class HaPanelConfig extends HassRouterPage { isWide, narrow: this.narrow, cloudStatus: this._cloudStatus, + supervisorUpdates: this._supervisorUpdates, }); } else { el.route = this.routeTail; @@ -402,6 +492,7 @@ class HaPanelConfig extends HassRouterPage { el.isWide = isWide; el.narrow = this.narrow; el.cloudStatus = this._cloudStatus; + el.supervisorUpdates = this._supervisorUpdates; } } @@ -419,6 +510,16 @@ 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/panels/config/integrations/ha-config-integrations.ts b/src/panels/config/integrations/ha-config-integrations.ts index a3dbdff879..a4d43cd7f4 100644 --- a/src/panels/config/integrations/ha-config-integrations.ts +++ b/src/panels/config/integrations/ha-config-integrations.ts @@ -319,7 +319,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { .narrow=${this.narrow} back-path="/config" .route=${this.route} - .tabs=${configSections.integrations} + .tabs=${configSections.devices} > ${this.narrow ? html` diff --git a/src/panels/config/scene/ha-scene-dashboard.ts b/src/panels/config/scene/ha-scene-dashboard.ts index 039611f15a..53310c5e8c 100644 --- a/src/panels/config/scene/ha-scene-dashboard.ts +++ b/src/panels/config/scene/ha-scene-dashboard.ts @@ -147,7 +147,7 @@ class HaSceneDashboard extends LitElement { .narrow=${this.narrow} back-path="/config" .route=${this.route} - .tabs=${configSections.automation} + .tabs=${configSections.automations} .columns=${this._columns(this.hass.language)} id="entity_id" .data=${this._scenes(this.scenes, this._filteredScenes)} diff --git a/src/panels/config/scene/ha-scene-editor.ts b/src/panels/config/scene/ha-scene-editor.ts index 308db8a732..0b09c7239d 100644 --- a/src/panels/config/scene/ha-scene-editor.ts +++ b/src/panels/config/scene/ha-scene-editor.ts @@ -202,7 +202,7 @@ export class HaSceneEditor extends SubscribeMixin( .narrow=${this.narrow} .route=${this.route} .backCallback=${this._backTapped} - .tabs=${configSections.automation} + .tabs=${configSections.automations} > ${this.narrow ? html` ${title} diff --git a/src/panels/config/tags/ha-config-tags.ts b/src/panels/config/tags/ha-config-tags.ts index d0c6b780c0..a8c138ae23 100644 --- a/src/panels/config/tags/ha-config-tags.ts +++ b/src/panels/config/tags/ha-config-tags.ts @@ -180,7 +180,7 @@ export class HaConfigTags extends SubscribeMixin(LitElement) { .narrow=${this.narrow} back-path="/config" .route=${this.route} - .tabs=${configSections.experiences} + .tabs=${configSections.tags} .columns=${this._columns( this.narrow, this._canWriteTags, diff --git a/src/translations/en.json b/src/translations/en.json index f288340b02..3c4ff69a96 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -915,7 +915,6 @@ }, "config": { "header": "Configure Home Assistant", - "introduction": "In this view it is possible to configure your components and Home Assistant. Not everything is possible to configure from the UI yet, but we're working on it.", "advanced_mode": { "hint_enable": "Missing config options? Enable advanced mode on", "link_profile_page": "your profile page" @@ -927,9 +926,11 @@ "learn_more": "Learn more" }, "updates": { + "title": "{count} {count, plural,\n one {update}\n other {updates}\n}", "unable_to_fetch": "Unable to fetch available updates", "version_available": "Version {version_available} is available", - "review": "review" + "show_all_updates": "Show all updates", + "show": "show" }, "areas": { "caption": "Areas", @@ -4165,7 +4166,7 @@ "save": "[%key:ui::common::save%]", "close": "[%key:ui::common::close%]", "menu": "[%key:ui::common::menu%]", - "review": "[%key:ui::panel::config::updates::review%]", + "show": "[%key:ui::panel::config::updates::show%]", "show_more": "Show more information about this", "update_available": "{count, plural,\n one {Update}\n other {{count} updates}\n} pending", "update": "Update", From 366aa8aed12c7afb94cda21e6804b0ddd2121b5f Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Sun, 28 Nov 2021 17:52:39 +0100 Subject: [PATCH 050/112] Fix typo on config page + adjust icon color (#10713) --- src/panels/config/ha-panel-config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index 30abfa3b6d..784e4d7a6b 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -51,13 +51,13 @@ export const configSections: { [name: string]: PageNavigation[] } = { name: "Devices & Services", description: "Integrations, devices, entities and areas", iconPath: mdiDevices, - iconColor: "#2D338F", + iconColor: "#0D47A1", core: true, }, { path: "/config/automation", name: "Automations", - description: "Automations, bludprints, scenes and scripts", + description: "Automations, blueprints, scenes and scripts", iconPath: mdiRobot, iconColor: "#518C43", components: ["automation", "blueprint", "scene", "script"], From 117b50f3ea40efcff016e3df15c9bfc3ca2518cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 29 Nov 2021 07:27:53 +0100 Subject: [PATCH 051/112] Add ha-faded (#10651) --- gallery/src/demos/demo-ha-faded.ts | 88 +++++++++++++++++++ .../update-available/update-available-card.ts | 6 +- src/components/ha-faded.ts | 84 ++++++++++++++++++ 3 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 gallery/src/demos/demo-ha-faded.ts create mode 100644 src/components/ha-faded.ts diff --git a/gallery/src/demos/demo-ha-faded.ts b/gallery/src/demos/demo-ha-faded.ts new file mode 100644 index 0000000000..65dcb4315d --- /dev/null +++ b/gallery/src/demos/demo-ha-faded.ts @@ -0,0 +1,88 @@ +import { css, html, LitElement, TemplateResult } from "lit"; +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. +`; + +const SMALL_TEXT = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."; + +@customElement("demo-ha-faded") +export class DemoHaFaded extends LitElement { + protected render(): TemplateResult { + return html` + +
+

Long text directly as slotted content

+ ${LONG_TEXT} +

Long text with slotted element

+ ${LONG_TEXT} +

No text

+ +

Smal text

+ ${SMALL_TEXT} +

Long text in markdown

+ + + +

Missing 1px from hiding

+ + + 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. + + +

1px over hiding point

+ + + 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. + + +
+
+ `; + } + + static get styles() { + return css` + ha-card { + max-width: 600px; + margin: 24px auto; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-ha-faded": DemoHaFaded; + } +} diff --git a/hassio/src/update-available/update-available-card.ts b/hassio/src/update-available/update-available-card.ts index 590cbb087f..e1643e223a 100644 --- a/hassio/src/update-available/update-available-card.ts +++ b/hassio/src/update-available/update-available-card.ts @@ -16,7 +16,7 @@ import "../../../src/components/ha-alert"; import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-card"; import "../../../src/components/ha-checkbox"; -import "../../../src/components/ha-expansion-panel"; +import "../../../src/components/ha-faded"; import "../../../src/components/ha-formfield"; import "../../../src/components/ha-icon-button"; import "../../../src/components/ha-markdown"; @@ -136,10 +136,10 @@ class UpdateAvailableCard extends LitElement { ? html` ${this._changelogContent ? html` - + - + ` : ""}
diff --git a/src/components/ha-faded.ts b/src/components/ha-faded.ts new file mode 100644 index 0000000000..974956dcfd --- /dev/null +++ b/src/components/ha-faded.ts @@ -0,0 +1,84 @@ +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; + +@customElement("ha-faded") +class HaFaded extends LitElement { + @property({ type: Number, attribute: "faded-height" }) + public fadedHeight = 102; + + @state() _contentShown = false; + + protected render(): TemplateResult { + return html` +
+ +
+ `; + } + + get _slottedHeight(): number { + return ( + ( + this.shadowRoot!.querySelector(".container") + ?.firstElementChild as HTMLSlotElement + ) + .assignedElements() + .reduce( + (partial, element) => partial + (element as HTMLElement).offsetHeight, + 0 + ) || 0 + ); + } + + private _setShowContent() { + const height = this._slottedHeight; + if (height !== 0 && height <= this.fadedHeight + 50) { + this._contentShown = true; + } + } + + protected firstUpdated(changedProps) { + super.firstUpdated(changedProps); + this._setShowContent(); + } + + private _showContent(): void { + this._contentShown = true; + } + + static get styles(): CSSResultGroup { + return css` + .container { + display: block; + height: auto; + cursor: default; + } + .faded { + cursor: pointer; + -webkit-mask-image: linear-gradient( + to bottom, + black 25%, + transparent 100% + ); + mask-image: linear-gradient(to bottom, black 25%, transparent 100%); + overflow-y: hidden; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-faded": HaFaded; + } +} From 367322415ee0a761ffbae84874435c5a15c8e649 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 29 Nov 2021 09:58:34 +0100 Subject: [PATCH 052/112] Use `ha-icon-button` in `ha-icon-overflow-menu` (#10692) --- src/components/ha-icon-overflow-menu.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/components/ha-icon-overflow-menu.ts b/src/components/ha-icon-overflow-menu.ts index c9cb642386..664dd55c5a 100644 --- a/src/components/ha-icon-overflow-menu.ts +++ b/src/components/ha-icon-overflow-menu.ts @@ -1,12 +1,12 @@ +import "@material/mwc-list/mwc-list-item"; +import { mdiDotsVertical } from "@mdi/js"; +import "@polymer/paper-tooltip/paper-tooltip"; import { css, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; -import "./ha-button-menu"; -import "@material/mwc-list/mwc-list-item"; -import "@material/mwc-icon-button"; -import "./ha-svg-icon"; -import { mdiDotsVertical } from "@mdi/js"; import { HomeAssistant } from "../types"; -import "@polymer/paper-tooltip/paper-tooltip"; +import "./ha-button-menu"; +import "./ha-icon-button"; +import "./ha-svg-icon"; export interface IconOverflowMenuItem { [key: string]: any; @@ -37,13 +37,11 @@ export class HaIconOverflowMenu extends LitElement { corner="BOTTOM_START" absolute > - - - + > ${this.items.map( (item) => html` From 0b7fc177f9eb615b0ae4b131e27e72be75a54493 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 29 Nov 2021 10:03:30 +0100 Subject: [PATCH 053/112] Prevent errors in `more-info-climate` if no modes are provided despite support flags (#10694) --- src/dialogs/more-info/controls/more-info-climate.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/dialogs/more-info/controls/more-info-climate.ts b/src/dialogs/more-info/controls/more-info-climate.ts index a98d669cf0..dbae025e7e 100644 --- a/src/dialogs/more-info/controls/more-info-climate.ts +++ b/src/dialogs/more-info/controls/more-info-climate.ts @@ -192,7 +192,7 @@ class MoreInfoClimate extends LitElement {
- ${supportPresetMode + ${supportPresetMode && stateObj.attributes.preset_modes ? html`
` : ""} - ${supportFanMode + ${supportFanMode && stateObj.attributes.fan_modes ? html`
` : ""} - ${supportSwingMode + ${supportSwingMode && stateObj.attributes.swing_modes ? html`
Date: Mon, 29 Nov 2021 10:09:54 +0100 Subject: [PATCH 054/112] Make "Energy distribution today" translatable (#10696) --- src/panels/lovelace/common/generate-lovelace-config.ts | 4 +++- src/translations/en.json | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/panels/lovelace/common/generate-lovelace-config.ts b/src/panels/lovelace/common/generate-lovelace-config.ts index 59b5a6b6fa..e75fb59771 100644 --- a/src/panels/lovelace/common/generate-lovelace-config.ts +++ b/src/panels/lovelace/common/generate-lovelace-config.ts @@ -425,7 +425,9 @@ export const generateDefaultViewConfig = ( if (grid && grid.flow_from.length > 0) { areaCards.push({ - title: "Energy distribution today", + title: localize( + "ui.panel.lovelace.cards.energy.energy_distribution.title_today" + ), type: "energy-distribution", link_dashboard: true, }); diff --git a/src/translations/en.json b/src/translations/en.json index 3c4ff69a96..53667b018c 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3030,6 +3030,7 @@ "grid_neutrality_not_calculated": "Grid neutrality could not be calculated" }, "energy_distribution": { + "title_today": "Energy distribution today", "grid": "Grid", "gas": "Gas", "solar": "Solar", From b79c06ad71a7295e5b4ae14ce774e36f67fc8d30 Mon Sep 17 00:00:00 2001 From: Nathan Orick Date: Mon, 29 Nov 2021 04:14:09 -0500 Subject: [PATCH 055/112] Default to yaml editing when there are multiple states in condition (#10481) --- src/data/automation.ts | 2 +- .../condition/ha-automation-condition-row.ts | 31 +++++++++++++++++++ .../types/ha-automation-condition-state.ts | 20 +++++++++++- src/translations/en.json | 3 +- 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/data/automation.ts b/src/data/automation.ts index eedb752483..f85a387210 100644 --- a/src/data/automation.ts +++ b/src/data/automation.ts @@ -179,7 +179,7 @@ export interface StateCondition extends BaseCondition { condition: "state"; entity_id: string; attribute?: string; - state: string | number; + state: string | number | string[]; for?: string | number | ForDict; } 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 325949c3b5..d83c454488 100644 --- a/src/panels/config/automation/condition/ha-automation-condition-row.ts +++ b/src/panels/config/automation/condition/ha-automation-condition-row.ts @@ -5,6 +5,7 @@ import "@polymer/paper-item/paper-item"; import { css, CSSResultGroup, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; 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/ha-icon-button"; @@ -51,6 +52,8 @@ export default class HaAutomationConditionRow extends LitElement { @state() private _yamlMode = false; + @state() private _warnings?: string[]; + protected render() { if (!this.condition) { return html``; @@ -87,7 +90,25 @@ export default class HaAutomationConditionRow extends LitElement {
+ ${this._warnings + ? html` + ${this._warnings!.length > 0 && this._warnings![0] !== undefined + ? html`
    + ${this._warnings!.map( + (warning) => html`
  • ${warning}
  • ` + )} +
` + : ""} + ${this.hass.localize("ui.errors.config.edit_in_yaml_supported")} +
` + : ""} ) { switch (ev.detail.index) { case 0: @@ -125,6 +155,7 @@ export default class HaAutomationConditionRow extends LitElement { } private _switchYamlMode() { + this._warnings = undefined; this._yamlMode = !this._yamlMode; } diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-state.ts b/src/panels/config/automation/condition/types/ha-automation-condition-state.ts index 22103067f8..03c9ce25c0 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-state.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-state.ts @@ -1,5 +1,5 @@ import "@polymer/paper-input/paper-input"; -import { html, LitElement } from "lit"; +import { html, LitElement, PropertyValues } from "lit"; import { customElement, property } from "lit/decorators"; import { createDurationData } from "../../../../../common/datetime/create_duration_data"; import "../../../../../components/entity/ha-entity-attribute-picker"; @@ -11,6 +11,7 @@ import { handleChangeEvent, } from "../ha-automation-condition-row"; import "../../../../../components/ha-duration-input"; +import { fireEvent } from "../../../../../common/dom/fire_event"; @customElement("ha-automation-condition-state") export class HaStateCondition extends LitElement implements ConditionElement { @@ -22,6 +23,23 @@ export class HaStateCondition extends LitElement implements ConditionElement { return { entity_id: "", state: "" }; } + public willUpdate(changedProperties: PropertyValues): boolean { + if ( + changedProperties.has("condition") && + Array.isArray(this.condition?.state) + ) { + fireEvent( + this, + "ui-mode-not-available", + Error(this.hass.localize("ui.errors.config.no_state_array_support")) + ); + // We have to stop the update if state is an array. + // Otherwise the state will be changed to a comma-separated string by the input element. + return false; + } + return true; + } + protected render() { const { entity_id, attribute, state } = this.condition; const forTime = createDurationData(this.condition.for); diff --git a/src/translations/en.json b/src/translations/en.json index 53667b018c..f3bbe68476 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -864,7 +864,8 @@ "key_missing": "Required key ''{key}'' is missing.", "key_not_expected": "Key ''{key}'' is not expected or not supported by the visual editor.", "key_wrong_type": "The provided value for ''{key}'' is not supported by the visual editor. We support ({type_correct}) but received ({type_wrong}).", - "no_template_editor_support": "Templates not supported in visual editor" + "no_template_editor_support": "Templates not supported in visual editor", + "no_state_array_support": "Multiple state values not supported in visual editor" }, "supervisor": { "title": "Could not load the Supervisor panel!", From faec09f0d1610ef0d0d94a361d3cfcbf34226737 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 29 Nov 2021 10:19:33 +0100 Subject: [PATCH 056/112] Filter out disabled entities in the statistics dev tools (#10677) --- .../config/entities/ha-config-entities.ts | 2 - .../statistics/developer-tools-statistics.ts | 52 +++++++++++++++---- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index c1b5073c6a..e74c44036d 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -320,8 +320,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { } } - entities.forEach((entity) => entity); - let filteredEntities = showReadOnly ? entities.concat(stateEntities) : entities; diff --git a/src/panels/developer-tools/statistics/developer-tools-statistics.ts b/src/panels/developer-tools/statistics/developer-tools-statistics.ts index a7573d7201..4b48ebb116 100644 --- a/src/panels/developer-tools/statistics/developer-tools-statistics.ts +++ b/src/panels/developer-tools/statistics/developer-tools-statistics.ts @@ -1,5 +1,5 @@ import "@material/mwc-button/mwc-button"; -import { HassEntity } from "home-assistant-js-websocket"; +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"; @@ -7,6 +7,7 @@ import { fireEvent } from "../../../common/dom/fire_event"; import { computeStateName } from "../../../common/entity/compute_state_name"; import "../../../components/data-table/ha-data-table"; import type { DataTableColumnContainer } from "../../../components/data-table/ha-data-table"; +import { subscribeEntityRegistry } from "../../../data/entity_registry"; import { clearStatistics, getStatisticIds, @@ -18,6 +19,7 @@ import { showAlertDialog, showConfirmationDialog, } from "../../../dialogs/generic/show-dialog-box"; +import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import { showFixStatisticsUnitsChangedDialog } from "./show-dialog-statistics-fix-units-changed"; @@ -33,7 +35,7 @@ const FIX_ISSUES_ORDER = { unsupported_unit_metadata: 5, }; @customElement("developer-tools-statistics") -class HaPanelDevStatistics extends LitElement { +class HaPanelDevStatistics extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @property({ type: Boolean }) public narrow!: boolean; @@ -43,6 +45,8 @@ class HaPanelDevStatistics extends LitElement { state?: HassEntity; })[] = [] as StatisticsMetaData[]; + private _disabledEntities = new Set(); + protected firstUpdated() { this._validateStatistics(); } @@ -130,6 +134,25 @@ class HaPanelDevStatistics extends LitElement { } } + public hassSubscribe(): UnsubscribeFunc[] { + return [ + subscribeEntityRegistry(this.hass.connection!, (entities) => { + const disabledEntities = new Set(); + for (const confEnt of entities) { + if (!confEnt.disabled_by) { + continue; + } + disabledEntities.add(confEnt.entity_id); + } + // If the disabled entities changed, re-validate the statistics + if (disabledEntities !== this._disabledEntities) { + this._disabledEntities = disabledEntities; + this._validateStatistics(); + } + }), + ]; + } + private async _validateStatistics() { const [statisticIds, issues] = await Promise.all([ getStatisticIds(this.hass), @@ -138,17 +161,24 @@ class HaPanelDevStatistics extends LitElement { const statsIds = new Set(); - this._data = statisticIds.map((statistic) => { - statsIds.add(statistic.statistic_id); - return { - ...statistic, - state: this.hass.states[statistic.statistic_id], - issues: issues[statistic.statistic_id], - }; - }); + this._data = statisticIds + .filter( + (statistic) => !this._disabledEntities.has(statistic.statistic_id) + ) + .map((statistic) => { + statsIds.add(statistic.statistic_id); + return { + ...statistic, + state: this.hass.states[statistic.statistic_id], + issues: issues[statistic.statistic_id], + }; + }); Object.keys(issues).forEach((statisticId) => { - if (!statsIds.has(statisticId)) { + if ( + !statsIds.has(statisticId) && + !this._disabledEntities.has(statisticId) + ) { this._data.push({ statistic_id: statisticId, unit_of_measurement: "", From 2c0b2f4bc5166861c0ca4fd0ba681cee80662613 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 29 Nov 2021 10:30:14 +0100 Subject: [PATCH 057/112] Convert cover UI to Lit + ensure proper tilt rendering (#10671) --- gallery/src/demos/demo-more-info-cover.ts | 164 ++++++++++++++++++ src/components/ha-cover-controls.ts | 60 +++---- src/components/ha-cover-tilt-controls.ts | 57 +++--- src/data/cover.ts | 95 ++++++++++ .../more-info/controls/more-info-cover.js | 124 ------------- .../more-info/controls/more-info-cover.ts | 140 +++++++++++++++ .../entity-rows/hui-cover-entity-row.ts | 4 +- src/state-summary/state-card-cover.js | 68 -------- src/state-summary/state-card-cover.ts | 56 ++++++ src/util/cover-model.js | 149 ---------------- 10 files changed, 511 insertions(+), 406 deletions(-) create mode 100644 gallery/src/demos/demo-more-info-cover.ts create mode 100644 src/data/cover.ts delete mode 100644 src/dialogs/more-info/controls/more-info-cover.js create mode 100644 src/dialogs/more-info/controls/more-info-cover.ts delete mode 100644 src/state-summary/state-card-cover.js create mode 100644 src/state-summary/state-card-cover.ts delete mode 100644 src/util/cover-model.js diff --git a/gallery/src/demos/demo-more-info-cover.ts b/gallery/src/demos/demo-more-info-cover.ts new file mode 100644 index 0000000000..f715d5fc9c --- /dev/null +++ b/gallery/src/demos/demo-more-info-cover.ts @@ -0,0 +1,164 @@ +import { html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { customElement, property, query } from "lit/decorators"; +import "../../../src/components/ha-card"; +import { + SUPPORT_OPEN, + SUPPORT_STOP, + SUPPORT_CLOSE, + SUPPORT_SET_POSITION, + SUPPORT_OPEN_TILT, + SUPPORT_STOP_TILT, + SUPPORT_CLOSE_TILT, + SUPPORT_SET_TILT_POSITION, +} from "../../../src/data/cover"; +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 ENTITIES = [ + getEntity("cover", "position_buttons", "on", { + friendly_name: "Position Buttons", + supported_features: SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE, + }), + getEntity("cover", "position_slider_half", "on", { + friendly_name: "Position Half-Open", + supported_features: + SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE + SUPPORT_SET_POSITION, + current_position: 50, + }), + getEntity("cover", "position_slider_open", "on", { + friendly_name: "Position Open", + supported_features: + SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE + SUPPORT_SET_POSITION, + current_position: 100, + }), + getEntity("cover", "position_slider_closed", "on", { + friendly_name: "Position Closed", + supported_features: + SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE + SUPPORT_SET_POSITION, + current_position: 0, + }), + getEntity("cover", "tilt_buttons", "on", { + friendly_name: "Tilt Buttons", + supported_features: + SUPPORT_OPEN_TILT + SUPPORT_STOP_TILT + SUPPORT_CLOSE_TILT, + }), + getEntity("cover", "tilt_slider_half", "on", { + friendly_name: "Tilt Half-Open", + supported_features: + SUPPORT_OPEN_TILT + + SUPPORT_STOP_TILT + + SUPPORT_CLOSE_TILT + + SUPPORT_SET_TILT_POSITION, + current_tilt_position: 50, + }), + getEntity("cover", "tilt_slider_open", "on", { + friendly_name: "Tilt Open", + supported_features: + SUPPORT_OPEN_TILT + + SUPPORT_STOP_TILT + + SUPPORT_CLOSE_TILT + + SUPPORT_SET_TILT_POSITION, + current_tilt_position: 100, + }), + getEntity("cover", "tilt_slider_closed", "on", { + friendly_name: "Tilt Closed", + supported_features: + SUPPORT_OPEN_TILT + + SUPPORT_STOP_TILT + + SUPPORT_CLOSE_TILT + + SUPPORT_SET_TILT_POSITION, + current_tilt_position: 0, + }), + getEntity("cover", "position_slider_tilt_slider", "on", { + friendly_name: "Both Sliders", + supported_features: + SUPPORT_OPEN + + SUPPORT_STOP + + SUPPORT_CLOSE + + SUPPORT_SET_POSITION + + SUPPORT_OPEN_TILT + + SUPPORT_STOP_TILT + + SUPPORT_CLOSE_TILT + + SUPPORT_SET_TILT_POSITION, + current_position: 30, + current_tilt_position: 70, + }), + getEntity("cover", "position_tilt_slider", "on", { + friendly_name: "Position & Tilt Slider", + supported_features: + SUPPORT_OPEN + + SUPPORT_STOP + + SUPPORT_CLOSE + + SUPPORT_OPEN_TILT + + SUPPORT_STOP_TILT + + SUPPORT_CLOSE_TILT + + SUPPORT_SET_TILT_POSITION, + current_tilt_position: 70, + }), + getEntity("cover", "position_slider_tilt", "on", { + friendly_name: "Position Slider & Tilt", + supported_features: + SUPPORT_OPEN + + SUPPORT_STOP + + SUPPORT_CLOSE + + SUPPORT_SET_POSITION + + SUPPORT_OPEN_TILT + + SUPPORT_STOP_TILT + + SUPPORT_CLOSE_TILT, + current_position: 30, + }), + getEntity("cover", "position_slider_only_tilt_slider", "on", { + friendly_name: "Position Slider Only & Tilt Buttons", + supported_features: + SUPPORT_SET_POSITION + + SUPPORT_OPEN_TILT + + SUPPORT_STOP_TILT + + SUPPORT_CLOSE_TILT, + current_position: 30, + }), + getEntity("cover", "position_slider_only_tilt", "on", { + friendly_name: "Position Slider Only & Tilt", + supported_features: + SUPPORT_SET_POSITION + + SUPPORT_OPEN_TILT + + SUPPORT_STOP_TILT + + SUPPORT_CLOSE_TILT + + SUPPORT_SET_TILT_POSITION, + current_position: 30, + current_tilt_position: 70, + }), +]; + +@customElement("demo-more-info-cover") +class DemoMoreInfoCover 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-cover": DemoMoreInfoCover; + } +} diff --git a/src/components/ha-cover-controls.ts b/src/components/ha-cover-controls.ts index 85be8bf786..9f28b010d9 100644 --- a/src/components/ha-cover-controls.ts +++ b/src/components/ha-cover-controls.ts @@ -1,39 +1,30 @@ import { mdiStop } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { computeCloseIcon, computeOpenIcon } from "../common/entity/cover_icon"; +import { + CoverEntity, + isClosing, + isFullyClosed, + isFullyOpen, + isOpening, + supportsClose, + supportsOpen, + supportsStop, +} from "../data/cover"; import { UNAVAILABLE } from "../data/entity"; import type { HomeAssistant } from "../types"; -import CoverEntity from "../util/cover-model"; import "./ha-icon-button"; @customElement("ha-cover-controls") class HaCoverControls extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public stateObj!: HassEntity; - - @state() private _entityObj?: CoverEntity; - - public willUpdate(changedProperties: PropertyValues): void { - super.willUpdate(changedProperties); - - if (changedProperties.has("stateObj")) { - this._entityObj = new CoverEntity(this.hass, this.stateObj); - } - } + @property({ attribute: false }) public stateObj!: CoverEntity; protected render(): TemplateResult { - if (!this._entityObj) { + if (!this.stateObj) { return html``; } @@ -41,7 +32,7 @@ class HaCoverControls extends LitElement {
+ supportsFeature(stateObj, SUPPORT_OPEN); + +export const supportsClose = (stateObj) => + supportsFeature(stateObj, SUPPORT_CLOSE); + +export const supportsSetPosition = (stateObj) => + supportsFeature(stateObj, SUPPORT_SET_POSITION); + +export const supportsStop = (stateObj) => + supportsFeature(stateObj, SUPPORT_STOP); + +export const supportsOpenTilt = (stateObj) => + supportsFeature(stateObj, SUPPORT_OPEN_TILT); + +export const supportsCloseTilt = (stateObj) => + supportsFeature(stateObj, SUPPORT_CLOSE_TILT); + +export const supportsStopTilt = (stateObj) => + supportsFeature(stateObj, SUPPORT_STOP_TILT); + +export const supportsSetTiltPosition = (stateObj) => + supportsFeature(stateObj, SUPPORT_SET_TILT_POSITION); + +export function isFullyOpen(stateObj: CoverEntity) { + if (stateObj.attributes.current_position !== undefined) { + return stateObj.attributes.current_position === 100; + } + return stateObj.state === "open"; +} + +export function isFullyClosed(stateObj: CoverEntity) { + if (stateObj.attributes.current_position !== undefined) { + return stateObj.attributes.current_position === 0; + } + return stateObj.state === "closed"; +} + +export function isFullyOpenTilt(stateObj: CoverEntity) { + return stateObj.attributes.current_tilt_position === 100; +} + +export function isFullyClosedTilt(stateObj: CoverEntity) { + return stateObj.attributes.current_tilt_position === 0; +} + +export function isOpening(stateObj: CoverEntity) { + return stateObj.state === "opening"; +} + +export function isClosing(stateObj: CoverEntity) { + return stateObj.state === "closing"; +} + +export function isTiltOnly(stateObj: CoverEntity) { + const supportsCover = + supportsOpen(stateObj) || supportsClose(stateObj) || supportsStop(stateObj); + const supportsTilt = + supportsOpenTilt(stateObj) || + supportsCloseTilt(stateObj) || + supportsStopTilt(stateObj); + return supportsTilt && !supportsCover; +} + +interface CoverEntityAttributes extends HassEntityAttributeBase { + current_position: number; + current_tilt_position: number; +} + +export interface CoverEntity extends HassEntityBase { + attributes: CoverEntityAttributes; +} diff --git a/src/dialogs/more-info/controls/more-info-cover.js b/src/dialogs/more-info/controls/more-info-cover.js deleted file mode 100644 index 6431959047..0000000000 --- a/src/dialogs/more-info/controls/more-info-cover.js +++ /dev/null @@ -1,124 +0,0 @@ -import "@polymer/iron-flex-layout/iron-flex-layout-classes"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { attributeClassNames } from "../../../common/entity/attribute_class_names"; -import { featureClassNames } from "../../../common/entity/feature_class_names"; -import "../../../components/ha-cover-tilt-controls"; -import "../../../components/ha-labeled-slider"; -import LocalizeMixin from "../../../mixins/localize-mixin"; -import CoverEntity from "../../../util/cover-model"; - -const FEATURE_CLASS_NAMES = { - 4: "has-set_position", - 128: "has-set_tilt_position", -}; -class MoreInfoCover extends LocalizeMixin(PolymerElement) { - static get template() { - return html` - - -
-
- -
- -
- - - -
-
- - `; - } - - static get properties() { - return { - hass: Object, - stateObj: { - type: Object, - observer: "stateObjChanged", - }, - entityObj: { - type: Object, - computed: "computeEntityObj(hass, stateObj)", - }, - coverPositionSliderValue: Number, - coverTiltPositionSliderValue: Number, - }; - } - - computeEntityObj(hass, stateObj) { - return new CoverEntity(hass, stateObj); - } - - stateObjChanged(newVal) { - if (newVal) { - this.setProperties({ - coverPositionSliderValue: newVal.attributes.current_position, - coverTiltPositionSliderValue: newVal.attributes.current_tilt_position, - }); - } - } - - computeClassNames(stateObj) { - const classes = [ - attributeClassNames(stateObj, [ - "current_position", - "current_tilt_position", - ]), - featureClassNames(stateObj, FEATURE_CLASS_NAMES), - ]; - return classes.join(" "); - } - - coverPositionSliderChanged(ev) { - this.entityObj.setCoverPosition(ev.target.value); - } - - coverTiltPositionSliderChanged(ev) { - this.entityObj.setCoverTiltPosition(ev.target.value); - } -} - -customElements.define("more-info-cover", MoreInfoCover); diff --git a/src/dialogs/more-info/controls/more-info-cover.ts b/src/dialogs/more-info/controls/more-info-cover.ts new file mode 100644 index 0000000000..61faf09af2 --- /dev/null +++ b/src/dialogs/more-info/controls/more-info-cover.ts @@ -0,0 +1,140 @@ +import { css, CSSResult, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; +import { attributeClassNames } from "../../../common/entity/attribute_class_names"; +import { featureClassNames } from "../../../common/entity/feature_class_names"; +import "../../../components/ha-attributes"; +import "../../../components/ha-cover-tilt-controls"; +import "../../../components/ha-labeled-slider"; +import { + CoverEntity, + FEATURE_CLASS_NAMES, + isTiltOnly, + supportsSetPosition, + supportsSetTiltPosition, +} from "../../../data/cover"; +import { HomeAssistant } from "../../../types"; + +@customElement("more-info-cover") +class MoreInfoCover extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj!: CoverEntity; + + protected render(): TemplateResult { + if (!this.stateObj) { + return html``; + } + + const _isTiltOnly = isTiltOnly(this.stateObj); + + return html` +
+
+ +
+ +
+ ${supportsSetTiltPosition(this.stateObj) + ? // Either render the labeled slider and put the tilt buttons into its slot + // or (if tilt position is not supported and therefore no slider is shown) + // render a title
(same style as for a labeled slider) and directly put + // the tilt controls on the more-info. + html` + ${!_isTiltOnly + ? html` ` + : html``} + ` + : !_isTiltOnly + ? html` +
+ ${this.hass.localize("ui.card.cover.tilt_position")} +
+ + ` + : html``} +
+
+ + `; + } + + private _computeClassNames(stateObj) { + const classes = [ + attributeClassNames(stateObj, [ + "current_position", + "current_tilt_position", + ]), + featureClassNames(stateObj, FEATURE_CLASS_NAMES), + ]; + return classes.join(" "); + } + + private _coverPositionSliderChanged(ev) { + this.hass.callService("cover", "set_cover_position", { + entity_id: this.stateObj.entity_id, + position: ev.target.value, + }); + } + + private _coverTiltPositionSliderChanged(ev) { + this.hass.callService("cover", "set_cover_tilt_position", { + entity_id: this.stateObj.entity_id, + tilt_position: ev.target.value, + }); + } + + static get styles(): CSSResult { + return css` + .current_position, + .tilt { + max-height: 0px; + overflow: hidden; + } + + .has-set_position .current_position, + .has-current_position .current_position, + .has-open_tilt .tilt, + .has-close_tilt .tilt, + .has-stop_tilt .tilt, + .has-set_tilt_position .tilt, + .has-current_tilt_position .tilt { + max-height: 208px; + } + + /* from ha-labeled-slider for consistent look */ + .title { + margin: 5px 0 8px; + color: var(--primary-text-color); + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "more-info-cover": MoreInfoCover; + } +} diff --git a/src/panels/lovelace/entity-rows/hui-cover-entity-row.ts b/src/panels/lovelace/entity-rows/hui-cover-entity-row.ts index 5503eac028..b42d0dc64d 100644 --- a/src/panels/lovelace/entity-rows/hui-cover-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-cover-entity-row.ts @@ -9,8 +9,8 @@ import { import { customElement, property, state } from "lit/decorators"; import "../../../components/ha-cover-controls"; import "../../../components/ha-cover-tilt-controls"; +import { CoverEntity, isTiltOnly } from "../../../data/cover"; import { HomeAssistant } from "../../../types"; -import { isTiltOnly } from "../../../util/cover-model"; import { hasConfigOrEntityChanged } from "../common/has-changed"; import "../components/hui-generic-entity-row"; import { createEntityNotFoundWarning } from "../components/hui-warning"; @@ -38,7 +38,7 @@ class HuiCoverEntityRow extends LitElement implements LovelaceRow { return html``; } - const stateObj = this.hass.states[this._config.entity]; + const stateObj = this.hass.states[this._config.entity] as CoverEntity; if (!stateObj) { return html` diff --git a/src/state-summary/state-card-cover.js b/src/state-summary/state-card-cover.js deleted file mode 100644 index e2e6b54277..0000000000 --- a/src/state-summary/state-card-cover.js +++ /dev/null @@ -1,68 +0,0 @@ -import "@polymer/iron-flex-layout/iron-flex-layout-classes"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "../components/entity/state-info"; -import "../components/ha-cover-controls"; -import "../components/ha-cover-tilt-controls"; -import CoverEntity from "../util/cover-model"; - -class StateCardCover extends PolymerElement { - static get template() { - return html` - - - -
- ${this.stateInfoTemplate} -
- - -
-
- `; - } - - static get stateInfoTemplate() { - return html` - - `; - } - - static get properties() { - return { - hass: Object, - stateObj: Object, - inDialog: { - type: Boolean, - value: false, - }, - entityObj: { - type: Object, - computed: "computeEntityObj(hass, stateObj)", - }, - }; - } - - computeEntityObj(hass, stateObj) { - const entity = new CoverEntity(hass, stateObj); - return entity; - } -} -customElements.define("state-card-cover", StateCardCover); diff --git a/src/state-summary/state-card-cover.ts b/src/state-summary/state-card-cover.ts new file mode 100644 index 0000000000..9134ff9257 --- /dev/null +++ b/src/state-summary/state-card-cover.ts @@ -0,0 +1,56 @@ +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; +import "../components/entity/state-info"; +import "../components/ha-cover-controls"; +import "../components/ha-cover-tilt-controls"; +import { CoverEntity, isTiltOnly } from "../data/cover"; +import { haStyle } from "../resources/styles"; +import { HomeAssistant } from "../types"; + +@customElement("state-card-cover") +class StateCardCover extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj!: CoverEntity; + + @property({ type: Boolean }) public inDialog = false; + + protected render(): TemplateResult { + return html` +
+ + + +
+ `; + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + css` + :host { + line-height: 1.5; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "state-card-cover": StateCardCover; + } +} diff --git a/src/util/cover-model.js b/src/util/cover-model.js deleted file mode 100644 index eae1007cf8..0000000000 --- a/src/util/cover-model.js +++ /dev/null @@ -1,149 +0,0 @@ -import { supportsFeature } from "../common/entity/supports-feature"; - -/* eslint-enable no-bitwise */ -export default class CoverEntity { - constructor(hass, stateObj) { - this.hass = hass; - this.stateObj = stateObj; - this._attr = stateObj.attributes; - this._feat = this._attr.supported_features; - } - - get isFullyOpen() { - if (this._attr.current_position !== undefined) { - return this._attr.current_position === 100; - } - return this.stateObj.state === "open"; - } - - get isFullyClosed() { - if (this._attr.current_position !== undefined) { - return this._attr.current_position === 0; - } - return this.stateObj.state === "closed"; - } - - get isFullyOpenTilt() { - return this._attr.current_tilt_position === 100; - } - - get isFullyClosedTilt() { - return this._attr.current_tilt_position === 0; - } - - get isOpening() { - return this.stateObj.state === "opening"; - } - - get isClosing() { - return this.stateObj.state === "closing"; - } - - get supportsOpen() { - return supportsFeature(this.stateObj, 1); - } - - get supportsClose() { - return supportsFeature(this.stateObj, 2); - } - - get supportsSetPosition() { - return supportsFeature(this.stateObj, 4); - } - - get supportsStop() { - return supportsFeature(this.stateObj, 8); - } - - get supportsOpenTilt() { - return supportsFeature(this.stateObj, 16); - } - - get supportsCloseTilt() { - return supportsFeature(this.stateObj, 32); - } - - get supportsStopTilt() { - return supportsFeature(this.stateObj, 64); - } - - get supportsSetTiltPosition() { - return supportsFeature(this.stateObj, 128); - } - - get isTiltOnly() { - const supportsCover = - this.supportsOpen || this.supportsClose || this.supportsStop; - const supportsTilt = - this.supportsOpenTilt || this.supportsCloseTilt || this.supportsStopTilt; - return supportsTilt && !supportsCover; - } - - openCover() { - this.callService("open_cover"); - } - - closeCover() { - this.callService("close_cover"); - } - - stopCover() { - this.callService("stop_cover"); - } - - openCoverTilt() { - this.callService("open_cover_tilt"); - } - - closeCoverTilt() { - this.callService("close_cover_tilt"); - } - - stopCoverTilt() { - this.callService("stop_cover_tilt"); - } - - setCoverPosition(position) { - this.callService("set_cover_position", { position }); - } - - setCoverTiltPosition(tiltPosition) { - this.callService("set_cover_tilt_position", { - tilt_position: tiltPosition, - }); - } - - // helper method - - callService(service, data = {}) { - data.entity_id = this.stateObj.entity_id; - this.hass.callService("cover", service, data); - } -} - -export const supportsOpen = (stateObj) => supportsFeature(stateObj, 1); - -export const supportsClose = (stateObj) => supportsFeature(stateObj, 2); - -export const supportsSetPosition = (stateObj) => supportsFeature(stateObj, 4); - -export const supportsStop = (stateObj) => supportsFeature(stateObj, 8); - -export const supportsOpenTilt = (stateObj) => supportsFeature(stateObj, 16); - -export const supportsCloseTilt = (stateObj) => supportsFeature(stateObj, 32); - -export const supportsStopTilt = (stateObj) => supportsFeature(stateObj, 64); - -export const supportsSetTiltPosition = (stateObj) => - supportsFeature(stateObj, 128); - -export function isTiltOnly(stateObj) { - const supportsCover = - supportsOpen(stateObj) || supportsClose(stateObj) || supportsStop(stateObj); - const supportsTilt = - supportsOpenTilt(stateObj) || - supportsCloseTilt(stateObj) || - supportsStopTilt(stateObj); - return supportsTilt && !supportsCover; -} From e7fd75703f86473db30e479df6189ad0089c25e4 Mon Sep 17 00:00:00 2001 From: Luca Cavalli Date: Mon, 29 Nov 2021 10:30:27 +0100 Subject: [PATCH 058/112] Fixed ellipsis usage on graph legend entries. (#10707) --- src/components/chart/ha-chart-base.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index ca49c02522..afc048e5aa 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -95,7 +95,7 @@ export default class HaChartBase extends LitElement { borderColor: dataset.borderColor as string, })} >
- ${dataset.label} +
${dataset.label}
` )} @@ -278,11 +278,9 @@ export default class HaChartBase extends LitElement { } .chartLegend li { cursor: pointer; - display: inline-flex; + display: inline-grid; + grid-auto-flow: column; padding: 0 8px; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; box-sizing: border-box; align-items: center; color: var(--secondary-text-color); @@ -290,6 +288,11 @@ export default class HaChartBase extends LitElement { .chartLegend .hidden { text-decoration: line-through; } + .chartLegend .label { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } .chartLegend .bullet, .chartTooltip .bullet { border-width: 1px; From 9361e4cf9c6e65182b8f7cf923bb532dc96ff88e Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 29 Nov 2021 10:34:25 +0100 Subject: [PATCH 059/112] Ensure required translations are loaded in safe-mode (#10709) --- src/panels/config/logs/error-log-card.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/panels/config/logs/error-log-card.ts b/src/panels/config/logs/error-log-card.ts index bd9c7f9e7d..d5a4c2c156 100644 --- a/src/panels/config/logs/error-log-card.ts +++ b/src/panels/config/logs/error-log-card.ts @@ -20,7 +20,7 @@ class ErrorLogCard extends LitElement {
${this._errorHTML}
@@ -38,6 +38,7 @@ class ErrorLogCard extends LitElement { super.firstUpdated(changedProps); if (this.hass?.config.safe_mode) { + this.hass.loadFragmentTranslation("config"); this._refreshErrorLog(); } } From 0ef07e4835ee530e5aa801e5815f98c192396aef Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 29 Nov 2021 10:50:08 +0100 Subject: [PATCH 060/112] Ensure markdown card input is a string (#10705) --- src/components/ha-markdown-element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ha-markdown-element.ts b/src/components/ha-markdown-element.ts index f063d62169..09b29a6ecc 100644 --- a/src/components/ha-markdown-element.ts +++ b/src/components/ha-markdown-element.ts @@ -24,7 +24,7 @@ class HaMarkdownElement extends ReactiveElement { private async _render() { this.innerHTML = await renderMarkdown( - this.content, + String(this.content), { breaks: this.breaks, gfm: true, From a5be143c3bf7f04b077fea142c3162121ddfb88b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 29 Nov 2021 11:31:49 +0100 Subject: [PATCH 061/112] Fix chip text color variable overrides (#10722) --- src/panels/lovelace/cards/hui-alarm-panel-card.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panels/lovelace/cards/hui-alarm-panel-card.ts b/src/panels/lovelace/cards/hui-alarm-panel-card.ts index 370add6330..ba67d54d79 100644 --- a/src/panels/lovelace/cards/hui-alarm-panel-card.ts +++ b/src/panels/lovelace/cards/hui-alarm-panel-card.ts @@ -274,7 +274,7 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard { ha-chip { --ha-chip-background-color: var(--alarm-state-color); - --ha-chip-text-color: var(--text-primary-color); + --primary-text-color: var(--text-primary-color); line-height: initial; } From e91d1777d02c6c62e17885d38f2898e0fbc79415 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 29 Nov 2021 12:30:08 +0100 Subject: [PATCH 062/112] Ensure `conditional` rows getting `state_color` value (#10708) Co-authored-by: Bram Kragten --- src/panels/lovelace/cards/hui-entities-card.ts | 3 ++- .../lovelace/special-rows/hui-conditional-row.ts | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/panels/lovelace/cards/hui-entities-card.ts b/src/panels/lovelace/cards/hui-entities-card.ts index f733c678a3..b1a1e7d5c7 100644 --- a/src/panels/lovelace/cards/hui-entities-card.ts +++ b/src/panels/lovelace/cards/hui-entities-card.ts @@ -289,7 +289,8 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard { private renderEntity(entityConf: LovelaceRowConfig): TemplateResult { const element = createRowElement( - !("type" in entityConf) && this._config!.state_color + (!("type" in entityConf) || entityConf.type === "conditional") && + this._config!.state_color ? ({ state_color: true, ...(entityConf as EntityConfig), diff --git a/src/panels/lovelace/special-rows/hui-conditional-row.ts b/src/panels/lovelace/special-rows/hui-conditional-row.ts index 9251a72bd7..e17b07901c 100644 --- a/src/panels/lovelace/special-rows/hui-conditional-row.ts +++ b/src/panels/lovelace/special-rows/hui-conditional-row.ts @@ -1,7 +1,12 @@ import { customElement } from "lit/decorators"; +import { EntityCardConfig } from "../cards/types"; import { HuiConditionalBase } from "../components/hui-conditional-base"; import { createRowElement } from "../create-element/create-row-element"; -import { ConditionalRowConfig, LovelaceRow } from "../entity-rows/types"; +import { + ConditionalRowConfig, + EntityConfig, + LovelaceRow, +} from "../entity-rows/types"; @customElement("hui-conditional-row") class HuiConditionalRow extends HuiConditionalBase implements LovelaceRow { @@ -12,7 +17,14 @@ class HuiConditionalRow extends HuiConditionalBase implements LovelaceRow { throw new Error("No row configured"); } - this._element = createRowElement(config.row) as LovelaceRow; + this._element = createRowElement( + (config as EntityCardConfig).state_color + ? ({ + state_color: true, + ...(config.row as EntityConfig), + } as EntityConfig) + : config.row + ) as LovelaceRow; } } From d2c20837a5023e101b7b5191bcf8129019abc3f3 Mon Sep 17 00:00:00 2001 From: amitfin Date: Mon, 29 Nov 2021 20:49:33 +0200 Subject: [PATCH 063/112] Fixed invalid hour handling in AMPM mode (#10717) Co-authored-by: Bram Kragten --- src/components/ha-time-input.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ha-time-input.ts b/src/components/ha-time-input.ts index c4ca542801..1b14f40579 100644 --- a/src/components/ha-time-input.ts +++ b/src/components/ha-time-input.ts @@ -27,7 +27,7 @@ export class HaTimeInput extends LitElement { const parts = this.value?.split(":") || []; let hours = parts[0]; const numberHours = Number(parts[0]); - if (numberHours && useAMPM && numberHours > 12) { + if (numberHours && useAMPM && numberHours > 12 && numberHours < 24) { hours = String(numberHours - 12).padStart(2, "0"); } if (useAMPM && numberHours === 0) { From dbbf246060fe3df8370cf78f58964f6711416fb4 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 29 Nov 2021 23:41:21 +0100 Subject: [PATCH 064/112] Installation type property during onboarding was misspelled (#10721) --- src/onboarding/ha-onboarding.ts | 6 +----- src/onboarding/onboarding-restore-backup.ts | 7 ++----- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/onboarding/ha-onboarding.ts b/src/onboarding/ha-onboarding.ts index cc73fcffb9..7095e8a887 100644 --- a/src/onboarding/ha-onboarding.ts +++ b/src/onboarding/ha-onboarding.ts @@ -13,12 +13,11 @@ import { extractSearchParamsObject } from "../common/url/search-params"; import { subscribeOne } from "../common/util/subscribe-one"; import { AuthUrlSearchParams, hassUrl } from "../data/auth"; import { - InstallationType, + fetchInstallationType, fetchOnboardingOverview, OnboardingResponses, OnboardingStep, onboardIntegrationStep, - fetchInstallationType, } from "../data/onboarding"; import { subscribeUser } from "../data/ws-user"; import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin"; @@ -69,8 +68,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) { @state() private _steps?: OnboardingStep[]; - @state() private _installation_type?: InstallationType; - protected render(): TemplateResult { const step = this._curStep()!; @@ -90,7 +87,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) { ? html` ` diff --git a/src/onboarding/onboarding-restore-backup.ts b/src/onboarding/onboarding-restore-backup.ts index fdd6cd9286..658593f5c9 100644 --- a/src/onboarding/onboarding-restore-backup.ts +++ b/src/onboarding/onboarding-restore-backup.ts @@ -2,15 +2,15 @@ import "@material/mwc-button/mwc-button"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import "../../hassio/src/components/hassio-ansi-to-html"; -import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup"; import { showBackupUploadDialog } from "../../hassio/src/dialogs/backup/show-dialog-backup-upload"; +import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup"; import type { LocalizeFunc } from "../common/translations/localize"; import "../components/ha-card"; +import { fetchInstallationType } from "../data/onboarding"; import { makeDialogManager } from "../dialogs/make-dialog-manager"; import { ProvideHassLitMixin } from "../mixins/provide-hass-lit-mixin"; import { haStyle } from "../resources/styles"; import "./onboarding-loading"; -import { fetchInstallationType, InstallationType } from "../data/onboarding"; declare global { interface HASSDomEvents { @@ -26,9 +26,6 @@ class OnboardingRestoreBackup extends ProvideHassLitMixin(LitElement) { @property({ type: Boolean }) public restoring = false; - @property({ attribute: false }) - public installationType?: InstallationType; - protected render(): TemplateResult { return this.restoring ? html` Date: Mon, 29 Nov 2021 23:56:59 +0100 Subject: [PATCH 065/112] Dashboard tweaks (#10729) --- .../config/dashboard/ha-config-dashboard.ts | 28 ++----------------- .../config/dashboard/ha-config-updates.ts | 13 +++------ src/panels/config/ha-panel-config.ts | 2 +- src/translations/en.json | 2 +- 4 files changed, 9 insertions(+), 36 deletions(-) diff --git a/src/panels/config/dashboard/ha-config-dashboard.ts b/src/panels/config/dashboard/ha-config-dashboard.ts index 39fd28c5be..eebd88c05a 100644 --- a/src/panels/config/dashboard/ha-config-dashboard.ts +++ b/src/panels/config/dashboard/ha-config-dashboard.ts @@ -1,4 +1,3 @@ -import "./ha-config-updates"; import { mdiCloudLock } from "@mdi/js"; import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-toolbar/app-toolbar"; @@ -9,13 +8,14 @@ import "../../../components/ha-card"; import "../../../components/ha-icon-next"; import "../../../components/ha-menu-button"; import { CloudStatus } from "../../../data/cloud"; +import { SupervisorAvailableUpdates } from "../../../data/supervisor/supervisor"; import "../../../layouts/ha-app-layout"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import "../ha-config-section"; import { configSections } from "../ha-panel-config"; import "./ha-config-navigation"; -import { SupervisorAvailableUpdates } from "../../../data/supervisor/supervisor"; +import "./ha-config-updates"; @customElement("ha-config-dashboard") class HaConfigDashboard extends LitElement { @@ -91,21 +91,7 @@ class HaConfigDashboard extends LitElement { .showAdvanced=${this.showAdvanced} .pages=${configSections.dashboard} > - - ${!this.showAdvanced - ? html` -
- ${this.hass.localize( - "ui.panel.config.advanced_mode.hint_enable" - )} - ${this.hass.localize( - "ui.panel.config.advanced_mode.link_profile_page" - )}. -
- ` - : ""}`} + `} `; @@ -139,14 +125,6 @@ class HaConfigDashboard extends LitElement { padding: 16px; padding-bottom: 0; } - .promo-advanced { - text-align: center; - color: var(--secondary-text-color); - margin-bottom: 24px; - } - .promo-advanced a { - color: var(--secondary-text-color); - } :host([narrow]) ha-card { background-color: var(--primary-background-color); box-shadow: unset; diff --git a/src/panels/config/dashboard/ha-config-updates.ts b/src/panels/config/dashboard/ha-config-updates.ts index e5cb58ebc9..e1c35f3418 100644 --- a/src/panels/config/dashboard/ha-config-updates.ts +++ b/src/panels/config/dashboard/ha-config-updates.ts @@ -76,11 +76,12 @@ class HaConfigUpdates extends LitElement { ` )} - ${!this._showAll && !this.narrow ? html`
` : ""} ${!this._showAll && this.supervisorUpdates.length >= 4 ? html` ` : ""} @@ -122,13 +123,7 @@ class HaConfigUpdates extends LitElement { button.show-all { color: var(--primary-color); text-decoration: none; - margin: 8px 16px; - } - .divider::before { - content: " "; - display: block; - height: 1px; - background-color: var(--divider-color); + margin: 16px; } `, ]; diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index 784e4d7a6b..f3dbc1e2a9 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -56,7 +56,7 @@ export const configSections: { [name: string]: PageNavigation[] } = { }, { path: "/config/automation", - name: "Automations", + name: "Automations & Scenes", description: "Automations, blueprints, scenes and scripts", iconPath: mdiRobot, iconColor: "#518C43", diff --git a/src/translations/en.json b/src/translations/en.json index f3bbe68476..91b813df21 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -930,7 +930,7 @@ "title": "{count} {count, plural,\n one {update}\n other {updates}\n}", "unable_to_fetch": "Unable to fetch available updates", "version_available": "Version {version_available} is available", - "show_all_updates": "Show all updates", + "more_updates": "+ {count} Updates", "show": "show" }, "areas": { From 49e39644f33c32be2d37bd40208cbeddb12b8c07 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 29 Nov 2021 16:56:08 -0800 Subject: [PATCH 066/112] Tweak how scenes behave in generated lovelace (#10730) --- .../common/generate-lovelace-config.ts | 31 ++++++++++++++++--- .../lovelace/components/hui-buttons-base.ts | 2 ++ .../hui-buttons-header-footer.ts | 21 ++++++++++--- 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/panels/lovelace/common/generate-lovelace-config.ts b/src/panels/lovelace/common/generate-lovelace-config.ts index e75fb59771..cc608f06e8 100644 --- a/src/panels/lovelace/common/generate-lovelace-config.ts +++ b/src/panels/lovelace/common/generate-lovelace-config.ts @@ -87,7 +87,8 @@ const splitByAreas = ( export const computeCards = ( states: Array<[string, HassEntity?]>, - entityCardOptions: Partial + entityCardOptions: Partial, + renderFooterEntities = true ): LovelaceCardConfig[] => { const cards: LovelaceCardConfig[] = []; @@ -146,12 +147,28 @@ export const computeCards = ( show_forecast: false, }; cards.push(cardConfig); - } else if (domain === "scene" || domain === "script") { - footerEntities.push({ + } else if ( + renderFooterEntities && + (domain === "scene" || domain === "script") + ) { + const conf: typeof footerEntities[0] = { entity: entityId, show_icon: true, show_name: true, - }); + }; + let name: string | undefined; + if ( + titlePrefix && + stateObj && + // eslint-disable-next-line no-cond-assign + (name = stripPrefixFromEntityName( + computeStateName(stateObj), + titlePrefix + )) + ) { + conf.name = name; + } + footerEntities.push(conf); } else if ( domain === "sensor" && stateObj?.attributes.device_class === SENSOR_DEVICE_CLASS_BATTERY @@ -177,6 +194,12 @@ export const computeCards = ( } } + // If we ended up with footer entities but no normal entities, + // render the footer entities as normal entities. + if (entities.length === 0 && footerEntities.length > 0) { + return computeCards(states, entityCardOptions, false); + } + if (entities.length > 0 || footerEntities.length > 0) { const card: EntitiesCardConfig = { type: "entities", diff --git a/src/panels/lovelace/components/hui-buttons-base.ts b/src/panels/lovelace/components/hui-buttons-base.ts index 4c7692adef..ac59da37f6 100644 --- a/src/panels/lovelace/components/hui-buttons-base.ts +++ b/src/panels/lovelace/components/hui-buttons-base.ts @@ -65,6 +65,8 @@ export class HuiButtonsBase extends LitElement { :host { display: flex; justify-content: space-evenly; + flex-wrap: wrap; + padding: 0 8px; } div { cursor: pointer; diff --git a/src/panels/lovelace/header-footer/hui-buttons-header-footer.ts b/src/panels/lovelace/header-footer/hui-buttons-header-footer.ts index c26da98184..01bf77ca05 100644 --- a/src/panels/lovelace/header-footer/hui-buttons-header-footer.ts +++ b/src/panels/lovelace/header-footer/hui-buttons-header-footer.ts @@ -1,5 +1,6 @@ import { html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { computeDomain } from "../../../common/entity/compute_domain"; import { HomeAssistant } from "../../../types"; import { processConfigEntities } from "../common/process-config-entities"; import "../components/hui-buttons-base"; @@ -26,11 +27,21 @@ export class HuiButtonsHeaderFooter public setConfig(config: ButtonsHeaderFooterConfig): void { this._configEntities = processConfigEntities(config.entities).map( - (entityConfig) => ({ - tap_action: { action: "toggle" }, - hold_action: { action: "more-info" }, - ...entityConfig, - }) + (entityConfig) => { + const conf = { + tap_action: { action: "toggle" }, + hold_action: { action: "more-info" }, + ...entityConfig, + }; + if (computeDomain(entityConfig.entity) === "scene") { + conf.tap_action = { + action: "call-service", + service: "scene.turn_on", + target: { entity_id: conf.entity }, + }; + } + return conf; + } ); } From 67f06112c6aebda47d5cb0027faa30313cc72ae2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 29 Nov 2021 16:57:58 -0800 Subject: [PATCH 067/112] Bumped version to 20211130.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d2623c8494..d90ddf3a43 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20211123.0", + version="20211130.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/frontend", author="The Home Assistant Authors", From 02644b923f2dcaf2965842c44a8e88bbe752aae6 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 29 Nov 2021 23:48:24 -0800 Subject: [PATCH 068/112] Improve hls stream view error handling (#10714) Co-authored-by: Bram Kragten --- src/components/ha-hls-player.ts | 55 +++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/src/components/ha-hls-player.ts b/src/components/ha-hls-player.ts index ac05a64317..dbabf84b20 100644 --- a/src/components/ha-hls-player.ts +++ b/src/components/ha-hls-player.ts @@ -7,10 +7,11 @@ import { PropertyValues, TemplateResult, } from "lit"; -import { customElement, property, query } from "lit/decorators"; +import { customElement, property, query, state } from "lit/decorators"; import { nextRender } from "../common/util/render-status"; import { getExternalConfig } from "../external_app/external_config"; import type { HomeAssistant } from "../types"; +import "./ha-alert"; type HlsLite = Omit< HlsType, @@ -41,6 +42,8 @@ class HaHLSPlayer extends LitElement { // don't cache this, as we remove it on disconnects @query("video") private _videoEl!: HTMLVideoElement; + @state() private _error?: string; + private _hlsPolyfillInstance?: HlsLite; private _exoPlayer = false; @@ -58,6 +61,9 @@ class HaHLSPlayer extends LitElement { } protected render(): TemplateResult { + if (this._error) { + return html`${this._error}`; + } return html`
`} - + ${!this.narrow ? html`` : ""} ` From ceac9834b9baeb2ff8d59bd92c5a66597a07c6b0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 1 Dec 2021 04:54:28 -0800 Subject: [PATCH 075/112] Change the area of scenes in editor (#10731) --- src/data/scene.ts | 11 +- src/panels/config/scene/ha-scene-editor.ts | 129 +++++++++++++++++++-- src/translations/en.json | 1 + 3 files changed, 127 insertions(+), 14 deletions(-) diff --git a/src/data/scene.ts b/src/data/scene.ts index 5851c48584..568db76998 100644 --- a/src/data/scene.ts +++ b/src/data/scene.ts @@ -18,10 +18,15 @@ export const SCENE_IGNORED_DOMAINS = [ "zone", ]; -let inititialSceneEditorData: Partial | undefined; +let inititialSceneEditorData: + | { config?: Partial; areaId?: string } + | undefined; -export const showSceneEditor = (data?: Partial) => { - inititialSceneEditorData = data; +export const showSceneEditor = ( + config?: Partial, + areaId?: string +) => { + inititialSceneEditorData = { config, areaId }; navigate("/config/scene/edit/new"); }; diff --git a/src/panels/config/scene/ha-scene-editor.ts b/src/panels/config/scene/ha-scene-editor.ts index 0b09c7239d..a59ecbd6b4 100644 --- a/src/panels/config/scene/ha-scene-editor.ts +++ b/src/panels/config/scene/ha-scene-editor.ts @@ -32,6 +32,7 @@ import "../../../components/ha-card"; import "../../../components/ha-fab"; import "../../../components/ha-icon-button"; import "../../../components/ha-icon-picker"; +import "../../../components/ha-area-picker"; import "../../../components/ha-svg-icon"; import { computeDeviceName, @@ -41,6 +42,7 @@ import { import { EntityRegistryEntry, subscribeEntityRegistry, + updateEntityRegistryEntry, } from "../../../data/entity_registry"; import { activateScene, @@ -121,6 +123,22 @@ export class HaSceneEditor extends SubscribeMixin( private _activateContextId?: string; + @state() private _saving = false; + + // undefined means not set in this session + // null means picked nothing. + @state() private _updatedAreaId?: string | null; + + // Callback to be called when scene is set. + private _scenesSet?: () => void; + + private _getRegistryAreaId = memoizeOne( + (entries: EntityRegistryEntry[], entity_id: string) => { + const entry = entries.find((ent) => ent.entity_id === entity_id); + return entry ? entry.area_id : null; + } + ); + private _getEntitiesDevices = memoizeOne( ( entities: string[], @@ -287,6 +305,16 @@ export class HaSceneEditor extends SubscribeMixin( @value-changed=${this._valueChanged} > + +
@@ -444,8 +472,9 @@ export class HaSceneEditor extends SubscribeMixin( slot="fab" .label=${this.hass.localize("ui.panel.config.scene.editor.save")} extended + .disabled=${this._saving} @click=${this._saveScene} - class=${classMap({ dirty: this._dirty })} + class=${classMap({ dirty: this._dirty, saving: this._saving })} > @@ -474,12 +503,15 @@ export class HaSceneEditor extends SubscribeMixin( this._config = { name: this.hass.localize("ui.panel.config.scene.editor.default_name"), entities: {}, - ...initData, + ...initData?.config, }; this._initEntities(this._config); - if (initData) { - this._dirty = true; + if (initData?.areaId) { + this._updatedAreaId = initData.areaId; } + this._dirty = + initData !== undefined && + (initData.areaId !== undefined || initData.config !== undefined); } if (changedProps.has("_entityRegistryEntries")) { @@ -514,6 +546,9 @@ export class HaSceneEditor extends SubscribeMixin( ) { this._setScene(); } + if (this._scenesSet && changedProps.has("scenes")) { + this._scenesSet(); + } } private async _handleMenuAction(ev: CustomEvent) { @@ -689,6 +724,21 @@ export class HaSceneEditor extends SubscribeMixin( this._dirty = true; } + private _areaChanged(ev: CustomEvent) { + const newValue = ev.detail.value === "" ? null : ev.detail.value; + + if (newValue === (this._sceneAreaIdWithUpdates || "")) { + return; + } + + if (newValue === this._sceneAreaIdCurrent) { + this._updatedAreaId = undefined; + } else { + this._updatedAreaId = newValue; + this._dirty = true; + } + } + private _stateChanged(event: HassEvent) { if ( event.context.id !== this._activateContextId && @@ -749,13 +799,16 @@ export class HaSceneEditor extends SubscribeMixin( // Wait for dialog to complete closing await new Promise((resolve) => setTimeout(resolve, 0)); } - showSceneEditor({ - ...this._config, - id: undefined, - name: `${this._config?.name} (${this.hass.localize( - "ui.panel.config.scene.picker.duplicate" - )})`, - }); + showSceneEditor( + { + ...this._config, + id: undefined, + name: `${this._config?.name} (${this.hass.localize( + "ui.panel.config.scene.picker.duplicate" + )})`, + }, + this._sceneAreaIdCurrent || undefined + ); } private _calculateStates(): SceneEntities { @@ -792,7 +845,41 @@ export class HaSceneEditor extends SubscribeMixin( const id = !this.sceneId ? "" + Date.now() : this.sceneId!; this._config = { ...this._config!, entities: this._calculateStates() }; try { + this._saving = true; await saveScene(this.hass, id, this._config); + + if (this._updatedAreaId !== undefined) { + let scene = + this._scene || + this.scenes.find( + (entity: SceneEntity) => entity.attributes.id === id + ); + + if (!scene) { + try { + await new Promise((resolve, reject) => { + setTimeout(reject, 3000); + this._scenesSet = resolve; + }); + scene = this.scenes.find( + (entity: SceneEntity) => entity.attributes.id === id + ); + } catch (err) { + // We do nothing. + } finally { + this._scenesSet = undefined; + } + } + + if (scene) { + await updateEntityRegistryEntry(this.hass, scene.entity_id, { + area_id: this._updatedAreaId, + }); + } + + this._updatedAreaId = undefined; + } + this._dirty = false; if (!this.sceneId) { @@ -804,6 +891,8 @@ export class HaSceneEditor extends SubscribeMixin( message: err.body.message || err.message, }); throw err; + } finally { + this._saving = false; } } @@ -811,6 +900,21 @@ export class HaSceneEditor extends SubscribeMixin( this._saveScene(); } + private get _sceneAreaIdWithUpdates(): string | undefined | null { + return this._updatedAreaId !== undefined + ? this._updatedAreaId + : this._sceneAreaIdCurrent; + } + + private get _sceneAreaIdCurrent(): string | undefined | null { + return this._scene + ? this._getRegistryAreaId( + this._entityRegistryEntries, + this._scene.entity_id + ) + : undefined; + } + static get styles(): CSSResultGroup { return [ haStyle, @@ -877,6 +981,9 @@ export class HaSceneEditor extends SubscribeMixin( ha-fab.dirty { bottom: 0; } + ha-fab.saving { + opacity: var(--light-disabled-opacity); + } `, ]; } diff --git a/src/translations/en.json b/src/translations/en.json index 8f8e05866a..e14e4f0652 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1900,6 +1900,7 @@ "unsaved_confirm": "You have unsaved changes. Are you sure you want to leave?", "name": "Name", "icon": "Icon", + "area": "Area", "devices": { "header": "Devices", "introduction": "Add the devices that you want to be included in your scene. Set all the devices to the state you want for this scene.", From 87f7981144da9e4bc9c9db6b70475af123478550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 1 Dec 2021 13:55:01 +0100 Subject: [PATCH 076/112] Fix faded element in change log (#10737) --- src/components/ha-faded.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/ha-faded.ts b/src/components/ha-faded.ts index 974956dcfd..8f53e1cb6c 100644 --- a/src/components/ha-faded.ts +++ b/src/components/ha-faded.ts @@ -42,9 +42,7 @@ class HaFaded extends LitElement { private _setShowContent() { const height = this._slottedHeight; - if (height !== 0 && height <= this.fadedHeight + 50) { - this._contentShown = true; - } + this._contentShown = height !== 0 && height <= this.fadedHeight + 50; } protected firstUpdated(changedProps) { From 01049e8eb81181ffa2cc354c29f2d542e1bde0e3 Mon Sep 17 00:00:00 2001 From: Matthias de Baat Date: Wed, 1 Dec 2021 15:10:32 +0100 Subject: [PATCH 077/112] Updated text (#10747) Co-authored-by: Bram Kragten --- src/translations/en.json | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/translations/en.json b/src/translations/en.json index e14e4f0652..1846d12698 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -306,11 +306,11 @@ }, "components": { "logbook": { - "entries_not_found": "No logbook entries found.", + "entries_not_found": "No logbook events found.", "by": "by", "by_service": "by service", "show_trace": "[%key:ui::panel::config::automation::editor::show_trace%]", - "retrieval_error": "Error during logbook entry retrieval", + "retrieval_error": "Could not load logbook", "messages": { "was_away": "was detected away", "was_at_state": "was detected at {state}", @@ -356,15 +356,15 @@ }, "target-picker": { "expand": "Expand", - "expand_area_id": "Expand this area into the separate devices and entities that it contains. After expanding, it will not update the devices and entities when the area changes.", - "expand_device_id": "Expand this device into the separate entities that it contains. After expanding, it will not update the entities when the device changes.", + "expand_area_id": "Split this area into separate devices and entities.", + "expand_device_id": "Split this device into separate entities.", "remove": "Remove", "remove_area_id": "Remove area", "remove_device_id": "Remove device", "remove_entity_id": "Remove entity", - "add_area_id": "Pick area", - "add_device_id": "Pick device", - "add_entity_id": "Pick entity" + "add_area_id": "Choose area", + "add_device_id": "Choose device", + "add_entity_id": "Choose entity" }, "user-picker": { "no_user": "No user", @@ -412,11 +412,11 @@ "error": { "no_supervisor": { "title": "No Supervisor", - "description": "No Supervisor found, so add-ons could not be loaded." + "description": "Add-ons are not supported." }, "fetch_addons": { - "title": "Error fetching add-ons", - "description": "Fetching add-ons returned an error." + "title": "Error loading add-ons", + "description": "There was an error loading add-ons." } } }, @@ -694,7 +694,7 @@ "icon": "Icon", "icon_error": "Icons should be in the format 'prefix:iconname', e.g. 'mdi:home'", "entity_id": "Entity ID", - "unavailable": "This entity is not currently available.", + "unavailable": "This entity is unavailable.", "enabled_label": "Enable entity", "enabled_cause": "Disabled by {cause}.", "device_disabled": "The device of this entity is disabled.", @@ -703,7 +703,7 @@ "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", "delete": "Delete", - "confirm_delete": "Are you sure you want to delete this entry?", + "confirm_delete": "Are you sure you want to delete this entity?", "update": "Update", "note": "Note: This might not work yet with all integrations.", "advanced": "Advanced settings", @@ -927,7 +927,7 @@ }, "updates": { "title": "{count} {count, plural,\n one {update}\n other {updates}\n}", - "unable_to_fetch": "Unable to fetch available updates", + "unable_to_fetch": "Unable to load updates", "version_available": "Version {version_available} is available", "more_updates": "+ {count} Updates", "show": "show" @@ -1195,11 +1195,11 @@ }, "core": { "caption": "General", - "description": "Unit system, location, time zone & other general parameters", + "description": "Location, network and analytics", "section": { "core": { "header": "General Configuration", - "introduction": "Changing your configuration can be a tiresome process. We know. This section will try to make your life a little bit easier.", + "introduction": "Manage your location, network and analytics.", "core_config": { "edit_requires_storage": "Editor disabled because config stored in configuration.yaml.", "location_name": "Name of your Home Assistant installation", @@ -1397,7 +1397,7 @@ }, "server_management": { "heading": "Server management", - "introduction": "Control your Home Assistant server… from Home Assistant.", + "introduction": "Control your Home Assistant.", "restart": "Restart", "confirm_restart": "Are you sure you want to restart Home Assistant?", "stop": "Stop", From 68373e63725d348416a838049a74fb837733dab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 1 Dec 2021 17:46:38 +0100 Subject: [PATCH 078/112] Focus Add-ons & Backups in config panel when clicking Supervisor in sidebar (#10745) Co-authored-by: Bram Kragten --- src/components/ha-sidebar.ts | 4 +++- .../config/dashboard/ha-config-dashboard.ts | 2 ++ .../config/dashboard/ha-config-navigation.ts | 24 ++++++++++++++++++- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index fa2ce7c2a5..af59b17c51 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -370,7 +370,9 @@ class HaSidebar extends LitElement { private _renderPanels(panels: PanelInfo[]) { return panels.map((panel) => this._renderPanel( - panel.url_path, + panel.url_path === "hassio" + ? "config/dashboard?focusedPath=hassio" + : panel.url_path, panel.url_path === this.hass.defaultPanel ? panel.title || this.hass.localize("panel.states") : this.hass.localize(`panel.${panel.title}`) || panel.title, diff --git a/src/panels/config/dashboard/ha-config-dashboard.ts b/src/panels/config/dashboard/ha-config-dashboard.ts index 3d7dc017f8..848eb98753 100644 --- a/src/panels/config/dashboard/ha-config-dashboard.ts +++ b/src/panels/config/dashboard/ha-config-dashboard.ts @@ -11,6 +11,7 @@ import { } from "lit"; import { customElement, property, state } from "lit/decorators"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; +import { extractSearchParam } from "../../../common/url/search-params"; import "../../../components/ha-card"; import "../../../components/ha-icon-next"; import "../../../components/ha-menu-button"; @@ -135,6 +136,7 @@ class HaConfigDashboard extends LitElement { .narrow=${this.narrow} .showAdvanced=${this.showAdvanced} .pages=${configSections.dashboard} + .focusedPath=${extractSearchParam("focusedPath")} > `} diff --git a/src/panels/config/dashboard/ha-config-navigation.ts b/src/panels/config/dashboard/ha-config-navigation.ts index 1f91f19cdf..4a505ffb46 100644 --- a/src/panels/config/dashboard/ha-config-navigation.ts +++ b/src/panels/config/dashboard/ha-config-navigation.ts @@ -1,6 +1,13 @@ import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-item-body"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; import { customElement, property } from "lit/decorators"; import { canShowPage } from "../../../common/config/can_show_page"; import "../../../components/ha-card"; @@ -19,6 +26,21 @@ class HaConfigNavigation extends LitElement { @property() public pages!: PageNavigation[]; + @property() public focusedPath?: string | null; + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + if (!this.focusedPath) { + return; + } + for (const a of this.shadowRoot!.querySelectorAll("a")) { + if (a.href.endsWith(this.focusedPath)) { + a.querySelector("paper-icon-item")?.focus(); + break; + } + } + } + protected render(): TemplateResult { return html` ${this.pages.map((page) => From 4b49da58b19bd20ac75a2ab21d2f110ad33f5246 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 1 Dec 2021 23:12:52 +0100 Subject: [PATCH 079/112] Add SmartStart/QR scan support for Z-Wave JS (#10726) --- build-scripts/gulp/gather-static.js | 8 + package.json | 1 + src/components/ha-qr-scanner.ts | 162 ++++++++++ src/data/zwave_js.ts | 122 ++++++-- .../zwave_js/ha-device-actions-zwave_js.ts | 4 +- .../zwave_js/ha-device-info-zwave_js.ts | 12 +- .../zwave/zwave-migration.ts | 23 +- .../zwave_js/dialog-zwave_js-add-node.ts | 293 +++++++++++++++--- .../zwave_js/dialog-zwave_js-heal-network.ts | 18 +- .../zwave_js/dialog-zwave_js-heal-node.ts | 12 +- .../dialog-zwave_js-reinterview-node.ts | 4 +- .../dialog-zwave_js-remove-failed-node.ts | 4 +- .../zwave_js/zwave_js-config-dashboard.ts | 16 +- .../zwave_js/zwave_js-node-config.ts | 12 +- src/translations/en.json | 13 +- yarn.lock | 8 + 16 files changed, 602 insertions(+), 110 deletions(-) create mode 100644 src/components/ha-qr-scanner.ts diff --git a/build-scripts/gulp/gather-static.js b/build-scripts/gulp/gather-static.js index c0f36d02af..306ae3158a 100644 --- a/build-scripts/gulp/gather-static.js +++ b/build-scripts/gulp/gather-static.js @@ -79,6 +79,11 @@ function copyFonts(staticDir) { ); } +function copyQrScannerWorker(staticDir) { + const staticPath = genStaticPath(staticDir); + copyFileDir(npmPath("qr-scanner/qr-scanner-worker.min.js"), staticPath("js")); +} + function copyMapPanel(staticDir) { const staticPath = genStaticPath(staticDir); copyFileDir( @@ -125,6 +130,9 @@ gulp.task("copy-static-app", async () => { // Panel assets copyMapPanel(staticDir); + + // Qr Scanner assets + copyQrScannerWorker(staticDir); }); gulp.task("copy-static-demo", async () => { diff --git a/package.json b/package.json index 0d9726b250..df1ce7d236 100644 --- a/package.json +++ b/package.json @@ -115,6 +115,7 @@ "node-vibrant": "3.2.1-alpha.1", "proxy-polyfill": "^0.3.2", "punycode": "^2.1.1", + "qr-scanner": "^1.3.0", "qrcode": "^1.4.4", "regenerator-runtime": "^0.13.8", "resize-observer-polyfill": "^1.5.1", diff --git a/src/components/ha-qr-scanner.ts b/src/components/ha-qr-scanner.ts new file mode 100644 index 0000000000..0872c2556d --- /dev/null +++ b/src/components/ha-qr-scanner.ts @@ -0,0 +1,162 @@ +import "@material/mwc-list/mwc-list-item"; +import "@material/mwc-select/mwc-select"; +import type { Select } from "@material/mwc-select/mwc-select"; +import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; +import type QrScanner from "qr-scanner"; +import { fireEvent } from "../common/dom/fire_event"; +import { stopPropagation } from "../common/dom/stop_propagation"; +import { LocalizeFunc } from "../common/translations/localize"; +import "./ha-alert"; + +@customElement("ha-qr-scanner") +class HaQrScanner extends LitElement { + @property() localize!: LocalizeFunc; + + @state() private _cameras?: QrScanner.Camera[]; + + @state() private _error?: string; + + private _qrScanner?: QrScanner; + + private _qrNotFoundCount = 0; + + @query("video", true) private _video!: HTMLVideoElement; + + @query("#canvas-container", true) private _canvasContainer!: HTMLDivElement; + + public disconnectedCallback(): void { + super.disconnectedCallback(); + this._qrNotFoundCount = 0; + if (this._qrScanner) { + this._qrScanner.stop(); + this._qrScanner.destroy(); + this._qrScanner = undefined; + } + while (this._canvasContainer.lastChild) { + this._canvasContainer.removeChild(this._canvasContainer.lastChild); + } + } + + public connectedCallback(): void { + super.connectedCallback(); + if (this.hasUpdated && navigator.mediaDevices) { + this._loadQrScanner(); + } + } + + protected firstUpdated() { + if (navigator.mediaDevices) { + this._loadQrScanner(); + } + } + + protected updated(changedProps: PropertyValues) { + if (changedProps.has("_error") && this._error) { + fireEvent(this, "qr-code-error", { message: this._error }); + } + } + + protected render(): TemplateResult { + return html`${this._cameras && this._cameras.length > 1 + ? html` + ${this._cameras!.map( + (camera) => html` + ${camera.label} + ` + )} + ` + : ""} + ${this._error + ? html`${this._error}` + : ""} + ${navigator.mediaDevices + ? html` +
` + : html`${!window.isSecureContext + ? "You can only use your camera to scan a QR core when using HTTPS." + : "Your browser doesn't support QR scanning."}`}`; + } + + private async _loadQrScanner() { + const QrScanner = (await import("qr-scanner")).default; + if (!(await QrScanner.hasCamera())) { + this._error = "No camera found"; + return; + } + QrScanner.WORKER_PATH = "/static/js/qr-scanner-worker.min.js"; + this._listCameras(QrScanner); + this._qrScanner = new QrScanner( + this._video, + this._qrCodeScanned, + this._qrCodeError + ); + // @ts-ignore + const canvas = this._qrScanner.$canvas; + this._canvasContainer.appendChild(canvas); + canvas.style.display = "block"; + try { + await this._qrScanner.start(); + } catch (err: any) { + this._error = err; + } + } + + private async _listCameras(qrScanner: typeof QrScanner): Promise { + this._cameras = await qrScanner.listCameras(true); + } + + private _qrCodeError = (err: any) => { + if (err === "No QR code found") { + this._qrNotFoundCount++; + if (this._qrNotFoundCount === 250) { + this._error = err; + } + return; + } + this._error = err.message || err; + // eslint-disable-next-line no-console + console.log(err); + }; + + private _qrCodeScanned = async (qrCodeString: string): Promise => { + this._qrNotFoundCount = 0; + fireEvent(this, "qr-code-scanned", { value: qrCodeString }); + }; + + private _cameraChanged(ev: CustomEvent): void { + this._qrScanner?.setCamera((ev.target as Select).value); + } + + static styles = css` + canvas { + width: 100%; + } + mwc-select { + width: 100%; + margin-bottom: 16px; + } + `; +} + +declare global { + // for fire event + interface HASSDomEvents { + "qr-code-scanned": { value: string }; + "qr-code-error": { message: string }; + } + + interface HTMLElementTagNameMap { + "ha-qr-scanner": HaQrScanner; + } +} diff --git a/src/data/zwave_js.ts b/src/data/zwave_js.ts index fbd9113d5c..cf278eaf2b 100644 --- a/src/data/zwave_js.ts +++ b/src/data/zwave_js.ts @@ -57,6 +57,45 @@ export enum SecurityClass { S0_Legacy = 7, } +/** A named list of Z-Wave features */ +export enum ZWaveFeature { + // Available starting with Z-Wave SDK 6.81 + SmartStart, +} + +enum QRCodeVersion { + S2 = 0, + SmartStart = 1, +} + +enum Protocols { + ZWave = 0, + ZWaveLongRange = 1, +} +export interface QRProvisioningInformation { + version: QRCodeVersion; + securityClasses: SecurityClass[]; + dsk: string; + genericDeviceClass: number; + specificDeviceClass: number; + installerIconType: number; + manufacturerId: number; + productType: number; + productId: number; + applicationVersion: string; + maxInclusionRequestInterval?: number | undefined; + uuid?: string | undefined; + supportedProtocols?: Protocols[] | undefined; +} + +export interface PlannedProvisioningEntry { + /** The device specific key (DSK) in the form aaaaa-bbbbb-ccccc-ddddd-eeeee-fffff-11111-22222 */ + dsk: string; + security_classes: SecurityClass[]; +} + +export const MINIMUM_QR_STRING_LENGTH = 52; + export interface ZWaveJSNodeIdentifiers { home_id: string; node_id: number; @@ -197,7 +236,7 @@ export const migrateZwave = ( dry_run, }); -export const fetchNetworkStatus = ( +export const fetchZwaveNetworkStatus = ( hass: HomeAssistant, entry_id: string ): Promise => @@ -206,7 +245,7 @@ export const fetchNetworkStatus = ( entry_id, }); -export const fetchDataCollectionStatus = ( +export const fetchZwaveDataCollectionStatus = ( hass: HomeAssistant, entry_id: string ): Promise => @@ -215,7 +254,7 @@ export const fetchDataCollectionStatus = ( entry_id, }); -export const setDataCollectionPreference = ( +export const setZwaveDataCollectionPreference = ( hass: HomeAssistant, entry_id: string, opted_in: boolean @@ -226,25 +265,31 @@ export const setDataCollectionPreference = ( opted_in, }); -export const subscribeAddNode = ( +export const subscribeAddZwaveNode = ( hass: HomeAssistant, entry_id: string, callbackFunction: (message: any) => void, - inclusion_strategy: InclusionStrategy = InclusionStrategy.Default + inclusion_strategy: InclusionStrategy = InclusionStrategy.Default, + qr_provisioning_information?: QRProvisioningInformation, + qr_code_string?: string, + planned_provisioning_entry?: PlannedProvisioningEntry ): Promise => hass.connection.subscribeMessage((message) => callbackFunction(message), { type: "zwave_js/add_node", entry_id: entry_id, inclusion_strategy, + qr_code_string, + qr_provisioning_information, + planned_provisioning_entry, }); -export const stopInclusion = (hass: HomeAssistant, entry_id: string) => +export const stopZwaveInclusion = (hass: HomeAssistant, entry_id: string) => hass.callWS({ type: "zwave_js/stop_inclusion", entry_id, }); -export const grantSecurityClasses = ( +export const zwaveGrantSecurityClasses = ( hass: HomeAssistant, entry_id: string, security_classes: SecurityClass[], @@ -257,7 +302,7 @@ export const grantSecurityClasses = ( client_side_auth, }); -export const validateDskAndEnterPin = ( +export const zwaveValidateDskAndEnterPin = ( hass: HomeAssistant, entry_id: string, pin: string @@ -268,7 +313,44 @@ export const validateDskAndEnterPin = ( pin, }); -export const fetchNodeStatus = ( +export const zwaveSupportsFeature = ( + hass: HomeAssistant, + entry_id: string, + feature: ZWaveFeature +): Promise<{ supported: boolean }> => + hass.callWS({ + type: "zwave_js/supports_feature", + entry_id, + feature, + }); + +export const zwaveParseQrCode = ( + hass: HomeAssistant, + entry_id: string, + qr_code_string: string +): Promise => + hass.callWS({ + type: "zwave_js/parse_qr_code_string", + entry_id, + qr_code_string, + }); + +export const provisionZwaveSmartStartNode = ( + hass: HomeAssistant, + entry_id: string, + qr_provisioning_information?: QRProvisioningInformation, + qr_code_string?: string, + planned_provisioning_entry?: PlannedProvisioningEntry +): Promise => + hass.callWS({ + type: "zwave_js/provision_smart_start_node", + entry_id, + qr_code_string, + qr_provisioning_information, + planned_provisioning_entry, + }); + +export const fetchZwaveNodeStatus = ( hass: HomeAssistant, entry_id: string, node_id: number @@ -279,7 +361,7 @@ export const fetchNodeStatus = ( node_id, }); -export const fetchNodeMetadata = ( +export const fetchZwaveNodeMetadata = ( hass: HomeAssistant, entry_id: string, node_id: number @@ -290,7 +372,7 @@ export const fetchNodeMetadata = ( node_id, }); -export const fetchNodeConfigParameters = ( +export const fetchZwaveNodeConfigParameters = ( hass: HomeAssistant, entry_id: string, node_id: number @@ -301,7 +383,7 @@ export const fetchNodeConfigParameters = ( node_id, }); -export const setNodeConfigParameter = ( +export const setZwaveNodeConfigParameter = ( hass: HomeAssistant, entry_id: string, node_id: number, @@ -320,7 +402,7 @@ export const setNodeConfigParameter = ( return hass.callWS(data); }; -export const reinterviewNode = ( +export const reinterviewZwaveNode = ( hass: HomeAssistant, entry_id: string, node_id: number, @@ -335,7 +417,7 @@ export const reinterviewNode = ( } ); -export const healNode = ( +export const healZwaveNode = ( hass: HomeAssistant, entry_id: string, node_id: number @@ -346,7 +428,7 @@ export const healNode = ( node_id, }); -export const removeFailedNode = ( +export const removeFailedZwaveNode = ( hass: HomeAssistant, entry_id: string, node_id: number, @@ -361,7 +443,7 @@ export const removeFailedNode = ( } ); -export const healNetwork = ( +export const healZwaveNetwork = ( hass: HomeAssistant, entry_id: string ): Promise => @@ -370,7 +452,7 @@ export const healNetwork = ( entry_id, }); -export const stopHealNetwork = ( +export const stopHealZwaveNetwork = ( hass: HomeAssistant, entry_id: string ): Promise => @@ -379,7 +461,7 @@ export const stopHealNetwork = ( entry_id, }); -export const subscribeNodeReady = ( +export const subscribeZwaveNodeReady = ( hass: HomeAssistant, entry_id: string, node_id: number, @@ -394,7 +476,7 @@ export const subscribeNodeReady = ( } ); -export const subscribeHealNetworkProgress = ( +export const subscribeHealZwaveNetworkProgress = ( hass: HomeAssistant, entry_id: string, callbackFunction: (message: ZWaveJSHealNetworkStatusMessage) => void @@ -407,7 +489,7 @@ export const subscribeHealNetworkProgress = ( } ); -export const getIdentifiersFromDevice = ( +export const getZwaveJsIdentifiersFromDevice = ( device: DeviceRegistryEntry ): ZWaveJSNodeIdentifiers | undefined => { if (!device) { diff --git a/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-actions-zwave_js.ts b/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-actions-zwave_js.ts index cd6b6e5f7d..2cd06cbea6 100644 --- a/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-actions-zwave_js.ts +++ b/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-actions-zwave_js.ts @@ -10,7 +10,7 @@ import { import { customElement, property, state } from "lit/decorators"; import { DeviceRegistryEntry } from "../../../../../../data/device_registry"; import { - getIdentifiersFromDevice, + getZwaveJsIdentifiersFromDevice, ZWaveJSNodeIdentifiers, } from "../../../../../../data/zwave_js"; import { haStyle } from "../../../../../../resources/styles"; @@ -34,7 +34,7 @@ export class HaDeviceActionsZWaveJS extends LitElement { this._entryId = this.device.config_entries[0]; const identifiers: ZWaveJSNodeIdentifiers | undefined = - getIdentifiersFromDevice(this.device); + getZwaveJsIdentifiersFromDevice(this.device); if (!identifiers) { return; } 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 b69484180e..dc24356fff 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 @@ -13,8 +13,8 @@ import { getConfigEntries, } from "../../../../../../data/config_entries"; import { - fetchNodeStatus, - getIdentifiersFromDevice, + fetchZwaveNodeStatus, + getZwaveJsIdentifiersFromDevice, nodeStatus, ZWaveJSNodeStatus, ZWaveJSNodeIdentifiers, @@ -42,7 +42,7 @@ export class HaDeviceInfoZWaveJS extends LitElement { protected updated(changedProperties: PropertyValues) { if (changedProperties.has("device")) { const identifiers: ZWaveJSNodeIdentifiers | undefined = - getIdentifiersFromDevice(this.device); + getZwaveJsIdentifiersFromDevice(this.device); if (!identifiers) { return; } @@ -76,7 +76,11 @@ export class HaDeviceInfoZWaveJS extends LitElement { zwaveJsConfEntries++; } - this._node = await fetchNodeStatus(this.hass, this._entryId, this._nodeId); + this._node = await fetchZwaveNodeStatus( + this.hass, + this._entryId, + this._nodeId + ); } protected render(): TemplateResult { diff --git a/src/panels/config/integrations/integration-panels/zwave/zwave-migration.ts b/src/panels/config/integrations/integration-panels/zwave/zwave-migration.ts index 99cf52fb36..ee19b1aae6 100644 --- a/src/panels/config/integrations/integration-panels/zwave/zwave-migration.ts +++ b/src/panels/config/integrations/integration-panels/zwave/zwave-migration.ts @@ -21,10 +21,10 @@ import { import { migrateZwave, ZWaveJsMigrationData, - fetchNetworkStatus as fetchZwaveJsNetworkStatus, - fetchNodeStatus, - getIdentifiersFromDevice, - subscribeNodeReady, + fetchZwaveNetworkStatus as fetchZwaveJsNetworkStatus, + fetchZwaveNodeStatus, + getZwaveJsIdentifiersFromDevice, + subscribeZwaveNodeReady, } from "../../../../../data/zwave_js"; import { fetchMigrationConfig, @@ -425,7 +425,7 @@ export class ZwaveMigration extends LitElement { this._zwaveJsEntryId! ); const nodeStatePromisses = networkStatus.controller.nodes.map((nodeId) => - fetchNodeStatus(this.hass, this._zwaveJsEntryId!, nodeId) + fetchZwaveNodeStatus(this.hass, this._zwaveJsEntryId!, nodeId) ); const nodesNotReady = (await Promise.all(nodeStatePromisses)).filter( (node) => !node.ready @@ -436,13 +436,18 @@ export class ZwaveMigration extends LitElement { return; } this._nodeReadySubscriptions = nodesNotReady.map((node) => - subscribeNodeReady(this.hass, this._zwaveJsEntryId!, node.node_id, () => { - this._getZwaveJSNodesStatus(); - }) + subscribeZwaveNodeReady( + this.hass, + this._zwaveJsEntryId!, + node.node_id, + () => { + this._getZwaveJSNodesStatus(); + } + ) ); const deviceReg = await fetchDeviceRegistry(this.hass); this._waitingOnDevices = deviceReg - .map((device) => getIdentifiersFromDevice(device)) + .map((device) => getZwaveJsIdentifiersFromDevice(device)) .filter(Boolean); } 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 b33c340cc5..b9f846fa09 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,30 +1,40 @@ import "@material/mwc-button/mwc-button"; -import { mdiAlertCircle, mdiCheckCircle, mdiCloseCircle } from "@mdi/js"; +import type { TextField } from "@material/mwc-textfield/mwc-textfield"; +import "@material/mwc-textfield/mwc-textfield"; +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"; import { fireEvent } from "../../../../../common/dom/fire_event"; +import "../../../../../components/ha-alert"; +import { HaCheckbox } from "../../../../../components/ha-checkbox"; import "../../../../../components/ha-circular-progress"; import { createCloseHeading } from "../../../../../components/ha-dialog"; import "../../../../../components/ha-formfield"; +import "../../../../../components/ha-radio"; import "../../../../../components/ha-switch"; import { - grantSecurityClasses, + zwaveGrantSecurityClasses, InclusionStrategy, + MINIMUM_QR_STRING_LENGTH, + zwaveParseQrCode, + provisionZwaveSmartStartNode, + QRProvisioningInformation, RequestedGrant, SecurityClass, - stopInclusion, - subscribeAddNode, - validateDskAndEnterPin, + stopZwaveInclusion, + subscribeAddZwaveNode, + 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-radio"; -import { HaCheckbox } from "../../../../../components/ha-checkbox"; -import "../../../../../components/ha-alert"; +import "../../../../../components/ha-qr-scanner"; export interface ZWaveJSAddNodeDevice { id: string; @@ -40,11 +50,14 @@ class DialogZWaveJSAddNode extends LitElement { @state() private _status?: | "loading" | "started" + | "started_specific" | "choose_strategy" + | "qr_scan" | "interviewing" | "failed" | "timed_out" | "finished" + | "provisioned" | "validate_dsk_enter_pin" | "grant_security_classes"; @@ -64,10 +77,14 @@ class DialogZWaveJSAddNode extends LitElement { @state() private _lowSecurity = false; + @state() private _supportsSmartStart?: boolean; + private _addNodeTimeoutHandle?: number; private _subscribed?: Promise; + private _qrProcessing = false; + public disconnectedCallback(): void { super.disconnectedCallback(); this._unsubscribe(); @@ -76,6 +93,7 @@ class DialogZWaveJSAddNode extends LitElement { public async showDialog(params: ZWaveJSAddNodeDialogParams): Promise { this._entryId = params.entry_id; this._status = "loading"; + this._checkSmartStartSupport(); this._startInclusion(); } @@ -157,6 +175,22 @@ class DialogZWaveJSAddNode extends LitElement { > Search device ` + : this._status === "qr_scan" + ? html` +

+ If scanning doesn't work, you can enter the QR code value + manually: +

+ ` : this._status === "validate_dsk_enter_pin" ? html`

@@ -241,18 +275,28 @@ class DialogZWaveJSAddNode extends LitElement { Retry ` + : this._status === "started_specific" + ? html`

+ ${this.hass.localize( + "ui.panel.config.zwave_js.add_node.searching_device" + )} +

+ +

+ ${this.hass.localize( + "ui.panel.config.zwave_js.add_node.follow_device_instructions" + )} +

` : this._status === "started" ? html` -
- -
-

- ${this.hass.localize( - "ui.panel.config.zwave_js.add_node.controller_in_inclusion_mode" - )} -

+
+
+

+ ${this.hass.localize( + "ui.panel.config.zwave_js.add_node.searching_device" + )} +

+

${this.hass.localize( "ui.panel.config.zwave_js.add_node.follow_device_instructions" @@ -263,15 +307,37 @@ class DialogZWaveJSAddNode extends LitElement { class="link" @click=${this._chooseInclusionStrategy} > - Advanced inclusion + ${this.hass.localize( + "ui.panel.config.zwave_js.add_node.choose_inclusion_strategy" + )}

+ ${this._supportsSmartStart + ? html`
+

+ ${this.hass.localize( + "ui.panel.config.zwave_js.add_node.qr_code" + )} +

+ +

+ ${this.hass.localize( + "ui.panel.config.zwave_js.add_node.qr_code_paragraph" + )} +

+

+ + ${this.hass.localize( + "ui.panel.config.zwave_js.add_node.scan_qr_code" + )} + +

+
` + : ""}
- ${this.hass.localize( - "ui.panel.config.zwave_js.add_node.cancel_inclusion" - )} + ${this.hass.localize("ui.common.cancel")} ` : this._status === "interviewing" @@ -310,16 +376,18 @@ class DialogZWaveJSAddNode extends LitElement { : this._status === "failed" ? html`
-
-

- ${this.hass.localize( + + > + ${this._error || + this.hass.localize( + "ui.panel.config.zwave_js.add_node.check_logs" + )} + ${this._stages ? html`

${this._stages.map( @@ -391,6 +459,23 @@ class DialogZWaveJSAddNode extends LitElement { ${this.hass.localize("ui.panel.config.zwave_js.common.close")} ` + : this._status === "provisioned" + ? html`
+ +
+

+ ${this.hass.localize( + "ui.panel.config.zwave_js.add_node.provisioning_finished" + )} +

+
+
+ + ${this.hass.localize("ui.panel.config.zwave_js.common.close")} + ` : ""} `; @@ -417,6 +502,83 @@ class DialogZWaveJSAddNode extends LitElement { } } + private async _scanQRCode(): Promise { + this._unsubscribe(); + this._status = "qr_scan"; + } + + private _qrKeyDown(ev: KeyboardEvent) { + if (this._qrProcessing) { + return; + } + if (ev.key === "Enter") { + this._handleQrCodeScanned((ev.target as TextField).value); + } + } + + private _qrCodeScanned(ev: CustomEvent): void { + if (this._qrProcessing) { + return; + } + this._handleQrCodeScanned(ev.detail.value); + } + + private async _handleQrCodeScanned(qrCodeString: string): Promise { + this._error = undefined; + if (this._status !== "qr_scan" || this._qrProcessing) { + return; + } + this._qrProcessing = true; + if ( + qrCodeString.length < MINIMUM_QR_STRING_LENGTH || + !qrCodeString.startsWith("90") + ) { + this._qrProcessing = false; + this._error = `Invalid QR code (${qrCodeString})`; + return; + } + let provisioningInfo: QRProvisioningInformation; + try { + provisioningInfo = await zwaveParseQrCode( + this.hass, + this._entryId!, + qrCodeString + ); + } catch (err: any) { + this._qrProcessing = false; + this._error = err.message; + return; + } + this._status = "loading"; + // wait for QR scanner to be removed before resetting qr processing + this.updateComplete.then(() => { + this._qrProcessing = false; + }); + if (provisioningInfo.version === 1) { + try { + await provisionZwaveSmartStartNode( + this.hass, + this._entryId!, + provisioningInfo + ); + this._status = "provisioned"; + } catch (err: any) { + this._error = err.message; + this._status = "failed"; + } + } else if (provisioningInfo.version === 0) { + this._inclusionStrategy = InclusionStrategy.Security_S2; + // this._startInclusion(provisioningInfo); + this._startInclusion(undefined, undefined, { + dsk: "34673-15546-46480-39591-32400-22155-07715-45994", + security_classes: [0, 1, 7], + }); + } else { + this._error = "This QR code is not supported"; + this._status = "failed"; + } + } + private _handlePinKeyUp(ev: KeyboardEvent) { if (ev.key === "Enter") { this._validateDskAndEnterPin(); @@ -427,7 +589,7 @@ class DialogZWaveJSAddNode extends LitElement { this._status = "loading"; this._error = undefined; try { - await validateDskAndEnterPin( + await zwaveValidateDskAndEnterPin( this.hass, this._entryId!, this._pinInput!.value as string @@ -442,7 +604,7 @@ class DialogZWaveJSAddNode extends LitElement { this._status = "loading"; this._error = undefined; try { - await grantSecurityClasses( + await zwaveGrantSecurityClasses( this.hass, this._entryId!, this._securityClasses @@ -460,17 +622,33 @@ class DialogZWaveJSAddNode extends LitElement { this._startInclusion(); } - private _startInclusion(): void { + private async _checkSmartStartSupport() { + this._supportsSmartStart = ( + await zwaveSupportsFeature( + this.hass, + this._entryId!, + ZWaveFeature.SmartStart + ) + ).supported; + } + + private _startInclusion( + qrProvisioningInformation?: QRProvisioningInformation, + qrCodeString?: string, + plannedProvisioningEntry?: PlannedProvisioningEntry + ): void { if (!this.hass) { return; } this._lowSecurity = false; - this._subscribed = subscribeAddNode( + const specificDevice = + qrProvisioningInformation || qrCodeString || plannedProvisioningEntry; + this._subscribed = subscribeAddZwaveNode( this.hass, this._entryId!, (message) => { if (message.event === "inclusion started") { - this._status = "started"; + this._status = specificDevice ? "started_specific" : "started"; } if (message.event === "inclusion failed") { this._unsubscribe(); @@ -491,7 +669,7 @@ class DialogZWaveJSAddNode extends LitElement { if (message.event === "grant security classes") { if (this._inclusionStrategy === undefined) { - grantSecurityClasses( + zwaveGrantSecurityClasses( this.hass, this._entryId!, message.requested_grant.securityClasses, @@ -525,7 +703,10 @@ class DialogZWaveJSAddNode extends LitElement { } } }, - this._inclusionStrategy + this._inclusionStrategy, + qrProvisioningInformation, + qrCodeString, + plannedProvisioningEntry ); this._addNodeTimeoutHandle = window.setTimeout(() => { this._unsubscribe(); @@ -539,7 +720,7 @@ class DialogZWaveJSAddNode extends LitElement { this._subscribed = undefined; } if (this._entryId) { - stopInclusion(this.hass, this._entryId); + stopZwaveInclusion(this.hass, this._entryId); } this._requestedGrant = undefined; this._dsk = undefined; @@ -558,6 +739,7 @@ class DialogZWaveJSAddNode extends LitElement { this._status = undefined; this._device = undefined; this._stages = undefined; + this._error = undefined; fireEvent(this, "dialog-closed", { dialog: this.localName }); } @@ -578,10 +760,6 @@ class DialogZWaveJSAddNode extends LitElement { color: var(--warning-color); } - .failed { - color: var(--error-color); - } - .stages { margin-top: 16px; display: grid; @@ -610,6 +788,39 @@ class DialogZWaveJSAddNode extends LitElement { padding: 8px 0; } + .select-inclusion { + display: flex; + align-items: center; + } + + .select-inclusion .outline:nth-child(2) { + margin-left: 16px; + } + + .select-inclusion .outline { + border: 1px solid var(--divider-color); + border-radius: 4px; + padding: 16px; + min-height: 250px; + text-align: center; + flex: 1; + } + + @media all and (max-width: 500px) { + .select-inclusion { + flex-direction: column; + } + + .select-inclusion .outline:nth-child(2) { + margin-left: 0; + margin-top: 16px; + } + } + + mwc-textfield { + width: 100%; + } + ha-svg-icon { width: 68px; height: 48px; diff --git a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-network.ts b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-network.ts index 33b60fe685..f99b073869 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-network.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-network.ts @@ -7,10 +7,10 @@ import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../../../common/dom/fire_event"; import { createCloseHeading } from "../../../../../components/ha-dialog"; import { - fetchNetworkStatus, - healNetwork, - stopHealNetwork, - subscribeHealNetworkProgress, + fetchZwaveNetworkStatus, + healZwaveNetwork, + stopHealZwaveNetwork, + subscribeHealZwaveNetworkProgress, ZWaveJSHealNetworkStatusMessage, ZWaveJSNetwork, } from "../../../../../data/zwave_js"; @@ -202,13 +202,13 @@ class DialogZWaveJSHealNetwork extends LitElement { if (!this.hass) { return; } - const network: ZWaveJSNetwork = await fetchNetworkStatus( + const network: ZWaveJSNetwork = await fetchZwaveNetworkStatus( this.hass!, this.entry_id! ); if (network.controller.is_heal_network_active) { this._status = "started"; - this._subscribed = subscribeHealNetworkProgress( + this._subscribed = subscribeHealZwaveNetworkProgress( this.hass, this.entry_id!, this._handleMessage.bind(this) @@ -220,9 +220,9 @@ class DialogZWaveJSHealNetwork extends LitElement { if (!this.hass) { return; } - healNetwork(this.hass, this.entry_id!); + healZwaveNetwork(this.hass, this.entry_id!); this._status = "started"; - this._subscribed = subscribeHealNetworkProgress( + this._subscribed = subscribeHealZwaveNetworkProgress( this.hass, this.entry_id!, this._handleMessage.bind(this) @@ -233,7 +233,7 @@ class DialogZWaveJSHealNetwork extends LitElement { if (!this.hass) { return; } - stopHealNetwork(this.hass, this.entry_id!); + stopHealZwaveNetwork(this.hass, this.entry_id!); this._unsubscribe(); this._status = "cancelled"; } diff --git a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-node.ts index 632c19286b..6db1483fb7 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-node.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-heal-node.ts @@ -10,8 +10,8 @@ import { computeDeviceName, } from "../../../../../data/device_registry"; import { - fetchNetworkStatus, - healNode, + fetchZwaveNetworkStatus, + healZwaveNode, ZWaveJSNetwork, } from "../../../../../data/zwave_js"; import { haStyleDialog } from "../../../../../resources/styles"; @@ -206,7 +206,7 @@ class DialogZWaveJSHealNode extends LitElement { if (!this.hass) { return; } - const network: ZWaveJSNetwork = await fetchNetworkStatus( + const network: ZWaveJSNetwork = await fetchZwaveNetworkStatus( this.hass!, this.entry_id! ); @@ -221,7 +221,11 @@ class DialogZWaveJSHealNode extends LitElement { } this._status = "started"; try { - this._status = (await healNode(this.hass, this.entry_id!, this.node_id!)) + this._status = (await healZwaveNode( + this.hass, + this.entry_id!, + this.node_id! + )) ? "finished" : "failed"; } catch (err: any) { diff --git a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-reinterview-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-reinterview-node.ts index a56f2ed050..fb4e12785c 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-reinterview-node.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-reinterview-node.ts @@ -6,7 +6,7 @@ import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../../../common/dom/fire_event"; import "../../../../../components/ha-circular-progress"; import { createCloseHeading } from "../../../../../components/ha-dialog"; -import { reinterviewNode } from "../../../../../data/zwave_js"; +import { reinterviewZwaveNode } from "../../../../../data/zwave_js"; import { haStyleDialog } from "../../../../../resources/styles"; import { HomeAssistant } from "../../../../../types"; import { ZWaveJSReinterviewNodeDialogParams } from "./show-dialog-zwave_js-reinterview-node"; @@ -157,7 +157,7 @@ class DialogZWaveJSReinterviewNode extends LitElement { if (!this.hass) { return; } - this._subscribed = reinterviewNode( + this._subscribed = reinterviewZwaveNode( this.hass, this.entry_id!, this.node_id!, diff --git a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-remove-failed-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-remove-failed-node.ts index aa8a264649..32faabced2 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-remove-failed-node.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-remove-failed-node.ts @@ -7,7 +7,7 @@ import { fireEvent } from "../../../../../common/dom/fire_event"; import "../../../../../components/ha-circular-progress"; import { createCloseHeading } from "../../../../../components/ha-dialog"; import { - removeFailedNode, + removeFailedZwaveNode, ZWaveJSRemovedNode, } from "../../../../../data/zwave_js"; import { haStyleDialog } from "../../../../../resources/styles"; @@ -164,7 +164,7 @@ class DialogZWaveJSRemoveFailedNode extends LitElement { return; } this._status = "started"; - this._subscribed = removeFailedNode( + this._subscribed = removeFailedZwaveNode( this.hass, this.entry_id!, this.node_id!, 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 dbd163508f..e7e97addf4 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 @@ -9,11 +9,11 @@ import "../../../../../components/ha-icon-next"; import "../../../../../components/ha-svg-icon"; import { getSignedPath } from "../../../../../data/auth"; import { - fetchDataCollectionStatus, - fetchNetworkStatus, - fetchNodeStatus, + fetchZwaveDataCollectionStatus, + fetchZwaveNetworkStatus, + fetchZwaveNodeStatus, NodeStatus, - setDataCollectionPreference, + setZwaveDataCollectionPreference, ZWaveJSNetwork, ZWaveJSNodeStatus, } from "../../../../../data/zwave_js"; @@ -317,8 +317,8 @@ class ZWaveJSConfigDashboard extends LitElement { } const [network, dataCollectionStatus] = await Promise.all([ - fetchNetworkStatus(this.hass!, this.configEntryId), - fetchDataCollectionStatus(this.hass!, this.configEntryId), + fetchZwaveNetworkStatus(this.hass!, this.configEntryId), + fetchZwaveDataCollectionStatus(this.hass!, this.configEntryId), ]); this._network = network; @@ -340,7 +340,7 @@ class ZWaveJSConfigDashboard extends LitElement { return; } const nodeStatePromisses = this._network.controller.nodes.map((nodeId) => - fetchNodeStatus(this.hass, this.configEntryId!, nodeId) + fetchZwaveNodeStatus(this.hass, this.configEntryId!, nodeId) ); this._nodes = await Promise.all(nodeStatePromisses); } @@ -364,7 +364,7 @@ class ZWaveJSConfigDashboard extends LitElement { } private _dataCollectionToggled(ev) { - setDataCollectionPreference( + setZwaveDataCollectionPreference( this.hass!, this.configEntryId!, ev.target.checked 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 9b2e0a4bdf..19b7d24dd8 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 @@ -32,9 +32,9 @@ import { subscribeDeviceRegistry, } from "../../../../../data/device_registry"; import { - fetchNodeConfigParameters, - fetchNodeMetadata, - setNodeConfigParameter, + fetchZwaveNodeConfigParameters, + fetchZwaveNodeMetadata, + setZwaveNodeConfigParameter, ZWaveJSNodeConfigParams, ZwaveJSNodeMetadata, ZWaveJSSetConfigParamResult, @@ -377,7 +377,7 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) { private async _updateConfigParameter(target, value) { const nodeId = getNodeId(this._device!); try { - const result = await setNodeConfigParameter( + const result = await setZwaveNodeConfigParameter( this.hass, this.configEntryId!, nodeId!, @@ -429,8 +429,8 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) { } [this._nodeMetadata, this._config] = await Promise.all([ - fetchNodeMetadata(this.hass, this.configEntryId, nodeId!), - fetchNodeConfigParameters(this.hass, this.configEntryId, nodeId!), + fetchZwaveNodeMetadata(this.hass, this.configEntryId, nodeId!), + fetchZwaveNodeConfigParameters(this.hass, this.configEntryId, nodeId!), ]); } diff --git a/src/translations/en.json b/src/translations/en.json index 1846d12698..a5cb447e03 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2855,11 +2855,18 @@ }, "add_node": { "title": "Add a Z-Wave Device", - "cancel_inclusion": "Cancel Inclusion", - "controller_in_inclusion_mode": "Your Z-Wave controller is now in inclusion mode.", + "searching_device": "Searching for device", "follow_device_instructions": "Follow the directions that came with your device to trigger pairing on the device.", - "inclusion_failed": "The device could not be added. Please check the logs for more information.", + "choose_inclusion_strategy": "How do you want to add your device", + "qr_code": "QR Code", + "qr_code_paragraph": "If your device supports SmartStart you can scan the QR code for easy pairing.", + "scan_qr_code": "Scan QR code", + "enter_qr_code": "Enter QR code value", + "select_camera": "Select camera", + "inclusion_failed": "The device could not be added.", + "check_logs": "Please check the logs for more information.", "inclusion_finished": "The device has been added.", + "provisioning_finished": "The device has been added. Once you power it on, it will become available.", "view_device": "View Device", "interview_started": "The device is being interviewed. This may take some time.", "interview_failed": "The device interview failed. Additional information may be available in the logs." diff --git a/yarn.lock b/yarn.lock index 0bfb5c39ae..1322aea8a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9139,6 +9139,7 @@ fsevents@^1.2.7: prettier: ^2.4.1 proxy-polyfill: ^0.3.2 punycode: ^2.1.1 + qr-scanner: ^1.3.0 qrcode: ^1.4.4 regenerator-runtime: ^0.13.8 require-dir: ^1.2.0 @@ -13007,6 +13008,13 @@ fsevents@^1.2.7: languageName: node linkType: hard +"qr-scanner@npm:^1.3.0": + version: 1.3.0 + resolution: "qr-scanner@npm:1.3.0" + checksum: 421ff00626252d0f9e50550fb550a463166e4d0438baffb469c9450079f1f802f6df22784509bb571ef50ece81aecaadc00f91d442959f37655ad29710c81c8b + languageName: node + linkType: hard + "qrcode@npm:^1.4.4": version: 1.4.4 resolution: "qrcode@npm:1.4.4" From 05333ac2d98d0dc89143d53cd15f3b3f04b733d0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 1 Dec 2021 14:46:40 -0800 Subject: [PATCH 080/112] Show disabled entity names on the device page (#10743) * Show disabled entity names on the device page * Update src/panels/config/devices/device-detail/ha-device-entities-card.ts Co-authored-by: Bram Kragten Co-authored-by: Bram Kragten --- .../device-detail/ha-device-entities-card.ts | 54 ++++++++++++++++--- 1 file changed, 48 insertions(+), 6 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 1ab80ff08c..73364c3969 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 @@ -9,7 +9,7 @@ import { PropertyValues, TemplateResult, } from "lit"; -import { customElement, property } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../../common/entity/compute_domain"; import { domainIcon } from "../../../../common/entity/domain_icon"; import "../../../../components/entity/state-badge"; @@ -25,6 +25,10 @@ import { showEntityEditorDialog } from "../../entities/show-dialog-entity-editor import { EntityRegistryStateEntry } from "../ha-config-device-page"; import { computeStateName } from "../../../../common/entity/compute_state_name"; import { stripPrefixFromEntityName } from "../../../../common/entity/strip_prefix_from_entity_name"; +import { + ExtEntityRegistryEntry, + getExtendedEntityRegistryEntry, +} from "../../../../data/entity_registry"; @customElement("ha-device-entities-card") export class HaDeviceEntitiesCard extends LitElement { @@ -38,6 +42,11 @@ export class HaDeviceEntitiesCard extends LitElement { @property() public showDisabled = false; + @state() private _extDisabledEntityEntries?: Record< + string, + ExtEntityRegistryEntry + >; + private _entityRows: Array = []; protected shouldUpdate(changedProps: PropertyValues) { @@ -60,7 +69,13 @@ export class HaDeviceEntitiesCard extends LitElement {
${this.entities.map((entry: EntityRegistryStateEntry) => { if (entry.disabled_by) { - disabledEntities.push(entry); + if (this._extDisabledEntityEntries) { + disabledEntities.push( + this._extDisabledEntityEntries[entry.entity_id] || entry + ); + } else { + disabledEntities.push(entry); + } return ""; } return this.hass.states[entry.entity_id] @@ -115,6 +130,28 @@ export class HaDeviceEntitiesCard extends LitElement { private _toggleShowDisabled() { this.showDisabled = !this.showDisabled; + if (!this.showDisabled || this._extDisabledEntityEntries !== undefined) { + return; + } + this._extDisabledEntityEntries = {}; + const toFetch = this.entities.filter((entry) => entry.disabled_by); + + const worker = async () => { + if (toFetch.length === 0) { + return; + } + + const entityId = toFetch.pop()!.entity_id; + const entry = await getExtendedEntityRegistryEntry(this.hass, entityId); + this._extDisabledEntityEntries![entityId] = entry; + this.requestUpdate("_extDisabledEntityEntries"); + worker(); + }; + + // Fetch 3 in parallel + worker(); + worker(); + worker(); } private _renderEntity(entry: EntityRegistryStateEntry): TemplateResult { @@ -125,9 +162,9 @@ export class HaDeviceEntitiesCard extends LitElement { const element = createRowElement(config); if (this.hass) { element.hass = this.hass; - const state = this.hass.states[entry.entity_id]; + const stateObj = this.hass.states[entry.entity_id]; const name = stripPrefixFromEntityName( - computeStateName(state), + computeStateName(stateObj), `${this.deviceName} `.toLowerCase() ); if (name) { @@ -141,6 +178,11 @@ export class HaDeviceEntitiesCard extends LitElement { } private _renderEntry(entry: EntityRegistryStateEntry): TemplateResult { + const name = + entry.stateName || + entry.name || + (entry as ExtEntityRegistryEntry).original_name; + return html`
- ${entry.stateName + ${name ? stripPrefixFromEntityName( - entry.stateName, + name, `${this.deviceName} `.toLowerCase() ) : entry.entity_id} From acf4d59fde26111a26a7182f1b79451d06797517 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 1 Dec 2021 14:47:17 -0800 Subject: [PATCH 081/112] Bumped version to 20211201.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d90ddf3a43..35a75b5596 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20211130.0", + version="20211201.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/frontend", author="The Home Assistant Authors", From cf062bf0f4395c4c474fa571aba5add774589d28 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Thu, 2 Dec 2021 10:48:30 +0100 Subject: [PATCH 082/112] Fix pointer/more-info inconsistencies for entity rows (#10025) Co-authored-by: Bram Kragten --- src/common/const.ts | 29 ++- .../more-info/state_more_info_control.ts | 4 +- .../components/hui-generic-entity-row.ts | 165 ++++++++++-------- .../entity-rows/hui-climate-entity-row.ts | 6 +- .../hui-input-number-entity-row.ts | 1 - .../hui-input-select-entity-row.ts | 67 +++---- .../entity-rows/hui-input-text-entity-row.ts | 17 +- .../entity-rows/hui-select-entity-row.ts | 86 ++++----- .../entity-rows/hui-text-entity-row.ts | 33 +--- .../entity-rows/hui-weather-entity-row.ts | 20 ++- .../special-rows/hui-attribute-row.ts | 30 ++-- 11 files changed, 203 insertions(+), 255 deletions(-) diff --git a/src/common/const.ts b/src/common/const.ts index fb5ee730f9..0aedba0806 100644 --- a/src/common/const.ts +++ b/src/common/const.ts @@ -188,8 +188,9 @@ export const DOMAINS_WITH_MORE_INFO = [ "weather", ]; -/** Domains that show no more info dialog. */ -export const DOMAINS_HIDE_MORE_INFO = [ +/** Domains that do not show the default more info dialog content (e.g. the attribute section) + * and do not have a separate more info (so not in DOMAINS_WITH_MORE_INFO). */ +export const DOMAINS_HIDE_DEFAULT_MORE_INFO = [ "input_number", "input_select", "input_text", @@ -198,6 +199,30 @@ export const DOMAINS_HIDE_MORE_INFO = [ "select", ]; +/** Domains that render an input element instead of a text value when rendered in a row. + * Those rows should then not show a cursor pointer when hovered (which would normally + * be the default) unless the element itself enforces it (e.g. a button). Also those elements + * should not act as a click target to open the more info dialog (the row name and state icon + * still do of course) as the click might instead e.g. activate the input field that this row shows. + */ +export const DOMAINS_INPUT_ROW = [ + "cover", + "fan", + "humidifier", + "input_boolean", + "input_datetime", + "input_number", + "input_select", + "input_text", + "light", + "lock", + "media_player", + "number", + "scene", + "script", + "select", +]; + /** Domains that should have the history hidden in the more info dialog. */ export const DOMAINS_MORE_INFO_NO_HISTORY = ["camera", "configurator", "scene"]; diff --git a/src/dialogs/more-info/state_more_info_control.ts b/src/dialogs/more-info/state_more_info_control.ts index 4efdca33ae..02abe5e731 100644 --- a/src/dialogs/more-info/state_more_info_control.ts +++ b/src/dialogs/more-info/state_more_info_control.ts @@ -1,6 +1,6 @@ import type { HassEntity } from "home-assistant-js-websocket"; import { - DOMAINS_HIDE_MORE_INFO, + DOMAINS_HIDE_DEFAULT_MORE_INFO, DOMAINS_WITH_MORE_INFO, } from "../../common/const"; import { computeStateDomain } from "../../common/entity/compute_state_domain"; @@ -40,7 +40,7 @@ export const domainMoreInfoType = (domain: string): string => { if (DOMAINS_WITH_MORE_INFO.includes(domain)) { return domain; } - if (DOMAINS_HIDE_MORE_INFO.includes(domain)) { + if (DOMAINS_HIDE_DEFAULT_MORE_INFO.includes(domain)) { return "hidden"; } return "default"; diff --git a/src/panels/lovelace/components/hui-generic-entity-row.ts b/src/panels/lovelace/components/hui-generic-entity-row.ts index 88094f7dd9..5f185bf46e 100644 --- a/src/panels/lovelace/components/hui-generic-entity-row.ts +++ b/src/panels/lovelace/components/hui-generic-entity-row.ts @@ -9,7 +9,7 @@ import { import { property } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { ifDefined } from "lit/directives/if-defined"; -import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const"; +import { DOMAINS_INPUT_ROW } from "../../../common/const"; import { toggleAttribute } from "../../../common/dom/toggle_attribute"; import { computeDomain } from "../../../common/entity/compute_domain"; import { computeStateName } from "../../../common/entity/compute_state_name"; @@ -31,6 +31,8 @@ class HuiGenericEntityRow extends LitElement { @property() public secondaryText?: string; + @property({ type: Boolean }) public hideName = false; + protected render(): TemplateResult { if (!this.hass || !this.config) { return html``; @@ -47,10 +49,10 @@ class HuiGenericEntityRow extends LitElement { `; } - const pointer = - (this.config.tap_action && this.config.tap_action.action !== "none") || - (this.config.entity && - !DOMAINS_HIDE_MORE_INFO.includes(computeDomain(this.config.entity))); + const domain = computeDomain(this.config.entity); + const pointer = !( + this.config.tap_action && this.config.tap_action.action !== "none" + ); const hasSecondary = this.secondaryText || this.config.secondary_info; const name = this.config.name ?? computeStateName(stateObj); @@ -72,75 +74,90 @@ class HuiGenericEntityRow extends LitElement { })} tabindex=${ifDefined(pointer ? "0" : undefined)} > -
- ${name} - ${hasSecondary - ? html` -
- ${this.secondaryText || - (this.config.secondary_info === "entity-id" - ? stateObj.entity_id - : this.config.secondary_info === "last-changed" - ? html` - - ` - : this.config.secondary_info === "last-updated" - ? html` - - ` - : this.config.secondary_info === "last-triggered" - ? stateObj.attributes.last_triggered - ? html` - - ` - : this.hass.localize( - "ui.panel.lovelace.cards.entities.never_triggered" - ) - : this.config.secondary_info === "position" && - stateObj.attributes.current_position !== undefined - ? `${this.hass.localize("ui.card.cover.position")}: ${ - stateObj.attributes.current_position - }` - : this.config.secondary_info === "tilt-position" && - stateObj.attributes.current_tilt_position !== undefined - ? `${this.hass.localize("ui.card.cover.tilt_position")}: ${ - stateObj.attributes.current_tilt_position - }` - : this.config.secondary_info === "brightness" && - stateObj.attributes.brightness - ? html`${Math.round( - (stateObj.attributes.brightness / 255) * 100 - )} - %` - : "")} -
- ` - : ""} -
- + ${!this.hideName + ? html`
+ ${this.config.name || computeStateName(stateObj)} + ${hasSecondary + ? html` +
+ ${this.secondaryText || + (this.config.secondary_info === "entity-id" + ? stateObj.entity_id + : this.config.secondary_info === "last-changed" + ? html` + + ` + : this.config.secondary_info === "last-updated" + ? html` + + ` + : this.config.secondary_info === "last-triggered" + ? stateObj.attributes.last_triggered + ? html` + + ` + : this.hass.localize( + "ui.panel.lovelace.cards.entities.never_triggered" + ) + : this.config.secondary_info === "position" && + stateObj.attributes.current_position !== undefined + ? `${this.hass.localize("ui.card.cover.position")}: ${ + stateObj.attributes.current_position + }` + : this.config.secondary_info === "tilt-position" && + stateObj.attributes.current_tilt_position !== undefined + ? `${this.hass.localize( + "ui.card.cover.tilt_position" + )}: ${stateObj.attributes.current_tilt_position}` + : this.config.secondary_info === "brightness" && + stateObj.attributes.brightness + ? html`${Math.round( + (stateObj.attributes.brightness / 255) * 100 + )} + %` + : "")} +
+ ` + : ""} +
` + : html``} + ${!DOMAINS_INPUT_ROW.includes(domain) + ? html`
+ +
` + : html``} `; } diff --git a/src/panels/lovelace/entity-rows/hui-climate-entity-row.ts b/src/panels/lovelace/entity-rows/hui-climate-entity-row.ts index 1ad1a78098..0e16f0b160 100644 --- a/src/panels/lovelace/entity-rows/hui-climate-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-climate-entity-row.ts @@ -49,10 +49,8 @@ class HuiClimateEntityRow extends LitElement implements LovelaceRow { return html` - + + `; } diff --git a/src/panels/lovelace/entity-rows/hui-input-number-entity-row.ts b/src/panels/lovelace/entity-rows/hui-input-number-entity-row.ts index 3285fc15eb..40163bce0b 100644 --- a/src/panels/lovelace/entity-rows/hui-input-number-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-input-number-entity-row.ts @@ -132,7 +132,6 @@ class HuiInputNumberEntityRow extends LitElement implements LovelaceRow { return css` :host { display: block; - cursor: pointer; } .flex { display: flex; 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 6e562f415c..d0f76ce9ed 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 @@ -9,11 +9,7 @@ import { TemplateResult, } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { classMap } from "lit/directives/class-map"; -import { ifDefined } from "lit/directives/if-defined"; -import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const"; import { stopPropagation } from "../../../common/dom/stop_propagation"; -import { computeDomain } from "../../../common/entity/compute_domain"; import { computeStateName } from "../../../common/entity/compute_state_name"; import "../../../components/entity/state-badge"; import "../../../components/ha-paper-dropdown-menu"; @@ -23,13 +19,10 @@ import { InputSelectEntity, setInputSelectOption, } from "../../../data/input_select"; -import { ActionHandlerEvent } from "../../../data/lovelace"; import { HomeAssistant } from "../../../types"; import { EntitiesCardEntityConfig } from "../cards/types"; -import { actionHandler } from "../common/directives/action-handler-directive"; -import { handleAction } from "../common/handle-action"; -import { hasAction } from "../common/has-action"; import { hasConfigOrEntityChanged } from "../common/has-changed"; +import "../components/hui-generic-entity-row"; import { createEntityNotFoundWarning } from "../components/hui-warning"; import { LovelaceRow } from "./types"; @@ -68,42 +61,28 @@ class HuiInputSelectEntityRow extends LitElement implements LovelaceRow { `; } - const pointer = - (this._config.tap_action && this._config.tap_action.action !== "none") || - (this._config.entity && - !DOMAINS_HIDE_MORE_INFO.includes(computeDomain(this._config.entity))); - return html` - - - - ${stateObj.attributes.options - ? stateObj.attributes.options.map( - (option) => html` ${option} ` - ) - : ""} - - + + + ${stateObj.attributes.options + ? stateObj.attributes.options.map( + (option) => html` ${option} ` + ) + : ""} + + + `; } @@ -129,10 +108,6 @@ class HuiInputSelectEntityRow extends LitElement implements LovelaceRow { } } - private _handleAction(ev: ActionHandlerEvent) { - handleAction(this, this.hass!, this._config!, ev.detail.action!); - } - static get styles(): CSSResultGroup { return css` :host { diff --git a/src/panels/lovelace/entity-rows/hui-input-text-entity-row.ts b/src/panels/lovelace/entity-rows/hui-input-text-entity-row.ts index 65c3ee84aa..40e33d7fe9 100644 --- a/src/panels/lovelace/entity-rows/hui-input-text-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-input-text-entity-row.ts @@ -1,12 +1,5 @@ import { PaperInputElement } from "@polymer/paper-input/paper-input"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; +import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { UNAVAILABLE } from "../../../data/entity"; import { setValue } from "../../../data/input_text"; @@ -80,14 +73,6 @@ class HuiInputTextEntityRow extends LitElement implements LovelaceRow { ev.target.blur(); } - - static get styles(): CSSResultGroup { - return css` - :host { - cursor: pointer; - } - `; - } } declare global { diff --git a/src/panels/lovelace/entity-rows/hui-select-entity-row.ts b/src/panels/lovelace/entity-rows/hui-select-entity-row.ts index a5867a4ca9..d6d978f409 100644 --- a/src/panels/lovelace/entity-rows/hui-select-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-select-entity-row.ts @@ -9,24 +9,17 @@ import { TemplateResult, } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { classMap } from "lit/directives/class-map"; -import { ifDefined } from "lit/directives/if-defined"; -import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const"; import { stopPropagation } from "../../../common/dom/stop_propagation"; -import { computeDomain } from "../../../common/entity/compute_domain"; import { computeStateName } from "../../../common/entity/compute_state_name"; import "../../../components/entity/state-badge"; import "../../../components/ha-paper-dropdown-menu"; import { UNAVAILABLE } from "../../../data/entity"; import { forwardHaptic } from "../../../data/haptics"; import { SelectEntity, setSelectOption } from "../../../data/select"; -import { ActionHandlerEvent } from "../../../data/lovelace"; import { HomeAssistant } from "../../../types"; import { EntitiesCardEntityConfig } from "../cards/types"; -import { actionHandler } from "../common/directives/action-handler-directive"; -import { handleAction } from "../common/handle-action"; -import { hasAction } from "../common/has-action"; import { hasConfigOrEntityChanged } from "../common/has-changed"; +import "../components/hui-generic-entity-row"; import { createEntityNotFoundWarning } from "../components/hui-warning"; import { LovelaceRow } from "./types"; @@ -65,52 +58,39 @@ class HuiSelectEntityRow extends LitElement implements LovelaceRow { `; } - const pointer = - (this._config.tap_action && this._config.tap_action.action !== "none") || - (this._config.entity && - !DOMAINS_HIDE_MORE_INFO.includes(computeDomain(this._config.entity))); - return html` - - - - ${stateObj.attributes.options - ? stateObj.attributes.options.map( - (option) => - html` - ${(stateObj.attributes.device_class && + + + ${stateObj.attributes.options + ? stateObj.attributes.options.map( + (option) => + html` + ${(stateObj.attributes.device_class && + this.hass!.localize( + `component.select.state.${stateObj.attributes.device_class}.${option}` + )) || this.hass!.localize( - `component.select.state.${stateObj.attributes.device_class}.${option}` - )) || - this.hass!.localize( - `component.select.state._.${option}` - ) || - option} - ` - ) - : ""} - - + `component.select.state._.${option}` + ) || + option} + ` + ) + : ""} + + + `; } @@ -136,10 +116,6 @@ class HuiSelectEntityRow extends LitElement implements LovelaceRow { } } - private _handleAction(ev: ActionHandlerEvent) { - handleAction(this, this.hass!, this._config!, ev.detail.action!); - } - static get styles(): CSSResultGroup { return css` :host { diff --git a/src/panels/lovelace/entity-rows/hui-text-entity-row.ts b/src/panels/lovelace/entity-rows/hui-text-entity-row.ts index 6117579ab3..0c82d55e56 100644 --- a/src/panels/lovelace/entity-rows/hui-text-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-text-entity-row.ts @@ -7,16 +7,9 @@ import { TemplateResult, } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { classMap } from "lit/directives/class-map"; -import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const"; -import { computeDomain } from "../../../common/entity/compute_domain"; import { computeStateDisplay } from "../../../common/entity/compute_state_display"; -import { ActionHandlerEvent } from "../../../data/lovelace"; import { HomeAssistant } from "../../../types"; import { EntitiesCardEntityConfig } from "../cards/types"; -import { actionHandler } from "../common/directives/action-handler-directive"; -import { handleAction } from "../common/handle-action"; -import { hasAction } from "../common/has-action"; import { hasConfigOrEntityChanged } from "../common/has-changed"; import "../components/hui-generic-entity-row"; import { createEntityNotFoundWarning } from "../components/hui-warning"; @@ -54,37 +47,13 @@ class HuiTextEntityRow extends LitElement implements LovelaceRow { `; } - const pointer = - (this._config.tap_action && this._config.tap_action.action !== "none") || - (this._config.entity && - !DOMAINS_HIDE_MORE_INFO.includes(computeDomain(this._config.entity))); - return html` -
- ${computeStateDisplay( - this.hass!.localize, - stateObj, - this.hass.locale - )} -
+ ${computeStateDisplay(this.hass!.localize, stateObj, this.hass.locale)}
`; } - private _handleAction(ev: ActionHandlerEvent) { - handleAction(this, this.hass!, this._config!, ev.detail.action); - } - static get styles(): CSSResultGroup { return css` div { diff --git a/src/panels/lovelace/entity-rows/hui-weather-entity-row.ts b/src/panels/lovelace/entity-rows/hui-weather-entity-row.ts index 8d06737195..e2e0971399 100644 --- a/src/panels/lovelace/entity-rows/hui-weather-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-weather-entity-row.ts @@ -9,8 +9,6 @@ import { import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { ifDefined } from "lit/directives/if-defined"; -import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const"; -import { computeDomain } from "../../../common/entity/compute_domain"; import { computeStateDisplay } from "../../../common/entity/compute_state_display"; import { computeStateName } from "../../../common/entity/compute_state_name"; import { formatNumber } from "../../../common/number/format_number"; @@ -67,10 +65,9 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow { `; } - const pointer = - (this._config.tap_action && this._config.tap_action.action !== "none") || - (this._config.entity && - !DOMAINS_HIDE_MORE_INFO.includes(computeDomain(this._config.entity))); + const pointer = !( + this._config.tap_action && this._config.tap_action.action !== "none" + ); const weatherStateIcon = getWeatherStateIcon(stateObj.state, this); @@ -106,7 +103,16 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow { > ${this._config.name || computeStateName(stateObj)}
-
+
${UNAVAILABLE_STATES.includes(stateObj.state) ? computeStateDisplay( diff --git a/src/panels/lovelace/special-rows/hui-attribute-row.ts b/src/panels/lovelace/special-rows/hui-attribute-row.ts index 99e4a57907..f50c8d4e41 100644 --- a/src/panels/lovelace/special-rows/hui-attribute-row.ts +++ b/src/panels/lovelace/special-rows/hui-attribute-row.ts @@ -63,22 +63,20 @@ class HuiAttributeRow extends LitElement implements LovelaceRow { return html` -
- ${this._config.prefix} - ${this._config.format && checkValidDate(date) - ? html` ` - : typeof attribute === "number" - ? formatNumber(attribute, this.hass.locale) - : attribute !== undefined - ? formatAttributeValue(this.hass, attribute) - : "-"} - ${this._config.suffix} -
+ ${this._config.prefix} + ${this._config.format && checkValidDate(date) + ? html` ` + : typeof attribute === "number" + ? formatNumber(attribute, this.hass.locale) + : attribute !== undefined + ? formatAttributeValue(this.hass, attribute) + : "-"} + ${this._config.suffix}
`; } From 0c75d5afc94778c0d942df43d263b7728034af27 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Thu, 2 Dec 2021 10:49:46 +0100 Subject: [PATCH 083/112] Make graph colors themable (#10698) --- src/common/color/colors.ts | 11 +++++++++++ src/components/chart/state-history-chart-line.ts | 6 +++--- src/components/chart/state-history-chart-timeline.ts | 4 ++-- src/components/chart/statistics-chart.ts | 12 +++++++++--- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/common/color/colors.ts b/src/common/color/colors.ts index fd9de4d3df..3f276c8ac9 100644 --- a/src/common/color/colors.ts +++ b/src/common/color/colors.ts @@ -61,3 +61,14 @@ export const COLORS = [ export function getColorByIndex(index: number) { return COLORS[index % COLORS.length]; } + +export function getGraphColorByIndex( + index: number, + style: CSSStyleDeclaration +) { + // The CSS vars for the colors use range 1..n, so we need to adjust the index from the internal 0..n color index range. + return ( + style.getPropertyValue(`--graph-color-${index + 1}`) || + getColorByIndex(index) + ); +} diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index e19b6586a5..100f9dd7af 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -1,7 +1,7 @@ import type { ChartData, ChartDataset, ChartOptions } from "chart.js"; import { html, LitElement, PropertyValues } from "lit"; import { property, state } from "lit/decorators"; -import { getColorByIndex } from "../../common/color/colors"; +import { getGraphColorByIndex } from "../../common/color/colors"; import { formatNumber, numberFormatToLocale, @@ -164,7 +164,7 @@ class StateHistoryChartLine extends LitElement { const pushData = (timestamp: Date, datavalues: any[] | null) => { if (!datavalues) return; if (timestamp > endTime) { - // Drop datapoints that are after the requested endTime. This could happen if + // Drop data points that are after the requested endTime. This could happen if // endTime is "now" and client time is not in sync with server time. return; } @@ -190,7 +190,7 @@ class StateHistoryChartLine extends LitElement { color?: string ) => { if (!color) { - color = getColorByIndex(colorIndex); + color = getGraphColorByIndex(colorIndex, computedStyles); colorIndex++; } data.push({ diff --git a/src/components/chart/state-history-chart-timeline.ts b/src/components/chart/state-history-chart-timeline.ts index 802baf85f4..413ee0a831 100644 --- a/src/components/chart/state-history-chart-timeline.ts +++ b/src/components/chart/state-history-chart-timeline.ts @@ -2,7 +2,7 @@ import type { ChartData, ChartDataset, ChartOptions } from "chart.js"; import { HassEntity } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { getColorByIndex } from "../../common/color/colors"; +import { getGraphColorByIndex } from "../../common/color/colors"; import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; import { computeDomain } from "../../common/entity/compute_domain"; import { numberFormatToLocale } from "../../common/number/format_number"; @@ -71,7 +71,7 @@ const getColor = ( stateColorMap.set(stateString, color); return color; } - const color = getColorByIndex(colorIndex); + const color = getGraphColorByIndex(colorIndex, computedStyles); colorIndex++; stateColorMap.set(stateString, color); return color; diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index 1c2615ea07..944b467a41 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -13,7 +13,7 @@ import { TemplateResult, } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { getColorByIndex } from "../../common/color/colors"; +import { getGraphColorByIndex } from "../../common/color/colors"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { computeStateName } from "../../common/entity/compute_state_name"; import { @@ -59,6 +59,8 @@ class StatisticsChart extends LitElement { @state() private _chartOptions?: ChartOptions; + private _computedStyle?: CSSStyleDeclaration; + protected shouldUpdate(changedProps: PropertyValues): boolean { return changedProps.size > 1 || !changedProps.has("hass"); } @@ -72,6 +74,10 @@ class StatisticsChart extends LitElement { } } + public firstUpdated() { + this._computedStyle = getComputedStyle(this); + } + protected render(): TemplateResult { if (!isComponentLoaded(this.hass, "history")) { return html`
@@ -261,7 +267,7 @@ class StatisticsChart extends LitElement { ) => { if (!dataValues) return; if (timestamp > endTime) { - // Drop datapoints that are after the requested endTime. This could happen if + // Drop data points that are after the requested endTime. This could happen if // endTime is "now" and client time is not in sync with server time. return; } @@ -280,7 +286,7 @@ class StatisticsChart extends LitElement { prevValues = dataValues; }; - const color = getColorByIndex(colorIndex); + const color = getGraphColorByIndex(colorIndex, this._computedStyle!); colorIndex++; const statTypes: this["statTypes"] = []; From 2fe8f5ff27863302500d6c2bae0804649c48f681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 2 Dec 2021 11:05:14 +0100 Subject: [PATCH 084/112] Use puzzle for addons and blur entries on click (#10755) --- src/panels/config/dashboard/ha-config-navigation.ts | 6 +++++- src/panels/config/ha-panel-config.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/panels/config/dashboard/ha-config-navigation.ts b/src/panels/config/dashboard/ha-config-navigation.ts index 4a505ffb46..ab1ee2e56e 100644 --- a/src/panels/config/dashboard/ha-config-navigation.ts +++ b/src/panels/config/dashboard/ha-config-navigation.ts @@ -47,7 +47,7 @@ class HaConfigNavigation extends LitElement { canShowPage(this.hass, page) ? html` - +
Date: Thu, 2 Dec 2021 11:54:05 +0100 Subject: [PATCH 085/112] Fix create backup checkbox (#10756) --- hassio/src/update-available/update-available-card.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/hassio/src/update-available/update-available-card.ts b/hassio/src/update-available/update-available-card.ts index e1643e223a..e45d10588e 100644 --- a/hassio/src/update-available/update-available-card.ts +++ b/hassio/src/update-available/update-available-card.ts @@ -194,7 +194,7 @@ class UpdateAvailableCard extends LitElement { @@ -224,7 +224,11 @@ class UpdateAvailableCard extends LitElement { } get _shouldCreateBackup(): boolean { - return this.shadowRoot?.querySelector("ha-checkbox")?.checked || true; + const checkbox = this.shadowRoot?.querySelector("ha-checkbox"); + if (checkbox) { + return checkbox.checked; + } + return true; } get _version(): string { From a6b5262d02aa1b171c99f76e49392e0c9177a7f4 Mon Sep 17 00:00:00 2001 From: rianadon Date: Thu, 2 Dec 2021 08:27:23 -0800 Subject: [PATCH 086/112] Use unit system definitions for weather units (#10657) --- package.json | 2 +- src/data/weather.ts | 10 ++-------- src/fake_data/demo_config.ts | 3 +++ yarn.lock | 10 +++++----- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index df1ce7d236..5b05a0ddcd 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "fuse.js": "^6.0.0", "google-timezones-json": "^1.0.2", "hls.js": "^1.0.11", - "home-assistant-js-websocket": "^5.11.1", + "home-assistant-js-websocket": "^5.11.3", "idb-keyval": "^5.1.3", "intl-messageformat": "^9.9.1", "js-yaml": "^4.1.0", diff --git a/src/data/weather.ts b/src/data/weather.ts index 17e91ccf6c..be668288d1 100644 --- a/src/data/weather.ts +++ b/src/data/weather.ts @@ -152,17 +152,11 @@ export const getWeatherUnit = ( hass: HomeAssistant, measure: string ): string => { - const lengthUnit = hass.config.unit_system.length || ""; switch (measure) { - case "pressure": - return lengthUnit === "km" ? "hPa" : "inHg"; - case "wind_speed": - return `${lengthUnit}/h`; case "visibility": - case "length": - return lengthUnit; + return hass.config.unit_system.length || ""; case "precipitation": - return lengthUnit === "km" ? "mm" : "in"; + return hass.config.unit_system.accumulated_precipitation || ""; case "humidity": case "precipitation_probability": return "%"; diff --git a/src/fake_data/demo_config.ts b/src/fake_data/demo_config.ts index b1a4d9837f..ae5a528ff3 100644 --- a/src/fake_data/demo_config.ts +++ b/src/fake_data/demo_config.ts @@ -10,6 +10,9 @@ export const demoConfig: HassConfig = { mass: "kg", temperature: "°C", volume: "L", + pressure: "Pa", + wind_speed: "m/s", + accumulated_precipitation: "mm", }, components: [ "notify.html5", diff --git a/yarn.lock b/yarn.lock index 1322aea8a2..e40476833a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9113,7 +9113,7 @@ fsevents@^1.2.7: gulp-rename: ^2.0.0 gulp-zopfli-green: ^3.0.1 hls.js: ^1.0.11 - home-assistant-js-websocket: ^5.11.1 + home-assistant-js-websocket: ^5.11.3 html-minifier: ^4.0.0 husky: ^1.3.1 idb-keyval: ^5.1.3 @@ -9184,10 +9184,10 @@ fsevents@^1.2.7: languageName: unknown linkType: soft -"home-assistant-js-websocket@npm:^5.11.1": - version: 5.11.1 - resolution: "home-assistant-js-websocket@npm:5.11.1" - checksum: 4b3f4310ea15f758a47082ddde06ed46eeddcae490a59a16dbcec4fb798507a5cc4761b9e880261aed9f83335475abea8356d5239c081774caa335e5e76fba50 +"home-assistant-js-websocket@npm:^5.11.3": + version: 5.11.3 + resolution: "home-assistant-js-websocket@npm:5.11.3" + checksum: 3ab90e5105c5f379d77fb23ab53eaec2789be7bf1fd507a7520d9cf329d36942b8e978a591b822cff96100630d43bd036a4e25e2f49c40d0c56a111808fb90a5 languageName: node linkType: hard From cea1a62867898a649a3e8502873dec892b39d6ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 2 Dec 2021 17:30:10 +0100 Subject: [PATCH 087/112] handle ha-radio and ha-checkbox in ha-formfield (#10759) --- src/components/ha-formfield.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/components/ha-formfield.ts b/src/components/ha-formfield.ts index e0452e0db7..8a1e60e480 100644 --- a/src/components/ha-formfield.ts +++ b/src/components/ha-formfield.ts @@ -5,6 +5,22 @@ import { customElement } from "lit/decorators"; @customElement("ha-formfield") // @ts-expect-error export class HaFormfield extends Formfield { + protected _labelClick() { + const input = this.input; + if (input) { + input.focus(); + switch (input.tagName) { + case "HA-CHECKBOX": + case "HA-RADIO": + (input as any).checked = !(input as any).checked; + break; + default: + input.click(); + break; + } + } + } + protected static get styles(): CSSResultGroup { return [ Formfield.styles, From 649417782152266995f460cc40d2397977cf73b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 2 Dec 2021 17:31:09 +0100 Subject: [PATCH 088/112] Fix SU sidebar issues (#10757) --- src/components/ha-sidebar.ts | 10 ++++++---- src/panels/config/dashboard/ha-config-navigation.ts | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index af59b17c51..962bf47e89 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -370,9 +370,7 @@ class HaSidebar extends LitElement { private _renderPanels(panels: PanelInfo[]) { return panels.map((panel) => this._renderPanel( - panel.url_path === "hassio" - ? "config/dashboard?focusedPath=hassio" - : panel.url_path, + panel.url_path, panel.url_path === this.hass.defaultPanel ? panel.title || this.hass.localize("panel.states") : this.hass.localize(`panel.${panel.title}`) || panel.title, @@ -395,7 +393,11 @@ class HaSidebar extends LitElement { return html` Date: Thu, 2 Dec 2021 17:31:41 +0100 Subject: [PATCH 089/112] Use add-ons for mobile header (#10760) --- hassio/src/dashboard/hassio-dashboard.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/hassio/src/dashboard/hassio-dashboard.ts b/hassio/src/dashboard/hassio-dashboard.ts index 17b9454310..967136dece 100644 --- a/hassio/src/dashboard/hassio-dashboard.ts +++ b/hassio/src/dashboard/hassio-dashboard.ts @@ -35,7 +35,11 @@ class HassioDashboard extends LitElement { hasFab > - ${this.supervisor.localize("panel.dashboard")} + ${this.supervisor.localize( + atLeastVersion(this.hass.config.version, 2021, 12) + ? "panel.addons" + : "panel.dashboard" + )}
${this.hass.config.version.includes("dev") || From 6877fd9e00e04ec8f772d7ec58d8e4b7339cec7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 2 Dec 2021 17:32:18 +0100 Subject: [PATCH 090/112] Hide updates for dev as well (#10761) --- hassio/src/dashboard/hassio-dashboard.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hassio/src/dashboard/hassio-dashboard.ts b/hassio/src/dashboard/hassio-dashboard.ts index 967136dece..917ff856a1 100644 --- a/hassio/src/dashboard/hassio-dashboard.ts +++ b/hassio/src/dashboard/hassio-dashboard.ts @@ -42,8 +42,7 @@ class HassioDashboard extends LitElement { )}
- ${this.hass.config.version.includes("dev") || - !atLeastVersion(this.hass.config.version, 2021, 12) + ${!atLeastVersion(this.hass.config.version, 2021, 12) ? html` Date: Thu, 2 Dec 2021 20:26:41 +0100 Subject: [PATCH 091/112] Remove thingtalk cleanup create new automation dialog (#10748) Co-authored-by: Paulus Schoutsen --- src/components/ha-blueprint-picker.ts | 4 + .../automation/dialog-new-automation.ts | 143 +++++------------- .../config/automation/ha-automation-picker.ts | 5 +- 3 files changed, 46 insertions(+), 106 deletions(-) diff --git a/src/components/ha-blueprint-picker.ts b/src/components/ha-blueprint-picker.ts index 73df1d44a2..ba17709a76 100644 --- a/src/components/ha-blueprint-picker.ts +++ b/src/components/ha-blueprint-picker.ts @@ -23,6 +23,10 @@ class HaBluePrintPicker extends LitElement { @property({ type: Boolean }) public disabled = false; + public open() { + this.shadowRoot!.querySelector("paper-dropdown-menu-light")!.open(); + } + private _processedBlueprints = memoizeOne((blueprints?: Blueprints) => { if (!blueprints) { return []; diff --git a/src/panels/config/automation/dialog-new-automation.ts b/src/panels/config/automation/dialog-new-automation.ts index a86d06bc19..3f00995b05 100644 --- a/src/panels/config/automation/dialog-new-automation.ts +++ b/src/panels/config/automation/dialog-new-automation.ts @@ -1,24 +1,19 @@ import "@material/mwc-button"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { fireEvent } from "../../../common/dom/fire_event"; import { nextRender } from "../../../common/util/render-status"; import "../../../components/ha-blueprint-picker"; import "../../../components/ha-card"; import "../../../components/ha-circular-progress"; import { createCloseHeading } from "../../../components/ha-dialog"; -import { - AutomationConfig, - showAutomationEditor, -} from "../../../data/automation"; -import { - HassDialog, - replaceDialog, -} from "../../../dialogs/make-dialog-manager"; +import { showAutomationEditor } from "../../../data/automation"; +import { HassDialog } from "../../../dialogs/make-dialog-manager"; import { haStyle, haStyleDialog } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; -import { showThingtalkDialog } from "./thingtalk/show-dialog-thingtalk"; +import "@material/mwc-list/mwc-list-item"; +import "../../../components/ha-icon-next"; +import "@material/mwc-list/mwc-list"; @customElement("ha-dialog-new-automation") class DialogNewAutomation extends LitElement implements HassDialog { @@ -42,84 +37,52 @@ class DialogNewAutomation extends LitElement implements HassDialog { return html` -
- ${this.hass.localize("ui.panel.config.automation.dialog_new.how")} -
- ${isComponentLoaded(this.hass, "cloud") - ? html` -
-

- ${this.hass.localize( - "ui.panel.config.automation.dialog_new.thingtalk.header" - )} -

- ${this.hass.localize( - "ui.panel.config.automation.dialog_new.thingtalk.intro" - )} -
- - ${this.hass.localize( - "ui.panel.config.automation.dialog_new.thingtalk.create" - )} -
-
-
` - : html``} - ${isComponentLoaded(this.hass, "blueprint") - ? html` -
-

- ${this.hass.localize( - "ui.panel.config.automation.dialog_new.blueprint.use_blueprint" - )} -

- -
-
` - : html``} -
-
- - ${this.hass.localize( - "ui.panel.config.automation.dialog_new.start_empty" - )} - + + + ${this.hass.localize( + "ui.panel.config.automation.dialog_new.blueprint.use_blueprint" + )} + + + + +
  • + + ${this.hass.localize( + "ui.panel.config.automation.dialog_new.start_empty" + )} + + ${this.hass.localize( + "ui.panel.config.automation.dialog_new.start_empty_description" + )} + + +
    `; } - private _thingTalk() { - replaceDialog(); - showThingtalkDialog(this, { - callback: (config: Partial | undefined) => - showAutomationEditor(config), - input: this.shadowRoot!.querySelector("paper-input")!.value as string, - }); - this.closeDialog(); - } - private async _blueprintPicked(ev: CustomEvent) { this.closeDialog(); await nextRender(); showAutomationEditor({ use_blueprint: { path: ev.detail.value } }); } + private async _blueprint() { + this.shadowRoot!.querySelector("ha-blueprint-picker")!.open(); + } + private async _blank() { this.closeDialog(); await nextRender(); @@ -131,38 +94,14 @@ class DialogNewAutomation extends LitElement implements HassDialog { haStyle, haStyleDialog, css` - .container { - display: flex; - } - ha-card { - width: calc(50% - 8px); - margin: 4px; - } - ha-card div { - height: 100%; - display: flex; - flex-direction: column; - justify-content: space-between; - } - ha-card { - box-sizing: border-box; - padding: 8px; + mwc-list-item.blueprint { + height: 92px; } ha-blueprint-picker { - width: 100%; + margin-top: -16px; } - .side-by-side { - display: flex; - flex-direction: row; - align-items: flex-end; - } - @media all and (max-width: 500px) { - .container { - flex-direction: column; - } - ha-card { - width: 100%; - } + ha-dialog { + --dialog-content-padding: 0; } `, ]; diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index 0008f75059..cf06bc92ea 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -315,10 +315,7 @@ class HaAutomationPicker extends LitElement { }; private _createNew() { - if ( - isComponentLoaded(this.hass, "cloud") || - isComponentLoaded(this.hass, "blueprint") - ) { + if (isComponentLoaded(this.hass, "blueprint")) { showNewAutomationDialog(this); } else { navigate("/config/automation/edit/new"); From 251416b51d7ff453973007d063c3776c3d943aab Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 2 Dec 2021 13:01:19 -0800 Subject: [PATCH 092/112] Add missing translation (#10769) --- 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 a5cb447e03..d92bd7b38f 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1440,7 +1440,8 @@ "input_label": "What should this automation do?", "create": "Create" }, - "start_empty": "Start with an empty automation" + "start_empty": "Start with an empty automation", + "start_empty_description": "Create a new automation from scratch" }, "editor": { "enable_disable": "Enable/Disable automation", From 60ce805b3b37d9fc9daf7a3be46a149a6d5fb22c Mon Sep 17 00:00:00 2001 From: Carlos Garcia Saura Date: Thu, 2 Dec 2021 22:32:38 +0100 Subject: [PATCH 093/112] Update hui-graph-header-footer.ts (#10476) --- .../lovelace/header-footer/hui-graph-header-footer.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/panels/lovelace/header-footer/hui-graph-header-footer.ts b/src/panels/lovelace/header-footer/hui-graph-header-footer.ts index 79b2f1b8ca..ec03889ba4 100644 --- a/src/panels/lovelace/header-footer/hui-graph-header-footer.ts +++ b/src/panels/lovelace/header-footer/hui-graph-header-footer.ts @@ -193,21 +193,13 @@ export class HuiGraphHeaderFooter this._stateHistory!.push(...stateHistory[0]); } - const limits = - this._config!.limits === undefined && - this._stateHistory?.some( - (entity) => entity.attributes?.unit_of_measurement === "%" - ) - ? { min: 0, max: 100 } - : this._config!.limits; - this._coordinates = coordinates( this._stateHistory, this._config!.hours_to_show!, 500, this._config!.detail!, - limits + this._config!.limits ) || []; this._date = endTime; From 48d12ceafe3d023d5f013cdeb4fcf2f6d678e75e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 2 Dec 2021 23:15:18 +0100 Subject: [PATCH 094/112] Group entities in area card by domain (#10767) * Group entities in area card by domain * Update hui-area-card.ts * Update * Add background color when no image * Add camera support * exclude unavailable states * Update hui-area-card.ts --- src/panels/lovelace/cards/hui-area-card.ts | 374 ++++++++++++++------- 1 file changed, 252 insertions(+), 122 deletions(-) diff --git a/src/panels/lovelace/cards/hui-area-card.ts b/src/panels/lovelace/cards/hui-area-card.ts index e9a72c7d16..abe4897942 100644 --- a/src/panels/lovelace/cards/hui-area-card.ts +++ b/src/panels/lovelace/cards/hui-area-card.ts @@ -1,4 +1,12 @@ import "@material/mwc-ripple"; +import { + mdiLightbulbMultiple, + mdiLightbulbMultipleOff, + mdiRun, + mdiToggleSwitch, + mdiToggleSwitchOff, + mdiWaterPercent, +} from "@mdi/js"; import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, @@ -10,13 +18,13 @@ import { } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; -import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; +import { STATES_OFF } from "../../../common/const"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; -import { fireEvent } from "../../../common/dom/fire_event"; import { computeDomain } from "../../../common/entity/compute_domain"; -import { computeStateDisplay } from "../../../common/entity/compute_state_display"; +import { domainIcon } from "../../../common/entity/domain_icon"; import { navigate } from "../../../common/navigate"; +import { formatNumber } from "../../../common/number/format_number"; import "../../../components/entity/state-badge"; import "../../../components/ha-card"; import "../../../components/ha-icon-button"; @@ -30,31 +38,40 @@ import { DeviceRegistryEntry, subscribeDeviceRegistry, } from "../../../data/device_registry"; +import { UNAVAILABLE_STATES } from "../../../data/entity"; import { EntityRegistryEntry, subscribeEntityRegistry, } from "../../../data/entity_registry"; import { forwardHaptic } from "../../../data/haptics"; -import { ActionHandlerEvent } from "../../../data/lovelace"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../../types"; -import { actionHandler } from "../common/directives/action-handler-directive"; -import { toggleEntity } from "../common/entity/toggle-entity"; import "../components/hui-warning"; import { LovelaceCard, LovelaceCardEditor } from "../types"; import { AreaCardConfig } from "./types"; -const SENSOR_DOMAINS = new Set(["sensor", "binary_sensor"]); +const SENSOR_DOMAINS = ["sensor"]; -const SENSOR_DEVICE_CLASSES = new Set([ - "temperature", - "humidity", - "motion", - "door", - "aqi", -]); +const ALERT_DOMAINS = ["binary_sensor"]; -const TOGGLE_DOMAINS = new Set(["light", "fan", "switch"]); +const TOGGLE_DOMAINS = ["light", "switch", "fan"]; + +const OTHER_DOMAINS = ["camera"]; + +const DEVICE_CLASSES = { + sensor: ["temperature"], + binary_sensor: ["motion"], +}; + +const DOMAIN_ICONS = { + light: { on: mdiLightbulbMultiple, off: mdiLightbulbMultipleOff }, + switch: { on: mdiToggleSwitch, off: mdiToggleSwitchOff }, + fan: { on: domainIcon("fan"), off: domainIcon("fan") }, + sensor: { humidity: mdiWaterPercent }, + binary_sensor: { + motion: mdiRun, + }, +}; @customElement("hui-area-card") export class HuiAreaCard @@ -80,7 +97,7 @@ export class HuiAreaCard @state() private _areas?: AreaRegistryEntry[]; - private _memberships = memoizeOne( + private _entitiesByDomain = memoizeOne( ( areaId: string, devicesInArea: Set, @@ -97,44 +114,98 @@ export class HuiAreaCard ) .map((entry) => entry.entity_id); - const sensorEntities: HassEntity[] = []; - const entitiesToggle: HassEntity[] = []; + const entitiesByDomain: { [domain: string]: HassEntity[] } = {}; for (const entity of entitiesInArea) { const domain = computeDomain(entity); - if (!TOGGLE_DOMAINS.has(domain) && !SENSOR_DOMAINS.has(domain)) { + if ( + !TOGGLE_DOMAINS.includes(domain) && + !SENSOR_DOMAINS.includes(domain) && + !ALERT_DOMAINS.includes(domain) && + !OTHER_DOMAINS.includes(domain) + ) { continue; } - const stateObj: HassEntity | undefined = states[entity]; if (!stateObj) { continue; } - if (entitiesToggle.length < 3 && TOGGLE_DOMAINS.has(domain)) { - entitiesToggle.push(stateObj); + if ( + (SENSOR_DOMAINS.includes(domain) || ALERT_DOMAINS.includes(domain)) && + !DEVICE_CLASSES[domain].includes( + stateObj.attributes.device_class || "" + ) + ) { continue; } - if ( - sensorEntities.length < 3 && - SENSOR_DOMAINS.has(domain) && - stateObj.attributes.device_class && - SENSOR_DEVICE_CLASSES.has(stateObj.attributes.device_class) - ) { - sensorEntities.push(stateObj); - } - - if (sensorEntities.length === 3 && entitiesToggle.length === 3) { - break; + if (!(domain in entitiesByDomain)) { + entitiesByDomain[domain] = []; } + entitiesByDomain[domain].push(stateObj); } - return { sensorEntities, entitiesToggle }; + return entitiesByDomain; } ); + private _isOn(domain: string, deviceClass?: string): boolean | undefined { + const entities = this._entitiesByDomain( + this._config!.area, + this._devicesInArea(this._config!.area, this._devices!), + this._entities!, + this.hass.states + )[domain]; + if (!entities) { + return undefined; + } + return ( + deviceClass + ? entities.filter( + (entity) => entity.attributes.device_class === deviceClass + ) + : entities + ).some( + (entity) => + !UNAVAILABLE_STATES.includes(entity.state) && + !STATES_OFF.includes(entity.state) + ); + } + + private _average(domain: string, deviceClass?: string): string | undefined { + const entities = this._entitiesByDomain( + this._config!.area, + this._devicesInArea(this._config!.area, this._devices!), + this._entities!, + this.hass.states + )[domain].filter((entity) => + deviceClass ? entity.attributes.device_class === deviceClass : true + ); + if (!entities) { + return undefined; + } + let uom; + const values = entities.filter((entity) => { + if (!entity.attributes.unit_of_measurement) { + return false; + } + if (!uom) { + uom = entity.attributes.unit_of_measurement; + return true; + } + return entity.attributes.unit_of_measurement === uom; + }); + if (!values.length) { + return undefined; + } + const sum = values.reduce((a, b) => a + Number(b.state), 0); + return `${formatNumber(sum / values.length, this.hass!.locale, { + maximumFractionDigits: 1, + })} ${uom}`; + } + private _area = memoizeOne( (areaId: string | undefined, areas: AreaRegistryEntry[]) => areas.find((area) => area.area_id === areaId) || null @@ -212,22 +283,18 @@ export class HuiAreaCard return false; } - const { sensorEntities, entitiesToggle } = this._memberships( + const entities = this._entitiesByDomain( this._config.area, this._devicesInArea(this._config.area, this._devices), this._entities, this.hass.states ); - for (const stateObj of sensorEntities) { - if (oldHass!.states[stateObj.entity_id] !== stateObj) { - return true; - } - } - - for (const stateObj of entitiesToggle) { - if (oldHass!.states[stateObj.entity_id] !== stateObj) { - return true; + for (const domainEntities of Object.values(entities)) { + for (const stateObj of domainEntities) { + if (oldHass!.states[stateObj.entity_id] !== stateObj) { + return true; + } } } @@ -245,13 +312,12 @@ export class HuiAreaCard return html``; } - const { sensorEntities, entitiesToggle } = this._memberships( + const entitiesByDomain = this._entitiesByDomain( this._config.area, this._devicesInArea(this._config.area, this._devices), this._entities, this.hass.states ); - const area = this._area(this._config.area, this._areas); if (area === null) { @@ -262,62 +328,98 @@ export class HuiAreaCard `; } + const sensors: TemplateResult[] = []; + SENSOR_DOMAINS.forEach((domain) => { + if (!(domain in entitiesByDomain)) { + return; + } + DEVICE_CLASSES[domain].forEach((deviceClass) => { + if ( + entitiesByDomain[domain].some( + (entity) => entity.attributes.device_class === deviceClass + ) + ) { + sensors.push(html` + ${DOMAIN_ICONS[domain][deviceClass] + ? html`` + : ""} + ${this._average(domain, deviceClass)} + `); + } + }); + }); + + let cameraEntityId: string | undefined; + if ("camera" in entitiesByDomain) { + cameraEntityId = entitiesByDomain.camera[0].entity_id; + } + return html` - -
    -
    - ${sensorEntities.map( - (stateObj) => html` - - - ${computeDomain(stateObj.entity_id) === "binary_sensor" - ? "" - : html` - ${computeStateDisplay( - this.hass!.localize, - stateObj, - this.hass!.locale - )} - `} - - ` - )} + + ${area.picture || cameraEntityId + ? html`` + : ""} + +
    - +
    + + +
    + ${this.hass.localize( + "ui.panel.config.zwave_js.dashboard.driver_version" + )}: + ${this._network.client.driver_version}
    + ${this.hass.localize( + "ui.panel.config.zwave_js.dashboard.server_version" + )}: + ${this._network.client.server_version}
    + ${this.hass.localize( + "ui.panel.config.zwave_js.dashboard.home_id" + )}: + ${this._network.controller.home_id}
    + ${this.hass.localize( + "ui.panel.config.zwave_js.dashboard.server_url" + )}: + ${this._network.client.ws_server_url}
    +
    +
    + ${this.hass.localize( - "ui.panel.config.zwave_js.common.add_node" + "ui.panel.config.zwave_js.dashboard.dump_debug" )} - + ${this.hass.localize( "ui.panel.config.zwave_js.common.remove_node" )} - + ${this.hass.localize( "ui.panel.config.zwave_js.common.heal_network" )} - + ${this.hass.localize( "ui.panel.config.zwave_js.common.reconfigure_server" )} @@ -229,12 +267,19 @@ class ZWaveJSConfigDashboard extends LitElement { ` : ``} - + + + `; } @@ -486,7 +531,6 @@ class ZWaveJSConfigDashboard extends LitElement { .network-status div.heading { display: flex; align-items: center; - margin-bottom: 16px; } .network-status div.heading .icon { diff --git a/src/translations/en.json b/src/translations/en.json index d92bd7b38f..4ee6773a57 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2808,8 +2808,10 @@ "driver_version": "Driver Version", "server_version": "Server Version", "home_id": "Home ID", - "nodes_ready": "Devices ready", - "dump_debug": "Download a dump of your network to help diagnose issues", + "server_url": "Server URL", + "devices": "{count} {count, plural,\n one {device}\n other {devices}\n}", + "not_ready": "{count} not ready", + "dump_debug": "Download data", "dump_dead_nodes_title": "Some of your devices are dead", "dump_dead_nodes_text": "Some of your devices didn't respond and are assumed dead. These will not be fully exported.", "dump_not_ready_title": "Not all devices are ready yet", From 46a9e365166caeffc965452f10931f7d724c8f3e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 3 Dec 2021 12:53:50 +0100 Subject: [PATCH 101/112] Guard for non numeric states (#10775) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joakim Sørensen --- src/panels/lovelace/cards/hui-area-card.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/panels/lovelace/cards/hui-area-card.ts b/src/panels/lovelace/cards/hui-area-card.ts index abe4897942..617f6336fc 100644 --- a/src/panels/lovelace/cards/hui-area-card.ts +++ b/src/panels/lovelace/cards/hui-area-card.ts @@ -188,7 +188,10 @@ export class HuiAreaCard } let uom; const values = entities.filter((entity) => { - if (!entity.attributes.unit_of_measurement) { + if ( + !entity.attributes.unit_of_measurement || + isNaN(Number(entity.state)) + ) { return false; } if (!uom) { @@ -200,7 +203,10 @@ export class HuiAreaCard if (!values.length) { return undefined; } - const sum = values.reduce((a, b) => a + Number(b.state), 0); + const sum = values.reduce( + (total, entity) => total + Number(entity.state), + 0 + ); return `${formatNumber(sum / values.length, this.hass!.locale, { maximumFractionDigits: 1, })} ${uom}`; From e28a11964e5e86a75e45ee61c6c3ed6f80e413e9 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Fri, 3 Dec 2021 15:08:49 +0100 Subject: [PATCH 102/112] Use correct styling for cloud certificate dialog (#10782) --- .../dialog-cloud-certificate/dialog-cloud-certificate.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts b/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts index 0616c0edd9..da05e069d8 100644 --- a/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts +++ b/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts @@ -3,7 +3,7 @@ import { css, CSSResultGroup, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { formatDateTime } from "../../../../common/datetime/format_date_time"; import { fireEvent } from "../../../../common/dom/fire_event"; -import { haStyle } from "../../../../resources/styles"; +import { haStyleDialog } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; import type { CloudCertificateParams as CloudCertificateDialogParams } from "./show-dialog-cloud-certificate"; @@ -68,7 +68,7 @@ class DialogCloudCertificate extends LitElement { static get styles(): CSSResultGroup { return [ - haStyle, + haStyleDialog, css` ha-dialog { --mdc-dialog-max-width: 535px; From 95dbc811d318e9331347639d0ab71cd7f68e96e1 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 3 Dec 2021 16:07:49 +0100 Subject: [PATCH 103/112] Allow overriding device class (#10777) --- src/data/entity_registry.ts | 3 + .../entities/entity-registry-settings.ts | 57 ++++++++++++++++++- src/translations/en.json | 14 +++++ 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts index becf455fc0..84fdbccec5 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -21,6 +21,8 @@ export interface ExtEntityRegistryEntry extends EntityRegistryEntry { capabilities: Record; original_name?: string; original_icon?: string; + device_class?: string; + original_device_class?: string; } export interface UpdateEntityRegistryEntryResult { @@ -32,6 +34,7 @@ export interface UpdateEntityRegistryEntryResult { export interface EntityRegistryEntryUpdateParams { name?: string | null; icon?: string | null; + device_class?: string | null; area_id?: string | null; disabled_by?: string | null; new_entity_id?: string; diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index 96fab20b01..0a489ae01a 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -1,5 +1,6 @@ import "@material/mwc-button/mwc-button"; import "@polymer/paper-input/paper-input"; +import type { PaperItemElement } from "@polymer/paper-item/paper-item"; import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, @@ -16,6 +17,7 @@ import { domainIcon } from "../../../common/entity/domain_icon"; import "../../../components/ha-area-picker"; import "../../../components/ha-expansion-panel"; import "../../../components/ha-icon-picker"; +import "../../../components/ha-paper-dropdown-menu"; import "../../../components/ha-switch"; import type { HaSwitch } from "../../../components/ha-switch"; import { @@ -39,6 +41,11 @@ import { haStyle } from "../../../resources/styles"; 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"], + binary_sensor: ["window", "door", "garage_door", "opening"], +}; + @customElement("entity-registry-settings") export class EntityRegistrySettings extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @@ -51,6 +58,8 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { @state() private _entityId!: string; + @state() private _deviceClass?: string; + @state() private _areaId?: string | null; @state() private _disabledBy!: string | null; @@ -85,6 +94,8 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { 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; @@ -102,9 +113,11 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { } const stateObj: HassEntity | undefined = this.hass.states[this.entry.entity_id]; - const invalidDomainUpdate = - computeDomain(this._entityId.trim()) !== - computeDomain(this.entry.entity_id); + + const domain = computeDomain(this.entry.entity_id); + + const invalidDomainUpdate = computeDomain(this._entityId.trim()) !== domain; + return html` ${!stateObj ? html` @@ -143,6 +156,31 @@ 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}` + )} + + ` + )} + + ` + : ""} ): void { + this._error = undefined; + if (ev.detail.value === null) { + return; + } + const value = (ev.detail.value as any).itemValue; + this._deviceClass = value === "null" ? null : value; + } + private _areaPicked(ev: CustomEvent) { this._error = undefined; this._areaId = ev.detail.value; @@ -289,6 +336,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { name: this._name.trim() || null, icon: this._icon.trim() || null, area_id: this._areaId || null, + device_class: this._deviceClass || null, new_entity_id: this._entityId.trim(), }; if ( @@ -378,6 +426,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { padding-bottom: max(env(safe-area-inset-bottom), 8px); background-color: var(--mdc-theme-surface, #fff); } + ha-paper-dropdown-menu { + width: 100%; + } ha-switch { margin-right: 16px; } diff --git a/src/translations/en.json b/src/translations/en.json index 4ee6773a57..bd2574bde1 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -694,6 +694,20 @@ "icon": "Icon", "icon_error": "Icons should be in the format 'prefix:iconname', e.g. 'mdi:home'", "entity_id": "Entity ID", + "device_class": "Show as", + "device_classes": { + "binary_sensor": { + "door": "Door", + "garage_door": "Garage door", + "window": "Window", + "opening": "Other" + }, + "cover": { + "door": "Door", + "garage": "Garage door", + "window": "Window" + } + }, "unavailable": "This entity is unavailable.", "enabled_label": "Enable entity", "enabled_cause": "Disabled by {cause}.", From 0bcb4d0e0969f268dae4b39ddee727c6d51687c8 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Fri, 3 Dec 2021 16:19:00 +0100 Subject: [PATCH 104/112] Restore flex alignment for select and input-select rows (#10783) --- src/panels/lovelace/entity-rows/hui-input-select-entity-row.ts | 2 +- src/panels/lovelace/entity-rows/hui-select-entity-row.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 d0f76ce9ed..55f384a6c9 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 @@ -110,7 +110,7 @@ class HuiInputSelectEntityRow extends LitElement implements LovelaceRow { static get styles(): CSSResultGroup { return css` - :host { + hui-generic-entity-row { display: flex; align-items: center; } diff --git a/src/panels/lovelace/entity-rows/hui-select-entity-row.ts b/src/panels/lovelace/entity-rows/hui-select-entity-row.ts index d6d978f409..5aa6f103fd 100644 --- a/src/panels/lovelace/entity-rows/hui-select-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-select-entity-row.ts @@ -118,7 +118,7 @@ class HuiSelectEntityRow extends LitElement implements LovelaceRow { static get styles(): CSSResultGroup { return css` - :host { + hui-generic-entity-row { display: flex; align-items: center; } From a54a2a54f8860c3c26efc7d4f915bc1cfe7574a2 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 3 Dec 2021 16:34:34 +0100 Subject: [PATCH 105/112] Add support for local only users (#10784) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joakim Sørensen --- src/data/user.ts | 6 +- .../config/person/dialog-person-detail.ts | 52 +++++++++++++----- src/panels/config/users/dialog-add-user.ts | 55 ++++++++++++++----- src/panels/config/users/dialog-user-detail.ts | 26 ++++++++- src/panels/config/users/ha-config-users.ts | 15 ++++- src/translations/en.json | 6 +- 6 files changed, 127 insertions(+), 33 deletions(-) diff --git a/src/data/user.ts b/src/data/user.ts index 7b108b3e7c..c4afd4aa68 100644 --- a/src/data/user.ts +++ b/src/data/user.ts @@ -13,6 +13,7 @@ export interface User { name: string; is_owner: boolean; is_active: boolean; + local_only: boolean; system_generated: boolean; group_ids: string[]; credentials: Credential[]; @@ -22,6 +23,7 @@ export interface UpdateUserParams { name?: User["name"]; is_active?: User["is_active"]; group_ids?: User["group_ids"]; + local_only?: boolean; } export const fetchUsers = async (hass: HomeAssistant) => @@ -33,12 +35,14 @@ export const createUser = async ( hass: HomeAssistant, name: string, // eslint-disable-next-line: variable-name - group_ids?: User["group_ids"] + group_ids?: User["group_ids"], + local_only?: boolean ) => hass.callWS<{ user: User }>({ type: "config/auth/create", name, group_ids, + local_only, }); export const updateUser = async ( diff --git a/src/panels/config/person/dialog-person-detail.ts b/src/panels/config/person/dialog-person-detail.ts index 06f1f05841..2724d8ef2e 100644 --- a/src/panels/config/person/dialog-person-detail.ts +++ b/src/panels/config/person/dialog-person-detail.ts @@ -51,6 +51,8 @@ class DialogPersonDetail extends LitElement { @state() private _isAdmin?: boolean; + @state() private _localOnly?: boolean; + @state() private _deviceTrackers!: string[]; @state() private _picture!: string | null; @@ -83,12 +85,14 @@ class DialogPersonDetail extends LitElement { ? this._params.users.find((user) => user.id === this._userId) : undefined; this._isAdmin = this._user?.group_ids.includes(SYSTEM_GROUP_ID_ADMIN); + this._localOnly = this._user?.local_only; } else { this._personExists = false; this._name = ""; this._userId = undefined; this._user = undefined; this._isAdmin = undefined; + this._localOnly = undefined; this._deviceTrackers = []; this._picture = null; } @@ -152,19 +156,31 @@ class DialogPersonDetail extends LitElement { ${this._user ? html` - - - ` + + + + + + + ` : ""} ${this._deviceTrackersAvailable(this.hass) ? html` @@ -266,10 +282,14 @@ class DialogPersonDetail extends LitElement { this._name = ev.detail.value; } - private async _adminChanged(ev): Promise { + private _adminChanged(ev): void { this._isAdmin = ev.target.checked; } + private _localOnlyChanged(ev): void { + this._localOnly = ev.target.checked; + } + private async _allowLoginChanged(ev): Promise { const target = ev.target; if (target.checked) { @@ -281,6 +301,7 @@ class DialogPersonDetail extends LitElement { this._user = user; this._userId = user.id; this._isAdmin = user.group_ids.includes(SYSTEM_GROUP_ID_ADMIN); + this._localOnly = user.local_only; this._params?.refreshUsers(); } }, @@ -373,13 +394,16 @@ class DialogPersonDetail extends LitElement { try { if ( (this._userId && this._name !== this._params!.entry?.name) || - this._isAdmin !== this._user?.group_ids.includes(SYSTEM_GROUP_ID_ADMIN) + this._isAdmin !== + this._user?.group_ids.includes(SYSTEM_GROUP_ID_ADMIN) || + this._localOnly !== this._user?.local_only ) { await updateUser(this.hass!, this._userId!, { name: this._name.trim(), group_ids: [ this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER, ], + local_only: this._localOnly, }); this._params?.refreshUsers(); } diff --git a/src/panels/config/users/dialog-add-user.ts b/src/panels/config/users/dialog-add-user.ts index 462dd46fde..65d11ac262 100644 --- a/src/panels/config/users/dialog-add-user.ts +++ b/src/panels/config/users/dialog-add-user.ts @@ -48,6 +48,8 @@ export class DialogAddUser extends LitElement { @state() private _isAdmin?: boolean; + @state() private _localOnly?: boolean; + @state() private _allowChangeName = true; public showDialog(params: AddUserDialogParams) { @@ -57,6 +59,7 @@ export class DialogAddUser extends LitElement { this._password = ""; this._passwordConfirm = ""; this._isAdmin = false; + this._localOnly = false; this._error = undefined; this._loading = false; @@ -153,14 +156,32 @@ export class DialogAddUser extends LitElement { "ui.panel.config.users.add_user.password_not_match" )} > - - - - - +
    + + + + +
    +
    + + + + +
    ${!this._isAdmin ? html`
    @@ -218,6 +239,10 @@ export class DialogAddUser extends LitElement { this._isAdmin = ev.target.checked; } + private _localOnlyChanged(ev): void { + this._localOnly = ev.target.checked; + } + private async _createUser(ev) { ev.preventDefault(); if (!this._name || !this._username || !this._password) { @@ -229,9 +254,12 @@ export class DialogAddUser extends LitElement { let user: User; try { - const userResponse = await createUser(this.hass, this._name, [ - this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER, - ]); + const userResponse = await createUser( + this.hass, + this._name, + [this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER], + this._localOnly + ); user = userResponse.user; } catch (err: any) { this._loading = false; @@ -266,8 +294,9 @@ export class DialogAddUser extends LitElement { --mdc-dialog-max-width: 500px; --dialog-z-index: 10; } - ha-switch { - margin-top: 8px; + .row { + display: flex; + padding: 8px 0; } `, ]; diff --git a/src/panels/config/users/dialog-user-detail.ts b/src/panels/config/users/dialog-user-detail.ts index f90f9c979d..3809131a65 100644 --- a/src/panels/config/users/dialog-user-detail.ts +++ b/src/panels/config/users/dialog-user-detail.ts @@ -30,6 +30,8 @@ class DialogUserDetail extends LitElement { @state() private _isAdmin?: boolean; + @state() private _localOnly?: boolean; + @state() private _isActive?: boolean; @state() private _error?: string; @@ -43,6 +45,7 @@ class DialogUserDetail extends LitElement { this._error = undefined; this._name = params.entry.name || ""; this._isAdmin = params.entry.group_ids.includes(SYSTEM_GROUP_ID_ADMIN); + this._localOnly = params.entry.local_only; this._isActive = params.entry.is_active; await this.updateComplete; } @@ -95,6 +98,20 @@ class DialogUserDetail extends LitElement { @value-changed=${this._nameChanged} label=${this.hass!.localize("ui.panel.config.users.editor.name")} > +
    + + + + +
    { + private _adminChanged(ev): void { this._isAdmin = ev.target.checked; } - private async _activeChanged(ev): Promise { + private _localOnlyChanged(ev): void { + this._localOnly = ev.target.checked; + } + + private _activeChanged(ev): void { this._isActive = ev.target.checked; } @@ -215,6 +236,7 @@ class DialogUserDetail extends LitElement { group_ids: [ this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER, ], + local_only: this._localOnly, }); this._close(); } catch (err: any) { diff --git a/src/panels/config/users/ha-config-users.ts b/src/panels/config/users/ha-config-users.ts index 84b0ca9f11..dfff21062c 100644 --- a/src/panels/config/users/ha-config-users.ts +++ b/src/panels/config/users/ha-config-users.ts @@ -90,7 +90,7 @@ export class HaConfigUsers extends LitElement { width: "80px", template: (is_active) => is_active - ? html` ` + ? html`` : "", }, system_generated: { @@ -103,9 +103,20 @@ export class HaConfigUsers extends LitElement { width: "160px", template: (generated) => generated - ? html` ` + ? html`` : "", }, + local_only: { + title: this.hass.localize( + "ui.panel.config.users.picker.headers.local" + ), + type: "icon", + sortable: true, + filterable: true, + width: "160px", + template: (local) => + local ? html`` : "", + }, }; return columns; diff --git a/src/translations/en.json b/src/translations/en.json index bd2574bde1..b6c5d18cd8 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2297,6 +2297,7 @@ "update": "Update", "confirm_delete_user": "Are you sure you want to delete the user account for {name}? You can still track the user, but the person will no longer be able to login.", "admin": "[%key:ui::panel::config::users::editor::admin%]", + "local_only": "[%key:ui::panel::config::users::editor::local_only%]", "allow_login": "Allow person to login" } }, @@ -2456,7 +2457,8 @@ "group": "Group", "system": "System generated", "is_active": "Active", - "is_owner": "Owner" + "is_owner": "Owner", + "local": "Local only" }, "add_user": "Add user" }, @@ -2476,6 +2478,7 @@ "admin": "Administrator", "group": "Group", "active": "Active", + "local_only": "Can only login from the local network", "system_generated": "System generated", "system_generated_users_not_removable": "Unable to remove system generated users.", "system_generated_users_not_editable": "Unable to update system generated users.", @@ -2488,6 +2491,7 @@ "password": "Password", "password_confirm": "Confirm Password", "password_not_match": "Passwords don't match", + "local_only": "Local only", "create": "Create" } }, From db4aa05bf4a314e858aa9a80980e1f86552299f3 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 3 Dec 2021 17:21:26 +0100 Subject: [PATCH 106/112] Differentiate between assigned and targeting scene/automations/script (#10781) --- .../config/areas/ha-config-area-page.ts | 299 +++++++++++------- .../config/areas/ha-config-areas-dashboard.ts | 9 +- 2 files changed, 191 insertions(+), 117 deletions(-) diff --git a/src/panels/config/areas/ha-config-area-page.ts b/src/panels/config/areas/ha-config-area-page.ts index a1a46886db..d81440ccc9 100644 --- a/src/panels/config/areas/ha-config-area-page.ts +++ b/src/panels/config/areas/ha-config-area-page.ts @@ -35,6 +35,10 @@ import { loadAreaRegistryDetailDialog, showAreaRegistryDetailDialog, } from "./show-dialog-area-registry-detail"; +import { computeDomain } from "../../../common/entity/compute_domain"; +import { SceneEntity } from "../../../data/scene"; +import { ScriptEntity } from "../../../data/script"; +import { AutomationEntity } from "../../../data/automation"; @customElement("ha-config-area-page") class HaConfigAreaPage extends LitElement { @@ -131,6 +135,16 @@ class HaConfigAreaPage extends LitElement { this.entities ); + const sceneEntities = entities.filter( + (entity) => computeDomain(entity.entity_id) === "scene" + ); + const scriptEntities = entities.filter( + (entity) => computeDomain(entity.entity_id) === "script" + ); + const automationEntities = entities.filter( + (entity) => computeDomain(entity.entity_id) === "automation" + ); + return html` ${entities.length - ? entities.map( - (entity) => - html` - - - ${computeEntityRegistryName(this.hass, entity)} - - - - ` + ? entities.map((entity) => + ["scene", "script", "automation"].includes( + computeDomain(entity.entity_id) + ) + ? "" + : html` + + + ${computeEntityRegistryName(this.hass, entity)} + + + + ` ) : html` ${this._related?.automation?.length - ? this._related.automation.map((automation) => { - const entityState = this.hass.states[automation]; - return entityState - ? html` -
    - - - - ${computeStateName(entityState)} - - - - - ${!entityState.attributes.id - ? html` - - ${this.hass.localize( - "ui.panel.config.devices.cant_edit" - )} - - ` - : ""} -
    - ` - : ""; - }) - : html` + > + ${automationEntities.length + ? html`

    Assigned to this area:

    + ${automationEntities.map((entity) => { + const entityState = this.hass.states[ + entity.entity_id + ] as AutomationEntity | undefined; + return entityState + ? this._renderAutomation(entityState) + : ""; + })}` + : ""} + ${this._related?.automation?.filter( + (entityId) => + !automationEntities.find( + (entity) => entity.entity_id === entityId + ) + ).length + ? html`

    Targeting this area:

    + ${this._related.automation.map((scene) => { + const entityState = this.hass.states[scene] as + | AutomationEntity + | undefined; + return entityState + ? this._renderAutomation(entityState) + : ""; + })}` + : ""} + ${!automationEntities.length && + !this._related?.automation?.length + ? html` ${this.hass.localize( "ui.panel.config.devices.automation.no_automations" )} - `} + ` + : ""} ` : ""} @@ -304,48 +317,40 @@ class HaConfigAreaPage extends LitElement { .header=${this.hass.localize( "ui.panel.config.devices.scene.scenes" )} - >${this._related?.scene?.length - ? this._related.scene.map((scene) => { - const entityState = this.hass.states[scene]; - return entityState - ? html` -
    - - - - ${computeStateName(entityState)} - - - - - ${!entityState.attributes.id - ? html` - - ${this.hass.localize( - "ui.panel.config.devices.cant_edit" - )} - - ` - : ""} -
    - ` - : ""; - }) - : html` + > + ${sceneEntities.length + ? html`

    Assigned to this area:

    + ${sceneEntities.map((entity) => { + const entityState = + this.hass.states[entity.entity_id]; + return entityState + ? this._renderScene(entityState) + : ""; + })}` + : ""} + ${this._related?.scene?.filter( + (entityId) => + !sceneEntities.find( + (entity) => entity.entity_id === entityId + ) + ).length + ? html`

    Targeting this area:

    + ${this._related.scene.map((scene) => { + const entityState = this.hass.states[scene]; + return entityState + ? this._renderScene(entityState) + : ""; + })}` + : ""} + ${!sceneEntities.length && !this._related?.scene?.length + ? html` ${this.hass.localize( "ui.panel.config.devices.scene.no_scenes" )} - `} + ` + : ""} ` : ""} @@ -355,31 +360,43 @@ class HaConfigAreaPage extends LitElement { .header=${this.hass.localize( "ui.panel.config.devices.script.scripts" )} - >${this._related?.script?.length - ? this._related.script.map((script) => { - const entityState = this.hass.states[script]; - return entityState - ? html` - - - - ${computeStateName(entityState)} - - - - - ` - : ""; - }) - : html` - - ${this.hass.localize( + > + ${scriptEntities.length + ? html`

    Assigned to this area:

    + ${scriptEntities.map((entity) => { + const entityState = this.hass.states[ + entity.entity_id + ] as ScriptEntity | undefined; + return entityState + ? this._renderScript(entityState) + : ""; + })}` + : ""} + ${this._related?.script?.filter( + (entityId) => + !scriptEntities.find( + (entity) => entity.entity_id === entityId + ) + ).length + ? html`

    Targeting this area:

    + ${this._related.script.map((scene) => { + const entityState = this.hass.states[scene] as + | ScriptEntity + | undefined; + return entityState + ? this._renderScript(entityState) + : ""; + })}` + : ""} + ${!scriptEntities.length && !this._related?.script?.length + ? html` + ${this.hass.localize( "ui.panel.config.devices.script.no_scripts" )} - `} + ` + : ""} ` : ""} @@ -389,6 +406,63 @@ class HaConfigAreaPage extends LitElement { `; } + private _renderScene(entityState: SceneEntity) { + return html`
    + + + ${computeStateName(entityState)} + + + + ${!entityState.attributes.id + ? html` + + ${this.hass.localize("ui.panel.config.devices.cant_edit")} + + ` + : ""} +
    `; + } + + private _renderAutomation(entityState: AutomationEntity) { + return html`
    + + + ${computeStateName(entityState)} + + + + ${!entityState.attributes.id + ? html` + + ${this.hass.localize("ui.panel.config.devices.cant_edit")} + + ` + : ""} +
    `; + } + + private _renderScript(entityState: ScriptEntity) { + return html` + + ${computeStateName(entityState)} + + + `; + } + private async _findRelated() { this._related = await findRelated(this.hass, "area", this.areaId); } @@ -457,6 +531,13 @@ class HaConfigAreaPage extends LitElement { align-items: center; } + h3 { + margin: 0; + padding: 0 16px; + font-weight: 500; + color: var(--secondary-text-color); + } + img { border-radius: var(--ha-card-border-radius, 4px); width: 100%; diff --git a/src/panels/config/areas/ha-config-areas-dashboard.ts b/src/panels/config/areas/ha-config-areas-dashboard.ts index 6f47226bf9..404639a6a6 100644 --- a/src/panels/config/areas/ha-config-areas-dashboard.ts +++ b/src/panels/config/areas/ha-config-areas-dashboard.ts @@ -50,11 +50,8 @@ export class HaConfigAreasDashboard extends LitElement { let noServicesInArea = 0; let noEntitiesInArea = 0; - const devicesInArea = new Set(); - for (const device of devices) { if (device.area_id === area.area_id) { - devicesInArea.add(device.id); if (device.entry_type === "service") { noServicesInArea++; } else { @@ -64,11 +61,7 @@ export class HaConfigAreasDashboard extends LitElement { } for (const entity of entities) { - if ( - entity.area_id - ? entity.area_id === area.area_id - : devicesInArea.has(entity.device_id) - ) { + if (entity.area_id === area.area_id) { noEntitiesInArea++; } } From c71b2e6b9d5a5db3071f07100a4417aaed6b598d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 3 Dec 2021 17:34:26 +0100 Subject: [PATCH 107/112] Add provisioned device overview to zwave js (#10785) --- src/data/zwave_js.ts | 32 +++++ .../entities/entity-registry-settings.ts | 5 +- .../zwave_js/zwave_js-config-dashboard.ts | 28 +++- .../zwave_js/zwave_js-config-router.ts | 4 + .../zwave_js/zwave_js-provisioned.ts | 128 ++++++++++++++++++ src/translations/en.json | 8 ++ 6 files changed, 197 insertions(+), 8 deletions(-) create mode 100644 src/panels/config/integrations/integration-panels/zwave_js/zwave_js-provisioned.ts diff --git a/src/data/zwave_js.ts b/src/data/zwave_js.ts index cf278eaf2b..9296ca8990 100644 --- a/src/data/zwave_js.ts +++ b/src/data/zwave_js.ts @@ -205,6 +205,16 @@ export const enum NodeStatus { Alive, } +export interface ZwaveJSProvisioningEntry { + /** The device specific key (DSK) in the form aaaaa-bbbbb-ccccc-ddddd-eeeee-fffff-11111-22222 */ + dsk: string; + securityClasses: SecurityClass[]; + /** + * Additional properties to be stored in this provisioning entry, e.g. the device ID from a scanned QR code + */ + [prop: string]: any; +} + export interface RequestedGrant { /** * An array of security classes that are requested or to be granted. @@ -265,6 +275,15 @@ export const setZwaveDataCollectionPreference = ( opted_in, }); +export const fetchZwaveProvisioningEntries = ( + hass: HomeAssistant, + entry_id: string +): Promise => + hass.callWS({ + type: "zwave_js/get_provisioning_entries", + entry_id, + }); + export const subscribeAddZwaveNode = ( hass: HomeAssistant, entry_id: string, @@ -350,6 +369,19 @@ export const provisionZwaveSmartStartNode = ( planned_provisioning_entry, }); +export const unprovisionZwaveSmartStartNode = ( + hass: HomeAssistant, + entry_id: string, + dsk?: string, + node_id?: number +): Promise => + hass.callWS({ + type: "zwave_js/unprovision_smart_start_node", + entry_id, + dsk, + node_id, + }); + export const fetchZwaveNodeStatus = ( hass: HomeAssistant, entry_id: string, diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index 0a489ae01a..798d9e1412 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -171,7 +171,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { > ${OVERRIDE_DEVICE_CLASSES[domain].map( (deviceClass: string) => html` - + ${this.hass.localize( `ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}` )} @@ -307,8 +307,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { if (ev.detail.value === null) { return; } - const value = (ev.detail.value as any).itemValue; - this._deviceClass = value === "null" ? null : value; + this._deviceClass = (ev.detail.value as any).itemValue; } private _areaPicked(ev: CustomEvent) { 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 fd68822b05..413e6bd099 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 @@ -19,10 +19,12 @@ import { fetchZwaveDataCollectionStatus, fetchZwaveNetworkStatus, fetchZwaveNodeStatus, + fetchZwaveProvisioningEntries, NodeStatus, setZwaveDataCollectionPreference, ZWaveJSNetwork, ZWaveJSNodeStatus, + ZwaveJSProvisioningEntry, } from "../../../../../data/zwave_js"; import { ConfigEntry, @@ -63,6 +65,8 @@ class ZWaveJSConfigDashboard extends LitElement { @state() private _nodes?: ZWaveJSNodeStatus[]; + @state() private _provisioningEntries?: ZwaveJSProvisioningEntry[]; + @state() private _status = "unknown"; @state() private _icon = mdiCircle; @@ -85,7 +89,7 @@ class ZWaveJSConfigDashboard extends LitElement { } const notReadyDevices = - this._nodes?.filter((node) => node.ready).length ?? 0; + this._nodes?.filter((node) => !node.ready).length ?? 0; return html` + ${this._provisioningEntries?.length + ? html` + ${this.hass.localize( + "ui.panel.config.zwave_js.dashboard.provisioned_devices" + )} + ` + : ""}
    @@ -361,10 +375,14 @@ class ZWaveJSConfigDashboard extends LitElement { return; } - const [network, dataCollectionStatus] = await Promise.all([ - fetchZwaveNetworkStatus(this.hass!, this.configEntryId), - fetchZwaveDataCollectionStatus(this.hass!, this.configEntryId), - ]); + const [network, dataCollectionStatus, provisioningEntries] = + await Promise.all([ + fetchZwaveNetworkStatus(this.hass!, this.configEntryId), + fetchZwaveDataCollectionStatus(this.hass!, this.configEntryId), + fetchZwaveProvisioningEntries(this.hass!, this.configEntryId), + ]); + + this._provisioningEntries = provisioningEntries; this._network = network; diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-router.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-router.ts index 685259c549..6f9f165aaf 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-router.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-router.ts @@ -49,6 +49,10 @@ class ZWaveJSConfigRouter extends HassRouterPage { tag: "zwave_js-logs", load: () => import("./zwave_js-logs"), }, + provisioned: { + tag: "zwave_js-provisioned", + load: () => import("./zwave_js-provisioned"), + }, }, }; diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-provisioned.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-provisioned.ts new file mode 100644 index 0000000000..cad49415cf --- /dev/null +++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-provisioned.ts @@ -0,0 +1,128 @@ +import { mdiDelete } from "@mdi/js"; +import { html, LitElement } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { DataTableColumnContainer } from "../../../../../components/data-table/ha-data-table"; +import { + ZwaveJSProvisioningEntry, + fetchZwaveProvisioningEntries, + SecurityClass, + unprovisionZwaveSmartStartNode, +} from "../../../../../data/zwave_js"; +import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box"; +import "../../../../../layouts/hass-tabs-subpage-data-table"; +import { HomeAssistant, Route } from "../../../../../types"; +import { configTabs } from "./zwave_js-config-router"; + +@customElement("zwave_js-provisioned") +class ZWaveJSProvisioned extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Object }) public route!: Route; + + @property({ type: Boolean }) public narrow!: boolean; + + @property() public configEntryId!: string; + + @state() private _provisioningEntries: ZwaveJSProvisioningEntry[] = []; + + protected render() { + return html` + + + `; + } + + private _columns = memoizeOne( + (narrow: boolean): DataTableColumnContainer => ({ + dsk: { + title: this.hass.localize("ui.panel.config.zwave_js.provisioned.dsk"), + sortable: true, + filterable: true, + grows: true, + }, + securityClasses: { + title: this.hass.localize( + "ui.panel.config.zwave_js.provisioned.security_classes" + ), + width: "15%", + hidden: narrow, + filterable: true, + sortable: true, + template: (securityClasses: SecurityClass[]) => + securityClasses + .map((secClass) => + this.hass.localize( + `ui.panel.config.zwave_js.security_classes.${SecurityClass[secClass]}` + ) + ) + .join(", "), + }, + unprovision: { + title: this.hass.localize( + "ui.panel.config.zwave_js.provisioned.unprovison" + ), + type: "icon-button", + template: (_info, provisioningEntry: any) => html` + + `, + }, + }) + ); + + protected firstUpdated(changedProps) { + super.firstUpdated(changedProps); + this._fetchData(); + } + + private async _fetchData() { + this._provisioningEntries = await fetchZwaveProvisioningEntries( + this.hass!, + this.configEntryId + ); + } + + private _unprovision = async (ev) => { + const confirm = await showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.zwave_js.provisioned.confirm_unprovision_title" + ), + text: this.hass.localize( + "ui.panel.config.zwave_js.provisioned.confirm_unprovision_text" + ), + confirmText: this.hass.localize( + "ui.panel.config.zwave_js.provisioned.unprovison" + ), + }); + + if (!confirm) { + return; + } + + await unprovisionZwaveSmartStartNode( + this.hass, + this.configEntryId, + ev.currentTarget.provisioningEntry.dsk + ); + }; +} + +declare global { + interface HTMLElementTagNameMap { + "zwave_js-provisioned": ZWaveJSProvisioned; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index b6c5d18cd8..72931282ad 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2828,6 +2828,7 @@ "home_id": "Home ID", "server_url": "Server URL", "devices": "{count} {count, plural,\n one {device}\n other {devices}\n}", + "provisioned_devices": "Provisioned devices", "not_ready": "{count} not ready", "dump_debug": "Download data", "dump_dead_nodes_title": "Some of your devices are dead", @@ -2892,6 +2893,13 @@ "interview_started": "The device is being interviewed. This may take some time.", "interview_failed": "The device interview failed. Additional information may be available in the logs." }, + "provisioned": { + "dsk": "DSK", + "security_classes": "Security classes", + "unprovison": "Unprovison", + "confirm_unprovision_title": "Are you sure you want to unprovision the device?", + "confirm_unprovision_text": "If you unprovision the device it will not be added to Home Assistant when it is powered on. If it is already added to Home Assistant, removing the provisioned device will not remove it from Home Assistant." + }, "security_classes": { "None": { "title": "None" From 1df11e9bf1b671f064211ddd8871a11e6441fafc Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 3 Dec 2021 17:42:23 +0100 Subject: [PATCH 108/112] Use groupBy (#10786) --- .../config/areas/ha-config-area-page.ts | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/src/panels/config/areas/ha-config-area-page.ts b/src/panels/config/areas/ha-config-area-page.ts index d81440ccc9..be0db42577 100644 --- a/src/panels/config/areas/ha-config-area-page.ts +++ b/src/panels/config/areas/ha-config-area-page.ts @@ -39,6 +39,7 @@ import { computeDomain } from "../../../common/entity/compute_domain"; import { SceneEntity } from "../../../data/scene"; import { ScriptEntity } from "../../../data/script"; import { AutomationEntity } from "../../../data/automation"; +import { groupBy } from "../../../common/util/group-by"; @customElement("ha-config-area-page") class HaConfigAreaPage extends LitElement { @@ -135,14 +136,8 @@ class HaConfigAreaPage extends LitElement { this.entities ); - const sceneEntities = entities.filter( - (entity) => computeDomain(entity.entity_id) === "scene" - ); - const scriptEntities = entities.filter( - (entity) => computeDomain(entity.entity_id) === "script" - ); - const automationEntities = entities.filter( - (entity) => computeDomain(entity.entity_id) === "automation" + const grouped = groupBy(entities, (entity) => + computeDomain(entity.entity_id) ); return html` @@ -269,9 +264,9 @@ class HaConfigAreaPage extends LitElement { "ui.panel.config.devices.automation.automations" )} > - ${automationEntities.length + ${grouped.automation?.length ? html`

    Assigned to this area:

    - ${automationEntities.map((entity) => { + ${grouped.automation.map((entity) => { const entityState = this.hass.states[ entity.entity_id ] as AutomationEntity | undefined; @@ -282,7 +277,7 @@ class HaConfigAreaPage extends LitElement { : ""} ${this._related?.automation?.filter( (entityId) => - !automationEntities.find( + !grouped.automation?.find( (entity) => entity.entity_id === entityId ) ).length @@ -296,7 +291,7 @@ class HaConfigAreaPage extends LitElement { : ""; })}` : ""} - ${!automationEntities.length && + ${!grouped.automation?.length && !this._related?.automation?.length ? html` - ${sceneEntities.length + ${grouped.scene?.length ? html`

    Assigned to this area:

    - ${sceneEntities.map((entity) => { + ${grouped.scene.map((entity) => { const entityState = this.hass.states[entity.entity_id]; return entityState @@ -330,7 +325,7 @@ class HaConfigAreaPage extends LitElement { : ""} ${this._related?.scene?.filter( (entityId) => - !sceneEntities.find( + !grouped.scene?.find( (entity) => entity.entity_id === entityId ) ).length @@ -342,7 +337,7 @@ class HaConfigAreaPage extends LitElement { : ""; })}` : ""} - ${!sceneEntities.length && !this._related?.scene?.length + ${!grouped.scene?.length && !this._related?.scene?.length ? html` ${this.hass.localize( @@ -361,9 +356,9 @@ class HaConfigAreaPage extends LitElement { "ui.panel.config.devices.script.scripts" )} > - ${scriptEntities.length + ${grouped.script?.length ? html`

    Assigned to this area:

    - ${scriptEntities.map((entity) => { + ${grouped.script.map((entity) => { const entityState = this.hass.states[ entity.entity_id ] as ScriptEntity | undefined; @@ -374,7 +369,7 @@ class HaConfigAreaPage extends LitElement { : ""} ${this._related?.script?.filter( (entityId) => - !scriptEntities.find( + !grouped.script?.find( (entity) => entity.entity_id === entityId ) ).length @@ -388,7 +383,7 @@ class HaConfigAreaPage extends LitElement { : ""; })}` : ""} - ${!scriptEntities.length && !this._related?.script?.length + ${!grouped.script?.length && !this._related?.script?.length ? html` ${this.hass.localize( From ea18fc0078b051487e7e67bcb7221a32b2f180fc Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Fri, 3 Dec 2021 18:02:54 +0100 Subject: [PATCH 109/112] Ensure we always have an active theme name (fixes dark theme issues) (#10780) Co-authored-by: Bram Kragten --- .../src/components/demo-black-white-row.ts | 18 +++----- gallery/src/demos/demo-ha-alert.ts | 18 +++----- src/auth/ha-authorize.ts | 18 +++----- src/common/dom/apply_themes_on_element.ts | 46 ++++++++++--------- src/data/ws-themes.ts | 2 + src/fake_data/provide_hass.ts | 1 + src/onboarding/ha-onboarding.ts | 18 +++----- src/state/themes-mixin.ts | 21 ++++----- src/types.ts | 5 +- 9 files changed, 69 insertions(+), 78 deletions(-) diff --git a/gallery/src/components/demo-black-white-row.ts b/gallery/src/components/demo-black-white-row.ts index a3b1233ffb..92a4beeffe 100644 --- a/gallery/src/components/demo-black-white-row.ts +++ b/gallery/src/components/demo-black-white-row.ts @@ -52,17 +52,13 @@ class DemoBlackWhiteRow extends LitElement { firstUpdated(changedProps) { super.firstUpdated(changedProps); - applyThemesOnElement( - this.shadowRoot!.querySelector(".dark"), - { - default_theme: "default", - default_dark_theme: "default", - themes: {}, - darkMode: false, - }, - "default", - { dark: true } - ); + applyThemesOnElement(this.shadowRoot!.querySelector(".dark"), { + default_theme: "default", + default_dark_theme: "default", + themes: {}, + darkMode: true, + theme: "default", + }); } handleSubmit(ev) { diff --git a/gallery/src/demos/demo-ha-alert.ts b/gallery/src/demos/demo-ha-alert.ts index 91286a1045..3a62c4a1d2 100644 --- a/gallery/src/demos/demo-ha-alert.ts +++ b/gallery/src/demos/demo-ha-alert.ts @@ -159,17 +159,13 @@ export class DemoHaAlert extends LitElement { firstUpdated(changedProps) { super.firstUpdated(changedProps); - applyThemesOnElement( - this.shadowRoot!.querySelector(".dark"), - { - default_theme: "default", - default_dark_theme: "default", - themes: {}, - darkMode: false, - }, - "default", - { dark: true } - ); + applyThemesOnElement(this.shadowRoot!.querySelector(".dark"), { + default_theme: "default", + default_dark_theme: "default", + themes: {}, + darkMode: true, + theme: "default", + }); } static get styles() { diff --git a/src/auth/ha-authorize.ts b/src/auth/ha-authorize.ts index acd98952c9..8f91bfe8bf 100644 --- a/src/auth/ha-authorize.ts +++ b/src/auth/ha-authorize.ts @@ -101,17 +101,13 @@ 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: false, - }, - "default", - { dark: true } - ); + applyThemesOnElement(document.documentElement, { + default_theme: "default", + default_dark_theme: null, + themes: {}, + darkMode: true, + theme: "default", + }); } if (!this.redirectUri) { diff --git a/src/common/dom/apply_themes_on_element.ts b/src/common/dom/apply_themes_on_element.ts index fca6e2262a..dd5aebc423 100644 --- a/src/common/dom/apply_themes_on_element.ts +++ b/src/common/dom/apply_themes_on_element.ts @@ -23,9 +23,9 @@ let PROCESSED_THEMES: Record = {}; * Apply a theme to an element by setting the CSS variables on it. * * element: Element to apply theme on. - * themes: HASS theme information. - * selectedTheme: Selected theme. - * themeSettings: Settings such as selected dark mode and colors. + * themes: HASS theme information (e.g. active dark mode and globally active theme name). + * selectedTheme: Selected theme (used to override the globally active theme for this element). + * themeSettings: Additional settings such as selected colors. */ export const applyThemesOnElement = ( element, @@ -33,31 +33,33 @@ export const applyThemesOnElement = ( selectedTheme?: string, themeSettings?: Partial ) => { - let cacheKey = selectedTheme; - let themeRules: Partial = {}; + // If there is no explicitly desired theme provided, we automatically + // use the active one from `themes`. + const themeToApply = selectedTheme || themes.theme; // If there is no explicitly desired dark mode provided, we automatically - // use the active one from hass.themes. - if (!themeSettings || themeSettings?.dark === undefined) { - themeSettings = { - ...themeSettings, - dark: themes.darkMode, - }; - } + // use the active one from `themes`. + const darkMode = + themeSettings && themeSettings?.dark !== undefined + ? themeSettings?.dark + : themes.darkMode; - if (themeSettings.dark) { + let cacheKey = themeToApply; + let themeRules: Partial = {}; + + if (darkMode) { cacheKey = `${cacheKey}__dark`; themeRules = { ...darkStyles }; } - if (selectedTheme === "default") { + if (themeToApply === "default") { // Determine the primary and accent colors from the current settings. // Fallbacks are implicitly the HA default blue and orange or the // derived "darkStyles" values, depending on the light vs dark mode. - const primaryColor = themeSettings.primaryColor; - const accentColor = themeSettings.accentColor; + const primaryColor = themeSettings?.primaryColor; + const accentColor = themeSettings?.accentColor; - if (themeSettings.dark && primaryColor) { + if (darkMode && primaryColor) { themeRules["app-header-background-color"] = hexBlend( primaryColor, "#121212", @@ -98,17 +100,17 @@ export const applyThemesOnElement = ( // Custom theme logic (not relevant for default theme, since it would override // the derived calculations from above) if ( - selectedTheme && - selectedTheme !== "default" && - themes.themes[selectedTheme] + themeToApply && + themeToApply !== "default" && + themes.themes[themeToApply] ) { // Apply theme vars that are relevant for all modes (but extract the "modes" section first) - const { modes, ...baseThemeRules } = themes.themes[selectedTheme]; + const { modes, ...baseThemeRules } = themes.themes[themeToApply]; themeRules = { ...themeRules, ...baseThemeRules }; // Apply theme vars for the specific mode if available if (modes) { - if (themeSettings?.dark) { + if (darkMode) { themeRules = { ...themeRules, ...modes.dark }; } else { themeRules = { ...themeRules, ...modes.light }; diff --git a/src/data/ws-themes.ts b/src/data/ws-themes.ts index 99a44064f9..619066e8a9 100644 --- a/src/data/ws-themes.ts +++ b/src/data/ws-themes.ts @@ -23,6 +23,8 @@ export interface Themes { // in theme picker, this property will still contain either true or false based on // what has been determined via system preferences and support from the selected theme. darkMode: boolean; + // Currently globally active theme name + theme: string; } const fetchThemes = (conn) => diff --git a/src/fake_data/provide_hass.ts b/src/fake_data/provide_hass.ts index 1be324de95..0f764be982 100644 --- a/src/fake_data/provide_hass.ts +++ b/src/fake_data/provide_hass.ts @@ -201,6 +201,7 @@ export const provideHass = ( default_dark_theme: null, themes: {}, darkMode: false, + theme: "default", }, panels: demoPanels, services: demoServices, diff --git a/src/onboarding/ha-onboarding.ts b/src/onboarding/ha-onboarding.ts index 7095e8a887..eb01f05877 100644 --- a/src/onboarding/ha-onboarding.ts +++ b/src/onboarding/ha-onboarding.ts @@ -133,17 +133,13 @@ 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: false, - }, - "default", - { dark: true } - ); + applyThemesOnElement(document.documentElement, { + default_theme: "default", + default_dark_theme: null, + themes: {}, + darkMode: true, + theme: "default", + }); } } diff --git a/src/state/themes-mixin.ts b/src/state/themes-mixin.ts index 9aa5a0ac2c..bb5c443a62 100644 --- a/src/state/themes-mixin.ts +++ b/src/state/themes-mixin.ts @@ -38,17 +38,13 @@ 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: false, - }, - "default", - { dark: true } - ); + applyThemesOnElement(document.documentElement, { + default_theme: "default", + default_dark_theme: null, + themes: {}, + darkMode: true, + theme: "default", + }); } } @@ -89,6 +85,9 @@ export default >(superClass: T) => } themeSettings = { ...this.hass.selectedTheme, dark: darkMode }; + this._updateHass({ + themes: { ...this.hass.themes!, theme: themeName }, + }); applyThemesOnElement( document.documentElement, diff --git a/src/types.ts b/src/types.ts index 539dcb610d..b6bab9a982 100644 --- a/src/types.ts +++ b/src/types.ts @@ -84,9 +84,12 @@ export interface CurrentUser { } // Currently selected theme and its settings. These are the values stored in local storage. +// Note: These values are not meant to be used at runtime to check whether dark mode is active +// or which theme name to use, as this interface represents the config data for the theme picker. +// The actually active dark mode and theme name can be read from hass.themes. export interface ThemeSettings { theme: string; - // Radio box selection for theme picker. Do not use in cards as + // Radio box selection for theme picker. Do not use in Lovelace rendering as // it can be undefined == auto. // Property hass.themes.darkMode carries effective current mode. dark?: boolean; From a283acaabfa6dad09d2331b88d3577cec0d9cd2c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 3 Dec 2021 18:04:03 +0100 Subject: [PATCH 110/112] safari doesnt support overflow-wrap: anywhere --- src/components/ha-alert.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/ha-alert.ts b/src/components/ha-alert.ts index 90c31c3cc6..b6f2f52a46 100644 --- a/src/components/ha-alert.ts +++ b/src/components/ha-alert.ts @@ -121,6 +121,7 @@ class HaAlert extends LitElement { } .main-content { overflow-wrap: anywhere; + word-break: break-word; margin-left: 8px; margin-right: 0; } From d43d19190e0573ed7b54ae563c97e603ea22b9b3 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 3 Dec 2021 18:04:50 +0100 Subject: [PATCH 111/112] Fix entity marker (#10787) --- src/components/map/ha-entity-marker.ts | 13 +++++++------ src/components/map/ha-map.ts | 4 +++- src/panels/lovelace/cards/hui-map-card.ts | 1 - 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/map/ha-entity-marker.ts b/src/components/map/ha-entity-marker.ts index b917c29259..3524c52a21 100644 --- a/src/components/map/ha-entity-marker.ts +++ b/src/components/map/ha-entity-marker.ts @@ -2,11 +2,8 @@ import { LitElement, html, css } from "lit"; import { property } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; import { fireEvent } from "../../common/dom/fire_event"; -import { HomeAssistant } from "../../types"; class HaEntityMarker extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: "entity-id" }) public entityId?: string; @property({ attribute: "entity-name" }) public entityName?: string; @@ -26,9 +23,7 @@ class HaEntityMarker extends LitElement { ? html`
    ` : this.entityName} @@ -69,3 +64,9 @@ class HaEntityMarker extends LitElement { } customElements.define("ha-entity-marker", HaEntityMarker); + +declare global { + interface HTMLElementTagNameMap { + "ha-entity-marker": HaEntityMarker; + } +} diff --git a/src/components/map/ha-map.ts b/src/components/map/ha-map.ts index 2835ebb2fc..9e56fa7379 100644 --- a/src/components/map/ha-map.ts +++ b/src/components/map/ha-map.ts @@ -412,7 +412,9 @@ export class HaMap extends ReactiveElement { Date: Fri, 3 Dec 2021 18:07:07 +0100 Subject: [PATCH 112/112] Bumped version to 20211203.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7618306ef3..3f9a1d8f35 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20211202.0", + version="20211203.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/frontend", author="The Home Assistant Authors",