Use nominatim from openstreetmap for location search in onboarding (#17287)
* Use nominatim from openstreetmap for location search in onboarding * Update text, add user agent * Handle errors better, add email address * remove detect text * Use `ui.common.search` * Update attribution location * Apply suggestions from code review Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> * Update src/translations/en.json Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> * Remove unused style * Increase line-height * Apply suggestions --------- Co-authored-by: Paul Bottein <paul.bottein@gmail.com> Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com>pull/17337/head
parent
bc3295d851
commit
d56273ec25
|
@ -182,6 +182,10 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
}
|
}
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import {
|
import type {
|
||||||
Circle,
|
Circle,
|
||||||
DivIcon,
|
DivIcon,
|
||||||
DragEndEvent,
|
DragEndEvent,
|
||||||
LatLng,
|
LatLng,
|
||||||
|
LatLngExpression,
|
||||||
Marker,
|
Marker,
|
||||||
MarkerOptions,
|
MarkerOptions,
|
||||||
} from "leaflet";
|
} from "leaflet";
|
||||||
|
@ -22,6 +23,8 @@ import type { HomeAssistant } from "../../types";
|
||||||
import "../ha-input-helper-text";
|
import "../ha-input-helper-text";
|
||||||
import "./ha-map";
|
import "./ha-map";
|
||||||
import type { HaMap } from "./ha-map";
|
import type { HaMap } from "./ha-map";
|
||||||
|
import { HaIcon } from "../ha-icon";
|
||||||
|
import { HaSvgIcon } from "../ha-svg-icon";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// for fire event
|
// for fire event
|
||||||
|
@ -40,6 +43,7 @@ export interface MarkerLocation {
|
||||||
name?: string;
|
name?: string;
|
||||||
id: string;
|
id: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
iconPath?: string;
|
||||||
radius_color?: string;
|
radius_color?: string;
|
||||||
location_editable?: boolean;
|
location_editable?: boolean;
|
||||||
radius_editable?: boolean;
|
radius_editable?: boolean;
|
||||||
|
@ -81,11 +85,21 @@ export class HaLocationsEditor extends LitElement {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public fitMap(): void {
|
public fitMap(options?: { zoom?: number; pad?: number }): void {
|
||||||
this.map.fitMap();
|
this.map.fitMap(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async fitMarker(id: string): Promise<void> {
|
public fitBounds(
|
||||||
|
boundingbox: LatLngExpression[],
|
||||||
|
options?: { zoom?: number; pad?: number }
|
||||||
|
) {
|
||||||
|
this.map.fitBounds(boundingbox, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async fitMarker(
|
||||||
|
id: string,
|
||||||
|
options?: { zoom?: number }
|
||||||
|
): Promise<void> {
|
||||||
if (!this.Leaflet) {
|
if (!this.Leaflet) {
|
||||||
await this._loadPromise;
|
await this._loadPromise;
|
||||||
}
|
}
|
||||||
|
@ -104,7 +118,10 @@ export class HaLocationsEditor extends LitElement {
|
||||||
if (circle) {
|
if (circle) {
|
||||||
this.map.leafletMap.fitBounds(circle.getBounds());
|
this.map.leafletMap.fitBounds(circle.getBounds());
|
||||||
} else {
|
} else {
|
||||||
this.map.leafletMap.setView(marker.getLatLng(), this.zoom);
|
this.map.leafletMap.setView(
|
||||||
|
marker.getLatLng(),
|
||||||
|
options?.zoom || this.zoom
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -199,15 +216,21 @@ export class HaLocationsEditor extends LitElement {
|
||||||
|
|
||||||
this.locations.forEach((location: MarkerLocation) => {
|
this.locations.forEach((location: MarkerLocation) => {
|
||||||
let icon: DivIcon | undefined;
|
let icon: DivIcon | undefined;
|
||||||
if (location.icon) {
|
if (location.icon || location.iconPath) {
|
||||||
// create icon
|
// create icon
|
||||||
const el = document.createElement("div");
|
const el = document.createElement("div");
|
||||||
el.className = "named-icon";
|
el.className = "named-icon";
|
||||||
if (location.name) {
|
if (location.name !== undefined) {
|
||||||
el.innerText = location.name;
|
el.innerText = location.name;
|
||||||
}
|
}
|
||||||
const iconEl = document.createElement("ha-icon");
|
let iconEl: HaIcon | HaSvgIcon;
|
||||||
iconEl.setAttribute("icon", location.icon);
|
if (location.icon) {
|
||||||
|
iconEl = document.createElement("ha-icon");
|
||||||
|
iconEl.setAttribute("icon", location.icon);
|
||||||
|
} else {
|
||||||
|
iconEl = document.createElement("ha-svg-icon");
|
||||||
|
iconEl.setAttribute("path", location.iconPath!);
|
||||||
|
}
|
||||||
el.prepend(iconEl);
|
el.prepend(iconEl);
|
||||||
|
|
||||||
icon = this.Leaflet!.divIcon({
|
icon = this.Leaflet!.divIcon({
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import {
|
import type {
|
||||||
Circle,
|
Circle,
|
||||||
CircleMarker,
|
CircleMarker,
|
||||||
LatLngTuple,
|
LatLngTuple,
|
||||||
|
LatLngExpression,
|
||||||
Layer,
|
Layer,
|
||||||
Map,
|
Map,
|
||||||
Marker,
|
Marker,
|
||||||
|
@ -162,7 +163,7 @@ export class HaMap extends ReactiveElement {
|
||||||
this._loaded = true;
|
this._loaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public fitMap(): void {
|
public fitMap(options?: { zoom?: number; pad?: number }): void {
|
||||||
if (!this.leafletMap || !this.Leaflet || !this.hass) {
|
if (!this.leafletMap || !this.Leaflet || !this.hass) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -173,7 +174,7 @@ export class HaMap extends ReactiveElement {
|
||||||
this.hass.config.latitude,
|
this.hass.config.latitude,
|
||||||
this.hass.config.longitude
|
this.hass.config.longitude
|
||||||
),
|
),
|
||||||
this.zoom
|
options?.zoom || this.zoom
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -196,11 +197,22 @@ export class HaMap extends ReactiveElement {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!this.layers) {
|
bounds = bounds.pad(options?.pad ?? 0.5);
|
||||||
bounds = bounds.pad(0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.leafletMap.fitBounds(bounds, { maxZoom: this.zoom });
|
this.leafletMap.fitBounds(bounds, { maxZoom: options?.zoom || this.zoom });
|
||||||
|
}
|
||||||
|
|
||||||
|
public fitBounds(
|
||||||
|
boundingbox: LatLngExpression[],
|
||||||
|
options?: { zoom?: number; pad?: number }
|
||||||
|
) {
|
||||||
|
if (!this.leafletMap || !this.Leaflet || !this.hass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bounds = this.Leaflet.latLngBounds(boundingbox).pad(
|
||||||
|
options?.pad ?? 0.5
|
||||||
|
);
|
||||||
|
this.leafletMap.fitBounds(bounds, { maxZoom: options?.zoom || this.zoom });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _drawLayers(prevLayers: Layer[] | undefined): void {
|
private _drawLayers(prevLayers: Layer[] | undefined): void {
|
||||||
|
|
|
@ -0,0 +1,254 @@
|
||||||
|
// From http://country.io/currency.json
|
||||||
|
|
||||||
|
export const countryCurrency = {
|
||||||
|
BD: "BDT",
|
||||||
|
BE: "EUR",
|
||||||
|
BF: "XOF",
|
||||||
|
BG: "BGN",
|
||||||
|
BA: "BAM",
|
||||||
|
BB: "BBD",
|
||||||
|
WF: "XPF",
|
||||||
|
BL: "EUR",
|
||||||
|
BM: "BMD",
|
||||||
|
BN: "BND",
|
||||||
|
BO: "BOB",
|
||||||
|
BH: "BHD",
|
||||||
|
BI: "BIF",
|
||||||
|
BJ: "XOF",
|
||||||
|
BT: "BTN",
|
||||||
|
JM: "JMD",
|
||||||
|
BV: "NOK",
|
||||||
|
BW: "BWP",
|
||||||
|
WS: "WST",
|
||||||
|
BQ: "USD",
|
||||||
|
BR: "BRL",
|
||||||
|
BS: "BSD",
|
||||||
|
JE: "GBP",
|
||||||
|
BY: "BYN",
|
||||||
|
BZ: "BZD",
|
||||||
|
RU: "RUB",
|
||||||
|
RW: "RWF",
|
||||||
|
RS: "RSD",
|
||||||
|
TL: "USD",
|
||||||
|
RE: "EUR",
|
||||||
|
TM: "TMT",
|
||||||
|
TJ: "TJS",
|
||||||
|
RO: "RON",
|
||||||
|
TK: "NZD",
|
||||||
|
GW: "XOF",
|
||||||
|
GU: "USD",
|
||||||
|
GT: "GTQ",
|
||||||
|
GS: "GBP",
|
||||||
|
GR: "EUR",
|
||||||
|
GQ: "XAF",
|
||||||
|
GP: "EUR",
|
||||||
|
JP: "JPY",
|
||||||
|
GY: "GYD",
|
||||||
|
GG: "GBP",
|
||||||
|
GF: "EUR",
|
||||||
|
GE: "GEL",
|
||||||
|
GD: "XCD",
|
||||||
|
GB: "GBP",
|
||||||
|
GA: "XAF",
|
||||||
|
SV: "USD",
|
||||||
|
GN: "GNF",
|
||||||
|
GM: "GMD",
|
||||||
|
GL: "DKK",
|
||||||
|
GI: "GIP",
|
||||||
|
GH: "GHS",
|
||||||
|
OM: "OMR",
|
||||||
|
TN: "TND",
|
||||||
|
JO: "JOD",
|
||||||
|
HR: "EUR",
|
||||||
|
HT: "HTG",
|
||||||
|
HU: "HUF",
|
||||||
|
HK: "HKD",
|
||||||
|
HN: "HNL",
|
||||||
|
HM: "AUD",
|
||||||
|
VE: "VEF",
|
||||||
|
PR: "USD",
|
||||||
|
PS: "ILS",
|
||||||
|
PW: "USD",
|
||||||
|
PT: "EUR",
|
||||||
|
SJ: "NOK",
|
||||||
|
PY: "PYG",
|
||||||
|
IQ: "IQD",
|
||||||
|
PA: "PAB",
|
||||||
|
PF: "XPF",
|
||||||
|
PG: "PGK",
|
||||||
|
PE: "PEN",
|
||||||
|
PK: "PKR",
|
||||||
|
PH: "PHP",
|
||||||
|
PN: "NZD",
|
||||||
|
PL: "PLN",
|
||||||
|
PM: "EUR",
|
||||||
|
ZM: "ZMK",
|
||||||
|
EH: "MAD",
|
||||||
|
EE: "EUR",
|
||||||
|
EG: "EGP",
|
||||||
|
ZA: "ZAR",
|
||||||
|
EC: "USD",
|
||||||
|
IT: "EUR",
|
||||||
|
VN: "VND",
|
||||||
|
SB: "SBD",
|
||||||
|
ET: "ETB",
|
||||||
|
SO: "SOS",
|
||||||
|
ZW: "ZWL",
|
||||||
|
SA: "SAR",
|
||||||
|
ES: "EUR",
|
||||||
|
ER: "ERN",
|
||||||
|
ME: "EUR",
|
||||||
|
MD: "MDL",
|
||||||
|
MG: "MGA",
|
||||||
|
MF: "EUR",
|
||||||
|
MA: "MAD",
|
||||||
|
MC: "EUR",
|
||||||
|
UZ: "UZS",
|
||||||
|
MM: "MMK",
|
||||||
|
ML: "XOF",
|
||||||
|
MO: "MOP",
|
||||||
|
MN: "MNT",
|
||||||
|
MH: "USD",
|
||||||
|
MK: "MKD",
|
||||||
|
MU: "MUR",
|
||||||
|
MT: "EUR",
|
||||||
|
MW: "MWK",
|
||||||
|
MV: "MVR",
|
||||||
|
MQ: "EUR",
|
||||||
|
MP: "USD",
|
||||||
|
MS: "XCD",
|
||||||
|
MR: "MRO",
|
||||||
|
IM: "GBP",
|
||||||
|
UG: "UGX",
|
||||||
|
TZ: "TZS",
|
||||||
|
MY: "MYR",
|
||||||
|
MX: "MXN",
|
||||||
|
IL: "ILS",
|
||||||
|
FR: "EUR",
|
||||||
|
IO: "USD",
|
||||||
|
SH: "SHP",
|
||||||
|
FI: "EUR",
|
||||||
|
FJ: "FJD",
|
||||||
|
FK: "FKP",
|
||||||
|
FM: "USD",
|
||||||
|
FO: "DKK",
|
||||||
|
NI: "NIO",
|
||||||
|
NL: "EUR",
|
||||||
|
NO: "NOK",
|
||||||
|
NA: "NAD",
|
||||||
|
VU: "VUV",
|
||||||
|
NC: "XPF",
|
||||||
|
NE: "XOF",
|
||||||
|
NF: "AUD",
|
||||||
|
NG: "NGN",
|
||||||
|
NZ: "NZD",
|
||||||
|
NP: "NPR",
|
||||||
|
NR: "AUD",
|
||||||
|
NU: "NZD",
|
||||||
|
CK: "NZD",
|
||||||
|
XK: "EUR",
|
||||||
|
CI: "XOF",
|
||||||
|
CH: "CHF",
|
||||||
|
CO: "COP",
|
||||||
|
CN: "CNY",
|
||||||
|
CM: "XAF",
|
||||||
|
CL: "CLP",
|
||||||
|
CC: "AUD",
|
||||||
|
CA: "CAD",
|
||||||
|
CG: "XAF",
|
||||||
|
CF: "XAF",
|
||||||
|
CD: "CDF",
|
||||||
|
CZ: "CZK",
|
||||||
|
CY: "EUR",
|
||||||
|
CX: "AUD",
|
||||||
|
CR: "CRC",
|
||||||
|
CW: "ANG",
|
||||||
|
CV: "CVE",
|
||||||
|
CU: "CUP",
|
||||||
|
SZ: "SZL",
|
||||||
|
SY: "SYP",
|
||||||
|
SX: "ANG",
|
||||||
|
KG: "KGS",
|
||||||
|
KE: "KES",
|
||||||
|
SS: "SSP",
|
||||||
|
SR: "SRD",
|
||||||
|
KI: "AUD",
|
||||||
|
KH: "KHR",
|
||||||
|
KN: "XCD",
|
||||||
|
KM: "KMF",
|
||||||
|
ST: "STD",
|
||||||
|
SK: "EUR",
|
||||||
|
KR: "KRW",
|
||||||
|
SI: "EUR",
|
||||||
|
KP: "KPW",
|
||||||
|
KW: "KWD",
|
||||||
|
SN: "XOF",
|
||||||
|
SM: "EUR",
|
||||||
|
SL: "SLL",
|
||||||
|
SC: "SCR",
|
||||||
|
KZ: "KZT",
|
||||||
|
KY: "KYD",
|
||||||
|
SG: "SGD",
|
||||||
|
SE: "SEK",
|
||||||
|
SD: "SDG",
|
||||||
|
DO: "DOP",
|
||||||
|
DM: "XCD",
|
||||||
|
DJ: "DJF",
|
||||||
|
DK: "DKK",
|
||||||
|
VG: "USD",
|
||||||
|
DE: "EUR",
|
||||||
|
YE: "YER",
|
||||||
|
DZ: "DZD",
|
||||||
|
US: "USD",
|
||||||
|
UY: "UYU",
|
||||||
|
YT: "EUR",
|
||||||
|
UM: "USD",
|
||||||
|
LB: "LBP",
|
||||||
|
LC: "XCD",
|
||||||
|
LA: "LAK",
|
||||||
|
TV: "AUD",
|
||||||
|
TW: "TWD",
|
||||||
|
TT: "TTD",
|
||||||
|
TR: "TRY",
|
||||||
|
LK: "LKR",
|
||||||
|
LI: "CHF",
|
||||||
|
LV: "EUR",
|
||||||
|
TO: "TOP",
|
||||||
|
LT: "EUR",
|
||||||
|
LU: "EUR",
|
||||||
|
LR: "LRD",
|
||||||
|
LS: "LSL",
|
||||||
|
TH: "THB",
|
||||||
|
TF: "EUR",
|
||||||
|
TG: "XOF",
|
||||||
|
TD: "XAF",
|
||||||
|
TC: "USD",
|
||||||
|
LY: "LYD",
|
||||||
|
VA: "EUR",
|
||||||
|
VC: "XCD",
|
||||||
|
AE: "AED",
|
||||||
|
AD: "EUR",
|
||||||
|
AG: "XCD",
|
||||||
|
AF: "AFN",
|
||||||
|
AI: "XCD",
|
||||||
|
VI: "USD",
|
||||||
|
IS: "ISK",
|
||||||
|
IR: "IRR",
|
||||||
|
AM: "AMD",
|
||||||
|
AL: "ALL",
|
||||||
|
AO: "AOA",
|
||||||
|
AQ: "",
|
||||||
|
AS: "USD",
|
||||||
|
AR: "ARS",
|
||||||
|
AU: "AUD",
|
||||||
|
AT: "EUR",
|
||||||
|
AW: "AWG",
|
||||||
|
IN: "INR",
|
||||||
|
AX: "EUR",
|
||||||
|
AZ: "AZN",
|
||||||
|
IE: "EUR",
|
||||||
|
ID: "IDR",
|
||||||
|
UA: "UAH",
|
||||||
|
QA: "QAR",
|
||||||
|
MZ: "MZN",
|
||||||
|
};
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
|
export interface OpenStreetMapPlace {
|
||||||
|
place_id: number;
|
||||||
|
licence: string;
|
||||||
|
osm_type: string;
|
||||||
|
osm_id: number;
|
||||||
|
lat: string;
|
||||||
|
lon: string;
|
||||||
|
place_rank: number;
|
||||||
|
category: string;
|
||||||
|
type: string;
|
||||||
|
importance: number;
|
||||||
|
addresstype: string;
|
||||||
|
name: string | null;
|
||||||
|
display_name: string;
|
||||||
|
address: {
|
||||||
|
house_number?: string;
|
||||||
|
road?: string;
|
||||||
|
neighbourhood?: string;
|
||||||
|
city?: string;
|
||||||
|
municipality?: string;
|
||||||
|
state?: string;
|
||||||
|
country?: string;
|
||||||
|
postcode?: string;
|
||||||
|
country_code: string;
|
||||||
|
[key: string]: string | undefined;
|
||||||
|
};
|
||||||
|
boundingbox: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const searchPlaces = (
|
||||||
|
address: string,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
addressdetails?: boolean,
|
||||||
|
limit?: number
|
||||||
|
): Promise<OpenStreetMapPlace[]> =>
|
||||||
|
fetch(
|
||||||
|
`https://nominatim.openstreetmap.org/search.php?q=${address}&format=jsonv2${
|
||||||
|
limit ? `&limit=${limit}` : ""
|
||||||
|
}${addressdetails ? "&addressdetails=1" : ""}&accept-language=${
|
||||||
|
hass.locale.language
|
||||||
|
}&email=abuse@home-assistant.io`,
|
||||||
|
{ headers: { "User-Agent": `HomeAssistant/${hass.config.version}` } }
|
||||||
|
).then((res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
throw new Error(res.statusText);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const reverseGeocode = (
|
||||||
|
location: [number, number],
|
||||||
|
hass: HomeAssistant,
|
||||||
|
zoom?: number
|
||||||
|
): Promise<OpenStreetMapPlace> =>
|
||||||
|
fetch(
|
||||||
|
`https://nominatim.openstreetmap.org/reverse.php?lat=${location[0]}&lon=${
|
||||||
|
location[1]
|
||||||
|
}&accept-language=${hass.locale.language}&zoom=${
|
||||||
|
zoom ?? 18
|
||||||
|
}&format=jsonv2&email=abuse@home-assistant.io`,
|
||||||
|
{ headers: { "User-Agent": `HomeAssistant/${hass.config.version}` } }
|
||||||
|
).then((res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
throw new Error(res.statusText);
|
||||||
|
});
|
|
@ -11,13 +11,14 @@
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
height: auto;
|
height: auto;
|
||||||
|
padding: 64px 0;
|
||||||
}
|
}
|
||||||
.content {
|
.content {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 20px 16px;
|
padding: 20px 16px;
|
||||||
border-radius: var(--ha-card-border-radius, 12px);
|
border-radius: var(--ha-card-border-radius, 12px);
|
||||||
max-width: 432px;
|
max-width: 432px;
|
||||||
margin: 64px auto 0;
|
margin: 0 auto;
|
||||||
box-shadow: var(
|
box-shadow: var(
|
||||||
--ha-card-box-shadow,
|
--ha-card-box-shadow,
|
||||||
rgba(0, 0, 0, 0.25) 0px 54px 55px,
|
rgba(0, 0, 0, 0.25) 0px 54px 55px,
|
||||||
|
|
|
@ -82,10 +82,13 @@ class OnboardingAnalytics extends LitElement {
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return css`
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
.error {
|
.error {
|
||||||
color: var(--error-color);
|
color: var(--error-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -93,7 +96,6 @@ class OnboardingAnalytics extends LitElement {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,10 @@ import {
|
||||||
html,
|
html,
|
||||||
LitElement,
|
LitElement,
|
||||||
nothing,
|
nothing,
|
||||||
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import type { LocalizeFunc } from "../common/translations/localize";
|
import type { LocalizeFunc } from "../common/translations/localize";
|
||||||
import "../components/ha-alert";
|
import "../components/ha-alert";
|
||||||
|
@ -22,22 +22,13 @@ import "../components/ha-textfield";
|
||||||
import type { HaTextField } from "../components/ha-textfield";
|
import type { HaTextField } from "../components/ha-textfield";
|
||||||
import "../components/ha-timezone-picker";
|
import "../components/ha-timezone-picker";
|
||||||
import "../components/map/ha-locations-editor";
|
import "../components/map/ha-locations-editor";
|
||||||
import type {
|
import { ConfigUpdateValues, saveCoreConfig } from "../data/core";
|
||||||
HaLocationsEditor,
|
import { countryCurrency } from "../data/currency";
|
||||||
MarkerLocation,
|
|
||||||
} from "../components/map/ha-locations-editor";
|
|
||||||
import {
|
|
||||||
ConfigUpdateValues,
|
|
||||||
detectCoreConfig,
|
|
||||||
saveCoreConfig,
|
|
||||||
} from "../data/core";
|
|
||||||
import { onboardCoreConfigStep } from "../data/onboarding";
|
import { onboardCoreConfigStep } from "../data/onboarding";
|
||||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
import { getLocalLanguage } from "../util/common-translation";
|
import { getLocalLanguage } from "../util/common-translation";
|
||||||
|
import "./onboarding-location";
|
||||||
const amsterdam: [number, number] = [52.3731339, 4.8903147];
|
import "./onboarding-name";
|
||||||
const mql = matchMedia("(prefers-color-scheme: dark)");
|
|
||||||
const locationMarkerId = "location";
|
|
||||||
|
|
||||||
@customElement("onboarding-core-config")
|
@customElement("onboarding-core-config")
|
||||||
class OnboardingCoreConfig extends LitElement {
|
class OnboardingCoreConfig extends LitElement {
|
||||||
|
@ -57,19 +48,29 @@ class OnboardingCoreConfig extends LitElement {
|
||||||
|
|
||||||
@state() private _currency?: ConfigUpdateValues["currency"];
|
@state() private _currency?: ConfigUpdateValues["currency"];
|
||||||
|
|
||||||
@state() private _timeZone? =
|
@state() private _timeZone?: ConfigUpdateValues["time_zone"];
|
||||||
Intl.DateTimeFormat?.().resolvedOptions?.().timeZone;
|
|
||||||
|
|
||||||
@state() private _language: ConfigUpdateValues["language"] =
|
@state() private _language: ConfigUpdateValues["language"];
|
||||||
getLocalLanguage();
|
|
||||||
|
|
||||||
@state() private _country?: ConfigUpdateValues["country"];
|
@state() private _country?: ConfigUpdateValues["country"];
|
||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
@query("ha-locations-editor", true) private map!: HaLocationsEditor;
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
|
if (!this._name) {
|
||||||
|
return html`<onboarding-name
|
||||||
|
.hass=${this.hass}
|
||||||
|
.onboardingLocalize=${this.onboardingLocalize}
|
||||||
|
@value-changed=${this._nameChanged}
|
||||||
|
></onboarding-name>`;
|
||||||
|
}
|
||||||
|
if (!this._location) {
|
||||||
|
return html`<onboarding-location
|
||||||
|
.hass=${this.hass}
|
||||||
|
.onboardingLocalize=${this.onboardingLocalize}
|
||||||
|
@value-changed=${this._locationChanged}
|
||||||
|
></onboarding-location>`;
|
||||||
|
}
|
||||||
return html`
|
return html`
|
||||||
${
|
${
|
||||||
this._error
|
this._error
|
||||||
|
@ -78,55 +79,11 @@ class OnboardingCoreConfig extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
${this.onboardingLocalize(
|
${this.onboardingLocalize(
|
||||||
"ui.panel.page-onboarding.core-config.intro",
|
"ui.panel.page-onboarding.core-config.intro_core_config"
|
||||||
"name",
|
)}
|
||||||
this.hass.user!.name
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ha-textfield
|
|
||||||
.label=${this.onboardingLocalize(
|
|
||||||
"ui.panel.page-onboarding.core-config.location_name"
|
|
||||||
)}
|
|
||||||
name="name"
|
|
||||||
.disabled=${this._working}
|
|
||||||
.value=${this._nameValue}
|
|
||||||
@change=${this._handleChange}
|
|
||||||
></ha-textfield>
|
|
||||||
|
|
||||||
<div class="middle-text">
|
|
||||||
<p>
|
|
||||||
${this.onboardingLocalize(
|
|
||||||
"ui.panel.page-onboarding.core-config.intro_location"
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div>
|
|
||||||
${this.onboardingLocalize(
|
|
||||||
"ui.panel.page-onboarding.core-config.intro_location_detect"
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<mwc-button @click=${this._detect}>
|
|
||||||
${this.onboardingLocalize(
|
|
||||||
"ui.panel.page-onboarding.core-config.button_detect"
|
|
||||||
)}
|
|
||||||
</mwc-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<ha-locations-editor
|
|
||||||
class="flex"
|
|
||||||
.hass=${this.hass}
|
|
||||||
.locations=${this._markerLocation(this._locationValue)}
|
|
||||||
zoom="14"
|
|
||||||
.darkMode=${mql.matches}
|
|
||||||
@location-updated=${this._locationChanged}
|
|
||||||
></ha-locations-editor>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<ha-country-picker
|
<ha-country-picker
|
||||||
class="flex"
|
class="flex"
|
||||||
|
@ -275,31 +232,29 @@ class OnboardingCoreConfig extends LitElement {
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated(changedProps) {
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
|
if (!changedProps.has("_country") || !this._country) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this._currency) {
|
||||||
|
this._currency = countryCurrency[this._country];
|
||||||
|
}
|
||||||
|
if (!this._unitSystem) {
|
||||||
|
this._unitSystem = ["US", "MM", "LR"].includes(this._country)
|
||||||
|
? "us_customary"
|
||||||
|
: "metric";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(changedProps: PropertyValues) {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
setTimeout(
|
this.addEventListener("keyup", (ev) => {
|
||||||
() => this.renderRoot.querySelector("ha-textfield")!.focus(),
|
if (this._location && ev.key === "Enter") {
|
||||||
100
|
|
||||||
);
|
|
||||||
this.addEventListener("keypress", (ev) => {
|
|
||||||
if (ev.key === "Enter") {
|
|
||||||
this._save(ev);
|
this._save(ev);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _nameValue() {
|
|
||||||
return this._name !== undefined
|
|
||||||
? this._name
|
|
||||||
: this.onboardingLocalize(
|
|
||||||
"ui.panel.page-onboarding.core-config.location_name_default"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private get _locationValue() {
|
|
||||||
return this._location || amsterdam;
|
|
||||||
}
|
|
||||||
|
|
||||||
private get _elevationValue() {
|
private get _elevationValue() {
|
||||||
return this._elevation !== undefined ? this._elevation : 0;
|
return this._elevation !== undefined ? this._elevation : 0;
|
||||||
}
|
}
|
||||||
|
@ -324,17 +279,6 @@ class OnboardingCoreConfig extends LitElement {
|
||||||
return this._currency !== undefined ? this._currency : "";
|
return this._currency !== undefined ? this._currency : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private _markerLocation = memoizeOne(
|
|
||||||
(location: [number, number]): MarkerLocation[] => [
|
|
||||||
{
|
|
||||||
id: locationMarkerId,
|
|
||||||
latitude: location[0],
|
|
||||||
longitude: location[1],
|
|
||||||
location_editable: true,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
private _handleValueChanged(ev: ValueChangedEvent<string>) {
|
private _handleValueChanged(ev: ValueChangedEvent<string>) {
|
||||||
const target = ev.currentTarget as HTMLElement;
|
const target = ev.currentTarget as HTMLElement;
|
||||||
this[`_${target.getAttribute("name")}`] = ev.detail.value;
|
this[`_${target.getAttribute("name")}`] = ev.detail.value;
|
||||||
|
@ -345,8 +289,25 @@ class OnboardingCoreConfig extends LitElement {
|
||||||
this[`_${target.name}`] = target.value;
|
this[`_${target.name}`] = target.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _locationChanged(ev) {
|
private _nameChanged(ev: CustomEvent) {
|
||||||
this._location = ev.detail.location;
|
this._name = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _locationChanged(ev) {
|
||||||
|
this._location = ev.detail.value.location;
|
||||||
|
this._country = ev.detail.value.country;
|
||||||
|
this._elevation = ev.detail.value.elevation;
|
||||||
|
this._currency = ev.detail.value.currency;
|
||||||
|
this._language = ev.detail.value.language || getLocalLanguage();
|
||||||
|
this._timeZone =
|
||||||
|
ev.detail.value.timezone ||
|
||||||
|
Intl.DateTimeFormat?.().resolvedOptions?.().timeZone;
|
||||||
|
this._unitSystem = ev.detail.value.unit_system;
|
||||||
|
await this.updateComplete;
|
||||||
|
setTimeout(
|
||||||
|
() => this.renderRoot.querySelector("ha-textfield")!.focus(),
|
||||||
|
100
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _unitSystemChanged(ev: CustomEvent) {
|
private _unitSystemChanged(ev: CustomEvent) {
|
||||||
|
@ -355,55 +316,17 @@ class OnboardingCoreConfig extends LitElement {
|
||||||
| "us_customary";
|
| "us_customary";
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _detect() {
|
|
||||||
this._working = true;
|
|
||||||
try {
|
|
||||||
const values = await detectCoreConfig(this.hass);
|
|
||||||
|
|
||||||
if (values.latitude && values.longitude) {
|
|
||||||
this.map.addEventListener(
|
|
||||||
"markers-updated",
|
|
||||||
() => {
|
|
||||||
this.map.fitMarker(locationMarkerId);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
once: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
this._location = [Number(values.latitude), Number(values.longitude)];
|
|
||||||
}
|
|
||||||
if (values.elevation) {
|
|
||||||
this._elevation = String(values.elevation);
|
|
||||||
}
|
|
||||||
if (values.unit_system) {
|
|
||||||
this._unitSystem = values.unit_system;
|
|
||||||
}
|
|
||||||
if (values.time_zone) {
|
|
||||||
this._timeZone = values.time_zone;
|
|
||||||
}
|
|
||||||
if (values.currency) {
|
|
||||||
this._currency = values.currency;
|
|
||||||
}
|
|
||||||
if (values.country) {
|
|
||||||
this._country = values.country;
|
|
||||||
}
|
|
||||||
this._language = getLocalLanguage();
|
|
||||||
} catch (err: any) {
|
|
||||||
this._error = `Failed to detect location information: ${err.message}`;
|
|
||||||
} finally {
|
|
||||||
this._working = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _save(ev) {
|
private async _save(ev) {
|
||||||
|
if (!this._location) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this._working = true;
|
this._working = true;
|
||||||
try {
|
try {
|
||||||
const location = this._locationValue;
|
|
||||||
await saveCoreConfig(this.hass, {
|
await saveCoreConfig(this.hass, {
|
||||||
location_name: this._nameValue,
|
location_name: this._name,
|
||||||
latitude: location[0],
|
latitude: this._location[0],
|
||||||
longitude: location[1],
|
longitude: this._location[1],
|
||||||
elevation: Number(this._elevationValue),
|
elevation: Number(this._elevationValue),
|
||||||
unit_system: this._unitSystemValue,
|
unit_system: this._unitSystemValue,
|
||||||
time_zone: this._timeZoneValue || "UTC",
|
time_zone: this._timeZoneValue || "UTC",
|
||||||
|
@ -436,12 +359,13 @@ class OnboardingCoreConfig extends LitElement {
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-textfield {
|
p {
|
||||||
display: block;
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-locations-editor {
|
ha-textfield {
|
||||||
height: 200px;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex {
|
.flex {
|
||||||
|
|
|
@ -211,6 +211,10 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return css`
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
.badges {
|
.badges {
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -0,0 +1,542 @@
|
||||||
|
import "@material/mwc-button/mwc-button";
|
||||||
|
import { mdiCrosshairsGps, mdiMapMarker, mdiMapSearchOutline } from "@mdi/js";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResultGroup,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
nothing,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import type { LocalizeFunc } from "../common/translations/localize";
|
||||||
|
import "../components/ha-alert";
|
||||||
|
import "../components/ha-formfield";
|
||||||
|
import "../components/ha-radio";
|
||||||
|
import "../components/ha-textfield";
|
||||||
|
import type { HaTextField } from "../components/ha-textfield";
|
||||||
|
import "../components/map/ha-locations-editor";
|
||||||
|
import type {
|
||||||
|
HaLocationsEditor,
|
||||||
|
MarkerLocation,
|
||||||
|
} from "../components/map/ha-locations-editor";
|
||||||
|
import { ConfigUpdateValues, detectCoreConfig } from "../data/core";
|
||||||
|
import { showConfirmationDialog } from "../dialogs/generic/show-dialog-box";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import {
|
||||||
|
OpenStreetMapPlace,
|
||||||
|
reverseGeocode,
|
||||||
|
searchPlaces,
|
||||||
|
} from "../data/openstreetmap";
|
||||||
|
|
||||||
|
const AMSTERDAM: [number, number] = [52.3731339, 4.8903147];
|
||||||
|
const mql = matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
const LOCATION_MARKER_ID = "location";
|
||||||
|
|
||||||
|
@customElement("onboarding-location")
|
||||||
|
class OnboardingLocation extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public onboardingLocalize!: LocalizeFunc;
|
||||||
|
|
||||||
|
@state() private _working = false;
|
||||||
|
|
||||||
|
@state() private _location?: [number, number];
|
||||||
|
|
||||||
|
@state() private _places?: OpenStreetMapPlace[] | null;
|
||||||
|
|
||||||
|
@state() private _error?: string;
|
||||||
|
|
||||||
|
@state() private _search = false;
|
||||||
|
|
||||||
|
@state() private _highlightedMarker?: number;
|
||||||
|
|
||||||
|
private _elevation?: string;
|
||||||
|
|
||||||
|
private _unitSystem?: ConfigUpdateValues["unit_system"];
|
||||||
|
|
||||||
|
private _currency?: ConfigUpdateValues["currency"];
|
||||||
|
|
||||||
|
private _timeZone?: ConfigUpdateValues["time_zone"];
|
||||||
|
|
||||||
|
private _country?: ConfigUpdateValues["country"];
|
||||||
|
|
||||||
|
@query("ha-locations-editor", true) private map!: HaLocationsEditor;
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
const addressAttribution = this.onboardingLocalize(
|
||||||
|
"ui.panel.page-onboarding.core-config.location_address",
|
||||||
|
{
|
||||||
|
openstreetmap: html`<a
|
||||||
|
href="https://www.openstreetmap.org/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>OpenStreetMap</a
|
||||||
|
>`,
|
||||||
|
osm_privacy_policy: html`<a
|
||||||
|
href="https://wiki.osmfoundation.org/wiki/Privacy_Policy"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>${this.onboardingLocalize(
|
||||||
|
"ui.panel.page-onboarding.core-config.osm_privacy_policy"
|
||||||
|
)}</a
|
||||||
|
>`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
${this._error
|
||||||
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
|
: nothing}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
${this.onboardingLocalize(
|
||||||
|
"ui.panel.page-onboarding.core-config.intro_location"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ha-textfield
|
||||||
|
label=${this.onboardingLocalize(
|
||||||
|
"ui.panel.page-onboarding.core-config.address_label"
|
||||||
|
)}
|
||||||
|
.disabled=${this._working}
|
||||||
|
iconTrailing
|
||||||
|
@keyup=${this._addressSearch}
|
||||||
|
>
|
||||||
|
${this._working
|
||||||
|
? html`
|
||||||
|
<ha-circular-progress
|
||||||
|
slot="trailingIcon"
|
||||||
|
active
|
||||||
|
size="small"
|
||||||
|
></ha-circular-progress>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<ha-icon-button
|
||||||
|
@click=${this._handleButtonClick}
|
||||||
|
slot="trailingIcon"
|
||||||
|
.disabled=${this._working}
|
||||||
|
.label=${this.onboardingLocalize(
|
||||||
|
this._search
|
||||||
|
? "ui.common.search"
|
||||||
|
: "ui.panel.page-onboarding.core-config.button_detect"
|
||||||
|
)}
|
||||||
|
.path=${this._search ? mdiMapSearchOutline : mdiCrosshairsGps}
|
||||||
|
></ha-icon-button>
|
||||||
|
`}
|
||||||
|
</ha-textfield>
|
||||||
|
${this._places !== undefined
|
||||||
|
? html`
|
||||||
|
<mwc-list activatable>
|
||||||
|
${this._places?.length
|
||||||
|
? this._places.map((place) => {
|
||||||
|
const primary = [
|
||||||
|
place.name || place.address[place.category],
|
||||||
|
place.address.house_number,
|
||||||
|
place.address.road || place.address.waterway,
|
||||||
|
place.address.village || place.address.town,
|
||||||
|
place.address.suburb || place.address.subdivision,
|
||||||
|
place.address.city || place.address.municipality,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(", ");
|
||||||
|
const secondary = [
|
||||||
|
place.address.county ||
|
||||||
|
place.address.state_district ||
|
||||||
|
place.address.region,
|
||||||
|
place.address.state,
|
||||||
|
place.address.country,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(", ");
|
||||||
|
return html`<ha-list-item
|
||||||
|
@click=${this._itemClicked}
|
||||||
|
.placeId=${place.place_id}
|
||||||
|
.selected=${this._highlightedMarker === place.place_id}
|
||||||
|
.activated=${this._highlightedMarker === place.place_id}
|
||||||
|
.twoline=${primary && secondary}
|
||||||
|
>
|
||||||
|
${primary || secondary}
|
||||||
|
<span slot="secondary">${primary ? secondary : ""}</span>
|
||||||
|
</ha-list-item>`;
|
||||||
|
})
|
||||||
|
: html`<ha-list-item noninteractive
|
||||||
|
>${this._places === null ? "" : "No results"}</ha-list-item
|
||||||
|
>`}
|
||||||
|
</mwc-list>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
<p class="attribution">${addressAttribution}</p>
|
||||||
|
<ha-locations-editor
|
||||||
|
class="flex"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.locations=${this._markerLocations(
|
||||||
|
this._location,
|
||||||
|
this._places,
|
||||||
|
this._highlightedMarker
|
||||||
|
)}
|
||||||
|
zoom="14"
|
||||||
|
.darkMode=${mql.matches}
|
||||||
|
.disabled=${this._working}
|
||||||
|
@location-updated=${this._locationChanged}
|
||||||
|
@marker-clicked=${this._markerClicked}
|
||||||
|
></ha-locations-editor>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<mwc-button
|
||||||
|
@click=${this._save}
|
||||||
|
.disabled=${!this._location || this._working}
|
||||||
|
>
|
||||||
|
${this.onboardingLocalize(
|
||||||
|
"ui.panel.page-onboarding.core-config.finish"
|
||||||
|
)}
|
||||||
|
</mwc-button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(changedProps) {
|
||||||
|
super.firstUpdated(changedProps);
|
||||||
|
setTimeout(
|
||||||
|
() => this.renderRoot.querySelector("ha-textfield")!.focus(),
|
||||||
|
100
|
||||||
|
);
|
||||||
|
this.addEventListener("keyup", (ev) => {
|
||||||
|
if (ev.key === "Enter") {
|
||||||
|
this._save(ev);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps) {
|
||||||
|
if (changedProps.has("_highlightedMarker") && this._highlightedMarker) {
|
||||||
|
const place = this._places?.find(
|
||||||
|
(plc) => plc.place_id === this._highlightedMarker
|
||||||
|
);
|
||||||
|
if (place?.boundingbox?.length === 4) {
|
||||||
|
this.map.fitBounds(
|
||||||
|
[
|
||||||
|
[place.boundingbox[0], place.boundingbox[2]],
|
||||||
|
[place.boundingbox[1], place.boundingbox[3]],
|
||||||
|
],
|
||||||
|
{ zoom: 16, pad: 0 }
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.map.fitMarker(String(this._highlightedMarker), { zoom: 16 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _markerLocations = memoizeOne(
|
||||||
|
(
|
||||||
|
location?: [number, number],
|
||||||
|
places?: OpenStreetMapPlace[] | null,
|
||||||
|
highlightedMarker?: number
|
||||||
|
): MarkerLocation[] => {
|
||||||
|
if (!places) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: LOCATION_MARKER_ID,
|
||||||
|
latitude: (location || AMSTERDAM)[0],
|
||||||
|
longitude: (location || AMSTERDAM)[1],
|
||||||
|
location_editable: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return places?.length
|
||||||
|
? places.map((place) => ({
|
||||||
|
id: String(place.place_id),
|
||||||
|
iconPath:
|
||||||
|
place.place_id === highlightedMarker ? undefined : mdiMapMarker,
|
||||||
|
latitude:
|
||||||
|
location && place.place_id === highlightedMarker
|
||||||
|
? location[0]
|
||||||
|
: Number(place.lat),
|
||||||
|
longitude:
|
||||||
|
location && place.place_id === highlightedMarker
|
||||||
|
? location[1]
|
||||||
|
: Number(place.lon),
|
||||||
|
location_editable: place.place_id === highlightedMarker,
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
private _locationChanged(ev) {
|
||||||
|
this._location = ev.detail.location;
|
||||||
|
if (ev.detail.id !== LOCATION_MARKER_ID) {
|
||||||
|
this._reverseGeocode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _markerClicked(ev) {
|
||||||
|
if (ev.detail.id === LOCATION_MARKER_ID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._highlightedMarker = ev.detail.id;
|
||||||
|
const place = this._places!.find((plc) => plc.place_id === ev.detail.id)!;
|
||||||
|
this._location = [Number(place.lat), Number(place.lon)];
|
||||||
|
this._country = place.address.country_code.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _itemClicked(ev) {
|
||||||
|
this._highlightedMarker = ev.currentTarget.placeId;
|
||||||
|
const place = this._places!.find(
|
||||||
|
(plc) => plc.place_id === ev.currentTarget.placeId
|
||||||
|
)!;
|
||||||
|
this._location = [Number(place.lat), Number(place.lon)];
|
||||||
|
this._country = place.address.country_code.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _addressSearch(ev: KeyboardEvent) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this._search = (ev.currentTarget as HaTextField).value.length > 0;
|
||||||
|
if (ev.key !== "Enter") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._searchAddress((ev.currentTarget as HaTextField).value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _searchAddress(address: string) {
|
||||||
|
this._working = true;
|
||||||
|
this._location = undefined;
|
||||||
|
this._highlightedMarker = undefined;
|
||||||
|
this._error = undefined;
|
||||||
|
this._places = null;
|
||||||
|
this.map.addEventListener(
|
||||||
|
"markers-updated",
|
||||||
|
() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if ((this._places?.length || 0) > 2) {
|
||||||
|
this.map.fitMap({ pad: 0.5 });
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
once: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
this._places = await searchPlaces(address, this.hass, true, 3);
|
||||||
|
if (this._places?.length === 1) {
|
||||||
|
this._highlightedMarker = this._places[0].place_id;
|
||||||
|
this._location = [
|
||||||
|
Number(this._places[0].lat),
|
||||||
|
Number(this._places[0].lon),
|
||||||
|
];
|
||||||
|
this._country = this._places[0].address.country_code.toUpperCase();
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
this._places = undefined;
|
||||||
|
this._error = e.message;
|
||||||
|
} finally {
|
||||||
|
this._working = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _reverseGeocode() {
|
||||||
|
if (!this._location) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._places = null;
|
||||||
|
const reverse = await reverseGeocode(this._location, this.hass);
|
||||||
|
this._country = reverse.address.country_code.toUpperCase();
|
||||||
|
this._places = [reverse];
|
||||||
|
this._highlightedMarker = reverse.place_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _handleButtonClick(ev) {
|
||||||
|
if (this._search) {
|
||||||
|
this._searchAddress(ev.target.parentElement.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._detectLocation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _detectLocation() {
|
||||||
|
if (window.isSecureContext && navigator.geolocation) {
|
||||||
|
this._working = true;
|
||||||
|
const options = {
|
||||||
|
enableHighAccuracy: true,
|
||||||
|
timeout: 5000,
|
||||||
|
maximumAge: 0,
|
||||||
|
};
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
async (result) => {
|
||||||
|
this.map.addEventListener(
|
||||||
|
"markers-updated",
|
||||||
|
() => {
|
||||||
|
this.map.fitMarker(LOCATION_MARKER_ID);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
once: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this._location = [result.coords.latitude, result.coords.longitude];
|
||||||
|
if (result.coords.altitude) {
|
||||||
|
this._elevation = String(result.coords.altitude);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this._reverseGeocode();
|
||||||
|
} finally {
|
||||||
|
this._working = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// GPS is not available, get location based on IP
|
||||||
|
this._working = false;
|
||||||
|
this._whoAmI();
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this._whoAmI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _whoAmI() {
|
||||||
|
const confirm = await showConfirmationDialog(this, {
|
||||||
|
title: this.onboardingLocalize(
|
||||||
|
"ui.panel.page-onboarding.core-config.title_location_detect"
|
||||||
|
),
|
||||||
|
text: this.onboardingLocalize(
|
||||||
|
"ui.panel.page-onboarding.core-config.intro_location_detect"
|
||||||
|
),
|
||||||
|
});
|
||||||
|
if (!confirm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._working = true;
|
||||||
|
try {
|
||||||
|
const values = await detectCoreConfig(this.hass);
|
||||||
|
|
||||||
|
if (values.latitude && values.longitude) {
|
||||||
|
this.map.addEventListener(
|
||||||
|
"markers-updated",
|
||||||
|
() => {
|
||||||
|
this.map.fitMarker(LOCATION_MARKER_ID);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
once: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this._location = [Number(values.latitude), Number(values.longitude)];
|
||||||
|
}
|
||||||
|
if (values.elevation) {
|
||||||
|
this._elevation = String(values.elevation);
|
||||||
|
}
|
||||||
|
if (values.unit_system) {
|
||||||
|
this._unitSystem = values.unit_system;
|
||||||
|
}
|
||||||
|
if (values.time_zone) {
|
||||||
|
this._timeZone = values.time_zone;
|
||||||
|
}
|
||||||
|
if (values.currency) {
|
||||||
|
this._currency = values.currency;
|
||||||
|
}
|
||||||
|
if (values.country) {
|
||||||
|
this._country = values.country;
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
this._error = `Failed to detect location information: ${err.message}`;
|
||||||
|
} finally {
|
||||||
|
this._working = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _save(ev) {
|
||||||
|
if (!this._location) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ev.preventDefault();
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: {
|
||||||
|
location: this._location!,
|
||||||
|
country: this._country,
|
||||||
|
elevation: this._elevation,
|
||||||
|
unit_system: this._unitSystem,
|
||||||
|
time_zone: this._timeZone,
|
||||||
|
currency: this._currency,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
ha-textfield {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
ha-textfield > ha-icon-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
--mdc-icon-button-size: 36px;
|
||||||
|
--mdc-icon-size: 20px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
inset-inline-start: initial;
|
||||||
|
inset-inline-end: 10px;
|
||||||
|
direction: var(--direction);
|
||||||
|
}
|
||||||
|
ha-textfield > ha-circular-progress {
|
||||||
|
position: relative;
|
||||||
|
left: 12px;
|
||||||
|
}
|
||||||
|
ha-locations-editor {
|
||||||
|
display: block;
|
||||||
|
height: 300px;
|
||||||
|
margin-top: 8px;
|
||||||
|
border-radius: var(--mdc-shape-small, 4px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
mwc-list {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-top-width: 0;
|
||||||
|
border-bottom-left-radius: var(--mdc-shape-small, 4px);
|
||||||
|
border-bottom-right-radius: var(--mdc-shape-small, 4px);
|
||||||
|
--mdc-list-vertical-padding: 0;
|
||||||
|
}
|
||||||
|
ha-list-item {
|
||||||
|
height: 72px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
margin-top: 16px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.attribution {
|
||||||
|
/* textfield helper style */
|
||||||
|
margin: 0;
|
||||||
|
padding: 4px 16px 12px 16px;
|
||||||
|
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
|
||||||
|
font-family: var(
|
||||||
|
--mdc-typography-caption-font-family,
|
||||||
|
var(--mdc-typography-font-family, Roboto, sans-serif)
|
||||||
|
);
|
||||||
|
font-size: var(--mdc-typography-caption-font-size, 0.75rem);
|
||||||
|
font-weight: var(--mdc-typography-caption-font-weight, 400);
|
||||||
|
letter-spacing: var(
|
||||||
|
--mdc-typography-caption-letter-spacing,
|
||||||
|
0.0333333333em
|
||||||
|
);
|
||||||
|
text-decoration: var(--mdc-typography-caption-text-decoration, inherit);
|
||||||
|
text-transform: var(--mdc-typography-caption-text-transform, inherit);
|
||||||
|
}
|
||||||
|
.attribution a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"onboarding-location": OnboardingLocation;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
import "@material/mwc-button/mwc-button";
|
||||||
|
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import type { LocalizeFunc } from "../common/translations/localize";
|
||||||
|
import "../components/ha-alert";
|
||||||
|
import "../components/ha-formfield";
|
||||||
|
import "../components/ha-radio";
|
||||||
|
import "../components/ha-textfield";
|
||||||
|
import "../components/map/ha-locations-editor";
|
||||||
|
import { ConfigUpdateValues } from "../data/core";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
|
||||||
|
@customElement("onboarding-name")
|
||||||
|
class OnboardingName extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public onboardingLocalize!: LocalizeFunc;
|
||||||
|
|
||||||
|
private _name?: ConfigUpdateValues["location_name"];
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<p>
|
||||||
|
${this.onboardingLocalize(
|
||||||
|
"ui.panel.page-onboarding.core-config.intro",
|
||||||
|
{ name: this.hass.user!.name }
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ha-textfield
|
||||||
|
.label=${this.onboardingLocalize(
|
||||||
|
"ui.panel.page-onboarding.core-config.location_name"
|
||||||
|
)}
|
||||||
|
.value=${this._nameValue}
|
||||||
|
@change=${this._nameChanged}
|
||||||
|
></ha-textfield>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
${this.onboardingLocalize(
|
||||||
|
"ui.panel.page-onboarding.core-config.intro_core"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<mwc-button @click=${this._save} .disabled=${!this._nameValue}>
|
||||||
|
${this.onboardingLocalize(
|
||||||
|
"ui.panel.page-onboarding.core-config.finish"
|
||||||
|
)}
|
||||||
|
</mwc-button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(changedProps) {
|
||||||
|
super.firstUpdated(changedProps);
|
||||||
|
setTimeout(
|
||||||
|
() => this.renderRoot.querySelector("ha-textfield")!.focus(),
|
||||||
|
100
|
||||||
|
);
|
||||||
|
this.addEventListener("keyup", (ev) => {
|
||||||
|
if (ev.key === "Enter") {
|
||||||
|
this._save(ev);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _nameValue() {
|
||||||
|
return this._name !== undefined
|
||||||
|
? this._name
|
||||||
|
: this.onboardingLocalize(
|
||||||
|
"ui.panel.page-onboarding.core-config.location_name_default"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _nameChanged(ev) {
|
||||||
|
this._name = ev.target.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _save(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: this._nameValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
ha-textfield {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
margin-top: 16px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"onboarding-name": OnboardingName;
|
||||||
|
}
|
||||||
|
}
|
|
@ -5589,10 +5589,16 @@
|
||||||
},
|
},
|
||||||
"core-config": {
|
"core-config": {
|
||||||
"intro": "Hello {name}, welcome to Home Assistant. How would you like to name your home?",
|
"intro": "Hello {name}, welcome to Home Assistant. How would you like to name your home?",
|
||||||
"intro_location": "We would like to know where you live. This information will help with displaying information and setting up sun-based automations. This data is never shared outside of your network.",
|
"intro_core": "We will set up the basics together. You can always change this later in the settings.",
|
||||||
"intro_location_detect": "We can help you fill in this information by making a one-time request to an external service.",
|
"intro_location": "Let's set up the location of your home so that you can display information such as the local weather and use sun-based or presence-based automations. This data is never shared outside of your network.",
|
||||||
|
"location_address": "Powered by {openstreetmap} ({osm_privacy_policy}).",
|
||||||
|
"osm_privacy_policy": "Privacy policy",
|
||||||
|
"title_location_detect": "Do you want us to detect your location?",
|
||||||
|
"intro_location_detect": "We can detect your location by making a one-time request to an external service.",
|
||||||
|
"intro_core_config": "We filled out some details about your location. Please check if they are correct and continue.",
|
||||||
"location_name": "Name of your Home Assistant installation",
|
"location_name": "Name of your Home Assistant installation",
|
||||||
"location_name_default": "Home",
|
"location_name_default": "Home",
|
||||||
|
"address_label": "Search address",
|
||||||
"button_detect": "Detect",
|
"button_detect": "Detect",
|
||||||
"finish": "Next"
|
"finish": "Next"
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue