From f1f1623d2fbbeeadd3748e5e7bd4971d0d280e48 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 30 Jan 2019 14:08:04 -0800 Subject: [PATCH] Add Area Registry (#2631) --- src/data/area_registry.ts | 39 ++++ src/data/device_registry.ts | 31 +++ .../dialog-area-registry-detail.ts | 160 +++++++++++++++ .../area_registry/ha-config-area-registry.ts | 190 ++++++++++++++++++ .../show-dialog-area-registry-detail.ts | 28 +++ .../config-entries/ha-config-entries.js | 11 + .../config-entries/ha-config-entry-page.js | 6 + .../config/config-entries/ha-device-card.js | 55 +++++ .../config/dashboard/ha-config-navigation.js | 3 +- src/panels/config/ha-panel-config.js | 14 ++ src/translations/en.json | 4 + 11 files changed, 540 insertions(+), 1 deletion(-) create mode 100644 src/data/area_registry.ts create mode 100644 src/data/device_registry.ts create mode 100644 src/panels/config/area_registry/dialog-area-registry-detail.ts create mode 100644 src/panels/config/area_registry/ha-config-area-registry.ts create mode 100644 src/panels/config/area_registry/show-dialog-area-registry-detail.ts diff --git a/src/data/area_registry.ts b/src/data/area_registry.ts new file mode 100644 index 0000000000..cf312bda3f --- /dev/null +++ b/src/data/area_registry.ts @@ -0,0 +1,39 @@ +import { HomeAssistant } from "../types"; + +export interface AreaRegistryEntry { + area_id: string; + name: string; +} + +export interface AreaRegistryEntryMutableParams { + name: string; +} + +export const fetchAreaRegistry = (hass: HomeAssistant) => + hass.callWS({ type: "config/area_registry/list" }); + +export const createAreaRegistryEntry = ( + hass: HomeAssistant, + values: AreaRegistryEntryMutableParams +) => + hass.callWS({ + type: "config/area_registry/create", + ...values, + }); + +export const updateAreaRegistryEntry = ( + hass: HomeAssistant, + areaId: string, + updates: Partial +) => + hass.callWS({ + type: "config/area_registry/update", + area_id: areaId, + ...updates, + }); + +export const deleteAreaRegistryEntry = (hass: HomeAssistant, areaId: string) => + hass.callWS({ + type: "config/area_registry/delete", + area_id: areaId, + }); diff --git a/src/data/device_registry.ts b/src/data/device_registry.ts new file mode 100644 index 0000000000..0a0b961dfe --- /dev/null +++ b/src/data/device_registry.ts @@ -0,0 +1,31 @@ +import { HomeAssistant } from "../types"; + +export interface DeviceRegistryEntry { + id: string; + config_entries: string[]; + connections: Array<[string, string]>; + manufacturer: string; + model?: string; + name?: string; + sw_version?: string; + hub_device_id?: string; + area_id?: string; +} + +export interface DeviceRegistryEntryMutableParams { + area_id: string; +} + +export const fetchDeviceRegistry = (hass: HomeAssistant) => + hass.callWS({ type: "config/device_registry/list" }); + +export const updateDeviceRegistryEntry = ( + hass: HomeAssistant, + deviceId: string, + updates: Partial +) => + hass.callWS({ + type: "config/device_registry/update", + device_id: deviceId, + ...updates, + }); diff --git a/src/panels/config/area_registry/dialog-area-registry-detail.ts b/src/panels/config/area_registry/dialog-area-registry-detail.ts new file mode 100644 index 0000000000..2c4114fbe1 --- /dev/null +++ b/src/panels/config/area_registry/dialog-area-registry-detail.ts @@ -0,0 +1,160 @@ +import { + LitElement, + html, + css, + PropertyDeclarations, + CSSResult, + TemplateResult, +} from "lit-element"; +import "@polymer/paper-dialog/paper-dialog"; +import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; +import "@polymer/paper-input/paper-input"; + +import { AreaRegistryDetailDialogParams } from "./show-dialog-area-registry-detail"; +import { PolymerChangedEvent } from "../../../polymer-types"; +import { haStyleDialog } from "../../../resources/ha-style"; +import { HomeAssistant } from "../../../types"; +import { AreaRegistryEntryMutableParams } from "../../../data/area_registry"; + +class DialogAreaDetail extends LitElement { + public hass!: HomeAssistant; + private _name!: string; + private _error?: string; + private _params?: AreaRegistryDetailDialogParams; + private _submitting?: boolean; + + static get properties(): PropertyDeclarations { + return { + _error: {}, + _name: {}, + _params: {}, + }; + } + + public async showDialog( + params: AreaRegistryDetailDialogParams + ): Promise { + this._params = params; + this._error = undefined; + this._name = this._params.entry ? this._params.entry.name : ""; + await this.updateComplete; + } + + protected render(): TemplateResult | void { + if (!this._params) { + return html``; + } + const nameInvalid = this._name.trim() === ""; + return html` + +

${this._params.entry ? this._params.entry.name : "New Area"}

+ + ${this._error + ? html` +
${this._error}
+ ` + : ""} +
+ +
+
+
+ ${this._params.entry + ? html` + + DELETE + + ` + : html``} + + ${this._params.entry ? "UPDATE" : "CREATE"} + +
+
+ `; + } + + private _nameChanged(ev: PolymerChangedEvent) { + this._error = undefined; + this._name = ev.detail.value; + } + + private async _updateEntry() { + try { + const values: AreaRegistryEntryMutableParams = { + name: this._name.trim(), + }; + if (this._params!.entry) { + await this._params!.updateEntry(values); + } else { + await this._params!.createEntry(values); + } + this._params = undefined; + } catch (err) { + this._error = err; + } + } + + private async _deleteEntry() { + if (await this._params!.removeEntry()) { + this._params = undefined; + } + } + + private _openedChanged(ev: PolymerChangedEvent): void { + if (!(ev.detail as any).value) { + this._params = undefined; + } + } + + static get styles(): CSSResult[] { + return [ + haStyleDialog, + css` + paper-dialog { + min-width: 400px; + } + .form { + padding-bottom: 24px; + } + paper-button { + font-weight: 500; + } + paper-button.danger { + font-weight: 500; + color: var(--google-red-500); + margin-left: -12px; + margin-right: auto; + } + .error { + color: var(--google-red-500); + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-area-registry-detail": DialogAreaDetail; + } +} + +customElements.define("dialog-area-registry-detail", DialogAreaDetail); diff --git a/src/panels/config/area_registry/ha-config-area-registry.ts b/src/panels/config/area_registry/ha-config-area-registry.ts new file mode 100644 index 0000000000..1abc25091f --- /dev/null +++ b/src/panels/config/area_registry/ha-config-area-registry.ts @@ -0,0 +1,190 @@ +import { + LitElement, + TemplateResult, + html, + css, + CSSResult, + PropertyDeclarations, +} from "lit-element"; +import "@polymer/paper-item/paper-item"; +import "@polymer/paper-item/paper-item-body"; +import "@polymer/paper-card/paper-card"; +import "@polymer/paper-fab/paper-fab"; + +import { HomeAssistant } from "../../../types"; +import { + AreaRegistryEntry, + fetchAreaRegistry, + updateAreaRegistryEntry, + deleteAreaRegistryEntry, + createAreaRegistryEntry, +} from "../../../data/area_registry"; +import "../../../layouts/hass-subpage"; +import "../../../layouts/hass-loading-screen"; +import compare from "../../../common/string/compare"; +import "../ha-config-section"; +import { + showAreaRegistryDetailDialog, + loadAreaRegistryDetailDialog, +} from "./show-dialog-area-registry-detail"; + +class HaConfigAreaRegistry extends LitElement { + public hass?: HomeAssistant; + public isWide?: boolean; + private _items?: AreaRegistryEntry[]; + + static get properties(): PropertyDeclarations { + return { + hass: {}, + isWide: {}, + _items: {}, + }; + } + + protected render(): TemplateResult | void { + if (!this.hass || this._items === undefined) { + return html` + + `; + } + return html` + + + Area Registry + + Areas are used to organize where devices are. This information will + be used throughout Home Assistant to help you in organizing your + interface, permissions and integrations with other systems. +

+ To place devices in an area, navigate to + the integrations page and then + click on a configured integration to get to the device cards. +

+
+ + ${this._items.map((entry) => { + return html` + + + ${entry.name} + + + `; + })} + ${this._items.length === 0 + ? html` +
+ Looks like you have no areas yet! + + CREATE AREA +
+ ` + : html``} +
+
+
+ + + `; + } + + protected firstUpdated(changedProps) { + super.firstUpdated(changedProps); + this._fetchData(); + loadAreaRegistryDetailDialog(); + } + + private async _fetchData() { + this._items = (await fetchAreaRegistry(this.hass!)).sort((ent1, ent2) => + compare(ent1.name, ent2.name) + ); + } + + private _createArea() { + this._openDialog(); + } + + private _openEditEntry(ev: MouseEvent) { + const entry: AreaRegistryEntry = (ev.currentTarget! as any).entry; + this._openDialog(entry); + } + private _openDialog(entry?: AreaRegistryEntry) { + showAreaRegistryDetailDialog(this, { + entry, + createEntry: async (values) => { + const created = await createAreaRegistryEntry(this.hass!, values); + this._items = this._items!.concat(created).sort((ent1, ent2) => + compare(ent1.name, ent2.name) + ); + }, + updateEntry: async (values) => { + const updated = await updateAreaRegistryEntry( + this.hass!, + entry!.area_id, + values + ); + this._items = this._items!.map((ent) => + ent === entry ? updated : ent + ); + }, + removeEntry: async () => { + if ( + !confirm(`Are you sure you want to delete this area? + +All devices in this area will become unassigned.`) + ) { + return false; + } + + try { + await deleteAreaRegistryEntry(this.hass!, entry!.area_id); + this._items = this._items!.filter((ent) => ent !== entry); + return true; + } catch (err) { + return false; + } + }, + }); + } + + static get styles(): CSSResult { + return css` + a { + color: var(--primary-color); + } + paper-card { + display: block; + max-width: 600px; + margin: 16px auto; + background-color: white; + } + .empty { + text-align: center; + } + paper-item { + cursor: pointer; + padding-top: 4px; + padding-bottom: 4px; + } + paper-fab { + position: fixed; + bottom: 16px; + right: 16px; + z-index: 1; + } + + paper-fab[is-wide] { + bottom: 24px; + right: 24px; + } + `; + } +} + +customElements.define("ha-config-area-registry", HaConfigAreaRegistry); diff --git a/src/panels/config/area_registry/show-dialog-area-registry-detail.ts b/src/panels/config/area_registry/show-dialog-area-registry-detail.ts new file mode 100644 index 0000000000..bbcc848b1c --- /dev/null +++ b/src/panels/config/area_registry/show-dialog-area-registry-detail.ts @@ -0,0 +1,28 @@ +import { fireEvent } from "../../../common/dom/fire_event"; +import { + AreaRegistryEntry, + AreaRegistryEntryMutableParams, +} from "../../../data/area_registry"; + +export interface AreaRegistryDetailDialogParams { + entry?: AreaRegistryEntry; + createEntry: (values: AreaRegistryEntryMutableParams) => Promise; + updateEntry: ( + updates: Partial + ) => Promise; + removeEntry: () => Promise; +} + +export const loadAreaRegistryDetailDialog = () => + import(/* webpackChunkName: "entity-registry-detail-dialog" */ "./dialog-area-registry-detail"); + +export const showAreaRegistryDetailDialog = ( + element: HTMLElement, + systemLogDetailParams: AreaRegistryDetailDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-area-registry-detail", + dialogImport: loadAreaRegistryDetailDialog, + dialogParams: systemLogDetailParams, + }); +}; diff --git a/src/panels/config/config-entries/ha-config-entries.js b/src/panels/config/config-entries/ha-config-entries.js index 3179c59bcb..c60c338f0a 100644 --- a/src/panels/config/config-entries/ha-config-entries.js +++ b/src/panels/config/config-entries/ha-config-entries.js @@ -8,6 +8,7 @@ import "./ha-config-entries-dashboard"; import "./ha-config-entry-page"; import NavigateMixin from "../../../mixins/navigate-mixin"; import compare from "../../../common/string/compare"; +import { fetchAreaRegistry } from "../../../data/area_registry"; class HaConfigEntries extends NavigateMixin(PolymerElement) { static get template() { @@ -23,6 +24,7 @@ class HaConfigEntries extends NavigateMixin(PolymerElement) { { this._devices = devices; }); + + fetchAreaRegistry(this.hass).then((areas) => { + this._areas = areas; + }); } _computeConfigEntry(routeData, entries) { diff --git a/src/panels/config/config-entries/ha-config-entry-page.js b/src/panels/config/config-entries/ha-config-entry-page.js index 06fb627380..c00da73c9d 100644 --- a/src/panels/config/config-entries/ha-config-entry-page.js +++ b/src/panels/config/config-entries/ha-config-entry-page.js @@ -53,6 +53,7 @@ class HaConfigEntryPage extends NavigateMixin( +
+ + + No Area + + + +