diff --git a/.github/workflows/auto-test.yml b/.github/workflows/auto-test.yml index e86d7dd00..9063c2e16 100644 --- a/.github/workflows/auto-test.yml +++ b/.github/workflows/auto-test.yml @@ -22,6 +22,7 @@ jobs: contents: read strategy: + fail-fast: false matrix: os: [macos-latest, ubuntu-22.04, windows-latest, ubuntu-22.04-arm] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ @@ -41,13 +42,13 @@ jobs: id: node-modules-cache with: path: node_modules - key: node-modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + key: node-modules-${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }} - name: Use Node.js ${{ matrix.node }} uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: ${{ matrix.node }} - - run: npm install + - run: npm clean-install --no-fund - name: Rebuild native modules for ARM64 if: matrix.os == 'ubuntu-22.04-arm' @@ -65,6 +66,7 @@ jobs: permissions: contents: read strategy: + fail-fast: false matrix: node: [ 20, 22 ] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ @@ -86,8 +88,8 @@ jobs: docker run --rm --platform linux/arm/v7 \ -v $PWD:/workspace \ -w /workspace \ - arm32v7/node:${{ matrix.node }}-slim \ - bash -c "npm install --production" + arm32v7/node:${{ matrix.node }} \ + npm clean-install --no-fund --production check-linters: runs-on: ubuntu-latest @@ -104,13 +106,13 @@ jobs: id: node-modules-cache with: path: node_modules - key: node-modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + key: node-modules-${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }} - name: Use Node.js 20 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: 20 - - run: npm install + - run: npm clean-install --no-fund - run: npm run lint:prod e2e-test: @@ -129,13 +131,13 @@ jobs: id: node-modules-cache with: path: node_modules - key: node-modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + key: node-modules-${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }} - name: Setup Node.js uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: 22 - - run: npm install + - run: npm clean-install --no-fund - name: Rebuild native modules for ARM64 run: npm rebuild @louislam/sqlite3 diff --git a/server/monitor-types/mqtt.js b/server/monitor-types/mqtt.js index 18595b3a4..f1c1ad23c 100644 --- a/server/monitor-types/mqtt.js +++ b/server/monitor-types/mqtt.js @@ -2,10 +2,22 @@ const { MonitorType } = require("./monitor-type"); const { log, UP } = require("../../src/util"); const mqtt = require("mqtt"); const jsonata = require("jsonata"); +const { ConditionVariable } = require("../monitor-conditions/variables"); +const { defaultStringOperators, defaultNumberOperators } = require("../monitor-conditions/operators"); +const { ConditionExpressionGroup } = require("../monitor-conditions/expression"); +const { evaluateExpressionGroup } = require("../monitor-conditions/evaluator"); class MqttMonitorType extends MonitorType { name = "mqtt"; + supportsConditions = true; + + conditionVariables = [ + new ConditionVariable("topic", defaultStringOperators), + new ConditionVariable("message", defaultStringOperators), + new ConditionVariable("json_value", defaultStringOperators.concat(defaultNumberOperators)), + ]; + /** * @inheritdoc */ @@ -19,32 +31,98 @@ class MqttMonitorType extends MonitorType { }); if (monitor.mqttCheckType == null || monitor.mqttCheckType === "") { - // use old default monitor.mqttCheckType = "keyword"; } - if (monitor.mqttCheckType === "keyword") { - if (receivedMessage != null && receivedMessage.includes(monitor.mqttSuccessMessage)) { - heartbeat.msg = `Topic: ${messageTopic}; Message: ${receivedMessage}`; - heartbeat.status = UP; - } else { - throw Error(`Message Mismatch - Topic: ${monitor.mqttTopic}; Message: ${receivedMessage}`); - } + // Check if conditions are defined + const conditions = monitor.conditions ? ConditionExpressionGroup.fromMonitor(monitor) : null; + const hasConditions = conditions && conditions.children && conditions.children.length > 0; + + if (hasConditions) { + await this.checkConditions(monitor, heartbeat, messageTopic, receivedMessage, conditions); + } else if (monitor.mqttCheckType === "keyword") { + this.checkKeyword(monitor, heartbeat, messageTopic, receivedMessage); } else if (monitor.mqttCheckType === "json-query") { - const parsedMessage = JSON.parse(receivedMessage); - - let expression = jsonata(monitor.jsonPath); - - let result = await expression.evaluate(parsedMessage); - - if (result?.toString() === monitor.expectedValue) { - heartbeat.msg = "Message received, expected value is found"; - heartbeat.status = UP; - } else { - throw new Error("Message received but value is not equal to expected value, value was: [" + result + "]"); - } + await this.checkJsonQuery(monitor, heartbeat, receivedMessage); } else { - throw Error("Unknown MQTT Check Type"); + throw new Error("Unknown MQTT Check Type"); + } + } + + /** + * Check using keyword matching + * @param {object} monitor Monitor object + * @param {object} heartbeat Heartbeat object + * @param {string} messageTopic Received MQTT topic + * @param {string} receivedMessage Received MQTT message + * @returns {void} + * @throws {Error} If keyword is not found in message + */ + checkKeyword(monitor, heartbeat, messageTopic, receivedMessage) { + if (receivedMessage != null && receivedMessage.includes(monitor.mqttSuccessMessage)) { + heartbeat.msg = `Topic: ${messageTopic}; Message: ${receivedMessage}`; + heartbeat.status = UP; + } else { + throw new Error(`Message Mismatch - Topic: ${monitor.mqttTopic}; Message: ${receivedMessage}`); + } + } + + /** + * Check using JSONata query + * @param {object} monitor Monitor object + * @param {object} heartbeat Heartbeat object + * @param {string} receivedMessage Received MQTT message + * @returns {Promise} + */ + async checkJsonQuery(monitor, heartbeat, receivedMessage) { + const parsedMessage = JSON.parse(receivedMessage); + const expression = jsonata(monitor.jsonPath); + const result = await expression.evaluate(parsedMessage); + + if (result?.toString() === monitor.expectedValue) { + heartbeat.msg = "Message received, expected value is found"; + heartbeat.status = UP; + } else { + throw new Error("Message received but value is not equal to expected value, value was: [" + result + "]"); + } + } + + /** + * Check using conditions system + * @param {object} monitor Monitor object + * @param {object} heartbeat Heartbeat object + * @param {string} messageTopic Received MQTT topic + * @param {string} receivedMessage Received MQTT message + * @param {ConditionExpressionGroup} conditions Parsed conditions + * @returns {Promise} + */ + async checkConditions(monitor, heartbeat, messageTopic, receivedMessage, conditions) { + let jsonValue = null; + + // Parse JSON and extract value if jsonPath is defined + if (monitor.jsonPath) { + try { + const parsedMessage = JSON.parse(receivedMessage); + const expression = jsonata(monitor.jsonPath); + jsonValue = await expression.evaluate(parsedMessage); + } catch (e) { + // JSON parsing failed, jsonValue remains null + } + } + + const conditionData = { + topic: messageTopic, + message: receivedMessage, + json_value: jsonValue?.toString() ?? "", + }; + + const conditionsResult = evaluateExpressionGroup(conditions, conditionData); + + if (conditionsResult) { + heartbeat.msg = `Topic: ${messageTopic}; Message: ${receivedMessage}`; + heartbeat.status = UP; + } else { + throw new Error(`Conditions not met - Topic: ${messageTopic}; Message: ${receivedMessage}`); } } diff --git a/src/components/APIKeyDialog.vue b/src/components/APIKeyDialog.vue index a103cf097..6e77c0245 100644 --- a/src/components/APIKeyDialog.vue +++ b/src/components/APIKeyDialog.vue @@ -7,7 +7,7 @@ -