Skip to content

Commit

Permalink
Allow GET/SET custom config param in Z-Wave device configuration (#22364
Browse files Browse the repository at this point in the history
)

* Allow GET/SET custom config param in Z-Wave device configuration

* update api calls and validation

* update imports

* fix import

* PR review comments

* fix import

* fix merge error

* fix merge
  • Loading branch information
MindFreeze authored Nov 11, 2024
1 parent f6cc435 commit 52a91d8
Show file tree
Hide file tree
Showing 4 changed files with 326 additions and 0 deletions.
39 changes: 39 additions & 0 deletions src/data/zwave_js.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,15 @@ export interface ZWaveJSSetConfigParamData {
value: string | number;
}

export interface ZWaveJSSetRawConfigParamData {
type: string;
device_id: string;
property: number;
value: number;
value_size: number;
value_format: number;
}

export interface ZWaveJSSetConfigParamResult {
value_id?: string;
status?: string;
Expand Down Expand Up @@ -677,6 +686,36 @@ export const setZwaveNodeConfigParameter = (
return hass.callWS(data);
};

export const setZwaveNodeRawConfigParameter = (
hass: HomeAssistant,
device_id: string,
property: number,
value: number,
value_size: number,
value_format: number
): Promise<ZWaveJSSetConfigParamResult> => {
const data: ZWaveJSSetRawConfigParamData = {
type: "zwave_js/set_raw_config_parameter",
device_id,
property,
value,
value_size,
value_format,
};
return hass.callWS(data);
};

export const getZwaveNodeRawConfigParameter = (
hass: HomeAssistant,
device_id: string,
property: number
): Promise<number> =>
hass.callWS({
type: "zwave_js/get_raw_config_parameter",
device_id,
property,
});

export const reinterviewZwaveNode = (
hass: HomeAssistant,
device_id: string,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
import { LitElement, html, css, type CSSResultGroup, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { mdiCloseCircle } from "@mdi/js";
import "../../../../../components/ha-textfield";
import "../../../../../components/ha-select";
import "../../../../../components/ha-button";
import "../../../../../components/ha-circular-progress";
import "../../../../../components/ha-list-item";
import type { HomeAssistant } from "../../../../../types";
import {
getZwaveNodeRawConfigParameter,
setZwaveNodeRawConfigParameter,
} from "../../../../../data/zwave_js";

@customElement("zwave_js-custom-param")
class ZWaveJSCustomParam extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;

@property() public deviceId!: string;

@state() private _customParamNumber?: number;

@state() private _valueSize = 1;

@state() private _value?: number;

@state() private _valueFormat = 0;

@state() private _isLoading = false;

@state() private _error = "";

protected render() {
return html`
<div class="custom-config-form">
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.zwave_js.node_config.parameter"
)}
.value=${this._customParamNumber ?? ""}
@input=${this._customParamNumberChanged}
type="number"
></ha-textfield>
<ha-select
.label=${this.hass.localize(
"ui.panel.config.zwave_js.node_config.size"
)}
.value=${String(this._valueSize)}
@selected=${this._customValueSizeChanged}
>
<ha-list-item value="1">1</ha-list-item>
<ha-list-item value="2">2</ha-list-item>
<ha-list-item value="4">4</ha-list-item>
</ha-select>
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.zwave_js.node_config.value"
)}
.value=${this._value ?? ""}
@input=${this._customValueChanged}
type="number"
></ha-textfield>
<ha-select
.label=${this.hass.localize(
"ui.panel.config.zwave_js.node_config.format"
)}
.value=${String(this._valueFormat)}
@selected=${this._customValueFormatChanged}
>
<ha-list-item value="0"
>${this.hass.localize(
"ui.panel.config.zwave_js.node_config.signed"
)}</ha-list-item
>
<ha-list-item value="1"
>${this.hass.localize(
"ui.panel.config.zwave_js.node_config.unsigned"
)}</ha-list-item
>
<ha-list-item value="2"
>${this.hass.localize(
"ui.panel.config.zwave_js.node_config.enumerated"
)}</ha-list-item
>
<ha-list-item value="3"
>${this.hass.localize(
"ui.panel.config.zwave_js.node_config.bitfield"
)}</ha-list-item
>
</ha-select>
</div>
<div class="custom-config-buttons">
${this._isLoading
? html`<ha-circular-progress indeterminate></ha-circular-progress>`
: nothing}
<ha-button @click=${this._getCustomConfigValue}>
${this.hass.localize(
"ui.panel.config.zwave_js.node_config.get_value"
)}
</ha-button>
<ha-button @click=${this._setCustomConfigValue}>
${this.hass.localize(
"ui.panel.config.zwave_js.node_config.set_value"
)}
</ha-button>
</div>
<div class="error">
${this._error
? html`<ha-svg-icon
.path=${mdiCloseCircle}
class="error-icon"
slot="item-icon"
></ha-svg-icon
><em>${this._error}</em>`
: nothing}
</div>
`;
}

private _customParamNumberChanged(ev: Event) {
this._customParamNumber =
Number((ev.target as HTMLInputElement).value) || undefined;
}

private _customValueSizeChanged(ev: Event) {
this._valueSize = Number((ev.target as HTMLSelectElement).value) || 1;
}

private _customValueChanged(ev: Event) {
this._value = Number((ev.target as HTMLInputElement).value) || undefined;
}

private _customValueFormatChanged(ev: Event) {
this._valueFormat = Number((ev.target as HTMLSelectElement).value) || 0;
}

private async _getCustomConfigValue() {
if (this._customParamNumber === undefined) {
this._error = this.hass.localize(
"ui.panel.config.zwave_js.node_config.error_required",
{
entity: this.hass.localize(
"ui.panel.config.zwave_js.node_config.parameter"
),
}
);
return;
}
this._error = "";
this._isLoading = true;
try {
const value = await getZwaveNodeRawConfigParameter(
this.hass,
this.deviceId,
this._customParamNumber
);
this._value = value;
} catch (err: any) {
this._error = err?.message || "Unknown error";
} finally {
this._isLoading = false;
}
}

private async _setCustomConfigValue() {
if (this._customParamNumber === undefined) {
this._error = this.hass.localize(
"ui.panel.config.zwave_js.node_config.error_required",
{
entity: this.hass.localize(
"ui.panel.config.zwave_js.node_config.parameter"
),
}
);
return;
}
if (this._value === undefined) {
this._error = this.hass.localize(
"ui.panel.config.zwave_js.node_config.error_required",
{
entity: this.hass.localize(
"ui.panel.config.zwave_js.node_config.value"
),
}
);
return;
}
this._error = "";
this._isLoading = true;
try {
await setZwaveNodeRawConfigParameter(
this.hass,
this.deviceId,
this._customParamNumber,
this._value,
this._valueSize,
this._valueFormat
);
} catch (err: any) {
this._error = err?.message || "Unknown error";
} finally {
this._isLoading = false;
}
}

static get styles(): CSSResultGroup {
return css`
.custom-config-form {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 8px;
}
ha-textfield,
ha-select {
flex-grow: 1;
flex-basis: calc(50% - 8px);
min-width: 120px;
}
@media (min-width: 681px) {
.custom-config-form {
flex-wrap: nowrap;
}
ha-textfield,
ha-select {
flex-basis: 0;
}
}
.custom-config-buttons {
display: flex;
justify-content: flex-end;
align-items: center;
}
.error {
color: var(--error-color);
}
.error-icon {
margin-right: 8px;
}
`;
}
}

declare global {
interface HTMLElementTagNameMap {
"zwave_js-custom-param": ZWaveJSCustomParam;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import "../../../ha-config-section";
import { configTabs } from "./zwave_js-config-router";
import "./zwave_js-custom-param";
import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
import { fireEvent } from "../../../../../common/dom/fire_event";

Expand Down Expand Up @@ -193,6 +194,22 @@ class ZWaveJSNodeConfig extends LitElement {
)}
</ha-progress-button>
</div>
<h3>
${this.hass.localize(
"ui.panel.config.zwave_js.node_config.custom_config"
)}
</h3>
<span class="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.node_config.custom_config_description"
)}
</span>
<ha-card class="custom-config">
<zwave_js-custom-param
.hass=${this.hass}
.deviceId=${this.deviceId}
></zwave_js-custom-param>
</ha-card>
</ha-config-section>
</hass-tabs-subpage>
`;
Expand Down Expand Up @@ -600,6 +617,10 @@ class ZWaveJSNodeConfig extends LitElement {
text-align: right;
}
.custom-config {
padding: 16px;
}
.reset {
display: flex;
justify-content: flex-end;
Expand Down
12 changes: 12 additions & 0 deletions src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -5020,12 +5020,24 @@
"parameter_is_read_only": "This parameter is read-only.",
"between_min_max": "Between {min} and {max}",
"error_not_in_range": "Value must be between {min} and {max}",
"error_required": "{entity} is required",
"error_device_not_found": "Device not found",
"set_param_accepted": "The parameter has been updated.",
"set_param_queued": "The parameter change has been queued, and will be updated when the device wakes up.",
"set_param_error": "An error occurred.",
"parameter": "Parameter",
"bitmask": "Bitmask",
"size": "Size",
"value": "Value",
"format": "Format",
"custom_config": "Custom configuration",
"custom_config_description": "You can use this section to get/set custom configuration parameters for your device. This is useful when working with devices that are not fully supported.",
"get_value": "Get value",
"set_value": "Set value",
"signed": "Signed",
"unsigned": "Unsigned",
"enumerated": "Enumerated",
"bitfield": "Bitfield",
"reset_to_default": {
"button_label": "Reset to default configuration",
"dialog": {
Expand Down

0 comments on commit 52a91d8

Please sign in to comment.