From adc1df0f38fdc3b71f12a191ee0bc7c05db28606 Mon Sep 17 00:00:00 2001 From: William Deren Date: Mon, 11 Nov 2024 09:40:39 +0100 Subject: [PATCH] Scene: add new action send zigbee2mqtt msg (#2160) --- front/src/config/i18n/de.json | 10 +++ front/src/config/i18n/en.json | 10 +++ front/src/config/i18n/fr.json | 10 +++ .../routes/scene/edit-scene/ActionCard.jsx | 14 +++ .../actions/ChooseActionTypeCard.jsx | 1 + .../actions/SendZigbee2MqttMessage.jsx | 66 ++++++++++++++ server/lib/scene/scene.actions.js | 8 ++ server/services/zigbee2mqtt/lib/index.js | 2 + server/services/zigbee2mqtt/lib/publish.js | 24 +++++ ...cene.action.sendZigbee2MqttMessage.test.js | 87 +++++++++++++++++++ .../services/zigbee2mqtt/lib/publish.test.js | 76 ++++++++++++++++ server/utils/constants.js | 3 + 12 files changed, 311 insertions(+) create mode 100644 front/src/routes/scene/edit-scene/actions/SendZigbee2MqttMessage.jsx create mode 100644 server/services/zigbee2mqtt/lib/publish.js create mode 100644 server/test/lib/scene/actions/scene.action.sendZigbee2MqttMessage.test.js create mode 100644 server/test/services/zigbee2mqtt/lib/publish.test.js diff --git a/front/src/config/i18n/de.json b/front/src/config/i18n/de.json index d4c1d6826b..58ee0174cc 100644 --- a/front/src/config/i18n/de.json +++ b/front/src/config/i18n/de.json @@ -1957,6 +1957,13 @@ "variablesExplanation": "Um eine Variable im Text einzufügen, gib \"{{\" ein. Um einen Variablenwert festzulegen, musst du vor diesem Feld das das Feld \"Gerätewert abrufen\" verwenden.", "messagePlaceholder": "Meine Message" }, + "zigbee2mqttMessage": { + "topic": "Topic", + "topicPlaceholder": "/gladys/mein-topic", + "messageLabel": "Message", + "variablesExplanation": "Um eine Variable im Text einzufügen, gib \"{{\" ein. Um einen Variablenwert festzulegen, musst du vor diesem Feld das das Feld \"Gerätewert abrufen\" verwenden.", + "messagePlaceholder": "Meine Message" + }, "playNotification": { "description": "Diese Aktion lässt Gladys auf dem ausgewählten Lautsprecher sprechen.", "needGladysPlus": "Erfordert Gladys Plus, da die Text-to-Speech-APIs kostenpflichtig sind.", @@ -2025,6 +2032,9 @@ "mqtt": { "send": "MQTT-Message senden" }, + "zigbee2mqtt": { + "send": "Zigbee2mqtt-Message senden" + }, "music": { "play-notification": "Auf einem Lautsprecher sprechen" }, diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index 72d7deb557..0ee6df62e6 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -1957,6 +1957,13 @@ "variablesExplanation": "To inject a variable in the text, press '{{ '. To set a variable value, you need to use the 'Get device value' box before this one.", "messagePlaceholder": "My message" }, + "zigbee2mqttMessage": { + "topic": "Topic", + "topicPlaceholder": "/gladys/my-topic", + "messageLabel": "Message", + "variablesExplanation": "To inject a variable in the text, press '{{ '. To set a variable value, you need to use the 'Get device value' box before this one.", + "messagePlaceholder": "My message" + }, "playNotification": { "description": "This action will make Gladys speak on the selected speaker.", "needGladysPlus": "Requires Gladys Plus as Text-To-Speech APIs are paid.", @@ -2025,6 +2032,9 @@ "mqtt": { "send": "Send MQTT Message" }, + "zigbee2mqtt": { + "send": "Send Zigbee2mqtt Message" + }, "music": { "play-notification": "Talk on a speaker" }, diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json index 93a14e446f..cafd79f2f1 100644 --- a/front/src/config/i18n/fr.json +++ b/front/src/config/i18n/fr.json @@ -1957,6 +1957,13 @@ "variablesExplanation": "Pour injecter une variable, tapez '{{ '. Attention, vous devez avoir défini une variable auparavant dans une action 'Récupérer le dernier état' placé avant ce bloc message.", "messagePlaceholder": "Mon message" }, + "zigbee2mqttMessage": { + "topic": "Topic", + "topicPlaceholder": "/gladys/my-topic", + "messageLabel": "Message", + "variablesExplanation": "Pour injecter une variable, tapez '{{ '. Attention, vous devez avoir défini une variable auparavant dans une action 'Récupérer le dernier état' placé avant ce bloc message.", + "messagePlaceholder": "Mon message" + }, "playNotification": { "description": "Cette action fera parler Gladys sur l'enceinte sélectionnée.", "needGladysPlus": "Nécessite Gladys Plus car les API de \"Text To Speech\" sont payantes.", @@ -2025,6 +2032,9 @@ "mqtt": { "send": "Envoyer un message MQTT" }, + "zigbee2mqtt": { + "send": "Envoyer un message Zigbee2mqtt" + }, "music": { "play-notification": "Parler sur une enceinte" }, diff --git a/front/src/routes/scene/edit-scene/ActionCard.jsx b/front/src/routes/scene/edit-scene/ActionCard.jsx index 7fae038dba..e7cfb314b4 100644 --- a/front/src/routes/scene/edit-scene/ActionCard.jsx +++ b/front/src/routes/scene/edit-scene/ActionCard.jsx @@ -29,6 +29,7 @@ import SendMessageCameraParams from './actions/SendMessageCameraParams'; import CheckAlarmMode from './actions/CheckAlarmMode'; import SetAlarmMode from './actions/SetAlarmMode'; import SendMqttMessage from './actions/SendMqttMessage'; +import SendZigbee2MqttMessage from './actions/SendZigbee2MqttMessage'; import PlayNotification from './actions/PlayNotification'; import EdfTempoCondition from './actions/EdfTempoCondition'; import AskAI from './actions/AskAI'; @@ -66,6 +67,7 @@ const ACTION_ICON = { [ACTIONS.ALARM.SET_ALARM_MODE]: 'fe fe-bell', [ACTIONS.MQTT.SEND]: 'fe fe-message-square', [ACTIONS.MUSIC.PLAY_NOTIFICATION]: 'fe fe-speaker', + [ACTIONS.ZIGBEE2MQTT.SEND]: 'fe fe-message-square', [ACTIONS.AI.ASK]: 'fe fe-cpu' }; @@ -104,6 +106,7 @@ const ActionCard = ({ children, ...props }) => { props.action.type === ACTIONS.MESSAGE.SEND || props.action.type === ACTIONS.CALENDAR.IS_EVENT_RUNNING || props.action.type === ACTIONS.MQTT.SEND || + props.action.type === ACTIONS.ZIGBEE2MQTT.SEND || props.action.type === ACTIONS.LIGHT.BLINK, 'col-lg-4': props.action.type !== ACTIONS.CONDITION.ONLY_CONTINUE_IF && @@ -392,6 +395,17 @@ const ActionCard = ({ children, ...props }) => { triggersVariables={props.triggersVariables} /> )} + {props.action.type === ACTIONS.ZIGBEE2MQTT.SEND && ( + + )} {props.action.type === ACTIONS.MUSIC.PLAY_NOTIFICATION && ( { + this.props.updateActionProperty(this.props.columnIndex, this.props.index, 'topic', e.target.value); + }; + handleChangeMessage = text => { + const newMessage = text && text.length > 0 ? text : undefined; + this.props.updateActionProperty(this.props.columnIndex, this.props.index, 'message', newMessage); + }; + + render(props) { + return ( +
+
+
+ + + } + /> + +
+
+ +
+ +
+ + } + /> + +
+
+
+ ); + } +} + +export default connect('httpClient', {})(SendZigbee2MqttMessage); diff --git a/server/lib/scene/scene.actions.js b/server/lib/scene/scene.actions.js index dafbcc422e..99df9acc72 100644 --- a/server/lib/scene/scene.actions.js +++ b/server/lib/scene/scene.actions.js @@ -565,6 +565,14 @@ const actionsFunc = { mqttService.device.publish(action.topic, messageWithVariables); } }, + [ACTIONS.ZIGBEE2MQTT.SEND]: (self, action, scope) => { + const zigbee2mqttService = self.service.getService('zigbee2mqtt'); + + if (zigbee2mqttService) { + const messageWithVariables = Handlebars.compile(action.message)(scope); + zigbee2mqttService.device.publish(action.topic, messageWithVariables); + } + }, [ACTIONS.MUSIC.PLAY_NOTIFICATION]: async (self, action, scope) => { // Get device const device = self.stateManager.get('device', action.device); diff --git a/server/services/zigbee2mqtt/lib/index.js b/server/services/zigbee2mqtt/lib/index.js index 058c3d79d3..15f70704d5 100644 --- a/server/services/zigbee2mqtt/lib/index.js +++ b/server/services/zigbee2mqtt/lib/index.js @@ -4,6 +4,7 @@ const { getConfiguration } = require('./getConfiguration'); const { saveConfiguration } = require('./saveConfiguration'); const { disconnect } = require('./disconnect'); const { handleMqttMessage } = require('./handleMqttMessage'); +const { publish } = require('./publish'); const { getDiscoveredDevices } = require('./getDiscoveredDevices'); const { findMatchingExpose } = require('./findMatchingExpose'); const { readValue } = require('./readValue'); @@ -70,6 +71,7 @@ Zigbee2mqttManager.prototype.getConfiguration = getConfiguration; Zigbee2mqttManager.prototype.saveConfiguration = saveConfiguration; Zigbee2mqttManager.prototype.disconnect = disconnect; Zigbee2mqttManager.prototype.handleMqttMessage = handleMqttMessage; +Zigbee2mqttManager.prototype.publish = publish; Zigbee2mqttManager.prototype.getDiscoveredDevices = getDiscoveredDevices; Zigbee2mqttManager.prototype.findMatchingExpose = findMatchingExpose; Zigbee2mqttManager.prototype.readValue = readValue; diff --git a/server/services/zigbee2mqtt/lib/publish.js b/server/services/zigbee2mqtt/lib/publish.js new file mode 100644 index 0000000000..c7a4b58683 --- /dev/null +++ b/server/services/zigbee2mqtt/lib/publish.js @@ -0,0 +1,24 @@ +const logger = require('../../../utils/logger'); +const { ServiceNotConfiguredError } = require('../../../utils/coreErrors'); + +/** + * @description Publish a MQTT message. + * @param {string} topic - MQTT Topic. + * @param {string} message - MQTT message. + * @example zigbee2mqtt.publish('zigbee2mqtt/test', '{}'); + */ +function publish(topic, message) { + if (!this.mqttClient) { + throw new ServiceNotConfiguredError('MQTT is not configured.'); + } + logger.debug(`Publishing MQTT message on topic ${topic}`); + this.mqttClient.publish(topic, message, undefined, (err) => { + if (err) { + logger.warn(`MQTT - Error publishing to ${topic} : ${err}`); + } + }); +} + +module.exports = { + publish, +}; diff --git a/server/test/lib/scene/actions/scene.action.sendZigbee2MqttMessage.test.js b/server/test/lib/scene/actions/scene.action.sendZigbee2MqttMessage.test.js new file mode 100644 index 0000000000..a9b710cf60 --- /dev/null +++ b/server/test/lib/scene/actions/scene.action.sendZigbee2MqttMessage.test.js @@ -0,0 +1,87 @@ +const { fake, assert } = require('sinon'); +const EventEmitter = require('events'); + +const { ACTIONS } = require('../../../../utils/constants'); +const { executeActions } = require('../../../../lib/scene/scene.executeActions'); + +const StateManager = require('../../../../lib/state'); + +const event = new EventEmitter(); + +describe('scene.send-zigbee2mqtt-message', () => { + it('should send message with value injected from device get-value', async () => { + const stateManager = new StateManager(event); + stateManager.setState('deviceFeature', 'my-device-feature', { + category: 'light', + type: 'binary', + last_value: 15, + }); + const zigbee2MqttService = { + device: { + publish: fake.resolves(null), + }, + }; + const service = { + getService: fake.returns(zigbee2MqttService), + }; + const scope = {}; + await executeActions( + { stateManager, event, service }, + [ + [ + { + type: ACTIONS.DEVICE.GET_VALUE, + device_feature: 'my-device-feature', + }, + ], + [ + { + type: ACTIONS.ZIGBEE2MQTT.SEND, + topic: '/my/mqtt/topic', + message: 'Temperature in the living room is {{0.0.last_value}} °C.', + }, + ], + ], + scope, + ); + assert.calledWith(zigbee2MqttService.device.publish, '/my/mqtt/topic', 'Temperature in the living room is 15 °C.'); + }); + it('should send message with value injected from http-request', async () => { + const stateManager = new StateManager(event); + const http = { + request: fake.resolves({ result: [15], error: null }), + }; + const zigbee2MqttService = { + device: { + publish: fake.resolves(null), + }, + }; + const service = { + getService: fake.returns(zigbee2MqttService), + }; + const scope = {}; + await executeActions( + { stateManager, event, service, http }, + [ + [ + { + type: ACTIONS.HTTP.REQUEST, + method: 'post', + url: 'http://test.test', + body: '{"toto":"toto"}', + headers: [], + }, + ], + [ + { + type: ACTIONS.ZIGBEE2MQTT.SEND, + topic: '/my/mqtt/topic', + message: 'Temperature in the living room is {{0.0.result.[0]}} °C.', + }, + ], + ], + scope, + ); + assert.calledWith(zigbee2MqttService.device.publish, '/my/mqtt/topic', 'Temperature in the living room is 15 °C.'); + }); +}); diff --git a/server/test/services/zigbee2mqtt/lib/publish.test.js b/server/test/services/zigbee2mqtt/lib/publish.test.js new file mode 100644 index 0000000000..b658bcfe56 --- /dev/null +++ b/server/test/services/zigbee2mqtt/lib/publish.test.js @@ -0,0 +1,76 @@ +const sinon = require('sinon'); +const { expect } = require('chai'); + +const { assert, fake } = sinon; + +const Zigbee2mqttManager = require('../../../../services/zigbee2mqtt/lib'); +const { ServiceNotConfiguredError } = require('../../../../utils/coreErrors'); + +const serviceId = 'f87b7af2-ca8e-44fc-b754-444354b42fee'; + +const gladys = { + job: { + wrapper: (type, func) => { + return async () => { + return func(); + }; + }, + }, + variable: { + getValue: fake.resolves('toto'), + }, + event: { + emit: fake.returns(null), + }, +}; + +describe('zigbee2mqttManager.publish', () => { + beforeEach(() => { + sinon.reset(); + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should publish MQTT message', () => { + const mqttClient = { + publish: fake.returns(null), + }; + const mqttLibrary = { + connect: fake.returns(mqttClient), + }; + const zigbee2mqttManager = new Zigbee2mqttManager(gladys, mqttLibrary, serviceId); + zigbee2mqttManager.mqttClient = mqttClient; + zigbee2mqttManager.publish('toto', 'message'); + assert.calledWith(mqttClient.publish, 'toto', 'message'); + }); + it('should publish MQTT message with error', () => { + const mqttClient = { + publish: (topic, message, random, cb) => { + cb('toto'); + }, + }; + const mqttLibrary = { + connect: fake.returns(mqttClient), + }; + const zigbee2mqttManager = new Zigbee2mqttManager(gladys, mqttLibrary, serviceId); + zigbee2mqttManager.mqttClient = mqttClient; + zigbee2mqttManager.publish('toto', 'mesage'); + }); + it('should not publish MQTT message', async () => { + const mqttLibrary = { + connect: fake.returns(null), + }; + const zigbee2mqttManager = new Zigbee2mqttManager(gladys, mqttLibrary, serviceId); + try { + zigbee2mqttManager.publish('toto', 'mesage'); + } catch (e) { + expect(e).instanceOf(ServiceNotConfiguredError); + + return; + } + + assert.fail(); + }); +}); diff --git a/server/utils/constants.js b/server/utils/constants.js index e52d06d851..b9c70e5bd7 100644 --- a/server/utils/constants.js +++ b/server/utils/constants.js @@ -404,6 +404,9 @@ const ACTIONS = { MQTT: { SEND: 'mqtt.send', }, + ZIGBEE2MQTT: { + SEND: 'zigbee2mqtt.send', + }, MUSIC: { PLAY_NOTIFICATION: 'music.play-notification', },