Add helper UI (#4940)

* Add helper UI

* Oops

* Update

* Update

* Update

* Lint

* Add all input forms

* Return extended entity registry entry from update

* Comments

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
Bram Kragten 2020-02-26 12:53:03 +01:00 committed by GitHub
parent 52ded635ff
commit b229071248
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 2426 additions and 182 deletions

View File

@ -452,7 +452,7 @@ class HassioAddonInfo extends LitElement {
`
: ""}
<ha-progress-button
.disabled=${!this.addon.available}
.disabled=${!this.addon.available || this._installing}
.progress=${this._installing}
@click=${this._installClicked}
>

View File

@ -4,7 +4,7 @@ export const dynamicElement = directive(
(tag: string, properties?: { [key: string]: any }) => (part: Part): void => {
if (!(part instanceof NodePart)) {
throw new Error(
"dynamicContentDirective can only be used in content bindings"
"dynamicElementDirective can only be used in content bindings"
);
}

View File

@ -23,7 +23,7 @@ const fixedIcons = {
homeassistant: "hass:home-assistant",
homekit: "hass:home-automation",
image_processing: "hass:image-filter-frames",
input_boolean: "hass:drawing",
input_boolean: "hass:toggle-switch-outline",
input_datetime: "hass:calendar-clock",
input_number: "hass:ray-vertex",
input_select: "hass:format-list-bulleted",

View File

@ -0,0 +1,65 @@
import {
html,
css,
LitElement,
TemplateResult,
property,
customElement,
} from "lit-element";
import "@polymer/paper-input/paper-input";
import "./ha-icon";
import { fireEvent } from "../common/dom/fire_event";
@customElement("ha-icon-input")
export class HaIconInput extends LitElement {
@property() public value?: string;
@property() public label?: string;
@property() public placeholder?: string;
@property({ attribute: "error-message" }) public errorMessage?: string;
@property({ type: Boolean }) public disabled = false;
protected render(): TemplateResult {
return html`
<paper-input
.value=${this.value}
.label=${this.label}
.placeholder=${this.placeholder}
@value-changed=${this._valueChanged}
.disabled=${this.disabled}
auto-validate
.errorMessage=${this.errorMessage}
pattern="^\\S+:\\S+$"
>
${this.value || this.placeholder
? html`
<ha-icon .icon=${this.value || this.placeholder} slot="suffix">
</ha-icon>
`
: ""}
</paper-input>
`;
}
private _valueChanged(ev: CustomEvent) {
this.value = ev.detail.value;
fireEvent(
this,
"value-changed",
{ value: ev.detail.value },
{
bubbles: false,
composed: false,
}
);
}
static get styles() {
return css`
ha-icon {
position: relative;
bottom: 4px;
}
`;
}
}

View File

@ -70,9 +70,7 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
}
if (Object.keys(this._related).length === 0) {
return html`
<p>
${this.hass.localize("ui.components.related-items.no_related_found")}
</p>
${this.hass.localize("ui.components.related-items.no_related_found")}
`;
}
return html`

View File

@ -6,14 +6,23 @@ import { debounce } from "../common/util/debounce";
export interface EntityRegistryEntry {
entity_id: string;
name: string;
icon?: string;
platform: string;
config_entry_id?: string;
device_id?: string;
disabled_by: string | null;
}
export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
unique_id: string;
capabilities: object;
original_name?: string;
original_icon?: string;
}
export interface EntityRegistryEntryUpdateParams {
name?: string | null;
icon?: string | null;
disabled_by?: string | null;
new_entity_id?: string;
}
@ -29,12 +38,21 @@ export const computeEntityRegistryName = (
return state ? computeStateName(state) : null;
};
export const getExtendedEntityRegistryEntry = (
hass: HomeAssistant,
entityId: string
): Promise<ExtEntityRegistryEntry> =>
hass.callWS({
type: "config/entity_registry/get",
entity_id: entityId,
});
export const updateEntityRegistryEntry = (
hass: HomeAssistant,
entityId: string,
updates: Partial<EntityRegistryEntryUpdateParams>
): Promise<EntityRegistryEntry> =>
hass.callWS<EntityRegistryEntry>({
): Promise<ExtEntityRegistryEntry> =>
hass.callWS({
type: "config/entity_registry/update",
entity_id: entityId,
...updates,

View File

@ -59,3 +59,12 @@ export const getOptimisticFrontendUserDataCollection = <
`_frontendUserData-${userDataKey}`,
() => fetchFrontendUserData(conn, userDataKey)
);
export const subscribeFrontendUserData = <UserDataKey extends ValidUserDataKey>(
conn: Connection,
userDataKey: UserDataKey,
onChange: (state: FrontendUserData[UserDataKey] | null) => void
) =>
getOptimisticFrontendUserDataCollection(conn, userDataKey).subscribe(
onChange
);

View File

@ -1,11 +0,0 @@
import { HomeAssistant } from "../types";
export const setInputSelectOption = (
hass: HomeAssistant,
entity: string,
option: string
) =>
hass.callService("input_select", "select_option", {
option,
entity_id: entity,
});

43
src/data/input_boolean.ts Normal file
View File

@ -0,0 +1,43 @@
import { HomeAssistant } from "../types";
export interface InputBoolean {
id: string;
name: string;
icon?: string;
initial?: boolean;
}
export interface InputBooleanMutableParams {
name: string;
icon: string;
initial: boolean;
}
export const fetchInputBoolean = (hass: HomeAssistant) =>
hass.callWS<InputBoolean[]>({ type: "input_boolean/list" });
export const createInputBoolean = (
hass: HomeAssistant,
values: InputBooleanMutableParams
) =>
hass.callWS<InputBoolean>({
type: "input_boolean/create",
...values,
});
export const updateInputBoolean = (
hass: HomeAssistant,
id: string,
updates: Partial<InputBooleanMutableParams>
) =>
hass.callWS<InputBoolean>({
type: "input_boolean/update",
input_boolean_id: id,
...updates,
});
export const deleteInputBoolean = (hass: HomeAssistant, id: string) =>
hass.callWS({
type: "input_boolean/delete",
input_boolean_id: id,
});

View File

@ -1,5 +1,22 @@
import { HomeAssistant } from "../types";
export interface InputDateTime {
id: string;
name: string;
icon?: string;
initial?: string;
has_time: boolean;
has_date: boolean;
}
export interface InputDateTimeMutableParams {
name: string;
icon: string;
initial: string;
has_time: boolean;
has_date: boolean;
}
export const setInputDateTimeValue = (
hass: HomeAssistant,
entityId: string,
@ -9,3 +26,32 @@ export const setInputDateTimeValue = (
const param = { entity_id: entityId, time, date };
hass.callService(entityId.split(".", 1)[0], "set_datetime", param);
};
export const fetchInputDateTime = (hass: HomeAssistant) =>
hass.callWS<InputDateTime[]>({ type: "input_datetime/list" });
export const createInputDateTime = (
hass: HomeAssistant,
values: InputDateTimeMutableParams
) =>
hass.callWS<InputDateTime>({
type: "input_datetime/create",
...values,
});
export const updateInputDateTime = (
hass: HomeAssistant,
id: string,
updates: Partial<InputDateTimeMutableParams>
) =>
hass.callWS<InputDateTime>({
type: "input_datetime/update",
input_datetime_id: id,
...updates,
});
export const deleteInputDateTime = (hass: HomeAssistant, id: string) =>
hass.callWS({
type: "input_datetime/delete",
input_datetime_id: id,
});

53
src/data/input_number.ts Normal file
View File

@ -0,0 +1,53 @@
import { HomeAssistant } from "../types";
export interface InputNumber {
id: string;
name: string;
min: number;
max: number;
icon?: string;
initial?: number;
step?: number;
mode?: "box" | "slider";
unit_of_measurement?: string;
}
export interface InputNumberMutableParams {
name: string;
icon: string;
initial: number;
min: number;
max: number;
step: number;
mode: "box" | "slider";
unit_of_measurement?: string;
}
export const fetchInputNumber = (hass: HomeAssistant) =>
hass.callWS<InputNumber[]>({ type: "input_number/list" });
export const createInputNumber = (
hass: HomeAssistant,
values: InputNumberMutableParams
) =>
hass.callWS<InputNumber>({
type: "input_number/create",
...values,
});
export const updateInputNumber = (
hass: HomeAssistant,
id: string,
updates: Partial<InputNumberMutableParams>
) =>
hass.callWS<InputNumber>({
type: "input_number/update",
input_number_id: id,
...updates,
});
export const deleteInputNumber = (hass: HomeAssistant, id: string) =>
hass.callWS({
type: "input_number/delete",
input_number_id: id,
});

55
src/data/input_select.ts Normal file
View File

@ -0,0 +1,55 @@
import { HomeAssistant } from "../types";
export interface InputSelect {
id: string;
name: string;
options: string[];
icon?: string;
initial?: string;
}
export interface InputSelectMutableParams {
name: string;
icon: string;
initial: string;
options: string[];
}
export const setInputSelectOption = (
hass: HomeAssistant,
entity: string,
option: string
) =>
hass.callService("input_select", "select_option", {
option,
entity_id: entity,
});
export const fetchInputSelect = (hass: HomeAssistant) =>
hass.callWS<InputSelect[]>({ type: "input_select/list" });
export const createInputSelect = (
hass: HomeAssistant,
values: InputSelectMutableParams
) =>
hass.callWS<InputSelect>({
type: "input_select/create",
...values,
});
export const updateInputSelect = (
hass: HomeAssistant,
id: string,
updates: Partial<InputSelectMutableParams>
) =>
hass.callWS<InputSelect>({
type: "input_select/update",
input_select_id: id,
...updates,
});
export const deleteInputSelect = (hass: HomeAssistant, id: string) =>
hass.callWS({
type: "input_select/delete",
input_select_id: id,
});

View File

@ -1,7 +1,57 @@
import { HomeAssistant } from "../types";
export interface InputText {
id: string;
name: string;
icon?: string;
initial?: string;
min?: number;
max?: number;
pattern?: string;
mode?: "text" | "password";
}
export interface InputTextMutableParams {
name: string;
icon: string;
initial: string;
min: number;
max: number;
pattern: string;
mode: "text" | "password";
}
export const setValue = (hass: HomeAssistant, entity: string, value: string) =>
hass.callService(entity.split(".", 1)[0], "set_value", {
value,
entity_id: entity,
});
export const fetchInputText = (hass: HomeAssistant) =>
hass.callWS<InputText[]>({ type: "input_text/list" });
export const createInputText = (
hass: HomeAssistant,
values: InputTextMutableParams
) =>
hass.callWS<InputText>({
type: "input_text/create",
...values,
});
export const updateInputText = (
hass: HomeAssistant,
id: string,
updates: Partial<InputTextMutableParams>
) =>
hass.callWS<InputText>({
type: "input_text/update",
input_text_id: id,
...updates,
});
export const deleteInputText = (hass: HomeAssistant, id: string) =>
hass.callWS({
type: "input_text/delete",
input_text_id: id,
});

View File

@ -129,6 +129,10 @@ class DialogBox extends LitElement {
return [
haStyleDialog,
css`
:host([inert]) {
pointer-events: initial !important;
cursor: initial !important;
}
ha-paper-dialog {
min-width: 400px;
max-width: 500px;

View File

@ -8,7 +8,6 @@ import "../resources/ha-style";
import "./more-info/more-info-controls";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import DialogMixin from "../mixins/dialog-mixin";
@ -81,7 +80,6 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) {
hass="[[hass]]"
state-obj="[[stateObj]]"
dialog-element="[[_dialogElement()]]"
registry-entry="[[_registryInfo]]"
large="{{large}}"
></more-info-controls>
`;
@ -102,8 +100,6 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) {
observer: "_largeChanged",
},
_registryInfo: Object,
dataDomain: {
computed: "_computeDomain(stateObj)",
reflectToAttribute: true,
@ -127,11 +123,10 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) {
return hass.states[hass.moreInfoEntityId] || null;
}
async _stateObjChanged(newVal, oldVal) {
async _stateObjChanged(newVal) {
if (!newVal) {
this.setProperties({
opened: false,
_registryInfo: null,
large: false,
});
return;
@ -144,25 +139,6 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) {
this.opened = true;
})
);
if (
!isComponentLoaded(this.hass, "config") ||
(oldVal && oldVal.entity_id === newVal.entity_id)
) {
return;
}
if (this.hass.user.is_admin) {
try {
const info = await this.hass.callWS({
type: "config/entity_registry/get",
entity_id: newVal.entity_id,
});
this._registryInfo = info;
} catch (err) {
this._registryInfo = null;
}
}
}
_dialogOpenChanged(newVal) {

View File

@ -39,7 +39,8 @@ class MoreInfoPerson extends LitElement {
></ha-map>
`
: ""}
${this.hass.user?.is_admin &&
${!__DEMO__ &&
this.hass.user?.is_admin &&
this.stateObj.state === "not_home" &&
this.stateObj.attributes.latitude &&
this.stateObj.attributes.longitude

View File

@ -22,7 +22,7 @@ import LocalizeMixin from "../../mixins/localize-mixin";
import { computeRTL } from "../../common/util/compute_rtl";
import { removeEntityRegistryEntry } from "../../data/entity_registry";
import { showConfirmationDialog } from "../generic/show-dialog-box";
import { showEntityRegistryDetailDialog } from "../../panels/config/entities/show-dialog-entity-registry-detail";
import { showEntityEditorDialog } from "../../panels/config/entities/show-dialog-entity-editor";
const DOMAINS_NO_INFO = ["camera", "configurator", "history_graph"];
const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"];
@ -88,7 +88,7 @@ class MoreInfoControls extends LocalizeMixin(EventsMixin(PolymerElement)) {
<div class="main-title" main-title="" on-click="enlarge">
[[_computeStateName(stateObj)]]
</div>
<template is="dom-if" if="[[registryEntry]]">
<template is="dom-if" if="[[_computeConfig(hass)]]">
<paper-icon-button
aria-label$="[[localize('ui.dialogs.more_info_control.settings')]]"
icon="hass:settings"
@ -221,6 +221,10 @@ class MoreInfoControls extends LocalizeMixin(EventsMixin(PolymerElement)) {
return stateObj ? computeStateName(stateObj) : "";
}
_computeConfig(hass) {
return hass.user.is_admin && isComponentLoaded(hass, "config");
}
_computeEdit(hass, stateObj) {
const domain = this._computeDomain(stateObj);
return (
@ -260,7 +264,9 @@ class MoreInfoControls extends LocalizeMixin(EventsMixin(PolymerElement)) {
}
_gotoSettings() {
showEntityRegistryDetailDialog(this, { entry: this.registryEntry });
showEntityEditorDialog(this, {
entity_id: this.stateObj.entity_id,
});
this.fire("hass-more-info", { entityId: null });
}

View File

@ -15,6 +15,7 @@ import { subscribeThemes } from "../data/ws-themes";
import { subscribeUser } from "../data/ws-user";
import { HomeAssistant } from "../types";
import { hassUrl } from "../data/auth";
import { subscribeFrontendUserData } from "../data/frontend";
import {
fetchConfig,
fetchResources,
@ -92,6 +93,7 @@ window.hassConnection.then(({ conn }) => {
subscribePanels(conn, noop);
subscribeThemes(conn, noop);
subscribeUser(conn, noop);
subscribeFrontendUserData(conn, "core", noop);
if (location.pathname === "/" || location.pathname.startsWith("/lovelace/")) {
(window as WindowWithLovelaceProm).llConfProm = fetchConfig(

View File

@ -20,7 +20,7 @@ import "@polymer/paper-item/paper-item-body";
import "../../../../components/ha-card";
import "../../../../components/ha-icon";
import { showEntityRegistryDetailDialog } from "../../entities/show-dialog-entity-registry-detail";
import { showEntityEditorDialog } from "../../entities/show-dialog-entity-editor";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { domainIcon } from "../../../../common/entity/domain_icon";
import { EntityRegistryStateEntry } from "../ha-config-device-page";
@ -150,7 +150,7 @@ export class HaDeviceEntitiesCard extends LitElement {
private _openEditEntry(ev: Event): void {
ev.stopPropagation();
const entry = (ev.currentTarget! as any).entry;
showEntityRegistryDetailDialog(this, {
showEntityEditorDialog(this, {
entry,
entity_id: entry.entity_id,
});

View File

@ -6,8 +6,6 @@ import {
TemplateResult,
property,
customElement,
CSSResult,
css,
} from "lit-element";
import { HomeAssistant, Route } from "../../../types";
import {
@ -253,17 +251,6 @@ export class HaConfigDeviceDashboard extends LitElement {
const deviceId = (ev.detail as RowClickedEvent).id;
navigate(this, `/config/devices/device/${deviceId}`);
}
static get styles(): CSSResult {
return css`
.content {
padding: 4px;
}
ha-devices-data-table {
width: 100%;
}
`;
}
}
declare global {

View File

@ -0,0 +1,8 @@
/** Platforms that have a settings tab. */
export const PLATFORMS_WITH_SETTINGS_TAB = {
input_number: "entity-settings-helper-tab",
input_select: "entity-settings-helper-tab",
input_text: "entity-settings-helper-tab",
input_boolean: "entity-settings-helper-tab",
input_datetime: "entity-settings-helper-tab",
};

View File

@ -13,25 +13,45 @@ import {
TemplateResult,
} from "lit-element";
import { cache } from "lit-html/directives/cache";
import { PLATFORMS_WITH_SETTINGS_TAB } from "./const";
import { dynamicElement } from "../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/dialog/ha-paper-dialog";
// tslint:disable-next-line: no-duplicate-imports
import { HaPaperDialog } from "../../../components/dialog/ha-paper-dialog";
import "../../../components/ha-related-items";
import "../../../dialogs/more-info/controls/more-info-content";
import {
EntityRegistryEntry,
ExtEntityRegistryEntry,
getExtendedEntityRegistryEntry,
} from "../../../data/entity_registry";
import { PolymerChangedEvent } from "../../../polymer-types";
import { haStyleDialog } from "../../../resources/styles";
import "../../../state-summary/state-card-content";
import { HomeAssistant } from "../../../types";
import "./entity-registry-settings";
import { EntityRegistryDetailDialogParams } from "./show-dialog-entity-registry-detail";
import { EntityRegistryDetailDialogParams } from "./show-dialog-entity-editor";
@customElement("dialog-entity-registry-detail")
export class DialogEntityRegistryDetail extends LitElement {
interface Tabs {
[key: string]: Tab;
}
interface Tab {
component: string;
translationKey: string;
}
@customElement("dialog-entity-editor")
export class DialogEntityEditor extends LitElement {
@property() public hass!: HomeAssistant;
@property() private _params?: EntityRegistryDetailDialogParams;
@property() private _entry?:
| EntityRegistryEntry
| ExtEntityRegistryEntry
| null;
@property() private _curTab?: string;
@property() private _extraTabs: Tabs = {};
@property() private _settingsElementTag?: string;
@query("ha-paper-dialog") private _dialog!: HaPaperDialog;
private _curTabIndex = 0;
@ -39,6 +59,10 @@ export class DialogEntityRegistryDetail extends LitElement {
params: EntityRegistryDetailDialogParams
): Promise<void> {
this._params = params;
this._entry = undefined;
this._settingsElementTag = undefined;
this._extraTabs = {};
this._getEntityReg();
await this.updateComplete;
}
@ -47,11 +71,11 @@ export class DialogEntityRegistryDetail extends LitElement {
}
protected render(): TemplateResult {
if (!this._params) {
if (!this._params || this._entry === undefined) {
return html``;
}
const entry = this._params.entry;
const entityId = this._params.entity_id;
const entry = this._entry;
const stateObj: HassEntity | undefined = this.hass.states[entityId];
return html`
@ -59,6 +83,7 @@ export class DialogEntityRegistryDetail extends LitElement {
with-backdrop
opened
@opened-changed=${this._openedChanged}
@close-dialog=${this.closeDialog}
>
<app-toolbar>
<paper-icon-button
@ -92,6 +117,13 @@ export class DialogEntityRegistryDetail extends LitElement {
<paper-tab id="tab-settings">
${this.hass.localize("ui.dialogs.entity_registry.settings")}
</paper-tab>
${Object.entries(this._extraTabs).map(
([key, tab]) => html`
<paper-tab id=${key}>
${this.hass.localize(tab.translationKey) || key}
</paper-tab>
`
)}
<paper-tab id="tab-related">
${this.hass.localize("ui.dialogs.entity_registry.related")}
</paper-tab>
@ -99,14 +131,16 @@ export class DialogEntityRegistryDetail extends LitElement {
${cache(
this._curTab === "tab-settings"
? entry
? html`
<entity-registry-settings
.hass=${this.hass}
.entry=${entry}
.dialogElement=${this._dialog}
@close-dialog=${this._closeDialog}
></entity-registry-settings>
`
? this._settingsElementTag
? html`
${dynamicElement(this._settingsElementTag, {
hass: this.hass,
entry,
entityId,
dialogElement: this._dialog,
})}
`
: ""
: html`
<paper-dialog-scrollable>
${this.hass.localize(
@ -121,7 +155,6 @@ export class DialogEntityRegistryDetail extends LitElement {
.hass=${this.hass}
.itemId=${entityId}
itemType="entity"
@close-dialog=${this._closeDialog}
></ha-related-items>
</paper-dialog-scrollable>
`
@ -131,6 +164,18 @@ export class DialogEntityRegistryDetail extends LitElement {
`;
}
private async _getEntityReg() {
try {
this._entry = await getExtendedEntityRegistryEntry(
this.hass,
this._params!.entity_id
);
this._loadPlatformSettingTabs();
} catch {
this._entry = null;
}
}
private _handleTabSelected(ev: CustomEvent): void {
if (!ev.detail.value) {
return;
@ -144,15 +189,26 @@ export class DialogEntityRegistryDetail extends LitElement {
fireEvent(this._dialog as HTMLElement, "iron-resize");
}
private async _loadPlatformSettingTabs(): Promise<void> {
if (!this._entry) {
return;
}
if (
!Object.keys(PLATFORMS_WITH_SETTINGS_TAB).includes(this._entry.platform)
) {
this._settingsElementTag = "entity-registry-settings";
return;
}
const tag = PLATFORMS_WITH_SETTINGS_TAB[this._entry.platform];
await import(`./editor-tabs/settings/${tag}`);
this._settingsElementTag = tag;
}
private _openMoreInfo(): void {
fireEvent(this, "hass-more-info", {
entityId: this._params!.entity_id,
});
this._params = undefined;
}
private _closeDialog(): void {
this._params = undefined;
this.closeDialog();
}
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
@ -250,6 +306,6 @@ export class DialogEntityRegistryDetail extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"dialog-entity-registry-detail": DialogEntityRegistryDetail;
"dialog-entity-editor": DialogEntityEditor;
}
}

View File

@ -0,0 +1,260 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
PropertyValues,
query,
TemplateResult,
} from "lit-element";
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { HaPaperDialog } from "../../../../../components/dialog/ha-paper-dialog";
import { ExtEntityRegistryEntry } from "../../../../../data/entity_registry";
import {
deleteInputBoolean,
fetchInputBoolean,
updateInputBoolean,
} from "../../../../../data/input_boolean";
import {
deleteInputDateTime,
fetchInputDateTime,
updateInputDateTime,
} from "../../../../../data/input_datetime";
import {
deleteInputNumber,
fetchInputNumber,
updateInputNumber,
} from "../../../../../data/input_number";
import {
deleteInputSelect,
fetchInputSelect,
updateInputSelect,
} from "../../../../../data/input_select";
import {
deleteInputText,
fetchInputText,
updateInputText,
} from "../../../../../data/input_text";
import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
import { HomeAssistant } from "../../../../../types";
import "../../../helpers/forms/ha-input_boolean-form";
import "../../../helpers/forms/ha-input_text-form";
import "../../../helpers/forms/ha-input_datetime-form";
import "../../../helpers/forms/ha-input_select-form";
import "../../../helpers/forms/ha-input_number-form";
import { Helper } from "../../../helpers/const";
import "../../entity-registry-basic-editor";
// tslint:disable-next-line: no-duplicate-imports
import { HaEntityRegistryBasicEditor } from "../../entity-registry-basic-editor";
const HELPERS = {
input_boolean: {
fetch: fetchInputBoolean,
update: updateInputBoolean,
delete: deleteInputBoolean,
},
input_text: {
fetch: fetchInputText,
update: updateInputText,
delete: deleteInputText,
},
input_number: {
fetch: fetchInputNumber,
update: updateInputNumber,
delete: deleteInputNumber,
},
input_datetime: {
fetch: fetchInputDateTime,
update: updateInputDateTime,
delete: deleteInputDateTime,
},
input_select: {
fetch: fetchInputSelect,
update: updateInputSelect,
delete: deleteInputSelect,
},
};
@customElement("entity-settings-helper-tab")
export class EntityRegistrySettingsHelper extends LitElement {
@property() public hass!: HomeAssistant;
@property() public entry!: ExtEntityRegistryEntry;
@property() public dialogElement!: HaPaperDialog;
@property() private _error?: string;
@property() private _item?: Helper | null;
@property() private _submitting?: boolean;
@property() private _componentLoaded?: boolean;
@query("ha-registry-basic-editor")
private _registryEditor?: HaEntityRegistryBasicEditor;
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
this._componentLoaded = isComponentLoaded(this.hass, this.entry.platform);
}
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (changedProperties.has("entry")) {
this._error = undefined;
this._item = undefined;
this._getItem();
}
}
protected render(): TemplateResult {
if (this._item === undefined) {
return html``;
}
if (!this._componentLoaded) {
return html`
<paper-dialog-scrollable .dialogElement=${this.dialogElement}>
The ${this.entry.platform} component is not loaded, please add it your
configuration. Either by adding 'default_config:' or
'${this.entry.platform}:'.
</paper-dialog-scrollable>
`;
}
if (this._item === null) {
return html`
<paper-dialog-scrollable .dialogElement=${this.dialogElement}>
This entity can not be edited from the UI. Only entities setup from
the UI are editable.
</paper-dialog-scrollable>
`;
}
return html`
<paper-dialog-scrollable .dialogElement=${this.dialogElement}>
${this._error
? html`
<div class="error">${this._error}</div>
`
: ""}
<div class="form">
<div @value-changed=${this._valueChanged}>
${dynamicElement(`ha-${this.entry.platform}-form`, {
hass: this.hass,
item: this._item,
entry: this.entry,
})}
</div>
<ha-registry-basic-editor
.hass=${this.hass}
.entry=${this.entry}
></ha-registry-basic-editor>
</div>
</paper-dialog-scrollable>
<div class="buttons">
<mwc-button
class="warning"
@click=${this._confirmDeleteItem}
.disabled=${this._submitting}
>
${this.hass.localize("ui.dialogs.entity_registry.editor.delete")}
</mwc-button>
<mwc-button
@click=${this._updateItem}
.disabled=${this._submitting || !this._item.name}
>
${this.hass.localize("ui.dialogs.entity_registry.editor.update")}
</mwc-button>
</div>
`;
}
private _valueChanged(ev: CustomEvent): void {
this._error = undefined;
this._item = ev.detail.value;
}
private async _getItem() {
const items = await HELPERS[this.entry.platform].fetch(this.hass!);
this._item = items.find((item) => item.id === this.entry.unique_id) || null;
await this.updateComplete;
fireEvent(this.dialogElement as HTMLElement, "iron-resize");
}
private async _updateItem(): Promise<void> {
if (!this._item) {
return;
}
this._submitting = true;
try {
await HELPERS[this.entry.platform].update(
this.hass!,
this._item.id,
this._item
);
await this._registryEditor?.updateEntry();
fireEvent(this, "close-dialog");
} catch (err) {
this._error = err.message || "Unknown error";
} finally {
this._submitting = false;
}
}
private async _confirmDeleteItem(): Promise<void> {
if (!this._item) {
return;
}
if (
!(await showConfirmationDialog(this, {
text: this.hass.localize(
"ui.dialogs.entity_registry.editor.confirm_delete"
),
}))
) {
return;
}
this._submitting = true;
try {
await HELPERS[this.entry.platform].delete(this.hass!, this._item.id);
fireEvent(this, "close-dialog");
} finally {
this._submitting = false;
}
}
static get styles(): CSSResult {
return css`
:host {
display: block;
padding: 0 !important;
}
.form {
padding-bottom: 24px;
}
.buttons {
display: flex;
justify-content: space-between;
padding: 8px;
margin-bottom: -20px;
}
mwc-button.warning {
--mdc-theme-primary: var(--google-red-500);
}
.error {
color: var(--google-red-500);
}
.row {
margin-top: 8px;
color: var(--primary-text-color);
}
.secondary {
color: var(--secondary-text-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"entity-platform-helper-tab": EntityRegistrySettingsHelper;
}
}

View File

@ -0,0 +1,138 @@
import {
html,
css,
LitElement,
TemplateResult,
property,
customElement,
PropertyValues,
} from "lit-element";
import "@polymer/paper-input/paper-input";
import "../../../components/ha-switch";
import {
ExtEntityRegistryEntry,
EntityRegistryEntryUpdateParams,
updateEntityRegistryEntry,
} from "../../../data/entity_registry";
import { HomeAssistant } from "../../../types";
import { PolymerChangedEvent } from "../../../polymer-types";
// tslint:disable-next-line: no-duplicate-imports
import { HaSwitch } from "../../../components/ha-switch";
import { computeDomain } from "../../../common/entity/compute_domain";
@customElement("ha-registry-basic-editor")
export class HaEntityRegistryBasicEditor extends LitElement {
@property() public hass!: HomeAssistant;
@property() public entry!: ExtEntityRegistryEntry;
@property() private _origEntityId!: string;
@property() private _entityId!: string;
@property() private _disabledBy!: string | null;
@property() private _submitting?: boolean;
public async updateEntry(): Promise<void> {
this._submitting = true;
const params: Partial<EntityRegistryEntryUpdateParams> = {
new_entity_id: this._entityId.trim(),
};
if (this._disabledBy === null || this._disabledBy === "user") {
params.disabled_by = this._disabledBy;
}
try {
await updateEntityRegistryEntry(this.hass!, this._origEntityId, params);
} catch (err) {
throw err;
} finally {
this._submitting = false;
}
}
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (!changedProperties.has("entry")) {
return;
}
if (this.entry) {
this._origEntityId = this.entry.entity_id;
this._entityId = this.entry.entity_id;
this._disabledBy = this.entry.disabled_by;
}
}
protected render(): TemplateResult {
if (
!this.hass ||
!this.entry ||
this.entry.entity_id !== this._origEntityId
) {
return html``;
}
const invalidDomainUpdate =
computeDomain(this._entityId.trim()) !==
computeDomain(this.entry.entity_id);
return html`
<paper-input
.value=${this._entityId}
@value-changed=${this._entityIdChanged}
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.entity_id"
)}
error-message="Domain needs to stay the same"
.invalid=${invalidDomainUpdate}
.disabled=${this._submitting}
></paper-input>
<div class="row">
<ha-switch
.checked=${!this._disabledBy}
@change=${this._disabledByChanged}
>
<div>
<div>
${this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_label"
)}
</div>
<div class="secondary">
${this._disabledBy && this._disabledBy !== "user"
? this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_cause",
"cause",
this.hass.localize(
`config_entry.disabled_by.${this._disabledBy}`
)
)
: ""}
${this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_description"
)}
<br />${this.hass.localize(
"ui.dialogs.entity_registry.editor.note"
)}
</div>
</div>
</ha-switch>
</div>
`;
}
private _entityIdChanged(ev: PolymerChangedEvent<string>): void {
this._entityId = ev.detail.value;
}
private _disabledByChanged(ev: Event): void {
this._disabledBy = (ev.target as HaSwitch).checked ? null : "user";
}
static get styles() {
return css`
.row {
margin-top: 8px;
color: var(--primary-text-color);
}
.secondary {
color: var(--secondary-text-color);
}
`;
}
}

View File

@ -12,26 +12,28 @@ import {
} from "lit-element";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/ha-switch";
import "../../../components/ha-icon-input";
// tslint:disable-next-line: no-duplicate-imports
import { HaSwitch } from "../../../components/ha-switch";
import {
EntityRegistryEntry,
removeEntityRegistryEntry,
updateEntityRegistryEntry,
EntityRegistryEntryUpdateParams,
ExtEntityRegistryEntry,
} from "../../../data/entity_registry";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import { PolymerChangedEvent } from "../../../polymer-types";
import { HomeAssistant } from "../../../types";
import { haStyle } from "../../../resources/styles";
@customElement("entity-registry-settings")
export class EntityRegistrySettings extends LitElement {
@property() public hass!: HomeAssistant;
@property() public entry!: EntityRegistryEntry;
@property() public entry!: ExtEntityRegistryEntry;
@property() public dialogElement!: HTMLElement;
@property() private _name!: string;
@property() private _icon!: string;
@property() private _entityId!: string;
@property() private _disabledBy!: string | null;
@property() private _error?: string;
@ -43,6 +45,7 @@ export class EntityRegistrySettings extends LitElement {
if (changedProperties.has("entry")) {
this._error = undefined;
this._name = this.entry.name || "";
this._icon = this.entry.icon || "";
this._origEntityId = this.entry.entity_id;
this._entityId = this.entry.entity_id;
this._disabledBy = this.entry.disabled_by;
@ -59,7 +62,6 @@ export class EntityRegistrySettings extends LitElement {
const invalidDomainUpdate =
computeDomain(this._entityId.trim()) !==
computeDomain(this.entry.entity_id);
return html`
<paper-dialog-scrollable .dialogElement=${this.dialogElement}>
${!stateObj
@ -83,9 +85,21 @@ export class EntityRegistrySettings extends LitElement {
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.name"
)}
.placeholder=${stateObj ? computeStateName(stateObj) : ""}
.placeholder=${this.entry.original_name}
.disabled=${this._submitting}
></paper-input>
<ha-icon-input
.value=${this._icon}
@value-changed=${this._iconChanged}
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.icon"
)}
.placeholder=${this.entry.original_icon}
.disabled=${this._submitting}
.errorMessage=${this.hass.localize(
"ui.dialogs.entity_registry.editor.icon_error"
)}
></ha-icon-input>
<paper-input
.value=${this._entityId}
@value-changed=${this._entityIdChanged}
@ -153,6 +167,11 @@ export class EntityRegistrySettings extends LitElement {
this._name = ev.detail.value;
}
private _iconChanged(ev: PolymerChangedEvent<string>): void {
this._error = undefined;
this._icon = ev.detail.value;
}
private _entityIdChanged(ev: PolymerChangedEvent<string>): void {
this._error = undefined;
this._entityId = ev.detail.value;
@ -162,6 +181,7 @@ export class EntityRegistrySettings extends LitElement {
this._submitting = true;
const params: Partial<EntityRegistryEntryUpdateParams> = {
name: this._name.trim() || null,
icon: this._icon.trim() || null,
new_entity_id: this._entityId.trim(),
};
if (this._disabledBy === null || this._disabledBy === "user") {
@ -192,7 +212,7 @@ export class EntityRegistrySettings extends LitElement {
try {
await removeEntityRegistryEntry(this.hass!, this._origEntityId);
fireEvent(this as HTMLElement, "close-dialog");
fireEvent(this, "close-dialog");
} finally {
this._submitting = false;
}
@ -202,36 +222,39 @@ export class EntityRegistrySettings extends LitElement {
this._disabledBy = (ev.target as HaSwitch).checked ? null : "user";
}
static get styles(): CSSResult {
return css`
:host {
display: block;
margin-bottom: 0 !important;
padding: 0 !important;
}
.form {
padding-bottom: 24px;
}
.buttons {
display: flex;
justify-content: flex-end;
padding: 8px;
}
mwc-button.warning {
margin-right: auto;
--mdc-theme-primary: var(--google-red-500);
}
.error {
color: var(--google-red-500);
}
.row {
margin-top: 8px;
color: var(--primary-text-color);
}
.secondary {
color: var(--secondary-text-color);
}
`;
static get styles(): CSSResult[] {
return [
haStyle,
css`
:host {
display: block;
margin-bottom: 0 !important;
padding: 0 !important;
}
.form {
padding-bottom: 24px;
}
.buttons {
display: flex;
justify-content: flex-end;
padding: 8px;
}
mwc-button.warning {
margin-right: auto;
--mdc-theme-primary: var(--google-red-500);
}
.error {
color: var(--google-red-500);
}
.row {
margin-top: 8px;
color: var(--primary-text-color);
}
.secondary {
color: var(--secondary-text-color);
}
`,
];
}
}

View File

@ -39,11 +39,11 @@ import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage-data-table";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { HomeAssistant, Route } from "../../../types";
import { DialogEntityRegistryDetail } from "./dialog-entity-registry-detail";
import { DialogEntityEditor } from "./dialog-entity-editor";
import {
loadEntityRegistryDetailDialog,
showEntityRegistryDetailDialog,
} from "./show-dialog-entity-registry-detail";
loadEntityEditorDialog,
showEntityEditorDialog,
} from "./show-dialog-entity-editor";
import { configSections } from "../ha-panel-config";
import { classMap } from "lit-html/directives/class-map";
import { computeStateName } from "../../../common/entity/compute_state_name";
@ -75,7 +75,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
@property() private _selectedEntities: string[] = [];
@query("hass-tabs-subpage-data-table")
private _dataTable!: HaTabsSubpageDataTable;
private getDialog?: () => DialogEntityRegistryDetail | undefined;
private getDialog?: () => DialogEntityEditor | undefined;
private _columns = memoize(
(narrow, _language): DataTableColumnContainer => {
@ -387,33 +387,35 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
.route=${this.route}
.tabs=${configSections.integrations}
.columns=${this._columns(this.narrow, this.hass.language)}
.data=${this._filteredEntities(
this._entities,
this.hass.states,
this._showDisabled,
this._showUnavailable,
this._showReadOnly
)}
.filter=${this._filter}
selectable
@selection-changed=${this._handleSelectionChanged}
@row-click=${this._openEditEntry}
id="entity_id"
.data=${this._filteredEntities(
this._entities,
this.hass.states,
this._showDisabled,
this._showUnavailable,
this._showReadOnly
)}
.filter=${this._filter}
selectable
@selection-changed=${this._handleSelectionChanged}
@row-click=${this._openEditEntry}
id="entity_id"
>
<div class=${classMap({
"search-toolbar": this.narrow,
"table-header": !this.narrow,
})} slot="header">
${headerToolbar}
</div>
</ha-data-table>
<div
class=${classMap({
"search-toolbar": this.narrow,
"table-header": !this.narrow,
})}
slot="header"
>
${headerToolbar}
</div>
</hass-tabs-subpage-data-table>
`;
}
protected firstUpdated(changedProps): void {
super.firstUpdated(changedProps);
loadEntityRegistryDetailDialog();
loadEntityEditorDialog();
}
private _showDisabledChanged() {
@ -524,7 +526,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
const entry = this._entities!.find(
(entity) => entity.entity_id === entityId
);
this.getDialog = showEntityRegistryDetailDialog(this, {
this.getDialog = showEntityEditorDialog(this, {
entry,
entity_id: entityId,
});

View File

@ -1,32 +1,33 @@
import { fireEvent } from "../../../common/dom/fire_event";
import { EntityRegistryEntry } from "../../../data/entity_registry";
import { DialogEntityRegistryDetail } from "./dialog-entity-registry-detail";
import { DialogEntityEditor } from "./dialog-entity-editor";
export interface EntityRegistryDetailDialogParams {
entry?: EntityRegistryEntry;
entity_id: string;
tab?: string;
}
export const loadEntityRegistryDetailDialog = () =>
export const loadEntityEditorDialog = () =>
import(
/* webpackChunkName: "entity-registry-detail-dialog" */ "./dialog-entity-registry-detail"
/* webpackChunkName: "entity-editor-dialog" */ "./dialog-entity-editor"
);
const getDialog = () => {
return document
.querySelector("home-assistant")!
.shadowRoot!.querySelector("dialog-entity-registry-detail") as
| DialogEntityRegistryDetail
.shadowRoot!.querySelector("dialog-entity-editor") as
| DialogEntityEditor
| undefined;
};
export const showEntityRegistryDetailDialog = (
export const showEntityEditorDialog = (
element: HTMLElement,
entityDetailParams: EntityRegistryDetailDialogParams
): (() => DialogEntityRegistryDetail | undefined) => {
): (() => DialogEntityEditor | undefined) => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-entity-registry-detail",
dialogImport: loadEntityRegistryDetailDialog,
dialogTag: "dialog-entity-editor",
dialogImport: loadEntityEditorDialog,
dialogParams: entityDetailParams,
});
return getDialog;

View File

@ -6,10 +6,6 @@ import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { HomeAssistant, Route } from "../../types";
import { CloudStatus, fetchCloudStatus } from "../../data/cloud";
import { listenMediaQuery } from "../../common/dom/media_query";
import {
getOptimisticFrontendUserDataCollection,
CoreFrontendUserData,
} from "../../data/frontend";
import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page";
import { PolymerElement } from "@polymer/polymer";
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
@ -71,6 +67,13 @@ export const configSections: { [name: string]: PageNavigation[] } = {
translationKey: "ui.panel.config.script.caption",
icon: "hass:script-text",
},
{
component: "helpers",
path: "/config/helpers",
translationKey: "ui.panel.config.helpers.caption",
icon: "hass:tools",
core: true,
},
],
persons: [
{
@ -235,6 +238,13 @@ class HaPanelConfig extends HassRouterPage {
/* webpackChunkName: "panel-config-scene" */ "./scene/ha-config-scene"
),
},
helpers: {
tag: "ha-config-helpers",
load: () =>
import(
/* webpackChunkName: "panel-config-helpers" */ "./helpers/ha-config-helpers"
),
},
users: {
tag: "ha-config-users",
load: () =>
@ -268,8 +278,6 @@ class HaPanelConfig extends HassRouterPage {
@property() private _wideSidebar: boolean = false;
@property() private _wide: boolean = false;
@property() private _coreUserData?: CoreFrontendUserData;
@property() private _showAdvanced = false;
@property() private _cloudStatus?: CloudStatus;
private _listeners: Array<() => void> = [];
@ -286,17 +294,6 @@ class HaPanelConfig extends HassRouterPage {
this._wideSidebar = matches;
})
);
this._listeners.push(
getOptimisticFrontendUserDataCollection(
this.hass.connection,
"core"
).subscribe((coreUserData) => {
this._coreUserData = coreUserData || {};
this._showAdvanced = !!(
this._coreUserData && this._coreUserData.showAdvanced
);
})
);
}
public disconnectedCallback() {
@ -337,7 +334,7 @@ class HaPanelConfig extends HassRouterPage {
(el as PolymerElement).setProperties({
route: this.routeTail,
hass: this.hass,
showAdvanced: this._showAdvanced,
showAdvanced: Boolean(this.hass.userData?.showAdvanced),
isWide,
narrow: this.narrow,
cloudStatus: this._cloudStatus,
@ -345,7 +342,7 @@ class HaPanelConfig extends HassRouterPage {
} else {
el.route = this.routeTail;
el.hass = this.hass;
el.showAdvanced = this._showAdvanced;
el.showAdvanced = Boolean(this.hass.userData?.showAdvanced);
el.isWide = isWide;
el.narrow = this.narrow;
el.cloudStatus = this._cloudStatus;

View File

@ -0,0 +1,24 @@
import { InputBoolean } from "../../../data/input_boolean";
import { InputText } from "../../../data/input_text";
import { InputNumber } from "../../../data/input_number";
import { InputSelect } from "../../../data/input_select";
import { InputDateTime } from "../../../data/input_datetime";
export const HELPER_DOMAINS = [
"input_boolean",
"input_text",
"input_number",
"input_datetime",
"input_select",
];
export type Helper =
| InputBoolean
| InputText
| InputNumber
| InputSelect
| InputDateTime;

View File

@ -0,0 +1,197 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import "../../../components/ha-dialog";
import { HomeAssistant } from "../../../types";
import { dynamicElement } from "../../../common/dom/dynamic-element-directive";
import { createInputBoolean } from "../../../data/input_boolean";
import { createInputText } from "../../../data/input_text";
import { createInputNumber } from "../../../data/input_number";
import { createInputDateTime } from "../../../data/input_datetime";
import { createInputSelect } from "../../../data/input_select";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { Helper } from "./const";
import "@polymer/paper-item/paper-icon-item";
import "./forms/ha-input_boolean-form";
import "./forms/ha-input_text-form";
import "./forms/ha-input_datetime-form";
import "./forms/ha-input_select-form";
import "./forms/ha-input_number-form";
import { domainIcon } from "../../../common/entity/domain_icon";
import { classMap } from "lit-html/directives/class-map";
const HELPERS = {
input_boolean: createInputBoolean,
input_text: createInputText,
input_number: createInputNumber,
input_datetime: createInputDateTime,
input_select: createInputSelect,
};
@customElement("dialog-helper-detail")
export class DialogHelperDetail extends LitElement {
@property() public hass!: HomeAssistant;
@property() private _item?: Helper;
@property() private _opened = false;
@property() private _platform?: string;
@property() private _error?: string;
@property() private _submitting = false;
public async showDialog(): Promise<void> {
this._platform = undefined;
this._item = undefined;
this._opened = true;
await this.updateComplete;
}
public closeDialog(): void {
this._opened = false;
this._error = "";
}
protected render(): TemplateResult {
return html`
<ha-dialog
.open=${this._opened}
@closing=${this.closeDialog}
class=${classMap({ "button-left": !this._platform })}
.heading=${this._platform
? this.hass.localize(
"ui.panel.config.helpers.dialog.add_platform",
"platform",
this.hass.localize(
`ui.panel.config.helpers.types.${this._platform}`
) || this._platform
)
: this.hass.localize("ui.panel.config.helpers.dialog.add_helper")}
>
${this._platform
? html`
<div class="form" @value-changed=${this._valueChanged}>
${this._error
? html`
<div class="error">${this._error}</div>
`
: ""}
${dynamicElement(`ha-${this._platform}-form`, {
hass: this.hass,
item: this._item,
new: true,
})}
</div>
<mwc-button
slot="primaryAction"
@click="${this._createItem}"
.disabled=${this._submitting}
>
${this.hass!.localize("ui.panel.config.helpers.dialog.create")}
</mwc-button>
<mwc-button
slot="secondaryAction"
@click="${this._goBack}"
.disabled=${this._submitting}
>
Back
</mwc-button>
`
: html`
${Object.keys(HELPERS).map((platform: string) => {
return html`
<paper-icon-item
.disabled=${!isComponentLoaded(this.hass, platform)}
@click="${this._platformPicked}"
.platform="${platform}"
>
<ha-icon
slot="item-icon"
.icon=${domainIcon(platform)}
></ha-icon>
<span class="item-text">
${this.hass.localize(
`ui.panel.config.helpers.types.${platform}`
) || platform}
</span>
</paper-icon-item>
`;
})}
<mwc-button slot="primaryAction" @click="${this.closeDialog}">
${this.hass!.localize("ui.common.cancel")}
</mwc-button>
`}
</ha-dialog>
`;
}
private _valueChanged(ev: CustomEvent): void {
this._item = ev.detail.value;
}
private async _createItem(): Promise<void> {
if (!this._platform || !this._item) {
return;
}
this._submitting = true;
this._error = "";
try {
await HELPERS[this._platform](this.hass, this._item);
this.closeDialog();
} catch (err) {
this._error = err.message || "Unknown error";
} finally {
this._submitting = false;
}
}
private _platformPicked(ev: Event): void {
this._platform = (ev.currentTarget! as any).platform;
}
private _goBack() {
this._platform = undefined;
}
static get styles(): CSSResult {
return css`
ha-dialog {
--mdc-dialog-title-ink-color: var(--primary-text-color);
--justify-action-buttons: space-between;
}
ha-dialog.button-left {
--justify-action-buttons: flex-start;
}
@media only screen and (min-width: 600px) {
ha-dialog {
--mdc-dialog-min-width: 600px;
}
}
/* make dialog fullscreen on small screens */
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-dialog {
--mdc-dialog-min-width: 100vw;
--mdc-dialog-max-height: 100vh;
--mdc-dialog-shape-radius: 0px;
--vertial-align-dialog: flex-end;
}
}
.error {
color: var(--google-red-500);
}
paper-icon-item {
cursor: pointer;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-helper-detail": DialogHelperDetail;
}
}

View File

@ -0,0 +1,137 @@
import {
LitElement,
html,
css,
CSSResult,
TemplateResult,
property,
customElement,
} from "lit-element";
import "@polymer/paper-input/paper-input";
import "../../../../components/ha-switch";
import "../../../../components/ha-icon-input";
import { HomeAssistant } from "../../../../types";
import { InputBoolean } from "../../../../data/input_boolean";
import { fireEvent } from "../../../../common/dom/fire_event";
import { haStyle } from "../../../../resources/styles";
@customElement("ha-input_boolean-form")
class HaInputBooleanForm extends LitElement {
@property() public hass!: HomeAssistant;
@property() public new?: boolean;
private _item?: InputBoolean;
@property() private _name!: string;
@property() private _icon!: string;
@property() private _initial?: boolean;
set item(item: InputBoolean) {
this._item = item;
if (item) {
this._name = item.name || "";
this._icon = item.icon || "";
this._initial = item.initial;
} else {
this._name = "";
this._icon = "";
}
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
const nameInvalid = !this._name || this._name.trim() === "";
return html`
<div class="form">
<paper-input
.value=${this._name}
.configValue=${"name"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
.errorMessage="${this.hass!.localize(
"ui.dialogs.helper_settings.required_error_msg"
)}"
.invalid=${nameInvalid}
></paper-input>
<ha-icon-input
.value=${this._icon}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
></ha-icon-input>
<br />
${this.hass!.localize(
"ui.dialogs.helper_settings.generic.initial_value_explain"
)}
${this.hass.userData?.showAdvanced
? html`
<div class="row layout horizontal justified">
${this.hass!.localize(
"ui.dialogs.helper_settings.generic.initial_value"
)}:
<ha-switch
.checked=${this._initial}
@change=${this._initialChanged}
></ha-switch>
</div>
`
: ""}
</div>
`;
}
private _initialChanged(ev) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: { ...this._item, initial: ev.target.checked },
});
}
private _valueChanged(ev: CustomEvent) {
if (!this.new && !this._item) {
return;
}
ev.stopPropagation();
const configValue = (ev.target as any).configValue;
const value = ev.detail.value;
if (this[`_${configValue}`] === value) {
return;
}
const newValue = { ...this._item };
if (!value) {
delete newValue[configValue];
} else {
newValue[configValue] = ev.detail.value;
}
fireEvent(this, "value-changed", {
value: newValue,
});
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
.form {
color: var(--primary-text-color);
}
.row {
padding: 16px 0;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-input_boolean-form": HaInputBooleanForm;
}
}

View File

@ -0,0 +1,165 @@
import {
LitElement,
html,
css,
CSSResult,
TemplateResult,
property,
customElement,
} from "lit-element";
import "@polymer/paper-input/paper-input";
import "../../../../components/ha-switch";
import "../../../../components/ha-icon-input";
import { HomeAssistant } from "../../../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { haStyle } from "../../../../resources/styles";
import { InputDateTime } from "../../../../data/input_datetime";
@customElement("ha-input_datetime-form")
class HaInputDateTimeForm extends LitElement {
@property() public hass!: HomeAssistant;
@property() public new?: boolean;
private _item?: InputDateTime;
@property() private _name!: string;
@property() private _icon!: string;
@property() private _initial?: string;
@property() private _hasTime?: boolean;
@property() private _hasDate?: boolean;
set item(item: InputDateTime) {
this._item = item;
if (item) {
this._name = item.name || "";
this._icon = item.icon || "";
this._initial = item.initial;
this._hasTime = item.has_time;
this._hasDate = item.has_date;
} else {
this._name = "";
this._icon = "";
}
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
const nameInvalid = !this._name || this._name.trim() === "";
return html`
<div class="form">
<paper-input
.value=${this._name}
.configValue=${"name"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
.errorMessage="${this.hass!.localize(
"ui.dialogs.helper_settings.required_error_msg"
)}"
.invalid=${nameInvalid}
></paper-input>
<ha-icon-input
.value=${this._icon}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
></ha-icon-input>
<div class="row layout horizontal justified">
${this.hass!.localize(
"ui.dialogs.helper_settings.input_datetime.has_time"
)}:
<ha-switch
.checked=${this._hasTime}
@change=${this._hasTimeChanged}
></ha-switch>
</div>
<div class="row layout horizontal justified">
${this.hass!.localize(
"ui.dialogs.helper_settings.input_datetime.has_date"
)}:
<ha-switch
.checked=${this._hasDate}
@change=${this._hasDateChanged}
></ha-switch>
</div>
${this.hass.userData?.showAdvanced
? html`
<br />
${this.hass!.localize(
"ui.dialogs.helper_settings.generic.initial_value_explain"
)}
<paper-input
.value=${this._initial}
.configValue=${"initial"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.initial_value"
)}
></paper-input>
`
: ""}
</div>
`;
}
private _hasTimeChanged(ev) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: { ...this._item, has_time: ev.target.checked },
});
}
private _hasDateChanged(ev) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: { ...this._item, has_date: ev.target.checked },
});
}
private _valueChanged(ev: CustomEvent) {
if (!this.new && !this._item) {
return;
}
ev.stopPropagation();
const configValue = (ev.target as any).configValue;
const value = ev.detail.value;
if (this[`_${configValue}`] === value) {
return;
}
const newValue = { ...this._item };
if (!value) {
delete newValue[configValue];
} else {
newValue[configValue] = ev.detail.value;
}
fireEvent(this, "value-changed", {
value: newValue,
});
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
.form {
color: var(--primary-text-color);
}
.row {
padding: 16px 0;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-input_datetime-form": HaInputDateTimeForm;
}
}

View File

@ -0,0 +1,214 @@
import {
LitElement,
html,
css,
CSSResult,
TemplateResult,
property,
customElement,
} from "lit-element";
import "@polymer/paper-input/paper-input";
import "../../../../components/ha-switch";
import "../../../../components/ha-icon-input";
import { HomeAssistant } from "../../../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { haStyle } from "../../../../resources/styles";
import { InputNumber } from "../../../../data/input_number";
@customElement("ha-input_number-form")
class HaInputNumberForm extends LitElement {
@property() public hass!: HomeAssistant;
@property() public new?: boolean;
private _item?: Partial<InputNumber>;
@property() private _name!: string;
@property() private _icon!: string;
@property() private _initial?: number;
@property() private _max?: number;
@property() private _min?: number;
@property() private _mode?: string;
@property() private _step?: number;
// tslint:disable-next-line: variable-name
@property() private _unit_of_measurement?: string;
set item(item: InputNumber) {
this._item = item;
if (item) {
this._name = item.name || "";
this._icon = item.icon || "";
this._max = item.max ?? 100;
this._min = item.min ?? 0;
this._initial = item.initial;
this._mode = item.mode || "slider";
this._step = item.step || 1;
this._unit_of_measurement = item.unit_of_measurement;
} else {
this._item = {
min: 0,
max: 0,
};
this._name = "";
this._icon = "";
this._max = 100;
this._min = 0;
this._mode = "slider";
this._step = 1;
}
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
const nameInvalid = !this._name || this._name.trim() === "";
return html`
<div class="form">
<paper-input
.value=${this._name}
.configValue=${"name"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
.errorMessage="${this.hass!.localize(
"ui.dialogs.helper_settings.required_error_msg"
)}"
.invalid=${nameInvalid}
></paper-input>
<ha-icon-input
.value=${this._icon}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
></ha-icon-input>
<paper-input
.value=${this._min}
.configValue=${"min"}
type="number"
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_number.min"
)}
></paper-input>
<paper-input
.value=${this._max}
.configValue=${"max"}
type="number"
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_number.max"
)}
></paper-input>
${this.hass.userData?.showAdvanced
? html`
<div class="layout horizontal center justified">
${this.hass.localize(
"ui.dialogs.helper_settings.input_number.mode"
)}
<paper-radio-group
.selected=${this._mode}
@selected-changed=${this._modeChanged}
>
<paper-radio-button name="slider">
${this.hass.localize(
"ui.dialogs.helper_settings.input_number.slider"
)}
</paper-radio-button>
<paper-radio-button name="box">
${this.hass.localize(
"ui.dialogs.helper_settings.input_number.box"
)}
</paper-radio-button>
</paper-radio-group>
</div>
${this._mode === "slider"
? html`
<paper-input
.value=${this._step}
.configValue=${"step"}
type="number"
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_number.step"
)}
></paper-input>
`
: ""}
<paper-input
.value=${this._unit_of_measurement}
.configValue=${"unit_of_measurement"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_number.unit_of_measurement"
)}
></paper-input>
<br />
${this.hass!.localize(
"ui.dialogs.helper_settings.generic.initial_value_explain"
)}
<paper-input
.value=${this._initial}
.configValue=${"initial"}
type="number"
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.initial_value"
)}
></paper-input>
`
: ""}
</div>
`;
}
private _modeChanged(ev: CustomEvent) {
fireEvent(this, "value-changed", {
value: { ...this._item, mode: ev.detail.value },
});
}
private _valueChanged(ev: CustomEvent) {
if (!this.new && !this._item) {
return;
}
ev.stopPropagation();
const configValue = (ev.target as any).configValue;
const value = ev.detail.value;
if (this[`_${configValue}`] === value) {
return;
}
const newValue = { ...this._item };
if (value === undefined || value === "") {
delete newValue[configValue];
} else {
newValue[configValue] = ev.detail.value;
}
fireEvent(this, "value-changed", {
value: newValue,
});
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
.form {
color: var(--primary-text-color);
}
ha-paper-dropdown-menu {
display: block;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-input_number-form": HaInputNumberForm;
}
}

View File

@ -0,0 +1,240 @@
import {
LitElement,
html,
css,
CSSResult,
TemplateResult,
property,
customElement,
query,
} from "lit-element";
import "@polymer/paper-input/paper-input";
import "../../../../components/ha-switch";
import "../../../../components/ha-icon-input";
import { HomeAssistant } from "../../../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { haStyle } from "../../../../resources/styles";
import { InputSelect } from "../../../../data/input_select";
// tslint:disable-next-line: no-duplicate-imports
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
@customElement("ha-input_select-form")
class HaInputSelectForm extends LitElement {
@property() public hass!: HomeAssistant;
@property() public new?: boolean;
private _item?: InputSelect;
@property() private _name!: string;
@property() private _icon!: string;
@property() private _options: string[] = [];
@property() private _initial?: string;
@query("#option_input") private _optionInput?: PaperInputElement;
set item(item: InputSelect) {
this._item = item;
if (item) {
this._name = item.name || "";
this._icon = item.icon || "";
this._initial = item.initial;
this._options = item.options || [];
} else {
this._name = "";
this._icon = "";
this._options = [];
}
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
const nameInvalid = !this._name || this._name.trim() === "";
return html`
<div class="form">
<paper-input
.value=${this._name}
.configValue=${"name"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
.errorMessage="${this.hass!.localize(
"ui.dialogs.helper_settings.required_error_msg"
)}"
.invalid=${nameInvalid}
></paper-input>
<ha-icon-input
.value=${this._icon}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
></ha-icon-input>
${this.hass!.localize(
"ui.dialogs.helper_settings.input_select.options"
)}:
${this._options.length
? this._options.map((option, index) => {
return html`
<paper-item class="option">
<paper-item-body> ${option} </paper-item-body>
<paper-icon-button
.index=${index}
.title=${this.hass.localize(
"ui.dialogs.helper_settings.input_select.remove_option"
)}
@click=${this._removeOption}
icon="hass:delete"
></paper-icon-button>
</paper-item>
`;
})
: html`
<paper-item>
${this.hass!.localize(
"ui.dialogs.helper_settings.input_select.no_options"
)}
</paper-item>
`}
<div class="layout horizontal bottom">
<paper-input
class="flex-auto"
id="option_input"
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_select.add_option"
)}
@keydown=${this._handleKeyAdd}
></paper-input>
<mwc-button @click=${this._addOption}
>${this.hass!.localize(
"ui.dialogs.helper_settings.input_select.add"
)}</mwc-button
>
</div>
${this.hass.userData?.showAdvanced
? html`
<br />
${this.hass!.localize(
"ui.dialogs.helper_settings.generic.initial_value_explain"
)}
<ha-paper-dropdown-menu
label-float
dynamic-align
.label=${this.hass.localize(
"ui.dialogs.helper_settings.generic.initial_value"
)}
>
<paper-listbox
slot="dropdown-content"
attr-for-selected="item-initial"
.selected=${this._initial}
@selected-changed=${this._initialChanged}
>
${this._options.map(
(option) => html`
<paper-item item-initial=${option}>${option}</paper-item>
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>
`
: ""}
</div>
`;
}
private _initialChanged(ev: CustomEvent) {
fireEvent(this, "value-changed", {
value: { ...this._item, initial: ev.detail.value },
});
}
private _handleKeyAdd(ev: KeyboardEvent) {
ev.stopPropagation();
if (ev.keyCode !== 13) {
return;
}
this._addOption();
}
private _addOption() {
const input = this._optionInput;
if (!input || !input.value) {
return;
}
fireEvent(this, "value-changed", {
value: { ...this._item, options: [...this._options, input.value] },
});
input.value = "";
}
private async _removeOption(ev: Event) {
if (
!(await showConfirmationDialog(this, {
title: "Delete this item?",
text: "Are you sure you want to delete this item?",
}))
) {
return;
}
const index = (ev.target as any).index;
const options = [...this._options];
options.splice(index, 1);
fireEvent(this, "value-changed", {
value: { ...this._item, options },
});
}
private _valueChanged(ev: CustomEvent) {
if (!this.new && !this._item) {
return;
}
ev.stopPropagation();
const configValue = (ev.target as any).configValue;
const value = ev.detail.value;
if (this[`_${configValue}`] === value) {
return;
}
const newValue = { ...this._item };
if (!value) {
delete newValue[configValue];
} else {
newValue[configValue] = ev.detail.value;
}
fireEvent(this, "value-changed", {
value: newValue,
});
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
.form {
color: var(--primary-text-color);
}
.option {
border: 1px solid var(--divider-color);
border-radius: 4px;
margin-top: 4px;
}
mwc-button {
margin-left: 8px;
}
ha-paper-dropdown-menu {
display: block;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-input_select-form": HaInputSelectForm;
}
}

View File

@ -0,0 +1,199 @@
import {
LitElement,
html,
css,
CSSResult,
TemplateResult,
property,
customElement,
} from "lit-element";
import "@polymer/paper-input/paper-input";
import "../../../../components/ha-switch";
import "../../../../components/ha-icon-input";
import { HomeAssistant } from "../../../../types";
import { InputText } from "../../../../data/input_text";
import { fireEvent } from "../../../../common/dom/fire_event";
import { haStyle } from "../../../../resources/styles";
@customElement("ha-input_text-form")
class HaInputTextForm extends LitElement {
@property() public hass!: HomeAssistant;
@property() public new?: boolean;
private _item?: InputText;
@property() private _name!: string;
@property() private _icon!: string;
@property() private _initial?: string;
@property() private _max?: number;
@property() private _min?: number;
@property() private _mode?: string;
@property() private _pattern?: string;
set item(item: InputText) {
this._item = item;
if (item) {
this._name = item.name || "";
this._icon = item.icon || "";
this._max = item.max || 100;
this._min = item.min || 0;
this._initial = item.initial;
this._mode = item.mode || "text";
this._pattern = item.pattern;
} else {
this._name = "";
this._icon = "";
this._max = 100;
this._min = 0;
this._mode = "text";
}
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
const nameInvalid = !this._name || this._name.trim() === "";
return html`
<div class="form">
<paper-input
.value=${this._name}
.configValue=${"name"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
.errorMessage="${this.hass!.localize(
"ui.dialogs.helper_settings.required_error_msg"
)}"
.invalid=${nameInvalid}
></paper-input>
<ha-icon-input
.value=${this._icon}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
></ha-icon-input>
${this.hass.userData?.showAdvanced
? html`
<paper-input
.value=${this._min}
.configValue=${"min"}
type="number"
min="0"
max="255"
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_text.min"
)}
></paper-input>
<paper-input
.value=${this._max}
.configValue=${"max"}
min="0"
max="255"
type="number"
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_text.max"
)}
></paper-input>
<div class="layout horizontal center justified">
${this.hass.localize(
"ui.dialogs.helper_settings.input_text.mode"
)}
<paper-radio-group
.selected=${this._mode}
@selected-changed=${this._modeChanged}
>
<paper-radio-button name="text">
${this.hass.localize(
"ui.dialogs.helper_settings.input_text.text"
)}
</paper-radio-button>
<paper-radio-button name="password">
${this.hass.localize(
"ui.dialogs.helper_settings.input_text.password"
)}
</paper-radio-button>
</paper-radio-group>
</div>
<paper-input
.value=${this._pattern}
.configValue=${"pattern"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_text.pattern"
)}
></paper-input>
<br />
${this.hass!.localize(
"ui.dialogs.helper_settings.generic.initial_value_explain"
)}
<paper-input
.value=${this._initial}
.configValue=${"initial"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.initial_value"
)}
></paper-input>
`
: ""}
</div>
`;
}
private _modeChanged(ev: CustomEvent) {
fireEvent(this, "value-changed", {
value: { ...this._item, mode: ev.detail.value },
});
}
private _valueChanged(ev: CustomEvent) {
if (!this.new && !this._item) {
return;
}
ev.stopPropagation();
const configValue = (ev.target as any).configValue;
const value = ev.detail.value;
if (this[`_${configValue}`] === value) {
return;
}
const newValue = { ...this._item };
if (!value) {
delete newValue[configValue];
} else {
newValue[configValue] = ev.detail.value;
}
fireEvent(this, "value-changed", {
value: newValue,
});
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
.form {
color: var(--primary-text-color);
}
.row {
padding: 16px 0;
}
ha-paper-dropdown-menu {
display: block;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-input_text-form": HaInputTextForm;
}
}

View File

@ -0,0 +1,184 @@
import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-tooltip/paper-tooltip";
import { HassEntity } from "home-assistant-js-websocket";
import {
customElement,
html,
LitElement,
property,
PropertyValues,
TemplateResult,
CSSResult,
css,
} from "lit-element";
import memoize from "memoize-one";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import "../../../common/search/search-input";
import {
DataTableColumnContainer,
RowClickedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/ha-icon";
import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage-data-table";
import { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config";
import { showEntityEditorDialog } from "../entities/show-dialog-entity-editor";
import { showHelperDetailDialog } from "./show-dialog-helper-detail";
import { HELPER_DOMAINS } from "./const";
@customElement("ha-config-helpers")
export class HaConfigHelpers extends LitElement {
@property() public hass!: HomeAssistant;
@property() public isWide!: boolean;
@property() public narrow!: boolean;
@property() public route!: Route;
@property() private _stateItems: HassEntity[] = [];
private _columns = memoize(
(_language): DataTableColumnContainer => {
return {
icon: {
title: "",
type: "icon",
template: (icon) => html`
<ha-icon slot="item-icon" .icon=${icon}></ha-icon>
`,
},
name: {
title: this.hass.localize(
"ui.panel.config.helpers.picker.headers.name"
),
sortable: true,
filterable: true,
direction: "asc",
template: (name, item: any) =>
html`
${name}
<div style="color: var(--secondary-text-color)">
${item.entity_id}
</div>
`,
},
type: {
title: this.hass.localize(
"ui.panel.config.helpers.picker.headers.type"
),
sortable: true,
filterable: true,
template: (type) =>
html`
${this.hass.localize(`ui.panel.config.helpers.types.${type}`) ||
type}
`,
},
};
}
);
private _getItems = memoize((stateItems: HassEntity[]) => {
return stateItems.map((state) => {
return {
id: state.entity_id,
icon: state.attributes.icon,
name: state.attributes.friendly_name || "",
entity_id: state.entity_id,
editable: state.attributes.editable,
type: computeStateDomain(state),
};
});
});
protected render(): TemplateResult {
if (!this.hass || this._stateItems === undefined) {
return html`
<hass-loading-screen></hass-loading-screen>
`;
}
return html`
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.route=${this.route}
.tabs=${configSections.automation}
.columns=${this._columns(this.hass.language)}
.data=${this._getItems(this._stateItems)}
@row-click=${this._openEditDialog}
>
</hass-tabs-subpage-data-table>
<ha-fab
?is-wide=${this.isWide}
?narrow=${this.narrow}
icon="hass:plus"
title="${this.hass.localize(
"ui.panel.config.helpers.picker.add_helper"
)}"
@click=${this._createHelpler}
></ha-fab>
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._getStates();
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (oldHass && this._stateItems) {
this._getStates(oldHass);
}
}
private _getStates(oldHass?: HomeAssistant) {
let changed = false;
const tempStates = Object.values(this.hass!.states).filter((entity) => {
if (!HELPER_DOMAINS.includes(computeStateDomain(entity))) {
return false;
}
if (oldHass?.states[entity.entity_id] !== entity) {
changed = true;
}
return true;
});
if (changed || this._stateItems.length !== tempStates.length) {
this._stateItems = tempStates;
}
}
private async _openEditDialog(ev: CustomEvent): Promise<void> {
const entityId = (ev.detail as RowClickedEvent).id;
showEntityEditorDialog(this, {
entity_id: entityId,
});
}
private _createHelpler() {
showHelperDetailDialog(this);
}
static get styles(): CSSResult {
return css`
ha-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
}
ha-fab[is-wide] {
bottom: 24px;
right: 24px;
}
ha-fab[narrow] {
bottom: 84px;
}
`;
}
}

View File

@ -0,0 +1,14 @@
import { fireEvent } from "../../../common/dom/fire_event";
export const loadHelperDetailDialog = () =>
import(
/* webpackChunkName: "helper-detail-dialog" */ "./dialog-helper-detail"
);
export const showHelperDetailDialog = (element: HTMLElement) => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-helper-detail",
dialogImport: loadHelperDetailDialog,
dialogParams: {},
});
};

View File

@ -148,6 +148,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
${this._config.show_icon
? html`
<ha-icon
tabindex="-1"
data-domain=${ifDefined(
this._config.state_color && stateObj
? computeStateDomain(stateObj)
@ -170,7 +171,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
: ""}
${this._config.show_name
? html`
<span>
<span tabindex="-1">
${this._config.name ||
(stateObj ? computeStateName(stateObj) : "")}
</span>
@ -224,6 +225,11 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
color: var(--paper-item-icon-color, #44739e);
}
ha-icon,
span {
outline: none;
}
${iconColorCSS}
`;
}

View File

@ -8,6 +8,8 @@ import {
import "@polymer/paper-input/paper-input";
import "../../components/hui-theme-select-editor";
import "../../../../components/ha-icon-input";
import "../../components/hui-entity-editor";
import { struct } from "../../common/structs/struct";
@ -17,6 +19,7 @@ import { LovelaceCardEditor } from "../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { configElementStyle } from "./config-elements-style";
import { LightCardConfig } from "../../cards/types";
import { stateIcon } from "../../../../common/entity/state_icon";
const cardConfigStruct = struct({
type: "string",
@ -34,8 +37,7 @@ export class HuiLightCardEditor extends LitElement
@property() private _config?: LightCardConfig;
public setConfig(config: LightCardConfig): void {
config = cardConfigStruct(config);
this._config = config;
this._config = cardConfigStruct(config);
}
get _name(): string {
@ -86,16 +88,18 @@ export class HuiLightCardEditor extends LitElement
.configValue="${"name"}"
@value-changed="${this._valueChanged}"
></paper-input>
<paper-input
<ha-icon-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.icon"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value="${this._icon}"
.placeholder=${this._icon ||
stateIcon(this.hass.states[this._entity])}
.configValue="${"icon"}"
@value-changed="${this._valueChanged}"
></paper-input>
></ha-icon-input>
</div>
<hui-theme-select-editor

View File

@ -20,7 +20,7 @@ import { computeStateName } from "../../../common/entity/compute_state_name";
import { HomeAssistant, InputSelectEntity } from "../../../types";
import { LovelaceRow } from "./types";
import { setInputSelectOption } from "../../../data/input-select";
import { setInputSelectOption } from "../../../data/input_select";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { forwardHaptic } from "../../../data/haptics";
import { stopPropagation } from "../../../common/dom/stop_propagation";

View File

@ -118,6 +118,9 @@ export const haStyle = css`
.layout.center-center {
align-items: center;
}
.layout.bottom {
align-items: flex-end;
}
.layout.center-justified,
.layout.center-center {
justify-content: center;

View File

@ -18,7 +18,7 @@ import "../components/entity/state-badge";
import { computeStateName } from "../common/entity/compute_state_name";
import { HomeAssistant, InputSelectEntity } from "../types";
import { setInputSelectOption } from "../data/input-select";
import { setInputSelectOption } from "../data/input_select";
import { PolymerIronSelectEvent } from "../polymer-types";
import { stopPropagation } from "../common/dom/stop_propagation";

View File

@ -20,6 +20,7 @@ import { fireEvent } from "../common/dom/fire_event";
import { Constructor, ServiceCallResponse } from "../types";
import { HassBaseEl } from "./hass-base-mixin";
import { broadcastConnectionStatus } from "../data/connection-status";
import { subscribeFrontendUserData } from "../data/frontend";
export const connectionMixin = <T extends Constructor<HassBaseEl>>(
superClass: T
@ -141,6 +142,9 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
subscribeConfig(conn, (config) => this._updateHass({ config }));
subscribeServices(conn, (services) => this._updateHass({ services }));
subscribePanels(conn, (panels) => this._updateHass({ panels }));
subscribeFrontendUserData(conn, "core", (userData) =>
this._updateHass({ userData })
);
}
protected hassReconnected() {

View File

@ -644,6 +644,8 @@
"no_unique_id": "This entity does not have a unique ID, therefore it's settings can not be managed from the UI.",
"editor": {
"name": "Name Override",
"icon": "Icon Override",
"icon_error": "Icons should be in the format 'prefix:iconname', e.g. 'mdi:home'",
"entity_id": "Entity ID",
"unavailable": "This entity is not currently available.",
"enabled_label": "Enable entity",
@ -655,6 +657,44 @@
"note": "Note: this might not work yet with all integrations."
}
},
"helper_settings": {
"not_editable": "Not editable",
"not_editable_text": "This entity can't be changed from the UI because it is defined in configuration.yaml.",
"required_error_msg": "This field is required",
"generic": {
"name": "Name",
"icon": "Icon",
"initial_value": "Initial value at start",
"initial_value_explain": "The value the element will have when Home Assistant starts. When left empty, the value will be restored to it's previous value."
},
"input_datetime": {
"has_time": "Time",
"has_date": "Date"
},
"input_text": {
"min": "Minimum lenght",
"max": "Maximum lenght",
"mode": "Display mode",
"text": "Text",
"password": "Password",
"pattern": "Regex pattern for client-side validation"
},
"input_number": {
"min": "Minimum value",
"max": "Maximum value",
"mode": "Display mode",
"box": "Input field",
"slider": "Slider",
"step": "Step size of the slider",
"unit_of_measurement": "Unit of measurement"
},
"input_select": {
"options": "Options",
"add_option": "Add option",
"no_options": "There are no options yet.",
"add": "Add"
}
},
"options_flow": {
"form": {
"header": "Options"
@ -757,6 +797,30 @@
"create": "CREATE"
}
},
"helpers": {
"caption": "Helpers",
"description": "Elements that can help build automations.",
"types": {
"input_text": "Text",
"input_number": "Number",
"input_select": "Dropdown",
"input_boolean": "Toggle",
"input_datetime": "Date and/or time"
},
"picker": {
"headers": {
"name": "Name",
"type": "Type",
"editable": "Editable"
},
"add_helper": "Add helper"
},
"dialog": {
"create": "Create",
"add_helper": "Add helper",
"add_platform": "Add {platform}"
}
},
"core": {
"caption": "General",
"description": "Change your general Home Assistant configuration",

View File

@ -10,6 +10,7 @@ import {
} from "home-assistant-js-websocket";
import { LocalizeFunc } from "./common/translations/localize";
import { ExternalMessaging } from "./external_app/external_messaging";
import { CoreFrontendUserData } from "./data/frontend";
declare global {
var __DEV__: boolean;
@ -155,6 +156,7 @@ export interface HomeAssistant {
dockedSidebar: "docked" | "always_hidden" | "auto";
moreInfoEntityId: string | null;
user?: CurrentUser;
userData?: CoreFrontendUserData | null;
hassUrl(path?): string;
callService(
domain: string,