Add support for integration type (#12077)

This commit is contained in:
Paulus Schoutsen 2022-03-22 12:47:12 -07:00 committed by GitHub
parent bdde5268c6
commit 73f5580555
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 602 additions and 377 deletions

View File

@ -1,4 +1,4 @@
import { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import { computeDomain } from "./compute_domain"; import { computeDomain } from "./compute_domain";
export const computeStateDomain = (stateObj: HassEntity) => export const computeStateDomain = (stateObj: HassEntity) =>

View File

@ -28,7 +28,11 @@ export class HaAreaSelector extends LitElement {
oldSelector !== this.selector && oldSelector !== this.selector &&
this.selector.area.device?.integration this.selector.area.device?.integration
) { ) {
this._loadConfigEntries(); getConfigEntries(this.hass, {
domain: this.selector.area.device.integration,
}).then((entries) => {
this._configEntries = entries;
});
} }
} }
} }
@ -85,12 +89,6 @@ export class HaAreaSelector extends LitElement {
} }
return true; return true;
}; };
private async _loadConfigEntries() {
this._configEntries = (await getConfigEntries(this.hass)).filter(
(entry) => entry.domain === this.selector.area.device?.integration
);
}
} }
declare global { declare global {

View File

@ -25,7 +25,11 @@ export class HaDeviceSelector extends LitElement {
if (changedProperties.has("selector")) { if (changedProperties.has("selector")) {
const oldSelector = changedProperties.get("selector"); const oldSelector = changedProperties.get("selector");
if (oldSelector !== this.selector && this.selector.device?.integration) { if (oldSelector !== this.selector && this.selector.device?.integration) {
this._loadConfigEntries(); getConfigEntries(this.hass, {
domain: this.selector.device.integration,
}).then((entries) => {
this._configEntries = entries;
});
} }
} }
} }
@ -88,12 +92,6 @@ export class HaDeviceSelector extends LitElement {
} }
return true; return true;
}; };
private async _loadConfigEntries() {
this._configEntries = (await getConfigEntries(this.hass)).filter(
(entry) => entry.domain === this.selector.device.integration
);
}
} }
declare global { declare global {

View File

@ -134,9 +134,8 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
private async _loadConfigEntries() { private async _loadConfigEntries() {
this._configEntries = (await getConfigEntries(this.hass)).filter( this._configEntries = (await getConfigEntries(this.hass)).filter(
(entry) => (entry) =>
entry.domain === entry.domain === this.selector.target.device?.integration ||
(this.selector.target.device?.integration || entry.domain === this.selector.target.entity?.integration
this.selector.target.entity?.integration)
); );
} }

View File

@ -34,8 +34,24 @@ export const ERROR_STATES: ConfigEntry["state"][] = [
"setup_retry", "setup_retry",
]; ];
export const getConfigEntries = (hass: HomeAssistant) => export const getConfigEntries = (
hass.callApi<ConfigEntry[]>("GET", "config/config_entries/entry"); hass: HomeAssistant,
filters?: { type?: "helper" | "integration"; domain?: string }
): Promise<ConfigEntry[]> => {
const params = new URLSearchParams();
if (filters) {
if (filters.type) {
params.append("type", filters.type);
}
if (filters.domain) {
params.append("domain", filters.domain);
}
}
return hass.callApi<ConfigEntry[]>(
"GET",
`config/config_entries/entry?${params.toString()}`
);
};
export const updateConfigEntry = ( export const updateConfigEntry = (
hass: HomeAssistant, hass: HomeAssistant,

View File

@ -65,8 +65,14 @@ export const ignoreConfigFlow = (
export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) => export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) =>
hass.callApi("DELETE", `config/config_entries/flow/${flowId}`); hass.callApi("DELETE", `config/config_entries/flow/${flowId}`);
export const getConfigFlowHandlers = (hass: HomeAssistant) => export const getConfigFlowHandlers = (
hass.callApi<string[]>("GET", "config/config_entries/flow_handlers"); hass: HomeAssistant,
type?: "helper" | "integration"
) =>
hass.callApi<string[]>(
"GET",
`config/config_entries/flow_handlers${type ? `?type=${type}` : ""}`
);
export const fetchConfigFlowInProgress = ( export const fetchConfigFlowInProgress = (
conn: Connection conn: Connection

View File

@ -247,14 +247,14 @@ const getEnergyData = async (
end?: Date end?: Date
): Promise<EnergyData> => { ): Promise<EnergyData> => {
const [configEntries, entityRegistryEntries, info] = await Promise.all([ const [configEntries, entityRegistryEntries, info] = await Promise.all([
getConfigEntries(hass), getConfigEntries(hass, { domain: "co2signal" }),
subscribeOne(hass.connection, subscribeEntityRegistry), subscribeOne(hass.connection, subscribeEntityRegistry),
getEnergyInfo(hass), getEnergyInfo(hass),
]); ]);
const co2SignalConfigEntry = configEntries.find( const co2SignalConfigEntry = configEntries.length
(entry) => entry.domain === "co2signal" ? configEntries[0]
); : undefined;
let co2SignalEntity: string | undefined; let co2SignalEntity: string | undefined;

71
src/data/helpers_crud.ts Normal file
View File

@ -0,0 +1,71 @@
import { fetchCounter, updateCounter, deleteCounter } from "./counter";
import {
fetchInputBoolean,
updateInputBoolean,
deleteInputBoolean,
} from "./input_boolean";
import {
fetchInputButton,
updateInputButton,
deleteInputButton,
} from "./input_button";
import {
fetchInputDateTime,
updateInputDateTime,
deleteInputDateTime,
} from "./input_datetime";
import {
fetchInputNumber,
updateInputNumber,
deleteInputNumber,
} from "./input_number";
import {
fetchInputSelect,
updateInputSelect,
deleteInputSelect,
} from "./input_select";
import { fetchInputText, updateInputText, deleteInputText } from "./input_text";
import { fetchTimer, updateTimer, deleteTimer } from "./timer";
export const HELPERS_CRUD = {
input_boolean: {
fetch: fetchInputBoolean,
update: updateInputBoolean,
delete: deleteInputBoolean,
},
input_button: {
fetch: fetchInputButton,
update: updateInputButton,
delete: deleteInputButton,
},
input_text: {
fetch: fetchInputText,
update: updateInputText,
delete: deleteInputText,
},
input_number: {
fetch: fetchInputNumber,
update: updateInputNumber,
delete: deleteInputNumber,
},
input_datetime: {
fetch: fetchInputDateTime,
update: updateInputDateTime,
delete: deleteInputDateTime,
},
input_select: {
fetch: fetchInputSelect,
update: updateInputSelect,
delete: deleteInputSelect,
},
counter: {
fetch: fetchCounter,
update: updateCounter,
delete: deleteCounter,
},
timer: {
fetch: fetchTimer,
update: updateTimer,
delete: deleteTimer,
},
};

View File

@ -24,7 +24,7 @@ export const showConfigFlowDialog = (
loadDevicesAndAreas: true, loadDevicesAndAreas: true,
getFlowHandlers: async (hass) => { getFlowHandlers: async (hass) => {
const [handlers] = await Promise.all([ const [handlers] = await Promise.all([
getConfigFlowHandlers(hass), getConfigFlowHandlers(hass, "integration"),
hass.loadBackendTranslation("title", undefined, true), hass.loadBackendTranslation("title", undefined, true),
]); ]);

View File

@ -216,15 +216,16 @@ class StepFlowPickHandler extends LitElement {
if (handler.is_add) { if (handler.is_add) {
if (handler.slug === "zwave_js") { if (handler.slug === "zwave_js") {
const entries = await getConfigEntries(this.hass); const entries = await getConfigEntries(this.hass, {
const entry = entries.find((ent) => ent.domain === "zwave_js"); domain: "zwave_js",
});
if (!entry) { if (!entries.length) {
return; return;
} }
showZWaveJSAddNodeDialog(this, { showZWaveJSAddNodeDialog(this, {
entry_id: entry.entry_id, entry_id: entries[0].entry_id,
}); });
} else if (handler.slug === "zha") { } else if (handler.slug === "zha") {
navigate("/config/zha/add"); navigate("/config/zha/add");

View File

@ -169,8 +169,8 @@ class OnboardingIntegrations extends LitElement {
} }
private async _loadConfigEntries() { private async _loadConfigEntries() {
const entries = await getConfigEntries(this.hass!); const entries = await getConfigEntries(this.hass!, { type: "integration" });
// We filter out the config entry for the local weather and rpi_power. // We filter out the config entries that are automatically created during onboarding.
// It is one that we create automatically and it will confuse the user // It is one that we create automatically and it will confuse the user
// if it starts showing up during onboarding. // if it starts showing up during onboarding.
this._entries = entries.filter( this._entries = entries.filter(

View File

@ -58,12 +58,11 @@ export class HaDeviceInfoZWaveJS extends LitElement {
return; return;
} }
const configEntries = await getConfigEntries(this.hass); const configEntries = await getConfigEntries(this.hass, {
domain: "zwave_js",
});
let zwaveJsConfEntries = 0; let zwaveJsConfEntries = 0;
for (const entry of configEntries) { for (const entry of configEntries) {
if (entry.domain !== "zwave_js") {
continue;
}
if (zwaveJsConfEntries) { if (zwaveJsConfEntries) {
this._multipleConfigEntries = true; this._multipleConfigEntries = true;
} }

View File

@ -54,7 +54,7 @@ export class EnergyGridSettings extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
public validationResult?: EnergyPreferencesValidation; public validationResult?: EnergyPreferencesValidation;
@state() private _configEntries?: ConfigEntry[]; @state() private _co2ConfigEntry?: ConfigEntry;
protected firstUpdated() { protected firstUpdated() {
this._fetchCO2SignalConfigEntries(); this._fetchCO2SignalConfigEntries();
@ -195,8 +195,8 @@ export class EnergyGridSettings extends LitElement {
"ui.panel.config.energy.grid.grid_carbon_footprint" "ui.panel.config.energy.grid.grid_carbon_footprint"
)} )}
</h3> </h3>
${this._configEntries?.map( ${this._co2ConfigEntry
(entry) => html`<div class="row" .entry=${entry}> ? html`<div class="row" .entry=${this._co2ConfigEntry}>
<img <img
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
src=${brandsUrl({ src=${brandsUrl({
@ -205,8 +205,10 @@ export class EnergyGridSettings extends LitElement {
darkOptimized: this.hass.themes?.darkMode, darkOptimized: this.hass.themes?.darkMode,
})} })}
/> />
<span class="content">${entry.title}</span> <span class="content">${this._co2ConfigEntry.title}</span>
<a href=${`/config/integrations#config_entry=${entry.entry_id}`}> <a
href=${`/config/integrations#config_entry=${this._co2ConfigEntry.entry_id}`}
>
<ha-icon-button .path=${mdiPencil}></ha-icon-button> <ha-icon-button .path=${mdiPencil}></ha-icon-button>
</a> </a>
<ha-icon-button <ha-icon-button
@ -214,9 +216,7 @@ export class EnergyGridSettings extends LitElement {
.path=${mdiDelete} .path=${mdiDelete}
></ha-icon-button> ></ha-icon-button>
</div>` </div>`
)} : html`
${this._configEntries?.length === 0
? html`
<div class="row border-bottom"> <div class="row border-bottom">
<img <img
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
@ -232,17 +232,15 @@ export class EnergyGridSettings extends LitElement {
)} )}
</mwc-button> </mwc-button>
</div> </div>
` `}
: ""}
</div> </div>
</ha-card> </ha-card>
`; `;
} }
private async _fetchCO2SignalConfigEntries() { private async _fetchCO2SignalConfigEntries() {
this._configEntries = (await getConfigEntries(this.hass)).filter( const entries = await getConfigEntries(this.hass, { domain: "co2signal" });
(entry) => entry.domain === "co2signal" this._co2ConfigEntry = entries.length ? entries[0] : undefined;
);
} }
private _addCO2Sensor() { private _addCO2Sensor() {

View File

@ -176,8 +176,16 @@ export class DialogEnergySolarSettings
private async _fetchSolarForecastConfigEntries() { private async _fetchSolarForecastConfigEntries() {
const domains = this._params!.info.solar_forecast_domains; const domains = this._params!.info.solar_forecast_domains;
this._configEntries = (await getConfigEntries(this.hass)).filter((entry) => this._configEntries =
domains.includes(entry.domain) domains.length === 0
? []
: domains.length === 1
? await getConfigEntries(this.hass, {
type: "integration",
domain: domains[0],
})
: (await getConfigEntries(this.hass, { type: "integration" })).filter(
(entry) => domains.includes(entry.domain)
); );
} }

View File

@ -10,50 +10,11 @@ import { customElement, property, state, query } from "lit/decorators";
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import {
deleteCounter,
fetchCounter,
updateCounter,
} from "../../../../../data/counter";
import { import {
ExtEntityRegistryEntry, ExtEntityRegistryEntry,
removeEntityRegistryEntry, removeEntityRegistryEntry,
} from "../../../../../data/entity_registry"; } from "../../../../../data/entity_registry";
import { import { HELPERS_CRUD } from "../../../../../data/helpers_crud";
deleteInputBoolean,
fetchInputBoolean,
updateInputBoolean,
} from "../../../../../data/input_boolean";
import {
deleteInputButton,
fetchInputButton,
updateInputButton,
} from "../../../../../data/input_button";
import {
deleteInputDateTime,
fetchInputDateTime,
updateInputDateTime,
} from "../../../../../data/input_datetime";
import {
deleteInputNumber,
fetchInputNumber,
updateInputNumber,
} from "../../../../../data/input_number";
import {
deleteInputSelect,
fetchInputSelect,
updateInputSelect,
} from "../../../../../data/input_select";
import {
deleteInputText,
fetchInputText,
updateInputText,
} from "../../../../../data/input_text";
import {
deleteTimer,
fetchTimer,
updateTimer,
} from "../../../../../data/timer";
import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box"; import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
@ -69,49 +30,6 @@ import "../../../helpers/forms/ha-timer-form";
import "../../entity-registry-basic-editor"; import "../../entity-registry-basic-editor";
import type { HaEntityRegistryBasicEditor } from "../../entity-registry-basic-editor"; import type { HaEntityRegistryBasicEditor } from "../../entity-registry-basic-editor";
const HELPERS = {
input_boolean: {
fetch: fetchInputBoolean,
update: updateInputBoolean,
delete: deleteInputBoolean,
},
input_button: {
fetch: fetchInputButton,
update: updateInputButton,
delete: deleteInputButton,
},
input_text: {
fetch: fetchInputText,
update: updateInputText,
delete: deleteInputText,
},
input_number: {
fetch: fetchInputNumber,
update: updateInputNumber,
delete: deleteInputNumber,
},
input_datetime: {
fetch: fetchInputDateTime,
update: updateInputDateTime,
delete: deleteInputDateTime,
},
input_select: {
fetch: fetchInputSelect,
update: updateInputSelect,
delete: deleteInputSelect,
},
counter: {
fetch: fetchCounter,
update: updateCounter,
delete: deleteCounter,
},
timer: {
fetch: fetchTimer,
update: updateTimer,
delete: deleteTimer,
},
};
@customElement("entity-settings-helper-tab") @customElement("entity-settings-helper-tab")
export class EntityRegistrySettingsHelper extends LitElement { export class EntityRegistrySettingsHelper extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -198,7 +116,7 @@ export class EntityRegistrySettingsHelper extends LitElement {
} }
private async _getItem() { private async _getItem() {
const items = await HELPERS[this.entry.platform].fetch(this.hass!); const items = await HELPERS_CRUD[this.entry.platform].fetch(this.hass!);
this._item = items.find((item) => item.id === this.entry.unique_id) || null; this._item = items.find((item) => item.id === this.entry.unique_id) || null;
} }
@ -206,7 +124,7 @@ export class EntityRegistrySettingsHelper extends LitElement {
this._submitting = true; this._submitting = true;
try { try {
if (this._componentLoaded && this._item) { if (this._componentLoaded && this._item) {
await HELPERS[this.entry.platform].update( await HELPERS_CRUD[this.entry.platform].update(
this.hass!, this.hass!,
this._item.id, this._item.id,
this._item this._item
@ -236,7 +154,10 @@ export class EntityRegistrySettingsHelper extends LitElement {
try { try {
if (this._componentLoaded && this._item) { if (this._componentLoaded && this._item) {
await HELPERS[this.entry.platform].delete(this.hass!, this._item.id); await HELPERS_CRUD[this.entry.platform].delete(
this.hass!,
this._item.id
);
} else { } else {
const stateObj = this.hass.states[this.entry.entity_id]; const stateObj = this.hass.states[this.entry.entity_id];
if (!stateObj?.attributes.restored) { if (!stateObj?.attributes.restored) {

View File

@ -42,6 +42,12 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail"; import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail";
import {
ConfigEntry,
deleteConfigEntry,
getConfigEntries,
} from "../../../data/config_entries";
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
const OVERRIDE_DEVICE_CLASSES = { const OVERRIDE_DEVICE_CLASSES = {
cover: [ cover: [
@ -83,6 +89,8 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@state() private _device?: DeviceRegistryEntry; @state() private _device?: DeviceRegistryEntry;
@state() private _helperConfigEntry?: ConfigEntry;
@state() private _error?: string; @state() private _error?: string;
@state() private _submitting?: boolean; @state() private _submitting?: boolean;
@ -103,6 +111,20 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
]; ];
} }
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
if (this.entry.config_entry_id) {
getConfigEntries(this.hass, {
type: "helper",
domain: this.entry.platform,
}).then((entries) => {
this._helperConfigEntry = entries.find(
(ent) => ent.entry_id === this.entry.config_entry_id
);
});
}
}
protected updated(changedProperties: PropertyValues) { protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties); super.updated(changedProperties);
if (changedProperties.has("entry")) { if (changedProperties.has("entry")) {
@ -215,6 +237,21 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@value-changed=${this._areaPicked} @value-changed=${this._areaPicked}
></ha-area-picker>` ></ha-area-picker>`
: ""} : ""}
${this._helperConfigEntry
? html`
<div class="row">
<mwc-button
@click=${this._showOptionsFlow}
.disabled=${this._submitting}
>
${this.hass.localize(
"ui.dialogs.entity_registry.editor.configure_state"
)}
</mwc-button>
</div>
`
: ""}
<ha-expansion-panel <ha-expansion-panel
.header=${this.hass.localize( .header=${this.hass.localize(
"ui.dialogs.entity_registry.editor.advanced" "ui.dialogs.entity_registry.editor.advanced"
@ -341,7 +378,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
class="warning" class="warning"
@click=${this._confirmDeleteEntry} @click=${this._confirmDeleteEntry}
.disabled=${this._submitting || .disabled=${this._submitting ||
!(stateObj && stateObj.attributes.restored)} (!this._helperConfigEntry && !stateObj.attributes.restored)}
> >
${this.hass.localize("ui.dialogs.entity_registry.editor.delete")} ${this.hass.localize("ui.dialogs.entity_registry.editor.delete")}
</mwc-button> </mwc-button>
@ -471,13 +508,21 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
this._submitting = true; this._submitting = true;
try { try {
if (this._helperConfigEntry) {
await deleteConfigEntry(this.hass, this._helperConfigEntry.entry_id);
} else {
await removeEntityRegistryEntry(this.hass!, this._origEntityId); await removeEntityRegistryEntry(this.hass!, this._origEntityId);
}
fireEvent(this, "close-dialog"); fireEvent(this, "close-dialog");
} finally { } finally {
this._submitting = false; this._submitting = false;
} }
} }
private async _showOptionsFlow() {
showOptionsFlowDialog(this, this._helperConfigEntry!);
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,

View File

@ -1,11 +1,11 @@
import { Counter } from "../../../data/counter"; import type { Counter } from "../../../data/counter";
import { InputBoolean } from "../../../data/input_boolean"; import type { InputBoolean } from "../../../data/input_boolean";
import { InputButton } from "../../../data/input_button"; import type { InputButton } from "../../../data/input_button";
import { InputDateTime } from "../../../data/input_datetime"; import type { InputDateTime } from "../../../data/input_datetime";
import { InputNumber } from "../../../data/input_number"; import type { InputNumber } from "../../../data/input_number";
import { InputSelect } from "../../../data/input_select"; import type { InputSelect } from "../../../data/input_select";
import { InputText } from "../../../data/input_text"; import type { InputText } from "../../../data/input_text";
import { Timer } from "../../../data/timer"; import type { Timer } from "../../../data/timer";
export const HELPER_DOMAINS = [ export const HELPER_DOMAINS = [
"input_boolean", "input_boolean",

View File

@ -8,6 +8,8 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { dynamicElement } from "../../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../../common/dom/dynamic-element-directive";
import { domainIcon } from "../../../common/entity/domain_icon"; import { domainIcon } from "../../../common/entity/domain_icon";
import "../../../components/ha-dialog"; import "../../../components/ha-dialog";
import "../../../components/ha-circular-progress";
import { getConfigFlowHandlers } from "../../../data/config_flow";
import { createCounter } from "../../../data/counter"; import { createCounter } from "../../../data/counter";
import { createInputBoolean } from "../../../data/input_boolean"; import { createInputBoolean } from "../../../data/input_boolean";
import { createInputButton } from "../../../data/input_button"; import { createInputButton } from "../../../data/input_button";
@ -16,6 +18,7 @@ import { createInputNumber } from "../../../data/input_number";
import { createInputSelect } from "../../../data/input_select"; import { createInputSelect } from "../../../data/input_select";
import { createInputText } from "../../../data/input_text"; import { createInputText } from "../../../data/input_text";
import { createTimer } from "../../../data/timer"; import { createTimer } from "../../../data/timer";
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import { haStyleDialog } from "../../../resources/styles"; import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { Helper } from "./const"; import { Helper } from "./const";
@ -27,6 +30,8 @@ import "./forms/ha-input_number-form";
import "./forms/ha-input_select-form"; import "./forms/ha-input_select-form";
import "./forms/ha-input_text-form"; import "./forms/ha-input_text-form";
import "./forms/ha-timer-form"; import "./forms/ha-timer-form";
import { domainToName } from "../../../data/integration";
import type { ShowDialogHelperDetailParams } from "./show-dialog-helper-detail";
const HELPERS = { const HELPERS = {
input_boolean: createInputBoolean, input_boolean: createInputBoolean,
@ -47,7 +52,7 @@ export class DialogHelperDetail extends LitElement {
@state() private _opened = false; @state() private _opened = false;
@state() private _platform?: string; @state() private _domain?: string;
@state() private _error?: string; @state() private _error?: string;
@ -55,43 +60,39 @@ export class DialogHelperDetail extends LitElement {
@query(".form") private _form?: HTMLDivElement; @query(".form") private _form?: HTMLDivElement;
public async showDialog(): Promise<void> { @state() private _helperFlows?: string[];
this._platform = undefined;
private _params?: ShowDialogHelperDetailParams;
public async showDialog(params: ShowDialogHelperDetailParams): Promise<void> {
this._params = params;
this._domain = undefined;
this._item = undefined; this._item = undefined;
this._opened = true; this._opened = true;
await this.updateComplete; await this.updateComplete;
Promise.all([
getConfigFlowHandlers(this.hass, "helper"),
// Ensure the titles are loaded before we render the flows.
this.hass.loadBackendTranslation("title", undefined, true),
]).then(([flows]) => {
this._helperFlows = flows;
});
} }
public closeDialog(): void { public closeDialog(): void {
this._opened = false; this._opened = false;
this._error = ""; this._error = "";
this._params = undefined;
} }
protected render(): TemplateResult { protected render(): TemplateResult {
return html` let content: TemplateResult;
<ha-dialog
.open=${this._opened} if (this._domain) {
@closed=${this.closeDialog} content = html`
class=${classMap({ "button-left": !this._platform })}
scrimClickAction
escapeKeyAction
.heading=${this._platform
? this.hass.localize(
"ui.panel.config.helpers.dialog.add_platform",
"platform",
this.hass.localize(
`ui.panel.config.helpers.types.${this._platform}`
) || this._platform
)
: this.hass.localize("ui.panel.config.helpers.dialog.add_helper")}
>
${this._platform
? html`
<div class="form" @value-changed=${this._valueChanged}> <div class="form" @value-changed=${this._valueChanged}>
${this._error ${this._error ? html` <div class="error">${this._error}</div> ` : ""}
? html` <div class="error">${this._error}</div> ` ${dynamicElement(`ha-${this._domain}-form`, {
: ""}
${dynamicElement(`ha-${this._platform}-form`, {
hass: this.hass, hass: this.hass,
item: this._item, item: this._item,
new: true, new: true,
@ -111,28 +112,45 @@ export class DialogHelperDetail extends LitElement {
> >
${this.hass!.localize("ui.common.back")} ${this.hass!.localize("ui.common.back")}
</mwc-button> </mwc-button>
` `;
: html` } else if (this._helperFlows === undefined) {
${Object.keys(HELPERS).map((platform: string) => { content = html`<ha-circular-progress active></ha-circular-progress>`;
const isLoaded = isComponentLoaded(this.hass, platform); } else {
const items: [string, string][] = [];
for (const helper of Object.keys(HELPERS)) {
items.push([
helper,
this.hass.localize(`ui.panel.config.helpers.types.${helper}`) ||
helper,
]);
}
for (const domain of this._helperFlows) {
items.push([domain, domainToName(this.hass.localize, domain)]);
}
items.sort((a, b) => a[1].localeCompare(b[1]));
content = html`
${items.map(([domain, label]) => {
// Only OG helpers need to be loaded prior adding one
const isLoaded =
!(domain in HELPERS) || isComponentLoaded(this.hass, domain);
return html` return html`
<mwc-list-item <mwc-list-item
.disabled=${!isLoaded} .disabled=${!isLoaded}
.platform=${platform} .domain=${domain}
@click=${this._platformPicked} @click=${this._domainPicked}
@keydown=${this._handleEnter} @keydown=${this._handleEnter}
dialogInitialFocus dialogInitialFocus
graphic="icon" graphic="icon"
> >
<ha-svg-icon <ha-svg-icon
slot="graphic" slot="graphic"
.path=${domainIcon(platform)} .path=${domainIcon(domain)}
></ha-svg-icon> ></ha-svg-icon>
<span class="item-text"> <span class="item-text"> ${label} </span>
${this.hass.localize(
`ui.panel.config.helpers.types.${platform}`
) || platform}
</span>
</mwc-list-item> </mwc-list-item>
${!isLoaded ${!isLoaded
? html` ? html`
@ -140,7 +158,7 @@ export class DialogHelperDetail extends LitElement {
>${this.hass.localize( >${this.hass.localize(
"ui.dialogs.helper_settings.platform_not_loaded", "ui.dialogs.helper_settings.platform_not_loaded",
"platform", "platform",
platform domain
)}</paper-tooltip )}</paper-tooltip
> >
` `
@ -150,7 +168,27 @@ export class DialogHelperDetail extends LitElement {
<mwc-button slot="primaryAction" @click=${this.closeDialog}> <mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass!.localize("ui.common.cancel")} ${this.hass!.localize("ui.common.cancel")}
</mwc-button> </mwc-button>
`} `;
}
return html`
<ha-dialog
.open=${this._opened}
@closed=${this.closeDialog}
class=${classMap({ "button-left": !this._domain })}
scrimClickAction
escapeKeyAction
.heading=${this._domain
? this.hass.localize(
"ui.panel.config.helpers.dialog.add_platform",
"platform",
this.hass.localize(
`ui.panel.config.helpers.types.${this._domain}`
) || this._domain
)
: this.hass.localize("ui.panel.config.helpers.dialog.add_helper")}
>
${content}
</ha-dialog> </ha-dialog>
`; `;
} }
@ -160,13 +198,13 @@ export class DialogHelperDetail extends LitElement {
} }
private async _createItem(): Promise<void> { private async _createItem(): Promise<void> {
if (!this._platform || !this._item) { if (!this._domain || !this._item) {
return; return;
} }
this._submitting = true; this._submitting = true;
this._error = ""; this._error = "";
try { try {
await HELPERS[this._platform](this.hass, this._item); await HELPERS[this._domain](this.hass, this._item);
this.closeDialog(); this.closeDialog();
} catch (err: any) { } catch (err: any) {
this._error = err.message || "Unknown error"; this._error = err.message || "Unknown error";
@ -181,12 +219,22 @@ export class DialogHelperDetail extends LitElement {
} }
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
this._platformPicked(ev); this._domainPicked(ev);
} }
private _platformPicked(ev: Event): void { private _domainPicked(ev: Event): void {
this._platform = (ev.currentTarget! as any).platform; const domain = (ev.currentTarget! as any).domain;
if (domain in HELPERS) {
this._domain = domain;
this._focusForm(); this._focusForm();
} else {
showConfigFlowDialog(this, {
startFlowHandler: domain,
dialogClosedCallback: this._params!.dialogClosedCallback,
});
this.closeDialog();
}
} }
private async _focusForm(): Promise<void> { private async _focusForm(): Promise<void> {
@ -195,7 +243,7 @@ export class DialogHelperDetail extends LitElement {
} }
private _goBack() { private _goBack() {
this._platform = undefined; this._domain = undefined;
this._item = undefined; this._item = undefined;
this._error = undefined; this._error = undefined;
} }

View File

@ -1,28 +1,58 @@
import { mdiPencilOff, mdiPlus } from "@mdi/js"; import { mdiPencilOff, mdiPlus } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-tooltip/paper-tooltip";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoize from "memoize-one"; import memoizeOne from "memoize-one";
import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { domainIcon } from "../../../common/entity/domain_icon"; import { domainIcon } from "../../../common/entity/domain_icon";
import { LocalizeFunc } from "../../../common/translations/localize";
import { import {
DataTableColumnContainer, DataTableColumnContainer,
RowClickedEvent, RowClickedEvent,
} from "../../../components/data-table/ha-data-table"; } from "../../../components/data-table/ha-data-table";
import "../../../components/ha-fab"; import "../../../components/ha-fab";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-icon"; import "../../../components/ha-icon";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../../data/entity_registry";
import { domainToName } from "../../../data/integration";
import "../../../layouts/hass-loading-screen"; import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage-data-table"; import "../../../layouts/hass-tabs-subpage-data-table";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../types";
import { showEntityEditorDialog } from "../entities/show-dialog-entity-editor"; import { showEntityEditorDialog } from "../entities/show-dialog-entity-editor";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
import { HELPER_DOMAINS } from "./const"; import { HELPER_DOMAINS } from "./const";
import { showHelperDetailDialog } from "./show-dialog-helper-detail"; import { showHelperDetailDialog } from "./show-dialog-helper-detail";
// This groups items by a key but only returns last entry per key.
const groupByOne = <T>(
items: T[],
keySelector: (item: T) => string
): Record<string, T> => {
const result: Record<string, T> = {};
for (const item of items) {
result[keySelector(item)] = item;
}
return result;
};
const getConfigEntry = (
entityEntries: Record<string, EntityRegistryEntry>,
configEntries: Record<string, ConfigEntry>,
entityId: string
) => {
const configEntryId = entityEntries![entityId]?.config_entry_id;
return configEntryId ? configEntries![configEntryId] : undefined;
};
@customElement("ha-config-helpers") @customElement("ha-config-helpers")
export class HaConfigHelpers extends LitElement { export class HaConfigHelpers extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public isWide!: boolean; @property() public isWide!: boolean;
@ -33,13 +63,16 @@ export class HaConfigHelpers extends LitElement {
@state() private _stateItems: HassEntity[] = []; @state() private _stateItems: HassEntity[] = [];
private _columns = memoize((narrow, _language): DataTableColumnContainer => { @state() private _entityEntries?: Record<string, EntityRegistryEntry>;
@state() private _configEntries?: Record<string, ConfigEntry>;
private _columns = memoizeOne(
(narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => {
const columns: DataTableColumnContainer = { const columns: DataTableColumnContainer = {
icon: { icon: {
title: "", title: "",
label: this.hass.localize( label: localize("ui.panel.config.helpers.picker.headers.icon"),
"ui.panel.config.helpers.picker.headers.icon"
),
type: "icon", type: "icon",
template: (icon, helper: any) => template: (icon, helper: any) =>
icon icon
@ -49,9 +82,7 @@ export class HaConfigHelpers extends LitElement {
></ha-svg-icon>`, ></ha-svg-icon>`,
}, },
name: { name: {
title: this.hass.localize( title: localize("ui.panel.config.helpers.picker.headers.name"),
"ui.panel.config.helpers.picker.headers.name"
),
sortable: true, sortable: true,
filterable: true, filterable: true,
grows: true, grows: true,
@ -67,22 +98,22 @@ export class HaConfigHelpers extends LitElement {
}; };
if (!narrow) { if (!narrow) {
columns.entity_id = { columns.entity_id = {
title: this.hass.localize( title: localize("ui.panel.config.helpers.picker.headers.entity_id"),
"ui.panel.config.helpers.picker.headers.entity_id"
),
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "25%", width: "25%",
}; };
} }
columns.type = { columns.type = {
title: this.hass.localize("ui.panel.config.helpers.picker.headers.type"), title: localize("ui.panel.config.helpers.picker.headers.type"),
sortable: true, sortable: true,
width: "25%", width: "25%",
filterable: true, filterable: true,
template: (type) => template: (type, row) =>
html` row.configEntry
${this.hass.localize(`ui.panel.config.helpers.types.${type}`) || type} ? domainToName(localize, type)
: html`
${localize(`ui.panel.config.helpers.types.${type}`) || type}
`, `,
}; };
columns.editable = { columns.editable = {
@ -110,21 +141,44 @@ export class HaConfigHelpers extends LitElement {
`, `,
}; };
return columns; return columns;
}); }
);
private _getItems = memoize((stateItems: HassEntity[]) => private _getItems = memoizeOne(
stateItems.map((entityState) => ({ (
stateItems: HassEntity[],
entityEntries: Record<string, EntityRegistryEntry>,
configEntries: Record<string, ConfigEntry>
) =>
stateItems.map((entityState) => {
const configEntry = getConfigEntry(
entityEntries,
configEntries,
entityState.entity_id
);
return {
id: entityState.entity_id, id: entityState.entity_id,
icon: entityState.attributes.icon, icon: entityState.attributes.icon,
name: entityState.attributes.friendly_name || "", name: entityState.attributes.friendly_name || "",
entity_id: entityState.entity_id, entity_id: entityState.entity_id,
editable: entityState.attributes.editable, editable:
type: computeStateDomain(entityState), configEntry !== undefined || entityState.attributes.editable,
})) type: configEntry
? configEntry.domain
: computeStateDomain(entityState),
configEntry,
};
})
); );
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.hass || this._stateItems === undefined) { if (
!this.hass ||
this._stateItems === undefined ||
this._entityEntries === undefined ||
this._configEntries === undefined
) {
return html` <hass-loading-screen></hass-loading-screen> `; return html` <hass-loading-screen></hass-loading-screen> `;
} }
@ -135,8 +189,12 @@ export class HaConfigHelpers extends LitElement {
back-path="/config" back-path="/config"
.route=${this.route} .route=${this.route}
.tabs=${configSections.automations} .tabs=${configSections.automations}
.columns=${this._columns(this.narrow, this.hass.language)} .columns=${this._columns(this.narrow, this.hass.localize)}
.data=${this._getItems(this._stateItems)} .data=${this._getItems(
this._stateItems,
this._entityEntries,
this._configEntries
)}
@row-click=${this._openEditDialog} @row-click=${this._openEditDialog}
hasFab hasFab
clickable clickable
@ -160,34 +218,69 @@ export class HaConfigHelpers extends LitElement {
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
this._getStates(); this._getConfigEntries();
} }
protected updated(changedProps: PropertyValues) { protected willUpdate(changedProps: PropertyValues) {
super.updated(changedProps); super.willUpdate(changedProps);
if (!this._entityEntries || !this._configEntries) {
return;
}
let changed =
!this._stateItems ||
changedProps.has("_entityEntries") ||
changedProps.has("_configEntries");
if (!changed && changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined; const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (oldHass && this._stateItems) { changed = !oldHass || oldHass.states !== this.hass.states;
this._getStates(oldHass); }
if (!changed) {
return;
}
const extraEntities = new Set<string>();
for (const entityEntry of Object.values(this._entityEntries)) {
if (
entityEntry.config_entry_id &&
entityEntry.config_entry_id in this._configEntries
) {
extraEntities.add(entityEntry.entity_id);
} }
} }
private _getStates(oldHass?: HomeAssistant) { const newStates = Object.values(this.hass!.states).filter(
let changed = false; (entity) =>
const tempStates = Object.values(this.hass!.states).filter((entity) => { extraEntities.has(entity.entity_id) ||
if (!HELPER_DOMAINS.includes(computeStateDomain(entity))) { HELPER_DOMAINS.includes(computeStateDomain(entity))
return false; );
}
if (oldHass?.states[entity.entity_id] !== entity) {
changed = true;
}
return true;
});
if (changed || this._stateItems.length !== tempStates.length) { if (
this._stateItems = tempStates; this._stateItems.length !== newStates.length ||
!this._stateItems.every((val, idx) => newStates[idx] === val)
) {
this._stateItems = newStates;
} }
} }
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entries) => {
this._entityEntries = groupByOne(entries, (entry) => entry.entity_id);
}),
];
}
private async _getConfigEntries() {
this._configEntries = groupByOne(
await getConfigEntries(this.hass, { type: "helper" }),
(entry) => entry.entry_id
);
}
private async _openEditDialog(ev: CustomEvent): Promise<void> { private async _openEditDialog(ev: CustomEvent): Promise<void> {
const entityId = (ev.detail as RowClickedEvent).id; const entityId = (ev.detail as RowClickedEvent).id;
showEntityEditorDialog(this, { showEntityEditorDialog(this, {
@ -196,6 +289,12 @@ export class HaConfigHelpers extends LitElement {
} }
private _createHelpler() { private _createHelpler() {
showHelperDetailDialog(this); showHelperDetailDialog(this, {
dialogClosedCallback: (params) => {
if (params.flowFinished) {
this._getConfigEntries();
}
},
});
} }
} }

View File

@ -1,11 +1,20 @@
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { DataEntryFlowDialogParams } from "../../../dialogs/config-flow/show-dialog-data-entry-flow";
export const loadHelperDetailDialog = () => import("./dialog-helper-detail"); export const loadHelperDetailDialog = () => import("./dialog-helper-detail");
export const showHelperDetailDialog = (element: HTMLElement) => { export interface ShowDialogHelperDetailParams {
// Only used for config entries
dialogClosedCallback: DataEntryFlowDialogParams["dialogClosedCallback"];
}
export const showHelperDetailDialog = (
element: HTMLElement,
params: ShowDialogHelperDetailParams
) => {
fireEvent(element, "show-dialog", { fireEvent(element, "show-dialog", {
dialogTag: "dialog-helper-detail", dialogTag: "dialog-helper-detail",
dialogImport: loadHelperDetailDialog, dialogImport: loadHelperDetailDialog,
dialogParams: {}, dialogParams: params,
}); });
}; };

View File

@ -521,7 +521,8 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
} }
private _loadConfigEntries() { private _loadConfigEntries() {
getConfigEntries(this.hass).then((configEntries) => { getConfigEntries(this.hass, { type: "integration" }).then(
(configEntries) => {
this._configEntries = configEntries this._configEntries = configEntries
.map( .map(
(entry: ConfigEntry): ConfigEntryExtended => ({ (entry: ConfigEntry): ConfigEntryExtended => ({
@ -538,7 +539,8 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
conf2.localized_domain_name + conf2.title conf2.localized_domain_name + conf2.title
) )
); );
}); }
);
} }
private async _scanUSBDevices() { private async _scanUSBDevices() {
@ -656,7 +658,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
if (!domain) { if (!domain) {
return; return;
} }
const handlers = await getConfigFlowHandlers(this.hass); const handlers = await getConfigFlowHandlers(this.hass, "integration");
if (!handlers.includes(domain)) { if (!handlers.includes(domain)) {
showAlertDialog(this, { showAlertDialog(this, {

View File

@ -111,7 +111,9 @@ class HaPanelDevMqtt extends LitElement {
return; return;
} }
const configEntryId = searchParams.get("config_entry") as string; const configEntryId = searchParams.get("config_entry") as string;
const configEntries = await getConfigEntries(this.hass); const configEntries = await getConfigEntries(this.hass, {
domain: "mqtt",
});
const configEntry = configEntries.find( const configEntry = configEntries.find(
(entry) => entry.entry_id === configEntryId (entry) => entry.entry_id === configEntryId
); );

View File

@ -384,7 +384,9 @@ class ZWaveJSConfigDashboard extends LitElement {
if (!this.configEntryId) { if (!this.configEntryId) {
return; return;
} }
const configEntries = await getConfigEntries(this.hass); const configEntries = await getConfigEntries(this.hass, {
domain: "zwave_js",
});
this._configEntry = configEntries.find( this._configEntry = configEntries.find(
(entry) => entry.entry_id === this.configEntryId! (entry) => entry.entry_id === this.configEntryId!
); );
@ -467,7 +469,9 @@ class ZWaveJSConfigDashboard extends LitElement {
if (!this.configEntryId) { if (!this.configEntryId) {
return; return;
} }
const configEntries = await getConfigEntries(this.hass); const configEntries = await getConfigEntries(this.hass, {
domain: "zwave_js",
});
const configEntry = configEntries.find( const configEntry = configEntries.find(
(entry) => entry.entry_id === this.configEntryId (entry) => entry.entry_id === this.configEntryId
); );

View File

@ -823,7 +823,8 @@
"area": "Set entity area only", "area": "Set entity area only",
"area_note": "By default the entities of a device are in the same area as the device. If you change the area of this entity, it will no longer follow the area of the device.", "area_note": "By default the entities of a device are in the same area as the device. If you change the area of this entity, it will no longer follow the area of the device.",
"follow_device_area": "Follow device area", "follow_device_area": "Follow device area",
"change_device_area": "Change device area" "change_device_area": "Change device area",
"configure_state": "Configure State"
} }
}, },
"helper_settings": { "helper_settings": {