Hallo,
ich hab jetzt ein paar Tage hier nicht mehr mitgelesen, möchte Euch aber dennoch meinen derzeitigen Stand mitteilen und meine "fertiges Projekt" vorstellen. Es läuft jetzt seit einigen Tagen, es sind da sicher noch die Ein oder Andere kosmetische Änderungen möglich.
Die ausgelesen Daten sollten auf einer Webseite angezeigt werden. Dazu sollte es eine numerische und eine grafische Anzeige geben, auf dem der zeitliche Verlauf der Leistung dargestellt wird. Ich liebe sowas. Dabei war dann auch der Umgang mit CSS und Flexbox mal wieder lehrreich.
Das Datenende der Telegramme vom Zähler wird mittels Pause erkannt 50ms reichen da aus. Die Prüfsumme wird nicht ausgewertet. Wer mag kann das ja ändern, mir reicht das. Mein Zähler liefert die Messwerte jede Sekunde, falls das einmal schief gehen sollte ist es halt so.
Hardware ESP8266 Node Modul
Lesekopf Ebay 18€
Zähler EMH metering
Für die aktuelle Leistungsdaten wird ein Integral über die Anzahl der Messwerte zwischen den Ereignissen gebildet. Der Ringspeicher nimmt 100 Messwerte auf. Eingabe der Log-Zeit 1-60 Minuten. Es gibt 3 Tabs und eine HTML Seite für das Filesystem des ESP. Die wechselnden Leistungswerte kommen von der Wärmepumpe, und vom Backofen in der Küche. Ich überlege noch auf der x-Achse die Uhrzeit anzuzeigen anstelle der vergangen Minuten, ist halt Kosmetik.
Zur Auswertung der Zählerdaten ist es , so finde ich , nicht nötig die gesamte sml Datei zu durchsuchen. Letztlich will man ein paar Messwerte haben. Ich bin da einen anderen Weg gegangen und werte die Teile ab dem zu suchenden Suchkriterium unter Berücksichtigung der sml Struktur aus. Getestet wurden drei verschieden Datensätze, alle drei liefern die richtigen Ergebnisse.
Zu meinem Lösungsansatz für den Parser.
ich denke letztlich wird man immer nach den gewünschen OBIS Codes suchen um die Messwerte dann zuordnen zu können. Zur Auswertung der Daten sind zwei sml-Teile wichtig. Ersten die Kopfdaten um an die Server_ID = Zählernummer zu kommen. Zweitens die Abfrage der betreffenden OBIS Codes um an die gewünschten Messwerte zu kommen. Dabei wird dann jeweils die Struktur der sml Datei berücksichtigt. Die gesuchten Werte können ja an unterschiedlichen Element Positionen liegen, das hängt davon ab ob eine zusätzliche Liste eingefügt wurde, und auch die Daten selbst können unterschiedliche Längen haben. Ich beziehe mich hierbei auf das Dokument. Technische Richtlinie BSI TR-03109-1 und auf die sehr ausführliche Vorarbeit von @my_xy_projekt . Seine Aufbröselei des Hex Dumps war für mich sehr hilfreich, nochmals vielen Dank dafür.
Mit der Ankündigung einer Liste ,und der Angabe der dazugehörigen Anzahl an Elementen, ist bekannt wie viele Elemente zu erwarten sind. Wird eine neue Liste erkannt , verändert sich dies Maximale Anzahl. Die gesuchten Einträge stehen immer eine fixe Anzahl an Elementen vor dem zu erwartenden Ende. Damit muss man eigentlich die Elemente zählen und die max Anzahl aktualisieren. Die Listenelemente selbst zähle ich mit. Mit dem Längenangaben zu octString Int und Uint kann man die entsprechende byte´s vorwärts springen um den nächsten relevanten Eintrag zu kommen.
Dazu gibt es jetzt zwei Fälle:
- Auswertung der Kopfdaten um an die Server_ID zu kommen. Die Auswertung beginnt in dem Fall am Anfang der Daten. Eine sml Datei fängt immer gleich an und startet mit einem SML_Message Liste. Das erste Element ist die Ankündigung dieses Liste die immer vorhanden sein muss. Ich brösel das jetzt mal zum Verständnis einzeln auf.
Erstes byte nach der Startsequenz ist 0x76
0x76 Anküdigung einer Liste 6 Elemente
SML_Message ::= SEQUENCE Struktur aus der Spez.
{
transactionId Octet String, 2 Elementzähler startet mit 2
groupNo Unsigned8, 3
abortOnError Unsigned8, 4
messageBody SML_MessageBody, 5 -> hier gehts weiter
crc16 Unsigned16,
endOfSmlMsg EndOfSmlMsg
}
Damit ist zunächst die max Anzahl 6 Elemente. Ersichtlich durch das erste byte 0x76 nach dem sml Start. (Bedeutung 7=Listenelemnt 6= Anzahl)
bei dem 5 Element wird nun eine neue Liste eingehängt die selbst wieder aus zwei Elementen besteht.
72 = messageBody SML_MessageBody, 5 Elementzähler
63 01 01 6 Elementzähler
damit ist maxElement nun 8
Dabei wird nun im erste Element die Länge selbst angegeben hier 2 und im nächsten als Integer der Typ der Liste hier hex 0101 = SML_PublicOpen.Res. Die folgende Liste selbst beginnt wieder mit der Listenkennzeichnung und der Anzahl der Elemente.
76 7 Elementzähler
damit ist maxElemnt nun 14 und der aktuelle Element Zähler auf 7
der zugehörige sml Block sieht so aus.
SML_PublicOpen.Res ::= SEQUENCE
{
codepage Octet String OPTIONAL, 8 Elemt Zähler
clientId Octet String OPTIONAL, 9
reqFileId Octet String, 10
serverId Octet String, 11 = max-3
refTime SML_Time OPTIONAL,
smlVersion Unsigned8 OPTIONAL,
}
bei den optionalen Einträgen ist entweder etwas gültiges eingetragen oder es handelt sich um ein leeres Element 0x01.
Die gesuchte serv_ID steht auf dem Element_serverid und an der Position maxElemnt -3. Fall es zusätzliche Listeneinträge vorher gegeben hätte würde das berücksichtigt, falls danach wäre das egal.
- gesuchter OBIS Code.
hier wird nach dem OBIS Code gesucht. Es handelt sich um den sml Block SML_GetList.Res.
und da der Block in dem der OBIS Code enthalten ist.
die Listenagabe startet mit
77
SML_ListEntry ::= SEQUENCE
{
objName Octet String, (enthält den OBIS code ) Elemtzähler startet hier mit 1
status SML_Status OPTIONAL, Elementzähler 2
valTime SML_Time OPTIONAL, 3
unit SML_Unit OPTIONAL, 4
scaler Integer8 OPTIONAL, 5
value SML_Value, 6 ( maxElemnt -1 )
valueSignature SML_Signature OPTIONAL
}
Bei dem gesuchten OBIS Code handelt es sich um den Eintrag objName, ab der Fundstelle wird ausgewerte. Dem Ansatz oben folgend ist das Element SML_Value bei maxElement -1 gefunden. Falls optionale Listen enthalten sind , z.B SML_Time , wird das wieder wie oben berücksichtigt. Die Elemente SML_Unit und scaler könnten auf gleiche Art ausgewertet werden. Nicht genutze optionale Einträge sind wieder als leere Elemente 0X01 vorhanden.
Nun muss ich zugeben das ich bei den möglichen Datentypen 64Bit nicht den vollen Umfang ausgenutzt habe. Aber da warte ich mal ab.
Wem da jetzt noch böse Fallen auffallen ich bin da offen für jeden Hinweis und jede Kritik.
Der Sketch hat eine debug print Ausgabe , eingeschaltet werden der Hex Dump und die Auswertung der Elemente und Listeneinträge auf dem Monitor angegeben.
Getestet hab ich das zunächst mit einem UNO als Simulator, der gibt die Daten eines HEX Dumps auf die normale Serielle mit 9600 aus. So konnte ich auch die drei unterschiedlichen Dumps testen.
Achtung !! Bei dem Test mit einen UNO beachten das der 5V auf TX ausgibt der ESP aber nur 3,3 V auf RX kann. Spannungsteiler verwenden. z.B 2KOhm und 1KOhm.
so jetzt der zuerst die HTML der Sketch kommt später. (passt nicht mehr) und ein Screen Copy
HTML Seite
<!doctype html>
<html lang="de">
<!--
Einbindung der externen Chart Lib https://www.chartjs.org
Chart.js v2.9.4
* https://www.chartjs.org
* (c) 2020 Chart.js Contributors
* Released under the MIT License
*/
-->
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Stromzähler</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded',()=>{
document.querySelector('#btn').addEventListener('click',sendData);
var P=[];
var xachse=[];
// objekt instanz erstellen
var config1=newConfig();
// Vorenstellung
config1.data.datasets[0].label="Leistung [w]";
//config1.data.datasets[0].borderColor="red";
//config1.data.datasets[1].borderColor="blue";
var ctx1 = document.getElementById("chart-1").getContext('2d');
window.myChart1=new Chart(ctx1,config1);
function newConfig(){
var config={
type: 'line',
data: {
labels: xachse,
datasets: [{
label: 'Reihe1',
borderColor: "red",
backgroundColor:"black",
borderWidth: 1,
pointRadius:2,
data:P,
fill:false,
}
]
},
options:{
title: {
display: false,
text: 'Chart'
},
animation: {
duration: 0 // general animation time
},
scales:{
xAxes:[{
display: true,
scaleLabel: {
display: true,
labelString: 'Vergangene Minuten'
}
}],
yAxes: [{
ticks:{
//precision:0.05
},
display: true,
scaleLabel: {
display: false,
labelString: ''
}
}]
}
} // options ende
};
return config;
}
async function loadData(){
let resp = await fetch('/daten')
let obj = await resp.json();
document.getElementById("kennung").innerHTML=obj.kn;
document.getElementById("kw180").innerHTML=obj.C180+" KWh";
document.getElementById("kw181").innerHTML=obj.C181+" KWh";
document.getElementById("kw280").innerHTML=obj.C280+" KWh";
document.getElementById("P").innerHTML=obj.P+" W";
document.getElementById("logtime").value=obj.logtime;
config1.data.labels = obj.time;
config1.data.datasets[0].data = obj.PChart;
window.myChart1.update();
}
async function loadcycle(){
let resp = await fetch('/cycle')
let obj = await resp.json();
document.getElementById("kennung").innerHTML = obj.kn;
document.getElementById("kw180").innerHTML = obj.C180+" KWh";
document.getElementById("kw181").innerHTML = obj.C181+" KWh";
document.getElementById("kw280").innerHTML = obj.C280+" KWh";
document.getElementById("P").innerHTML = obj.P+" W";
config1.data.labels = obj.time;
config1.data.datasets[0].data = obj.PChart;
window.myChart1.update();
}
async function sendData(){
let data = document.querySelector('form');
let resp=await fetch('btnsend',{
method:'post',
body:new FormData(data)
});
let obj=await resp.json();
document.getElementById("logtime").value = obj.logtime;
}
loadData();
setInterval(loadcycle, 10000);
});
</script>
<style>
body {
background-color: #bdb;
display: flex;
flex-flow: column;
font-size: 1.3em;
}
.flex-container {
display: flex;
flex-direction:column;
gap:1em;
}
.flex-item {
border: 2px solid;
margin: .5em;
padding: .5em;
min-width:20em;
}
.flex-item:nth-of-type(1) {
background: #fdfcf3;
}
.flex-item:nth-of-type(2) {
background: #ffebeb;
}
/* große Viewports */
@media all and (min-width:45em) {
.flex-container {
flex-direction:row;
}
.flex-item:nth-of-type(2) {
min-width:30em
}
}
div{
width:auto
}
button{
width:5em;
height:1.5em;
font-size:1em;
}
input{
width:4.5em;
height:1em;
font-size:1em;
}
</style>
</head>
<body>
<h1>Stromzähler</h1>
<main class="flex-container">
<section class="flex-item">
<h3>Zählerstand</h3>
Zähler :<span id="kennung"></span>
<table>
<tr>
<td>Bezug gesamt </td><td><span id="kw180"></span></td>
</tr>
<tr>
<td>Bezug T1 </td><td><span id="kw181"></span></span></td>
</tr>
<tr>
<td>Einspeisung </td><td><span id="kw280"></span></td>
</tr>
<tr>
<td>akt. Leistung </td><td><span id="P"></span></td>
</tr>
</table>
<form>
<p>
Log Zyklus
<input type="text" id="logtime" name="logtime">min
<button type="button" id="btn">send</button>
</p>
</form>
</section>
<section class="flex-item">
<h3> Leistung Verlauf</h3>
<div>
<canvas id="chart-1"> </canvas>
</div>
</section>
</main>
</body>
</html>
Screen