Decoder for NKE TIC sensor

Hello,
I just buy a sensor from NKE.
http://support.nke-watteco.com/tic/
but… no decoder in javascript is available! So need to write one.
Maybe someone have already done this work? and could share it here?
http://support.nke-watteco.com/wp-content/uploads/2019/09/50-70-045_TIC_PMEPMI_User_Guide_1.4.pdf
http://support.nke-watteco.com/wp-content/uploads/2019/09/TIC_Application_Layer_Description_1.1.pdf

thanks in advance if someone share it !

Hi @kelu28, there appears to be a JavaScript decoder page on their website.

http://support.nke-watteco.com/wp-content/uploads/2017/05/decodeZCL_svn5031.js

I don’t know if this is correct for your sensor.

No, this decoder have not all the sensors. Only those befor 05/2017.

Answer to me! but not finished

/*
* Code pour decoder le TIC du compteur PMEPMI
*/

// ----------------------------------------------------------------
// ----------------------- FUNCTIONS PART -------------------------
// ----------------------------------------------------------------

function UintToInt(Uint, Size) {
	if (Size === 2) {
		if ((Uint & 0x8000) > 0) {
			Uint = Uint - 0x10000;
		}
	}
	if (Size === 3) {
		if ((Uint & 0x800000) > 0) {
			Uint = Uint - 0x1000000;
		}
	}
	if (Size === 4) {
		if ((Uint & 0x80000000) > 0) {
			Uint = Uint - 0x100000000;
		}
	}


	return Uint;
}

function toHexString(byteArray) {
	var s = '';
	byteArray.forEach(function(byte) {
		s += ('0' + (byte & 0xFF).toString(16)).slice(-2);
	});
	return s;
}

function decimalToHex(d, padding) {
	var hex = Number(d).toString(16).toUpperCase();
	padding = typeof (padding) === "undefined" || padding === null ? padding = 2 : padding;

	while (hex.length < padding) {
		hex = "0" + hex;
	}

	return "0x" + hex;
}
function hex_to_ascii(str1)
{
	var hex  = str1.toString();
	var str = '';
	for (var n = 0; n < hex.length; n += 2) {
		str += String.fromCharCode(parseInt(hex.substr(n, 2), 16));
	}
	return str;
}

function dec2String(array) {
	var result = "";
	for (var i = 0; i < array.length; i++) 
	{
		result += String.fromCharCode(array[i]);
	}
	return result;
}

function Bytes2Float32(bytes) {
	var sign = (bytes & 0x80000000) ? -1 : 1;
	var exponent = ((bytes >> 23) & 0xFF) - 127;
	var significand = (bytes & ~(-1 << 23));

	if (exponent == 128) 
	return sign * ((significand) ? Number.NaN : Number.POSITIVE_INFINITY);

	if (exponent == -127) {
		if (significand == 0) return sign * 0.0;
		exponent = -126;
		significand /= (1 << 22);
	} else significand = (significand | (1 << 23)) / (1 << 23);

	return sign * significand * Math.pow(2, exponent);
}

function parseHexString(str) { 
	var result = [];
	while (str.length >= 2) { 
		result.push(parseInt(str.substring(0, 2), 16));

		str = str.substring(2, str.length);
	}

	return result;
}

