diff --git a/package.json b/package.json
index 3cf476e4b9..edaa0308dc 100644
--- a/package.json
+++ b/package.json
@@ -75,7 +75,7 @@
"es6-object-assign": "^1.1.0",
"fecha": "^3.0.2",
"hls.js": "^0.12.4",
- "home-assistant-js-websocket": "^4.1.1",
+ "home-assistant-js-websocket": "^4.1.2",
"intl-messageformat": "^2.2.0",
"jquery": "^3.3.1",
"js-yaml": "^3.13.0",
diff --git a/setup.py b/setup.py
index ccee72852d..f52c3ac7b7 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
- version="20190507.0",
+ version="20190508.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",
diff --git a/src/auth/ha-auth-flow.js b/src/auth/ha-auth-flow.js
deleted file mode 100644
index d397aa4f4e..0000000000
--- a/src/auth/ha-auth-flow.js
+++ /dev/null
@@ -1,270 +0,0 @@
-import { PolymerElement } from "@polymer/polymer/polymer-element";
-import "@material/mwc-button";
-import { html } from "@polymer/polymer/lib/utils/html-tag";
-import "../components/ha-form";
-import "../components/ha-markdown";
-import { localizeLiteMixin } from "../mixins/localize-lite-mixin";
-
-class HaAuthFlow extends localizeLiteMixin(PolymerElement) {
- static get template() {
- return html`
-
-
- `;
- }
-
- static get properties() {
- return {
- authProvider: {
- type: Object,
- observer: "_providerChanged",
- },
- clientId: String,
- redirectUri: String,
- oauth2State: String,
- _state: {
- type: String,
- value: "loading",
- },
- _stepData: {
- type: Object,
- value: () => ({}),
- },
- _step: {
- type: Object,
- notify: true,
- },
- _errorMsg: String,
- };
- }
-
- ready() {
- super.ready();
-
- this.addEventListener("keypress", (ev) => {
- if (ev.keyCode === 13) {
- this._handleSubmit(ev);
- }
- });
- }
-
- async _providerChanged(newProvider, oldProvider) {
- if (oldProvider && this._step && this._step.type === "form") {
- fetch(`/auth/login_flow/${this._step.flow_id}`, {
- method: "DELETE",
- credentials: "same-origin",
- }).catch(() => {});
- }
-
- try {
- const response = await fetch("/auth/login_flow", {
- method: "POST",
- credentials: "same-origin",
- body: JSON.stringify({
- client_id: this.clientId,
- handler: [newProvider.type, newProvider.id],
- redirect_uri: this.redirectUri,
- }),
- });
-
- const data = await response.json();
-
- if (response.ok) {
- // allow auth provider bypass the login form
- if (data.type === "create_entry") {
- this._redirect(data.result);
- return;
- }
-
- this._updateStep(data);
- } else {
- this.setProperties({
- _state: "error",
- _errorMsg: data.message,
- });
- }
- } catch (err) {
- // eslint-disable-next-line
- console.error("Error starting auth flow", err);
- this.setProperties({
- _state: "error",
- _errorMsg: this.localize("ui.panel.page-authorize.form.unknown_error"),
- });
- }
- }
-
- _redirect(authCode) {
- // OAuth 2: 3.1.2 we need to retain query component of a redirect URI
- let url = this.redirectUri;
- if (!url.includes("?")) {
- url += "?";
- } else if (!url.endsWith("&")) {
- url += "&";
- }
-
- url += `code=${encodeURIComponent(authCode)}`;
-
- if (this.oauth2State) {
- url += `&state=${encodeURIComponent(this.oauth2State)}`;
- }
-
- document.location = url;
- }
-
- _updateStep(step) {
- const props = {
- _step: step,
- _state: "step",
- };
-
- if (
- this._step &&
- (step.flow_id !== this._step.flow_id ||
- step.step_id !== this._step.step_id)
- ) {
- props._stepData = {};
- }
-
- this.setProperties(props);
- }
-
- _equals(a, b) {
- return a === b;
- }
-
- _computeSubmitCaption(stepType) {
- return stepType === "form" ? "Next" : "Start over";
- }
-
- _computeStepAbortedReason(localize, step) {
- return localize(
- `ui.panel.page-authorize.form.providers.${step.handler[0]}.abort.${
- step.reason
- }`
- );
- }
-
- _computeStepDescription(localize, step) {
- const args = [
- `ui.panel.page-authorize.form.providers.${step.handler[0]}.step.${
- step.step_id
- }.description`,
- ];
- const placeholders = step.description_placeholders || {};
- Object.keys(placeholders).forEach((key) => {
- args.push(key);
- args.push(placeholders[key]);
- });
- return localize(...args);
- }
-
- _computeLabelCallback(localize, step) {
- // Returns a callback for ha-form to calculate labels per schema object
- return (schema) =>
- localize(
- `ui.panel.page-authorize.form.providers.${step.handler[0]}.step.${
- step.step_id
- }.data.${schema.name}`
- );
- }
-
- _computeErrorCallback(localize, step) {
- // Returns a callback for ha-form to calculate error messages
- return (error) =>
- localize(
- `ui.panel.page-authorize.form.providers.${
- step.handler[0]
- }.error.${error}`
- );
- }
-
- async _handleSubmit(ev) {
- ev.preventDefault();
- if (this._step.type !== "form") {
- this._providerChanged(this.authProvider, null);
- return;
- }
- this._state = "loading";
- // To avoid a jumping UI.
- this.style.setProperty("min-height", `${this.offsetHeight}px`);
-
- const postData = Object.assign({}, this._stepData, {
- client_id: this.clientId,
- });
-
- try {
- const response = await fetch(`/auth/login_flow/${this._step.flow_id}`, {
- method: "POST",
- credentials: "same-origin",
- body: JSON.stringify(postData),
- });
-
- const newStep = await response.json();
-
- if (newStep.type === "create_entry") {
- this._redirect(newStep.result);
- return;
- }
- this._updateStep(newStep);
- } catch (err) {
- // eslint-disable-next-line
- console.error("Error submitting step", err);
- this._state = "error-loading";
- } finally {
- this.style.setProperty("min-height", "");
- }
- }
-}
-customElements.define("ha-auth-flow", HaAuthFlow);
diff --git a/src/auth/ha-auth-flow.ts b/src/auth/ha-auth-flow.ts
new file mode 100644
index 0000000000..72e488117e
--- /dev/null
+++ b/src/auth/ha-auth-flow.ts
@@ -0,0 +1,296 @@
+import { LitElement, html, property, PropertyValues } from "lit-element";
+import "@material/mwc-button";
+import "../components/ha-form";
+import "../components/ha-markdown";
+import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
+import { AuthProvider } from "../data/auth";
+import { ConfigFlowStep, ConfigFlowStepForm } from "../data/config_entries";
+
+type State = "loading" | "error" | "step";
+
+class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
+ @property() public authProvider?: AuthProvider;
+ @property() public clientId?: string;
+ @property() public redirectUri?: string;
+ @property() public oauth2State?: string;
+ @property() private _state: State = "loading";
+ @property() private _stepData: any = {};
+ @property() private _step?: ConfigFlowStep;
+ @property() private _errorMessage?: string;
+
+ protected render() {
+ return html`
+
+
+ `;
+ }
+
+ protected firstUpdated(changedProps: PropertyValues) {
+ super.firstUpdated(changedProps);
+
+ if (this.clientId == null || this.redirectUri == null) {
+ // tslint:disable-next-line: no-console
+ console.error(
+ "clientId and redirectUri must not be null",
+ this.clientId,
+ this.redirectUri
+ );
+ this._state = "error";
+ this._errorMessage = this._unknownError();
+ return;
+ }
+
+ this.addEventListener("keypress", (ev) => {
+ if (ev.keyCode === 13) {
+ this._handleSubmit(ev);
+ }
+ });
+ }
+
+ protected updated(changedProps: PropertyValues) {
+ super.updated(changedProps);
+ if (changedProps.has("authProvider")) {
+ this._providerChanged(this.authProvider);
+ }
+ }
+
+ private _renderForm() {
+ switch (this._state) {
+ case "step":
+ if (this._step == null) {
+ return html``;
+ }
+ return html`
+ ${this._renderStep(this._step)}
+
+ ${this._step.type === "form" ? "Next" : "Start over"}
+
+ `;
+ case "error":
+ return html`
+ Error: ${this._errorMessage}
+ `;
+ case "loading":
+ return html`
+ ${this.localize("ui.panel.page-authorize.form.working")}
+ `;
+ }
+ }
+
+ private _renderStep(step: ConfigFlowStep) {
+ switch (step.type) {
+ case "abort":
+ return html`
+ ${this.localize("ui.panel.page-authorize.abort_intro")}:
+
+ `;
+ case "form":
+ return html`
+ ${this._computeStepDescription(step)
+ ? html`
+
+ `
+ : html``}
+
+ `;
+ default:
+ return html``;
+ }
+ }
+
+ private async _providerChanged(newProvider?: AuthProvider) {
+ if (this._step && this._step.type === "form") {
+ fetch(`/auth/login_flow/${this._step.flow_id}`, {
+ method: "DELETE",
+ credentials: "same-origin",
+ }).catch((err) => {
+ // tslint:disable-next-line: no-console
+ console.error("Error delete obsoleted auth flow", err);
+ });
+ }
+
+ if (newProvider == null) {
+ // tslint:disable-next-line: no-console
+ console.error("No auth provider");
+ this._state = "error";
+ this._errorMessage = this._unknownError();
+ return;
+ }
+
+ try {
+ const response = await fetch("/auth/login_flow", {
+ method: "POST",
+ credentials: "same-origin",
+ body: JSON.stringify({
+ client_id: this.clientId,
+ handler: [newProvider.type, newProvider.id],
+ redirect_uri: this.redirectUri,
+ }),
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ // allow auth provider bypass the login form
+ if (data.type === "create_entry") {
+ this._redirect(data.result);
+ return;
+ }
+
+ this._updateStep(data);
+ } else {
+ this._state = "error";
+ this._errorMessage = data.message;
+ }
+ } catch (err) {
+ // tslint:disable-next-line: no-console
+ console.error("Error starting auth flow", err);
+ this._state = "error";
+ this._errorMessage = this._unknownError();
+ }
+ }
+
+ private _redirect(authCode: string) {
+ // OAuth 2: 3.1.2 we need to retain query component of a redirect URI
+ let url = this.redirectUri!!;
+ if (!url.includes("?")) {
+ url += "?";
+ } else if (!url.endsWith("&")) {
+ url += "&";
+ }
+
+ url += `code=${encodeURIComponent(authCode)}`;
+
+ if (this.oauth2State) {
+ url += `&state=${encodeURIComponent(this.oauth2State)}`;
+ }
+
+ document.location.assign(url);
+ }
+
+ private _updateStep(step: ConfigFlowStep) {
+ let stepData: any = null;
+ if (
+ this._step &&
+ (step.flow_id !== this._step.flow_id ||
+ (step.type === "form" &&
+ this._step.type === "form" &&
+ step.step_id !== this._step.step_id))
+ ) {
+ stepData = {};
+ }
+ this._step = step;
+ this._state = "step";
+ if (stepData != null) {
+ this._stepData = stepData;
+ }
+ }
+
+ private _computeStepDescription(step: ConfigFlowStepForm) {
+ const resourceKey = `ui.panel.page-authorize.form.providers.${
+ step.handler[0]
+ }.step.${step.step_id}.description`;
+ const args: string[] = [];
+ const placeholders = step.description_placeholders || {};
+ Object.keys(placeholders).forEach((key) => {
+ args.push(key);
+ args.push(placeholders[key]);
+ });
+ return this.localize(resourceKey, ...args);
+ }
+
+ private _computeLabelCallback(step: ConfigFlowStepForm) {
+ // Returns a callback for ha-form to calculate labels per schema object
+ return (schema) =>
+ this.localize(
+ `ui.panel.page-authorize.form.providers.${step.handler[0]}.step.${
+ step.step_id
+ }.data.${schema.name}`
+ );
+ }
+
+ private _computeErrorCallback(step: ConfigFlowStepForm) {
+ // Returns a callback for ha-form to calculate error messages
+ return (error) =>
+ this.localize(
+ `ui.panel.page-authorize.form.providers.${
+ step.handler[0]
+ }.error.${error}`
+ );
+ }
+
+ private _unknownError() {
+ return this.localize("ui.panel.page-authorize.form.unknown_error");
+ }
+
+ private async _handleSubmit(ev: Event) {
+ ev.preventDefault();
+ if (this._step == null) {
+ return;
+ }
+ if (this._step.type !== "form") {
+ this._providerChanged(this.authProvider);
+ return;
+ }
+ this._state = "loading";
+ // To avoid a jumping UI.
+ this.style.setProperty("min-height", `${this.offsetHeight}px`);
+
+ const postData = { ...this._stepData, client_id: this.clientId };
+
+ try {
+ const response = await fetch(`/auth/login_flow/${this._step.flow_id}`, {
+ method: "POST",
+ credentials: "same-origin",
+ body: JSON.stringify(postData),
+ });
+
+ const newStep = await response.json();
+
+ if (newStep.type === "create_entry") {
+ this._redirect(newStep.result);
+ return;
+ }
+ this._updateStep(newStep);
+ } catch (err) {
+ // tslint:disable-next-line: no-console
+ console.error("Error submitting step", err);
+ this._state = "error";
+ this._errorMessage = this._unknownError();
+ } finally {
+ this.style.setProperty("min-height", "");
+ }
+ }
+}
+customElements.define("ha-auth-flow", HaAuthFlow);
diff --git a/src/auth/ha-authorize.ts b/src/auth/ha-authorize.ts
index 4935fac9dd..0a7ce2a70a 100644
--- a/src/auth/ha-authorize.ts
+++ b/src/auth/ha-authorize.ts
@@ -108,7 +108,7 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
.resources="${this.resources}"
.clientId="${this.clientId}"
.authProviders="${inactiveProviders}"
- @pick="${this._handleAuthProviderPick}"
+ @pick-auth-provider="${this._handleAuthProviderPick}"
>
`
: ""}
diff --git a/src/auth/ha-pick-auth-provider.js b/src/auth/ha-pick-auth-provider.js
deleted file mode 100644
index d1c6fd00f8..0000000000
--- a/src/auth/ha-pick-auth-provider.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import "@polymer/paper-item/paper-item";
-import "@polymer/paper-item/paper-item-body";
-import { html } from "@polymer/polymer/lib/utils/html-tag";
-import { PolymerElement } from "@polymer/polymer/polymer-element";
-
-import { EventsMixin } from "../mixins/events-mixin";
-import { localizeLiteMixin } from "../mixins/localize-lite-mixin";
-import "../components/ha-icon-next";
-
-/*
- * @appliesMixin EventsMixin
- */
-class HaPickAuthProvider extends EventsMixin(
- localizeLiteMixin(PolymerElement)
-) {
- static get template() {
- return html`
-
- [[localize('ui.panel.page-authorize.pick_auth_provider')]]:
-
-
- [[item.name]]
-
-
-
- `;
- }
-
- static get properties() {
- return {
- _state: {
- type: String,
- value: "loading",
- },
- authProviders: Array,
- };
- }
-
- _handlePick(ev) {
- this.fire("pick", ev.model.item);
- }
-
- _equal(a, b) {
- return a === b;
- }
-}
-customElements.define("ha-pick-auth-provider", HaPickAuthProvider);
diff --git a/src/auth/ha-pick-auth-provider.ts b/src/auth/ha-pick-auth-provider.ts
new file mode 100644
index 0000000000..942fb5b4cc
--- /dev/null
+++ b/src/auth/ha-pick-auth-provider.ts
@@ -0,0 +1,44 @@
+import { LitElement, html, property } from "lit-element";
+import "@polymer/paper-item/paper-item";
+import "@polymer/paper-item/paper-item-body";
+import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
+import { fireEvent } from "../common/dom/fire_event";
+import "../components/ha-icon-next";
+import { AuthProvider } from "../data/auth";
+
+declare global {
+ interface HASSDomEvents {
+ "pick-auth-provider": AuthProvider;
+ }
+}
+
+class HaPickAuthProvider extends litLocalizeLiteMixin(LitElement) {
+ @property() public authProviders: AuthProvider[] = [];
+
+ protected render() {
+ return html`
+
+ ${this.localize("ui.panel.page-authorize.pick_auth_provider")}:
+ ${this.authProviders.map(
+ (provider) => html`
+
+ ${provider.name}
+
+
+ `
+ )}
+ `;
+ }
+
+ private _handlePick(ev) {
+ fireEvent(this, "pick-auth-provider", ev.currentTarget.auth_provider);
+ }
+}
+customElements.define("ha-pick-auth-provider", HaPickAuthProvider);
diff --git a/src/common/util/subscribe-one.ts b/src/common/util/subscribe-one.ts
new file mode 100644
index 0000000000..f7fee163a7
--- /dev/null
+++ b/src/common/util/subscribe-one.ts
@@ -0,0 +1,16 @@
+import { HomeAssistant } from "../../types";
+import { UnsubscribeFunc } from "home-assistant-js-websocket";
+
+export const subscribeOne = async (
+ hass: HomeAssistant,
+ subscribe: (
+ hass: HomeAssistant,
+ onChange: (items: T) => void
+ ) => UnsubscribeFunc
+) =>
+ new Promise((resolve) => {
+ const unsub = subscribe(hass, (items) => {
+ unsub();
+ resolve(items);
+ });
+ });
diff --git a/src/dialogs/more-info/controls/more-info-media_player.js b/src/dialogs/more-info/controls/more-info-media_player.js
index 6f3541d3fe..54d741fd22 100644
--- a/src/dialogs/more-info/controls/more-info-media_player.js
+++ b/src/dialogs/more-info/controls/more-info-media_player.js
@@ -111,6 +111,7 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
disabled$="[[playerObj.isMuted]]"
on-mousedown="handleVolumeDown"
on-touchstart="handleVolumeDown"
+ on-touchend="handleVolumeTouchEnd"
icon="hass:volume-medium"
>
@@ -357,6 +359,12 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
this.playerObj.volumeMute(!this.playerObj.isMuted);
}
+ handleVolumeTouchEnd(ev) {
+ /* when touch ends, we must prevent this from
+ * becoming a mousedown, up, click by emulation */
+ ev.preventDefault();
+ }
+
handleVolumeUp() {
const obj = this.$.volumeUp;
this.handleVolumeWorker("volume_up", obj, true);
diff --git a/src/onboarding/onboarding-create-user.ts b/src/onboarding/onboarding-create-user.ts
index 837bba1b7d..b5b06b43cb 100644
--- a/src/onboarding/onboarding-create-user.ts
+++ b/src/onboarding/onboarding-create-user.ts
@@ -130,7 +130,7 @@ class OnboardingCreateUser extends LitElement {
);
this.addEventListener("keypress", (ev) => {
if (ev.keyCode === 13) {
- this._submitForm();
+ this._submitForm(ev);
}
});
}
@@ -152,7 +152,8 @@ class OnboardingCreateUser extends LitElement {
}
}
- private async _submitForm(): Promise {
+ private async _submitForm(ev): Promise {
+ ev.preventDefault();
if (!this._name || !this._username || !this._password) {
this._errorMsg = "required_fields";
return;
diff --git a/src/panels/config/users/ha-dialog-add-user.js b/src/panels/config/users/ha-dialog-add-user.js
index 08c0319919..dd710bb8bb 100644
--- a/src/panels/config/users/ha-dialog-add-user.js
+++ b/src/panels/config/users/ha-dialog-add-user.js
@@ -108,7 +108,7 @@ class HaDialogAddUser extends LocalizeMixin(PolymerElement) {
super.ready();
this.addEventListener("keypress", (ev) => {
if (ev.keyCode === 13) {
- this._createUser();
+ this._createUser(ev);
}
});
}
@@ -131,7 +131,8 @@ class HaDialogAddUser extends LocalizeMixin(PolymerElement) {
}
}
- async _createUser() {
+ async _createUser(ev) {
+ ev.preventDefault();
if (!this._name || !this._username || !this._password) return;
this._loading = true;
diff --git a/src/panels/lovelace/common/generate-lovelace-config.ts b/src/panels/lovelace/common/generate-lovelace-config.ts
index c0b115e523..666f9fe3ff 100644
--- a/src/panels/lovelace/common/generate-lovelace-config.ts
+++ b/src/panels/lovelace/common/generate-lovelace-config.ts
@@ -17,6 +17,19 @@ import computeDomain from "../../../common/entity/compute_domain";
import { EntityRowConfig, WeblinkConfig } from "../entity-rows/types";
import { LocalizeFunc } from "../../../common/translations/localize";
import { EntitiesCardConfig } from "../cards/types";
+import {
+ subscribeAreaRegistry,
+ AreaRegistryEntry,
+} from "../../../data/area_registry";
+import { subscribeOne } from "../../../common/util/subscribe-one";
+import {
+ subscribeDeviceRegistry,
+ DeviceRegistryEntry,
+} from "../../../data/device_registry";
+import {
+ subscribeEntityRegistry,
+ EntityRegistryEntry,
+} from "../../../data/entity_registry";
const DEFAULT_VIEW_ENTITY_ID = "group.default_view";
const DOMAINS_BADGES = [
@@ -34,6 +47,55 @@ const HIDE_DOMAIN = new Set([
"geo_location",
]);
+interface Registries {
+ areas: AreaRegistryEntry[];
+ devices: DeviceRegistryEntry[];
+ entities: EntityRegistryEntry[];
+}
+
+let subscribedRegistries = false;
+
+interface SplittedByAreas {
+ areasWithEntities: Array<[AreaRegistryEntry, HassEntity[]]>;
+ otherEntities: HassEntities;
+}
+
+const splitByAreas = (
+ registries: Registries,
+ entities: HassEntities
+): SplittedByAreas => {
+ const allEntities = { ...entities };
+ const areasWithEntities: SplittedByAreas["areasWithEntities"] = [];
+
+ for (const area of registries.areas) {
+ const areaEntities: HassEntity[] = [];
+ const areaDevices = new Set(
+ registries.devices
+ .filter((device) => device.area_id === area.area_id)
+ .map((device) => device.id)
+ );
+ for (const entity of registries.entities) {
+ if (
+ areaDevices.has(
+ // @ts-ignore
+ entity.device_id
+ ) &&
+ entity.entity_id in allEntities
+ ) {
+ areaEntities.push(allEntities[entity.entity_id]);
+ delete allEntities[entity.entity_id];
+ }
+ }
+ if (areaEntities.length > 0) {
+ areasWithEntities.push([area, areaEntities]);
+ }
+ }
+ return {
+ areasWithEntities,
+ otherEntities: allEntities,
+ };
+};
+
const computeCards = (
states: Array<[string, HassEntity]>,
entityCardOptions: Partial
@@ -124,6 +186,51 @@ const computeDefaultViewStates = (hass: HomeAssistant): HassEntities => {
return states;
};
+const generateDefaultViewConfig = (
+ hass: HomeAssistant,
+ registries: Registries
+): LovelaceViewConfig => {
+ const states = computeDefaultViewStates(hass);
+ const path = "default_view";
+ const title = "Home";
+ const icon = undefined;
+
+ // In the case of a default view, we want to use the group order attribute
+ const groupOrders = {};
+ Object.keys(states).forEach((entityId) => {
+ const stateObj = states[entityId];
+ if (stateObj.attributes.order) {
+ groupOrders[entityId] = stateObj.attributes.order;
+ }
+ });
+
+ const splittedByAreas = splitByAreas(registries, states);
+
+ const config = generateViewConfig(
+ hass.localize,
+ path,
+ title,
+ icon,
+ splittedByAreas.otherEntities,
+ groupOrders
+ );
+
+ const areaCards: LovelaceCardConfig[] = [];
+
+ splittedByAreas.areasWithEntities.forEach(([area, entities]) => {
+ areaCards.push(
+ ...computeCards(entities.map((entity) => [entity.entity_id, entity]), {
+ title: area.name,
+ show_header_toggle: true,
+ })
+ );
+ });
+
+ config.cards!.unshift(...areaCards);
+
+ return config;
+};
+
const generateViewConfig = (
localize: LocalizeFunc,
path: string,
@@ -208,10 +315,10 @@ const generateViewConfig = (
return view;
};
-export const generateLovelaceConfig = (
+export const generateLovelaceConfig = async (
hass: HomeAssistant,
localize: LocalizeFunc
-): LovelaceConfig => {
+): Promise => {
const viewEntities = extractViews(hass.states);
const views = viewEntities.map((viewEntity: GroupEntity) => {
@@ -241,27 +348,23 @@ export const generateLovelaceConfig = (
viewEntities.length === 0 ||
viewEntities[0].entity_id !== DEFAULT_VIEW_ENTITY_ID
) {
- const states = computeDefaultViewStates(hass);
+ // We want to keep the registry subscriptions alive after generating the UI
+ // 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);
+ }
- // In the case of a default view, we want to use the group order attribute
- const groupOrders = {};
- Object.keys(states).forEach((entityId) => {
- const stateObj = states[entityId];
- if (stateObj.attributes.order) {
- groupOrders[entityId] = stateObj.attributes.order;
- }
- });
+ const [areas, devices, entities] = await Promise.all([
+ subscribeOne(hass, subscribeAreaRegistry),
+ subscribeOne(hass, subscribeDeviceRegistry),
+ subscribeOne(hass, subscribeEntityRegistry),
+ ]);
+ const registries = { areas, devices, entities };
- views.unshift(
- generateViewConfig(
- localize,
- "default_view",
- "Home",
- undefined,
- states,
- groupOrders
- )
- );
+ views.unshift(generateDefaultViewConfig(hass, registries));
// Add map of geo locations to default view if loaded
if (hass.config.components.includes("geo_location")) {
diff --git a/src/panels/lovelace/entity-rows/hui-input-select-entity-row.ts b/src/panels/lovelace/entity-rows/hui-input-select-entity-row.ts
index ef4ac3f22c..cda3d1d66e 100644
--- a/src/panels/lovelace/entity-rows/hui-input-select-entity-row.ts
+++ b/src/panels/lovelace/entity-rows/hui-input-select-entity-row.ts
@@ -123,18 +123,14 @@ class HuiInputSelectEntityRow extends LitElement implements EntityRow {
private _selectedChanged(ev): void {
const stateObj = this.hass!.states[this._config!.entity];
- const option = ev.detail.item.innerText;
+ const option = ev.target.selectedItem.innerText.trim();
if (option === stateObj.state) {
return;
}
forwardHaptic("light");
- setInputSelectOption(
- this.hass!,
- stateObj.entity_id,
- ev.target.selectedItem.innerText
- );
+ setInputSelectOption(this.hass!, stateObj.entity_id, option);
}
}
diff --git a/src/panels/lovelace/ha-panel-lovelace.ts b/src/panels/lovelace/ha-panel-lovelace.ts
index b3b4168d11..98a97411c8 100644
--- a/src/panels/lovelace/ha-panel-lovelace.ts
+++ b/src/panels/lovelace/ha-panel-lovelace.ts
@@ -171,7 +171,7 @@ class LovelacePanel extends LitElement {
this._errorMsg = err.message;
return;
}
- conf = generateLovelaceConfig(this.hass!, this.hass!.localize);
+ conf = await generateLovelaceConfig(this.hass!, this.hass!.localize);
confMode = "generated";
}
diff --git a/src/state-summary/state-card-input_select.ts b/src/state-summary/state-card-input_select.ts
index 19befdfb29..5566c27a6c 100644
--- a/src/state-summary/state-card-input_select.ts
+++ b/src/state-summary/state-card-input_select.ts
@@ -58,7 +58,7 @@ class StateCardInputSelect extends LitElement {
private async _selectedOptionChanged(
ev: PolymerIronSelectEvent
) {
- const option = ev.detail.item.innerText;
+ const option = ev.detail.item.innerText.trim();
if (option === this.stateObj.state) {
return;
}
diff --git a/translations/cs.json b/translations/cs.json
index b6fb6f87e1..f2a7acc04e 100644
--- a/translations/cs.json
+++ b/translations/cs.json
@@ -855,6 +855,11 @@
"required_fields": "Vyplňte všechna povinná pole",
"password_not_match": "Hesla se neshodují"
}
+ },
+ "integration": {
+ "intro": "Zařízení a služby jsou v programu Home Assistant reprezentovány jako integrace. Můžete je nyní nastavit nebo provést později z konfigurační obrazovky.",
+ "more_integrations": "Více",
+ "finish": "Dokončit"
}
},
"lovelace": {
@@ -868,6 +873,14 @@
"title": "Vítejte doma",
"no_devices": "Tato stránka umožňuje ovládat vaše zařízení; zdá se však, že ještě nemáte žádné zařízení nastavené. Nejprve tedy přejděte na stránku integrace.",
"go_to_integrations_page": "Přejděte na stránku integrace."
+ },
+ "picture-elements": {
+ "hold": "Podržte:",
+ "tap": "Klepněte:",
+ "navigate_to": "Přejděte na {location}",
+ "toggle": "Přepnout {name}",
+ "call_service": "Zavolat službu {name}",
+ "more_info": "Zobrazit více informací: {name}"
}
},
"editor": {
@@ -925,7 +938,8 @@
},
"sidebar": {
"log_out": "Odhlásit se",
- "developer_tools": "Vývojářské nástroje"
+ "developer_tools": "Vývojářské nástroje",
+ "external_app_configuration": "Konfigurace aplikace"
},
"common": {
"loading": "Načítání",
diff --git a/translations/ko.json b/translations/ko.json
index 49c6e1e40e..190f1bcbc7 100644
--- a/translations/ko.json
+++ b/translations/ko.json
@@ -855,6 +855,11 @@
"required_fields": "필수 입력란을 모두 채워주세요",
"password_not_match": "비밀번호가 일치하지 않습니다"
}
+ },
+ "integration": {
+ "intro": "기기 및 서비스는 Home Assistant 에서 통합 구성요소로 표시됩니다. 지금 구성하거나 설정 화면에서 나중에 구성할 수 있습니다.",
+ "more_integrations": "더보기",
+ "finish": "완료"
}
},
"lovelace": {
diff --git a/translations/lb.json b/translations/lb.json
index a560d2c504..0dbf1b7053 100644
--- a/translations/lb.json
+++ b/translations/lb.json
@@ -855,6 +855,11 @@
"required_fields": "Fëllt all néideg Felder aus",
"password_not_match": "Passwierder stëmmen net iwwereneen"
}
+ },
+ "integration": {
+ "intro": "Apparaten a Servicë ginn am Home Assistant als Integratioune representéiert. Dir kënnt si elo astellen, oder méi spéit vun der Konfiguratioun's Säit aus.",
+ "more_integrations": "Méi",
+ "finish": "Ofschléissen"
}
},
"lovelace": {
diff --git a/translations/nl.json b/translations/nl.json
index ea3a8b4063..3f880bd706 100644
--- a/translations/nl.json
+++ b/translations/nl.json
@@ -855,6 +855,11 @@
"required_fields": "Vul alle verplichte velden in",
"password_not_match": "Wachtwoorden komen niet overeen"
}
+ },
+ "integration": {
+ "intro": "Apparaten en services worden in Home Assistant weergegeven als integraties. U kunt ze nu instellen of later via het configuratiescherm.",
+ "more_integrations": "Meer",
+ "finish": "Voltooien"
}
},
"lovelace": {
@@ -870,8 +875,11 @@
"go_to_integrations_page": "Ga naar de integraties pagina."
},
"picture-elements": {
+ "hold": "Vasthouden:",
+ "tap": "Tik:",
"navigate_to": "Navigeer naar {location}",
"toggle": "Omschakelen {name}",
+ "call_service": "Roep service {name} aan",
"more_info": "Meer informatie weergeven: {name}"
}
},
@@ -930,7 +938,8 @@
},
"sidebar": {
"log_out": "Uitloggen",
- "developer_tools": "Ontwikkelaarstools"
+ "developer_tools": "Ontwikkelaarstools",
+ "external_app_configuration": "App configuratie"
},
"common": {
"loading": "Bezig met laden",
diff --git a/translations/ru.json b/translations/ru.json
index 7ce553ec9e..3584cc6437 100644
--- a/translations/ru.json
+++ b/translations/ru.json
@@ -575,7 +575,7 @@
"name": "Имя",
"username": "Логин",
"password": "Пароль",
- "create": "Создать"
+ "create": "Добавить"
}
},
"cloud": {
@@ -589,7 +589,7 @@
"description": "Управляйте подключенными устройствами и службами",
"discovered": "Обнаружено",
"configured": "Настроено",
- "new": "Создать новую интеграцию",
+ "new": "Интеграции",
"configure": "Настроить",
"none": "Пока ничего не настроено",
"config_entry": {
@@ -745,8 +745,8 @@
},
"page-authorize": {
"initializing": "Инициализация",
- "authorizing_client": "Вы собираетесь предоставить доступ {clientId} к вашему Home Assistant.",
- "logging_in_with": "Вход с помощью **{authProviderName}**.",
+ "authorizing_client": "Получение доступа к Home Assistant через {clientId}.",
+ "logging_in_with": "Провайдер аутентификации: **{authProviderName}**.",
"pick_auth_provider": "Или войти с помощью",
"abort_intro": "Вход прерван",
"form": {
@@ -855,6 +855,11 @@
"required_fields": "Заполните все обязательные поля",
"password_not_match": "Пароли не совпадают"
}
+ },
+ "integration": {
+ "intro": "Устройства и сервисы представлены в Home Assistant как интеграции. Вы можете добавить их сейчас или сделать это позже в разделе настроек.",
+ "more_integrations": "Все интеграции",
+ "finish": "Готово"
}
},
"lovelace": {
diff --git a/translations/zh-Hant.json b/translations/zh-Hant.json
index 60b4b00cdf..4d3761ee47 100644
--- a/translations/zh-Hant.json
+++ b/translations/zh-Hant.json
@@ -855,6 +855,11 @@
"required_fields": "填寫所有所需欄位",
"password_not_match": "密碼不相符"
}
+ },
+ "integration": {
+ "intro": "將會於 Home Assistant 整合中呈現的裝置與服務。可以現在進行設定,或者稍後於設定選單中進行。",
+ "more_integrations": "更多",
+ "finish": "完成"
}
},
"lovelace": {
diff --git a/yarn.lock b/yarn.lock
index 593634e982..42d45f0325 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7237,10 +7237,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.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-4.1.1.tgz#b85152c223a20bfe8827b817b927fd97cc021157"
- integrity sha512-hNk8bj9JObd3NpgQ1+KtQCbSoz/TWockC8T/L8KvsPrDtkl1oQddajirumaMDgrJg/su4QsxFNUcDPGJyJ05UA==
+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==
homedir-polyfill@^1.0.0, homedir-polyfill@^1.0.1:
version "1.0.3"