diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index c55578ffec..5bbb751eb2 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: 90 days stale policy - uses: actions/stale@v5.2.0 + uses: actions/stale@v6.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 90 diff --git a/demo/src/ha-demo.ts b/demo/src/ha-demo.ts index b583245e5a..b80c4d71de 100644 --- a/demo/src/ha-demo.ts +++ b/demo/src/ha-demo.ts @@ -68,6 +68,7 @@ class HaDemo extends HomeAssistantAppEl { hidden_by: null, entity_category: null, has_entity_name: false, + unique_id: "co2_intensity", }, { config_entry_id: "co2signal", @@ -82,6 +83,7 @@ class HaDemo extends HomeAssistantAppEl { hidden_by: null, entity_category: null, has_entity_name: false, + unique_id: "grid_fossil_fuel_percentage", }, ]); diff --git a/demo/src/stubs/energy.ts b/demo/src/stubs/energy.ts index 00e7ef7c3f..80f10b968c 100644 --- a/demo/src/stubs/energy.ts +++ b/demo/src/stubs/energy.ts @@ -11,14 +11,12 @@ export const mockEnergy = (hass: MockHomeAssistant) => { { stat_energy_from: "sensor.energy_consumption_tarif_1", stat_cost: "sensor.energy_consumption_tarif_1_cost", - entity_energy_from: "sensor.energy_consumption_tarif_1", entity_energy_price: null, number_energy_price: null, }, { stat_energy_from: "sensor.energy_consumption_tarif_2", stat_cost: "sensor.energy_consumption_tarif_2_cost", - entity_energy_from: "sensor.energy_consumption_tarif_2", entity_energy_price: null, number_energy_price: null, }, @@ -27,14 +25,12 @@ export const mockEnergy = (hass: MockHomeAssistant) => { { stat_energy_to: "sensor.energy_production_tarif_1", stat_compensation: "sensor.energy_production_tarif_1_compensation", - entity_energy_to: "sensor.energy_production_tarif_1", entity_energy_price: null, number_energy_price: null, }, { stat_energy_to: "sensor.energy_production_tarif_2", stat_compensation: "sensor.energy_production_tarif_2_compensation", - entity_energy_to: "sensor.energy_production_tarif_2", entity_energy_price: null, number_energy_price: null, }, @@ -55,7 +51,6 @@ export const mockEnergy = (hass: MockHomeAssistant) => { type: "gas", stat_energy_from: "sensor.energy_gas", stat_cost: "sensor.energy_gas_cost", - entity_energy_from: "sensor.energy_gas", entity_energy_price: null, number_energy_price: null, }, diff --git a/gallery/src/pages/misc/integration-card.ts b/gallery/src/pages/misc/integration-card.ts index 7d3fc2eda4..5b6b064803 100644 --- a/gallery/src/pages/misc/integration-card.ts +++ b/gallery/src/pages/misc/integration-card.ts @@ -196,6 +196,7 @@ const createEntityRegistryEntries = ( icon: null, platform: "updater", has_entity_name: false, + unique_id: "updater", }, ]; diff --git a/script/core b/script/core index 8ac6da6284..93b7021d57 100755 --- a/script/core +++ b/script/core @@ -46,6 +46,14 @@ frontend: # development_repo: ${WD}" >> "${WD}/config/configuration.yaml" fi + if [ ! -z "${CODESPACES}" ]; then + echo " +http: + use_x_forwarded_for: true + trusted_proxies: + - 127.0.0.1 +" >> "${WD}/config/configuration.yaml" + fi fi hass -c "${WD}/config" diff --git a/src/components/ha-selector/ha-selector-select.ts b/src/components/ha-selector/ha-selector-select.ts index d051b5f125..c454d339cd 100644 --- a/src/components/ha-selector/ha-selector-select.ts +++ b/src/components/ha-selector/ha-selector-select.ts @@ -41,7 +41,7 @@ export class HaSelectSelector extends LitElement { ); if (!this.selector.select.custom_value && this._mode === "list") { - if (!this.selector.select.multiple || this.required) { + if (!this.selector.select.multiple) { return html`
${this.label} @@ -64,7 +64,8 @@ export class HaSelectSelector extends LitElement { return html`
- ${this.label}${options.map( + ${this.label} + ${options.map( (item: SelectOption) => html` !item.disabled && !this.value?.includes(item.value) + (option) => !option.disabled && !value?.includes(option.value) )} @filter-changed=${this._filterChanged} @value-changed=${this._comboBoxValueChanged} @@ -290,6 +291,9 @@ export class HaSelectSelector extends LitElement { ha-formfield { display: block; } + mwc-list-item[disabled] { + --mdc-theme-text-primary-on-background: var(--disabled-text-color); + } `; } diff --git a/src/data/energy.ts b/src/data/energy.ts index c8bb6cf55a..8420419978 100644 --- a/src/data/energy.ts +++ b/src/data/energy.ts @@ -29,7 +29,6 @@ export const emptyFlowFromGridSourceEnergyPreference = (): FlowFromGridSourceEnergyPreference => ({ stat_energy_from: "", stat_cost: null, - entity_energy_from: null, entity_energy_price: null, number_energy_price: null, }); @@ -38,7 +37,6 @@ export const emptyFlowToGridSourceEnergyPreference = (): FlowToGridSourceEnergyPreference => ({ stat_energy_to: "", stat_compensation: null, - entity_energy_to: null, entity_energy_price: null, number_energy_price: null, }); @@ -68,7 +66,6 @@ export const emptyGasEnergyPreference = (): GasSourceTypeEnergyPreference => ({ type: "gas", stat_energy_from: "", stat_cost: null, - entity_energy_from: null, entity_energy_price: null, number_energy_price: null, }); @@ -93,7 +90,6 @@ export interface FlowFromGridSourceEnergyPreference { stat_cost: string | null; // Can be used to generate costs if stat_cost omitted - entity_energy_from: string | null; entity_energy_price: string | null; number_energy_price: number | null; } @@ -105,8 +101,7 @@ export interface FlowToGridSourceEnergyPreference { // $ meter stat_compensation: string | null; - // Can be used to generate costs if stat_cost omitted - entity_energy_to: string | null; + // Can be used to generate costs if stat_compensation omitted entity_energy_price: string | null; number_energy_price: number | null; } @@ -142,7 +137,6 @@ export interface GasSourceTypeEnergyPreference { stat_cost: string | null; // Can be used to generate costs if stat_cost omitted - entity_energy_from: string | null; entity_energy_price: string | null; number_energy_price: number | null; unit_of_measurement?: string | null; diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts index 78da19f6f2..3802b26dca 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -20,10 +20,10 @@ export interface EntityRegistryEntry { entity_category: "config" | "diagnostic" | null; has_entity_name: boolean; original_name?: string; + unique_id: string; } export interface ExtEntityRegistryEntry extends EntityRegistryEntry { - unique_id: string; capabilities: Record; original_icon?: string; device_class?: string; @@ -61,7 +61,7 @@ export interface EntityRegistryEntryUpdateParams { hidden_by: string | null; new_entity_id?: string; options_domain?: string; - options?: SensorEntityOptions | WeatherEntityOptions; + options?: SensorEntityOptions | NumberEntityOptions | WeatherEntityOptions; } export const findBatteryEntity = ( diff --git a/src/data/lovelace.ts b/src/data/lovelace.ts index 28d1c4886f..432cb6167b 100644 --- a/src/data/lovelace.ts +++ b/src/data/lovelace.ts @@ -93,6 +93,8 @@ export interface LovelaceViewConfig { panel?: boolean; background?: string; visible?: boolean | ShowViewConfig[]; + subview?: boolean; + back_path?: string; } export interface LovelaceViewElement extends HTMLElement { diff --git a/src/data/zha.ts b/src/data/zha.ts index fb0e2b0f31..55bf16d7c4 100644 --- a/src/data/zha.ts +++ b/src/data/zha.ts @@ -309,7 +309,7 @@ export const fetchCommandsForCluster = ( cluster_type: clusterType, }); -export const fetchClustersForZhaNode = ( +export const fetchClustersForZhaDevice = ( hass: HomeAssistant, ieeeAddress: string ): Promise => diff --git a/src/dialogs/more-info/ha-more-info-dialog.ts b/src/dialogs/more-info/ha-more-info-dialog.ts index f07a3c3863..d47a02fbad 100644 --- a/src/dialogs/more-info/ha-more-info-dialog.ts +++ b/src/dialogs/more-info/ha-more-info-dialog.ts @@ -90,7 +90,7 @@ export class MoreInfoDialog extends LitElement { const stateObj = this.hass.states[entityId]; const domain = computeDomain(entityId); - const name = stateObj ? computeStateName(stateObj) : entityId; + const name = (stateObj && computeStateName(stateObj)) || entityId; const tabs = this._getTabs(entityId, this.hass.user!.is_admin); return html` diff --git a/src/panels/config/application_credentials/dialog-add-application-credential.ts b/src/panels/config/application_credentials/dialog-add-application-credential.ts index e01ded0e87..f1fc6ecf87 100644 --- a/src/panels/config/application_credentials/dialog-add-application-credential.ts +++ b/src/panels/config/application_credentials/dialog-add-application-credential.ts @@ -1,8 +1,9 @@ import "@material/mwc-button"; import "@material/mwc-list/mwc-list-item"; +import { mdiOpenInNew } from "@mdi/js"; +import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-circular-progress"; import "../../../components/ha-combo-box"; @@ -10,14 +11,15 @@ import { createCloseHeading } from "../../../components/ha-dialog"; import "../../../components/ha-markdown"; import "../../../components/ha-textfield"; import { - fetchApplicationCredentialsConfig, - createApplicationCredential, - ApplicationCredentialsConfig, ApplicationCredential, + ApplicationCredentialsConfig, + createApplicationCredential, + fetchApplicationCredentialsConfig, } from "../../../data/application_credential"; import { domainToName } from "../../../data/integration"; import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; +import { documentationUrl } from "../../../util/documentation-url"; import { AddApplicationCredentialDialogParams } from "./show-dialog-add-application-credential"; interface Domain { @@ -98,6 +100,25 @@ export class DialogAddApplicationCredential extends LitElement { >
${this._error ? html`
${this._error}
` : ""} +

+ ${this.hass.localize( + "ui.panel.config.application_credentials.editor.description" + )} +
+ + ${this.hass!.localize( + "ui.panel.config.application_credentials.editor.view_documentation" + )} + + +

${this._loading @@ -163,15 +192,18 @@ export class DialogAddApplicationCredential extends LitElement {
` : html` + + ${this.hass.localize("ui.common.cancel")} + ${this.hass.localize( - "ui.panel.config.application_credentials.editor.create" + "ui.panel.config.application_credentials.editor.add" )} `} @@ -213,7 +245,7 @@ export class DialogAddApplicationCredential extends LitElement { this.closeDialog(); } - private async _createApplicationCredential(ev) { + private async _addApplicationCredential(ev) { ev.preventDefault(); if (!this._domain || !this._clientId || !this._clientSecret) { return; @@ -260,6 +292,12 @@ export class DialogAddApplicationCredential extends LitElement { display: block; margin-bottom: 24px; } + a { + text-decoration: none; + } + a ha-svg-icon { + --mdc-icon-size: 16px; + } `, ]; } diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index 627a052450..e7c7236fd3 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -575,10 +575,15 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { private async _deleteConfirm() { showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.automation.picker.delete_confirm_title" + ), text: this.hass.localize( - "ui.panel.config.automation.picker.delete_confirm" + "ui.panel.config.automation.picker.delete_confirm_text", + { name: this._config?.alias } ), confirmText: this.hass!.localize("ui.common.delete"), + destructive: true, dismissText: this.hass!.localize("ui.common.cancel"), confirm: () => this._delete(), }); diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index dc395aefaf..3fe409f34b 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -341,12 +341,17 @@ class HaAutomationPicker extends LitElement { private async _deleteConfirm(automation) { showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.automation.picker.delete_confirm_title" + ), text: this.hass.localize( - "ui.panel.config.automation.picker.delete_confirm" + "ui.panel.config.automation.picker.delete_confirm_text", + { name: automation.name } ), confirmText: this.hass!.localize("ui.common.delete"), dismissText: this.hass!.localize("ui.common.cancel"), confirm: () => this._delete(automation), + destructive: true, }); } diff --git a/src/panels/config/blueprint/dialog-import-blueprint.ts b/src/panels/config/blueprint/dialog-import-blueprint.ts index ef43378c16..1b97d52e8d 100644 --- a/src/panels/config/blueprint/dialog-import-blueprint.ts +++ b/src/panels/config/blueprint/dialog-import-blueprint.ts @@ -1,4 +1,5 @@ import "@material/mwc-button"; +import { mdiOpenInNew } from "@mdi/js"; import { css, html, LitElement, TemplateResult } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; @@ -105,50 +106,61 @@ class DialogImportBlueprint extends LitElement { >
${this._result.raw_data}
` - : html`${this.hass.localize( - "ui.panel.config.blueprint.add.import_introduction_link", - "community_link", - html`${this.hass.localize( - "ui.panel.config.blueprint.add.community_forums" - )}` - )} + ${this.hass.localize( + "ui.panel.config.blueprint.add.import_introduction" + )} +

+ + ${this.hass.localize( + "ui.panel.config.blueprint.add.community_forums" + )} + + + `} + >
+ `}
+ + ${this.hass.localize("ui.common.cancel")} + ${!this._result - ? html` - ${this._importing - ? html`` - : ""} - ${this.hass.localize("ui.panel.config.blueprint.add.import_btn")} - ` - : html` - ${this.hass.localize("ui.common.cancel")} + ${this._importing + ? html`` + : ""} + ${this.hass.localize( + "ui.panel.config.blueprint.add.import_btn" + )} + ` + : html` ` : ""} ${this.hass.localize("ui.panel.config.blueprint.add.save_btn")} - `} + + `} `; } @@ -215,9 +228,19 @@ class DialogImportBlueprint extends LitElement { static styles = [ haStyleDialog, css` + p { + margin-top: 0; + margin-bottom: 8px; + } ha-textfield { display: block; - margin-top: 8px; + margin-top: 24px; + } + a { + text-decoration: none; + } + a ha-svg-icon { + --mdc-icon-size: 16px; } `, ]; diff --git a/src/panels/config/devices/device-detail/integration-elements/zha/device-actions.ts b/src/panels/config/devices/device-detail/integration-elements/zha/device-actions.ts index e9e17a3f20..907393e7c1 100644 --- a/src/panels/config/devices/device-detail/integration-elements/zha/device-actions.ts +++ b/src/panels/config/devices/device-detail/integration-elements/zha/device-actions.ts @@ -1,9 +1,7 @@ import { mdiCogRefresh, mdiDelete, - mdiDrawPen, mdiFamilyTree, - mdiFileTree, mdiGroup, mdiPlus, } from "@mdi/js"; @@ -12,9 +10,7 @@ import type { DeviceRegistryEntry } from "../../../../../../data/device_registry import { fetchZHADevice } from "../../../../../../data/zha"; import { showConfirmationDialog } from "../../../../../../dialogs/generic/show-dialog-box"; import type { HomeAssistant } from "../../../../../../types"; -import { showZHAClusterDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-cluster"; -import { showZHADeviceChildrenDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-device-children"; -import { showZHADeviceZigbeeInfoDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-device-zigbee-info"; +import { showZHAManageZigbeeDeviceDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-manage-zigbee-device"; import { showZHAReconfigureDeviceDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-reconfigure-device"; import type { DeviceAction } from "../../../ha-config-device-page"; @@ -59,13 +55,6 @@ export const getZHADeviceActions = async ( icon: mdiPlus, action: () => navigate(`/config/zha/add/${zhaDevice!.ieee}`), }, - { - label: hass.localize( - "ui.dialogs.zha_device_info.buttons.device_children" - ), - icon: mdiFileTree, - action: () => showZHADeviceChildrenDialog(el, { device: zhaDevice! }), - }, ] ); } @@ -73,16 +62,10 @@ export const getZHADeviceActions = async ( actions.push( ...[ { - label: hass.localize( - "ui.dialogs.zha_device_info.buttons.zigbee_information" - ), - icon: mdiDrawPen, - action: () => showZHADeviceZigbeeInfoDialog(el, { device: zhaDevice }), - }, - { - label: hass.localize("ui.dialogs.zha_device_info.buttons.clusters"), + label: hass.localize("ui.dialogs.zha_device_info.buttons.manage"), icon: mdiGroup, - action: () => showZHAClusterDialog(el, { device: zhaDevice }), + action: () => + showZHAManageZigbeeDeviceDialog(el, { device: zhaDevice }), }, { label: hass.localize("ui.dialogs.zha_device_info.buttons.view_network"), diff --git a/src/panels/config/devices/device-detail/integration-elements/zha/ha-device-info-zha.ts b/src/panels/config/devices/device-detail/integration-elements/zha/ha-device-info-zha.ts index 5ccaf7c5e5..3f1f0fad16 100644 --- a/src/panels/config/devices/device-detail/integration-elements/zha/ha-device-info-zha.ts +++ b/src/panels/config/devices/device-detail/integration-elements/zha/ha-device-info-zha.ts @@ -23,6 +23,7 @@ export class HaDeviceActionsZha extends LitElement { @state() private _zhaDevice?: ZHADevice; protected updated(changedProperties: PropertyValues) { + super.updated(changedProperties); if (changedProperties.has("device")) { const zigbeeConnection = this.device.connections.find( (conn) => conn[0] === "zigbee" diff --git a/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts index 2b382a40f4..e98621f903 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts @@ -280,7 +280,6 @@ export class DialogEnergyGasSettings this._source = { ...this._source!, stat_energy_from: ev.detail.value, - entity_energy_from: ev.detail.value, }; } diff --git a/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts index a2d688f2b3..c2cab0d7b2 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts @@ -269,9 +269,6 @@ export class DialogEnergyGridFlowSettings [this._params!.direction === "from" ? "stat_energy_from" : "stat_energy_to"]: ev.detail.value, - [this._params!.direction === "from" - ? "entity_energy_from" - : "entity_energy_to"]: ev.detail.value, }; } diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index 55ca2ac749..68e286d4f2 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -111,9 +111,12 @@ const OVERRIDE_NUMBER_UNITS = { }; const OVERRIDE_SENSOR_UNITS = { + distance: ["cm", "ft", "in", "km", "m", "mi", "mm", "yd"], mass: ["g", "kg", "lb", "mg", "oz", "µg"], pressure: ["hPa", "Pa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"], + speed: ["ft/s", "in/d", "in/h", "km/h", "kn", "m/s", "mm/d", "mph"], temperature: ["°C", "°F", "K"], + volume: ["fl. oz.", "ft³", "gal", "L", "mL", "m³"], }; const OVERRIDE_WEATHER_UNITS = { diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index ac02d2e048..8e3448994f 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -68,10 +68,12 @@ import type { HomeAssistant, Route } from "../../../types"; import { configSections } from "../ha-panel-config"; import "../integrations/ha-integration-overflow-menu"; -export interface StateEntity extends Omit { +export interface StateEntity + extends Omit { readonly?: boolean; selectable?: boolean; id?: string; + unique_id?: string; } export interface EntityRow extends StateEntity { diff --git a/src/panels/config/integrations/integration-panels/zha/dialog-zha-cluster.ts b/src/panels/config/integrations/integration-panels/zha/dialog-zha-cluster.ts deleted file mode 100644 index 84ea96c629..0000000000 --- a/src/panels/config/integrations/integration-panels/zha/dialog-zha-cluster.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { HASSDomEvent } from "../../../../../common/dom/fire_event"; -import "../../../../../components/ha-code-editor"; -import { createCloseHeading } from "../../../../../components/ha-dialog"; -import { - Cluster, - fetchBindableDevices, - fetchGroups, - ZHADevice, - ZHAGroup, -} from "../../../../../data/zha"; -import { haStyleDialog } from "../../../../../resources/styles"; -import { HomeAssistant } from "../../../../../types"; -import { sortZHADevices, sortZHAGroups } from "./functions"; -import { ZHADeviceZigbeeInfoDialogParams } from "./show-dialog-zha-device-zigbee-info"; -import { ZHAClusterSelectedParams } from "./types"; -import "./zha-cluster-attributes"; -import "./zha-cluster-commands"; -import "./zha-clusters"; -import "./zha-device-binding"; -import "./zha-group-binding"; - -@customElement("dialog-zha-cluster") -class DialogZHACluster extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @state() private _device?: ZHADevice; - - @state() private _selectedCluster?: Cluster; - - @state() private _bindableDevices: ZHADevice[] = []; - - @state() private _groups: ZHAGroup[] = []; - - public async showDialog( - params: ZHADeviceZigbeeInfoDialogParams - ): Promise { - this._device = params.device; - } - - protected updated(changedProperties: PropertyValues): void { - super.update(changedProperties); - if (changedProperties.has("_device")) { - this._fetchData(); - } - } - - protected render(): TemplateResult { - if (!this._device) { - return html``; - } - - return html` - - - ${this._selectedCluster - ? html` - - - ` - : ""} - ${this._bindableDevices.length > 0 - ? html` - - ` - : ""} - ${this._device && this._groups.length > 0 - ? html` - - ` - : ""} - - `; - } - - private _onClusterSelected( - selectedClusterEvent: HASSDomEvent - ): void { - this._selectedCluster = selectedClusterEvent.detail.cluster; - } - - private _close(): void { - this._device = undefined; - } - - private async _fetchData(): Promise { - if (this._device && this.hass) { - this._bindableDevices = - this._device && this._device.device_type !== "Coordinator" - ? (await fetchBindableDevices(this.hass, this._device.ieee)).sort( - sortZHADevices - ) - : []; - this._groups = (await fetchGroups(this.hass!)).sort(sortZHAGroups); - } - } - - static get styles(): CSSResultGroup { - return haStyleDialog; - } -} - -declare global { - interface HTMLElementTagNameMap { - "dialog-zha-cluster": DialogZHACluster; - } -} diff --git a/src/panels/config/integrations/integration-panels/zha/dialog-zha-device-zigbee-info.ts b/src/panels/config/integrations/integration-panels/zha/dialog-zha-device-zigbee-info.ts deleted file mode 100644 index b9334e46d4..0000000000 --- a/src/panels/config/integrations/integration-panels/zha/dialog-zha-device-zigbee-info.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import "../../../../../components/ha-code-editor"; -import { createCloseHeading } from "../../../../../components/ha-dialog"; -import { haStyleDialog } from "../../../../../resources/styles"; -import { HomeAssistant } from "../../../../../types"; -import { ZHADeviceZigbeeInfoDialogParams } from "./show-dialog-zha-device-zigbee-info"; - -@customElement("dialog-zha-device-zigbee-info") -class DialogZHADeviceZigbeeInfo extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @state() private _signature: any; - - public async showDialog( - params: ZHADeviceZigbeeInfoDialogParams - ): Promise { - this._signature = JSON.stringify( - { - ...params.device.signature, - manufacturer: params.device.manufacturer, - model: params.device.model, - class: params.device.quirk_class, - }, - null, - 2 - ); - } - - protected render(): TemplateResult { - if (!this._signature) { - return html``; - } - - return html` - - - - - `; - } - - private _close(): void { - this._signature = undefined; - } - - static get styles(): CSSResultGroup { - return haStyleDialog; - } -} - -declare global { - interface HTMLElementTagNameMap { - "dialog-zha-device-zigbee-info": DialogZHADeviceZigbeeInfo; - } -} diff --git a/src/panels/config/integrations/integration-panels/zha/dialog-zha-manage-zigbee-device.ts b/src/panels/config/integrations/integration-panels/zha/dialog-zha-manage-zigbee-device.ts new file mode 100644 index 0000000000..5ccacb9d90 --- /dev/null +++ b/src/panels/config/integrations/integration-panels/zha/dialog-zha-manage-zigbee-device.ts @@ -0,0 +1,291 @@ +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { mdiClose } from "@mdi/js"; +import { customElement, property, state } from "lit/decorators"; +import { cache } from "lit/directives/cache"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import "../../../../../components/ha-code-editor"; +import { createCloseHeading } from "../../../../../components/ha-dialog"; +import { + fetchBindableDevices, + fetchGroups, + ZHADevice, + ZHAGroup, +} from "../../../../../data/zha"; +import { haStyleDialog } from "../../../../../resources/styles"; +import { HomeAssistant } from "../../../../../types"; +import { sortZHADevices, sortZHAGroups } from "./functions"; +import "./zha-cluster-attributes"; +import "./zha-cluster-commands"; +import "./zha-manage-clusters"; +import "./zha-device-binding"; +import "./zha-group-binding"; +import "./zha-device-children"; +import "./zha-device-signature"; +import { + Tab, + ZHAManageZigbeeDeviceDialogParams, +} from "./show-dialog-zha-manage-zigbee-device"; +import "../../../../../components/ha-header-bar"; +import "@material/mwc-tab-bar/mwc-tab-bar"; +import "@material/mwc-tab/mwc-tab"; + +@customElement("dialog-zha-manage-zigbee-device") +class DialogZHAManageZigbeeDevice extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean, reflect: true }) public large = false; + + @state() private _currTab: Tab = "clusters"; + + @state() private _device?: ZHADevice; + + @state() private _bindableDevices: ZHADevice[] = []; + + @state() private _groups: ZHAGroup[] = []; + + public async showDialog( + params: ZHAManageZigbeeDeviceDialogParams + ): Promise { + this._device = params.device; + if (!this._device) { + this.closeDialog(); + return; + } + this._currTab = params.tab || "clusters"; + this.large = false; + } + + public closeDialog() { + this._device = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + this.addEventListener("close-dialog", () => this.closeDialog()); + } + + protected willUpdate(changedProps: PropertyValues) { + super.willUpdate(changedProps); + if (!this._device) { + return; + } + if (changedProps.has("_device")) { + const tabs = this._getTabs(this._device); + if (!tabs.includes(this._currTab)) { + this._currTab = tabs[0]; + } + this._fetchData(); + } + } + + protected render(): TemplateResult { + if (!this._device) { + return html``; + } + + const tabs = this._getTabs(this._device); + + return html` + +
+ + +
+ ${this.hass.localize("ui.dialogs.zha_manage_device.heading")} +
+
+ + ${tabs.map( + (tab) => html` + + ` + )} + +
+ +
+ ${cache( + this._currTab === "clusters" + ? html` + + ` + : this._currTab === "bindings" + ? html` + ${this._bindableDevices.length > 0 + ? html` + + ` + : ""} + ${this._device && this._groups.length > 0 + ? html` + + ` + : ""} + ` + : this._currTab === "signature" + ? html` + + ` + : html` + + ` + )} +
+
+ `; + } + + private async _fetchData(): Promise { + if (this._device && this.hass) { + this._bindableDevices = + this._device && this._device.device_type !== "Coordinator" + ? (await fetchBindableDevices(this.hass, this._device.ieee)).sort( + sortZHADevices + ) + : []; + this._groups = (await fetchGroups(this.hass!)).sort(sortZHAGroups); + } + } + + private _enlarge() { + this.large = !this.large; + } + + private _handleTabChanged(ev: CustomEvent): void { + const newTab = this._getTabs(this._device)[ev.detail.index]; + if (newTab === this._currTab) { + return; + } + this._currTab = newTab; + } + + private _getTabs = memoizeOne((device: ZHADevice | undefined) => { + const tabs: Tab[] = ["clusters", "bindings", "signature"]; + + if ( + device && + (device.device_type === "Router" || device.device_type === "Coordinator") + ) { + tabs.push("children"); + } + + return tabs; + }); + + static get styles(): CSSResultGroup { + return [ + haStyleDialog, + css` + ha-dialog { + --dialog-surface-position: static; + --dialog-content-position: static; + --vertial-align-dialog: flex-start; + } + + ha-header-bar { + --mdc-theme-on-primary: var(--primary-text-color); + --mdc-theme-primary: var(--mdc-theme-surface); + flex-shrink: 0; + display: block; + } + .content { + outline: none; + } + @media all and (max-width: 450px), all and (max-height: 500px) { + ha-header-bar { + --mdc-theme-primary: var(--app-header-background-color); + --mdc-theme-on-primary: var(--app-header-text-color, white); + border-bottom: none; + } + } + + .heading { + border-bottom: 1px solid + var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12)); + } + + @media all and (min-width: 600px) and (min-height: 501px) { + ha-dialog { + --mdc-dialog-min-width: 560px; + --mdc-dialog-max-width: 560px; + --dialog-surface-margin-top: 40px; + --mdc-dialog-max-height: calc(100% - 72px); + } + + .main-title { + overflow: hidden; + text-overflow: ellipsis; + cursor: default; + } + + :host([large]) ha-dialog, + ha-dialog[data-domain="camera"] { + --mdc-dialog-min-width: 90vw; + --mdc-dialog-max-width: 90vw; + } + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-zha-manage-zigbee-device": DialogZHAManageZigbeeDevice; + } +} diff --git a/src/panels/config/integrations/integration-panels/zha/dialog-zha-reconfigure-device.ts b/src/panels/config/integrations/integration-panels/zha/dialog-zha-reconfigure-device.ts index 1ae26ad04d..4c43266385 100644 --- a/src/panels/config/integrations/integration-panels/zha/dialog-zha-reconfigure-device.ts +++ b/src/panels/config/integrations/integration-panels/zha/dialog-zha-reconfigure-device.ts @@ -12,7 +12,7 @@ import { Cluster, ClusterConfigurationEvent, ClusterConfigurationStatus, - fetchClustersForZhaNode, + fetchClustersForZhaDevice, reconfigureNode, ZHA_CHANNEL_CFG_DONE, ZHA_CHANNEL_MSG_BIND, @@ -321,16 +321,16 @@ class DialogZHAReconfigureDevice extends LitElement { return; } this._clusterConfigurationStatuses = new Map( - (await fetchClustersForZhaNode(this.hass, this._params.device.ieee)).map( - (cluster: Cluster) => [ - cluster.id, - { - cluster: cluster, - bindSuccess: undefined, - attributes: new Map(), - }, - ] - ) + ( + await fetchClustersForZhaDevice(this.hass, this._params.device.ieee) + ).map((cluster: Cluster) => [ + cluster.id, + { + cluster: cluster, + bindSuccess: undefined, + attributes: new Map(), + }, + ]) ); this._subscribe(this._params); this._status = "started"; diff --git a/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-cluster.ts b/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-cluster.ts deleted file mode 100644 index 9b0c44ad5d..0000000000 --- a/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-cluster.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { fireEvent } from "../../../../../common/dom/fire_event"; -import { ZHADevice } from "../../../../../data/zha"; - -export interface ZHAClusterDialogParams { - device: ZHADevice; -} - -export const loadZHAClusterDialog = () => import("./dialog-zha-cluster"); - -export const showZHAClusterDialog = ( - element: HTMLElement, - params: ZHAClusterDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-zha-cluster", - dialogImport: loadZHAClusterDialog, - dialogParams: params, - }); -}; diff --git a/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-device-children.ts b/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-device-children.ts deleted file mode 100644 index f8a3e64ab0..0000000000 --- a/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-device-children.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { fireEvent } from "../../../../../common/dom/fire_event"; -import { ZHADevice } from "../../../../../data/zha"; - -export interface ZHADeviceChildrenDialogParams { - device: ZHADevice; -} - -export const loadZHADeviceChildrenDialog = () => - import("./dialog-zha-device-children"); - -export const showZHADeviceChildrenDialog = ( - element: HTMLElement, - zhaDeviceChildrenParams: ZHADeviceChildrenDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-zha-device-children", - dialogImport: loadZHADeviceChildrenDialog, - dialogParams: zhaDeviceChildrenParams, - }); -}; diff --git a/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-device-zigbee-info.ts b/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-device-zigbee-info.ts deleted file mode 100644 index e4a2191563..0000000000 --- a/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-device-zigbee-info.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { fireEvent } from "../../../../../common/dom/fire_event"; -import { ZHADevice } from "../../../../../data/zha"; - -export interface ZHADeviceZigbeeInfoDialogParams { - device: ZHADevice; -} - -export const loadZHADeviceZigbeeInfoDialog = () => - import("./dialog-zha-device-zigbee-info"); - -export const showZHADeviceZigbeeInfoDialog = ( - element: HTMLElement, - zhaDeviceZigbeeInfoParams: ZHADeviceZigbeeInfoDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-zha-device-zigbee-info", - dialogImport: loadZHADeviceZigbeeInfoDialog, - dialogParams: zhaDeviceZigbeeInfoParams, - }); -}; diff --git a/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-manage-zigbee-device.ts b/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-manage-zigbee-device.ts new file mode 100644 index 0000000000..a2854748ff --- /dev/null +++ b/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-manage-zigbee-device.ts @@ -0,0 +1,23 @@ +import { fireEvent } from "../../../../../common/dom/fire_event"; +import { ZHADevice } from "../../../../../data/zha"; + +export type Tab = "clusters" | "bindings" | "signature" | "children"; + +export interface ZHAManageZigbeeDeviceDialogParams { + device: ZHADevice; + tab?: Tab; +} + +export const loadZHAManageZigbeeDeviceDialog = () => + import("./dialog-zha-manage-zigbee-device"); + +export const showZHAManageZigbeeDeviceDialog = ( + element: HTMLElement, + params: ZHAManageZigbeeDeviceDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-zha-manage-zigbee-device", + dialogImport: loadZHAManageZigbeeDeviceDialog, + dialogParams: params, + }); +}; diff --git a/src/panels/config/integrations/integration-panels/zha/types.ts b/src/panels/config/integrations/integration-panels/zha/types.ts index dbc2954f2d..3e9af96a18 100644 --- a/src/panels/config/integrations/integration-panels/zha/types.ts +++ b/src/panels/config/integrations/integration-panels/zha/types.ts @@ -1,5 +1,5 @@ import { HaSelect } from "../../../../../components/ha-select"; -import { Cluster, ZHADevice } from "../../../../../data/zha"; +import { ZHADevice } from "../../../../../data/zha"; export interface ItemSelectedEvent { target?: HaSelect; @@ -41,10 +41,6 @@ export interface ZHADeviceSelectedParams { node: ZHADevice; } -export interface ZHAClusterSelectedParams { - cluster: Cluster; -} - export interface NodeServiceData { ieee_address: string; } diff --git a/src/panels/config/integrations/integration-panels/zha/zha-cluster-attributes.ts b/src/panels/config/integrations/integration-panels/zha/zha-cluster-attributes.ts index c764a135b3..a0a3695dea 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-cluster-attributes.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-cluster-attributes.ts @@ -1,6 +1,4 @@ -import "@material/mwc-button"; import "@material/mwc-list/mwc-list-item"; -import { mdiHelpCircle } from "@mdi/js"; import "@polymer/paper-input/paper-input"; import { css, @@ -10,13 +8,12 @@ import { PropertyValues, TemplateResult, } from "lit"; -import { property, state } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import { stopPropagation } from "../../../../../common/dom/stop_propagation"; import "../../../../../components/buttons/ha-call-service-button"; import "../../../../../components/ha-card"; -import "../../../../../components/ha-icon-button"; import "../../../../../components/ha-select"; -import "../../../../../components/ha-service-description"; +import "../../../../../components/buttons/ha-progress-button"; import { Attribute, Cluster, @@ -27,7 +24,7 @@ import { } from "../../../../../data/zha"; import { haStyle } from "../../../../../resources/styles"; import { HomeAssistant } from "../../../../../types"; -import "../../../ha-config-section"; +import { forwardHaptic } from "../../../../../data/haptics"; import { formatAsPaddedHex } from "./functions"; import { ChangeEvent, @@ -35,18 +32,15 @@ import { SetAttributeServiceData, } from "./types"; +@customElement("zha-cluster-attributes") export class ZHAClusterAttributes extends LitElement { @property({ attribute: false }) public hass?: HomeAssistant; - @property() public isWide?: boolean; - - @property() public showHelp = false; - - @property() public selectedNode?: ZHADevice; + @property() public device?: ZHADevice; @property() public selectedCluster?: Cluster; - @state() private _attributes: Attribute[] = []; + @state() private _attributes: Attribute[] | undefined; @state() private _selectedAttributeId?: number; @@ -54,78 +48,52 @@ export class ZHAClusterAttributes extends LitElement { @state() private _manufacturerCodeOverride?: string | number; + @state() private _readingAttribute = false; + @state() private _setAttributeServiceData?: SetAttributeServiceData; protected updated(changedProperties: PropertyValues): void { if (changedProperties.has("selectedCluster")) { - this._attributes = []; + this._attributes = undefined; this._selectedAttributeId = undefined; this._attributeValue = ""; this._fetchAttributesForCluster(); } - super.update(changedProperties); + super.updated(changedProperties); } protected render(): TemplateResult { + if (!this.device || !this.selectedCluster || !this._attributes) { + return html``; + } return html` - -
- - ${this.hass!.localize( - "ui.panel.config.zha.cluster_attributes.header" + +
+ - - -
- - ${this.hass!.localize( - "ui.panel.config.zha.cluster_attributes.introduction" - )} - - - -
- - ${this._attributes.map( - (entry) => html` - - ${entry.name + " (id: " + formatAsPaddedHex(entry.id) + ")"} - - ` - )} - -
- ${this.showHelp - ? html` -
- ${this.hass!.localize( - "ui.panel.config.zha.cluster_attributes.help_attribute_dropdown" - )} -
+ ${this._attributes.map( + (entry) => html` + + ${`${entry.name} (id: ${formatAsPaddedHex(entry.id)})`} + ` - : ""} - ${this._selectedAttributeId !== undefined - ? this._renderAttributeInteractions() - : ""} -
- + )} + +
+ ${this._selectedAttributeId !== undefined + ? this._renderAttributeInteractions() + : ""} + `; } @@ -152,20 +120,15 @@ export class ZHAClusterAttributes extends LitElement { >
- + ${this.hass!.localize( - "ui.panel.config.zha.cluster_attributes.get_zigbee_attribute" + "ui.panel.config.zha.cluster_attributes.read_zigbee_attribute" )} - - ${this.showHelp - ? html` -
- ${this.hass!.localize( - "ui.panel.config.zha.cluster_attributes.help_get_zigbee_attribute" - )} -
- ` - : ""} + ${this.hass!.localize( - "ui.panel.config.zha.cluster_attributes.set_zigbee_attribute" + "ui.panel.config.zha.cluster_attributes.write_zigbee_attribute" )} - ${this.showHelp - ? html` - - ` - : ""}
`; } private async _fetchAttributesForCluster(): Promise { - if (this.selectedNode && this.selectedCluster && this.hass) { + if (this.device && this.selectedCluster && this.hass) { this._attributes = await fetchAttributesForCluster( this.hass, - this.selectedNode!.ieee, + this.device!.ieee, this.selectedCluster!.endpoint_id, this.selectedCluster!.id, this.selectedCluster!.type ); this._attributes.sort((a, b) => a.name.localeCompare(b.name)); + if (this._attributes.length > 0) { + this._selectedAttributeId = this._attributes[0].id; + } } } private _computeReadAttributeServiceData(): | ReadAttributeServiceData | undefined { - if (!this.selectedCluster || !this.selectedNode) { + if (!this.selectedCluster || !this.device) { return undefined; } return { - ieee: this.selectedNode!.ieee, + ieee: this.device!.ieee, endpoint_id: this.selectedCluster!.endpoint_id, cluster_id: this.selectedCluster!.id, cluster_type: this.selectedCluster!.type, @@ -224,11 +180,11 @@ export class ZHAClusterAttributes extends LitElement { private _computeSetAttributeServiceData(): | SetAttributeServiceData | undefined { - if (!this.selectedCluster || !this.selectedNode) { + if (!this.selectedCluster || !this.device) { return undefined; } return { - ieee: this.selectedNode!.ieee, + ieee: this.device!.ieee, endpoint_id: this.selectedCluster!.endpoint_id, cluster_id: this.selectedCluster!.id, cluster_type: this.selectedCluster!.type, @@ -250,17 +206,24 @@ export class ZHAClusterAttributes extends LitElement { this._setAttributeServiceData = this._computeSetAttributeServiceData(); } - private async _onGetZigbeeAttributeClick(): Promise { + private async _onGetZigbeeAttributeClick(ev: CustomEvent): Promise { + const button = ev.currentTarget as any; const data = this._computeReadAttributeServiceData(); if (data && this.hass) { - this._attributeValue = await readAttributeValue(this.hass, data); + this._readingAttribute = true; + try { + this._attributeValue = await readAttributeValue(this.hass, data); + forwardHaptic("success"); + button.actionSuccess(); + } catch (err: any) { + forwardHaptic("failure"); + button.actionError(); + } finally { + this._readingAttribute = false; + } } } - private _onHelpTap(): void { - this.showHelp = !this.showHelp; - } - private _selectedAttributeChanged(event: ItemSelectedEvent): void { this._selectedAttributeId = Number(event.target!.value); this._attributeValue = ""; @@ -278,14 +241,6 @@ export class ZHAClusterAttributes extends LitElement { width: 100%; } - .content { - margin-top: 24px; - } - - ha-card { - max-width: 680px; - } - .card-actions.warning ha-call-service-button { color: var(--error-color); } @@ -306,33 +261,6 @@ export class ZHAClusterAttributes extends LitElement { .header { flex-grow: 1; } - - .toggle-help-icon { - float: right; - top: -6px; - right: 0; - padding-right: 0px; - color: var(--primary-color); - } - - ha-service-description { - display: block; - color: grey; - } - - [hidden] { - display: none; - } - .help-text { - color: grey; - padding-left: 28px; - padding-right: 28px; - padding-bottom: 16px; - } - .help-text2 { - color: grey; - padding: 16px; - } `, ]; } @@ -343,5 +271,3 @@ declare global { "zha-cluster-attributes": ZHAClusterAttributes; } } - -customElements.define("zha-cluster-attributes", ZHAClusterAttributes); diff --git a/src/panels/config/integrations/integration-panels/zha/zha-cluster-commands.ts b/src/panels/config/integrations/integration-panels/zha/zha-cluster-commands.ts index ca3a29be5f..129e42e74f 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-cluster-commands.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-cluster-commands.ts @@ -1,5 +1,4 @@ import "@material/mwc-list/mwc-list-item"; -import { mdiHelpCircle } from "@mdi/js"; import "@polymer/paper-input/paper-input"; import { css, @@ -13,9 +12,7 @@ import { property, state } from "lit/decorators"; import { stopPropagation } from "../../../../../common/dom/stop_propagation"; import "../../../../../components/buttons/ha-call-service-button"; import "../../../../../components/ha-card"; -import "../../../../../components/ha-icon-button"; import "../../../../../components/ha-select"; -import "../../../../../components/ha-service-description"; import { Cluster, Command, @@ -24,7 +21,6 @@ import { } from "../../../../../data/zha"; import { haStyle } from "../../../../../resources/styles"; import { HomeAssistant } from "../../../../../types"; -import "../../../ha-config-section"; import { formatAsPaddedHex } from "./functions"; import { ChangeEvent, IssueCommandServiceData } from "./types"; @@ -33,13 +29,11 @@ export class ZHAClusterCommands extends LitElement { @property() public isWide?: boolean; - @property() public selectedNode?: ZHADevice; + @property() public device?: ZHADevice; @property() public selectedCluster?: Cluster; - @state() private _showHelp = false; - - @state() private _commands: Command[] = []; + @state() private _commands: Command[] | undefined; @state() private _selectedCommandId?: number; @@ -50,132 +44,97 @@ export class ZHAClusterCommands extends LitElement { protected updated(changedProperties: PropertyValues): void { if (changedProperties.has("selectedCluster")) { - this._commands = []; + this._commands = undefined; this._selectedCommandId = undefined; this._fetchCommandsForCluster(); } - super.update(changedProperties); + super.updated(changedProperties); } protected render(): TemplateResult { + if (!this.device || !this.selectedCluster || !this._commands) { + return html``; + } return html` - -
- - ${this.hass!.localize( - "ui.panel.config.zha.cluster_commands.header" + +
+ - - + ${this._commands.map( + (entry) => html` + + ${entry.name + " (id: " + formatAsPaddedHex(entry.id) + ")"} + + ` + )} +
- - ${this.hass!.localize( - "ui.panel.config.zha.cluster_commands.introduction" - )} - - - -
- - ${this._commands.map( - (entry) => html` - - ${entry.name + " (id: " + formatAsPaddedHex(entry.id) + ")"} - - ` - )} - -
- ${this._showHelp - ? html` -
- ${this.hass!.localize( - "ui.panel.config.zha.cluster_commands.help_command_dropdown" + ${this._selectedCommandId !== undefined + ? html` +
+ - ` - : ""} - ${this._selectedCommandId !== undefined - ? html` -
- -
-
- - ${this.hass!.localize( - "ui.panel.config.zha.cluster_commands.issue_zigbee_command" - )} - - ${this._showHelp - ? html` - - ` - : ""} -
- ` - : ""} - - + type="number" + .value=${this._manufacturerCodeOverride} + @value-changed=${this._onManufacturerCodeOverrideChanged} + placeholder=${this.hass!.localize( + "ui.panel.config.zha.common.value" + )} + >
+
+
+ + ${this.hass!.localize( + "ui.panel.config.zha.cluster_commands.issue_zigbee_command" + )} + +
+ ` + : ""} + `; } private async _fetchCommandsForCluster(): Promise { - if (this.selectedNode && this.selectedCluster && this.hass) { + if (this.device && this.selectedCluster && this.hass) { this._commands = await fetchCommandsForCluster( this.hass, - this.selectedNode!.ieee, + this.device!.ieee, this.selectedCluster!.endpoint_id, this.selectedCluster!.id, this.selectedCluster!.type ); this._commands.sort((a, b) => a.name.localeCompare(b.name)); + if (this._commands.length > 0) { + this._selectedCommandId = this._commands[0].id; + } } } private _computeIssueClusterCommandServiceData(): | IssueCommandServiceData | undefined { - if (!this.selectedNode || !this.selectedCluster) { + if (!this.device || !this.selectedCluster || !this._commands) { return undefined; } return { - ieee: this.selectedNode!.ieee, + ieee: this.device!.ieee, endpoint_id: this.selectedCluster!.endpoint_id, cluster_id: this.selectedCluster!.id, cluster_type: this.selectedCluster!.type, @@ -192,10 +151,6 @@ export class ZHAClusterCommands extends LitElement { this._computeIssueClusterCommandServiceData(); } - private _onHelpTap(): void { - this._showHelp = !this._showHelp; - } - private _selectedCommandChanged(event): void { this._selectedCommandId = Number(event.target.value); this._issueClusterCommandServiceData = @@ -213,14 +168,6 @@ export class ZHAClusterCommands extends LitElement { width: 100%; } - .content { - margin-top: 24px; - } - - ha-card { - max-width: 680px; - } - .card-actions.warning ha-call-service-button { color: var(--error-color); } @@ -238,18 +185,6 @@ export class ZHAClusterCommands extends LitElement { padding-bottom: 10px; } - .help-text { - color: grey; - padding-left: 28px; - padding-right: 28px; - padding-bottom: 16px; - } - - .help-text2 { - color: grey; - padding: 16px; - } - .header { flex-grow: 1; } @@ -261,15 +196,6 @@ export class ZHAClusterCommands extends LitElement { padding-right: 0px; color: var(--primary-color); } - - ha-service-description { - display: block; - color: grey; - } - - [hidden] { - display: none; - } `, ]; } diff --git a/src/panels/config/integrations/integration-panels/zha/zha-clusters-data-table.ts b/src/panels/config/integrations/integration-panels/zha/zha-clusters-data-table.ts index f4f0e5fdbf..9cebc071b2 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-clusters-data-table.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-clusters-data-table.ts @@ -59,7 +59,7 @@ export class ZHAClustersDataTable extends LitElement { title: "ID", template: (id: number) => html` ${formatAsPaddedHex(id)} `, sortable: true, - width: "15%", + width: "25%", }, endpoint_id: { title: "Endpoint ID", diff --git a/src/panels/config/integrations/integration-panels/zha/zha-clusters.ts b/src/panels/config/integrations/integration-panels/zha/zha-clusters.ts deleted file mode 100644 index 19f2d270d2..0000000000 --- a/src/panels/config/integrations/integration-panels/zha/zha-clusters.ts +++ /dev/null @@ -1,195 +0,0 @@ -import "@material/mwc-list/mwc-list-item"; -import { mdiHelpCircle } from "@mdi/js"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { property, state } from "lit/decorators"; -import { fireEvent } from "../../../../../common/dom/fire_event"; -import { stopPropagation } from "../../../../../common/dom/stop_propagation"; -import "../../../../../components/buttons/ha-call-service-button"; -import "../../../../../components/ha-card"; -import "../../../../../components/ha-icon-button"; -import "../../../../../components/ha-select"; -import "../../../../../components/ha-service-description"; -import { - Cluster, - fetchClustersForZhaNode, - ZHADevice, -} from "../../../../../data/zha"; -import { haStyle } from "../../../../../resources/styles"; -import { HomeAssistant } from "../../../../../types"; -import "../../../ha-config-section"; -import { computeClusterKey } from "./functions"; - -declare global { - // for fire event - interface HASSDomEvents { - "zha-cluster-selected": { - cluster?: Cluster; - }; - } -} - -export class ZHAClusters extends LitElement { - @property({ attribute: false }) public hass?: HomeAssistant; - - @property() public isWide?: boolean; - - @property() public selectedDevice?: ZHADevice; - - @property() public showHelp = false; - - @state() private _selectedClusterIndex = -1; - - @state() private _clusters: Cluster[] = []; - - protected updated(changedProperties: PropertyValues): void { - if (changedProperties.has("selectedDevice")) { - this._clusters = []; - this._selectedClusterIndex = -1; - fireEvent(this, "zha-cluster-selected", { - cluster: undefined, - }); - this._fetchClustersForZhaNode(); - } - super.update(changedProperties); - } - - protected render(): TemplateResult { - return html` - -
- - -
- - ${this.hass!.localize("ui.panel.config.zha.clusters.introduction")} - - - -
- - ${this._clusters.map( - (entry, idx) => html` - ${computeClusterKey(entry)} - ` - )} - -
- ${this.showHelp - ? html` -
- ${this.hass!.localize( - "ui.panel.config.zha.clusters.help_cluster_dropdown" - )} -
- ` - : ""} -
-
- `; - } - - private async _fetchClustersForZhaNode(): Promise { - if (this.hass) { - this._clusters = await fetchClustersForZhaNode( - this.hass, - this.selectedDevice!.ieee - ); - this._clusters.sort((a, b) => a.name.localeCompare(b.name)); - } - } - - private _selectedClusterChanged(event): void { - this._selectedClusterIndex = Number(event.target!.value); - fireEvent(this, "zha-cluster-selected", { - cluster: this._clusters[this._selectedClusterIndex], - }); - } - - private _onHelpTap(): void { - this.showHelp = !this.showHelp; - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - ha-select { - margin-top: 16px; - } - .menu { - width: 100%; - } - - .content { - margin-top: 24px; - } - - .header { - flex-grow: 1; - } - - ha-card { - max-width: 680px; - } - - .node-picker { - align-items: center; - padding-left: 28px; - padding-right: 28px; - padding-bottom: 10px; - } - - .toggle-help-icon { - float: right; - top: -6px; - right: 0; - padding-right: 0px; - color: var(--primary-color); - } - - [hidden] { - display: none; - } - - .help-text { - color: grey; - padding-left: 28px; - padding-right: 28px; - padding-bottom: 16px; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "zha-cluster": ZHAClusters; - } -} - -customElements.define("zha-clusters", ZHAClusters); diff --git a/src/panels/config/integrations/integration-panels/zha/zha-device-binding.ts b/src/panels/config/integrations/integration-panels/zha/zha-device-binding.ts index 1445a572d5..993aaad79e 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-device-binding.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-device-binding.ts @@ -1,6 +1,4 @@ -import "@material/mwc-button/mwc-button"; import "@material/mwc-list/mwc-list-item"; -import { mdiHelpCircle } from "@mdi/js"; import { css, CSSResultGroup, @@ -11,26 +9,19 @@ import { } from "lit"; import { customElement, property, state } from "lit/decorators"; import { stopPropagation } from "../../../../../common/dom/stop_propagation"; -import "../../../../../components/buttons/ha-call-service-button"; +import "../../../../../components/buttons/ha-progress-button"; import "../../../../../components/ha-card"; -import "../../../../../components/ha-icon-button"; import "../../../../../components/ha-select"; -import "../../../../../components/ha-service-description"; import { bindDevices, unbindDevices, ZHADevice } from "../../../../../data/zha"; import { haStyle } from "../../../../../resources/styles"; import { HomeAssistant } from "../../../../../types"; -import "../../../ha-config-section"; import { ItemSelectedEvent } from "./types"; @customElement("zha-device-binding-control") export class ZHADeviceBindingControl extends LitElement { @property({ attribute: false }) public hass?: HomeAssistant; - @property() public isWide?: boolean; - - @property() public selectedDevice?: ZHADevice; - - @state() private _showHelp = false; + @property() public device?: ZHADevice; @state() private _bindTargetIndex = -1; @@ -38,77 +29,58 @@ export class ZHADeviceBindingControl extends LitElement { @state() private _deviceToBind?: ZHADevice; + @state() private _bindingOperationInProgress = false; + protected updated(changedProperties: PropertyValues): void { - if (changedProperties.has("selectedDevice")) { + if (changedProperties.has("device")) { this._bindTargetIndex = -1; } - super.update(changedProperties); + super.updated(changedProperties); } protected render(): TemplateResult { return html` - -
- Device Binding - +
+ - -
- Bind and unbind devices. - - -
- - ${this.bindableDevices.map( - (device, idx) => html` - - ${device.user_given_name - ? device.user_given_name - : device.name} - - ` - )} - -
- ${this._showHelp - ? html` -
- Select a device to issue a bind command. -
+ ${this.bindableDevices.map( + (device, idx) => html` + + ${device.user_given_name + ? device.user_given_name + : device.name} + ` - : ""} -
- Bind - ${this._showHelp - ? html`
Bind devices.
` - : ""} - Unbind - ${this._showHelp - ? html`
Unbind devices.
` - : ""} -
-
- + )} + +
+
+ + ${this.hass!.localize("ui.panel.config.zha.device_binding.bind")} + + + ${this.hass!.localize("ui.panel.config.zha.device_binding.unbind")} + +
+ `; } @@ -120,27 +92,41 @@ export class ZHADeviceBindingControl extends LitElement { : this.bindableDevices[this._bindTargetIndex]; } - private _onHelpTap(): void { - this._showHelp = !this._showHelp; - } - - private async _onBindDevicesClick(): Promise { - if (this.hass && this._deviceToBind && this.selectedDevice) { - await bindDevices( - this.hass, - this.selectedDevice.ieee, - this._deviceToBind.ieee - ); + private async _onBindDevicesClick(ev: CustomEvent): Promise { + const button = ev.currentTarget as any; + if (this.hass && this._deviceToBind && this.device) { + this._bindingOperationInProgress = true; + button.progress = true; + try { + await bindDevices(this.hass, this.device.ieee, this._deviceToBind.ieee); + button.actionSuccess(); + } catch (err: any) { + button.actionError(); + } finally { + this._bindingOperationInProgress = false; + button.progress = false; + } } } - private async _onUnbindDevicesClick(): Promise { - if (this.hass && this._deviceToBind && this.selectedDevice) { - await unbindDevices( - this.hass, - this.selectedDevice.ieee, - this._deviceToBind.ieee - ); + private async _onUnbindDevicesClick(ev: CustomEvent): Promise { + const button = ev.currentTarget as any; + if (this.hass && this._deviceToBind && this.device) { + this._bindingOperationInProgress = true; + button.progress = true; + try { + await unbindDevices( + this.hass, + this.device.ieee, + this._deviceToBind.ieee + ); + button.actionSuccess(); + } catch (err: any) { + button.actionError(); + } finally { + this._bindingOperationInProgress = false; + button.progress = false; + } } } @@ -152,18 +138,6 @@ export class ZHADeviceBindingControl extends LitElement { width: 100%; } - .content { - margin-top: 24px; - } - - ha-card { - max-width: 680px; - } - - .card-actions.warning ha-call-service-button { - color: var(--error-color); - } - .command-picker { align-items: center; padding-left: 28px; @@ -171,33 +145,9 @@ export class ZHADeviceBindingControl extends LitElement { padding-bottom: 10px; } - .helpText { - color: grey; - padding-left: 28px; - padding-right: 28px; - padding-bottom: 10px; - } - .header { flex-grow: 1; } - - .toggle-help-icon { - float: right; - top: -6px; - right: 0; - padding-right: 0px; - color: var(--primary-color); - } - - ha-service-description { - display: block; - color: grey; - } - - [hidden] { - display: none; - } `, ]; } diff --git a/src/panels/config/integrations/integration-panels/zha/dialog-zha-device-children.ts b/src/panels/config/integrations/integration-panels/zha/zha-device-children.ts similarity index 52% rename from src/panels/config/integrations/integration-panels/zha/dialog-zha-device-children.ts rename to src/panels/config/integrations/integration-panels/zha/zha-device-children.ts index 91bd86246c..52a1bdf1b3 100644 --- a/src/panels/config/integrations/integration-panels/zha/dialog-zha-device-children.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-device-children.ts @@ -1,12 +1,9 @@ -import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import memoizeOne from "memoize-one"; import { customElement, property, state } from "lit/decorators"; import { computeRTLDirection } from "../../../../../common/util/compute_rtl"; import "../../../../../components/ha-code-editor"; -import { createCloseHeading } from "../../../../../components/ha-dialog"; -import { haStyleDialog } from "../../../../../resources/styles"; import { HomeAssistant } from "../../../../../types"; -import { ZHADeviceChildrenDialogParams } from "./show-dialog-zha-device-children"; import "../../../../../components/data-table/ha-data-table"; import type { DataTableColumnContainer, @@ -14,7 +11,6 @@ import type { } from "../../../../../components/data-table/ha-data-table"; import "../../../../../components/ha-circular-progress"; import { fetchDevices, ZHADevice } from "../../../../../data/zha"; -import { fireEvent } from "../../../../../common/dom/fire_event"; export interface DeviceRowData extends DataTableRowData { id: string; @@ -22,14 +18,21 @@ export interface DeviceRowData extends DataTableRowData { lqi: number; } -@customElement("dialog-zha-device-children") -class DialogZHADeviceChildren extends LitElement { +@customElement("zha-device-children") +class ZHADeviceChildren extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @state() private _device: ZHADevice | undefined; + @property() public device: ZHADevice | undefined; @state() private _devices: Map | undefined; + protected updated(changedProperties: PropertyValues) { + super.updated(changedProperties); + if (this.hass && changedProperties.has("device")) { + this._fetchData(); + } + } + private _deviceChildren = memoizeOne( ( device: ZHADevice | undefined, @@ -69,70 +72,45 @@ class DialogZHADeviceChildren extends LitElement { }, }; - public showDialog(params: ZHADeviceChildrenDialogParams): void { - this._device = params.device; - this._fetchData(); - } - - public closeDialog(): void { - this._device = undefined; - this._devices = undefined; - fireEvent(this, "dialog-closed", { dialog: this.localName }); - } - protected render(): TemplateResult { - if (!this._device) { + if (!this.device) { return html``; } return html` - - ${!this._devices - ? html`` - : html``} - + ${!this._devices + ? html`` + : html``} `; } private async _fetchData(): Promise { - if (this._device && this.hass) { + if (this.device && this.hass) { const devices = await fetchDevices(this.hass!); this._devices = new Map( devices.map((device: ZHADevice) => [device.ieee, device]) ); } } - - static get styles(): CSSResultGroup { - return haStyleDialog; - } } declare global { interface HTMLElementTagNameMap { - "dialog-zha-device-children": DialogZHADeviceChildren; + "zha-device-children": ZHADeviceChildren; } } diff --git a/src/panels/config/integrations/integration-panels/zha/zha-device-signature.ts b/src/panels/config/integrations/integration-panels/zha/zha-device-signature.ts new file mode 100644 index 0000000000..0ce2b9767a --- /dev/null +++ b/src/panels/config/integrations/integration-panels/zha/zha-device-signature.ts @@ -0,0 +1,47 @@ +import { html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import "../../../../../components/ha-code-editor"; +import { ZHADevice } from "../../../../../data/zha"; +import { HomeAssistant } from "../../../../../types"; + +@customElement("zha-device-zigbee-info") +class ZHADeviceZigbeeInfo extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public device: ZHADevice | undefined; + + @state() private _signature: any; + + protected updated(changedProperties: PropertyValues): void { + if (changedProperties.has("device") && this.hass && this.device) { + this._signature = JSON.stringify( + { + ...this.device.signature, + manufacturer: this.device.manufacturer, + model: this.device.model, + class: this.device.quirk_class, + }, + null, + 2 + ); + } + super.updated(changedProperties); + } + + protected render(): TemplateResult { + if (!this._signature) { + return html``; + } + + return html` + + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "zha-device-zigbee-info": ZHADeviceZigbeeInfo; + } +} diff --git a/src/panels/config/integrations/integration-panels/zha/zha-group-binding.ts b/src/panels/config/integrations/integration-panels/zha/zha-group-binding.ts index 67671e9047..d96b27c9ad 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-group-binding.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-group-binding.ts @@ -1,5 +1,3 @@ -import "@material/mwc-button/mwc-button"; -import { mdiHelpCircle } from "@mdi/js"; import { css, CSSResultGroup, @@ -11,37 +9,29 @@ import { import { customElement, property, state, query } from "lit/decorators"; import type { HASSDomEvent } from "../../../../../common/dom/fire_event"; import { stopPropagation } from "../../../../../common/dom/stop_propagation"; -import "../../../../../components/buttons/ha-call-service-button"; +import "../../../../../components/buttons/ha-progress-button"; import { SelectionChangedEvent } from "../../../../../components/data-table/ha-data-table"; import "../../../../../components/ha-card"; -import "../../../../../components/ha-icon-button"; -import "../../../../../components/ha-service-description"; import { bindDeviceToGroup, Cluster, - fetchClustersForZhaNode, + fetchClustersForZhaDevice, unbindDeviceFromGroup, ZHADevice, ZHAGroup, } from "../../../../../data/zha"; import { haStyle } from "../../../../../resources/styles"; import type { HomeAssistant } from "../../../../../types"; -import "../../../ha-config-section"; import { ItemSelectedEvent } from "./types"; import "./zha-clusters-data-table"; import type { ZHAClustersDataTable } from "./zha-clusters-data-table"; +import "@material/mwc-list/mwc-list-item"; @customElement("zha-group-binding-control") export class ZHAGroupBindingControl extends LitElement { @property({ attribute: false }) public hass?: HomeAssistant; - @property() public isWide?: boolean; - - @property() public narrow?: boolean; - - @property() public selectedDevice?: ZHADevice; - - @state() private _showHelp = false; + @property() public device?: ZHADevice; @state() private _bindTargetIndex = -1; @@ -51,6 +41,8 @@ export class ZHAGroupBindingControl extends LitElement { @state() private _clusters: Cluster[] = []; + @state() private _bindingOperationInProgress = false; + private _groupToBind?: ZHAGroup; private _clustersToBind?: Cluster[]; @@ -59,38 +51,17 @@ export class ZHAGroupBindingControl extends LitElement { private _zhaClustersDataTable!: ZHAClustersDataTable; protected updated(changedProperties: PropertyValues): void { - if (changedProperties.has("selectedDevice")) { + if (changedProperties.has("device")) { this._bindTargetIndex = -1; this._selectedClusters = []; this._clustersToBind = []; this._fetchClustersForZhaNode(); } - super.update(changedProperties); + super.updated(changedProperties); } protected render(): TemplateResult { return html` - -
- ${this.hass!.localize( - "ui.panel.config.zha.group_binding.header" - )} - - -
- ${this.hass!.localize( - "ui.panel.config.zha.group_binding.introduction" - )} -
- ${this._showHelp - ? html` -
- ${this.hass!.localize( - "ui.panel.config.zha.group_binding.group_picker_help" - )} -
- ` - : ""}
- ${this._showHelp - ? html` -
- ${this.hass!.localize( - "ui.panel.config.zha.group_binding.cluster_selection_help" - )} -
- ` - : ""}
- ${this.hass!.localize( - "ui.panel.config.zha.group_binding.bind_button_label" - )} - ${this._showHelp - ? html` -
- ${this.hass!.localize( - "ui.panel.config.zha.group_binding.bind_button_help" - )} -
- ` - : ""} - ${this.hass!.localize( - "ui.panel.config.zha.group_binding.unbind_button_label" - )} - ${this._showHelp - ? html` -
- ${this.hass!.localize( - "ui.panel.config.zha.group_binding.unbind_button_help" - )} -
- ` - : ""} + + ${this.hass!.localize( + "ui.panel.config.zha.group_binding.bind_button_label" + )} + + + + ${this.hass!.localize( + "ui.panel.config.zha.group_binding.unbind_button_label" + )} +
@@ -186,31 +123,49 @@ export class ZHAGroupBindingControl extends LitElement { : this.groups[this._bindTargetIndex]; } - private _onHelpTap(): void { - this._showHelp = !this._showHelp; - } - - private async _onBindGroupClick(): Promise { + private async _onBindGroupClick(ev: CustomEvent): Promise { + const button = ev.currentTarget as any; if (this.hass && this._canBind) { - await bindDeviceToGroup( - this.hass, - this.selectedDevice!.ieee, - this._groupToBind!.group_id, - this._clustersToBind! - ); - this._zhaClustersDataTable.clearSelection(); + this._bindingOperationInProgress = true; + button.progress = true; + try { + await bindDeviceToGroup( + this.hass, + this.device!.ieee, + this._groupToBind!.group_id, + this._clustersToBind! + ); + this._zhaClustersDataTable.clearSelection(); + button.actionSuccess(); + } catch (err: any) { + button.actionError(); + } finally { + this._bindingOperationInProgress = false; + button.progress = false; + } } } - private async _onUnbindGroupClick(): Promise { + private async _onUnbindGroupClick(ev: CustomEvent): Promise { + const button = ev.currentTarget as any; if (this.hass && this._canBind) { - await unbindDeviceFromGroup( - this.hass, - this.selectedDevice!.ieee, - this._groupToBind!.group_id, - this._clustersToBind! - ); - this._zhaClustersDataTable.clearSelection(); + this._bindingOperationInProgress = true; + button.progress = true; + try { + await unbindDeviceFromGroup( + this.hass, + this.device!.ieee, + this._groupToBind!.group_id, + this._clustersToBind! + ); + this._zhaClustersDataTable.clearSelection(); + button.actionSuccess(); + } catch (err: any) { + button.actionError(); + } finally { + this._bindingOperationInProgress = false; + button.progress = false; + } } } @@ -230,9 +185,9 @@ export class ZHAGroupBindingControl extends LitElement { private async _fetchClustersForZhaNode(): Promise { if (this.hass) { - this._clusters = await fetchClustersForZhaNode( + this._clusters = await fetchClustersForZhaDevice( this.hass, - this.selectedDevice!.ieee + this.device!.ieee ); this._clusters = this._clusters .filter((cluster) => cluster.type.toLowerCase() === "out") @@ -245,7 +200,7 @@ export class ZHAGroupBindingControl extends LitElement { this._groupToBind && this._clustersToBind && this._clustersToBind?.length > 0 && - this.selectedDevice + this.device ); } @@ -257,18 +212,6 @@ export class ZHAGroupBindingControl extends LitElement { width: 100%; } - .content { - margin-top: 24px; - } - - ha-card { - max-width: 680px; - } - - .card-actions.warning ha-call-service-button { - color: var(--error-color); - } - .command-picker { align-items: center; padding-left: 28px; @@ -285,30 +228,6 @@ export class ZHAGroupBindingControl extends LitElement { .sectionHeader { flex-grow: 1; } - - .helpText { - color: grey; - padding-left: 28px; - padding-right: 28px; - padding-bottom: 10px; - } - - .toggle-help-icon { - float: right; - top: -6px; - right: 0; - padding-right: 0px; - color: var(--primary-color); - } - - ha-service-description { - display: block; - color: grey; - } - - [hidden] { - display: none; - } `, ]; } diff --git a/src/panels/config/integrations/integration-panels/zha/zha-manage-clusters.ts b/src/panels/config/integrations/integration-panels/zha/zha-manage-clusters.ts new file mode 100644 index 0000000000..d956723242 --- /dev/null +++ b/src/panels/config/integrations/integration-panels/zha/zha-manage-clusters.ts @@ -0,0 +1,198 @@ +import "@material/mwc-list/mwc-list-item"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { cache } from "lit/directives/cache"; +import { stopPropagation } from "../../../../../common/dom/stop_propagation"; +import "../../../../../components/ha-card"; +import "../../../../../components/ha-select"; +import { + Cluster, + fetchClustersForZhaDevice, + ZHADevice, +} from "../../../../../data/zha"; +import { haStyle } from "../../../../../resources/styles"; +import { HomeAssistant } from "../../../../../types"; +import { computeClusterKey } from "./functions"; +import "@material/mwc-tab-bar/mwc-tab-bar"; +import "@material/mwc-tab/mwc-tab"; +import "./zha-cluster-attributes"; +import "./zha-cluster-commands"; + +declare global { + // for fire event + interface HASSDomEvents { + "zha-cluster-selected": { + cluster?: Cluster; + }; + } +} + +const tabs = ["attributes", "commands"] as const; + +@customElement("zha-manage-clusters") +export class ZHAManageClusters extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public isWide?: boolean; + + @property() public device?: ZHADevice; + + @state() private _selectedClusterIndex = -1; + + @state() private _clusters: Cluster[] = []; + + @state() private _selectedCluster?: Cluster; + + @state() private _currTab: typeof tabs[number] = "attributes"; + + @state() private _clustersLoaded = false; + + protected willUpdate(changedProps: PropertyValues) { + super.willUpdate(changedProps); + if (!this.device) { + return; + } + if (!tabs.includes(this._currTab)) { + this._currTab = tabs[0]; + } + } + + protected updated(changedProperties: PropertyValues): void { + if (changedProperties.has("device")) { + this._clusters = []; + this._selectedClusterIndex = -1; + this._clustersLoaded = false; + this._fetchClustersForZhaDevice(); + } + super.updated(changedProperties); + } + + protected render(): TemplateResult { + if (!this.device || !this._clustersLoaded) { + return html``; + } + return html` + +
+ + ${this._clusters.map( + (entry, idx) => html` + ${computeClusterKey(entry)} + ` + )} + +
+ ${this._selectedCluster + ? html` + + ${tabs.map( + (tab) => html` + + ` + )} + + +
+ ${cache( + this._currTab === "attributes" + ? html` + + ` + : html` + + ` + )} +
+ ` + : ""} +
+ `; + } + + private async _fetchClustersForZhaDevice(): Promise { + if (this.hass) { + this._clusters = await fetchClustersForZhaDevice( + this.hass, + this.device!.ieee + ); + this._clusters.sort((a, b) => a.name.localeCompare(b.name)); + if (this._clusters.length > 0) { + this._selectedClusterIndex = 0; + this._selectedCluster = this._clusters[0]; + } + this._clustersLoaded = true; + } + } + + private _handleTabChanged(ev: CustomEvent): void { + const newTab = tabs[ev.detail.index]; + if (newTab === this._currTab) { + return; + } + this._currTab = newTab; + } + + private _selectedClusterChanged(event): void { + this._selectedClusterIndex = Number(event.target!.value); + this._selectedCluster = this._clusters[this._selectedClusterIndex]; + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + css` + ha-select { + margin-top: 16px; + } + .menu { + width: 100%; + } + .header { + flex-grow: 1; + } + .node-picker { + align-items: center; + padding-bottom: 10px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "zha-manage-clusters": ZHAManageClusters; + } +} diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index 7029ba4e3f..629cff5412 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -332,7 +332,6 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { "yaml-mode": this._mode === "yaml", })}" > - ${this._errors ? html`
${this._errors}
` : ""} ${this._mode === "gui" ? html`
+ ${this._errors + ? html` + + ${this._errors} + + ` + : ""}
${this._errors} + ` + : ""} = { + alias: values.alias ?? "", + icon: values.icon, + mode: values.mode, + max: isMaxMode(values.mode) ? values.max : undefined, + }; - for (const key of Object.keys(values)) { - if (key === "sequence") { + if (!this.scriptEntityId) { + this.updateEntityId(values.id, values.alias); + } + + for (const key of Object.keys(newValues)) { + const value = newValues[key]; + + if (value === this._config![key]) { continue; } - - const value = values[key]; - - if ( - value === this._config![key] || - (key === "id" && currentId === value) - ) { - continue; - } - - changed = true; - - switch (key) { - case "id": - this._idChanged(value); - break; - case "alias": - this._aliasChanged(value); - break; - case "mode": - this._modeChanged(value); - break; - } - - if (values[key] === undefined) { + if (value === undefined) { const newConfig = { ...this._config! }; delete newConfig![key]; this._config = newConfig; } else { this._config = { ...this._config!, [key]: value }; } + changed = true; } if (changed) { @@ -638,6 +640,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { private _configChanged(ev) { this._config = ev.detail.value; + this._errors = undefined; this._dirty = true; } @@ -759,7 +762,6 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { this.hass!.callApi("POST", "config/script/config/" + id, this._config).then( () => { this._dirty = false; - if (!this.scriptEntityId) { navigate(`/config/script/edit/${id}`, { replace: true }); } @@ -806,6 +808,10 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { max-width: 1040px; padding: 28px 20px 0; } + .config-container ha-alert { + margin-bottom: 16px; + display: block; + } ha-yaml-editor { flex-grow: 1; --code-mirror-height: 100%; diff --git a/src/panels/config/storage/dialog-move-datadisk.ts b/src/panels/config/storage/dialog-move-datadisk.ts index cfe29c95ca..40a55395f0 100644 --- a/src/panels/config/storage/dialog-move-datadisk.ts +++ b/src/panels/config/storage/dialog-move-datadisk.ts @@ -130,13 +130,18 @@ class MoveDatadiskDialog extends LitElement { @selected=${this._select_device} @closed=${stopPropagation} dialogInitialFocus + fixedMenuPosition > ${this._devices.map( (device) => - html`${device}` + html` + ${device} + ` )} + Test + Test + Test + Test + (localize: LocalizeFunc, subview: boolean, showAdvanced: boolean) => [ { name: "title", selector: { text: {} } }, { @@ -63,6 +63,20 @@ export class HuiViewEditor extends LitElement { }, }, }, + { + name: "subview", + selector: { + boolean: {}, + }, + }, + ...(subview && showAdvanced + ? [ + { + name: "back_path", + selector: { navigation: {} }, + }, + ] + : []), ] as const ); @@ -84,7 +98,12 @@ export class HuiViewEditor extends LitElement { return html``; } - const schema = this._schema(this.hass.localize); + const schema = this._schema( + this.hass.localize, + this._config.subview ?? false, + this.hass.userData?.showAdvanced ?? false + ); + const data = { theme: "Backend-selected", ...this._config, @@ -96,18 +115,22 @@ export class HuiViewEditor extends LitElement { .hass=${this.hass} .data=${data} .schema=${schema} - .computeLabel=${this._computeLabelCallback} + .computeLabel=${this._computeLabel} + .computeHelper=${this._computeHelper} @value-changed=${this._valueChanged} > `; } private _valueChanged(ev: CustomEvent): void { - const config = ev.detail.value; + const config = ev.detail.value as LovelaceViewConfig; if (config.type === "masonry") { delete config.type; } + if (!config.subview) { + delete config.back_path; + } if ( this.isNew && @@ -122,7 +145,7 @@ export class HuiViewEditor extends LitElement { fireEvent(this, "view-config-changed", { config }); } - private _computeLabelCallback = ( + private _computeLabel = ( schema: SchemaUnion> ) => { switch (schema.name) { @@ -130,12 +153,35 @@ export class HuiViewEditor extends LitElement { return this.hass!.localize("ui.panel.lovelace.editor.card.generic.url"); case "type": return this.hass.localize("ui.panel.lovelace.editor.edit_view.type"); + case "subview": + return this.hass.localize("ui.panel.lovelace.editor.edit_view.subview"); + case "back_path": + return this.hass.localize( + "ui.panel.lovelace.editor.edit_view.back_path" + ); default: return this.hass!.localize( `ui.panel.lovelace.editor.card.generic.${schema.name}` ); } }; + + private _computeHelper = ( + schema: SchemaUnion> + ) => { + switch (schema.name) { + case "subview": + return this.hass.localize( + "ui.panel.lovelace.editor.edit_view.subview_helper" + ); + case "back_path": + return this.hass.localize( + "ui.panel.lovelace.editor.edit_view.back_path_helper" + ); + default: + return undefined; + } + }; } declare global { diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts index 3539e6f26f..05e5187259 100644 --- a/src/panels/lovelace/hui-root.ts +++ b/src/panels/lovelace/hui-root.ts @@ -112,6 +112,11 @@ class HUIRoot extends LitElement { } protected render(): TemplateResult { + const views = this.lovelace?.config.views ?? []; + + const curViewConfig = + typeof this._curView === "number" ? views[this._curView] : undefined; + return html` - - ${this.lovelace!.config.views.length > 1 + ${curViewConfig?.subview + ? html` + + ` + : html` + + `} + ${curViewConfig?.subview + ? html`
${curViewConfig.title}
` + : views.filter((view) => !view.subview).length > 1 ? html` - ${this.lovelace!.config.views.map( + ${views.map( (view) => html` e.user === this.hass!.user!.id - )) || - view.visible === false) + view.subview || + (view.visible !== undefined && + ((Array.isArray(view.visible) && + !view.visible.some( + (e) => + e.user === this.hass!.user!.id + )) || + view.visible === false)) ), })} > @@ -473,7 +490,7 @@ class HUIRoot extends LitElement { @iron-activate=${this._handleViewSelected} dir=${computeRTLDirection(this.hass!)} > - ${this.lovelace!.config.views.map( + ${views.map( (view) => html` @@ -528,7 +548,7 @@ class HUIRoot extends LitElement { class="edit-icon view" @click=${this._moveViewRight} ?disabled=${(this._curView! as number) + 1 === - this.lovelace!.config.views.length} + views.length} > ` : ""} @@ -720,6 +740,20 @@ class HUIRoot extends LitElement { }); } + private _goBack(): void { + const views = this.lovelace?.config.views ?? []; + const curViewConfig = + typeof this._curView === "number" ? views[this._curView] : undefined; + + if (curViewConfig?.back_path) { + navigate(curViewConfig.back_path); + } else if (history.length > 0) { + history.back(); + } else { + navigate(views[0].path!); + } + } + private _handleRawEditor(ev: CustomEvent): void { if (!shouldHandleRequestSelectedEvent(ev)) { return; @@ -1019,6 +1053,9 @@ class HUIRoot extends LitElement { --mdc-button-outline-color: var(--app-header-edit-text-color, #fff); --mdc-typography-button-font-size: 14px; } + .child-view-icon { + opacity: 0.5; + } `, ]; } diff --git a/src/translations/en.json b/src/translations/en.json index 45cbcfcaba..23933d2f6d 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1002,6 +1002,15 @@ "attribute": "Attribute", "min_max_change": "min/max/change" }, + "zha_manage_device": { + "heading": "Manage Zigbee Device", + "tabs": { + "clusters": "Clusters", + "bindings": "Bindings", + "signature": "Signature", + "children": "Children" + } + }, "zha_device_info": { "manuf": "by {manufacturer}", "no_area": "No Area", @@ -1010,10 +1019,8 @@ "buttons": { "add": "Add devices via this device", "remove": "Remove", - "clusters": "Manage clusters", + "manage": "Manage zigbee device", "reconfigure": "Reconfigure", - "zigbee_information": "Zigbee signature", - "device_children": "View children", "view_network": "View network" }, "services": { @@ -1796,7 +1803,8 @@ "dev_automation": "Debug automation", "show_info_automation": "Show info about automation", "delete": "[%key:ui::common::delete%]", - "delete_confirm": "Are you sure you want to delete this automation?", + "delete_confirm_title": "Delete automation?", + "delete_confirm_text": "{name} will be permanently deleted.", "duplicate": "[%key:ui::common::duplicate%]", "disabled": "Disabled", "headers": { @@ -2257,15 +2265,15 @@ "add": { "header": "Import a blueprint", "import_header": "Blueprint ''{name}''", - "import_introduction_link": "You can import blueprints of other users from Github and the {community_link}. Enter the URL of the blueprint below.", - "community_forums": "community forums", - "url": "URL of the blueprint", + "import_introduction": "Import blueprints of other users from GitHub and the community forums by pasting the address below.", + "community_forums": "View blueprints on the community forums", + "url": "Blueprint address", "raw_blueprint": "Blueprint content", "importing": "Loading blueprint…", - "import_btn": "Preview blueprint", + "import_btn": "Preview", "saving": "Importing blueprint…", "save_btn": "Import blueprint", - "error_no_url": "Please enter the URL of the blueprint.", + "error_no_url": "Please enter the blueprint address.", "unsupported_blueprint": "This blueprint is not supported", "file_name": "Blueprint Path" } @@ -3013,12 +3021,16 @@ "caption": "Application Credentials", "description": "Manage the OAuth Application Credentials used by Integrations", "editor": { - "caption": "Add Application Credential", - "create": "Create", + "caption": "Add Credential", + "description": "OAuth is used to grant Home Assistant access to information on other websites without giving a passwords. This mechanism is used by companies such as Spotify, Google, Withings, Microsoft, and Twitter.", + "view_documentation": "View documentation", + "add": "Add", "domain": "Integration", "name": "Name", "client_id": "OAuth Client ID", - "client_secret": "OAuth Client Secret" + "client_id_helper": "Public identifier of the OAuth application", + "client_secret": "OAuth Client Secret", + "client_secret_helper": "Secret of the OAuth application" }, "picker": { "add_application_credential": "Add Application Credential", @@ -3073,17 +3085,17 @@ "clusters": { "header": "Clusters", "help_cluster_dropdown": "Select a cluster to view attributes and commands.", - "introduction": "Clusters are the building blocks for Zigbee functionality. They separate functionality into logical units. There are client and server types and that are comprised of attributes and commands." + "tabs": { + "attributes": "Attributes", + "commands": "Commands" + } }, "cluster_attributes": { "header": "Cluster Attributes", "introduction": "View and edit cluster attributes.", "attributes_of_cluster": "Attributes of the selected cluster", - "get_zigbee_attribute": "Get Zigbee Attribute", - "set_zigbee_attribute": "Set Zigbee Attribute", - "help_attribute_dropdown": "Select an attribute to view or set its value.", - "help_get_zigbee_attribute": "Get the value for the selected attribute.", - "help_set_zigbee_attribute": "Set attribute value for the specified cluster on the specified entity." + "read_zigbee_attribute": "Read Attribute", + "write_zigbee_attribute": "Write Attribute" }, "cluster_commands": { "header": "Cluster Commands", @@ -3133,6 +3145,11 @@ "enable_physics": "Enable Physics", "refresh_topology": "Refresh Topology" }, + "device_binding": { + "bind": "Bind", + "unbind": "Unbind", + "picker_label": "Bindable Devices" + }, "group_binding": { "header": "Group Binding", "introduction": "Bind and unbind groups.", @@ -3748,7 +3765,11 @@ "masonry": "Masonry (default)", "sidebar": "Sidebar", "panel": "Panel (1 card)" - } + }, + "subview": "Subview", + "subview_helper": "Subviews don't appear in tabs and have a back button.", + "back_path": "Back path (optional)", + "back_path_helper": "Only for subviews. If empty, clicking on back button will go to the previous page." }, "edit_badges": { "view_no_badges": "Badges are not be supported by the current view type."