function Decoder( bytes, fPort) {
	// Decode an uplink message from a buffer
	// (array) of bytes to an object of fields.

	// la ligne du dessous est pour la compatibilité avec Lora-server
	var byte_array = bytes;
	
	// recuperer le numero de port
	var port_nb = parseInt(fPort, 10);
	var decoded = {};
	decoded.lora = {};
	decoded.lora.port  = port_nb;
	
	// Get raw payload
	var bytes_len_	= byte_array.length;
	var temp_hex_str = ""
	decoded.lora.payload  = "";

	for( var j = 0; j < bytes_len_; j++ )
	{
		temp_hex_str   = byte_array[j].toString( 16 ).toUpperCase( );
		if( temp_hex_str.length == 1 )
		{
			temp_hex_str = "0" + temp_hex_str;
		}
		decoded.lora.payload += temp_hex_str;
		var date = new Date();
		decoded.lora.date = date.toISOString();
	}
	// ici on a le payload et la date du message
	
	// Nke utilise toujours le port 125, sinon c'est pas du Nke
	if (port_nb === 125) 
	{
		//On verifie si c'est un message de type batch ou une trame standard
		batch = !(byte_array[0] & 0x01);
		
		//On traite la trame standard 
		if (batch === false)
		{
			// On va decoder le header de la trame standard
			decoded.zclheader = {};
			decoded.zclheader.report =  "standard";
			attributID = -1;
			cmdID = -1;
			clusterdID = -1;
			//endpoint
			decoded.zclheader.endpoint = ((byte_array[0]&0xE0)>>5) | ((byte_array[0]&0x06)<<2);
			//command ID
			cmdID =  byte_array[1]; decoded.zclheader.cmdID = decimalToHex(cmdID,2);
			//Cluster ID
			clusterdID = byte_array[2]*256 + byte_array[3]; decoded.zclheader.clusterdID = decimalToHex(clusterdID,4);
			
			// decode report and read attribut response
			if((cmdID === 0x0a)|(cmdID === 0x8a)|(cmdID === 0x01))
			{
				decoded.data = {};
				//Attribut ID 
				attributID = byte_array[4]*256 + byte_array[5];decoded.zclheader.attributID = decimalToHex(attributID,4);
				
				// TIC PMEPMI est le Cluster 0057, attributID à 00 car on gère que le cas courant
				if ((clusterdID === 0x0057) & (attributID === 0x0000))
				{
					// le zcldatatype donne la facon d'encoder le nombre d'octets à suivre: ie sur 1 octet(0x41) ou 2 octets(0x43)
					decoded.zclheader.zcldatatype = decimalToHex(byte_array[6],2);
					decoded.zclheader.nboctet = byte_array[7]; // parce que byte_array[6]=0x41
					//decoded.zclheader.nboctet =  byte_array[7]*256 + byte_array[8]; // parce que byte_array[6]=0x43 et dans ce cas on décale tout les bytes dans la suite
					
					// on va la TICFieldsList
					decoded.TICFieldsList = {};
					decoded.TICFieldsList.TICFieldsSelector={};
					decoded.TICFieldsList.TICFieldsSelector.DescHeader={};
					// en premier decodage de l'entete
					decoded.TICFieldsList.TICFieldsSelector.DescHeader.b7=(byte_array[8]& parseInt('10000000', 2)) >>7;
					decoded.TICFieldsList.TICFieldsSelector.DescHeader.b6=(byte_array[8]& parseInt('01000000', 2)) >>6;
					decoded.TICFieldsList.TICFieldsSelector.DescHeader.b5=(byte_array[8]& parseInt('00100000', 2)) >>5;
					decoded.TICFieldsList.TICFieldsSelector.DescHeader.size=byte_array[8]& parseInt('00011111', 2); //0x1F=11111
					// le bit 7 pour l'etat du TIC					
					if(decoded.TICFieldsList.TICFieldsSelector.DescHeader.b7 === 0)
					{
						decoded.TICFieldsList.TICFieldsSelector.DescHeader.signalTIC = "TIC OK";
					}
					else{
						decoded.TICFieldsList.TICFieldsSelector.DescHeader.signalTIC = "Obsolete";
					}
					// le Bit 6 pour la validité du rapport
					if(decoded.TICFieldsList.TICFieldsSelector.DescHeader.b6 === 0)
					{
						decoded.TICFieldsList.TICFieldsSelector.DescHeader.report = "Report standard";
					}
					else{
						decoded.TICFieldsList.TICFieldsSelector.DescHeader.report = "Report decalé";
					}
					// Le bit 5 pour le codage de la suite -> notre compteur envoi du descvarIndexes
					if(decoded.TICFieldsList.TICFieldsSelector.DescHeader.b5 === 0)
					{
						decoded.TICFieldsList.TICFieldsSelector.DescHeader.descvarbitfield = "oui";
						decoded.TICFieldsList.TICFieldsSelector.DescHeader.descvarIndexes = "non";
						// code non réalisé
					}
					else
					{
						decoded.TICFieldsList.TICFieldsSelector.DescHeader.descvarbitfield = "non";
						decoded.TICFieldsList.TICFieldsSelector.DescHeader.descvarIndexes = "oui";
						
						var selectiondechamps=[];						
						var emplacement=0;

						for (n=1+8 ;n<=decoded.TICFieldsList.TICFieldsSelector.DescHeader.size-1+8;n++)
						{
							selectiondechamps[emplacement++]=byte_array[n];
						}
						//verification des champs trouvé
						console.log (selectiondechamps);
						
						var nbchamps= decoded.TICFieldsList.TICFieldsSelector.DescHeader.size-1+8;
						var bytesplace = decoded.TICFieldsList.TICFieldsSelector.DescHeader.size-1+8+1;

						decoded.FieldsList = {};
						// on decode chacun des champs, il faudrait en faire 75
						
						for (n=0; n<= nbchamps; n++)
						{
							if (selectiondechamps[n] === 0)
							{
								decoded.FieldsList.TRAME= byte_array[bytesplace]; //test
								bytesplace=bytesplace+1;
							}
							// numero du compteur
							if (selectiondechamps[n] === 1)
							{
								var adssize= byte_array[bytesplace++];
								decoded.FieldsList.ADS= toHexString(byte_array.slice(bytesplace, bytesplace+adssize));
								bytesplace=bytesplace+adssize;
							}
							// Traitement tarifaire
							if (selectiondechamps[n] === 2)
							{
								
								var MESURES1= byte_array[bytesplace++] ;
								
								if (((MESURES1& parseInt('10000000', 2)) >>7 ) ===0)
								{
									console.log ("0");
									var enumere = MESURES1& parseInt('01111111', 2);
									if (enumere === 3)
									{
										decoded.FieldsList.MESURES1 = "BT 4 SUP 36"	;
									}
									if (enumere === 4)
									{
										decoded.FieldsList.MESURES1 = "BT 5 SUP 36"	;
									}
									if (enumere === 5)
									{
										decoded.FieldsList.MESURES1 = "HTA 5"	;
									}
									if (enumere === 6)
									{
										decoded.FieldsList.MESURES1 = "HTA 8"	;
									}
									if (enumere === 7)
									{
										decoded.FieldsList.MESURES1 = "TJ EJP"	;
									}
									if (enumere === 8)
									{
										decoded.FieldsList.MESURES1 = "TJ EJP-HH"	;
									}
									if (enumere === 9)
									{
										decoded.FieldsList.MESURES1 = "TJ EJP-PM"	;
									}
									if (enumere === 10)
									{
										decoded.FieldsList.MESURES1 = "TJ EJP-SD"	;
									}
									if (enumere === 11)
									{
										decoded.FieldsList.MESURES1 = "TJ LU"	;
									}
									if (enumere === 12)
									{
										decoded.FieldsList.MESURES1 = "TJ LU-CH"	;
									}
									if (enumere === 13)
									{
										decoded.FieldsList.MESURES1 = "TJ LU-P"	;
									}
									if (enumere === 14)
									{
										decoded.FieldsList.MESURES1 = "TJ LU-PH"	;
									}
									if (enumere === 15)
									{
										decoded.FieldsList.MESURES1 = "TJ LU-SD"	;
									}
									if (enumere === 16)
									{
										decoded.FieldsList.MESURES1 = "TJ MU"	;
									}
									if (enumere === 17)
									{
										decoded.FieldsList.MESURES1 = "TJ A5 BASE"	;
									}
									if (enumere === 18)
									{
										decoded.FieldsList.MESURES1 = "TJ A8 BASE"	;
									}
									
								}
								if (((MESURES1& parseInt('10000000', 2)) >>7 )===1)
								{
									var nbcaractere = MESURES1& parseInt('01111111', 2);
									var tabdedonne = byte_array.slice(bytesplace, bytesplace+nbcaractere);
									decoded.FieldsList.MESURES1 =dec2String(tabdedonne);
									bytesplace=bytesplace+nbcaractere;
								}
							}
							// Date et heure courante DMYhms
							if (selectiondechamps[n] === 3)
							{
								var date= new Date (2000+byte_array[bytesplace+2], byte_array[bytesplace+1]-1, byte_array[bytesplace], byte_array[bytesplace+3],byte_array[bytesplace+4],byte_array[bytesplace+5])
								decoded.FieldsList.DATE= date.toString();
								bytesplace=bytesplace+6;
							}
							
							// à traiter: 4 à 11
							
							if (selectiondechamps[n] === 12)
							{
								var PTCOUR1= byte_array[bytesplace++] ;
								if (((PTCOUR1& parseInt('10000000', 2)) >>7 ) ===0)
								{
									var enumere = PTCOUR1& parseInt('01111111', 2);
									if (enumere === 3)
									{
										decoded.FieldsList.PTCOUR1 = " ? "	;
									}
									if (enumere === 4)
									{
										decoded.FieldsList.PTCOUR1 = "000"	;
									}
									if (enumere === 5)
									{
										decoded.FieldsList.PTCOUR1 = "HC"	;
									}
									if (enumere === 6)
									{
										decoded.FieldsList.PTCOUR1 = "HCD"	;
									}
									if (enumere === 7)
									{
										decoded.FieldsList.PTCOUR1 = "HCE"	;
									}
									if (enumere === 8)
									{
										decoded.FieldsList.PTCOUR1 = "HCH"	;
									}
									if (enumere === 9)
									{
										decoded.FieldsList.PTCOUR1 = "HH"	;
									}
									if (enumere === 10)
									{
										decoded.FieldsList.PTCOUR1 = "HH "	;
									}
									if (enumere === 11)
									{
										decoded.FieldsList.PTCOUR1 = "HP"	;
									}
									if (enumere === 12)
									{
										decoded.FieldsList.PTCOUR1 = "HP "	;
									}
									if (enumere === 13)
									{
										decoded.FieldsList.PTCOUR1 = "HPD"	;
									}
									if (enumere === 14)
									{
										decoded.FieldsList.PTCOUR1 = "HPE"	;
									}
									if (enumere === 15)
									{
										decoded.FieldsList.PTCOUR1 = "HPH"	;
									}
									if (enumere === 16)
									{
										decoded.FieldsList.PTCOUR1 = "JA"	;
									}
									if (enumere === 17)
									{
										decoded.FieldsList.PTCOUR1 = "JA "	;
									}
									if (enumere === 18)
									{
										decoded.FieldsList.PTCOUR1 = "P"	;
									}
									if (enumere === 19)
									{
										decoded.FieldsList.PTCOUR1 = "P  "	;
									}
									if (enumere === 20)
									{
										decoded.FieldsList.PTCOUR1 = "PM"	;
									}
									if (enumere === 21)
									{
										decoded.FieldsList.PTCOUR1 = "PM "	;
									}
									if (enumere === 22)
									{
										decoded.FieldsList.PTCOUR1 = "XXX"	;
									}
									
								}
								if (((PTCOUR1& parseInt('10000000', 2)) >>7 )===1)
								{
									var nbcaractere = PTCOUR1& parseInt('01111111', 2);
									var tabdedonne = byte_array.slice(bytesplace, bytesplace+nbcaractere);
									decoded.FieldsList.PTCOUR1 =dec2String(tabdedonne);
									bytesplace=bytesplace+nbcaractere;
								}
							}
							// à traiter: 13 à 39
							
							if (selectiondechamps[n] === 40) //tsDMYhms
							{
								var Timestampdebp = byte_array[bytesplace++]<<24 |byte_array[bytesplace++]<<16 | byte_array[bytesplace++]<<8 |byte_array[bytesplace++];
								var Timestampdebpdate= new Date ( Timestampdebp*1000);
								Timestampdebpdate.setFullYear(Timestampdebpdate.getFullYear()+30);
								decoded.FieldsList.DebP= Timestampdebpdate.toString() ;
							}
							if (selectiondechamps[n] === 41)
							{
								decoded.FieldsList.EAP_s= byte_array[bytesplace++]<<16 |byte_array[bytesplace++]<<8 |byte_array[bytesplace++]
							}
							// à traiter: 42 à 54
							if (selectiondechamps[n] === 55)
							{
								//decoded.FieldsList.PS=toHexString(byte_array.slice(bytesplace, bytesplace+4));
								decoded.FieldsList.PS= byte_array[bytesplace++]<<16 |byte_array[bytesplace++]<<8 |byte_array[bytesplace++] ;
								var PSunit= byte_array[bytesplace++] ;
								if (((PSunit& parseInt('10000000', 2)) >>7 ) ===0)
								{
									var enumere = PSunit& parseInt('01111111', 2);
									if (enumere === 3)
									{
										decoded.FieldsList.PSunit = "  ACTIF"	;
									}
									if (enumere === 4)
									{
										decoded.FieldsList.PSunit = "ACTIF"	;
									}
									if (enumere === 5)
									{
										decoded.FieldsList.PSunit = "CONSO"	;
									}
									if (enumere === 6)
									{
										decoded.FieldsList.PSunit = "CONTROLE"	;
									}
									if (enumere === 7)
									{
										decoded.FieldsList.PSunit = "DEP"	;
									}
									if (enumere === 8)
									{
										decoded.FieldsList.PSunit = "INACTIF"	;
									}
									if (enumere === 9)
									{
										decoded.FieldsList.PSunit = "PROD"	;
									}
									if (enumere === 10)
									{
										decoded.FieldsList.PSunit = "TEST"	;
									}
									if (enumere === 11)
									{
										decoded.FieldsList.PSunit = "kVA"	;
									}
									if (enumere === 12)
									{
										decoded.FieldsList.PSunit = "kW"	;
									}
								}
								if (((PSunit& parseInt('10000000', 2)) >>7 )===1)
								{
									var nbcaractere = PSunit& parseInt('01111111', 2);
									var tabdedonne = byte_array.slice(bytesplace, bytesplace+nbcaractere);
									decoded.FieldsList.PSunit =dec2String(tabdedonne);
									bytesplace=bytesplace+nbcaractere;
								}
								decoded.FieldsList.PS=decoded.FieldsList.PS + " " +  decoded.FieldsList.PSunit;
							}
							// à traiter: 56 à 75
						}
					}
				}
			}
			//decode configuration response
			if(cmdID === 0x07)
			{
			}
			//decode read configuration response
			if(cmdID === 0x09)
			{
			}
		}
		else
		{
			decoded.batch = {};
			decoded.batch.report = "batch";
		}
	}
	return decoded;
}

Things would be very wrong if the port number as passed by TTN is not already an integer. So, the original code referring to port should really be okay, for TTN. Why do you think you need this change? Is that to support ChirpStack (formerly known as LoRa Server)?

yes! it’s because I use ChirpStack for a another application