diff --git a/.gitignore b/.gitignore index 3af0ccb..0657d03 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /data +/nodeApp/node_modules +/nodeApp/package-lock.json \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 590b11a..ef654d3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,24 +5,23 @@ services: image: influxdb:2.7-alpine env_file: - ./influxv2.env + restart: always volumes: # Mount for influxdb data directory and configuration - /DockerDataPool/solarCtrl/influxDB:/var/lib/influxdb2:rw - ./influxDbConfig.yml:/etc/influxdb2/config.yml ports: - - 8080:8086 - networks: - - solarctrlInternal -# telegraf: -# image: telegraf:1.27-alpine -# depends_on: -# - influxdb -# volumes: -# # Mount for telegraf config -# - ./telegraf/telegraf.conf:/etc/telegraf/telegraf.conf:ro -# env_file: -# - ./influxv2.env - mosquitto: + - 8086:8086 + # telegraf: + # image: telegraf:1.27-alpine + # depends_on: + # - influxdb + # volumes: + # # Mount for telegraf config + # - ./telegraf/telegraf.conf:/etc/telegraf/telegraf.conf:ro + # env_file: + # - ./influxv2.env + mosquitto: image: eclipse-mosquitto:2.0.17 restart: always volumes: @@ -32,8 +31,22 @@ services: - ./data/mosquitto/log:/mosquitto/log ports: - 1883:1883 - networks: - - solarctrlInternal -networks: - solarctrlInternal: - + app: + depends_on: + - influxdb + - mosquitto + build: ./nodeApp + restart: always + volumes: + - ./nodeApp/config.json:/app/config.json + grafana: + image: grafana/grafana:10.3.1 + container_name: grafana + depends_on: + - influxdb + restart: always + ports: + - 3000:3000 + volumes: + - "/DockerDataPool/solarCtrl/grafana:/var/lib/grafana" + user: "1000" diff --git a/mosquitto.passwd b/mosquitto.passwd index 115a14e..e5553d2 100644 --- a/mosquitto.passwd +++ b/mosquitto.passwd @@ -1 +1,2 @@ -heatCtrl:$7$101$Q0URo3gi5IgUwTLB$2M0eLGYBq4xP/RD1p3SC3IBJQs2hkEOH7HNm/DNt2/Zq+qmDMVNUic0Mom34sgXSLYKqmXfLQhVi8TjO8Tcu0Q== +heatCtrl:$7$101$7MPYVzvwM+vfvapK$gF4xFS7QLeWa3mDuWmKqnI2rNysyPZyo5ZnYyIYF+R00FmU/uHxP1me7w3xRKJlwZ28rXKX0eEaUKNluiBjToQ== +relais:$7$101$KKzqmsAlu8/mmywt$DhhJvf51AmXBrRW1BLOqhKYla/uNhRTFUbeope8/RJ2/FLfupz42spBZij3lHs8GobNrGylo9aT0NcrZcrtnvA== diff --git a/nodeApp/Dockerfile b/nodeApp/Dockerfile new file mode 100644 index 0000000..4d235f8 --- /dev/null +++ b/nodeApp/Dockerfile @@ -0,0 +1,7 @@ +FROM node:lts-iron +WORKDIR /app +COPY package.json . +RUN npm install +COPY index.js . +ENTRYPOINT ["node"] +CMD ["index.js"] diff --git a/nodeApp/config.json b/nodeApp/config.json new file mode 100644 index 0000000..ba7691b --- /dev/null +++ b/nodeApp/config.json @@ -0,0 +1,201 @@ +{ + "inputs": [ + { + "register": 8708, + "label": "PufferHeizraumTemperatur1", + "unitConversionDivider": 10, + "type": "s16" + }, + { + "register": 8776, + "label": "PufferHeizraumTemperatur3", + "unitConversionDivider": 10, + "type": "s16" + }, + { + "register": 8810, + "label": "PufferHeizraumTemperatur4", + "unitConversionDivider": 10, + "type": "s16" + }, + { + "register": 8844, + "label": "PufferHeizraumTemperatur5", + "unitConversionDivider": 10, + "type": "s16" + }, + { + "register": 8710, + "label": "PufferHolzraumTemperatur1", + "unitConversionDivider": 10, + "type": "s16" + }, + { + "register": 8778, + "label": "PufferHolzraumTemperatur3", + "unitConversionDivider": 10, + "type": "s16" + }, + { + "register": 8846, + "label": "PufferHolzraumTemperatur5", + "unitConversionDivider": 10, + "type": "s16" + }, + { + "register": 8215, + "label": "Flammtemperatur", + "unitConversionDivider": 10, + "type": "s16" + }, + { + "register": 8214, + "label": "Sauerstoff", + "unitConversionDivider": 10, + "type": "s16" + }, + { + "register": 8242, + "label": "Primärluftklappe", + "unitConversionDivider": 10, + "type": "s16" + }, + { + "register": 8243, + "label": "Sekundärluftklappe", + "unitConversionDivider": 10, + "type": "s16" + }, + { + "register": 9498, + "label": "DrehzahlSaugzug", + "unitConversionDivider": 1, + "type": "s16" + }, + { + "register": 8197, + "label": "Kesseltemperatur", + "unitConversionDivider": 10, + "type": "s16" + }, + { + "register": 8202, + "label": "Rücklauftemperatur", + "unitConversionDivider": 10, + "type": "s16" + }, + { + "register": 8212, + "label": "Kesselstatus", + "unitConversionDivider": 1, + "type": "u16" + }, + { + "register": 8250, + "label": "Außentemperatur", + "unitConversionDivider": 10, + "type": "s16" + }, + { + "register": 9049, + "label": "StatusSolar", + "unitConversionDivider": 1, + "type": "u16" + }, + { + "register": 9080, + "label": "SolarKollektortemperatur", + "unitConversionDivider": 10, + "type": "s16" + }, + { + "register": 9110, + "label": "SolarSpeichertemperatur", + "unitConversionDivider": 10, + "type": "s16" + }, + { + "register": 9169, + "label": "Solarpumpe", + "unitConversionDivider": 1, + "type": "bool" + }, + { + "register": 9215, + "label": "SolarWärmeleistung", + "unitConversionDivider": 1000, + "type": "u32" + }, + { + "register": 9245, + "label": "SolarWärmemengeTag", + "unitConversionDivider": 1000, + "type": "u32" + }, + { + "register": 9275, + "label": "SolarWärmemengeGesamt", + "unitConversionDivider": 1000, + "type": "u32" + }, + { + "register": 9305, + "label": "SolarKollektorVorlauftemperatur", + "unitConversionDivider": 10, + "type": "s16" + }, + { + "register": 9335, + "label": "SolarKollektorRücklauftemperatur", + "unitConversionDivider": 10, + "type": "s16" + }, + { + "register": 9365, + "label": "SolarDurchfluss", + "unitConversionDivider": 100, + "type": "s16" + }, + { + "register": 9466, + "label": "SolarpumpePWM", + "unitConversionDivider": 10, + "type": "u16" + }, + { + "register": 8200, + "label": "Kesselpumpe", + "unitConversionDivider": 1, + "type": "bool" + }, + { + "register": 8201, + "label": "KesselpumpePWM", + "unitConversionDivider": 10, + "type": "u16" + }, + { + "register": 8206, + "label": "VerbraucherAnforderung", + "unitConversionDivider": 1, + "type": "u16" + } + ], + "influxToken": "cMHBovac4VeXe60efDcBjItDxXz5LvQ7xcrN0R8yNMM9tEHFPK41hUSBsPsZWrVigXHm6T55EsnegIBSVeUFaQ==", + "interval": 5000, + "mqttBrokerAddress": "mqtt://192.168.1.241:1883", + "mqttUser": "heatCtrl", + "mqttPassword": "637013", + "UmschichtungSolar": { + "Notumschichtung": { + "Temperaturlabel": "PufferHeizraumTemperatur5", + "Ton": 70.0, + "Toff": 65.0, + "mqtt": { + "ctrlTopic": "RelaisUmschichtung/cmnd/POWER1", + "msgOn": "ON", + "msgOff": "OFF" + } + } + } +} \ No newline at end of file diff --git a/nodeApp/index.js b/nodeApp/index.js new file mode 100644 index 0000000..b7b44b9 --- /dev/null +++ b/nodeApp/index.js @@ -0,0 +1,163 @@ +const Modbus = require('jsmodbus') +var fs = require("fs") +const net = require('net') +const mqtt = require("mqtt"); + +const { InfluxDB, Point } = require('@influxdata/influxdb-client') +const socket = new net.Socket() +const modbusClient = new Modbus.client.TCP(socket, 0); +let mqttClient = null; +const options = { + 'host': '192.168.178.63', + 'port': 502 +}; +let config = null; + +let influxOrg = 'heatctrlOrg' +let influxBucket = 'measurements' +const influxUrl = 'http://influxdb:8086' + +let influxClient = null; +let influxWriteClient = null; + +let inputValues = {}; +let configuredInputs = {}; + + +// for reconnecting see node-net-reconnect npm module + +// use socket.on('open', ...) when using serialport +socket.on('connect', function () { + + // make some calls + setInterval(() => { + requestTemps(); + }, config.interval); +}); +fs.readFile('config.json', function (err, data) { + if (err) { + return console.error(err); + } else { + config = JSON.parse(data.toString()); + + configuredInputs = config.inputs; + configuredInputs.forEach(element => { + inputValues[element.label] = { value: null, synced: false, unitConversionDivider: 1 }; + }); + influxClient = new InfluxDB({ url: influxUrl, token: config.influxToken }); + influxWriteClient = influxClient.getWriteApi(influxOrg, influxBucket, 'ms'); + socket.connect(options); + mqttClient = mqtt.connect(config.mqttBrokerAddress, { + username: config.mqttUser, + password: config.mqttPassword + }); + } +}); +async function dispatchModbus() { + //console.log(CF2values); + calculateLogic(); + writeToInflux(); + clearSynced(); +} +async function clearSynced() { + for (const key in inputValues) { + if (Object.hasOwnProperty.call(inputValues, key)) { + inputValues[key].synced = false; + } + } +} +async function requestTemps() { + configuredInputs.forEach(element => { + + modbusClient.readInputRegisters(element.register, (element.type === "u32" | element.type === "s32") ? 2 : 1).then(function (resp) { + let value = null; + switch (element.type) { + case "u32": { + value = resp.response._body._valuesAsBuffer.readUInt32BE() / element.unitConversionDivider; + } break; + case "s32": { + value = resp.response._body._valuesAsBuffer.readInt32BE() / element.unitConversionDivider; + } break; + case "u16": { + value = resp.response._body._valuesAsBuffer.readUInt16BE() / element.unitConversionDivider; + } break; + case "s16": { + value = resp.response._body._valuesAsBuffer.readInt16BE() / element.unitConversionDivider; + } break; + case "bool": { + value = resp.response._body._values[0] / element.unitConversionDivider; + } break; + default: { + value = resp.response._body._values[0] / element.unitConversionDivider; + } break; + } + inputValues[element.label] = { value: value, synced: true }; + let allSynced = true; + /* + if (resp.request._body.start === 9169) { + console.log(resp); + console.log(resp.response._body._values[0]); + } + */ + for (const key in inputValues) { + if (Object.hasOwnProperty.call(inputValues, key)) { + const element = inputValues[key]; + allSynced &= element.synced; + } + } + if (allSynced) { + dispatchModbus(); + } + }, console.error); + }); +} +async function writeToInflux() { + for (const key in inputValues) { + if (Object.hasOwnProperty.call(inputValues, key)) { + let point = new Point('CF2').floatField(key, inputValues[key].value); + influxWriteClient.writePoint(point); + influxWriteClient.flush(); + } + } +} +async function sendMQTT(topic, msg) { + let notumschichtung = config.UmschichtungSolar.Notumschichtung + if (typeof mqttClient !== 'undefined' || mqttClient !== null) { + if (mqttClient.connected) { + mqttClient.publish(topic, msg, { retain: true }); + console.log(msg); + } else { + console.log("client not connected to the mqtt broker"); + } + } else { + console.log("mqttClient is undefined or null"); + } + + //send status over mqtt + let val = 0.0; + if (msg == notumschichtung.mqtt.msgOn) { + val = 100.0; + } else if (msg == notumschichtung.mqtt.msgOff) { + val = 0.0; + } + let point = new Point('Solarumschichtung').floatField("pumpePercent", val); + influxWriteClient.writePoint(point); + influxWriteClient.flush(); +} + +async function calculateLogic() { + let notumschichtung = config.UmschichtungSolar.Notumschichtung + let temp = inputValues[notumschichtung.Temperaturlabel]; + let topic = notumschichtung.mqtt.ctrlTopic; + let msgOn = notumschichtung.mqtt.msgOn; + let msgOff = notumschichtung.mqtt.msgOff; + let Ton = notumschichtung.Ton; + let Toff = notumschichtung.Toff; + + if (temp.value >= Ton) { + sendMQTT(topic, msgOn); + } else if (temp.value <= Toff) { + sendMQTT(topic, msgOff); + } +} + diff --git a/nodeApp/ips b/nodeApp/ips new file mode 100644 index 0000000..88be401 --- /dev/null +++ b/nodeApp/ips @@ -0,0 +1,3 @@ +192.168.178.63 heizung relais +192.168.1.19 umschichtung relais +pw mqtt: 637013 \ No newline at end of file diff --git a/nodeApp/package.json b/nodeApp/package.json new file mode 100644 index 0000000..943dcd4 --- /dev/null +++ b/nodeApp/package.json @@ -0,0 +1,16 @@ +{ + "name": "solarctrl", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@influxdata/influxdb-client": "^1.33.2", + "jsmodbus": "^4.0.10", + "mqtt": "^5.9.1" + } +}