Re-do developer tools service (#8410)

This commit is contained in:
Bram Kragten 2021-02-22 19:53:52 +01:00 committed by GitHub
parent 627424b8b9
commit 6092af8de6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 693 additions and 474 deletions

View File

@ -101,7 +101,7 @@
"fuse.js": "^6.0.0", "fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2", "google-timezones-json": "^1.0.2",
"hls.js": "^0.13.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", "idb-keyval": "^3.2.0",
"intl-messageformat": "^8.3.9", "intl-messageformat": "^8.3.9",
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",

View File

@ -6,6 +6,7 @@ import {
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
PropertyValues, PropertyValues,
@ -107,8 +108,9 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc; @property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property({ type: Boolean }) @property({ type: Boolean }) public disabled?: boolean;
private _opened?: boolean;
@internalProperty() private _opened?: boolean;
@query("ha-combo-box", true) private _comboBox!: HaComboBox; @query("ha-combo-box", true) private _comboBox!: HaComboBox;
@ -290,6 +292,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
: this.label} : this.label}
.value=${this._value} .value=${this._value}
.renderer=${rowRenderer} .renderer=${rowRenderer}
.disabled=${this.disabled}
item-value-path="id" item-value-path="id"
item-id-path="id" item-id-path="id"
item-label-path="name" item-label-path="name"

View File

@ -117,6 +117,8 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean; @property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
@property({ type: Boolean }) public disabled?: boolean;
@internalProperty() private _areas?: AreaRegistryEntry[]; @internalProperty() private _areas?: AreaRegistryEntry[];
@internalProperty() private _devices?: DeviceRegistryEntry[]; @internalProperty() private _devices?: DeviceRegistryEntry[];
@ -339,6 +341,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
item-label-path="name" item-label-path="name"
.value=${this._value} .value=${this._value}
.renderer=${rowRenderer} .renderer=${rowRenderer}
.disabled=${this.disabled}
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
@value-changed=${this._areaChanged} @value-changed=${this._areaChanged}
> >
@ -349,6 +352,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
.placeholder=${this.placeholder .placeholder=${this.placeholder
? this._area(this.placeholder)?.name ? this._area(this.placeholder)?.name
: undefined} : undefined}
.disabled=${this.disabled}
class="input" class="input"
autocapitalize="none" autocapitalize="none"
autocomplete="off" autocomplete="off"

View File

@ -10,6 +10,7 @@ import {
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
query, query,
@ -67,8 +68,9 @@ export class HaComboBox extends LitElement {
model: { item: any } model: { item: any }
) => void; ) => void;
@property({ type: Boolean }) @property({ type: Boolean }) public disabled?: boolean;
private _opened?: boolean;
@internalProperty() private _opened?: boolean;
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement; @query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
@ -95,12 +97,14 @@ export class HaComboBox extends LitElement {
.filteredItems=${this.filteredItems} .filteredItems=${this.filteredItems}
.renderer=${this.renderer || defaultRowRenderer} .renderer=${this.renderer || defaultRowRenderer}
.allowCustomValue=${this.allowCustomValue} .allowCustomValue=${this.allowCustomValue}
.disabled=${this.disabled}
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
@filter-changed=${this._filterChanged} @filter-changed=${this._filterChanged}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
> >
<paper-input <paper-input
.label=${this.label} .label=${this.label}
.disabled=${this.disabled}
class="input" class="input"
autocapitalize="none" autocapitalize="none"
autocomplete="off" autocomplete="off"

View File

@ -21,8 +21,11 @@ export class HaActionSelector extends LitElement {
@property() public label?: string; @property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
protected render() { protected render() {
return html`<ha-automation-action return html`<ha-automation-action
.disabled=${this.disabled}
.actions=${this.value || []} .actions=${this.value || []}
.hass=${this.hass} .hass=${this.hass}
></ha-automation-action>`; ></ha-automation-action>`;
@ -34,6 +37,10 @@ export class HaActionSelector extends LitElement {
display: block; display: block;
margin-bottom: 16px; margin-bottom: 16px;
} }
:host([disabled]) ha-automation-action {
opacity: var(--light-disabled-opacity);
pointer-events: none;
}
`; `;
} }
} }

View File

