diff --git a/CHANGELOG.md b/CHANGELOG.md index 57c302a..02f0fc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## future release * Add Modbus TCP support (https://github.com/Jalle19/eda-modbus-bridge/issues/86) +* Expose "room temperature average" if available (https://github.com/Jalle19/eda-modbus-bridge/issues/88) +* Expose optional analog inputs (CO2, RH, and room temperature sensors) if available (https://github.com/Jalle19/eda-modbus-bridge/issues/88) ## 2.5.0 diff --git a/app/homeassistant.mjs b/app/homeassistant.mjs index 8c0842e..b514058 100644 --- a/app/homeassistant.mjs +++ b/app/homeassistant.mjs @@ -108,6 +108,49 @@ export const configureMqttDiscovery = async (modbusClient, mqttClient) => { 'Ventilation level (actual)', { 'unit_of_measurement': '%' } ), + // Optional sensors. These are not available for all users, so the entities are disabled by default. + 'roomTemperatureAvg': createTemperatureSensorConfiguration( + configurationBase, + 'roomTemperatureAvg', + 'Room temperature (average)', + { 'enabled_by_default': false } + ), + 'analogInputCo21': createSensorConfiguration(configurationBase, 'analogInputCo21', 'CO2 #1', { + 'enabled_by_default': false, + }), + 'analogInputCo22': createSensorConfiguration(configurationBase, 'analogInputCo22', 'CO2 #2', { + 'enabled_by_default': false, + }), + 'analogInputCo23': createSensorConfiguration(configurationBase, 'analogInputCo23', 'CO2 #3', { + 'enabled_by_default': false, + }), + 'analogInputHumidity1': createHumiditySensorConfiguration(configurationBase, 'analogInputHumidity1', 'RH #1', { + 'enabled_by_default': false, + }), + 'analogInputHumidity2': createHumiditySensorConfiguration(configurationBase, 'analogInputHumidity2', 'RH #2', { + 'enabled_by_default': false, + }), + 'analogInputHumidity3': createHumiditySensorConfiguration(configurationBase, 'analogInputHumidity3', 'RH #3', { + 'enabled_by_default': false, + }), + 'analogRoomTemperature1': createTemperatureSensorConfiguration( + configurationBase, + 'analogRoomTemperature1', + 'Room temperature #1', + { 'enabled_by_default': false } + ), + 'analogRoomTemperature2': createTemperatureSensorConfiguration( + configurationBase, + 'analogRoomTemperature2', + 'Room temperature #2', + { 'enabled_by_default': false } + ), + 'analogRoomTemperature3': createTemperatureSensorConfiguration( + configurationBase, + 'analogRoomTemperature3', + 'Room temperature #3', + { 'enabled_by_default': false } + ), } // Configurable numbers @@ -239,17 +282,27 @@ export const configureMqttDiscovery = async (modbusClient, mqttClient) => { } } -const createTemperatureSensorConfiguration = (configurationBase, readingName, entityName) => { +const createTemperatureSensorConfiguration = (configurationBase, readingName, entityName, extraProperties) => { + if (!extraProperties) { + extraProperties = {} + } + return createSensorConfiguration(configurationBase, readingName, entityName, { 'device_class': 'temperature', 'unit_of_measurement': '°C', + ...extraProperties, }) } -const createHumiditySensorConfiguration = (configurationBase, readingName, entityName) => { +const createHumiditySensorConfiguration = (configurationBase, readingName, entityName, extraProperties) => { + if (!extraProperties) { + extraProperties = {} + } + return createSensorConfiguration(configurationBase, readingName, entityName, { 'device_class': 'humidity', 'unit_of_measurement': '%', + ...extraProperties, }) } diff --git a/app/modbus.mjs b/app/modbus.mjs index ca33d43..ae44d87 100644 --- a/app/modbus.mjs +++ b/app/modbus.mjs @@ -61,6 +61,28 @@ export let AVAILABLE_ALARMS = { 21: { name: 'ExtractFanPressureError', description: 'Waste fan pressure' }, } +const SENSOR_TYPE_NONE = 'NONE' +const SENSOR_TYPE_CO2 = 'CO2' +const SENSOR_TYPE_RH = 'RH' +const SENSOR_TYPE_ROOM_TEMP = 'ROOM_TEMP' + +// 0=NA, 1=CO2_1, 2=CO2_2, 3=CO2_3, 4=RH_1, 5=RH_2, 6=RH_3, 7=OUT_TERM, 8=ROOM_TERM_1, +// 9=ROOM_TERM_2, 10=ROOM_TERM_3, 11=TEMP_SP, 12=Time relay, 13=External heating disable, 14=External cooling disable, +// 15=PDE10, 16=PDE30 +const ANALOG_INPUT_SENSOR_TYPES = { + // Skip sensor types we can't handle + 0: { type: SENSOR_TYPE_NONE }, + 1: { type: SENSOR_TYPE_CO2, name: 'analogInputCo21', description: 'CO2 #1' }, + 2: { type: SENSOR_TYPE_CO2, name: 'analogInputCo22', description: 'CO2 #1' }, + 3: { type: SENSOR_TYPE_CO2, name: 'analogInputCo23', description: 'CO2 #1' }, + 4: { type: SENSOR_TYPE_RH, name: 'analogInputHumidity1', description: 'RH #1' }, + 5: { type: SENSOR_TYPE_RH, name: 'analogInputHumidity2', description: 'RH #1' }, + 6: { type: SENSOR_TYPE_RH, name: 'analogInputHumidity3', description: 'RH #1' }, + 8: { type: SENSOR_TYPE_ROOM_TEMP, name: 'analogInputRoomTemperature1', description: 'Room temperature #1' }, + 9: { type: SENSOR_TYPE_ROOM_TEMP, name: 'analogInputRoomTemperature2', description: 'Room temperature #1' }, + 10: { type: SENSOR_TYPE_ROOM_TEMP, name: 'analogInputRoomTemperature3', description: 'Room temperature #1' }, +} + export const MODBUS_DEVICE_TYPE = { 'RTU': 'rtu', 'TCP': 'tcp', @@ -179,6 +201,28 @@ export const getReadings = async (modbusClient) => { 'ventilationLevelTarget': result.data[3], } + // Analog input sensors. We need to query for the type of sensor in order to parse the data correctly. An + // analog input can be configured but not present - in this case we trust the user and simply report 0 for the + // value. + const sensorTypesResult = await mutex.runExclusive(async () => tryReadHoldingRegisters(modbusClient, 104, 6)) + const sensorValuesResult = await mutex.runExclusive(async () => tryReadHoldingRegisters(modbusClient, 23, 6)) + const sensorReadings = parseAnalogSensors(sensorTypesResult, sensorValuesResult) + + readings = { + ...readings, + ...sensorReadings, + } + + // Room temperature average is always available, but its value is always zero unless one or more optional + // room temperature sensor are installed + if (hasRoomTemperatureSensor(sensorTypesResult)) { + result = await mutex.runExclusive(async () => tryReadHoldingRegisters(modbusClient, 46, 1)) + readings = { + ...readings, + 'roomTemperatureAvg': parseTemperature(result.data[0]), + } + } + return readings } @@ -470,6 +514,40 @@ export const parseStateBitField = (state) => { } } +const hasRoomTemperatureSensor = (sensorTypesResult) => { + for (let i = 0; i < 6; i++) { + const sensorType = ANALOG_INPUT_SENSOR_TYPES[sensorTypesResult.data[i]] + + if (sensorType.type === SENSOR_TYPE_ROOM_TEMP) { + return true + } + } + + return false +} + +export const parseAnalogSensors = (sensorTypesResult, sensorValuesResult) => { + const sensorReadings = {} + + for (let i = 0; i < 6; i++) { + const sensorType = ANALOG_INPUT_SENSOR_TYPES[sensorTypesResult.data[i]] + + switch (sensorType.type) { + // Use raw value + case SENSOR_TYPE_CO2: + case SENSOR_TYPE_RH: + sensorReadings[sensorType.name] = sensorValuesResult.data[i] + break + // Parse as temperature + case SENSOR_TYPE_ROOM_TEMP: + sensorReadings[sensorType.name] = parseTemperature(sensorValuesResult.data[i]) + break + } + } + + return sensorReadings +} + export const validateDevice = (device) => { return device.startsWith('/') || device.startsWith('tcp://') } diff --git a/tests/modbus.test.mjs b/tests/modbus.test.mjs index 95f13aa..e0805e3 100644 --- a/tests/modbus.test.mjs +++ b/tests/modbus.test.mjs @@ -8,6 +8,7 @@ import { validateDevice, parseDevice, MODBUS_DEVICE_TYPE, + parseAnalogSensors, } from '../app/modbus.mjs' test('parse temperature', () => { @@ -166,6 +167,32 @@ test('parse state bitfield', () => { }) }) +test('parseAnalogSensors', () => { + // No sensors configured + let typesResult = { data: [0, 0, 0, 0, 0, 0] } + let valuesResult = { data: [0, 0, 0, 0, 0, 0] } + expect(parseAnalogSensors(typesResult, valuesResult)).toEqual({}) + + // Single CO2 sensor + typesResult = { data: [1, 0, 0, 0, 0, 0] } + valuesResult = { data: [450, 0, 0, 0, 0, 0] } + expect(parseAnalogSensors(typesResult, valuesResult)).toEqual({ + 'analogInputCo21': 450, + }) + + // Multitude of sensors + typesResult = { data: [1, 2, 4, 5, 8, 9] } + valuesResult = { data: [450, 481, 45, 46, 192, 201] } + expect(parseAnalogSensors(typesResult, valuesResult)).toEqual({ + 'analogInputCo21': 450, + 'analogInputCo22': 481, + 'analogInputHumidity1': 45, + 'analogInputHumidity2': 46, + 'analogInputRoomTemperature1': 19.2, + 'analogInputRoomTemperature2': 20.1, + }) +}) + test('validateDevice', () => { expect(validateDevice('/dev/ttyUSB0')).toEqual(true) expect(validateDevice('dev/ttyUSB0')).toEqual(false)