OTmonitor and Domoticz parallel access to OTGW

For OpenTherm-gateway related questions in Domoticz

Moderator: leecollings

ernorv
Posts: 17
Joined: Sunday 13 November 2016 10:42
Target OS: -
Domoticz version:
Contact:

Re: OTmonitor and Domoticz parallel access to OTGW

Post by ernorv »

@jake let's see if we can get this done this XMas ;)

I have changed it a little, you can now tell which data field is to be replaced by 'fieldnumberInPsOutputToReplace'. As there are 25 of them in the PS1 output, choose wisely (and yes, I've filled in the default fieldnumber 16 now (+1 :mrgreen: ) (and in the code is the complete list as on the OTGW website ;). Next to that you can also choose the Opentherm ID of what to put in that (e.g. 29 (Decimal, not hex) for the Solar).

Code: Select all

#!/usr/bin/node
/*
Make a small listener that Domoticz can connect to as if it was the real otmonitor application. 
Then relay the messages between OTGW and Domoticz such that otmonitor is still fully functional.

author: ernorv

v1.0 - Special
v1.0 - Special with '=>' replaced for better compatibility

*/

const portNumberForDomoticzToUse     = 7689;   // The new port that Domoticz needs to use. In Domoticz, set this number as the port under the OTGW hardware
const portNumberOfOtMonitor          = 7686;   // The network port OTGW provides, as can be set under the Configuration parts

const replaceTTwithTC                = true;   // If true: use TC instead of TT (if false, just standard Domoticz TT usage)

// Normally no further settings required below this line

// -------------- some very specific settings, do not normally use
const resetOTGW_PS_state             = true; // default true: sets the PS state back to 0 after Domoticz has its data, is what allows otmonitor to keep making nice graphs as if Domoticz was not there

// just if you really know what you do, set these
const replaceAnIdInDomoticzOutput    = false; // default: false! set this to true for replacement of the DHW Setpoing with Solar Temperature
const idToUseForReplacement          = 29;   // (Decimal) ID of the Opentherm message to use. 29 refers to the Solar, I use 25 for test purposes
const fieldnumberInPsOutputToReplace = 16;   // which field to replace in PS output to replace:

/*
	The PS1 output has 25 field: see http://otgw.tclcode.com/firmware.html#dataids
	
	The OTGW outputs the following fields (normally that is). I have given them below. Set the 'fieldnumberInPsOutputToReplace' to the 
	fieldnumber in accordance with what you like to be replace, given these fields (default is 16):
	
	1)  Status (MsgID=0) - Printed as two 8-bit bitfields
	2)  Control setpoint (MsgID=1) - Printed as a floating point value
	3)  Remote parameter flags (MsgID= 6) - Printed as two 8-bit bitfields
	4)  Maximum relative modulation level (MsgID=14) - Printed as a floating point value
	5)  Boiler capacity and modulation limits (MsgID=15) - Printed as two bytes
	6)  Room Setpoint (MsgID=16) - Printed as a floating point value
	7)  Relative modulation level (MsgID=17) - Printed as a floating point value
	8)  CH water pressure (MsgID=18) - Printed as a floating point value
	9)  Room temperature (MsgID=24) - Printed as a floating point value
	10) Boiler water temperature (MsgID=25) - Printed as a floating point value
	11) DHW temperature (MsgID=26) - Printed as a floating point value
	12) Outside temperature (MsgID=27) - Printed as a floating point value
	13) Return water temperature (MsgID=28) - Printed as a floating point value
	14) DHW setpoint boundaries (MsgID=48) - Printed as two bytes
	15) Max CH setpoint boundaries (MsgID=49) - Printed as two bytes
	16) DHW setpoint (MsgID=56) - Printed as a floating point value
	17) Max CH water setpoint (MsgID=57) - Printed as a floating point value
	18) Burner starts (MsgID=116) - Printed as a decimal value
	19) CH pump starts (MsgID=117) - Printed as a decimal value
	20) DHW pump/valve starts (MsgID=118) - Printed as a decimal value
	21) DHW burner starts (MsgID=119) - Printed as a decimal value
	22)	Burner operation hours (MsgID=120) - Printed as a decimal value
	23)	CH pump operation hours (MsgID=121) - Printed as a decimal value
	24) DHW pump/valve operation hours (MsgID=122) - Printed as a decimal value
	25) DHW burner operation hours (MsgID=123) - Printed as a decimal value
	
*/
// ----------------------------------------------------------

var net = require('net');
var server 
var otgwSocket
var domSocketsArray = [];

// -------------- DEFINE SERVER LISTENING FOR CONNECTIONS FROM DOM ---------------
server = net.createServer(function (socket) {
	console.log('Incoming Domoticz Connection...');
	domSocketsArray.push(socket);
	
	socket.on('data', function (data) {
		data = data.toString(); // data may come in as Buffer
		console.log('DOM: ' + data.replace(/[\r\n]*$/, "").replace(/\r\n/g, ','));
		if (! otgwSocket.destroyed) {
			if (replaceTTwithTC) {
				otgwSocket.write(data.replace("TT=", "TC="));
			} else {
				otgwSocket.write(data);
			};
		}
	});

	socket.on('end', function() {
		console.log('end');
		domSocketsArray.splice(domSocketsArray.indexOf(socket), 1);
	});
	
	socket.on('error', function () {
		console.log('DOM: detected error on socket: probably otgwSocket died');	
		domSocketsArray.splice(domSocketsArray.indexOf(socket), 1);
	});
	
	socket.on('close', function () {
		console.log('DOM: socket closed');
		domSocketsArray.splice(domSocketsArray.indexOf(socket), 1);
	});
});

server.on('error', function(err) {
  throw err;
});


// ---------------- CONNECT TO OTGW ----------------
function relayToClients(data) {
	for (var i =0 ; i < domSocketsArray.length; i++) {
		if (! domSocketsArray[i].destroyed) {
			domSocketsArray[i].write(data);
		}
	}
}

var resetPS1 = false;
var currentSolarTemperature = 0

function connectToOTGW() {
	otgwSocket = net.createConnection({port: portNumberOfOtMonitor}, function() {
	  console.log('Connected to OTGW server at port number ' + portNumberOfOtMonitor + '!');
	});

	otgwSocket.on('data', function(data) {
		data = data.toString(); // data comes in as Buffer, we want to use it as string
		
		if (data.match(/[BTAR][0-9ABCDEF,]{8}/)) {
			// this data is not for Domoticz, it is what makes the normal otmonitor app work
			// console.log('<not relayed> OTGW: ' + data.toString().replace("\r", '').replace('\n', ''));
		
			// for solar temperature we are looking for the following: B C01D 0F00
			// solar temp: id 29 R - Solar storage temperature f8.8 -40..127 Solar storage temperature (°C)

			// for test purposes, lets look at B40192B00 : 49.00 degrees of boiler water temperature
			// 25 R - Boiler water temp. f8.8 -40..127 Flow water temperature from boiler (°C)
			// note: sign bit!
			// byte 2 is data-id, byte 3 and 4 contain the data value
			// byte 1: parity bit, 3 bits msg type and 4 bits spare, hence mind the parity
			if (replaceAnIdInDomoticzOutput && (data[0] == 'B')) {
				if ((parseInt(data.substr(1,2), 16)&0x70) == 0x40){ // test whether this is a read-ack
					//console.log('that was a read-ack')
					if ((parseInt(data.substr(3,2), 16)) == idToUseForReplacement){ // new data to use
						var f1, f2, s
						s  = parseInt(data.substr(5,2))&0x80;
						f1 = parseInt(data.substr(5,2), 16)&0x7F;
						f2 = parseInt(data.substr(7,2), 16);
						
						currentSolarTemperature = f1 + f2/256.0;
						if (s > 0) currentSolarTemperature *= -1;
						console.log(data.replace(/[\n\l\r]/g, "") + ': New replacement Temperature with temp ' + currentSolarTemperature.toFixed(2))
					}
				}
			}
		} else {
			
			console.log('OTGW: ' + data.replace(/[\r\n]*$/, "").replace(/\r\n/g, ','));
			// sometimes there is a PR:,I=00 on the line which ends up in Domoticz status log. It looks like
			// malfomed data coming from the OTGW itself. Let's do some cleaning up, keeping the status log clean
			data = data.replace(/PR:[ ]*,/g, "PR: ");
			data = data.replace(/PR, /g, "PR: ");
			// a difficult one: this happens every so often, a comma missing in the output of the OTGW PS=0 output, let's repair this
			data = data.replace(/([01]{8}\/[01]{8},[0-9\.]{4,6},[01]{8}\/[01]{8})([0-9\.]{4,6}.*)/, "$1,$2");
			
			/* Now we can replace a field in the output to Domoticz with another one, as long as the one to read has also a 
			 * floating point definition (bit technical, but if you like, google for the specification of the Opentherm itself).
			 * This implies that we can only put it into a Domoticz field that also understand this floating point definition. 
			 * Most temperatures are by the way. Also the (Opentherm ID) needs to be filled in. These are both done above in the 
			 * const definition section of the script. 
			 */
			
			if (replaceAnIdInDomoticzOutput) {
				tmp = data.split(',')
				if (tmp.length == 25) { // for the PS1 output we expect 25 fields: see http://otgw.tclcode.com/firmware.html#dataids
					tmp[fieldnumberInPsOutputToReplace] = currentSolarTemperature.toFixed(2);
					data = tmp.join(",");
					console.log("RPLC: " + data);
				}
			}
			relayToClients(data);
		}
		
		if (data.indexOf('PS: 1') !== -1 ) {
			// Domoticz is issueing a PS1 command, reset it if indicated by the settings above
			resetPS1 = resetOTGW_PS_state;
		};
		if (resetPS1 && (data.match(/[01]{7,9}\/[01]{7,9},.*/))) {
			// ok the data requested by domoticz has been produced, now go back to normal otmonitor operation
			otgwSocket.write('PS=0\r\n'); // no need to test for destroyed here, data just arrived from it
			resetPS1 = false;
		};
	});

	otgwSocket.on('end', function() {
		console.log('OTGW disconnected from server, reconnecting in 1 second.');
		setTimeout(function() { connectToOTGW(); }, 1000)
	});
}
connectToOTGW();

// ---------------------- start the server for active listening ----------------
server.listen(portNumberForDomoticzToUse, function() {
   console.log('Server for Domoticz is now listening, point Domoticz OTGW Hardware to port '+ portNumberForDomoticzToUse + '.');
});

console.log('Application started.');
Specifying id = id is a bit more complicated, as I then have to get all the IDs used in the first place, although they are now in copied list of course.

BTW Hope the Dutch is gone now
jake
Posts: 742
Joined: Saturday 30 May 2015 22:40
Target OS: Raspberry Pi / ODroid
Domoticz version: beta
Contact:

Re: OTmonitor and Domoticz parallel access to OTGW

Post by jake »

ernorv wrote:@jake let's see if we can get this done this XMas ;)

I have changed it a little, you can now tell which data field is to be replaced by 'fieldnumberInPsOutputToReplace'. As there are 25 of them in the PS1 output, choose wisely (and yes, I've filled in the default fieldnumber 16 now (+1 :mrgreen: ) (and in the code is the complete list as on the OTGW website ;). Next to that you can also choose the Opentherm ID of what to put in that (e.g. 29 (Decimal, not hex) for the Solar).
...
Specifying id = id is a bit more complicated, as I then have to get all the IDs used in the first place, although they are now in copied list of course.

BTW Hope the Dutch is gone now
Thanks for this nice improvement to the code. This makes it for more people intesting to modify to their needs, if needed. Unfortunately there must be a small error somewhere, since this is the response while starting the script:

Code: Select all

pi@domoticz-pi ~/node_modules $ node otgwmod.js
Application started.
Server for Domoticz is now listening, point Domoticz OTGW Hardware to port 7689.
events.js:85
      throw er; // Unhandled 'error' event
            ^
Error: connect ECONNREFUSED
    at exports._errnoException (util.js:746:11)
    at TCPConnectWrap.afterConnect [as oncomplete] (net.js:983:19)
pi@domoticz-pi ~/node_modules $
This time, I wisely chose 11 (DHW temperaturate) as my replacement parameter, to nicely blend in the Solar Boiler acting as a normal hot water boiler, but then with a free energy source, the sun. :)

I added the startup of the otgw.js to Supervisor. Therefore I have a hard time figuring out if something is wrong. Since the RPI is already booted and therefore both otmonitor and otgw scripts are running, I can't see very well if there was an error.
Is it possible to manually stop (and not auto restart) the otgw.js script?
Is it also possilbe that the startup order of both scripts is important?
ernorv
Posts: 17
Joined: Sunday 13 November 2016 10:42
Target OS: -
Domoticz version:
Contact:

Re: OTmonitor and Domoticz parallel access to OTGW

Post by ernorv »

@jake yes you are right, there is a dependecy on OTGW to be already running :oops: therefore now with actual checking if the port is opened, without throwing an error, and if no connection could be made to the OTGW, simply reschedule after 10 seconds. This should be much better (also in my own setup that is :D ). Next to that I realised some minor other errors, like choosing number '11' would actually be off with 1, and actual checking if the port for Domoticz is free or not (as mentioned earlier somewhere in this thread).

And because I was enjoying it too much, I also rewrote the Opentherm parsing a bit to be more splitted into it's own function, but that's just for the coders out there I guess.

as for your supervisor parts, you could actually stop the service for a while and start manually to look at the output:
- sudo supervisorctl stop otgwNode <- or whatever you called it
- node otgw.js

and later start it again with sudo supervisorctl start otgwNode

or enable the loggins to be generated in the supervisor configuration files and use: sudo supervisorctl tail -f otgwNode


So here we go again:

Code: Select all

#!/usr/bin/node
/*
Make a small listener that Domoticz can connect to as if it was the real otmonitor application. 
Then relay the messages between OTGW and Domoticz such that otmonitor is still fully functional.

author: ernorv

v2.0 
	- rewrote the OpenTherm parts to be splitted into separate function
	- added check whether port are actually opened, if no connection to OTGW, wait ten seconds and retry 
		this can happen if starting both this script and OTGW, where OTGW needs some time
	- added check if we could open a port for Domoticz, now nicely goes out of the function without throwing an error
	- added specific author section for debugging purposes
	- fieldnumberInPsOutputToReplace was actually off with 1, so now if you set it to 11, it will be 11 according to the list below
*/

var portNumberForDomoticzToUse     = 7689;   // The new port that Domoticz needs to use. In Domoticz, set this number as the port under the OTGW hardware
var portNumberOfOtMonitor          = 7686;   // The network port OTGW provides, as can be set under the Configuration parts
const replaceTTwithTC              = true;   // If true: use TC instead of TT (if false, just standard Domoticz TT usage)

// -------------- Normally no further settings required below this line
var resetOTGW_PS_state             = true; // default true: sets the PS state back to 0 after Domoticz has its data, is what allows otmonitor to keep making nice graphs as if Domoticz was not there

// -------------- just if you really know what you do, set these
var replaceAnIdInDomoticzOutput    = true; // default: false! set this to true for replacement of the DHW Setpoing with Solar Temperature
var idToUseForReplacement          = 29;   // (Decimal) ID of the Opentherm message to use. 29 refers to the Solar Boiler Temperature
var fieldnumberInPsOutputToReplace = 11;   // which field to replace in PS output to replace, see numbers below

/*
	The PS1 output has 25 field: see http://otgw.tclcode.com/firmware.html#dataids
	
	The OTGW outputs the following fields (normally that is). I have given them below. Set the 'fieldnumberInPsOutputToReplace' to the 
	fieldnumber in accordance with what you like to be replace, given these fields (default is 16):
	
	1)  Status (MsgID=0) - Printed as two 8-bit bitfields
	2)  Control setpoint (MsgID=1) - Printed as a floating point value
	3)  Remote parameter flags (MsgID= 6) - Printed as two 8-bit bitfields
	4)  Maximum relative modulation level (MsgID=14) - Printed as a floating point value
	5)  Boiler capacity and modulation limits (MsgID=15) - Printed as two bytes
	6)  Room Setpoint (MsgID=16) - Printed as a floating point value
	7)  Relative modulation level (MsgID=17) - Printed as a floating point value
	8)  CH water pressure (MsgID=18) - Printed as a floating point value
	9)  Room temperature (MsgID=24) - Printed as a floating point value
	10) Boiler water temperature (MsgID=25) - Printed as a floating point value
	11) DHW temperature (MsgID=26) - Printed as a floating point value
	12) Outside temperature (MsgID=27) - Printed as a floating point value
	13) Return water temperature (MsgID=28) - Printed as a floating point value
	14) DHW setpoint boundaries (MsgID=48) - Printed as two bytes
	15) Max CH setpoint boundaries (MsgID=49) - Printed as two bytes
	16) DHW setpoint (MsgID=56) - Printed as a floating point value
	17) Max CH water setpoint (MsgID=57) - Printed as a floating point value
	18) Burner starts (MsgID=116) - Printed as a decimal value
	19) CH pump starts (MsgID=117) - Printed as a decimal value
	20) DHW pump/valve starts (MsgID=118) - Printed as a decimal value
	21) DHW burner starts (MsgID=119) - Printed as a decimal value
	22)	Burner operation hours (MsgID=120) - Printed as a decimal value
	23)	CH pump operation hours (MsgID=121) - Printed as a decimal value
	24) DHW pump/valve operation hours (MsgID=122) - Printed as a decimal value
	25) DHW burner operation hours (MsgID=123) - Printed as a decimal value
	
*/

// ------------- do not use, specific for author ---------------
var   extraDebug      = false;
const authorOverrides = false; // for anybody out there, keep this to false !!! 

if (authorOverrides) {
	portNumberForDomoticzToUse     = 17689;   // for test, do not interrupt normal running process, so throw it somewhere else
	portNumberOfOtMonitor          = 7686; 
	resetOTGW_PS_state             = false;   // not to interfere with the actual program already runing
    replaceAnIdInDomoticzOutput    = true;    // default: false! set this to true for replacement of the DHW Setpoing with Solar Temperature
	idToUseForReplacement          = 25;      // (Decimal) ID of the Opentherm message to use. I use 25 for test purposes
	fieldnumberInPsOutputToReplace = 11;      // which field to replace in PS output to replace, see list above
	extraDebug = true;
}


// ----------------------------------------------------------

var net = require('net');
var server 
var otgwSocket
var domSocketsArray = [];

// -------------- DEFINE SERVER LISTENING FOR CONNECTIONS FROM DOM ---------------
server = net.createServer(function (socket) {
	console.log('Incoming Domoticz Connection...');
	domSocketsArray.push(socket);
	
	socket.on('data', function (data) {
		data = data.toString(); // data may come in as Buffer
		console.log('DOM: ' + data.replace(/[\r\n]*$/, "").replace(/\r\n/g, ','));
		if (! otgwSocket.destroyed) {
			if (replaceTTwithTC) {
				otgwSocket.write(data.replace("TT=", "TC="));
			} else {
				otgwSocket.write(data);
			};
		}
	});

	socket.on('end', function() {
		console.log('end');
		domSocketsArray.splice(domSocketsArray.indexOf(socket), 1);
	});
	
	socket.on('error', function () {
		console.log('DOM: detected error on socket: probably otgwSocket died');	
		domSocketsArray.splice(domSocketsArray.indexOf(socket), 1);
	});
	
	socket.on('close', function () {
		console.log('DOM: socket closed');
		domSocketsArray.splice(domSocketsArray.indexOf(socket), 1);
	});
});

server.on('error', function(err) {
	console.log('Could not open the port ' + portNumberForDomoticzToUse + ' for Domoticz to connect to.');
	console.log('Please check whether the port is actually free to use, and adapt the "portNumberForDomoticzToUse" in the script.');
	console.log('A common source for this is that one uses the port number of the OTGW instead of a free one.')
	process.exit(1);
});


// ---------------- CONNECT TO OTGW ----------------
function relayToClients(data) {
	for (var i =0 ; i < domSocketsArray.length; i++) {
		if (! domSocketsArray[i].destroyed) {
			domSocketsArray[i].write(data);
		}
	}
}

var resetPS1 = false;
var currentReplacementValue = 0; // init this with 0

function connectToOTGW() {
	
	otgwSocket = net.createConnection({port: portNumberOfOtMonitor}, function() {
	  console.log('Connected to OTGW server at port number ' + portNumberOfOtMonitor + '!');
	});

	otgwSocket.on('data', function(data) {
		data = data.toString(); // data comes in as Buffer, we want to use it as string
		
		if (data.match(/[BTARE][0-9ABCDEF,]{8}/)) {
			// this data is not for Domoticz, it is what makes the normal otmonitor app work
			// console.log('<not relayed> OTGW: ' + data.toString().replace("\r", '').replace('\n', ''));
			
			// this data can contain a possible value for replacement purposes, inspect it.
			if (replaceAnIdInDomoticzOutput || extraDebug) {
				var otObj = parseMessage(data);
				if (replaceAnIdInDomoticzOutput && (otObj.readAck || otObj.writeAck) && (otObj.otMsgId == idToUseForReplacement)) {
					currentReplacementValue = (otObj.recognized) ? otObj.valstr : otObj.asFloat.toFixed(2);
					console.log("New replacement value found: (msgid " + otObj.otMsgId + ": '" + otObj.name + "') : " + currentReplacementValue);
				}
			}			
		} else {
			
			console.log('OTGW : ' + data.replace(/[\r\n]*$/, "").replace(/\r\n/g, ','));
			// sometimes there is a PR:,I=00 on the line which ends up in Domoticz status log. It looks like
			// malfomed data coming from the OTGW itself. Let's do some cleaning up, keeping the status log clean
			data = data.replace(/PR:[ ]*,/g, "PR: ");
			data = data.replace(/PR, /g, "PR: ");
			// a difficult one: this happens every so often, a comma missing in the output of the OTGW PS=0 output, let's repair this
			data = data.replace(/([01]{8}\/[01]{8},[0-9\.]{4,6},[01]{8}\/[01]{8})([0-9\.]{4,6}.*)/, "$1,$2");
			
			/* Now we can replace a field in the output to Domoticz with another one, as long as the one to read has also a 
			 * floating point definition (bit technical, but if you like, google for the specification of the Opentherm itself).
			 * This implies that we can only put it into a Domoticz field that also understand this floating point definition. 
			 * Most temperatures are by the way. Also the (Opentherm ID) needs to be filled in. These are both done above in the 
			 * const definition section of the script. 
			 */
			
			if (replaceAnIdInDomoticzOutput) {
				tmp = data.split(',')
				if (tmp.length == 25) { // for the PS1 output we expect 25 fields: see http://otgw.tclcode.com/firmware.html#dataids
					tmp[fieldnumberInPsOutputToReplace-1] = currentReplacementValue;
					data = tmp.join(",");
					console.log("RPLC : " + data.replace(/[\r\n]*$/, ""));
				}
			}
			relayToClients(data);
		}
		
		if (data.indexOf('PS: 1') !== -1 ) {
			// Domoticz is issueing a PS1 command, reset it if indicated by the settings above
			resetPS1 = resetOTGW_PS_state;
		};
		if (resetPS1 && (data.match(/[01]{7,9}\/[01]{7,9},.*/))) {
			// ok the data requested by domoticz has been produced, now go back to normal otmonitor operation
			otgwSocket.write('PS=0\r\n'); // no need to test for destroyed here, data just arrived from it
			resetPS1 = false;
		};
	});

	otgwSocket.on('end', function() {
		console.log('OTGW disconnected from server, reconnecting in 1 second.');
		setTimeout(function() { connectToOTGW(); }, 1000)
	});
	
	otgwSocket.on('error', function(err) {
		console.log('==> Could not connect to the OTGW at port ' + portNumberOfOtMonitor);
		console.log('==> Make sure the OTGW is running and set to provide the port as given above.');
		console.log('==> Will try to reconnect in 10 seconds, maybe OTGW is just starting up here.');
		setTimeout(function() { connectToOTGW(); }, 10000);
	});
	
	otgwSocket.on('close', function() {
		console.log('otgwSocket Close part');
	});
}

//-------------------------- function for extra debug --------------------

// the following is a list of already known IDs and how to convert them
// otdef[OTMSGIDasDecimal] = {'readable name', '[asFlags|asFloat|asUInt|asSInt|asFlags]'
var otdef  = {};
otdef[0]   = {name:"Master and slave status flags", val:"asFlags"}; // read ack: slave, read-data: master
otdef[5]   = {name:"Application Specific Flags", val: "asFlags"};
otdef[9]   = {name:"Remote Override Room Setpoint", val: "asFloat"};
otdef[14]  = {name:"Maximum Relative Modulation Level", val: "asFloat"};
otdef[16]  = {name:"Room Setpoint", val: "asFloat"};
otdef[17]  = {name:"Relative Modulation Level", val: "asFloat"};
otdef[18]  = {name:"Water Pressure", val: "asFloat"};
otdef[24]  = {name:"Room Temperature", val: "asFloat"};
otdef[25]  = {name:"Boiler Water Temperature", val: "asFloat"};
otdef[26]  = {name:"DHW Temperature", val: "asFloat"};
otdef[27]  = {name:"Outside Temperature", val: "asFloat"};
otdef[28]  = {name:"Return Water Temperature", val: "asFloat"};
otdef[29]  = {name:"Solar Boiler Temperature", val: "asFloat"};
otdef[56]  = {name:"DHW Setpoint", val: "asFloat"};
otdef[57]  = {name:"Max CH water Setpoint", val: "asFloat"};
otdef[100] = {name:"Remote Override Function", val: "asROF"};
otdef[120] = {name:"Burner Operation Hours", val: "asUInt"};
otdef[121] = {name:"CH Pump Operation Hours", val: "asUInt"};
otdef[122] = {name:"DHW Pump/Valve Operation Hours", val: "asUInt"};

function parseMessage(data) {
	/*
	 * this function parses those B12345678 like messages and returns the information
	 * in case it is either a read-ack or a write-ack, such to simplify the msgs as 
	 * from the OTGW side. The instruction before are not so interesting, as we would
	 * like to see the actual information in the datastreams.
	 */
	
	// byte 2 is data-id, byte 3 and 4 contain the data value
	// byte 1: parity bit, 3 bits msg type and 4 bits spare, hence mind the parity
	
	data = data.substr(0,9);  // remove extra newline stuff
	data = data[0] + (parseInt(data[1], 16)&0x7) + data.substr(2); // remove that parity bit for more consistent reading
	
	var otObject = {recognized: false,
					readData  : parseInt(data.substr(1,1), 16) == 0,
					writeData : parseInt(data.substr(1,1), 16) == 1,
					readAck   : parseInt(data.substr(1,1), 16) == 4,
					writeAck  : parseInt(data.substr(1,1), 16) == 5,
					otMsgId   : parseInt(data.substr(3,2), 16), 
					asFloat   : ((parseInt(data.substr(5,2))&0x80)>0?-1:1) * 
								((parseInt(data.substr(5,2), 16)&0x7F) + (parseInt(data.substr(7,2), 16))/256.0 ), 
					asUInt    : parseInt(data.substr(5,4), 16), 
					asSInt    : ((parseInt(data.substr(5,4), 16) + 0x8000) & 0xFFFF)-0x8000,
					asStatus  : "",
					data      : data};		
		
	var valstr
	var value
	
	if (otdef[otObject.otMsgId]) {
		otObject.recognized = true;
		switch (otdef[otObject.otMsgId].val) {
			case "asFloat" :
				valstr = otObject[otdef[otObject.otMsgId].val].toFixed(2);
				value  = otObject[otdef[otObject.otMsgId].val];
				break;
				
			case "asFlags":
				var tmpstr = (otObject.asUInt >>> 8).toString(2);
				valstr = ("0").repeat(8-tmpstr.length) + tmpstr + "/" ;
				tmpstr = ((otObject.asUInt >>> 0)&0xFF).toString(2);
				valstr += ("0").repeat(8-tmpstr.length) + tmpstr;
				value  = otObject["asUInt"];
				break;
				
			case "asROF": // remote override function: the TT/TC, special treatment here
				var tmpstr = (otObject.asUInt >>> 8).toString(2);
				valstr = ("0").repeat(8-tmpstr.length) + tmpstr + "/" ;
				tmpstr = ((otObject.asUInt >>> 0)&0xFF).toString(2);
				valstr += ("0").repeat(8-tmpstr.length) + tmpstr;
				otObject.asStatus = ((otObject.asUInt & 0x0300) ? (((otObject["asUInt"] & 0x0100) ? "TC" : "TT")) : "") ;
				value = valstr;
				break;
				
			default :
				valstr = otObject[otdef[otObject.otMsgId].val] + "";
				value  = otObject[otdef[otObject.otMsgId].val];
		}
		
		otObject.name = otdef[otObject.otMsgId].name;
		otObject.valstr = valstr;
		otObject.value  = value;
	} else {
		// unknown id used here, do not know how to convert
	}
	
	if ((otObject.otMsgId != 0) & (otObject.otMsgId != 1) & 
	    (otObject.readAck | otObject.writeAck) & extraDebug) { // 0 is the standard status report, bit boring
		if (otObject.recognized) {
			console.log(otObject.data.replace(/[\n\l\r]/g, "") +" ("+otObject.otMsgId+")" + " " + otObject.name + " : " + otObject.valstr);
		} else {
			console.log(otObject.data.replace(/[\n\l\r]/g, "") + " : " + JSON.stringify(otObject));
		}
	}
	
	return otObject
} // parseMessage

// instruct this program to connect to the otmonitor application
connectToOTGW();

// ---------------------- start the server for active listening ----------------
server.listen(portNumberForDomoticzToUse, function() {
   console.log('Server for Domoticz is now listening, point Domoticz OTGW Hardware to port '+ portNumberForDomoticzToUse + '.');
});

console.log('Application started.');
jake
Posts: 742
Joined: Saturday 30 May 2015 22:40
Target OS: Raspberry Pi / ODroid
Domoticz version: beta
Contact:

Re: RE: Re: OTmonitor and Domoticz parallel access to OTGW

Post by jake »

ernorv wrote:@jake yes you are right, there is a dependecy on OTGW to be already running :oops: therefore now with actual checking if the port is opened, without throwing an error, and if no connection could be made to the OTGW, simply reschedule after 10 seconds. This should be much better (also in my own setup that is :D ). Next to that I realised some minor other errors, like choosing number '11' would actually be off with 1, and actual checking if the port for Domoticz is free or not (as mentioned earlier somewhere in this thread).

And because I was enjoying it too much, I also rewrote the Opentherm parsing a bit to be more splitted into it's own function, but that's just for the coders out there I guess.

as for your supervisor parts, you could actually stop the service for a while and start manually to look at the output:
- sudo supervisorctl stop otgwNode <- or whatever you called it
- node otgw.js

and later start it again with sudo supervisorctl start otgwNode

or enable the loggins to be generated in the supervisor configuration files and use: sudo supervisorctl tail -f otgwNode


So here we go again:

....
Thanks for explaining the supervisor commands. I looked on the web for a manual on https://pypi.python.org/pypi/ordered-st ... pervisord/ in supervisor. The addition of priority = 100, 200 etc, combined with autostart false did not result in running scripts of otmonitor and otgwmod, therefore your autocheck is helpful. With the 'new' supervisor commands I stopped, replaced the code and manually restarted the script again, with the following output as a result:

Code: Select all

pi@domoticz-pi ~ $ sudo supervisorctl stop otgw_mod
otgw_mod: stopped
pi@domoticz-pi ~ $ node ./node_modules/otgwmod.js
Application started.
Server for Domoticz is now listening, point Domoticz OTGW Hardware to port 7689.
Connected to OTGW server at port number 7686!
/home/pi/node_modules/otgwmod.js:280
            valstr = ("0").repeat(8-tmpstr.length) + tmpstr + "/" ;
                           ^
TypeError: undefined is not a function
    at parseMessage (/home/pi/node_modules/otgwmod.js:280:28)
    at Socket.<anonymous> (/home/pi/node_modules/otgwmod.js:153:25)
    at Socket.emit (events.js:107:17)
    at readableAddChunk (_stream_readable.js:163:16)
    at Socket.Readable.push (_stream_readable.js:126:10)
    at TCP.onread (net.js:529:20)
I also looked in the messages in OTmonitor again and was puzzled by the every changing string from the solar storage temperature. Finally I think I understand that part, it is the read-data 'string' that is all the time the same "T001D0000". While trying to understand your code, I don't see how the MsgID=29 gets translated in a search for T001D0000, but I assume I miss a link between them, T001D0000 probably being a chain of bits that all mean something.
Last edited by jake on Tuesday 27 December 2016 11:22, edited 2 times in total.
ernorv
Posts: 17
Joined: Sunday 13 November 2016 10:42
Target OS: -
Domoticz version:
Contact:

Re: OTmonitor and Domoticz parallel access to OTGW

Post by ernorv »

@jake looks like your node version again, maybe update the node version? (google that, is what I do usually) for now, comment those lines out: place a '//' in front of it, they are there twice (two lines below the first one, somewhere around line number 288)

have a look at: http://thisdavej.com/upgrading-to-more- ... pberry-pi/ for how to change your (MAJOR) version of NodeJS

With respect to the searching: yes those Txxxxxxx do mean a lot, that is actually where the magic happens. As I found on the OTGW website, the T means that the Thermostat is sending something, and the B is the Boiler answering. Now there are requests for reads and writes going on there, that is where the first character after the T/B comes into play. This one is nasty, as there is some other info in there, a so-called parity bit, makes it difficult for humans to read. This is why I now first get rid of the parity bit. Now I can see what kind of message it is, an instruction to read or write, an acknowledge to that (normally actually containing the real information we are looking for). The (opentherm) ID itself is coded in hexadecimal by characters number 4 and 5, this is what I now store under otMsgId.
Having this ID, I have pre-coded for some messages how to decode the values, as Float / Integer etc. The list is expandable for future use of course. Id 29 is defined as a Float value ;) So now I can decode the message and provide it back to the main part of the code. The latter checks that it is an acknowledge type (either read or write) and checks whether it is the right ID. If so stores it for when domoticz asks for a new update with the PS1 thingy.
jake
Posts: 742
Joined: Saturday 30 May 2015 22:40
Target OS: Raspberry Pi / ODroid
Domoticz version: beta
Contact:

