Merged in feature/CAPAUT-253 (pull request #1)
Feature/CAPAUT-253 Approved-by: Tobias Lehmann
This commit is contained in:
19
LICENSE
Normal file
19
LICENSE
Normal file
@@ -0,0 +1,19 @@
|
||||
MIT Licence Copyright (c) captica GmbH - Marc Böhm (marc.boehm@captica.de)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
43
README.md
Normal file
43
README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
<img src="img/captica_logo.svg" width="200" style="float: right;" />
|
||||
<p style="padding-top: 50px"></p>
|
||||
|
||||
# captica: Node-RED Node for SystaComfort II
|
||||
|
||||
Receive data from SystaComfort II devices in Node-RED flows
|
||||
|
||||
This Node-RED node is capturing the packets from a SystaComfort II device and prepares the data for further processing.
|
||||
|
||||
## Supported values
|
||||
|
||||
|Value-Key|Topic|Description|
|
||||
|--|--|--|
|
||||
|BOILER_START_COUNTER|boiler/starts/get|Starts of boiler gas unit|
|
||||
|BOILER_WORKING_HOURS|boiler/workinghours/get|Overall working hours of boiler|
|
||||
|BUFFER_TANK_TEMPERATURE_TOP|buffer/temperature-top/get|Top temperature of buffer tank|
|
||||
|BUFFER_TANK_TEMPERATURE_BOTTOM|buffer/temperature-bottom/get|Bottom temperature of buffer tank|
|
||||
|SOLAR_YIELD_CURRENT|solar/yield-current/get|Current yield of solar|
|
||||
|SOLAR_YIELD_DAY|solar/yield-day/get|Day yield of solar|
|
||||
|SOLAR_YIELD_TOTAL|solar/yield-total/get|Total yield of solar|
|
||||
|SOLAR_TEMPERATURE_COLLECTOR|solar/temperature-collector/get|Current temperature of solar collector|
|
||||
|SYSTEM_TEMPERATURE_FLOW|system/temperature-flow/get|Current temperature of system flow|
|
||||
|SYSTEM_TEMPERATURE_FLOW_RETURN|system/temperature-flowreturn/get|Current temperature of system flow (return)|
|
||||
|SYSTEM_TEMPERATURE_EXTERNAL|system/temperature-external/get|External temperature of system|
|
||||
|SYSTEM_TEMPERATURE_PROCESS_WATER_CURRENT|system/temperature-processwater-current/get|Current temperature of system process water|
|
||||
|SYSTEM_TEMPERATURE_PROCESS_WATER_TARGET|system/temperature-processwater-target/get|Target temperature of system process water|
|
||||
|SYSTEM_TEMPERATURE_ROOM_TARGET|system/temperature-room-target/get|Target room temperature|
|
||||
|
||||
## Compatible SystaComfort II devices
|
||||
|
||||
|Device|Software-Version|Hardware-Version|Basis-System|Compatible?|
|
||||
|--|--|--|--|--|
|
||||
|SystaComfort II|V1.16|V1.21|V1.13|yes - except SYSTEM_TEMPERATURE_FLOW_RETURN is not available|
|
||||
|
||||
## Node Parameters
|
||||
|Parameter|Description|
|
||||
|--|--|
|
||||
|Name|Name of node on flow|
|
||||
|Topic Prefix|Prefix will be added to topic property on output message payload|
|
||||
|Listen Port|Port of UDP Server to listen on. Useful on multiple devices|
|
||||
|
||||
## Connectivity
|
||||
To capture the published data from your SystaComfort II device you have to redirect the traffic on your local network. If you have a local DNS Forwarder you can just create a static host override. The domain `pradigma.remoteportal.de` needs to be resolved to your internal Node-RED ip address.
|
||||
29
bitbucket-pipelines.yml
Normal file
29
bitbucket-pipelines.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
image: node:16
|
||||
|
||||
definitions:
|
||||
steps:
|
||||
# Build nodes
|
||||
- step: &step-build-nodes
|
||||
name: Build
|
||||
caches:
|
||||
- node
|
||||
script:
|
||||
- npm install
|
||||
|
||||
# Publish node
|
||||
- step: &step-publish-nodes
|
||||
name: Publish Nodes
|
||||
script:
|
||||
- pipe: atlassian/npm-publish:0.3.2
|
||||
variables:
|
||||
NPM_TOKEN: $CAPTICA_NPM_TOKEN
|
||||
FOLDER: './'
|
||||
EXTRA_ARGS: '--access public'
|
||||
pipelines:
|
||||
default:
|
||||
- step: *step-build-nodes
|
||||
|
||||
tags:
|
||||
v*:
|
||||
- step: *step-build-nodes
|
||||
- step: *step-publish-nodes
|
||||
BIN
img/captica_favicon.png
Normal file
BIN
img/captica_favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
1
img/captica_logo.svg
Normal file
1
img/captica_logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Ebene_1" data-name="Ebene 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 376.28 292.81"><defs><style>.cls-1{fill:#fff;}</style></defs><path class="cls-1" d="M148.48,178a15.21,15.21,0,0,0,9.11-2.83q4-2.82,4-8.47h1.73l.07.23a11.12,11.12,0,0,1-4.38,9.54,16.48,16.48,0,0,1-10.49,3.56q-7.78,0-12.21-5.61t-4.43-14.76v-1.58q0-9.1,4.41-14.72t12.16-5.61a16.06,16.06,0,0,1,10.71,3.71q4.38,3.7,4.23,10.45l-.07.22h-1.73q0-6-3.84-9.16a14.1,14.1,0,0,0-9.3-3.18q-7.16,0-10.84,5.12t-3.69,13.17v1.58q0,8.13,3.69,13.24T148.48,178Z"/><path class="cls-1" d="M199.35,171.75a16,16,0,0,1-6.29,6,20.67,20.67,0,0,1-10.13,2.31q-5.69,0-8.92-3.1a11.09,11.09,0,0,1-3.24-8.38q0-5.11,4.84-8.59a20.31,20.31,0,0,1,12.1-3.46h11.64v-5.95a10,10,0,0,0-3.26-7.92q-3.26-2.85-9.17-2.84a13.9,13.9,0,0,0-8.9,2.67,8.39,8.39,0,0,0-3.37,6.89l-1.7-.08-.07-.19a9.48,9.48,0,0,1,3.69-8q3.92-3.3,10.35-3.3t10.51,3.32q3.94,3.3,3.95,9.56v20.4a35.59,35.59,0,0,0,.24,4.18,27.92,27.92,0,0,0,.81,4h-2.26c-.32-1.78-.54-3.12-.66-4a23.58,23.58,0,0,1-.16-2.88Zm-16.42,6.33a18.66,18.66,0,0,0,10.26-2.68,15.53,15.53,0,0,0,6.16-7.64v-9.15H187.83a18.7,18.7,0,0,0-10.68,2.92c-2.9,1.94-4.35,4.35-4.35,7.21a8.84,8.84,0,0,0,2.77,6.72A10.31,10.31,0,0,0,182.93,178.08Z"/><path class="cls-1" d="M243.9,159.67q0,9.41-4.06,14.9a13.17,13.17,0,0,1-11.22,5.5,17.34,17.34,0,0,1-8.27-1.88,14.58,14.58,0,0,1-5.55-5.12v21.87h-2V138.55h1.58l.41,7.19a15.35,15.35,0,0,1,5.54-5.82,15.53,15.53,0,0,1,8.24-2.13,13.11,13.11,0,0,1,11.28,5.71q4.08,5.7,4.08,15.38Zm-2.07-.79q0-8.48-3.41-13.76a11,11,0,0,0-9.84-5.29c-3.87,0-6.88,1-9.05,2.89a16.67,16.67,0,0,0-4.73,7.16v19.46a13.13,13.13,0,0,0,5,6.34,15.43,15.43,0,0,0,8.8,2.36,11.1,11.1,0,0,0,9.81-5q3.38-5,3.37-13.4Z"/><path class="cls-1" d="M258.33,127.55v11h10v2.07h-10v28.16c0,3.34.57,5.71,1.7,7.13a5.43,5.43,0,0,0,4.48,2.13c.67,0,1.27,0,1.8-.08s1.26-.13,2.19-.26l.37,1.84a10.5,10.5,0,0,1-2,.42,22.24,22.24,0,0,1-2.29.11,7.47,7.47,0,0,1-6.23-2.67c-1.42-1.78-2.13-4.66-2.13-8.62V140.62h-7.53v-2.07h7.53v-11Z"/><path class="cls-1" d="M307.65,178a15.23,15.23,0,0,0,9.11-2.83q4-2.82,3.95-8.47h1.73l.08.23a11.1,11.1,0,0,1-4.39,9.54,16.45,16.45,0,0,1-10.48,3.56q-7.8,0-12.22-5.61T291,159.7v-1.58q0-9.1,4.4-14.72t12.16-5.61a16,16,0,0,1,10.71,3.71q4.39,3.7,4.24,10.45l-.08.22h-1.73q0-6-3.84-9.16a14.1,14.1,0,0,0-9.3-3.18q-7.14,0-10.84,5.12T293,158.12v1.58q0,8.13,3.69,13.24T307.65,178Z"/><path class="cls-1" d="M358.51,171.75a15.88,15.88,0,0,1-6.29,6,20.62,20.62,0,0,1-10.12,2.31q-5.69,0-8.93-3.1a11.12,11.12,0,0,1-3.23-8.38q0-5.11,4.83-8.59a20.34,20.34,0,0,1,12.11-3.46h11.63v-5.95a9.92,9.92,0,0,0-3.26-7.92q-3.25-2.85-9.16-2.84a14,14,0,0,0-8.91,2.67,8.42,8.42,0,0,0-3.37,6.89l-1.69-.08-.07-.19a9.44,9.44,0,0,1,3.68-8q3.91-3.3,10.36-3.3c4.36,0,7.87,1.11,10.5,3.32s4,5.39,4,9.56v20.4a35.56,35.56,0,0,0,.25,4.18,25.46,25.46,0,0,0,.81,4h-2.26c-.33-1.78-.55-3.12-.66-4a23.49,23.49,0,0,1-.17-2.88Zm-16.41,6.33a18.66,18.66,0,0,0,10.26-2.68,15.52,15.52,0,0,0,6.15-7.64v-9.15H347a18.67,18.67,0,0,0-10.67,2.92c-2.9,1.94-4.35,4.35-4.35,7.21a8.81,8.81,0,0,0,2.77,6.72A10.27,10.27,0,0,0,342.1,178.08Z"/><circle class="cls-1" cx="281.95" cy="131.51" r="2.14"/><path class="cls-1" d="M147.81,280.87A135.73,135.73,0,0,1,49.9,51.2,136.62,136.62,0,0,1,145.76,9.41l2.17,0a135.78,135.78,0,0,1,134,114,1.09,1.09,0,0,1-1.05,1.26,1.12,1.12,0,0,1-1.1-.91A132,132,0,0,0,239.8,48.22a135,135,0,0,0-92-36.65c-1.33,0-2.65,0-4,.06A133.57,133.57,0,0,0,55.62,241.72a132.87,132.87,0,0,0,92.17,37l3.15,0A133.56,133.56,0,0,0,281.42,142.08c0-1.14,0-1.29-.07-2.43a1.09,1.09,0,0,1,.29-.79,1.07,1.07,0,0,1,.77-.35,1.09,1.09,0,0,1,1.1,1c.06,1.15.06,1.32.08,2.48A135.73,135.73,0,0,1,151,280.84Q149.4,280.87,147.81,280.87Z"/><path class="cls-1" d="M147.81,280.87A135.73,135.73,0,0,1,49.91,51.2,136.58,136.58,0,0,1,145.77,9.41l2.16,0a135.78,135.78,0,0,1,134,114,1.06,1.06,0,0,1-.24.87,1.09,1.09,0,0,1-.81.39,1.11,1.11,0,0,1-1.09-.91,132.12,132.12,0,0,0-39.94-75.52,135,135,0,0,0-92-36.65q-2,0-4,.06A133.57,133.57,0,0,0,55.63,241.72a132.85,132.85,0,0,0,92.16,37q1.59,0,3.15,0A133.56,133.56,0,0,0,281.42,142.08c0-1.14,0-1.29-.07-2.43a1.09,1.09,0,0,1,.29-.79,1.07,1.07,0,0,1,.77-.35,1.11,1.11,0,0,1,1.11,1c.06,1.15.05,1.32.08,2.48A135.75,135.75,0,0,1,151,280.84Z"/></svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
19
package.json
Normal file
19
package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@captica/node-red-systacomfort2",
|
||||
"version": "1.0.0",
|
||||
"description": "Node-RED node to connect and read data from SystaComfort II heating/solar systems",
|
||||
"author": "Marc Böhm (marc.boehm@captica.de)",
|
||||
"homepage": "http://www.captica.de",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "bitbucket:captica-dev/de.captica.nodered.node.systacomfort2"
|
||||
},
|
||||
"dependencies": {
|
||||
},
|
||||
"keywords": [ "node-red" ],
|
||||
"node-red" : {
|
||||
"nodes": {
|
||||
"systacomfort2": "systacomfort2/systacomfort2.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
systacomfort2/icons/captica.png
Normal file
BIN
systacomfort2/icons/captica.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
39
systacomfort2/systacomfort2.html
Normal file
39
systacomfort2/systacomfort2.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('systacomfort2', {
|
||||
category: 'captica',
|
||||
color: '#37BEE4',
|
||||
defaults: {
|
||||
name: {value:""},
|
||||
topicPrefix: {value:""},
|
||||
listenPort: {value:""}
|
||||
},
|
||||
inputs: 0,
|
||||
outputs: 1,
|
||||
icon: 'captica.png',
|
||||
label: function () {
|
||||
return this.name || 'SystaComfort2'
|
||||
},
|
||||
outputLabels: 'data'
|
||||
})
|
||||
</script>
|
||||
|
||||
<script type="text/html" data-template-name="systacomfort2">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-topicPrefix"><i class="fa fa-tag"></i> Topic Prefix</label>
|
||||
<input type="text" id="node-input-topicPrefix" placeholder="my/topic/prefix">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-listenPort"><i class="fa fa-tag"></i> Listen Port</label>
|
||||
<input type="text" id="node-input-listenPort" placeholder="default: 22460">
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/html" data-help-name="systacomfort2">
|
||||
<p>
|
||||
A capture device to read data from SystaComfort II devices
|
||||
</p>
|
||||
</script>
|
||||
168
systacomfort2/systacomfort2.js
Normal file
168
systacomfort2/systacomfort2.js
Normal file
@@ -0,0 +1,168 @@
|
||||
|
||||
/**
|
||||
* SystaComfort II node implementation
|
||||
*
|
||||
* @param {*} RED
|
||||
*/
|
||||
const DGRAM = require('node:dgram')
|
||||
const BITMASK_2_BYTES = 0xFF
|
||||
const BITMASK_4_BYTES = 0xFFFF
|
||||
const PARADIGMA_EMPTY_VALUE = -300
|
||||
const PARADIGMA_OFFSET_COUNTER_REPLY = 0x3FBF
|
||||
const PARADIGMA_OFFSET_MAC_REPLY = 0x8E83
|
||||
const PARADIGMA_REPLY_BYTE_12 = 0x01
|
||||
const PARADIGMA_UDP_PORT_DEFAULT = 22460
|
||||
const REPLY_LENGTH = 20
|
||||
const MAC_UNKNOWN_SEGEMENT = '??'
|
||||
|
||||
// Enumeration of available data channels
|
||||
const SYSTACOMFORT_VALUES = {
|
||||
BOILER_START_COUNTER: { dataPosition: 748, topic: 'boiler/starts/get', description: 'Starts Brenner', unit: null, valueDivider: 1 },
|
||||
BOILER_WORKING_HOURS: { dataPosition: 740, topic: 'boiler/workinghours/get', description: 'Betriebsstunden Brenner', unit: 'h', valueDivider: 1 },
|
||||
BUFFER_TANK_TEMPERATURE_TOP: { dataPosition: 40, topic: 'buffer/temperature-top/get', description: 'Temperatur Pufferspeicher oben (TPO)', unit: '°C', valueDivider: 10 },
|
||||
BUFFER_TANK_TEMPERATURE_BOTTOM: { dataPosition: 44, topic: 'buffer/temperature-bottom/get', description: 'Temperatur Pufferspeicher unten (TPU)', unit: '°C', valueDivider: 10 },
|
||||
SOLAR_YIELD_CURRENT: { dataPosition: 752, topic: 'solar/yield-current/get', description: 'Solargewinn aktuell', unit: 'kW', valueDivider: 10 },
|
||||
SOLAR_YIELD_DAY: { dataPosition: 756, topic: 'solar/yield-day/get', description: 'Solargewinn pro Tag', unit: 'kWh', valueDivider: 1 },
|
||||
SOLAR_YIELD_TOTAL: { dataPosition: 760, topic: 'solar/yield-total/get', description: 'Solargewinn gesamt', unit: 'kWh', valueDivider: 1 },
|
||||
SOLAR_TEMPERATURE_COLLECTOR: { dataPosition: 68, topic: 'solar/temperature-collector/get', description: 'Kollektortemperatur', unit: '°C', valueDivider: 10 },
|
||||
SYSTEM_TEMPERATURE_FLOW: { dataPosition: 28, topic: 'system/temperature-flow/get', description: 'Vorlauftemperatur Heizung (Ist)', unit: '°C', valueDivider: 10 },
|
||||
SYSTEM_TEMPERATURE_FLOW_RETURN: { dataPosition: 32, topic: 'system/temperature-flowreturn/get', description: 'Rücklauftemperatur Heizung', unit: '°C', valueDivider: 10 },
|
||||
SYSTEM_TEMPERATURE_EXTERNAL: { dataPosition: 24, topic: 'system/temperature-external/get', description: 'Außentemperatur', unit: '°C', valueDivider: 10 },
|
||||
SYSTEM_TEMPERATURE_PROCESS_WATER_CURRENT: { dataPosition: 36, topic: 'system/temperature-processwater-current/get', description: 'Warmwassertemperatur (Ist / TWO)', unit: '°C', valueDivider: 10 },
|
||||
SYSTEM_TEMPERATURE_PROCESS_WATER_TARGET: { dataPosition: 112, topic: 'system/temperature-processwater-target/get', description: 'Warmwassertemperatur (Soll)', unit: '°C', valueDivider: 10 },
|
||||
SYSTEM_TEMPERATURE_ROOM_TARGET: { dataPosition: 116, topic: 'system/temperature-room-target/get', description: 'Raumtemperatur (Soll)', unit: '°C', valueDivider: 10 }
|
||||
}
|
||||
|
||||
module.exports = function(RED) {
|
||||
|
||||
/**
|
||||
* Create a two character based mac address segment
|
||||
* @param {*} segment
|
||||
* @returns
|
||||
*/
|
||||
function createMACPart(segment) {
|
||||
return ( segment === null ? MAC_UNKNOWN_SEGEMENT : segment.toString(16).padStart(2,'0') )
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse datagram message recieved on upd socket from systa device
|
||||
* @param {*} data
|
||||
* @returns
|
||||
*/
|
||||
function parseData(data, topicPrefix) {
|
||||
// Create mac address from first 5 bytes (reorder)
|
||||
let deviceAddress = `${createMACPart(data[3])}:${createMACPart(data[2])}:${createMACPart(data[1])}:${createMACPart(data[0])}:${createMACPart(data[5])}:${createMACPart(data[4])}`
|
||||
// Message type
|
||||
let packetType = data[16];
|
||||
// Calculate counter from two bytes
|
||||
let counter = ( ( data[6] & BITMASK_2_BYTES ) << 0 ) | ( ( data[7] & BITMASK_2_BYTES ) << 8 );
|
||||
// Process message of type 1 only
|
||||
if ( packetType == 0x01 ) {
|
||||
let resultMessage = {
|
||||
payload: {},
|
||||
deviceAddress: deviceAddress,
|
||||
messageCounter: counter,
|
||||
packetType: packetType
|
||||
}
|
||||
|
||||
Object.entries(SYSTACOMFORT_VALUES).forEach( ([key, valueDescriptor]) => {
|
||||
let dataValue = null
|
||||
let startPosition = valueDescriptor.dataPosition
|
||||
if ( data.length > startPosition ) {
|
||||
let dataValueRaw = ((data[startPosition] & BITMASK_2_BYTES)) |
|
||||
((data[startPosition+1] & BITMASK_2_BYTES) << 8 ) |
|
||||
((data[startPosition+2] & BITMASK_2_BYTES) << 16 ) |
|
||||
((data[startPosition+3] & BITMASK_2_BYTES) << 24 )
|
||||
if ( dataValueRaw != PARADIGMA_EMPTY_VALUE ) {
|
||||
dataValue = (dataValueRaw / valueDescriptor.valueDivider)
|
||||
}
|
||||
let topic = valueDescriptor.topic
|
||||
// Add prefix if necessar
|
||||
if ( topicPrefix ) {
|
||||
topic = topicPrefix + (topicPrefix.endsWith('/') ? '' : '/') + valueDescriptor.topic
|
||||
}
|
||||
resultMessage.payload[key] = { value: dataValue, topic: topic, description: valueDescriptor.description, unit: valueDescriptor.unit }
|
||||
}
|
||||
})
|
||||
return resultMessage
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Create reply message to answer based on incoming data
|
||||
* @param {*} msg
|
||||
*/
|
||||
function createResponse(data) {
|
||||
let reply = Buffer.alloc(REPLY_LENGTH)
|
||||
// Initialize the first 8 bytes by copy the recieved one. Fill all others with 0
|
||||
for ( let i = 0; i < REPLY_LENGTH; i++) {
|
||||
reply[i] = ( i < 8 ? data[i] : 0x00 )
|
||||
}
|
||||
// Static part of reply, always 1
|
||||
reply[12] = PARADIGMA_REPLY_BYTE_12
|
||||
// Generate a mac id. Using as first byte the second last mac byte and the second byte is the last mac byte
|
||||
// Adding an offset and forcing 4 bytes (0xFFFF) mask
|
||||
let macID = ( ( data[4] & BITMASK_2_BYTES ) + ( data[5] << 8 ) + PARADIGMA_OFFSET_MAC_REPLY ) & BITMASK_4_BYTES
|
||||
// first (right) byte is used on position 16, second (left) byte on 17. using bit shifting to force 1 byte
|
||||
reply[16] = macID & BITMASK_2_BYTES
|
||||
reply[17] = macID >> 8
|
||||
// Generate a counter id. Sum the counter bytes and add a static offset
|
||||
// Adding an offset and forcing 4 bytes (0xFFFF) mask
|
||||
let counterReply = ( ( data[6] & BITMASK_2_BYTES ) + ( data[7] << 8 ) + PARADIGMA_OFFSET_COUNTER_REPLY ) & BITMASK_4_BYTES
|
||||
reply[18] = counterReply & BITMASK_2_BYTES
|
||||
reply[19] = counterReply >> 8
|
||||
return reply
|
||||
}
|
||||
|
||||
/**
|
||||
* Node object function
|
||||
* @param {*} config
|
||||
*/
|
||||
function systaComfort2Device(config) {
|
||||
RED.nodes.createNode(this, config)
|
||||
const node = this
|
||||
const udpServer = DGRAM.createSocket('udp4')
|
||||
// Handle exceptions on server
|
||||
udpServer.on('error', (err) => {
|
||||
node.error(`SystaComfort2: Error on udp server connection: ${err.stack}`)
|
||||
node.status({ fill: 'red', shape: 'ring', text: 'Error on udp server connection' })
|
||||
udpServer.close()
|
||||
})
|
||||
// Handle messages
|
||||
udpServer.on('message', (data, rinfo) => {
|
||||
node.log(`Recieved message from ${rinfo.address}:${rinfo.port} at ${new Date().toLocaleString()}`)
|
||||
node.status({ fill: 'green', shape: 'dot', text: `Recieved message from ${rinfo.address}:${rinfo.port} at ${new Date().toLocaleString()}` })
|
||||
// Create response and send back to client
|
||||
let responseData = createResponse(data)
|
||||
const client = DGRAM.createSocket('udp4')
|
||||
client.send(responseData, rinfo.port, rinfo.address, (err) => {
|
||||
client.close()
|
||||
})
|
||||
// Parse data receieved from systa comfort device
|
||||
let msgData = parseData(data, config.topicPrefix)
|
||||
if ( msgData ) {
|
||||
msgData.deviceIP = rinfo.address
|
||||
msgData.devicePort = rinfo.port
|
||||
node.send(msgData)
|
||||
}
|
||||
})
|
||||
// Handle listen state
|
||||
udpServer.on('listening', () => {
|
||||
const address = udpServer.address()
|
||||
node.status({ fill: 'green', shape: 'dot', text: `Server is listening: ${address.address}:${address.port}` })
|
||||
})
|
||||
udpServer.bind(config.listenPort || PARADIGMA_UDP_PORT_DEFAULT)
|
||||
// Cleanup hooks
|
||||
node.on('close', () => {
|
||||
if ( udpServer ) {
|
||||
udpServer.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Register node object to Node-RED instance
|
||||
*/
|
||||
RED.nodes.registerType('systacomfort2', systaComfort2Device)
|
||||
}
|
||||
Reference in New Issue
Block a user