Location/zone editor updates (#19994)

This commit is contained in:
karwosts 2024-04-12 11:51:23 -07:00 committed by GitHub
parent 178feb7330
commit b82f1128fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 308 additions and 67 deletions

View File

@ -7,8 +7,10 @@ import type {
LocationSelectorValue,
} from "../../data/selector";
import type { HomeAssistant } from "../../types";
import type { SchemaUnion } from "../ha-form/types";
import type { MarkerLocation } from "../map/ha-locations-editor";
import "../map/ha-locations-editor";
import "../ha-form/ha-form";
@customElement("ha-selector-location")
export class HaLocationSelector extends LitElement {
@ -24,6 +26,49 @@ export class HaLocationSelector extends LitElement {
@property({ type: Boolean, reflect: true }) public disabled = false;
private _schema = memoizeOne(
(radius?: boolean, radius_readonly?: boolean) =>
[
{
name: "",
type: "grid",
schema: [
{
name: "latitude",
required: true,
selector: { number: { step: "any" } },
},
{
name: "longitude",
required: true,
selector: { number: { step: "any" } },
},
],
},
...(radius
? [
{
name: "radius",
required: true,
default: 1000,
disabled: !!radius_readonly,
selector: { number: { min: 0, step: 1, mode: "box" } as const },
} as const,
]
: []),
] as const
);
protected willUpdate() {
if (!this.value) {
this.value = {
latitude: this.hass.config.latitude,
longitude: this.hass.config.longitude,
radius: this.selector.location?.radius ? 1000 : undefined,
};
}
}
protected render() {
return html`
<p>${this.label ? this.label : ""}</p>
@ -35,6 +80,17 @@ export class HaLocationSelector extends LitElement {
@location-updated=${this._locationChanged}
@radius-updated=${this._radiusChanged}
></ha-locations-editor>
<ha-form
.hass=${this.hass}
.schema=${this._schema(
this.selector.location?.radius,
this.selector.location?.radius_readonly
)}
.data=${this.value}
.computeLabel=${this._computeLabel}
.disabled=${this.disabled}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
@ -66,7 +122,8 @@ export class HaLocationSelector extends LitElement {
? "mdi:map-marker-radius"
: "mdi:map-marker",
location_editable: true,
radius_editable: true,
radius_editable:
!!selector.location?.radius && !selector.location?.radius_readonly,
},
];
}
@ -80,14 +137,39 @@ export class HaLocationSelector extends LitElement {
}
private _radiusChanged(ev: CustomEvent) {
const radius = ev.detail.radius;
const radius = Math.round(ev.detail.radius);
fireEvent(this, "value-changed", { value: { ...this.value, radius } });
}
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = ev.detail.value;
const radius = Math.round(ev.detail.value.radius);
fireEvent(this, "value-changed", {
value: {
latitude: value.latitude,
longitude: value.longitude,
...(this.selector.location?.radius &&
!this.selector.location?.radius_readonly
? {
radius,
}
: {}),
},
});
}
private _computeLabel = (
entry: SchemaUnion<ReturnType<typeof this._schema>>
): string =>
this.hass.localize(`ui.components.selectors.location.${entry.name}`);
static styles = css`
ha-locations-editor {
display: block;
height: 400px;
margin-bottom: 16px;
}
p {
margin-top: 0;

View File

@ -270,7 +270,11 @@ export interface LanguageSelector {
}
export interface LocationSelector {
location: { radius?: boolean; icon?: string } | null;
location: {
radius?: boolean;
radius_readonly?: boolean;
icon?: string;
} | null;
}
export interface LocationSelectorValue {

View File

@ -11,6 +11,11 @@ export interface Zone {
radius?: number;
}
export interface HomeZoneMutableParams {
latitude: number;
longitude: number;
}
export interface ZoneMutableParams {
name: string;
icon?: string;

View File

@ -18,18 +18,20 @@ import "../../../components/ha-language-picker";
import "../../../components/ha-radio";
import type { HaRadio } from "../../../components/ha-radio";
import "../../../components/ha-select";
import "../../../components/ha-selector/ha-selector-location";
import type { LocationSelectorValue } from "../../../data/selector";
import "../../../components/ha-settings-row";
import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield";
import "../../../components/ha-timezone-picker";
import "../../../components/map/ha-locations-editor";
import type { MarkerLocation } from "../../../components/map/ha-locations-editor";
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-subpage";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
const LOCATION_SELECTOR = { location: {} };
@customElement("ha-config-section-general")
class HaConfigSectionGeneral extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -244,15 +246,16 @@ class HaConfigSectionGeneral extends LitElement {
</div>
${this.narrow
? html`
<ha-locations-editor
<ha-selector-location
.hass=${this.hass}
.locations=${this._markerLocation(
.selector=${LOCATION_SELECTOR}
.value=${this._selectorLocation(
this.hass.config.latitude,
this.hass.config.longitude,
this._location
)}
@location-updated=${this._locationChanged}
></ha-locations-editor>
@value-changed=${this._locationChanged}
></ha-selector-location>
`
: html`
<ha-settings-row>
@ -320,7 +323,7 @@ class HaConfigSectionGeneral extends LitElement {
}
private _locationChanged(ev: CustomEvent) {
this._location = ev.detail.location;
this._location = [ev.detail.value.latitude, ev.detail.value.longitude];
}
private async _updateEntry(ev: CustomEvent) {
@ -381,19 +384,15 @@ class HaConfigSectionGeneral extends LitElement {
}
}
private _markerLocation = memoizeOne(
private _selectorLocation = memoizeOne(
(
lat: number,
lng: number,
latDefault: number,
lngDefault: number,
location?: [number, number]
): MarkerLocation[] => [
{
id: "location",
latitude: location ? location[0] : lat,
longitude: location ? location[1] : lng,
location_editable: true,
},
]
): LocationSelectorValue => ({
latitude: location != null ? location[0] : latDefault,
longitude: location != null ? location[1] : lngDefault,
})
);
private _editLocation() {
@ -441,11 +440,6 @@ class HaConfigSectionGeneral extends LitElement {
margin-top: 8px;
display: inline-block;
}
ha-locations-editor {
display: block;
height: 400px;
padding: 16px;
}
`,
];
}

View File

@ -0,0 +1,150 @@
import "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-form/ha-form";
import { HomeZoneMutableParams } from "../../../data/zone";
import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { HomeZoneDetailDialogParams } from "./show-dialog-home-zone-detail";
const SCHEMA = [
{
name: "location",
required: true,
selector: { location: { radius: true, radius_readonly: true } },
},
];
class DialogHomeZoneDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _error?: Record<string, string>;
@state() private _data?: HomeZoneMutableParams;
@state() private _params?: HomeZoneDetailDialogParams;
@state() private _submitting = false;
public showDialog(params: HomeZoneDetailDialogParams): void {
this._params = params;
this._error = undefined;
this._data = {
latitude: this.hass.config.latitude,
longitude: this.hass.config.longitude,
};
}
public closeDialog(): void {
this._params = undefined;
this._data = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._params || !this._data) {
return nothing;
}
const latInvalid = String(this._data.latitude) === "";
const lngInvalid = String(this._data.longitude) === "";
const valid = !latInvalid && !lngInvalid;
return html`
<ha-dialog
open
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this.hass!.localize("ui.panel.config.zone.edit_home")
)}
>
<div>
<ha-form
.hass=${this.hass}
.schema=${SCHEMA}
.data=${this._formData(this._data)}
.error=${this._error}
.computeLabel=${this._computeLabel}
@value-changed=${this._valueChanged}
></ha-form>
<p>
${this.hass!.localize(
"ui.panel.config.zone.detail.no_edit_home_zone_radius"
)}
</p>
</div>
<mwc-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${!valid || this._submitting}
>
${this.hass!.localize("ui.panel.config.zone.detail.update")}
</mwc-button>
</ha-dialog>
`;
}
private _formData = memoizeOne((data: HomeZoneMutableParams) => ({
...data,
location: {
latitude: data.latitude,
longitude: data.longitude,
radius: this.hass.states["zone.home"]?.attributes?.radius || 100,
},
}));
private _valueChanged(ev: CustomEvent) {
this._error = undefined;
const value = { ...ev.detail.value };
value.latitude = value.location.latitude;
value.longitude = value.location.longitude;
delete value.location;
this._data = value;
}
private _computeLabel = (): string => "";
private async _updateEntry() {
this._submitting = true;
try {
await this._params!.updateEntry!(this._data!);
this.closeDialog();
} catch (err: any) {
this._error = { base: err ? err.message : "Unknown error" };
} finally {
this._submitting = false;
}
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-min-width: min(600px, 95vw);
}
@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)
);
}
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-home-zone-detail": DialogHomeZoneDetail;
}
}
customElements.define("dialog-home-zone-detail", DialogHomeZoneDetail);

View File

@ -145,30 +145,8 @@ class DialogZoneDetail extends LitElement {
required: true,
selector: { location: { radius: true, icon } },
},
{
name: "",
type: "grid",
schema: [
{
name: "latitude",
required: true,
selector: { number: {} },
},
{
name: "longitude",
required: true,
selector: { number: {} },
},
],
},
{ name: "passive_note", type: "constant" },
{ name: "passive", selector: { boolean: {} } },
{
name: "radius",
required: false,
selector: { number: { min: 0, max: 999999, mode: "box" } },
},
] as const
);
@ -184,15 +162,9 @@ class DialogZoneDetail extends LitElement {
private _valueChanged(ev: CustomEvent) {
this._error = undefined;
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);
}
value.latitude = value.location.latitude;
value.longitude = value.location.longitude;
value.radius = value.location.radius;
delete value.location;
if (!value.icon) {
delete value.icon;

View File

@ -1,4 +1,4 @@
import { mdiCog, mdiPencil, mdiPencilOff, mdiPlus } from "@mdi/js";
import { mdiPencil, mdiPencilOff, mdiPlus } from "@mdi/js";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-listbox/paper-listbox";
@ -35,6 +35,7 @@ import {
updateZone,
Zone,
ZoneMutableParams,
HomeZoneMutableParams,
} from "../../../data/zone";
import {
showAlertDialog,
@ -47,6 +48,7 @@ import type { HomeAssistant, Route } from "../../../types";
import "../ha-config-section";
import { configSections } from "../ha-panel-config";
import { showZoneDetailDialog } from "./show-dialog-zone-detail";
import { showHomeZoneDetailDialog } from "./show-dialog-home-zone-detail";
@customElement("ha-config-zone")
export class HaConfigZone extends SubscribeMixin(LitElement) {
@ -193,12 +195,12 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
!this._canEditCore}
.path=${stateObject.entity_id === "zone.home" &&
this._canEditCore
? mdiCog
? mdiPencil
: mdiPencilOff}
.label=${stateObject.entity_id === "zone.home"
? hass.localize("ui.panel.config.zone.edit_home")
: hass.localize("ui.panel.config.zone.edit_zone")}
@click=${this._openCoreConfig}
@click=${this._editHomeZone}
></ha-icon-button>
${stateObject.entity_id !== "zone.home"
? html`
@ -400,7 +402,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
this._openDialog(entry);
}
private async _openCoreConfig(ev) {
private async _editHomeZone(ev) {
if (ev.currentTarget.noEdit) {
showAlertDialog(this, {
title: this.hass.localize("ui.panel.config.zone.can_not_edit"),
@ -409,7 +411,9 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
});
return;
}
navigate("/config/general");
showHomeZoneDetailDialog(this, {
updateEntry: (values) => this._updateHomeZoneEntry(values),
});
}
private async _createEntry(values: ZoneMutableParams) {
@ -427,6 +431,14 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
this._map?.fitMarker(created.id);
}
private async _updateHomeZoneEntry(values: HomeZoneMutableParams) {
await saveCoreConfig(this.hass, {
latitude: values.latitude,
longitude: values.longitude,
});
this._zoomZone("zone.home");
}
private async _updateEntry(
entry: Zone,
values: Partial<ZoneMutableParams>,

View File

@ -0,0 +1,20 @@
import { fireEvent } from "../../../common/dom/fire_event";
import { HomeZoneMutableParams } from "../../../data/zone";
export interface HomeZoneDetailDialogParams {
updateEntry?: (updates: HomeZoneMutableParams) => Promise<unknown>;
}
export const loadHomeZoneDetailDialog = () =>
import("./dialog-home-zone-detail");
export const showHomeZoneDetailDialog = (
element: HTMLElement,
params: HomeZoneDetailDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-home-zone-detail",
dialogImport: loadHomeZoneDetailDialog,
dialogParams: params,
});
};

View File

@ -377,6 +377,11 @@
"upload_failed": "Upload failed",
"unknown_file": "Unknown file"
},
"location": {
"latitude": "[%key:ui::panel::config::zone::detail::latitude%]",
"longitude": "[%key:ui::panel::config::zone::detail::longitude%]",
"radius": "[%key:ui::panel::config::zone::detail::radius%]"
},
"selector": {
"options": "Selector Options",
"types": {
@ -4122,10 +4127,6 @@
"confirm_delete": "Are you sure you want to delete this zone?",
"can_not_edit": "Unable to edit zone",
"configured_in_yaml": "Zones configured via configuration.yaml cannot be edited via the UI.",
"edit_home_zone": "The radius of the Home zone can't be edited from the frontend yet. Drag the marker on the map to move the home zone.",
"edit_home_zone_narrow": "The radius of the Home zone can't be edited from the frontend yet. The location can be changed from the general configuration.",
"go_to_core_config": "Go to general configuration?",
"home_zone_core_config": "The location of your home zone is editable from the general configuration page. The radius of the Home zone can't be edited from the frontend yet. Do you want to go to the general configuration?",
"detail": {
"new_zone": "New zone",
"name": "Name",
@ -4140,7 +4141,8 @@
"required_error_msg": "This field is required",
"delete": "Delete",
"create": "Add",
"update": "Update"
"update": "Update",
"no_edit_home_zone_radius": "The radius of the home zone is not editable in the UI."
},
"core_location_dialog": "Home Assistant location"
},