UNPKG

iobroker.e3oncan

Version:

Collect data on CAN bus for Viessmann E3 devices, e.g. Vitocal, Vitocharge, Energy Meters E380CA and E3100CB

645 lines (584 loc) 18.4 kB
const enums = require('./enums'); const C2POW08 = 0x100; const C2POW15 = 0x08000; const C2POW16 = 0x10000; const C2POW24 = 0x1000000; const C2POW31 = 0x080000000; const C2POW32 = 0x100000000; function arr2Hex(arr) { // Convert byte array to hex string let hs = ''; for (const v in arr) { hs += toHex(arr[v],2); } return hs; } function toHex(d, len) { // Convert integer to hex string of length len return (('00000000'+(Number(d).toString(16))).slice(-len)); } function toByteArray(hs) { // Convert hex string, e.g. '21A8' to byte array: [33,168] const ba = []; for (let i=0; i<hs.length/2; i++) { ba.push(parseInt(hs.slice(2*i,2*i+2), 16)); } return ba; } function uint08toVal(j) { return j[0]; } function sint08toVal(j) { return (j[0]<128 ? j[0] : j[0]-256); } function uint16toVal(j) { return C2POW08*j[1]+j[0]; } function sint16toVal(j) { let v = C2POW08*j[1]+j[0]; if (v >= C2POW15) { v -= C2POW16; } return v; } function uint32toVal(j,) { return C2POW24*j[3]+C2POW16*j[2]+C2POW08*j[1]+j[0]; } function sint32toVal(j) { let v = C2POW24*j[3]+C2POW16*j[2]+C2POW08*j[1]+j[0]; if (v >= C2POW31) { v -= C2POW32; } return v; } function int2val(j, signed = false) { // Convert byte array to int08, int16, int32. Signed or unsigned. switch (j.length) { case 1 : return (signed ? sint08toVal(j) : uint08toVal(j) ); case 2 : return (signed ? sint16toVal(j) : uint16toVal(j) ); case 4 : return (signed ? sint32toVal(j) : uint32toVal(j) ); default: return null; } } function val2byteArr(v, byte_width, scale = 1, signed = false) { // Convert int08, int16, int32 to byte array. Signed or unsigned. let val = Math.round(eval(v) * scale); if ( (signed) && (val < 0) ) { val += (2**(8*byte_width)); } const string_bin = toByteArray(toHex(val, byte_width*2)); return string_bin.reverse(); } function RawEncode(data, len) { const string_bin = toByteArray(data); if (string_bin.length !== len) { throw new Error(`String must be ${len} long`); } return string_bin; } function RawDecode(string_bin) { return arr2Hex(string_bin); } class O3ERawCodec { constructor(string_len, idStr, _args) { this.string_len = string_len; this.id = idStr; } encode(data) { return RawEncode(data, this.string_len); } decode(string_bin) { return RawDecode(string_bin); } __len__() { return this.string_len; } } class O3EInt { constructor(string_len, idStr, byte_width, _args) { this.string_len = string_len; this.id = idStr; this.byte_width = byte_width; this.scale = _args.scale; this.signed = _args.signed; } encode(data) { return val2byteArr(data, this.byte_width, this.scale, this.signed); } decode(data) { return int2val(data.slice(0,this.byte_width), this.signed) / this.scale; } __len__() { return this.string_len; } } // Named parameters in Javascript: https://masteringjs.io/tutorials/fundamentals/parameters // function (parg1, parg2, { narg1 = 1, narg2 = 2, narg3 = 3 } = {} ) {} class O3EInt8 extends O3EInt { constructor(string_len, idStr, _args) { super(string_len, idStr, 1, _args); } } class O3EInt16 extends O3EInt { constructor(string_len, idStr, _args) { super(string_len, idStr, 2, _args); } } class O3EInt32 extends O3EInt { constructor(string_len, idStr, _args) { super(string_len, idStr, 4, _args); } } class O3EByteVal { constructor(string_len, idStr, _args) { this.string_len = string_len; this.id = idStr; } encode(data) { return val2byteArr(data, this.string_len, 1, false); } decode(string_bin) { let val = 0; for (let i = 0; i < this.string_len; i++) { val += string_bin[i] << (i * 8); } return val; } __len__() { return this.string_len; } } class O3EFloat32 { // IEEE-754 Converter for float32: https://www.h-schmidt.net/FloatConverter/IEEE754de.html // Convert byte values to float: https://stackoverflow.com/questions/4414077/read-write-bytes-of-float-in-js constructor(string_len, idStr, _args) { this.string_len = string_len; this.id = idStr; this.scale = _args.scale; } encode(_data) { throw new Error('O3EFloat32.encode(): not implemented yet'); } decode(string_bin) { const buffer = new ArrayBuffer(4); const intView = new Int32Array(buffer); const floatView = new Float32Array(buffer); intView[0] = int2val(string_bin, false); return floatView[0]/this.scale; } __len__() { return this.string_len; } } class O3EBool { constructor(string_len, idStr, _args) { this.string_len = string_len; this.id = idStr; } encode(data) { return (data == 'off' ? 0 : 1); } decode(string_bin) { const val = string_bin[0]; return (val == 0 ? 'off' : 'on' ); } __len__() { return this.string_len; } } class O3EStateEM { constructor(string_len, idStr, _args) { this.string_len = string_len; this.id = idStr; } encode(data) { return (data == -1 ? 4 : 0); } decode(string_bin) { const val = string_bin[0]; if (val == 4) return -1; if (val == 0) return 1; return 0; } __len__() { return this.string_len; } } class O3EUtf8 { constructor(string_len, idStr, _args) { this.string_len = string_len; this.id = idStr; } encode(data) { let result; try { result = Object.values(new TextEncoder().encode(data)); } catch(e) { throw new Error('O3EUtf8.encode(): Conversion from Utf8 failed '+this.id+'; err='+JSON.stringify(e)); } if (result.length > this.string_len) throw new Error('O3EUtf8.encode(): Result too long: '+this.id+' - '+String(result.length)+' > '+String(this.string_len)); return result.concat(Array.from(Array(this.string_len-result.length), () => 0)); } decode(string_bin) { let i = string_bin.slice(0).indexOf(0); if (i == -1) { i = string_bin.length; } let result; try { result = new TextDecoder().decode(new Uint8Array(string_bin.slice(0,i))); } catch(e) { throw new Error('O3EUtf8.decode(): Conversion to Utf8 failed '+this.id+'; err='+JSON.stringify(e)); } return result; } __len__() { return this.string_len; } } class O3ESoftVers { constructor(string_len, idStr, _args) { this.string_len = string_len; this.id = idStr; } encode(data) { let result = []; const arr = data.split('.'); for (const v of Object.values(arr)) result = result.concat(val2byteArr(v,2,1,false)); if (result.length > this.string_len) throw new Error('O3ESoftVers.encode() result too long: '+this.id+' - '+String(result.length)+' > '+String(this.string_len)); return result.concat(Array.from(Array(this.string_len-result.length), () => 0)); } decode(string_bin) { const lstv = []; for (let i = 0; i < this.string_len; i += 2) { lstv.push(string_bin[i] + (string_bin[i+1] << 8)); } return lstv.join('.'); } __len__() { return this.string_len; } } class O3EMacAddr { constructor(string_len, idStr, _args) { this.string_len = string_len; this.id = idStr; } encode(data) { return(data.split('-').map(function(str) {return parseInt('0x'+str);})); } decode(string_bin) { const lstv = []; for (let i = 0; i < this.string_len; i++) { lstv.push(toHex(string_bin[i],2).toUpperCase()); } return lstv.join('-'); } __len__() { return this.string_len; } } class O3EIp4Addr { constructor(string_len, idStr, _args) { this.string_len = string_len; this.id = idStr; } encode(data) { return(data.split('.').map(function(str) {return parseInt(str);})); } decode(string_bin) { const lstv = []; for (let i = 0; i < this.string_len; i++) { lstv.push(string_bin[i].toString().padStart(3,'0')); } return lstv.join('.'); } __len__() { return this.string_len; } } class O3ESdate { constructor(string_len, idStr, _args) { this.string_len = string_len; this.id = idStr; } encode(data) { const dt = new Date(data); if (!dt.valueOf()) throw new Error('could not convert date value'); return [dt.getDate(),dt.getMonth()+1,dt.getFullYear()%100]; } decode(string_bin) { const dt = new Date( string_bin[2]+2000, // year string_bin[1]-1, // month string_bin[0] // day ); return dt.toDateString(); } __len__() { return this.string_len; } } class O3EStime { constructor(string_len, idStr, _args) { this.string_len = string_len; this.id = idStr; } encode(data) { if (this.string_len == 1) data += ':00'; // Date() needs at least hour:minute const now = new Date(); const dt = new Date(now.toDateString()+' '+data); if (!dt.valueOf()) throw new Error('could not convert time value'); const retVal = [dt.getHours(),dt.getMinutes(),dt.getSeconds()]; return retVal.slice(0,this.string_len); } decode(string_bin) { const now = new Date(); const dt = new Date(now.getFullYear(), now.getMonth(), now.getDate(), string_bin[0], // hour ((this.string_len >= 2) ? string_bin[1] : 0), // minute ((this.string_len >= 3) ? string_bin[2] : 0) // second ); return dt.toLocaleTimeString(); } __len__() { return this.string_len; } } class O3EDateTime { constructor(string_len, idStr, _args) { this.string_len = string_len; this.id = idStr; this.timeformat = _args.timeformat; } encode(data) { if (this.timeformat == 'VM') { const dt = new Date(data.Timestamp); if (!dt.valueOf()) throw new Error('could not convert datetime value'); const fill = 0x05; // Unknown byte between date and time. Known values are 0x05 and 0x06 return([Math.floor(dt.getFullYear()/100),dt.getFullYear()%100,dt.getMonth()+1,dt.getDate(),fill, dt.getHours(),dt.getMinutes(),dt.getSeconds()]); } else { return(val2byteArr(data.Timestamp,this.string_len,0.001,false)); } } decode(string_bin) { let dt = new Date(); if (this.timeformat == 'VM') { dt = new Date( string_bin[0]*100+string_bin[1], // year string_bin[2]-1, // month string_bin[3], // day string_bin[5], // hour string_bin[6], // minute string_bin[7] // second ); } if (this.timeformat == 'ts') { dt = new Date(uint32toVal(string_bin.slice(0,4))*1000); } return { 'DateTime': dt.toLocaleString(), 'Timestamp': Math.round(dt.getTime()) }; } __len__() { return this.string_len; } } class O3EUtc { constructor(string_len, idStr, _args) { this.string_len = string_len; this.id = idStr; } encode(data) { const dt = new Date(data); if (!dt) throw new Error('could not convert Utc date value'); return val2byteArr(dt.getTime(),this.string_len,0.001,false); } decode(string_bin) { const dt = new Date(uint32toVal(string_bin.slice(0,4))*1000); return dt.toUTCString(); } __len__() { return this.string_len; } } class O3EEnum { constructor(string_len, idStr, _args) { this.string_len = string_len; this.id = idStr; this.listStr = _args.listStr; } encode(data) { return val2byteArr(data.ID, this.string_len, 1, false); } decode(string_bin) { const val = int2val(string_bin); let txt = ''; if ( (this.listStr in enums.enums) && (String(val) in enums.enums[this.listStr]) ) { txt = enums.enums[this.listStr][String(val)]; } else { txt = 'Enum not found in ' + this.listStr; } return {'ID': val, 'Text': txt }; } __len__() { return this.string_len; } } class O3EList { constructor(string_len, idStr, _args) { this.string_len = string_len; this.id = idStr; this.subTypes = _args.subTypes; } encode(data) { let result = []; for (const cdi of Object.values(this.subTypes)) { const subT = new O3Ecodecs[cdi.codec](cdi.len, cdi.id, cdi.args); if (cdi.args.subTypes) { for (const dataElement of Object.values(data[cdi.id])) { result = result.concat(subT.encode(dataElement)); } } else { result = result.concat(subT.encode(data[cdi.id])); } } if (result.length > this.string_len) throw new Error('O3EList.encode() result too long: '+String(result.length)+' > '+String(this.string_len)); return result.concat(Array.from(Array(this.string_len-result.length), () => 0)); } decode(string_bin) { const result = {}; let index = 0; let count = 0; for (const cdi of Object.values(this.subTypes)) { const subT = new O3Ecodecs[cdi.codec](cdi.len, cdi.id, cdi.args); if (subT.id.toLowerCase() == 'count') { count = subT.decode(string_bin.slice(index,index+subT.string_len)); result[subT.id]=count; index += subT.string_len; } else { if (('subTypes' in subT)) { // O3EComplexType result[subT.id] = []; if (count <= 100) { for (let i=0; i<count; i++) { result[subT.id].push(subT.decode(string_bin.slice(index,index+subT.string_len))); index += subT.string_len; } } else { result[subT.id] = ['Implausible number of elements. Decoding aborted.']; } } else { result[subT.id] = subT.decode(string_bin.slice(index,index+subT.string_len)); index += subT.string_len; } } } return result; } __len__() { return this.string_len; } } class O3EArray { constructor(string_len, idStr, _args) { this.string_len = string_len; this.id = idStr; this.subTypes = _args.subTypes; this.len = _args.arrayLength; } encode(_data) { throw new Error('not implemented yet'); } decode(string_bin) { const result = {}; let index = 0; const count = this.len; for (const cdi of Object.values(this.subTypes)) { const subT = new O3Ecodecs[cdi.codec](cdi.len, cdi.id, cdi.args); result[subT.id]=[]; for (let i=0;i<count;i++) { result[subT.id].push((subT.decode(string_bin.slice(index,index+subT.string_len)))); index += subT.string_len; } } return result; } __len__() { return this.string_len; } } class O3EComplexType { constructor(string_len, idStr, _args) { this.string_len = string_len; this.id = idStr; this.subTypes = _args.subTypes; } encode(data) { let result = []; for (const cdi of Object.values(this.subTypes)) { const subT = new O3Ecodecs[cdi.codec](cdi.len, cdi.id, cdi.args); result = result.concat(subT.encode(data[cdi.id])); } if (result.length > this.string_len) throw new Error('O3EComplexType.encode() result too long: '+this.id+' - '+String(result.length)+' > '+String(this.string_len)); return result.concat(Array.from(Array(this.string_len-result.length), () => 0)); } decode(string_bin) { const result = {}; let index = 0; for (const cdi of Object.values(this.subTypes)) { const subT = new O3Ecodecs[cdi.codec](cdi.len, cdi.id, cdi.args); result[subT.id] = subT.decode(string_bin.slice(index,index+subT.string_len)); index+=subT.string_len; } return result; } __len__() { return this.string_len; } } class O3EcosPhi { constructor(string_len, idStr, _args) { this.string_len = string_len; this.id = idStr; this.scale = _args.scale; } encode(_data) { throw new Error('not implemented yet'); } decode(string_bin) { let val = string_bin[1]; if (string_bin[0] == 0x04) { val = -1.0*val; } return val/this.scale; } __len__() { return this.string_len; } } const O3Ecodecs = { 'RawCodec':O3ERawCodec, 'O3EInt8':O3EInt8, 'O3EInt16':O3EInt16, 'O3EInt32':O3EInt32, 'O3EByteVal':O3EByteVal, 'O3EFloat32':O3EFloat32, 'O3EBool':O3EBool, 'O3EStateEM':O3EStateEM, 'O3EUtf8':O3EUtf8, 'O3ESoftVers':O3ESoftVers, 'O3EMacAddr':O3EMacAddr, 'O3EIp4Addr':O3EIp4Addr, 'O3ESdate':O3ESdate, 'O3EStime':O3EStime, 'O3EDateTime':O3EDateTime, 'O3EUtc':O3EUtc, 'O3EEnum':O3EEnum, 'O3EList':O3EList, 'O3EArray':O3EArray, 'O3EComplexType':O3EComplexType, 'O3EcosPhi':O3EcosPhi }; module.exports = { O3Ecodecs, arr2Hex, toByteArray, val2byteArr };