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

Add support for optional analog inputs #90

Merged
merged 5 commits into from
Sep 16, 2023
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: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
57 changes: 55 additions & 2 deletions app/homeassistant.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
})
}

Expand Down
78 changes: 78 additions & 0 deletions app/modbus.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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://')
}
Expand Down
27 changes: 27 additions & 0 deletions tests/modbus.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
validateDevice,
parseDevice,
MODBUS_DEVICE_TYPE,
parseAnalogSensors,
} from '../app/modbus.mjs'

test('parse temperature', () => {
Expand Down Expand Up @@ -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)
Expand Down