From 4afa0d54f7b472f042a56dc7c3344bb99021b7f2 Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Thu, 15 Aug 2024 09:16:35 +0300 Subject: [PATCH 1/3] Expose alarm summary over HTTP too, remove alarm history --- app/enervent.mjs | 2 -- app/http.mjs | 8 +++++-- app/modbus.mjs | 61 ++++++++++++++++++------------------------------ app/mqtt.mjs | 7 +++--- 4 files changed, 33 insertions(+), 45 deletions(-) diff --git a/app/enervent.mjs b/app/enervent.mjs index dca5c83..d2183df 100644 --- a/app/enervent.mjs +++ b/app/enervent.mjs @@ -38,8 +38,6 @@ export const AVAILABLE_SETTINGS = { 'defrostingAllowed': 55, } -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/http.mjs b/app/http.mjs index f3014db..d032d3b 100644 --- a/app/http.mjs +++ b/app/http.mjs @@ -6,8 +6,9 @@ import { getSettings, setMode as modbusSetMode, setSetting as modbusSetSetting, - getAlarmHistory, getDeviceState, + getNewestAlarm, + getAlarmSummary, } from './modbus.mjs' import { createLogger } from './logger.mjs' @@ -20,6 +21,8 @@ const root = async (req, res) => { const summary = async (modbusClient, req, res) => { try { let modeSummary = await getModeSummary(modbusClient) + const newestAlarm = await getNewestAlarm(modbusClient) + const summary = { // TODO: Remove in next major version 'flags': modeSummary, @@ -27,8 +30,9 @@ const summary = async (modbusClient, req, res) => { 'readings': await getReadings(modbusClient), 'settings': await getSettings(modbusClient), 'deviceInformation': await getDeviceInformation(modbusClient), - 'alarmHistory': await getAlarmHistory(modbusClient), 'deviceState': await getDeviceState(modbusClient), + 'alarmSummary': await getAlarmSummary(modbusClient), + 'activeAlarm': newestAlarm?.state === 2 ? newestAlarm : null, } res.json(summary) diff --git a/app/modbus.mjs b/app/modbus.mjs index 85ab9f0..a73d202 100644 --- a/app/modbus.mjs +++ b/app/modbus.mjs @@ -1,8 +1,6 @@ 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, @@ -339,53 +337,40 @@ export const getDeviceInformation = async (modbusClient) => { return deviceInformation } -export const getAlarmHistory = async (modbusClient) => { - let alarmHistory = [] +export const getAlarmSummary = async (modbusClient) => { + let alarmSummary = { ...AVAILABLE_ALARMS } + const newestAlarm = await getNewestAlarm(modbusClient) - const startRegister = ALARM_REGISTERS_START - const endRegister = ALARM_REGISTERS_END - const alarmOffset = 7 - - for (let register = startRegister; register <= endRegister; register += alarmOffset) { - const result = await mutex.runExclusive(async () => - tryReadHoldingRegisters(modbusClient, register, alarmOffset) - ) - const code = result.data[0] - const state = result.data[1] + for (const type in alarmSummary) { + // Use "off" as the default alarm state, most likely to be true + alarmSummary[type].state = 0 - // Skip unset alarm slots and unknown alarm types - if (AVAILABLE_ALARMS[code] === undefined) { - continue + // Use the state from the newest alarm + if (type === newestAlarm.type) { + alarmSummary[type].state = newestAlarm.state } - - let alarm = Object.assign({}, AVAILABLE_ALARMS[code]) - alarm.state = state - alarm.date = parseAlarmTimestamp(result) - - alarmHistory.push(alarm) } - return alarmHistory + return alarmSummary } -export const getAlarmStatuses = async (modbusClient) => { - let alarms = { ...AVAILABLE_ALARMS } +export const getNewestAlarm = async (modbusClient) => { + const result = await mutex.runExclusive(async () => tryReadHoldingRegisters(modbusClient, 385, 7)) - // Use the alarm history to determine the state of each alarm - const alarmHistory = await getAlarmHistory(modbusClient) + const type = result.data[0] + const state = result.data[1] + const timestamp = parseAlarmTimestamp(result) - for (const code in alarms) { - // Use "off" as the default alarm state, most likely to be true - alarms[code].state = 0 - - for (const historicAlarm of alarmHistory) { - if (historicAlarm.name === alarms[code].name && historicAlarm.state > 0) { - alarms[code].state = historicAlarm.state - } - } + if (AVAILABLE_ALARMS[type] === undefined) { + return null } - return alarms + return { + ...AVAILABLE_ALARMS[type], + type, + state, + timestamp, + } } export const getDeviceState = async (modbusClient) => { diff --git a/app/mqtt.mjs b/app/mqtt.mjs index ad5508a..88150d1 100644 --- a/app/mqtt.mjs +++ b/app/mqtt.mjs @@ -5,7 +5,7 @@ import { setSetting, getModeSummary, setMode, - getAlarmStatuses, + getAlarmSummary, getDeviceState, } from './modbus.mjs' import { createLogger } from './logger.mjs' @@ -41,9 +41,10 @@ export const publishValues = async (modbusClient, mqttClient) => { // Publish each setting await publishSettings(modbusClient, mqttClient) - const alarmStatuses = await getAlarmStatuses(modbusClient) + // Publish alarm summary + const alarmSummary = await getAlarmSummary(modbusClient) - for (const [, alarm] of Object.entries(alarmStatuses)) { + for (const [, alarm] of Object.entries(alarmSummary)) { const topicName = `${TOPIC_PREFIX_ALARM}/${alarm.name}` topicMap[topicName] = createBinaryValue(alarm.state === 2) From 0701b9c6699604c5beedc9c301c8edb70156ae8e Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Thu, 15 Aug 2024 09:22:26 +0300 Subject: [PATCH 2/3] Add a tryWriteHoldingRegister wrapper --- app/modbus.mjs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/app/modbus.mjs b/app/modbus.mjs index a73d202..450be74 100644 --- a/app/modbus.mjs +++ b/app/modbus.mjs @@ -229,7 +229,8 @@ export const getSettings = async (modbusClient) => { } export const setSetting = async (modbusClient, setting, value) => { - if (AVAILABLE_SETTINGS[setting] === undefined) { + const dataAddress = AVAILABLE_SETTINGS[setting] + if (dataAddress === undefined) { throw new Error('Unknown setting') } @@ -275,9 +276,9 @@ 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 () => tryWriteCoil(modbusClient, AVAILABLE_SETTINGS[setting], value)) + await mutex.runExclusive(async () => tryWriteCoil(modbusClient, dataAddress, value)) } else { - await mutex.runExclusive(async () => modbusClient.writeRegister(AVAILABLE_SETTINGS[setting], intValue)) + await mutex.runExclusive(async () => tryWriteHoldingRegister(modbusClient, dataAddress, intValue)) } } @@ -430,3 +431,13 @@ const tryReadHoldingRegisters = async (modbusClient, dataAddress, length) => { throw e } } + +const tryWriteHoldingRegister = async (modbusClient, dataAddress, value) => { + try { + logger.debug(`Writing ${value} to holding register address ${dataAddress}`) + return await modbusClient.writeRegister(dataAddress, value) + } catch (e) { + logger.error(`Failed to write holding register address ${dataAddress}, value ${value}`) + throw e + } +} From 83883ce1a94f5178aebb54179c0f65e8bfb9706f Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Thu, 15 Aug 2024 10:13:51 +0300 Subject: [PATCH 3/3] Expose "acknowledge alarm" functionality over MQTT and HTTP Closes #114 A button is exposed to Home Assistant and a POST /alarm/acknowledge route is exposed via HTTP --- app/homeassistant.mjs | 20 ++++++++++++++++++++ app/http.mjs | 14 ++++++++++++++ app/modbus.mjs | 4 ++++ app/mqtt.mjs | 17 ++++++++++++++--- 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/app/homeassistant.mjs b/app/homeassistant.mjs index 89a36a3..c304e04 100644 --- a/app/homeassistant.mjs +++ b/app/homeassistant.mjs @@ -337,12 +337,22 @@ export const configureMqttDiscovery = async (modbusClient, mqttClient) => { 'defrosting': createDeviceStateConfiguration(configurationBase, 'defrosting', 'Defrosting'), } + // Button for acknowledging alarms + const buttonConfigurationMap = { + 'acknowledgeAlarm': createButtonConfiguration( + configurationBase, + 'acknowledgeAlarm', + 'Acknowledge newest alarm' + ), + } + // Final map that describes everything we want to be auto-discovered const configurationMap = { 'sensor': sensorConfigurationMap, 'number': numberConfigurationMap, 'switch': switchConfigurationMap, 'binary_sensor': binarySensorConfigurationMap, + 'button': buttonConfigurationMap, } // Publish configurations @@ -476,3 +486,13 @@ const createDeviceStateConfiguration = (configurationBase, stateName, entityName 'entity_category': 'diagnostic', } } + +const createButtonConfiguration = (configurationBase, buttonName, entityName) => { + return { + ...configurationBase, + 'unique_id': `eda-button-${buttonName}`, + 'name': entityName, + 'object_id': `eda_button_${buttonName}`, + 'command_topic': `${TOPIC_PREFIX_ALARM}/acknowledge`, + } +} diff --git a/app/http.mjs b/app/http.mjs index d032d3b..7373fab 100644 --- a/app/http.mjs +++ b/app/http.mjs @@ -6,6 +6,7 @@ import { getSettings, setMode as modbusSetMode, setSetting as modbusSetSetting, + acknowledgeAlarm as modbusAcknowledgeAlarm, getDeviceState, getNewestAlarm, getAlarmSummary, @@ -92,6 +93,16 @@ const setSetting = async (modbusClient, req, res) => { } } +const acknowledgeAlarm = async (modbusClient, req, res) => { + try { + logger.info('Acknowledging currently active alarm (if any)') + + await modbusAcknowledgeAlarm(modbusClient) + } catch (e) { + handleError(e, res) + } +} + export const configureRoutes = (httpServer, modbusClient) => { httpServer.get('/', root) httpServer.get('/summary', (req, res) => { @@ -106,6 +117,9 @@ export const configureRoutes = (httpServer, modbusClient) => { httpServer.post('/setting/:setting/:value', (req, res) => { return setSetting(modbusClient, req, res) }) + httpServer.post('/alarm/acknowledge', (req, res) => { + return acknowledgeAlarm(modbusClient, req, res) + }) } const handleError = (e, res, statusCode = undefined) => { diff --git a/app/modbus.mjs b/app/modbus.mjs index 450be74..65baee5 100644 --- a/app/modbus.mjs +++ b/app/modbus.mjs @@ -374,6 +374,10 @@ export const getNewestAlarm = async (modbusClient) => { } } +export const acknowledgeAlarm = async (modbusClient) => { + await tryWriteHoldingRegister(modbusClient, 386, 1) +} + export const getDeviceState = async (modbusClient) => { const result = await mutex.runExclusive(async () => tryReadHoldingRegisters(modbusClient, 44, 1)) diff --git a/app/mqtt.mjs b/app/mqtt.mjs index 88150d1..a0e7f4f 100644 --- a/app/mqtt.mjs +++ b/app/mqtt.mjs @@ -7,6 +7,7 @@ import { setMode, getAlarmSummary, getDeviceState, + acknowledgeAlarm, } from './modbus.mjs' import { createLogger } from './logger.mjs' @@ -121,8 +122,12 @@ const publishTopics = async (mqttClient, topicMap, publishOptions = {}) => { } export const subscribeToChanges = async (modbusClient, mqttClient) => { - // Subscribe to settings and mode changes - const topicNames = [`${TOPIC_PREFIX_MODE}/+/set`, `${TOPIC_PREFIX_SETTINGS}/+/set`] + // Subscribe to writable topics + const topicNames = [ + `${TOPIC_PREFIX_MODE}/+/set`, + `${TOPIC_PREFIX_SETTINGS}/+/set`, + `${TOPIC_PREFIX_ALARM}/acknowledge`, + ] for (const topicName of topicNames) { logger.info(`Subscribing to topic(s) ${topicName}`) @@ -136,8 +141,8 @@ export const handleMessage = async (modbusClient, mqttClient, topicName, rawPayl const payload = parsePayload(rawPayload) - // Handle settings updates if (topicName.startsWith(TOPIC_PREFIX_SETTINGS) && topicName.endsWith('/set')) { + // Handle settings updates const settingName = topicName.substring(TOPIC_PREFIX_SETTINGS.length + 1, topicName.lastIndexOf('/')) logger.info(`Updating setting ${settingName} to ${payload}`) @@ -145,12 +150,18 @@ export const handleMessage = async (modbusClient, mqttClient, topicName, rawPayl await setSetting(modbusClient, settingName, payload) await publishSettings(modbusClient, mqttClient) } else if (topicName.startsWith(TOPIC_PREFIX_MODE) && topicName.endsWith('/set')) { + // Handle mode changes const mode = topicName.substring(TOPIC_PREFIX_MODE.length + 1, topicName.lastIndexOf('/')) logger.info(`Updating mode ${mode} to ${payload}`) await setMode(modbusClient, mode, payload) await publishModeSummary(modbusClient, mqttClient) + } else if (topicName.startsWith(TOPIC_PREFIX_ALARM) && topicName.endsWith('/acknowledge')) { + // Acknowledge alarm + logger.info('Acknowledging currently active alarm (if any)') + + await acknowledgeAlarm(modbusClient) } }