Merge branch 'dev' into entity_registry_mass_units

This commit is contained in:
Erik Montnemery 2022-09-28 10:00:54 +02:00 committed by GitHub
commit a9a69a8995
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1252 additions and 1281 deletions

View File

@ -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

View File

@ -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",
},
]);

View File

@ -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,
},

View File

@ -196,6 +196,7 @@ const createEntityRegistryEntries = (
icon: null,
platform: "updater",
has_entity_name: false,
unique_id: "updater",
},
];

View File

@ -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"

View File

@ -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`
<div>
${this.label}
@ -64,7 +64,8 @@ export class HaSelectSelector extends LitElement {
return html`
<div>
${this.label}${options.map(
${this.label}
${options.map(
(item: SelectOption) => html`
<ha-formfield .label=${item.label}>
<ha-checkbox
@ -114,7 +115,7 @@ export class HaSelectSelector extends LitElement {
.required=${this.required && !value.length}
.value=${this._filter}
.items=${options.filter(
(item) => !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);
}
`;
}

View File

@ -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;

View File

@ -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<string, unknown>;
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 = (

View File

@ -93,6 +93,8 @@ export interface LovelaceViewConfig {
panel?: boolean;
background?: string;
visible?: boolean | ShowViewConfig[];
subview?: boolean;
back_path?: string;
}
export interface LovelaceViewElement extends HTMLElement {

View File

@ -309,7 +309,7 @@ export const fetchCommandsForCluster = (
cluster_type: clusterType,
});
export const fetchClustersForZhaNode = (
export const fetchClustersForZhaDevice = (
hass: HomeAssistant,
ieeeAddress: string
): Promise<Cluster[]> =>

View File

@ -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`

View File

@ -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 {
>
<div>
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
<p>
${this.hass.localize(
"ui.panel.config.application_credentials.editor.description"
)}
<br />
<a
href=${documentationUrl(
this.hass!,
"/integrations/application_credentials"
)}
target="_blank"
rel="noreferrer"
>
${this.hass!.localize(
"ui.panel.config.application_credentials.editor.view_documentation"
)}
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
</a>
</p>
<ha-combo-box
name="domain"
.hass=${this.hass}
@ -143,6 +164,10 @@ export class DialogAddApplicationCredential extends LitElement {
@input=${this._handleValueChanged}
error-message=${this.hass.localize("ui.common.error_required")}
dialogInitialFocus
.helper=${this.hass.localize(
"ui.panel.config.application_credentials.editor.client_id_helper"
)}
helperPersistent
></ha-textfield>
<ha-textfield
.label=${this.hass.localize(
@ -154,6 +179,10 @@ export class DialogAddApplicationCredential extends LitElement {
required
@input=${this._handleValueChanged}
error-message=${this.hass.localize("ui.common.error_required")}
.helper=${this.hass.localize(
"ui.panel.config.application_credentials.editor.client_secret_helper"
)}
helperPersistent
></ha-textfield>
</div>
${this._loading
@ -163,15 +192,18 @@ export class DialogAddApplicationCredential extends LitElement {
</div>
`
: html`
<mwc-button slot="primaryAction" @click=${this._abortDialog}>
${this.hass.localize("ui.common.cancel")}
</mwc-button>
<mwc-button
slot="primaryAction"
.disabled=${!this._domain ||
!this._clientId ||
!this._clientSecret}
@click=${this._createApplicationCredential}
@click=${this._addApplicationCredential}
>
${this.hass.localize(
"ui.panel.config.application_credentials.editor.create"
"ui.panel.config.application_credentials.editor.add"
)}
</mwc-button>
`}
@ -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;
}
`,
];
}

View File

@ -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(),
});

View File

@ -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,
});
}

View File

@ -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 {
>
<pre>${this._result.raw_data}</pre>
</ha-expansion-panel>`
: html`${this.hass.localize(
"ui.panel.config.blueprint.add.import_introduction_link",
"community_link",
html`<a
href="https://www.home-assistant.io/get-blueprints"
target="_blank"
rel="noreferrer noopener"
>${this.hass.localize(
"ui.panel.config.blueprint.add.community_forums"
)}</a
>`
)}<ha-textfield
: html`
<p>
${this.hass.localize(
"ui.panel.config.blueprint.add.import_introduction"
)}
</p>
<a
href="https://www.home-assistant.io/get-blueprints"
target="_blank"
rel="noreferrer noopener"
>
${this.hass.localize(
"ui.panel.config.blueprint.add.community_forums"
)}
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
</a>
<ha-textfield
id="input"
.label=${this.hass.localize(
"ui.panel.config.blueprint.add.url"
)}
.value=${this._url || ""}
dialogInitialFocus
></ha-textfield>`}
></ha-textfield>
`}
</div>
<mwc-button
slot="primaryAction"
@click=${this.closeDialog}
.disabled=${this._saving}
>
${this.hass.localize("ui.common.cancel")}
</mwc-button>
${!this._result
? html`<mwc-button
slot="primaryAction"
@click=${this._import}
.disabled=${this._importing}
>
${this._importing
? html`<ha-circular-progress
active
size="small"
.title=${this.hass.localize(
"ui.panel.config.blueprint.add.importing"
)}
></ha-circular-progress>`
: ""}
${this.hass.localize("ui.panel.config.blueprint.add.import_btn")}
</mwc-button>`
: html`<mwc-button
slot="secondaryAction"
@click=${this.closeDialog}
.disabled=${this._saving}
? html`
<mwc-button
slot="primaryAction"
@click=${this._import}
.disabled=${this._importing}
>
${this.hass.localize("ui.common.cancel")}
${this._importing
? html`<ha-circular-progress
active
size="small"
.title=${this.hass.localize(
"ui.panel.config.blueprint.add.importing"
)}
></ha-circular-progress>`
: ""}
${this.hass.localize(
"ui.panel.config.blueprint.add.import_btn"
)}
</mwc-button>
`
: html`
<mwc-button
slot="primaryAction"
@click=${this._save}
@ -164,7 +176,8 @@ class DialogImportBlueprint extends LitElement {
></ha-circular-progress>`
: ""}
${this.hass.localize("ui.panel.config.blueprint.add.save_btn")}
</mwc-button>`}
</mwc-button>
`}
</ha-dialog>
`;
}
@ -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;
}
`,
];

View File

@ -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"),

View File

@ -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"

View File

@ -280,7 +280,6 @@ export class DialogEnergyGasSettings
this._source = {
...this._source!,
stat_energy_from: ev.detail.value,
entity_energy_from: ev.detail.value,
};
}

View File

@ -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,
};
}

View File

@ -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 = {

View File

@ -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<EntityRegistryEntry, "id"> {
export interface StateEntity
extends Omit<EntityRegistryEntry, "id" | "unique_id"> {
readonly?: boolean;
selectable?: boolean;
id?: string;
unique_id?: string;
}
export interface EntityRow extends StateEntity {

View File

@ -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<void> {
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`
<ha-dialog
open
hideActions
@closed=${this._close}
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.zha.clusters.header")
)}
>
<zha-clusters
.hass=${this.hass}
.selectedDevice=${this._device}
@zha-cluster-selected=${this._onClusterSelected}
></zha-clusters>
${this._selectedCluster
? html`
<zha-cluster-attributes
.hass=${this.hass}
.selectedNode=${this._device}
.selectedCluster=${this._selectedCluster}
></zha-cluster-attributes>
<zha-cluster-commands
.hass=${this.hass}
.selectedNode=${this._device}
.selectedCluster=${this._selectedCluster}
></zha-cluster-commands>
`
: ""}
${this._bindableDevices.length > 0
? html`
<zha-device-binding-control
.hass=${this.hass}
.selectedDevice=${this._device}
.bindableDevices=${this._bindableDevices}
></zha-device-binding-control>
`
: ""}
${this._device && this._groups.length > 0
? html`
<zha-group-binding-control
.hass=${this.hass}
.selectedDevice=${this._device}
.groups=${this._groups}
></zha-group-binding-control>
`
: ""}
</ha-dialog>
`;
}
private _onClusterSelected(
selectedClusterEvent: HASSDomEvent<ZHAClusterSelectedParams>
): void {
this._selectedCluster = selectedClusterEvent.detail.cluster;
}
private _close(): void {
this._device = undefined;
}
private async _fetchData(): Promise<void> {
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;
}
}

View File

@ -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<void> {
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`
<ha-dialog
open
hideActions
@closed=${this._close}
.heading=${createCloseHeading(
this.hass,
this.hass.localize(`ui.dialogs.zha_device_info.device_signature`)
)}
>
<ha-code-editor
mode="yaml"
readOnly
.value=${this._signature}
dir="ltr"
>
</ha-code-editor>
</ha-dialog>
`;
}
private _close(): void {
this._signature = undefined;
}
static get styles(): CSSResultGroup {
return haStyleDialog;
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-zha-device-zigbee-info": DialogZHADeviceZigbeeInfo;
}
}

View File

@ -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<void> {
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`
<ha-dialog
open
hideActions
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.dialogs.zha_manage_device.heading")
)}
>
<div slot="heading" class="heading">
<ha-header-bar>
<ha-icon-button
slot="navigationIcon"
dialogAction="cancel"
.label=${this.hass.localize(
"ui.dialogs.more_info_control.dismiss"
)}
.path=${mdiClose}
></ha-icon-button>
<div
slot="title"
class="main-title"
.title=${this.hass.localize(
"ui.dialogs.zha_manage_device.heading"
)}
@click=${this._enlarge}
>
${this.hass.localize("ui.dialogs.zha_manage_device.heading")}
</div>
</ha-header-bar>
<mwc-tab-bar
.activeIndex=${tabs.indexOf(this._currTab)}
@MDCTabBar:activated=${this._handleTabChanged}
>
${tabs.map(
(tab) => html`
<mwc-tab
.label=${this.hass.localize(
`ui.dialogs.zha_manage_device.tabs.${tab}`
)}
></mwc-tab>
`
)}
</mwc-tab-bar>
</div>
<div class="content" tabindex="-1" dialogInitialFocus>
${cache(
this._currTab === "clusters"
? html`
<zha-manage-clusters
.hass=${this.hass}
.device=${this._device}
></zha-manage-clusters>
`
: this._currTab === "bindings"
? html`
${this._bindableDevices.length > 0
? html`
<zha-device-binding-control
.hass=${this.hass}
.device=${this._device}
.bindableDevices=${this._bindableDevices}
></zha-device-binding-control>
`
: ""}
${this._device && this._groups.length > 0
? html`
<zha-group-binding-control
.hass=${this.hass}
.device=${this._device}
.groups=${this._groups}
></zha-group-binding-control>
`
: ""}
`
: this._currTab === "signature"
? html`
<zha-device-zigbee-info
.hass=${this.hass}
.device=${this._device}
></zha-device-zigbee-info>
`
: html`
<zha-device-children
.hass=${this.hass}
.device=${this._device}
></zha-device-children>
`
)}
</div>
</ha-dialog>
`;
}
private async _fetchData(): Promise<void> {
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;
}
}

View File

@ -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<number, AttributeConfigurationStatus>(),
},
]
)
(
await fetchClustersForZhaDevice(this.hass, this._params.device.ieee)
).map((cluster: Cluster) => [
cluster.id,
{
cluster: cluster,
bindSuccess: undefined,
attributes: new Map<number, AttributeConfigurationStatus>(),
},
])
);
this._subscribe(this._params);
this._status = "started";

View File

@ -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,
});
};

View File

@ -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,
});
};

View File

@ -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,
});
};

View File

@ -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,
});
};

View File

@ -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;
}

View File

@ -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`
<ha-config-section .isWide=${this.isWide}>
<div class="header" slot="header">
<span>
${this.hass!.localize(
"ui.panel.config.zha.cluster_attributes.header"
<ha-card class="content">
<div class="attribute-picker">
<ha-select
.label=${this.hass!.localize(
"ui.panel.config.zha.cluster_attributes.attributes_of_cluster"
)}
</span>
<ha-icon-button
class="toggle-help-icon"
@click=${this._onHelpTap}
.path=${mdiHelpCircle}
.label=${this.hass!.localize("ui.common.help")}
class="menu"
.value=${String(this._selectedAttributeId)}
@selected=${this._selectedAttributeChanged}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
</ha-icon-button>
</div>
<span slot="introduction">
${this.hass!.localize(
"ui.panel.config.zha.cluster_attributes.introduction"
)}
</span>
<ha-card class="content">
<div class="attribute-picker">
<ha-select
.label=${this.hass!.localize(
"ui.panel.config.zha.cluster_attributes.attributes_of_cluster"
)}
class="menu"
.value=${String(this._selectedAttributeId)}
@selected=${this._selectedAttributeChanged}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${this._attributes.map(
(entry) => html`
<mwc-list-item .value=${String(entry.id)}>
${entry.name + " (id: " + formatAsPaddedHex(entry.id) + ")"}
</mwc-list-item>
`
)}
</ha-select>
</div>
${this.showHelp
? html`
<div class="help-text">
${this.hass!.localize(
"ui.panel.config.zha.cluster_attributes.help_attribute_dropdown"
)}
</div>
${this._attributes.map(
(entry) => html`
<mwc-list-item .value=${String(entry.id)}>
${`${entry.name} (id: ${formatAsPaddedHex(entry.id)})`}
</mwc-list-item>
`
: ""}
${this._selectedAttributeId !== undefined
? this._renderAttributeInteractions()
: ""}
</ha-card>
</ha-config-section>
)}
</ha-select>
</div>
${this._selectedAttributeId !== undefined
? this._renderAttributeInteractions()
: ""}
</ha-card>
`;
}
@ -152,20 +120,15 @@ export class ZHAClusterAttributes extends LitElement {
></paper-input>
</div>
<div class="card-actions">
<mwc-button @click=${this._onGetZigbeeAttributeClick}>
<ha-progress-button
@click=${this._onGetZigbeeAttributeClick}
.progress=${this._readingAttribute}
.disabled=${this._readingAttribute}
>
${this.hass!.localize(
"ui.panel.config.zha.cluster_attributes.get_zigbee_attribute"
"ui.panel.config.zha.cluster_attributes.read_zigbee_attribute"
)}
</mwc-button>
${this.showHelp
? html`
<div class="help-text2">
${this.hass!.localize(
"ui.panel.config.zha.cluster_attributes.help_get_zigbee_attribute"
)}
</div>
`
: ""}
</ha-progress-button>
<ha-call-service-button
.hass=${this.hass}
domain="zha"
@ -173,44 +136,37 @@ export class ZHAClusterAttributes extends LitElement {
.serviceData=${this._setAttributeServiceData}
>
${this.hass!.localize(
"ui.panel.config.zha.cluster_attributes.set_zigbee_attribute"
"ui.panel.config.zha.cluster_attributes.write_zigbee_attribute"
)}
</ha-call-service-button>
${this.showHelp
? html`
<ha-service-description
.hass=${this.hass}
domain="zha"
service="set_zigbee_cluster_attribute"
class="help-text2"
></ha-service-description>
`
: ""}
</div>
`;
}
private async _fetchAttributesForCluster(): Promise<void> {
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<void> {
private async _onGetZigbeeAttributeClick(ev: CustomEvent): Promise<void> {
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);

View File

@ -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`
<ha-config-section .isWide=${this.isWide}>
<div class="header" slot="header">
<span>
${this.hass!.localize(
"ui.panel.config.zha.cluster_commands.header"
<ha-card class="content">
<div class="command-picker">
<ha-select
.label=${this.hass!.localize(
"ui.panel.config.zha.cluster_commands.commands_of_cluster"
)}
</span>
<ha-icon-button
class="toggle-help-icon"
@click=${this._onHelpTap}
.path=${mdiHelpCircle}
.label=${this.hass!.localize("ui.common.help")}
class="menu"
.value=${String(this._selectedCommandId)}
@selected=${this._selectedCommandChanged}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
</ha-icon-button>
${this._commands.map(
(entry) => html`
<mwc-list-item .value=${String(entry.id)}>
${entry.name + " (id: " + formatAsPaddedHex(entry.id) + ")"}
</mwc-list-item>
`
)}
</ha-select>
</div>
<span slot="introduction">
${this.hass!.localize(
"ui.panel.config.zha.cluster_commands.introduction"
)}
</span>
<ha-card class="content">
<div class="command-picker">
<ha-select
.label=${this.hass!.localize(
"ui.panel.config.zha.cluster_commands.commands_of_cluster"
)}
class="menu"
.value=${String(this._selectedCommandId)}
@selected=${this._selectedCommandChanged}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${this._commands.map(
(entry) => html`
<mwc-list-item .value=${String(entry.id)}>
${entry.name + " (id: " + formatAsPaddedHex(entry.id) + ")"}
</mwc-list-item>
`
)}
</ha-select>
</div>
${this._showHelp
? html`
<div class="help-text">
${this.hass!.localize(
"ui.panel.config.zha.cluster_commands.help_command_dropdown"
${this._selectedCommandId !== undefined
? html`
<div class="input-text">
<paper-input
label=${this.hass!.localize(
"ui.panel.config.zha.common.manufacturer_code_override"
)}
</div>
`
: ""}
${this._selectedCommandId !== undefined
? html`
<div class="input-text">
<paper-input
label=${this.hass!.localize(
"ui.panel.config.zha.common.manufacturer_code_override"
)}
type="number"
.value=${this._manufacturerCodeOverride}
@value-changed=${this._onManufacturerCodeOverrideChanged}
placeholder=${this.hass!.localize(
"ui.panel.config.zha.common.value"
)}
></paper-input>
</div>
<div class="card-actions">
<ha-call-service-button
.hass=${this.hass}
domain="zha"
service="issue_zigbee_cluster_command"
.serviceData=${this._issueClusterCommandServiceData}
>
${this.hass!.localize(
"ui.panel.config.zha.cluster_commands.issue_zigbee_command"
)}
</ha-call-service-button>
${this._showHelp
? html`
<ha-service-description
.hass=${this.hass}
domain="zha"
service="issue_zigbee_cluster_command"
class="help-text2"
></ha-service-description>
`
: ""}
</div>
`
: ""}
</ha-card>
</ha-config-section>
type="number"
.value=${this._manufacturerCodeOverride}
@value-changed=${this._onManufacturerCodeOverrideChanged}
placeholder=${this.hass!.localize(
"ui.panel.config.zha.common.value"
)}
></paper-input>
</div>
<div class="card-actions">
<ha-call-service-button
.hass=${this.hass}
domain="zha"
service="issue_zigbee_cluster_command"
.serviceData=${this._issueClusterCommandServiceData}
>
${this.hass!.localize(
"ui.panel.config.zha.cluster_commands.issue_zigbee_command"
)}
</ha-call-service-button>
</div>
`
: ""}
</ha-card>
`;
}
private async _fetchCommandsForCluster(): Promise<void> {
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;
}
`,
];
}

View File

@ -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",

View File

@ -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`
<ha-config-section .isWide=${this.isWide}>
<div class="header" slot="header">
<ha-icon-button
class="toggle-help-icon"
@click=${this._onHelpTap}
.path=${mdiHelpCircle}
.label=${this.hass!.localize("ui.common.help")}
>
</ha-icon-button>
</div>
<span slot="introduction">
${this.hass!.localize("ui.panel.config.zha.clusters.introduction")}
</span>
<ha-card class="content">
<div class="node-picker">
<ha-select
.label=${this.hass!.localize(
"ui.panel.config.zha.common.clusters"
)}
class="menu"
.value=${String(this._selectedClusterIndex)}
@selected=${this._selectedClusterChanged}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${this._clusters.map(
(entry, idx) => html`
<mwc-list-item .value=${String(idx)}
>${computeClusterKey(entry)}</mwc-list-item
>
`
)}
</ha-select>
</div>
${this.showHelp
? html`
<div class="help-text">
${this.hass!.localize(
"ui.panel.config.zha.clusters.help_cluster_dropdown"
)}
</div>
`
: ""}
</ha-card>
</ha-config-section>
`;
}
private async _fetchClustersForZhaNode(): Promise<void> {
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);

View File

@ -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`
<ha-config-section .isWide=${this.isWide}>
<div class="header" slot="header">
<span>Device Binding</span>
<ha-icon-button
class="toggle-help-icon"
@click=${this._onHelpTap}
.path=${mdiHelpCircle}
.label=${this.hass!.localize("ui.common.help")}
<ha-card class="content">
<div class="command-picker">
<ha-select
label=${this.hass!.localize(
"ui.panel.config.zha.device_binding.picker_label"
)}
class="menu"
.value=${String(this._bindTargetIndex)}
@selected=${this._bindTargetIndexChanged}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
</ha-icon-button>
</div>
<span slot="introduction">Bind and unbind devices.</span>
<ha-card class="content">
<div class="command-picker">
<ha-select
label="Bindable Devices"
class="menu"
.value=${String(this._bindTargetIndex)}
@selected=${this._bindTargetIndexChanged}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${this.bindableDevices.map(
(device, idx) => html`
<mwc-list-item .value=${String(idx)}>
${device.user_given_name
? device.user_given_name
: device.name}
</mwc-list-item>
`
)}
</ha-select>
</div>
${this._showHelp
? html`
<div class="helpText">
Select a device to issue a bind command.
</div>
${this.bindableDevices.map(
(device, idx) => html`
<mwc-list-item .value=${String(idx)}>
${device.user_given_name
? device.user_given_name
: device.name}
</mwc-list-item>
`
: ""}
<div class="card-actions">
<mwc-button
@click=${this._onBindDevicesClick}
.disabled=${!(this._deviceToBind && this.selectedDevice)}
>Bind</mwc-button
>
${this._showHelp
? html` <div class="helpText">Bind devices.</div> `
: ""}
<mwc-button
@click=${this._onUnbindDevicesClick}
.disabled=${!(this._deviceToBind && this.selectedDevice)}
>Unbind</mwc-button
>
${this._showHelp
? html` <div class="helpText">Unbind devices.</div> `
: ""}
</div>
</ha-card>
</ha-config-section>
)}
</ha-select>
</div>
<div class="card-actions">
<ha-progress-button
@click=${this._onBindDevicesClick}
.disabled=${!(this._deviceToBind && this.device) ||
this._bindingOperationInProgress}
>
${this.hass!.localize("ui.panel.config.zha.device_binding.bind")}
</ha-progress-button>
<ha-progress-button
@click=${this._onUnbindDevicesClick}
.disabled=${!(this._deviceToBind && this.device) ||
this._bindingOperationInProgress}
>
${this.hass!.localize("ui.panel.config.zha.device_binding.unbind")}
</ha-progress-button>
</div>
</ha-card>
`;
}
@ -120,27 +92,41 @@ export class ZHADeviceBindingControl extends LitElement {
: this.bindableDevices[this._bindTargetIndex];
}
private _onHelpTap(): void {
this._showHelp = !this._showHelp;
}
private async _onBindDevicesClick(): Promise<void> {
if (this.hass && this._deviceToBind && this.selectedDevice) {
await bindDevices(
this.hass,
this.selectedDevice.ieee,
this._deviceToBind.ieee
);
private async _onBindDevicesClick(ev: CustomEvent): Promise<void> {
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<void> {
if (this.hass && this._deviceToBind && this.selectedDevice) {
await unbindDevices(
this.hass,
this.selectedDevice.ieee,
this._deviceToBind.ieee
);
private async _onUnbindDevicesClick(ev: CustomEvent): Promise<void> {
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;
}
`,
];
}

