mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-08 09:56:36 +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";
|
event: "enter" | "leave";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TagTrigger {
|
||||||
|
platform: "tag";
|
||||||
|
tag_id: string;
|
||||||
|
device_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TimeTrigger {
|
export interface TimeTrigger {
|
||||||
platform: "time";
|
platform: "time";
|
||||||
at: string;
|
at: string;
|
||||||
@ -116,6 +122,7 @@ export type Trigger =
|
|||||||
| TimePatternTrigger
|
| TimePatternTrigger
|
||||||
| WebhookTrigger
|
| WebhookTrigger
|
||||||
| ZoneTrigger
|
| ZoneTrigger
|
||||||
|
| TagTrigger
|
||||||
| TimeTrigger
|
| TimeTrigger
|
||||||
| TemplateTrigger
|
| TemplateTrigger
|
||||||
| EventTrigger
|
| 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 {
|
export interface ExternalConfig {
|
||||||
hasSettingsScreen: boolean;
|
hasSettingsScreen: boolean;
|
||||||
|
canWriteTag: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getExternalConfig = (
|
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-time_pattern";
|
||||||
import "./types/ha-automation-trigger-webhook";
|
import "./types/ha-automation-trigger-webhook";
|
||||||
import "./types/ha-automation-trigger-zone";
|
import "./types/ha-automation-trigger-zone";
|
||||||
|
import "./types/ha-automation-trigger-tag";
|
||||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||||
import { haStyle } from "../../../../resources/styles";
|
import { haStyle } from "../../../../resources/styles";
|
||||||
|
|
||||||
@ -46,6 +47,7 @@ const OPTIONS = [
|
|||||||
"mqtt",
|
"mqtt",
|
||||||
"numeric_state",
|
"numeric_state",
|
||||||
"sun",
|
"sun",
|
||||||
|
"tag",
|
||||||
"template",
|
"template",
|
||||||
"time",
|
"time",
|
||||||
"time_pattern",
|
"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,
|
mdiInformation,
|
||||||
mdiMathLog,
|
mdiMathLog,
|
||||||
mdiPencil,
|
mdiPencil,
|
||||||
|
mdiNfcVariant,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@ -99,6 +100,15 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
|||||||
core: true,
|
core: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
experimental: [
|
||||||
|
{
|
||||||
|
component: "tags",
|
||||||
|
path: "/config/tags",
|
||||||
|
translationKey: "ui.panel.config.tags.caption",
|
||||||
|
iconPath: mdiNfcVariant,
|
||||||
|
core: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
lovelace: [
|
lovelace: [
|
||||||
{
|
{
|
||||||
component: "lovelace",
|
component: "lovelace",
|
||||||
@ -195,6 +205,13 @@ class HaPanelConfig extends HassRouterPage {
|
|||||||
/* webpackChunkName: "panel-config-automation" */ "./automation/ha-config-automation"
|
/* webpackChunkName: "panel-config-automation" */ "./automation/ha-config-automation"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
tags: {
|
||||||
|
tag: "ha-config-tags",
|
||||||
|
load: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "panel-config-tags" */ "./tags/ha-config-tags"
|
||||||
|
),
|
||||||
|
},
|
||||||
cloud: {
|
cloud: {
|
||||||
tag: "ha-config-cloud",
|
tag: "ha-config-cloud",
|
||||||
load: () =>
|
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."
|
"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": {
|
"helpers": {
|
||||||
"caption": "Helpers",
|
"caption": "Helpers",
|
||||||
"description": "Manage elements that help build automations",
|
"description": "Manage elements that help build automations",
|
||||||
@ -878,7 +903,7 @@
|
|||||||
"duplicate": "Duplicate",
|
"duplicate": "Duplicate",
|
||||||
"delete": "[%key:ui::panel::mailbox::delete_button%]",
|
"delete": "[%key:ui::panel::mailbox::delete_button%]",
|
||||||
"delete_confirm": "Are you sure you want to delete this?",
|
"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_select": "Trigger type",
|
||||||
"type": {
|
"type": {
|
||||||
"device": {
|
"device": {
|
||||||
@ -933,6 +958,9 @@
|
|||||||
"sunset": "Sunset",
|
"sunset": "Sunset",
|
||||||
"offset": "Offset (optional)"
|
"offset": "Offset (optional)"
|
||||||
},
|
},
|
||||||
|
"tag": {
|
||||||
|
"label": "Tag"
|
||||||
|
},
|
||||||
"template": {
|
"template": {
|
||||||
"label": "Template",
|
"label": "Template",
|
||||||
"value_template": "Value template"
|
"value_template": "Value template"
|
||||||
@ -970,7 +998,7 @@
|
|||||||
"duplicate": "[%key:ui::panel::config::automation::editor::triggers::duplicate%]",
|
"duplicate": "[%key:ui::panel::config::automation::editor::triggers::duplicate%]",
|
||||||
"delete": "[%key:ui::panel::mailbox::delete_button%]",
|
"delete": "[%key:ui::panel::mailbox::delete_button%]",
|
||||||
"delete_confirm": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm%]",
|
"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_select": "Condition type",
|
||||||
"type": {
|
"type": {
|
||||||
"and": {
|
"and": {
|
||||||
@ -1035,7 +1063,7 @@
|
|||||||
"duplicate": "[%key:ui::panel::config::automation::editor::triggers::duplicate%]",
|
"duplicate": "[%key:ui::panel::config::automation::editor::triggers::duplicate%]",
|
||||||
"delete": "[%key:ui::panel::mailbox::delete_button%]",
|
"delete": "[%key:ui::panel::mailbox::delete_button%]",
|
||||||
"delete_confirm": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm%]",
|
"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_select": "Action type",
|
||||||
"type": {
|
"type": {
|
||||||
"service": {
|
"service": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user