Add Area Registry (#2631)

This commit is contained in:
Paulus Schoutsen 2019-01-30 14:08:04 -08:00 committed by GitHub
parent b86bfa0395
commit f1f1623d2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 540 additions and 1 deletions

39
src/data/area_registry.ts Normal file
View File

@ -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<AreaRegistryEntry[]>({ type: "config/area_registry/list" });
export const createAreaRegistryEntry = (
hass: HomeAssistant,
values: AreaRegistryEntryMutableParams
) =>
hass.callWS<AreaRegistryEntry>({
type: "config/area_registry/create",
...values,
});
export const updateAreaRegistryEntry = (
hass: HomeAssistant,
areaId: string,
updates: Partial<AreaRegistryEntryMutableParams>
) =>
hass.callWS<AreaRegistryEntry>({
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,
});

View File

@ -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<DeviceRegistryEntry[]>({ type: "config/device_registry/list" });
export const updateDeviceRegistryEntry = (
hass: HomeAssistant,
deviceId: string,
updates: Partial<DeviceRegistryEntryMutableParams>
) =>
hass.callWS<DeviceRegistryEntry>({
type: "config/device_registry/update",
device_id: deviceId,
...updates,
});

View File

@ -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<void> {
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`
<paper-dialog
with-backdrop
opened
@opened-changed="${this._openedChanged}"
>
<h2>${this._params.entry ? this._params.entry.name : "New Area"}</h2>
<paper-dialog-scrollable>
${this._error
? html`
<div class="error">${this._error}</div>
`
: ""}
<div class="form">
<paper-input
.value=${this._name}
@value-changed=${this._nameChanged}
label="Name"
error-message="Name is required"
.invalid=${nameInvalid}
></paper-input>
</div>
</paper-dialog-scrollable>
<div class="paper-dialog-buttons">
${this._params.entry
? html`
<paper-button
class="danger"
@click="${this._deleteEntry}"
.disabled=${this._submitting}
>
DELETE
</paper-button>
`
: html``}
<paper-button
@click="${this._updateEntry}"
.disabled=${nameInvalid || this._submitting}
>
${this._params.entry ? "UPDATE" : "CREATE"}
</paper-button>
</div>
</paper-dialog>
`;
}
private _nameChanged(ev: PolymerChangedEvent<string>) {
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<boolean>): 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);

View File

@ -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`
<hass-loading-screen></hass-loading-screen>
`;
}
return html`
<hass-subpage header="Area Registry">
<ha-config-section .isWide=${this.isWide}>
<span slot="header">Area Registry</span>
<span slot="introduction">
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.
<p>
To place devices in an area, navigate to
<a href="/config/integrations">the integrations page</a> and then
click on a configured integration to get to the device cards.
</p>
</span>
<paper-card>
${this._items.map((entry) => {
return html`
<paper-item @click=${this._openEditEntry} .entry=${entry}>
<paper-item-body>
${entry.name}
</paper-item-body>
</paper-item>
`;
})}
${this._items.length === 0
? html`
<div class="empty">
Looks like you have no areas yet!
<paper-button @click=${this._createArea}>
CREATE AREA</paper-button
>
</div>
`
: html``}
</paper-card>
</ha-config-section>
</hass-subpage>
<paper-fab
?is-wide=${this.isWide}
icon="hass:plus"
title="Create Area"
@click=${this._createArea}
></paper-fab>
`;
}
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);

View File

@ -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<unknown>;
updateEntry: (
updates: Partial<AreaRegistryEntryMutableParams>
) => Promise<unknown>;
removeEntry: () => Promise<boolean>;
}
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,
});
};

View File

