mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-15 13:26:34 +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",
|
"intl-messageformat": "^2.2.0",
|
||||||
"js-yaml": "^3.13.1",
|
"js-yaml": "^3.13.1",
|
||||||
"leaflet": "^1.4.0",
|
"leaflet": "^1.4.0",
|
||||||
|
"leaflet-draw": "^1.0.4",
|
||||||
"lit-element": "^2.2.1",
|
"lit-element": "^2.2.1",
|
||||||
"lit-html": "^1.1.0",
|
"lit-html": "^1.1.0",
|
||||||
"lit-virtualizer": "^0.4.2",
|
"lit-virtualizer": "^0.4.2",
|
||||||
@ -123,6 +124,7 @@
|
|||||||
"@types/hls.js": "^0.12.3",
|
"@types/hls.js": "^0.12.3",
|
||||||
"@types/js-yaml": "^3.12.1",
|
"@types/js-yaml": "^3.12.1",
|
||||||
"@types/leaflet": "^1.4.3",
|
"@types/leaflet": "^1.4.3",
|
||||||
|
"@types/leaflet-draw": "^1.0.1",
|
||||||
"@types/memoize-one": "4.1.0",
|
"@types/memoize-one": "4.1.0",
|
||||||
"@types/mocha": "^5.2.6",
|
"@types/mocha": "^5.2.6",
|
||||||
"@types/webspeechapi": "^0.0.29",
|
"@types/webspeechapi": "^0.0.29",
|
||||||
|
@ -2,10 +2,12 @@ import { Map } from "leaflet";
|
|||||||
|
|
||||||
// Sets up a Leaflet map on the provided DOM element
|
// Sets up a Leaflet map on the provided DOM element
|
||||||
export type LeafletModuleType = typeof import("leaflet");
|
export type LeafletModuleType = typeof import("leaflet");
|
||||||
|
export type LeafletDrawModuleType = typeof import("leaflet-draw");
|
||||||
|
|
||||||
export const setupLeafletMap = async (
|
export const setupLeafletMap = async (
|
||||||
mapElement: HTMLElement,
|
mapElement: HTMLElement,
|
||||||
darkMode = false
|
darkMode = false,
|
||||||
|
draw = false
|
||||||
): Promise<[Map, LeafletModuleType]> => {
|
): Promise<[Map, LeafletModuleType]> => {
|
||||||
if (!mapElement.parentNode) {
|
if (!mapElement.parentNode) {
|
||||||
throw new Error("Cannot setup Leaflet map on disconnected element");
|
throw new Error("Cannot setup Leaflet map on disconnected element");
|
||||||
@ -16,6 +18,10 @@ export const setupLeafletMap = async (
|
|||||||
)) as LeafletModuleType;
|
)) as LeafletModuleType;
|
||||||
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
|
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
|
||||||
|
|
||||||
|
if (draw) {
|
||||||
|
await import(/* webpackChunkName: "leaflet-draw" */ "leaflet-draw");
|
||||||
|
}
|
||||||
|
|
||||||
const map = Leaflet.map(mapElement);
|
const map = Leaflet.map(mapElement);
|
||||||
const style = document.createElement("link");
|
const style = document.createElement("link");
|
||||||
style.setAttribute("href", "/static/images/leaflet/leaflet.css");
|
style.setAttribute("href", "/static/images/leaflet/leaflet.css");
|
||||||
|
@ -8,24 +8,35 @@ import {
|
|||||||
customElement,
|
customElement,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import { Marker, Map, LeafletMouseEvent, DragEndEvent, LatLng } from "leaflet";
|
import {
|
||||||
|
Marker,
|
||||||
|
Map,
|
||||||
|
LeafletMouseEvent,
|
||||||
|
DragEndEvent,
|
||||||
|
LatLng,
|
||||||
|
Circle,
|
||||||
|
DivIcon,
|
||||||
|
} from "leaflet";
|
||||||
import {
|
import {
|
||||||
setupLeafletMap,
|
setupLeafletMap,
|
||||||
LeafletModuleType,
|
LeafletModuleType,
|
||||||
} from "../../common/dom/setup-leaflet-map";
|
} from "../../common/dom/setup-leaflet-map";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import { nextRender } from "../../common/util/render-status";
|
||||||
|
|
||||||
@customElement("ha-location-editor")
|
@customElement("ha-location-editor")
|
||||||
class LocationEditor extends LitElement {
|
class LocationEditor extends LitElement {
|
||||||
@property() public location?: [number, number];
|
@property() public location?: [number, number];
|
||||||
|
@property() public radius?: number;
|
||||||
|
@property() public icon?: string;
|
||||||
public fitZoom = 16;
|
public fitZoom = 16;
|
||||||
|
private _iconEl?: DivIcon;
|
||||||
private _ignoreFitToMap?: [number, number];
|
private _ignoreFitToMap?: [number, number];
|
||||||
|
|
||||||
// tslint:disable-next-line
|
// tslint:disable-next-line
|
||||||
private Leaflet?: LeafletModuleType;
|
private Leaflet?: LeafletModuleType;
|
||||||
private _leafletMap?: Map;
|
private _leafletMap?: Map;
|
||||||
private _locationMarker?: Marker;
|
private _locationMarker?: Marker | Circle;
|
||||||
|
|
||||||
public fitMap(): void {
|
public fitMap(): void {
|
||||||
if (!this._leafletMap || !this.location) {
|
if (!this._leafletMap || !this.location) {
|
||||||
@ -53,30 +64,48 @@ class LocationEditor extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (changedProps.has("location")) {
|
||||||
this._updateMarker();
|
this._updateMarker();
|
||||||
if (!this._ignoreFitToMap || this._ignoreFitToMap !== this.location) {
|
if (
|
||||||
|
this.location &&
|
||||||
|
(!this._ignoreFitToMap ||
|
||||||
|
this._ignoreFitToMap[0] !== this.location[0] ||
|
||||||
|
this._ignoreFitToMap[1] !== this.location[1])
|
||||||
|
) {
|
||||||
this.fitMap();
|
this.fitMap();
|
||||||
}
|
}
|
||||||
this._ignoreFitToMap = undefined;
|
this._ignoreFitToMap = undefined;
|
||||||
}
|
}
|
||||||
|
if (changedProps.has("radius")) {
|
||||||
|
this._updateRadius();
|
||||||
|
}
|
||||||
|
if (changedProps.has("icon")) {
|
||||||
|
this._updateIcon();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private get _mapEl(): HTMLDivElement {
|
private get _mapEl(): HTMLDivElement {
|
||||||
return this.shadowRoot!.querySelector("div")!;
|
return this.shadowRoot!.querySelector("div")!;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _initMap(): Promise<void> {
|
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(
|
this._leafletMap.addEventListener(
|
||||||
"click",
|
"click",
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
(ev: LeafletMouseEvent) => this._updateLocation(ev.latlng)
|
(ev: LeafletMouseEvent) => this._locationUpdated(ev.latlng)
|
||||||
);
|
);
|
||||||
|
this._updateIcon();
|
||||||
this._updateMarker();
|
this._updateMarker();
|
||||||
this.fitMap();
|
this.fitMap();
|
||||||
this._leafletMap.invalidateSize();
|
this._leafletMap.invalidateSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _updateLocation(latlng: LatLng) {
|
private _locationUpdated(latlng: LatLng) {
|
||||||
let longitude = latlng.lng;
|
let longitude = latlng.lng;
|
||||||
if (Math.abs(longitude) > 180.0) {
|
if (Math.abs(longitude) > 180.0) {
|
||||||
// Normalize longitude if map provides values beyond -180 to +180 degrees.
|
// 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 });
|
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.location) {
|
||||||
if (this._locationMarker) {
|
if (this._locationMarker) {
|
||||||
this._locationMarker.remove();
|
this._locationMarker.remove();
|
||||||
@ -97,17 +187,41 @@ class LocationEditor extends LitElement {
|
|||||||
|
|
||||||
if (this._locationMarker) {
|
if (this._locationMarker) {
|
||||||
this._locationMarker.setLatLng(this.location);
|
this._locationMarker.setLatLng(this.location);
|
||||||
|
if (this.radius) {
|
||||||
|
// @ts-ignore
|
||||||
|
this._locationMarker.editing.disable();
|
||||||
|
await nextRender();
|
||||||
|
this._setupEdit();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.radius) {
|
||||||
this._locationMarker = this.Leaflet!.marker(this.location, {
|
this._locationMarker = this.Leaflet!.marker(this.location, {
|
||||||
draggable: true,
|
draggable: true,
|
||||||
});
|
});
|
||||||
|
this._setIcon();
|
||||||
this._locationMarker.addEventListener(
|
this._locationMarker.addEventListener(
|
||||||
"dragend",
|
"dragend",
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
(ev: DragEndEvent) => this._updateLocation(ev.target.getLatLng())
|
(ev: DragEndEvent) => this._locationUpdated(ev.target.getLatLng())
|
||||||
);
|
);
|
||||||
this._leafletMap!.addLayer(this._locationMarker);
|
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 {
|
static get styles(): CSSResult {
|
||||||
@ -119,6 +233,13 @@ class LocationEditor extends LitElement {
|
|||||||
#map {
|
#map {
|
||||||
height: 100%;
|
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=${[
|
.pages=${[
|
||||||
{ page: "integrations", core: true },
|
{ page: "integrations", core: true },
|
||||||
{ page: "devices", core: true },
|
{ page: "devices", core: true },
|
||||||
|
{ page: "entities", core: true },
|
||||||
{ page: "automation" },
|
{ page: "automation" },
|
||||||
{ page: "script" },
|
{ page: "script" },
|
||||||
{ page: "scene" },
|
{ page: "scene" },
|
||||||
@ -107,8 +108,8 @@ class HaConfigDashboard extends LitElement {
|
|||||||
.pages=${[
|
.pages=${[
|
||||||
{ page: "core", core: true },
|
{ page: "core", core: true },
|
||||||
{ page: "server_control", core: true },
|
{ page: "server_control", core: true },
|
||||||
{ page: "entities", core: true },
|
|
||||||
{ page: "areas", core: true },
|
{ page: "areas", core: true },
|
||||||
|
{ page: "zone" },
|
||||||
{ page: "person" },
|
{ page: "person" },
|
||||||
{ page: "users", core: true },
|
{ page: "users", core: true },
|
||||||
{ page: "zha" },
|
{ page: "zha" },
|
||||||
|
@ -125,6 +125,13 @@ class HaConfigRouter extends HassRouterPage {
|
|||||||
/* webpackChunkName: "panel-config-users" */ "./users/ha-config-users"
|
/* webpackChunkName: "panel-config-users" */ "./users/ha-config-users"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
zone: {
|
||||||
|
tag: "ha-config-zone",
|
||||||
|
load: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "panel-config-zone" */ "./zone/ha-config-zone"
|
||||||
|
),
|
||||||
|
},
|
||||||
zha: {
|
zha: {
|
||||||
tag: "zha-config-dashboard-router",
|
tag: "zha-config-dashboard-router",
|
||||||
load: () =>
|
load: () =>
|
||||||
|
@ -115,6 +115,7 @@ class HaPanelConfig extends LitElement {
|
|||||||
{ page: "scene" },
|
{ page: "scene" },
|
||||||
{ page: "core", core: true },
|
{ page: "core", core: true },
|
||||||
{ page: "areas", core: true },
|
{ page: "areas", core: true },
|
||||||
|
{ page: "zone" },
|
||||||
{ page: "person" },
|
{ page: "person" },
|
||||||
{ page: "users", core: true },
|
{ page: "users", core: true },
|
||||||
{ page: "server_control", core: true },
|
{ page: "server_control", core: true },
|
||||||
@ -163,7 +164,7 @@ class HaPanelConfig extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.side-bar {
|
.side-bar {
|
||||||
border-right: 1px solid #e0e0e0;
|
border-right: 1px solid var(--divider-color);
|
||||||
background: white;
|
background: white;
|
||||||
width: 320px;
|
width: 320px;
|
||||||
float: left;
|
float: left;
|
||||||
|
@ -148,6 +148,24 @@ class HaConfigSectionServerControl extends LocalizeMixin(PolymerElement) {
|
|||||||
</ha-call-service-button>
|
</ha-call-service-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
</ha-card>
|
||||||
</template>
|
</template>
|
||||||
<ha-card
|
<ha-card
|
||||||
@ -225,6 +243,10 @@ class HaConfigSectionServerControl extends LocalizeMixin(PolymerElement) {
|
|||||||
return isComponentLoaded(hass, "scene");
|
return isComponentLoaded(hass, "scene");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
personLoaded(hass) {
|
||||||
|
return isComponentLoaded(hass, "person");
|
||||||
|
}
|
||||||
|
|
||||||
validateConfig() {
|
validateConfig() {
|
||||||
this.validating = true;
|
this.validating = true;
|
||||||
this.validateLog = "";
|
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",
|
"group": "Reload groups",
|
||||||
"automation": "Reload automations",
|
"automation": "Reload automations",
|
||||||
"script": "Reload scripts",
|
"script": "Reload scripts",
|
||||||
"scene": "Reload scenes"
|
"scene": "Reload scenes",
|
||||||
|
"person": "Reload persons",
|
||||||
|
"zone": "Reload zones"
|
||||||
},
|
},
|
||||||
"server_management": {
|
"server_management": {
|
||||||
"heading": "Server management",
|
"heading": "Server management",
|
||||||
@ -1335,6 +1337,30 @@
|
|||||||
"update": "Update"
|
"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": {
|
"integrations": {
|
||||||
"caption": "Integrations",
|
"caption": "Integrations",
|
||||||
"description": "Manage and setup 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"
|
resolved "https://registry.yarnpkg.com/@types/launchpad/-/launchpad-0.6.0.tgz#37296109b7f277f6e6c5fd7e0c0706bc918fbb51"
|
||||||
integrity sha1-NylhCbfyd/bmxf1+DAcGvJGPu1E=
|
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":
|
"@types/leaflet@^1.4.3":
|
||||||
version "1.4.3"
|
version "1.4.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.4.3.tgz#62638cb73770eeaed40222042afbcc7b495f0cc4"
|
resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.4.3.tgz#62638cb73770eeaed40222042afbcc7b495f0cc4"
|
||||||
@ -8649,6 +8663,11 @@ lead@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flush-write-stream "^1.0.2"
|
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:
|
leaflet@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.4.0.tgz#d5f56eeb2aa32787c24011e8be4c77e362ae171b"
|
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.4.0.tgz#d5f56eeb2aa32787c24011e8be4c77e362ae171b"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user