mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-14 12:56:37 +00:00
Add zones config UI (#4556)
* Add zones config UI * Update en.json * Update dialog-zone-detail.ts * Update hc-cast.ts * Update more-info-content.ts * Add drag radius and icon to dialog * Review comments
This commit is contained in:
parent
def0c51669
commit
49611e285f
@ -87,6 +87,7 @@
|
||||
"intl-messageformat": "^2.2.0",
|
||||
"js-yaml": "^3.13.1",
|
||||
"leaflet": "^1.4.0",
|
||||
"leaflet-draw": "^1.0.4",
|
||||
"lit-element": "^2.2.1",
|
||||
"lit-html": "^1.1.0",
|
||||
"lit-virtualizer": "^0.4.2",
|
||||
@ -123,6 +124,7 @@
|
||||
"@types/hls.js": "^0.12.3",
|
||||
"@types/js-yaml": "^3.12.1",
|
||||
"@types/leaflet": "^1.4.3",
|
||||
"@types/leaflet-draw": "^1.0.1",
|
||||
"@types/memoize-one": "4.1.0",
|
||||
"@types/mocha": "^5.2.6",
|
||||
"@types/webspeechapi": "^0.0.29",
|
||||
|
@ -2,10 +2,12 @@ import { Map } from "leaflet";
|
||||
|
||||
// Sets up a Leaflet map on the provided DOM element
|
||||
export type LeafletModuleType = typeof import("leaflet");
|
||||
export type LeafletDrawModuleType = typeof import("leaflet-draw");
|
||||
|
||||
export const setupLeafletMap = async (
|
||||
mapElement: HTMLElement,
|
||||
darkMode = false
|
||||
darkMode = false,
|
||||
draw = false
|
||||
): Promise<[Map, LeafletModuleType]> => {
|
||||
if (!mapElement.parentNode) {
|
||||
throw new Error("Cannot setup Leaflet map on disconnected element");
|
||||
@ -16,6 +18,10 @@ export const setupLeafletMap = async (
|
||||
)) as LeafletModuleType;
|
||||
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
|
||||
|
||||
if (draw) {
|
||||
await import(/* webpackChunkName: "leaflet-draw" */ "leaflet-draw");
|
||||
}
|
||||
|
||||
const map = Leaflet.map(mapElement);
|
||||
const style = document.createElement("link");
|
||||
style.setAttribute("href", "/static/images/leaflet/leaflet.css");
|
||||
|
@ -8,24 +8,35 @@ import {
|
||||
customElement,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import { Marker, Map, LeafletMouseEvent, DragEndEvent, LatLng } from "leaflet";
|
||||
import {
|
||||
Marker,
|
||||
Map,
|
||||
LeafletMouseEvent,
|
||||
DragEndEvent,
|
||||
LatLng,
|
||||
Circle,
|
||||
DivIcon,
|
||||
} from "leaflet";
|
||||
import {
|
||||
setupLeafletMap,
|
||||
LeafletModuleType,
|
||||
} from "../../common/dom/setup-leaflet-map";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
|
||||
@customElement("ha-location-editor")
|
||||
class LocationEditor extends LitElement {
|
||||
@property() public location?: [number, number];
|
||||
@property() public radius?: number;
|
||||
@property() public icon?: string;
|
||||
public fitZoom = 16;
|
||||
|
||||
private _iconEl?: DivIcon;
|
||||
private _ignoreFitToMap?: [number, number];
|
||||
|
||||
// tslint:disable-next-line
|
||||
private Leaflet?: LeafletModuleType;
|
||||
private _leafletMap?: Map;
|
||||
private _locationMarker?: Marker;
|
||||
private _locationMarker?: Marker | Circle;
|
||||
|
||||
public fitMap(): void {
|
||||
if (!this._leafletMap || !this.location) {
|
||||
@ -53,11 +64,24 @@ class LocationEditor extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this._updateMarker();
|
||||
if (!this._ignoreFitToMap || this._ignoreFitToMap !== this.location) {
|
||||
this.fitMap();
|
||||
if (changedProps.has("location")) {
|
||||
this._updateMarker();
|
||||
if (
|
||||
this.location &&
|
||||
(!this._ignoreFitToMap ||
|
||||
this._ignoreFitToMap[0] !== this.location[0] ||
|
||||
this._ignoreFitToMap[1] !== this.location[1])
|
||||
) {
|
||||
this.fitMap();
|
||||
}
|
||||
this._ignoreFitToMap = undefined;
|
||||
}
|
||||
if (changedProps.has("radius")) {
|
||||
this._updateRadius();
|
||||
}
|
||||
if (changedProps.has("icon")) {
|
||||
this._updateIcon();
|
||||
}
|
||||
this._ignoreFitToMap = undefined;
|
||||
}
|
||||
|
||||
private get _mapEl(): HTMLDivElement {
|
||||
@ -65,18 +89,23 @@ class LocationEditor extends LitElement {
|
||||
}
|
||||
|
||||
private async _initMap(): Promise<void> {
|
||||
[this._leafletMap, this.Leaflet] = await setupLeafletMap(this._mapEl);
|
||||
[this._leafletMap, this.Leaflet] = await setupLeafletMap(
|
||||
this._mapEl,
|
||||
false,
|
||||
Boolean(this.radius)
|
||||
);
|
||||
this._leafletMap.addEventListener(
|
||||
"click",
|
||||
// @ts-ignore
|
||||
(ev: LeafletMouseEvent) => this._updateLocation(ev.latlng)
|
||||
(ev: LeafletMouseEvent) => this._locationUpdated(ev.latlng)
|
||||
);
|
||||
this._updateIcon();
|
||||
this._updateMarker();
|
||||
this.fitMap();
|
||||
this._leafletMap.invalidateSize();
|
||||
}
|
||||
|
||||
private _updateLocation(latlng: LatLng) {
|
||||
private _locationUpdated(latlng: LatLng) {
|
||||
let longitude = latlng.lng;
|
||||
if (Math.abs(longitude) > 180.0) {
|
||||
// Normalize longitude if map provides values beyond -180 to +180 degrees.
|
||||
@ -86,7 +115,68 @@ class LocationEditor extends LitElement {
|
||||
fireEvent(this, "change", undefined, { bubbles: false });
|
||||
}
|
||||
|
||||
private _updateMarker(): void {
|
||||
private _radiusUpdated() {
|
||||
this._ignoreFitToMap = this.location;
|
||||
this.radius = (this._locationMarker as Circle).getRadius();
|
||||
fireEvent(this, "change", undefined, { bubbles: false });
|
||||
}
|
||||
|
||||
private _updateIcon() {
|
||||
if (!this.icon) {
|
||||
this._iconEl = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
// create icon
|
||||
let iconHTML = "";
|
||||
const el = document.createElement("ha-icon");
|
||||
el.setAttribute("icon", this.icon);
|
||||
iconHTML = el.outerHTML;
|
||||
|
||||
this._iconEl = this.Leaflet!.divIcon({
|
||||
html: iconHTML,
|
||||
iconSize: [24, 24],
|
||||
className: "light leaflet-edit-move",
|
||||
});
|
||||
this._setIcon();
|
||||
}
|
||||
|
||||
private _setIcon() {
|
||||
if (!this._locationMarker || !this._iconEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.radius) {
|
||||
(this._locationMarker as Marker).setIcon(this._iconEl);
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const moveMarker = this._locationMarker.editing._moveMarker;
|
||||
moveMarker.setIcon(this._iconEl);
|
||||
}
|
||||
|
||||
private _setupEdit() {
|
||||
// @ts-ignore
|
||||
this._locationMarker.editing.enable();
|
||||
// @ts-ignore
|
||||
const moveMarker = this._locationMarker.editing._moveMarker;
|
||||
// @ts-ignore
|
||||
const resizeMarker = this._locationMarker.editing._resizeMarkers[0];
|
||||
this._setIcon();
|
||||
moveMarker.addEventListener(
|
||||
"dragend",
|
||||
// @ts-ignore
|
||||
(ev: DragEndEvent) => this._locationUpdated(ev.target.getLatLng())
|
||||
);
|
||||
resizeMarker.addEventListener(
|
||||
"dragend",
|
||||
// @ts-ignore
|
||||
(ev: DragEndEvent) => this._radiusUpdated(ev)
|
||||
);
|
||||
}
|
||||
|
||||
private async _updateMarker(): Promise<void> {
|
||||
if (!this.location) {
|
||||
if (this._locationMarker) {
|
||||
this._locationMarker.remove();
|
||||
@ -97,17 +187,41 @@ class LocationEditor extends LitElement {
|
||||
|
||||
if (this._locationMarker) {
|
||||
this._locationMarker.setLatLng(this.location);
|
||||
if (this.radius) {
|
||||
// @ts-ignore
|
||||
this._locationMarker.editing.disable();
|
||||
await nextRender();
|
||||
this._setupEdit();
|
||||
}
|
||||
return;
|
||||
}
|
||||
this._locationMarker = this.Leaflet!.marker(this.location, {
|
||||
draggable: true,
|
||||
});
|
||||
this._locationMarker.addEventListener(
|
||||
"dragend",
|
||||
// @ts-ignore
|
||||
(ev: DragEndEvent) => this._updateLocation(ev.target.getLatLng())
|
||||
);
|
||||
this._leafletMap!.addLayer(this._locationMarker);
|
||||
|
||||
if (!this.radius) {
|
||||
this._locationMarker = this.Leaflet!.marker(this.location, {
|
||||
draggable: true,
|
||||
});
|
||||
this._setIcon();
|
||||
this._locationMarker.addEventListener(
|
||||
"dragend",
|
||||
// @ts-ignore
|
||||
(ev: DragEndEvent) => this._locationUpdated(ev.target.getLatLng())
|
||||
);
|
||||
this._leafletMap!.addLayer(this._locationMarker);
|
||||
} else {
|
||||
this._locationMarker = this.Leaflet!.circle(this.location, {
|
||||
color: "#FF9800",
|
||||
radius: this.radius,
|
||||
});
|
||||
this._leafletMap!.addLayer(this._locationMarker);
|
||||
this._setupEdit();
|
||||
}
|
||||
}
|
||||
|
||||
private _updateRadius(): void {
|
||||
if (!this._locationMarker || !this.radius) {
|
||||
return;
|
||||
}
|
||||
(this._locationMarker as Circle).setRadius(this.radius);
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
@ -119,6 +233,13 @@ class LocationEditor extends LitElement {
|
||||
#map {
|
||||
height: 100%;
|
||||
}
|
||||
.leaflet-edit-move {
|
||||
cursor: move !important;
|
||||
}
|
||||
.leaflet-edit-resize {
|
||||
border-radius: 50%;
|
||||
cursor: nesw-resize !important;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
273
src/components/map/ha-locations-editor.ts
Normal file
273
src/components/map/ha-locations-editor.ts
Normal file
@ -0,0 +1,273 @@
|
||||
import {
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
html,
|
||||
CSSResult,
|
||||
css,
|
||||
customElement,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import {
|
||||
Marker,
|
||||
Map,
|
||||
DragEndEvent,
|
||||
LatLng,
|
||||
Circle,
|
||||
MarkerOptions,
|
||||
DivIcon,
|
||||
} from "leaflet";
|
||||
import {
|
||||
setupLeafletMap,
|
||||
LeafletModuleType,
|
||||
} from "../../common/dom/setup-leaflet-map";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"location-updated": { id: string; location: [number, number] };
|
||||
"radius-updated": { id: string; radius: number };
|
||||
"marker-clicked": { id: string };
|
||||
}
|
||||
}
|
||||
|
||||
export interface Location {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
radius: number;
|
||||
name: string;
|
||||
id: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
@customElement("ha-locations-editor")
|
||||
export class HaLocationsEditor extends LitElement {
|
||||
@property() public locations?: Location[];
|
||||
public fitZoom = 16;
|
||||
|
||||
// tslint:disable-next-line
|
||||
private Leaflet?: LeafletModuleType;
|
||||
// tslint:disable-next-line
|
||||
private _leafletMap?: Map;
|
||||
private _locationMarkers?: { [key: string]: Marker | Circle };
|
||||
|
||||
public fitMap(): void {
|
||||
if (
|
||||
!this._leafletMap ||
|
||||
!this._locationMarkers ||
|
||||
!Object.keys(this._locationMarkers).length
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const bounds = this.Leaflet!.latLngBounds(
|
||||
Object.values(this._locationMarkers).map((item) => item.getLatLng())
|
||||
);
|
||||
this._leafletMap.fitBounds(bounds.pad(0.5));
|
||||
}
|
||||
|
||||
public fitMarker(id: string): void {
|
||||
if (!this._leafletMap || !this._locationMarkers) {
|
||||
return;
|
||||
}
|
||||
const marker = this._locationMarkers[id];
|
||||
if (!marker) {
|
||||
return;
|
||||
}
|
||||
this._leafletMap.setView(marker.getLatLng(), this.fitZoom);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
return html`
|
||||
<div id="map"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues): void {
|
||||
super.firstUpdated(changedProps);
|
||||
this._initMap();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
|
||||
// Still loading.
|
||||
if (!this.Leaflet) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (changedProps.has("locations")) {
|
||||
this._updateMarkers();
|
||||
}
|
||||
}
|
||||
|
||||
private get _mapEl(): HTMLDivElement {
|
||||
return this.shadowRoot!.querySelector("div")!;
|
||||
}
|
||||
|
||||
private async _initMap(): Promise<void> {
|
||||
[this._leafletMap, this.Leaflet] = await setupLeafletMap(
|
||||
this._mapEl,
|
||||
false,
|
||||
true
|
||||
);
|
||||
this._updateMarkers();
|
||||
this.fitMap();
|
||||
this._leafletMap.invalidateSize();
|
||||
}
|
||||
|
||||
private _updateLocation(ev: DragEndEvent) {
|
||||
const marker = ev.target;
|
||||
const latlng: LatLng = marker.getLatLng();
|
||||
let longitude: number = latlng.lng;
|
||||
if (Math.abs(longitude) > 180.0) {
|
||||
// Normalize longitude if map provides values beyond -180 to +180 degrees.
|
||||
longitude = (((longitude % 360.0) + 540.0) % 360.0) - 180.0;
|
||||
}
|
||||
const location: [number, number] = [latlng.lat, longitude];
|
||||
fireEvent(
|
||||
this,
|
||||
"location-updated",
|
||||
{ id: marker.id, location },
|
||||
{ bubbles: false }
|
||||
);
|
||||
}
|
||||
|
||||
private _updateRadius(ev: DragEndEvent) {
|
||||
const marker = ev.target;
|
||||
const circle = this._locationMarkers![marker.id] as Circle;
|
||||
fireEvent(
|
||||
this,
|
||||
"radius-updated",
|
||||
{ id: marker.id, radius: circle.getRadius() },
|
||||
{ bubbles: false }
|
||||
);
|
||||
}
|
||||
|
||||
private _markerClicked(ev: DragEndEvent) {
|
||||
const marker = ev.target;
|
||||
fireEvent(this, "marker-clicked", { id: marker.id }, { bubbles: false });
|
||||
}
|
||||
|
||||
private _updateMarkers(): void {
|
||||
if (this._locationMarkers) {
|
||||
Object.values(this._locationMarkers).forEach((marker) => {
|
||||
marker.remove();
|
||||
});
|
||||
this._locationMarkers = undefined;
|
||||
}
|
||||
|
||||
if (!this.locations || !this.locations.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._locationMarkers = {};
|
||||
|
||||
this.locations.forEach((location: Location) => {
|
||||
let icon: DivIcon | undefined;
|
||||
if (location.icon) {
|
||||
// create icon
|
||||
let iconHTML = "";
|
||||
const el = document.createElement("ha-icon");
|
||||
el.setAttribute("icon", location.icon);
|
||||
iconHTML = el.outerHTML;
|
||||
|
||||
icon = this.Leaflet!.divIcon({
|
||||
html: iconHTML,
|
||||
iconSize: [24, 24],
|
||||
className: "light leaflet-edit-move",
|
||||
});
|
||||
}
|
||||
if (location.radius) {
|
||||
const circle = this.Leaflet!.circle(
|
||||
[location.latitude, location.longitude],
|
||||
{
|
||||
color: "#FF9800",
|
||||
radius: location.radius,
|
||||
}
|
||||
);
|
||||
// @ts-ignore
|
||||
circle.editing.enable();
|
||||
circle.addTo(this._leafletMap!);
|
||||
// @ts-ignore
|
||||
const moveMarker = circle.editing._moveMarker;
|
||||
// @ts-ignore
|
||||
const resizeMarker = circle.editing._resizeMarkers[0];
|
||||
if (icon) {
|
||||
moveMarker.setIcon(icon);
|
||||
}
|
||||
resizeMarker.id = moveMarker.id = location.id;
|
||||
moveMarker
|
||||
.addEventListener(
|
||||
"dragend",
|
||||
// @ts-ignore
|
||||
(ev: DragEndEvent) => this._updateLocation(ev)
|
||||
)
|
||||
.addEventListener(
|
||||
"click",
|
||||
// @ts-ignore
|
||||
(ev: MouseEvent) => this._markerClicked(ev)
|
||||
);
|
||||
resizeMarker.addEventListener(
|
||||
"dragend",
|
||||
// @ts-ignore
|
||||
(ev: DragEndEvent) => this._updateRadius(ev)
|
||||
);
|
||||
this._locationMarkers![location.id] = circle;
|
||||
} else {
|
||||
const options: MarkerOptions = {
|
||||
draggable: true,
|
||||
title: location.name,
|
||||
};
|
||||
|
||||
if (icon) {
|
||||
options.icon = icon;
|
||||
}
|
||||
|
||||
const marker = this.Leaflet!.marker(
|
||||
[location.latitude, location.longitude],
|
||||
options
|
||||
)
|
||||
.addEventListener(
|
||||
"dragend",
|
||||
// @ts-ignore
|
||||
(ev: DragEndEvent) => this._updateLocation(ev)
|
||||
)
|
||||
.addEventListener(
|
||||
"click",
|
||||
// @ts-ignore
|
||||
(ev: MouseEvent) => this._markerClicked(ev)
|
||||
)
|
||||
.addTo(this._leafletMap);
|
||||
marker.id = location.id;
|
||||
|
||||
this._locationMarkers![location.id] = marker;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 300px;
|
||||
}
|
||||
#map {
|
||||
height: 100%;
|
||||
}
|
||||
.leaflet-edit-move {
|
||||
cursor: move !important;
|
||||
}
|
||||
.leaflet-edit-resize {
|
||||
border-radius: 50%;
|
||||
cursor: nesw-resize !important;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-locations-editor": HaLocationsEditor;
|
||||
}
|
||||
}
|
46
src/data/zone.ts
Normal file
46
src/data/zone.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface Zone {
|
||||
id: string;
|
||||
name: string;
|
||||
icon?: string;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
passive?: boolean;
|
||||
radius?: number;
|
||||
}
|
||||
|
||||
export interface ZoneMutableParams {
|
||||
icon: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
name: string;
|
||||
passive: boolean;
|
||||
radius: number;
|
||||
}
|
||||
|
||||
export const fetchZones = (hass: HomeAssistant) =>
|
||||
hass.callWS<Zone[]>({ type: "zone/list" });
|
||||
|
||||
export const createZone = (hass: HomeAssistant, values: ZoneMutableParams) =>
|
||||
hass.callWS<Zone>({
|
||||
type: "zone/create",
|
||||
...values,
|
||||
});
|
||||
|
||||
export const updateZone = (
|
||||
hass: HomeAssistant,
|
||||
zoneId: string,
|
||||
updates: Partial<ZoneMutableParams>
|
||||
) =>
|
||||
hass.callWS<Zone>({
|
||||
type: "zone/update",
|
||||
zone_id: zoneId,
|
||||
...updates,
|
||||
});
|
||||
|
||||
export const deleteZone = (hass: HomeAssistant, zoneId: string) =>
|
||||
hass.callWS({
|
||||
type: "zone/delete",
|
||||
zone_id: zoneId,
|
||||
});
|
@ -94,6 +94,7 @@ class HaConfigDashboard extends LitElement {
|
||||
.pages=${[
|
||||
{ page: "integrations", core: true },
|
||||
{ page: "devices", core: true },
|
||||
{ page: "entities", core: true },
|
||||
{ page: "automation" },
|
||||
{ page: "script" },
|
||||
{ page: "scene" },
|
||||
@ -107,8 +108,8 @@ class HaConfigDashboard extends LitElement {
|
||||
.pages=${[
|
||||
{ page: "core", core: true },
|
||||
{ page: "server_control", core: true },
|
||||
{ page: "entities", core: true },
|
||||
{ page: "areas", core: true },
|
||||
{ page: "zone" },
|
||||
{ page: "person" },
|
||||
{ page: "users", core: true },
|
||||
{ page: "zha" },
|
||||
|
@ -125,6 +125,13 @@ class HaConfigRouter extends HassRouterPage {
|
||||
/* webpackChunkName: "panel-config-users" */ "./users/ha-config-users"
|
||||
),
|
||||
},
|
||||
zone: {
|
||||
tag: "ha-config-zone",
|
||||
load: () =>
|
||||
import(
|
||||
/* webpackChunkName: "panel-config-zone" */ "./zone/ha-config-zone"
|
||||
),
|
||||
},
|
||||
zha: {
|
||||
tag: "zha-config-dashboard-router",
|
||||
load: () =>
|
||||
|
@ -115,6 +115,7 @@ class HaPanelConfig extends LitElement {
|
||||
{ page: "scene" },
|
||||
{ page: "core", core: true },
|
||||
{ page: "areas", core: true },
|
||||
{ page: "zone" },
|
||||
{ page: "person" },
|
||||
{ page: "users", core: true },
|
||||
{ page: "server_control", core: true },
|
||||
@ -163,7 +164,7 @@ class HaPanelConfig extends LitElement {
|
||||
}
|
||||
|
||||
.side-bar {
|
||||
border-right: 1px solid #e0e0e0;
|
||||
border-right: 1px solid var(--divider-color);
|
||||
background: white;
|
||||
width: 320px;
|
||||
float: left;
|
||||
|
@ -148,6 +148,24 @@ class HaConfigSectionServerControl extends LocalizeMixin(PolymerElement) {
|
||||
</ha-call-service-button>
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[personLoaded(hass)]]">
|
||||
<div class="card-actions">
|
||||
<ha-call-service-button
|
||||
hass="[[hass]]"
|
||||
domain="person"
|
||||
service="reload"
|
||||
>[[localize('ui.panel.config.server_control.section.reloading.person')]]
|
||||
</ha-call-service-button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="card-actions">
|
||||
<ha-call-service-button
|
||||
hass="[[hass]]"
|
||||
domain="zone"
|
||||
service="reload"
|
||||
>[[localize('ui.panel.config.server_control.section.reloading.zone')]]
|
||||
</ha-call-service-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
</template>
|
||||
<ha-card
|
||||
@ -225,6 +243,10 @@ class HaConfigSectionServerControl extends LocalizeMixin(PolymerElement) {
|
||||
return isComponentLoaded(hass, "scene");
|
||||
}
|
||||
|
||||
personLoaded(hass) {
|
||||
return isComponentLoaded(hass, "person");
|
||||
}
|
||||
|
||||
validateConfig() {
|
||||
this.validating = true;
|
||||
this.validateLog = "";
|
||||
|
275
src/panels/config/zone/dialog-zone-detail.ts
Normal file
275
src/panels/config/zone/dialog-zone-detail.ts
Normal file
@ -0,0 +1,275 @@
|
||||
import {
|
||||
LitElement,
|
||||
html,
|
||||
css,
|
||||
CSSResult,
|
||||
TemplateResult,
|
||||
property,
|
||||
} from "lit-element";
|
||||
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@material/mwc-button";
|
||||
import "@material/mwc-dialog";
|
||||
|
||||
import "../../../components/map/ha-location-editor";
|
||||
import "../../../components/ha-switch";
|
||||
|
||||
import { ZoneDetailDialogParams } from "./show-dialog-zone-detail";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { ZoneMutableParams } from "../../../data/zone";
|
||||
|
||||
class DialogZoneDetail extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() private _name!: string;
|
||||
@property() private _icon!: string;
|
||||
@property() private _latitude!: number;
|
||||
@property() private _longitude!: number;
|
||||
@property() private _passive!: boolean;
|
||||
@property() private _radius!: number;
|
||||
@property() private _error?: string;
|
||||
@property() private _params?: ZoneDetailDialogParams;
|
||||
@property() private _submitting: boolean = false;
|
||||
|
||||
public async showDialog(params: ZoneDetailDialogParams): Promise<void> {
|
||||
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;
|
||||
} else {
|
||||
this._name = "";
|
||||
this._icon = "";
|
||||
this._latitude = this.hass.config.latitude;
|
||||
this._longitude = this.hass.config.longitude;
|
||||
this._passive = false;
|
||||
this._radius = 100;
|
||||
}
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this._params) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<mwc-dialog
|
||||
open
|
||||
@closing="${this._close}"
|
||||
.title=${this._params.entry
|
||||
? this._params.entry.name
|
||||
: this.hass!.localize("ui.panel.config.zone.detail.new_zone")}
|
||||
>
|
||||
<div>
|
||||
${this._error
|
||||
? html`
|
||||
<div class="error">${this._error}</div>
|
||||
`
|
||||
: ""}
|
||||
<div class="form">
|
||||
<paper-input
|
||||
.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"
|
||||
)}"
|
||||
.invalid=${this._name.trim() === ""}
|
||||
></paper-input>
|
||||
<paper-input
|
||||
.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=${!this._icon.trim().includes(":")}
|
||||
></paper-input>
|
||||
<ha-location-editor
|
||||
class="flex"
|
||||
.location=${this._locationValue}
|
||||
.radius=${this._radius}
|
||||
.icon=${this._icon || "hass:home"}
|
||||
@change=${this._locationChanged}
|
||||
></ha-location-editor>
|
||||
<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=${String(this._latitude) === ""}
|
||||
></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=${String(this._longitude) === ""}
|
||||
></paper-input>
|
||||
<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=${String(this._radius) === ""}
|
||||
></paper-input>
|
||||
<p>
|
||||
${this.hass!.localize("ui.panel.config.zone.detail.passive_note")}
|
||||
</p>
|
||||
<ha-switch .checked=${this._passive} @change=${this._passiveChanged}
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.config.zone.detail.passive"
|
||||
)}</ha-switch
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
${this._params.entry
|
||||
? html`
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
class="warning"
|
||||
@click="${this._deleteEntry}"
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass!.localize("ui.panel.config.zone.detail.delete")}
|
||||
</mwc-button>
|
||||
`
|
||||
: html``}
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click="${this._updateEntry}"
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this._params.entry
|
||||
? this.hass!.localize("ui.panel.config.zone.detail.update")
|
||||
: this.hass!.localize("ui.panel.config.zone.detail.create")}
|
||||
</mwc-button>
|
||||
</mwc-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _locationValue() {
|
||||
return [Number(this._latitude), Number(this._longitude)];
|
||||
}
|
||||
|
||||
private _locationChanged(ev) {
|
||||
[this._latitude, this._longitude] = ev.currentTarget.location;
|
||||
this._radius = ev.currentTarget.radius;
|
||||
}
|
||||
|
||||
private _passiveChanged(ev) {
|
||||
this._passive = ev.target.checked;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
const configValue = (ev.target as any).configValue;
|
||||
|
||||
this._error = undefined;
|
||||
this[`_${configValue}`] = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _updateEntry() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
const values: ZoneMutableParams = {
|
||||
name: this._name.trim(),
|
||||
icon: this._icon.trim(),
|
||||
latitude: this._latitude,
|
||||
longitude: this._longitude,
|
||||
passive: this._passive,
|
||||
radius: this._radius,
|
||||
};
|
||||
if (this._params!.entry) {
|
||||
await this._params!.updateEntry!(values);
|
||||
} else {
|
||||
await this._params!.createEntry(values);
|
||||
}
|
||||
this._params = undefined;
|
||||
} catch (err) {
|
||||
this._error = err ? err.message : "Unknown error";
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _deleteEntry() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
if (await this._params!.removeEntry!()) {
|
||||
this._params = undefined;
|
||||
}
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _close(): void {
|
||||
this._params = undefined;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
css`
|
||||
mwc-dialog {
|
||||
--mdc-dialog-title-ink-color: var(--primary-text-color);
|
||||
}
|
||||
@media only screen and (min-width: 600px) {
|
||||
mwc-dialog {
|
||||
--mdc-dialog-min-width: 600px;
|
||||
}
|
||||
}
|
||||
.form {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
ha-user-picker {
|
||||
margin-top: 16px;
|
||||
}
|
||||
mwc-button.warning {
|
||||
--mdc-theme-primary: var(--google-red-500);
|
||||
}
|
||||
.error {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
p {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-zone-detail": DialogZoneDetail;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("dialog-zone-detail", DialogZoneDetail);
|
296
src/panels/config/zone/ha-config-zone.ts
Normal file
296
src/panels/config/zone/ha-config-zone.ts
Normal file
@ -0,0 +1,296 @@
|
||||
import {
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
html,
|
||||
css,
|
||||
CSSResult,
|
||||
property,
|
||||
customElement,
|
||||
query,
|
||||
} from "lit-element";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import "@polymer/paper-item/paper-icon-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
|
||||
import "../../../components/map/ha-locations-editor";
|
||||
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import { compare } from "../../../common/string/compare";
|
||||
import "../ha-config-section";
|
||||
import { showZoneDetailDialog } from "./show-dialog-zone-detail";
|
||||
import {
|
||||
Zone,
|
||||
fetchZones,
|
||||
createZone,
|
||||
updateZone,
|
||||
deleteZone,
|
||||
ZoneMutableParams,
|
||||
} from "../../../data/zone";
|
||||
// tslint:disable-next-line
|
||||
import { HaLocationsEditor } from "../../../components/map/ha-locations-editor";
|
||||
|
||||
@customElement("ha-config-zone")
|
||||
export class HaConfigZone extends LitElement {
|
||||
@property() public hass?: HomeAssistant;
|
||||
@property() public isWide?: boolean;
|
||||
@property() public narrow?: boolean;
|
||||
@property() private _storageItems?: Zone[];
|
||||
@property() private _activeEntry: string = "";
|
||||
@query("ha-locations-editor") private _map?: HaLocationsEditor;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.hass || this._storageItems === undefined) {
|
||||
return html`
|
||||
<hass-loading-screen></hass-loading-screen>
|
||||
`;
|
||||
}
|
||||
const hass = this.hass;
|
||||
const listBox = html`
|
||||
<paper-listbox
|
||||
attr-for-selected="data-id"
|
||||
.selected=${this._activeEntry || ""}
|
||||
>
|
||||
${this._storageItems.map((entry) => {
|
||||
return html`
|
||||
<paper-icon-item data-id=${entry.id} @click=${
|
||||
this._itemClicked
|
||||
} .entry=${entry}>
|
||||
<ha-icon
|
||||
.icon=${entry.icon}
|
||||
slot="item-icon"
|
||||
>
|
||||
</ha-icon>
|
||||
<paper-item-body>
|
||||
${entry.name}
|
||||
</paper-item-body>
|
||||
${
|
||||
!this.narrow
|
||||
? html`
|
||||
<paper-icon-button
|
||||
icon="hass:information-outline"
|
||||
.entry=${entry}
|
||||
@click=${this._openEditEntry}
|
||||
></paper-icon-button>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</ha-icon>
|
||||
</paper-icon-item>
|
||||
`;
|
||||
})}
|
||||
</paper-listbox>
|
||||
${this._storageItems.length === 0
|
||||
? html`
|
||||
<div class="empty">
|
||||
${hass.localize("ui.panel.config.zone.no_zones_created_yet")}
|
||||
<mwc-button @click=${this._createZone}>
|
||||
${hass.localize("ui.panel.config.zone.create_zone")}</mwc-button
|
||||
>
|
||||
</div>
|
||||
`
|
||||
: html``}
|
||||
`;
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
.header=${hass.localize("ui.panel.config.zone.caption")}
|
||||
.showBackButton=${!this.isWide}
|
||||
>
|
||||
${this.narrow
|
||||
? html`
|
||||
<ha-config-section .isWide=${this.isWide}>
|
||||
<span slot="introduction">
|
||||
${hass.localize("ui.panel.config.zone.introduction")}
|
||||
</span>
|
||||
<ha-card>${listBox}</ha-card>
|
||||
</ha-config-section>
|
||||
`
|
||||
: ""}
|
||||
${!this.narrow
|
||||
? html`
|
||||
<div class="flex">
|
||||
<ha-locations-editor
|
||||
.locations=${this._storageItems}
|
||||
@location-updated=${this._locationUpdated}
|
||||
@radius-updated=${this._radiusUpdated}
|
||||
@marker-clicked=${this._markerClicked}
|
||||
></ha-locations-editor>
|
||||
${listBox}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</hass-subpage>
|
||||
|
||||
<ha-fab
|
||||
?is-wide=${this.isWide}
|
||||
icon="hass:plus"
|
||||
title="${hass.localize("ui.panel.config.zone.add_zone")}"
|
||||
@click=${this._createZone}
|
||||
></ha-fab>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._fetchData();
|
||||
}
|
||||
|
||||
private async _fetchData() {
|
||||
this._storageItems = (await fetchZones(this.hass!)).sort((ent1, ent2) =>
|
||||
compare(ent1.name, ent2.name)
|
||||
);
|
||||
}
|
||||
|
||||
private _locationUpdated(ev: CustomEvent) {
|
||||
this._activeEntry = ev.detail.id;
|
||||
const entry = this._storageItems!.find((item) => item.id === ev.detail.id);
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
this._updateEntry(entry, {
|
||||
latitude: ev.detail.location[0],
|
||||
longitude: ev.detail.location[1],
|
||||
});
|
||||
}
|
||||
|
||||
private _radiusUpdated(ev: CustomEvent) {
|
||||
this._activeEntry = ev.detail.id;
|
||||
const entry = this._storageItems!.find((item) => item.id === ev.detail.id);
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
this._updateEntry(entry, {
|
||||
radius: ev.detail.radius,
|
||||
});
|
||||
}
|
||||
|
||||
private _markerClicked(ev: CustomEvent) {
|
||||
this._activeEntry = ev.detail.id;
|
||||
}
|
||||
|
||||
private _createZone() {
|
||||
this._openDialog();
|
||||
}
|
||||
|
||||
private _itemClicked(ev: MouseEvent) {
|
||||
if (this.narrow) {
|
||||
this._openEditEntry(ev);
|
||||
return;
|
||||
}
|
||||
|
||||
const entry: Zone = (ev.currentTarget! as any).entry;
|
||||
this._map?.fitMarker(entry.id);
|
||||
}
|
||||
|
||||
private _openEditEntry(ev: MouseEvent) {
|
||||
const entry: Zone = (ev.currentTarget! as any).entry;
|
||||
this._openDialog(entry);
|
||||
}
|
||||
|
||||
private async _createEntry(values: ZoneMutableParams) {
|
||||
const created = await createZone(this.hass!, values);
|
||||
this._storageItems = this._storageItems!.concat(
|
||||
created
|
||||
).sort((ent1, ent2) => compare(ent1.name, ent2.name));
|
||||
}
|
||||
|
||||
private async _updateEntry(entry: Zone, values: Partial<ZoneMutableParams>) {
|
||||
const updated = await updateZone(this.hass!, entry!.id, values);
|
||||
this._storageItems = this._storageItems!.map((ent) =>
|
||||
ent === entry ? updated : ent
|
||||
);
|
||||
}
|
||||
|
||||
private async _removeEntry(entry: Zone) {
|
||||
if (
|
||||
!confirm(`${this.hass!.localize("ui.panel.config.zone.confirm_delete")}
|
||||
|
||||
${this.hass!.localize("ui.panel.config.zone.confirm_delete2")}`)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteZone(this.hass!, entry!.id);
|
||||
this._storageItems = this._storageItems!.filter((ent) => ent !== entry);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _openDialog(entry?: Zone) {
|
||||
showZoneDetailDialog(this, {
|
||||
entry,
|
||||
createEntry: (values) => this._createEntry(values),
|
||||
updateEntry: entry
|
||||
? (values) => this._updateEntry(entry, values)
|
||||
: undefined,
|
||||
removeEntry: entry ? () => this._removeEntry(entry) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 16px auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
ha-locations-editor {
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
}
|
||||
.flex paper-listbox {
|
||||
border-left: 1px solid var(--divider-color);
|
||||
width: 250px;
|
||||
}
|
||||
paper-icon-item {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
paper-icon-item.iron-selected:before {
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
background-color: var(--sidebar-selected-icon-color);
|
||||
opacity: 0.12;
|
||||
transition: opacity 15ms linear;
|
||||
will-change: opacity;
|
||||
}
|
||||
ha-card paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
ha-fab {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
ha-fab[is-wide] {
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
23
src/panels/config/zone/show-dialog-zone-detail.ts
Normal file
23
src/panels/config/zone/show-dialog-zone-detail.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { Zone, ZoneMutableParams } from "../../../data/zone";
|
||||
|
||||
export interface ZoneDetailDialogParams {
|
||||
entry?: Zone;
|
||||
createEntry: (values: ZoneMutableParams) => Promise<unknown>;
|
||||
updateEntry?: (updates: Partial<ZoneMutableParams>) => Promise<unknown>;
|
||||
removeEntry?: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export const loadZoneDetailDialog = () =>
|
||||
import(/* webpackChunkName: "zone-detail-dialog" */ "./dialog-zone-detail");
|
||||
|
||||
export const showZoneDetailDialog = (
|
||||
element: HTMLElement,
|
||||
systemLogDetailParams: ZoneDetailDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-zone-detail",
|
||||
dialogImport: loadZoneDetailDialog,
|
||||
dialogParams: systemLogDetailParams,
|
||||
});
|
||||
};
|
@ -739,7 +739,9 @@
|
||||
"group": "Reload groups",
|
||||
"automation": "Reload automations",
|
||||
"script": "Reload scripts",
|
||||
"scene": "Reload scenes"
|
||||
"scene": "Reload scenes",
|
||||
"person": "Reload persons",
|
||||
"zone": "Reload zones"
|
||||
},
|
||||
"server_management": {
|
||||
"heading": "Server management",
|
||||
@ -1335,6 +1337,30 @@
|
||||
"update": "Update"
|
||||
}
|
||||
},
|
||||
"zone": {
|
||||
"caption": "Zones",
|
||||
"description": "Manage the zones you want to track persons in.",
|
||||
"introduction": "Zones allow you to specify certain regions on earth. When a person is within a zone, the state will take the name from the zone. Zones can also be used as a trigger or condition inside automation setups.",
|
||||
"no_zones_created_yet": "Looks like you have not created any zones yet.",
|
||||
"create_zone": "Create Zone",
|
||||
"add_zone": "Add Zone",
|
||||
"confirm_delete": "Are you sure you want to delete this zone?",
|
||||
"detail": {
|
||||
"new_zone": "New Zone",
|
||||
"name": "Name",
|
||||
"icon": "Icon",
|
||||
"icon_error_msg": "Icon should be in the format prefix:iconname, for example: mdi:home",
|
||||
"radius": "Radius",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude",
|
||||
"passive": "Passive",
|
||||
"passive_note": "Passive zones are hidden in the frontend and are not used as location for device trackers. This is usefull if you just want to use it for automations.",
|
||||
"required_error_msg": "This field is required",
|
||||
"delete": "Delete",
|
||||
"create": "Create",
|
||||
"update": "Update"
|
||||
}
|
||||
},
|
||||
"integrations": {
|
||||
"caption": "Integrations",
|
||||
"description": "Manage and setup integrations",
|
||||
|
19
yarn.lock
19
yarn.lock
@ -2473,6 +2473,20 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/launchpad/-/launchpad-0.6.0.tgz#37296109b7f277f6e6c5fd7e0c0706bc918fbb51"
|
||||
integrity sha1-NylhCbfyd/bmxf1+DAcGvJGPu1E=
|
||||
|
||||
"@types/leaflet-draw@^1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/leaflet-draw/-/leaflet-draw-1.0.1.tgz#66e0c2c8b93b23487f836a8d65a769b98aa0bc5b"
|
||||
integrity sha512-/urwtXkpvv7rtre5A6plvXHSUDmFvDrwqpQRKseBCC2bIhIhBtMDf+plqQmi0vhvSk0Pqgk8qH1rtC8EVxPdmg==
|
||||
dependencies:
|
||||
"@types/leaflet" "*"
|
||||
|
||||
"@types/leaflet@*":
|
||||
version "1.5.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.5.8.tgz#1c550803672fc5866b8b2c38512009f2b5d4205d"
|
||||
integrity sha512-qpi5n4LmwenUFZ+VZ7ytRgHK+ZAclIvloL2zoKCmmj244WD2hBcLbUZ6Szvajfe3sIkSYEJ8WZ1p9VYl8tRsMA==
|
||||
dependencies:
|
||||
"@types/geojson" "*"
|
||||
|
||||
"@types/leaflet@^1.4.3":
|
||||
version "1.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.4.3.tgz#62638cb73770eeaed40222042afbcc7b495f0cc4"
|
||||
@ -8649,6 +8663,11 @@ lead@^1.0.0:
|
||||
dependencies:
|
||||
flush-write-stream "^1.0.2"
|
||||
|
||||
leaflet-draw@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/leaflet-draw/-/leaflet-draw-1.0.4.tgz#45be92f378ed253e7202fdeda1fcc71885198d46"
|
||||
integrity sha512-rsQ6saQO5ST5Aj6XRFylr5zvarWgzWnrg46zQ1MEOEIHsppdC/8hnN8qMoFvACsPvTioAuysya/TVtog15tyAQ==
|
||||
|
||||
leaflet@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.4.0.tgz#d5f56eeb2aa32787c24011e8be4c77e362ae171b"
|
||||
|
Loading…
x
Reference in New Issue
Block a user