Skip to content

Commit

Permalink
ZWave JS: Support Air Temperature & Power properties from Multilevel …
Browse files Browse the repository at this point in the history
…Sensor Command Class (#2027)

Co-authored-by: Stéphane Escandell <stephane.escandell@sociabble.com>
  • Loading branch information
sescandell and Stéphane Escandell authored Feb 26, 2024
1 parent ab7ffa9 commit d198227
Show file tree
Hide file tree
Showing 7 changed files with 273 additions and 17 deletions.
2 changes: 1 addition & 1 deletion server/services/usb/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

70 changes: 61 additions & 9 deletions server/services/zwavejs-ui/lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const {
DEVICE_FEATURE_TYPES,
OPENING_SENSOR_STATE,
STATE,
DEVICE_FEATURE_UNITS,
} = require('../../../utils/constants');

const CONFIGURATION = {
Expand All @@ -11,31 +12,60 @@ const CONFIGURATION = {
ZWAVEJS_UI_MQTT_PASSWORD_KEY: 'ZWAVEJS_UI_MQTT_PASSWORD',
};

/**
* Convert a zWave value format to the
* Gladys format.
*/
const STATES = {
binary_switch: {
currentvalue: {
[STATE.OFF]: false,
[STATE.ON]: true,
false: STATE.OFF,
true: STATE.ON,
currentvalue: (val) => {
switch (val) {
case false:
return STATE.OFF;
case true:
return STATE.ON;
default:
return null;
}
},
},
multilevel_sensor: {
air_temperature: (val) => val,
power: (val) => val,
},
notification: {
access_control: {
door_state_simple: {
22: OPENING_SENSOR_STATE.OPEN,
23: OPENING_SENSOR_STATE.CLOSE,
door_state_simple: (val) => {
switch (val) {
case 22:
return OPENING_SENSOR_STATE.OPEN;
case 23:
return OPENING_SENSOR_STATE.CLOSE;
default:
return null;
}
},
},
},
};

/**
* Convert value from Gladys format to
* the Zwave MQTT expected format.
*/
const COMMANDS = {
binary_switch: {
currentvalue: {
getName: (_nodeFeature) => 'set',
getArgs: (value, _nodeFeature) => {
return [STATES.binary_switch.currentvalue[value]];
switch (value) {
case STATE.OFF:
return [false];
case STATE.ON:
return [true];
default:
return null;
}
},
},
},
Expand All @@ -53,6 +83,28 @@ const EXPOSES = {
has_feedback: true,
},
},
multilevel_sensor: {
air_temperature: {
category: DEVICE_FEATURE_CATEGORIES.TEMPERATURE_SENSOR,
type: DEVICE_FEATURE_TYPES.SENSOR.DECIMAL,
unit: DEVICE_FEATURE_UNITS.CELSIUS,
min: -100,
max: 150,
keep_history: true,
read_only: true,
has_feedback: false,
},
power: {
category: DEVICE_FEATURE_CATEGORIES.ENERGY_SENSOR,
type: DEVICE_FEATURE_TYPES.ENERGY_SENSOR.POWER,
unit: DEVICE_FEATURE_UNITS.WATT,
min: 0,
max: 5000,
keep_history: true,
read_only: true,
has_feedback: false,
},
},
notification: {
access_control: {
door_state_simple: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ async function onNodeValueUpdated(message) {
if (propertyKeyNameClean !== '') {
statePath += `.${propertyKeyNameClean}`;
}
const valueConverted = get(STATES, `${statePath}.${newValue}`);

const nodeId = `zwavejs-ui:${messageNode.id}`;
const node = this.devices.find((n) => n.external_id === nodeId);
Expand All @@ -34,10 +33,13 @@ async function onNodeValueUpdated(message) {
return;
}

if (valueConverted !== undefined) {
const valueConverter = get(STATES, statePath);
const convertedValue = valueConverter !== undefined ? valueConverter(newValue) : null;

if (convertedValue !== null) {
await this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, {
device_feature_external_id: nodeFeature.external_id,
state: valueConverted,
state: convertedValue,
});
}
}
Expand Down
9 changes: 7 additions & 2 deletions server/services/zwavejs-ui/lib/zwaveJSUI.setValue.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function getNodeFeature(node, nodeFeatureId) {
* @description Set the new device value from Gladys to MQTT.
* @param {object} device - Updated Gladys device.
* @param {object} deviceFeature - Updated Gladys device feature.
* @param {string} value - The new device feature value.
* @param {string|number} value - The new device feature value.
* @returns {Promise} - The execution promise.
* @example
* setValue(device, deviceFeature, 0);
Expand Down Expand Up @@ -79,6 +79,11 @@ function setValue(device, deviceFeature, value) {
throw new BadParameters(`ZWaveJS-UI command not found: "${deviceFeature.external_id}"`);
}

const commandArgs = command.getArgs(value, nodeFeature);
if (commandArgs === null) {
throw new BadParameters(`ZWaveJS-UI command value not supported: "${value}"`);
}

// https://zwave-js.github.io/zwave-js-ui/#/guide/mqtt?id=send-command
// https://zwave-js.github.io/zwave-js-ui/#/guide/mqtt?id=sendcommand
const mqttPayload = {
Expand All @@ -89,7 +94,7 @@ function setValue(device, deviceFeature, value) {
endpoint: nodeFeature.endpoint,
},
command.getName(nodeFeature),
command.getArgs(value, nodeFeature),
commandArgs,
],
};
this.publish('zwave/_CLIENTS/ZWAVE_GATEWAY-zwave-js-ui/api/sendCommand/set', JSON.stringify(mqttPayload));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,26 @@ describe('zwaveJSUIHandler.onNewDeviceDiscover.js', () => {
service_id: 'ffa13430-df93-488a-9733-5c540e9558e0',
should_poll: false,
features: [
{
category: 'temperature-sensor',
command_class: 49,
command_class_name: 'Multilevel Sensor',
command_class_version: 5,
endpoint: 0,
external_id: 'zwavejs-ui:2:0:multilevel_sensor:air_temperature',
has_feedback: false,
keep_history: true,
max: 150,
min: -100,
name: '2-49-0-Air temperature',
node_id: 2,
property_key_name: undefined,
property_name: 'Air temperature',
read_only: true,
selector: 'zwavejs-ui:2:0:multilevel_sensor:air_temperature',
type: 'decimal',
unit: 'celsius',
},
{
category: 'opening-sensor',
type: 'binary',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,37 @@ describe('zwaveJSUIHandler.onNodeValueUpdated', () => {
state: 1,
});
});
it('should not fail on unsupported door value', async () => {
const zwaveJSUIHandler = new ZwaveJSUIHandler(gladys, {}, serviceId);
zwaveJSUIHandler.devices = [
{
external_id: 'zwavejs-ui:2',
features: [
{
external_id: 'zwavejs-ui:2:0:notification:access_control:door_state_simple',
},
],
},
];

await zwaveJSUIHandler.onNodeValueUpdated({
data: [
{ id: 2 },
{
commandClassName: 'Notification',
commandClass: 113,
property: 'Access Control',
endpoint: 0,
newValue: 45,
prevValue: 22,
propertyName: 'Access Control',
propertyKey: 'Door state (simple)',
propertyKeyName: 'Door state (simple)',
},
],
});
assert.notCalled(gladys.event.emit);
});
it('should save a new true binary value', async () => {
const zwaveJSUIHandler = new ZwaveJSUIHandler(gladys, {}, serviceId);
zwaveJSUIHandler.devices = [
Expand Down Expand Up @@ -212,4 +243,99 @@ describe('zwaveJSUIHandler.onNodeValueUpdated', () => {
state: 0,
});
});

it('should not fail on unsupported binary switch value', async () => {
const zwaveJSUIHandler = new ZwaveJSUIHandler(gladys, {}, serviceId);
zwaveJSUIHandler.devices = [
{
external_id: 'zwavejs-ui:3',
features: [
{
external_id: 'zwavejs-ui:3:0:binary_switch:currentvalue',
},
],
},
];

await zwaveJSUIHandler.onNodeValueUpdated({
data: [
{ id: 3 },
{
commandClassName: 'Binary Switch',
commandClass: 37,
property: 'currentValue',
endpoint: 0,
newValue: -1,
prevValue: true,
propertyName: 'currentValue',
},
],
});
assert.notCalled(gladys.event.emit);
});
it('should save a new air temperature value', async () => {
const zwaveJSUIHandler = new ZwaveJSUIHandler(gladys, {}, serviceId);
zwaveJSUIHandler.devices = [
{
external_id: 'zwavejs-ui:2',
features: [
{
external_id: 'zwavejs-ui:2:0:multilevel_sensor:air_temperature',
},
],
},
];

await zwaveJSUIHandler.onNodeValueUpdated({
data: [
{ id: 2 },
{
commandClassName: 'Multilevel Sensor',
commandClass: 49,
property: 'Air temperature',
endpoint: 0,
newValue: 20.8,
prevValue: 17.5,
propertyName: 'Air temperature',
},
],
});
assert.calledWith(gladys.event.emit, 'device.new-state', {
device_feature_external_id: 'zwavejs-ui:2:0:multilevel_sensor:air_temperature',
state: 20.8,
});
});

it('should save a new power value', async () => {
const zwaveJSUIHandler = new ZwaveJSUIHandler(gladys, {}, serviceId);
zwaveJSUIHandler.devices = [
{
external_id: 'zwavejs-ui:2',
features: [
{
external_id: 'zwavejs-ui:2:0:multilevel_sensor:power',
},
],
},
];

await zwaveJSUIHandler.onNodeValueUpdated({
data: [
{ id: 2 },
{
commandClassName: 'Multilevel Sensor',
commandClass: 49,
property: 'Power',
endpoint: 0,
newValue: 1.7,
prevValue: 0,
propertyName: 'Power',
},
],
});
assert.calledWith(gladys.event.emit, 'device.new-state', {
device_feature_external_id: 'zwavejs-ui:2:0:multilevel_sensor:power',
state: 1.7,
});
});
});
Loading

0 comments on commit d198227

Please sign in to comment.