Person: Pick device tracker (#2726)

* Allow picking devices to track

* Tweak translation

* Update translation
This commit is contained in:
Paulus Schoutsen 2019-02-12 11:52:30 -08:00 committed by GitHub
parent 2f2cdad16b
commit abbfea0b6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 381 additions and 196 deletions

View File

@ -81,6 +81,7 @@
"lit-html": "^1.0.0",
"marked": "^0.6.0",
"mdn-polyfills": "^5.12.0",
"memoize-one": "^5.0.0",
"moment": "^2.22.2",
"preact": "^8.3.1",
"preact-compat": "^3.18.4",
@ -105,6 +106,7 @@
"@gfx/zopfli": "^1.0.9",
"@types/chai": "^4.1.7",
"@types/codemirror": "^0.0.71",
"@types/memoize-one": "^4.1.0",
"@types/mocha": "^5.2.5",
"babel-eslint": "^10",
"babel-loader": "^8.0.4",

View File

@ -0,0 +1,120 @@
import {
LitElement,
TemplateResult,
property,
html,
customElement,
} from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button-light";
import { HomeAssistant } from "../../types";
import { PolymerChangedEvent } from "../../polymer-types";
import { fireEvent } from "../../common/dom/fire_event";
import isValidEntityId from "../../common/entity/valid_entity_id";
import "./ha-entity-picker";
// Not a duplicate, type import
// tslint:disable-next-line
import { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker";
import { HassEntity } from "home-assistant-js-websocket";
@customElement("ha-entities-picker")
class HaEntitiesPickerLight extends LitElement {
@property() public hass?: HomeAssistant;
@property() public value?: string[];
@property() public domainFilter?: string;
@property() public pickedEntityLabel?: string;
@property() public pickEntityLabel?: string;
protected render(): TemplateResult | void {
if (!this.hass) {
return;
}
const currentEntities = this._currentEntities;
return html`
${currentEntities.map(
(entityId) => html`
<div>
<ha-entity-picker
allow-custom-entity
.curValue=${entityId}
.hass=${this.hass}
.domainFilter=${this.domainFilter}
.entityFilter=${this._entityFilter}
.value=${entityId}
.label=${this.pickedEntityLabel}
@value-changed=${this._entityChanged}
></ha-entity-picker>
</div>
`
)}
<div>
<ha-entity-picker
.hass=${this.hass}
.domainFilter=${this.domainFilter}
.entityFilter=${this._entityFilter}
.label=${this.pickEntityLabel}
@value-changed=${this._addEntity}
></ha-entity-picker>
</div>
`;
}
private _entityFilter: HaEntityPickerEntityFilterFunc = (
stateObj: HassEntity
) => !this.value || !this.value.includes(stateObj.entity_id);
private get _currentEntities() {
return this.value || [];
}
private async _updateEntities(entities) {
fireEvent(this, "value-changed", {
value: entities,
});
this.value = entities;
}
private _entityChanged(event: PolymerChangedEvent<string>) {
event.stopPropagation();
const curValue = (event.currentTarget as any).curValue;
const newValue = event.detail.value;
if (
newValue === curValue ||
(newValue !== "" && !isValidEntityId(newValue))
) {
return;
}
if (newValue === "") {
this._updateEntities(
this._currentEntities.filter((ent) => ent !== curValue)
);
} else {
this._updateEntities(
this._currentEntities.map((ent) => (ent === curValue ? newValue : ent))
);
}
}
private async _addEntity(event: PolymerChangedEvent<string>) {
event.stopPropagation();
const toAdd = event.detail.value;
(event.currentTarget as any).value = "";
if (!toAdd) {
return;
}
const currentEntities = this._currentEntities;
if (currentEntities.includes(toAdd)) {
return;
}
this._updateEntities([...currentEntities, toAdd]);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-entities-picker": HaEntitiesPickerLight;
}
}

View File

@ -1,179 +0,0 @@
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@vaadin/vaadin-combo-box/vaadin-combo-box-light";
import "./state-badge";
import computeStateName from "../../common/entity/compute_state_name";
import LocalizeMixin from "../../mixins/localize-mixin";
import EventsMixin from "../../mixins/events-mixin";
/*
* @appliesMixin LocalizeMixin
*/
class HaEntityPicker extends EventsMixin(LocalizeMixin(PolymerElement)) {
static get template() {
return html`
<style>
paper-input > paper-icon-button {
width: 24px;
height: 24px;
padding: 2px;
color: var(--secondary-text-color);
}
[hidden] {
display: none;
}
</style>
<vaadin-combo-box-light
items="[[_states]]"
item-value-path="entity_id"
item-label-path="entity_id"
value="{{value}}"
opened="{{opened}}"
allow-custom-value="[[allowCustomEntity]]"
on-change="_fireChanged"
>
<paper-input
autofocus="[[autofocus]]"
label="[[_computeLabel(label, localize)]]"
class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
value="[[value]]"
disabled="[[disabled]]"
>
<paper-icon-button
slot="suffix"
class="clear-button"
icon="hass:close"
no-ripple=""
hidden$="[[!value]]"
>Clear</paper-icon-button
>
<paper-icon-button
slot="suffix"
class="toggle-button"
icon="[[_computeToggleIcon(opened)]]"
hidden="[[!_states.length]]"
>Toggle</paper-icon-button
>
</paper-input>
<template>
<style>
paper-icon-item {
margin: -10px;
padding: 0;
}
</style>
<paper-icon-item>
<state-badge state-obj="[[item]]" slot="item-icon"></state-badge>
<paper-item-body two-line="">
<div>[[_computeStateName(item)]]</div>
<div secondary="">[[item.entity_id]]</div>
</paper-item-body>
</paper-icon-item>
</template>
</vaadin-combo-box-light>
`;
}
static get properties() {
return {
allowCustomEntity: {
type: Boolean,
value: false,
},
hass: {
type: Object,
observer: "_hassChanged",
},
_hass: Object,
_states: {
type: Array,
computed: "_computeStates(_hass, domainFilter, entityFilter)",
},
autofocus: Boolean,
label: {
type: String,
},
value: {
type: String,
notify: true,
},
opened: {
type: Boolean,
value: false,
observer: "_openedChanged",
},
domainFilter: {
type: String,
value: null,
},
entityFilter: {
type: Function,
value: null,
},
disabled: Boolean,
};
}
_computeLabel(label, localize) {
return label === undefined
? localize("ui.components.entity.entity-picker.entity")
: label;
}
_computeStates(hass, domainFilter, entityFilter) {
if (!hass) return [];
let entityIds = Object.keys(hass.states);
if (domainFilter) {
entityIds = entityIds.filter(
(eid) => eid.substr(0, eid.indexOf(".")) === domainFilter
);
}
let entities = entityIds.sort().map((key) => hass.states[key]);
if (entityFilter) {
entities = entities.filter(entityFilter);
}
return entities;
}
_computeStateName(state) {
return computeStateName(state);
}
_openedChanged(newVal) {
if (!newVal) {
this._hass = this.hass;
}
}
_hassChanged(newVal) {
if (!this.opened) {
this._hass = newVal;
}
}
_computeToggleIcon(opened) {
return opened ? "hass:menu-up" : "hass:menu-down";
}
_fireChanged(ev) {
ev.stopPropagation();
this.fire("change");
}
}
customElements.define("ha-entity-picker", HaEntityPicker);

View File

@ -0,0 +1,200 @@
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import "@vaadin/vaadin-combo-box/vaadin-combo-box-light";
import memoizeOne from "memoize-one";
import "./state-badge";
import computeStateName from "../../common/entity/compute_state_name";
import {
LitElement,
TemplateResult,
html,
css,
CSSResult,
property,
PropertyValues,
} from "lit-element";
import { HomeAssistant } from "../../types";
import { HassEntity } from "home-assistant-js-websocket";
import { PolymerChangedEvent } from "../../polymer-types";
import { fireEvent } from "../../common/dom/fire_event";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
const rowRenderer = (
root: HTMLElement,
_owner,
model: { item: HassEntity }
) => {
if (!root.firstElementChild) {
root.innerHTML = `
<style>
paper-icon-item {
margin: -10px;
padding: 0;
}
</style>
<paper-icon-item>
<state-badge state-obj="[[item]]" slot="item-icon"></state-badge>
<paper-item-body two-line="">
<div class='name'>[[_computeStateName(item)]]</div>
<div secondary>[[item.entity_id]]</div>
</paper-item-body>
</paper-icon-item>
`;
}
root.querySelector("state-badge")!.stateObj = model.item;
root.querySelector(".name")!.textContent = computeStateName(model.item);
root.querySelector("[secondary]")!.textContent = model.item.entity_id;
};
class HaEntityPicker extends LitElement {
@property({ type: Boolean }) public autofocus?: boolean;
@property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean }) public allowCustomEntity;
@property() public hass?: HomeAssistant;
@property() public label?: string;
@property() public value?: string;
@property() public domainFilter?: string;
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ type: Boolean }) private _opened?: boolean;
@property() private _hass?: HomeAssistant;
private _getStates = memoizeOne(
(
hass: this["hass"],
domainFilter: this["domainFilter"],
entityFilter: this["entityFilter"]
) => {
let states: HassEntity[] = [];
if (!hass) {
return [];
}
let entityIds = Object.keys(hass.states);
if (domainFilter) {
entityIds = entityIds.filter(
(eid) => eid.substr(0, eid.indexOf(".")) === domainFilter
);
}
states = entityIds.sort().map((key) => hass!.states[key]);
if (entityFilter) {
states = states.filter(
(stateObj) =>
// We always want to include the entity of the current value
stateObj.entity_id === this.value || entityFilter!(stateObj)
);
}
return states;
}
);
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("hass") && !this._opened) {
this._hass = this.hass;
}
}
protected render(): TemplateResult | void {
const states = this._getStates(
this._hass,
this.domainFilter,
this.entityFilter
);
return html`
<vaadin-combo-box-light
item-value-path="entity_id"
item-label-path="entity_id"
.items=${states}
.value=${this._value}
.allowCustomValue=${this.allowCustomEntity}
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
>
<paper-input
.autofocus=${this.autofocus}
.label=${this.label === undefined && this._hass
? this._hass.localize("ui.components.entity.entity-picker.entity")
: this.label}
.value=${this._value}
.disabled=${this.disabled}
class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
>
${this.value
? html`
<paper-icon-button
slot="suffix"
class="clear-button"
icon="hass:close"
no-ripple
>
Clear
</paper-icon-button>
`
: ""}
${states.length > 0
? html`
<paper-icon-button
slot="suffix"
class="toggle-button"
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
>
Toggle
</paper-icon-button>
`
: ""}
</paper-input>
</vaadin-combo-box-light>
`;
}
private get _value() {
return this.value || "";
}
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private _valueChanged(ev: PolymerChangedEvent<string>) {
const newValue = ev.detail.value;
if (newValue !== this._value) {
this.value = ev.detail.value;
setTimeout(() => {
fireEvent(this, "value-changed", { value: this.value });
fireEvent(this, "change");
}, 0);
}
}
static get styles(): CSSResult {
return css`
paper-input > paper-icon-button {
width: 24px;
height: 24px;
padding: 2px;
color: var(--secondary-text-color);
}
[hidden] {
display: none;
}
`;
}
}
customElements.define("ha-entity-picker", HaEntityPicker);

View File

@ -2,14 +2,15 @@ import {
LitElement,
html,
css,
PropertyDeclarations,
CSSResult,
TemplateResult,
property,
} from "lit-element";
import "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-input/paper-input";
import "../../../components/entity/ha-entities-picker";
import { PersonDetailDialogParams } from "./show-dialog-person-detail";
import { PolymerChangedEvent } from "../../../polymer-types";
import { haStyleDialog } from "../../../resources/ha-style";
@ -17,24 +18,20 @@ import { HomeAssistant } from "../../../types";
import { PersonMutableParams } from "../../../data/person";
class DialogPersonDetail extends LitElement {
public hass!: HomeAssistant;
private _name!: string;
private _error?: string;
private _params?: PersonDetailDialogParams;
private _submitting?: boolean;
static get properties(): PropertyDeclarations {
return {
_error: {},
_name: {},
_params: {},
};
}
@property() public hass!: HomeAssistant;
@property() private _name!: string;
@property() private _deviceTrackers!: string[];
@property() private _error?: string;
@property() private _params?: PersonDetailDialogParams;
@property() private _submitting?: boolean;
public async showDialog(params: PersonDetailDialogParams): Promise<void> {
this._params = params;
this._error = undefined;
this._name = this._params.entry ? this._params.entry.name : "";
this._deviceTrackers = this._params.entry
? this._params.entry.device_trackers || []
: [];
await this.updateComplete;
}
@ -64,6 +61,23 @@ class DialogPersonDetail extends LitElement {
error-message="Name is required"
.invalid=${nameInvalid}
></paper-input>
<p>
${this.hass.localize(
"ui.panel.config.person.detail.device_tracker_intro"
)}
</p>
<ha-entities-picker
.hass=${this.hass}
.value=${this._deviceTrackers}
domainFilter="device_tracker"
.pickedEntityLabel=${this.hass.localize(
"ui.panel.config.person.detail.device_tracker_picked"
)}
.pickEntityLabel=${this.hass.localize(
"ui.panel.config.person.detail.device_tracker_pick"
)}
@value-changed=${this._deviceTrackersChanged}
></ha-entities-picker>
</div>
</paper-dialog-scrollable>
<div class="paper-dialog-buttons">
@ -94,14 +108,19 @@ class DialogPersonDetail extends LitElement {
this._name = ev.detail.value;
}
private _deviceTrackersChanged(ev: PolymerChangedEvent<string[]>) {
this._error = undefined;
this._deviceTrackers = ev.detail.value;
}
private async _updateEntry() {
this._submitting = true;
try {
const values: PersonMutableParams = {
name: this._name.trim(),
device_trackers: this._deviceTrackers,
// Temp, we will add this in a future PR.
user_id: null,
device_trackers: [],
};
if (this._params!.entry) {
await this._params!.updateEntry(values);

View File

@ -783,8 +783,14 @@
}
},
"person": {
"caption": "People",
"description": "Manage the people in Home Assistant."
"caption": "Persons",
"description": "Manage the persons that Home Assistant tracks.",
"detail": {
"name": "Name",
"device_tracker_intro": "Select the devices that belong to this person.",
"device_tracker_picked": "Track Device",
"device_tracker_pick": "Pick device to track"
}
},
"integrations": {
"caption": "Integrations",

View File

@ -29,6 +29,13 @@ declare global {
getComputedStyleValue(element, propertyName);
};
}
// for fire event
interface HASSDomEvents {
"value-changed": {
value: unknown;
};
change: undefined;
}
}
export interface WebhookError {

View File

@ -1642,6 +1642,11 @@
resolved "https://registry.yarnpkg.com/@types/launchpad/-/launchpad-0.6.0.tgz#37296109b7f277f6e6c5fd7e0c0706bc918fbb51"
integrity sha1-NylhCbfyd/bmxf1+DAcGvJGPu1E=
"@types/memoize-one@^4.1.0":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@types/memoize-one/-/memoize-one-4.1.0.tgz#62119f26055b3193ae43ca1882c5b29b88b71ece"
integrity sha512-cmSgi6JMX/yBwgpVm4GooNWIH+vEeJoa8FAa6ExOhpJbC0Juq32/uYKiKb3VPSqrEA0aOnjvwZanla3O1WZMbw==
"@types/merge-stream@^1.0.28":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@types/merge-stream/-/merge-stream-1.1.2.tgz#a880ff66b1fbbb5eef4958d015c5947a9334dbb1"
@ -9466,6 +9471,11 @@ mem@^4.0.0:
mimic-fn "^1.0.0"
p-is-promise "^1.1.0"
memoize-one@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.0.0.tgz#d55007dffefb8de7546659a1722a5d42e128286e"
integrity sha512-7g0+ejkOaI9w5x6LvQwmj68kUj6rxROywPSCqmclG/HBacmFnZqhVscQ8kovkn9FBCNJmOz6SY42+jnvZzDWdw==
memory-fs@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.2.0.tgz#f2bb25368bc121e391c2520de92969caee0a0290"