Re: RE: Re: OTmonitor and Domoticz parallel access to OTGW

Post by jake »

ernorv wrote:@jake looks like your node version again, maybe update the node version? (google that, is what I do usually) for now, comment those lines out: place a '//' in front of it, they are there twice (two lines below the first one, somewhere around line number 288)

have a look at: http://thisdavej.com/upgrading-to-more- ... pberry-pi/
With the following lines commented out:

Code: Select all

 case "asFlags":
    var tmpstr = (otObject.asUInt >>> 8).toString(2);
    valstr = ("0").repeat(8-tmpstr.length) + tmpstr + "/" ;
    tmpstr = ((otObject.asUInt >>> 0)&0xFF).toString(2);
    // valstr += ("0").repeat(8-tmpstr.length) + tmpstr;
    value  = otObject["asUInt"];
    break;
    
 case "asROF": // remote override function: the TT/TC, special treatment here
    var tmpstr = (otObject.asUInt >>> 8).toString(2);
    valstr = ("0").repeat(8-tmpstr.length) + tmpstr + "/" ;
    tmpstr = ((otObject.asUInt >>> 0)&0xFF).toString(2);
    // valstr += ("0").repeat(8-tmpstr.length) + tmpstr;
    otObject.asStatus = ((otObject.asUInt & 0x0300) ? (((otObject["asUInt"] & 0x0100) ? "TC" : "T$
    value = valstr;
    break;
I still have the following error thrown at me, which is te line I commented out, very strange:

Code: Select all

pi@domoticz-pi ~ $ node ./node_modules/otgwmod.js
Application started.
Server for Domoticz is now listening, point Domoticz OTGW Hardware to port 7689.
Connected to OTGW server at port number 7686!
/home/pi/node_modules/otgwmod.js:280
            valstr = ("0").repeat(8-tmpstr.length) + tmpstr + "/" ;
                           ^
TypeError: undefined is not a function
    at parseMessage (/home/pi/node_modules/otgwmod.js:280:28)
    at Socket.<anonymous> (/home/pi/node_modules/otgwmod.js:153:25)
    at Socket.emit (events.js:107:17)
    at readableAddChunk (_stream_readable.js:163:16)
    at Socket.Readable.push (_stream_readable.js:126:10)
    at TCP.onread (net.js:529:20)
About nodejs, I have this strange V0.12.1 version running, something I can't match at all with a current version of 7.3.0. 2 suspected reasons for this: I'm running on a RPI 1b (arm6, not arm7) and Wheezy, not Jessie. I checked the website, but commands like 'apt' are probably not meant for my old system, since that only accepts the apt-get. A sudo apt-get install nodejs tells me that I am 'up to date', although I expect that there are more up to date versions out there.

I guess, I should follow the instructions on this page, right?
ernorv
Posts: 17
Joined: Sunday 13 November 2016 10:42
Target OS: -
Domoticz version:
Contact:

Re: OTmonitor and Domoticz parallel access to OTGW

Post by ernorv »

@jake for both parts where you commented out, look also two lines above and do the same ;)

for your case you do not need these representations, they are just there fore genericity.... will think a bit how to check if the function exists....... hmmm
- edit -
v0.12.x is not strange, don't worry, just bit old. On my pi3 I use version 7.x already.

- edit - Found it, was easy actually. Now also still compatible with 0.12.x :D

Code: Select all

#!/usr/bin/node
/*
Make a small listener that Domoticz can connect to as if it was the real otmonitor application. 
Then relay the messages between OTGW and Domoticz such that otmonitor is still fully functional.

author: ernorv

v2.0 
	- rewrote the OpenTherm parts to be splitted into separate function
	- added check whether port are actually opened, if no connection to OTGW, wait ten seconds and retry 
		this can happen if starting both this script and OTGW, where OTGW needs some time
	- added check if we could open a port for Domoticz, now nicely goes out of the function without throwing an error
	- added specific author section for debugging purposes
	- fieldnumberInPsOutputToReplace was actually off with 1, so now if you set it to 11, it will be 11 according to the list below
*/

var portNumberForDomoticzToUse     = 7689;   // The new port that Domoticz needs to use. In Domoticz, set this number as the port under the OTGW hardware
var portNumberOfOtMonitor          = 7686;   // The network port OTGW provides, as can be set under the Configuration parts
const replaceTTwithTC              = true;   // If true: use TC instead of TT (if false, just standard Domoticz TT usage)

// -------------- Normally no further settings required below this line
var resetOTGW_PS_state             = true; // default true: sets the PS state back to 0 after Domoticz has its data, is what allows otmonitor to keep making nice graphs as if Domoticz was not there

// -------------- just if you really know what you do, set these
var replaceAnIdInDomoticzOutput    = false; // default: false! set this to true for replacement of the DHW Setpoing with Solar Temperature
var idToUseForReplacement          = 29;   // (Decimal) ID of the Opentherm message to use. 29 refers to the Solar Boiler Temperature
var fieldnumberInPsOutputToReplace = 11;   // which field to replace in PS output to replace, see numbers below

/*
	The PS1 output has 25 field: see http://otgw.tclcode.com/firmware.html#dataids
	
	The OTGW outputs the following fields (normally that is). I have given them below. Set the 'fieldnumberInPsOutputToReplace' to the 
	fieldnumber in accordance with what you like to be replace, given these fields (default is 16):
	
	1)  Status (MsgID=0) - Printed as two 8-bit bitfields
	2)  Control setpoint (MsgID=1) - Printed as a floating point value
	3)  Remote parameter flags (MsgID= 6) - Printed as two 8-bit bitfields
	4)  Maximum relative modulation level (MsgID=14) - Printed as a floating point value
	5)  Boiler capacity and modulation limits (MsgID=15) - Printed as two bytes
	6)  Room Setpoint (MsgID=16) - Printed as a floating point value
	7)  Relative modulation level (MsgID=17) - Printed as a floating point value
	8)  CH water pressure (MsgID=18) - Printed as a floating point value
	9)  Room temperature (MsgID=24) - Printed as a floating point value
	10) Boiler water temperature (MsgID=25) - Printed as a floating point value
	11) DHW temperature (MsgID=26) - Printed as a floating point value
	12) Outside temperature (MsgID=27) - Printed as a floating point value
	13) Return water temperature (MsgID=28) - Printed as a floating point value
	14) DHW setpoint boundaries (MsgID=48) - Printed as two bytes
	15) Max CH setpoint boundaries (MsgID=49) - Printed as two bytes
	16) DHW setpoint (MsgID=56) - Printed as a floating point value
	17) Max CH water setpoint (MsgID=57) - Printed as a floating point value
	18) Burner starts (MsgID=116) - Printed as a decimal value
	19) CH pump starts (MsgID=117) - Printed as a decimal value
	20) DHW pump/valve starts (MsgID=118) - Printed as a decimal value
	21) DHW burner starts (MsgID=119) - Printed as a decimal value
	22)	Burner operation hours (MsgID=120) - Printed as a decimal value
	23)	CH pump operation hours (MsgID=121) - Printed as a decimal value
	24) DHW pump/valve operation hours (MsgID=122) - Printed as a decimal value
	25) DHW burner operation hours (MsgID=123) - Printed as a decimal value
	
*/

// ------------- do not use, specific for author ---------------
var   extraDebug      = false;
const authorOverrides = false; // for anybody out there, keep this to false !!! 

if (authorOverrides) {
	portNumberForDomoticzToUse     = 17689;   // for test, do not interrupt normal running process, so throw it somewhere else
	portNumberOfOtMonitor          = 7686; 
	resetOTGW_PS_state             = false;   // not to interfere with the actual program already runing
    replaceAnIdInDomoticzOutput    = true;    // default: false! set this to true for replacement of the DHW Setpoing with Solar Temperature
	idToUseForReplacement          = 25;      // (Decimal) ID of the Opentherm message to use. I use 25 for test purposes
	fieldnumberInPsOutputToReplace = 11;      // which field to replace in PS output to replace, see list above
	extraDebug = true;
}


// ----------------------------------------------------------

var net = require('net');
var server 
var otgwSocket
var domSocketsArray = [];

// -------------- DEFINE SERVER LISTENING FOR CONNECTIONS FROM DOM ---------------
server = net.createServer(function (socket) {
	console.log('Incoming Domoticz Connection...');
	domSocketsArray.push(socket);
	
	socket.on('data', function (data) {
		data = data.toString(); // data may come in as Buffer
		console.log('DOM: ' + data.replace(/[\r\n]*$/, "").replace(/\r\n/g, ','));
		if (! otgwSocket.destroyed) {
			if (replaceTTwithTC) {
				otgwSocket.write(data.replace("TT=", "TC="));
			} else {
				otgwSocket.write(data);
			};
		}
	});

	socket.on('end', function() {
		console.log('end');
		domSocketsArray.splice(domSocketsArray.indexOf(socket), 1);
	});
	
	socket.on('error', function () {
		console.log('DOM: detected error on socket: probably otgwSocket died');	
		domSocketsArray.splice(domSocketsArray.indexOf(socket), 1);
	});
	
	socket.on('close', function () {
		console.log('DOM: socket closed');
		domSocketsArray.splice(domSocketsArray.indexOf(socket), 1);
	});
});

server.on('error', function(err) {
	console.log('Could not open the port ' + portNumberForDomoticzToUse + ' for Domoticz to connect to.');
	console.log('Please check whether the port is actually free to use, and adapt the "portNumberForDomoticzToUse" in the script.');
	console.log('A common source for this is that one uses the port number of the OTGW instead of a free one.')
	process.exit(1);
});


// ---------------- CONNECT TO OTGW ----------------
function relayToClients(data) {
	for (var i =0 ; i < domSocketsArray.length; i++) {
		if (! domSocketsArray[i].destroyed) {
			domSocketsArray[i].write(data);
		}
	}
}

var resetPS1 = false;
var currentReplacementValue = 0; // init this with 0

function connectToOTGW() {
	
	otgwSocket = net.createConnection({port: portNumberOfOtMonitor}, function() {
	  console.log('Connected to OTGW server at port number ' + portNumberOfOtMonitor + '!');
	});

	otgwSocket.on('data', function(data) {
		data = data.toString(); // data comes in as Buffer, we want to use it as string
		
		if (data.match(/[BTARE][0-9ABCDEF,]{8}/)) {
			// this data is not for Domoticz, it is what makes the normal otmonitor app work
			// console.log('<not relayed> OTGW: ' + data.toString().replace("\r", '').replace('\n', ''));
			
			// this data can contain a possible value for replacement purposes, inspect it.
			if (replaceAnIdInDomoticzOutput || extraDebug) {
				var otObj = parseMessage(data);
				if (replaceAnIdInDomoticzOutput && (otObj.readAck || otObj.writeAck) && (otObj.otMsgId == idToUseForReplacement)) {
					currentReplacementValue = (otObj.recognized) ? otObj.valstr : otObj.asFloat.toFixed(2);
					console.log("New replacement value found: (msgid " + otObj.otMsgId + ": '" + otObj.name + "') : " + currentReplacementValue);
				}
			}			
		} else {
			
			console.log('OTGW : ' + data.replace(/[\r\n]*$/, "").replace(/\r\n/g, ','));
			// sometimes there is a PR:,I=00 on the line which ends up in Domoticz status log. It looks like
			// malfomed data coming from the OTGW itself. Let's do some cleaning up, keeping the status log clean
			data = data.replace(/PR:[ ]*,/g, "PR: ");
			data = data.replace(/PR, /g, "PR: ");
			// a difficult one: this happens every so often, a comma missing in the output of the OTGW PS=0 output, let's repair this
			data = data.replace(/([01]{8}\/[01]{8},[0-9\.]{4,6},[01]{8}\/[01]{8})([0-9\.]{4,6}.*)/, "$1,$2");
			
			/* Now we can replace a field in the output to Domoticz with another one, as long as the one to read has also a 
			 * floating point definition (bit technical, but if you like, google for the specification of the Opentherm itself).
			 * This implies that we can only put it into a Domoticz field that also understand this floating point definition. 
			 * Most temperatures are by the way. Also the (Opentherm ID) needs to be filled in. These are both done above in the 
			 * const definition section of the script. 
			 */
			
			if (replaceAnIdInDomoticzOutput) {
				tmp = data.split(',')
				if (tmp.length == 25) { // for the PS1 output we expect 25 fields: see http://otgw.tclcode.com/firmware.html#dataids
					tmp[fieldnumberInPsOutputToReplace-1] = currentReplacementValue;
					data = tmp.join(",");
					console.log("RPLC : " + data.replace(/[\r\n]*$/, ""));
				}
			}
			relayToClients(data);
		}
		
		if (data.indexOf('PS: 1') !== -1 ) {
			// Domoticz is issueing a PS1 command, reset it if indicated by the settings above
			resetPS1 = resetOTGW_PS_state;
		};
		if (resetPS1 && (data.match(/[01]{7,9}\/[01]{7,9},.*/))) {
			// ok the data requested by domoticz has been produced, now go back to normal otmonitor operation
			otgwSocket.write('PS=0\r\n'); // no need to test for destroyed here, data just arrived from it
			resetPS1 = false;
		};
	});

	otgwSocket.on('end', function() {
		console.log('OTGW disconnected from server, reconnecting in 1 second.');
		setTimeout(function() { connectToOTGW(); }, 1000)
	});
	
	otgwSocket.on('error', function(err) {
		console.log('==> Could not connect to the OTGW at port ' + portNumberOfOtMonitor);
		console.log('==> Make sure the OTGW is running and set to provide the port as given above.');
		console.log('==> Will try to reconnect in 10 seconds, maybe OTGW is just starting up here.');
		setTimeout(function() { connectToOTGW(); }, 10000);
	});
	
	otgwSocket.on('close', function() {
		console.log('otgwSocket Close part');
	});
}

//-------------------------- function for extra debug --------------------

// apparently the String.repeat does not always exists, depending on Node version, define one if needed
if (typeof(String.prototype.repeat) == 'undefined') {
    console.log('String.repeat does not exist, adding a prototype definition.');
	String.prototype.repeat = function(n) {
		var tmpstr = "";
		for (var i=0; i < n; i++) tmpstr += this ;
		return tmpstr
	}
}


// the following is a list of already known IDs and how to convert them
// otdef[OTMSGIDasDecimal] = {'readable name', '[asFlags|asFloat|asUInt|asSInt|asFlags]'
var otdef  = {};
otdef[0]   = {name:"Master and slave status flags", val:"asFlags"}; // read ack: slave, read-data: master
otdef[5]   = {name:"Application Specific Flags", val: "asFlags"};
otdef[9]   = {name:"Remote Override Room Setpoint", val: "asFloat"};
otdef[14]  = {name:"Maximum Relative Modulation Level", val: "asFloat"};
otdef[16]  = {name:"Room Setpoint", val: "asFloat"};
otdef[17]  = {name:"Relative Modulation Level", val: "asFloat"};
otdef[18]  = {name:"Water Pressure", val: "asFloat"};
otdef[24]  = {name:"Room Temperature", val: "asFloat"};
otdef[25]  = {name:"Boiler Water Temperature", val: "asFloat"};
otdef[26]  = {name:"DHW Temperature", val: "asFloat"};
otdef[27]  = {name:"Outside Temperature", val: "asFloat"};
otdef[28]  = {name:"Return Water Temperature", val: "asFloat"};
otdef[29]  = {name:"Solar Boiler Temperature", val: "asFloat"};
otdef[56]  = {name:"DHW Setpoint", val: "asFloat"};
otdef[57]  = {name:"Max CH water Setpoint", val: "asFloat"};
otdef[100] = {name:"Remote Override Function", val: "asROF"};
otdef[120] = {name:"Burner Operation Hours", val: "asUInt"};
otdef[121] = {name:"CH Pump Operation Hours", val: "asUInt"};
otdef[122] = {name:"DHW Pump/Valve Operation Hours", val: "asUInt"};

function parseMessage(data) {
	/*
	 * this function parses those B12345678 like messages and returns the information
	 * in case it is either a read-ack or a write-ack, such to simplify the msgs as 
	 * from the OTGW side. The instruction before are not so interesting, as we would
	 * like to see the actual information in the datastreams.
	 */
	
	// byte 2 is data-id, byte 3 and 4 contain the data value
	// byte 1: parity bit, 3 bits msg type and 4 bits spare, hence mind the parity
	
	data = data.substr(0,9);  // remove extra newline stuff
	data = data[0] + (parseInt(data[1], 16)&0x7) + data.substr(2); // remove that parity bit for more consistent reading
	
	var otObject = {recognized: false,
					readData  : parseInt(data.substr(1,1), 16) == 0,
					writeData : parseInt(data.substr(1,1), 16) == 1,
					readAck   : parseInt(data.substr(1,1), 16) == 4,
					writeAck  : parseInt(data.substr(1,1), 16) == 5,
					otMsgId   : parseInt(data.substr(3,2), 16), 
					asFloat   : ((parseInt(data.substr(5,2))&0x80)>0?-1:1) * 
								((parseInt(data.substr(5,2), 16)&0x7F) + (parseInt(data.substr(7,2), 16))/256.0 ), 
					asUInt    : parseInt(data.substr(5,4), 16), 
					asSInt    : ((parseInt(data.substr(5,4), 16) + 0x8000) & 0xFFFF)-0x8000,
					asStatus  : "",
					data      : data};		
		
	var valstr
	var value
	
	if (otdef[otObject.otMsgId]) {
		otObject.recognized = true;
		switch (otdef[otObject.otMsgId].val) {
			case "asFloat" :
				valstr = otObject[otdef[otObject.otMsgId].val].toFixed(2);
				value  = otObject[otdef[otObject.otMsgId].val];
				break;
				
			case "asFlags":
				var tmpstr = (otObject.asUInt >>> 8).toString(2);
				valstr = ("0").repeat(8-tmpstr.length) + tmpstr + "/" ;
				tmpstr = ((otObject.asUInt >>> 0)&0xFF).toString(2);
				valstr += ("0").repeat(8-tmpstr.length) + tmpstr;
				value  = otObject["asUInt"];
				break;
				
			case "asROF": // remote override function: the TT/TC, special treatment here
				var tmpstr = (otObject.asUInt >>> 8).toString(2);
				valstr = ("0").repeat(8-tmpstr.length) + tmpstr + "/" ;
				tmpstr = ((otObject.asUInt >>> 0)&0xFF).toString(2);
				valstr += ("0").repeat(8-tmpstr.length) + tmpstr;
				otObject.asStatus = ((otObject.asUInt & 0x0300) ? (((otObject["asUInt"] & 0x0100) ? "TC" : "TT")) : "") ;
				value = valstr;
				break;
				
			default :
				valstr = otObject[otdef[otObject.otMsgId].val] + "";
				value  = otObject[otdef[otObject.otMsgId].val];
		}
		
		otObject.name = otdef[otObject.otMsgId].name;
		otObject.valstr = valstr;
		otObject.value  = value;
	} else {
		// unknown id used here, do not know how to convert
	}
	
	if ((otObject.otMsgId != 1) & (otObject.otMsgId != 1) & 
	    (otObject.readAck | otObject.writeAck) & extraDebug) { // 0 is the standard status report, bit boring
		if (otObject.recognized) {
			console.log(otObject.data.replace(/[\n\l\r]/g, "") +" ("+otObject.otMsgId+")" + " " + otObject.name + " : " + otObject.valstr);
		} else {
			console.log(otObject.data.replace(/[\n\l\r]/g, "") + " : " + JSON.stringify(otObject));
		}
	}
	
	return otObject
} // parseMessage

// instruct this program to connect to the otmonitor application
connectToOTGW();

// ---------------------- start the server for active listening ----------------
server.listen(portNumberForDomoticzToUse, function() {
   console.log('Server for Domoticz is now listening, point Domoticz OTGW Hardware to port '+ portNumberForDomoticzToUse + '.');
});

console.log('Application started.');
Last edited by ernorv on Wednesday 28 December 2016 19:36, edited 1 time in total.
jake
Posts: 742
Joined: Saturday 30 May 2015 22:40
Target OS: Raspberry Pi / ODroid
Domoticz version: beta
Contact:

Re: OTmonitor and Domoticz parallel access to OTGW

Post by jake »

ernorv wrote:@jake for both parts where you commented out, look also two lines above and do the same ;)

for your case you do not need these representations, they are just there fore genericity.... will think a bit how to check if the function exists....... hmmm
- edit -
v0.12.x is not strange, don't worry, just bit old. On my pi3 I use version 7.x already.

- edit - Found it, was easy actually. Now also still compatible with 0.12.x :D
...
It's hard to believe, but it actually runs :lol:

I had to wait for 15 minutes to see the solar boiler value updating in Domoticz, since I already knew that this value only comes around every 15-20 minutes in the OT-log, but now it is working as expected.

Well done and thanks a lot!

Although not important anymore, I installed the 4.00 version of Node as explained. However, 'node -v' still gives me the V0.12 instead. What should I do to make the version 4 the actual one?
ernorv
Posts: 17
Joined: Sunday 13 November 2016 10:42
Target OS: -
Domoticz version:
Contact:

Re: OTmonitor and Domoticz parallel access to OTGW

Post by ernorv »

@jake Nice!

wrt the two versions of Node, no clue, guess you need a rapsberry/debian expert for that.
Heelderpeel
Posts: 15
Joined: Sunday 26 April 2015 21:56
Target OS: Raspberry Pi / ODroid
Domoticz version: V2020.2
Location: Nederland
Contact:

Re: OTmonitor and Domoticz parallel access to OTGW

Post by Heelderpeel »

[quote="ernorv"]@heelderpeel have a look at the updated code, should be a lot user friendlier without that kind of errors.

After a lot of trouble with de OTGW hardware, today I was able to run the script form ERNORV for the first time, it works perfect.
Super thanks for that. :D
And now I can see the solar temperature verry verry nice.

But, how can I make it autostarting / running? (supervisor)
I tried:

[Service]
ExecStart=/home/pi/otgw/otmonitor_full.js
Restart=always
User=nobody
Group=nobody
Environment=PATH=/usr/bin:/usr/local/bin
Environment=NODE_ENV=production
WorkingDirectory=/home/pi/otgw
:?
In a new file.

Or is there a possibility to do it in "monit"
ernorv
Posts: 17
Joined: Sunday 13 November 2016 10:42
Target OS: -
Domoticz version:
Contact:

Re: OTmonitor and Domoticz parallel access to OTGW

Post by ernorv »

@heelderpeel

Not being an expert in Supervisor, I do start it with that one.

I created in /etc/supervisor/conf.d a file caller otgwNode.conf, with the contents as below. It works fine for me (note: take care of the path and file name of the program, just however you've named it)

Code: Select all

pi@raspberrypi:/etc/supervisor/conf.d $ cat otgwNode.conf
[program:otgwNode]
command = /usr/bin/node /home/pi/node/domOtgwSub.js
directory = /home/pi/node
autostart = true
autorestart = true
stderr_logfile = NONE
stdout_logfile = NONE
I think the important difference is already in the command part, where I first mention the path to node itself, and then the path to the script.

hope this helps you
Heelderpeel
Posts: 15
Joined: Sunday 26 April 2015 21:56
Target OS: Raspberry Pi / ODroid
Domoticz version: V2020.2
Location: Nederland
Contact:

Re: OTmonitor and Domoticz parallel access to OTGW

Post by Heelderpeel »

Unfortunately that does not work for me :(


i tried your script whit en whitout te first line.

[program:otgwNODE]
command=/usr/bin/node /home/pi/otgw/otgwNODE.js
directory=/home/pi/otgw
autostart=true
autorestart=true
stderr_logfile=NONE
stdout_logfile=NONE

I always get: otgwNODE.conf: ERROR (no such process)

When i type in putty: node otgwNODE.js the script runs perfectly.
ernorv
Posts: 17
Joined: Sunday 13 November 2016 10:42
Target OS: -
Domoticz version:
Contact:

Re: OTmonitor and Domoticz parallel access to OTGW

Post by ernorv »

@heelderpeel
not an expert, but let's see what we can do:
When you are in putty, and typing 'node otgwNODE.js' works fine for you, can you show me the output of the following commands:

pwd
ls -als
which node
Heelderpeel
Posts: 15
Joined: Sunday 26 April 2015 21:56
Target OS: Raspberry Pi / ODroid
Domoticz version: V2020.2
Location: Nederland
Contact:

Re: OTmonitor and Domoticz parallel access to OTGW

Post by Heelderpeel »

Solved :)

I think Supervisor had some trouble with mij temp files (otgwNODEoud.conf and otgwNODEoud2.conf etc.)

Now i used

[program:otgwNODE]
command=/usr/bin/node /home/pi/otgw/otgwNODE.js
directory=/home/pi/otgw
autostart=true
autorestart=true
startretries=10
stderr_logfile=NONE
stdout_logfile=NONE

I'am so happy.
so, now I can got to bed.

ERNORV thnx for your help.
jake
Posts: 742
Joined: Saturday 30 May 2015 22:40
Target OS: Raspberry Pi / ODroid
Domoticz version: beta
Contact:

Re: RE: Re: OTmonitor and Domoticz parallel access to OTGW

Post by jake »

ernorv wrote:@jake Nice!
After the successful implementation of the solar boiler temperature in Domoticz, I thought I was al set to start controlling the DHW Setpoint (MsgID 56), based on the solar boiler temperature. However, the PS=1 string is not updated with a new DHW setpoint, although both the Write-Data and Write_Ack can be found in the OT monitor monitoring page. The new DHW setpoint is also correctly displayed on the status and summary page.

In my humble opinion it is a bug and I therefore started a new topic on the domotica forum. I am happy to see a reply from the designer of the OTGW, it is great to have such support. Unfortunately I don't fully understand the answer, but...

if, if it turns out that nothing can be done from the OTGW side of things, is it possible that your excellent script can help out in this matter as well? In worst case, a forced override of the Msg ID value with the SR command? (I might be completely wrong in my ideas for what I would like to accomplish)

Update:
according the reply on the domoticaforum.eu, the behaviour of PS=1 is normal and I will have to work around the iSense myself:
hvxl wrote:As I explained before, the gateway stores the values from Write-Data messages received from the thermostat and Read-Ack messages received from the boiler. As a general rule that provides the best results. Normally people want to know the values the attached equipment produce, not the values that the gateway was instructed to send. Those are already known. I can't cater for one user who wants something different for one specific message. The gateway doesn't have RMMADWIM functionality. If you need customized firmware, the sources are available for you to modify to your taste.
I did a check with the otgwmod script and instead of Msg ID=29 on position 11, I changed it to MsgID=56 on position 16. Bingo! With that change Domoticz will keep showing the set temperature after doing an override. This is absolutely necessary, in order to have some programming around this setpoint.

Therefore..... is it possible that I can have 2 (for now :lol: ) parameters altered in the PS=1 string, both 29 on pos 11 and 56 on pos 16?
ernorv
Posts: 17
Joined: Sunday 13 November 2016 10:42
Target OS: -
Domoticz version:
Contact:

Re: OTmonitor and Domoticz parallel access to OTGW

Post by ernorv »

@jake

Voila

Code: Select all

#!/usr/bin/node
/*
Make a small listener that Domoticz can connect to as if it was the real otmonitor application. 
Then relay the messages between OTGW and Domoticz such that otmonitor is still fully functional.

author: ernorv

v2.0 
	- rewrote the OpenTherm parts to be splitted into separate function
	- added check whether port are actually opened, if no connection to OTGW, wait ten seconds and retry 
		this can happen if starting both this script and OTGW, where OTGW needs some time
	- added check if we could open a port for Domoticz, now nicely goes out of the function without throwing an error
	- added specific author section for debugging purposes
	- fieldnumberInPsOutputToReplace was actually off with 1, so now if you set it to 11, it will be 11 according to the list below
	- for older node versions, plug in the repeat function for string.
	
V2.1
	- add possibility to replace multiple values
*/

var portNumberForDomoticzToUse     = 7689;   // The new port that Domoticz needs to use. In Domoticz, set this number as the port under the OTGW hardware
var portNumberOfOtMonitor          = 7686;   // The network port OTGW provides, as can be set under the Configuration parts
const replaceTTwithTC              = true;   // If true: use TC instead of TT (if false, just standard Domoticz TT usage)

// -------------- Normally no further settings required below this line
var resetOTGW_PS_state             = true; // default true: sets the PS state back to 0 after Domoticz has its data, is what allows otmonitor to keep making nice graphs as if Domoticz was not there

// -------------- just if you really know what you do, set these
var replaceAnIdInDomoticzOutput    = true; // default: false! set this to true for replacement of the DHW Setpoing with Solar Temperature

/* 	Definition of what to replace and where. This is now a list, to allow multiple replacements 
	idToUseForReplacement          (example:  29)   (Decimal) ID of the Opentherm message to use. 29 refers to the Solar Boiler Temperature
	fieldnumberInPsOutputToReplace (example: 11)    which field to replace in PS output to replace, see numbers below

	example single field:
	replaceDefinition = [ {fieldnumberInPsOutputToReplace : 11, idToUseForReplacement : 25} ];
	
	example two fields:
	replaceDefinition = [
		{fieldnumberInPsOutputToReplace : 11, idToUseForReplacement : 25},
		{fieldnumberInPsOutputToReplace : 12, idToUseForReplacement : 24}
	];
	
	(mind the ',' required when adding a definition to the list)
*/

var replaceDefinition = [
	{fieldnumberInPsOutputToReplace:11, idToUseForReplacement:29}
];


/*
	The PS1 output has 25 field: see http://otgw.tclcode.com/firmware.html#dataids
	
	The OTGW outputs the following fields (normally that is). I have given them below. Set the 'fieldnumberInPsOutputToReplace' to the 
	fieldnumber in accordance with what you like to be replace, given these fields (default is 16):
	
	1)  Status (MsgID=0) - Printed as two 8-bit bitfields
	2)  Control setpoint (MsgID=1) - Printed as a floating point value
	3)  Remote parameter flags (MsgID= 6) - Printed as two 8-bit bitfields
	4)  Maximum relative modulation level (MsgID=14) - Printed as a floating point value
	5)  Boiler capacity and modulation limits (MsgID=15) - Printed as two bytes
	6)  Room Setpoint (MsgID=16) - Printed as a floating point value
	7)  Relative modulation level (MsgID=17) - Printed as a floating point value
	8)  CH water pressure (MsgID=18) - Printed as a floating point value
	9)  Room temperature (MsgID=24) - Printed as a floating point value
	10) Boiler water temperature (MsgID=25) - Printed as a floating point value
	11) DHW temperature (MsgID=26) - Printed as a floating point value
	12) Outside temperature (MsgID=27) - Printed as a floating point value
	13) Return water temperature (MsgID=28) - Printed as a floating point value
	14) DHW setpoint boundaries (MsgID=48) - Printed as two bytes
	15) Max CH setpoint boundaries (MsgID=49) - Printed as two bytes
	16) DHW setpoint (MsgID=56) - Printed as a floating point value
	17) Max CH water setpoint (MsgID=57) - Printed as a floating point value
	18) Burner starts (MsgID=116) - Printed as a decimal value
	19) CH pump starts (MsgID=117) - Printed as a decimal value
	20) DHW pump/valve starts (MsgID=118) - Printed as a decimal value
	21) DHW burner starts (MsgID=119) - Printed as a decimal value
	22)	Burner operation hours (MsgID=120) - Printed as a decimal value
	23)	CH pump operation hours (MsgID=121) - Printed as a decimal value
	24) DHW pump/valve operation hours (MsgID=122) - Printed as a decimal value
	25) DHW burner operation hours (MsgID=123) - Printed as a decimal value
	
*/

// ------------- do not use, specific for author ---------------
var   extraDebug      = false;
const authorOverrides = false; // for anybody out there, keep this to false !!! 

if (authorOverrides) {
	portNumberForDomoticzToUse     = 17689;   // for test, do not interrupt normal running process, so throw it somewhere else
	portNumberOfOtMonitor          = 7686; 
	resetOTGW_PS_state             = false;   // not to interfere with the actual program already runing
    replaceAnIdInDomoticzOutput    = true;    // default: false! set this to true for replacement of the DHW Setpoing with Solar Temperature
	extraDebug = true;
	
	replaceDefinition = [
		{fieldnumberInPsOutputToReplace : 11, idToUseForReplacement : 25},
		{fieldnumberInPsOutputToReplace : 12, idToUseForReplacement : 24}
	];
} // authorOverrides


// ----------------------------------------------------------

var net = require('net');
var server 
var otgwSocket
var domSocketsArray = [];

// -------------- some init stuff to do ------------------------------------------

for (var k=0; k<replaceDefinition.length; k++) {
	replaceDefinition[k].currentReplacementValue = 0; // init with a value to start with until seen for the first time
}

// -------------- DEFINE SERVER LISTENING FOR CONNECTIONS FROM DOM ---------------
server = net.createServer(function (socket) {
	console.log('Incoming Domoticz Connection...');
	domSocketsArray.push(socket);
	
	socket.on('data', function (data) {
		data = data.toString(); // data may come in as Buffer
		console.log('DOM: ' + data.replace(/[\r\n]*$/, "").replace(/\r\n/g, ','));
		if (! otgwSocket.destroyed) {
			if (replaceTTwithTC) {
				otgwSocket.write(data.replace("TT=", "TC="));
			} else {
				otgwSocket.write(data);
			};
		}
	});

	socket.on('end', function() {
		console.log('end');
		domSocketsArray.splice(domSocketsArray.indexOf(socket), 1);
	});
	
	socket.on('error', function () {
		console.log('DOM: detected error on socket: probably otgwSocket died');	
		domSocketsArray.splice(domSocketsArray.indexOf(socket), 1);
	});
	
	socket.on('close', function () {
		console.log('DOM: socket closed');
		domSocketsArray.splice(domSocketsArray.indexOf(socket), 1);
	});
});

server.on('error', function(err) {
	console.log('Could not open the port ' + portNumberForDomoticzToUse + ' for Domoticz to connect to.');
	console.log('Please check whether the port is actually free to use, and adapt the "portNumberForDomoticzToUse" in the script.');
	console.log('A common source for this is that one uses the port number of the OTGW instead of a free one.')
	process.exit(1);
});


// ---------------- CONNECT TO OTGW ----------------
function relayToClients(data) {
	for (var i =0 ; i < domSocketsArray.length; i++) {
		if (! domSocketsArray[i].destroyed) {
			domSocketsArray[i].write(data);
		}
	}
}

var resetPS1 = false;
var currentReplacementValue = 0; // init this with 0

function connectToOTGW() {
	
	otgwSocket = net.createConnection({port: portNumberOfOtMonitor}, function() {
	  console.log('Connected to OTGW server at port number ' + portNumberOfOtMonitor + '!');
	});

	otgwSocket.on('data', function(data) {
		data = data.toString(); // data comes in as Buffer, we want to use it as string
		
		if (data.match(/[BTARE][0-9ABCDEF,]{8}/)) {
			// this data is not for Domoticz, it is what makes the normal otmonitor app work
			// console.log('<not relayed> OTGW: ' + data.toString().replace("\r", '').replace('\n', ''));
			
			// this data can contain a possible value for replacement purposes, inspect it.
			if (replaceAnIdInDomoticzOutput || extraDebug) {
				var otObj = parseMessage(data);
				if (replaceAnIdInDomoticzOutput && (otObj.readAck || otObj.writeAck)) {
					for (var k=0; k < replaceDefinition.length; k++) {
						if (otObj.otMsgId == replaceDefinition[k].idToUseForReplacement) {
							replaceDefinition[k].currentReplacementValue = (otObj.recognized) ? otObj.valstr : otObj.asFloat.toFixed(2);
							console.log("New replacement value found: (msgid " + otObj.otMsgId + ": '" + otObj.name + "') : " + replaceDefinition[k].currentReplacementValue);
						}
					}
				}
			}			
		} else {
			
			console.log('OTGW : ' + data.replace(/[\r\n]*$/, "").replace(/\r\n/g, ','));
			// sometimes there is a PR:,I=00 on the line which ends up in Domoticz status log. It looks like
			// malfomed data coming from the OTGW itself. Let's do some cleaning up, keeping the status log clean
			data = data.replace(/PR:[ ]*,/g, "PR: ");
			data = data.replace(/PR, /g, "PR: ");
			// a difficult one: this happens every so often, a comma missing in the output of the OTGW PS=0 output, let's repair this
			data = data.replace(/([01]{8}\/[01]{8},[0-9\.]{4,6},[01]{8}\/[01]{8})([0-9\.]{4,6}.*)/, "$1,$2");
			
			/* Now we can replace a field in the output to Domoticz with another one, as long as the one to read has also a 
			 * floating point definition (bit technical, but if you like, google for the specification of the Opentherm itself).
			 * This implies that we can only put it into a Domoticz field that also understand this floating point definition. 
			 * Most temperatures are by the way. Also the (Opentherm ID) needs to be filled in. These are both done above in the 
			 * const definition section of the script. 
			 */
			
			if (replaceAnIdInDomoticzOutput) {
				tmp = data.split(',')
				if (tmp.length == 25) { // for the PS1 output we expect 25 fields: see http://otgw.tclcode.com/firmware.html#dataids
					
					for (var k=0; k<replaceDefinition.length; k++) {
						tmp[replaceDefinition[k].fieldnumberInPsOutputToReplace-1] = replaceDefinition[k].currentReplacementValue;
					}
					
					data = tmp.join(",");
					console.log("RPLC : " + data.replace(/[\r\n]*$/, ""));
				}
			}
			relayToClients(data);
		}
		
		if (data.indexOf('PS: 1') !== -1 ) {
			// Domoticz is issueing a PS1 command, reset it if indicated by the settings above
			resetPS1 = resetOTGW_PS_state;
		};
		if (resetPS1 && (data.match(/[01]{7,9}\/[01]{7,9},.*/))) {
			// ok the data requested by domoticz has been produced, now go back to normal otmonitor operation
			otgwSocket.write('PS=0\r\n'); // no need to test for destroyed here, data just arrived from it
			resetPS1 = false;
		};
	});

	otgwSocket.on('end', function() {
		console.log('OTGW disconnected from server, reconnecting in 1 second.');
		setTimeout(function() { connectToOTGW(); }, 1000)
	});
	
	otgwSocket.on('error', function(err) {
		console.log('==> Could not connect to the OTGW at port ' + portNumberOfOtMonitor);
		console.log('==> Make sure the OTGW is running and set to provide the port as given above.');
		console.log('==> Will try to reconnect in 10 seconds, maybe OTGW is just starting up here.');
		setTimeout(function() { connectToOTGW(); }, 10000);
	});
	
	otgwSocket.on('close', function() {
		console.log('otgwSocket Close part');
	});
}

//-------------------------- function for extra debug --------------------

// apparently the String.repeat does not always exists, depending on Node version, define one if needed
if (typeof(String.prototype.repeat) == 'undefined') {
    console.log('String.repeat does not exist, adding a prototype definition (normal for older NodeJS versions).');
	String.prototype.repeat = function(n) {
		var tmpstr = "";
		for (var i=0; i < n; i++) tmpstr += this ;
		return tmpstr
	}
}


// the following is a list of already known IDs and how to convert them
// otdef[OTMSGIDasDecimal] = {'readable name', '[asFlags|asFloat|asUInt|asSInt|asFlags]'
var otdef  = {};
otdef[0]   = {name:"Master and slave status flags", val:"asFlags"}; // read ack: slave, read-data: master
otdef[5]   = {name:"Application Specific Flags", val: "asFlags"};
otdef[9]   = {name:"Remote Override Room Setpoint", val: "asFloat"};
otdef[11]  = {name: "TSP Setting", val: "asTSP"}
otdef[14]  = {name:"Maximum Relative Modulation Level", val: "asFloat"};
otdef[16]  = {name:"Room Setpoint", val: "asFloat"};
otdef[17]  = {name:"Relative Modulation Level", val: "asFloat"};
otdef[18]  = {name:"Water Pressure", val: "asFloat"};
otdef[24]  = {name:"Room Temperature", val: "asFloat"};
otdef[25]  = {name:"Boiler Water Temperature", val: "asFloat"};
otdef[26]  = {name:"DHW Temperature", val: "asFloat"};
otdef[27]  = {name:"Outside Temperature", val: "asFloat"};
otdef[28]  = {name:"Return Water Temperature", val: "asFloat"};
otdef[29]  = {name:"Solar Boiler Temperature", val: "asFloat"};
otdef[56]  = {name:"DHW Setpoint", val: "asFloat"};
otdef[57]  = {name:"Max CH water Setpoint", val: "asFloat"};
otdef[100] = {name:"Remote Override Function", val: "asROF"};
otdef[120] = {name:"Burner Operation Hours", val: "asUInt"};
otdef[121] = {name:"CH Pump Operation Hours", val: "asUInt"};
otdef[122] = {name:"DHW Pump/Valve Operation Hours", val: "asUInt"};

function parseMessage(data) {
	/*
	 * this function parses those B12345678 like messages and returns the information
	 * in case it is either a read-ack or a write-ack, such to simplify the msgs as 
	 * from the OTGW side. The instruction before are not so interesting, as we would
	 * like to see the actual information in the datastreams.
	 */
	
	// byte 2 is data-id, byte 3 and 4 contain the data value
	// byte 1: parity bit, 3 bits msg type and 4 bits spare, hence mind the parity
	
	data = data.substr(0,9);  // remove extra newline stuff
	data = data[0] + (parseInt(data[1], 16)&0x7) + data.substr(2); // remove that parity bit for more consistent reading
	
	var otObject = {recognized: false,
					readData  : parseInt(data.substr(1,1), 16) == 0,
					writeData : parseInt(data.substr(1,1), 16) == 1,
					readAck   : parseInt(data.substr(1,1), 16) == 4,
					writeAck  : parseInt(data.substr(1,1), 16) == 5,
					otMsgId   : parseInt(data.substr(3,2), 16), 
					asFloat   : ((parseInt(data.substr(5,2))&0x80)>0?-1:1) * 
								((parseInt(data.substr(5,2), 16)&0x7F) + (parseInt(data.substr(7,2), 16))/256.0 ), 
					asUInt    : parseInt(data.substr(5,4), 16), 
					asSInt    : ((parseInt(data.substr(5,4), 16) + 0x8000) & 0xFFFF)-0x8000,
					asStatus  : "",
					data      : data};		
		
	var valstr
	var value
	
	if (otdef[otObject.otMsgId]) {
		otObject.recognized = true;
		switch (otdef[otObject.otMsgId].val) {
			case "asFloat" :
				valstr = otObject[otdef[otObject.otMsgId].val].toFixed(2);
				value  = otObject[otdef[otObject.otMsgId].val];
				break;
				
			case "asFlags":
				var tmpstr = (otObject.asUInt >>> 8).toString(2);
				valstr = ("0").repeat(8-tmpstr.length) + tmpstr + "/" ;
				tmpstr = ((otObject.asUInt >>> 0)&0xFF).toString(2);
				valstr += ("0").repeat(8-tmpstr.length) + tmpstr;
				value  = otObject["asUInt"];
				break;
				
			case "asROF": // remote override function: the TT/TC, special treatment here
				var tmpstr = (otObject.asUInt >>> 8).toString(2);
				valstr = ("0").repeat(8-tmpstr.length) + tmpstr + "/" ;
				tmpstr = ((otObject.asUInt >>> 0)&0xFF).toString(2);
				valstr += ("0").repeat(8-tmpstr.length) + tmpstr;
				otObject.asStatus = ((otObject.asUInt & 0x0300) ? (((otObject["asUInt"] & 0x0100) ? "TC" : "TT")) : "") ;
				value = valstr;
				break;
			case "asTSP": // special: just print it
				var hb = (otObject.asUInt >>> 8);
				var lb = ((otObject.asUInt >>> 0)&0xFF);
				valstr = "Parameter " + (hb + 1) + ": " + lb ;
				value = otObject.asUInt;
				//console.log("====>  Parameter " + (hb + 1) + ": " + lb );
				break;
			default :
				valstr = otObject[otdef[otObject.otMsgId].val] + "";
				value  = otObject[otdef[otObject.otMsgId].val];
		}
		
		otObject.name = otdef[otObject.otMsgId].name;
		otObject.valstr = valstr;
		otObject.value  = value;
	} else {
		// unknown id used here, do not know how to convert
	}
	
	if ((otObject.otMsgId != 0) & (otObject.otMsgId != 1) & 
	    (otObject.readAck | otObject.writeAck) & extraDebug) { // 0 is the standard status report, bit boring
		if (otObject.recognized) {
			console.log(otObject.data.replace(/[\n\l\r]/g, "") +" ("+otObject.otMsgId+")" + " " + otObject.name + " : " + otObject.valstr);
		} else {
			console.log(otObject.data.replace(/[\n\l\r]/g, "") + " : " + JSON.stringify(otObject));
		}
	}
	
	return otObject
} // parseMessage

// instruct this program to connect to the otmonitor application
connectToOTGW();

// ---------------------- start the server for active listening ----------------
server.listen(portNumberForDomoticzToUse, function() {
   console.log('Server for Domoticz is now listening, point Domoticz OTGW Hardware to port '+ portNumberForDomoticzToUse + '.');
});

console.log('Application started.');

Last edited by ernorv on Tuesday 07 February 2017 5:59, edited 1 time in total.
jake
Posts: 742
Joined: Saturday 30 May 2015 22:40
Target OS: Raspberry Pi / ODroid
Domoticz version: beta
Contact:

Re: OTmonitor and Domoticz parallel access to OTGW

Post by jake »

ernorv wrote:@jake

Voila
Thanks a bunch, that was a nice 'Thursday morning present'. I will install the file later on, but reading through the code, I had one question about the following lines:

Code: Select all

// ------------- do not use, specific for author ---------------
var   extraDebug      = false;
const authorOverrides = true; // for anybody out there, keep this to false !!!
Should this const authorOverrides not be 'false' as the comment itself states?

UPDATE:
I decided to take the 'risk' of putting it to false and it seems to work fine.
The new code works exactly as it should do, thanks for that!

However, although the new DHW setpoint was updated successfully, the Domoticz value stayed correct for 10 minutes before it returned again to the Thermostat value as is displayed in the standard PS=1 as well. It has not to do with the code, because exchanging DHW Setpoint with CH Setpoint, the new value remains correct all the time).
I cut and past a piece of the OTMonitor log and filtered on 'DHW Setpoint'

Code: Select all

21:12:32.413237  R90383800  Write-Data  DHW setpoint: 56.00
21:12:32.621121  B50383800  Write-Ack   DHW setpoint: 56.00
21:14:02.491461  T00300000  Read-Data   DHW setpoint boundaries: 0 0
21:14:02.672634  BC0304B14  Read-Ack    DHW setpoint boundaries: 75 20
21:15:23.588456  T90383B00  Write-Data  DHW setpoint: 59.00
21:15:23.598304  R90383800  Write-Data  DHW setpoint: 56.00
21:25:24.133634  T00300000  Read-Data   DHW setpoint boundaries: 0 0
21:25:24.313342  BC0304B14  Read-Ack    DHW setpoint boundaries: 75 20
21:26:44.211802  T90383B00  Write-Data  DHW setpoint: 59.00
21:26:44.222696  R90383800  Write-Data  DHW setpoint: 56.00
21:26:44.396908  B50383800  Write-Ack   DHW setpoint: 56.00
21:26:44.410144  A50383B00  Write-Ack   DHW setpoint: 59.00
On 21:12 I updated DHW Setpoint through Domoticz from 59 to 56'C
Around 21:25 the value within Domoticz was changed back to 59'C, while the OTMonitor Summary and Status page still show the 56'C after half an hour (21:42)
It seems that both T90383B00 (59) and R90383800 (56) write data and that 'T' acknowledges through A50383B00 (59) and 'R' through B50383800 (56)
User avatar
sincze
Posts: 1299
Joined: Monday 02 June 2014 22:46
Target OS: Raspberry Pi / ODroid
Domoticz version: 2024.4
Location: Netherlands / Breda Area
Contact:

Re: OTmonitor and Domoticz parallel access to OTGW

Post by sincze »

Finally had some time to set this up:
OTGW_nodejs.jpg
OTGW_nodejs.jpg (367.47 KiB) Viewed 6773 times
Works excellent with the instructions provided in this thread. I use the node file with version v2.0.0.

Unfortunately It did not yet help me resolve the issue where otmonitor says:

Code: Select all

Opentherm Gateway 4.2.5
WDT reset!
Only a powercycle brings the OTGW back to live, as I did not implement the reset button. I could use Mysensors to toggle the reset button ofcourse... now I just powercycle using a KaKu plug... Well in the end a reset should not be necessary. Just trying to figure out why this happens.
Pass2php
LAN: RFLink, P1, OTGW, MySensors
USB: RFXCom, ZWave, Sonoff 3
MQTT: ZIgbee2MQTT,
ZWAVE: Zwave-JS-UI
WIFI: Mi-light, Tasmota, Xiaomi Shelly
Solar: Omnik, PVOutput
Video: Kodi, Harmony HUB, Chromecast
Sensors: You name it I got 1.
User avatar
sincze
Posts: 1299
Joined: Monday 02 June 2014 22:46
Target OS: Raspberry Pi / ODroid
Domoticz version: 2024.4
Location: Netherlands / Breda Area
Contact:

Re: OTmonitor and Domoticz parallel access to OTGW

Post by sincze »

sincze wrote:Finally had some time to set this up:
OTGW_nodejs.jpg
Works excellent with the instructions provided in this thread. I use the node file with version v2.0.0.

Unfortunately It did not yet help me resolve the issue where otmonitor says:

Code: Select all

Opentherm Gateway 4.2.5
WDT reset!
Only a powercycle brings the OTGW back to live, as I did not implement the reset button. I could use Mysensors to toggle the reset button ofcourse... now I just powercycle using a KaKu plug... Well in the end a reset should not be necessary. Just trying to figure out why this happens.
In the meantime I implemented the reset button. As a result the OTGW is now automatically being reset. I have on average 7 resets a day. Completely random during the day. Image

Sent from my SM-G925F using Tapatalk

Pass2php
LAN: RFLink, P1, OTGW, MySensors
USB: RFXCom, ZWave, Sonoff 3
MQTT: ZIgbee2MQTT,
ZWAVE: Zwave-JS-UI
WIFI: Mi-light, Tasmota, Xiaomi Shelly
Solar: Omnik, PVOutput
Video: Kodi, Harmony HUB, Chromecast
Sensors: You name it I got 1.
w1ll14m
Posts: 3
Joined: Tuesday 09 February 2016 16:19
Target OS: Linux
Domoticz version: v4.9xxx
Location: NL
Contact:

Re: OTmonitor and Domoticz parallel access to OTGW

Post by w1ll14m »

Hi!

I'm note sure wether or not this is usefull for other Domoticz/OTGW users.

My situation:
I run OTGW and i like to use some features of OTmonitor but at the same time i want to have control of the thermostat using Domoticz. I run otmonitor as daemon on Linux and patched the OTGWBase.cpp to re-enable PS=0 after the Domoticz plugin received the PS=1 information so that OTMonitor remains functional. This generates some extra logging but besides that it works quite nicely. This is probably not the best way to handle this but this has helped me for more than a year now and I'm able to use both otmonitor and Domoticz parallel. I hope it helps others with the same problems ;)

usleep is required so that the serial data is received before sending data, lowering this value might cause missing data.

Code: Select all

diff --git a/hardware/OTGWBase.cpp b/hardware/OTGWBase.cpp
index 7fd1dc7..e119efe 100644
--- a/hardware/OTGWBase.cpp
+++ b/hardware/OTGWBase.cpp
@@ -249,6 +249,9 @@ void OTGWBase::GetGatewayDetails()
 	WriteInt((const unsigned char*)&szCmd, (const unsigned char)strlen(szCmd));
 	strcpy(szCmd, "PS=1\r\n");
 	WriteInt((const unsigned char*)&szCmd, (const unsigned char)strlen(szCmd));
+	usleep(500000);
+	strcpy(szCmd, "PS=0\r\n");
+	WriteInt((const unsigned char*)&szCmd, (const unsigned char)strlen(szCmd));
 }
 
 void OTGWBase::SendTime()
Post Reply

Who is online

Users browsing this forum: No registered users and 0 guests