@ -24,6 +24,8 @@ export class HaAreaSelector extends LitElement {
@internalProperty() public _configEntries?: ConfigEntry[]; @internalProperty() public _configEntries?: ConfigEntry[];
@property({ type: Boolean }) public disabled = false;
protected updated(changedProperties) { protected updated(changedProperties) {
if (changedProperties.has("selector")) { if (changedProperties.has("selector")) {
const oldSelector = changedProperties.get("selector"); const oldSelector = changedProperties.get("selector");
@ -50,6 +52,7 @@ export class HaAreaSelector extends LitElement {
.includeDomains=${this.selector.area.entity?.domain .includeDomains=${this.selector.area.entity?.domain
? [this.selector.area.entity.domain] ? [this.selector.area.entity.domain]
: undefined} : undefined}
.disabled=${this.disabled}
></ha-area-picker>`; ></ha-area-picker>`;
} }

View File

@ -19,11 +19,14 @@ export class HaBooleanSelector extends LitElement {
@property() public label?: string; @property() public label?: string;
@property({ type: Boolean }) public disabled = false;
protected render() { protected render() {
return html` <ha-formfield alignEnd spaceBetween .label=${this.label}> return html` <ha-formfield alignEnd spaceBetween .label=${this.label}>
<ha-switch <ha-switch
.checked=${this.value} .checked=${this.value}
@change=${this._handleChange} @change=${this._handleChange}
.disabled=${this.disabled}
></ha-switch> ></ha-switch>
</ha-formfield>`; </ha-formfield>`;
} }

View File

@ -23,10 +23,12 @@ export class HaDeviceSelector extends LitElement {
@internalProperty() public _configEntries?: ConfigEntry[]; @internalProperty() public _configEntries?: ConfigEntry[];
@property({ type: Boolean }) public disabled = false;
protected updated(changedProperties) { protected updated(changedProperties) {
if (changedProperties.has("selector")) { if (changedProperties.has("selector")) {
const oldSelector = changedProperties.get("selector"); const oldSelector = changedProperties.get("selector");
if (oldSelector !== this.selector && this.selector.device.integration) { if (oldSelector !== this.selector && this.selector.device?.integration) {
this._loadConfigEntries(); this._loadConfigEntries();
} }
} }
@ -44,24 +46,25 @@ export class HaDeviceSelector extends LitElement {
.includeDomains=${this.selector.device.entity?.domain .includeDomains=${this.selector.device.entity?.domain
? [this.selector.device.entity.domain] ? [this.selector.device.entity.domain]
: undefined} : undefined}
.disabled=${this.disabled}
allow-custom-entity allow-custom-entity
></ha-device-picker>`; ></ha-device-picker>`;
} }
private _filterDevices(device: DeviceRegistryEntry): boolean { private _filterDevices(device: DeviceRegistryEntry): boolean {
if ( if (
this.selector.device.manufacturer && this.selector.device?.manufacturer &&
device.manufacturer !== this.selector.device.manufacturer device.manufacturer !== this.selector.device.manufacturer
) { ) {
return false; return false;
} }
if ( if (
this.selector.device.model && this.selector.device?.model &&
device.model !== this.selector.device.model device.model !== this.selector.device.model
) { ) {
return false; return false;
} }
if (this.selector.device.integration) { if (this.selector.device?.integration) {
if ( if (
this._configEntries && this._configEntries &&
!this._configEntries.some((entry) => !this._configEntries.some((entry) =>

View File

@ -25,12 +25,15 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
@property() public label?: string; @property() public label?: string;
@property({ type: Boolean }) public disabled = false;
protected render() { protected render() {
return html`<ha-entity-picker return html`<ha-entity-picker
.hass=${this.hass} .hass=${this.hass}
.value=${this.value} .value=${this.value}
.label=${this.label} .label=${this.label}
.entityFilter=${(entity) => this._filterEntities(entity)} .entityFilter=${(entity) => this._filterEntities(entity)}
.disabled=${this.disabled}
allow-custom-entity allow-custom-entity
></ha-entity-picker>`; ></ha-entity-picker>`;
} }
@ -51,12 +54,12 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
} }
private _filterEntities(entity: HassEntity): boolean { private _filterEntities(entity: HassEntity): boolean {
if (this.selector.entity.domain) { if (this.selector.entity?.domain) {
if (computeStateDomain(entity) !== this.selector.entity.domain) { if (computeStateDomain(entity) !== this.selector.entity.domain) {
return false; return false;
} }
} }
if (this.selector.entity.device_class) { if (this.selector.entity?.device_class) {
if ( if (
!entity.attributes.device_class || !entity.attributes.device_class ||
entity.attributes.device_class !== this.selector.entity.device_class entity.attributes.device_class !== this.selector.entity.device_class
@ -64,7 +67,7 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
return false; return false;
} }
} }
if (this.selector.entity.integration) { if (this.selector.entity?.integration) {
if ( if (
!this._entityPlaformLookup || !this._entityPlaformLookup ||
this._entityPlaformLookup[entity.entity_id] !== this._entityPlaformLookup[entity.entity_id] !==

View File

@ -21,8 +21,12 @@ export class HaNumberSelector extends LitElement {
@property() public value?: number; @property() public value?: number;
@property() public placeholder?: number;
@property() public label?: string; @property() public label?: string;
@property({ type: Boolean }) public disabled = false;
protected render() { protected render() {
return html`${this.label} return html`${this.label}
${this.selector.number.mode === "slider" ${this.selector.number.mode === "slider"
@ -31,6 +35,7 @@ export class HaNumberSelector extends LitElement {
.max=${this.selector.number.max} .max=${this.selector.number.max}
.value=${this._value} .value=${this._value}
.step=${this.selector.number.step} .step=${this.selector.number.step}
.disabled=${this.disabled}
pin pin
ignore-bar-touch ignore-bar-touch
@change=${this._handleSliderChange} @change=${this._handleSliderChange}
@ -42,12 +47,14 @@ export class HaNumberSelector extends LitElement {
.label=${this.selector.number.mode === "slider" .label=${this.selector.number.mode === "slider"
? undefined ? undefined
: this.label} : this.label}
.placeholder=${this.placeholder}
.noLabelFloat=${this.selector.number.mode === "slider"} .noLabelFloat=${this.selector.number.mode === "slider"}
class=${classMap({ single: this.selector.number.mode === "box" })} class=${classMap({ single: this.selector.number.mode === "box" })}
.min=${this.selector.number.min} .min=${this.selector.number.min}
.max=${this.selector.number.max} .max=${this.selector.number.max}
.value=${this.value} .value=${this.value}
.step=${this.selector.number.step} .step=${this.selector.number.step}
.disabled=${this.disabled}
type="number" type="number"
auto-validate auto-validate
@value-changed=${this._handleInputChange} @value-changed=${this._handleInputChange}

View File

@ -11,8 +11,14 @@ export class HaObjectSelector extends LitElement {
@property() public label?: string; @property() public label?: string;
@property() public placeholder?: string;
@property({ type: Boolean }) public disabled = false;
protected render() { protected render() {
return html`<ha-yaml-editor return html`<ha-yaml-editor
.disabled=${this.disabled}
.placeholder=${this.placeholder}
.defaultValue=${this.value} .defaultValue=${this.value}
@value-changed=${this._handleChange} @value-changed=${this._handleChange}
></ha-yaml-editor>`; ></ha-yaml-editor>`;

View File

@ -21,8 +21,13 @@ export class HaSelectSelector extends LitElement {
@property() public label?: string; @property() public label?: string;
@property({ type: Boolean }) public disabled = false;
protected render() { protected render() {
return html`<ha-paper-dropdown-menu .label=${this.label}> return html`<ha-paper-dropdown-menu
.disabled=${this.disabled}
.label=${this.label}
>
<paper-listbox <paper-listbox
slot="dropdown-content" slot="dropdown-content"
attr-for-selected="item-value" attr-for-selected="item-value"
@ -41,7 +46,7 @@ export class HaSelectSelector extends LitElement {
} }
private _valueChanged(ev) { private _valueChanged(ev) {
if (!ev.detail.value) { if (this.disabled || !ev.detail.value) {
return; return;
} }
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {

View File

@ -42,6 +42,8 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
@internalProperty() private _configEntries?: ConfigEntry[]; @internalProperty() private _configEntries?: ConfigEntry[];
@property({ type: Boolean }) public disabled = false;
public hassSubscribe(): UnsubscribeFunc[] { public hassSubscribe(): UnsubscribeFunc[] {
return [ return [
subscribeEntityRegistry(this.hass.connection!, (entities) => { subscribeEntityRegistry(this.hass.connection!, (entities) => {
@ -84,6 +86,7 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
.includeDomains=${this.selector.target.entity?.domain .includeDomains=${this.selector.target.entity?.domain
? [this.selector.target.entity.domain] ? [this.selector.target.entity.domain]
: undefined} : undefined}
.disabled=${this.disabled}
></ha-target-picker>`; ></ha-target-picker>`;
} }

View File

@ -13,14 +13,20 @@ export class HaTextSelector extends LitElement {
@property() public label?: string; @property() public label?: string;
@property() public placeholder?: string;
@property() public selector!: StringSelector; @property() public selector!: StringSelector;
@property({ type: Boolean }) public disabled = false;
protected render() { protected render() {
if (this.selector.text?.multiline) { if (this.selector.text?.multiline) {
return html`<paper-textarea return html`<paper-textarea
.label=${this.label} .label=${this.label}
.value="${this.value}" .placeholder=${this.placeholder}
@value-changed="${this._handleChange}" .value=${this.value}
.disabled=${this.disabled}
@value-changed=${this._handleChange}
autocapitalize="none" autocapitalize="none"
autocomplete="off" autocomplete="off"
spellcheck="false" spellcheck="false"
@ -29,6 +35,8 @@ export class HaTextSelector extends LitElement {
return html`<paper-input return html`<paper-input
required required
.value=${this.value} .value=${this.value}
.placeholder=${this.placeholder}
.disabled=${this.disabled}
@value-changed=${this._handleChange} @value-changed=${this._handleChange}
.label=${this.label} .label=${this.label}
></paper-input>`; ></paper-input>`;

View File

@ -17,6 +17,8 @@ export class HaTimeSelector extends LitElement {
@property() public label?: string; @property() public label?: string;
@property({ type: Boolean }) public disabled = false;
protected render() { protected render() {
const parts = this.value?.split(":") || []; const parts = this.value?.split(":") || [];
const hours = useAMPM ? parts[0] ?? "12" : parts[0] ?? "0"; const hours = useAMPM ? parts[0] ?? "12" : parts[0] ?? "0";
@ -29,6 +31,7 @@ export class HaTimeSelector extends LitElement {
.sec=${parts[2] ?? "00"} .sec=${parts[2] ?? "00"}
.format=${useAMPM ? 12 : 24} .format=${useAMPM ? 12 : 24}
.amPm=${useAMPM && (Number(hours) > 12 ? "PM" : "AM")} .amPm=${useAMPM && (Number(hours) > 12 ? "PM" : "AM")}
.disabled=${this.disabled}
@change=${this._timeChanged} @change=${this._timeChanged}
@am-pm-changed=${this._timeChanged} @am-pm-changed=${this._timeChanged}
hide-label hide-label

View File

@ -24,6 +24,10 @@ export class HaSelector extends LitElement {
@property() public label?: string; @property() public label?: string;
@property() public placeholder?: any;
@property({ type: Boolean }) public disabled = false;
public focus() { public focus() {
const input = this.shadowRoot!.getElementById("selector"); const input = this.shadowRoot!.getElementById("selector");
if (!input) { if (!input) {
@ -43,6 +47,8 @@ export class HaSelector extends LitElement {
selector: this.selector, selector: this.selector,
value: this.value, value: this.value,
label: this.label, label: this.label,
placeholder: this.placeholder,
disabled: this.disabled,
id: "selector", id: "selector",
})} })}
`; `;

View File

@ -22,6 +22,7 @@ import "./ha-selector/ha-selector";
import "./ha-service-picker"; import "./ha-service-picker";
import "./ha-settings-row"; import "./ha-settings-row";
import "./ha-yaml-editor"; import "./ha-yaml-editor";
import "./ha-checkbox";
import type { HaYamlEditor } from "./ha-yaml-editor"; import type { HaYamlEditor } from "./ha-yaml-editor";
interface ExtHassService extends Omit<HassService, "fields"> { interface ExtHassService extends Omit<HassService, "fields"> {
@ -30,6 +31,7 @@ interface ExtHassService extends Omit<HassService, "fields"> {
name?: string; name?: string;
description: string; description: string;
required?: boolean; required?: boolean;
advanced?: boolean;
default?: any; default?: any;
example?: any; example?: any;
selector?: Selector; selector?: Selector;
@ -48,14 +50,26 @@ export class HaServiceControl extends LitElement {
@property({ reflect: true, type: Boolean }) public narrow!: boolean; @property({ reflect: true, type: Boolean }) public narrow!: boolean;
@property({ type: Boolean }) public showAdvanced?: boolean;
@internalProperty() private _serviceData?: ExtHassService; @internalProperty() private _serviceData?: ExtHassService;
@internalProperty() private _checkedKeys = new Set();
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor; @query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
protected updated(changedProperties: PropertyValues) { protected updated(changedProperties: PropertyValues) {
if (!changedProperties.has("value")) { if (!changedProperties.has("value")) {
return; 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._serviceData = this.value?.service
? this._getServiceInfo(this.value.service) ? this._getServiceInfo(this.value.service)
: undefined; : undefined;
@ -63,13 +77,33 @@ export class HaServiceControl extends LitElement {
if ( if (
this._serviceData && this._serviceData &&
"target" in 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 = {
...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!.entity_id;
delete this.value.data!.device_id;
delete this.value.data!.area_id;
} }
if (this.value?.data) { if (this.value?.data) {
@ -125,24 +159,46 @@ export class HaServiceControl extends LitElement {
legacy && legacy &&
this._serviceData?.fields.find((field) => field.key === "entity_id"); this._serviceData?.fields.find((field) => field.key === "entity_id");
const hasOptional = Boolean(
!legacy &&
this._serviceData?.fields.some(
(field) => field.selector && !field.required
)
);
return html`<ha-service-picker return html`<ha-service-picker
.hass=${this.hass} .hass=${this.hass}
.value=${this.value?.service} .value=${this.value?.service}
@value-changed=${this._serviceChanged} @value-changed=${this._serviceChanged}
></ha-service-picker> ></ha-service-picker>
<p>${this._serviceData?.description}</p>
${this._serviceData && "target" in this._serviceData ${this._serviceData && "target" in this._serviceData
? html`<ha-selector ? html`<ha-settings-row .narrow=${this.narrow}>
.hass=${this.hass} ${hasOptional
.selector=${this._serviceData.target ? html`<div slot="prefix" class="checkbox-spacer"></div>`
? { target: this._serviceData.target } : ""}
: { <span slot="heading"
target: { >${this.hass.localize(
entity: { domain: computeDomain(this.value!.service) }, "ui.components.service-control.target"
}, )}</span
}} >
@value-changed=${this._targetChanged} <span slot="description"
.value=${this.value?.target} >${this.hass.localize(
></ha-selector>` "ui.components.service-control.target_description"
)}</span
><ha-selector
.hass=${this.hass}
.selector=${this._serviceData.target
? { target: this._serviceData.target }
: {
target: {
entity: { domain: computeDomain(this.value!.service) },
},
}}
@value-changed=${this._targetChanged}
.value=${this.value?.target}
></ha-selector
></ha-settings-row>`
: entityId : entityId
? html`<ha-entity-picker ? html`<ha-entity-picker
.hass=${this.hass} .hass=${this.hass}
@ -156,38 +212,76 @@ export class HaServiceControl extends LitElement {
${legacy ${legacy
? html`<ha-yaml-editor ? html`<ha-yaml-editor
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.service.service_data" "ui.components.service-control.service_data"
)} )}
.name=${"data"} .name=${"data"}
.defaultValue=${this.value?.data} .defaultValue=${this.value?.data}
@value-changed=${this._dataChanged} @value-changed=${this._dataChanged}
></ha-yaml-editor>` ></ha-yaml-editor>`
: this._serviceData?.fields.map((dataField) => : this._serviceData?.fields.map((dataField) =>
dataField.selector dataField.selector && (!dataField.advanced || this.showAdvanced)
? html`<ha-settings-row .narrow=${this.narrow}> ? html`<ha-settings-row .narrow=${this.narrow}>
${dataField.required
? hasOptional
? html`<div slot="prefix" class="checkbox-spacer"></div>`
: ""
: html`<ha-checkbox
.key=${dataField.key}
.checked=${this._checkedKeys.has(dataField.key) ||
(this.value?.data &&
this.value.data[dataField.key] !== undefined)}
@change=${this._checkboxChanged}
slot="prefix"
></ha-checkbox>`}
<span slot="heading">${dataField.name || dataField.key}</span> <span slot="heading">${dataField.name || dataField.key}</span>
<span slot="description">${dataField?.description}</span <span slot="description">${dataField?.description}</span
><ha-selector ><ha-selector
.disabled=${!dataField.required &&
!this._checkedKeys.has(dataField.key) &&
(!this.value?.data ||
this.value.data[dataField.key] === undefined)}
.hass=${this.hass} .hass=${this.hass}
.selector=${dataField.selector} .selector=${dataField.selector}
.key=${dataField.key} .key=${dataField.key}
@value-changed=${this._serviceDataChanged} @value-changed=${this._serviceDataChanged}
.value=${(this.value?.data && .value=${this.value?.data &&
this.value.data[dataField.key]) || this.value.data[dataField.key] !== undefined
dataField.default} ? this.value.data[dataField.key]
: dataField.default}
></ha-selector ></ha-selector
></ha-settings-row>` ></ha-settings-row>`
: "" : ""
)} `; )} `;
} }
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<string>) { private _serviceChanged(ev: PolymerChangedEvent<string>) {
ev.stopPropagation(); ev.stopPropagation();
if (ev.detail.value === this.value?.service) { if (ev.detail.value === this.value?.service) {
return; return;
} }
fireEvent(this, "value-changed", { 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 { static get styles(): CSSResult {
return css` return css`
ha-settings-row { ha-settings-row {
padding: 0; padding: var(--service-control-padding, 0 16px);
} }
ha-settings-row { ha-settings-row {
--paper-time-input-justify-content: flex-end; --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 { :host(:not([narrow])) ha-settings-row paper-input {
width: 60%; width: 60%;
@ -279,6 +390,12 @@ export class HaServiceControl extends LitElement {
:host(:not([narrow])) ha-settings-row ha-selector { :host(:not([narrow])) ha-settings-row ha-selector {
width: 60%; width: 60%;
} }
.checkbox-spacer {
width: 32px;
}
ha-checkbox {
margin-left: -16px;
}
`; `;
} }
} }

View File

@ -1,13 +1,15 @@
import { html, internalProperty, LitElement, property } from "lit-element"; import { html, internalProperty, LitElement, property } from "lit-element";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { LocalizeFunc } from "../common/translations/localize";
import { domainToName } from "../data/integration";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-combo-box"; import "./ha-combo-box";
const rowRenderer = ( const rowRenderer = (
root: HTMLElement, root: HTMLElement,
_owner, _owner,
model: { item: { service: string; description: string } } model: { item: { service: string; name: string } }
) => { ) => {
if (!root.firstElementChild) { if (!root.firstElementChild) {
root.innerHTML = ` root.innerHTML = `
@ -19,15 +21,16 @@ const rowRenderer = (
</style> </style>
<paper-item> <paper-item>
<paper-item-body two-line=""> <paper-item-body two-line="">
<div class='name'>[[item.description]]</div> <div class='name'>[[item.name]]</div>
<div secondary>[[item.service]]</div> <div secondary>[[item.service]]</div>
</paper-item-body> </paper-item-body>
</paper-item> </paper-item>
`; `;
} }
root.querySelector(".name")!.textContent = model.item.description; root.querySelector(".name")!.textContent = model.item.name;
root.querySelector("[secondary]")!.textContent = model.item.service; root.querySelector("[secondary]")!.textContent =
model.item.name === model.item.service ? "" : model.item.service;
}; };
class HaServicePicker extends LitElement { class HaServicePicker extends LitElement {
@ -43,13 +46,14 @@ class HaServicePicker extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.label=${this.hass.localize("ui.components.service-picker.service")} .label=${this.hass.localize("ui.components.service-picker.service")}
.filteredItems=${this._filteredServices( .filteredItems=${this._filteredServices(
this.hass.localize,
this.hass.services, this.hass.services,
this._filter this._filter
)} )}
.value=${this.value} .value=${this.value}
.renderer=${rowRenderer} .renderer=${rowRenderer}
item-value-path="service" item-value-path="service"
item-label-path="description" item-label-path="name"
allow-custom-value allow-custom-value
@filter-changed=${this._filterChanged} @filter-changed=${this._filterChanged}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@ -57,38 +61,48 @@ class HaServicePicker extends LitElement {
`; `;
} }
private _services = memoizeOne((services: HomeAssistant["services"]): { private _services = memoizeOne(
service: string; (
description: string; localize: LocalizeFunc,
}[] => { services: HomeAssistant["services"]
if (!services) { ): {
return []; service: string;
} name: string;
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) { if (!services) {
return []; 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) { if (!filter) {
return processedServices; return processedServices;
@ -96,7 +110,7 @@ class HaServicePicker extends LitElement {
return processedServices.filter( return processedServices.filter(
(service) => (service) =>
service.service.toLowerCase().includes(filter) || service.service.toLowerCase().includes(filter) ||
service.description.toLowerCase().includes(filter) service.name?.toLowerCase().includes(filter)
); );
} }
); );

View File

@ -6,7 +6,7 @@ import {
html, html,
LitElement, LitElement,
property, property,
SVGTemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
@customElement("ha-settings-row") @customElement("ha-settings-row")
@ -16,15 +16,18 @@ export class HaSettingsRow extends LitElement {
@property({ type: Boolean, attribute: "three-line" }) @property({ type: Boolean, attribute: "three-line" })
public threeLine = false; public threeLine = false;
protected render(): SVGTemplateResult { protected render(): TemplateResult {
return html` return html`
<paper-item-body <div class="prefix-wrap">
?two-line=${!this.threeLine} <slot name="prefix"></slot>
?three-line=${this.threeLine} <paper-item-body
> ?two-line=${!this.threeLine}
<slot name="heading"></slot> ?three-line=${this.threeLine}
<div secondary><slot name="description"></slot></div> >
</paper-item-body> <slot name="heading"></slot>
<div secondary><slot name="description"></slot></div>
</paper-item-body>
</div>
<slot></slot> <slot></slot>
`; `;
} }
@ -59,6 +62,13 @@ export class HaSettingsRow extends LitElement {
div[secondary] { div[secondary] {
white-space: normal; white-space: normal;
} }
.prefix-wrap {
display: contents;
}
:host([narrow]) .prefix-wrap {
display: flex;
align-items: center;
}
`; `;
} }
} }

View File

@ -84,6 +84,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
@property() public entityFilter?: HaEntityPickerEntityFilterFunc; @property() public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ type: Boolean, reflect: true }) public disabled = false;
@internalProperty() private _areas?: { [areaId: string]: AreaRegistryEntry }; @internalProperty() private _areas?: { [areaId: string]: AreaRegistryEntry };
@internalProperty() private _devices?: { @internalProperty() private _devices?: {
@ -438,7 +440,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
type: string, type: string,
id: string id: string
): this["value"] { ): this["value"] {
const newVal = ensureArray(value![type])!.filter((val) => val !== id); const newVal = ensureArray(value![type])!.filter(
(val) => String(val) !== id
);
if (newVal.length) { if (newVal.length) {
return { return {
...value, ...value,
@ -599,6 +603,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
paper-tooltip.expand { paper-tooltip.expand {
min-width: 200px; min-width: 200px;
} }
:host([disabled]) .mdc-chip {
opacity: var(--light-disabled-opacity);
pointer-events: none;
}
`; `;
} }
} }

View File

@ -44,14 +44,14 @@ export class HaYamlEditor extends LitElement {
@internalProperty() private _yaml = ""; @internalProperty() private _yaml = "";
@query("ha-code-editor", true) private _editor?: HaCodeEditor; @query("ha-code-editor") private _editor?: HaCodeEditor;
public setValue(value): void { public setValue(value): void {
try { try {
this._yaml = value && !isEmpty(value) ? safeDump(value) : ""; this._yaml = value && !isEmpty(value) ? safeDump(value) : "";
} catch (err) { } catch (err) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(err); console.error(err, value);
alert(`There was an error converting to YAML: ${err}`); alert(`There was an error converting to YAML: ${err}`);
} }
afterNextRender(() => { afterNextRender(() => {
@ -73,7 +73,7 @@ export class HaYamlEditor extends LitElement {
return html``; return html``;
} }
return html` return html`
${this.label ? html` <p>${this.label}</p> ` : ""} ${this.label ? html`<p>${this.label}</p>` : ""}
<ha-code-editor <ha-code-editor
.value=${this._yaml} .value=${this._yaml}
mode="yaml" mode="yaml"
@ -85,13 +85,13 @@ export class HaYamlEditor extends LitElement {
private _onChange(ev: CustomEvent): void { private _onChange(ev: CustomEvent): void {
ev.stopPropagation(); ev.stopPropagation();
const value = ev.detail.value; this._yaml = ev.detail.value;
let parsed; let parsed;
let isValid = true; let isValid = true;
if (value) { if (this._yaml) {
try { try {
parsed = safeLoad(value); parsed = safeLoad(this._yaml);
} catch (err) { } catch (err) {
// Invalid YAML // Invalid YAML
isValid = false; isValid = false;
@ -107,7 +107,7 @@ export class HaYamlEditor extends LitElement {
} }
get yaml() { get yaml() {
return this._editor?.value; return this._yaml;
} }
} }

View File

@ -1,5 +1,7 @@
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import { import {
css,
CSSResult,
customElement, customElement,
internalProperty, internalProperty,
LitElement, LitElement,
@ -62,6 +64,7 @@ export class HaServiceAction extends LitElement implements ActionElement {
.narrow=${this.narrow} .narrow=${this.narrow}
.hass=${this.hass} .hass=${this.hass}
.value=${this._action} .value=${this._action}
.showAdvanced=${this.hass.userData?.showAdvanced}
@value-changed=${this._actionChanged} @value-changed=${this._actionChanged}
></ha-service-control> ></ha-service-control>
`; `;
@ -72,6 +75,15 @@ export class HaServiceAction extends LitElement implements ActionElement {
ev.stopPropagation(); ev.stopPropagation();
} }
} }
static get styles(): CSSResult {
return css`
ha-service-control {
display: block;
margin: 0 -16px;
}
`;
}
} }
declare global { declare global {

View File

@ -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`
<style include="ha-style">
:host {
-ms-user-select: initial;
-webkit-user-select: initial;
-moz-user-select: initial;
display: block;
padding: 16px;
}
.ha-form {
margin-right: 16px;
max-width: 400px;
}
ha-progress-button {
margin-top: 8px;
}
ha-card {
margin-top: 12px;
}
.description {
margin-top: 12px;
white-space: pre-wrap;
}
.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;
}
pre {
margin: 0;
font-family: var(--code-font-family, monospace);
}
td {
padding: 4px;
}
.error {
color: var(--error-color);
}
:host([rtl]) .desc-container {
text-align: right;
}
:host([rtl]) .desc-container h3 {
direction: ltr;
}
</style>
<app-localstorage-document
key="panel-dev-service-state-domain-service"
data="{{domainService}}"
>
</app-localstorage-document>
<app-localstorage-document
key="[[_computeServiceDataKey(domainService)]]"
data="{{serviceData}}"
>
</app-localstorage-document>
<div class="content">
<p>
[[localize('ui.panel.developer-tools.tabs.services.description')]]
</p>
<div class="ha-form">
<ha-service-picker
hass="[[hass]]"
value="{{domainService}}"
></ha-service-picker>
<template is="dom-if" if="[[_computeHasEntity(_attributes)]]">
<ha-entity-picker
hass="[[hass]]"
value="[[_computeEntityValue(parsedJSON)]]"
on-change="_entityPicked"
disabled="[[!validJSON]]"
include-domains="[[_computeEntityDomainFilter(_domain)]]"
allow-custom-entity
></ha-entity-picker>
</template>
<p>[[localize('ui.panel.developer-tools.tabs.services.data')]]</p>
<ha-code-editor
mode="yaml"
value="[[serviceData]]"
error="[[!validJSON]]"
on-value-changed="_yamlChanged"
></ha-code-editor>
<ha-progress-button
on-click="_callService"
raised
disabled="[[!validJSON]]"
>
[[localize('ui.panel.developer-tools.tabs.services.call_service')]]
</ha-progress-button>
</div>
<ha-card>
<div class="card-header">
<template is="dom-if" if="[[!domainService]]">
[[localize('ui.panel.developer-tools.tabs.services.select_service')]]
</template>
<template is="dom-if" if="[[domainService]]">
<template is="dom-if" if="[[!_description]]">
[[localize('ui.panel.developer-tools.tabs.services.no_description')]]
</template>
<template is="dom-if" if="[[_description]]">
[[_description]]
</template>
</template>
</div>
<div class="card-content">
<template is="dom-if" if="[[_description]]">
<template is="dom-if" if="[[!_attributes.length]]">
[[localize('ui.panel.developer-tools.tabs.services.no_parameters')]]
</template>
<template is="dom-if" if="[[_attributes.length]]">
<table class="attributes">
<tr>
<th>
[[localize('ui.panel.developer-tools.tabs.services.column_parameter')]]
</th>
<th>
[[localize('ui.panel.developer-tools.tabs.services.column_description')]]
</th>
<th>
[[localize('ui.panel.developer-tools.tabs.services.column_example')]]
</th>
</tr>
<template is="dom-repeat" items="[[_attributes]]" as="attribute">
<tr>
<td><pre>[[attribute.key]]</pre></td>
<td>[[attribute.description]]</td>
<td>[[attribute.example]]</td>
</tr>
</template>
</table>
</template>
<template is="dom-if" if="[[_attributes.length]]">
<mwc-button on-click="_fillExampleData">
[[localize('ui.panel.developer-tools.tabs.services.fill_example_data')]]
</mwc-button>
</template>
</template>
</template>
</div>
</ha-card>
</div>
`;
}
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);

View File

@ -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`
<div class="content">
<p>
${this.hass.localize(
"ui.panel.developer-tools.tabs.services.description"
)}
</p>
${this._yamlMode
? html`<ha-yaml-editor
.defaultValue=${this._serviceData}
@value-changed=${this._yamlChanged}
></ha-yaml-editor>`
: html`<ha-card
><div>
<ha-service-control
.hass=${this.hass}
.value=${this._serviceData}
.narrow=${this.narrow}
showAdvanced
@value-changed=${this._serviceChanged}
></ha-service-control></div
></ha-card>`}
</div>
<div class="button-row">
<div class="buttons">
<mwc-button @click=${this._toggleYaml}>
${this._yamlMode
? this.hass.localize(
"ui.panel.developer-tools.tabs.services.ui_mode"
)
: this.hass.localize(
"ui.panel.developer-tools.tabs.services.yaml_mode"
)}
</mwc-button>
<mwc-button .disabled=${!isValid} raised @click=${this._callService}>
${this.hass.localize(
"ui.panel.developer-tools.tabs.services.call_service"
)}
</mwc-button>
</div>
</div>
${(this._yamlMode ? fields : this._filterSelectorFields(fields)).length
? html`<div class="content">
<ha-expansion-panel
.header=${this._yamlMode
? this.hass.localize(
"ui.panel.developer-tools.tabs.services.all_parameters"
)
: this.hass.localize(
"ui.panel.developer-tools.tabs.services.yaml_parameters"
)}
outlined
.expanded=${this._yamlMode}
>
${this._yamlMode && target
? html`<h3>
${this.hass.localize(
"ui.panel.developer-tools.tabs.services.accepts_target"
)}
</h3>`
: ""}
<table class="attributes">
<tr>
<th>
${this.hass.localize(
"ui.panel.developer-tools.tabs.services.column_parameter"
)}
</th>
<th>
${this.hass.localize(
"ui.panel.developer-tools.tabs.services.column_description"
)}
</th>
<th>
${this.hass.localize(
"ui.panel.developer-tools.tabs.services.column_example"
)}
</th>
</tr>
${fields.map(
(field) => html` <tr>
<td><pre>${field.key}</pre></td>
<td>${field.description}</td>
<td>${field.example}</td>
</tr>`
)}
</table>
${this._yamlMode
? html`<mwc-button @click=${this._fillExampleData}
>${this.hass.localize(
"ui.panel.developer-tools.tabs.services.fill_example_data"
)}</mwc-button
>`
: ""}
</ha-expansion-panel>
</div>`
: ""}
`;
}
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;
}
}

View File

@ -194,6 +194,9 @@ export class HuiActionEditor extends LitElement {
.dropdown { .dropdown {
display: flex; display: flex;
} }
ha-service-control {
--service-control-padding: 0;
}
`; `;
} }
} }

View File

@ -9,7 +9,11 @@ export const configElementStyle = css`
} }
.side-by-side > * { .side-by-side > * {
flex: 1; flex: 1;
padding-right: 4px; padding-right: 8px;
}
.side-by-side > *:last-child {
flex: 1;
padding-right: 0;
} }
.suffix { .suffix {
margin: 0 8px; margin: 0 8px;

View File

@ -424,6 +424,12 @@
"service-picker": { "service-picker": {
"service": "Service" "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": { "related-items": {
"no_related_found": "No related items found.", "no_related_found": "No related items found.",
"integration": "Integration", "integration": "Integration",
@ -1401,8 +1407,7 @@
"type_select": "Action type", "type_select": "Action type",
"type": { "type": {
"service": { "service": {
"label": "Call service", "label": "Call service"
"service_data": "Service data"
}, },
"delay": { "delay": {
"label": "Delay", "label": "Delay",
@ -1425,7 +1430,7 @@
"event": { "event": {
"label": "Fire event", "label": "Fire event",
"event": "[%key:ui::panel::config::automation::editor::triggers::type::homeassistant::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": { "device_id": {
"label": "Device", "label": "Device",
@ -2694,7 +2699,6 @@
"action-editor": { "action-editor": {
"navigation_path": "Navigation Path", "navigation_path": "Navigation Path",
"url_path": "URL Path", "url_path": "URL Path",
"editor_service_data": "Service data can only be entered in the code editor",
"actions": { "actions": {
"default_action": "Default Action", "default_action": "Default Action",
"call-service": "Call Service", "call-service": "Call Service",
@ -3273,16 +3277,16 @@
"services": { "services": {
"title": "Services", "title": "Services",
"description": "The service dev tool allows you to call any available service in Home Assistant.", "description": "The service dev tool allows you to call any available service in Home Assistant.",
"data": "Service Data (YAML, optional)",
"call_service": "Call Service", "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_parameter": "Parameter",
"column_description": "Description", "column_description": "Description",
"column_example": "Example", "column_example": "Example",
"fill_example_data": "Fill Example Data", "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": { "states": {
"title": "States", "title": "States",

View File

@ -8174,10 +8174,10 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0" minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1" minimalistic-crypto-utils "^1.0.1"
home-assistant-js-websocket@^5.8.1: home-assistant-js-websocket@^5.9.0:
version "5.8.1" version "5.9.0"
resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-5.8.1.tgz#4c5930aa47e7089f5806bb3d190ebe53697d2edc" resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-5.9.0.tgz#85f73cc7aa23362e93d7e8208026fbcf25934022"
integrity sha512-2H3q8NK3WrT50iYODv95iz0E2E+nAUOD452V6lhBxhUTQlVFBsuxNMRTTbIZp+6Xab7ad84uF0z+hHFmBMq/Sw== integrity sha512-HSAhX+s2JgsE77sYKKqcNsukiO6Zm4CcCIwugq17MwHcEyLoecChsbQtgtbvg1dHctUAk+IHxuZ0JBx10B1YGQ==
homedir-polyfill@^1.0.1: homedir-polyfill@^1.0.1:
version "1.0.3" version "1.0.3"