Alow setting up integrations during onboarding (#3163)

* Allow setting up integrations during onboarding

* Fix compress static

* Don't compress static files in CI

* Remove unused file

* Fix static compress disabled in CI build

* Work with new integration step

* Import fix

* Lint

* Upgrade HAWS to 4.1.1
This commit is contained in:
Paulus Schoutsen 2019-05-07 22:27:10 -07:00 committed by GitHub
parent 8c904fb012
commit 82e8ca2754
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 513 additions and 47 deletions

View File

@ -37,7 +37,11 @@ gulp.task(
"clean", "clean",
gulp.parallel("gen-icons", "build-translations"), gulp.parallel("gen-icons", "build-translations"),
"copy-static", "copy-static",
gulp.parallel("webpack-prod-app", "compress-static"), gulp.parallel(
"webpack-prod-app",
// Do not compress static files in CI, it's SLOW.
...(process.env.CI === "true" ? [] : ["compress-static"])
),
gulp.parallel( gulp.parallel(
"gen-pages-prod", "gen-pages-prod",
"gen-index-html-prod", "gen-index-html-prod",

View File

@ -95,7 +95,7 @@ gulp.task("copy-static", (done) => {
done(); done();
}); });
gulp.task("compress-static", () => compressStatic(paths.root)); gulp.task("compress-static", () => compressStatic(paths.static));
gulp.task("copy-static-demo", (done) => { gulp.task("copy-static-demo", (done) => {
// Copy app static files // Copy app static files

View File

@ -75,7 +75,7 @@
"es6-object-assign": "^1.1.0", "es6-object-assign": "^1.1.0",
"fecha": "^3.0.2", "fecha": "^3.0.2",
"hls.js": "^0.12.4", "hls.js": "^0.12.4",
"home-assistant-js-websocket": "^3.4.0", "home-assistant-js-websocket": "^4.1.1",
"intl-messageformat": "^2.2.0", "intl-messageformat": "^2.2.0",
"jquery": "^3.3.1", "jquery": "^3.3.1",
"js-yaml": "^3.13.0", "js-yaml": "^3.13.0",

View File

@ -14,6 +14,8 @@ export interface SignedPath {
path: string; path: string;
} }
export const hassUrl = `${location.protocol}//${location.host}`;
export const getSignedPath = ( export const getSignedPath = (
hass: HomeAssistant, hass: HomeAssistant,
path: string path: string

View File

@ -1,6 +1,17 @@
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { createCollection } from "home-assistant-js-websocket"; import { createCollection } from "home-assistant-js-websocket";
import { debounce } from "../common/util/debounce"; import { debounce } from "../common/util/debounce";
import { LocalizeFunc } from "../common/translations/localize";
export interface ConfigEntry {
entry_id: string;
domain: string;
title: string;
source: string;
state: string;
connection_class: string;
supports_options: boolean;
}
export interface FieldSchema { export interface FieldSchema {
name: string; name: string;
@ -11,7 +22,10 @@ export interface FieldSchema {
export interface ConfigFlowProgress { export interface ConfigFlowProgress {
flow_id: string; flow_id: string;
handler: string; handler: string;
context: { [key: string]: any }; context: {
title_placeholders: { [key: string]: string };
[key: string]: any;
};
} }
export interface ConfigFlowStepForm { export interface ConfigFlowStepForm {
@ -106,3 +120,23 @@ export const subscribeConfigFlowInProgress = (
hass.connection, hass.connection,
onChange onChange
); );
export const getConfigEntries = (hass: HomeAssistant) =>
hass.callApi<ConfigEntry[]>("GET", "config/config_entries/entry");
export const localizeConfigFlowTitle = (
localize: LocalizeFunc,
flow: ConfigFlowProgress
) => {
const placeholders = flow.context.title_placeholders || {};
const placeholderKeys = Object.keys(placeholders);
if (placeholderKeys.length === 0) {
return localize(`component.${flow.handler}.config.title`);
}
const args: string[] = [];
placeholderKeys.forEach((key) => {
args.push(key);
args.push(placeholders[key]);
});
return localize(`component.${flow.handler}.config.flow_title`, ...args);
};

View File

@ -1,12 +1,17 @@
import { handleFetchPromise } from "../util/hass-call-api"; import { handleFetchPromise } from "../util/hass-call-api";
import { HomeAssistant } from "../types";
export interface OnboardingUserStepResponse { export interface OnboardingUserStepResponse {
auth_code: string; auth_code: string;
} }
export interface OnboardingIntegrationStepResponse {
auth_code: string;
}
export interface OnboardingResponses { export interface OnboardingResponses {
user: OnboardingUserStepResponse; user: OnboardingUserStepResponse;
bla: number; integration: OnboardingIntegrationStepResponse;
} }
export type ValidOnboardingStep = keyof OnboardingResponses; export type ValidOnboardingStep = keyof OnboardingResponses;
@ -24,6 +29,7 @@ export const onboardUserStep = (params: {
name: string; name: string;
username: string; username: string;
password: string; password: string;
language: string;
}) => }) =>
handleFetchPromise<OnboardingUserStepResponse>( handleFetchPromise<OnboardingUserStepResponse>(
fetch("/api/onboarding/users", { fetch("/api/onboarding/users", {
@ -32,3 +38,13 @@ export const onboardUserStep = (params: {
body: JSON.stringify(params), body: JSON.stringify(params),
}) })
); );
export const onboardIntegrationStep = (
hass: HomeAssistant,
params: { client_id: string }
) =>
hass.callApi<OnboardingIntegrationStepResponse>(
"POST",
"onboarding/integration",
params
);

View File

@ -169,6 +169,18 @@ class StepFlowCreateEntry extends LitElement {
.buttons > *:last-child { .buttons > *:last-child {
margin-left: auto; margin-left: auto;
} }
paper-dropdown-menu-light {
cursor: pointer;
}
paper-item {
cursor: pointer;
white-space: nowrap;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
.device {
width: auto;
}
}
`, `,
]; ];
} }

View File

@ -14,6 +14,7 @@ import { subscribePanels } from "../data/ws-panels";
import { subscribeThemes } from "../data/ws-themes"; import { subscribeThemes } from "../data/ws-themes";
import { subscribeUser } from "../data/ws-user"; import { subscribeUser } from "../data/ws-user";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { hassUrl } from "../data/auth";
declare global { declare global {
interface Window { interface Window {
@ -21,7 +22,6 @@ declare global {
} }
} }
const hassUrl = `${location.protocol}//${location.host}`;
const isExternal = location.search.includes("external_auth=1"); const isExternal = location.search.includes("external_auth=1");
const authProm = isExternal const authProm = isExternal

View File

@ -44,7 +44,7 @@
Home Assistant Home Assistant
</div> </div>
<ha-onboarding>Initializing</ha-onboarding> <ha-onboarding></ha-onboarding>
</div> </div>
<%= renderTemplate('_js_base') %> <%= renderTemplate('_js_base') %>

View File

@ -31,10 +31,10 @@ export const localizeLiteBaseMixin = (superClass) =>
return; return;
} }
this._updateResources(); this._downloadResources();
} }
private async _updateResources() { private async _downloadResources() {
const { language, data } = await getTranslation( const { language, data } = await getTranslation(
this.translationFragment, this.translationFragment,
this.language this.language

View File

@ -1,22 +1,29 @@
import { import {
LitElement,
html, html,
PropertyValues, PropertyValues,
customElement, customElement,
TemplateResult, TemplateResult,
property, property,
} from "lit-element"; } from "lit-element";
import { genClientId } from "home-assistant-js-websocket"; import {
getAuth,
createConnection,
genClientId,
Auth,
} from "home-assistant-js-websocket";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin"; import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import { import {
OnboardingStep, OnboardingStep,
ValidOnboardingStep, ValidOnboardingStep,
OnboardingResponses, OnboardingResponses,
fetchOnboardingOverview,
} from "../data/onboarding"; } from "../data/onboarding";
import { registerServiceWorker } from "../util/register-service-worker"; import { registerServiceWorker } from "../util/register-service-worker";
import { HASSDomEvent } from "../common/dom/fire_event"; import { HASSDomEvent } from "../common/dom/fire_event";
import "./onboarding-create-user"; import "./onboarding-create-user";
import "./onboarding-loading"; import "./onboarding-loading";
import { hassUrl } from "../data/auth";
import { HassElement } from "../state/hass-element";
interface OnboardingEvent<T extends ValidOnboardingStep> { interface OnboardingEvent<T extends ValidOnboardingStep> {
type: T; type: T;
@ -34,43 +41,55 @@ declare global {
} }
@customElement("ha-onboarding") @customElement("ha-onboarding")
class HaOnboarding extends litLocalizeLiteMixin(LitElement) { class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
public translationFragment = "page-onboarding"; public translationFragment = "page-onboarding";
@property() private _loading = false;
@property() private _steps?: OnboardingStep[]; @property() private _steps?: OnboardingStep[];
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
if (!this._steps) { const step = this._curStep()!;
if (this._loading || !step) {
return html` return html`
<onboarding-loading></onboarding-loading> <onboarding-loading></onboarding-loading>
`; `;
} } else if (step.step === "user") {
const step = this._steps.find((stp) => !stp.done)!;
if (step.step === "user") {
return html` return html`
<onboarding-create-user <onboarding-create-user
.localize=${this.localize} .localize=${this.localize}
.language=${this.language}
></onboarding-create-user> ></onboarding-create-user>
`; `;
} else if (step.step === "integration") {
return html`
<onboarding-integrations
.hass=${this.hass}
.onboardingLocalize=${this.localize}
></onboarding-integrations>
`;
} }
} }
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
this._fetchOnboardingSteps(); this._fetchOnboardingSteps();
import("./onboarding-integrations");
registerServiceWorker(false); registerServiceWorker(false);
this.addEventListener("onboarding-step", (ev) => this._handleStepDone(ev)); this.addEventListener("onboarding-step", (ev) => this._handleStepDone(ev));
} }
private _curStep() {
return this._steps ? this._steps.find((stp) => !stp.done) : undefined;
}
private async _fetchOnboardingSteps() { private async _fetchOnboardingSteps() {
try { try {
const response = await window.stepsPromise; const response = await (window.stepsPromise || fetchOnboardingOverview());
if (response.status === 404) { if (response.status === 404) {
// We don't load the component when onboarding is done // We don't load the component when onboarding is done
document.location.href = "/"; document.location.assign("/");
return; return;
} }
@ -78,7 +97,16 @@ class HaOnboarding extends litLocalizeLiteMixin(LitElement) {
if (steps.every((step) => step.done)) { if (steps.every((step) => step.done)) {
// Onboarding is done! // Onboarding is done!
document.location.href = "/"; document.location.assign("/");
return;
}
if (steps[0].done) {
// First step is already done, so we need to get auth somewhere else.
const auth = await getAuth({
hassUrl,
});
await this._connectHass(auth);
} }
this._steps = steps; this._steps = steps;
@ -91,20 +119,52 @@ class HaOnboarding extends litLocalizeLiteMixin(LitElement) {
ev: HASSDomEvent<OnboardingEvent<ValidOnboardingStep>> ev: HASSDomEvent<OnboardingEvent<ValidOnboardingStep>>
) { ) {
const stepResult = ev.detail; const stepResult = ev.detail;
this._steps = this._steps!.map((step) =>
step.step === stepResult.type ? { ...step, done: true } : step
);
if (stepResult.type === "user") { if (stepResult.type === "user") {
const result = stepResult.result as OnboardingResponses["user"]; const result = stepResult.result as OnboardingResponses["user"];
this._loading = true;
try {
const auth = await getAuth({
hassUrl,
authCode: result.auth_code,
});
await this._connectHass(auth);
} catch (err) {
alert("Ah snap, something went wrong!");
location.reload();
} finally {
this._loading = false;
}
} else if (stepResult.type === "integration") {
const result = stepResult.result as OnboardingResponses["integration"];
this._loading = true;
// Revoke current auth token.
await this.hass!.auth.revoke();
const state = btoa( const state = btoa(
JSON.stringify({ JSON.stringify({
hassUrl: `${location.protocol}//${location.host}`, hassUrl: `${location.protocol}//${location.host}`,
clientId: genClientId(), clientId: genClientId(),
}) })
); );
document.location.href = `/?auth_callback=1&code=${encodeURIComponent( document.location.assign(
result.auth_code `/?auth_callback=1&code=${encodeURIComponent(
)}&state=${state}`; result.auth_code
)}&state=${state}`
);
} }
} }
private async _connectHass(auth: Auth) {
const conn = await createConnection({ auth });
this.initializeHass(auth, conn);
// Load config strings for integrations
(this as any)._loadFragmentTranslations(this.hass!.language, "config");
}
} }
declare global { declare global {

View File

@ -0,0 +1,86 @@
import {
LitElement,
TemplateResult,
html,
customElement,
property,
CSSResult,
css,
} from "lit-element";
import "../components/ha-icon";
@customElement("integration-badge")
class IntegrationBadge extends LitElement {
@property() public icon!: string;
@property() public title!: string;
@property() public badgeIcon?: string;
@property({ type: Boolean, reflect: true }) public clickable = false;
protected render(): TemplateResult | void {
return html`
<div class="icon">
<iron-icon .icon=${this.icon}></iron-icon>
${this.badgeIcon
? html`
<ha-icon class="badge" .icon=${this.badgeIcon}></ha-icon>
`
: ""}
</div>
<div class="title">${this.title}</div>
`;
}
static get styles(): CSSResult {
return css`
:host {
display: inline-flex;
flex-direction: column;
text-align: center;
color: var(--primary-text-color);
}
:host([clickable]) {
color: var(--primary-text-color);
}
.icon {
position: relative;
margin: 0 auto 8px;
height: 40px;
width: 40px;
border-radius: 50%;
border: 1px solid var(--secondary-text-color);
display: flex;
align-items: center;
justify-content: center;
}
:host([clickable]) .icon {
border-color: var(--primary-color);
border-width: 2px;
}
.badge {
position: absolute;
color: var(--primary-color);
bottom: -5px;
right: -5px;
background-color: white;
border-radius: 50%;
width: 18px;
display: block;
height: 18px;
}
.title {
min-height: 2.3em;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"integration-badge": IntegrationBadge;
}
}

View File

@ -19,6 +19,7 @@ import { fireEvent } from "../common/dom/fire_event";
@customElement("onboarding-create-user") @customElement("onboarding-create-user")
class OnboardingCreateUser extends LitElement { class OnboardingCreateUser extends LitElement {
@property() public localize!: LocalizeFunc; @property() public localize!: LocalizeFunc;
@property() public language!: string;
@property() private _name = ""; @property() private _name = "";
@property() private _username = ""; @property() private _username = "";
@ -173,6 +174,7 @@ class OnboardingCreateUser extends LitElement {
name: this._name, name: this._name,
username: this._username, username: this._username,
password: this._password, password: this._password,
language: this.language,
}); });
fireEvent(this, "onboarding-step", { fireEvent(this, "onboarding-step", {

View File

@ -0,0 +1,196 @@
import {
LitElement,
TemplateResult,
html,
customElement,
PropertyValues,
property,
CSSResult,
css,
} from "lit-element";
import "@material/mwc-button/mwc-button";
import {
loadConfigFlowDialog,
showConfigFlowDialog,
} from "../dialogs/config-flow/show-dialog-config-flow";
import { HomeAssistant } from "../types";
import {
getConfigFlowsInProgress,
getConfigEntries,
ConfigEntry,
ConfigFlowProgress,
localizeConfigFlowTitle,
} from "../data/config_entries";
import { compare } from "../common/string/compare";
import "./integration-badge";
import { LocalizeFunc } from "../common/translations/localize";
import { debounce } from "../common/util/debounce";
import { fireEvent } from "../common/dom/fire_event";
import { onboardIntegrationStep } from "../data/onboarding";
import { genClientId } from "home-assistant-js-websocket";
@customElement("onboarding-integrations")
class OnboardingIntegrations extends LitElement {
@property() public hass!: HomeAssistant;
@property() public onboardingLocalize!: LocalizeFunc;
@property() private _entries?: ConfigEntry[];
@property() private _discovered?: ConfigFlowProgress[];
private _unsubEvents?: () => void;
public connectedCallback() {
super.connectedCallback();
this.hass.connection
.subscribeEvents(
debounce(() => this._loadData(), 500),
"config_entry_discovered"
)
.then((unsub) => {
this._unsubEvents = unsub;
});
}
public disconnectedCallback() {
super.disconnectedCallback();
if (this._unsubEvents) {
this._unsubEvents();
}
}
protected render(): TemplateResult | void {
if (!this._entries || !this._discovered) {
return html``;
}
// Render discovered and existing entries together sorted by localized title.
const entries: Array<[string, TemplateResult]> = this._entries.map(
(entry) => {
const title = this.hass.localize(
`component.${entry.domain}.config.title`
);
return [
title,
html`
<integration-badge
.title=${title}
icon="hass:check"
></integration-badge>
`,
];
}
);
const discovered: Array<[string, TemplateResult]> = this._discovered.map(
(flow) => {
const title = localizeConfigFlowTitle(this.hass.localize, flow);
return [
title,
html`
<button .flowId=${flow.flow_id} @click=${this._continueFlow}>
<integration-badge
clickable
.title=${title}
icon="hass:plus"
></integration-badge>
</button>
`,
];
}
);
const content = [...entries, ...discovered]
.sort((a, b) => compare(a[0], b[0]))
.map((item) => item[1]);
return html`
<p>
${this.onboardingLocalize("ui.panel.page-onboarding.integration.intro")}
</p>
<div class="badges">
${content}
<button @click=${this._createFlow}>
<integration-badge
clickable
title=${this.onboardingLocalize(
"ui.panel.page-onboarding.integration.more_integrations"
)}
icon="hass:dots-horizontal"
></integration-badge>
</button>
</div>
<div class="footer">
<mwc-button @click=${this._finish}>
${this.onboardingLocalize(
"ui.panel.page-onboarding.integration.finish"
)}
</mwc-button>
</div>
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
loadConfigFlowDialog();
this._loadData();
/* polyfill for paper-dropdown */
import(/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min");
}
private _createFlow() {
showConfigFlowDialog(this, {
dialogClosedCallback: () => this._loadData(),
});
}
private _continueFlow(ev) {
showConfigFlowDialog(this, {
continueFlowId: ev.currentTarget.flowId,
dialogClosedCallback: () => this._loadData(),
});
}
private async _loadData() {
const [discovered, entries] = await Promise.all([
getConfigFlowsInProgress(this.hass!),
getConfigEntries(this.hass!),
]);
this._discovered = discovered;
this._entries = entries;
}
private async _finish() {
const result = await onboardIntegrationStep(this.hass, {
client_id: genClientId(),
});
fireEvent(this, "onboarding-step", {
type: "integration",
result,
});
}
static get styles(): CSSResult {
return css`
.badges {
margin-top: 24px;
}
.badges > * {
width: 24%;
min-width: 90px;
margin-bottom: 24px;
}
button {
display: inline-block;
cursor: pointer;
padding: 0;
border: 0;
background: 0;
font: inherit;
}
.footer {
text-align: right;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"onboarding-integrations": OnboardingIntegrations;
}
}

View File

@ -1,10 +1,64 @@
import { LitElement, TemplateResult, html, customElement } from "lit-element"; import {
LitElement,
TemplateResult,
html,
customElement,
CSSResult,
css,
} from "lit-element";
@customElement("onboarding-loading") @customElement("onboarding-loading")
class OnboardingLoading extends LitElement { class OnboardingLoading extends LitElement {
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
return html` return html`
Loading <div class="loader"></div>
`;
}
static get styles(): CSSResult {
return css`
/* MIT License (MIT). Copyright (c) 2014 Luke Haas */
.loader,
.loader:after {
border-radius: 50%;
width: 40px;
height: 40px;
}
.loader {
margin: 60px auto;
font-size: 4px;
position: relative;
text-indent: -9999em;
border-top: 1.1em solid rgba(3, 169, 244, 0.2);
border-right: 1.1em solid rgba(3, 169, 244, 0.2);
border-bottom: 1.1em solid rgba(3, 169, 244, 0.2);
border-left: 1.1em solid rgb(3, 168, 244);
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
-webkit-animation: load8 1.4s infinite linear;
animation: load8 1.4s infinite linear;
}
@-webkit-keyframes load8 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes load8 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
`; `;
} }
} }

View File

@ -23,6 +23,7 @@ import {
loadConfigFlowDialog, loadConfigFlowDialog,
showConfigFlowDialog, showConfigFlowDialog,
} from "../../../dialogs/config-flow/show-dialog-config-flow"; } from "../../../dialogs/config-flow/show-dialog-config-flow";
import { localizeConfigFlowTitle } from "../../../data/config_entries";
/* /*
* @appliesMixin LocalizeMixin * @appliesMixin LocalizeMixin
@ -207,21 +208,8 @@ class HaConfigManagerDashboard extends LocalizeMixin(
return localize(`component.${integration}.config.title`); return localize(`component.${integration}.config.title`);
} }
_computeActiveFlowTitle(localize, integration) { _computeActiveFlowTitle(localize, flow) {
const placeholders = integration.context.title_placeholders || {}; return localizeConfigFlowTitle(localize, flow);
const placeholderKeys = Object.keys(placeholders);
if (placeholderKeys.length === 0) {
return localize(`component.${integration.handler}.config.title`);
}
const args = [];
placeholderKeys.forEach((key) => {
args.push(key);
args.push(placeholders[key]);
});
return localize(
`component.${integration.handler}.config.flow_title`,
...args
);
} }
_computeConfigEntryEntities(hass, configEntry, entities) { _computeConfigEntryEntities(hass, configEntry, entities) {

View File

@ -107,6 +107,7 @@ export const connectionMixin = (
return resp; return resp;
}, },
...getState(), ...getState(),
...this._pendingHass,
}; };
this.hassConnected(); this.hassConnected();

View File

@ -10,6 +10,7 @@ import { HomeAssistant } from "../types";
export class HassBaseEl { export class HassBaseEl {
protected hass?: HomeAssistant; protected hass?: HomeAssistant;
protected _pendingHass: Partial<HomeAssistant> = {};
protected initializeHass(_auth: Auth, _conn: Connection) {} protected initializeHass(_auth: Auth, _conn: Connection) {}
protected hassConnected() {} protected hassConnected() {}
protected hassReconnected() {} protected hassReconnected() {}
@ -23,6 +24,7 @@ export class HassBaseEl {
export default <T>(superClass: Constructor<T>): Constructor<T & HassBaseEl> => export default <T>(superClass: Constructor<T>): Constructor<T & HassBaseEl> =>
// @ts-ignore // @ts-ignore
class extends superClass { class extends superClass {
protected _pendingHass: Partial<HomeAssistant> = {};
private __provideHass: HTMLElement[] = []; private __provideHass: HTMLElement[] = [];
// @ts-ignore // @ts-ignore
@property() protected hass: HomeAssistant; @property() protected hass: HomeAssistant;
@ -55,7 +57,11 @@ export default <T>(superClass: Constructor<T>): Constructor<T & HassBaseEl> =>
el.hass = this.hass; el.hass = this.hass;
} }
protected async _updateHass(obj) { protected async _updateHass(obj: Partial<HomeAssistant>) {
if (!this.hass) {
this._pendingHass = { ...this._pendingHass, ...obj };
return;
}
this.hass = { ...this.hass, ...obj }; this.hass = { ...this.hass, ...obj };
} }
}; };

View File

@ -115,7 +115,7 @@ export default (superClass: Constructor<LitElement & HassBaseEl>) =>
}, },
}; };
const changes: Partial<HomeAssistant> = { resources }; const changes: Partial<HomeAssistant> = { resources };
if (language === this.hass!.language) { if (this.hass && language === this.hass.language) {
changes.localize = computeLocalize(this, language, resources); changes.localize = computeLocalize(this, language, resources);
} }
this._updateHass(changes); this._updateHass(changes);

View File

@ -1172,6 +1172,11 @@
"required_fields": "Fill in all required fields", "required_fields": "Fill in all required fields",
"password_not_match": "Passwords don't match" "password_not_match": "Passwords don't match"
} }
},
"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",
"finish": "Finish"
} }
} }
} }

View File

@ -7237,10 +7237,10 @@ hoek@6.x.x:
resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.3.tgz#73b7d33952e01fe27a38b0457294b79dd8da242c" resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.3.tgz#73b7d33952e01fe27a38b0457294b79dd8da242c"
integrity sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ== integrity sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ==
home-assistant-js-websocket@^3.4.0: home-assistant-js-websocket@^4.1.1:
version "3.4.0" version "4.1.1"
resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-3.4.0.tgz#3ba47cc8f8b7620619a675e7488d6108e8733a70" resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-4.1.1.tgz#b85152c223a20bfe8827b817b927fd97cc021157"
integrity sha512-Uq5/KIAh4kF13MKzMyd0efBDoU+pNF0O1CfdGpSmT3La3tpt5h+ykpUYlq/vEBj6WwzU6iv3Czt4UK1o0IJHcA== integrity sha512-hNk8bj9JObd3NpgQ1+KtQCbSoz/TWockC8T/L8KvsPrDtkl1oQddajirumaMDgrJg/su4QsxFNUcDPGJyJ05UA==
homedir-polyfill@^1.0.0, homedir-polyfill@^1.0.1: homedir-polyfill@^1.0.0, homedir-polyfill@^1.0.1:
version "1.0.3" version "1.0.3"