Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 10 commits into from
Nov 11, 2024
Merged
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")
wendevlin marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}

MindFreeze marked this conversation as resolved.
Show resolved Hide resolved
.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;
MindFreeze marked this conversation as resolved.
Show resolved Hide resolved
}
}
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>
MindFreeze marked this conversation as resolved.
Show resolved Hide resolved
</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 @@ -4959,12 +4959,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
Loading