@ -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) {
<ha-config-entry-page
hass="[[hass]]"
config-entry="[[_configEntry]]"
areas="[[_areas]]"
entries="[[_entries]]"
entities="[[_entities]]"
devices="[[_devices]]"
@ -68,6 +70,11 @@ class HaConfigEntries extends NavigateMixin(PolymerElement) {
*/
_devices: Array,
/**
* Area Registry entries.
*/
_areas: Array,
/**
* Current flows that are in progress and have not been started by a user.
* For example, can be discovered devices that require more config.
@ -136,6 +143,10 @@ class HaConfigEntries extends NavigateMixin(PolymerElement) {
.then((devices) => {
this._devices = devices;
});
fetchAreaRegistry(this.hass).then((areas) => {
this._areas = areas;
});
}
_computeConfigEntry(routeData, entries) {

View File

@ -53,6 +53,7 @@ class HaConfigEntryPage extends NavigateMixin(
<ha-device-card
class="card"
hass="[[hass]]"
areas="[[areas]]"
devices="[[devices]]"
device="[[device]]"
entities="[[entities]]"
@ -97,6 +98,11 @@ class HaConfigEntryPage extends NavigateMixin(
computed: "_computeNoDeviceEntities(configEntry, entities)",
},
/**
* Area registry entries
*/
areas: Array,
/**
* Device registry entries
*/

View File

@ -1,6 +1,9 @@
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
@ -11,6 +14,7 @@ import LocalizeMixin from "../../../mixins/localize-mixin";
import computeStateName from "../../../common/entity/compute_state_name";
import "../../../components/entity/state-badge";
import compare from "../../../common/string/compare";
import { updateDeviceRegistryEntry } from "../../../data/device_registry";
function computeEntityName(hass, entity) {
if (entity.name) return entity.name;
@ -68,6 +72,22 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
[[localize('ui.panel.config.integrations.config_entry.manuf',
'manufacturer', device.manufacturer)]]
</div>
<div class="area">
<paper-dropdown-menu
selected-item-label="{{selectedArea}}"
label="Area"
>
<paper-listbox
slot="dropdown-content"
selected="[[_computeSelectedArea(areas, device)]]"
>
<paper-item>No Area</paper-item>
<template is="dom-repeat" items="[[areas]]">
<paper-item area="[[item]]">[[item.name]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
</div>
</div>
<template is="dom-if" if="[[device.hub_device_id]]">
<div class="extra-info">
@ -111,6 +131,7 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
return {
device: Object,
devices: Array,
areas: Array,
entities: Array,
hass: Object,
narrow: {
@ -121,9 +142,43 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
type: Array,
computed: "_computeChildDevices(device, devices)",
},
selectedArea: {
type: String,
observer: "_selectedAreaChanged",
},
};
}
_computeSelectedArea(areas, device) {
if (!areas || !device || !device.area_id) {
return 0;
}
// +1 because of "No Area" entry
return areas.findIndex((area) => area.area_id === device.area_id) + 1;
}
async _selectedAreaChanged(option) {
// Selected Option will transition to '' before transitioning to new value
if (option === "" || !this.device || !this.areas) {
return;
}
const area =
option === "No Area"
? undefined
: this.areas.find((ar) => ar.name === option);
if (
(!area && !this.device.area_id) ||
(area && area.area_id === this.device.area_id)
) {
return;
}
await updateDeviceRegistryEntry(this.hass, this.device.id, {
area_id: area ? area.area_id : null,
});
}
_computeChildDevices(device, devices) {
return devices
.filter((dev) => dev.hub_device_id === device.id)

View File

@ -10,7 +10,7 @@ import LocalizeMixin from "../../../mixins/localize-mixin";
import isComponentLoaded from "../../../common/config/is_component_loaded";
const CORE_PAGES = ["core", "customize", "entity_registry"];
const CORE_PAGES = ["core", "customize", "entity_registry", "area_registry"];
/*
* @appliesMixin LocalizeMixin
* @appliesMixin NavigateMixin
@ -54,6 +54,7 @@ class HaConfigNavigation extends LocalizeMixin(NavigateMixin(PolymerElement)) {
"core",
"customize",
"entity_registry",
"area_registry",
"automation",
"script",
"zha",

View File

@ -9,6 +9,7 @@ import isComponentLoaded from "../../common/config/is_component_loaded";
import EventsMixin from "../../mixins/events-mixin";
import NavigateMixin from "../../mixins/navigate-mixin";
import(/* webpackChunkName: "panel-config-area-registry" */ "./area_registry/ha-config-area-registry");
import(/* webpackChunkName: "panel-config-automation" */ "./automation/ha-config-automation");
import(/* webpackChunkName: "panel-config-cloud" */ "./cloud/ha-config-cloud");
import(/* webpackChunkName: "panel-config-config" */ "./config-entries/ha-config-entries");
@ -42,6 +43,19 @@ class HaPanelConfig extends EventsMixin(NavigateMixin(PolymerElement)) {
>
</iron-media-query>
<template
is="dom-if"
if='[[_equals(_routeData.page, "area_registry")]]'
restamp
>
<ha-config-area-registry
page-name="area_registry"
route="[[route]]"
hass="[[hass]]"
is-wide="[[isWide]]"
></ha-config-area-registry>
</template>
<template is="dom-if" if='[[_equals(_routeData.page, "core")]]' restamp>
<ha-config-core
page-name="core"

View File

@ -527,6 +527,10 @@
"config": {
"header": "Configure Home Assistant",
"introduction": "Here it is possible to configure your components and Home Assistant. Not everything is possible to configure from the UI yet, but we're working on it.",
"area_registry": {
"caption": "Area Registry",
"description": "Overview of all areas in your home."
},
"core": {
"caption": "General",
"description": "Validate your configuration file and control the server",