View File

@ -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<string, ZHADevice> | 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`
<ha-dialog
hideActions
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize(`ui.dialogs.zha_device_info.device_children`)
)}
>
${!this._devices
? html`<ha-circular-progress
alt="Loading"
size="large"
active
></ha-circular-progress>`
: html`<ha-data-table
.hass=${this.hass}
.columns=${this._columns}
.data=${this._deviceChildren(this._device, this._devices)}
auto-height
.dir=${computeRTLDirection(this.hass)}
.searchLabel=${this.hass.localize(
"ui.components.data-table.search"
)}
.noDataText=${this.hass.localize(
"ui.components.data-table.no-data"
)}
></ha-data-table>`}
</ha-dialog>
${!this._devices
? html`<ha-circular-progress
alt="Loading"
size="large"
active
></ha-circular-progress>`
: html`<ha-data-table
.hass=${this.hass}
.columns=${this._columns}
.data=${this._deviceChildren(this.device, this._devices)}
auto-height
.dir=${computeRTLDirection(this.hass)}
.searchLabel=${this.hass.localize(
"ui.components.data-table.search"
)}
.noDataText=${this.hass.localize(
"ui.components.data-table.no-data"
)}
></ha-data-table>`}
`;
}
private async _fetchData(): Promise<void> {
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;
}
}

