diff --git a/src/data/cloud.ts b/src/data/cloud.ts index b7fea8fae2..59882cae75 100644 --- a/src/data/cloud.ts +++ b/src/data/cloud.ts @@ -73,6 +73,7 @@ export interface CloudWebhook { interface CloudLoginBase { hass: HomeAssistant; email: string; + check_connection?: boolean; } export interface CloudLoginPassword extends CloudLoginBase { diff --git a/src/dialogs/voice-assistant-setup/cloud/cloud-step-signin.ts b/src/dialogs/voice-assistant-setup/cloud/cloud-step-signin.ts index 2de5a88bb5..fe7adc84a0 100644 --- a/src/dialogs/voice-assistant-setup/cloud/cloud-step-signin.ts +++ b/src/dialogs/voice-assistant-setup/cloud/cloud-step-signin.ts @@ -10,6 +10,7 @@ import "../../../components/ha-svg-icon"; import "../../../components/ha-textfield"; import type { HaTextField } from "../../../components/ha-textfield"; import { cloudLogin } from "../../../data/cloud"; +import { showCloudAlreadyConnectedDialog } from "../../../panels/config/cloud/dialog-cloud-already-connected/show-dialog-cloud-already-connected"; import type { HomeAssistant } from "../../../types"; import { showAlertDialog, @@ -25,6 +26,8 @@ export class CloudStepSignin extends LitElement { @state() private _error?: string; + @state() private _checkConnection = true; + @query("#email", true) private _emailField!: HaTextField; @query("#password", true) private _passwordField!: HaPasswordField; @@ -115,6 +118,7 @@ export class CloudStepSignin extends LitElement { hass: this.hass, email: username, ...(code ? { code } : { password }), + check_connection: this._checkConnection, }); } catch (err: any) { const errCode = err && err.body && err.body.code; @@ -139,6 +143,20 @@ export class CloudStepSignin extends LitElement { } } + if (errCode === "alreadyconnectederror") { + showCloudAlreadyConnectedDialog(this, { + details: JSON.parse(err.body.message), + logInHereAction: () => { + this._checkConnection = false; + doLogin(username); + }, + closeDialog: () => { + this._requestInProgress = false; + }, + }); + return; + } + if (errCode === "usernotfound" && username !== username.toLowerCase()) { await doLogin(username.toLowerCase()); return; diff --git a/src/panels/config/cloud/dialog-cloud-already-connected/dialog-cloud-already-connected.ts b/src/panels/config/cloud/dialog-cloud-already-connected/dialog-cloud-already-connected.ts new file mode 100644 index 0000000000..8daccffeea --- /dev/null +++ b/src/panels/config/cloud/dialog-cloud-already-connected/dialog-cloud-already-connected.ts @@ -0,0 +1,171 @@ +import type { CSSResultGroup } from "lit"; +import { css, html, LitElement, nothing } from "lit"; +import { customElement, state } from "lit/decorators"; +import { mdiEye, mdiEyeOff } from "@mdi/js"; +import { formatDateTime } from "../../../../common/datetime/format_date_time"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-alert"; +import "../../../../components/ha-button"; +import "../../../../components/ha-icon-button"; +import { createCloseHeading } from "../../../../components/ha-dialog"; +import { haStyleDialog } from "../../../../resources/styles"; +import type { HomeAssistant } from "../../../../types"; +import type { CloudAlreadyConnectedParams as CloudAlreadyConnectedDialogParams } from "./show-dialog-cloud-already-connected"; +import { obfuscateUrl } from "../../../../util/url"; + +@customElement("dialog-cloud-already-connected") +class DialogCloudAlreadyConnected extends LitElement { + public hass!: HomeAssistant; + + @state() private _params?: CloudAlreadyConnectedDialogParams; + + @state() private _obfuscateIp = true; + + public showDialog(params: CloudAlreadyConnectedDialogParams) { + this._params = params; + } + + public closeDialog() { + this._params?.closeDialog(); + this._params = undefined; + this._obfuscateIp = true; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render() { + if (!this._params) { + return nothing; + } + const { details } = this._params; + + return html` + +
+ + ${this.hass.localize( + "ui.panel.config.cloud.dialog_already_connected.description" + )} + + + ${this.hass.localize( + "ui.panel.config.cloud.dialog_already_connected.other_home_assistant" + )} + +
+
+
+ + ${this.hass.localize( + "ui.panel.config.cloud.dialog_already_connected.ip_address" + )}: + +
+ + ${this._obfuscateIp + ? obfuscateUrl(details.remote_ip_address) + : details.remote_ip_address} + + + +
+
+
+ + ${this.hass.localize( + "ui.panel.config.cloud.dialog_already_connected.connected_at" + )}: + + + ${formatDateTime( + new Date(details.connected_at), + this.hass.locale, + this.hass.config + )} + +
+
+ + ${this.hass.localize( + "ui.panel.config.cloud.dialog_already_connected.info_backups.description" + )} + + + + ${this.hass!.localize("ui.common.cancel")} + + + ${this.hass!.localize( + "ui.panel.config.cloud.dialog_already_connected.login_here" + )} + +
+ `; + } + + private _toggleObfuscateIp() { + this._obfuscateIp = !this._obfuscateIp; + } + + private _logInHere() { + this._params?.logInHereAction(); + this.closeDialog(); + } + + static get styles(): CSSResultGroup { + return [ + haStyleDialog, + css` + ha-dialog { + --mdc-dialog-max-width: 535px; + } + .intro b { + display: block; + margin-top: 16px; + } + .instance-details { + display: flex; + flex-direction: column; + margin-bottom: 16px; + } + .instance-detail { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + } + .obfuscated { + align-items: center; + display: flex; + flex-direction: row; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-cloud-already-connected": DialogCloudAlreadyConnected; + } +} diff --git a/src/panels/config/cloud/dialog-cloud-already-connected/show-dialog-cloud-already-connected.ts b/src/panels/config/cloud/dialog-cloud-already-connected/show-dialog-cloud-already-connected.ts new file mode 100644 index 0000000000..7c95db2247 --- /dev/null +++ b/src/panels/config/cloud/dialog-cloud-already-connected/show-dialog-cloud-already-connected.ts @@ -0,0 +1,21 @@ +import { fireEvent } from "../../../../common/dom/fire_event"; + +export interface CloudAlreadyConnectedParams { + details: { + remote_ip_address: string; + connected_at: string; + }; + logInHereAction: () => void; + closeDialog: () => void; +} + +export const showCloudAlreadyConnectedDialog = ( + element: HTMLElement, + webhookDialogParams: CloudAlreadyConnectedParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-cloud-already-connected", + dialogImport: () => import("./dialog-cloud-already-connected"), + dialogParams: webhookDialogParams, + }); +}; diff --git a/src/panels/config/cloud/login/cloud-login.ts b/src/panels/config/cloud/login/cloud-login.ts index 5fe14e24ba..bc03fe8cb8 100644 --- a/src/panels/config/cloud/login/cloud-login.ts +++ b/src/panels/config/cloud/login/cloud-login.ts @@ -28,6 +28,7 @@ import { haStyle } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; import "../../ha-config-section"; import { showSupportPackageDialog } from "../account/show-dialog-cloud-support-package"; +import { showCloudAlreadyConnectedDialog } from "../dialog-cloud-already-connected/show-dialog-cloud-already-connected"; @customElement("cloud-login") export class CloudLogin extends LitElement { @@ -47,6 +48,8 @@ export class CloudLogin extends LitElement { @state() private _error?: string; + @state() private _checkConnection = true; + @query("#email", true) private _emailField!: HaTextField; @query("#password", true) private _passwordField!: HaPasswordField; @@ -244,6 +247,7 @@ export class CloudLogin extends LitElement { hass: this.hass, email: username, ...(code ? { code } : { password }), + check_connection: this._checkConnection, }); this.email = ""; this._password = ""; @@ -283,6 +287,21 @@ export class CloudLogin extends LitElement { return; } } + if (errCode === "alreadyconnectederror") { + showCloudAlreadyConnectedDialog(this, { + details: JSON.parse(err.body.message), + logInHereAction: () => { + this._checkConnection = false; + doLogin(username); + }, + closeDialog: () => { + this._requestInProgress = false; + this.email = ""; + this._password = ""; + }, + }); + return; + } if (errCode === "PasswordChangeRequired") { showAlertDialog(this, { title: this.hass.localize( diff --git a/src/translations/en.json b/src/translations/en.json index d039401df7..382aaf3193 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4730,6 +4730,23 @@ "fingerprint": "Certificate fingerprint:", "close": "Close" }, + "dialog_already_connected": { + "heading": "Account linked to other Home Assistant", + "description": "We noticed that another instance is currently connected to your Home Assistant Cloud account. Your Home Assistant Cloud account can only be signed into one Home Assistant instance at a time. If you log in here, the other instance will be disconnected along with its Cloud services.", + "other_home_assistant": "Other Home Assistant", + "ip_address": "IP Address", + "connected_at": "Connected at", + "obfuscated_ip": { + "show": "Show IP address", + "hide": "Hide IP address" + }, + "info_backups": { + "title": "Home Assistant Cloud backups", + "description": "Your Cloud backup may be overwritten if you proceed. We strongly recommend downloading your current backup from your Nabu Casa account page before continuing." + }, + "close": "Close", + "login_here": "Log in here" + }, "dialog_cloudhook": { "webhook_for": "Webhook for {name}", "managed_by_integration": "This webhook is managed by an integration and cannot be disabled.",