Add location selector, convert zone editor (#11902)

This commit is contained in:
Bram Kragten 2022-03-07 15:47:20 +01:00 committed by GitHub
parent 0dac10aa23
commit 4cdff3faea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 226 additions and 204 deletions

View File

@ -38,6 +38,7 @@ const SCHEMAS: {
select: "Select", select: "Select",
icon: "Icon", icon: "Icon",
media: "Media", media: "Media",
location: "Location",
}, },
schema: [ schema: [
{ name: "addon", selector: { addon: {} } }, { name: "addon", selector: { addon: {} } },
@ -75,6 +76,10 @@ const SCHEMAS: {
media: {}, media: {},
}, },
}, },
{
name: "location",
selector: { location: { radius: true, icon: "mdi:home" } },
},
], ],
}, },
{ {

View File

@ -168,6 +168,11 @@ const SCHEMAS: {
}, },
icon: { name: "Icon", selector: { icon: {} } }, icon: { name: "Icon", selector: { icon: {} } },
media: { name: "Media", selector: { media: {} } }, media: { name: "Media", selector: { media: {} } },
location: { name: "Location", selector: { location: {} } },
location_radius: {
name: "Location with radius",
selector: { location: { radius: true, icon: "mdi:home" } },
},
}, },
}, },
]; ];

View File

