Merged in feature/CAPAUT-254 (pull request #2)

Feature/CAPAUT-254

Approved-by: Tobias Lehmann
This commit is contained in:
2022-07-28 19:44:41 +00:00
committed by Tobias Lehmann
7 changed files with 4813 additions and 126 deletions

2
.gitignore vendored
View File

@@ -48,3 +48,5 @@ Thumbs.db
*.mov
*.wmv
.nyc_output

View File

@@ -9,6 +9,7 @@ definitions:
- node
script:
- npm install
- npm test
# Publish node
- step: &step-publish-nodes

4509
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,17 +3,28 @@
"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": {
"homepage": "http://www.captica.de",
"repository": {
"type": "git",
"url": "bitbucket:captica-dev/de.captica.nodered.node.systacomfort2"
},
"keywords": [ "node-red" ],
"node-red" : {
"keywords": [
"captica",
"systacomfort",
"systacomfort2",
"node-red"
],
"node-red": {
"nodes": {
"systacomfort2": "systacomfort2/systacomfort2.js"
}
},
"scripts": {
"test": "nyc mocha"
},
"devDependencies": {
"chai": "^4.3.6",
"mocha": "^10.0.0",
"nyc": "^15.1.0"
}
}

113
systacomfort2/dataparser.js Normal file
View File

@@ -0,0 +1,113 @@
/**
* SystaComfort II data parser
*/
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 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 }
}
const dataParser = {
/**
* Create a two character based mac address segment
* @param {*} segment
* @returns
*/
createMACPart: function(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
*/
parseData: function(data, topicPrefix) {
let resultMessage = null
// Create mac address from first 5 bytes (reorder)
let deviceAddress = `${this.createMACPart(data[3])}:${this.createMACPart(data[2])}:${this.createMACPart(data[1])}:${this.createMACPart(data[0])}:${this.createMACPart(data[5])}:${this.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 ) {
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
},
/**
* Create reply message to answer based on incoming data
* @param {*} msg
*/
createResponse: function(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
}
}
module.exports = { dataParser, SYSTACOMFORT_VALUES }

View File

@@ -4,128 +4,24 @@
*
* @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 = '??'
const dgram = require('node:dgram')
const { dataParser } = require('./dataparser')
// 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 }
}
const PARADIGMA_UDP_PORT_DEFAULT = 22460
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) {
function systaComfort2(config) {
RED.nodes.createNode(this, config)
const node = this
const udpServer = DGRAM.createSocket('udp4')
const udpServer = dgram.createSocket('udp4')
// Handle exceptions on server
udpServer.on('error', (err) => {
node.error(`SystaComfort2: Error on udp server connection: ${err.stack}`)
node.error(`SystaComfort II: Error on udp server connection: ${err.stack}`)
node.status({ fill: 'red', shape: 'ring', text: 'Error on udp server connection' })
udpServer.close()
})
@@ -134,13 +30,13 @@ module.exports = function(RED) {
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')
let responseData = dataParser.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)
let msgData = dataParser.parseData(data, config.topicPrefix)
if ( msgData ) {
msgData.deviceIP = rinfo.address
msgData.devicePort = rinfo.port
@@ -164,5 +60,5 @@ module.exports = function(RED) {
/**
* Register node object to Node-RED instance
*/
RED.nodes.registerType('systacomfort2', systaComfort2Device)
RED.nodes.registerType('systacomfort2', systaComfort2)
}

155
test/systacomfort2-spec.js Normal file
View File

@@ -0,0 +1,155 @@
/*
* Test implementation of systacomfort2 node
*
* Project: de.captica.nodered.node.systacomfort2
* Author: Marc Böhm <marc.boehm@captica.de>
* Copyright (c) captica GmbH est. 2022 / MIT License
*/
const { expect } = require('chai')
const dgram = require('node:dgram')
const systaComfort2Node = require('../systacomfort2/systacomfort2')
const { dataParser, SYSTACOMFORT_VALUES } = require('../systacomfort2/dataparser')
const { assert } = require('node:console')
const MESSAGE_INIT = 'AJe+LI4NzW0JCQwAMtoAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWAIAAAEAAAACAAAAAAAAADIAAAAyAAAAMgAAAGQAAAC4MP4/APgBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABqAwAAGgMAAE4DAAA8AwAAUgEAAAAAAAA5AAAAlwMAAA0AAAAAAAAAvAIAAIQDAAC8AgAAvAAAAC6TBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAy9tHAsmsAQBQAAAANwAAAAAAAAAEAAAAGgMAAGsDAABOAwAAUgEAAAAAAABrAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAruTDkA=='
const MESSAGE_INIT_ANSWER = 'AJe+LI4NzW0AAAAAAQAAABGcjK0='
const MESSAGE_DATA = 'AJe+LI4NzG0JCQwAMtoAAAEAAAAAAAAAOgEAAFYBAADU/v//SwMAAEUDAABCAwAA1P7//9T+///U/v//AAAAAAAAAABrAwAAXwIAAFoCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFgCAADNAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAyAAAAOYAAACgAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAA+gAAAAAAAAAGAAAAAAAAACYCAADIAAAAZAAAABQAAAB4AAAAAAAAADIAAABQAAAAZAAAAAUAAAACAAAAMgAAAB4AAAAAAAAAAAAAAAAAAAADAAAAMgAAAPQBAAAEAAAAMgAAAHISAADYAQAAAAAAAAAAAAABAAAAyAAAANwAAACWAAAAAAAAAAAAAAAAAAAAIwcAACQHAAAAAAAAXgEAAAAAAAANAAAAAAAAALwCAADIAAAAZAAAABQAAAB4AAAAAAAAAAAAAADIAAAAZAAAAAUAAAACAAAAMgAAAB4AAAAAAAAAAAAAAAAAAAADAAAAMgAAAPQBAAAEAAAAMgAAACMHAAAAAAAAAAAAAAAAAAABAAAAyAAAANwAAACWAAAAAAAAAAAAAAAAAAAAIwcAACQHAABeAQAAAAAAAA0AAAAAAAAAvAIAAMgAAABkAAAAFAAAAHgAAAAAAAAAAAAAAMgAAABkAAAABQAAAAIAAAAAAAAAAAAAAAMAAAAyAAAA9AEAAAQAAAAyAAAAIwcAAAAAAABYAgAAigIAAAEAAABkAAAAZAAAAAAAAAAyAAAAUgMAAAAAAAAFAAAAtgMAAAAAAAAyAAAAAAAAAJABAABkAAAAAQAAAAEAAAADAAAAAwAAAAMAAAAPAAAAMgAAAKQAAAAAAAAAAAAAABMAAAAFAAAAAAAAAAAAAAAjHAAAIwAAAAVZAAANAAAAEgAAAB51AAAcAAAAAAAAAAAAAAAAAAAALAEAAIoCAABkAAAAGQAAAAAAAAAAAAAA+gAAABgBAAAAAAAAAAAAAAAAAAC8AgAAAAAAAMgAAABkAAAAAgAAAMgAAAAeAAAAAAAAABQAAAAeAAAAHgAAADIAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAQAAAAEAAAAHAAAAAAAAAAAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/h54Ug=='
const TOPIC_PREFIX = 'my/custom/topic/prefix'
const CONFIG = { listenPort: 44556 }
const OUTPUT_MESSAGE_PAYLOAD = {
BOILER_START_COUNTER: 22789,
BOILER_WORKING_HOURS: 7203,
BUFFER_TANK_TEMPERATURE_TOP: 83.7,
BUFFER_TANK_TEMPERATURE_BOTTOM: 83.4,
SOLAR_YIELD_CURRENT: 1.3,
SOLAR_YIELD_DAY: 18,
SOLAR_YIELD_TOTAL: 29982,
SOLAR_TEMPERATURE_COLLECTOR:87.5,
SYSTEM_TEMPERATURE_FLOW: 34.2,
SYSTEM_TEMPERATURE_FLOW_RETURN: null,
SYSTEM_TEMPERATURE_EXTERNAL: 31.4,
SYSTEM_TEMPERATURE_PROCESS_WATER_CURRENT: 84.3,
SYSTEM_TEMPERATURE_PROCESS_WATER_TARGET: 60,
SYSTEM_TEMPERATURE_ROOM_TARGET: 20.5,
}
/**
* Node-RED registration/handling
*/
describe('SystaComfort2 - Node-RED node', () => {
// Constructor will be announced on register. We have to keep the object for later tests
let testNodeConstructor = null
let closeCallback = null
// Check node registration first. We fake the RED-objec to capture the registration part
systaComfort2Node({
nodes: {
registerType: function(name, nodeConstructor) {
it('should be registered on node-RED as a new node type', () => {
expect(name).to.equal('systacomfort2')
expect(nodeConstructor).to.not.be.undefined
})
testNodeConstructor = nodeConstructor
},
createNode: function(context, config) {
// Extend context with additional special function (normally provided by node red context object)
context.on = function(eventName, callback) {
closeCallback = callback
}
context.status = function(statusObj) {
it('status object should be present', () => {
expect(configObj).to.not.be.undefined
})
}
it('should be provided the same config object', () => {
expect(config).to.be.equal(CONFIG)
})
}
}
})
/**
* Call the node constructor
*/
it('should be possible to call node constructor and access udp server', () => {
try {
testNodeConstructor(CONFIG)
// Create a udp client to connect to service
const client = dgram.createSocket('udp4')
client.connect(CONFIG.listenPort, (err) => {
expect(err).to.be.undefined
client.close()
})
}
finally {
// Don't forget to send close event after tests
closeCallback()
}
})
})
/**
* Data parser
*/
describe('SystaComfort2 - Data Parser', function(done) {
/**
* MAC address utils
*/
describe('#createMACPart()', () => {
it('should convert a byte into string and pad with a leading "0"', () => {
expect(dataParser.createMACPart(15)).to.equal('0f')
})
it('should convert a byte into as string and NOT pad with a leading "0"', () => {
expect(dataParser.createMACPart(255)).to.equal('ff')
})
})
/**
* Check init answer conversion
*/
describe('#createResponse()', () => {
it('should create an response data buffer based on the input message', () => {
let answerBuffer = dataParser.createResponse(Buffer.from(MESSAGE_INIT, 'base64'))
expect(answerBuffer).to.not.be.undefined
expect(answerBuffer.toString('base64')).to.equal(MESSAGE_INIT_ANSWER)
})
})
/**
* Check data parser
*/
describe('#parseData()', () => {
it('should parse the data message and return an null object because of invalid packet type != 1', () => {
let outputMsg = dataParser.parseData(Buffer.from(MESSAGE_INIT, 'base64'))
expect(outputMsg).to.be.null
})
it('should parse the data message and provide a structured output message', () => {
let outputMsg = dataParser.parseData(Buffer.from(MESSAGE_DATA, 'base64'))
expect(outputMsg).to.not.be.undefined
})
it('should parse the data message and return an structured output message where payload topic starts with: ' + TOPIC_PREFIX, () => {
let outputMsg = dataParser.parseData(Buffer.from(MESSAGE_DATA, 'base64'), TOPIC_PREFIX)
expect(outputMsg).to.not.be.undefined
expect(outputMsg).to.have.own.property('payload')
expect(outputMsg.payload).to.not.be.undefined
Object.values(outputMsg.payload).forEach(valueObj => {
expect(valueObj).to.not.be.undefined
expect(valueObj).to.have.own.property('topic')
expect(valueObj.topic).to.not.be.null
expect(valueObj.topic.startsWith(TOPIC_PREFIX)).to.be.true
})
})
/**
* Check values
*/
let outputMsg = dataParser.parseData(Buffer.from(MESSAGE_DATA, 'base64'))
Object.entries(SYSTACOMFORT_VALUES).forEach(([key, valueObj]) => {
const expectedValue = OUTPUT_MESSAGE_PAYLOAD[key]
it(`should have the value ${key} in payload object and with value ${expectedValue}, description, topic and unit`, () => {
expect(outputMsg.payload).to.have.own.property(key)
let payloadValue = outputMsg.payload[key]
expect(payloadValue).to.not.be.undefined
expect(payloadValue.value).to.be.equal(expectedValue)
expect(payloadValue.topic).to.be.equal(valueObj.topic)
expect(payloadValue.description).to.be.equal(valueObj.description)
expect(payloadValue.unit).to.be.equal(valueObj.unit)
})
})
})
})