Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement support for acknowledging alarms #115

Merged
merged 3 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions app/enervent.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions app/homeassistant.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`,
}
}
22 changes: 20 additions & 2 deletions app/http.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import {
getSettings,
setMode as modbusSetMode,
setSetting as modbusSetSetting,
getAlarmHistory,
acknowledgeAlarm as modbusAcknowledgeAlarm,
getDeviceState,
getNewestAlarm,
getAlarmSummary,
} from './modbus.mjs'
import { createLogger } from './logger.mjs'

Expand All @@ -20,15 +22,18 @@ 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,
'modes': modeSummary,
'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)
Expand Down Expand Up @@ -88,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) => {
Expand All @@ -102,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) => {
Expand Down
80 changes: 40 additions & 40 deletions app/modbus.mjs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -231,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')
}

Expand Down Expand Up @@ -277,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))
}
}

Expand Down Expand Up @@ -339,53 +338,44 @@ 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
if (AVAILABLE_ALARMS[type] === undefined) {
return null
}

for (const historicAlarm of alarmHistory) {
if (historicAlarm.name === alarms[code].name && historicAlarm.state > 0) {
alarms[code].state = historicAlarm.state
}
}
return {
...AVAILABLE_ALARMS[type],
type,
state,
timestamp,
}
}

return alarms
export const acknowledgeAlarm = async (modbusClient) => {
await tryWriteHoldingRegister(modbusClient, 386, 1)
}

export const getDeviceState = async (modbusClient) => {
Expand Down Expand Up @@ -445,3 +435,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
}
}
24 changes: 18 additions & 6 deletions app/mqtt.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import {
setSetting,
getModeSummary,
setMode,
getAlarmStatuses,
getAlarmSummary,
getDeviceState,
acknowledgeAlarm,
} from './modbus.mjs'
import { createLogger } from './logger.mjs'

Expand Down Expand Up @@ -41,9 +42,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)
Expand Down Expand Up @@ -120,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}`)
Expand All @@ -135,21 +141,27 @@ 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}`)

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)
}
}

Expand Down
Loading