mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-19 07:16:39 +00:00
Add UI to set/update core config (#3208)
* Add UI to set/update core config * Types * Disable editor in config.yaml mode * Fix type
This commit is contained in:
parent
be0bef3f1b
commit
575eb22608
@ -75,8 +75,9 @@
|
||||
"es6-object-assign": "^1.1.0",
|
||||
"fecha": "^3.0.2",
|
||||
"fuse.js": "^3.4.4",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"hls.js": "^0.12.4",
|
||||
"home-assistant-js-websocket": "^4.1.2",
|
||||
"home-assistant-js-websocket": "^4.2.1",
|
||||
"intl-messageformat": "^2.2.0",
|
||||
"jquery": "^3.3.1",
|
||||
"js-yaml": "^3.13.0",
|
||||
|
@ -1,15 +1,11 @@
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { UnsubscribeFunc, Connection } from "home-assistant-js-websocket";
|
||||
|
||||
export const subscribeOne = async <T>(
|
||||
hass: HomeAssistant,
|
||||
subscribe: (
|
||||
hass: HomeAssistant,
|
||||
onChange: (items: T) => void
|
||||
) => UnsubscribeFunc
|
||||
conn: Connection,
|
||||
subscribe: (conn: Connection, onChange: (items: T) => void) => UnsubscribeFunc
|
||||
) =>
|
||||
new Promise<T>((resolve) => {
|
||||
const unsub = subscribe(hass, (items) => {
|
||||
const unsub = subscribe(conn, (items) => {
|
||||
unsub();
|
||||
resolve(items);
|
||||
});
|
||||
|
13
src/components/timezone-datalist.ts
Normal file
13
src/components/timezone-datalist.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import timezones from "google-timezones-json";
|
||||
|
||||
export const createTimezoneListEl = () => {
|
||||
const list = document.createElement("datalist");
|
||||
list.id = "timezones";
|
||||
Object.keys(timezones).forEach((key) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = key;
|
||||
option.innerHTML = timezones[key];
|
||||
list.appendChild(option);
|
||||
});
|
||||
return list;
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import { createCollection } from "home-assistant-js-websocket";
|
||||
import { createCollection, Connection } from "home-assistant-js-websocket";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { compare } from "../common/string/compare";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
@ -57,13 +57,13 @@ const subscribeAreaRegistryUpdates = (conn, store) =>
|
||||
);
|
||||
|
||||
export const subscribeAreaRegistry = (
|
||||
hass: HomeAssistant,
|
||||
conn: Connection,
|
||||
onChange: (areas: AreaRegistryEntry[]) => void
|
||||
) =>
|
||||
createCollection<AreaRegistryEntry[]>(
|
||||
"_areaRegistry",
|
||||
fetchAreaRegistry,
|
||||
subscribeAreaRegistryUpdates,
|
||||
hass.connection,
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
|
25
src/data/core.ts
Normal file
25
src/data/core.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
import { HassConfig } from "home-assistant-js-websocket";
|
||||
|
||||
export interface ConfigUpdateValues {
|
||||
location_name: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
elevation: number;
|
||||
unit_system: "metric" | "imperial";
|
||||
time_zone: string;
|
||||
}
|
||||
|
||||
export const saveCoreConfig = (
|
||||
hass: HomeAssistant,
|
||||
values: Partial<ConfigUpdateValues>
|
||||
) =>
|
||||
hass.callWS<HassConfig>({
|
||||
type: "config/core/update",
|
||||
...values,
|
||||
});
|
||||
|
||||
export const detectCoreConfig = (hass: HomeAssistant) =>
|
||||
hass.callWS<Partial<ConfigUpdateValues>>({
|
||||
type: "config/core/detect",
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
import { createCollection } from "home-assistant-js-websocket";
|
||||
import { createCollection, Connection } from "home-assistant-js-websocket";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
|
||||
export interface DeviceRegistryEntry {
|
||||
@ -50,13 +50,13 @@ const subscribeDeviceRegistryUpdates = (conn, store) =>
|
||||
);
|
||||
|
||||
export const subscribeDeviceRegistry = (
|
||||
hass: HomeAssistant,
|
||||
conn: Connection,
|
||||
onChange: (devices: DeviceRegistryEntry[]) => void
|
||||
) =>
|
||||
createCollection<DeviceRegistryEntry[]>(
|
||||
"_dr",
|
||||
fetchDeviceRegistry,
|
||||
subscribeDeviceRegistryUpdates,
|
||||
hass.connection,
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createCollection } from "home-assistant-js-websocket";
|
||||
import { createCollection, Connection } from "home-assistant-js-websocket";
|
||||
import { HomeAssistant } from "../types";
|
||||
import computeStateName from "../common/entity/compute_state_name";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
@ -67,13 +67,13 @@ const subscribeEntityRegistryUpdates = (conn, store) =>
|
||||
);
|
||||
|
||||
export const subscribeEntityRegistry = (
|
||||
hass: HomeAssistant,
|
||||
conn: Connection,
|
||||
onChange: (entities: EntityRegistryEntry[]) => void
|
||||
) =>
|
||||
createCollection<EntityRegistryEntry[]>(
|
||||
"_entityRegistry",
|
||||
fetchEntityRegistry,
|
||||
subscribeEntityRegistryUpdates,
|
||||
hass.connection,
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { handleFetchPromise } from "../util/hass-call-api";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
// tslint:disable-next-line: no-empty-interface
|
||||
export interface OnboardingCoreConfigStepResponse {}
|
||||
|
||||
export interface OnboardingUserStepResponse {
|
||||
auth_code: string;
|
||||
}
|
||||
@ -11,6 +14,7 @@ export interface OnboardingIntegrationStepResponse {
|
||||
|
||||
export interface OnboardingResponses {
|
||||
user: OnboardingUserStepResponse;
|
||||
core_config: OnboardingCoreConfigStepResponse;
|
||||
integration: OnboardingIntegrationStepResponse;
|
||||
}
|
||||
|
||||
@ -39,6 +43,12 @@ export const onboardUserStep = (params: {
|
||||
})
|
||||
);
|
||||
|
||||
export const onboardCoreConfigStep = (hass: HomeAssistant) =>
|
||||
hass.callApi<OnboardingCoreConfigStepResponse>(
|
||||
"POST",
|
||||
"onboarding/core_config"
|
||||
);
|
||||
|
||||
export const onboardIntegrationStep = (
|
||||
hass: HomeAssistant,
|
||||
params: { client_id: string }
|
||||
|
@ -219,15 +219,18 @@ class ConfigFlowDialog extends LitElement {
|
||||
}
|
||||
|
||||
private async _fetchDevices(configEntryId) {
|
||||
this._unsubDevices = subscribeDeviceRegistry(this.hass, (devices) => {
|
||||
this._unsubDevices = subscribeDeviceRegistry(
|
||||
this.hass.connection,
|
||||
(devices) => {
|
||||
this._devices = devices.filter((device) =>
|
||||
device.config_entries.includes(configEntryId)
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async _fetchAreas() {
|
||||
this._unsubAreas = subscribeAreaRegistry(this.hass, (areas) => {
|
||||
this._unsubAreas = subscribeAreaRegistry(this.hass.connection, (areas) => {
|
||||
this._areas = areas;
|
||||
});
|
||||
}
|
||||
|
@ -16,4 +16,5 @@ export const demoConfig: HassConfig = {
|
||||
config_dir: "/config",
|
||||
version: "DEMO",
|
||||
whitelist_external_dirs: [],
|
||||
config_source: "storage",
|
||||
};
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
createConnection,
|
||||
genClientId,
|
||||
Auth,
|
||||
subscribeConfig,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
|
||||
import {
|
||||
@ -24,6 +25,8 @@ import "./onboarding-create-user";
|
||||
import "./onboarding-loading";
|
||||
import { hassUrl } from "../data/auth";
|
||||
import { HassElement } from "../state/hass-element";
|
||||
import { subscribeOne } from "../common/util/subscribe-one";
|
||||
import { subscribeUser } from "../data/ws-user";
|
||||
|
||||
interface OnboardingEvent<T extends ValidOnboardingStep> {
|
||||
type: T;
|
||||
@ -61,6 +64,13 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
.language=${this.language}
|
||||
></onboarding-create-user>
|
||||
`;
|
||||
} else if (step.step === "core_config") {
|
||||
return html`
|
||||
<onboarding-core-config
|
||||
.hass=${this.hass}
|
||||
.onboardingLocalize=${this.localize}
|
||||
></onboarding-core-config>
|
||||
`;
|
||||
} else if (step.step === "integration") {
|
||||
return html`
|
||||
<onboarding-integrations
|
||||
@ -75,6 +85,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._fetchOnboardingSteps();
|
||||
import("./onboarding-integrations");
|
||||
import("./onboarding-core-config");
|
||||
registerServiceWorker(false);
|
||||
this.addEventListener("onboarding-step", (ev) => this._handleStepDone(ev));
|
||||
}
|
||||
@ -106,6 +117,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
const auth = await getAuth({
|
||||
hassUrl,
|
||||
});
|
||||
history.replaceState(null, "", location.pathname);
|
||||
await this._connectHass(auth);
|
||||
}
|
||||
|
||||
@ -138,6 +150,8 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
} else if (stepResult.type === "core_config") {
|
||||
// We do nothing
|
||||
} else if (stepResult.type === "integration") {
|
||||
const result = stepResult.result as OnboardingResponses["integration"];
|
||||
this._loading = true;
|
||||
@ -161,6 +175,12 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
|
||||
private async _connectHass(auth: Auth) {
|
||||
const conn = await createConnection({ auth });
|
||||
// Make sure config and user info is loaded before we initialize.
|
||||
// It is needed for the core config step.
|
||||
await Promise.all([
|
||||
subscribeOne(conn, subscribeConfig),
|
||||
subscribeOne(conn, subscribeUser),
|
||||
]);
|
||||
this.initializeHass(auth, conn);
|
||||
// Load config strings for integrations
|
||||
(this as any)._loadFragmentTranslations(this.hass!.language, "config");
|
||||
|
320
src/onboarding/onboarding-core-config.ts
Normal file
320
src/onboarding/onboarding-core-config.ts
Normal file
@ -0,0 +1,320 @@
|
||||
import {
|
||||
LitElement,
|
||||
customElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
html,
|
||||
CSSResult,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-radio-group/paper-radio-group";
|
||||
import "@polymer/paper-radio-button/paper-radio-button";
|
||||
// tslint:disable-next-line: no-duplicate-imports
|
||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import { HomeAssistant } from "../types";
|
||||
import {
|
||||
ConfigUpdateValues,
|
||||
detectCoreConfig,
|
||||
saveCoreConfig,
|
||||
} from "../data/core";
|
||||
import { UNIT_C } from "../common/const";
|
||||
import { PolymerChangedEvent } from "../polymer-types";
|
||||
import { onboardCoreConfigStep } from "../data/onboarding";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
import { createTimezoneListEl } from "../components/timezone-datalist";
|
||||
|
||||
@customElement("onboarding-core-config")
|
||||
class OnboardingCoreConfig extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public onboardingLocalize!: LocalizeFunc;
|
||||
|
||||
@property() private _working = false;
|
||||
|
||||
@property() private _name!: ConfigUpdateValues["location_name"];
|
||||
@property() private _latitude!: string;
|
||||
@property() private _longitude!: string;
|
||||
@property() private _elevation!: string;
|
||||
@property() private _unitSystem!: ConfigUpdateValues["unit_system"];
|
||||
@property() private _timeZone!: string;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
return html`
|
||||
<p>
|
||||
${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.intro",
|
||||
"name",
|
||||
this.hass.user!.name
|
||||
)}
|
||||
</p>
|
||||
|
||||
<paper-input
|
||||
.label=${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.location_name"
|
||||
)}
|
||||
name="name"
|
||||
.disabled=${this._working}
|
||||
.value=${this._nameValue}
|
||||
@value-changed=${this._handleChange}
|
||||
></paper-input>
|
||||
|
||||
<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">
|
||||
<paper-input
|
||||
class="flex"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.latitude"
|
||||
)}
|
||||
name="latitude"
|
||||
.disabled=${this._working}
|
||||
.value=${this._latitudeValue}
|
||||
@value-changed=${this._handleChange}
|
||||
></paper-input>
|
||||
<paper-input
|
||||
class="flex"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.longitude"
|
||||
)}
|
||||
name="longitude"
|
||||
.disabled=${this._working}
|
||||
.value=${this._longitudeValue}
|
||||
@value-changed=${this._handleChange}
|
||||
></paper-input>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<paper-input
|
||||
class="flex"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.time_zone"
|
||||
)}
|
||||
name="timeZone"
|
||||
list="timezones"
|
||||
.disabled=${this._working}
|
||||
.value=${this._timeZoneValue}
|
||||
@value-changed=${this._handleChange}
|
||||
></paper-input>
|
||||
|
||||
<paper-input
|
||||
class="flex"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation"
|
||||
)}
|
||||
name="elevation"
|
||||
type="number"
|
||||
.disabled=${this._working}
|
||||
.value=${this._elevationValue}
|
||||
@value-changed=${this._handleChange}
|
||||
>
|
||||
<span slot="suffix">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation_meters"
|
||||
)}
|
||||
</span>
|
||||
</paper-input>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="flex">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system"
|
||||
)}
|
||||
</div>
|
||||
<paper-radio-group class="flex" .selected=${this._unitSystemValue}>
|
||||
<paper-radio-button name="metric" .disabled=${this._working}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system_metric"
|
||||
)}
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.metric_example"
|
||||
)}
|
||||
</div>
|
||||
</paper-radio-button>
|
||||
<paper-radio-button name="imperial" .disabled=${this._working}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system_imperial"
|
||||
)}
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.imperial_example"
|
||||
)}
|
||||
</div>
|
||||
</paper-radio-button>
|
||||
</paper-radio-group>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<mwc-button @click=${this._save} .disabled=${this._working}>
|
||||
${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.finish"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
setTimeout(
|
||||
() => this.shadowRoot!.querySelector("paper-input")!.focus(),
|
||||
100
|
||||
);
|
||||
this.addEventListener("keypress", (ev) => {
|
||||
if (ev.keyCode === 13) {
|
||||
this._save(ev);
|
||||
}
|
||||
});
|
||||
const input = this.shadowRoot!.querySelector(
|
||||
"[name=timeZone]"
|
||||
) as PaperInputElement;
|
||||
input.inputElement.appendChild(createTimezoneListEl());
|
||||
}
|
||||
|
||||
private get _nameValue() {
|
||||
return this._name !== undefined
|
||||
? this._name
|
||||
: this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.location_name_default"
|
||||
);
|
||||
}
|
||||
|
||||
private get _latitudeValue() {
|
||||
return this._latitude !== undefined
|
||||
? this._latitude
|
||||
: this.hass.config.latitude;
|
||||
}
|
||||
|
||||
private get _longitudeValue() {
|
||||
return this._longitude !== undefined
|
||||
? this._longitude
|
||||
: this.hass.config.longitude;
|
||||
}
|
||||
|
||||
private get _elevationValue() {
|
||||
return this._elevation !== undefined
|
||||
? this._elevation
|
||||
: this.hass.config.elevation;
|
||||
}
|
||||
|
||||
private get _timeZoneValue() {
|
||||
return this._timeZone !== undefined
|
||||
? this._timeZone
|
||||
: this.hass.config.time_zone;
|
||||
}
|
||||
|
||||
private get _unitSystemValue() {
|
||||
return this._unitSystem !== undefined
|
||||
? this._unitSystem
|
||||
: this.hass.config.unit_system.temperature === UNIT_C
|
||||
? "metric"
|
||||
: "imperial";
|
||||
}
|
||||
|
||||
private _handleChange(ev: PolymerChangedEvent<string>) {
|
||||
const target = ev.currentTarget as PaperInputElement;
|
||||
this[`_${target.name}`] = target.value;
|
||||
}
|
||||
|
||||
private async _detect() {
|
||||
this._working = true;
|
||||
try {
|
||||
const values = await detectCoreConfig(this.hass);
|
||||
for (const key in values) {
|
||||
if (key === "unit_system") {
|
||||
this._unitSystem = values[key]!;
|
||||
} else if (key === "time_zone") {
|
||||
this._timeZone = values[key]!;
|
||||
} else {
|
||||
this[`_${key}`] = values[key];
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
alert(`Failed to detect location information: ${err.message}`);
|
||||
} finally {
|
||||
this._working = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _save(ev) {
|
||||
ev.preventDefault();
|
||||
this._working = true;
|
||||
try {
|
||||
await saveCoreConfig(this.hass, {
|
||||
location_name: this._nameValue,
|
||||
latitude: Number(this._latitudeValue),
|
||||
longitude: Number(this._longitudeValue),
|
||||
elevation: Number(this._elevationValue),
|
||||
unit_system: this._unitSystemValue,
|
||||
time_zone: this._timeZoneValue,
|
||||
});
|
||||
const result = await onboardCoreConfigStep(this.hass);
|
||||
fireEvent(this, "onboarding-step", {
|
||||
type: "core_config",
|
||||
result,
|
||||
});
|
||||
} catch (err) {
|
||||
this._working = false;
|
||||
alert("FAIL");
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0 -8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.flex {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.middle-text {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.row > * {
|
||||
margin: 0 8px;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 16px;
|
||||
text-align: right;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"onboarding-core-config": OnboardingCoreConfig;
|
||||
}
|
||||
}
|
@ -122,9 +122,12 @@ class HaConfigAreaRegistry extends LitElement {
|
||||
protected updated(changedProps) {
|
||||
super.updated(changedProps);
|
||||
if (!this._unsubAreas) {
|
||||
this._unsubAreas = subscribeAreaRegistry(this.hass, (areas) => {
|
||||
this._unsubAreas = subscribeAreaRegistry(
|
||||
this.hass.connection,
|
||||
(areas) => {
|
||||
this._areas = areas;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
255
src/panels/config/core/ha-config-core-form.ts
Normal file
255
src/panels/config/core/ha-config-core-form.ts
Normal file
@ -0,0 +1,255 @@
|
||||
import {
|
||||
LitElement,
|
||||
customElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
html,
|
||||
CSSResult,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-radio-group/paper-radio-group";
|
||||
import "@polymer/paper-radio-button/paper-radio-button";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "../../../components/ha-card";
|
||||
import { PolymerChangedEvent } from "../../../polymer-types";
|
||||
// tslint:disable-next-line: no-duplicate-imports
|
||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import { UNIT_C } from "../../../common/const";
|
||||
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
|
||||
import { createTimezoneListEl } from "../../../components/timezone-datalist";
|
||||
|
||||
@customElement("ha-config-core-form")
|
||||
class ConfigCoreForm extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() private _working = false;
|
||||
|
||||
@property() private _latitude!: string;
|
||||
@property() private _longitude!: string;
|
||||
@property() private _elevation!: string;
|
||||
@property() private _unitSystem!: ConfigUpdateValues["unit_system"];
|
||||
@property() private _timeZone!: string;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
const isStorage = this.hass.config.config_source === "storage";
|
||||
const disabled = this._working || !isStorage;
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.form.heading"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
${!isStorage
|
||||
? html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.edit_requires_storage"
|
||||
)}
|
||||
</p>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<div class="row">
|
||||
<paper-input
|
||||
class="flex"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.latitude"
|
||||
)}
|
||||
name="latitude"
|
||||
.disabled=${disabled}
|
||||
.value=${this._latitudeValue}
|
||||
@value-changed=${this._handleChange}
|
||||
></paper-input>
|
||||
<paper-input
|
||||
class="flex"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.longitude"
|
||||
)}
|
||||
name="longitude"
|
||||
.disabled=${disabled}
|
||||
.value=${this._longitudeValue}
|
||||
@value-changed=${this._handleChange}
|
||||
></paper-input>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="flex">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.time_zone"
|
||||
)}
|
||||
</div>
|
||||
|
||||
<paper-input
|
||||
class="flex"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.time_zone"
|
||||
)}
|
||||
name="timeZone"
|
||||
list="timezones"
|
||||
.disabled=${disabled}
|
||||
.value=${this._timeZoneValue}
|
||||
@value-changed=${this._handleChange}
|
||||
></paper-input>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="flex">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation"
|
||||
)}
|
||||
</div>
|
||||
|
||||
<paper-input
|
||||
class="flex"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation"
|
||||
)}
|
||||
name="elevation"
|
||||
type="number"
|
||||
.disabled=${disabled}
|
||||
.value=${this._elevationValue}
|
||||
@value-changed=${this._handleChange}
|
||||
>
|
||||
<span slot="suffix">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation_meters"
|
||||
)}
|
||||
</span>
|
||||
</paper-input>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="flex">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system"
|
||||
)}
|
||||
</div>
|
||||
<paper-radio-group class="flex" .selected=${this._unitSystemValue}>
|
||||
<paper-radio-button name="metric" .disabled=${disabled}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system_metric"
|
||||
)}
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.metric_example"
|
||||
)}
|
||||
</div>
|
||||
</paper-radio-button>
|
||||
<paper-radio-button name="imperial" .disabled=${disabled}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system_imperial"
|
||||
)}
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.imperial_example"
|
||||
)}
|
||||
</div>
|
||||
</paper-radio-button>
|
||||
</paper-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._save} .disabled=${disabled}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.save_button"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
const input = this.shadowRoot!.querySelector(
|
||||
"[name=timeZone]"
|
||||
) as PaperInputElement;
|
||||
input.inputElement.appendChild(createTimezoneListEl());
|
||||
}
|
||||
|
||||
private get _latitudeValue() {
|
||||
return this._latitude !== undefined
|
||||
? this._latitude
|
||||
: this.hass.config.latitude;
|
||||
}
|
||||
|
||||
private get _longitudeValue() {
|
||||
return this._longitude !== undefined
|
||||
? this._longitude
|
||||
: this.hass.config.longitude;
|
||||
}
|
||||
|
||||
private get _elevationValue() {
|
||||
return this._elevation !== undefined
|
||||
? this._elevation
|
||||
: this.hass.config.elevation;
|
||||
}
|
||||
|
||||
private get _timeZoneValue() {
|
||||
return this._timeZone !== undefined
|
||||
? this._timeZone
|
||||
: this.hass.config.time_zone;
|
||||
}
|
||||
|
||||
private get _unitSystemValue() {
|
||||
return this._unitSystem !== undefined
|
||||
? this._unitSystem
|
||||
: this.hass.config.unit_system.temperature === UNIT_C
|
||||
? "metric"
|
||||
: "imperial";
|
||||
}
|
||||
|
||||
private _handleChange(ev: PolymerChangedEvent<string>) {
|
||||
const target = ev.currentTarget as PaperInputElement;
|
||||
this[`_${target.name}`] = target.value;
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
this._working = true;
|
||||
try {
|
||||
await saveCoreConfig(this.hass, {
|
||||
latitude: Number(this._latitudeValue),
|
||||
longitude: Number(this._longitudeValue),
|
||||
elevation: Number(this._elevationValue),
|
||||
unit_system: this._unitSystemValue,
|
||||
time_zone: this._timeZoneValue,
|
||||
});
|
||||
} catch (err) {
|
||||
alert("FAIL");
|
||||
} finally {
|
||||
this._working = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0 -8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.flex {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.row > * {
|
||||
margin: 0 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-core-form": ConfigCoreForm;
|
||||
}
|
||||
}
|
94
src/panels/config/core/ha-config-name-form.ts
Normal file
94
src/panels/config/core/ha-config-name-form.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import {
|
||||
LitElement,
|
||||
customElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
html,
|
||||
} from "lit-element";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-radio-group/paper-radio-group";
|
||||
import "@polymer/paper-radio-button/paper-radio-button";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "../../../components/ha-card";
|
||||
import { PolymerChangedEvent } from "../../../polymer-types";
|
||||
// tslint:disable-next-line: no-duplicate-imports
|
||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
|
||||
|
||||
@customElement("ha-config-name-form")
|
||||
class ConfigNameForm extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() private _working = false;
|
||||
|
||||
@property() private _name!: ConfigUpdateValues["location_name"];
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
const isStorage = this.hass.config.config_source === "storage";
|
||||
const disabled = this._working || !isStorage;
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
${!isStorage
|
||||
? html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.edit_requires_storage"
|
||||
)}
|
||||
</p>
|
||||
`
|
||||
: ""}
|
||||
<paper-input
|
||||
class="flex"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.location_name"
|
||||
)}
|
||||
name="name"
|
||||
.disabled=${disabled}
|
||||
.value=${this._nameValue}
|
||||
@value-changed=${this._handleChange}
|
||||
></paper-input>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._save} .disabled=${disabled}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.save_button"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _nameValue() {
|
||||
return this._name !== undefined
|
||||
? this._name
|
||||
: this.hass.config.location_name;
|
||||
}
|
||||
|
||||
private _handleChange(ev: PolymerChangedEvent<string>) {
|
||||
const target = ev.currentTarget as PaperInputElement;
|
||||
this[`_${target.name}`] = target.value;
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
this._working = true;
|
||||
try {
|
||||
await saveCoreConfig(this.hass, {
|
||||
location_name: this._nameValue,
|
||||
});
|
||||
} catch (err) {
|
||||
alert("FAIL");
|
||||
} finally {
|
||||
this._working = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-name-form": ConfigNameForm;
|
||||
}
|
||||
}
|
@ -12,6 +12,9 @@ import "../ha-config-section";
|
||||
import isComponentLoaded from "../../../common/config/is_component_loaded";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
|
||||
import "./ha-config-name-form";
|
||||
import "./ha-config-core-form";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
@ -57,6 +60,9 @@ class HaConfigSectionCore extends LocalizeMixin(PolymerElement) {
|
||||
>[[localize('ui.panel.config.core.section.core.introduction')]]</span
|
||||
>
|
||||
|
||||
<ha-config-name-form hass="[[hass]]"></ha-config-name-form>
|
||||
<ha-config-core-form hass="[[hass]]"></ha-config-core-form>
|
||||
|
||||
<ha-card
|
||||
header="[[localize('ui.panel.config.core.section.core.validation.heading')]]"
|
||||
>
|
||||
|
@ -118,11 +118,14 @@ class HaConfigEntityRegistry extends LitElement {
|
||||
protected updated(changedProps) {
|
||||
super.updated(changedProps);
|
||||
if (!this._unsubEntities) {
|
||||
this._unsubEntities = subscribeEntityRegistry(this.hass, (entities) => {
|
||||
this._unsubEntities = subscribeEntityRegistry(
|
||||
this.hass.connection,
|
||||
(entities) => {
|
||||
this._entities = entities.sort((ent1, ent2) =>
|
||||
compare(ent1.entity_id, ent2.entity_id)
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,7 +96,7 @@ class HaConfigIntegrations extends NavigateMixin(PolymerElement) {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._loadData();
|
||||
this._unsubAreas = subscribeAreaRegistry(this.hass, (areas) => {
|
||||
this._unsubAreas = subscribeAreaRegistry(this.hass.connection, (areas) => {
|
||||
this._areas = areas;
|
||||
});
|
||||
|
||||
|
@ -90,9 +90,12 @@ class ZHADeviceCard extends LitElement {
|
||||
this._userGivenName = this.device!.user_given_name;
|
||||
}
|
||||
if (!this._unsubAreas) {
|
||||
this._unsubAreas = subscribeAreaRegistry(this.hass, (areas) => {
|
||||
this._unsubAreas = subscribeAreaRegistry(
|
||||
this.hass.connection,
|
||||
(areas) => {
|
||||
this._areas = areas;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
super.update(changedProperties);
|
||||
}
|
||||
|
@ -62,6 +62,10 @@ export class HuiEmptyStateCard extends LitElement implements LovelaceCard {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.card-actions a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
mwc-button {
|
||||
margin-left: -8px;
|
||||
}
|
||||
|
@ -352,15 +352,15 @@ export const generateLovelaceConfig = async (
|
||||
// so that we don't serve up stale data after changing areas.
|
||||
if (!subscribedRegistries) {
|
||||
subscribedRegistries = true;
|
||||
subscribeAreaRegistry(hass, () => undefined);
|
||||
subscribeDeviceRegistry(hass, () => undefined);
|
||||
subscribeEntityRegistry(hass, () => undefined);
|
||||
subscribeAreaRegistry(hass.connection, () => undefined);
|
||||
subscribeDeviceRegistry(hass.connection, () => undefined);
|
||||
subscribeEntityRegistry(hass.connection, () => undefined);
|
||||
}
|
||||
|
||||
const [areas, devices, entities] = await Promise.all([
|
||||
subscribeOne(hass, subscribeAreaRegistry),
|
||||
subscribeOne(hass, subscribeDeviceRegistry),
|
||||
subscribeOne(hass, subscribeEntityRegistry),
|
||||
subscribeOne(hass.connection, subscribeAreaRegistry),
|
||||
subscribeOne(hass.connection, subscribeDeviceRegistry),
|
||||
subscribeOne(hass.connection, subscribeEntityRegistry),
|
||||
]);
|
||||
const registries = { areas, devices, entities };
|
||||
|
||||
|
@ -310,6 +310,9 @@ class HUIRoot extends LitElement {
|
||||
ha-app-layout {
|
||||
min-height: 100%;
|
||||
}
|
||||
paper-menu-button {
|
||||
padding: 0;
|
||||
}
|
||||
paper-tabs {
|
||||
margin-left: 12px;
|
||||
--paper-tabs-selection-bar-color: var(--text-primary-color, #fff);
|
||||
|
@ -581,6 +581,21 @@
|
||||
"core": {
|
||||
"header": "Configuration and Server Control",
|
||||
"introduction": "Changing your configuration can be a tiresome process. We know. This section will try to make your life a little bit easier.",
|
||||
"core_config": {
|
||||
"edit_requires_storage": "Editor disabled because config stored in configuration.yaml.",
|
||||
"location_name": "Name of your Home Assistant installation",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude",
|
||||
"elevation": "Elevation",
|
||||
"elevation_meters": "meters",
|
||||
"time_zone": "Time Zone",
|
||||
"unit_system": "Unit System",
|
||||
"unit_system_imperial": "Imperial",
|
||||
"unit_system_metric": "Metric",
|
||||
"imperial_example": "Fahrenheit, pounds",
|
||||
"metric_example": "Celsius, kilograms",
|
||||
"save_button": "Save"
|
||||
},
|
||||
"validation": {
|
||||
"heading": "Configuration validation",
|
||||
"introduction": "Validate your configuration if you recently made some changes to your configuration and want to make sure that it is all valid",
|
||||
@ -1179,6 +1194,14 @@
|
||||
"password_not_match": "Passwords don't match"
|
||||
}
|
||||
},
|
||||
"core-config": {
|
||||
"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_location_detect": "We can help you fill in this information by making a one-time request to an external service.",
|
||||
"location_name_default": "Home",
|
||||
"button_detect": "Detect",
|
||||
"finish": "Next"
|
||||
},
|
||||
"integration": {
|
||||
"intro": "Devices and services are represented in Home Assistant as integrations. You can set them up now, or do it later from the configuration screen.",
|
||||
"more_integrations": "More",
|
||||
|
13
yarn.lock
13
yarn.lock
@ -6807,6 +6807,11 @@ glogg@^1.0.0:
|
||||
dependencies:
|
||||
sparkles "^1.0.0"
|
||||
|
||||
google-timezones-json@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/google-timezones-json/-/google-timezones-json-1.0.2.tgz#6800000d7ebc2dd660611aad8c1c68196db78cab"
|
||||
integrity sha512-UWXQ7BpSCW8erDespU2I4cri22xsKgwOCyhsJal0OJhi2tFpwJpsYNJt4vCiFPL1p2HzCGiS713LKpNR25n9Kg==
|
||||
|
||||
got@^5.0.0:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/got/-/got-5.7.1.tgz#5f81635a61e4a6589f180569ea4e381680a51f35"
|
||||
@ -7242,10 +7247,10 @@ hoek@6.x.x:
|
||||
resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.3.tgz#73b7d33952e01fe27a38b0457294b79dd8da242c"
|
||||
integrity sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ==
|
||||
|
||||
home-assistant-js-websocket@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-4.1.2.tgz#dbcdb4b67df8d189d29bbf5603771d5bc80ef031"
|
||||
integrity sha512-/I0m6FTDEq3LkzFc4tmgHJHTj9gWA6Wn/fgaa1ghIJJY0Yqb3x6whovN5pRNFsl6bnKzOCR+nmJ2ruVTBa5mVQ==
|
||||
home-assistant-js-websocket@^4.2.1:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-4.2.1.tgz#8acdf2a404b4204669213d8405cca027b8b1de1c"
|
||||
integrity sha512-lF4owDhAAUY70FNvTzgg6MAEOpKbJDLsRDX3gW48muna03s3CRGQzbLmy621pJWK757CkXSW/rWbr34r3Wyi8Q==
|
||||
|
||||
homedir-polyfill@^1.0.0, homedir-polyfill@^1.0.1:
|
||||
version "1.0.3"
|
||||
|
Loading…
x
Reference in New Issue
Block a user