@ -9,7 +9,9 @@ export class HaFormConstant extends LitElement implements HaFormElement {
@property() public label!: string; @property() public label!: string;
protected render(): TemplateResult { protected render(): TemplateResult {
return html`<span class="label">${this.label}</span>: ${this.schema.value}`; return html`<span class="label">${this.label}</span>${this.schema.value
? `: ${this.schema.value}`
: ""}`;
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@ -25,6 +25,8 @@ import { HomeAssistant } from "../../types";
const getValue = (obj, item) => const getValue = (obj, item) =>
obj ? (!item.name ? obj : obj[item.name]) : null; obj ? (!item.name ? obj : obj[item.name]) : null;
const getError = (obj, item) => (obj && item.name ? obj[item.name] : null);
let selectorImported = false; let selectorImported = false;
@customElement("ha-form") @customElement("ha-form")
@ -84,7 +86,7 @@ export class HaForm extends LitElement implements HaFormElement {
` `
: ""} : ""}
${this.schema.map((item) => { ${this.schema.map((item) => {
const error = getValue(this.error, item); const error = getError(this.error, item);
return html` return html`
${error ${error

View File

@ -40,7 +40,7 @@ export interface HaFormSelector extends HaFormBaseSchema {
export interface HaFormConstantSchema extends HaFormBaseSchema { export interface HaFormConstantSchema extends HaFormBaseSchema {
type: "constant"; type: "constant";
value: string; value?: string;
} }
export interface HaFormIntegerSchema extends HaFormBaseSchema { export interface HaFormIntegerSchema extends HaFormBaseSchema {

View File

@ -0,0 +1,80 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import type {
LocationSelector,
LocationSelectorValue,
} from "../../data/selector";
import "../../panels/lovelace/components/hui-theme-select-editor";
import type { HomeAssistant } from "../../types";
import type { MarkerLocation } from "../map/ha-locations-editor";
import "../map/ha-locations-editor";
@customElement("ha-selector-location")
export class HaLocationSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: LocationSelector;
@property() public value?: LocationSelectorValue;
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
protected render() {
return html`
<ha-locations-editor
class="flex"
.hass=${this.hass}
.locations=${this._location(this.selector, this.value)}
@location-updated=${this._locationChanged}
@radius-updated=${this._radiusChanged}
></ha-locations-editor>
`;
}
private _location = memoizeOne(
(
selector: LocationSelector,
value?: LocationSelectorValue
): MarkerLocation[] => {
const computedStyles = getComputedStyle(this);
const zoneRadiusColor = selector.location.radius
? computedStyles.getPropertyValue("--zone-radius-color") ||
computedStyles.getPropertyValue("--accent-color")
: undefined;
return [
{
id: "location",
latitude: value?.latitude || this.hass.config.latitude,
longitude: value?.longitude || this.hass.config.longitude,
radius: selector.location.radius ? value?.radius || 1000 : undefined,
radius_color: zoneRadiusColor,
icon: selector.location.icon,
location_editable: true,
radius_editable: true,
},
];
}
);
private _locationChanged(ev: CustomEvent) {
const [latitude, longitude] = ev.detail.location;
fireEvent(this, "value-changed", {
value: { ...this.value, latitude, longitude },
});
}
private _radiusChanged(ev: CustomEvent) {
const radius = ev.detail.radius;
fireEvent(this, "value-changed", { value: { ...this.value, radius } });
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-location": HaLocationSelector;
}
}

View File

@ -20,6 +20,7 @@ import "./ha-selector-time";
import "./ha-selector-icon"; import "./ha-selector-icon";
import "./ha-selector-media"; import "./ha-selector-media";
import "./ha-selector-theme"; import "./ha-selector-theme";
import "./ha-selector-location";
@customElement("ha-selector") @customElement("ha-selector")
export class HaSelector extends LitElement { export class HaSelector extends LitElement {

View File

@ -15,7 +15,8 @@ export type Selector =
| SelectSelector | SelectSelector
| IconSelector | IconSelector
| MediaSelector | MediaSelector
| ThemeSelector; | ThemeSelector
| LocationSelector;
export interface EntitySelector { export interface EntitySelector {
entity: { entity: {
@ -164,6 +165,16 @@ export interface MediaSelector {
media: {}; media: {};
} }
export interface LocationSelector {
location: { radius?: boolean; icon?: string };
}
export interface LocationSelectorValue {
latitude: number;
longitude: number;
radius?: number;
}
export interface MediaSelectorValue { export interface MediaSelectorValue {
entity_id?: string; entity_id?: string;
media_content_id?: string; media_content_id?: string;

View File

@ -12,12 +12,12 @@ export interface Zone {
} }
export interface ZoneMutableParams { export interface ZoneMutableParams {
name: string;
icon?: string; icon?: string;
latitude: number; latitude: number;
longitude: number; longitude: number;
name: string; passive?: boolean;
passive: boolean; radius?: number;
radius: number;
} }
export const fetchZones = (hass: HomeAssistant) => export const fetchZones = (hass: HomeAssistant) =>

View File

@ -1,17 +1,12 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property, state } from "lit/decorators"; import { property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { addDistanceToCoord } from "../../../common/location/add_distance_to_coord"; import { addDistanceToCoord } from "../../../common/location/add_distance_to_coord";
import { computeRTLDirection } from "../../../common/util/compute_rtl";
import { createCloseHeading } from "../../../components/ha-dialog"; import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-formfield"; import "../../../components/ha-form/ha-form";
import "../../../components/ha-icon-picker"; import { HaFormSchema } from "../../../components/ha-form/types";
import "../../../components/ha-switch";
import "../../../components/map/ha-locations-editor";
import type { MarkerLocation } from "../../../components/map/ha-locations-editor";
import { getZoneEditorInitData, ZoneMutableParams } from "../../../data/zone"; import { getZoneEditorInitData, ZoneMutableParams } from "../../../data/zone";
import { haStyleDialog } from "../../../resources/styles"; import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
@ -20,19 +15,9 @@ import { ZoneDetailDialogParams } from "./show-dialog-zone-detail";
class DialogZoneDetail extends LitElement { class DialogZoneDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@state() private _name!: string; @state() private _error?: Record<string, string>;
@state() private _icon!: string; @state() private _data?: ZoneMutableParams;
@state() private _latitude!: number;
@state() private _longitude!: number;
@state() private _passive!: boolean;
@state() private _radius!: number;
@state() private _error?: string;
@state() private _params?: ZoneDetailDialogParams; @state() private _params?: ZoneDetailDialogParams;
@ -42,13 +27,7 @@ class DialogZoneDetail extends LitElement {
this._params = params; this._params = params;
this._error = undefined; this._error = undefined;
if (this._params.entry) { if (this._params.entry) {
this._name = this._params.entry.name || ""; this._data = this._params.entry;
this._icon = this._params.entry.icon || "";
this._latitude = this._params.entry.latitude || this.hass.config.latitude;
this._longitude =
this._params.entry.longitude || this.hass.config.longitude;
this._passive = this._params.entry.passive || false;
this._radius = this._params.entry.radius || 100;
} else { } else {
const initConfig = getZoneEditorInitData(); const initConfig = getZoneEditorInitData();
let movedHomeLocation; let movedHomeLocation;
@ -59,30 +38,34 @@ class DialogZoneDetail extends LitElement {
Math.random() * 500 * (Math.random() < 0.5 ? -1 : 1) Math.random() * 500 * (Math.random() < 0.5 ? -1 : 1)
); );
} }
this._latitude = initConfig?.latitude || movedHomeLocation[0]; this._data = {
this._longitude = initConfig?.longitude || movedHomeLocation[1]; latitude: initConfig?.latitude || movedHomeLocation[0],
this._name = initConfig?.name || ""; longitude: initConfig?.longitude || movedHomeLocation[1],
this._icon = initConfig?.icon || "mdi:map-marker"; name: initConfig?.name || "",
icon: initConfig?.icon || "mdi:map-marker",
this._passive = false; passive: false,
this._radius = 100; radius: 100,
};
} }
} }
public closeDialog(): void { public closeDialog(): void {
this._params = undefined; this._params = undefined;
this._data = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._params) { if (!this._params || !this._data) {
return html``; return html``;
} }
const nameInvalid = this._name.trim() === ""; const nameInvalid = this._data.name.trim() === "";
const iconInvalid = Boolean(this._icon && !this._icon.trim().includes(":")); const iconInvalid = Boolean(
const latInvalid = String(this._latitude) === ""; this._data.icon && !this._data.icon.trim().includes(":")
const lngInvalid = String(this._longitude) === ""; );
const radiusInvalid = String(this._radius) === ""; const latInvalid = String(this._data.latitude) === "";
const lngInvalid = String(this._data.longitude) === "";
const radiusInvalid = String(this._data.radius) === "";
const valid = const valid =
!nameInvalid && !nameInvalid &&
@ -105,96 +88,15 @@ class DialogZoneDetail extends LitElement {
)} )}
> >
<div> <div>
${this._error ? html` <div class="error">${this._error}</div> ` : ""} <ha-form
<div class="form"> .hass=${this.hass}
<paper-input .schema=${this._schema(this._data.icon)}
dialogInitialFocus .data=${this._formData(this._data)}
.value=${this._name} .error=${this._error}
.configValue=${"name"} .computeLabel=${this._computeLabel}
@value-changed=${this._valueChanged} class=${this._data.passive ? "passive" : ""}
.label=${this.hass!.localize("ui.panel.config.zone.detail.name")} @value-changed=${this._valueChanged}
.errorMessage=${this.hass!.localize( ></ha-form>
"ui.panel.config.zone.detail.required_error_msg"
)}
required
auto-validate
></paper-input>
<ha-icon-picker
.value=${this._icon}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize("ui.panel.config.zone.detail.icon")}
.errorMessage=${this.hass!.localize(
"ui.panel.config.zone.detail.icon_error_msg"
)}
.invalid=${iconInvalid}
></ha-icon-picker>
<ha-locations-editor
class="flex"
.hass=${this.hass}
.locations=${this._location(
this._latitude,
this._longitude,
this._radius,
this._passive,
this._icon
)}
@location-updated=${this._locationChanged}
@radius-updated=${this._radiusChanged}
></ha-locations-editor>
<div class="location">
<paper-input
.value=${this._latitude}
.configValue=${"latitude"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.panel.config.zone.detail.latitude"
)}
.errorMessage=${this.hass!.localize(
"ui.panel.config.zone.detail.required_error_msg"
)}
.invalid=${latInvalid}
></paper-input>
<paper-input
.value=${this._longitude}
.configValue=${"longitude"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.panel.config.zone.detail.longitude"
)}
.errorMessage=${this.hass!.localize(
"ui.panel.config.zone.detail.required_error_msg"
)}
.invalid=${lngInvalid}
></paper-input>
</div>
<paper-input
.value=${this._radius}
.configValue=${"radius"}
@value-changed=${this._valueChanged}
.label=${this.hass!.localize(
"ui.panel.config.zone.detail.radius"
)}
.errorMessage=${this.hass!.localize(
"ui.panel.config.zone.detail.required_error_msg"
)}
.invalid=${radiusInvalid}
></paper-input>
<p>
${this.hass!.localize("ui.panel.config.zone.detail.passive_note")}
</p>
<ha-formfield
.label=${this.hass!.localize(
"ui.panel.config.zone.detail.passive"
)}
.dir=${computeRTLDirection(this.hass)}
>
<ha-switch
.checked=${this._passive}
@change=${this._passiveChanged}
></ha-switch>
</ha-formfield>
</div>
</div> </div>
${this._params.entry ${this._params.entry
? html` ? html`
@ -221,74 +123,94 @@ class DialogZoneDetail extends LitElement {
`; `;
} }
private _location = memoizeOne( private _schema = memoizeOne((icon?: string): HaFormSchema[] => [
( {
lat: number, name: "name",
lng: number, required: true,
radius: number, selector: {
passive: boolean, text: {},
icon: string },
): MarkerLocation[] => { },
const computedStyles = getComputedStyle(this); {
const zoneRadiusColor = computedStyles.getPropertyValue("--accent-color"); name: "icon",
const passiveRadiusColor = computedStyles.getPropertyValue( required: false,
"--secondary-text-color" selector: {
); icon: {},
return [ },
},
{
name: "location",
required: true,
selector: { location: { radius: true, icon } },
},
{
name: "",
type: "grid",
schema: [
{ {
id: "location", name: "latitude",
latitude: Number(lat), required: true,
longitude: Number(lng), selector: { text: {} },
radius,
radius_color: passive ? passiveRadiusColor : zoneRadiusColor,
icon,
location_editable: true,
radius_editable: true,
}, },
]; {
} name: "longitude",
); required: true,
private _locationChanged(ev: CustomEvent) { selector: { text: {} },
[this._latitude, this._longitude] = ev.detail.location; },
} ],
},
{ name: "passive_note", type: "constant" },
{ name: "passive", selector: { boolean: {} } },
{
name: "radius",
required: false,
selector: { number: { min: 0, max: 999999, mode: "box" } },
},
]);
private _radiusChanged(ev: CustomEvent) { private _formData = memoizeOne((data: ZoneMutableParams) => ({
this._radius = ev.detail.radius; ...data,
} location: {
latitude: data.latitude,
private _passiveChanged(ev) { longitude: data.longitude,
this._passive = ev.target.checked; radius: data.radius,
} },
}));
private _valueChanged(ev: CustomEvent) { private _valueChanged(ev: CustomEvent) {
const configValue = (ev.target as any).configValue;
this._error = undefined; this._error = undefined;
this[`_${configValue}`] = ev.detail.value; const value = ev.detail.value;
if (
value.location.latitude !== this._data!.latitude ||
value.location.longitude !== this._data!.longitude ||
value.location.radius !== this._data!.radius
) {
value.latitude = value.location.latitude;
value.longitude = value.location.longitude;
value.radius = Math.round(value.location.radius);
}
delete value.location;
if (!value.icon) {
delete value.icon;
}
this._data = value;
} }
private _computeLabel = (entry: HaFormSchema): string =>
this.hass.localize(`ui.panel.config.zone.detail.${entry.name}`);
private async _updateEntry() { private async _updateEntry() {
this._submitting = true; this._submitting = true;
try { try {
const values: ZoneMutableParams = {
name: this._name.trim(),
latitude: this._latitude,
longitude: this._longitude,
passive: this._passive,
radius: this._radius,
};
if (this._icon) {
values.icon = this._icon.trim();
}
if (this._params!.entry) { if (this._params!.entry) {
await this._params!.updateEntry!(values); await this._params!.updateEntry!(this._data!);
} else { } else {
await this._params!.createEntry(values); await this._params!.createEntry(this._data!);
} }
this._params = undefined; this.closeDialog();
} catch (err: any) { } catch (err: any) {
this._error = err ? err.message : "Unknown error"; this._error = { base: err ? err.message : "Unknown error" };
} finally { } finally {
this._submitting = false; this._submitting = false;
} }
@ -309,24 +231,18 @@ class DialogZoneDetail extends LitElement {
return [ return [
haStyleDialog, haStyleDialog,
css` css`
.location { ha-dialog {
display: flex; --mdc-dialog-min-width: 600px;
} }
.location > * { @media all and (max-width: 450px), all and (max-height: 500px) {
flex-grow: 1; ha-dialog {
min-width: 0; --mdc-dialog-min-width: calc(
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
);
}
} }
.location > *:first-child { ha-form.passive {
margin-right: 4px; --zone-radius-color: var(--secondary-text-color);
}
.location > *:last-child {
margin-left: 4px;
}
ha-locations-editor {
margin-top: 16px;
}
a {
color: var(--primary-color);
} }
`, `,
]; ];