From 1d498349c50b2cd4cd991f1943e35e1b54240a2f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 8 Feb 2021 16:09:40 +0100 Subject: [PATCH 01/44] Update container port (#8352) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update container port * Update .devcontainer/devcontainer.json Co-authored-by: Joakim Sørensen --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 99f9047671..977620c9dd 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,7 +4,7 @@ "dockerfile": "Dockerfile", "context": ".." }, - "appPort": 8123, + "appPort": "8124:8123", "context": "..", "postCreateCommand": "script/bootstrap", "extensions": [ From b75dc0efe01d019628fdf8d0c669118a6278a7d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 8 Feb 2021 16:18:01 +0100 Subject: [PATCH 02/44] Fix issue with jumping config (#8355) --- hassio/src/addon-view/config/hassio-addon-config.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/hassio/src/addon-view/config/hassio-addon-config.ts b/hassio/src/addon-view/config/hassio-addon-config.ts index 5762d17da9..be4a924fe9 100644 --- a/hassio/src/addon-view/config/hassio-addon-config.ts +++ b/hassio/src/addon-view/config/hassio-addon-config.ts @@ -150,13 +150,11 @@ class HassioAddonConfig extends LitElement { if (this.addon.schema && this._canShowSchema && !this._yamlMode) { this._valid = true; this._configHasChanged = true; + this._options! = ev.detail.value; } else { this._configHasChanged = true; this._valid = ev.detail.isValid; } - if (this._valid) { - this._options! = ev.detail.value; - } } private async _resetTapped(ev: CustomEvent): Promise { @@ -204,8 +202,9 @@ class HassioAddonConfig extends LitElement { try { await setHassioAddonOption(this.hass, this.addon.slug, { - options: this._options!, + options: this._yamlMode ? this._editor?.value : this._options, }); + this._configHasChanged = false; const eventdata = { success: true, From 48de8b073954860ca9ea1ebb6703e086ce7a1ded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 8 Feb 2021 16:18:33 +0100 Subject: [PATCH 03/44] Block snapshots when system is not running (#8350) --- .../snapshot/dialog-hassio-snapshot.ts | 29 ++++++++++++++++++- .../snapshot/show-dialog-hassio-snapshot.ts | 2 ++ hassio/src/snapshots/hassio-snapshots.ts | 17 ++++++++++- src/data/hassio/supervisor.ts | 9 ++++++ 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts b/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts index adf2612b9f..9bdeec7793 100755 --- a/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts +++ b/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts @@ -22,7 +22,11 @@ import { fetchHassioSnapshotInfo, HassioSnapshotDetail, } from "../../../../src/data/hassio/snapshot"; -import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box"; +import { Supervisor } from "../../../../src/data/supervisor/supervisor"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../../../src/dialogs/generic/show-dialog-box"; import { PolymerChangedEvent } from "../../../../src/polymer-types"; import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; import { HomeAssistant } from "../../../../src/types"; @@ -75,6 +79,8 @@ interface FolderItem { class HassioSnapshotDialog extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public supervisor?: Supervisor; + @internalProperty() private _error?: string; @internalProperty() private _onboarding = false; @@ -102,6 +108,7 @@ class HassioSnapshotDialog extends LitElement { this._dialogParams = params; this._onboarding = params.onboarding ?? false; + this.supervisor = params.supervisor; } protected render(): TemplateResult { @@ -298,6 +305,16 @@ class HassioSnapshotDialog extends LitElement { } private async _partialRestoreClicked() { + if ( + this.supervisor !== undefined && + this.supervisor.info.state !== "running" + ) { + await showAlertDialog(this, { + title: "Could not restore snapshot", + text: `Restoring a snapshot is not possible right now because the system is in ${this.supervisor.info.state} state.`, + }); + return; + } if ( !(await showConfirmationDialog(this, { title: "Are you sure you want partially to restore this snapshot?", @@ -359,6 +376,16 @@ class HassioSnapshotDialog extends LitElement { } private async _fullRestoreClicked() { + if ( + this.supervisor !== undefined && + this.supervisor.info.state !== "running" + ) { + await showAlertDialog(this, { + title: "Could not restore snapshot", + text: `Restoring a snapshot is not possible right now because the system is in ${this.supervisor.info.state} state.`, + }); + return; + } if ( !(await showConfirmationDialog(this, { title: diff --git a/hassio/src/dialogs/snapshot/show-dialog-hassio-snapshot.ts b/hassio/src/dialogs/snapshot/show-dialog-hassio-snapshot.ts index b5f6d964e9..8c7bcd2be7 100644 --- a/hassio/src/dialogs/snapshot/show-dialog-hassio-snapshot.ts +++ b/hassio/src/dialogs/snapshot/show-dialog-hassio-snapshot.ts @@ -1,9 +1,11 @@ import { fireEvent } from "../../../../src/common/dom/fire_event"; +import { Supervisor } from "../../../../src/data/supervisor/supervisor"; export interface HassioSnapshotDialogParams { slug: string; onDelete?: () => void; onboarding?: boolean; + supervisor?: Supervisor; } export const showHassioSnapshotDialog = ( diff --git a/hassio/src/snapshots/hassio-snapshots.ts b/hassio/src/snapshots/hassio-snapshots.ts index 4cce8cc0ff..1fd11ed1ff 100644 --- a/hassio/src/snapshots/hassio-snapshots.ts +++ b/hassio/src/snapshots/hassio-snapshots.ts @@ -41,6 +41,7 @@ import { reloadHassioSnapshots, } from "../../../src/data/hassio/snapshot"; import { Supervisor } from "../../../src/data/supervisor/supervisor"; +import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; import "../../../src/layouts/hass-tabs-subpage"; import { PolymerChangedEvent } from "../../../src/polymer-types"; import { haStyle } from "../../../src/resources/styles"; @@ -211,7 +212,13 @@ class HassioSnapshots extends LitElement { : undefined}
- + Create
@@ -325,6 +332,12 @@ class HassioSnapshots extends LitElement { } private async _createSnapshot(ev: CustomEvent): Promise { + if (this.supervisor.info.state !== "running") { + await showAlertDialog(this, { + title: "Could not create snapshot", + text: `Creating a snapshot is not possible right now because the system is in ${this.supervisor.info.state} state.`, + }); + } const button = ev.currentTarget as any; button.progress = true; @@ -386,6 +399,7 @@ class HassioSnapshots extends LitElement { private _snapshotClicked(ev) { showHassioSnapshotDialog(this, { slug: ev.currentTarget!.snapshot.slug, + supervisor: this.supervisor, onDelete: () => this._updateSnapshots(), }); } @@ -395,6 +409,7 @@ class HassioSnapshots extends LitElement { showSnapshot: (slug: string) => showHassioSnapshotDialog(this, { slug, + supervisor: this.supervisor, onDelete: () => this._updateSnapshots(), }), reloadSnapshot: () => this.refreshData(), diff --git a/src/data/hassio/supervisor.ts b/src/data/hassio/supervisor.ts index 0db84b3d6c..a21cfd1915 100644 --- a/src/data/hassio/supervisor.ts +++ b/src/data/hassio/supervisor.ts @@ -49,6 +49,15 @@ export type HassioInfo = { hostname: string; logging: string; machine: string; + state: + | "initialize" + | "setup" + | "startup" + | "running" + | "freeze" + | "shutdown" + | "stopping" + | "close"; operating_system: string; supervisor: string; supported: boolean; From e275f1f4b9fd16cfe98c7d16da247a7645bc2ea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Hansl=C3=ADk?= Date: Mon, 8 Feb 2021 16:28:28 +0100 Subject: [PATCH 04/44] Fixed state card of number entity (#8325) --- src/state-summary/state-card-number.js | 73 +------------------------- 1 file changed, 1 insertion(+), 72 deletions(-) diff --git a/src/state-summary/state-card-number.js b/src/state-summary/state-card-number.js index 635d76bcc0..61fa0118d9 100644 --- a/src/state-summary/state-card-number.js +++ b/src/state-summary/state-card-number.js @@ -6,7 +6,6 @@ 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-slider"; class StateCardNumber extends mixinBehaviors( [IronResizableBehavior], @@ -16,9 +15,6 @@ class StateCardNumber extends mixinBehaviors( return html` - - - Clear - Toggle - - - - `; - } - - static get properties() { - return { - allowCustomValue: Boolean, - items: { - type: Object, - observer: "_itemsChanged", - }, - _items: Object, - itemLabelPath: String, - itemValuePath: String, - autofocus: Boolean, - label: String, - opened: { - type: Boolean, - value: false, - observer: "_openedChanged", - }, - value: { - type: String, - notify: true, - }, - }; - } - - _openedChanged(newVal) { - if (!newVal) { - this._items = this.items; - } - } - - _itemsChanged(newVal) { - if (!this.opened) { - this._items = newVal; - } - } - - _computeToggleIcon(opened) { - return opened ? "hass:menu-up" : "hass:menu-down"; - } - - _computeItemLabel(item, itemLabelPath) { - return itemLabelPath ? item[itemLabelPath] : item; - } - - _fireChanged(ev) { - ev.stopPropagation(); - this.fire("change"); - } -} - -customElements.define("ha-combo-box", HaComboBox); diff --git a/src/components/ha-combo-box.ts b/src/components/ha-combo-box.ts new file mode 100644 index 0000000000..ca40f37be9 --- /dev/null +++ b/src/components/ha-combo-box.ts @@ -0,0 +1,177 @@ +import "@material/mwc-icon-button/mwc-icon-button"; +import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; +import "@polymer/paper-input/paper-input"; +import "@polymer/paper-item/paper-item"; +import "@polymer/paper-item/paper-item-body"; +import "@polymer/paper-listbox/paper-listbox"; +import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + query, + TemplateResult, +} from "lit-element"; +import { fireEvent } from "../common/dom/fire_event"; +import { PolymerChangedEvent } from "../polymer-types"; +import { HomeAssistant } from "../types"; +import "./ha-svg-icon"; + +const defaultRowRenderer = ( + root: HTMLElement, + _owner, + model: { item: any } +) => { + if (!root.firstElementChild) { + root.innerHTML = ` + + + `; + } + + root.querySelector("paper-item")!.textContent = model.item; +}; + +@customElement("ha-combo-box") +export class HaComboBox extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public label?: string; + + @property() public value?: string; + + @property() public items?: []; + + @property() public filteredItems?: []; + + @property({ attribute: "allow-custom-value", type: Boolean }) + public allowCustomValue?: boolean; + + @property({ attribute: "item-value-path" }) public itemValuePath?: string; + + @property({ attribute: "item-label-path" }) public itemLabelPath?: string; + + @property({ attribute: "item-id-path" }) public itemIdPath?: string; + + @property() public renderer?: ( + root: HTMLElement, + owner: HTMLElement, + model: { item: any } + ) => void; + + @property({ type: Boolean }) + private _opened?: boolean; + + @query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement; + + public open() { + this.updateComplete.then(() => { + (this._comboBox as any)?.open(); + }); + } + + public focus() { + this.updateComplete.then(() => { + this.shadowRoot?.querySelector("paper-input")?.focus(); + }); + } + + protected render(): TemplateResult { + return html` + + + ${this.value + ? html` + + + + ` + : ""} + + + + + + + `; + } + + private _clearValue(ev: Event) { + ev.stopPropagation(); + fireEvent(this, "value-changed", { value: undefined }); + } + + private _openedChanged(ev: PolymerChangedEvent) { + this._opened = ev.detail.value; + // @ts-ignore + fireEvent(this, ev.type, ev.detail); + } + + private _filterChanged(ev: PolymerChangedEvent) { + // @ts-ignore + fireEvent(this, ev.type, ev.detail); + } + + private _valueChanged(ev: PolymerChangedEvent) { + ev.stopPropagation(); + const newValue = ev.detail.value; + + if (newValue !== this.value) { + fireEvent(this, "value-changed", { value: newValue }); + } + } + + static get styles(): CSSResult { + return css` + paper-input > mwc-icon-button { + --mdc-icon-button-size: 24px; + padding: 2px; + color: var(--secondary-text-color); + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-combo-box": HaComboBox; + } +} diff --git a/src/components/ha-selector/ha-selector-number.ts b/src/components/ha-selector/ha-selector-number.ts index 3a819cf9c5..5daacda2d9 100644 --- a/src/components/ha-selector/ha-selector-number.ts +++ b/src/components/ha-selector/ha-selector-number.ts @@ -46,7 +46,7 @@ export class HaNumberSelector extends LitElement { class=${classMap({ single: this.selector.number.mode === "box" })} .min=${this.selector.number.min} .max=${this.selector.number.max} - .value=${this._value} + .value=${this.value} .step=${this.selector.number.step} type="number" auto-validate @@ -65,16 +65,21 @@ export class HaNumberSelector extends LitElement { } private _handleInputChange(ev) { - const value = ev.detail.value; - if (this._value === value) { + ev.stopPropagation(); + const value = + ev.detail.value === "" || isNaN(ev.detail.value) + ? undefined + : Number(ev.detail.value); + if (this.value === value) { return; } fireEvent(this, "value-changed", { value }); } private _handleSliderChange(ev) { - const value = ev.target.value; - if (this._value === value) { + ev.stopPropagation(); + const value = Number(ev.target.value); + if (this.value === value) { return; } fireEvent(this, "value-changed", { value }); diff --git a/src/components/ha-selector/ha-selector-target.ts b/src/components/ha-selector/ha-selector-target.ts index 23c383e647..f86cc415a1 100644 --- a/src/components/ha-selector/ha-selector-target.ts +++ b/src/components/ha-selector/ha-selector-target.ts @@ -3,7 +3,11 @@ import "@material/mwc-list/mwc-list-item"; import "@material/mwc-tab-bar/mwc-tab-bar"; import "@material/mwc-tab/mwc-tab"; import "@polymer/paper-input/paper-input"; -import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; +import { + HassEntity, + HassServiceTarget, + UnsubscribeFunc, +} from "home-assistant-js-websocket"; import { css, CSSResult, @@ -20,7 +24,6 @@ import { subscribeEntityRegistry, } from "../../data/entity_registry"; import { TargetSelector } from "../../data/selector"; -import { Target } from "../../data/target"; import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../types"; import "../ha-target-picker"; @@ -31,7 +34,7 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) { @property() public selector!: TargetSelector; - @property() public value?: Target; + @property() public value?: HassServiceTarget; @property() public label?: string; @@ -59,7 +62,8 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) { const oldSelector = changedProperties.get("selector"); if ( oldSelector !== this.selector && - this.selector.target.device?.integration + (this.selector.target.device?.integration || + this.selector.target.entity?.integration) ) { this._loadConfigEntries(); } @@ -84,11 +88,15 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) { } private _filterEntities(entity: HassEntity): boolean { - if (this.selector.target.entity?.integration) { + if ( + this.selector.target.entity?.integration || + this.selector.target.device?.integration + ) { if ( !this._entityPlaformLookup || this._entityPlaformLookup[entity.entity_id] !== - this.selector.target.entity.integration + (this.selector.target.entity?.integration || + this.selector.target.device?.integration) ) { return false; } @@ -118,7 +126,10 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) { ) { return false; } - if (this.selector.target.device?.integration) { + if ( + this.selector.target.device?.integration || + this.selector.target.entity?.integration + ) { if ( !this._configEntries?.some((entry) => device.config_entries.includes(entry.entry_id) @@ -132,14 +143,16 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) { private async _loadConfigEntries() { this._configEntries = (await getConfigEntries(this.hass)).filter( - (entry) => entry.domain === this.selector.target.device?.integration + (entry) => + entry.domain === + (this.selector.target.device?.integration || + this.selector.target.entity?.integration) ); } static get styles(): CSSResult { return css` ha-target-picker { - margin: 0 -8px; display: block; } `; diff --git a/src/components/ha-service-control.ts b/src/components/ha-service-control.ts new file mode 100644 index 0000000000..7a874164b8 --- /dev/null +++ b/src/components/ha-service-control.ts @@ -0,0 +1,290 @@ +import { HassService, HassServiceTarget } from "home-assistant-js-websocket"; +import { + css, + CSSResult, + customElement, + html, + internalProperty, + LitElement, + property, + PropertyValues, + query, +} from "lit-element"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../common/dom/fire_event"; +import { computeDomain } from "../common/entity/compute_domain"; +import { computeObjectId } from "../common/entity/compute_object_id"; +import { ENTITY_COMPONENT_DOMAINS } from "../data/entity"; +import { Selector } from "../data/selector"; +import { PolymerChangedEvent } from "../polymer-types"; +import { HomeAssistant } from "../types"; +import "./ha-selector/ha-selector"; +import "./ha-service-picker"; +import "./ha-settings-row"; +import "./ha-yaml-editor"; +import type { HaYamlEditor } from "./ha-yaml-editor"; + +interface ExtHassService extends Omit { + fields: { + key: string; + name?: string; + description: string; + required?: boolean; + default?: any; + example?: any; + selector?: Selector; + }[]; +} + +@customElement("ha-service-control") +export class HaServiceControl extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public value?: { + service: string; + target?: HassServiceTarget; + data?: Record; + }; + + @property({ reflect: true, type: Boolean }) public narrow!: boolean; + + @internalProperty() private _serviceData?: ExtHassService; + + @query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor; + + protected updated(changedProperties: PropertyValues) { + if (!changedProperties.has("value")) { + return; + } + this._serviceData = this.value?.service + ? this._getServiceInfo(this.value.service) + : undefined; + + if ( + this._serviceData && + "target" in this._serviceData && + this.value?.data?.entity_id + ) { + this.value = { + ...this.value, + target: { ...this.value.target, entity_id: this.value.data.entity_id }, + }; + delete this.value.data!.entity_id; + } + + if (this.value?.data) { + const yamlEditor = this._yamlEditor; + if (yamlEditor && yamlEditor.value !== this.value.data) { + yamlEditor.setValue(this.value.data); + } + } + } + + private _domainFilter = memoizeOne((service: string) => { + const domain = computeDomain(service); + return ENTITY_COMPONENT_DOMAINS.includes(domain) ? [domain] : null; + }); + + private _getServiceInfo = memoizeOne((service: string): + | ExtHassService + | undefined => { + if (!service) { + return undefined; + } + const domain = computeDomain(service); + const serviceName = computeObjectId(service); + const serviceDomains = this.hass.services; + if (!(domain in serviceDomains)) { + return undefined; + } + if (!(serviceName in serviceDomains[domain])) { + return undefined; + } + + const fields = Object.entries( + serviceDomains[domain][serviceName].fields + ).map(([key, value]) => { + return { + key, + ...value, + selector: value.selector as Selector | undefined, + }; + }); + return { + ...serviceDomains[domain][serviceName], + fields, + }; + }); + + protected render() { + const legacy = + this._serviceData?.fields.length && + !this._serviceData.fields.some((field) => field.selector); + + const entityId = + legacy && + this._serviceData?.fields.find((field) => field.key === "entity_id"); + + return html` + ${this._serviceData && "target" in this._serviceData + ? html`` + : entityId + ? html`` + : ""} + ${legacy + ? html`` + : this._serviceData?.fields.map((dataField) => + dataField.selector + ? html` + ${dataField.name || dataField.key} + ${dataField?.description}` + : "" + )} `; + } + + private _serviceChanged(ev: PolymerChangedEvent) { + ev.stopPropagation(); + if (ev.detail.value === this.value?.service) { + return; + } + fireEvent(this, "value-changed", { + value: { service: ev.detail.value || "", data: {} }, + }); + } + + private _entityPicked(ev: CustomEvent) { + ev.stopPropagation(); + const newValue = ev.detail.value; + if (this.value?.data?.entity_id === newValue) { + return; + } + let value; + if (!newValue && this.value?.data) { + value = { ...this.value }; + delete value.data.entity_id; + } else { + value = { + ...this.value, + data: { ...this.value?.data, entity_id: ev.detail.value }, + }; + } + fireEvent(this, "value-changed", { + value, + }); + } + + private _targetChanged(ev: CustomEvent) { + ev.stopPropagation(); + const newValue = ev.detail.value; + if (this.value?.target === newValue) { + return; + } + let value; + if (!newValue) { + value = { ...this.value }; + delete value.target; + } else { + value = { ...this.value, target: ev.detail.value }; + } + fireEvent(this, "value-changed", { + value, + }); + } + + private _serviceDataChanged(ev: CustomEvent) { + ev.stopPropagation(); + const key = (ev.currentTarget as any).key; + const value = ev.detail.value; + if (this.value?.data && this.value.data[key] === value) { + return; + } + + const data = { ...this.value?.data, [key]: value }; + + if (value === "" || value === undefined) { + delete data[key]; + } + + fireEvent(this, "value-changed", { + value: { + ...this.value, + data, + }, + }); + } + + private _dataChanged(ev: CustomEvent) { + ev.stopPropagation(); + if (!ev.detail.isValid) { + return; + } + fireEvent(this, "value-changed", { + value: { + ...this.value, + data: ev.detail.value, + }, + }); + } + + static get styles(): CSSResult { + return css` + ha-settings-row { + padding: 0; + } + ha-settings-row { + --paper-time-input-justify-content: flex-end; + } + :host(:not([narrow])) ha-settings-row paper-input { + width: 60%; + } + :host(:not([narrow])) ha-settings-row ha-selector { + width: 60%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-service-control": HaServiceControl; + } +} diff --git a/src/components/ha-service-picker.js b/src/components/ha-service-picker.js deleted file mode 100644 index 32aee922c9..0000000000 --- a/src/components/ha-service-picker.js +++ /dev/null @@ -1,60 +0,0 @@ -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import LocalizeMixin from "../mixins/localize-mixin"; -import "./ha-combo-box"; - -/* - * @appliesMixin LocalizeMixin - */ -class HaServicePicker extends LocalizeMixin(PolymerElement) { - static get template() { - return html` - - `; - } - - static get properties() { - return { - hass: { - type: Object, - observer: "_hassChanged", - }, - _services: Array, - value: { - type: String, - notify: true, - }, - }; - } - - _hassChanged(hass, oldHass) { - if (!hass) { - this._services = []; - return; - } - if (oldHass && hass.services === oldHass.services) { - return; - } - const result = []; - - Object.keys(hass.services) - .sort() - .forEach((domain) => { - const services = Object.keys(hass.services[domain]).sort(); - - for (let i = 0; i < services.length; i++) { - result.push(`${domain}.${services[i]}`); - } - }); - - this._services = result; - } -} - -customElements.define("ha-service-picker", HaServicePicker); diff --git a/src/components/ha-service-picker.ts b/src/components/ha-service-picker.ts new file mode 100644 index 0000000000..03379bb218 --- /dev/null +++ b/src/components/ha-service-picker.ts @@ -0,0 +1,121 @@ +import { html, internalProperty, LitElement, property } from "lit-element"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../common/dom/fire_event"; +import { HomeAssistant } from "../types"; +import "./ha-combo-box"; + +const rowRenderer = ( + root: HTMLElement, + _owner, + model: { item: { service: string; description: string } } +) => { + if (!root.firstElementChild) { + root.innerHTML = ` + + + +
[[item.description]]
+
[[item.service]]
+
+
+ `; + } + + root.querySelector(".name")!.textContent = model.item.description; + root.querySelector("[secondary]")!.textContent = model.item.service; +}; + +class HaServicePicker extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public value?: string; + + @internalProperty() private _filter?: string; + + protected render() { + return html` + + `; + } + + private _services = memoizeOne((services: HomeAssistant["services"]): { + service: string; + description: string; + }[] => { + if (!services) { + return []; + } + const result: { service: string; description: string }[] = []; + + Object.keys(services) + .sort() + .forEach((domain) => { + const services_keys = Object.keys(services[domain]).sort(); + + for (const service of services_keys) { + result.push({ + service: `${domain}.${service}`, + description: + services[domain][service].description || `${domain}.${service}`, + }); + } + }); + + return result; + }); + + private _filteredServices = memoizeOne( + (services: HomeAssistant["services"], filter?: string) => { + if (!services) { + return []; + } + const processedServices = this._services(services); + + if (!filter) { + return processedServices; + } + return processedServices.filter( + (service) => + service.service.toLowerCase().includes(filter) || + service.description.toLowerCase().includes(filter) + ); + } + ); + + private _filterChanged(ev: CustomEvent): void { + this._filter = ev.detail.value.toLowerCase(); + } + + private _valueChanged(ev) { + this.value = ev.detail.value; + fireEvent(this, "change"); + fireEvent(this, "value-changed", { value: this.value }); + } +} + +customElements.define("ha-service-picker", HaServicePicker); + +declare global { + interface HTMLElementTagNameMap { + "ha-service-picker": HaServicePicker; + } +} diff --git a/src/components/ha-settings-row.ts b/src/components/ha-settings-row.ts index efa3b95cf4..8500813910 100644 --- a/src/components/ha-settings-row.ts +++ b/src/components/ha-settings-row.ts @@ -45,6 +45,7 @@ export class HaSettingsRow extends LitElement { min-height: calc( var(--paper-item-body-two-line-min-height, 72px) - 16px ); + flex: 1; } :host([narrow]) { align-items: normal; diff --git a/src/components/ha-target-picker.ts b/src/components/ha-target-picker.ts index c324b6b9eb..7c3aec553d 100644 --- a/src/components/ha-target-picker.ts +++ b/src/components/ha-target-picker.ts @@ -10,7 +10,10 @@ import { mdiUnfoldMoreVertical, } from "@mdi/js"; import "@polymer/paper-tooltip/paper-tooltip"; -import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { + HassServiceTarget, + UnsubscribeFunc, +} from "home-assistant-js-websocket"; import { css, CSSResult, @@ -41,7 +44,6 @@ import { EntityRegistryEntry, subscribeEntityRegistry, } from "../data/entity_registry"; -import { Target } from "../data/target"; import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { HomeAssistant } from "../types"; import "./device/ha-device-picker"; @@ -56,7 +58,7 @@ import "./ha-svg-icon"; export class HaTargetPicker extends SubscribeMixin(LitElement) { @property() public hass!: HomeAssistant; - @property() public value?: Target; + @property() public value?: HassServiceTarget; @property() public label?: string; @@ -530,6 +532,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { .items { z-index: 2; } + .mdc-chip-set { + padding: 4px 0; + } .mdc-chip.add { color: rgba(0, 0, 0, 0.87); } diff --git a/src/data/script.ts b/src/data/script.ts index e528754f5e..c4b518e820 100644 --- a/src/data/script.ts +++ b/src/data/script.ts @@ -1,6 +1,7 @@ import { HassEntityAttributeBase, HassEntityBase, + HassServiceTarget, } from "home-assistant-js-websocket"; import { computeObjectId } from "../common/entity/compute_object_id"; import { navigate } from "../common/navigate"; @@ -36,6 +37,7 @@ export interface EventAction { export interface ServiceAction { service: string; entity_id?: string; + target?: HassServiceTarget; data?: Record; } diff --git a/src/data/target.ts b/src/data/target.ts deleted file mode 100644 index afddff0688..0000000000 --- a/src/data/target.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface Target { - entity_id?: string[]; - device_id?: string[]; - area_id?: string[]; -} diff --git a/src/fake_data/demo_config.ts b/src/fake_data/demo_config.ts index 86b5484b33..68a99ee8e2 100644 --- a/src/fake_data/demo_config.ts +++ b/src/fake_data/demo_config.ts @@ -15,7 +15,8 @@ export const demoConfig: HassConfig = { time_zone: "America/Los_Angeles", config_dir: "/config", version: "DEMO", - whitelist_external_dirs: [], + allowlist_external_dirs: [], + allowlist_external_urls: [], config_source: "storage", safe_mode: false, state: STATE_RUNNING, diff --git a/src/panels/config/automation/action/ha-automation-action-row.ts b/src/panels/config/automation/action/ha-automation-action-row.ts index c13b6d4905..ed4f1be90c 100644 --- a/src/panels/config/automation/action/ha-automation-action-row.ts +++ b/src/panels/config/automation/action/ha-automation-action-row.ts @@ -42,7 +42,6 @@ import "./types/ha-automation-action-wait_template"; const OPTIONS = [ "condition", "delay", - "device_id", "event", "scene", "service", @@ -50,6 +49,7 @@ const OPTIONS = [ "wait_for_trigger", "repeat", "choose", + "device_id", ]; const getType = (action: Action) => { @@ -99,6 +99,8 @@ export default class HaAutomationActionRow extends LitElement { @property() public totalActions!: number; + @property({ type: Boolean }) public narrow = false; + @internalProperty() private _warnings?: string[]; @internalProperty() private _uiModeAvailable = true; @@ -116,8 +118,9 @@ export default class HaAutomationActionRow extends LitElement { this._yamlMode = true; } - if (this._yamlMode && this._yamlEditor) { - this._yamlEditor.setValue(this.action); + const yamlEditor = this._yamlEditor; + if (this._yamlMode && yamlEditor && yamlEditor.value !== this.action) { + yamlEditor.setValue(this.action); } } @@ -242,6 +245,7 @@ export default class HaAutomationActionRow extends LitElement { ${dynamicElement(`ha-automation-action-${type}`, { hass: this.hass, action: this.action, + narrow: this.narrow, })} `} diff --git a/src/panels/config/automation/action/ha-automation-action.ts b/src/panels/config/automation/action/ha-automation-action.ts index 77db982c3b..568dc9676b 100644 --- a/src/panels/config/automation/action/ha-automation-action.ts +++ b/src/panels/config/automation/action/ha-automation-action.ts @@ -18,6 +18,8 @@ import { HaDeviceAction } from "./types/ha-automation-action-device_id"; export default class HaAutomationAction extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; + @property({ type: Boolean }) public narrow = false; + @property() public actions!: Action[]; protected render() { @@ -28,6 +30,7 @@ export default class HaAutomationAction extends LitElement { .index=${idx} .totalActions=${this.actions.length} .action=${action} + .narrow=${this.narrow} @duplicate=${this._duplicateAction} @move-action=${this._move} @value-changed=${this._actionChanged} diff --git a/src/panels/config/automation/action/types/ha-automation-action-service.ts b/src/panels/config/automation/action/types/ha-automation-action-service.ts index f9351c6e13..2584d3c7c5 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-service.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-service.ts @@ -1,30 +1,24 @@ import "@polymer/paper-input/paper-input"; import { customElement, + internalProperty, LitElement, property, PropertyValues, - query, } from "lit-element"; import { html } from "lit-html"; -import memoizeOne from "memoize-one"; import { any, assert, object, optional, string } from "superstruct"; import { fireEvent } from "../../../../../common/dom/fire_event"; -import { computeDomain } from "../../../../../common/entity/compute_domain"; -import { computeObjectId } from "../../../../../common/entity/compute_object_id"; -import "../../../../../components/entity/ha-entity-picker"; -import "../../../../../components/ha-service-picker"; -import "../../../../../components/ha-yaml-editor"; -import type { HaYamlEditor } from "../../../../../components/ha-yaml-editor"; import { ServiceAction } from "../../../../../data/script"; -import type { PolymerChangedEvent } from "../../../../../polymer-types"; import type { HomeAssistant } from "../../../../../types"; import { EntityIdOrAll } from "../../../../../common/structs/is-entity-id"; -import { ActionElement, handleChangeEvent } from "../ha-automation-action-row"; +import { ActionElement } from "../ha-automation-action-row"; +import "../../../../../components/ha-service-control"; const actionStruct = object({ service: optional(string()), entity_id: optional(EntityIdOrAll), + target: optional(any()), data: optional(any()), }); @@ -34,36 +28,14 @@ export class HaServiceAction extends LitElement implements ActionElement { @property({ attribute: false }) public action!: ServiceAction; - @query("ha-yaml-editor", true) private _yamlEditor?: HaYamlEditor; + @property({ type: Boolean }) public narrow = false; - private _actionData?: ServiceAction["data"]; + @internalProperty() private _action!: ServiceAction; public static get defaultConfig() { return { service: "", data: {} }; } - private _domain = memoizeOne((service: string) => [computeDomain(service)]); - - private _getServiceData = memoizeOne((service: string) => { - if (!service) { - return []; - } - const domain = computeDomain(service); - const serviceName = computeObjectId(service); - const serviceDomains = this.hass.services; - if (!(domain in serviceDomains)) { - return []; - } - if (!(serviceName in serviceDomains[domain])) { - return []; - } - - const fields = serviceDomains[domain][serviceName].fields; - return Object.keys(fields).map((field) => { - return { key: field, ...fields[field] }; - }); - }); - protected updated(changedProperties: PropertyValues) { if (!changedProperties.has("action")) { return; @@ -73,73 +45,32 @@ export class HaServiceAction extends LitElement implements ActionElement { } catch (error) { fireEvent(this, "ui-mode-not-available", error); } - if (this._actionData && this._actionData !== this.action.data) { - if (this._yamlEditor) { - this._yamlEditor.setValue(this.action.data); - } + if (this.action.entity_id) { + this._action = { + ...this.action, + data: { ...this.action.data, entity_id: this.action.entity_id }, + }; + delete this._action.entity_id; + } else { + this._action = this.action; } - this._actionData = this.action.data; } protected render() { - const { service, data, entity_id } = this.action; - - const serviceData = this._getServiceData(service); - const entity = serviceData.find((attr) => attr.key === "entity_id"); - return html` - - ${entity - ? html` - - ` - : ""} - + .value=${this._action} + @value-changed=${this._actionChanged} + > `; } - private _dataChanged(ev: CustomEvent): void { - ev.stopPropagation(); - if (!ev.detail.isValid) { - return; + private _actionChanged(ev) { + if (ev.detail.value === this._action) { + ev.stopPropagation(); } - this._actionData = ev.detail.value; - handleChangeEvent(this, ev); - } - - private _serviceChanged(ev: PolymerChangedEvent) { - ev.stopPropagation(); - if (ev.detail.value === this.action.service) { - return; - } - fireEvent(this, "value-changed", { - value: { ...this.action, service: ev.detail.value }, - }); - } - - private _entityPicked(ev: PolymerChangedEvent) { - ev.stopPropagation(); - fireEvent(this, "value-changed", { - value: { ...this.action, entity_id: ev.detail.value }, - }); } } diff --git a/src/panels/config/automation/blueprint-automation-editor.ts b/src/panels/config/automation/blueprint-automation-editor.ts index 3b13cc8d4b..37221fe56a 100644 --- a/src/panels/config/automation/blueprint-automation-editor.ts +++ b/src/panels/config/automation/blueprint-automation-editor.ts @@ -252,10 +252,7 @@ export class HaBlueprintAutomationEditor extends LitElement { if (!name) { return; } - let newVal = ev.detail.value; - if (target.type === "number") { - newVal = Number(newVal); - } + const newVal = ev.detail.value; if ((this.config![name] || "") === newVal) { return; } diff --git a/src/panels/config/automation/manual-automation-editor.ts b/src/panels/config/automation/manual-automation-editor.ts index 21a9e1514e..19071ae06c 100644 --- a/src/panels/config/automation/manual-automation-editor.ts +++ b/src/panels/config/automation/manual-automation-editor.ts @@ -42,7 +42,7 @@ export class HaManualAutomationEditor extends LitElement { @property() public stateObj?: HassEntity; protected render() { - return html` + return html` ${!this.narrow ? html` ${this.config.alias} ` : ""} @@ -151,7 +151,7 @@ export class HaManualAutomationEditor extends LitElement { - + ${this.hass.localize( "ui.panel.config.automation.editor.triggers.header" @@ -180,7 +180,7 @@ export class HaManualAutomationEditor extends LitElement { > - + ${this.hass.localize( "ui.panel.config.automation.editor.conditions.header" @@ -209,7 +209,7 @@ export class HaManualAutomationEditor extends LitElement { > - + ${this.hass.localize( "ui.panel.config.automation.editor.actions.header" @@ -235,6 +235,7 @@ export class HaManualAutomationEditor extends LitElement { .actions=${this.config.action} @value-changed=${this._actionChanged} .hass=${this.hass} + .narrow=${this.narrow} > `; } diff --git a/src/panels/config/ha-config-section.ts b/src/panels/config/ha-config-section.ts index d9e89e6e6a..f98ef27012 100644 --- a/src/panels/config/ha-config-section.ts +++ b/src/panels/config/ha-config-section.ts @@ -80,13 +80,16 @@ export class HaConfigSection extends LitElement { font-weight: var(--paper-font-subhead_-_font-weight); line-height: var(--paper-font-subhead_-_line-height); width: 100%; - max-width: 400px; - margin-right: 40px; opacity: var(--dark-primary-opacity); font-size: 14px; padding-bottom: 20px; } + .horizontal .intro { + max-width: 400px; + margin-right: 40px; + } + .panel { margin-top: -24px; } diff --git a/src/panels/config/scene/ha-scene-editor.ts b/src/panels/config/scene/ha-scene-editor.ts index 5a3e8c965a..1f02dec32c 100644 --- a/src/panels/config/scene/ha-scene-editor.ts +++ b/src/panels/config/scene/ha-scene-editor.ts @@ -221,7 +221,7 @@ export class HaSceneEditor extends SubscribeMixin( > ${this._config ? html` - + ${!this.narrow ? html` ${name} ` : ""} @@ -253,7 +253,7 @@ export class HaSceneEditor extends SubscribeMixin( - +
${this.hass.localize( "ui.panel.config.scene.editor.devices.header" @@ -324,7 +324,7 @@ export class HaSceneEditor extends SubscribeMixin( ${this.showAdvanced ? html` - +
${this.hass.localize( "ui.panel.config.scene.editor.entities.header" diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index d658ea7fe4..167e96d57e 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -189,7 +189,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { > ${this._config ? html` - + ${!this.narrow ? html` ${this._config.alias} @@ -313,7 +313,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { - + ${this.hass.localize( "ui.panel.config.script.editor.sequence" @@ -350,7 +350,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { ` : this._mode === "yaml" ? html` - + ${!this.narrow ? html`${this._config?.alias}` : ``} diff --git a/src/state/connection-mixin.ts b/src/state/connection-mixin.ts index c4414f0a7b..b8a03f756d 100644 --- a/src/state/connection-mixin.ts +++ b/src/state/connection-mixin.ts @@ -51,17 +51,24 @@ export const connectionMixin = >( enableShortcuts: true, moreInfoEntityId: null, hassUrl: (path = "") => new URL(path, auth.data.hassUrl).toString(), - callService: async (domain, service, serviceData = {}) => { + callService: async (domain, service, serviceData = {}, target) => { if (__DEV__) { // eslint-disable-next-line no-console - console.log("Calling service", domain, service, serviceData); + console.log( + "Calling service", + domain, + service, + serviceData, + target + ); } try { return (await callService( conn, domain, service, - serviceData + serviceData, + target )) as Promise; } catch (err) { if (__DEV__) { @@ -71,6 +78,7 @@ export const connectionMixin = >( domain, service, serviceData, + target, err ); } diff --git a/src/types.ts b/src/types.ts index d973c441b7..91b06b564b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,6 +3,7 @@ import { Connection, HassConfig, HassEntities, + HassServiceTarget, HassServices, MessageBase, } from "home-assistant-js-websocket"; @@ -178,6 +179,7 @@ export interface ServiceCallRequest { domain: string; service: string; serviceData?: Record; + target?: HassServiceTarget; } export interface HomeAssistant { @@ -216,7 +218,8 @@ export interface HomeAssistant { callService( domain: ServiceCallRequest["domain"], service: ServiceCallRequest["service"], - serviceData?: ServiceCallRequest["serviceData"] + serviceData?: ServiceCallRequest["serviceData"], + target?: ServiceCallRequest["target"] ): Promise; callApi( method: "GET" | "POST" | "PUT" | "DELETE", diff --git a/yarn.lock b/yarn.lock index e29445f16b..271bc0443f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8174,10 +8174,10 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -home-assistant-js-websocket@^5.4.1: - version "5.4.1" - resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-5.4.1.tgz#3f677391b38e4feb24f1670e3a9b695767332a51" - integrity sha512-FTVoO5yMSa2dy1ffZDvJy/r79VTjwFOzyP/bPld5lDHKbNyXC8wgqpn8Kdf5ZQISYJf1T1dfH+v2NYEngn5NgQ== +home-assistant-js-websocket@^5.8.1: + version "5.8.1" + resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-5.8.1.tgz#4c5930aa47e7089f5806bb3d190ebe53697d2edc" + integrity sha512-2H3q8NK3WrT50iYODv95iz0E2E+nAUOD452V6lhBxhUTQlVFBsuxNMRTTbIZp+6Xab7ad84uF0z+hHFmBMq/Sw== homedir-polyfill@^1.0.1: version "1.0.3" From 03d4174163166fa420b0cc7077ef2fa469f1164d Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 17 Feb 2021 01:18:01 +0000 Subject: [PATCH 21/44] Translation update --- translations/frontend/el.json | 6 +++ translations/frontend/en.json | 1 + translations/frontend/fa.json | 19 +++++--- translations/frontend/sk.json | 72 +++++++++++++++++------------- translations/frontend/zh-Hans.json | 4 +- 5 files changed, 61 insertions(+), 41 deletions(-) diff --git a/translations/frontend/el.json b/translations/frontend/el.json index 962fd6f37e..92b3515e6b 100644 --- a/translations/frontend/el.json +++ b/translations/frontend/el.json @@ -2002,6 +2002,7 @@ }, "configure": "Διαμόρφωση", "configured": "Διαμορφώθηκε", + "confirm_new": "Θέλετε να ρυθμίσετε το {integration};", "description": "Διαχείριση ενοποιήσεων με υπηρεσίες, συσκευές, ...", "details": "Λεπτομέρειες ενσωμάτωσης", "discovered": "Ανακαλύφθηκε", @@ -3407,6 +3408,11 @@ "empty": "Δεν έχετε μηνύματα", "playback_title": "Αναπαραγωγή μηνύματος" }, + "my": { + "error": "Παρουσιάστηκε άγνωστο σφάλμα", + "faq_link": "Συνήθεις ερωτήσεις για το Home Assistant", + "not_supported": "Αυτή η ανακατεύθυνση δεν υποστηρίζεται από τo Home Assistant σας. Ελέγξτε το {link} για τις υποστηριζόμενες ανακατευθύνσεις και την έκδοση που εισήγαγαν." + }, "page-authorize": { "abort_intro": "Σύνδεση ματαιώθηκε", "authorizing_client": "Πρόκειται να δώσετε στο {clientId} πρόσβαση στο Home Assistant.", diff --git a/translations/frontend/en.json b/translations/frontend/en.json index 140db7a34a..032cfdaade 100644 --- a/translations/frontend/en.json +++ b/translations/frontend/en.json @@ -3409,6 +3409,7 @@ "playback_title": "Message playback" }, "my": { + "component_not_loaded": "This redirect is not supported by your Home Assistant instance. You need the integration {integration} to use this redirect.", "error": "An unknown error occured", "faq_link": "My Home Assistant FAQ", "not_supported": "This redirect is not supported by your Home Assistant instance. Check the {link} for the supported redirects and the version they where introduced." diff --git a/translations/frontend/fa.json b/translations/frontend/fa.json index 12622b4e2b..e8548806e4 100644 --- a/translations/frontend/fa.json +++ b/translations/frontend/fa.json @@ -1055,18 +1055,23 @@ "title": "الکسا" }, "connected": "متصل", - "integrations": "Integrari", - "integrations_introduction2": "Verifica website-ul pentru", - "integrations_link_all_features": "toate caracteristicile disponibile", - "manage_account": "Administrare Cont", - "nabu_casa_account": "Cont Nabu Casa", + "integrations": "ادغام ها", + "integrations_introduction2": "بررسی وب سایت برای ", + "integrations_link_all_features": "تمام ویژگی های موجود", + "manage_account": "مدیریت حساب", + "nabu_casa_account": "حساب Nabu Casa", "not_connected": "متصل نیست", "remote": { "title": "کنترل از راه دور" }, "sign_out": "خروج از سیستم", + "tts": { + "male": "مرد", + "title": "متن به گفتار", + "try": "تلاش كردن" + }, "webhooks": { - "loading": "Se incarca...", + "loading": "بارگذاری ...", "manage": "مدیریت" } }, @@ -2157,7 +2162,7 @@ "cards": { "demo": { "demo_by": "توسط {name}", - "learn_more": "Afla mai multe despre Home Assistant ", + "learn_more": "درباره Home Assistant بیشتر بدانید", "next_demo": "نسخه ی نمایشی بعدی" } }, diff --git a/translations/frontend/sk.json b/translations/frontend/sk.json index 971e979879..4232820522 100644 --- a/translations/frontend/sk.json +++ b/translations/frontend/sk.json @@ -340,8 +340,8 @@ "ui": { "auth_store": { "ask": "Chcete tieto prihlasovacie údaje uložiť?", - "confirm": "Uložiť prihlasovacie údaje", - "decline": "Nie ďakujem" + "confirm": "Áno", + "decline": "Nie" }, "card": { "alarm_control_panel": { @@ -694,10 +694,10 @@ "enabled_description": "Zakázané entity nebudú pridané do Home Assistanta", "enabled_label": "Povoliť entitu", "entity_id": "Entity ID", - "icon": "Zmeniť ikonu", + "icon": "Ikona", "icon_error": "Ikony by mali byť vo formáte 'prefix:iconname', napr 'mdi:home'", - "name": "Prepísať názov", - "note": "Poznámka: toto nemusí zatiaľ fungovať so všetkými integráciami.", + "name": "Názov", + "note": "Poznámka: Toto nemusí zatiaľ fungovať so všetkými integráciami.", "unavailable": "Táto entita nie je momentálne k dispozícii.", "update": "AKTUALIZOVAŤ" }, @@ -883,6 +883,7 @@ "editor_not_supported": "Vizuálny editor nie je pre túto konfiguráciu podporovaný", "error_detected": "Zistili sa chyby v konfigurácii", "key_missing": "Chýba požadovaný kľúč \"{key}\".", + "key_wrong_type": "Vizuálny editor nepodporuje zadanú hodnotu pre \"{key}\". Podporujeme ({type_correct}), ale dostali sme ({type_wrong}).", "no_type_provided": "Nie je uvedený žiadny typ." } }, @@ -944,7 +945,7 @@ }, "automation": { "caption": "Automatizácie", - "description": "Vytvárajte a upravujte automatizácie", + "description": "Vytvorte si vlastné pravidlá správania pre svoj domov", "editor": { "actions": { "add": "Pridať akciu", @@ -1004,7 +1005,7 @@ "wait_template": "Šablóna čakania" } }, - "unsupported_action": "Nepodporovaná akcia: {action}" + "unsupported_action": "Žiadna podpora používateľského rozhrania pre akciu: {action}" }, "alias": "Názov", "conditions": { @@ -1076,7 +1077,7 @@ "zone": "Zóna" } }, - "unsupported_condition": "Nepodporovaná podmienka: {condition}" + "unsupported_condition": "Žiadna podpora používateľského rozhrania pre podmienku: {condition}" }, "default_name": "Nová automatizácia", "description": { @@ -1105,7 +1106,7 @@ "triggers": { "add": "Pridať spúšťač", "delete": "Odstrániť", - "delete_confirm": "Ste si istý odstránením ?", + "delete_confirm": "Naozaj to chcete odstrániť?", "duplicate": "Duplikovať", "header": "Spúšťače", "introduction": "Spúšťače spúšťajú spracovanie pravidla automatizácie. Pre rovnaké pravidlo je možné určiť viac spúšťačov. Po spustení spúšťača aplikácia Home Assistant overí prípadné podmienky a zavolá akciu. \n\n [Viac informácií o spúšťačov.](https://home-assistant.io/docs/automation/trigger/)", @@ -1178,7 +1179,7 @@ "seconds": "Sekúnd" }, "time": { - "at": "Čas o", + "at": "V čase", "label": "Čas", "type_value": "Pevný čas" }, @@ -1195,7 +1196,7 @@ "zone": "Zóna" } }, - "unsupported_platform": "Nepodporovaná platforma: {platform}" + "unsupported_platform": "Žiadna podpora používateľského rozhrania pre platformu: {platform}" }, "unsaved_confirm": "Máte neuložené zmeny. Naozaj chcete odísť?" }, @@ -1238,6 +1239,7 @@ "confirm_delete_header": "Odstrániť tento plán?", "confirm_delete_text": "Naozaj chcete odstrániť tento plán?", "discover_more": "Objavte viac plánov", + "introduction": "Konfigurácia plánu vám umožňuje importovať a spravovať svoje plány.", "learn_more": "Získajte viac informácií o používaní plánov" } }, @@ -1314,7 +1316,7 @@ "title": "Alexa" }, "caption": "Home Assistant Cloud", - "description_features": "Ovládanie mimo domova, integrácia s Alexa a Google Assistant.", + "description_features": "Ovládajte svoj domov, keď ste preč, a integrujte ho s Alexa a Google Assistant", "description_login": "Prihlásený ako {email}", "description_not_login": "Neprihlásený", "dialog_certificate": { @@ -1467,6 +1469,8 @@ "cant_edit": "Môžete upravovať iba položky, ktoré sú vytvorené v používateľskom rozhraní.", "caption": "Zariadenia", "confirm_delete": "Naozaj chcete odstrániť toto zariadenie?", + "confirm_rename_entity_ids": "Chcete tiež premenovať ID entít svojich entít?", + "confirm_rename_entity_ids_warning": "To nezmení žiadnu konfiguráciu (napríklad automatizácie, skripty, scény, dashboardy), ktorá momentálne používa tieto entity! Aby ste mohli používať nové ID entít, budete si ich musieť sami aktualizovať!", "data_table": { "area": "Oblasť", "battery": "Batérie", @@ -1544,7 +1548,7 @@ "remove_selected": { "button": "Odstrániť vybraté", "confirm_partly_title": "Iba {number} {number, plural,\n one {vybraná entita}\n other {vybrané entity}\n} môžu byť odstránené.", - "confirm_text": "Entity je možné odstrániť, iba ak ich integrácia už neposkytuje.", + "confirm_text": "Mali by ste ich odstrániť z Lovelace konfigurácie a automatizácií, ak obsahujú tieto entity.", "confirm_title": "Chcete odstrániť {number} {number, plural,\n one {entita}\n other {entity}\n}?" }, "search": "Vyhľadať entity", @@ -1652,14 +1656,14 @@ "configure": "Konfigurovať", "configured": "Nakonfigurovaný", "confirm_new": "Chcete nastaviť {integration}?", - "description": "Spravujte a nastavujte integrácie", + "description": "Spravujte integrácie so službami, zariadeniami, ...", "details": "Podrobnosti o integrácii", "discovered": "Objavené", "home_assistant_website": "Home Assistant webová stránka", "ignore": { "confirm_delete_ignore": "Potom, ako bude integrácia automaticky objavená (discovered), sa opäť ukáže medzi objavenými (discovered) integráciami. Môže to chvíľu trvať, alebo vyžadovať reštart.", "confirm_delete_ignore_title": "Prestať ignorovať {name} ?", - "confirm_ignore": "Naozaj nechcete nastaviť túto integráciu? Túto akciu môžete vrátiť späť kliknutím na položku „Zobraziť ignorované integrácie“ v ponuke v pravom hornom rohu.", + "confirm_ignore": "Naozaj nechcete nastaviť túto integráciu? Túto akciu môžete vrátiť späť kliknutím na položku 'Zobraziť ignorované integrácie' v ponuke v pravom hornom rohu.", "confirm_ignore_title": "Ignorovať objavenie (discovery) {name}?", "hide_ignored": "Skryť ignorované integrácie", "ignore": "Ignorovať", @@ -1840,11 +1844,11 @@ }, "person": { "add_person": "Pridať osobu", - "caption": "Osoby", + "caption": "Ľudia", "confirm_delete": "Naozaj chcete túto osobu odstrániť?", "confirm_delete2": "Všetky zariadenia patriace k tejto osobe sa stanú nepriradené.", "create_person": "Vytvoriť osobu", - "description": "Spravujte osoby, ktoré Home Assistant sleduje.", + "description": "Spravujte ľudí, ktorých Home Assistant sleduje", "detail": { "admin": "Administrator", "allow_login": "Povoliť osobe prihlásiť sa", @@ -1866,7 +1870,7 @@ "introduction": "Tu môžete definovať každú osobu, ktorá vás zaujíma v Home Assistant.", "learn_more": "Získajte viac informácií o osobách", "no_persons_created_yet": "Zdá sa, že ste ešte nevytvorili žiadne osoby.", - "note_about_persons_configured_in_yaml": "Poznámka: osoby nakonfigurované prostredníctvom configuration.yaml nie je možné upravovať pomocou používateľského rozhrania.", + "note_about_persons_configured_in_yaml": "Poznámka: Osoby konfigurované prostredníctvom configuration.yaml nie je možné upravovať prostredníctvom používateľského rozhrania.", "person_not_found": "Nemohli sme nájsť osobu, ktorú ste sa snažili upraviť.", "person_not_found_title": "Osoba sa nenašla" }, @@ -1917,7 +1921,7 @@ }, "script": { "caption": "Skripty", - "description": "Vytvárajte a upravujte skripty", + "description": "Spustite sekvencie akcií", "editor": { "alias": "Názov", "default_name": "Nový skript", @@ -2022,6 +2026,7 @@ "create": "Vytvoriť", "delete": "Odstrániť", "name": "Názov", + "tag_id": "ID značky", "update": "Aktualizovať" }, "headers": { @@ -2075,7 +2080,8 @@ "system": "Systémom vytvorený", "username": "Užívateľské meno" } - } + }, + "users_privileges_note": "Funkcia skupiny používateľov je vo vývoji. Používateľ nebude môcť spravovať inštanciu prostredníctvom používateľského rozhrania. Stále kontrolujeme všetky koncové body rozhrania API na správu, aby sme sa uistili, že správne obmedzujú prístup na správcov." }, "zha": { "add_device_page": { @@ -2111,7 +2117,7 @@ "clusters": { "header": "Zoskupenia", "help_cluster_dropdown": "Vyberte klaster na zobrazenie atribútov a príkazov.", - "introduction": "Zoskupenia (klastre) sú stavebnými kameňmi funkčnosti Zigbee. Rozdeľujú funkčnosť na logické jednotky. Existujú typy klientov a serverov, ktoré sa skladajú z atribútov a príkazov." + "introduction": "Klastre sú stavebnými kameňmi pre funkčnosť Zigbee. Rozdeľujú funkčnosť na logické jednotky. Existujú typy klientov a serverov, ktoré pozostávajú z atribútov a príkazov." }, "common": { "add_devices": "Pridať zariadenia", @@ -2183,7 +2189,7 @@ "create": "Vytvoriť", "delete": "Odstrániť", "icon": "Ikona", - "icon_error_msg": "Ikona by mala byť vo formáte predpona:názov-ikony, napríklad: mdi:home", + "icon_error_msg": "Ikona by mala byť vo formáte \"predpona:názov-ikony\", napríklad: \"mdi:home\"", "latitude": "Zemepisná šírka", "longitude": "Zemepisná dĺžka", "name": "Názov", @@ -2234,7 +2240,7 @@ "config_parameter": "Konfigurovať parameter", "config_value": "Konfiguračná hodnota", "false": "False", - "header": "Upraviť konfiguráciu zariadenia", + "header": "Možnosti konfigurácie uzla", "seconds": "Sekúnd", "set_config_parameter": "Nastavte konfiguračný parameter", "set_wakeup": "Nastaviť interval prebudenia", @@ -2242,6 +2248,7 @@ }, "node_management": { "add_to_group": "Pridať do skupiny", + "nodes_in_group": "Ostatné uzly v tejto skupine:", "remove_from_group": "Odstrániť zo skupiny" }, "ozw_log": { @@ -2393,7 +2400,7 @@ } }, "changed_toast": { - "message": "Konfigurácia Lovelace bola aktualizovaná, chcete ju obnoviť?", + "message": "Konfigurácia používateľského rozhrania Lovelace pre tento dashboard bola aktualizovaná. Obnoviť a zobraziť zmeny?", "refresh": "Obnoviť" }, "editor": { @@ -2444,6 +2451,7 @@ }, "entities": { "description": "Karta Entity je najbežnejším typom karty. Zoskupuje položky do zoznamov.", + "edit_special_row": "Kliknutím na tlačidlo upraviť zobrazíte podrobnosti tohto riadku", "entity_row_editor": "Editor riadkov entít", "entity_row": { "attribute": "Atribút", @@ -2535,7 +2543,7 @@ "name": "Zvlhčovač" }, "iframe": { - "name": "iFrame" + "name": "Web stránka" }, "light": { "description": "Karta Svetlo umožňuje zmeniť jas svetla.", @@ -2567,7 +2575,7 @@ "name": "Entita obrázka" }, "picture-glance": { - "description": "Karta Náhľad obrázka zobrazuje obrázok a stav zodpovedajúcej entity ako ikonu. Subjekty na pravej strane umožňujú prepínacie akcie, iné zobrazujú dialógové okno s ďalšími informáciami.", + "description": "Karta Picture Glance zobrazuje obrázok a príslušné stavy entity ako ikonu. Entity na pravej strane umožňujú prepínať akcie, iné zobrazujú dialógové okno s ďalšími informáciami.", "name": "Náhľad obrázka" }, "picture": { @@ -2658,7 +2666,7 @@ "migrate": { "header": "Nekompatibilná konfigurácia", "migrate": "Migrovať konfiguráciu", - "para_migrate": "Home Assistant môže automaticky pridať identifikátory pre všetky vaše karty a zobrazenia, keď kliknete na tlačidlo 'Migrovať konfiguráciu'.", + "para_migrate": "Home Assistant môže automaticky pridávať ID na všetky vaše karty a zobrazenia stlačením tlačidla 'Migrovať konfiguráciu'.", "para_no_id": "Tento prvok nemá žiadne ID. Doplňte, prosím, ID pre tento prvok v súbore 'ui-lovelace.yaml'." }, "move_card": { @@ -2666,7 +2674,7 @@ }, "raw_editor": { "confirm_remove_config_text": "Ak odstránite svoju konfiguráciu používateľského rozhrania Lovelace, automaticky vygenerujeme vaše zobrazenia používateľského rozhrania Lovelace s vašimi oblasťami a zariadeniami.", - "confirm_remove_config_title": "Naozaj chcete odstrániť konfiguráciu používateľského rozhrania Lovelace? Vaše zobrazenia používateľské rozhranie Lovelace automaticky vygenerujeme s vašimi oblasťami a zariadeniami.", + "confirm_remove_config_title": "Naozaj chcete odstrániť svoju konfiguráciu používateľského rozhrania Lovelace?", "confirm_unsaved_changes": "Máte neuložené zmeny. Naozaj chcete odísť?", "confirm_unsaved_comments": "Vaša konfigurácia obsahuje komentáre, ktoré sa neuložia. Chcete pokračovať?", "error_invalid_config": "Vaša konfigurácia je neplatná: {error}", @@ -2701,7 +2709,7 @@ }, "view": { "panel_mode": { - "description": "Takto bude prvá karta v plnej šírke; ostatné karty v tomto zobrazení sa nebudú vykresľovať.", + "description": "Týmto sa prvá karta vykreslí v plnej šírke. Ostatné karty v tomto zobrazení, ako aj odznaky sa nebudú zobrazovať.", "title": "Režim panelu?", "warning_multiple_cards": "Toto zobrazenie obsahuje viac ako jednu kartu, ale na panelovom zobrazení môže byť iba jedna karta." } @@ -2732,7 +2740,7 @@ "title": "Nepoužité entity" }, "views": { - "confirm_delete": "Naozaj chcete odstrániť toto zobrazenie?", + "confirm_delete": "Odstrániť zobrazenie?", "confirm_delete_existing_cards": "Odstránením tohto zobrazenia sa odstránia aj karty", "confirm_delete_existing_cards_text": "Naozaj chcete odstrániť zobrazenie ''{name}''? Toto zobrazenie obsahuje {number} kariet, ktoré budú odstránené. Túto akciu nie je možné vrátiť späť.", "confirm_delete_text": "Naozaj chcete odstrániť zobrazenie ''{name}''?" @@ -2935,7 +2943,7 @@ }, "profile": { "advanced_mode": { - "description": "Home Asistent v predvolenom nastavení skryje rozšírené funkcie a možnosti. Začiarknutím tohto prepínača môžete tieto funkcie sprístupniť. Toto nastavenie je špecifické pre konkrétneho používateľa a nemá vplyv na ostatných používateľov používajúcich Home Assistant.", + "description": "Odomkne pokročilé funkcie.", "link_promo": "Viac informácií", "title": "Rozšírený režim" }, @@ -2989,7 +2997,7 @@ "name": "Názov", "not_used": "Nikdy nebol použitý", "prompt_copy_token": "Skopírujte svoj nový prístupový token. Znova sa nezobrazí.", - "prompt_name": "Názov?" + "prompt_name": "Pomenujte token" }, "mfa_setup": { "close": "Zavrieť", diff --git a/translations/frontend/zh-Hans.json b/translations/frontend/zh-Hans.json index 3ff491583f..0f72d61e05 100644 --- a/translations/frontend/zh-Hans.json +++ b/translations/frontend/zh-Hans.json @@ -1243,9 +1243,9 @@ }, "sun": { "after": "晚于:", - "after_offset": "延后:(可选)", + "after_offset": "“晚于”的偏移 (可选)", "before": "早于:", - "before_offset": "提前:(可选)", + "before_offset": "“早于”的偏移 (可选)", "label": "日出/日落", "sunrise": "日出", "sunset": "日落" From 17410874e30d3a9be9d92d3fadd5dee77245cfdc Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 18 Feb 2021 01:18:34 +0000 Subject: [PATCH 22/44] Translation update --- translations/frontend/ca.json | 3 ++- translations/frontend/es.json | 1 + translations/frontend/et.json | 5 +++-- translations/frontend/it.json | 1 + translations/frontend/ru.json | 1 + translations/frontend/zh-Hant.json | 1 + 6 files changed, 9 insertions(+), 3 deletions(-) diff --git a/translations/frontend/ca.json b/translations/frontend/ca.json index 90ca18d783..babcf88b73 100644 --- a/translations/frontend/ca.json +++ b/translations/frontend/ca.json @@ -3409,9 +3409,10 @@ "playback_title": "Reproducció de missatges" }, "my": { + "component_not_loaded": "La instància de Home Assistant no admet aquesta redirecció. Necessites la integració {integration} per poder fer aquesta redirecció.", "error": "S'ha produït un error desconegut", "faq_link": "Preguntes freqüents de Home Assistant", - "not_supported": "La teva instància de Home Assistant no admet aquesta redirecció. Consulta {link} per veure les redireccions compatibles i en quina versió es van introduir." + "not_supported": "La instància de Home Assistant no admet aquesta redirecció. Consulta {link} per veure les redireccions compatibles i en quina versió es van introduir." }, "page-authorize": { "abort_intro": "S'ha avortat l'inici de sessió", diff --git a/translations/frontend/es.json b/translations/frontend/es.json index d80e9e6542..3fdb99dc77 100644 --- a/translations/frontend/es.json +++ b/translations/frontend/es.json @@ -3409,6 +3409,7 @@ "playback_title": "Reproducción de mensajes" }, "my": { + "component_not_loaded": "Esta redirección no es compatible con tu instancia de Home Assistant. Necesitas la integración {integration} para usar este redireccionamiento.", "error": "Se ha producido un error desconocido", "faq_link": "Preguntas frecuentes sobre mi Home Assistant", "not_supported": "Esta redirección no es compatible con tu instancia de Home Assistant. Consulta el {link} para conocer las redirecciones admitidas y la versión en la que se introdujeron." diff --git a/translations/frontend/et.json b/translations/frontend/et.json index b880928d04..5fe6585fba 100644 --- a/translations/frontend/et.json +++ b/translations/frontend/et.json @@ -1243,9 +1243,9 @@ }, "sun": { "after": "Peale:", - "after_offset": "Pärastine viiteaeg (valikuline)", + "after_offset": "Pärastine ajanihe (valikuline)", "before": "Enne:", - "before_offset": "Eelnev viiteaeg (valikuline)", + "before_offset": "Eelnev ajanihe (valikuline)", "label": "Päike", "sunrise": "Tõusu", "sunset": "Loojangut" @@ -3409,6 +3409,7 @@ "playback_title": "Sõnumi taasesitus" }, "my": { + "component_not_loaded": "Home Assistant ei toeta seda ümbersuunamist. Selle ümbersuunamise kasutamiseks on vaja sidumist {integration}.", "error": "Ilmnes tundmatu viga", "faq_link": "Home Assistanti My sidumise KKK", "not_supported": "Home Assistant ei toeta seda ümbersuunamist. Kontrolli kas {link} on toetatud ümbersuunamiseks ja nende kasutusele võetud HA versiooni." diff --git a/translations/frontend/it.json b/translations/frontend/it.json index 336938c320..1e63e6192d 100644 --- a/translations/frontend/it.json +++ b/translations/frontend/it.json @@ -3409,6 +3409,7 @@ "playback_title": "Riproduzione messaggio" }, "my": { + "component_not_loaded": "Questo reindirizzamento non è supportato dalla tua istanza di Home Assistant. È necessaria l'integrazione {integration} per utilizzare questo reindirizzamento.", "error": "Si è verificato un errore sconosciuto", "faq_link": "My Home Assistant FAQ", "not_supported": "Questo reindirizzamento non è supportato dall'istanza di Home Assistant. Controlla il {link} per i reindirizzamenti supportati e la versione in cui sono stati introdotti." diff --git a/translations/frontend/ru.json b/translations/frontend/ru.json index 84e677b63d..b8d478413b 100644 --- a/translations/frontend/ru.json +++ b/translations/frontend/ru.json @@ -3409,6 +3409,7 @@ "playback_title": "Воспроизвести сообщение" }, "my": { + "component_not_loaded": "Это перенаправление не поддерживается Вашим Home Assistant. Настройте интеграцию \"{integration}\" для использования этого перенаправления.", "error": "Произошла неизвестная ошибка.", "faq_link": "часто задаваемыми вопросами по My Home Assistant", "not_supported": "Это перенаправление не поддерживается Вашим Home Assistant. Ознакомьтесь с {link}, чтобы узнать поддерживаемые перенаправления и версии, в которых они были добавлены." diff --git a/translations/frontend/zh-Hant.json b/translations/frontend/zh-Hant.json index 5ca0c5765f..25c6ddd9b5 100644 --- a/translations/frontend/zh-Hant.json +++ b/translations/frontend/zh-Hant.json @@ -3409,6 +3409,7 @@ "playback_title": "訊息播放" }, "my": { + "component_not_loaded": "Home Assistant 不支援此重新導向。需要使用整合 {integration} 方能使用。", "error": "發生未知錯誤", "faq_link": "Home Assistant 常見問答集", "not_supported": "Home Assistant 不支援此重新導向。點選 {link} 獲取支援之重新導向與版本。" From 09e7600d8639b81600f3917d7678ebd93779a936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 18 Feb 2021 18:18:05 +0100 Subject: [PATCH 23/44] Use websockets (#8403) Co-authored-by: Bram Kragten --- cast/src/launcher/layout/hc-cast.ts | 2 +- hassio/src/addon-store/hassio-addon-store.ts | 77 ++++----- .../src/addon-view/hassio-addon-dashboard.ts | 16 +- .../src/addon-view/info/hassio-addon-info.ts | 105 ++++++++++-- hassio/src/dashboard/hassio-update.ts | 7 + hassio/src/hassio-main.ts | 11 +- hassio/src/hassio-router.ts | 3 +- hassio/src/supervisor-base-element.ts | 90 +++++++++- hassio/src/system/hassio-core-info.ts | 2 + hassio/src/system/hassio-host-info.ts | 17 +- hassio/src/system/hassio-supervisor-info.ts | 5 +- src/common/config/version.ts | 10 +- src/data/hassio/addon.ts | 156 ++++++++++++++++-- src/data/hassio/common.ts | 9 + src/data/hassio/docker.ts | 32 +++- src/data/hassio/hardware.ts | 25 ++- src/data/hassio/host.ts | 70 +++++++- src/data/hassio/ingress.ts | 45 +++-- src/data/hassio/network.ts | 35 +++- src/data/hassio/resolution.ts | 13 +- src/data/hassio/snapshot.ts | 52 +++++- src/data/hassio/supervisor.ts | 76 ++++++++- src/data/supervisor/core.ts | 11 ++ src/data/supervisor/supervisor.ts | 116 +++++++++++++ test-mocha/hassio/create_session.spec.ts | 25 ++- 25 files changed, 868 insertions(+), 142 deletions(-) diff --git a/cast/src/launcher/layout/hc-cast.ts b/cast/src/launcher/layout/hc-cast.ts index 1a00cd62dd..7bcbe70d59 100644 --- a/cast/src/launcher/layout/hc-cast.ts +++ b/cast/src/launcher/layout/hc-cast.ts @@ -48,7 +48,7 @@ class HcCast extends LitElement { protected render(): TemplateResult { if (this.lovelaceConfig === undefined) { - return html` > `; + return html``; } const error = diff --git a/hassio/src/addon-store/hassio-addon-store.ts b/hassio/src/addon-store/hassio-addon-store.ts index 2e21b19f84..6add263da7 100644 --- a/hassio/src/addon-store/hassio-addon-store.ts +++ b/hassio/src/addon-store/hassio-addon-store.ts @@ -11,19 +11,18 @@ import { PropertyValues, } from "lit-element"; import { html, TemplateResult } from "lit-html"; +import memoizeOne from "memoize-one"; import { atLeastVersion } from "../../../src/common/config/version"; import { fireEvent } from "../../../src/common/dom/fire_event"; import "../../../src/common/search/search-input"; import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-svg-icon"; import { - fetchHassioAddonsInfo, HassioAddonInfo, HassioAddonRepository, reloadHassioAddons, } from "../../../src/data/hassio/addon"; -import { extractApiErrorMessage } from "../../../src/data/hassio/common"; -import { fetchHassioSupervisorInfo } from "../../../src/data/hassio/supervisor"; +import { Supervisor } from "../../../src/data/supervisor/supervisor"; import "../../../src/layouts/hass-loading-screen"; import "../../../src/layouts/hass-tabs-subpage"; import { HomeAssistant, Route } from "../../../src/types"; @@ -51,46 +50,27 @@ const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => { class HassioAddonStore 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; - @property({ attribute: false }) private _addons?: HassioAddonInfo[]; - - @property({ attribute: false }) private _repos?: HassioAddonRepository[]; - @internalProperty() private _filter?: string; public async refreshData() { - this._repos = undefined; - this._addons = undefined; - this._filter = undefined; await reloadHassioAddons(this.hass); await this._loadData(); } protected render(): TemplateResult { - const repos: TemplateResult[] = []; + let repos: TemplateResult[] = []; - if (this._repos) { - for (const repo of this._repos) { - const addons = this._addons!.filter( - (addon) => addon.repository === repo.slug - ); - - if (addons.length === 0) { - continue; - } - - repos.push(html` - - `); - } + if (this.supervisor.addon.repositories) { + repos = this.addonRepositories( + this.supervisor.addon.repositories, + this.supervisor.addon.addons + ); } return html` @@ -159,6 +139,27 @@ class HassioAddonStore extends LitElement { this._loadData(); } + private addonRepositories = memoizeOne( + (repositories: HassioAddonRepository[], addons: HassioAddonInfo[]) => { + return repositories.sort(sortRepos).map((repo) => { + const filteredAddons = addons.filter( + (addon) => addon.repository === repo.slug + ); + + return filteredAddons.length !== 0 + ? html` + + ` + : html``; + }); + } + ); + private _handleAction(ev: CustomEvent) { switch (ev.detail.index) { case 0: @@ -181,7 +182,7 @@ class HassioAddonStore extends LitElement { private async _manageRepositories() { showRepositoriesDialog(this, { - repos: this._repos!, + repos: this.supervisor.addon.repositories, loadData: () => this._loadData(), }); } @@ -191,18 +192,8 @@ class HassioAddonStore extends LitElement { } private async _loadData() { - try { - const [addonsInfo, supervisor] = await Promise.all([ - fetchHassioAddonsInfo(this.hass), - fetchHassioSupervisorInfo(this.hass), - ]); - fireEvent(this, "supervisor-update", { supervisor }); - this._repos = addonsInfo.repositories; - this._repos.sort(sortRepos); - this._addons = addonsInfo.addons; - } catch (err) { - alert(extractApiErrorMessage(err)); - } + fireEvent(this, "supervisor-store-refresh", { store: "addon" }); + fireEvent(this, "supervisor-store-refresh", { store: "supervisor" }); } private async _filterChanged(e) { diff --git a/hassio/src/addon-view/hassio-addon-dashboard.ts b/hassio/src/addon-view/hassio-addon-dashboard.ts index add2c91e49..4c01e88c87 100644 --- a/hassio/src/addon-view/hassio-addon-dashboard.ts +++ b/hassio/src/addon-view/hassio-addon-dashboard.ts @@ -24,9 +24,7 @@ import { HassioAddonDetails, } from "../../../src/data/hassio/addon"; import { extractApiErrorMessage } from "../../../src/data/hassio/common"; -import { fetchHassioSupervisorInfo } from "../../../src/data/hassio/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor"; -import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; import "../../../src/layouts/hass-error-screen"; import "../../../src/layouts/hass-loading-screen"; import "../../../src/layouts/hass-tabs-subpage"; @@ -192,7 +190,7 @@ class HassioAddonDashboard extends LitElement { const path: string = pathSplit[pathSplit.length - 1]; if (["uninstall", "install", "update", "start", "stop"].includes(path)) { - await this._updateSupervisor(); + fireEvent(this, "supervisor-store-refresh", { store: "supervisor" }); } if (path === "uninstall") { @@ -221,18 +219,6 @@ class HassioAddonDashboard extends LitElement { this.addon = undefined; } } - - private async _updateSupervisor(): Promise { - try { - const supervisor = await fetchHassioSupervisorInfo(this.hass); - fireEvent(this, "supervisor-update", { supervisor }); - } catch (err) { - showAlertDialog(this, { - title: "Failed to fetch supervisor information", - text: extractApiErrorMessage(err), - }); - } - } } declare global { diff --git a/hassio/src/addon-view/info/hassio-addon-info.ts b/hassio/src/addon-view/info/hassio-addon-info.ts index 5c43d2a1db..dfb1386662 100644 --- a/hassio/src/addon-view/info/hassio-addon-info.ts +++ b/hassio/src/addon-view/info/hassio-addon-info.ts @@ -43,10 +43,13 @@ import { HassioAddonSetOptionParams, HassioAddonSetSecurityParams, installHassioAddon, + restartHassioAddon, setHassioAddonOption, setHassioAddonSecurity, startHassioAddon, + stopHassioAddon, uninstallHassioAddon, + updateHassioAddon, validateHassioAddonOption, } from "../../../../src/data/hassio/addon"; import { @@ -196,13 +199,9 @@ class HassioAddonInfo extends LitElement { : ""}
- + Update - + ${this.addon.changelog ? html` @@ -579,20 +578,18 @@ class HassioAddonInfo extends LitElement { ${this.addon.version ? this._computeIsRunning ? html` - Stop - - + Restart - + ` : html` @@ -883,6 +880,82 @@ class HassioAddonInfo extends LitElement { button.progress = false; } + private async _stopClicked(ev: CustomEvent): Promise { + const button = ev.currentTarget as any; + button.progress = true; + + try { + await stopHassioAddon(this.hass, this.addon.slug); + const eventdata = { + success: true, + response: undefined, + path: "stop", + }; + fireEvent(this, "hass-api-called", eventdata); + } catch (err) { + showAlertDialog(this, { + title: "Failed to stop addon", + text: extractApiErrorMessage(err), + }); + } + button.progress = false; + } + + private async _restartClicked(ev: CustomEvent): Promise { + const button = ev.currentTarget as any; + button.progress = true; + + try { + await restartHassioAddon(this.hass, this.addon.slug); + const eventdata = { + success: true, + response: undefined, + path: "stop", + }; + fireEvent(this, "hass-api-called", eventdata); + } catch (err) { + showAlertDialog(this, { + title: "Failed to restart addon", + text: extractApiErrorMessage(err), + }); + } + button.progress = false; + } + + private async _updateClicked(ev: CustomEvent): Promise { + const button = ev.currentTarget as any; + button.progress = true; + + const confirmed = await showConfirmationDialog(this, { + title: this.addon.name, + text: "Are you sure you want to update this add-on?", + confirmText: "update add-on", + dismissText: "no", + }); + + if (!confirmed) { + button.progress = false; + return; + } + + this._error = undefined; + try { + await updateHassioAddon(this.hass, this.addon.slug); + const eventdata = { + success: true, + response: undefined, + path: "update", + }; + fireEvent(this, "hass-api-called", eventdata); + } catch (err) { + showAlertDialog(this, { + title: "Failed to update addon", + text: extractApiErrorMessage(err), + }); + } + button.progress = false; + } + private async _startClicked(ev: CustomEvent): Promise { const button = ev.currentTarget as any; button.progress = true; @@ -891,10 +964,10 @@ class HassioAddonInfo extends LitElement { this.hass, this.addon.slug ); - if (!validate.data.valid) { + if (!validate.valid) { await showConfirmationDialog(this, { title: "Failed to start addon - configuration validation failed!", - text: validate.data.message.split(" Got ")[0], + text: validate.message.split(" Got ")[0], confirm: () => this._openConfiguration(), confirmText: "Go to configuration", dismissText: "Cancel", diff --git a/hassio/src/dashboard/hassio-update.ts b/hassio/src/dashboard/hassio-update.ts index b2c8bb8bd9..488e541f2f 100644 --- a/hassio/src/dashboard/hassio-update.ts +++ b/hassio/src/dashboard/hassio-update.ts @@ -10,6 +10,7 @@ import { TemplateResult, } from "lit-element"; import memoizeOne from "memoize-one"; +import { fireEvent } from "../../../src/common/dom/fire_event"; import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/ha-card"; import "../../../src/components/ha-svg-icon"; @@ -64,6 +65,7 @@ export class HassioUpdate extends LitElement {
${this._renderUpdateCard( "Home Assistant Core", + "core", this.supervisor.core, "hassio/homeassistant/update", `https://${ @@ -72,6 +74,7 @@ export class HassioUpdate extends LitElement { )} ${this._renderUpdateCard( "Supervisor", + "supervisor", this.supervisor.supervisor, "hassio/supervisor/update", `https://github.com//home-assistant/hassio/releases/tag/${this.supervisor.supervisor.version_latest}` @@ -79,6 +82,7 @@ export class HassioUpdate extends LitElement { ${this.supervisor.host.features.includes("hassos") ? this._renderUpdateCard( "Operating System", + "os", this.supervisor.os, "hassio/os/update", `https://github.com//home-assistant/hassos/releases/tag/${this.supervisor.os.version_latest}` @@ -91,6 +95,7 @@ export class HassioUpdate extends LitElement { private _renderUpdateCard( name: string, + key: string, object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo, apiPath: string, releaseNotesUrl: string @@ -116,6 +121,7 @@ export class HassioUpdate extends LitElement { @@ -142,6 +148,7 @@ export class HassioUpdate extends LitElement { } try { await this.hass.callApi>("POST", item.apiPath); + fireEvent(this, "supervisor-store-refresh", { store: item.key }); } catch (err) { // Only show an error if the status code was not expected (user behind proxy) // or no status at all(connection terminated) diff --git a/hassio/src/hassio-main.ts b/hassio/src/hassio-main.ts index 81d736e881..31de2006d7 100644 --- a/hassio/src/hassio-main.ts +++ b/hassio/src/hassio-main.ts @@ -3,7 +3,9 @@ import { atLeastVersion } from "../../src/common/config/version"; import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element"; import { fireEvent } from "../../src/common/dom/fire_event"; import { HassioPanelInfo } from "../../src/data/hassio/supervisor"; +import { supervisorStore } 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 "./hassio-router"; import { SupervisorBaseElement } from "./supervisor-base-element"; @@ -71,8 +73,15 @@ export class HassioMain extends SupervisorBaseElement { protected render() { if (!this.supervisor || !this.hass) { - return html``; + return html``; } + + if ( + Object.keys(supervisorStore).some((store) => !this.supervisor![store]) + ) { + return html``; + } + return html` ; + "supervisor-store-refresh": { store: SupervisorObject }; } } @@ -25,6 +41,20 @@ export class SupervisorBaseElement extends urlSyncMixin( ) { @property({ attribute: false }) public supervisor?: Supervisor; + @internalProperty() private _unsubs: Record = {}; + + @internalProperty() private _collections: Record< + string, + Collection + > = {}; + + public disconnectedCallback() { + super.disconnectedCallback(); + Object.keys(this._unsubs).forEach((unsub) => { + this._unsubs[unsub](); + }); + } + protected _updateSupervisor(obj: Partial): void { this.supervisor = { ...this.supervisor!, ...obj }; } @@ -32,13 +62,59 @@ export class SupervisorBaseElement extends urlSyncMixin( protected firstUpdated(changedProps: PropertyValues): void { super.firstUpdated(changedProps); this._initSupervisor(); - this.addEventListener("supervisor-update", (ev) => - this._updateSupervisor(ev.detail) + } + + private async _handleSupervisorStoreRefreshEvent(ev) { + const store = ev.detail.store; + if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) { + this._collections[store].refresh(); + return; + } + + const response = await this.hass.callApi>( + "GET", + `hassio${supervisorStore[store]}` ); + this._updateSupervisor({ [store]: response.data }); } private async _initSupervisor(): Promise { + this.addEventListener( + "supervisor-store-refresh", + this._handleSupervisorStoreRefreshEvent + ); + + if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) { + Object.keys(supervisorStore).forEach((store) => { + this._unsubs[store] = subscribeSupervisorEvents( + this.hass, + (data) => this._updateSupervisor({ [store]: data }), + store, + supervisorStore[store] + ); + if (this._collections[store]) { + this._collections[store].refresh(); + } else { + this._collections[store] = getSupervisorEventCollection( + this.hass.connection, + store, + supervisorStore[store] + ); + } + }); + + if (this.supervisor === undefined) { + Object.keys(this._collections).forEach((collection) => + this._updateSupervisor({ + [collection]: this._collections[collection].state, + }) + ); + } + return; + } + const [ + addon, supervisor, host, core, @@ -47,6 +123,7 @@ export class SupervisorBaseElement extends urlSyncMixin( network, resolution, ] = await Promise.all([ + fetchHassioAddonsInfo(this.hass), fetchHassioSupervisorInfo(this.hass), fetchHassioHostInfo(this.hass), fetchHassioHomeAssistantInfo(this.hass), @@ -57,6 +134,7 @@ export class SupervisorBaseElement extends urlSyncMixin( ]); this.supervisor = { + addon, supervisor, host, core, @@ -65,5 +143,9 @@ export class SupervisorBaseElement extends urlSyncMixin( network, resolution, }; + + this.addEventListener("supervisor-update", (ev) => + this._updateSupervisor(ev.detail) + ); } } diff --git a/hassio/src/system/hassio-core-info.ts b/hassio/src/system/hassio-core-info.ts index 96c3aad65f..985a242773 100644 --- a/hassio/src/system/hassio-core-info.ts +++ b/hassio/src/system/hassio-core-info.ts @@ -10,6 +10,7 @@ import { property, TemplateResult, } from "lit-element"; +import { fireEvent } from "../../../src/common/dom/fire_event"; import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-card"; @@ -166,6 +167,7 @@ class HassioCoreInfo extends LitElement { try { await updateCore(this.hass); + fireEvent(this, "supervisor-store-refresh", { store: "core" }); } catch (err) { showAlertDialog(this, { title: "Failed to update Home Assistant Core", diff --git a/hassio/src/system/hassio-host-info.ts b/hassio/src/system/hassio-host-info.ts index b4fd17f9b2..69245d8663 100644 --- a/hassio/src/system/hassio-host-info.ts +++ b/hassio/src/system/hassio-host-info.ts @@ -13,6 +13,7 @@ import { TemplateResult, } from "lit-element"; import memoizeOne from "memoize-one"; +import { atLeastVersion } from "../../../src/common/config/version"; import { fireEvent } from "../../../src/common/dom/fire_event"; import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/ha-button-menu"; @@ -26,7 +27,6 @@ import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware"; import { changeHostOptions, configSyncOS, - fetchHassioHostInfo, rebootHost, shutdownHost, updateOS, @@ -340,6 +340,7 @@ class HassioHostInfo extends LitElement { try { await updateOS(this.hass); + fireEvent(this, "supervisor-store-refresh", { store: "os" }); } catch (err) { showAlertDialog(this, { title: "Failed to update", @@ -368,8 +369,7 @@ class HassioHostInfo extends LitElement { if (hostname && hostname !== curHostname) { try { await changeHostOptions(this.hass, { hostname }); - const host = await fetchHassioHostInfo(this.hass); - fireEvent(this, "supervisor-update", { host }); + fireEvent(this, "supervisor-store-refresh", { store: "host" }); } catch (err) { showAlertDialog(this, { title: "Setting hostname failed", @@ -382,8 +382,7 @@ class HassioHostInfo extends LitElement { private async _importFromUSB(): Promise { try { await configSyncOS(this.hass); - const host = await fetchHassioHostInfo(this.hass); - fireEvent(this, "supervisor-update", { host }); + fireEvent(this, "supervisor-store-refresh", { store: "host" }); } catch (err) { showAlertDialog(this, { title: "Failed to import from USB", @@ -393,8 +392,12 @@ class HassioHostInfo extends LitElement { } private async _loadData(): Promise { - const network = await fetchNetworkInfo(this.hass); - fireEvent(this, "supervisor-update", { network }); + if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) { + fireEvent(this, "supervisor-store-refresh", { store: "network" }); + } else { + const network = await fetchNetworkInfo(this.hass); + fireEvent(this, "supervisor-update", { network }); + } } static get styles(): CSSResult[] { diff --git a/hassio/src/system/hassio-supervisor-info.ts b/hassio/src/system/hassio-supervisor-info.ts index b29252d2ad..ebc96ab32a 100644 --- a/hassio/src/system/hassio-supervisor-info.ts +++ b/hassio/src/system/hassio-supervisor-info.ts @@ -19,7 +19,6 @@ import { HassioStats, } from "../../../src/data/hassio/common"; import { - fetchHassioSupervisorInfo, reloadSupervisor, restartSupervisor, setSupervisorOption, @@ -318,8 +317,7 @@ class HassioSupervisorInfo extends LitElement { private async _reloadSupervisor(): Promise { await reloadSupervisor(this.hass); - const supervisor = await fetchHassioSupervisorInfo(this.hass); - fireEvent(this, "supervisor-update", { supervisor }); + fireEvent(this, "supervisor-store-refresh", { store: "supervisor" }); } private async _supervisorRestart(ev: CustomEvent): Promise { @@ -368,6 +366,7 @@ class HassioSupervisorInfo extends LitElement { try { await updateSupervisor(this.hass); + fireEvent(this, "supervisor-store-refresh", { store: "supervisor" }); } catch (err) { showAlertDialog(this, { title: "Failed to update the supervisor", diff --git a/src/common/config/version.ts b/src/common/config/version.ts index cc6ccf7f7f..affdf9d94d 100644 --- a/src/common/config/version.ts +++ b/src/common/config/version.ts @@ -1,11 +1,15 @@ export const atLeastVersion = ( version: string, major: number, - minor: number + minor: number, + patch?: number ): boolean => { - const [haMajor, haMinor] = version.split(".", 2); + const [haMajor, haMinor, haPatch] = version.split(".", 3); return ( Number(haMajor) > major || - (Number(haMajor) === major && Number(haMinor) >= minor) + (Number(haMajor) === major && Number(haMinor) >= minor) || + (patch !== undefined && + Number(haMajor) === major && Number(haMinor) === minor && + Number(haPatch) >= patch) ); }; diff --git a/src/data/hassio/addon.ts b/src/data/hassio/addon.ts index 0429734bbd..b17f327097 100644 --- a/src/data/hassio/addon.ts +++ b/src/data/hassio/addon.ts @@ -1,3 +1,4 @@ +import { atLeastVersion } from "../../common/config/version"; import { HaFormSchema } from "../../components/ha-form/ha-form"; import { HomeAssistant } from "../../types"; import { SupervisorArch } from "../supervisor/supervisor"; @@ -102,10 +103,28 @@ export interface HassioAddonSetOptionParams { } export const reloadHassioAddons = async (hass: HomeAssistant) => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + await hass.callWS({ + type: "supervisor/api", + endpoint: "/addons/reload", + method: "post", + }); + return; + } await hass.callApi>("POST", `hassio/addons/reload`); }; -export const fetchHassioAddonsInfo = async (hass: HomeAssistant) => { +export const fetchHassioAddonsInfo = async ( + hass: HomeAssistant +): Promise => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + return await hass.callWS({ + type: "supervisor/api", + endpoint: "/addons", + method: "get", + }); + } + return hassioApiResultExtractor( await hass.callApi>("GET", `hassio/addons`) ); @@ -114,7 +133,15 @@ export const fetchHassioAddonsInfo = async (hass: HomeAssistant) => { export const fetchHassioAddonInfo = async ( hass: HomeAssistant, slug: string -) => { +): Promise => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + return await hass.callWS({ + type: "supervisor/api", + endpoint: `/addons/${slug}/info`, + method: "get", + }); + } + return hassioApiResultExtractor( await hass.callApi>( "GET", @@ -149,6 +176,16 @@ export const setHassioAddonOption = async ( slug: string, data: HassioAddonSetOptionParams ) => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + await hass.callWS({ + type: "supervisor/api", + endpoint: `/addons/${slug}/options`, + method: "post", + data, + }); + return; + } + await hass.callApi>( "POST", `hassio/addons/${slug}/options`, @@ -159,21 +196,64 @@ export const setHassioAddonOption = async ( export const validateHassioAddonOption = async ( hass: HomeAssistant, slug: string -) => { - return await hass.callApi< - HassioResponse<{ message: string; valid: boolean }> - >("POST", `hassio/addons/${slug}/options/validate`); +): Promise<{ message: string; valid: boolean }> => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + return await hass.callWS({ + type: "supervisor/api", + endpoint: `/addons/${slug}/options/validate`, + method: "post", + }); + } + + return ( + await hass.callApi>( + "POST", + `hassio/addons/${slug}/options/validate` + ) + ).data; }; export const startHassioAddon = async (hass: HomeAssistant, slug: string) => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + return await hass.callWS({ + type: "supervisor/api", + endpoint: `/addons/${slug}/start`, + method: "post", + timeout: null, + }); + } + return hass.callApi("POST", `hassio/addons/${slug}/start`); }; +export const stopHassioAddon = async (hass: HomeAssistant, slug: string) => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + return await hass.callWS({ + type: "supervisor/api", + endpoint: `/addons/${slug}/stop`, + method: "post", + timeout: null, + }); + } + + return hass.callApi("POST", `hassio/addons/${slug}/stop`); +}; + export const setHassioAddonSecurity = async ( hass: HomeAssistant, slug: string, data: HassioAddonSetSecurityParams ) => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + await hass.callWS({ + type: "supervisor/api", + endpoint: `/addons/${slug}/security`, + method: "post", + data, + }); + return; + } + await hass.callApi>( "POST", `hassio/addons/${slug}/security`, @@ -181,15 +261,61 @@ export const setHassioAddonSecurity = async ( ); }; -export const installHassioAddon = async (hass: HomeAssistant, slug: string) => { - return hass.callApi>( +export const installHassioAddon = async ( + hass: HomeAssistant, + slug: string +): Promise => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + await hass.callWS({ + type: "supervisor/api", + endpoint: `/addons/${slug}/install`, + method: "post", + timeout: null, + }); + return; + } + + await hass.callApi>( "POST", `hassio/addons/${slug}/install` ); }; -export const restartHassioAddon = async (hass: HomeAssistant, slug: string) => { - return hass.callApi>( +export const updateHassioAddon = async ( + hass: HomeAssistant, + slug: string +): Promise => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + await hass.callWS({ + type: "supervisor/api", + endpoint: `/addons/${slug}/update`, + method: "post", + timeout: null, + }); + return; + } + + await hass.callApi>( + "POST", + `hassio/addons/${slug}/update` + ); +}; + +export const restartHassioAddon = async ( + hass: HomeAssistant, + slug: string +): Promise => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + await hass.callWS({ + type: "supervisor/api", + endpoint: `/addons/${slug}/restart`, + method: "post", + timeout: null, + }); + return; + } + + await hass.callApi>( "POST", `hassio/addons/${slug}/restart` ); @@ -199,6 +325,16 @@ export const uninstallHassioAddon = async ( hass: HomeAssistant, slug: string ) => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + await hass.callWS({ + type: "supervisor/api", + endpoint: `/addons/${slug}/uninstall`, + method: "post", + timeout: null, + }); + return; + } + await hass.callApi>( "POST", `hassio/addons/${slug}/uninstall` diff --git a/src/data/hassio/common.ts b/src/data/hassio/common.ts index 57a0afa59a..de6a28c68b 100644 --- a/src/data/hassio/common.ts +++ b/src/data/hassio/common.ts @@ -1,3 +1,4 @@ +import { atLeastVersion } from "../../common/config/version"; import { HomeAssistant } from "../../types"; export interface HassioResponse { @@ -33,6 +34,14 @@ export const fetchHassioStats = async ( hass: HomeAssistant, container: string ): Promise => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + return await hass.callWS({ + type: "supervisor/api", + endpoint: `/${container}/stats`, + method: "get", + }); + } + return hassioApiResultExtractor( await hass.callApi>( "GET", diff --git a/src/data/hassio/docker.ts b/src/data/hassio/docker.ts index 4bc9a194c5..c8884336e0 100644 --- a/src/data/hassio/docker.ts +++ b/src/data/hassio/docker.ts @@ -1,3 +1,4 @@ +import { atLeastVersion } from "../../common/config/version"; import { HomeAssistant } from "../../types"; import { hassioApiResultExtractor, HassioResponse } from "./common"; @@ -5,7 +6,17 @@ interface HassioDockerRegistries { [key: string]: { username: string; password?: string }; } -export const fetchHassioDockerRegistries = async (hass: HomeAssistant) => { +export const fetchHassioDockerRegistries = async ( + hass: HomeAssistant +): Promise => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + return await hass.callWS({ + type: "supervisor/api", + endpoint: `/docker/registries`, + method: "get", + }); + } + return hassioApiResultExtractor( await hass.callApi>( "GET", @@ -18,6 +29,16 @@ export const addHassioDockerRegistry = async ( hass: HomeAssistant, data: HassioDockerRegistries ) => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + await hass.callWS({ + type: "supervisor/api", + endpoint: `/docker/registries`, + method: "post", + data, + }); + return; + } + await hass.callApi>( "POST", "hassio/docker/registries", @@ -29,6 +50,15 @@ export const removeHassioDockerRegistry = async ( hass: HomeAssistant, registry: string ) => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + await hass.callWS({ + type: "supervisor/api", + endpoint: `/docker/registries/${registry}`, + method: "delete", + }); + return; + } + await hass.callApi>( "DELETE", `hassio/docker/registries/${registry}` diff --git a/src/data/hassio/hardware.ts b/src/data/hassio/hardware.ts index 2df7a8bcdd..11e70e8b69 100644 --- a/src/data/hassio/hardware.ts +++ b/src/data/hassio/hardware.ts @@ -1,3 +1,4 @@ +import { atLeastVersion } from "../../common/config/version"; import { HomeAssistant } from "../../types"; import { hassioApiResultExtractor, HassioResponse } from "./common"; @@ -21,7 +22,17 @@ export interface HassioHardwareInfo { audio: Record; } -export const fetchHassioHardwareAudio = async (hass: HomeAssistant) => { +export const fetchHassioHardwareAudio = async ( + hass: HomeAssistant +): Promise => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + return await hass.callWS({ + type: "supervisor/api", + endpoint: `/hardware/audio`, + method: "get", + }); + } + return hassioApiResultExtractor( await hass.callApi>( "GET", @@ -30,7 +41,17 @@ export const fetchHassioHardwareAudio = async (hass: HomeAssistant) => { ); }; -export const fetchHassioHardwareInfo = async (hass: HomeAssistant) => { +export const fetchHassioHardwareInfo = async ( + hass: HomeAssistant +): Promise => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + return await hass.callWS({ + type: "supervisor/api", + endpoint: `/hardware/info`, + method: "get", + }); + } + return hassioApiResultExtractor( await hass.callApi>( "GET", diff --git a/src/data/hassio/host.ts b/src/data/hassio/host.ts index 79e518b4c9..2718adbcb4 100644 --- a/src/data/hassio/host.ts +++ b/src/data/hassio/host.ts @@ -1,3 +1,4 @@ +import { atLeastVersion } from "../../common/config/version"; import { HomeAssistant } from "../../types"; import { hassioApiResultExtractor, HassioResponse } from "./common"; @@ -23,7 +24,17 @@ export interface HassioHassOSInfo { version: string | null; } -export const fetchHassioHostInfo = async (hass: HomeAssistant) => { +export const fetchHassioHostInfo = async ( + hass: HomeAssistant +): Promise => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + return await hass.callWS({ + type: "supervisor/api", + endpoint: "/host/info", + method: "get", + }); + } + const response = await hass.callApi>( "GET", "hassio/host/info" @@ -31,7 +42,17 @@ export const fetchHassioHostInfo = async (hass: HomeAssistant) => { return hassioApiResultExtractor(response); }; -export const fetchHassioHassOsInfo = async (hass: HomeAssistant) => { +export const fetchHassioHassOsInfo = async ( + hass: HomeAssistant +): Promise => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + return await hass.callWS({ + type: "supervisor/api", + endpoint: "/os/info", + method: "get", + }); + } + return hassioApiResultExtractor( await hass.callApi>( "GET", @@ -41,22 +62,67 @@ export const fetchHassioHassOsInfo = async (hass: HomeAssistant) => { }; export const rebootHost = async (hass: HomeAssistant) => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + return await hass.callWS({ + type: "supervisor/api", + endpoint: "/host/reboot", + method: "post", + timeout: null, + }); + } + return hass.callApi>("POST", "hassio/host/reboot"); }; export const shutdownHost = async (hass: HomeAssistant) => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + return await hass.callWS({ + type: "supervisor/api", + endpoint: "/host/shutdown", + method: "post", + timeout: null, + }); + } + return hass.callApi>("POST", "hassio/host/shutdown"); }; export const updateOS = async (hass: HomeAssistant) => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + return await hass.callWS({ + type: "supervisor/api", + endpoint: "/os/update", + method: "post", + timeout: null, + }); + } + return hass.callApi>("POST", "hassio/os/update"); }; export const configSyncOS = async (hass: HomeAssistant) => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + return await hass.callWS({ + type: "supervisor/api", + endpoint: "os/config/sync", + method: "post", + timeout: null, + }); + } + return hass.callApi>("POST", "hassio/os/config/sync"); }; export const changeHostOptions = async (hass: HomeAssistant, options: any) => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + return await hass.callWS({ + type: "supervisor/api", + endpoint: "/host/options", + method: "post", + data: options, + }); + } + return hass.callApi>( "POST", "hassio/host/options", diff --git a/src/data/hassio/ingress.ts b/src/data/hassio/ingress.ts index ced84a2698..83a211fc2e 100644 --- a/src/data/hassio/ingress.ts +++ b/src/data/hassio/ingress.ts @@ -1,26 +1,49 @@ +import { atLeastVersion } from "../../common/config/version"; import { HomeAssistant } from "../../types"; import { HassioResponse } from "./common"; import { CreateSessionResponse } from "./supervisor"; -export const createHassioSession = async (hass: HomeAssistant) => { - const response = await hass.callApi>( - "POST", - "hassio/ingress/session" - ); - document.cookie = `ingress_session=${ - response.data.session - };path=/api/hassio_ingress/;SameSite=Strict${ +function setIngressCookie(session: string): string { + document.cookie = `ingress_session=${session};path=/api/hassio_ingress/;SameSite=Strict${ location.protocol === "https:" ? ";Secure" : "" }`; - return response.data.session; + return session; +} + +export const createHassioSession = async ( + hass: HomeAssistant +): Promise => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + const wsResponse: { session: string } = await hass.callWS({ + type: "supervisor/api", + endpoint: "/ingress/session", + method: "post", + }); + return setIngressCookie(wsResponse.session); + } + + const restResponse: { data: { session: string } } = await hass.callApi< + HassioResponse + >("POST", "hassio/ingress/session"); + return setIngressCookie(restResponse.data.session); }; export const validateHassioSession = async ( hass: HomeAssistant, session: string -) => - await hass.callApi>( +): Promise => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + await hass.callWS({ + type: "supervisor/api", + endpoint: "/ingress/validate_session", + method: "post", + data: session, + }); + } + + await hass.callApi>( "POST", "hassio/ingress/validate_session", { session } ); +}; diff --git a/src/data/hassio/network.ts b/src/data/hassio/network.ts index 542ee25d95..5aef6cc336 100644 --- a/src/data/hassio/network.ts +++ b/src/data/hassio/network.ts @@ -1,3 +1,4 @@ +import { atLeastVersion } from "../../common/config/version"; import { HomeAssistant } from "../../types"; import { hassioApiResultExtractor, HassioResponse } from "./common"; @@ -51,7 +52,17 @@ export interface NetworkInfo { docker: DockerNetwork; } -export const fetchNetworkInfo = async (hass: HomeAssistant) => { +export const fetchNetworkInfo = async ( + hass: HomeAssistant +): Promise => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + return await hass.callWS({ + type: "supervisor/api", + endpoint: "/network/info", + method: "get", + }); + } + return hassioApiResultExtractor( await hass.callApi>( "GET", @@ -65,6 +76,17 @@ export const updateNetworkInterface = async ( network_interface: string, options: Partial ) => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + await hass.callWS({ + type: "supervisor/api", + endpoint: `/network/interface/${network_interface}/update`, + method: "post", + data: options, + timeout: null, + }); + return; + } + await hass.callApi>( "POST", `hassio/network/interface/${network_interface}/update`, @@ -75,7 +97,16 @@ export const updateNetworkInterface = async ( export const accesspointScan = async ( hass: HomeAssistant, network_interface: string -) => { +): Promise => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + return await hass.callWS({ + type: "supervisor/api", + endpoint: `/network/interface/${network_interface}/accesspoints`, + method: "get", + timeout: null, + }); + } + return hassioApiResultExtractor( await hass.callApi>( "GET", diff --git a/src/data/hassio/resolution.ts b/src/data/hassio/resolution.ts index 677404f551..f108083cc5 100644 --- a/src/data/hassio/resolution.ts +++ b/src/data/hassio/resolution.ts @@ -1,3 +1,4 @@ +import { atLeastVersion } from "../../common/config/version"; import { HomeAssistant } from "../../types"; import { hassioApiResultExtractor, HassioResponse } from "./common"; @@ -8,7 +9,17 @@ export interface HassioResolution { suggestions: string[]; } -export const fetchHassioResolution = async (hass: HomeAssistant) => { +export const fetchHassioResolution = async ( + hass: HomeAssistant +): Promise => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + return await hass.callWS({ + type: "supervisor/api", + endpoint: "/resolution/info", + method: "get", + }); + } + return hassioApiResultExtractor( await hass.callApi>( "GET", diff --git a/src/data/hassio/snapshot.ts b/src/data/hassio/snapshot.ts index 18157d1255..ecd82ded1c 100644 --- a/src/data/hassio/snapshot.ts +++ b/src/data/hassio/snapshot.ts @@ -1,3 +1,4 @@ +import { atLeastVersion } from "../../common/config/version"; import { HomeAssistant } from "../../types"; import { hassioApiResultExtractor, HassioResponse } from "./common"; @@ -33,7 +34,18 @@ export interface HassioPartialSnapshotCreateParams { password?: string; } -export const fetchHassioSnapshots = async (hass: HomeAssistant) => { +export const fetchHassioSnapshots = async ( + hass: HomeAssistant +): Promise => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + const data: { snapshots: HassioSnapshot[] } = await hass.callWS({ + type: "supervisor/api", + endpoint: `/snapshots`, + method: "get", + }); + return data.snapshots; + } + return hassioApiResultExtractor( await hass.callApi>( "GET", @@ -45,8 +57,15 @@ export const fetchHassioSnapshots = async (hass: HomeAssistant) => { export const fetchHassioSnapshotInfo = async ( hass: HomeAssistant, snapshot: string -) => { +): Promise => { if (hass) { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + return await hass.callWS({ + type: "supervisor/api", + endpoint: `/snapshots/${snapshot}/info`, + method: "get", + }); + } return hassioApiResultExtractor( await hass.callApi>( "GET", @@ -63,6 +82,15 @@ export const fetchHassioSnapshotInfo = async ( }; export const reloadHassioSnapshots = async (hass: HomeAssistant) => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + await hass.callWS({ + type: "supervisor/api", + endpoint: "/snapshots/reload", + method: "post", + }); + return; + } + await hass.callApi>("POST", `hassio/snapshots/reload`); }; @@ -70,6 +98,15 @@ export const createHassioFullSnapshot = async ( hass: HomeAssistant, data: HassioFullSnapshotCreateParams ) => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + await hass.callWS({ + type: "supervisor/api", + endpoint: "/snapshots/new/full", + method: "post", + timeout: null, + }); + return; + } await hass.callApi>( "POST", `hassio/snapshots/new/full`, @@ -81,6 +118,17 @@ export const createHassioPartialSnapshot = async ( hass: HomeAssistant, data: HassioFullSnapshotCreateParams ) => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + await hass.callWS({ + type: "supervisor/api", + endpoint: "/snapshots/new/partial", + method: "post", + timeout: null, + data, + }); + return; + } + await hass.callApi>( "POST", `hassio/snapshots/new/partial`, diff --git a/src/data/hassio/supervisor.ts b/src/data/hassio/supervisor.ts index a21cfd1915..f7ce56a64e 100644 --- a/src/data/hassio/supervisor.ts +++ b/src/data/hassio/supervisor.ts @@ -1,3 +1,4 @@ +import { atLeastVersion } from "../../common/config/version"; import { HomeAssistant, PanelInfo } from "../../types"; import { SupervisorArch } from "../supervisor/supervisor"; import { HassioAddonInfo, HassioAddonRepository } from "./addon"; @@ -83,18 +84,57 @@ export interface SupervisorOptions { } export const reloadSupervisor = async (hass: HomeAssistant) => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + await hass.callWS({ + type: "supervisor/api", + endpoint: "/supervisor/reload", + method: "post", + }); + return; + } + await hass.callApi>("POST", `hassio/supervisor/reload`); }; export const restartSupervisor = async (hass: HomeAssistant) => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + await hass.callWS({ + type: "supervisor/api", + endpoint: "/supervisor/restart", + method: "post", + timeout: null, + }); + return; + } + await hass.callApi>("POST", `hassio/supervisor/restart`); }; export const updateSupervisor = async (hass: HomeAssistant) => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + await hass.callWS({ + type: "supervisor/api", + endpoint: "/supervisor/update", + method: "post", + timeout: null, + }); + return; + } + await hass.callApi>("POST", `hassio/supervisor/update`); }; -export const fetchHassioHomeAssistantInfo = async (hass: HomeAssistant) => { +export const fetchHassioHomeAssistantInfo = async ( + hass: HomeAssistant +): Promise => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + return await hass.callWS({ + type: "supervisor/api", + endpoint: "/core/info", + method: "get", + }); + } + return hassioApiResultExtractor( await hass.callApi>( "GET", @@ -103,7 +143,17 @@ export const fetchHassioHomeAssistantInfo = async (hass: HomeAssistant) => { ); }; -export const fetchHassioSupervisorInfo = async (hass: HomeAssistant) => { +export const fetchHassioSupervisorInfo = async ( + hass: HomeAssistant +): Promise => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + return await hass.callWS({ + type: "supervisor/api", + endpoint: "/supervisor/info", + method: "get", + }); + } + return hassioApiResultExtractor( await hass.callApi>( "GET", @@ -112,7 +162,17 @@ export const fetchHassioSupervisorInfo = async (hass: HomeAssistant) => { ); }; -export const fetchHassioInfo = async (hass: HomeAssistant) => { +export const fetchHassioInfo = async ( + hass: HomeAssistant +): Promise => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + return await hass.callWS({ + type: "supervisor/api", + endpoint: "/info", + method: "get", + }); + } + return hassioApiResultExtractor( await hass.callApi>("GET", "hassio/info") ); @@ -129,6 +189,16 @@ export const setSupervisorOption = async ( hass: HomeAssistant, data: SupervisorOptions ) => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + await hass.callWS({ + type: "supervisor/api", + endpoint: "/supervisor/options", + method: "post", + data, + }); + return; + } + await hass.callApi>( "POST", "hassio/supervisor/options", diff --git a/src/data/supervisor/core.ts b/src/data/supervisor/core.ts index 611fbabd36..0191dbe278 100644 --- a/src/data/supervisor/core.ts +++ b/src/data/supervisor/core.ts @@ -1,3 +1,4 @@ +import { atLeastVersion } from "../../common/config/version"; import { HomeAssistant } from "../../types"; import { HassioResponse } from "../hassio/common"; @@ -6,5 +7,15 @@ export const restartCore = async (hass: HomeAssistant) => { }; export const updateCore = async (hass: HomeAssistant) => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + await hass.callWS({ + type: "supervisor/api", + endpoint: "/core/update", + method: "post", + timeout: null, + }); + return; + } + await hass.callApi>("POST", `hassio/core/update`); }; diff --git a/src/data/supervisor/supervisor.ts b/src/data/supervisor/supervisor.ts index bc0161becf..1dbaecef2c 100644 --- a/src/data/supervisor/supervisor.ts +++ b/src/data/supervisor/supervisor.ts @@ -1,3 +1,7 @@ +import { Connection, getCollection } from "home-assistant-js-websocket"; +import { Store } from "home-assistant-js-websocket/dist/store"; +import { HomeAssistant } from "../../types"; +import { HassioAddonsInfo } from "../hassio/addon"; import { HassioHassOSInfo, HassioHostInfo } from "../hassio/host"; import { NetworkInfo } from "../hassio/network"; import { HassioResolution } from "../hassio/resolution"; @@ -7,7 +11,46 @@ import { HassioSupervisorInfo, } from "../hassio/supervisor"; +export const supervisorWSbaseCommand = { + type: "supervisor/api", + method: "GET", +}; + +export const supervisorStore = { + host: "/host/info", + supervisor: "/supervisor/info", + info: "/info", + core: "/core/info", + network: "/network/info", + resolution: "/resolution/info", + os: "/os/info", + addon: "/addons", +}; + export type SupervisorArch = "armhf" | "armv7" | "aarch64" | "i386" | "amd64"; +export type SupervisorObject = + | "host" + | "supervisor" + | "info" + | "core" + | "network" + | "resolution" + | "os" + | "addon"; + +interface supervisorApiRequest { + endpoint: string; + method?: "get" | "post" | "delete" | "put"; + force_rest?: boolean; + data?: any; +} + +export interface SupervisorEvent { + event: string; + update_key?: SupervisorObject; + data?: any; + [key: string]: any; +} export interface Supervisor { host: HassioHostInfo; @@ -17,4 +60,77 @@ export interface Supervisor { network: NetworkInfo; resolution: HassioResolution; os: HassioHassOSInfo; + addon: HassioAddonsInfo; } + +export const supervisorApiWsRequest = ( + conn: Connection, + request: supervisorApiRequest +): Promise => + conn.sendMessagePromise({ ...supervisorWSbaseCommand, ...request }); + +async function processEvent( + conn: Connection, + store: Store, + event: SupervisorEvent, + key: string +) { + if ( + !event.data || + event.data.event !== "supervisor-update" || + event.data.update_key !== key + ) { + return; + } + + if (Object.keys(event.data.data).length === 0) { + const data = await supervisorApiWsRequest(conn, { + endpoint: supervisorStore[key], + }); + store.setState(data); + return; + } + + const state = store.state; + if (state === undefined) { + return; + } + + store.setState({ + ...state, + ...event.data.data, + }); +} + +const subscribeSupervisorEventUpdates = ( + conn: Connection, + store: Store, + key: string +) => + conn.subscribeEvents( + (event) => processEvent(conn, store, event as SupervisorEvent, key), + "supervisor_event" + ); + +export const getSupervisorEventCollection = ( + conn: Connection, + key: string, + endpoint: string +) => + getCollection( + conn, + `_supervisor${key}Event`, + () => supervisorApiWsRequest(conn, { endpoint }), + (connection, store) => + subscribeSupervisorEventUpdates(connection, store, key) + ); + +export const subscribeSupervisorEvents = ( + hass: HomeAssistant, + onChange: (event) => void, + key: string, + endpoint: string +) => + getSupervisorEventCollection(hass.connection, key, endpoint).subscribe( + onChange + ); diff --git a/test-mocha/hassio/create_session.spec.ts b/test-mocha/hassio/create_session.spec.ts index 5fb5e7f59d..e308036eef 100644 --- a/test-mocha/hassio/create_session.spec.ts +++ b/test-mocha/hassio/create_session.spec.ts @@ -1,20 +1,21 @@ import * as assert from "assert"; import { createHassioSession } from "../../src/data/hassio/ingress"; -const sessionID = "fhdsu73rh3io4h8f3irhjel8ousafehf8f3yh"; - describe("Create hassio session", function () { + const hass = { + config: { version: "1.0.0" }, + callApi: async function () { + return { data: { session: "fhdsu73rh3io4h8f3irhjel8ousafehf8f3yh" } }; + }, + }; + it("Test create session without HTTPS", async function () { // @ts-ignore global.document = {}; // @ts-ignore global.location = {}; - await createHassioSession({ - // @ts-ignore - callApi: async function () { - return { data: { session: sessionID } }; - }, - }); + // @ts-ignore + await createHassioSession(hass); assert.strictEqual( // @ts-ignore global.document.cookie, @@ -26,12 +27,8 @@ describe("Create hassio session", function () { global.document = {}; // @ts-ignore global.location = { protocol: "https:" }; - await createHassioSession({ - // @ts-ignore - callApi: async function () { - return { data: { session: sessionID } }; - }, - }); + // @ts-ignore + await createHassioSession(hass); assert.strictEqual( // @ts-ignore global.document.cookie, From fc7c4af27ac0d8ccfa0d23b2405b7523adcb0f92 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 18 Feb 2021 20:35:16 +0100 Subject: [PATCH 24/44] Add more redirects (#8413) --- src/panels/my/ha-panel-my.ts | 92 +++++++++++++++++++++++++++++++----- 1 file changed, 81 insertions(+), 11 deletions(-) diff --git a/src/panels/my/ha-panel-my.ts b/src/panels/my/ha-panel-my.ts index 97f12ef6aa..474c52880e 100644 --- a/src/panels/my/ha-panel-my.ts +++ b/src/panels/my/ha-panel-my.ts @@ -17,14 +17,42 @@ import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { domainToName } from "../../data/integration"; const REDIRECTS: Redirects = { - info: { - redirect: "/config/info", + developer_states: { + redirect: "/developer-tools/state", }, - logs: { - redirect: "/config/logs", + developer_services: { + redirect: "/developer-tools/service", }, - profile: { - redirect: "/profile/dashboard", + developer_template: { + redirect: "/developer-tools/template", + }, + developer_events: { + redirect: "/developer-tools/event", + }, + cloud: { + component: "cloud", + redirect: "/config/cloud", + }, + integrations: { + redirect: "/config/integrations", + }, + config_flow_start: { + redirect: "/config/integrations/add", + params: { + domain: "string", + }, + }, + devices: { + redirect: "/config/devices/dashboard", + }, + entities: { + redirect: "/config/entities", + }, + areas: { + redirect: "/config/areas/dashboard", + }, + blueprints: { + redirect: "/config/blueprint/dashboard", }, blueprint_import: { redirect: "/config/blueprint/dashboard/import", @@ -32,11 +60,53 @@ const REDIRECTS: Redirects = { blueprint_url: "url", }, }, - config_flow_start: { - redirect: "/config/integrations/add", - params: { - domain: "string", - }, + automations: { + redirect: "/config/automation/dashboard", + }, + scenes: { + redirect: "/config/scene/dashboard", + }, + scripts: { + redirect: "/config/script/dashboard", + }, + helpers: { + redirect: "/config/helpers", + }, + tags: { + redirect: "/config/tags", + }, + lovelace_dashboards: { + redirect: "/config/lovelace/dashboards", + }, + lovelace_resources: { + redirect: "/config/lovelace/resources", + }, + people: { + redirect: "/config/person", + }, + zones: { + redirect: "/config/zone", + }, + users: { + redirect: "/config/users", + }, + general: { + redirect: "/config/core", + }, + server_controls: { + redirect: "/config/server_control", + }, + logs: { + redirect: "/config/logs", + }, + info: { + redirect: "/config/info", + }, + customize: { + redirect: "/config/customize", + }, + profile: { + redirect: "/profile/dashboard", }, }; From fe54f8eb166a846c42e4452f39648b8e87eab81d Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 19 Feb 2021 01:18:52 +0000 Subject: [PATCH 25/44] Translation update --- translations/frontend/fr.json | 4 +++- translations/frontend/ko.json | 5 +++-- translations/frontend/sk.json | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/translations/frontend/fr.json b/translations/frontend/fr.json index 612f868f5b..37b039693d 100644 --- a/translations/frontend/fr.json +++ b/translations/frontend/fr.json @@ -3409,8 +3409,10 @@ "playback_title": "Lecture de messages" }, "my": { + "component_not_loaded": "Cette redirection n'est pas prise en charge par votre instance Home Assistant. Vous avez besoin de l'intégration {integration} pour utiliser cette redirection.", "error": "Une erreur inconnue est survenue", - "faq_link": "Ma Home Assistant FAQ" + "faq_link": "Ma Home Assistant FAQ", + "not_supported": "Cette redirection n'est pas prise en charge par votre instance Home Assistant. Vérifiez le {link} pour les redirections prises en charge et la version dans laquelle elles ont été introduites." }, "page-authorize": { "abort_intro": "Connexion interrompue", diff --git a/translations/frontend/ko.json b/translations/frontend/ko.json index 37f5e0aeb8..f9806dd803 100644 --- a/translations/frontend/ko.json +++ b/translations/frontend/ko.json @@ -429,7 +429,7 @@ "media_previous_track": "이전", "sound_mode": "사운드 모드", "source": "입력 소스", - "text_to_speak": "음성합성 내용 입력 (TTS)", + "text_to_speak": "텍스트 음성 변환 입력", "turn_off": "끄기", "turn_on": "켜기" }, @@ -1474,7 +1474,7 @@ "tts": { "dialog": { "example_message": "안녕하세요, {name}님. 지원되는 모든 미디어 플레이어에서 텍스트를 재생할 수 있습니다!", - "header": "음성합성 사용해보기", + "header": "텍스트 음성 변환 사용해보기", "play": "재생", "target": "대상", "target_browser": "브라우저" @@ -3199,6 +3199,7 @@ "playback_title": "메시지 재생" }, "my": { + "component_not_loaded": "이 리디렉션은 Home Assistant 인스턴스에서 지원되지 않습니다. 이 리디렉션을 사용하려면 {integration} 통합 구성요소가 필요합니다.", "error": "알 수 없는 오류가 발생했습니다", "faq_link": "내 Home Assistant 자주 묻는 질문", "not_supported": "이 리디렉션은 Home Assistant 인스턴스에서 지원되지 않습니다. {link} 에서 지원되는 리디렉션과 리디렉션이 도입된 버전을 확인해주세요." diff --git a/translations/frontend/sk.json b/translations/frontend/sk.json index 4232820522..25bcbf055f 100644 --- a/translations/frontend/sk.json +++ b/translations/frontend/sk.json @@ -2760,6 +2760,7 @@ "playback_title": "Prehrávanie správ" }, "my": { + "component_not_loaded": "Inštancia Home Assistant toto presmerovanie nepodporuje. Na použitie tohto presmerovania potrebujete integráciu {integration}.", "error": "Vyskytla sa neznáma chyba" }, "page-authorize": { From d51fd1e2f9c6f74245be5039aea916e7b20c43ba Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 19 Feb 2021 10:01:21 +0100 Subject: [PATCH 26/44] Add supervisor_logs and supervisor_info redirects (#8417) --- hassio/src/hassio-my-redirect.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/hassio/src/hassio-my-redirect.ts b/hassio/src/hassio-my-redirect.ts index 9391da6914..389816b435 100644 --- a/hassio/src/hassio-my-redirect.ts +++ b/hassio/src/hassio-my-redirect.ts @@ -21,7 +21,10 @@ import { navigate } from "../../src/common/navigate"; import { HomeAssistant, Route } from "../../src/types"; const REDIRECTS: Redirects = { - supervisor_system: { + supervisor_logs: { + redirect: "/hassio/system", + }, + supervisor_info: { redirect: "/hassio/system", }, supervisor_snapshots: { From 77911980cbc04bb227eab5780170efa749a67eb0 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Fri, 19 Feb 2021 11:03:25 +0100 Subject: [PATCH 27/44] Correctly handle seconds in top "delay" key (#8415) Co-authored-by: Bram Kragten --- .../types/ha-automation-action-delay.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/panels/config/automation/action/types/ha-automation-action-delay.ts b/src/panels/config/automation/action/types/ha-automation-action-delay.ts index b8e7cee0a5..2358eb24d5 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-delay.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-delay.ts @@ -22,13 +22,17 @@ export class HaDelayAction extends LitElement implements ActionElement { let data: HaFormTimeData = {}; if (typeof this.action.delay !== "object") { - const parts = this.action.delay?.toString().split(":") || []; - data = { - hours: Number(parts[0]), - minutes: Number(parts[1]), - seconds: Number(parts[2]), - milliseconds: Number(parts[3]), - }; + if (isNaN(this.action.delay)) { + const parts = this.action.delay?.toString().split(":") || []; + data = { + hours: Number(parts[0]) || 0, + minutes: Number(parts[1]) || 0, + seconds: Number(parts[2]) || 0, + milliseconds: Number(parts[3]) || 0, + }; + } else { + data = { seconds: this.action.delay }; + } } else { const { days, minutes, seconds, milliseconds } = this.action.delay; let { hours } = this.action.delay || 0; @@ -46,7 +50,8 @@ export class HaDelayAction extends LitElement implements ActionElement { .data=${data} enableMillisecond @value-changed=${this._valueChanged} - > + > + `; } From d54a129605f7da9f07c93f0e0ee7b7aeccde1a33 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 19 Feb 2021 17:46:33 +0100 Subject: [PATCH 28/44] Bump marked (#8420) --- package.json | 4 ++-- yarn.lock | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 6ce23f478d..5b1c549ea7 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "lit-element": "^2.4.0", "lit-html": "^1.3.0", "lit-virtualizer": "^0.4.2", - "marked": "^1.1.1", + "marked": "2.0.0", "mdn-polyfills": "^5.16.0", "memoize-one": "^5.0.2", "node-vibrant": "3.2.1-alpha.1", @@ -161,7 +161,7 @@ "@types/js-yaml": "^3.12.1", "@types/leaflet": "^1.4.3", "@types/leaflet-draw": "^1.0.1", - "@types/marked": "^1.1.0", + "@types/marked": "^1.2.2", "@types/memoize-one": "4.1.0", "@types/mocha": "^7.0.2", "@types/resize-observer-browser": "^0.1.3", diff --git a/yarn.lock b/yarn.lock index 271bc0443f..256a315b3c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3591,10 +3591,10 @@ dependencies: "@types/geojson" "*" -"@types/marked@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@types/marked/-/marked-1.1.0.tgz#53509b5f127e0c05c19176fcf1d743a41e00ff19" - integrity sha512-j8XXj6/l9kFvCwMyVqozznqpd/nk80krrW+QiIJN60Uu9gX5Pvn4/qPJ2YngQrR3QREPwmrE1f9/EWKVTFzoEw== +"@types/marked@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@types/marked/-/marked-1.2.2.tgz#1f858a0e690247ecf3b2eef576f98f86e8d960d4" + integrity sha512-wLfw1hnuuDYrFz97IzJja0pdVsC0oedtS4QsKH1/inyW9qkLQbXgMUqEQT0MVtUBx3twjWeInUfjQbhBVLECXw== "@types/memoize-one@4.1.0": version "4.1.0" @@ -9761,10 +9761,10 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -marked@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/marked/-/marked-1.1.1.tgz#e5d61b69842210d5df57b05856e0c91572703e6a" - integrity sha512-mJzT8D2yPxoPh7h0UXkB+dBj4FykPJ2OIfxAWeIHrvoHDkFxukV/29QxoFQoPM6RLEwhIFdJpmKBlqVM3s2ZIw== +marked@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/marked/-/marked-2.0.0.tgz#9662bbcb77ebbded0662a7be66ff929a8611cee5" + integrity sha512-NqRSh2+LlN2NInpqTQnS614Y/3NkVMFFU6sJlRFEpxJ/LHuK/qJECH7/fXZjk4VZstPW/Pevjil/VtSONsLc7Q== matchdep@^2.0.0: version "2.0.0" From d93d2b5945e0095170e6b5dc51f0c20e91efb7de Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 19 Feb 2021 17:47:51 +0100 Subject: [PATCH 29/44] Fix password field in ha-form (#8400) --- src/components/ha-form/ha-form-string.ts | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/components/ha-form/ha-form-string.ts b/src/components/ha-form/ha-form-string.ts index 5775051dce..a376520783 100644 --- a/src/components/ha-form/ha-form-string.ts +++ b/src/components/ha-form/ha-form-string.ts @@ -1,6 +1,9 @@ +import { mdiEye, mdiEyeOff } from "@mdi/js"; import "@polymer/paper-input/paper-input"; import type { PaperInputElement } from "@polymer/paper-input/paper-input"; import { + css, + CSSResult, customElement, html, internalProperty, @@ -10,12 +13,13 @@ import { TemplateResult, } from "lit-element"; import { fireEvent } from "../../common/dom/fire_event"; -import "../ha-icon-button"; +import "../ha-svg-icon"; import type { HaFormElement, HaFormStringData, HaFormStringSchema, } from "./ha-form"; +import "@material/mwc-icon-button/mwc-icon-button"; @customElement("ha-form-string") export class HaFormString extends LitElement implements HaFormElement { @@ -48,16 +52,17 @@ export class HaFormString extends LitElement implements HaFormElement { .autoValidate=${this.schema.required} @value-changed=${this._valueChanged} > - - + > + ` : html` @@ -98,6 +103,15 @@ export class HaFormString extends LitElement implements HaFormElement { } return "text"; } + + static get styles(): CSSResult { + return css` + mwc-icon-button { + --mdc-icon-button-size: 24px; + color: var(--secondary-text-color); + } + `; + } } declare global { From a41afcd714efd0f2c4034b44151439d457ef24e6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 19 Feb 2021 17:58:14 +0100 Subject: [PATCH 30/44] Update lovelace call service action (#8421) --- src/data/lovelace.ts | 3 +- src/panels/lovelace/common/handle-action.ts | 7 ++- .../lovelace/components/hui-action-editor.ts | 45 ++++++++++++------- src/panels/lovelace/editor/types.ts | 13 ++++-- 4 files changed, 48 insertions(+), 20 deletions(-) diff --git a/src/data/lovelace.ts b/src/data/lovelace.ts index 090bb0dfc0..447b803dec 100644 --- a/src/data/lovelace.ts +++ b/src/data/lovelace.ts @@ -2,6 +2,7 @@ import { Connection, getCollection, HassEventBase, + HassServiceTarget, } from "home-assistant-js-websocket"; import { HASSDomEvent } from "../common/dom/fire_event"; import { HuiErrorCard } from "../panels/lovelace/cards/hui-error-card"; @@ -120,8 +121,8 @@ export interface ToggleActionConfig extends BaseActionConfig { export interface CallServiceActionConfig extends BaseActionConfig { action: "call-service"; service: string; + target?: HassServiceTarget; service_data?: { - entity_id?: string | [string]; [key: string]: any; }; } diff --git a/src/panels/lovelace/common/handle-action.ts b/src/panels/lovelace/common/handle-action.ts index ad95273e5d..a4339e2fe4 100644 --- a/src/panels/lovelace/common/handle-action.ts +++ b/src/panels/lovelace/common/handle-action.ts @@ -130,7 +130,12 @@ export const handleAction = async ( return; } const [domain, service] = actionConfig.service.split(".", 2); - hass.callService(domain, service, actionConfig.service_data); + hass.callService( + domain, + service, + actionConfig.service_data, + actionConfig.target + ); forwardHaptic("light"); break; } diff --git a/src/panels/lovelace/components/hui-action-editor.ts b/src/panels/lovelace/components/hui-action-editor.ts index 7946138d6e..831c493735 100644 --- a/src/panels/lovelace/components/hui-action-editor.ts +++ b/src/panels/lovelace/components/hui-action-editor.ts @@ -15,15 +15,17 @@ import { } from "lit-element"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-help-tooltip"; -import "../../../components/ha-service-picker"; import { ActionConfig, CallServiceActionConfig, NavigateActionConfig, UrlActionConfig, } from "../../../data/lovelace"; +import { ServiceAction } from "../../../data/script"; import { HomeAssistant } from "../../../types"; import { EditorTarget } from "../editor/types"; +import "../../../components/ha-service-control"; +import memoizeOne from "memoize-one"; @customElement("hui-action-editor") export class HuiActionEditor extends LitElement { @@ -47,10 +49,15 @@ export class HuiActionEditor extends LitElement { return config.url_path || ""; } - get _service(): string { - const config = this.config as CallServiceActionConfig; - return config.service || ""; - } + private _serviceAction = memoizeOne( + (config: CallServiceActionConfig): ServiceAction => { + return { + service: config.service || "", + data: config.service_data, + target: config.target, + }; + } + ); protected render(): TemplateResult { if (!this.hass || !this.actions) { @@ -117,17 +124,13 @@ export class HuiActionEditor extends LitElement { : ""} ${this.config?.action === "call-service" ? html` - - - ${this.hass!.localize( - "ui.panel.lovelace.editor.action-editor.editor_service_data" - )} - + .value=${this._serviceAction(this.config)} + .showAdvanced=${this.hass.userData?.showAdvanced} + narrow + @value-changed=${this._serviceValueChanged} + > ` : ""} `; @@ -174,6 +177,18 @@ export class HuiActionEditor extends LitElement { } } + private _serviceValueChanged(ev: CustomEvent) { + ev.stopPropagation(); + fireEvent(this, "value-changed", { + value: { + ...this.config!, + service: ev.detail.value.service || "", + service_data: ev.detail.value.data || {}, + target: ev.detail.value.target || {}, + }, + }); + } + static get styles(): CSSResult { return css` .dropdown { diff --git a/src/panels/lovelace/editor/types.ts b/src/panels/lovelace/editor/types.ts index 2cd040cf37..c37c3e084d 100644 --- a/src/panels/lovelace/editor/types.ts +++ b/src/panels/lovelace/editor/types.ts @@ -120,20 +120,27 @@ const actionConfigStructConfirmation = union([ const actionConfigStructUrl = object({ action: literal("url"), - url_path: string(), + url_path: optional(string()), confirmation: optional(actionConfigStructConfirmation), }); const actionConfigStructService = object({ action: literal("call-service"), - service: string(), + service: optional(string()), service_data: optional(object()), + target: optional( + object({ + entity_id: optional(union([string(), array(string())])), + device_id: optional(union([string(), array(string())])), + area_id: optional(union([string(), array(string())])), + }) + ), confirmation: optional(actionConfigStructConfirmation), }); const actionConfigStructNavigate = object({ action: literal("navigate"), - navigation_path: string(), + navigation_path: optional(string()), confirmation: optional(actionConfigStructConfirmation), }); From 9807d0aede78f3c4a47dff8df308a2f84ea07692 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 19 Feb 2021 18:02:25 +0100 Subject: [PATCH 31/44] Move localizing to render (#8419) --- src/panels/my/ha-panel-my.ts | 68 +++++++++++++++++++++------------ src/state/translations-mixin.ts | 14 +++++-- 2 files changed, 54 insertions(+), 28 deletions(-) diff --git a/src/panels/my/ha-panel-my.ts b/src/panels/my/ha-panel-my.ts index 474c52880e..2a9887e6c3 100644 --- a/src/panels/my/ha-panel-my.ts +++ b/src/panels/my/ha-panel-my.ts @@ -127,7 +127,7 @@ class HaPanelMy extends LitElement { @property() public route!: Route; - @internalProperty() public _error = ""; + @internalProperty() public _error?: string; connectedCallback() { super.connectedCallback(); @@ -135,11 +135,7 @@ class HaPanelMy extends LitElement { if (path.startsWith("supervisor")) { if (!isComponentLoaded(this.hass, "hassio")) { - this._error = this.hass.localize( - "ui.panel.my.component_not_loaded", - "integration", - domainToName(this.hass.localize, "hassio") - ); + this._error = "no_supervisor"; return; } navigate( @@ -153,16 +149,7 @@ class HaPanelMy extends LitElement { const redirect = REDIRECTS[path]; if (!redirect) { - this._error = this.hass.localize( - "ui.panel.my.not_supported", - "link", - html`${this.hass.localize("ui.panel.my.faq_link")}` - ); + this._error = "not_supported"; return; } @@ -170,11 +157,7 @@ class HaPanelMy extends LitElement { redirect.component && !isComponentLoaded(this.hass, redirect.component) ) { - this._error = this.hass.localize( - "ui.panel.my.component_not_loaded", - "integration", - domainToName(this.hass.localize, redirect.component) - ); + this._error = "no_component"; return; } @@ -182,7 +165,7 @@ class HaPanelMy extends LitElement { try { url = this._createRedirectUrl(redirect); } catch (err) { - this._error = this.hass.localize("ui.panel.my.error"); + this._error = "url_error"; return; } @@ -191,9 +174,44 @@ class HaPanelMy extends LitElement { protected render() { if (this._error) { - return html``; + let error = "Unknown error"; + switch (this._error) { + case "not_supported": + error = + this.hass.localize( + "ui.panel.my.not_supported", + "link", + html`${this.hass.localize("ui.panel.my.faq_link")}` + ) || "This redirect is not supported."; + break; + case "no_component": + error = + this.hass.localize( + "ui.panel.my.component_not_loaded", + "integration", + domainToName( + this.hass.localize, + REDIRECTS[this.route.path.substr(1)].component! + ) + ) || "This redirect is not supported."; + break; + case "no_supervisor": + error = + this.hass.localize( + "ui.panel.my.component_not_loaded", + "integration", + "Home Assistant Supervisor" + ) || "This redirect requires Home Assistant Supervisor."; + break; + default: + error = this.hass.localize("ui.panel.my.error") || "Unknown error"; + } + return html``; } return html``; } diff --git a/src/state/translations-mixin.ts b/src/state/translations-mixin.ts index e0fb4ed00a..8be391e3a3 100644 --- a/src/state/translations-mixin.ts +++ b/src/state/translations-mixin.ts @@ -51,6 +51,17 @@ export default >(superClass: T) => this._loadCoreTranslations(getLocalLanguage()); } + protected updated(changedProps) { + super.updated(changedProps); + if (!changedProps.has("hass")) { + return; + } + const oldHass = changedProps.get("hass"); + if (this.hass?.panels && oldHass.panels !== this.hass.panels) { + this._loadFragmentTranslations(this.hass.language, this.hass.panelUrl); + } + } + protected hassConnected() { super.hassConnected(); getUserLanguage(this.hass!).then((language) => { @@ -204,13 +215,10 @@ export default >(superClass: T) => const panelComponent = this.hass?.panels?.[panelUrl]?.component_name; // If it's the first call we don't have panel info yet to check the component. - // If the url is not known it might be a custom lovelace dashboard, so we load lovelace translations const fragment = translationMetadata.fragments.includes( panelComponent || panelUrl ) ? panelComponent || panelUrl - : !panelComponent - ? "lovelace" : undefined; if (!fragment) { From 2052a5351c480092b9a50d0adfc6705eb87d40f7 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 19 Feb 2021 18:03:31 +0100 Subject: [PATCH 32/44] Ha-form: Don't change data (#8277) --- src/components/ha-form/ha-form.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/ha-form/ha-form.ts b/src/components/ha-form/ha-form.ts index 25cf8cd18d..adfe4fd71f 100644 --- a/src/components/ha-form/ha-form.ts +++ b/src/components/ha-form/ha-form.ts @@ -202,9 +202,8 @@ export class HaForm extends LitElement implements HaFormElement { ev.stopPropagation(); const schema = (ev.target as HaFormElement).schema as HaFormSchema; const data = this.data as HaFormDataContainer; - data[schema.name] = ev.detail.value; fireEvent(this, "value-changed", { - value: { ...data }, + value: { ...data, [schema.name]: ev.detail.value }, }); } From f251d4267f18e3addd28a1d8b5474c3fb13354a1 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 19 Feb 2021 18:06:36 +0100 Subject: [PATCH 33/44] Revert "Allow viewport scaling (zooming) of frontend" (#8353) This reverts commit da9faccada662171593e91334ba4fcaf63aa7d8f. --- src/html/_style_base.html.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/html/_style_base.html.template b/src/html/_style_base.html.template index 0777efeb57..21d047c1b8 100644 --- a/src/html/_style_base.html.template +++ b/src/html/_style_base.html.template @@ -1,4 +1,4 @@ - + - -

- - - -

- - - - - -
- - - -
-
- `; - } - - static get properties() { - return { - _hass: Object, - _dialogClosedCallback: Function, - _instance: Number, - - _loading: { - type: Boolean, - value: false, - }, - - // Error message when can't talk to server etc - _errorMsg: String, - - _opened: { - type: Boolean, - value: false, - }, - - _step: { - type: Object, - value: null, - }, - - /* - * Store user entered data. - */ - _stepData: Object, - }; - } - - ready() { - super.ready(); - this.hass.loadBackendTranslation("mfa_setup", "auth"); - this.addEventListener("keypress", (ev) => { - if (ev.keyCode === 13) { - this._submitStep(); - } - }); - } - - showDialog({ hass, continueFlowId, mfaModuleId, dialogClosedCallback }) { - this.hass = hass; - this._instance = instance++; - this._dialogClosedCallback = dialogClosedCallback; - this._createdFromHandler = !!mfaModuleId; - this._loading = true; - this._opened = true; - - const fetchStep = continueFlowId - ? this.hass.callWS({ - type: "auth/setup_mfa", - flow_id: continueFlowId, - }) - : this.hass.callWS({ - type: "auth/setup_mfa", - mfa_module_id: mfaModuleId, - }); - - const curInstance = this._instance; - - fetchStep.then((step) => { - if (curInstance !== this._instance) return; - - this._processStep(step); - this._loading = false; - // When the flow changes, center the dialog. - // Don't do it on each step or else the dialog keeps bouncing. - setTimeout(() => this.$.dialog.center(), 0); - }); - } - - _submitStep() { - this._loading = true; - this._errorMsg = null; - - const curInstance = this._instance; - - this.hass - .callWS({ - type: "auth/setup_mfa", - flow_id: this._step.flow_id, - user_input: this._stepData, - }) - .then( - (step) => { - if (curInstance !== this._instance) return; - - this._processStep(step); - this._loading = false; - }, - (err) => { - this._errorMsg = - (err && err.body && err.body.message) || "Unknown error occurred"; - this._loading = false; - } - ); - } - - _processStep(step) { - if (!step.errors) step.errors = {}; - this._step = step; - // We got a new form if there are no errors. - if (Object.keys(step.errors).length === 0) { - this._stepData = {}; - } - } - - _flowDone() { - this._opened = false; - const flowFinished = - this._step && ["create_entry", "abort"].includes(this._step.type); - - if (this._step && !flowFinished && this._createdFromHandler) { - // console.log('flow not finish'); - } - - this._dialogClosedCallback({ - flowFinished, - }); - - this._errorMsg = null; - this._step = null; - this._stepData = {}; - this._dialogClosedCallback = null; - } - - _equals(a, b) { - return a === b; - } - - _openedChanged(ev) { - // Closed dialog by clicking on the overlay - if (this._step && !ev.detail.value) { - this._flowDone(); - } - } - - _computeStepAbortedReason(localize, step) { - return localize( - `component.auth.mfa_setup.${step.handler}.abort.${step.reason}` - ); - } - - _computeStepTitle(localize, step) { - return ( - localize( - `component.auth.mfa_setup.${step.handler}.step.${step.step_id}.title` - ) || "Setup Multi-factor Authentication" - ); - } - - _computeStepDescription(localize, step) { - const args = [ - `component.auth.mfa_setup.${step.handler}.step.${step.step_id}.description`, - ]; - const placeholders = step.description_placeholders || {}; - Object.keys(placeholders).forEach((key) => { - args.push(key); - args.push(placeholders[key]); - }); - return localize(...args); - } - - _computeLabelCallback(localize, step) { - // Returns a callback for ha-form to calculate labels per schema object - return (schema) => - localize( - `component.auth.mfa_setup.${step.handler}.step.${step.step_id}.data.${schema.name}` - ) || schema.name; - } - - _computeErrorCallback(localize, step) { - // Returns a callback for ha-form to calculate error messages - return (error) => - localize(`component.auth.mfa_setup.${step.handler}.error.${error}`) || - error; - } -} - -customElements.define("ha-mfa-module-setup-flow", HaMfaModuleSetupFlow); diff --git a/src/panels/profile/ha-mfa-modules-card.js b/src/panels/profile/ha-mfa-modules-card.js deleted file mode 100644 index a118d6e1cc..0000000000 --- a/src/panels/profile/ha-mfa-modules-card.js +++ /dev/null @@ -1,130 +0,0 @@ -import "@material/mwc-button"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-item/paper-item-body"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "../../components/ha-card"; -import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box"; -import { EventsMixin } from "../../mixins/events-mixin"; -import LocalizeMixin from "../../mixins/localize-mixin"; -import "../../styles/polymer-ha-style"; - -let registeredDialog = false; - -/* - * @appliesMixin EventsMixin - * @appliesMixin LocalizeMixin - */ -class HaMfaModulesCard extends EventsMixin(LocalizeMixin(PolymerElement)) { - static get template() { - return html` - - - - - `; - } - - static get properties() { - return { - hass: Object, - - _loading: { - type: Boolean, - value: false, - }, - - // Error message when can't talk to server etc - _statusMsg: String, - _errorMsg: String, - - mfaModules: Array, - }; - } - - connectedCallback() { - super.connectedCallback(); - - if (!registeredDialog) { - registeredDialog = true; - this.fire("register-dialog", { - dialogShowEvent: "show-mfa-module-setup-flow", - dialogTag: "ha-mfa-module-setup-flow", - dialogImport: () => import("./ha-mfa-module-setup-flow"), - }); - } - } - - _enable(ev) { - this.fire("show-mfa-module-setup-flow", { - hass: this.hass, - mfaModuleId: ev.model.module.id, - dialogClosedCallback: () => this._refreshCurrentUser(), - }); - } - - async _disable(ev) { - const mfamodule = ev.model.module; - if ( - !(await showConfirmationDialog(this, { - text: this.localize( - "ui.panel.profile.mfa.confirm_disable", - "name", - mfamodule.name - ), - })) - ) { - return; - } - - const mfaModuleId = mfamodule.id; - - this.hass - .callWS({ - type: "auth/depose_mfa", - mfa_module_id: mfaModuleId, - }) - .then(() => { - this._refreshCurrentUser(); - }); - } - - _refreshCurrentUser() { - this.fire("hass-refresh-current-user"); - } -} - -customElements.define("ha-mfa-modules-card", HaMfaModulesCard); diff --git a/src/panels/profile/ha-mfa-modules-card.ts b/src/panels/profile/ha-mfa-modules-card.ts new file mode 100644 index 0000000000..f5680cb127 --- /dev/null +++ b/src/panels/profile/ha-mfa-modules-card.ts @@ -0,0 +1,101 @@ +import "@material/mwc-button"; +import "@polymer/paper-item/paper-item"; +import "@polymer/paper-item/paper-item-body"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import { fireEvent } from "../../common/dom/fire_event"; +import "../../components/ha-card"; +import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box"; +import { HomeAssistant, MFAModule } from "../../types"; +import { showMfaModuleSetupFlowDialog } from "./show-ha-mfa-module-setup-flow-dialog"; + +@customElement("ha-mfa-modules-card") +class HaMfaModulesCard extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public mfaModules!: MFAModule[]; + + protected render(): TemplateResult { + return html` + + ${this.mfaModules.map( + (module) => html` + +
${module.name}
+
${module.id}
+
+ ${module.enabled + ? html`${this.hass.localize( + "ui.panel.profile.mfa.disable" + )}` + : html`${this.hass.localize( + "ui.panel.profile.mfa.enable" + )}`} +
` + )} +
+ `; + } + + static get styles(): CSSResult { + return css` + mwc-button { + margin-right: -0.57em; + } + `; + } + + private _enable(ev) { + showMfaModuleSetupFlowDialog(this, { + mfaModuleId: ev.currentTarget.module.id, + dialogClosedCallback: () => this._refreshCurrentUser(), + }); + } + + private async _disable(ev) { + const mfamodule = ev.currentTarget.module; + if ( + !(await showConfirmationDialog(this, { + text: this.hass.localize( + "ui.panel.profile.mfa.confirm_disable", + "name", + mfamodule.name + ), + })) + ) { + return; + } + + const mfaModuleId = mfamodule.id; + + this.hass + .callWS({ + type: "auth/depose_mfa", + mfa_module_id: mfaModuleId, + }) + .then(() => { + this._refreshCurrentUser(); + }); + } + + private _refreshCurrentUser() { + fireEvent(this, "hass-refresh-current-user"); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-mfa-modules-card": HaMfaModulesCard; + } +} diff --git a/src/panels/profile/show-ha-mfa-module-setup-flow-dialog.ts b/src/panels/profile/show-ha-mfa-module-setup-flow-dialog.ts new file mode 100644 index 0000000000..6c5e6495cb --- /dev/null +++ b/src/panels/profile/show-ha-mfa-module-setup-flow-dialog.ts @@ -0,0 +1,21 @@ +import { fireEvent } from "../../common/dom/fire_event"; + +export interface MfaModuleSetupFlowDialogParams { + continueFlowId?: string; + mfaModuleId?: string; + dialogClosedCallback: (params: { flowFinished: boolean }) => void; +} + +export const loadMfaModuleSetupFlowDialog = () => + import("./dialog-ha-mfa-module-setup-flow"); + +export const showMfaModuleSetupFlowDialog = ( + element: HTMLElement, + dialogParams: MfaModuleSetupFlowDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "ha-mfa-module-setup-flow", + dialogImport: loadMfaModuleSetupFlowDialog, + dialogParams, + }); +}; From 6092af8de6f577d42b52aefb4cadaebd1fab1143 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 22 Feb 2021 19:53:52 +0100 Subject: [PATCH 42/44] Re-do developer tools service (#8410) --- package.json | 2 +- src/components/device/ha-device-picker.ts | 7 +- src/components/ha-area-picker.ts | 4 + src/components/ha-combo-box.ts | 8 +- .../ha-selector/ha-selector-action.ts | 7 + .../ha-selector/ha-selector-area.ts | 3 + .../ha-selector/ha-selector-boolean.ts | 3 + .../ha-selector/ha-selector-device.ts | 11 +- .../ha-selector/ha-selector-entity.ts | 9 +- .../ha-selector/ha-selector-number.ts | 7 + .../ha-selector/ha-selector-object.ts | 6 + .../ha-selector/ha-selector-select.ts | 9 +- .../ha-selector/ha-selector-target.ts | 3 + .../ha-selector/ha-selector-text.ts | 12 +- .../ha-selector/ha-selector-time.ts | 3 + src/components/ha-selector/ha-selector.ts | 6 + src/components/ha-service-control.ts | 159 +++++++- src/components/ha-service-picker.ts | 84 ++-- src/components/ha-settings-row.ts | 28 +- src/components/ha-target-picker.ts | 10 +- src/components/ha-yaml-editor.ts | 14 +- .../types/ha-automation-action-service.ts | 12 + .../service/developer-tools-service.js | 371 ------------------ .../service/developer-tools-service.ts | 350 +++++++++++++++++ .../lovelace/components/hui-action-editor.ts | 3 + .../config-elements/config-elements-style.ts | 6 +- src/translations/en.json | 22 +- yarn.lock | 8 +- 28 files changed, 693 insertions(+), 474 deletions(-) delete mode 100644 src/panels/developer-tools/service/developer-tools-service.js create mode 100644 src/panels/developer-tools/service/developer-tools-service.ts diff --git a/package.json b/package.json index 41c0c61752..0956efd2c2 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "fuse.js": "^6.0.0", "google-timezones-json": "^1.0.2", "hls.js": "^0.13.2", - "home-assistant-js-websocket": "^5.8.1", + "home-assistant-js-websocket": "^5.9.0", "idb-keyval": "^3.2.0", "intl-messageformat": "^8.3.9", "js-yaml": "^3.13.1", diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts index 9ef2319aa0..201efb3164 100644 --- a/src/components/device/ha-device-picker.ts +++ b/src/components/device/ha-device-picker.ts @@ -6,6 +6,7 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, PropertyValues, @@ -107,8 +108,9 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { @property() public deviceFilter?: HaDevicePickerDeviceFilterFunc; - @property({ type: Boolean }) - private _opened?: boolean; + @property({ type: Boolean }) public disabled?: boolean; + + @internalProperty() private _opened?: boolean; @query("ha-combo-box", true) private _comboBox!: HaComboBox; @@ -290,6 +292,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { : this.label} .value=${this._value} .renderer=${rowRenderer} + .disabled=${this.disabled} item-value-path="id" item-id-path="id" item-label-path="name" diff --git a/src/components/ha-area-picker.ts b/src/components/ha-area-picker.ts index c6b263fd54..793a426e8f 100644 --- a/src/components/ha-area-picker.ts +++ b/src/components/ha-area-picker.ts @@ -117,6 +117,8 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { @property() public entityFilter?: (entity: EntityRegistryEntry) => boolean; + @property({ type: Boolean }) public disabled?: boolean; + @internalProperty() private _areas?: AreaRegistryEntry[]; @internalProperty() private _devices?: DeviceRegistryEntry[]; @@ -339,6 +341,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { item-label-path="name" .value=${this._value} .renderer=${rowRenderer} + .disabled=${this.disabled} @opened-changed=${this._openedChanged} @value-changed=${this._areaChanged} > @@ -349,6 +352,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { .placeholder=${this.placeholder ? this._area(this.placeholder)?.name : undefined} + .disabled=${this.disabled} class="input" autocapitalize="none" autocomplete="off" diff --git a/src/components/ha-combo-box.ts b/src/components/ha-combo-box.ts index ca40f37be9..6f6acd3346 100644 --- a/src/components/ha-combo-box.ts +++ b/src/components/ha-combo-box.ts @@ -10,6 +10,7 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, query, @@ -67,8 +68,9 @@ export class HaComboBox extends LitElement { model: { item: any } ) => void; - @property({ type: Boolean }) - private _opened?: boolean; + @property({ type: Boolean }) public disabled?: boolean; + + @internalProperty() private _opened?: boolean; @query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement; @@ -95,12 +97,14 @@ export class HaComboBox extends LitElement { .filteredItems=${this.filteredItems} .renderer=${this.renderer || defaultRowRenderer} .allowCustomValue=${this.allowCustomValue} + .disabled=${this.disabled} @opened-changed=${this._openedChanged} @filter-changed=${this._filterChanged} @value-changed=${this._valueChanged} > `; @@ -34,6 +37,10 @@ export class HaActionSelector extends LitElement { display: block; margin-bottom: 16px; } + :host([disabled]) ha-automation-action { + opacity: var(--light-disabled-opacity); + pointer-events: none; + } `; } } diff --git a/src/components/ha-selector/ha-selector-area.ts b/src/components/ha-selector/ha-selector-area.ts index 8023dc4844..c3443291d6 100644 --- a/src/components/ha-selector/ha-selector-area.ts +++ b/src/components/ha-selector/ha-selector-area.ts @@ -24,6 +24,8 @@ export class HaAreaSelector extends LitElement { @internalProperty() public _configEntries?: ConfigEntry[]; + @property({ type: Boolean }) public disabled = false; + protected updated(changedProperties) { if (changedProperties.has("selector")) { const oldSelector = changedProperties.get("selector"); @@ -50,6 +52,7 @@ export class HaAreaSelector extends LitElement { .includeDomains=${this.selector.area.entity?.domain ? [this.selector.area.entity.domain] : undefined} + .disabled=${this.disabled} >`; } diff --git a/src/components/ha-selector/ha-selector-boolean.ts b/src/components/ha-selector/ha-selector-boolean.ts index 8339763f81..0f56180fe5 100644 --- a/src/components/ha-selector/ha-selector-boolean.ts +++ b/src/components/ha-selector/ha-selector-boolean.ts @@ -19,11 +19,14 @@ export class HaBooleanSelector extends LitElement { @property() public label?: string; + @property({ type: Boolean }) public disabled = false; + protected render() { return html` `; } diff --git a/src/components/ha-selector/ha-selector-device.ts b/src/components/ha-selector/ha-selector-device.ts index d9ec80655c..9446c16a51 100644 --- a/src/components/ha-selector/ha-selector-device.ts +++ b/src/components/ha-selector/ha-selector-device.ts @@ -23,10 +23,12 @@ export class HaDeviceSelector extends LitElement { @internalProperty() public _configEntries?: ConfigEntry[]; + @property({ type: Boolean }) public disabled = false; + protected updated(changedProperties) { if (changedProperties.has("selector")) { const oldSelector = changedProperties.get("selector"); - if (oldSelector !== this.selector && this.selector.device.integration) { + if (oldSelector !== this.selector && this.selector.device?.integration) { this._loadConfigEntries(); } } @@ -44,24 +46,25 @@ export class HaDeviceSelector extends LitElement { .includeDomains=${this.selector.device.entity?.domain ? [this.selector.device.entity.domain] : undefined} + .disabled=${this.disabled} allow-custom-entity >`; } private _filterDevices(device: DeviceRegistryEntry): boolean { if ( - this.selector.device.manufacturer && + this.selector.device?.manufacturer && device.manufacturer !== this.selector.device.manufacturer ) { return false; } if ( - this.selector.device.model && + this.selector.device?.model && device.model !== this.selector.device.model ) { return false; } - if (this.selector.device.integration) { + if (this.selector.device?.integration) { if ( this._configEntries && !this._configEntries.some((entry) => diff --git a/src/components/ha-selector/ha-selector-entity.ts b/src/components/ha-selector/ha-selector-entity.ts index 78c7003e1f..21977aa46c 100644 --- a/src/components/ha-selector/ha-selector-entity.ts +++ b/src/components/ha-selector/ha-selector-entity.ts @@ -25,12 +25,15 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) { @property() public label?: string; + @property({ type: Boolean }) public disabled = false; + protected render() { return html` this._filterEntities(entity)} + .disabled=${this.disabled} allow-custom-entity >`; } @@ -51,12 +54,12 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) { } private _filterEntities(entity: HassEntity): boolean { - if (this.selector.entity.domain) { + if (this.selector.entity?.domain) { if (computeStateDomain(entity) !== this.selector.entity.domain) { return false; } } - if (this.selector.entity.device_class) { + if (this.selector.entity?.device_class) { if ( !entity.attributes.device_class || entity.attributes.device_class !== this.selector.entity.device_class @@ -64,7 +67,7 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) { return false; } } - if (this.selector.entity.integration) { + if (this.selector.entity?.integration) { if ( !this._entityPlaformLookup || this._entityPlaformLookup[entity.entity_id] !== diff --git a/src/components/ha-selector/ha-selector-number.ts b/src/components/ha-selector/ha-selector-number.ts index 5daacda2d9..6360ed1568 100644 --- a/src/components/ha-selector/ha-selector-number.ts +++ b/src/components/ha-selector/ha-selector-number.ts @@ -21,8 +21,12 @@ export class HaNumberSelector extends LitElement { @property() public value?: number; + @property() public placeholder?: number; + @property() public label?: string; + @property({ type: Boolean }) public disabled = false; + protected render() { return html`${this.label} ${this.selector.number.mode === "slider" @@ -31,6 +35,7 @@ export class HaNumberSelector extends LitElement { .max=${this.selector.number.max} .value=${this._value} .step=${this.selector.number.step} + .disabled=${this.disabled} pin ignore-bar-touch @change=${this._handleSliderChange} @@ -42,12 +47,14 @@ export class HaNumberSelector extends LitElement { .label=${this.selector.number.mode === "slider" ? undefined : this.label} + .placeholder=${this.placeholder} .noLabelFloat=${this.selector.number.mode === "slider"} class=${classMap({ single: this.selector.number.mode === "box" })} .min=${this.selector.number.min} .max=${this.selector.number.max} .value=${this.value} .step=${this.selector.number.step} + .disabled=${this.disabled} type="number" auto-validate @value-changed=${this._handleInputChange} diff --git a/src/components/ha-selector/ha-selector-object.ts b/src/components/ha-selector/ha-selector-object.ts index 29159e3e8f..208bbaa6d4 100644 --- a/src/components/ha-selector/ha-selector-object.ts +++ b/src/components/ha-selector/ha-selector-object.ts @@ -11,8 +11,14 @@ export class HaObjectSelector extends LitElement { @property() public label?: string; + @property() public placeholder?: string; + + @property({ type: Boolean }) public disabled = false; + protected render() { return html``; diff --git a/src/components/ha-selector/ha-selector-select.ts b/src/components/ha-selector/ha-selector-select.ts index 0c89ec8bfd..448138234d 100644 --- a/src/components/ha-selector/ha-selector-select.ts +++ b/src/components/ha-selector/ha-selector-select.ts @@ -21,8 +21,13 @@ export class HaSelectSelector extends LitElement { @property() public label?: string; + @property({ type: Boolean }) public disabled = false; + protected render() { - return html` + return html` { @@ -84,6 +86,7 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) { .includeDomains=${this.selector.target.entity?.domain ? [this.selector.target.entity.domain] : undefined} + .disabled=${this.disabled} >`; } diff --git a/src/components/ha-selector/ha-selector-text.ts b/src/components/ha-selector/ha-selector-text.ts index 32fa638ff0..9d2fbbd248 100644 --- a/src/components/ha-selector/ha-selector-text.ts +++ b/src/components/ha-selector/ha-selector-text.ts @@ -13,14 +13,20 @@ export class HaTextSelector extends LitElement { @property() public label?: string; + @property() public placeholder?: string; + @property() public selector!: StringSelector; + @property({ type: Boolean }) public disabled = false; + protected render() { if (this.selector.text?.multiline) { return html``; diff --git a/src/components/ha-selector/ha-selector-time.ts b/src/components/ha-selector/ha-selector-time.ts index 688b23dad3..f573773868 100644 --- a/src/components/ha-selector/ha-selector-time.ts +++ b/src/components/ha-selector/ha-selector-time.ts @@ -17,6 +17,8 @@ export class HaTimeSelector extends LitElement { @property() public label?: string; + @property({ type: Boolean }) public disabled = false; + protected render() { const parts = this.value?.split(":") || []; const hours = useAMPM ? parts[0] ?? "12" : parts[0] ?? "0"; @@ -29,6 +31,7 @@ export class HaTimeSelector extends LitElement { .sec=${parts[2] ?? "00"} .format=${useAMPM ? 12 : 24} .amPm=${useAMPM && (Number(hours) > 12 ? "PM" : "AM")} + .disabled=${this.disabled} @change=${this._timeChanged} @am-pm-changed=${this._timeChanged} hide-label diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index 410d46f883..db071febdc 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -24,6 +24,10 @@ export class HaSelector extends LitElement { @property() public label?: string; + @property() public placeholder?: any; + + @property({ type: Boolean }) public disabled = false; + public focus() { const input = this.shadowRoot!.getElementById("selector"); if (!input) { @@ -43,6 +47,8 @@ export class HaSelector extends LitElement { selector: this.selector, value: this.value, label: this.label, + placeholder: this.placeholder, + disabled: this.disabled, id: "selector", })} `; diff --git a/src/components/ha-service-control.ts b/src/components/ha-service-control.ts index 7a874164b8..3997bfcf52 100644 --- a/src/components/ha-service-control.ts +++ b/src/components/ha-service-control.ts @@ -22,6 +22,7 @@ import "./ha-selector/ha-selector"; import "./ha-service-picker"; import "./ha-settings-row"; import "./ha-yaml-editor"; +import "./ha-checkbox"; import type { HaYamlEditor } from "./ha-yaml-editor"; interface ExtHassService extends Omit { @@ -30,6 +31,7 @@ interface ExtHassService extends Omit { name?: string; description: string; required?: boolean; + advanced?: boolean; default?: any; example?: any; selector?: Selector; @@ -48,14 +50,26 @@ export class HaServiceControl extends LitElement { @property({ reflect: true, type: Boolean }) public narrow!: boolean; + @property({ type: Boolean }) public showAdvanced?: boolean; + @internalProperty() private _serviceData?: ExtHassService; + @internalProperty() private _checkedKeys = new Set(); + @query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor; protected updated(changedProperties: PropertyValues) { if (!changedProperties.has("value")) { return; } + const oldValue = changedProperties.get("value") as + | undefined + | this["value"]; + + if (oldValue?.service !== this.value?.service) { + this._checkedKeys = new Set(); + } + this._serviceData = this.value?.service ? this._getServiceInfo(this.value.service) : undefined; @@ -63,13 +77,33 @@ export class HaServiceControl extends LitElement { if ( this._serviceData && "target" in this._serviceData && - this.value?.data?.entity_id + (this.value?.data?.entity_id || + this.value?.data?.area_id || + this.value?.data?.device_id) ) { + const target = { + ...this.value.target, + }; + + if (this.value.data.entity_id && !this.value.target?.entity_id) { + target.entity_id = this.value.data.entity_id; + } + if (this.value.data.area_id && !this.value.target?.area_id) { + target.area_id = this.value.data.area_id; + } + if (this.value.data.device_id && !this.value.target?.device_id) { + target.device_id = this.value.data.device_id; + } + this.value = { ...this.value, - target: { ...this.value.target, entity_id: this.value.data.entity_id }, + target, + data: { ...this.value.data }, }; + delete this.value.data!.entity_id; + delete this.value.data!.device_id; + delete this.value.data!.area_id; } if (this.value?.data) { @@ -125,24 +159,46 @@ export class HaServiceControl extends LitElement { legacy && this._serviceData?.fields.find((field) => field.key === "entity_id"); + const hasOptional = Boolean( + !legacy && + this._serviceData?.fields.some( + (field) => field.selector && !field.required + ) + ); + return html` +

${this._serviceData?.description}

${this._serviceData && "target" in this._serviceData - ? html`` + ? html` + ${hasOptional + ? html`
` + : ""} + ${this.hass.localize( + "ui.components.service-control.target" + )} + ${this.hass.localize( + "ui.components.service-control.target_description" + )}
` : entityId ? html`` : this._serviceData?.fields.map((dataField) => - dataField.selector + dataField.selector && (!dataField.advanced || this.showAdvanced) ? html` + ${dataField.required + ? hasOptional + ? html`
` + : "" + : html``} ${dataField.name || dataField.key} ${dataField?.description}
` : "" )} `; } + private _checkboxChanged(ev) { + const checked = ev.currentTarget.checked; + const key = ev.currentTarget.key; + if (checked) { + this._checkedKeys.add(key); + } else { + this._checkedKeys.delete(key); + const data = { ...this.value?.data }; + + delete data[key]; + + fireEvent(this, "value-changed", { + value: { + ...this.value, + data, + }, + }); + } + this.requestUpdate("_checkedKeys"); + } + private _serviceChanged(ev: PolymerChangedEvent) { ev.stopPropagation(); if (ev.detail.value === this.value?.service) { return; } fireEvent(this, "value-changed", { - value: { service: ev.detail.value || "", data: {} }, + value: { service: ev.detail.value || "" }, }); } @@ -268,10 +362,27 @@ export class HaServiceControl extends LitElement { static get styles(): CSSResult { return css` ha-settings-row { - padding: 0; + padding: var(--service-control-padding, 0 16px); } ha-settings-row { --paper-time-input-justify-content: flex-end; + border-top: var( + --service-control-items-border-top, + 1px solid var(--divider-color) + ); + } + ha-service-picker, + ha-entity-picker, + ha-yaml-editor { + display: block; + margin: var(--service-control-padding, 0 16px); + } + ha-yaml-editor { + padding: 16px 0; + } + p { + margin: var(--service-control-padding, 0 16px); + padding: 16px 0; } :host(:not([narrow])) ha-settings-row paper-input { width: 60%; @@ -279,6 +390,12 @@ export class HaServiceControl extends LitElement { :host(:not([narrow])) ha-settings-row ha-selector { width: 60%; } + .checkbox-spacer { + width: 32px; + } + ha-checkbox { + margin-left: -16px; + } `; } } diff --git a/src/components/ha-service-picker.ts b/src/components/ha-service-picker.ts index 03379bb218..17bff6027f 100644 --- a/src/components/ha-service-picker.ts +++ b/src/components/ha-service-picker.ts @@ -1,13 +1,15 @@ import { html, internalProperty, LitElement, property } from "lit-element"; import memoizeOne from "memoize-one"; import { fireEvent } from "../common/dom/fire_event"; +import { LocalizeFunc } from "../common/translations/localize"; +import { domainToName } from "../data/integration"; import { HomeAssistant } from "../types"; import "./ha-combo-box"; const rowRenderer = ( root: HTMLElement, _owner, - model: { item: { service: string; description: string } } + model: { item: { service: string; name: string } } ) => { if (!root.firstElementChild) { root.innerHTML = ` @@ -19,15 +21,16 @@ const rowRenderer = ( -
[[item.description]]
+
[[item.name]]
[[item.service]]
`; } - root.querySelector(".name")!.textContent = model.item.description; - root.querySelector("[secondary]")!.textContent = model.item.service; + root.querySelector(".name")!.textContent = model.item.name; + root.querySelector("[secondary]")!.textContent = + model.item.name === model.item.service ? "" : model.item.service; }; class HaServicePicker extends LitElement { @@ -43,13 +46,14 @@ class HaServicePicker extends LitElement { .hass=${this.hass} .label=${this.hass.localize("ui.components.service-picker.service")} .filteredItems=${this._filteredServices( + this.hass.localize, this.hass.services, this._filter )} .value=${this.value} .renderer=${rowRenderer} item-value-path="service" - item-label-path="description" + item-label-path="name" allow-custom-value @filter-changed=${this._filterChanged} @value-changed=${this._valueChanged} @@ -57,38 +61,48 @@ class HaServicePicker extends LitElement { `; } - private _services = memoizeOne((services: HomeAssistant["services"]): { - service: string; - description: string; - }[] => { - if (!services) { - return []; - } - const result: { service: string; description: string }[] = []; - - Object.keys(services) - .sort() - .forEach((domain) => { - const services_keys = Object.keys(services[domain]).sort(); - - for (const service of services_keys) { - result.push({ - service: `${domain}.${service}`, - description: - services[domain][service].description || `${domain}.${service}`, - }); - } - }); - - return result; - }); - - private _filteredServices = memoizeOne( - (services: HomeAssistant["services"], filter?: string) => { + private _services = memoizeOne( + ( + localize: LocalizeFunc, + services: HomeAssistant["services"] + ): { + service: string; + name: string; + }[] => { if (!services) { return []; } - const processedServices = this._services(services); + const result: { service: string; name: string }[] = []; + + Object.keys(services) + .sort() + .forEach((domain) => { + const services_keys = Object.keys(services[domain]).sort(); + + for (const service of services_keys) { + result.push({ + service: `${domain}.${service}`, + name: `${domainToName(localize, domain)}: ${ + services[domain][service].name || service + }`, + }); + } + }); + + return result; + } + ); + + private _filteredServices = memoizeOne( + ( + localize: LocalizeFunc, + services: HomeAssistant["services"], + filter?: string + ) => { + if (!services) { + return []; + } + const processedServices = this._services(localize, services); if (!filter) { return processedServices; @@ -96,7 +110,7 @@ class HaServicePicker extends LitElement { return processedServices.filter( (service) => service.service.toLowerCase().includes(filter) || - service.description.toLowerCase().includes(filter) + service.name?.toLowerCase().includes(filter) ); } ); diff --git a/src/components/ha-settings-row.ts b/src/components/ha-settings-row.ts index 8500813910..12f37bfe79 100644 --- a/src/components/ha-settings-row.ts +++ b/src/components/ha-settings-row.ts @@ -6,7 +6,7 @@ import { html, LitElement, property, - SVGTemplateResult, + TemplateResult, } from "lit-element"; @customElement("ha-settings-row") @@ -16,15 +16,18 @@ export class HaSettingsRow extends LitElement { @property({ type: Boolean, attribute: "three-line" }) public threeLine = false; - protected render(): SVGTemplateResult { + protected render(): TemplateResult { return html` - - -
-
+
+ + + +
+
+
`; } @@ -59,6 +62,13 @@ export class HaSettingsRow extends LitElement { div[secondary] { white-space: normal; } + .prefix-wrap { + display: contents; + } + :host([narrow]) .prefix-wrap { + display: flex; + align-items: center; + } `; } } diff --git a/src/components/ha-target-picker.ts b/src/components/ha-target-picker.ts index 7c3aec553d..574c1b36f2 100644 --- a/src/components/ha-target-picker.ts +++ b/src/components/ha-target-picker.ts @@ -84,6 +84,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { @property() public entityFilter?: HaEntityPickerEntityFilterFunc; + @property({ type: Boolean, reflect: true }) public disabled = false; + @internalProperty() private _areas?: { [areaId: string]: AreaRegistryEntry }; @internalProperty() private _devices?: { @@ -438,7 +440,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { type: string, id: string ): this["value"] { - const newVal = ensureArray(value![type])!.filter((val) => val !== id); + const newVal = ensureArray(value![type])!.filter( + (val) => String(val) !== id + ); if (newVal.length) { return { ...value, @@ -599,6 +603,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { paper-tooltip.expand { min-width: 200px; } + :host([disabled]) .mdc-chip { + opacity: var(--light-disabled-opacity); + pointer-events: none; + } `; } } diff --git a/src/components/ha-yaml-editor.ts b/src/components/ha-yaml-editor.ts index 473a7dd41a..2c300f4433 100644 --- a/src/components/ha-yaml-editor.ts +++ b/src/components/ha-yaml-editor.ts @@ -44,14 +44,14 @@ export class HaYamlEditor extends LitElement { @internalProperty() private _yaml = ""; - @query("ha-code-editor", true) private _editor?: HaCodeEditor; + @query("ha-code-editor") private _editor?: HaCodeEditor; public setValue(value): void { try { this._yaml = value && !isEmpty(value) ? safeDump(value) : ""; } catch (err) { // eslint-disable-next-line no-console - console.error(err); + console.error(err, value); alert(`There was an error converting to YAML: ${err}`); } afterNextRender(() => { @@ -73,7 +73,7 @@ export class HaYamlEditor extends LitElement { return html``; } return html` - ${this.label ? html`

${this.label}

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

${this.label}

` : ""} `; @@ -72,6 +75,15 @@ export class HaServiceAction extends LitElement implements ActionElement { ev.stopPropagation(); } } + + static get styles(): CSSResult { + return css` + ha-service-control { + display: block; + margin: 0 -16px; + } + `; + } } declare global { diff --git a/src/panels/developer-tools/service/developer-tools-service.js b/src/panels/developer-tools/service/developer-tools-service.js deleted file mode 100644 index 73eee72788..0000000000 --- a/src/panels/developer-tools/service/developer-tools-service.js +++ /dev/null @@ -1,371 +0,0 @@ -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { safeDump, safeLoad } from "js-yaml"; -import { computeRTL } from "../../../common/util/compute_rtl"; -import "../../../components/buttons/ha-progress-button"; -import "../../../components/entity/ha-entity-picker"; -import "../../../components/ha-card"; -import "../../../components/ha-code-editor"; -import "../../../components/ha-service-picker"; -import { ENTITY_COMPONENT_DOMAINS } from "../../../data/entity"; -import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; -import LocalizeMixin from "../../../mixins/localize-mixin"; -import "../../../styles/polymer-ha-style"; -import "../../../util/app-localstorage-document"; - -const ERROR_SENTINEL = {}; -/* - * @appliesMixin LocalizeMixin - */ -class HaPanelDevService extends LocalizeMixin(PolymerElement) { - static get template() { - return html` - - - - - - - -
-

- [[localize('ui.panel.developer-tools.tabs.services.description')]] -

- -
- - -

[[localize('ui.panel.developer-tools.tabs.services.data')]]

- - - [[localize('ui.panel.developer-tools.tabs.services.call_service')]] - -
- - -
- - - -
-
- - -
-
-
- `; - } - - static get properties() { - return { - hass: { - type: Object, - }, - - domainService: { - type: String, - observer: "_domainServiceChanged", - }, - - _domain: { - type: String, - computed: "_computeDomain(domainService)", - }, - - _service: { - type: String, - computed: "_computeService(domainService)", - }, - - serviceData: { - type: String, - value: "", - }, - - parsedJSON: { - type: Object, - computed: "_computeParsedServiceData(serviceData)", - }, - - validJSON: { - type: Boolean, - computed: "_computeValidJSON(parsedJSON)", - }, - - _attributes: { - type: Array, - computed: "_computeAttributesArray(hass, _domain, _service)", - }, - - _description: { - type: String, - computed: "_computeDescription(hass, _domain, _service)", - }, - - rtl: { - reflectToAttribute: true, - computed: "_computeRTL(hass)", - }, - }; - } - - _domainServiceChanged() { - this.serviceData = ""; - } - - _computeAttributesArray(hass, domain, service) { - const serviceDomains = hass.services; - if (!(domain in serviceDomains)) return []; - if (!(service in serviceDomains[domain])) return []; - - const fields = serviceDomains[domain][service].fields; - return Object.keys(fields).map(function (field) { - return { key: field, ...fields[field] }; - }); - } - - _computeDescription(hass, domain, service) { - const serviceDomains = hass.services; - if (!(domain in serviceDomains)) return undefined; - if (!(service in serviceDomains[domain])) return undefined; - return serviceDomains[domain][service].description; - } - - _computeServiceDataKey(domainService) { - return `panel-dev-service-state-servicedata.${domainService}`; - } - - _computeDomain(domainService) { - return domainService.split(".", 1)[0]; - } - - _computeService(domainService) { - return domainService.split(".", 2)[1] || null; - } - - _computeParsedServiceData(serviceData) { - try { - return serviceData.trim() ? safeLoad(serviceData) : {}; - } catch (err) { - return ERROR_SENTINEL; - } - } - - _computeValidJSON(parsedJSON) { - return parsedJSON !== ERROR_SENTINEL; - } - - _computeHasEntity(attributes) { - return attributes.some((attr) => attr.key === "entity_id"); - } - - _computeEntityValue(parsedJSON) { - return parsedJSON === ERROR_SENTINEL ? "" : parsedJSON.entity_id; - } - - _computeEntityDomainFilter(domain) { - return ENTITY_COMPONENT_DOMAINS.includes(domain) ? [domain] : null; - } - - _callService(ev) { - const button = ev.target; - if (this.parsedJSON === ERROR_SENTINEL) { - showAlertDialog(this, { - text: this.hass.localize( - "ui.panel.developer-tools.tabs.services.alert_parsing_yaml", - "data", - this.serviceData - ), - }); - button.actionError(); - return; - } - this.hass - .callService(this._domain, this._service, this.parsedJSON) - .then(() => { - button.actionSuccess(); - }) - .catch(() => { - button.actionError(); - }); - } - - _fillExampleData() { - const example = {}; - this._attributes.forEach((attribute) => { - if (attribute.example) { - let value = ""; - try { - value = safeLoad(attribute.example); - } catch (err) { - value = attribute.example; - } - example[attribute.key] = value; - } - }); - this.serviceData = safeDump(example); - } - - _entityPicked(ev) { - this.serviceData = safeDump({ - ...this.parsedJSON, - entity_id: ev.target.value, - }); - } - - _yamlChanged(ev) { - this.serviceData = ev.detail.value; - } - - _computeRTL(hass) { - return computeRTL(hass); - } -} - -customElements.define("developer-tools-service", HaPanelDevService); diff --git a/src/panels/developer-tools/service/developer-tools-service.ts b/src/panels/developer-tools/service/developer-tools-service.ts new file mode 100644 index 0000000000..22abadf25f --- /dev/null +++ b/src/panels/developer-tools/service/developer-tools-service.ts @@ -0,0 +1,350 @@ +import { safeLoad } from "js-yaml"; +import { + css, + CSSResultArray, + html, + LitElement, + property, + query, +} from "lit-element"; +import memoizeOne from "memoize-one"; +import { LocalStorage } from "../../../common/decorators/local-storage"; +import { computeDomain } from "../../../common/entity/compute_domain"; +import { computeObjectId } from "../../../common/entity/compute_object_id"; +import "../../../components/buttons/ha-progress-button"; +import "../../../components/entity/ha-entity-picker"; +import "../../../components/ha-card"; +import "../../../components/ha-expansion-panel"; +import "../../../components/ha-service-control"; +import "../../../components/ha-service-picker"; +import "../../../components/ha-yaml-editor"; +import type { HaYamlEditor } from "../../../components/ha-yaml-editor"; +import { ServiceAction } from "../../../data/script"; +import { haStyle } from "../../../resources/styles"; +import "../../../styles/polymer-ha-style"; +import { HomeAssistant } from "../../../types"; +import "../../../util/app-localstorage-document"; + +class HaPanelDevService extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public narrow!: boolean; + + @LocalStorage("panel-dev-service-state-service-data", true) + private _serviceData?: ServiceAction = { service: "", target: {}, data: {} }; + + @LocalStorage("panel-dev-service-state-yaml-mode", true) + private _yamlMode = false; + + @query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor; + + protected firstUpdated(params) { + super.firstUpdated(params); + if (!this._serviceData?.service) { + const domain = Object.keys(this.hass.services).sort()[0]; + const service = Object.keys(this.hass.services[domain]).sort()[0]; + this._serviceData = { + service: `${domain}.${service}`, + target: {}, + data: {}, + }; + } + } + + protected render() { + const { target, fields } = this._fields( + this.hass.services, + this._serviceData?.service + ); + + const isValid = this._isValid(this._serviceData, fields, target); + + return html` +
+

+ ${this.hass.localize( + "ui.panel.developer-tools.tabs.services.description" + )} +

+ + ${this._yamlMode + ? html`` + : html`
+
`} +
+
+
+ + ${this._yamlMode + ? this.hass.localize( + "ui.panel.developer-tools.tabs.services.ui_mode" + ) + : this.hass.localize( + "ui.panel.developer-tools.tabs.services.yaml_mode" + )} + + + ${this.hass.localize( + "ui.panel.developer-tools.tabs.services.call_service" + )} + +
+
+ + ${(this._yamlMode ? fields : this._filterSelectorFields(fields)).length + ? html`
+ + ${this._yamlMode && target + ? html`

+ ${this.hass.localize( + "ui.panel.developer-tools.tabs.services.accepts_target" + )} +

` + : ""} + + + + + + + ${fields.map( + (field) => html` + + + + ` + )} +
+ ${this.hass.localize( + "ui.panel.developer-tools.tabs.services.column_parameter" + )} + + ${this.hass.localize( + "ui.panel.developer-tools.tabs.services.column_description" + )} + + ${this.hass.localize( + "ui.panel.developer-tools.tabs.services.column_example" + )} +
${field.key}
${field.description}${field.example}
+ ${this._yamlMode + ? html`${this.hass.localize( + "ui.panel.developer-tools.tabs.services.fill_example_data" + )}` + : ""} +
+
` + : ""} + `; + } + + private _filterSelectorFields = memoizeOne((fields) => + fields.filter((field) => !field.selector) + ); + + private _isValid = memoizeOne((serviceData, fields, target): boolean => { + if (!serviceData?.service) { + return false; + } + const domain = computeDomain(serviceData.service); + const service = computeObjectId(serviceData.service); + if (!domain || !service) { + return false; + } + if ( + target && + !serviceData.target && + !serviceData.data?.entity_id && + !serviceData.data?.device_id && + !serviceData.data?.area_id + ) { + return false; + } + for (const field of fields) { + if ( + field.required && + (!serviceData.data || serviceData.data[field.key] === undefined) + ) { + return false; + } + } + return true; + }); + + private _fields = memoizeOne( + ( + serviceDomains: HomeAssistant["services"], + domainService: string | undefined + ): { target: boolean; fields: any[] } => { + if (!domainService) { + return { target: false, fields: [] }; + } + const domain = computeDomain(domainService); + const service = computeObjectId(domainService); + if (!(domain in serviceDomains)) { + return { target: false, fields: [] }; + } + if (!(service in serviceDomains[domain])) { + return { target: false, fields: [] }; + } + const target = "target" in serviceDomains[domain][service]; + const fields = serviceDomains[domain][service].fields; + const result = Object.keys(fields).map((field) => { + return { key: field, ...fields[field] }; + }); + + return { + target, + fields: result, + }; + } + ); + + private _callService() { + const domain = computeDomain(this._serviceData!.service); + const service = computeObjectId(this._serviceData!.service); + if (!domain || !service) { + return; + } + this.hass.callService( + domain, + service, + this._serviceData!.data, + this._serviceData!.target + ); + } + + private _toggleYaml() { + this._yamlMode = !this._yamlMode; + } + + private _yamlChanged(ev) { + if (!ev.detail.isValid) { + return; + } + this._serviceChanged(ev); + } + + private _serviceChanged(ev) { + this._serviceData = ev.detail.value; + } + + private _fillExampleData() { + const { fields } = this._fields( + this.hass.services, + this._serviceData?.service + ); + const example = {}; + fields.forEach((field) => { + if (field.example) { + let value = ""; + try { + value = safeLoad(field.example); + } catch (err) { + value = field.example; + } + example[field.key] = value; + } + }); + this._serviceData = { ...this._serviceData!, data: example }; + this._yamlEditor?.setValue(this._serviceData); + } + + static get styles(): CSSResultArray { + return [ + haStyle, + css` + .content { + padding: 16px; + max-width: 1200px; + margin: auto; + } + .button-row { + padding: 8px 16px; + border-top: 1px solid var(--divider-color); + border-bottom: 1px solid var(--divider-color); + background: var(--card-background-color); + position: sticky; + bottom: 0; + box-sizing: border-box; + width: 100%; + } + + .button-row .buttons { + display: flex; + justify-content: space-between; + max-width: 1200px; + margin: auto; + } + + .attributes { + width: 100%; + } + + .attributes th { + text-align: left; + background-color: var(--card-background-color); + border-bottom: 1px solid var(--primary-text-color); + } + + :host([rtl]) .attributes th { + text-align: right; + } + + .attributes tr { + vertical-align: top; + direction: ltr; + } + + .attributes tr:nth-child(odd) { + background-color: var(--table-row-background-color, #eee); + } + + .attributes tr:nth-child(even) { + background-color: var(--table-row-alternative-background-color, #eee); + } + + .attributes td:nth-child(3) { + white-space: pre-wrap; + word-break: break-word; + } + + .attributes td { + padding: 4px; + vertical-align: middle; + } + `, + ]; + } +} + +customElements.define("developer-tools-service", HaPanelDevService); + +declare global { + interface HTMLElementTagNameMap { + "developer-tools-service": HaPanelDevService; + } +} diff --git a/src/panels/lovelace/components/hui-action-editor.ts b/src/panels/lovelace/components/hui-action-editor.ts index 831c493735..d6b57b239e 100644 --- a/src/panels/lovelace/components/hui-action-editor.ts +++ b/src/panels/lovelace/components/hui-action-editor.ts @@ -194,6 +194,9 @@ export class HuiActionEditor extends LitElement { .dropdown { display: flex; } + ha-service-control { + --service-control-padding: 0; + } `; } } diff --git a/src/panels/lovelace/editor/config-elements/config-elements-style.ts b/src/panels/lovelace/editor/config-elements/config-elements-style.ts index 0b5f7a0314..4a1c4bf12b 100644 --- a/src/panels/lovelace/editor/config-elements/config-elements-style.ts +++ b/src/panels/lovelace/editor/config-elements/config-elements-style.ts @@ -9,7 +9,11 @@ export const configElementStyle = css` } .side-by-side > * { flex: 1; - padding-right: 4px; + padding-right: 8px; + } + .side-by-side > *:last-child { + flex: 1; + padding-right: 0; } .suffix { margin: 0 8px; diff --git a/src/translations/en.json b/src/translations/en.json index ef30609c90..c8da5c3475 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -424,6 +424,12 @@ "service-picker": { "service": "Service" }, + "service-control": { + "required": "This field is required", + "target": "Target", + "target_description": "What should this service call target", + "service_data": "Service data" + }, "related-items": { "no_related_found": "No related items found.", "integration": "Integration", @@ -1401,8 +1407,7 @@ "type_select": "Action type", "type": { "service": { - "label": "Call service", - "service_data": "Service data" + "label": "Call service" }, "delay": { "label": "Delay", @@ -1425,7 +1430,7 @@ "event": { "label": "Fire event", "event": "[%key:ui::panel::config::automation::editor::triggers::type::homeassistant::event%]", - "service_data": "[%key:ui::panel::config::automation::editor::actions::type::service::service_data%]" + "service_data": "[%key:ui::components::service-control::service_data%]" }, "device_id": { "label": "Device", @@ -2694,7 +2699,6 @@ "action-editor": { "navigation_path": "Navigation Path", "url_path": "URL Path", - "editor_service_data": "Service data can only be entered in the code editor", "actions": { "default_action": "Default Action", "call-service": "Call Service", @@ -3273,16 +3277,16 @@ "services": { "title": "Services", "description": "The service dev tool allows you to call any available service in Home Assistant.", - "data": "Service Data (YAML, optional)", "call_service": "Call Service", - "select_service": "Select a service to see the description", - "no_description": "No description is available", - "no_parameters": "This service takes no parameters.", "column_parameter": "Parameter", "column_description": "Description", "column_example": "Example", "fill_example_data": "Fill Example Data", - "alert_parsing_yaml": "Error parsing YAML: {data}" + "yaml_mode": "Go to YAML mode", + "ui_mode": "Go to UI mode", + "yaml_parameters": "Parameters only available in YAML mode", + "all_parameters": "All available parameters", + "accepts_target": "This service accepts a target, for example: `entity_id: light.bed_light`" }, "states": { "title": "States", diff --git a/yarn.lock b/yarn.lock index 201b93b60e..dd3bdf8903 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8174,10 +8174,10 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -home-assistant-js-websocket@^5.8.1: - version "5.8.1" - resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-5.8.1.tgz#4c5930aa47e7089f5806bb3d190ebe53697d2edc" - integrity sha512-2H3q8NK3WrT50iYODv95iz0E2E+nAUOD452V6lhBxhUTQlVFBsuxNMRTTbIZp+6Xab7ad84uF0z+hHFmBMq/Sw== +home-assistant-js-websocket@^5.9.0: + version "5.9.0" + resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-5.9.0.tgz#85f73cc7aa23362e93d7e8208026fbcf25934022" + integrity sha512-HSAhX+s2JgsE77sYKKqcNsukiO6Zm4CcCIwugq17MwHcEyLoecChsbQtgtbvg1dHctUAk+IHxuZ0JBx10B1YGQ== homedir-polyfill@^1.0.1: version "1.0.3" From b7d4c4073687b7d9e334459f59c11a167875b488 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 22 Feb 2021 20:06:18 +0100 Subject: [PATCH 43/44] Show flows in progress when picking a handler (#8368) --- src/data/config_flow.ts | 8 +- .../config-flow/dialog-data-entry-flow.ts | 231 +++++++++++------- .../config-flow/step-flow-pick-flow.ts | 130 ++++++++++ .../config-flow/step-flow-pick-handler.ts | 30 ++- src/translations/en.json | 8 +- 5 files changed, 303 insertions(+), 104 deletions(-) create mode 100644 src/dialogs/config-flow/step-flow-pick-flow.ts diff --git a/src/data/config_flow.ts b/src/data/config_flow.ts index 6a3a8ca8db..04ecff9915 100644 --- a/src/data/config_flow.ts +++ b/src/data/config_flow.ts @@ -65,16 +65,18 @@ export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) => export const getConfigFlowHandlers = (hass: HomeAssistant) => hass.callApi("GET", "config/config_entries/flow_handlers"); -const fetchConfigFlowInProgress = (conn) => +export const fetchConfigFlowInProgress = ( + conn: Connection +): Promise => conn.sendMessagePromise({ type: "config_entries/flow/progress", }); -const subscribeConfigFlowInProgressUpdates = (conn, store) => +const subscribeConfigFlowInProgressUpdates = (conn: Connection, store) => conn.subscribeEvents( debounce( () => - fetchConfigFlowInProgress(conn).then((flows) => + fetchConfigFlowInProgress(conn).then((flows: DataEntryFlowProgress[]) => store.setState(flows, true) ), 500, diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index 206276ce50..76ac0d966b 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -22,7 +22,9 @@ import { AreaRegistryEntry, subscribeAreaRegistry, } from "../../data/area_registry"; +import { fetchConfigFlowInProgress } from "../../data/config_flow"; import type { + DataEntryFlowProgress, DataEntryFlowProgressedEvent, DataEntryFlowStep, } from "../../data/data_entry_flow"; @@ -41,6 +43,7 @@ import "./step-flow-form"; import "./step-flow-loading"; import "./step-flow-pick-handler"; import "./step-flow-progress"; +import "./step-flow-pick-flow"; let instance = 0; @@ -76,6 +79,10 @@ class DataEntryFlowDialog extends LitElement { @internalProperty() private _handlers?: string[]; + @internalProperty() private _handler?: string; + + @internalProperty() private _flowsInProgress?: DataEntryFlowProgress[]; + private _unsubAreas?: UnsubscribeFunc; private _unsubDevices?: UnsubscribeFunc; @@ -84,59 +91,93 @@ class DataEntryFlowDialog extends LitElement { this._params = params; this._instance = instance++; + if (params.startFlowHandler) { + this._checkFlowsInProgress(params.startFlowHandler); + return; + } + + if (params.continueFlowId) { + this._loading = true; + const curInstance = this._instance; + let step: DataEntryFlowStep; + try { + step = await params.flowConfig.fetchFlow( + this.hass, + params.continueFlowId + ); + } catch (err) { + this._step = undefined; + this._params = undefined; + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.integrations.config_flow.error" + ), + text: this.hass.localize( + "ui.panel.config.integrations.config_flow.could_not_load" + ), + }); + return; + } + + // Happens if second showDialog called + if (curInstance !== this._instance) { + return; + } + + this._processStep(step); + this._loading = false; + return; + } + // Create a new config flow. Show picker - if (!params.continueFlowId && !params.startFlowHandler) { - if (!params.flowConfig.getFlowHandlers) { - throw new Error("No getFlowHandlers defined in flow config"); + if (!params.flowConfig.getFlowHandlers) { + throw new Error("No getFlowHandlers defined in flow config"); + } + this._step = null; + + // We only load the handlers once + if (this._handlers === undefined) { + this._loading = true; + try { + this._handlers = await params.flowConfig.getFlowHandlers(this.hass); + } finally { + this._loading = false; } - this._step = null; - - // We only load the handlers once - if (this._handlers === undefined) { - this._loading = true; - try { - this._handlers = await params.flowConfig.getFlowHandlers(this.hass); - } finally { - this._loading = false; - } - } - await this.updateComplete; - return; } - - this._loading = true; - const curInstance = this._instance; - let step: DataEntryFlowStep; - try { - step = await (params.continueFlowId - ? params.flowConfig.fetchFlow(this.hass, params.continueFlowId) - : params.flowConfig.createFlow(this.hass, params.startFlowHandler!)); - } catch (err) { - this._step = undefined; - this._params = undefined; - showAlertDialog(this, { - title: "Error", - text: "Config flow could not be loaded", - }); - return; - } - - // Happens if second showDialog called - if (curInstance !== this._instance) { - return; - } - - this._processStep(step); - this._loading = false; + await this.updateComplete; } public closeDialog() { - if (this._step) { - this._flowDone(); - } else if (this._step === null) { - // Flow aborted during picking flow - this._step = undefined; - this._params = undefined; + if (!this._params) { + return; + } + const flowFinished = Boolean( + this._step && ["create_entry", "abort"].includes(this._step.type) + ); + + // If we created this flow, delete it now. + if (this._step && !flowFinished && !this._params.continueFlowId) { + this._params.flowConfig.deleteFlow(this.hass, this._step.flow_id); + } + + if (this._step !== null && this._params.dialogClosedCallback) { + this._params.dialogClosedCallback({ + flowFinished, + }); + } + + this._step = undefined; + this._params = undefined; + this._devices = undefined; + this._flowsInProgress = undefined; + this._handler = undefined; + if (this._unsubAreas) { + this._unsubAreas(); + this._unsubAreas = undefined; + } + if (this._unsubDevices) { + this._unsubDevices(); + this._unsubDevices = undefined; } fireEvent(this, "dialog-closed", { dialog: this.localName }); } @@ -156,7 +197,9 @@ class DataEntryFlowDialog extends LitElement { >
${this._loading || - (this._step === null && this._handlers === undefined) + (this._step === null && + this._handlers === undefined && + this._handler === undefined) ? html` ${this._step === null - ? // Show handler picker - html` - - ` + .handler=${this._handler} + .flowsInProgress=${this._flowsInProgress} + >` + : // Show handler picker + html` + + ` : this._step.type === "form" ? html` flow.handler === handler); + + if (!flowsInProgress.length) { + let step: DataEntryFlowStep; + try { + step = await this._params!.flowConfig.createFlow(this.hass, handler); + } catch (err) { + this._step = undefined; + this._params = undefined; + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.integrations.config_flow.error" + ), + text: this.hass.localize( + "ui.panel.config.integrations.config_flow.could_not_load" + ), + }); + return; + } + this._processStep(step); + } else { + this._step = null; + this._handler = handler; + this._flowsInProgress = flowsInProgress; + } + this._loading = false; + } + + private _handlerPicked(ev) { + this._checkFlowsInProgress(ev.detail.handler); + } + private async _processStep( step: DataEntryFlowStep | undefined | Promise ): Promise { @@ -305,7 +392,7 @@ class DataEntryFlowDialog extends LitElement { } if (step === undefined) { - this._flowDone(); + this.closeDialog(); return; } this._step = undefined; @@ -313,38 +400,6 @@ class DataEntryFlowDialog extends LitElement { this._step = step; } - private _flowDone(): void { - if (!this._params) { - return; - } - const flowFinished = Boolean( - this._step && ["create_entry", "abort"].includes(this._step.type) - ); - - // If we created this flow, delete it now. - if (this._step && !flowFinished && !this._params.continueFlowId) { - this._params.flowConfig.deleteFlow(this.hass, this._step.flow_id); - } - - if (this._params.dialogClosedCallback) { - this._params.dialogClosedCallback({ - flowFinished, - }); - } - - this._step = undefined; - this._params = undefined; - this._devices = undefined; - if (this._unsubAreas) { - this._unsubAreas(); - this._unsubAreas = undefined; - } - if (this._unsubDevices) { - this._unsubDevices(); - this._unsubDevices = undefined; - } - } - static get styles(): CSSResultArray { return [ haStyleDialog, diff --git a/src/dialogs/config-flow/step-flow-pick-flow.ts b/src/dialogs/config-flow/step-flow-pick-flow.ts new file mode 100644 index 0000000000..b75f2e3b38 --- /dev/null +++ b/src/dialogs/config-flow/step-flow-pick-flow.ts @@ -0,0 +1,130 @@ +import "@polymer/paper-item/paper-icon-item"; +import "@polymer/paper-item"; +import "@polymer/paper-item/paper-item-body"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import { fireEvent } from "../../common/dom/fire_event"; +import "../../components/ha-icon-next"; +import { localizeConfigFlowTitle } from "../../data/config_flow"; +import { DataEntryFlowProgress } from "../../data/data_entry_flow"; +import { domainToName } from "../../data/integration"; +import { HomeAssistant } from "../../types"; +import { brandsUrl } from "../../util/brands-url"; +import { FlowConfig } from "./show-dialog-data-entry-flow"; +import { configFlowContentStyles } from "./styles"; + +@customElement("step-flow-pick-flow") +class StepFlowPickFlow extends LitElement { + public flowConfig!: FlowConfig; + + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) + public flowsInProgress!: DataEntryFlowProgress[]; + + @property() public handler!: string; + + protected render(): TemplateResult { + return html` +

+ ${this.hass.localize( + "ui.panel.config.integrations.config_flow.pick_flow_step.title" + )} +

+ +
+ ${this.flowsInProgress.map( + (flow) => html` + + + + ${localizeConfigFlowTitle(this.hass.localize, flow)} + + + ` + )} + + + ${this.hass.localize( + "ui.panel.config.integrations.config_flow.pick_flow_step.new_flow", + "integration", + domainToName(this.hass.localize, this.handler) + )} + + + +
+ `; + } + + private _startNewFlowPicked(ev) { + this._startFlow(ev.currentTarget.handler); + } + + private _startFlow(handler: string) { + fireEvent(this, "flow-update", { + stepPromise: this.flowConfig.createFlow(this.hass, handler), + }); + } + + private _flowInProgressPicked(ev) { + const flow: DataEntryFlowProgress = ev.currentTarget.flow; + fireEvent(this, "flow-update", { + stepPromise: this.flowConfig.fetchFlow(this.hass, flow.flow_id), + }); + } + + static get styles(): CSSResult[] { + return [ + configFlowContentStyles, + css` + img { + width: 40px; + height: 40px; + } + ha-icon-next { + margin-right: 8px; + } + div { + overflow: auto; + max-height: 600px; + margin: 16px 0; + } + h2 { + padding-right: 66px; + } + @media all and (max-height: 900px) { + div { + max-height: calc(100vh - 134px); + } + } + paper-icon-item, + paper-item { + cursor: pointer; + margin-bottom: 4px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "step-flow-pick-flow": StepFlowPickFlow; + } +} diff --git a/src/dialogs/config-flow/step-flow-pick-handler.ts b/src/dialogs/config-flow/step-flow-pick-handler.ts index 774c9778b0..9c4da85cb4 100644 --- a/src/dialogs/config-flow/step-flow-pick-handler.ts +++ b/src/dialogs/config-flow/step-flow-pick-handler.ts @@ -22,7 +22,6 @@ import { domainToName } from "../../data/integration"; import { HomeAssistant } from "../../types"; import { brandsUrl } from "../../util/brands-url"; import { documentationUrl } from "../../util/documentation-url"; -import { FlowConfig } from "./show-dialog-data-entry-flow"; import { configFlowContentStyles } from "./styles"; interface HandlerObj { @@ -30,17 +29,24 @@ interface HandlerObj { slug: string; } +declare global { + // for fire event + interface HASSDomEvents { + "handler-picked": { + handler: string; + }; + } +} + @customElement("step-flow-pick-handler") class StepFlowPickHandler extends LitElement { - public flowConfig!: FlowConfig; - @property({ attribute: false }) public hass!: HomeAssistant; @property() public handlers!: string[]; @property() public showAdvanced?: boolean; - @internalProperty() private filter?: string; + @internalProperty() private _filter?: string; private _width?: number; @@ -74,7 +80,7 @@ class StepFlowPickHandler extends LitElement { protected render(): TemplateResult { const handlers = this._getHandlers( this.handlers, - this.filter, + this._filter, this.hass.localize ); @@ -82,7 +88,7 @@ class StepFlowPickHandler extends LitElement {

${this.hass.localize("ui.panel.config.integrations.new")}

@@ -164,15 +170,12 @@ class StepFlowPickHandler extends LitElement { } private async _filterChanged(e) { - this.filter = e.detail.value; + this._filter = e.detail.value; } private async _handlerPicked(ev) { - fireEvent(this, "flow-update", { - stepPromise: this.flowConfig.createFlow( - this.hass, - ev.currentTarget.handler.slug - ), + fireEvent(this, "handler-picked", { + handler: ev.currentTarget.handler.slug, }); } @@ -195,6 +198,9 @@ class StepFlowPickHandler extends LitElement { overflow: auto; max-height: 600px; } + h2 { + padding-right: 66px; + } @media all and (max-height: 900px) { div { max-height: calc(100vh - 134px); diff --git a/src/translations/en.json b/src/translations/en.json index c8da5c3475..4a93556872 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2074,7 +2074,13 @@ "description": "This step requires you to visit an external website to be completed.", "open_site": "Open website" }, - "loading_first_time": "Please wait while the integration is being installed" + "pick_flow_step": { + "title": "We discovered these, want to set them up?", + "new_flow": "No, set up an other instance of {integration}" + }, + "loading_first_time": "Please wait while the integration is being installed", + "error": "Error", + "could_not_load": "Config flow could not be loaded" } }, "users": { From d76af2cb61cb22c7f2963958f8050a938fe351a4 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 22 Feb 2021 20:06:30 +0100 Subject: [PATCH 44/44] Bumped version to 20210222.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index efdb01ab43..f63d078d8b 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20210208.0", + version="20210222.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/home-assistant-polymer", author="The Home Assistant Authors",