diff --git a/package.json b/package.json index 3ff0c23af5..6ce23f478d 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.4.1", + "home-assistant-js-websocket": "^5.8.1", "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 0139a08c42..9ef2319aa0 100644 --- a/src/components/device/ha-device-picker.ts +++ b/src/components/device/ha-device-picker.ts @@ -1,10 +1,5 @@ -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 { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, @@ -38,7 +33,7 @@ import { import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import { PolymerChangedEvent } from "../../polymer-types"; import { HomeAssistant } from "../../types"; -import "../ha-svg-icon"; +import { HaComboBox } from "../ha-combo-box"; interface Device { name: string; @@ -115,7 +110,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { @property({ type: Boolean }) private _opened?: boolean; - @query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement; + @query("ha-combo-box", true) private _comboBox!: HaComboBox; private _init = false; @@ -244,15 +239,11 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { ); public open() { - this.updateComplete.then(() => { - (this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open(); - }); + this._comboBox?.open(); } public focus() { - this.updateComplete.then(() => { - this.shadowRoot?.querySelector("paper-input")?.focus(); - }); + this._comboBox?.focus(); } public hassSubscribe(): UnsubscribeFunc[] { @@ -292,70 +283,28 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { return html``; } return html` - - - ${this.value - ? html` - - - - ` - : ""} - - - - - - + > `; } - private _clearValue(ev: Event) { - ev.stopPropagation(); - this._setValue(""); - } - private get _value() { return this.value || ""; } - private _openedChanged(ev: PolymerChangedEvent) { - this._opened = ev.detail.value; - } - private _deviceChanged(ev: PolymerChangedEvent) { + ev.stopPropagation(); const newValue = ev.detail.value; if (newValue !== this._value) { @@ -363,6 +312,10 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { } } + private _openedChanged(ev: PolymerChangedEvent) { + this._opened = ev.detail.value; + } + private _setValue(value: string) { this.value = value; setTimeout(() => { diff --git a/src/components/ha-combo-box.js b/src/components/ha-combo-box.js deleted file mode 100644 index b844d27496..0000000000 --- a/src/components/ha-combo-box.js +++ /dev/null @@ -1,116 +0,0 @@ -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-item/paper-item"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light"; -import { EventsMixin } from "../mixins/events-mixin"; -import "./ha-icon-button"; - -class HaComboBox extends EventsMixin(PolymerElement) { - static get template() { - 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"