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` + +
+ ${this._renderForm()} +
+ `; + } + + 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')]]:

- - `; - } - - 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"