mirror of
https://github.com/home-assistant/frontend.git
synced 2025-04-24 13:27:22 +00:00
Add tag config panel (#6601)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
parent
a0b28e8ad1
commit
d7e409b042
@ -90,6 +90,12 @@ export interface ZoneTrigger {
|
||||
event: "enter" | "leave";
|
||||
}
|
||||
|
||||
export interface TagTrigger {
|
||||
platform: "tag";
|
||||
tag_id: string;
|
||||
device_id?: string;
|
||||
}
|
||||
|
||||
export interface TimeTrigger {
|
||||
platform: "time";
|
||||
at: string;
|
||||
@ -116,6 +122,7 @@ export type Trigger =
|
||||
| TimePatternTrigger
|
||||
| WebhookTrigger
|
||||
| ZoneTrigger
|
||||
| TagTrigger
|
||||
| TimeTrigger
|
||||
| TemplateTrigger
|
||||
| EventTrigger
|
||||
|
57
src/data/tag.ts
Normal file
57
src/data/tag.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
import { HassEventBase } from "home-assistant-js-websocket";
|
||||
|
||||
export const EVENT_TAG_SCANNED = "tag_scanned";
|
||||
|
||||
export interface TagScannedEvent extends HassEventBase {
|
||||
event_type: "tag_scanned";
|
||||
data: {
|
||||
tag_id: string;
|
||||
device_id?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
last_scanned?: string;
|
||||
}
|
||||
|
||||
export interface UpdateTagParams {
|
||||
name?: Tag["name"];
|
||||
description?: Tag["description"];
|
||||
}
|
||||
|
||||
export const fetchTags = async (hass: HomeAssistant) =>
|
||||
hass.callWS<Tag[]>({
|
||||
type: "tag/list",
|
||||
});
|
||||
|
||||
export const createTag = async (
|
||||
hass: HomeAssistant,
|
||||
params: UpdateTagParams,
|
||||
tagId?: string
|
||||
) =>
|
||||
hass.callWS<Tag>({
|
||||
type: "tag/create",
|
||||
tag_id: tagId,
|
||||
...params,
|
||||
});
|
||||
|
||||
export const updateTag = async (
|
||||
hass: HomeAssistant,
|
||||
tagId: string,
|
||||
params: UpdateTagParams
|
||||
) =>
|
||||
hass.callWS<Tag>({
|
||||
...params,
|
||||
type: "tag/update",
|
||||
tag_id: tagId,
|
||||
});
|
||||
|
||||
export const deleteTag = async (hass: HomeAssistant, tagId: string) =>
|
||||
hass.callWS<void>({
|
||||
type: "tag/delete",
|
||||
tag_id: tagId,
|
||||
});
|
@ -2,6 +2,7 @@ import { ExternalMessaging } from "./external_messaging";
|
||||
|
||||
export interface ExternalConfig {
|
||||
hasSettingsScreen: boolean;
|
||||
canWriteTag: boolean;
|
||||
}
|
||||
|
||||
export const getExternalConfig = (
|
||||
|
@ -34,6 +34,7 @@ import "./types/ha-automation-trigger-time";
|
||||
import "./types/ha-automation-trigger-time_pattern";
|
||||
import "./types/ha-automation-trigger-webhook";
|
||||
import "./types/ha-automation-trigger-zone";
|
||||
import "./types/ha-automation-trigger-tag";
|
||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
|
||||
@ -46,6 +47,7 @@ const OPTIONS = [
|
||||
"mqtt",
|
||||
"numeric_state",
|
||||
"sun",
|
||||
"tag",
|
||||
"template",
|
||||
"time",
|
||||
"time_pattern",
|
||||
|
@ -0,0 +1,72 @@
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import { TagTrigger } from "../../../../../data/automation";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import { TriggerElement } from "../ha-automation-trigger-row";
|
||||
import { Tag, fetchTags } from "../../../../../data/tag";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
|
||||
@customElement("ha-automation-trigger-tag")
|
||||
export class HaTagTrigger extends LitElement implements TriggerElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public trigger!: TagTrigger;
|
||||
|
||||
@internalProperty() private _tags: Tag[] = [];
|
||||
|
||||
public static get defaultConfig() {
|
||||
return { tag_id: "" };
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this._fetchTags();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const { tag_id } = this.trigger;
|
||||
return html`
|
||||
<ha-paper-dropdown-menu
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.type.tag.label"
|
||||
)}
|
||||
?disabled=${this._tags.length === 0}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
.selected=${tag_id}
|
||||
attr-for-selected="tag_id"
|
||||
@iron-select=${this._tagChanged}
|
||||
>
|
||||
${this._tags.map(
|
||||
(tag) => html`
|
||||
<paper-item tag_id=${tag.id} .tag=${tag}>
|
||||
${tag.name || tag.id}
|
||||
</paper-item>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchTags() {
|
||||
this._tags = await fetchTags(this.hass);
|
||||
}
|
||||
|
||||
private _tagChanged(ev) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.trigger,
|
||||
tag_id: ev.detail.item.tag.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
@ -32,6 +32,7 @@ import {
|
||||
mdiInformation,
|
||||
mdiMathLog,
|
||||
mdiPencil,
|
||||
mdiNfcVariant,
|
||||
} from "@mdi/js";
|
||||
|
||||
declare global {
|
||||
@ -99,6 +100,15 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
||||
core: true,
|
||||
},
|
||||
],
|
||||
experimental: [
|
||||
{
|
||||
component: "tags",
|
||||
path: "/config/tags",
|
||||
translationKey: "ui.panel.config.tags.caption",
|
||||
iconPath: mdiNfcVariant,
|
||||
core: true,
|
||||
},
|
||||
],
|
||||
lovelace: [
|
||||
{
|
||||
component: "lovelace",
|
||||
@ -195,6 +205,13 @@ class HaPanelConfig extends HassRouterPage {
|
||||
/* webpackChunkName: "panel-config-automation" */ "./automation/ha-config-automation"
|
||||
),
|
||||
},
|
||||
tags: {
|
||||
tag: "ha-config-tags",
|
||||
load: () =>
|
||||
import(
|
||||
/* webpackChunkName: "panel-config-tags" */ "./tags/ha-config-tags"
|
||||
),
|
||||
},
|
||||
cloud: {
|
||||
tag: "ha-config-cloud",
|
||||
load: () =>
|
||||
|
209
src/panels/config/tags/dialog-tag-detail.ts
Normal file
209
src/panels/config/tags/dialog-tag-detail.ts
Normal file
@ -0,0 +1,209 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
customElement,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/map/ha-location-editor";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import { TagDetailDialogParams } from "./show-dialog-tag-detail";
|
||||
import { UpdateTagParams, Tag } from "../../../data/tag";
|
||||
|
||||
@customElement("dialog-tag-detail")
|
||||
class DialogTagDetail extends LitElement implements HassDialog {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@internalProperty() private _id?: string;
|
||||
|
||||
@internalProperty() private _name!: string;
|
||||
|
||||
@internalProperty() private _error?: string;
|
||||
|
||||
@internalProperty() private _params?: TagDetailDialogParams;
|
||||
|
||||
@internalProperty() private _submitting = false;
|
||||
|
||||
public showDialog(params: TagDetailDialogParams): void {
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
if (this._params.entry) {
|
||||
this._name = this._params.entry.name || "";
|
||||
} else {
|
||||
this._id = "";
|
||||
this._name = "";
|
||||
}
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._params) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this._params.entry
|
||||
? this._params.entry.name || this._params.entry.id
|
||||
: this.hass!.localize("ui.panel.config.tags.detail.new_tag")
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||
<div class="form">
|
||||
${this._params.entry
|
||||
? html`${this.hass!.localize(
|
||||
"ui.panel.config.tags.detail.tag_id"
|
||||
)}:
|
||||
${this._params.entry.id}`
|
||||
: ""}
|
||||
<paper-input
|
||||
dialogInitialFocus
|
||||
.value=${this._name}
|
||||
.configValue=${"name"}
|
||||
@value-changed=${this._valueChanged}
|
||||
.label="${this.hass!.localize(
|
||||
"ui.panel.config.tags.detail.name"
|
||||
)}"
|
||||
.errorMessage="${this.hass!.localize(
|
||||
"ui.panel.config.tags.detail.required_error_msg"
|
||||
)}"
|
||||
required
|
||||
auto-validate
|
||||
></paper-input>
|
||||
${!this._params.entry
|
||||
? html` <paper-input
|
||||
.value=${this._id}
|
||||
.configValue=${"id"}
|
||||
@value-changed=${this._valueChanged}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.tags.detail.tag_id"
|
||||
)}
|
||||
.placeholder=${this.hass!.localize(
|
||||
"ui.panel.config.tags.detail.tag_id_placeholder"
|
||||
)}
|
||||
></paper-input>`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
${this._params.entry
|
||||
? html`
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
class="warning"
|
||||
@click="${this._deleteEntry}"
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass!.localize("ui.panel.config.tags.detail.delete")}
|
||||
</mwc-button>
|
||||
`
|
||||
: html``}
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click="${this._updateEntry}"
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this._params.entry
|
||||
? this.hass!.localize("ui.panel.config.tags.detail.update")
|
||||
: this.hass!.localize("ui.panel.config.tags.detail.create")}
|
||||
</mwc-button>
|
||||
${this._params.openWrite && !this._params.entry
|
||||
? html` <mwc-button
|
||||
slot="primaryAction"
|
||||
@click="${this._updateWriteEntry}"
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.tags.detail.create_and_write"
|
||||
)}
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
const configValue = (ev.target as any).configValue;
|
||||
|
||||
this._error = undefined;
|
||||
this[`_${configValue}`] = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _updateEntry() {
|
||||
this._submitting = true;
|
||||
let newValue: Tag | undefined;
|
||||
try {
|
||||
const values: UpdateTagParams = {
|
||||
name: this._name.trim(),
|
||||
};
|
||||
if (this._params!.entry) {
|
||||
newValue = await this._params!.updateEntry!(values);
|
||||
} else {
|
||||
newValue = await this._params!.createEntry(values, this._id);
|
||||
}
|
||||
this._params = undefined;
|
||||
} catch (err) {
|
||||
this._error = err ? err.message : "Unknown error";
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
return newValue;
|
||||
}
|
||||
|
||||
private async _updateWriteEntry() {
|
||||
const tag = await this._updateEntry();
|
||||
if (!tag) {
|
||||
return;
|
||||
}
|
||||
this._params?.openWrite!(tag);
|
||||
}
|
||||
|
||||
private async _deleteEntry() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
if (await this._params!.removeEntry!()) {
|
||||
this._params = undefined;
|
||||
}
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-tag-detail": DialogTagDetail;
|
||||
}
|
||||
}
|
293
src/panels/config/tags/ha-config-tags.ts
Normal file
293
src/panels/config/tags/ha-config-tags.ts
Normal file
@ -0,0 +1,293 @@
|
||||
import "@material/mwc-fab";
|
||||
import { mdiCog, mdiContentDuplicate, mdiPlus, mdiRobot } from "@mdi/js";
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-relative-time";
|
||||
import {
|
||||
createTag,
|
||||
deleteTag,
|
||||
EVENT_TAG_SCANNED,
|
||||
fetchTags,
|
||||
Tag,
|
||||
TagScannedEvent,
|
||||
updateTag,
|
||||
UpdateTagParams,
|
||||
} from "../../../data/tag";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showTagDetailDialog } from "./show-dialog-tag-detail";
|
||||
import "./tag-image";
|
||||
import { getExternalConfig } from "../../../external_app/external_config";
|
||||
import { showAutomationEditor, TagTrigger } from "../../../data/automation";
|
||||
|
||||
export interface TagRowData extends Tag {
|
||||
last_scanned_datetime: Date | null;
|
||||
}
|
||||
|
||||
@customElement("ha-config-tags")
|
||||
export class HaConfigTags extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public isWide!: boolean;
|
||||
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
@property() public route!: Route;
|
||||
|
||||
@internalProperty() private _tags: Tag[] = [];
|
||||
|
||||
@internalProperty() private _canWriteTags = false;
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(
|
||||
narrow: boolean,
|
||||
canWriteTags: boolean,
|
||||
_language
|
||||
): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer = {
|
||||
icon: {
|
||||
title: "",
|
||||
type: "icon",
|
||||
template: (_icon, tag) => html`<tag-image .tag=${tag}></tag-image>`,
|
||||
},
|
||||
display_name: {
|
||||
title: this.hass.localize("ui.panel.config.tags.headers.name"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
template: (name, tag: any) => html`${name}
|
||||
${narrow
|
||||
? html`<div class="secondary">
|
||||
${tag.last_scanned
|
||||
? html`<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetimeObj=${tag.last_scanned_datetime}
|
||||
></ha-relative-time>`
|
||||
: this.hass.localize("ui.components.relative_time.never")}
|
||||
</div>`
|
||||
: ""}`,
|
||||
},
|
||||
};
|
||||
if (!narrow) {
|
||||
columns.last_scanned_datetime = {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.tags.headers.last_scanned"
|
||||
),
|
||||
sortable: true,
|
||||
direction: "desc",
|
||||
width: "20%",
|
||||
template: (last_scanned_datetime) => html`
|
||||
${last_scanned_datetime
|
||||
? html`<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetimeObj=${last_scanned_datetime}
|
||||
></ha-relative-time>`
|
||||
: this.hass.localize("ui.components.relative_time.never")}
|
||||
`,
|
||||
};
|
||||
}
|
||||
if (canWriteTags) {
|
||||
columns.write = {
|
||||
title: "",
|
||||
type: "icon-button",
|
||||
template: (_write, tag: any) => html` <mwc-icon-button
|
||||
.tag=${tag}
|
||||
@click=${(ev: Event) =>
|
||||
this._openWrite((ev.currentTarget as any).tag)}
|
||||
title=${this.hass.localize("ui.panel.config.tags.write")}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiContentDuplicate}></ha-svg-icon>
|
||||
</mwc-icon-button>`,
|
||||
};
|
||||
}
|
||||
columns.automation = {
|
||||
title: "",
|
||||
type: "icon-button",
|
||||
template: (_automation, tag: any) => html` <mwc-icon-button
|
||||
.tag=${tag}
|
||||
@click=${(ev: Event) =>
|
||||
this._createAutomation((ev.currentTarget as any).tag)}
|
||||
title=${this.hass.localize("ui.panel.config.tags.create_automation")}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiRobot}></ha-svg-icon>
|
||||
</mwc-icon-button>`,
|
||||
};
|
||||
columns.edit = {
|
||||
title: "",
|
||||
type: "icon-button",
|
||||
template: (_settings, tag: any) => html` <mwc-icon-button
|
||||
.tag=${tag}
|
||||
@click=${(ev: Event) =>
|
||||
this._openDialog((ev.currentTarget as any).tag)}
|
||||
title=${this.hass.localize("ui.panel.config.tags.edit")}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiCog}></ha-svg-icon>
|
||||
</mwc-icon-button>`,
|
||||
};
|
||||
return columns;
|
||||
}
|
||||
);
|
||||
|
||||
private _data = memoizeOne((tags: Tag[]): TagRowData[] => {
|
||||
return tags.map((tag) => {
|
||||
return {
|
||||
...tag,
|
||||
display_name: tag.name || tag.id,
|
||||
last_scanned_datetime: tag.last_scanned
|
||||
? new Date(tag.last_scanned)
|
||||
: null,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this._fetchTags();
|
||||
if (this.hass && this.hass.auth.external) {
|
||||
getExternalConfig(this.hass.auth.external).then((conf) => {
|
||||
this._canWriteTags = conf.canWriteTag;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected hassSubscribe() {
|
||||
return [
|
||||
this.hass.connection.subscribeEvents<TagScannedEvent>((ev) => {
|
||||
const foundTag = this._tags.find((tag) => tag.id === ev.data.tag_id);
|
||||
if (!foundTag) {
|
||||
this._fetchTags();
|
||||
return;
|
||||
}
|
||||
foundTag.last_scanned = ev.time_fired;
|
||||
this._tags = [...this._tags];
|
||||
}, EVENT_TAG_SCANNED),
|
||||
];
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.experimental}
|
||||
.columns=${this._columns(
|
||||
this.narrow,
|
||||
this._canWriteTags,
|
||||
this.hass.language
|
||||
)}
|
||||
.data=${this._data(this._tags)}
|
||||
.noDataText=${this.hass.localize("ui.panel.config.tags.no_tags")}
|
||||
hasFab
|
||||
>
|
||||
<mwc-fab
|
||||
slot="fab"
|
||||
title=${this.hass.localize("ui.panel.config.tags.add_tag")}
|
||||
@click=${this._addTag}
|
||||
>
|
||||
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
|
||||
</mwc-fab>
|
||||
</hass-tabs-subpage-data-table>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchTags() {
|
||||
this._tags = await fetchTags(this.hass);
|
||||
}
|
||||
|
||||
private _openWrite(tag: Tag) {
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "tag/write",
|
||||
payload: { name: tag.name || null, tag: tag.id },
|
||||
});
|
||||
}
|
||||
|
||||
private _createAutomation(tag: Tag) {
|
||||
const data = {
|
||||
alias: this.hass.localize(
|
||||
"ui.panel.config.tags.automation_title",
|
||||
"name",
|
||||
tag.name || tag.id
|
||||
),
|
||||
trigger: [{ platform: "tag", tag_id: tag.id } as TagTrigger],
|
||||
};
|
||||
showAutomationEditor(this, data);
|
||||
}
|
||||
|
||||
private _addTag() {
|
||||
this._openDialog();
|
||||
}
|
||||
|
||||
private _openDialog(entry?: Tag) {
|
||||
showTagDetailDialog(this, {
|
||||
entry,
|
||||
openWrite: this._canWriteTags ? (tag) => this._openWrite(tag) : undefined,
|
||||
createEntry: (values, tagId) => this._createTag(values, tagId),
|
||||
updateEntry: entry
|
||||
? (values) => this._updateTag(entry, values)
|
||||
: undefined,
|
||||
removeEntry: entry ? () => this._removeTag(entry) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private async _createTag(
|
||||
values: Partial<UpdateTagParams>,
|
||||
tagId?: string
|
||||
): Promise<Tag> {
|
||||
const newTag = await createTag(this.hass, values, tagId);
|
||||
this._tags = [...this._tags, newTag];
|
||||
return newTag;
|
||||
}
|
||||
|
||||
private async _updateTag(
|
||||
selectedTag: Tag,
|
||||
values: Partial<UpdateTagParams>
|
||||
): Promise<Tag> {
|
||||
const updated = await updateTag(this.hass, selectedTag.id, values);
|
||||
this._tags = this._tags.map((tag) =>
|
||||
tag.id === selectedTag.id ? updated : tag
|
||||
);
|
||||
return updated;
|
||||
}
|
||||
|
||||
private async _removeTag(selectedTag: Tag) {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: "Remove tag?",
|
||||
text: `Are you sure you want to remove tag ${
|
||||
selectedTag.name || selectedTag.id
|
||||
}?`,
|
||||
dismissText: this.hass!.localize("ui.common.no"),
|
||||
confirmText: this.hass!.localize("ui.common.yes"),
|
||||
}))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await deleteTag(this.hass, selectedTag.id);
|
||||
this._tags = this._tags.filter((tag) => tag.id !== selectedTag.id);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-tags": HaConfigTags;
|
||||
}
|
||||
}
|
27
src/panels/config/tags/show-dialog-tag-detail.ts
Normal file
27
src/panels/config/tags/show-dialog-tag-detail.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { Tag, UpdateTagParams } from "../../../data/tag";
|
||||
|
||||
export interface TagDetailDialogParams {
|
||||
entry?: Tag;
|
||||
openWrite?: (tag: Tag) => void;
|
||||
createEntry: (
|
||||
values: Partial<UpdateTagParams>,
|
||||
tagId?: string
|
||||
) => Promise<Tag>;
|
||||
updateEntry?: (updates: Partial<UpdateTagParams>) => Promise<Tag>;
|
||||
removeEntry?: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export const loadTagDetailDialog = () =>
|
||||
import(/* webpackChunkName: "dialog-tag-detail" */ "./dialog-tag-detail");
|
||||
|
||||
export const showTagDetailDialog = (
|
||||
element: HTMLElement,
|
||||
systemLogDetailParams: TagDetailDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-tag-detail",
|
||||
dialogImport: loadTagDetailDialog,
|
||||
dialogParams: systemLogDetailParams,
|
||||
});
|
||||
};
|
93
src/panels/config/tags/tag-image.ts
Normal file
93
src/panels/config/tags/tag-image.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import {
|
||||
property,
|
||||
customElement,
|
||||
LitElement,
|
||||
html,
|
||||
CSSResult,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { mdiNfcVariant } from "@mdi/js";
|
||||
import { TagRowData } from "./ha-config-tags";
|
||||
|
||||
@customElement("tag-image")
|
||||
export class HaTagImage extends LitElement {
|
||||
@property() public tag?: TagRowData;
|
||||
|
||||
private _timeout?: number;
|
||||
|
||||
protected updated() {
|
||||
const msSinceLastScaned = this.tag?.last_scanned_datetime
|
||||
? new Date().getTime() - this.tag.last_scanned_datetime.getTime()
|
||||
: undefined;
|
||||
|
||||
if (msSinceLastScaned && msSinceLastScaned < 1000) {
|
||||
if (this._timeout) {
|
||||
clearTimeout(this._timeout);
|
||||
this._timeout = undefined;
|
||||
this.classList.remove("just-scanned");
|
||||
requestAnimationFrame(() => this.classList.add("just-scanned"));
|
||||
} else {
|
||||
this.classList.add("just-scanned");
|
||||
}
|
||||
this._timeout = window.setTimeout(() => {
|
||||
this.classList.remove("just-scanned");
|
||||
this._timeout = undefined;
|
||||
}, 10000);
|
||||
} else if (!msSinceLastScaned || msSinceLastScaned > 10000) {
|
||||
clearTimeout(this._timeout);
|
||||
this._timeout = undefined;
|
||||
this.classList.remove("just-scanned");
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.tag) {
|
||||
return html``;
|
||||
}
|
||||
return html`<div class="container">
|
||||
<div class="image">
|
||||
<ha-svg-icon .path=${mdiNfcVariant}></ha-svg-icon>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
.image {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-size: cover;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.container {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
:host(.just-scanned) .container {
|
||||
animation: glow 10s;
|
||||
}
|
||||
@keyframes glow {
|
||||
0% {
|
||||
box-shadow: 0px 0px 24px 0px rgba(var(--rgb-primary-color), 0);
|
||||
}
|
||||
10% {
|
||||
box-shadow: 0px 0px 24px 0px rgba(var(--rgb-primary-color), 1);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0px 0px 24px 0px rgba(var(--rgb-primary-color), 0);
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"tag-image": HaTagImage;
|
||||
}
|
||||
}
|
@ -601,6 +601,31 @@
|
||||
"confirmation_text": "All devices in this area will become unassigned."
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"caption": "Tags",
|
||||
"description": "Manage tags",
|
||||
"no_tags": "No tags",
|
||||
"add_tag": "Add tag",
|
||||
"write": "Write",
|
||||
"edit": "Edit",
|
||||
"create_automation": "Create automation with tag",
|
||||
"automation_title": "Tag {name} is scanned",
|
||||
"headers": {
|
||||
"name": "Name",
|
||||
"last_scanned": "Last scanned"
|
||||
},
|
||||
"detail": {
|
||||
"new_tag": "New tag",
|
||||
"name": "Name",
|
||||
"description": "Description",
|
||||
"tag_id": "Tag id",
|
||||
"tag_id_placeholder": "Autogenerated when left empty",
|
||||
"delete": "Delete",
|
||||
"update": "Update",
|
||||
"create": "Create",
|
||||
"create_and_write": "Create and Write"
|
||||
}
|
||||
},
|
||||
"helpers": {
|
||||
"caption": "Helpers",
|
||||
"description": "Manage elements that help build automations",
|
||||
@ -878,7 +903,7 @@
|
||||
"duplicate": "Duplicate",
|
||||
"delete": "[%key:ui::panel::mailbox::delete_button%]",
|
||||
"delete_confirm": "Are you sure you want to delete this?",
|
||||
"unsupported_platform": "Unsupported platform: {platform}",
|
||||
"unsupported_platform": "No UI support for platform: {platform}",
|
||||
"type_select": "Trigger type",
|
||||
"type": {
|
||||
"device": {
|
||||
@ -933,6 +958,9 @@
|
||||
"sunset": "Sunset",
|
||||
"offset": "Offset (optional)"
|
||||
},
|
||||
"tag": {
|
||||
"label": "Tag"
|
||||
},
|
||||
"template": {
|
||||
"label": "Template",
|
||||
"value_template": "Value template"
|
||||
@ -970,7 +998,7 @@
|
||||
"duplicate": "[%key:ui::panel::config::automation::editor::triggers::duplicate%]",
|
||||
"delete": "[%key:ui::panel::mailbox::delete_button%]",
|
||||
"delete_confirm": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm%]",
|
||||
"unsupported_condition": "Unsupported condition: {condition}",
|
||||
"unsupported_condition": "No UI support for condition: {condition}",
|
||||
"type_select": "Condition type",
|
||||
"type": {
|
||||
"and": {
|
||||
@ -1035,7 +1063,7 @@
|
||||
"duplicate": "[%key:ui::panel::config::automation::editor::triggers::duplicate%]",
|
||||
"delete": "[%key:ui::panel::mailbox::delete_button%]",
|
||||
"delete_confirm": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm%]",
|
||||
"unsupported_action": "Unsupported action: {action}",
|
||||
"unsupported_action": "No UI support for action: {action}",
|
||||
"type_select": "Action type",
|
||||
"type": {
|
||||
"service": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user