From 1970a9e58827bb6e87ae4b4d4fb9ef9929322ecd Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Thu, 8 Feb 2024 09:37:27 +0200 Subject: [PATCH 1/6] Rename tryWriteCoils to tryWriteCoil Use it when updating settings too --- app/modbus.mjs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/modbus.mjs b/app/modbus.mjs index 43a0a4e..a52c3c3 100644 --- a/app/modbus.mjs +++ b/app/modbus.mjs @@ -73,7 +73,7 @@ export const setFlag = async (modbusClient, flag, value) => { throw new Error('Unknown flag') } - await mutex.runExclusive(async () => tryWriteCoils(modbusClient, AVAILABLE_FLAGS[flag], value)) + await mutex.runExclusive(async () => tryWriteCoil(modbusClient, AVAILABLE_FLAGS[flag], value)) // Flags are mutually exclusive, disable all others when enabling one if (value) { @@ -87,7 +87,7 @@ const disableAllModesExcept = async (modbusClient, exceptedMode) => { continue } - await mutex.runExclusive(async () => tryWriteCoils(modbusClient, AVAILABLE_FLAGS[mode], false)) + await mutex.runExclusive(async () => tryWriteCoil(modbusClient, AVAILABLE_FLAGS[mode], false)) } } @@ -267,7 +267,7 @@ export const setSetting = async (modbusClient, setting, value) => { // This isn't very nice, but it's good enough for now if (coil) { - await mutex.runExclusive(async () => modbusClient.writeCoil(AVAILABLE_SETTINGS[setting], value)) + await mutex.runExclusive(async () => tryWriteCoil(modbusClient, AVAILABLE_SETTINGS[setting], value)) } else { await mutex.runExclusive(async () => modbusClient.writeRegister(AVAILABLE_SETTINGS[setting], intValue)) } @@ -416,7 +416,7 @@ const tryReadCoils = async (modbusClient, dataAddress, length) => { } } -const tryWriteCoils = async (modbusClient, dataAddress, value) => { +const tryWriteCoil = async (modbusClient, dataAddress, value) => { try { logger.debug(`Writing ${value} to coil address ${dataAddress}`) return await modbusClient.writeCoil(dataAddress, value) From 9741949a068e046273b6418eff265bf30592aad0 Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Thu, 8 Feb 2024 09:43:49 +0200 Subject: [PATCH 2/6] Move alarm register start/end to enervent.mjs --- app/enervent.mjs | 2 ++ app/modbus.mjs | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/enervent.mjs b/app/enervent.mjs index bafd87a..dc0905c 100644 --- a/app/enervent.mjs +++ b/app/enervent.mjs @@ -37,6 +37,8 @@ export const AVAILABLE_SETTINGS = { 'longAwayHeatingAllowed': 20, } +export const ALARM_REGISTERS_START = 385 +export const ALARM_REGISTERS_END = 518 export const AVAILABLE_ALARMS = { // Alarm number // Name and descr based on Enervent EN EDA Modbus regirsters: 3x0385 diff --git a/app/modbus.mjs b/app/modbus.mjs index a52c3c3..c54c4e5 100644 --- a/app/modbus.mjs +++ b/app/modbus.mjs @@ -1,6 +1,8 @@ import { Mutex } from 'async-mutex' import { createLogger } from './logger.mjs' import { + ALARM_REGISTERS_END, + ALARM_REGISTERS_START, AUTOMATION_TYPE_LEGACY_EDA, AUTOMATION_TYPE_MD, AVAILABLE_ALARMS, @@ -332,8 +334,8 @@ export const getDeviceInformation = async (modbusClient) => { export const getAlarmHistory = async (modbusClient) => { let alarmHistory = [] - const startRegister = 385 - const endRegister = 518 + const startRegister = ALARM_REGISTERS_START + const endRegister = ALARM_REGISTERS_END const alarmOffset = 7 for (let register = startRegister; register <= endRegister; register += alarmOffset) { From 7ebda589f72b6b1029ec45ad7d2c85d03f3adc54 Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Thu, 8 Feb 2024 10:03:23 +0200 Subject: [PATCH 3/6] Use "mode" instead of "flag" consistently "flag" was a remnant from the very first proof-of-concept version of this app. "mode" makes way more sense, since that's what it actually represents. --- CHANGELOG.md | 1 + README.md | 25 ++++++++++++++++++------- app/enervent.mjs | 2 +- app/http.mjs | 27 +++++++++++++++------------ app/modbus.mjs | 26 +++++++++++++------------- app/mqtt.mjs | 16 ++++++++-------- eda-modbus-bridge.mjs | 10 +++++----- 7 files changed, 61 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f5a877..282b2e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * Disable "heating/cooling allowed" on legacy EDA units, fixes crash (https://github.com/Jalle19/eda-modbus-bridge/issues/105) * Disable "eco" mode switch on older units (https://github.com/Jalle19/eda-modbus-bridge/issues/104) * Add optional sensors for control panel temperature, supply fan speed and exhaust fan speed (https://github.com/Jalle19/eda-modbus-bridge/issues/96) +* Remove references to "flags", use "modes" everywhere instead ## 2.7.1 diff --git a/README.md b/README.md index f63ca00..af1e8cb 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,9 @@ https://www.home-assistant.io/integrations/switch.rest/ with minimal effort. See * [Usage](#usage) * [HTTP endpoints](#http-endpoints) * [GET /summary](#get-summary) - * [GET /mode/{flag}](#get-modeflag) + * [GET /mode/{mode}](#get-modemode) * [GET /alarms](#get-alarms) - * [POST /mode/{flag}](#post-modeflag) + * [POST /mode/{mode}](#post-modemode) * [POST /setting/{setting}/{value}](#post-settingsettingvalue) * [MQTT support](#mqtt-support) * [Home Assistant MQTT discovery](#home-assistant-mqtt-discovery) @@ -138,6 +138,17 @@ Returns a JSON object like this: "manualBoost": false, "summerNightCooling": false }, + "modes": { + "away": false, + "longAway": false, + "overPressure": false, + "cookerHood": false, + "centralVacuumCleaner": false, + "maxHeating": false, + "maxCooling": false, + "manualBoost": false, + "summerNightCooling": false + }, "readings": { "freshAirTemperature": -2.9, "supplyAirTemperatureAfterHeatRecovery": 16.8, @@ -215,9 +226,9 @@ Returns a JSON object like this: ``` -### GET /mode/{flag} +### GET /mode/{mode} -Returns the status of the specified mode/flag. The response looks like this: +Returns the status of the specified mode. The response looks like this: ```json {"active":false} @@ -240,15 +251,15 @@ Returns the active or dismissed alarms. The response looks like this: } ``` -### POST /mode/{flag} +### POST /mode/{mode} -Enables/disables the specified mode/flag depending on the boolean value in the following request body: +Enables/disables the specified mode depending on the boolean value in the following request body: ```json {"active":false} ``` -The response is identical to that of `GET /mode/{flag}`. +The response is identical to that of `GET /mode/{mode}`. ### POST /setting/{setting}/{value} diff --git a/app/enervent.mjs b/app/enervent.mjs index dc0905c..af88528 100644 --- a/app/enervent.mjs +++ b/app/enervent.mjs @@ -1,4 +1,4 @@ -export const AVAILABLE_FLAGS = { +export const AVAILABLE_MODES = { 'away': 1, 'longAway': 2, 'overPressure': 3, diff --git a/app/http.mjs b/app/http.mjs index baeb651..0928e4b 100644 --- a/app/http.mjs +++ b/app/http.mjs @@ -1,10 +1,10 @@ import { getDeviceInformation, - getFlag, - getFlagSummary, + getMode as modbusGetMode, + getModeSummary, getReadings, getSettings, - setFlag, + setMode as modbusSetMode, setSetting as modbusSetSetting, getAlarmHistory, getDeviceState, @@ -19,8 +19,11 @@ export const root = async (req, res) => { export const summary = async (modbusClient, req, res) => { try { + let modeSummary = await getModeSummary(modbusClient) const summary = { - 'flags': await getFlagSummary(modbusClient), + // TODO: Remove in next major version + 'flags': modeSummary, + 'modes': modeSummary, 'readings': await getReadings(modbusClient), 'settings': await getSettings(modbusClient), 'deviceInformation': await getDeviceInformation(modbusClient), @@ -34,10 +37,10 @@ export const summary = async (modbusClient, req, res) => { } } -export const getFlagStatus = async (modbusClient, req, res) => { +export const getMode = async (modbusClient, req, res) => { try { - const flag = req.params['flag'] - const status = await getFlag(modbusClient, flag) + const mode = req.params['mode'] + const status = await modbusGetMode(modbusClient, mode) res.json({ 'active': status, @@ -47,17 +50,17 @@ export const getFlagStatus = async (modbusClient, req, res) => { } } -export const setFlagStatus = async (modbusClient, req, res) => { +export const setMode = async (modbusClient, req, res) => { try { - const flag = req.params['flag'] + const mode = req.params['mode'] const status = !!req.body['active'] - logger.info(`Setting flag ${flag} to ${status}`) + logger.info(`Setting mode ${mode} to ${status}`) - await setFlag(modbusClient, flag, status) + await modbusSetMode(modbusClient, mode, status) res.json({ - 'active': await getFlag(modbusClient, flag), + 'active': await modbusGetMode(modbusClient, mode), }) } catch (e) { handleError(e, res) diff --git a/app/modbus.mjs b/app/modbus.mjs index c54c4e5..7a152be 100644 --- a/app/modbus.mjs +++ b/app/modbus.mjs @@ -6,7 +6,7 @@ import { AUTOMATION_TYPE_LEGACY_EDA, AUTOMATION_TYPE_MD, AVAILABLE_ALARMS, - AVAILABLE_FLAGS, + AVAILABLE_MODES, AVAILABLE_SETTINGS, createModelNameString, determineAutomationType, @@ -32,7 +32,7 @@ const logger = createLogger('modbus') // Runtime cache for device information (retrieved once per run only since the information doesn't change) let CACHED_DEVICE_INFORMATION -export const getFlagSummary = async (modbusClient) => { +export const getModeSummary = async (modbusClient) => { let result = await mutex.runExclusive(async () => tryReadCoils(modbusClient, 0, 13)) let summary = { // 'stop': result.data[0], // - Can not return value if stopped. @@ -60,26 +60,26 @@ export const getFlagSummary = async (modbusClient) => { return summary } -export const getFlag = async (modbusClient, flag) => { - if (AVAILABLE_FLAGS[flag] === undefined) { - throw new Error('Unknown flag') +export const getMode = async (modbusClient, mode) => { + if (AVAILABLE_MODES[mode] === undefined) { + throw new Error('Unknown mode') } - const result = await mutex.runExclusive(async () => tryReadCoils(modbusClient, AVAILABLE_FLAGS[flag], 1)) + const result = await mutex.runExclusive(async () => tryReadCoils(modbusClient, AVAILABLE_MODES[mode], 1)) return result.data[0] } -export const setFlag = async (modbusClient, flag, value) => { - if (AVAILABLE_FLAGS[flag] === undefined) { - throw new Error('Unknown flag') +export const setMode = async (modbusClient, mode, value) => { + if (AVAILABLE_MODES[mode] === undefined) { + throw new Error('Unknown mode') } - await mutex.runExclusive(async () => tryWriteCoil(modbusClient, AVAILABLE_FLAGS[flag], value)) + await mutex.runExclusive(async () => tryWriteCoil(modbusClient, AVAILABLE_MODES[mode], value)) - // Flags are mutually exclusive, disable all others when enabling one + // Modes are mutually exclusive, disable all others when enabling one if (value) { - await disableAllModesExcept(modbusClient, flag) + await disableAllModesExcept(modbusClient, mode) } } @@ -89,7 +89,7 @@ const disableAllModesExcept = async (modbusClient, exceptedMode) => { continue } - await mutex.runExclusive(async () => tryWriteCoil(modbusClient, AVAILABLE_FLAGS[mode], false)) + await mutex.runExclusive(async () => tryWriteCoil(modbusClient, AVAILABLE_MODES[mode], false)) } } diff --git a/app/mqtt.mjs b/app/mqtt.mjs index 4db9263..ad5508a 100644 --- a/app/mqtt.mjs +++ b/app/mqtt.mjs @@ -3,8 +3,8 @@ import { getDeviceInformation, getSettings, setSetting, - getFlagSummary, - setFlag, + getModeSummary, + setMode, getAlarmStatuses, getDeviceState, } from './modbus.mjs' @@ -27,8 +27,8 @@ export const publishValues = async (modbusClient, mqttClient) => { [TOPIC_NAME_STATUS]: 'online', } - // Publish state for each mode. - await publishFlags(modbusClient, mqttClient) + // Publish mode summary + await publishModeSummary(modbusClient, mqttClient) // Publish each reading const readings = await getReadings(modbusClient) @@ -79,12 +79,12 @@ export const publishDeviceInformation = async (modbusClient, mqttClient) => { }) } -const publishFlags = async (modbusClient, mqttClient) => { +const publishModeSummary = async (modbusClient, mqttClient) => { // Create a map from topic name to value that should be published let topicMap = {} // Publish state for each mode. - const modeSummary = await getFlagSummary(modbusClient) + const modeSummary = await getModeSummary(modbusClient) for (const [mode, state] of Object.entries(modeSummary)) { const topicName = `${TOPIC_PREFIX_MODE}/${mode}` @@ -148,8 +148,8 @@ export const handleMessage = async (modbusClient, mqttClient, topicName, rawPayl logger.info(`Updating mode ${mode} to ${payload}`) - await setFlag(modbusClient, mode, payload) - await publishFlags(modbusClient, mqttClient) + await setMode(modbusClient, mode, payload) + await publishModeSummary(modbusClient, mqttClient) } } diff --git a/eda-modbus-bridge.mjs b/eda-modbus-bridge.mjs index 26c4f45..56a1eb5 100644 --- a/eda-modbus-bridge.mjs +++ b/eda-modbus-bridge.mjs @@ -3,7 +3,7 @@ import expressWinston from 'express-winston' import mqtt from 'mqtt' import yargs from 'yargs' import ModbusRTU from 'modbus-serial' -import { getFlagStatus, root, setFlagStatus, setSetting, summary } from './app/http.mjs' +import { getMode, root, setMode, setSetting, summary } from './app/http.mjs' import { publishValues, subscribeToChanges, @@ -125,11 +125,11 @@ const argv = yargs(process.argv.slice(2)) httpServer.get('/summary', (req, res) => { return summary(modbusClient, req, res) }) - httpServer.get('/mode/:flag', (req, res) => { - return getFlagStatus(modbusClient, req, res) + httpServer.get('/mode/:mode', (req, res) => { + return getMode(modbusClient, req, res) }) - httpServer.post('/mode/:flag', (req, res) => { - return setFlagStatus(modbusClient, req, res) + httpServer.post('/mode/:mode', (req, res) => { + return setMode(modbusClient, req, res) }) httpServer.post('/setting/:setting/:value', (req, res) => { return setSetting(modbusClient, req, res) From 00c60d44c2f3df65d45fa6ad4bc5a28ba2b091dc Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Thu, 8 Feb 2024 10:05:29 +0200 Subject: [PATCH 4/6] Move HTTP router configuration to http.mjs --- app/http.mjs | 26 +++++++++++++++++++++----- eda-modbus-bridge.mjs | 16 ++-------------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/app/http.mjs b/app/http.mjs index 0928e4b..74f0ea5 100644 --- a/app/http.mjs +++ b/app/http.mjs @@ -13,11 +13,11 @@ import { createLogger } from './logger.mjs' const logger = createLogger('http') -export const root = async (req, res) => { +const root = async (req, res) => { res.send('eda-modbus-bridge') } -export const summary = async (modbusClient, req, res) => { +const summary = async (modbusClient, req, res) => { try { let modeSummary = await getModeSummary(modbusClient) const summary = { @@ -37,7 +37,7 @@ export const summary = async (modbusClient, req, res) => { } } -export const getMode = async (modbusClient, req, res) => { +const getMode = async (modbusClient, req, res) => { try { const mode = req.params['mode'] const status = await modbusGetMode(modbusClient, mode) @@ -50,7 +50,7 @@ export const getMode = async (modbusClient, req, res) => { } } -export const setMode = async (modbusClient, req, res) => { +const setMode = async (modbusClient, req, res) => { try { const mode = req.params['mode'] const status = !!req.body['active'] @@ -67,7 +67,7 @@ export const setMode = async (modbusClient, req, res) => { } } -export const setSetting = async (modbusClient, req, res) => { +const setSetting = async (modbusClient, req, res) => { try { const setting = req.params['setting'] const value = req.params['value'] @@ -84,6 +84,22 @@ export const setSetting = async (modbusClient, req, res) => { } } +export const configureRoutes = (httpServer, modbusClient) => { + httpServer.get('/', root) + httpServer.get('/summary', (req, res) => { + return summary(modbusClient, req, res) + }) + httpServer.get('/mode/:mode', (req, res) => { + return getMode(modbusClient, req, res) + }) + httpServer.post('/mode/:mode', (req, res) => { + return setMode(modbusClient, req, res) + }) + httpServer.post('/setting/:setting/:value', (req, res) => { + return setSetting(modbusClient, req, res) + }) +} + const handleError = (e, res) => { logger.error(`An exception occurred: ${e.name}: ${e.message}`, e.stack) res.status(400) diff --git a/eda-modbus-bridge.mjs b/eda-modbus-bridge.mjs index 56a1eb5..3d41287 100644 --- a/eda-modbus-bridge.mjs +++ b/eda-modbus-bridge.mjs @@ -3,7 +3,7 @@ import expressWinston from 'express-winston' import mqtt from 'mqtt' import yargs from 'yargs' import ModbusRTU from 'modbus-serial' -import { getMode, root, setMode, setSetting, summary } from './app/http.mjs' +import { configureRoutes } from './app/http.mjs' import { publishValues, subscribeToChanges, @@ -121,19 +121,7 @@ const argv = yargs(process.argv.slice(2)) httpServer.use(express.json()) // Define routes - httpServer.get('/', root) - httpServer.get('/summary', (req, res) => { - return summary(modbusClient, req, res) - }) - httpServer.get('/mode/:mode', (req, res) => { - return getMode(modbusClient, req, res) - }) - httpServer.post('/mode/:mode', (req, res) => { - return setMode(modbusClient, req, res) - }) - httpServer.post('/setting/:setting/:value', (req, res) => { - return setSetting(modbusClient, req, res) - }) + configureRoutes(httpServer, modbusClient) httpServer.listen(argv.httpPort, argv.httpListenAddress, () => { httpLogger.info(`Listening on http://${argv.httpListenAddress}:${argv.httpPort}`) From 8584d7ab7e13a01974f170dc9934872bd7f35fe3 Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Thu, 8 Feb 2024 10:09:49 +0200 Subject: [PATCH 5/6] Use HTTP 500 instead of HTTP 400 for generic errors --- CHANGELOG.md | 1 + README.md | 2 +- app/http.mjs | 11 ++++++++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 282b2e9..9804417 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * Disable "eco" mode switch on older units (https://github.com/Jalle19/eda-modbus-bridge/issues/104) * Add optional sensors for control panel temperature, supply fan speed and exhaust fan speed (https://github.com/Jalle19/eda-modbus-bridge/issues/96) * Remove references to "flags", use "modes" everywhere instead +* Use HTTP 500 instead of HTTP 400 for generic errors ## 2.7.1 diff --git a/README.md b/README.md index af1e8cb..aad3d94 100644 --- a/README.md +++ b/README.md @@ -263,7 +263,7 @@ The response is identical to that of `GET /mode/{mode}`. ### POST /setting/{setting}/{value} -Changes the setting to the specified value. HTTP 400 is thrown if the value specified is out of range or invalid. +Changes the setting to the specified value. HTTP 400 is returned if the value specified is out of range or invalid. Returns the new setting values, like this: diff --git a/app/http.mjs b/app/http.mjs index 74f0ea5..9ea15d2 100644 --- a/app/http.mjs +++ b/app/http.mjs @@ -80,7 +80,11 @@ const setSetting = async (modbusClient, req, res) => { 'settings': await getSettings(modbusClient), }) } catch (e) { - handleError(e, res) + if (e instanceof RangeError) { + handleError(e, res, 400) + } else { + handleError(e, res) + } } } @@ -100,8 +104,9 @@ export const configureRoutes = (httpServer, modbusClient) => { }) } -const handleError = (e, res) => { +const handleError = (e, res, statusCode = undefined) => { logger.error(`An exception occurred: ${e.name}: ${e.message}`, e.stack) - res.status(400) + // Use HTTP 500 if no status code has been set + res.status(statusCode ?? 500) res.json(e) } From 4b7ec1303e79453809c3a3d954080219f7abf2ef Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Thu, 8 Feb 2024 10:22:38 +0200 Subject: [PATCH 6/6] Properly serialize errors as JSON --- app/http.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/http.mjs b/app/http.mjs index 9ea15d2..f3014db 100644 --- a/app/http.mjs +++ b/app/http.mjs @@ -108,5 +108,8 @@ const handleError = (e, res, statusCode = undefined) => { logger.error(`An exception occurred: ${e.name}: ${e.message}`, e.stack) // Use HTTP 500 if no status code has been set res.status(statusCode ?? 500) - res.json(e) + res.json({ + error: e.name, + message: e.message, + }) }