Skip to content

Commit

Permalink
ZwaveJS UI : Binary Switch support (#2002)
Browse files Browse the repository at this point in the history
Co-authored-by: Stéphane Escandell <stephane.escandell@sociabble.com>
  • Loading branch information
sescandell and Stéphane Escandell authored Feb 12, 2024
1 parent ad48004 commit fa39291
Show file tree
Hide file tree
Showing 23 changed files with 747 additions and 108 deletions.
4 changes: 2 additions & 2 deletions front/src/config/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1037,7 +1037,7 @@
"saveButton": "Save",
"alreadyCreatedButton": "Already Created",
"deleteButton": "Delete",
"alphaWarning": "This integration is in alpha and currently only supports one device: The Fibaro Door/Window sensor. We welcome any assistance to integrate other devices!",
"alphaWarning": "This integration is in alpha and currently only supports few devices. We welcome any assistance to integrate other devices!",
"device": {
"title": "Z-Wave JS UI Devices in Gladys",
"editButton": "Edit",
Expand All @@ -1046,7 +1046,7 @@
},
"discover": {
"title": "Devices detected on your Z-Wave JS UI instance",
"description": "Z-Wave JS UI devices are automatically discovered. Note that only devices with a name and a 'location' are displayed here. If you edit these settings, you will need to re-pair the device in Gladys.",
"description": "Z-Wave JS UI devices are automatically discovered. Note that only devices with a name are displayed here.",
"error": "Error discovering Z-Wave JS UI devices. Is your MQTT broker available and accessible?",
"noDeviceFound": "No Z-Wave JS UI devices were discovered.",
"errorWhileScanning": "An error occurred during discovery.",
Expand Down
4 changes: 2 additions & 2 deletions front/src/config/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -1166,7 +1166,7 @@
"saveButton": "Sauvegarder",
"alreadyCreatedButton": "Déjà créé",
"deleteButton": "Supprimer",
"alphaWarning": "Cette intégration est une alpha et ne gère pour l'instant qu'un seul appareil: Le capteur d'ouverture de porte Fibaro. Nous sommes preneur de toute aide pour intégrer d'autres appareils !",
"alphaWarning": "Cette intégration est une alpha et ne gère pour l'instant qu'un nombre limité d'appareils. Nous sommes preneur de toute aide pour intégrer d'autres appareils !",
"device": {
"title": "Appareils Z-Wave JS UI dans Gladys",
"editButton": "Editer",
Expand All @@ -1175,7 +1175,7 @@
},
"discover": {
"title": "Appareils détectés sur votre instance Z-Wave JS UI",
"description": "Les appareils Z-Wave JS UI sont automatiquement découverts. Attention, seuls les appareils avec un nom et une \"location\" sont affichés ici. Si vous éditez ces paramètres, vous devrez réappairer l'appareil dans Gladys.",
"description": "Les appareils Z-Wave JS UI sont automatiquement découverts. Attention, seuls les appareils avec un nom sont affichés ici.",
"error": "Erreur de découverte des appareils Z-Wave JS UI. Est-ce que votre broker MQTT est bien disponible et accessible ?",
"noDeviceFound": "Aucun appareil Z-Wave JS UI n'a été découvert.",
"errorWhileScanning": "Une erreur est survenue lors de la découverte.",
Expand Down
2 changes: 1 addition & 1 deletion server/lib/device/device.create.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ async function create(device) {

const deviceFeaturesIdsToPurge = [];

// we execute the whole insert in a transaction to avoir inconsistent state
// we execute the whole insert in a transaction to avoid inconsistent state
await db.sequelize.transaction(async (transaction) => {
// external_id is a required parameter
if (!device.external_id) {
Expand Down
2 changes: 1 addition & 1 deletion server/services/usb/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const logger = require('../../utils/logger');
const UsbController = require('./api/usb.controller');

module.exports = function ZwaveService(gladys, serviceId) {
module.exports = function UsbService(gladys, serviceId) {
const SerialPort = require('serialport');
/**
* @public
Expand Down
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.

8 changes: 4 additions & 4 deletions server/services/zwavejs-ui/api/zwaveJSUI.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ module.exports = function ZwaveJSUIController(zwaveJSUIHandler) {
* @apiName discover
* @apiGroup ZwaveJSUI
*/
async function discover(req, res) {
await zwaveJSUIHandler.scan();
function discover(req, res) {
zwaveJSUIHandler.scan();
res.json({ success: true });
}
/**
Expand Down Expand Up @@ -44,7 +44,7 @@ module.exports = function ZwaveJSUIController(zwaveJSUIHandler) {
* @apiName getNodes
* @apiGroup ZwaveJSUI
*/
async function getNodes(req, res) {
function getNodes(req, res) {
res.json(zwaveJSUIHandler.devices);
}

Expand All @@ -53,7 +53,7 @@ module.exports = function ZwaveJSUIController(zwaveJSUIHandler) {
* @apiName getStatus
* @apiGroup ZwaveJSUI
*/
async function getStatus(req, res) {
function getStatus(req, res) {
res.json({
connected: zwaveJSUIHandler.connected,
configured: zwaveJSUIHandler.configured,
Expand Down
60 changes: 48 additions & 12 deletions server/services/zwavejs-ui/lib/constants.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,75 @@
const { DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES, OPENING_SENSOR_STATE } = require('../../../utils/constants');
const {
DEVICE_FEATURE_CATEGORIES,
DEVICE_FEATURE_TYPES,
OPENING_SENSOR_STATE,
STATE,
} = require('../../../utils/constants');

const CONFIGURATION = {
ZWAVEJS_UI_MQTT_URL_KEY: 'ZWAVEJS_UI_MQTT_URL',
ZWAVEJS_UI_MQTT_USERNAME_KEY: 'ZWAVEJS_UI_MQTT_USERNAME',
ZWAVEJS_UI_MQTT_PASSWORD_KEY: 'ZWAVEJS_UI_MQTT_PASSWORD',
};

const EXPOSES = {
const STATES = {
binary_switch: {
currentvalue: {
[STATE.OFF]: false,
[STATE.ON]: true,
false: STATE.OFF,
true: STATE.ON,
},
},
notification: {
access_control: {
door_state_simple: {
category: DEVICE_FEATURE_CATEGORIES.OPENING_SENSOR,
type: DEVICE_FEATURE_TYPES.SENSOR.BINARY,
min: 0,
max: 1,
keep_history: true,
read_only: true,
has_feedback: true,
22: OPENING_SENSOR_STATE.OPEN,
23: OPENING_SENSOR_STATE.CLOSE,
},
},
},
};

const STATES = {
const COMMANDS = {
binary_switch: {
currentvalue: {
getName: (_nodeFeature) => 'set',
getArgs: (value, _nodeFeature) => {
return [STATES.binary_switch.currentvalue[value]];
},
},
},
};

const EXPOSES = {
binary_switch: {
currentvalue: {
category: DEVICE_FEATURE_CATEGORIES.SWITCH,
type: DEVICE_FEATURE_TYPES.SWITCH.BINARY,
min: 0,
max: 1,
keep_history: true,
read_only: false,
has_feedback: true,
},
},
notification: {
access_control: {
door_state_simple: {
22: OPENING_SENSOR_STATE.OPEN,
23: OPENING_SENSOR_STATE.CLOSE,
category: DEVICE_FEATURE_CATEGORIES.OPENING_SENSOR,
type: DEVICE_FEATURE_TYPES.SENSOR.BINARY,
min: 0,
max: 1,
keep_history: true,
read_only: true,
has_feedback: true,
},
},
},
};

module.exports = {
COMMANDS,
CONFIGURATION,
EXPOSES,
STATES,
Expand Down
4 changes: 4 additions & 0 deletions server/services/zwavejs-ui/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ const { disconnect } = require('./zwaveJSUI.disconnect');
const { getConfiguration } = require('./zwaveJSUI.getConfiguration');
const { handleNewMessage } = require('./zwaveJSUI.handleNewMessage');
const { onNewDeviceDiscover } = require('./zwaveJSUI.onNewDeviceDiscover');
const { onNodeValueUpdated } = require('./zwaveJSUI.onNodeValueUpdated');
const { publish } = require('./zwaveJSUI.publish');
const { scan } = require('./zwaveJSUI.scan');
const { saveConfiguration } = require('./zwaveJSUI.saveConfiguration');
const { setValue } = require('./zwaveJSUI.setValue');

/**
* @description Z-Wave JS UI handler.
Expand All @@ -32,8 +34,10 @@ ZwaveJSUIHandler.prototype.disconnect = disconnect;
ZwaveJSUIHandler.prototype.getConfiguration = getConfiguration;
ZwaveJSUIHandler.prototype.handleNewMessage = handleNewMessage;
ZwaveJSUIHandler.prototype.onNewDeviceDiscover = onNewDeviceDiscover;
ZwaveJSUIHandler.prototype.onNodeValueUpdated = onNodeValueUpdated;
ZwaveJSUIHandler.prototype.publish = publish;
ZwaveJSUIHandler.prototype.scan = scan;
ZwaveJSUIHandler.prototype.saveConfiguration = saveConfiguration;
ZwaveJSUIHandler.prototype.setValue = setValue;

module.exports = ZwaveJSUIHandler;
16 changes: 8 additions & 8 deletions server/services/zwavejs-ui/lib/zwaveJSUI.connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,37 +24,37 @@ async function connect() {
clientId: `gladys-main-instance-zwavejs-ui-${Math.floor(Math.random() * 1000000)}`,
});

this.mqttClient.on('connect', () => {
this.mqttClient.on('connect', async () => {
logger.info(`Connected to MQTT server ${mqttUrl}`);

this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, {
await this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, {
type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJS_UI.CONNECTED,
});

this.mqttClient.subscribe('zwave/#');
this.connected = true;
this.scan();
});
this.mqttClient.on('error', (err) => {
this.mqttClient.on('error', async (err) => {
logger.warn(`Error while connecting to MQTT - ${err}`);

this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, {
await this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, {
type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJS_UI.ERROR,
payload: err,
});

this.disconnect();
});
this.mqttClient.on('offline', () => {
this.mqttClient.on('offline', async () => {
logger.warn(`Disconnected from MQTT server`);
this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, {
await this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, {
type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJS_UI.ERROR,
payload: 'DISCONNECTED',
});
this.connected = false;
});
this.mqttClient.on('message', (topic, message) => {
this.handleNewMessage(topic, message.toString());
this.mqttClient.on('message', async (topic, message) => {
await this.handleNewMessage(topic, message.toString());
});
}

Expand Down
2 changes: 1 addition & 1 deletion server/services/zwavejs-ui/lib/zwaveJSUI.disconnect.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const logger = require('../../../utils/logger');
* @description This function will disconnect the MQTT broker.
* @example zwaveJSUI.disconnect();
*/
async function disconnect() {
function disconnect() {
this.connected = false;

if (this.mqttClient) {
Expand Down
34 changes: 4 additions & 30 deletions server/services/zwavejs-ui/lib/zwaveJSUI.handleNewMessage.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
const get = require('get-value');
const { EVENTS } = require('../../../utils/constants');
const logger = require('../../../utils/logger');
const { STATES } = require('./constants');
const { cleanNames, getDeviceFeatureExternalId } = require('../utils/convertToGladysDevice');

/**
* @description Handle a new message receive in MQTT.
Expand All @@ -11,38 +7,16 @@ const { cleanNames, getDeviceFeatureExternalId } = require('../utils/convertToGl
* @example
* handleNewMessage('/zwave/#', '{}');
*/
function handleNewMessage(topic, message) {
async function handleNewMessage(topic, message) {
logger.debug(`Receives MQTT message from ${topic}`);

try {
const splittedTopic = topic.split('/');
const parsedMessage = JSON.parse(message);
// On list of devices received
if (topic === 'zwave/_CLIENTS/ZWAVE_GATEWAY-zwave-js-ui/api/getNodes') {
this.onNewDeviceDiscover(parsedMessage);
} else if (splittedTopic.length === 7) {
// trying to match example: zwave/living-room/my-sensor/notification/endpoint_0/Access_Control/Door_state_simple
const [, , , comClassName, endpoint, property, propertyKey] = splittedTopic;
const [, endpointNumber] = endpoint.split('_');
const comClassNameClean = cleanNames(comClassName);
const propertyClean = cleanNames(property);
const propertyKeyClean = cleanNames(propertyKey);
const valueConverted = get(
STATES,
`${comClassNameClean}.${propertyClean}.${propertyKeyClean}.${parsedMessage.value}`,
);
if (valueConverted !== undefined) {
this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, {
device_feature_external_id: getDeviceFeatureExternalId(
parsedMessage.nodeId,
endpointNumber,
comClassNameClean,
propertyClean,
propertyKeyClean,
),
state: valueConverted,
});
}
await this.onNewDeviceDiscover(parsedMessage);
} else if (topic === 'zwave/_EVENTS/ZWAVE_GATEWAY-zwave-js-ui/node/node_value_updated') {
await this.onNodeValueUpdated(parsedMessage);
}
} catch (e) {
logger.warn(`Unable to handle new MQTT message in topic ${topic}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ const { convertToGladysDevice } = require('../utils/convertToGladysDevice');
async function onNewDeviceDiscover(data) {
const devices = [];
data.result.forEach((zwaveJSDevice) => {
if (zwaveJSDevice.name && zwaveJSDevice.name.length > 0 && zwaveJSDevice.loc && zwaveJSDevice.loc.length > 0) {
if (zwaveJSDevice.name && zwaveJSDevice.name.length > 0) {
devices.push(convertToGladysDevice(this.serviceId, zwaveJSDevice));
}
});
this.devices = devices;
this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, {
await this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, {
type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJS_UI.SCAN_COMPLETED,
});
}
Expand Down
47 changes: 47 additions & 0 deletions server/services/zwavejs-ui/lib/zwaveJSUI.onNodeValueUpdated.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const get = require('get-value');
const { EVENTS } = require('../../../utils/constants');
const { STATES } = require('./constants');
const { cleanNames, getDeviceFeatureId } = require('../utils/convertToGladysDevice');

/**
* @description This will be called when new Z-Wave node value is updated.
* @param {object} message - Data sent by ZWave JS UI.
* @example zwaveJSUI.onNodeValueUpdated({data: [{node}, {value}]});
*/
async function onNodeValueUpdated(message) {
// A value has been updated: https://zwave-js.github.io/node-zwave-js/#/api/node?id=quotvalue-addedquot-quotvalue-updatedquot-quotvalue-removedquot
const messageNode = message.data[0];
const updatedValue = message.data[1];
const { commandClassName, propertyName, propertyKeyName, endpoint, newValue } = updatedValue;
const comClassNameClean = cleanNames(commandClassName);
const propertyNameClean = cleanNames(propertyName);
const propertyKeyNameClean = cleanNames(propertyKeyName);
let statePath = `${comClassNameClean}.${propertyNameClean}`;
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);
if (!node) {
return;
}

const featureId = getDeviceFeatureId(messageNode.id, commandClassName, endpoint, propertyName, propertyKeyName);
const nodeFeature = node.features.find((f) => f.external_id === featureId);
if (!nodeFeature) {
return;
}

if (valueConverted !== undefined) {
await this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, {
device_feature_external_id: nodeFeature.external_id,
state: valueConverted,
});
}
}

module.exports = {
onNodeValueUpdated,
};
2 changes: 1 addition & 1 deletion server/services/zwavejs-ui/lib/zwaveJSUI.publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const { ServiceNotConfiguredError } = require('../../../utils/coreErrors');
* @param {string} message - MQTT message.
* @example zwaveJSUI.publish('zwave/test', '{}');
*/
async function publish(topic, message) {
function publish(topic, message) {
if (!this.mqttClient) {
throw new ServiceNotConfiguredError('MQTT is not configured.');
}
Expand Down
2 changes: 1 addition & 1 deletion server/services/zwavejs-ui/lib/zwaveJSUI.scan.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const logger = require('../../../utils/logger');
* @description This will discovery Z-Wave JS UI devices.
* @example zwaveJSUI.scan();
*/
async function scan() {
function scan() {
logger.info('Asking ZWave JS UI for the list of devices');
this.publish('zwave/_CLIENTS/ZWAVE_GATEWAY-zwave-js-ui/api/getNodes/set', 'true');
}
Expand Down
Loading

0 comments on commit fa39291

Please sign in to comment.