View File

@ -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`
<ha-code-editor mode="yaml" readOnly .value=${this._signature} dir="ltr">
</ha-code-editor>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-device-zigbee-info": ZHADeviceZigbeeInfo;
}
}

View File

@ -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`
<ha-config-section .isWide=${this.isWide}>
<div class="sectionHeader" slot="header">
<span
>${this.hass!.localize(
"ui.panel.config.zha.group_binding.header"
)}</span
>
<ha-icon-button
class="toggle-help-icon"
@click=${this._onHelpTap}
.path=${mdiHelpCircle}
.label=${this.hass!.localize("ui.common.help")}
>
</ha-icon-button>
</div>
<span slot="introduction"
>${this.hass!.localize(
"ui.panel.config.zha.group_binding.introduction"
)}</span
>
<ha-card class="content">
<div class="command-picker">
<ha-select
@ -112,66 +83,32 @@ export class ZHAGroupBindingControl extends LitElement {
)}
</ha-select>
</div>
${this._showHelp
? html`
<div class="helpText">
${this.hass!.localize(
"ui.panel.config.zha.group_binding.group_picker_help"
)}
</div>
`
: ""}
<div class="command-picker">
<zha-clusters-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.clusters=${this._clusters}
@selection-changed=${this._handleClusterSelectionChanged}
class="menu"
></zha-clusters-data-table>
</div>
${this._showHelp
? html`
<div class="helpText">
${this.hass!.localize(
"ui.panel.config.zha.group_binding.cluster_selection_help"
)}
</div>
`
: ""}
<div class="card-actions">
<mwc-button
@click=${this._onBindGroupClick}
.disabled=${!this._canBind}
>${this.hass!.localize(
"ui.panel.config.zha.group_binding.bind_button_label"
)}</mwc-button
>
${this._showHelp
? html`
<div class="helpText">
${this.hass!.localize(
"ui.panel.config.zha.group_binding.bind_button_help"
)}
</div>
`
: ""}
<mwc-button
@click=${this._onUnbindGroupClick}
.disabled=${!this._canBind}
>${this.hass!.localize(
"ui.panel.config.zha.group_binding.unbind_button_label"
)}</mwc-button
>
${this._showHelp
? html`
<div class="helpText">
${this.hass!.localize(
"ui.panel.config.zha.group_binding.unbind_button_help"
)}
</div>
`
: ""}
<ha-progress-button
@click=${this._onBindGroupClick}
.disabled=${!this._canBind || this._bindingOperationInProgress}
>
${this.hass!.localize(
"ui.panel.config.zha.group_binding.bind_button_label"
)}
</ha-progress-button>
<ha-progress-button
@click=${this._onUnbindGroupClick}
.disabled=${!this._canBind || this._bindingOperationInProgress}
>
${this.hass!.localize(
"ui.panel.config.zha.group_binding.unbind_button_label"
)}
</ha-progress-button>
</div>
</ha-card>
</ha-config-section>
@ -186,31 +123,49 @@ export class ZHAGroupBindingControl extends LitElement {
: this.groups[this._bindTargetIndex];
}
private _onHelpTap(): void {
this._showHelp = !this._showHelp;
}
private async _onBindGroupClick(): Promise<void> {
private async _onBindGroupClick(ev: CustomEvent): Promise<void> {
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<void> {
private async _onUnbindGroupClick(ev: CustomEvent): Promise<void> {
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<void> {
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;
}
`,
];
}

View File

@ -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`
<ha-card class="content">
<div class="node-picker">
<ha-select
.label=${this.hass!.localize("ui.panel.config.zha.common.clusters")}
class="menu"
.value=${String(this._selectedClusterIndex)}
@selected=${this._selectedClusterChanged}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${this._clusters.map(
(entry, idx) => html`
<mwc-list-item .value=${String(idx)}
>${computeClusterKey(entry)}</mwc-list-item
>
`
)}
</ha-select>
</div>
${this._selectedCluster
? html`
<mwc-tab-bar
.activeIndex=${tabs.indexOf(this._currTab)}
@MDCTabBar:activated=${this._handleTabChanged}
>
${tabs.map(
(tab) => html`
<mwc-tab
.label=${this.hass.localize(
`ui.panel.config.zha.clusters.tabs.${tab}`
)}
></mwc-tab>
`
)}
</mwc-tab-bar>
<div class="content" tabindex="-1" dialogInitialFocus>
${cache(
this._currTab === "attributes"
? html`
<zha-cluster-attributes
.hass=${this.hass}
.device=${this.device}
.selectedCluster=${this._selectedCluster}
></zha-cluster-attributes>
`
: html`
<zha-cluster-commands
.hass=${this.hass}
.device=${this.device}
.selectedCluster=${this._selectedCluster}
></zha-cluster-commands>
`
)}
</div>
`
: ""}
</ha-card>
`;
}
private async _fetchClustersForZhaDevice(): Promise<void> {
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;
}
}

View File

@ -332,7 +332,6 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
"yaml-mode": this._mode === "yaml",
})}"
>
${this._errors ? html`<div class="errors">${this._errors}</div>` : ""}
${this._mode === "gui"
? html`
<div
@ -343,6 +342,13 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
${this._config
? html`
<div class="config-container">
${this._errors
? html`
<ha-alert alert-type="error">
${this._errors}
</ha-alert>
`
: ""}
<ha-card outlined>
<div class="card-content">
<ha-form
@ -382,6 +388,11 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
`
: this._mode === "yaml"
? html`
${this._errors
? html`
<ha-alert alert-type="error">${this._errors}</ha-alert>
`
: ""}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this._preprocessYaml()}
@ -546,28 +557,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
});
}
private _modeChanged(mode) {
const curMode = this._config!.mode || MODES[0];
if (mode === curMode) {
return;
}
this._config = { ...this._config!, mode };
if (!isMaxMode(mode)) {
delete this._config.max;
}
this._dirty = true;
}
private _aliasChanged(alias: string) {
if (
this.scriptEntityId ||
(this._entityId && this._entityId !== slugify(this._config!.alias))
) {
return;
}
private _computeEntityIdFromAlias(alias: string) {
const aliasSlugify = slugify(alias);
let id = aliasSlugify;
let i = 2;
@ -575,11 +565,10 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
id = `${aliasSlugify}_${i}`;
i++;
}
this._entityId = id;
return id;
}
private _idChanged(id: string) {
private _setEntityId(id?: string) {
this._entityId = id;
if (this.hass.states[`script.${this._entityId}`]) {
this._idError = true;
@ -588,47 +577,60 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
}
}
private updateEntityId(
newId: string | undefined,
newAlias: string | undefined
) {
const currentAlias = this._config?.alias ?? "";
const currentEntityId = this._entityId ?? "";
if (newId !== this._entityId) {
this._setEntityId(newId || undefined);
return;
}
const currentComputedEntity = this._computeEntityIdFromAlias(currentAlias);
if (currentComputedEntity === currentEntityId || !this._entityId) {
const newComputedId = newAlias
? this._computeEntityIdFromAlias(newAlias)
: undefined;
this._setEntityId(newComputedId);
}
}
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
this._errors = undefined;
const values = ev.detail.value as any;
const currentId = this._entityId;
let changed = false;
const newValues: Omit<ScriptConfig, "sequence"> = {
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%;

View File

@ -130,13 +130,18 @@ class MoveDatadiskDialog extends LitElement {
@selected=${this._select_device}
@closed=${stopPropagation}
dialogInitialFocus
fixedMenuPosition
>
${this._devices.map(
(device) =>
html`<mwc-list-item .value=${device}
>${device}</mwc-list-item
>`
html`<mwc-list-item .value=${device}>
${device}
</mwc-list-item>`
)}
<mwc-list-item>Test</mwc-list-item>
<mwc-list-item>Test</mwc-list-item>
<mwc-list-item>Test</mwc-list-item>
<mwc-list-item>Test</mwc-list-item>
</ha-select>
<mwc-button
@ -175,8 +180,9 @@ class MoveDatadiskDialog extends LitElement {
),
text: extractApiErrorMessage(err),
});
this.closeDialog();
}
} finally {
this.closeDialog();
}
}

View File

@ -1,6 +1,7 @@
import { PropertyValues, ReactiveElement } from "lit";
import { property } from "lit/decorators";
import { navigate, NavigateOptions } from "../../common/navigate";
import { deepEqual } from "../../common/util/deep-equal";
import { CustomPanelInfo } from "../../data/panel_custom";
import { HomeAssistant, Route } from "../../types";
import { createCustomPanelElement } from "../../util/custom-panel/create-custom-panel-element";
@ -54,12 +55,15 @@ export class HaPanelCustom extends ReactiveElement {
protected update(changedProps: PropertyValues) {
super.update(changedProps);
if (changedProps.has("panel")) {
// Clean up old things if we had a panel
if (changedProps.get("panel")) {
this._cleanupPanel();
// Clean up old things if we had a panel and the new one is different.
const oldPanel = changedProps.get("panel") as CustomPanelInfo | undefined;
if (!deepEqual(oldPanel, this.panel)) {
if (oldPanel) {
this._cleanupPanel();
}
this._createPanel(this.panel);
return;
}
this._createPanel(this.panel);
return;
}
if (!this._setProperties) {
return;

View File

@ -1,10 +1,10 @@
import "../../../../components/ha-form/ha-form";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import { slugify } from "../../../../common/string/slugify";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { LovelaceViewConfig } from "../../../../data/lovelace";
import type { HomeAssistant } from "../../../../types";
@ -33,7 +33,7 @@ export class HuiViewEditor extends LitElement {
private _suggestedPath = false;
private _schema = memoizeOne(
(localize: LocalizeFunc) =>
(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}
></ha-form>
`;
}
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<ReturnType<typeof this._schema>>
) => {
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<ReturnType<typeof this._schema>>
) => {
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 {

View File

@ -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`
<ha-app-layout
class=${classMap({
@ -229,11 +234,21 @@ class HUIRoot extends LitElement {
`
: html`
<app-toolbar>
<ha-menu-button
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
${this.lovelace!.config.views.length > 1
${curViewConfig?.subview
? html`
<ha-icon-button-arrow-prev
@click=${this._goBack}
></ha-icon-button-arrow-prev>
`
: html`
<ha-menu-button
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`}
${curViewConfig?.subview
? html`<div main-title>${curViewConfig.title}</div>`
: views.filter((view) => !view.subview).length > 1
? html`
<ha-tabs
scrollable
@ -241,18 +256,20 @@ class HUIRoot extends LitElement {
@iron-activate=${this._handleViewSelected}
dir=${computeRTLDirection(this.hass!)}
>
${this.lovelace!.config.views.map(
${views.map(
(view) => html`
<paper-tab
aria-label=${ifDefined(view.title)}
class=${classMap({
"hide-tab": Boolean(
view.visible !== undefined &&
((Array.isArray(view.visible) &&
!view.visible.some(
(e) => 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`
<paper-tab
aria-label=${ifDefined(view.title)}
@ -505,6 +522,9 @@ class HUIRoot extends LitElement {
${view.icon
? html`
<ha-icon
class=${classMap({
"child-view-icon": Boolean(view.subview),
})}
title=${ifDefined(view.title)}
.icon=${view.icon}
></ha-icon>
@ -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}
></ha-icon-button-arrow-next>
`
: ""}
@ -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<RequestSelectedDetail>): 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;
}
`,
];
}

View File

@ -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."