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",
icon: "Icon",
media: "Media",
location: "Location",
},
schema: [
{ name: "addon", selector: { addon: {} } },
@ -75,6 +76,10 @@ const SCHEMAS: {
media: {},
},
},
{
name: "location",
selector: { location: { radius: true, icon: "mdi:home" } },
},
],
},
{

View File

@ -168,6 +168,11 @@ const SCHEMAS: {
},
icon: { name: "Icon", selector: { icon: {} } },
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;
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 {

View File

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

View File

@ -40,7 +40,7 @@ export interface HaFormSelector extends HaFormBaseSchema {
export interface HaFormConstantSchema extends HaFormBaseSchema {
type: "constant";
value: string;
value?: string;
}
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-media";
import "./ha-selector-theme";
import "./ha-selector-location";
@customElement("ha-selector")
export class HaSelector extends LitElement {

View File

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

View File

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

View File

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