diff --git a/front/src/components/app.jsx b/front/src/components/app.jsx
index f6d2b0d807..e921dfff8c 100644
--- a/front/src/components/app.jsx
+++ b/front/src/components/app.jsx
@@ -103,6 +103,7 @@ import LANManagerSettingsPage from '../routes/integration/all/lan-manager/settin
import MqttDevicePage from '../routes/integration/all/mqtt/device-page';
import MqttDeviceSetupPage from '../routes/integration/all/mqtt/device-page/setup';
import MqttSetupPage from '../routes/integration/all/mqtt/setup-page';
+import MqttDebugPage from '../routes/integration/all/mqtt/debug-page/Debug';
// Zigbee2mqtt
import Zigbee2mqttPage from '../routes/integration/all/zigbee2mqtt/device-page';
@@ -262,6 +263,7 @@ const AppRouter = connect(
+
diff --git a/front/src/config/i18n/de.json b/front/src/config/i18n/de.json
index e2859716b1..5b086815e5 100644
--- a/front/src/config/i18n/de.json
+++ b/front/src/config/i18n/de.json
@@ -1191,6 +1191,7 @@
"title": "MQTT",
"description": "Verbinde dich mit einem lokalen oder entfernten MQTT-Server.",
"deviceTab": "Meine Geräte",
+ "debugTab": "Debug MQTT",
"setupTab": "Einrichtung",
"documentation": "MQTT-Dokumentation",
"apiDocumentation": "MQTT-API-Dokumentation ",
@@ -1235,7 +1236,13 @@
"mqttTopicToPublishExampleDescription": "Gladys hört auf dieses Topic. Wenn du einen Wert in diesem Thema veröffentlichst, wird er diesem Gerät zugeordnet. (siehe Dokumentation )",
"mqttTopicToListenExampleLabel": "MQTT-Hörensthema",
"mqttTopicToListenExampleDescription": "Gladys veröffentlicht in diesem Topic, wenn das Gerät in Gladys gesteuert wird. Du musst diesem Topic zuhören. (siehe Dokumentation )",
+ "mqttCustomTopic": "Benutzerdefiniertes MQTT-Thema",
+ "mqttCustomTopicDescription": "Gladys bietet eine Standard-MQTT-API an, ermöglicht Ihnen jedoch die Verwendung benutzerdefinierter Themen, wenn Sie ein Gerät verwenden, das nativ auf einem bestimmten Thema veröffentlicht.",
+ "mqttCustomTopicPlaceholder": "Geben Sie das MQTT-Thema ein",
"copyMqttTopic": "MQTT-Thema kopieren",
+ "mqttCustomObjectPath": "MQTT JSON Nachricht",
+ "mqttCustomObjectPathDescription": "Wenn Ihr verbundenes Objekt MQTT-Nachrichten in Form eines JSON sendet, können Sie hier den Pfad der Eigenschaft angeben, der von Gladys verwendet werden soll. Beispiel: Eigenschaft1.Eigenschaft2.Eigenschaft3. Lassen Sie es leer, wenn das Format kein JSON ist.",
+ "mqttCustomObjectPathPlaceholder": "Pfad im JSON folgen",
"copied": "Kopiert!",
"copyFailed": "Kopieren fehlgeschlagen",
"deleteLabel": "Funktion löschen"
@@ -1264,6 +1271,15 @@
"connectionError": "Die Verbindung ist fehlgeschlagen. Bitte überprüfe deine Konfiguration.",
"networkError": "Gladys kann nicht erreicht werden. Ist deine Gladys-Instanz verbunden und erreichbar?",
"disconnected": "Vom MQTT-Broker getrennt."
+ },
+ "debug": {
+ "title": "Debug MQTT",
+ "description": "Dieses Register ermöglicht es Ihnen, in Echtzeit die MQTT-Nachrichten zu sehen, die auf Ihrer Gladys-Instanz eingehen, und erleichtert so das Debuggen. Nach 2 Minuten auf diesem Register wird der Debug-Modus automatisch deaktiviert. Sie können ihn mit der Aktualisierungsschaltfläche oben rechts wieder aktivieren.",
+ "activateDebugMode": "Aktivieren",
+ "debugModeActivated": "Debug-Modus aktiviert",
+ "date": "Zeit",
+ "topic": "Thema",
+ "message": "Nachrichteninhalt"
}
},
"broadlink": {
diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json
index 0d0a328c01..291a8cb79a 100644
--- a/front/src/config/i18n/en.json
+++ b/front/src/config/i18n/en.json
@@ -1191,6 +1191,7 @@
"title": "MQTT",
"description": "Connect to a local or remote MQTT server",
"deviceTab": "Devices",
+ "debugTab": "MQTT Debug",
"setupTab": "Setup",
"documentation": "MQTT documentation",
"apiDocumentation": "MQTT API documentation ",
@@ -1235,7 +1236,13 @@
"mqttTopicToPublishExampleDescription": "Gladys is listening to this topic. If you publish a value in this topic, it'll be associated with this device. (see doc )",
"mqttTopicToListenExampleLabel": "MQTT Topic to listen",
"mqttTopicToListenExampleDescription": "Gladys will publish in this topic if the device is controlled in Gladys. You need to listen to this topic. (see doc )",
+ "mqttCustomTopic": "Custom MQTT Topic",
+ "mqttCustomTopicDescription": "Gladys offers a default MQTT API but allows you to use custom topics if you are using a device that publishes on a particular topic natively.",
+ "mqttCustomTopicPlaceholder": "Enter MQTT topic",
"copyMqttTopic": "Copy MQTT Topic",
+ "mqttCustomObjectPath": "MQTT JSON Message",
+ "mqttCustomObjectPathDescription": "If your connected object sends MQTT messages in the form of a JSON, Gladys allows you to specify here the path of the property to be used by Gladys. Example: property1.property2.property3. Leave blank if the format is not JSON.",
+ "mqttCustomObjectPathPlaceholder": "Path to follow in the JSON",
"copied": "Copied!",
"copyFailed": "Fail to copy",
"deleteLabel": "Delete feature"
@@ -1264,6 +1271,15 @@
"connectionError": "Error while connecting, please check your configuration.",
"networkError": "Unable to contact Gladys, is your Gladys instance connected and accessible?",
"disconnected": "Disconnected from MQTT broker."
+ },
+ "debug": {
+ "title": "Debug MQTT",
+ "description": "This tab allows you to see in real-time the MQTT messages arriving on your Gladys instance, enabling you to debug easily. After 2 minutes on this tab, debug mode will automatically deactivate. You can reactivate it with the refresh button at the top right.",
+ "activateDebugMode": "Activate",
+ "debugModeActivated": "Debug mode activated",
+ "date": "Time",
+ "topic": "Topic",
+ "message": "Message Content"
}
},
"broadlink": {
diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json
index 41d73e083d..bc6948fb30 100644
--- a/front/src/config/i18n/fr.json
+++ b/front/src/config/i18n/fr.json
@@ -1319,6 +1319,7 @@
"title": "MQTT",
"description": "Connexion à un serveur MQTT, en local ou en distant.",
"deviceTab": "Appareils",
+ "debugTab": "Debug MQTT",
"setupTab": "Configuration",
"documentation": "Documentation MQTT",
"apiDocumentation": "Documentation MQTT API",
@@ -1363,7 +1364,13 @@
"mqttTopicToPublishExampleDescription": "Gladys écoute ce topic. Si vous publiez une valeur dedans, Gladys l'associera à cette fonctionnalité. ( voir doc )",
"mqttTopicToListenExampleLabel": "Topic MQTT à écouter",
"mqttTopicToListenExampleDescription": "Gladys publiera un message dans ce topic si cet appareil est contrôlé depuis Gladys. Vous devez écouter ce topic. ( voir doc )",
+ "mqttCustomTopic": "Topic MQTT personnalisé",
+ "mqttCustomTopicDescription": "Gladys propose une API MQTT par défaut, mais vous permet d'utiliser des topics personnalisés si vous utilisez un appareil qui publie nativement sur un topic particulier.",
+ "mqttCustomTopicPlaceholder": "Entrez un topic MQTT",
"copyMqttTopic": "Copier le topic MQTT",
+ "mqttCustomObjectPath": "Message MQTT JSON",
+ "mqttCustomObjectPathDescription": "Si votre objet connecté envoie des messages MQTT sous forme d'un JSON, Gladys vous permet de spécifier ici le chemin de la propriété à utiliser par Gladys. Exemple: property1.property2.property3. Laissez vide si le format n'est pas du JSON.",
+ "mqttCustomObjectPathPlaceholder": "Chemin à suivre dans le JSON",
"copied": "Copié !",
"copyFailed": "Erreur lors de la copie",
"deleteLabel": "Supprimer la fonctionnalité"
@@ -1392,6 +1399,15 @@
"connectionError": "Erreur lors de la connexion, veuillez vérifier votre configuration.",
"networkError": "Impossible de contacter Gladys, est-ce que votre instance Gladys est bien connectée et accessible ?",
"disconnected": "Déconnecté du broker MQTT."
+ },
+ "debug": {
+ "title": "Debug MQTT",
+ "description": "Cet onglet vous permet de voir en temps réel les messages MQTT qui arrivent sur votre instance Gladys, ce qui vous permet de débugger simplement. Au bout de 2 minutes sur cet onglet, le mode débug se désactivera automatiquement. Vous pouvez le réactiver avec le bouton rafraichir en haut à droite.",
+ "activateDebugMode": "Activer",
+ "debugModeActivated": "Mode debug activé",
+ "date": "Heure",
+ "topic": "Topic",
+ "message": "Contenu du message"
}
},
"broadlink": {
diff --git a/front/src/routes/integration/all/mqtt/MqttPage.js b/front/src/routes/integration/all/mqtt/MqttPage.js
index dfc91f2395..48bbb88eac 100644
--- a/front/src/routes/integration/all/mqtt/MqttPage.js
+++ b/front/src/routes/integration/all/mqtt/MqttPage.js
@@ -25,6 +25,17 @@ const MqttPage = ({ children, user }) => (
+
+
+
+
+
+
+
{
+ try {
+ await this.props.httpClient.post('/api/v1/service/mqtt/debug_mode', {
+ debug_mode: true
+ });
+ this.setState({
+ debugModeActivated: true
+ });
+ clearTimeout(this.disableDebugModeTimeout);
+ this.disableDebugModeTimeout = setTimeout(() => {
+ this.setState({
+ debugModeActivated: false
+ });
+ }, 120 * 1000);
+ } catch (e) {
+ console.error(e);
+ }
+ };
+
+ displayNewMqttMessage = payload => {
+ const now = dayjs()
+ .locale(this.props.user.language)
+ .format('HH:mm:ss');
+ const message = { ...payload, date: now };
+ const newMessages = update(this.state.messages, {
+ $unshift: [message]
+ });
+ this.setState({ messages: newMessages });
+ };
+
+ componentWillMount() {
+ this.setDebugMode();
+ this.props.session.dispatcher.addListener(
+ WEBSOCKET_MESSAGE_TYPES.MQTT.DEBUG_NEW_MQTT_MESSAGE,
+ this.displayNewMqttMessage
+ );
+ }
+
+ componentWillUnmount() {
+ this.props.session.dispatcher.removeListener(
+ WEBSOCKET_MESSAGE_TYPES.MQTT.DEBUG_NEW_MQTT_MESSAGE,
+ this.displayNewMqttMessage
+ );
+ }
+
+ render(props, { messages, debugModeActivated }) {
+ return (
+
+
+
+
+ {debugModeActivated && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {messages.map(message => (
+
+ {message.date}
+ {message.topic}
+
+ {message.message}
+
+
+ ))}
+
+
+
+
+
+ );
+ }
+}
+
+export default connect('user,session,httpClient', {})(MqttNodePage);
diff --git a/front/src/routes/integration/all/mqtt/device-page/setup/Feature.jsx b/front/src/routes/integration/all/mqtt/device-page/setup/Feature.jsx
index 54bd458f34..0c607c2193 100644
--- a/front/src/routes/integration/all/mqtt/device-page/setup/Feature.jsx
+++ b/front/src/routes/integration/all/mqtt/device-page/setup/Feature.jsx
@@ -5,6 +5,7 @@ import {
DEVICE_FEATURE_CATEGORIES
} from '../../../../../../../../server/utils/constants';
import { DeviceFeatureCategoriesIcon, RequestStatus } from '../../../../../../utils/consts';
+import { getDeviceParam } from '../../../../../../utils/device';
import get from 'get-value';
const MqttFeatureBox = ({ children, feature, featureIndex, ...props }) => {
@@ -168,6 +169,46 @@ const MqttFeatureBox = ({ children, feature, featureIndex, ...props }) => {
+
+
+
+
{feature.read_only === false && (
@@ -227,6 +268,24 @@ class MqttFeatureBoxComponent extends Component {
};
this.props.updateFeatureProperty(e, 'keep_history', this.props.featureIndex);
};
+ getCustomMqttTopicParamPrefix = () => {
+ return `mqtt_custom_topic_feature:${this.props.feature.id}`;
+ };
+ getCustomMqttObjectPathParamPrefix = () => {
+ return `mqtt_custom_object_path_feature:${this.props.feature.id}`;
+ };
+ getCustomMqttTopicValue = () => {
+ return getDeviceParam(this.props.device, this.getCustomMqttTopicParamPrefix());
+ };
+ getCustomMqttObjectPathValue = () => {
+ return getDeviceParam(this.props.device, this.getCustomMqttObjectPathParamPrefix());
+ };
+ updateMqttCustomTopic = e => {
+ this.props.updateDeviceParam(this.getCustomMqttTopicParamPrefix(), e.target.value);
+ };
+ updateMqttCustomObjectPath = e => {
+ this.props.updateDeviceParam(this.getCustomMqttObjectPathParamPrefix(), e.target.value);
+ };
deleteFeature = () => {
this.props.deleteFeature(this.props.featureIndex);
};
@@ -256,6 +315,8 @@ class MqttFeatureBoxComponent extends Component {
};
render() {
const { publishMqttTopic, listenMqttTopic } = this.getMqttTopic();
+ const mqttCustomTopic = this.getCustomMqttTopicValue();
+ const mqttCustomObjectPath = this.getCustomMqttObjectPathValue();
return (
);
}
diff --git a/front/src/routes/integration/all/mqtt/device-page/setup/index.js b/front/src/routes/integration/all/mqtt/device-page/setup/index.js
index 652504e322..04be129bef 100644
--- a/front/src/routes/integration/all/mqtt/device-page/setup/index.js
+++ b/front/src/routes/integration/all/mqtt/device-page/setup/index.js
@@ -9,6 +9,7 @@ import update from 'immutability-helper';
import { RequestStatus } from '../../../../../../utils/consts';
import { slugify } from '../../../../../../../../server/utils/slugify';
import withIntlAsProp from '../../../../../../utils/withIntlAsProp';
+
import { DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES } from '../../../../../../../../server/utils/constants';
class MqttDeviceSetupPage extends Component {
@@ -105,6 +106,38 @@ class MqttDeviceSetupPage extends Component {
});
}
+ updateDeviceParam = (paramName, paramValue) => {
+ let device;
+ // Find if this param already exist
+ const paramIndex = this.state.device.params.findIndex(p => p.name === paramName);
+
+ // If no, create it
+ if (paramIndex === -1) {
+ device = update(this.state.device, {
+ params: {
+ $push: [
+ {
+ name: paramName,
+ value: paramValue
+ }
+ ]
+ }
+ });
+ } else {
+ // If yes, update value in the param
+ device = update(this.state.device, {
+ params: {
+ [paramIndex]: {
+ value: {
+ $set: paramValue
+ }
+ }
+ }
+ });
+ }
+ this.setState({ device });
+ };
+
updateFeatureProperty(e, property, featureIndex) {
let value = e.target.value;
let device;
@@ -243,7 +276,8 @@ class MqttDeviceSetupPage extends Component {
should_poll: false,
external_id: 'mqtt:',
service_id: this.props.currentIntegration.id,
- features: []
+ features: [],
+ params: []
};
} else {
const loadedDevice = await this.props.httpClient.get(`/api/v1/device/${deviceSelector}`);
@@ -274,6 +308,7 @@ class MqttDeviceSetupPage extends Component {
deleteFeature={this.deleteFeature}
updateDeviceProperty={this.updateDeviceProperty}
updateFeatureProperty={this.updateFeatureProperty}
+ updateDeviceParam={this.updateDeviceParam}
saveDevice={this.saveDevice}
/>
diff --git a/server/lib/device/device.add.js b/server/lib/device/device.add.js
index a6a0735f94..3039ebb72d 100644
--- a/server/lib/device/device.add.js
+++ b/server/lib/device/device.add.js
@@ -13,6 +13,7 @@ function add(device) {
this.stateManager.setState('deviceById', device.id, device);
device.features.forEach((feature) => {
this.stateManager.setState('deviceFeature', feature.selector, feature);
+ this.stateManager.setState('deviceFeatureById', feature.id, feature);
this.stateManager.setState('deviceFeatureByExternalId', feature.external_id, feature);
});
if (device.should_poll === true && device.poll_frequency) {
@@ -44,6 +45,12 @@ function add(device) {
}
});
}
+ // Handle MQTT custom topic
+ const mqttService = this.serviceManager.getService('mqtt');
+ if (mqttService) {
+ mqttService.device.listenToCustomMqttTopicIfNeeded(device);
+ }
+
return null;
}
diff --git a/server/lib/device/device.create.js b/server/lib/device/device.create.js
index 2d61306991..6c1432c945 100644
--- a/server/lib/device/device.create.js
+++ b/server/lib/device/device.create.js
@@ -87,6 +87,12 @@ async function create(device) {
actionEvent = EVENTS.DEVICE.UPDATE;
oldPollFrequency = deviceInDb.poll_frequency;
+ // Remove MQTT subscription to custom MQTT topic
+ const mqttService = this.serviceManager.getService('mqtt');
+ if (mqttService) {
+ mqttService.device.unListenToCustomMqttTopic(deviceInDb);
+ }
+
// or update it
await deviceInDb.update(device, { transaction });
diff --git a/server/lib/state/index.js b/server/lib/state/index.js
index 8b673cacbb..739717f70b 100644
--- a/server/lib/state/index.js
+++ b/server/lib/state/index.js
@@ -71,6 +71,7 @@ const StateManager = function StateManager(event) {
deviceByExternalId: {},
deviceById: {},
deviceFeature: {},
+ deviceFeatureById: {},
deviceFeatureByExternalId: {},
service: {},
serviceById: {},
diff --git a/server/services/mqtt/api/mqtt.controller.js b/server/services/mqtt/api/mqtt.controller.js
index 48ea414286..4e3fd9a013 100644
--- a/server/services/mqtt/api/mqtt.controller.js
+++ b/server/services/mqtt/api/mqtt.controller.js
@@ -14,6 +14,16 @@ module.exports = function MqttController(mqttManager) {
});
}
+ /**
+ * @api {post} /api/v1/service/mqtt/debug_mode Get MQTT connection status.
+ * @apiName status
+ * @apiGroup Mqtt
+ */
+ async function setDebugMode(req, res) {
+ const debugMode = mqttManager.setDebugMode(req.body.debug_mode);
+ res.json({ debug_mode: debugMode });
+ }
+
/**
* @api {get} /api/v1/service/mqtt/status Get MQTT connection status.
* @apiName status
@@ -54,6 +64,11 @@ module.exports = function MqttController(mqttManager) {
authenticated: true,
controller: status,
},
+ 'post /api/v1/service/mqtt/debug_mode': {
+ authenticated: true,
+ admin: true,
+ controller: asyncMiddleware(setDebugMode),
+ },
'get /api/v1/service/mqtt/config': {
authenticated: true,
controller: asyncMiddleware(getConfiguration),
diff --git a/server/services/mqtt/lib/handleNewMessage.js b/server/services/mqtt/lib/handleNewMessage.js
index 18437fb39a..2d505c7f94 100644
--- a/server/services/mqtt/lib/handleNewMessage.js
+++ b/server/services/mqtt/lib/handleNewMessage.js
@@ -1,4 +1,5 @@
const logger = require('../../../utils/logger');
+const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../utils/constants');
/**
* @description Handle a new message receive in MQTT.
@@ -11,6 +12,17 @@ function handleNewMessage(topic, message) {
logger.debug(`Receives MQTT message from ${topic} : ${message}`);
try {
+ // If debug mode is enabled, send message to UI
+ if (this.debugMode) {
+ this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.MQTT.DEBUG_NEW_MQTT_MESSAGE,
+ payload: {
+ topic,
+ message,
+ },
+ });
+ }
+
let forwardedMessage = false;
// foreach topic, we see if it matches
@@ -22,6 +34,18 @@ function handleNewMessage(topic, message) {
}
}, this);
+ // Handle custom device listener
+ const customListenersFound = this.deviceFeatureCustomMqttTopics.filter((t) => topic.match(t.regex_key));
+ customListenersFound.forEach((foundCustomListener) => {
+ forwardedMessage = true;
+ this.handleDeviceCustomTopicMessage(
+ topic,
+ message,
+ foundCustomListener.device_feature_id,
+ foundCustomListener.object_path,
+ );
+ });
+
if (!forwardedMessage) {
logger.warn(`No subscription found for MQTT topic ${topic}`);
}
diff --git a/server/services/mqtt/lib/handler/handleDeviceCustomTopicMessage.js b/server/services/mqtt/lib/handler/handleDeviceCustomTopicMessage.js
new file mode 100644
index 0000000000..850c6a7555
--- /dev/null
+++ b/server/services/mqtt/lib/handler/handleDeviceCustomTopicMessage.js
@@ -0,0 +1,38 @@
+const get = require('get-value');
+const logger = require('../../../../utils/logger');
+const { EVENTS } = require('../../../../utils/constants');
+
+/**
+ * @description When a new MQTT message is received on a custom topic.
+ * @param {string} topic - The topic where the message was published.
+ * @param {string} message - The content of the message.
+ * @param {string} deviceFeatureId - The device_feature_id.
+ * @param {string} objectPath - The path where to look for, if the message is a JSON.
+ * @example
+ * handleDeviceCustomTopicMessage('custom_topic', '{"data": 12}', 'ea841937-74e5-401a-8b08-c546d9a322b5', 'data');
+ */
+function handleDeviceCustomTopicMessage(topic, message, deviceFeatureId, objectPath) {
+ logger.debug(`New value on custom topic ${topic} for device_feature = ${deviceFeatureId}`);
+ // If objectPath is not null, it's supposed to be a JSON
+ if (objectPath) {
+ try {
+ const state = get(JSON.parse(message), objectPath);
+ this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, {
+ device_feature_external_id: this.gladys.stateManager.get('deviceFeatureById', deviceFeatureId).external_id,
+ state,
+ });
+ } catch (e) {
+ logger.warn(`Fail to parse message from custom MQTT topic.`);
+ logger.warn(message);
+ logger.warn(e);
+ }
+ } else {
+ // else, it's supposed to be a normal string, send it raw
+ this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, {
+ device_feature_external_id: this.gladys.stateManager.get('deviceFeatureById', deviceFeatureId).external_id,
+ state: message,
+ });
+ }
+}
+
+module.exports = { handleDeviceCustomTopicMessage };
diff --git a/server/services/mqtt/lib/index.js b/server/services/mqtt/lib/index.js
index 3cb0150784..85691b4f0c 100644
--- a/server/services/mqtt/lib/index.js
+++ b/server/services/mqtt/lib/index.js
@@ -3,6 +3,7 @@ const { connect } = require('./connect');
const { disconnect } = require('./disconnect');
const { handleNewMessage } = require('./handleNewMessage');
const { handleGladysMessage } = require('./handler/handleGladysMessage');
+const { handleDeviceCustomTopicMessage } = require('./handler/handleDeviceCustomTopicMessage');
const { publish } = require('./publish');
const { subscribe } = require('./subscribe');
const { unsubscribe } = require('./unsubscribe');
@@ -10,10 +11,13 @@ const { status } = require('./status');
const { getConfiguration } = require('./getConfiguration');
const { saveConfiguration } = require('./saveConfiguration');
const { installContainer } = require('./installContainer');
+const { listenToCustomMqttTopicIfNeeded } = require('./listenToCustomMqttTopicIfNeeded');
+const { unListenToCustomMqttTopic } = require('./unListenToCustomMqttTopic');
const { configureContainer } = require('./configureContainer');
const { updateContainer } = require('./updateContainer');
const { checkDockerNetwork } = require('./checkDockerNetwork');
const { setValue } = require('./setValue');
+const { setDebugMode } = require('./setDebugMode');
/**
* @description Add ability to connect to a MQTT broker.
@@ -30,8 +34,11 @@ const MqttHandler = function MqttHandler(gladys, mqttLibrary, serviceId) {
this.mqttClient = null;
this.topicBinds = {};
+ this.deviceFeatureCustomMqttTopics = [];
this.configured = false;
this.connected = false;
+ this.debugMode = false;
+ this.debugModeTimeout = 120 * 1000;
};
MqttHandler.prototype.init = init;
@@ -39,6 +46,7 @@ MqttHandler.prototype.connect = connect;
MqttHandler.prototype.disconnect = disconnect;
MqttHandler.prototype.handleNewMessage = handleNewMessage;
MqttHandler.prototype.handleGladysMessage = handleGladysMessage;
+MqttHandler.prototype.handleDeviceCustomTopicMessage = handleDeviceCustomTopicMessage;
MqttHandler.prototype.publish = publish;
MqttHandler.prototype.subscribe = subscribe;
MqttHandler.prototype.unsubscribe = unsubscribe;
@@ -46,9 +54,12 @@ MqttHandler.prototype.status = status;
MqttHandler.prototype.getConfiguration = getConfiguration;
MqttHandler.prototype.saveConfiguration = saveConfiguration;
MqttHandler.prototype.installContainer = installContainer;
+MqttHandler.prototype.listenToCustomMqttTopicIfNeeded = listenToCustomMqttTopicIfNeeded;
+MqttHandler.prototype.unListenToCustomMqttTopic = unListenToCustomMqttTopic;
MqttHandler.prototype.configureContainer = configureContainer;
MqttHandler.prototype.updateContainer = updateContainer;
MqttHandler.prototype.checkDockerNetwork = checkDockerNetwork;
MqttHandler.prototype.setValue = setValue;
+MqttHandler.prototype.setDebugMode = setDebugMode;
module.exports = MqttHandler;
diff --git a/server/services/mqtt/lib/listenToCustomMqttTopicIfNeeded.js b/server/services/mqtt/lib/listenToCustomMqttTopicIfNeeded.js
new file mode 100644
index 0000000000..f68ee4c94c
--- /dev/null
+++ b/server/services/mqtt/lib/listenToCustomMqttTopicIfNeeded.js
@@ -0,0 +1,40 @@
+const logger = require('../../../utils/logger');
+
+/**
+ * @description Listen to MQTT custom topic if needed.
+ * @param {object} device - Device object.
+ * @example listenToCustomMqttTopicIfNeeded(device);
+ */
+async function listenToCustomMqttTopicIfNeeded(device) {
+ // Search if a custom topic param is here
+ const deviceCustomTopicParams = device.params.filter((p) => p.name.includes('mqtt_custom_topic_feature:'));
+ deviceCustomTopicParams.forEach((deviceCustomTopicParam) => {
+ const paramName = deviceCustomTopicParam.name;
+ const deviceFeatureId = paramName.split(':')[1];
+ logger.debug(`Adding custom listener for device ${device.selector}, feature = ${deviceFeatureId}`);
+ const deviceCustomObjectPathParam = device.params.find((p) =>
+ p.name.includes(`mqtt_custom_object_path_feature:${deviceFeatureId}`),
+ );
+ let objectPath = null;
+ if (deviceCustomObjectPathParam) {
+ objectPath = deviceCustomObjectPathParam.value;
+ }
+ const mqttTopic = deviceCustomTopicParam.value;
+ // Add listener to list of custom listeners
+ this.deviceFeatureCustomMqttTopics.push({
+ topic: mqttTopic,
+ regex_key: mqttTopic.replace('+', '[^/]+').replace('#', '.+'),
+ device_feature_id: deviceFeatureId,
+ object_path: objectPath,
+ });
+ // Listen to MQTT topic
+ if (this.mqttClient) {
+ logger.info(`Subscribing to MQTT topic ${mqttTopic}`);
+ this.mqttClient.subscribe(mqttTopic);
+ }
+ });
+}
+
+module.exports = {
+ listenToCustomMqttTopicIfNeeded,
+};
diff --git a/server/services/mqtt/lib/setDebugMode.js b/server/services/mqtt/lib/setDebugMode.js
new file mode 100644
index 0000000000..e7d4d66462
--- /dev/null
+++ b/server/services/mqtt/lib/setDebugMode.js
@@ -0,0 +1,21 @@
+/**
+ * @description Set debug mode param.
+ * @param {boolean} debugMode - Debug Mode.
+ * @returns {boolean} Return current debug mode.
+ * @example setDebugMode(true);
+ */
+function setDebugMode(debugMode) {
+ this.debugMode = debugMode;
+ if (debugMode) {
+ clearTimeout(this.debugModeTimeout);
+ // disable debug mode after xx seconds
+ this.debugModeTimeout = setTimeout(() => {
+ this.setDebugMode(false);
+ }, this.debugModeTimeout);
+ }
+ return debugMode;
+}
+
+module.exports = {
+ setDebugMode,
+};
diff --git a/server/services/mqtt/lib/unListenToCustomMqttTopic.js b/server/services/mqtt/lib/unListenToCustomMqttTopic.js
new file mode 100644
index 0000000000..192de0496b
--- /dev/null
+++ b/server/services/mqtt/lib/unListenToCustomMqttTopic.js
@@ -0,0 +1,35 @@
+const logger = require('../../../utils/logger');
+
+/**
+ * @description Remove listener to custom mqtt topics.
+ * @param {object} device - Device object.
+ * @example unListenToCustomMqttTopic(device);
+ */
+async function unListenToCustomMqttTopic(device) {
+ // Foreach feature in the device
+ device.features.forEach((feature) => {
+ // We look if there is a listener for this feature
+ const deviceFeatureListener = this.deviceFeatureCustomMqttTopics.find((t) => t.device_feature_id === feature.id);
+ // If not, we stop here
+ if (!deviceFeatureListener) {
+ return;
+ }
+ // If yes, we look if there are other listener listening to the same topic
+ const remainingSimilarTopics = this.deviceFeatureCustomMqttTopics.filter(
+ (t) => t.topic === deviceFeatureListener.topic,
+ );
+ // If no one listen this topic (except the one currently request), we unsuscribe from it
+ if (remainingSimilarTopics.length <= 1 && !this.topicBinds[deviceFeatureListener.topic] && this.mqttClient) {
+ logger.debug(`Unsubscribing from MQTT topic = ${deviceFeatureListener.topic}`);
+ this.mqttClient.unsubscribe(deviceFeatureListener.topic);
+ }
+ // Remove topic from array
+ this.deviceFeatureCustomMqttTopics = this.deviceFeatureCustomMqttTopics.filter(
+ (t) => t.device_feature_id !== feature.id,
+ );
+ });
+}
+
+module.exports = {
+ unListenToCustomMqttTopic,
+};
diff --git a/server/services/mqtt/package-lock.json b/server/services/mqtt/package-lock.json
index 9a95d5f217..16ca001743 100644
--- a/server/services/mqtt/package-lock.json
+++ b/server/services/mqtt/package-lock.json
@@ -16,6 +16,7 @@
"win32"
],
"dependencies": {
+ "get-value": "^3.0.1",
"lodash.clonedeep": "^4.5.0",
"mqtt": "^4.0.0"
}
@@ -232,6 +233,17 @@
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
+ "node_modules/get-value": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/get-value/-/get-value-3.0.1.tgz",
+ "integrity": "sha512-mKZj9JLQrwMBtj5wxi6MH8Z5eSKaERpAwjg43dPtlGI1ZVEgH/qC7T8/6R2OBSUA+zzHBZgICsVJaEIV2tKTDA==",
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
"node_modules/glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
@@ -379,6 +391,14 @@
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
+ "node_modules/isobject": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+ "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
@@ -907,6 +927,14 @@
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
+ "get-value": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/get-value/-/get-value-3.0.1.tgz",
+ "integrity": "sha512-mKZj9JLQrwMBtj5wxi6MH8Z5eSKaERpAwjg43dPtlGI1ZVEgH/qC7T8/6R2OBSUA+zzHBZgICsVJaEIV2tKTDA==",
+ "requires": {
+ "isobject": "^3.0.1"
+ }
+ },
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
@@ -1024,6 +1052,11 @@
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
+ "isobject": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+ "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="
+ },
"json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
diff --git a/server/services/mqtt/package.json b/server/services/mqtt/package.json
index 282b6aed91..012abe08c2 100644
--- a/server/services/mqtt/package.json
+++ b/server/services/mqtt/package.json
@@ -12,6 +12,7 @@
"arm64"
],
"dependencies": {
+ "get-value": "^3.0.1",
"lodash.clonedeep": "^4.5.0",
"mqtt": "^4.0.0"
}
diff --git a/server/test/lib/device/device.addFeature.test.js b/server/test/lib/device/device.addFeature.test.js
index b6ef8fc021..35ac42aa3b 100644
--- a/server/test/lib/device/device.addFeature.test.js
+++ b/server/test/lib/device/device.addFeature.test.js
@@ -5,6 +5,9 @@ const StateManager = require('../../../lib/state');
const Job = require('../../../lib/job');
const event = new EventEmitter();
+const service = {
+ getService: () => {},
+};
describe('Device.addFeature', () => {
it('should add one feature', async () => {
@@ -30,7 +33,7 @@ describe('Device.addFeature', () => {
params: [],
});
const job = new Job(event);
- const device = new Device(event, {}, stateManager, {}, {}, {}, job);
+ const device = new Device(event, {}, stateManager, service, {}, {}, job);
const newDevice = await device.addFeature('test-device', {
name: 'On/Off',
external_id: 'philips-hue:1:new',
@@ -73,7 +76,7 @@ describe('Device.addFeature', () => {
params: [],
});
const job = new Job(event);
- const device = new Device(event, {}, stateManager, {}, {}, {}, job);
+ const device = new Device(event, {}, stateManager, service, {}, {}, job);
const newDevice = await device.addFeature('test-device', {
name: 'NEW NAME, SHOULD NOT BE UPDATED',
external_id: 'philips-hue:1:binary',
diff --git a/server/test/lib/device/device.addParam.test.js b/server/test/lib/device/device.addParam.test.js
index 92212821e6..4db9ca2b85 100644
--- a/server/test/lib/device/device.addParam.test.js
+++ b/server/test/lib/device/device.addParam.test.js
@@ -5,6 +5,9 @@ const StateManager = require('../../../lib/state');
const Job = require('../../../lib/job');
const event = new EventEmitter();
+const service = {
+ getService: () => {},
+};
describe('Device.addParam', () => {
it('should add one param', async () => {
@@ -22,7 +25,7 @@ describe('Device.addParam', () => {
],
});
const job = new Job(event);
- const device = new Device(event, {}, stateManager, {}, {}, {}, job);
+ const device = new Device(event, {}, stateManager, service, {}, {}, job);
const newDevice = await device.addParam('test-device', {
name: 'NEW_VALUE',
value: '10',
@@ -49,7 +52,7 @@ describe('Device.addParam', () => {
],
});
const job = new Job(event);
- const device = new Device(event, {}, stateManager, {}, {}, {}, job);
+ const device = new Device(event, {}, stateManager, service, {}, {}, job);
const newDevice = await device.addParam('test-device', {
name: 'TEST_PARAM',
value: '1000',
diff --git a/server/test/lib/device/device.create.test.js b/server/test/lib/device/device.create.test.js
index f96531cd16..73edcc47b9 100644
--- a/server/test/lib/device/device.create.test.js
+++ b/server/test/lib/device/device.create.test.js
@@ -32,6 +32,52 @@ describe('Device', () => {
expect(newDevice).to.have.property('features');
expect(newDevice).to.have.property('params');
});
+ it('should create device and check if need to subscribe to custom MQTT topic', async () => {
+ const stateManager = new StateManager(event);
+ const mqttService = {
+ device: {
+ listenToCustomMqttTopicIfNeeded: fake.returns(null),
+ },
+ };
+ const serviceManager = {
+ getService: fake.returns(mqttService),
+ };
+ const device = new Device(event, {}, stateManager, serviceManager, {}, {}, job, brain);
+ await device.create({
+ service_id: 'a810b8db-6d04-4697-bed3-c4b72c996279',
+ name: 'Philips Hue 1',
+ external_id: 'philips-hue-new',
+ params: [],
+ });
+ assert.calledOnce(mqttService.device.listenToCustomMqttTopicIfNeeded);
+ });
+ it('should create device then update device and handle custom MQTT topic', async () => {
+ const stateManager = new StateManager(event);
+ const mqttService = {
+ device: {
+ listenToCustomMqttTopicIfNeeded: fake.returns(null),
+ unListenToCustomMqttTopic: fake.returns(null),
+ },
+ };
+ const serviceManager = {
+ getService: fake.returns(mqttService),
+ };
+ const device = new Device(event, {}, stateManager, serviceManager, {}, {}, job, brain);
+ await device.create({
+ service_id: 'a810b8db-6d04-4697-bed3-c4b72c996279',
+ name: 'Philips Hue 1',
+ external_id: 'philips-hue-new',
+ params: [],
+ });
+ await device.create({
+ service_id: 'a810b8db-6d04-4697-bed3-c4b72c996279',
+ name: 'Philips Hue 1',
+ external_id: 'philips-hue-new',
+ params: [],
+ });
+ assert.calledTwice(mqttService.device.listenToCustomMqttTopicIfNeeded);
+ assert.calledOnce(mqttService.device.unListenToCustomMqttTopic);
+ });
it('should update device which already exist', async () => {
const stateManager = new StateManager(event);
stateManager.setState('deviceByExternalId', 'test-device-external', {
diff --git a/server/test/lib/device/device.init.test.js b/server/test/lib/device/device.init.test.js
index 7f8595e259..c06c7b8ff6 100644
--- a/server/test/lib/device/device.init.test.js
+++ b/server/test/lib/device/device.init.test.js
@@ -12,11 +12,13 @@ const event = {
const brain = {
addNamedEntity: fake.returns(null),
};
+const service = {
+ getService: () => {},
+};
describe('Device.init', () => {
it('should init device', async () => {
const stateManager = new StateManager(event);
- const service = {};
const job = new Job(event);
const device = new Device(event, {}, stateManager, service, {}, {}, job, brain);
diff --git a/server/test/lib/device/light/light.init.test.js b/server/test/lib/device/light/light.init.test.js
index c4864afe5d..22e9879864 100644
--- a/server/test/lib/device/light/light.init.test.js
+++ b/server/test/lib/device/light/light.init.test.js
@@ -6,11 +6,14 @@ const Job = require('../../../../lib/job');
const event = new EventEmitter();
const job = new Job(event);
+const service = {
+ getService: () => {},
+};
describe('Light', () => {
it('should get all lights and store them in the stateManager', async () => {
const stateManager = new StateManager(event);
- const deviceManager = new Device(event, {}, stateManager, {}, {}, {}, job);
+ const deviceManager = new Device(event, {}, stateManager, service, {}, {}, job);
const lights = await deviceManager.lightManager.init();
lights.forEach((light) => {
expect(light).to.have.property('id');
diff --git a/server/test/services/mqtt/api/mqtt.controler.test.js b/server/test/services/mqtt/api/mqtt.controler.test.js
index 7b2a99283f..84a1bf29ea 100644
--- a/server/test/services/mqtt/api/mqtt.controler.test.js
+++ b/server/test/services/mqtt/api/mqtt.controler.test.js
@@ -9,6 +9,7 @@ const mqttHandler = {
getConfiguration: fake.returns(configuration),
saveConfiguration: fake.returns(true),
installContainer: fake.returns(true),
+ setDebugMode: fake.returns(null),
};
describe('POST /api/v1/service/mqtt/connect', () => {
@@ -146,3 +147,27 @@ describe('POST /api/v1/service/mqtt/config/docker', () => {
assert.calledOnce(res.json);
});
});
+
+describe('POST /api/v1/service/mqtt/debug_mode', () => {
+ let controller;
+
+ beforeEach(() => {
+ controller = MqttController(mqttHandler);
+ sinon.reset();
+ });
+
+ it('Set debug mode to true', async () => {
+ const req = {
+ body: {
+ debug_mode: true,
+ },
+ };
+ const res = {
+ json: fake.returns(null),
+ };
+
+ await controller['post /api/v1/service/mqtt/debug_mode'].controller(req, res);
+ assert.calledWith(mqttHandler.setDebugMode, true);
+ assert.calledOnce(res.json);
+ });
+});
diff --git a/server/test/services/mqtt/lib/handleNewMessage.test.js b/server/test/services/mqtt/lib/handleNewMessage.test.js
index 3365e845ac..5161913a80 100644
--- a/server/test/services/mqtt/lib/handleNewMessage.test.js
+++ b/server/test/services/mqtt/lib/handleNewMessage.test.js
@@ -1,13 +1,18 @@
const sinon = require('sinon');
const { assert, fake } = sinon;
-const { EVENTS } = require('../../../../utils/constants');
+const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../utils/constants');
const { MockedMqttClient } = require('../mocks.test');
const gladys = {
variable: {
getValue: fake.resolves('result'),
},
+ stateManager: {
+ get: fake.returns({
+ external_id: 'device_feature_external_id',
+ }),
+ },
event: {
emit: fake.returns(null),
},
@@ -26,6 +31,7 @@ describe('Mqtt handle message', () => {
afterEach(() => {
mqttHandler.disconnect();
+ mqttHandler.debugMode = false;
});
it('should not do anything, topic not found', () => {
@@ -79,6 +85,105 @@ describe('Mqtt handle message', () => {
it('handle strict topic', () => {
mqttHandler.handleNewMessage('gladys/master/random-topic', '{}');
+ assert.notCalled(gladys.event.emit);
+ });
+ it('handle device with custom topic and debug mode', () => {
+ mqttHandler.debugMode = true;
+ mqttHandler.deviceFeatureCustomMqttTopics = [];
+ mqttHandler.handleNewMessage('custom_mqtt_topic/test/test', '12');
+
+ assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.MQTT.DEBUG_NEW_MQTT_MESSAGE,
+ payload: {
+ topic: 'custom_mqtt_topic/test/test',
+ message: '12',
+ },
+ });
+ });
+ it('handle device with custom topic', () => {
+ mqttHandler.deviceFeatureCustomMqttTopics = [
+ {
+ device_feature_id: 'b42d3688-4403-479a-9376-9f5227ab543a',
+ regex_key: 'custom_mqtt_topic/test/test',
+ topic: 'custom_mqtt_topic/test/test',
+ object_path: null,
+ },
+ ];
+ mqttHandler.handleNewMessage('custom_mqtt_topic/test/test', '12');
+
+ assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, {
+ device_feature_external_id: 'device_feature_external_id',
+ state: '12',
+ });
+ });
+ it('handle device with custom topic and custom object path', () => {
+ mqttHandler.deviceFeatureCustomMqttTopics = [
+ {
+ device_feature_id: 'b42d3688-4403-479a-9376-9f5227ab543a',
+ regex_key: 'custom_mqtt_topic/test/test',
+ topic: 'custom_mqtt_topic/test/test',
+ object_path: 'test.temperature',
+ },
+ ];
+ mqttHandler.handleNewMessage(
+ 'custom_mqtt_topic/test/test',
+ JSON.stringify({
+ test: {
+ temperature: 18,
+ },
+ }),
+ );
+
+ assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, {
+ device_feature_external_id: 'device_feature_external_id',
+ state: 18,
+ });
+ });
+ it('handle device with multiple features on same custom topic', () => {
+ mqttHandler.deviceFeatureCustomMqttTopics = [
+ {
+ device_feature_id: 'b42d3688-4403-479a-9376-9f5227ab543a',
+ regex_key: 'custom_mqtt_topic/test/test',
+ topic: 'custom_mqtt_topic/test/test',
+ object_path: 'test.temperature',
+ },
+ {
+ device_feature_id: '309c9ec6-193b-4fb5-a4db-29874984e834',
+ regex_key: 'custom_mqtt_topic/test/test',
+ topic: 'custom_mqtt_topic/test/test',
+ object_path: 'test.co2',
+ },
+ ];
+ mqttHandler.handleNewMessage(
+ 'custom_mqtt_topic/test/test',
+ JSON.stringify({
+ test: {
+ temperature: 18,
+ co2: 500,
+ },
+ }),
+ );
+
+ assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, {
+ device_feature_external_id: 'device_feature_external_id',
+ state: 18,
+ });
+ assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, {
+ device_feature_external_id: 'device_feature_external_id',
+ state: 500,
+ });
+ });
+ it('handle device with custom topic, custom object path and broken JSON', () => {
+ mqttHandler.deviceFeatureCustomMqttTopics = [
+ {
+ device_feature_id: 'b42d3688-4403-479a-9376-9f5227ab543a',
+ regex_key: 'custom_mqtt_topic/test/test',
+ topic: 'custom_mqtt_topic/test/test',
+ object_path: 'test.temperature',
+ },
+ ];
+ mqttHandler.handleNewMessage('custom_mqtt_topic/test/test', 'broken-JSON');
+
assert.notCalled(gladys.event.emit);
});
});
diff --git a/server/test/services/mqtt/lib/listenToCustomMqttTopicIfNeeded.test.js b/server/test/services/mqtt/lib/listenToCustomMqttTopicIfNeeded.test.js
new file mode 100644
index 0000000000..bf2baa071e
--- /dev/null
+++ b/server/test/services/mqtt/lib/listenToCustomMqttTopicIfNeeded.test.js
@@ -0,0 +1,125 @@
+const { expect } = require('chai');
+const sinon = require('sinon');
+
+const { assert, fake } = sinon;
+const { MockedMqttClient, mqttApi } = require('../mocks.test');
+
+const gladys = {
+ variable: {
+ getValue: fake.resolves('result'),
+ },
+ event: {
+ emit: fake.returns(null),
+ },
+};
+
+const MqttHandler = require('../../../../services/mqtt/lib');
+
+describe('Mqtt.listenToCustomMqttTopicIfNeeded', () => {
+ const mqttHandler = new MqttHandler(gladys, MockedMqttClient, 'faea9c35-759a-44d5-bcc9-2af1de37b8b4');
+
+ before(async () => {
+ await mqttHandler.connect({ mqttUrl: 'url' });
+ });
+
+ beforeEach(async () => {
+ sinon.reset();
+ mqttHandler.deviceFeatureCustomMqttTopics = [];
+ });
+
+ it('should not connect, as device doesnt have the param', async () => {
+ const device = {
+ selector: 'my-device',
+ params: [],
+ };
+ await mqttHandler.listenToCustomMqttTopicIfNeeded(device);
+ assert.notCalled(mqttApi.subscribe);
+ });
+
+ it('should connect to custom mqtt topic', async () => {
+ const device = {
+ selector: 'my-device',
+ params: [
+ {
+ name: 'mqtt_custom_topic_feature:b42d3688-4403-479a-9376-9f5227ab543a',
+ value: 'custom_mqtt_topic/test/test',
+ },
+ ],
+ };
+ await mqttHandler.listenToCustomMqttTopicIfNeeded(device);
+ assert.calledWith(mqttApi.subscribe, 'custom_mqtt_topic/test/test');
+ expect(mqttHandler.deviceFeatureCustomMqttTopics).to.deep.equal([
+ {
+ device_feature_id: 'b42d3688-4403-479a-9376-9f5227ab543a',
+ regex_key: 'custom_mqtt_topic/test/test',
+ topic: 'custom_mqtt_topic/test/test',
+ object_path: null,
+ },
+ ]);
+ });
+
+ it('should connect to custom mqtt topic with custom and custom object path', async () => {
+ const device = {
+ selector: 'my-device',
+ params: [
+ {
+ name: 'mqtt_custom_topic_feature:b42d3688-4403-479a-9376-9f5227ab543a',
+ value: 'custom_mqtt_topic/test/test',
+ },
+ {
+ name: 'mqtt_custom_object_path_feature:b42d3688-4403-479a-9376-9f5227ab543a',
+ value: 'data.temperature',
+ },
+ ],
+ };
+ await mqttHandler.listenToCustomMqttTopicIfNeeded(device);
+ assert.calledWith(mqttApi.subscribe, 'custom_mqtt_topic/test/test');
+ expect(mqttHandler.deviceFeatureCustomMqttTopics).to.deep.equal([
+ {
+ device_feature_id: 'b42d3688-4403-479a-9376-9f5227ab543a',
+ regex_key: 'custom_mqtt_topic/test/test',
+ topic: 'custom_mqtt_topic/test/test',
+ object_path: 'data.temperature',
+ },
+ ]);
+ });
+ it('should connect to custom mqtt topic with multiple custom and custom object path', async () => {
+ const device = {
+ selector: 'my-device',
+ params: [
+ {
+ name: 'mqtt_custom_topic_feature:b42d3688-4403-479a-9376-9f5227ab543a',
+ value: 'custom_mqtt_topic/test/test',
+ },
+ {
+ name: 'mqtt_custom_object_path_feature:b42d3688-4403-479a-9376-9f5227ab543a',
+ value: 'data.temperature',
+ },
+ {
+ name: 'mqtt_custom_topic_feature:a6dd0aef-9432-4ed4-a313-5d4800acdcfb',
+ value: 'custom_mqtt_topic/test/test',
+ },
+ {
+ name: 'mqtt_custom_object_path_feature:a6dd0aef-9432-4ed4-a313-5d4800acdcfb',
+ value: 'data.co2',
+ },
+ ],
+ };
+ await mqttHandler.listenToCustomMqttTopicIfNeeded(device);
+ assert.calledWith(mqttApi.subscribe, 'custom_mqtt_topic/test/test');
+ expect(mqttHandler.deviceFeatureCustomMqttTopics).to.deep.equal([
+ {
+ device_feature_id: 'b42d3688-4403-479a-9376-9f5227ab543a',
+ regex_key: 'custom_mqtt_topic/test/test',
+ topic: 'custom_mqtt_topic/test/test',
+ object_path: 'data.temperature',
+ },
+ {
+ device_feature_id: 'a6dd0aef-9432-4ed4-a313-5d4800acdcfb',
+ regex_key: 'custom_mqtt_topic/test/test',
+ topic: 'custom_mqtt_topic/test/test',
+ object_path: 'data.co2',
+ },
+ ]);
+ });
+});
diff --git a/server/test/services/mqtt/lib/setDebugMode.test.js b/server/test/services/mqtt/lib/setDebugMode.test.js
new file mode 100644
index 0000000000..9e2bad9f17
--- /dev/null
+++ b/server/test/services/mqtt/lib/setDebugMode.test.js
@@ -0,0 +1,42 @@
+const sinon = require('sinon');
+const Promise = require('bluebird');
+const { expect } = require('chai');
+
+const { fake } = sinon;
+
+const { MockedMqttClient } = require('../mocks.test');
+
+const gladys = {
+ variable: {
+ getValue: fake.resolves('result'),
+ },
+ event: {
+ emit: fake.returns(null),
+ },
+};
+
+const MqttHandler = require('../../../../services/mqtt/lib');
+
+describe('Mqtt.setDebugMode', () => {
+ const mqttHandler = new MqttHandler(gladys, MockedMqttClient, 'faea9c35-759a-44d5-bcc9-2af1de37b8b4');
+
+ beforeEach(async () => {
+ sinon.reset();
+ });
+
+ it('should set debug mode at true', () => {
+ mqttHandler.setDebugMode(true);
+ expect(mqttHandler).to.have.property('debugMode', true);
+ });
+ it('should set debug mode at true, then have it set to false', async () => {
+ mqttHandler.debugModeTimeout = 0;
+ mqttHandler.setDebugMode(true);
+ expect(mqttHandler).to.have.property('debugMode', true);
+ await Promise.delay(5);
+ expect(mqttHandler).to.have.property('debugMode', false);
+ });
+ it('should set debug mode at false', () => {
+ mqttHandler.setDebugMode(false);
+ expect(mqttHandler).to.have.property('debugMode', false);
+ });
+});
diff --git a/server/test/services/mqtt/lib/unListenToCustomMqttTopic.test.js b/server/test/services/mqtt/lib/unListenToCustomMqttTopic.test.js
new file mode 100644
index 0000000000..a80bfb84c6
--- /dev/null
+++ b/server/test/services/mqtt/lib/unListenToCustomMqttTopic.test.js
@@ -0,0 +1,75 @@
+const sinon = require('sinon');
+
+const { assert, fake } = sinon;
+const { MockedMqttClient, mqttApi } = require('../mocks.test');
+
+const gladys = {
+ variable: {
+ getValue: fake.resolves('result'),
+ },
+ event: {
+ emit: fake.returns(null),
+ },
+};
+
+const MqttHandler = require('../../../../services/mqtt/lib');
+
+describe('Mqtt.unListenToCustomMqttTopic', () => {
+ const mqttHandler = new MqttHandler(gladys, MockedMqttClient, 'faea9c35-759a-44d5-bcc9-2af1de37b8b4');
+
+ before(async () => {
+ await mqttHandler.connect({ mqttUrl: 'url' });
+ });
+
+ beforeEach(async () => {
+ sinon.reset();
+ });
+
+ it('should connect to custom mqtt topic then disconnect', async () => {
+ const device = {
+ selector: 'my-device',
+ features: [{ id: 'b42d3688-4403-479a-9376-9f5227ab543a' }],
+ params: [
+ {
+ name: 'mqtt_custom_topic_feature:b42d3688-4403-479a-9376-9f5227ab543a',
+ value: 'custom_mqtt_topic/test/test',
+ },
+ ],
+ };
+ await mqttHandler.listenToCustomMqttTopicIfNeeded(device);
+ assert.calledWith(mqttApi.subscribe, 'custom_mqtt_topic/test/test');
+ await mqttHandler.unListenToCustomMqttTopic(device);
+ assert.calledWith(mqttApi.unsubscribe, 'custom_mqtt_topic/test/test');
+ });
+ it('should connect to custom mqtt topic then disconnect and not unsubscribe because used in normal listeners', async () => {
+ const device = {
+ selector: 'my-device',
+ features: [{ id: 'b42d3688-4403-479a-9376-9f5227ab543a' }],
+ params: [
+ {
+ name: 'mqtt_custom_topic_feature:b42d3688-4403-479a-9376-9f5227ab543a',
+ value: 'custom_mqtt_topic/test/test',
+ },
+ ],
+ };
+ await mqttHandler.listenToCustomMqttTopicIfNeeded(device);
+ await mqttHandler.subscribe('custom_mqtt_topic/test/test', fake.returns(null));
+ assert.calledWith(mqttApi.subscribe, 'custom_mqtt_topic/test/test');
+ await mqttHandler.unListenToCustomMqttTopic(device);
+ assert.notCalled(mqttApi.unsubscribe);
+ });
+ it('should disconnect without connecting', async () => {
+ const device = {
+ selector: 'my-device',
+ features: [{ id: 'b42d3688-4403-479a-9376-9f5227ab543a' }],
+ params: [
+ {
+ name: 'mqtt_custom_topic_feature:b42d3688-4403-479a-9376-9f5227ab543a',
+ value: 'custom_mqtt_topic/test/test',
+ },
+ ],
+ };
+ await mqttHandler.unListenToCustomMqttTopic(device);
+ assert.notCalled(mqttApi.unsubscribe);
+ });
+});
diff --git a/server/utils/constants.js b/server/utils/constants.js
index 1fc33689be..3cdc7e5e74 100644
--- a/server/utils/constants.js
+++ b/server/utils/constants.js
@@ -957,6 +957,7 @@ const WEBSOCKET_MESSAGE_TYPES = {
CONNECTED: 'mqtt.connected',
ERROR: 'mqtt.error',
INSTALLATION_STATUS: 'mqtt.install-status',
+ DEBUG_NEW_MQTT_MESSAGE: 'mqtt.debug.new-mqtt-message',
},
ZWAVEJS_UI: {
CONNECTED: 'zwavejs-ui.connected',