Compare commits

...

2 Commits

Author SHA1 Message Date
Bram Kragten 2dc4004323 Add device step to matter add flow 2026-05-26 15:30:02 +02:00
Aidan Timson fb0a54231a Show device name tip with link to editor, disable update button when state is clean (#52024) 2026-05-26 13:32:16 +02:00
8 changed files with 594 additions and 14 deletions
+16 -5
View File
@@ -2,7 +2,10 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { navigate } from "../common/navigate";
import type { HomeAssistant } from "../types";
import { subscribeDeviceRegistry } from "./device/device_registry";
import {
subscribeDeviceRegistry,
type DeviceRegistryEntry,
} from "./device/device_registry";
import { getThreadDataSetTLV, listThreadDataSets } from "./thread";
export enum NetworkType {
@@ -77,9 +80,9 @@ export const startExternalCommissioning = async (hass: HomeAssistant) => {
});
};
export const redirectOnNewMatterDevice = (
export const watchForNewMatterDevice = (
hass: HomeAssistant,
callback?: () => void
callback: (device: DeviceRegistryEntry) => void
): UnsubscribeFunc => {
let curMatterDevices: Set<string> | undefined;
const unsubDeviceReg = subscribeDeviceRegistry(hass.connection, (entries) => {
@@ -101,8 +104,7 @@ export const redirectOnNewMatterDevice = (
if (newMatterDevices.length) {
unsubDeviceReg();
curMatterDevices = undefined;
callback?.();
navigate(`/config/devices/device/${newMatterDevices[0].id}`);
callback(newMatterDevices[0]);
}
});
return () => {
@@ -111,6 +113,15 @@ export const redirectOnNewMatterDevice = (
};
};
export const redirectOnNewMatterDevice = (
hass: HomeAssistant,
callback?: () => void
): UnsubscribeFunc =>
watchForNewMatterDevice(hass, (device) => {
callback?.();
navigate(`/config/devices/device/${device.id}`);
});
export const addMatterDevice = (hass: HomeAssistant) => {
startExternalCommissioning(hass);
};
@@ -39,11 +39,15 @@ export class EntitySettingsHelperTab extends LitElement {
@state() private _submitting = false;
@state() private _dirty = false;
@state() private _componentLoaded?: boolean;
@query("entity-registry-settings-editor")
private _registryEditor?: EntityRegistrySettingsEditor;
private _originalItemJson?: string;
protected firstUpdated(changedProperties: PropertyValues<this>) {
super.firstUpdated(changedProperties);
this._componentLoaded = isComponentLoaded(
@@ -120,7 +124,9 @@ export class EntitySettingsHelperTab extends LitElement {
</ha-button>
<ha-button
@click=${this._updateItem}
.disabled=${!!this._submitting || !!(this._item && !this._item.name)}
.disabled=${!this._dirty ||
!!this._submitting ||
!!(this._item && !this._item.name)}
>
${this.hass.localize("ui.dialogs.entity_registry.editor.update")}
</ha-button>
@@ -128,8 +134,18 @@ export class EntitySettingsHelperTab extends LitElement {
`;
}
private get _isHelperDirty(): boolean {
if (!this._item || !this._originalItemJson) return false;
return JSON.stringify(this._item) !== this._originalItemJson;
}
private _updateDirty() {
this._dirty = (this._registryEditor?.dirty ?? false) || this._isHelperDirty;
}
private _entityRegistryChanged() {
this._error = undefined;
this._updateDirty();
}
private _valueChanged(ev: CustomEvent): void {
@@ -138,11 +154,15 @@ export class EntitySettingsHelperTab extends LitElement {
}
this._error = undefined;
this._item = ev.detail.value;
this._updateDirty();
}
private async _getItem() {
const items = await HELPERS_CRUD[this.entry.platform].fetch(this.hass!);
this._item = items.find((item) => item.id === this.entry.unique_id) || null;
this._originalItemJson = this._item
? JSON.stringify(this._item)
: undefined;
}
private async _updateItem(): Promise<void> {
@@ -97,7 +97,7 @@ import type { HomeAssistant } from "../../../types";
import { showToast } from "../../../util/toast";
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail";
const OVERRIDE_DEVICE_CLASSES = {
export const OVERRIDE_DEVICE_CLASSES = {
cover: [
[
"awning",
@@ -208,6 +208,34 @@ export class EntityRegistrySettingsEditor extends LitElement {
private _deviceClassOptions?: string[][];
private _initialStateJson!: string;
private _lastDirty = false;
private _currentState() {
return {
name: this._name.trim() || null,
icon: this._icon.trim() || null,
entityId: this._entityId.trim(),
areaId: this._areaId ?? null,
labels: this._labels ?? [],
deviceClass: this._deviceClass,
disabledBy: this._disabledBy,
hiddenBy: this._hiddenBy,
unitOfMeasurement: this._unit_of_measurement,
precision: this._precision,
defaultCode: this._defaultCode,
calendarColor: this._calendarColor ?? null,
precipitationUnit: this._precipitation_unit,
pressureUnit: this._pressure_unit,
temperatureUnit: this._temperature_unit,
visibilityUnit: this._visibility_unit,
windSpeedUnit: this._wind_speed_unit,
switchAsDomain: this._switchAsDomain,
switchAsInvert: this._switchAsInvert,
};
}
protected willUpdate(changedProperties: PropertyValues<this>) {
super.willUpdate(changedProperties);
if (
@@ -274,6 +302,9 @@ export class EntityRegistrySettingsEditor extends LitElement {
this._wind_speed_unit = stateObj?.attributes?.wind_speed_unit;
}
this._initialStateJson = JSON.stringify(this._currentState());
this._lastDirty = false;
const deviceClasses: string[][] = OVERRIDE_DEVICE_CLASSES[domain];
if (!deviceClasses || this._hideDeviceClassOverride(domain)) {
@@ -372,6 +403,16 @@ export class EntityRegistrySettingsEditor extends LitElement {
this._switchAsDomain = "switch";
this._switchAsInvert = false;
}
this._initialStateJson = JSON.stringify(this._currentState());
this._lastDirty = false;
}
if (this._initialStateJson) {
const dirty = this.dirty;
if (dirty !== this._lastDirty) {
this._lastDirty = dirty;
fireEvent(this, "change");
}
}
}
@@ -407,6 +448,23 @@ export class EntityRegistrySettingsEditor extends LitElement {
.disabled=${this.disabled}
@input=${this._nameChanged}
>
${this._device
? html`<span slot="hint"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.device_name_tip",
{
link: html`<button
class="link"
@click=${this._resetNameAndOpenDeviceSettings}
>
${this.hass.localize(
"ui.dialogs.entity_registry.editor.open_device_settings"
)}
</button>`,
}
)}</span
>`
: nothing}
</ha-input>`}
${this.hideIcon
? nothing
@@ -1060,6 +1118,10 @@ export class EntityRegistrySettingsEditor extends LitElement {
`;
}
public get dirty(): boolean {
return JSON.stringify(this._currentState()) !== this._initialStateJson;
}
public async updateEntry(): Promise<{
close: boolean;
entry: ExtEntityRegistryEntry;
@@ -1518,6 +1580,13 @@ export class EntityRegistrySettingsEditor extends LitElement {
}
}
private _resetNameAndOpenDeviceSettings() {
this._name = this.entry.name || "";
fireEvent(this, "change");
this._openDeviceSettings();
}
private _openDeviceSettings() {
showDeviceRegistryDetailDialog(this, {
device: this._device!,
@@ -44,6 +44,8 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@state() private _submitting?: boolean;
@state() private _dirty = false;
@query("entity-registry-settings-editor")
private _registryEditor?: EntityRegistrySettingsEditor;
@@ -144,7 +146,11 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
>
${this.hass.localize("ui.dialogs.entity_registry.editor.delete")}
</ha-button>
<ha-button @click=${this._updateEntry} .loading=${!!this._submitting}>
<ha-button
@click=${this._updateEntry}
.disabled=${!this._dirty || !!this._submitting}
.loading=${!!this._submitting}
>
${this.hass.localize("ui.dialogs.entity_registry.editor.update")}
</ha-button>
</div>
@@ -153,6 +159,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
private _entityRegistryChanged() {
this._error = undefined;
this._dirty = this._registryEditor?.dirty ?? false;
}
private _openDeviceSettings() {
@@ -3,16 +3,28 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { computeDomain } from "../../../../../common/entity/compute_domain";
import { computeDeviceName } from "../../../../../common/entity/compute_device_name";
import { navigate } from "../../../../../common/navigate";
import "../../../../../components/ha-dialog-footer";
import "../../../../../components/ha-icon-button-arrow-prev";
import "../../../../../components/ha-button";
import "../../../../../components/ha-dialog";
import {
commissionMatterDevice,
redirectOnNewMatterDevice,
watchForNewMatterDevice,
} from "../../../../../data/matter";
import { haStyleDialog } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
import { updateDeviceRegistryEntry } from "../../../../../data/device/device_registry";
import {
getAutomaticEntityIds,
getExtendedEntityRegistryEntries,
updateEntityRegistryEntry,
type ExtEntityRegistryEntry,
} from "../../../../../data/entity/entity_registry";
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
import "./matter-add-device/matter-add-device-apple-home";
import "./matter-add-device/matter-add-device-existing";
import "./matter-add-device/matter-add-device-generic";
@@ -21,6 +33,7 @@ import "./matter-add-device/matter-add-device-google-home-fallback";
import "./matter-add-device/matter-add-device-main";
import "./matter-add-device/matter-add-device-new";
import "./matter-add-device/matter-add-device-commissioning";
import "./matter-add-device/matter-add-device-device-added";
import { showToast } from "../../../../../util/toast";
export type MatterAddDeviceStep =
@@ -31,7 +44,8 @@ export type MatterAddDeviceStep =
| "google_home_fallback"
| "apple_home"
| "generic"
| "commissioning";
| "commissioning"
| "device_added";
declare global {
interface HASSDomEvents {
@@ -50,6 +64,7 @@ const BACK_STEP: Record<MatterAddDeviceStep, MatterAddDeviceStep | undefined> =
apple_home: "existing",
generic: "existing",
commissioning: undefined,
device_added: undefined,
};
@customElement("dialog-matter-add-device")
@@ -62,23 +77,90 @@ class DialogMatterAddDevice extends LitElement {
@state() _step: MatterAddDeviceStep = "main";
@state() private _newDevice?: DeviceRegistryEntry;
@state() private _mainEntity?: ExtEntityRegistryEntry;
@state() private _deviceAddedState: {
name: string;
area: string | undefined;
deviceClass: string | undefined;
hasPendingUpdates: boolean;
} = {
name: "",
area: undefined,
deviceClass: undefined,
hasPendingUpdates: false,
};
private _mainEntityFetched = false;
private _unsub?: UnsubscribeFunc;
public showDialog(): void {
this._open = true;
this._unsub = redirectOnNewMatterDevice(this.hass, () =>
this.closeDialog()
);
this._unsub = watchForNewMatterDevice(this.hass, (device) => {
this._newDevice = device;
this._step = "device_added";
this._fetchMainEntity();
});
}
public closeDialog(): void {
this._open = false;
}
protected updated(changedProps: Map<string, unknown>): void {
// Retry fetching main entity when hass updates (entities may not be available immediately)
if (
changedProps.has("hass") &&
this._newDevice &&
!this._mainEntityFetched
) {
this._fetchMainEntity();
}
}
private async _fetchMainEntity(): Promise<void> {
if (!this._newDevice || this._mainEntityFetched) return;
const entityIds = Object.values(this.hass.entities)
.filter((e) => e.device_id === this._newDevice!.id)
.map((e) => e.entity_id);
if (!entityIds.length) return;
this._mainEntityFetched = true;
const entries = await getExtendedEntityRegistryEntries(
this.hass,
entityIds
);
const mainEntry = Object.values(entries).find(
(e) => e.original_name === null
);
if (!mainEntry) return;
const domain = computeDomain(mainEntry.entity_id);
if (domain === "cover" || domain === "binary_sensor") {
this._mainEntity = mainEntry;
}
}
private _dialogClosed(): void {
this._open = false;
this._step = "main";
this._pairingCode = "";
this._newDevice = undefined;
this._mainEntity = undefined;
this._mainEntityFetched = false;
this._deviceAddedState = {
name: "",
area: undefined,
deviceClass: undefined,
hasPendingUpdates: false,
};
this._unsub?.();
this._unsub = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
@@ -93,6 +175,17 @@ class DialogMatterAddDevice extends LitElement {
this._pairingCode = ev.detail.code;
}
private _handleDeviceAddedChanged(
ev: CustomEvent<{
name: string;
area: string | undefined;
deviceClass: string | undefined;
hasPendingUpdates: boolean;
}>
) {
this._deviceAddedState = ev.detail;
}
private _back() {
const backStep = BACK_STEP[this._step];
if (!backStep) return;
@@ -104,12 +197,15 @@ class DialogMatterAddDevice extends LitElement {
<div
@pairing-code-changed=${this._handlePairingCodeChanged}
@step-selected=${this._handleStepSelected}
@device-added-changed=${this._handleDeviceAddedChanged}
.hass=${this.hass}
>
${dynamicElement(
`matter-add-device-${this._step.replaceAll("_", "-")}`,
{
hass: this.hass,
device: this._newDevice,
mainEntity: this._mainEntity,
}
)}
</div>
@@ -129,8 +225,86 @@ class DialogMatterAddDevice extends LitElement {
),
duration: 2000,
});
this._step = savedStep;
}
this._step = savedStep;
// On success, keep showing commissioning spinner until watchForNewMatterDevice fires
}
private async _finishDeviceAdded(): Promise<void> {
const device = this._newDevice!;
const { name, area, deviceClass, hasPendingUpdates } =
this._deviceAddedState;
if (hasPendingUpdates) {
const origName = computeDeviceName(device) ?? "";
const nameChanged = name !== origName;
const origArea = device.area_id ?? undefined;
const areaChanged = area !== origArea;
if (nameChanged || areaChanged) {
await updateDeviceRegistryEntry(this.hass, device.id, {
...(nameChanged && { name_by_user: name || null }),
...(areaChanged && { area_id: area || null }),
}).catch((err: Error) =>
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_flow.error_saving_device",
{ error: err.message }
),
})
);
}
if (nameChanged && name) {
const entityIds = Object.values(this.hass.entities)
.filter((e) => e.device_id === device.id)
.map((e) => e.entity_id);
if (entityIds.length) {
const mapping = await getAutomaticEntityIds(this.hass, entityIds);
await Promise.allSettled(
Object.entries(mapping)
.filter((entry): entry is [string, string] => !!entry[1])
.map(([oldId, newId]) =>
updateEntityRegistryEntry(this.hass, oldId, {
new_entity_id: newId,
}).catch((err: Error) =>
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_flow.error_saving_entity",
{ error: err.message }
),
})
)
)
);
}
}
if (this._mainEntity) {
const origClass =
this._mainEntity.device_class ??
this._mainEntity.original_device_class ??
undefined;
if (deviceClass !== origClass) {
await updateEntityRegistryEntry(
this.hass,
this._mainEntity.entity_id,
{ device_class: deviceClass || null }
).catch((err: Error) =>
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_flow.error_saving_entity",
{ error: err.message }
),
})
);
}
}
}
this.closeDialog();
navigate(`/config/devices/device/${device.id}`);
}
private _renderActions() {
@@ -156,6 +330,19 @@ class DialogMatterAddDevice extends LitElement {
</ha-button>
`;
}
if (this._step === "device_added") {
return html`
<ha-button slot="primaryAction" @click=${this._finishDeviceAdded}>
${this._deviceAddedState.hasPendingUpdates
? this.hass.localize(
"ui.dialogs.matter-add-device.device_added.finish"
)
: this.hass.localize(
"ui.dialogs.matter-add-device.device_added.skip"
)}
</ha-button>
`;
}
return nothing;
}
@@ -0,0 +1,274 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeDomain } from "../../../../../../common/entity/compute_domain";
import { computeDeviceName } from "../../../../../../common/entity/compute_device_name";
import { caseInsensitiveStringCompare } from "../../../../../../common/string/compare";
import { fireEvent } from "../../../../../../common/dom/fire_event";
import "../../../../../../components/ha-area-picker";
import "../../../../../../components/input/ha-input";
import "../../../../../../components/ha-select";
import "../../../../../../components/ha-dropdown-item";
import type { HaSelectSelectEvent } from "../../../../../../components/ha-select";
import type { ExtEntityRegistryEntry } from "../../../../../../data/entity/entity_registry";
import type { DeviceRegistryEntry } from "../../../../../../data/device/device_registry";
import type { HomeAssistant } from "../../../../../../types";
import { brandsUrl } from "../../../../../../util/brands-url";
import { sharedStyles } from "./matter-add-device-shared-styles";
import { OVERRIDE_DEVICE_CLASSES } from "../../../../entities/entity-registry-settings-editor";
declare global {
interface HASSDomEvents {
"device-added-changed": {
name: string;
area: string | undefined;
deviceClass: string | undefined;
hasPendingUpdates: boolean;
};
}
}
@customElement("matter-add-device-device-added")
class MatterAddDeviceDeviceAdded extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public device!: DeviceRegistryEntry;
@property({ attribute: false }) public mainEntity?: ExtEntityRegistryEntry;
@state() private _deviceName = "";
@state() private _area: string | undefined;
@state() private _deviceClass: string | undefined;
private _initialized = false;
protected willUpdate() {
if (!this._initialized && this.device) {
this._initialized = true;
this._deviceName = computeDeviceName(this.device) ?? "";
this._area = this.device.area_id ?? undefined;
this._deviceClass =
this.mainEntity?.device_class ??
this.mainEntity?.original_device_class ??
undefined;
}
}
private get _deviceClassOptions(): string[][] | undefined {
if (!this.mainEntity) return undefined;
const domain = computeDomain(this.mainEntity.entity_id);
const deviceClasses = OVERRIDE_DEVICE_CLASSES[domain];
if (!deviceClasses) return undefined;
const options: string[][] = [[], []];
for (const deviceClass of deviceClasses) {
if (
this.mainEntity.original_device_class &&
deviceClass.includes(this.mainEntity.original_device_class)
) {
options[0] = deviceClass;
} else {
options[1].push(...deviceClass);
}
}
return options;
}
private get _hasPendingUpdates(): boolean {
const origName = computeDeviceName(this.device) ?? "";
const origArea = this.device.area_id ?? undefined;
const origDeviceClass =
this.mainEntity?.device_class ??
this.mainEntity?.original_device_class ??
undefined;
return (
this._deviceName !== origName ||
this._area !== origArea ||
(this.mainEntity !== undefined && this._deviceClass !== origDeviceClass)
);
}
protected updated(changedProps: Map<string, unknown>) {
if (
changedProps.has("_deviceName") ||
changedProps.has("_area") ||
changedProps.has("_deviceClass")
) {
fireEvent(this, "device-added-changed", {
name: this._deviceName,
area: this._area,
deviceClass: this._deviceClass,
hasPendingUpdates: this._hasPendingUpdates,
});
}
}
private _deviceClassesSorted = memoizeOne(
(domain: string, deviceClasses: string[]) =>
deviceClasses
.map((deviceClass) => ({
deviceClass,
label: this.hass.localize(
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}`
),
}))
.sort((a, b) =>
caseInsensitiveStringCompare(
a.label,
b.label,
this.hass.locale.language
)
)
);
protected render() {
if (!this.device) return nothing;
const domain = this.mainEntity
? computeDomain(this.mainEntity.entity_id)
: undefined;
const deviceClassOptions = this._deviceClassOptions;
return html`
<div class="content">
<div class="device">
<div class="device-info">
<img
alt="Matter"
src=${brandsUrl(
{
domain: "matter",
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<div class="device-name">
<span>${computeDeviceName(this.device)}</span>
<span class="secondary">Matter</span>
</div>
</div>
<ha-input
.label=${this.hass.localize(
"ui.panel.config.integrations.config_flow.device_name"
)}
.value=${this._deviceName}
@change=${this._deviceNameChanged}
></ha-input>
<ha-area-picker
.hass=${this.hass}
.value=${this._area}
@value-changed=${this._areaPicked}
></ha-area-picker>
${deviceClassOptions && domain
? html`
<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.device_class"
)}
.value=${this._deviceClass
? this.hass.localize(
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${this._deviceClass}`
)
: undefined}
clearable
@selected=${this._deviceClassChanged}
>
${this._deviceClassesSorted(
domain,
deviceClassOptions[0]
).map(
(entry) => html`
<ha-dropdown-item
.value=${entry.deviceClass}
.selected=${entry.deviceClass === this._deviceClass}
>
${entry.label}
</ha-dropdown-item>
`
)}
${deviceClassOptions[0].length && deviceClassOptions[1].length
? html`<wa-divider></wa-divider>`
: nothing}
${this._deviceClassesSorted(
domain,
deviceClassOptions[1]
).map(
(entry) => html`
<ha-dropdown-item
.value=${entry.deviceClass}
.selected=${entry.deviceClass === this._deviceClass}
>
${entry.label}
</ha-dropdown-item>
`
)}
</ha-select>
`
: nothing}
</div>
</div>
`;
}
private _deviceNameChanged(ev: InputEvent) {
this._deviceName = (ev.currentTarget as HTMLInputElement).value;
}
private _areaPicked(ev: CustomEvent<{ value: string }>) {
this._area = ev.detail.value || undefined;
}
private _deviceClassChanged(ev: HaSelectSelectEvent<string, true>) {
this._deviceClass = ev.detail.value;
}
static styles = [
sharedStyles,
css`
.device {
border: 1px solid var(--divider-color);
padding: var(--ha-space-2);
border-radius: var(--ha-border-radius-sm);
}
.device-info {
display: flex;
align-items: center;
gap: var(--ha-space-2);
margin-bottom: var(--ha-space-1);
}
.device-info img {
width: 40px;
height: 40px;
}
.device-name {
display: flex;
flex-direction: column;
justify-content: center;
}
.secondary {
color: var(--secondary-text-color);
font-size: var(--ha-font-size-s);
}
ha-input {
margin: var(--ha-space-2) 0;
}
ha-area-picker,
ha-select {
display: block;
margin-top: var(--ha-space-2);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"matter-add-device-device-added": MatterAddDeviceDeviceAdded;
}
}
@@ -85,6 +85,12 @@ class MatterAddDeviceNew extends LitElement {
static styles = [
sharedStyles,
css`
.content {
display: flex;
align-items: center;
flex-direction: column;
text-align: center;
}
.app-qr {
margin: 24px auto 0 auto;
display: flex;
+6
View File
@@ -1969,6 +1969,7 @@
"entity_disabled": "This entity is disabled.",
"enable_entity": "Enable",
"open_device_settings": "Open device settings",
"device_name_tip": "Consider renaming the device instead to update all its entities at once. {link}",
"switch_as_x_confirm": "This switch will be hidden and a new {domain} will be added. Your existing configurations using the switch will continue to work.",
"switch_as_x_remove_confirm": "This {domain} will be removed and the original switch will be visible again. Your existing configurations using the {domain} will no longer work!",
"switch_as_x_change_confirm": "This {domain_1} will be removed and will be replaced by a new {domain_2}. Your existing configurations using the {domain_1} will no longer work!",
@@ -2421,6 +2422,11 @@
"header": "Enter setup code",
"code_instructions": "Search for the sharing mode in the app of your controller, and activate it. You will get a setup code, enter that below.",
"setup_code": "Setup code"
},
"device_added": {
"header": "Device added",
"finish": "Finish",
"skip": "Skip and finish"
}
},
"shortcuts": {