diff --git a/README.md b/README.md index 9f34755..f4a2670 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,11 @@ Récupérer l'instance d'EventEmitter du module en appelant la fonction teleinfo var trameEvents = teleinfo('/dev/ttyAMA0'); ``` +```javascript +// Configuration TIC Standard +var trameEvents = teleinfo('/dev/ttyAMA0', teleinfo.version.STANDARD); +``` + Les trames téléinfo sont envoyées sous forme d'évènements : * trame : trames brutes non vérifiées (utile uniquement à des fins de debug) * tramedecodee : trames sous forme d'un objet (checksums validés pour chaque propriété) diff --git a/configs.js b/configs.js new file mode 100644 index 0000000..c8c8f4f --- /dev/null +++ b/configs.js @@ -0,0 +1,136 @@ +module.exports = { + HISTORIQUE: { + baudRate: 1200, + separator: ' ', + computeChecksum: (line) => { + // Spec chk : somme des codes ASCII + ET logique 03Fh + ajout 20 en hexadécimal + // Résultat toujours un caractère ASCII imprimable allant de 20 à 5F en hexadécimal + // Checksum calculé sur etiquette+space+données => retirer les 2 derniers caractères + var sum = 0; + for (var j=0; j < line.length-2; j++) { + sum += line.charCodeAt(j); + } + return (sum & 63) + 32; + }, + labels: { + 'ADCO': { numeric: false }, + 'OPTARIF': { numeric: false }, + 'PTEC': { numeric: false }, + 'DEMAIN': { numeric: false }, + 'HHPHC': { numeric: false }, + 'MOTDETAT': { numeric: false }, + 'PPOT': { numeric: false }, + 'ISOUSC': { numeric: true }, + 'BASE': { numeric: true }, + 'HCHC': { numeric: true }, + 'HCHP': { numeric: true }, + 'EJPHN': { numeric: true }, + 'EJPHPM': { numeric: true }, + 'BBRHCJB': { numeric: true }, + 'BBRHPJB': { numeric: true }, + 'BBRHCJW': { numeric: true }, + 'BBRHPJW': { numeric: true }, + 'BBRHCJR': { numeric: true }, + 'BBRHPJR': { numeric: true }, + 'PEJP': { numeric: true }, + 'IINST': { numeric: true }, + 'IINST1': { numeric: true }, + 'IINST2': { numeric: true }, + 'IINST3': { numeric: true }, + 'ADPS': { numeric: true }, + 'IMAX': { numeric: true }, + 'IMAX1': { numeric: true }, + 'IMAX2': { numeric: true }, + 'IMAX3': { numeric: true }, + 'PMAX': { numeric: true }, + 'PAPP': { numeric: true }, + } + }, + STANDARD: { + baudRate: 9600, + separator: '\t', + computeChecksum: (line) => { + // Spec chk : somme des codes ASCII + ET logique 03Fh + ajout 20 en hexadécimal + // Résultat toujours un caractère ASCII imprimable allant de 20 à 5F en hexadécimal + // Checksum calculé sur etiquette+tab+données(+tab+données)+tab => retirer le dernier caractères + var sum = 0; + for (var j=0; j < line.length-1; j++) { + sum += line.charCodeAt(j); + } + return (sum & 63) + 32; + }, + labels: { + 'ADSC': { numeric: false, date: false }, + 'VTIC': { numeric: true, date: false }, + 'DATE': { numeric: false, date: true }, + 'NGTF': { numeric: false, date: false }, + 'LTARF': { numeric: false, date: false }, + 'EAST': { numeric: true, date: false }, + 'EASF01': { numeric: true, date: false }, + 'EASF02': { numeric: true, date: false }, + 'EASF03': { numeric: true, date: false }, + 'EASF04': { numeric: true, date: false }, + 'EASF05': { numeric: true, date: false }, + 'EASF06': { numeric: true, date: false }, + 'EASF07': { numeric: true, date: false }, + 'EASF08': { numeric: true, date: false }, + 'EASF09': { numeric: true, date: false }, + 'EASF10': { numeric: true, date: false }, + 'EASD01': { numeric: true, date: false }, + 'EASD02': { numeric: true, date: false }, + 'EASD03': { numeric: true, date: false }, + 'EASD04': { numeric: true, date: false }, + 'EAIT': { numeric: true, date: false }, + 'ERQ1': { numeric: true, date: false }, + 'ERQ2': { numeric: true, date: false }, + 'ERQ3': { numeric: true, date: false }, + 'ERQ4': { numeric: true, date: false }, + 'IRMS1': { numeric: true, date: false }, + 'IRMS2': { numeric: true, date: false }, + 'IRMS3': { numeric: true, date: false }, + 'URMS1': { numeric: true, date: false }, + 'URMS2': { numeric: true, date: false }, + 'URMS3': { numeric: true, date: false }, + 'PREF': { numeric: true, date: false }, + 'PCOUP': { numeric: true, date: false }, + 'SINSTS': { numeric: true, date: false }, + 'SINSTS1': { numeric: true, date: false }, + 'SINSTS2': { numeric: true, date: false }, + 'SINSTS3': { numeric: true, date: false }, + 'SMAXSN': { numeric: true, date: true }, + 'SMAXSN1': { numeric: true, date: true }, + 'SMAXSN2': { numeric: true, date: true }, + 'SMAXSN3': { numeric: true, date: true }, + 'SMAXSN-1': { numeric: true, date: true }, + 'SMAXSN1-1': { numeric: true, date: true }, + 'SMAXSN2-1': { numeric: true, date: true }, + 'SMAXSN3-1': { numeric: true, date: true }, + 'SINSTI': { numeric: true, date: false }, + 'SMAXIN': { numeric: true, date: true }, + 'SMAXIN-1': { numeric: true, date: true }, + 'CCASN': { numeric: true, date: true }, + 'CCASN-1': { numeric: true, date: true }, + 'CCAIN': { numeric: true, date: true }, + 'CCAIN-1': { numeric: true, date: true }, + 'UMOY1': { numeric: true, date: true }, + 'UMOY2': { numeric: true, date: true }, + 'UMOY3': { numeric: true, date: true }, + 'STGE': { numeric: false, date: false }, + 'DPM1': { numeric: false, date: true }, + 'FPM1': { numeric: false, date: true }, + 'DPM2': { numeric: false, date: true }, + 'FPM2': { numeric: false, date: true }, + 'DPM3': { numeric: false, date: true }, + 'FPM3': { numeric: false, date: true }, + 'MSG1': { numeric: false, date: false }, + 'MSG2': { numeric: false, date: false }, + 'PRM': { numeric: false, date: false }, + 'RELAIS': { numeric: false, date: false }, + 'NTARF': { numeric: false, date: false }, + 'NJOURF': { numeric: false, date: false }, + 'NJOURF+1': { numeric: false, date: false }, + 'PJOURF+1': { numeric: false, date: false }, + 'PPOINTE': { numeric: false, date: false }, + } + } +}; diff --git a/package.json b/package.json index 4aa1234..dd9b1fe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "teleinfo", - "version": "0.2.0", + "version": "0.3.0", "description": "Decode teleinfo data (from french electricity meter) from serial port", "main": "teleinfo.js", "keywords": [ @@ -13,7 +13,7 @@ "url": "https://github.com/lhuet/teleinfo-node.git" }, "dependencies": { - "serialport": "1.2.5" + "serialport": "^7.1.4" }, "author": { "name": "Laurent HUET", diff --git a/teleinfo.js b/teleinfo.js index e6a52a7..da60d26 100644 --- a/teleinfo.js +++ b/teleinfo.js @@ -1,37 +1,38 @@ -var serialport = require('serialport'); +var SerialPort = require('serialport'); var events = require('events'); -var util = require('util'); +var configs = require('./configs'); +const { REPL_MODE_STRICT } = require('repl'); -function teleinfo(port) { +function teleinfo(portName, config = configs.HISTORIQUE) { // Evénements 'trame' et 'tramedecodee' var trameEvents = new events.EventEmitter(); - var serialPort = new serialport.SerialPort(port, { - baudrate: 1200, + const Readline = require('@serialport/parser-readline'); + const port = new SerialPort(portName, { + baudRate: config.baudRate, dataBits: 7, parity: 'even', - stopBits: 1, - // Caractères séparateurs = fin de trame + début de trame - parser: serialport.parsers.readline(String.fromCharCode(13,3,2,10)) + stopBits: 1 }); + const parser = port.pipe(new Readline({ delimiter: String.fromCharCode(13,3,2,10) })); // Caractères séparateurs = fin de trame + début de trame - serialPort.on('data', function(data) { + parser.on('data', function(data) { trameEvents.emit('trame', data); }); - serialPort.on('error', function(err) { + port.on('error', function(err) { trameEvents.emit('error', err); }); trameEvents.on('trame', function(data) { - // Decode trame '9 lignes en tarif bleu base' var trame = {}; + // Decode trame '9 lignes en tarif bleu base' var arrayOfData = data.split('\r\n'); for (var i=0; i < arrayOfData.length; i++) { - decodeLigne(arrayOfData[i], trame, trameEvents); + decodeLigne(arrayOfData[i], trame, trameEvents, config); } // trame incomplete s'il manque la première ligne ADCO - if (!(trame.ADCO===undefined)) { + if (trame.ADCO || trame.ADSC) { trameEvents.emit('tramedecodee', trame); } else { @@ -44,46 +45,68 @@ function teleinfo(port) { } -function decodeLigne(ligneBrute, trame, trameEvents) { - // Ligne du type "PAPP 00290 ," (Etiquette / Donnée / Checksum) - var elementsLigne = ligneBrute.split(' '); - if (elementsLigne.length === 3) { - // Spec chk : somme des codes ASCII + ET logique 03Fh + ajout 20 en hexadécimal - // Résultat toujours un caractère ASCII imprimable allant de 20 à 5F en hexadécimal - // Checksum calculé sur etiquette+space+données => retirer les 2 derniers caractères - var sum = 0; - for (var j=0; j < ligneBrute.length-2; j++) { - sum += ligneBrute.charCodeAt(j); - } - sum = (sum & 63) + 32; - if (sum === ligneBrute.charCodeAt(j+1)) { - // Checksum ok -> on ajoute la propriété à la trame - // Conversion en valeur numérique pour certains cas - switch (elementsLigne[0].substr(0,4)) { - case 'BASE': // Index Tarif bleu - case 'HCHC': // Index Heures creuses - case 'HCHP': // Index Heures pleines - case 'EJPH': // Index EJP (HN et HPM) - case 'BBRH': // Index Tempo (HC/HP en jours Blanc, Bleu et Rouge) - case 'ISOU': // Intensité souscrite - case 'IINS': // Intensité instantannée (1/2/3 pour triphasé) - case 'ADPS': // Avertissement de dépassement - case 'IMAX': // Intensité max appelée (1/2/3 pour triphasé) - case 'PAPP': // Puissance apparente - case 'PMAX': // Puissance max triphasée atteinte - trame[elementsLigne[0]]= Number(elementsLigne[1]); - break; - default: - trame[elementsLigne[0]]= elementsLigne[1]; +function decodeLigne(ligneBrute, trame, trameEvents, config) { + var sum = config.computeChecksum(ligneBrute); + var checksum = ligneBrute.charCodeAt(ligneBrute.length-1); + if (sum === checksum) { + // Checksum ok -> on ajoute la propriété à la trame + var elementsLigne = ligneBrute.split(config.separator); + if (elementsLigne.length >= 3) { + const label = elementsLigne[0]; + const props = config.labels[label]; + if(!props) { + trameEvents.emit('error', new Error('Label inconnu: ' + label)); + return false; + } + if(props.date) { + if(elementsLigne.length >= 4) { + const date = convertDate(elementsLigne[1]); + const value = props.numeric ? Number(elementsLigne[2]) : elementsLigne[2]; + if(Number.isNaN(date)) { + trameEvents.emit('error', new Error('Date invalide: ' + elementsLigne[1])); + return false; + } + if(Number.isNaN(value)) { + trameEvents.emit('error', new Error('Valeur invalide: ' + elementsLigne[2])); + return false; + } + if(label == 'DATE') { + trame[label] = date.toISOString(); + } else { + trame[label] = { + date: date.toISOString(), + value: props.numeric ? Number(elementsLigne[2]) : elementsLigne[2] + }; + } + return true; + } + } else { + const value = props.numeric ? Number(elementsLigne[1]) : elementsLigne[1]; + if(Number.isNaN(value)) { + trameEvents.emit('error', new Error('Valeur invalide: ' + elementsLigne[1])); + return false; + } + trame[label] = props.numeric ? Number(elementsLigne[1]) : elementsLigne[1]; + return true; } - return true; - } else { - var err = new Error('Erreur de checksum : \n' + ligneBrute + '\n Checksum calculé/reçu : ' + sum + ' / ' + ligneBrute.charCodeAt(j+1)); - trameEvents.emit('error', err); } + } else { + var err = new Error('Erreur de checksum : \n' + ligneBrute + '\n Checksum calculé/reçu : ' + sum + ' / ' + checksum); + trameEvents.emit('error', err); }; + return false; }; +function convertDate(str) { + const dateStr = new String((new Date()).getFullYear()).substring(0,2) + str.substring(1,3) + '-' + str.substring(3,5) + '-' + str.substring(5,7) + 'T' + str.substring(7,9) + ':' + str.substring(9,11) + ':' + str.substring(11,13) + 'Z'; + const timestamp = Date.parse(dateStr); + if(Number.isNaN(timestamp)) { + return timestamp; + } else { + return new Date(timestamp - (str.substring(0,1) === 'E' ? (2*60*60*1000) : (60*60*1000))); + } +}; module.exports = teleinfo; +module.exports.version = configs;