Logging 2 Electricity Smart Meters Using Arduino Nano Every
Data logging from two electricity meters (type "eHZ") simultaneously and store data (time and value of reading) to an SD card.
Components and supplies
Resistor 1M ohm
Arduino Nano Every
LED, Blue
VMA 304 SD card shield
2 wire cable (phototransistor PCB)
5 mm LED: Red
IR Phototransistor, NPN, SFH309FA
Custom PCB
DC Power Connector, Jack
Level shifter, bidirectional, 5V 3.3V
BC337-25 Transistor, NPN
VMA 301 RTC shield
5 mm LED: Yellow
Through Hole Resistor, 220 ohm
Resistor 22.1k ohm
Tools and machines
Solder Wire, Lead Free
Soldering iron (generic)
Project description
Code
eHZ-IW8E2A5_EMLogger_ArdNanoEvery_TMG_09.ino
arduino
Recording of 2 eHZ readings to an SD card incl. supply voltage monitoring and recording
1/* 2 * Read output of 2 electricity meters of type eHZ-IW8E2A5 and write data to SD card. 3 * 4 * Written by Torsten Geppert (2020/2021) based on ideas/parts of code from 5 * - https://www.volkszaehler.org/ 6 * - and hardware / software by Volker Wegert https://sourceforge.net/p/ehzy/wiki/Hardware/ 7 * - and "set_clock.ino" by Matt Sparks, 2009, http://quadpoint.org/projects/arduino-ds1302 (seems to be one of the very few FUNCTIONAL DS1302 libraries out there in the www) 8 * 9 * Major changes/adjustments 10 * 1) Hardware modified. Replaced resistors 11 * - between IR diode and GND: 15 kOhm --> 1 MOhm (470 kOhm also sufficient). Necessary to bring BC337(-25) transistor to achieve the necessary gain (probably because of lower gain of BC337-25 as compared to BC337-40 or maybe signal of IR sender of eHZ too weak or alingment IR sender to IR receiver not precise enough 12 * - between collector of transistor and VCC: 15 kOhm --> 22 kOhm 13 * 2) w/ resepect to Volker Wegert's code: 14 * - Changes in syntax of PROGMEM related texts and corresponding array necessary (probably due to other version of IDE as compared to Volker Wegert back in in 2012) 15 * - in the end removal of parts of code (e.g piezo control),I did not use in my final code (Nevertheless, those things were really helpfull for debugging during development phase) 16 * 3) Change in Arduino related Hardware 17 * - Use Arduino Nano Every + DS1302 based RTC module and SD card shield (instead of Arduino Uno + SD card shield incl. RTC). 18 * Reason: Limitation of Arduino Uno's RAM, making code run unstable w/ already 1 electricity meter (of type eHZ-IW8E2A5). It was not possible to realize readout of 2 eHZs using Arduino UNO (at least my experience/impression) 19 * - HW used: 20 * - RTC: VMA301 by Velleman (e.g., from https://www.conrad.de/de/p/makerfactory-echtzeit-uhr-vma301-passend-fuer-arduino-boards-arduino-arduino-uno-fayaduino-freeduino-seeeduino-1612764.html) 21 * - SD shield: VMA304 by Velleman (e.g., https://www.conrad.de/de/p/makerfactory-sd-karten-logging-shield-fuer-arduino-2-tlg-1612765.html) 22 * - Level shifter (needed to adjust output from Arduino (5V) to SD shield (3.3V). In case NOT used the SD (shield ?) card might be irreversibly damaged !!! 23 * - added 3 LEDs to show SUCCESS or FAILURE of SD card initialization and data aqcuisition 24 * - added piezo speaker (accoustic warning in case supply voiltage - e.g., from battery) drops below defineable limit 25 * - additional volatge display (manually triggered by push-button (NOT connected to/controlled by Arduino) 26 * 4) Time of RTC shield can be set in section "RTC setup". Should be sufficient to do this initially (i.e., when you record your data for the first time and have to set the actual time) or in case RTC time deviates too much from real time (e.g., due to drift of oscillator in RC module due to missing temperature compensatioen or similar) 27 * see comment below "Uncomment in case current time should be written to RTC shield" 28 * 5) Ouptut into 1 file, at defined interval, for two meters type eHZ-IW8E2A5. (Unfortunately - and currently not understood - sometimes data is written into several files. However, due to naming (increasing number) it is stil possible to bring those files in (temporal) order. In addition time stamp is written into file for each datapoint aacquired. 29 * Output data: OBIS Kennzahl 1.8.0 (at Position 9x16+12 = 156 of SML protocol (In buffer w/ 1st element being number 0: position 155). 4 bytes (right after HEX value "56" at position 155 ## ## ## ## ##) 30 * To understand the SML protocol (and where to find the OBIS data I was interested in) Stefan Weigerts explanation is highly appreciated: http://www.stefan-weigert.de/php_loader/sml.php 31 */ 32 33/* ======================================================================================================= 34 * Include Libraries and alike 35 * ======================================================================================================= */ 36 37#include <stdarg.h> 38#include <stdio.h> 39#include <avr/pgmspace.h> 40#include <SoftwareSerial.h> // Serial interfaces realized by software (to allow, e.g., reading of more than just 1 sensor/eHz) 41#include <SD.h> // e.g., to write files to SD card etc. 42#include <SPI.h> // needed for SD.h 43#include <DS1302.h> // needed for RTC shield 44 45/*************************************************************************************************** 46 * CONSTANTS and alike 47 ***************************************************************************************************/ 48 49// Software Serial stuff ============================================================================================= 50// Serial Data from source 1 (IR diode of 1st eHZ) 51#define IR_RX_PIN_1 A0 // Pin 14 on Arduino Nano Every 52#define IR_TX_PIN_1 A2 // not used, but required for SoftwareSerial; Pin 16 on Arduino Nano Every 53// Serial Data from source 2 (IR diode of 2nd eHZ) 54#define IR_RX_PIN_2 A1 // Pin 14 on Arduino Nano Every 55#define IR_TX_PIN_2 A2 // not used, but required for SoftwareSerial; Pin 16 on Arduino Nano Every 56// Dummy software serial 57#define DUMMY_RX_PIN 9 // not connected 58#define DUMMY_TX_PIN A3 // not connected 59 60// SD card shield VMA304 stuff ============================================================================================= 61#define SD_CS_PIN 8 // Pin for SD card selection.Typically use hardware SS pin. (Pin 10 on most Arduino boards; however pin 8=A12 on Arduino Nano Every) 62#define SD_SS_PIN 10 // ###really needed ? Check if can be deleted ### 63 64// RTC stuff shield VMA301 stuff =========================================================================================== 65// uses Pins A3, A4, A5 on Arduino Nano Every (Physical Pins 7, 8, 9; aka ???, SDA, SCL) 66const int kCePin = 17; // Chip Enable 67const int kIoPin = 18; // Input/Output 68const int kSclkPin = 19; // Serial Clock 69 70// Other stuff ============================================================================================= 71#define loggingInterval 3600000 // time between readings in milli-seconds, e.g., 72 // 5 min = 5 * 60 * 1000 = 300 000 73 // 60 min = 60 * 60 * 1000 = 3 600 000 74// supply voltage monitoring stuff =========================================================================================== 75#define buzzPin 3 // Buzzer connected to PWM Pin D3 76#define supplVoltPin A2 // center contact of voltage divider (GND <-> supply Voltage (NOT 5V ! but V_in)) 77#define operatVolt 5 // operating voltage of Arduino (used as reference for ADC) 78#define buzzTimeEarly 8 // earliest time (hour of day; e.g., 8 AM = 8; 10PM -> 22) buzzer is triggered in case supply voltage below limit 79#define buzzTimeLate 20 // latest time (hour of day) buzzer is triggered in case supply voltage below limit 80float supplVoltLim = 5.6; // absolute supply voltage limit; serves as trigger for buzzer 81 82int numberSMLBytesRead = 0; // used to restart reading of SML data in case less than specified number of bytes (typically 397 for my electricity meter) read 83 84const int LED_WriteWarning = 4; // Pin D4 (physical pin no. 22): LED to signalize approaching or ongoing write to SD card 85const int LED_SML_Acquisition_eHZ1 = 5; // Pin PD1 (phyiscal pin no. 10): LED to signalize status of SML data acquisition for eHZ1 (OK: flashiung @ 1Hz; NOK: flashing @4 Hz*) 86const int LED_SML_Acquisition_eHZ2 = 6; // Pin A13 (phyical pin no. 24): LED to signalize status of SML data acquisition for eHZ1 (OK: flashiung @ 1Hz; NOK: flashing @4 Hz*); for whatever reason does NOT work w/ originally intended Pin D7 (phys pin 3; AVR Pin 39) 87// *frequency can be changed below to, e.g. 2 Hz by setting on time to 500 88int eHZ1Blink_ON=1000; // time LED for eHZ1 is turned on 89int eHZ2Blink_ON=1000; // time LED for eHZ2 is turned on 90int eHZ1Blink_OFF=250; // time LED for eHZ1 is turned off 91int eHZ2Blink_OFF=250; // time LED for eHZ2 is turned off 92bool SML_DataAcquisition_eHZ1 = false; // TRUE: SML data for eHZ1 acquired successfully; FALSE: error (e.g., buffer overflow, wrong header) during SML data acquisition for eHZ1 93bool SML_DataAcquisition_eHZ2 = false; // TRUE: SML data for eHZ2 acquired successfully; FALSE: error (e.g., buffer overflow, wrong header) during SML data acquisition for eHZ2 94const int eHZ1_ID = 1; 95const int eHZ2_ID = 2; 96unsigned long startTime; 97 98/** 99 * size of the SML message buffer = meximum number of bytes that can be received 100 */ 101#define SML_MSG_BUFFER_SIZE 400 // eHZ-IW8E2A5 delivers SML telegram of 396 bytes 102 103/** 104 * size of a filename (8.3 + terminating 0x00 = 13 chars) 105 */ 106#define FILENAME_SIZE 13 107 108/** 109 * maximum number of files that can be stored 110 */ 111#define MAX_FILE_NUMBER 99999999 112 113/** 114 * maximum time to wait for the end of a data packet received via IR 115 */ 116#define SERIAL_READ_TIMEOUT_MS 500 117 118/*************************************************************************************************** 119 * MESSAGES 120 ***************************************************************************************************/ 121 122/** 123 * maximum length of a message (entire text after variable substitution!) 124 */ 125#define MAX_MESSAGE_LENGTH 50 126 127/** 128 * message numbers 129 */ 130#define MSG_PROGRAM_STOPPED 0 131#define MSG_NEXT_FILENAME 1 132#define MSG_NO_FILE_NAMES_LEFT 2 133#define MSG_INIT_HARDWARE 3 134#define MSG_INIT_SD_CARD 4 135#define MSG_INIT_SD_CARD_ERROR 5 136#define MSG_BYTE_READ 6 137#define MSG_BUFFER_OVERFLOW 7 138#define MSG_SERIAL_OVERFLOW 8 139#define MSG_INVALID_HEADER 9 140#define MSG_FILE_OPEN_FAILED 10 141#define MSG_FILE_WRITTEN 11 142#define MSG_FREE_MEMORY 12 143#define MSG_NUM_BYTES_READ 13 144 145/** 146 * actual message texts - caution, adapt MAX_MESSAGE_LENGTH if required! 147 * ....+....1....+....2....+....3....+....4....+....5 148 */ 149const char msgText00[] PROGMEM = "Program stopped."; 150const char msgText01[] PROGMEM = "Next output file name is '%s'"; 151const char msgText02[] PROGMEM = "No more file names left"; 152const char msgText03[] PROGMEM = "Initializing Hardware..."; 153const char msgText04[] PROGMEM = "Initializing SD Card..."; 154const char msgText05[] PROGMEM = "SD Card initialization failed"; 155const char msgText06[] PROGMEM = "Read byte %02hhu from IR receiver"; 156const char msgText07[] PROGMEM = "Message buffer overflow"; 157const char msgText08[] PROGMEM = "Serial buffer overflow"; 158const char msgText09[] PROGMEM = "Invalid escape sequence"; 159const char msgText10[] PROGMEM = "Unable to open output file"; 160const char msgText11[] PROGMEM = "%u bytes of data written to file '%s'"; 161const char msgText12[] PROGMEM = "%u bytes of memory available"; 162const char msgText13[] PROGMEM = "%u bytes read"; 163 164/** 165 * table for easier access to the message texts 166 */ 167const char *const msgTextTable[] PROGMEM = { 168 msgText00, 169 msgText01, 170 msgText02, 171 msgText03, 172 msgText04, 173 msgText05, 174 msgText06, 175 msgText07, 176 msgText08, 177 msgText09, 178 msgText10, 179 msgText11, 180 msgText12, 181 msgText13 182}; 183 184/*************************************************************************************************** 185 * GLOBAL VARIABLES (YUCK!) 186 ***************************************************************************************************/ 187 188/** 189 * instance of the SoftwareSerial library to handle the IR communication 190 */ 191SoftwareSerial mySerial_1(IR_RX_PIN_1, IR_TX_PIN_1); // used for readout of SML data of eHZ1 192SoftwareSerial mySerial_2(IR_RX_PIN_2, IR_TX_PIN_2); // used for readout of SML data of eHZ2 193SoftwareSerial dummySerial(DUMMY_RX_PIN, DUMMY_TX_PIN); 194 195// Create a RTC DS1302 object. 196DS1302 rtc(kCePin, kIoPin, kSclkPin); 197 198/** 199 * variables to keep track of the name of the next file to be written 200 */ 201unsigned long nextFileNumber = 0; 202char nextFileName[FILENAME_SIZE]; 203 204/** 205 * the global buffer to store the SML message currently being read 206 */ 207unsigned char buffer[SML_MSG_BUFFER_SIZE]; 208 209/** 210 * Readings of eHZ 1 and 2 (after extraction from SML data) and corresponding timestamps 211 */ 212unsigned long eHZ_1_Reading; 213unsigned long eHZ_2_Reading; 214char eHZ_1_Reading_timeStamp[50]; 215char eHZ_2_Reading_timeStamp[50]; 216 217/** 218 * Supply voltage 219 */ 220float supplVolt; 221 222/*************************************************************************************************** 223 * SUBROUTINES 224 ***************************************************************************************************/ 225 226/** 227 * printMessage - reads a message text from the PROGMEM, performs variable substitution and 228 * writes the resulting text to the serial console. Use MSG_* constants for 229 * messageNumber. 230 */ 231void printMessage(int messageNumber, ...) { 232 va_list args; 233 char format[MAX_MESSAGE_LENGTH]; 234 char buffer[MAX_MESSAGE_LENGTH]; 235 va_start(args, messageNumber); 236 strncpy_P(format, (char*)pgm_read_word(&(msgTextTable[messageNumber])), MAX_MESSAGE_LENGTH); 237 vsnprintf(buffer, MAX_MESSAGE_LENGTH, format, args); 238 Serial.println(buffer); 239 va_end(args); 240} 241 242/** 243 * reportFreeMemory - determines the amount of free memory and logs it to the serial console. 244 */ 245void reportFreeMemory() { 246 int freeMemory; 247 uint8_t * heapptr, * stackptr; 248 stackptr = (uint8_t *) malloc(4); // use stackptr temporarily 249 heapptr = stackptr; // save value of heap pointer 250 free(stackptr); // free up the memory again (sets stackptr to 0) 251 stackptr = (uint8_t *) (SP); // save value of stack pointer 252 freeMemory = stackptr - heapptr; 253 printMessage(MSG_FREE_MEMORY, freeMemory); 254} 255 256/** 257 * stop - stops program execution in a controlled fashion (endless loop). 258 */ 259void stop() { 260 printMessage(MSG_PROGRAM_STOPPED); 261 while(1); 262} 263 264/** 265 * findNextFileNumber - determines the number and name of the next file available. This will change 266 * the global variables nextFileName and nextFileNumber. This routine will 267 * stop the entire program if no free filenames can be found. 268 */ 269void findNextFileNumber() { 270 do { 271 sprintf(nextFileName, "%08lu.CSV", nextFileNumber); 272 if (!SD.exists(nextFileName)) { 273 printMessage(MSG_NEXT_FILENAME, nextFileName); 274 return; 275 } else { 276 nextFileNumber += 1; 277 if (nextFileNumber > MAX_FILE_NUMBER) { 278 printMessage(MSG_NO_FILE_NAMES_LEFT); 279// errorSound(ERR_NO_FILE_NAMES_LEFT); 280 stop(); 281 } 282 } 283 } while (true); 284} 285 286void createTimeStamp(int eHZ_Number) { 287 // Get the current time and date from the chip. 288 Time t = rtc.time(); 289 // Format the time and date and insert into the temporary buffer. 290 char currDAndT_buf[50]; 291 snprintf(currDAndT_buf, sizeof(currDAndT_buf), "%04d-%02d-%02d %02d:%02d:%02d;", 292 t.yr, t.mon, t.date, 293 t.hr, t.min, t.sec); 294 295 // assign determined timestamp to either eHZ1 or eHz2 296 if ( eHZ_Number == eHZ1_ID ) { 297 for (int i = 0; i<50; i++) { 298 eHZ_1_Reading_timeStamp[i]=currDAndT_buf[i]; 299 } 300//Serial.println("eHZ_1 Timestamp copied"); 301 } 302 if ( eHZ_Number == eHZ2_ID ) { 303 for (int i = 0; i<50; i++) { 304 eHZ_2_Reading_timeStamp[i]=currDAndT_buf[i]; 305 } 306//Serial.println("eHZ_2 Timestamp copied"); 307 } 308} 309 310void writeTimeAndReadingToFile() { 311 Serial.println("Writing data to SD card..."); 312 313 File outputFile; 314 315 outputFile = SD.open(nextFileName, FILE_WRITE); 316 if (!outputFile) { 317 printMessage(MSG_FILE_OPEN_FAILED); 318 } 319 else { 320 outputFile.print(eHZ_1_Reading_timeStamp); 321 outputFile.print(eHZ_1_Reading); 322 outputFile.print(";"); 323 outputFile.print(eHZ_2_Reading_timeStamp); 324 outputFile.print(eHZ_2_Reading); 325 outputFile.print(";"); 326 outputFile.print(eHZ_2_Reading_timeStamp); 327 outputFile.println(supplVolt); 328 outputFile.close(); 329 Serial.println("...done..."); 330 } 331} 332 333void getSMLData(SoftwareSerial mySoftSer, int eHZ_Number) { 334 unsigned int nextBufferPosition = 0; 335 unsigned long lastReadTime = 0; 336 337 // clear the message buffer 338 memset(buffer, 0, sizeof(buffer)); 339 340 // initialize data acquisition LEDs assuming everything is fine 341 if ( eHZ_Number == eHZ1_ID ) { // remember NO error during SML data acquisition for eHZ1 342 SML_DataAcquisition_eHZ1 = true; 343 } 344 if ( eHZ_Number == eHZ2_ID ) { // remember NO error during SML data acquisition for eHZ2 345 SML_DataAcquisition_eHZ2 = true; 346 } 347 348 // remove a pending overflow flag (might have been left over from a previous run) 349 // and ensure that the SoftwareSerial library is listening 350 mySoftSer.overflow(); 351 mySoftSer.listen(); 352 353 // wait until actual data is available 354 while (!mySoftSer.available()); 355 // Next 2 lines ("Serial.print...") not necessary for algorithm, however might be useful for information purpose on serial monitor. 356 // In addition reading of serial data SEEMs to be more stable (less errors such as "invalid escape sequence"). Maybe 357 // something like "delay(200);" might also work... 358 Serial.print("Number of bytes available at SW serial:"); 359 Serial.println(mySoftSer.available()); 360 361 // keep reading data until either the message buffer is filled or no more data was 362 // received for SERIAL_READ_TIMEOUT_MS ms 363 364 lastReadTime = millis(); 365 while (millis() - lastReadTime < SERIAL_READ_TIMEOUT_MS) { 366 if (mySoftSer.available()) { 367 buffer[nextBufferPosition] = mySoftSer.read(); 368 lastReadTime = millis(); 369 if (nextBufferPosition >= SML_MSG_BUFFER_SIZE) { 370 dummySerial.listen(); // disable further IR input by switching software serial input to a dummy instance 371 printMessage(MSG_BUFFER_OVERFLOW); 372 373 if ( eHZ_Number == eHZ1_ID ) { // remember error during SML data acquisition for eHZ1 374 SML_DataAcquisition_eHZ1 = false; 375 } 376 if ( eHZ_Number == eHZ2_ID ) { // remember error during SML data acquisition for eHZ2 377 SML_DataAcquisition_eHZ2 = false; 378 } 379 return; 380 } 381 nextBufferPosition += 1; 382 numberSMLBytesRead = nextBufferPosition+1; // used to restart reading of SML data in case less than specified number of bytes (typically 397 for my electricity meter) read 383 } 384 } 385 386 // report an error if an overflow condition was encountered - the data received is useless 387 // in this case :-( 388 if (mySoftSer.overflow()) { 389 dummySerial.listen(); // disable further IR input 390 printMessage(MSG_SERIAL_OVERFLOW); 391 392 if ( eHZ_Number == eHZ1_ID ) { // remember error during SML data acquisition for eHZ1 393 SML_DataAcquisition_eHZ1 = false; 394 } 395 if ( eHZ_Number == eHZ2_ID ) { // remember error during SML data acquisition for eHZ2 396 SML_DataAcquisition_eHZ2 = false; 397 } 398 } 399 else { 400 dummySerial.listen(); // disable further IR input 401 // check the header 402 printMessage(MSG_NUM_BYTES_READ, nextBufferPosition + 1); 403 if (!isValidHeader()) { 404 // not a valid header - notify the user... 405 printMessage(MSG_INVALID_HEADER); 406 // ...and empty the receiver buffer (wait for the end of the current data stream 407 while (mySoftSer.available() > 0) { 408 mySoftSer.read(); 409 } 410 if ( eHZ_Number == eHZ1_ID ) { // remember error during SML data acquisition for eHZ1 411 SML_DataAcquisition_eHZ1 = false; 412 } 413 if ( eHZ_Number == eHZ2_ID ) { // remember error during SML data acquisition for eHZ2 414 SML_DataAcquisition_eHZ2 = false; 415 } 416 } 417 } 418} 419 420unsigned long getElectricityMeterReading(char SML_buff) { 421 /* Returns electricity meter reading in kWh 422 * Electricity meter reading (for eHZ-IW8E2A5) is represented by the five bytes at buffer positions 155-159. 423 * Make those 5 bytes to 1 HEX value, convert to DEC, divide by 10 (due to sacler in eHZ), divide by 1000 to get kWh 424 * However, use only lowest 4 bytes (i.e., meaningful data start at position 156). Highest number represented by these four bytes is 425 * in HEX FF FF FF FF or in DEC 4294967295 (= 2^32). This would correspond to approx. 429 496 kWh. 426 * Assuming an (realistic) average energy consumption of about 5000 kWh per year for the heat pump, this would allow 427 * to store the meter reading for about 85 years ! 428 * Therefore use data type unsigned long (allows values from 0 ... 2^32-1 = 0 ... 4 294 967 295) 429 */ 430 unsigned char EMReading_buf[4]; 431 unsigned long EMReading; 432 433 // clear the EMReading buffer 434 memset(EMReading_buf, 0, sizeof(EMReading_buf)); 435 436 // get 4 bytes representing the electricity meter reading and write them in opposite order to new array. Then create unsigned long. 437 // Algorhithm see https://forum.arduino.cc/index.php?topic=108865.0 438 for(int i=3; i>=0; i--) { 439 EMReading_buf[3-i] = (buffer[156+i]); 440 } 441 EMReading = *((unsigned long *) (& EMReading_buf[0])); 442 EMReading = EMReading/10; // consider scaler in SML data -> divide by 10 to get Wh 443 return EMReading; 444} 445 446/** 447 * isValidHeader - returns true if the global message buffer begins with a valid SML escape sequence. 448 */ 449inline boolean isValidHeader() { 450 return ((buffer[0] == 0x1b) && 451 (buffer[1] == 0x1b) && 452 (buffer[2] == 0x1b) && 453 (buffer[3] == 0x1b) && 454 (buffer[4] == 0x01) && 455 (buffer[5] == 0x01) && 456 (buffer[6] == 0x01) && 457 (buffer[7] == 0x01)); 458} 459 460/** 461 * for DS1302 based RTC time. Not necessarily (to be) used for 462 */ 463namespace { 464 465String dayAsString(const Time::Day day) { 466 switch (day) { 467 case Time::kSunday: return "Sunday"; 468 case Time::kMonday: return "Monday"; 469 case Time::kTuesday: return "Tuesday"; 470 case Time::kWednesday: return "Wednesday"; 471 case Time::kThursday: return "Thursday"; 472 case Time::kFriday: return "Friday"; 473 case Time::kSaturday: return "Saturday"; 474 } 475 return "(unknown day)"; 476} 477 478} // namespace 479 480 481/*************************************************************************************************** 482 * MAIN ROUTINES 483 ***************************************************************************************************/ 484 485/** ######################################################################################################## 486 * SETUP 487 ######################################################################################################## 488 */ 489void setup() { 490 // Serial communication 491 // ===================================================================================================== 492 Serial.begin(9600); 493 494 Serial.println("===== Starting setup ====="); 495 // Pin configuration 496 // ===================================================================================================== 497 printMessage(MSG_INIT_HARDWARE); 498 pinMode(IR_RX_PIN_1, INPUT); 499 pinMode(IR_TX_PIN_1, OUTPUT); 500 pinMode(IR_RX_PIN_2, INPUT); 501 pinMode(IR_TX_PIN_2, OUTPUT); 502 pinMode(SD_CS_PIN, OUTPUT); 503 pinMode(SD_SS_PIN, OUTPUT); 504 pinMode(DUMMY_RX_PIN, INPUT); 505 pinMode(DUMMY_TX_PIN, OUTPUT); 506 Serial.println("...done !... pinMode set !"); 507 508 // RTC setup 509 // ===================================================================================================== 510 // Initialize a new chip by turning off write protection and clearing the clock halt flag. These methods needn't always be called. See the DS1302 datasheet for details. 511 rtc.writeProtect(false); 512 rtc.halt(false); 513/* // Uncomment in case current time should be written to RTC shield 514 // Make a new time object to set the date and time and set the time 515 // Format: year, moth, day, hour, minute, second, day of the week (1=Sunday)) 516 Time t(2021, 3, 03, 21, 48, 0, 4); 517 rtc.time(t); 518*/ 519 520 // Setup of supply voltage monitoring 521 // ===================================================================================================== 522 Serial.println("Testing LEDs & piezo speaker..."); 523 pinMode(buzzPin, OUTPUT); 524 pinMode(supplVoltPin, INPUT); 525 // quick sound check to prove piezo speaker works (only during day time...) 526 527 Time t2 = rtc.time(); 528 if( t2.hr > buzzTimeEarly && t2.hr < buzzTimeLate ) { 529 for (int i = 0; i <5; i++) { 530 tone(buzzPin, 1000, 250); 531 delay(500); 532 tone(buzzPin, 5000, 250); 533 delay(500); 534 } 535 } 536 537 // Setup of data acquisition and diagnosis / warning LEDs 538 // ===================================================================================================== 539 pinMode(LED_SML_Acquisition_eHZ1, OUTPUT); 540 digitalWrite(LED_SML_Acquisition_eHZ1, LOW); 541 pinMode(LED_SML_Acquisition_eHZ2, OUTPUT); 542 digitalWrite(LED_SML_Acquisition_eHZ2, LOW); 543 pinMode(LED_WriteWarning, OUTPUT); 544 digitalWrite(LED_WriteWarning, LOW); 545 546 // ...let blink 5 times to proove the're working 547 //------------------------------------------------------------------------------------------------------- 548 Serial.println("Data acquisition LED 1 (BLUE): ...blink 5 times"); 549 for(byte i=0; i<5; i++) { 550 digitalWrite(LED_SML_Acquisition_eHZ1, HIGH); 551 delay(250); 552 digitalWrite(LED_SML_Acquisition_eHZ1, LOW); 553 delay(250); 554 } 555 Serial.println("...done !"); 556 Serial.println("Data acquisition LED 2 (YELLOW): ...blink 5 times"); 557 for(byte i=0; i<5; i++) { 558 digitalWrite(LED_SML_Acquisition_eHZ2, HIGH); 559 delay(250); 560 digitalWrite(LED_SML_Acquisition_eHZ2, LOW); 561 delay(250); 562 } 563 Serial.println("...done !"); 564 Serial.println("SD status LED (RED): ...blink 5 times"); 565 for(byte i=0; i<5; i++) { 566 digitalWrite(LED_WriteWarning, HIGH); 567 delay(250); 568 digitalWrite(LED_WriteWarning, LOW); 569 delay(250); 570 } 571 Serial.println("...done !"); 572 573 // SD card setup 574 // ===================================================================================================== 575 Serial.println("Initializing SD card..."); 576 if (!SD.begin(SD_CS_PIN)) { 577 printMessage(MSG_INIT_SD_CARD_ERROR); 578// errorSound(ERR_INIT_SD_CARD_ERROR); 579 stop(); 580 } 581 else { 582 Serial.println("...done !"); 583 Time t2 = rtc.time(); 584 if( t2.hr > buzzTimeEarly && t2.hr < buzzTimeLate ) { 585 for (int i = 0; i <10; i++) { 586 tone(buzzPin, 250, 250); 587 delay(500); 588 } 589 } 590 } 591 592 // Sofware Serial setup 593 // ===================================================================================================== 594 dummySerial.begin(9600); 595 mySerial_1.begin(9600); // eHZ sends at fix rate of 9600 Baud 596 mySerial_2.begin(9600); // eHZ sends at fix rate of 9600 Baud 597 598 // determine the first file name to use 599 findNextFileNumber(); 600 601} 602 603/** ######################################################################################################## 604 * END of SETUP 605 ######################################################################################################## 606 */ 607 608void loop() { 609 // turn off all LEDs 610 digitalWrite(LED_WriteWarning, LOW); 611 digitalWrite(LED_SML_Acquisition_eHZ1, LOW); 612 digitalWrite(LED_SML_Acquisition_eHZ2, LOW); 613// reportFreeMemory(); // for information purposes only 614 615 numberSMLBytesRead = 0; 616 while(numberSMLBytesRead!=397 || SML_DataAcquisition_eHZ1 == false){ // in case SML data is plausible (i.e., SML header is OK and also no buffer overflow) it still happens that eHZ reading is derived from this data. Examining this showed that in those cases typically less than 397 bytes SML data were read. This while loop/condition should prevent this. 617 getSMLData(mySerial_1, eHZ1_ID); // get SML data from 1st electricity meter (Input: A0), store it into (global) buffer 618 } 619 createTimeStamp(eHZ1_ID); 620 Serial.println("Time stamp eHZ_1 created..."); 621 eHZ_1_Reading = getElectricityMeterReading(buffer); // extract OBIS 1.8.0. in Wh x 10 and store it in (global) variable 622 Serial.print("eHZ_1 Reading: "); 623 Serial.println(eHZ_1_Reading); 624 625 numberSMLBytesRead = 0; 626 while(numberSMLBytesRead!=397 || SML_DataAcquisition_eHZ2 == false){ // in case SML data is plausible (i.e., SML header is OK and also no buffer overflow) it still happens that eHZ reading is derived from this data. Examining this showed that in those cases typically less than 397 bytes SML data were read. This while loop/condition should prevent this. 627 getSMLData(mySerial_2, eHZ2_ID); // get SML data from 2nd electricity meter (Input: A1), store it into (global) buffer 628 } 629 createTimeStamp(eHZ2_ID); 630 Serial.println("Time stamp eHZ_2 created..."); 631 eHZ_2_Reading = getElectricityMeterReading(buffer); // extract OBIS 1.8.0. in Wh x 10 and store it in (global) variable 632 Serial.print("eHZ_2 Reading: "); 633 Serial.println(eHZ_2_Reading); 634 635 supplVolt = analogRead(supplVoltPin) * 2 / 1024.0 * operatVolt; // get actual supply voltage in V (multiply analog reading by 2 due to 1:1 voltage divider 636 637 Serial.println(); 638 639 // turn ON red LED to show that writing is beeing performed and SD card should NOT be removed. Prewarn by blinking for about 3s. 640 // ----------------------------------------------------------------------------------------------------------------------------- 641 for(byte i=0; i<3; i++) { 642 digitalWrite(LED_WriteWarning, HIGH); 643 delay(500); 644 digitalWrite(LED_WriteWarning, LOW); 645 delay(500); 646 } 647 // Write data to SD card 648 // --------------------- 649 digitalWrite(LED_WriteWarning, HIGH); 650 // write data to file (time 1; reading 1; time 2; reading 2; time 2; supply voltage) 651 writeTimeAndReadingToFile(); 652 // turn OFF red LED to show that writing is FINISHED and SD card CAN be removed 653 digitalWrite(LED_WriteWarning, LOW); 654 delay(1000); 655 656 // Show result (SUCCESS/FAIL) of data acquisition for each channel 657 // --------------------------------------------------------------- 658 // set blinking behaviour of LED depending on result of SML data acquisition of eHZ1 659 if(SML_DataAcquisition_eHZ1 == true) { 660 eHZ1Blink_ON = 1000; 661 } 662 if(SML_DataAcquisition_eHZ1 == false) { 663 eHZ1Blink_ON = 250; 664 } 665 // set blinking behaviour of LED depending on result of SML data acquisition of eHZ1 666 if(SML_DataAcquisition_eHZ2 == true) { 667 eHZ2Blink_ON = 1000; 668 } 669 if(SML_DataAcquisition_eHZ2 == false) { 670 eHZ2Blink_ON = 250; 671 } 672 673 // wait for specified time interval until next data acquisition to be performed 674 startTime = millis(); 675 // show result of data acquisition by blinking slow (success) or fast (fail). slow/fast defined above. 676 while(millis()-startTime < loggingInterval){ 677 delay(60000); // only blink once a minute (to save battery energy) 678 for(int i=0; i<3; i++) { 679 digitalWrite(LED_SML_Acquisition_eHZ1, HIGH); 680 delay(eHZ1Blink_ON); 681 digitalWrite(LED_SML_Acquisition_eHZ1, LOW); 682 delay(eHZ1Blink_OFF); 683 } 684 for(int i=0; i<3; i++) { 685 digitalWrite(LED_SML_Acquisition_eHZ2, HIGH); 686 delay(eHZ2Blink_ON); 687 digitalWrite(LED_SML_Acquisition_eHZ2, LOW); 688 delay(eHZ2Blink_OFF); 689 } 690 // Sound buzzer in case supply voltage drops below defined limit; trigger alarm only during specified time slots. 691 Time t2 = rtc.time(); 692Serial.print("supplyVolt: "); 693Serial.println(supplVolt); 694 if( supplVolt < supplVoltLim && t2.hr > buzzTimeEarly && t2.hr < buzzTimeLate ) { 695 for (int i = 0; i <5; i++) { 696 tone(buzzPin, 1000, 250); 697 delay(750); 698 } 699 } 700 } 701 702} 703
eHZ-IW8E2A5_EMLogger_ArdNanoEvery_TMG_06.ino
arduino
Recording of 2 eHZ readings to an SD card.
1/* 2 * Read output of 2 electricity meters of type eHZ-IW8E2A5 and write data to SD card. 3 * 4 * Written by Torsten Geppert (2020) based on ideas/parts of code from 5 * - https://www.volkszaehler.org/ 6 * - and hardware / software by Volker Wegert https://sourceforge.net/p/ehzy/wiki/Hardware/ 7 * - and "set_clock.ino" by Matt Sparks, 2009, http://quadpoint.org/projects/arduino-ds1302 (seems to be one of the very few FUNCTIONAL DS1302 libraries out there in the www) 8 * 9 * Major changes/adjustments 10 * 1) Hardware modified. Replaced resistors 11 * - between IR diode and GND: 15 kOhm --> 1 MOhm (470 kOhm also sufficient). Necessary to bring BC337(-25) transistor to achieve the necessary gain (probably because of lower gain of BC337-25 as compared to BC337-40 or maybe signal of IR sender of eHZ too weak or alingment IR sender to IR receiver not precise enough 12 * - between collector of transistor and VCC: 15 kOhm --> 22 kOhm 13 * 2) w/ resepect to Volker Wegert's code: 14 * - Changes in syntax of PROGMEM related texts and corresponding array necessary (probably due to other version of IDE as compared to Volker Wegert back in in 2012) 15 * - in the end removal of parts of code (e.g piezo control),I did not use in my final code (Nevertheless, those things were really helpfull for debugging during development phase) 16 * 3) Change in Arduino related Hardware 17 * - Use Arduino Nano Every + DS1302 based RTC module and SD card shield (instead of Arduino Uno + SD card shield incl. RTC). 18 * Reason: Limitation of Arduino Uno's RAM, making code run unstable w/ already 1 electricity meter (of type eHZ-IW8E2A5). It was not possible to realize readout of 2 eHZs using Arduino UNO (at least my experience/impression) 19 * - HW used: 20 * - RTC: VMA301 by Velleman (e.g., from https://www.conrad.de/de/p/makerfactory-echtzeit-uhr-vma301-passend-fuer-arduino-boards-arduino-arduino-uno-fayaduino-freeduino-seeeduino-1612764.html) 21 * - SD shield: VMA304 by Velleman (e.g., https://www.conrad.de/de/p/makerfactory-sd-karten-logging-shield-fuer-arduino-2-tlg-1612765.html) 22 * - Level shifter (needed to adjust output from Arduino (5V) to SD shield (3.3V). In case NOT used the SD (shield ?) card might be irreversibly damaged !!! 23 * 4) Time of RTC shield can be set in section "RTC setup". Should be sufficient to do this initially (i.e., when you record your data for the first time and have to set the actual time) or in case RTC time deviates too much from real time (e.g., due to drift of oscillator in RC module due to missing temperature compensatioen or similar) 24 * see comment below "Uncomment in case current time should be written to RTC shield" 25 * 5) Ouptut into 1 file, at defined interval, for two meters type eHZ-IW8E2A5. (Unfortunately - and currently not understood - sometimes data is written into several files. However, due to naming (increasing number) it is stil possible to bring those files in (temporal) order. In addition time stamp is written into file for each datapoint aacquired. 26 * Output data: OBIS Kennzahl 1.8.0 (at Position 9x16+12 = 156 of SML protocol (In buffer w/ 1st element being number 0: position 155). 4 bytes (right after HEX value "56" at position 155 ## ## ## ## ##) 27 * To understand the SML protocol (and where to find the OBIS data I was interested in) Stefan Weigerts explanation is highly appreciated: http://www.stefan-weigert.de/php_loader/sml.php 28 */ 29 30/* ======================================================================================================= 31 * Include Libraries and alike 32 * ======================================================================================================= */ 33 34#include <stdarg.h> 35#include <stdio.h> 36#include <avr/pgmspace.h> 37#include <SoftwareSerial.h> // Serial interfaces realized by software (to allow, e.g., reading of more than just 1 sensor/eHz) 38#include <SD.h> // e.g., to write files to SD card etc. 39#include <DS1302.h> 40 41/*************************************************************************************************** 42 * CONSTANTS and alike 43 ***************************************************************************************************/ 44 45// Software Serial stuff ============================================================================================= 46// Serial Data from source 1 (IR diode of 1st eHZ) 47#define IR_RX_PIN_1 A0 // Pin 14 on Arduino Nano Every 48#define IR_TX_PIN_1 A2 // not used, but required for SoftwareSerial; Pin 16 on Arduino Nano Every 49// Serial Data from source 2 (IR diode of 2nd eHZ) 50#define IR_RX_PIN_2 A1 // Pin 14 on Arduino Nano Every 51#define IR_TX_PIN_2 A2 // not used, but required for SoftwareSerial; Pin 16 on Arduino Nano Every 52// Dummy software serial 53#define DUMMY_RX_PIN 9 // not connected 54#define DUMMY_TX_PIN A3 // not connected 55 56// SD card shield VMA304 stuff ============================================================================================= 57#define SD_CS_PIN 8 // Pin for SD card selection.Typically use hardware SS pin. (Pin 10 on most Arduino boards; however pin 8=A12 on Arduino Nano Every) 58#define SD_SS_PIN 10 // ###really needed ? Check if can be deleted ### 59 60// RTC stuff shield VMA301 stuff =========================================================================================== 61// uses Pins A3, A4, A5 on Arduino Nano Every (Physical Pins 7, 8, 9; aka ???, SDA, SCL) 62const int kCePin = 17; // Chip Enable 63const int kIoPin = 18; // Input/Output 64const int kSclkPin = 19; // Serial Clock 65 66// Other stuff ============================================================================================= 67#define loggingInterval 300000 // time between readings in milli-seconds (e.g., 5 min = 5 * 60 * 1000 = 300 000) 68 69int numberSMLBytesRead = 0; // used to restart reading of SML data in case less than specified number of bytes (typically 397 for my electricity meter) read 70 71const int LED_WriteWarning = 4; // Pin D4 (physical pin no. 22): LED to signalize approaching or ongoing write to SD card 72const int LED_SML_Acquisition_eHZ1 = 16; // Pin PD1 (phyiscal pin no. 10): LED to signalize status of SML data acquisition for eHZ1 (OK: flashiung @ 1Hz; NOK: flashing @4 Hz*) 73const int LED_SML_Acquisition_eHZ2 = 6; // Pin A13 (phyical pin no. 24): LED to signalize status of SML data acquisition for eHZ1 (OK: flashiung @ 1Hz; NOK: flashing @4 Hz*); for whatever reason does NOT work w/ originally intended Pin D7 (phys pin 3; AVR Pin 39) 74// *frequency can be changed below to, e.g. 2 Hz by setting on time to 500 75int eHZ1Blink_ON=1000; // time LED for eHZ1 is turned on 76int eHZ2Blink_ON=1000; // time LED for eHZ2 is turned on 77int eHZ1Blink_OFF=250; // time LED for eHZ1 is turned off 78int eHZ2Blink_OFF=250; // time LED for eHZ2 is turned off 79bool SML_DataAcquisition_eHZ1 = false; // TRUE: SML data for eHZ1 acquired successfully; FALSE: error (e.g., buffer overflow, wrong header) during SML data acquisition for eHZ1 80bool SML_DataAcquisition_eHZ2 = false; // TRUE: SML data for eHZ2 acquired successfully; FALSE: error (e.g., buffer overflow, wrong header) during SML data acquisition for eHZ2 81const int eHZ1_ID = 1; 82const int eHZ2_ID = 2; 83unsigned long startTime; 84 85/** 86 * size of the SML message buffer = meximum number of bytes that can be received 87 */ 88#define SML_MSG_BUFFER_SIZE 400 // eHZ-IW8E2A5 delivers SML telegram of 396 bytes 89 90/** 91 * size of a filename (8.3 + terminating 0x00 = 13 chars) 92 */ 93#define FILENAME_SIZE 13 94 95/** 96 * maximum number of files that can be stored 97 */ 98#define MAX_FILE_NUMBER 99999999 99 100/** 101 * maximum time to wait for the end of a data packet received via IR 102 */ 103#define SERIAL_READ_TIMEOUT_MS 500 104 105/*************************************************************************************************** 106 * MESSAGES 107 ***************************************************************************************************/ 108 109/** 110 * maximum length of a message (entire text after variable substitution!) 111 */ 112#define MAX_MESSAGE_LENGTH 50 113 114/** 115 * message numbers 116 */ 117#define MSG_PROGRAM_STOPPED 0 118#define MSG_NEXT_FILENAME 1 119#define MSG_NO_FILE_NAMES_LEFT 2 120#define MSG_INIT_HARDWARE 3 121#define MSG_INIT_SD_CARD 4 122#define MSG_INIT_SD_CARD_ERROR 5 123#define MSG_BYTE_READ 6 124#define MSG_BUFFER_OVERFLOW 7 125#define MSG_SERIAL_OVERFLOW 8 126#define MSG_INVALID_HEADER 9 127#define MSG_FILE_OPEN_FAILED 10 128#define MSG_FILE_WRITTEN 11 129#define MSG_FREE_MEMORY 12 130#define MSG_NUM_BYTES_READ 13 131 132/** 133 * actual message texts - caution, adapt MAX_MESSAGE_LENGTH if required! 134 * ....+....1....+....2....+....3....+....4....+....5 135 */ 136const char msgText00[] PROGMEM = "Program stopped."; 137const char msgText01[] PROGMEM = "Next output file name is '%s'"; 138const char msgText02[] PROGMEM = "No more file names left"; 139const char msgText03[] PROGMEM = "Initializing Hardware..."; 140const char msgText04[] PROGMEM = "Initializing SD Card..."; 141const char msgText05[] PROGMEM = "SD Card initialization failed"; 142const char msgText06[] PROGMEM = "Read byte %02hhu from IR receiver"; 143const char msgText07[] PROGMEM = "Message buffer overflow"; 144const char msgText08[] PROGMEM = "Serial buffer overflow"; 145const char msgText09[] PROGMEM = "Invalid escape sequence"; 146const char msgText10[] PROGMEM = "Unable to open output file"; 147const char msgText11[] PROGMEM = "%u bytes of data written to file '%s'"; 148const char msgText12[] PROGMEM = "%u bytes of memory available"; 149const char msgText13[] PROGMEM = "%u bytes read"; 150 151/** 152 * table for easier access to the message texts 153 */ 154const char *const msgTextTable[] PROGMEM = { 155 msgText00, 156 msgText01, 157 msgText02, 158 msgText03, 159 msgText04, 160 msgText05, 161 msgText06, 162 msgText07, 163 msgText08, 164 msgText09, 165 msgText10, 166 msgText11, 167 msgText12, 168 msgText13 169}; 170 171/*************************************************************************************************** 172 * GLOBAL VARIABLES (YUCK!) 173 ***************************************************************************************************/ 174 175/** 176 * instance of the SoftwareSerial library to handle the IR communication 177 */ 178SoftwareSerial mySerial_1(IR_RX_PIN_1, IR_TX_PIN_1); // used for readout of SML data of eHZ1 179SoftwareSerial mySerial_2(IR_RX_PIN_2, IR_TX_PIN_2); // used for readout of SML data of eHZ2 180SoftwareSerial dummySerial(DUMMY_RX_PIN, DUMMY_TX_PIN); 181 182// Create a RTC DS1302 object. 183DS1302 rtc(kCePin, kIoPin, kSclkPin); 184 185/** 186 * variables to keep track of the name of the next file to be written 187 */ 188unsigned long nextFileNumber = 0; 189char nextFileName[FILENAME_SIZE]; 190 191/** 192 * the global buffer to store the SML message currently being read 193 */ 194unsigned char buffer[SML_MSG_BUFFER_SIZE]; 195 196/** 197 * Readings of eHZ 1 and 2 (after extraction from SML data) and corresponding timestamps 198 */ 199unsigned long eHZ_1_Reading; 200unsigned long eHZ_2_Reading; 201char eHZ_1_Reading_timeStamp[50]; 202char eHZ_2_Reading_timeStamp[50]; 203 204/*************************************************************************************************** 205 * SUBROUTINES 206 ***************************************************************************************************/ 207 208/** 209 * printMessage - reads a message text from the PROGMEM, performs variable substitution and 210 * writes the resulting text to the serial console. Use MSG_* constants for 211 * messageNumber. 212 */ 213void printMessage(int messageNumber, ...) { 214 va_list args; 215 char format[MAX_MESSAGE_LENGTH]; 216 char buffer[MAX_MESSAGE_LENGTH]; 217 va_start(args, messageNumber); 218 strncpy_P(format, (char*)pgm_read_word(&(msgTextTable[messageNumber])), MAX_MESSAGE_LENGTH); 219 vsnprintf(buffer, MAX_MESSAGE_LENGTH, format, args); 220 Serial.println(buffer); 221 va_end(args); 222} 223 224/** 225 * reportFreeMemory - determines the amount of free memory and logs it to the serial console. 226 */ 227void reportFreeMemory() { 228 int freeMemory; 229 uint8_t * heapptr, * stackptr; 230 stackptr = (uint8_t *) malloc(4); // use stackptr temporarily 231 heapptr = stackptr; // save value of heap pointer 232 free(stackptr); // free up the memory again (sets stackptr to 0) 233 stackptr = (uint8_t *) (SP); // save value of stack pointer 234 freeMemory = stackptr - heapptr; 235 printMessage(MSG_FREE_MEMORY, freeMemory); 236} 237 238/** 239 * stop - stops program execution in a controlled fashion (endless loop). 240 */ 241void stop() { 242 printMessage(MSG_PROGRAM_STOPPED); 243 while(1); 244} 245 246/** 247 * findNextFileNumber - determines the number and name of the next file available. This will change 248 * the global variables nextFileName and nextFileNumber. This routine will 249 * stop the entire program if no free filenames can be found. 250 */ 251void findNextFileNumber() { 252 do { 253 sprintf(nextFileName, "%08lu.CSV", nextFileNumber); 254 if (!SD.exists(nextFileName)) { 255 printMessage(MSG_NEXT_FILENAME, nextFileName); 256 return; 257 } else { 258 nextFileNumber += 1; 259 if (nextFileNumber > MAX_FILE_NUMBER) { 260 printMessage(MSG_NO_FILE_NAMES_LEFT); 261// errorSound(ERR_NO_FILE_NAMES_LEFT); 262 stop(); 263 } 264 } 265 } while (true); 266} 267 268void createTimeStamp(int eHZ_Number) { 269 // Get the current time and date from the chip. 270 Time t = rtc.time(); 271 // Format the time and date and insert into the temporary buffer. 272 char currDAndT_buf[50]; 273 snprintf(currDAndT_buf, sizeof(currDAndT_buf), "%04d-%02d-%02d %02d:%02d:%02d;", 274 t.yr, t.mon, t.date, 275 t.hr, t.min, t.sec); 276 277 // assign determined timestamp to either eHZ1 or eHz2 278 if ( eHZ_Number == eHZ1_ID ) { 279 for (int i = 0; i<50; i++) { 280 eHZ_1_Reading_timeStamp[i]=currDAndT_buf[i]; 281 } 282//Serial.println("eHZ_1 Timestamp copied"); 283 } 284 if ( eHZ_Number == eHZ2_ID ) { 285 for (int i = 0; i<50; i++) { 286 eHZ_2_Reading_timeStamp[i]=currDAndT_buf[i]; 287 } 288//Serial.println("eHZ_2 Timestamp copied"); 289 } 290} 291 292void writeTimeAndReadingToFile() { 293 File outputFile; 294 295 outputFile = SD.open(nextFileName, FILE_WRITE); 296 if (!outputFile) { 297 printMessage(MSG_FILE_OPEN_FAILED); 298 } 299 else { 300 outputFile.print(eHZ_1_Reading_timeStamp); 301 outputFile.print(eHZ_1_Reading); 302 outputFile.print(";"); 303 outputFile.print(eHZ_2_Reading_timeStamp); 304 outputFile.println(eHZ_2_Reading); 305 306 outputFile.close(); 307 } 308} 309 310void getSMLData(SoftwareSerial mySoftSer, int eHZ_Number) { 311 unsigned int nextBufferPosition = 0; 312 unsigned long lastReadTime = 0; 313 314 // clear the message buffer 315 memset(buffer, 0, sizeof(buffer)); 316 317 // initialize data acquisition LEDs assuming everything is fine 318 if ( eHZ_Number == eHZ1_ID ) { // remember NO error during SML data acquisition for eHZ1 319 SML_DataAcquisition_eHZ1 = true; 320 } 321 if ( eHZ_Number == eHZ2_ID ) { // remember NO error during SML data acquisition for eHZ2 322 SML_DataAcquisition_eHZ2 = true; 323 } 324 325 // remove a pending overflow flag (might have been left over from a previous run) 326 // and ensure that the SoftwareSerial library is listening 327 mySoftSer.overflow(); 328 mySoftSer.listen(); 329 330 // wait until actual data is available 331 while (!mySoftSer.available()); 332 // Next 2 lines ("Serial.print...") not necessary for algorithm, however might be useful for information purpose on serial monitor. 333 // In addition reading of serial data SEEMs to be more stable (less errors such as "invalid escape sequence"). Maybe 334 // something like "delay(200);" might also work... 335 Serial.print("Number of bytes available at SW serial:"); 336 Serial.println(mySoftSer.available()); 337 338 // keep reading data until either the message buffer is filled or no more data was 339 // received for SERIAL_READ_TIMEOUT_MS ms 340 341 lastReadTime = millis(); 342 while (millis() - lastReadTime < SERIAL_READ_TIMEOUT_MS) { 343 if (mySoftSer.available()) { 344 buffer[nextBufferPosition] = mySoftSer.read(); 345 lastReadTime = millis(); 346 if (nextBufferPosition >= SML_MSG_BUFFER_SIZE) { 347 dummySerial.listen(); // disable further IR input by switching software serial input to a dummy instance 348 printMessage(MSG_BUFFER_OVERFLOW); 349 350 if ( eHZ_Number == eHZ1_ID ) { // remember error during SML data acquisition for eHZ1 351 SML_DataAcquisition_eHZ1 = false; 352 } 353 if ( eHZ_Number == eHZ2_ID ) { // remember error during SML data acquisition for eHZ2 354 SML_DataAcquisition_eHZ2 = false; 355 } 356 return; 357 } 358 nextBufferPosition += 1; 359 numberSMLBytesRead = nextBufferPosition+1; // used to restart reading of SML data in case less than specified number of bytes (typically 397 for my electricity meter) read 360 } 361 } 362 363 // report an error if an overflow condition was encountered - the data received is useless 364 // in this case :-( 365 if (mySoftSer.overflow()) { 366 dummySerial.listen(); // disable further IR input 367 printMessage(MSG_SERIAL_OVERFLOW); 368 369 if ( eHZ_Number == eHZ1_ID ) { // remember error during SML data acquisition for eHZ1 370 SML_DataAcquisition_eHZ1 = false; 371 } 372 if ( eHZ_Number == eHZ2_ID ) { // remember error during SML data acquisition for eHZ2 373 SML_DataAcquisition_eHZ2 = false; 374 } 375 } 376 else { 377 dummySerial.listen(); // disable further IR input 378 // check the header 379 printMessage(MSG_NUM_BYTES_READ, nextBufferPosition + 1); 380 if (!isValidHeader()) { 381 // not a valid header - notify the user... 382 printMessage(MSG_INVALID_HEADER); 383 // ...and empty the receiver buffer (wait for the end of the current data stream 384 while (mySoftSer.available() > 0) { 385 mySoftSer.read(); 386 } 387 if ( eHZ_Number == eHZ1_ID ) { // remember error during SML data acquisition for eHZ1 388 SML_DataAcquisition_eHZ1 = false; 389 } 390 if ( eHZ_Number == eHZ2_ID ) { // remember error during SML data acquisition for eHZ2 391 SML_DataAcquisition_eHZ2 = false; 392 } 393 } 394 } 395} 396 397unsigned long getElectricityMeterReading(char SML_buff) { 398 /* Returns electricity meter reading in kWh 399 * Electricity meter reading (for eHZ-IW8E2A5) is represented by the five bytes at buffer positions 155-159. 400 * Make those 5 bytes to 1 HEX value, convert to DEC, divide by 10 (due to sacler in eHZ), divide by 1000 to get kWh 401 * However, use only lowest 4 bytes (i.e., meaningful data start at position 156). Highest number represented by these four bytes is 402 * in HEX FF FF FF FF or in DEC 4294967295 (= 2^32). This would correspond to approx. 429 496 kWh. 403 * Assuming an (realistic) average energy consumption of about 5000 kWh per year for the heat pump, this would allow 404 * to store the meter reading for about 85 years ! 405 * Therefore use data type unsigned long (allows values from 0 ... 2^32-1 = 0 ... 4 294 967 295) 406 */ 407 unsigned char EMReading_buf[4]; 408 unsigned long EMReading; 409 410 // clear the EMReading buffer 411 memset(EMReading_buf, 0, sizeof(EMReading_buf)); 412 413 // get 4 bytes representing the electricity meter reading and write them in opposite order to new array. Then create unsigned long. 414 // Algorhithm see https://forum.arduino.cc/index.php?topic=108865.0 415 for(int i=3; i>=0; i--) { 416 EMReading_buf[3-i] = (buffer[156+i]); 417 } 418 EMReading = *((unsigned long *) (& EMReading_buf[0])); 419 EMReading = EMReading/10; // consider scaler in SML data -> divide by 10 to get Wh 420 return EMReading; 421} 422 423/** 424 * isValidHeader - returns true if the global message buffer begins with a valid SML escape sequence. 425 */ 426inline boolean isValidHeader() { 427 return ((buffer[0] == 0x1b) && 428 (buffer[1] == 0x1b) && 429 (buffer[2] == 0x1b) && 430 (buffer[3] == 0x1b) && 431 (buffer[4] == 0x01) && 432 (buffer[5] == 0x01) && 433 (buffer[6] == 0x01) && 434 (buffer[7] == 0x01)); 435} 436 437/** 438 * for DS1302 based RTC time. Not necessarily (to be) used for 439 */ 440namespace { 441 442String dayAsString(const Time::Day day) { 443 switch (day) { 444 case Time::kSunday: return "Sunday"; 445 case Time::kMonday: return "Monday"; 446 case Time::kTuesday: return "Tuesday"; 447 case Time::kWednesday: return "Wednesday"; 448 case Time::kThursday: return "Thursday"; 449 case Time::kFriday: return "Friday"; 450 case Time::kSaturday: return "Saturday"; 451 } 452 return "(unknown day)"; 453} 454 455} // namespace 456 457 458/*************************************************************************************************** 459 * MAIN ROUTINES 460 ***************************************************************************************************/ 461 462/** 463 * SETUP 464 */ 465void setup() { 466 // Serial communication 467 // ===================================================================================================== 468 Serial.begin(9600); 469 470 // Pin configuration 471 // ===================================================================================================== 472 printMessage(MSG_INIT_HARDWARE); 473 pinMode(IR_RX_PIN_1, INPUT); 474 pinMode(IR_TX_PIN_1, OUTPUT); 475 pinMode(IR_RX_PIN_2, INPUT); 476 pinMode(IR_TX_PIN_2, OUTPUT); 477 pinMode(SD_CS_PIN, OUTPUT); 478 pinMode(SD_SS_PIN, OUTPUT); 479 pinMode(DUMMY_RX_PIN, INPUT); 480 pinMode(DUMMY_TX_PIN, OUTPUT); 481 Serial.println("...done !"); 482 483 // Setup of diagnosis / warning LEDs 484 // ===================================================================================================== 485 // Data acquisition diagnosis LED ( let blink 5 times to prove it's working) 486 //------------------------------------------------------------------------------------------------------- 487 pinMode(LED_SML_Acquisition_eHZ1, OUTPUT); 488 digitalWrite(LED_SML_Acquisition_eHZ1, LOW); 489 pinMode(LED_SML_Acquisition_eHZ2, OUTPUT); 490 digitalWrite(LED_SML_Acquisition_eHZ2, LOW); 491 492 493 // SD card setup 494 // ===================================================================================================== 495 Serial.println("Initializing SD card..."); 496 if (!SD.begin(SD_CS_PIN)) { 497 printMessage(MSG_INIT_SD_CARD_ERROR); 498// errorSound(ERR_INIT_SD_CARD_ERROR); 499 stop(); 500 } 501 else { 502 Serial.println("...done !"); 503 } 504 505 // RTC setup 506 // ===================================================================================================== 507 // Initialize a new chip by turning off write protection and clearing the clock halt flag. These methods needn't always be called. See the DS1302 datasheet for details. 508 rtc.writeProtect(false); 509 rtc.halt(false); 510/* // Uncomment in case current time should be written to RTC shield 511 // Make a new time object to set the date and time and set the time 512 // Format: year, moth, day, hour, minute, second, day of the week (1=Sunday)) 513 Time t(2020, 3, 14, 9, 12, 0, 7); 514 rtc.time(t); 515*/ 516 517 // Sofware Serial setup 518 // ===================================================================================================== 519 dummySerial.begin(9600); 520 mySerial_1.begin(9600); // eHZ sends at fix rate of 9600 Baud 521 mySerial_2.begin(9600); // eHZ sends at fix rate of 9600 Baud 522 523 // Warning LED ( let blink 5 times to proove it's working) 524 //------------------------------------------------------------------------------------------------------- 525 pinMode(LED_WriteWarning, OUTPUT); 526 for(byte i=0; i<5; i++) { 527 digitalWrite(LED_WriteWarning, HIGH); 528 delay(250); 529 digitalWrite(LED_WriteWarning, LOW); 530 delay(250); 531 } 532 533 // determine the first file name to use 534 findNextFileNumber(); 535 536} 537 538/** 539 * END of SETUP 540 */ 541 542void loop() { 543 // turn off all LEDs 544 digitalWrite(LED_WriteWarning, LOW); 545 digitalWrite(LED_SML_Acquisition_eHZ1, LOW); 546 digitalWrite(LED_SML_Acquisition_eHZ2, LOW); 547// reportFreeMemory(); // for information purposes only 548 549 numberSMLBytesRead = 0; 550 while(numberSMLBytesRead!=397 || SML_DataAcquisition_eHZ1 == false){ // in case SML data is plausible (i.e., SML header is OK and also no buffer overflow) it still happens that eHZ reading is derived from this data. Examining this showed that in those cases typically less than 397 bytes SML data were read. This while loop/condition should prevent this. 551 getSMLData(mySerial_1, eHZ1_ID); // get SML data from 1st electricity meter (Input: A0), store it into (global) buffer 552 } 553 createTimeStamp(eHZ1_ID); 554 Serial.println("Time stamp eHZ_1 created..."); 555 eHZ_1_Reading = getElectricityMeterReading(buffer); // extract OBIS 1.8.0. in Wh x 10 and store it in (global) variable 556 Serial.print("eHZ_1 Reading: "); 557 Serial.println(eHZ_1_Reading); 558 559 numberSMLBytesRead = 0; 560 while(numberSMLBytesRead!=397 || SML_DataAcquisition_eHZ2 == false){ // in case SML data is plausible (i.e., SML header is OK and also no buffer overflow) it still happens that eHZ reading is derived from this data. Examining this showed that in those cases typically less than 397 bytes SML data were read. This while loop/condition should prevent this. 561 getSMLData(mySerial_2, eHZ2_ID); // get SML data from 2nd electricity meter (Input: A1), store it into (global) buffer 562 } 563 createTimeStamp(eHZ2_ID); 564 Serial.println("Time stamp eHZ_2 created..."); 565 eHZ_2_Reading = getElectricityMeterReading(buffer); // extract OBIS 1.8.0. in Wh x 10 and store it in (global) variable 566 Serial.print("eHZ_2 Reading: "); 567 Serial.println(eHZ_2_Reading); 568 569 Serial.println(); 570 571 // turn ON red LED to show that writing is beeing performed and SD card should NOT be removed. Prewarn by blinking for about 3s. 572 for(byte i=0; i<3; i++) { 573 digitalWrite(LED_WriteWarning, HIGH); 574 delay(500); 575 digitalWrite(LED_WriteWarning, LOW); 576 delay(500); 577 } 578 digitalWrite(LED_WriteWarning, HIGH); 579 // write data to file (time + 2 readings) 580 writeTimeAndReadingToFile(); 581 // turn OFF red LED to show that writing is FINISHED and SD card CAN be removed 582 digitalWrite(LED_WriteWarning, LOW); 583 delay(1000); 584 585 // set blinking behaviour of LED depending on result of SML data acquisition of eHZ1 586 if(SML_DataAcquisition_eHZ1 == true) { 587 eHZ1Blink_ON = 1000; 588 } 589 if(SML_DataAcquisition_eHZ1 == false) { 590 eHZ1Blink_ON = 250; 591 } 592 // set blinking behaviour of LED depending on result of SML data acquisition of eHZ1 593 if(SML_DataAcquisition_eHZ2 == true) { 594 eHZ2Blink_ON = 1000; 595 } 596 if(SML_DataAcquisition_eHZ2 == false) { 597 eHZ2Blink_ON = 250; 598 } 599 600 startTime = millis(); 601 // show result of data acquisition by blinking slow (success) or fast (fail). slow/fast defined above. 602 while(millis()-startTime < loggingInterval){ 603 for(int i=0; i<3; i++) { 604 digitalWrite(LED_SML_Acquisition_eHZ1, HIGH); 605 delay(eHZ1Blink_ON); 606 digitalWrite(LED_SML_Acquisition_eHZ1, LOW); 607 delay(eHZ1Blink_OFF); 608 } 609 for(int i=0; i<3; i++) { 610 digitalWrite(LED_SML_Acquisition_eHZ2, HIGH); 611 delay(eHZ2Blink_ON); 612 digitalWrite(LED_SML_Acquisition_eHZ2, LOW); 613 delay(eHZ2Blink_OFF); 614 } 615 } 616 617} 618
IR-Diode-Schaltung_Test_NanoEvery.ino
arduino
Short Arduino sketch used to optimize position of receiving IR phototransistor in front of eHZ's IR sender diode.
1// A0: amplified signal of 1st IR diode (output=collector of 1st transistor) 2// A1: amplified signal of 2nd IR diode (output=collector of 2nd transistor) (in case 2 IR diodes fo 2 eHZs used) 3// A6: raw signal of 1st IR photo diode (base of 1st transistor) 4// A7: raw signal of 2nd IR photo diode (base of 2nd transistor) (in case 2 IR diodes fo 2 eHZs used) 5 6 7void setup() { 8 9 //for 1st IR diode 10 pinMode(A0, INPUT); 11 pinMode(A6, INPUT); 12 13 //for 2nd IR diode 14 pinMode(A1, INPUT); 15 pinMode(A7, INPUT); 16 Serial.begin(9600); 17} 18 19void loop() { 20 //for 1st IR diode 21 22 Serial.print("A0: "); 23 Serial.print(analogRead(A0)); 24 Serial.print(" ; A6: "); 25 Serial.print(analogRead(A6)); 26 27 28 //for 2nd IR diode 29 Serial.print(" ; A1: "); 30 Serial.print(analogRead(A1)); 31 Serial.print(" ; A7: "); 32 Serial.println(analogRead(A7)); 33 34 35} 36
IR-Diode-Schaltung_Test_NanoEvery.ino
arduino
Short Arduino sketch used to optimize position of receiving IR phototransistor in front of eHZ's IR sender diode.
1// A0: amplified signal of 1st IR diode (output=collector of 1st transistor) 2// 3 A1: amplified signal of 2nd IR diode (output=collector of 2nd transistor) (in case 4 2 IR diodes fo 2 eHZs used) 5// A6: raw signal of 1st IR photo diode (base of 6 1st transistor) 7// A7: raw signal of 2nd IR photo diode (base of 2nd transistor) 8 (in case 2 IR diodes fo 2 eHZs used) 9 10 11void setup() { 12 13 //for 14 1st IR diode 15 pinMode(A0, INPUT); 16 pinMode(A6, INPUT); 17 18 //for 19 2nd IR diode 20 pinMode(A1, INPUT); 21 pinMode(A7, INPUT); 22 Serial.begin(9600); 23 24} 25 26void loop() { 27 //for 1st IR diode 28 29 Serial.print("A0: 30 "); 31 Serial.print(analogRead(A0)); 32 Serial.print(" ; A6: "); 33 Serial.print(analogRead(A6)); 34 35 36 37 //for 2nd IR diode 38 Serial.print(" ; A1: "); 39 Serial.print(analogRead(A1)); 40 41 Serial.print(" ; A7: "); 42 Serial.println(analogRead(A7)); 43 44 45} 46
eHZ-IW8E2A5_EMLogger_ArdNanoEvery_TMG_06.ino
arduino
Recording of 2 eHZ readings to an SD card.
1/* 2 * Read output of 2 electricity meters of type eHZ-IW8E2A5 and write 3 data to SD card. 4 * 5 * Written by Torsten Geppert (2020) based on ideas/parts 6 of code from 7 * - https://www.volkszaehler.org/ 8 * - and hardware / software 9 by Volker Wegert https://sourceforge.net/p/ehzy/wiki/Hardware/ 10 * - and "set_clock.ino" 11 by Matt Sparks, 2009, http://quadpoint.org/projects/arduino-ds1302 (seems to be 12 one of the very few FUNCTIONAL DS1302 libraries out there in the www) 13 * 14 15 * Major changes/adjustments 16 * 1) Hardware modified. Replaced resistors 17 * 18 - between IR diode and GND: 15 kOhm --> 1 MOhm (470 kOhm also sufficient). Necessary 19 to bring BC337(-25) transistor to achieve the necessary gain (probably because of 20 lower gain of BC337-25 as compared to BC337-40 or maybe signal of IR sender of eHZ 21 too weak or alingment IR sender to IR receiver not precise enough 22 * - between 23 collector of transistor and VCC: 15 kOhm --> 22 kOhm 24 * 2) w/ resepect to Volker 25 Wegert's code: 26 * - Changes in syntax of PROGMEM related texts and corresponding 27 array necessary (probably due to other version of IDE as compared to Volker Wegert 28 back in in 2012) 29 * - in the end removal of parts of code (e.g piezo control),I 30 did not use in my final code (Nevertheless, those things were really helpfull for 31 debugging during development phase) 32 * 3) Change in Arduino related Hardware 33 34 * - Use Arduino Nano Every + DS1302 based RTC module and SD card shield 35 (instead of Arduino Uno + SD card shield incl. RTC). 36 * Reason: Limitation 37 of Arduino Uno's RAM, making code run unstable w/ already 1 electricity meter (of 38 type eHZ-IW8E2A5). It was not possible to realize readout of 2 eHZs using Arduino 39 UNO (at least my experience/impression) 40 * - HW used: 41 * - RTC: 42 VMA301 by Velleman (e.g., from https://www.conrad.de/de/p/makerfactory-echtzeit-uhr-vma301-passend-fuer-arduino-boards-arduino-arduino-uno-fayaduino-freeduino-seeeduino-1612764.html) 43 44 * - SD shield: VMA304 by Velleman (e.g., https://www.conrad.de/de/p/makerfactory-sd-karten-logging-shield-fuer-arduino-2-tlg-1612765.html) 45 46 * - Level shifter (needed to adjust output from Arduino (5V) to SD shield 47 (3.3V). In case NOT used the SD (shield ?) card might be irreversibly damaged !!! 48 49 * 4) Time of RTC shield can be set in section "RTC setup". Should be sufficient 50 to do this initially (i.e., when you record your data for the first time and have 51 to set the actual time) or in case RTC time deviates too much from real time (e.g., 52 due to drift of oscillator in RC module due to missing temperature compensatioen 53 or similar) 54 * see comment below "Uncomment in case current time should be 55 written to RTC shield" 56 * 5) Ouptut into 1 file, at defined interval, for two 57 meters type eHZ-IW8E2A5. (Unfortunately - and currently not understood - sometimes 58 data is written into several files. However, due to naming (increasing number) it 59 is stil possible to bring those files in (temporal) order. In addition time stamp 60 is written into file for each datapoint aacquired. 61 * Output data: OBIS Kennzahl 62 1.8.0 (at Position 9x16+12 = 156 of SML protocol (In buffer w/ 1st element being 63 number 0: position 155). 4 bytes (right after HEX value "56" at position 155 ## 64 ## ## ## ##) 65 * To understand the SML protocol (and where to find the OBIS 66 data I was interested in) Stefan Weigerts explanation is highly appreciated: http://www.stefan-weigert.de/php_loader/sml.php 67 68 */ 69 70/* ======================================================================================================= 71 72 * Include Libraries and alike 73 * ======================================================================================================= 74 */ 75 76#include <stdarg.h> 77#include <stdio.h> 78#include <avr/pgmspace.h> 79#include 80 <SoftwareSerial.h> // Serial interfaces realized by software (to allow, e.g., reading 81 of more than just 1 sensor/eHz) 82#include <SD.h> // e.g., to write 83 files to SD card etc. 84#include <DS1302.h> 85 86/*************************************************************************************************** 87 88 * CONSTANTS and alike 89 ***************************************************************************************************/ 90 91// 92 Software Serial stuff ============================================================================================= 93// 94 Serial Data from source 1 (IR diode of 1st eHZ) 95#define IR_RX_PIN_1 A0 // 96 Pin 14 on Arduino Nano Every 97#define IR_TX_PIN_1 A2 // not 98 used, but required for SoftwareSerial; Pin 16 on Arduino Nano Every 99// Serial 100 Data from source 2 (IR diode of 2nd eHZ) 101#define IR_RX_PIN_2 A1 // 102 Pin 14 on Arduino Nano Every 103#define IR_TX_PIN_2 A2 // not 104 used, but required for SoftwareSerial; Pin 16 on Arduino Nano Every 105// Dummy 106 software serial 107#define DUMMY_RX_PIN 9 // not connected 108#define 109 DUMMY_TX_PIN A3 // not connected 110 111// SD card shield VMA304 112 stuff ============================================================================================= 113#define 114 SD_CS_PIN 8 // Pin for SD card selection.Typically use hardware 115 SS pin. (Pin 10 on most Arduino boards; however pin 8=A12 on Arduino Nano Every) 116#define 117 SD_SS_PIN 10 // ###really needed ? Check if can be deleted 118 ### 119 120// RTC stuff shield VMA301 stuff =========================================================================================== 121// 122 uses Pins A3, A4, A5 on Arduino Nano Every (Physical Pins 7, 8, 9; aka ???, SDA, 123 SCL) 124const int kCePin = 17; // Chip Enable 125const int 126 kIoPin = 18; // Input/Output 127const int kSclkPin = 19; // 128 Serial Clock 129 130// Other stuff ============================================================================================= 131#define 132 loggingInterval 300000 // time between readings in milli-seconds (e.g., 133 5 min = 5 * 60 * 1000 = 300 000) 134 135int numberSMLBytesRead = 0; // 136 used to restart reading of SML data in case less than specified number of bytes 137 (typically 397 for my electricity meter) read 138 139const int LED_WriteWarning 140 = 4; // Pin D4 (physical pin no. 22): LED to signalize approaching 141 or ongoing write to SD card 142const int LED_SML_Acquisition_eHZ1 = 16; // Pin 143 PD1 (phyiscal pin no. 10): LED to signalize status of SML data acquisition for eHZ1 144 (OK: flashiung @ 1Hz; NOK: flashing @4 Hz*) 145const int LED_SML_Acquisition_eHZ2 146 = 6; // Pin A13 (phyical pin no. 24): LED to signalize status of SML data acquisition 147 for eHZ1 (OK: flashiung @ 1Hz; NOK: flashing @4 Hz*); for whatever reason does NOT 148 work w/ originally intended Pin D7 (phys pin 3; AVR Pin 39) 149// *frequency can 150 be changed below to, e.g. 2 Hz by setting on time to 500 151int eHZ1Blink_ON=1000; 152 // time LED for eHZ1 is turned on 153int eHZ2Blink_ON=1000; 154 // time LED for eHZ2 is turned on 155int eHZ1Blink_OFF=250; 156 // time LED for eHZ1 is turned off 157int eHZ2Blink_OFF=250; 158 // time LED for eHZ2 is turned off 159bool SML_DataAcquisition_eHZ1 160 = false; // TRUE: SML data for eHZ1 acquired successfully; FALSE: error (e.g., 161 buffer overflow, wrong header) during SML data acquisition for eHZ1 162bool SML_DataAcquisition_eHZ2 163 = false; // TRUE: SML data for eHZ2 acquired successfully; FALSE: error (e.g., 164 buffer overflow, wrong header) during SML data acquisition for eHZ2 165const int 166 eHZ1_ID = 1; 167const int eHZ2_ID = 2; 168unsigned long startTime; 169 170/** 171 172 * size of the SML message buffer = meximum number of bytes that can be received 173 174 */ 175#define SML_MSG_BUFFER_SIZE 400 // eHZ-IW8E2A5 delivers SML telegram 176 of 396 bytes 177 178/** 179 * size of a filename (8.3 + terminating 0x00 = 13 chars) 180 181 */ 182#define FILENAME_SIZE 13 183 184/** 185 * maximum number of files 186 that can be stored 187 */ 188#define MAX_FILE_NUMBER 99999999 189 190/** 191 * 192 maximum time to wait for the end of a data packet received via IR 193 */ 194#define 195 SERIAL_READ_TIMEOUT_MS 500 196 197/*************************************************************************************************** 198 199 * MESSAGES 200 ***************************************************************************************************/ 201 202/** 203 204 * maximum length of a message (entire text after variable substitution!) 205 */ 206#define 207 MAX_MESSAGE_LENGTH 50 208 209/** 210 * message numbers 211 */ 212#define 213 MSG_PROGRAM_STOPPED 0 214#define MSG_NEXT_FILENAME 1 215#define 216 MSG_NO_FILE_NAMES_LEFT 2 217#define MSG_INIT_HARDWARE 3 218#define 219 MSG_INIT_SD_CARD 4 220#define MSG_INIT_SD_CARD_ERROR 5 221#define 222 MSG_BYTE_READ 6 223#define MSG_BUFFER_OVERFLOW 7 224#define 225 MSG_SERIAL_OVERFLOW 8 226#define MSG_INVALID_HEADER 9 227#define 228 MSG_FILE_OPEN_FAILED 10 229#define MSG_FILE_WRITTEN 11 230#define 231 MSG_FREE_MEMORY 12 232#define MSG_NUM_BYTES_READ 13 233 234/** 235 236 * actual message texts - caution, adapt MAX_MESSAGE_LENGTH if required! 237 * ....+....1....+....2....+....3....+....4....+....5 238 239 */ 240const char msgText00[] PROGMEM = "Program stopped."; 241const char msgText01[] 242 PROGMEM = "Next output file name is '%s'"; 243const char msgText02[] PROGMEM = 244 "No more file names left"; 245const char msgText03[] PROGMEM = "Initializing 246 Hardware..."; 247const char msgText04[] PROGMEM = "Initializing SD Card..."; 248const 249 char msgText05[] PROGMEM = "SD Card initialization failed"; 250const char msgText06[] 251 PROGMEM = "Read byte %02hhu from IR receiver"; 252const char msgText07[] PROGMEM 253 = "Message buffer overflow"; 254const char msgText08[] PROGMEM = "Serial buffer 255 overflow"; 256const char msgText09[] PROGMEM = "Invalid escape sequence"; 257const 258 char msgText10[] PROGMEM = "Unable to open output file"; 259const char msgText11[] 260 PROGMEM = "%u bytes of data written to file '%s'"; 261const char msgText12[] PROGMEM 262 = "%u bytes of memory available"; 263const char msgText13[] PROGMEM = "%u bytes 264 read"; 265 266/** 267 * table for easier access to the message texts 268 */ 269const 270 char *const msgTextTable[] PROGMEM = { 271 msgText00, 272 msgText01, 273 msgText02, 274 275 msgText03, 276 msgText04, 277 msgText05, 278 msgText06, 279 msgText07, 280 281 msgText08, 282 msgText09, 283 msgText10, 284 msgText11, 285 msgText12, 286 287 msgText13 288}; 289 290/*************************************************************************************************** 291 292 * GLOBAL VARIABLES (YUCK!) 293 ***************************************************************************************************/ 294 295/** 296 297 * instance of the SoftwareSerial library to handle the IR communication 298 */ 299SoftwareSerial 300 mySerial_1(IR_RX_PIN_1, IR_TX_PIN_1); // used for readout of SML data 301 of eHZ1 302SoftwareSerial mySerial_2(IR_RX_PIN_2, IR_TX_PIN_2); // 303 used for readout of SML data of eHZ2 304SoftwareSerial dummySerial(DUMMY_RX_PIN, 305 DUMMY_TX_PIN); 306 307// Create a RTC DS1302 object. 308DS1302 rtc(kCePin, kIoPin, 309 kSclkPin); 310 311/** 312 * variables to keep track of the name of the next file 313 to be written 314 */ 315unsigned long nextFileNumber = 0; 316char nextFileName[FILENAME_SIZE]; 317 318/** 319 320 * the global buffer to store the SML message currently being read 321 */ 322unsigned 323 char buffer[SML_MSG_BUFFER_SIZE]; 324 325/** 326 * Readings of eHZ 1 and 2 (after 327 extraction from SML data) and corresponding timestamps 328 */ 329unsigned long eHZ_1_Reading; 330 331unsigned long eHZ_2_Reading; 332char eHZ_1_Reading_timeStamp[50]; 333char 334 eHZ_2_Reading_timeStamp[50]; 335 336/*************************************************************************************************** 337 338 * SUBROUTINES 339 ***************************************************************************************************/ 340 341/** 342 343 * printMessage - reads a message text from the PROGMEM, performs variable substitution 344 and 345 * writes the resulting text to the serial console. Use MSG_* 346 constants for 347 * messageNumber. 348 */ 349void printMessage(int 350 messageNumber, ...) { 351 va_list args; 352 char format[MAX_MESSAGE_LENGTH]; 353 354 char buffer[MAX_MESSAGE_LENGTH]; 355 va_start(args, messageNumber); 356 strncpy_P(format, 357 (char*)pgm_read_word(&(msgTextTable[messageNumber])), MAX_MESSAGE_LENGTH); 358 vsnprintf(buffer, 359 MAX_MESSAGE_LENGTH, format, args); 360 Serial.println(buffer); 361 va_end(args); 362} 363 364/** 365 366 * reportFreeMemory - determines the amount of free memory and logs it to the serial 367 console. 368 */ 369void reportFreeMemory() { 370 int freeMemory; 371 uint8_t * 372 heapptr, * stackptr; 373 stackptr = (uint8_t *) malloc(4); // use stackptr 374 temporarily 375 heapptr = stackptr; // save value of heap pointer 376 377 free(stackptr); // free up the memory again (sets stackptr 378 to 0) 379 stackptr = (uint8_t *) (SP); // save value of stack pointer 380 381 freeMemory = stackptr - heapptr; 382 printMessage(MSG_FREE_MEMORY, freeMemory); 383} 384 385 386/** 387 * stop - stops program execution in a controlled fashion (endless 388 loop). 389 */ 390void stop() { 391 printMessage(MSG_PROGRAM_STOPPED); 392 while(1); 393} 394 395/** 396 397 * findNextFileNumber - determines the number and name of the next file available. 398 This will change 399 * the global variables nextFileName and 400 nextFileNumber. This routine will 401 * stop the entire program 402 if no free filenames can be found. 403 */ 404void findNextFileNumber() { 405 do 406 { 407 sprintf(nextFileName, "%08lu.CSV", nextFileNumber); 408 if (!SD.exists(nextFileName)) 409 { 410 printMessage(MSG_NEXT_FILENAME, nextFileName); 411 return; 412 } 413 else { 414 nextFileNumber += 1; 415 if (nextFileNumber > MAX_FILE_NUMBER) 416 { 417 printMessage(MSG_NO_FILE_NAMES_LEFT); 418// errorSound(ERR_NO_FILE_NAMES_LEFT); 419 420 stop(); 421 } 422 } 423 } while (true); 424} 425 426void createTimeStamp(int 427 eHZ_Number) { 428 // Get the current time and date from the chip. 429 Time t = 430 rtc.time(); 431 // Format the time and date and insert into the temporary buffer. 432 433 char currDAndT_buf[50]; 434 snprintf(currDAndT_buf, sizeof(currDAndT_buf), "%04d-%02d-%02d 435 %02d:%02d:%02d;", 436 t.yr, t.mon, t.date, 437 t.hr, t.min, t.sec); 438 439 440 // assign determined timestamp to either eHZ1 or eHz2 441 if ( eHZ_Number == 442 eHZ1_ID ) { 443 for (int i = 0; i<50; i++) { 444 eHZ_1_Reading_timeStamp[i]=currDAndT_buf[i]; 445 446 } 447//Serial.println("eHZ_1 Timestamp copied"); 448 } 449 if ( eHZ_Number 450 == eHZ2_ID ) { 451 for (int i = 0; i<50; i++) { 452 eHZ_2_Reading_timeStamp[i]=currDAndT_buf[i]; 453 454 } 455//Serial.println("eHZ_2 Timestamp copied"); 456 } 457} 458 459void writeTimeAndReadingToFile() 460 { 461 File outputFile; 462 463 outputFile = SD.open(nextFileName, FILE_WRITE); 464 465 if (!outputFile) { 466 printMessage(MSG_FILE_OPEN_FAILED); 467 } 468 else 469 { 470 outputFile.print(eHZ_1_Reading_timeStamp); 471 outputFile.print(eHZ_1_Reading); 472 473 outputFile.print(";"); 474 outputFile.print(eHZ_2_Reading_timeStamp); 475 476 outputFile.println(eHZ_2_Reading); 477 478 outputFile.close(); 479 } 480} 481 482void 483 getSMLData(SoftwareSerial mySoftSer, int eHZ_Number) { 484 unsigned int nextBufferPosition 485 = 0; 486 unsigned long lastReadTime = 0; 487 488 // clear the message buffer 489 490 memset(buffer, 0, sizeof(buffer)); 491 492 // initialize data acquisition LEDs 493 assuming everything is fine 494 if ( eHZ_Number == eHZ1_ID ) { // remember 495 NO error during SML data acquisition for eHZ1 496 SML_DataAcquisition_eHZ1 = 497 true; 498 } 499 if ( eHZ_Number == eHZ2_ID ) { // remember NO error 500 during SML data acquisition for eHZ2 501 SML_DataAcquisition_eHZ2 = true; 502 503 } 504 505 // remove a pending overflow flag (might have been left over from 506 a previous run) 507 // and ensure that the SoftwareSerial library is listening 508 509 mySoftSer.overflow(); 510 mySoftSer.listen(); 511 512 // wait until actual data 513 is available 514 while (!mySoftSer.available()); 515 // Next 2 lines ("Serial.print...") 516 not necessary for algorithm, however might be useful for information purpose on 517 serial monitor. 518 // In addition reading of serial data SEEMs to be more stable 519 (less errors such as "invalid escape sequence"). Maybe 520 // something like 521 "delay(200);" might also work... 522 Serial.print("Number of bytes available 523 at SW serial:"); 524 Serial.println(mySoftSer.available()); 525 526 // keep reading 527 data until either the message buffer is filled or no more data was 528 // received 529 for SERIAL_READ_TIMEOUT_MS ms 530 531 lastReadTime = millis(); 532 while (millis() 533 - lastReadTime < SERIAL_READ_TIMEOUT_MS) { 534 if (mySoftSer.available()) { 535 536 buffer[nextBufferPosition] = mySoftSer.read(); 537 lastReadTime = millis(); 538 539 if (nextBufferPosition >= SML_MSG_BUFFER_SIZE) { 540 dummySerial.listen(); 541 // disable further IR input by switching software serial input 542 to a dummy instance 543 printMessage(MSG_BUFFER_OVERFLOW); 544 545 546 if ( eHZ_Number == eHZ1_ID ) { // remember error during SML data 547 acquisition for eHZ1 548 SML_DataAcquisition_eHZ1 = false; 549 } 550 551 if ( eHZ_Number == eHZ2_ID ) { // remember error during SML data 552 acquisition for eHZ2 553 SML_DataAcquisition_eHZ2 = false; 554 } 555 556 return; 557 } 558 nextBufferPosition += 1; 559 numberSMLBytesRead 560 = nextBufferPosition+1; // used to restart reading of SML data in case less than 561 specified number of bytes (typically 397 for my electricity meter) read 562 } 563 564 } 565 566 // report an error if an overflow condition was encountered - the data 567 received is useless 568 // in this case :-( 569 if (mySoftSer.overflow()) { 570 571 dummySerial.listen(); // disable further IR input 572 printMessage(MSG_SERIAL_OVERFLOW); 573 574 575 if ( eHZ_Number == eHZ1_ID ) { // remember error during SML 576 data acquisition for eHZ1 577 SML_DataAcquisition_eHZ1 = false; 578 } 579 580 if ( eHZ_Number == eHZ2_ID ) { // remember error during SML data acquisition 581 for eHZ2 582 SML_DataAcquisition_eHZ2 = false; 583 } 584 } 585 else { 586 587 dummySerial.listen(); // disable further IR input 588 // check the header 589 590 printMessage(MSG_NUM_BYTES_READ, nextBufferPosition + 1); 591 if (!isValidHeader()) 592 { 593 // not a valid header - notify the user... 594 printMessage(MSG_INVALID_HEADER); 595 596 // ...and empty the receiver buffer (wait for the end of the current data 597 stream 598 while (mySoftSer.available() > 0) { 599 mySoftSer.read(); 600 601 } 602 if ( eHZ_Number == eHZ1_ID ) { // remember error during 603 SML data acquisition for eHZ1 604 SML_DataAcquisition_eHZ1 = false; 605 } 606 607 if ( eHZ_Number == eHZ2_ID ) { // remember error during SML data 608 acquisition for eHZ2 609 SML_DataAcquisition_eHZ2 = false; 610 } 611 612 } 613 } 614} 615 616unsigned long getElectricityMeterReading(char SML_buff) 617 { 618 /* Returns electricity meter reading in kWh 619 * Electricity meter reading 620 (for eHZ-IW8E2A5) is represented by the five bytes at buffer positions 155-159. 621 622 * Make those 5 bytes to 1 HEX value, convert to DEC, divide by 10 (due to sacler 623 in eHZ), divide by 1000 to get kWh 624 * However, use only lowest 4 bytes (i.e., 625 meaningful data start at position 156). Highest number represented by these four 626 bytes is 627 * in HEX FF FF FF FF or in DEC 4294967295 (= 2^32). This would correspond 628 to approx. 429 496 kWh. 629 * Assuming an (realistic) average energy consumption 630 of about 5000 kWh per year for the heat pump, this would allow 631 * to store 632 the meter reading for about 85 years ! 633 * Therefore use data type unsigned 634 long (allows values from 0 ... 2^32-1 = 0 ... 4 294 967 295) 635 */ 636 unsigned 637 char EMReading_buf[4]; 638 unsigned long EMReading; 639 640 // clear the EMReading 641 buffer 642 memset(EMReading_buf, 0, sizeof(EMReading_buf)); 643 644 // get 4 bytes 645 representing the electricity meter reading and write them in opposite order to new 646 array. Then create unsigned long. 647 // Algorhithm see https://forum.arduino.cc/index.php?topic=108865.0 648 649 for(int i=3; i>=0; i--) { 650 EMReading_buf[3-i] = (buffer[156+i]); 651 } 652 653 EMReading = *((unsigned long *) (& EMReading_buf[0])); 654 EMReading = EMReading/10; 655 // consider scaler in SML data -> divide by 10 to 656 get Wh 657 return EMReading; 658} 659 660/** 661 * isValidHeader - returns true 662 if the global message buffer begins with a valid SML escape sequence. 663 */ 664inline 665 boolean isValidHeader() { 666 return ((buffer[0] == 0x1b) && 667 (buffer[1] 668 == 0x1b) && 669 (buffer[2] == 0x1b) && 670 (buffer[3] == 0x1b) 671 && 672 (buffer[4] == 0x01) && 673 (buffer[5] == 0x01) && 674 (buffer[6] 675 == 0x01) && 676 (buffer[7] == 0x01)); 677} 678 679/** 680 * for DS1302 681 based RTC time. Not necessarily (to be) used for 682 */ 683namespace { 684 685String 686 dayAsString(const Time::Day day) { 687 switch (day) { 688 case Time::kSunday: 689 return "Sunday"; 690 case Time::kMonday: return "Monday"; 691 case Time::kTuesday: 692 return "Tuesday"; 693 case Time::kWednesday: return "Wednesday"; 694 case 695 Time::kThursday: return "Thursday"; 696 case Time::kFriday: return "Friday"; 697 698 case Time::kSaturday: return "Saturday"; 699 } 700 return "(unknown day)"; 701} 702 703} 704 // namespace 705 706 707/*************************************************************************************************** 708 709 * MAIN ROUTINES 710 ***************************************************************************************************/ 711 712/** 713 714 * SETUP 715 */ 716void setup() { 717 // Serial communication 718 // ===================================================================================================== 719 720 Serial.begin(9600); 721 722 // Pin configuration 723 // ===================================================================================================== 724 725 printMessage(MSG_INIT_HARDWARE); 726 pinMode(IR_RX_PIN_1, INPUT); 727 pinMode(IR_TX_PIN_1, 728 OUTPUT); 729 pinMode(IR_RX_PIN_2, INPUT); 730 pinMode(IR_TX_PIN_2, OUTPUT); 731 732 pinMode(SD_CS_PIN, OUTPUT); 733 pinMode(SD_SS_PIN, OUTPUT); 734 pinMode(DUMMY_RX_PIN, 735 INPUT); 736 pinMode(DUMMY_TX_PIN, OUTPUT); 737 Serial.println("...done !"); 738 739 740 // Setup of diagnosis / warning LEDs 741 // ===================================================================================================== 742 743 // Data acquisition diagnosis LED ( let blink 5 times to prove it's working) 744 745 //------------------------------------------------------------------------------------------------------- 746 747 pinMode(LED_SML_Acquisition_eHZ1, OUTPUT); 748 digitalWrite(LED_SML_Acquisition_eHZ1, 749 LOW); 750 pinMode(LED_SML_Acquisition_eHZ2, OUTPUT); 751 digitalWrite(LED_SML_Acquisition_eHZ2, 752 LOW); 753 754 755 // SD card setup 756 // ===================================================================================================== 757 758 Serial.println("Initializing SD card..."); 759 if (!SD.begin(SD_CS_PIN)) { 760 761 printMessage(MSG_INIT_SD_CARD_ERROR); 762// errorSound(ERR_INIT_SD_CARD_ERROR); 763 764 stop(); 765 } 766 else { 767 Serial.println("...done !"); 768 } 769 770 771 // RTC setup 772 // ===================================================================================================== 773 774 // Initialize a new chip by turning off write protection and clearing the clock 775 halt flag. These methods needn't always be called. See the DS1302 datasheet for 776 details. 777 rtc.writeProtect(false); 778 rtc.halt(false); 779/* // Uncomment 780 in case current time should be written to RTC shield 781 // Make a new time object 782 to set the date and time and set the time 783 // Format: year, moth, day, hour, 784 minute, second, day of the week (1=Sunday)) 785 Time t(2020, 3, 14, 9, 12, 0, 7); 786 787 rtc.time(t); 788*/ 789 790 // Sofware Serial setup 791 // ===================================================================================================== 792 793 dummySerial.begin(9600); 794 mySerial_1.begin(9600); // eHZ 795 sends at fix rate of 9600 Baud 796 mySerial_2.begin(9600); // 797 eHZ sends at fix rate of 9600 Baud 798 799 // Warning LED ( let blink 5 times 800 to proove it's working) 801 //------------------------------------------------------------------------------------------------------- 802 803 pinMode(LED_WriteWarning, OUTPUT); 804 for(byte i=0; i<5; i++) { 805 digitalWrite(LED_WriteWarning, 806 HIGH); 807 delay(250); 808 digitalWrite(LED_WriteWarning, LOW); 809 delay(250); 810 811 } 812 813 // determine the first file name to use 814 findNextFileNumber(); 815 816} 817 818/** 819 820 * END of SETUP 821 */ 822 823void loop() { 824 // turn off all LEDs 825 digitalWrite(LED_WriteWarning, 826 LOW); 827 digitalWrite(LED_SML_Acquisition_eHZ1, LOW); 828 digitalWrite(LED_SML_Acquisition_eHZ2, 829 LOW); 830// reportFreeMemory(); // for information 831 purposes only 832 833 numberSMLBytesRead = 0; 834 while(numberSMLBytesRead!=397 835 || SML_DataAcquisition_eHZ1 == false){ // in case SML data is plausible (i.e., 836 SML header is OK and also no buffer overflow) it still happens that eHZ reading 837 is derived from this data. Examining this showed that in those cases typically less 838 than 397 bytes SML data were read. This while loop/condition should prevent this. 839 840 getSMLData(mySerial_1, eHZ1_ID); // get SML data from 841 1st electricity meter (Input: A0), store it into (global) buffer 842 } 843 createTimeStamp(eHZ1_ID); 844 845 Serial.println("Time stamp eHZ_1 created..."); 846 eHZ_1_Reading = getElectricityMeterReading(buffer); 847 // extract OBIS 1.8.0. in Wh x 10 and store it in (global) variable 848 Serial.print("eHZ_1 849 Reading: "); 850 Serial.println(eHZ_1_Reading); 851 852 numberSMLBytesRead 853 = 0; 854 while(numberSMLBytesRead!=397 || SML_DataAcquisition_eHZ2 == false){ // 855 in case SML data is plausible (i.e., SML header is OK and also no buffer overflow) 856 it still happens that eHZ reading is derived from this data. Examining this showed 857 that in those cases typically less than 397 bytes SML data were read. This while 858 loop/condition should prevent this. 859 getSMLData(mySerial_2, eHZ2_ID); // 860 get SML data from 2nd electricity meter (Input: A1), store it into (global) buffer 861 862 } 863 createTimeStamp(eHZ2_ID); 864 Serial.println("Time stamp eHZ_2 created..."); 865 866 eHZ_2_Reading = getElectricityMeterReading(buffer); // extract OBIS 1.8.0. 867 in Wh x 10 and store it in (global) variable 868 Serial.print("eHZ_2 Reading: 869 "); 870 Serial.println(eHZ_2_Reading); 871 872 Serial.println(); 873 874 // 875 turn ON red LED to show that writing is beeing performed and SD card should NOT 876 be removed. Prewarn by blinking for about 3s. 877 for(byte i=0; i<3; i++) { 878 879 digitalWrite(LED_WriteWarning, HIGH); 880 delay(500); 881 digitalWrite(LED_WriteWarning, 882 LOW); 883 delay(500); 884 } 885 digitalWrite(LED_WriteWarning, HIGH); 886 // 887 write data to file (time + 2 readings) 888 writeTimeAndReadingToFile(); 889 // 890 turn OFF red LED to show that writing is FINISHED and SD card CAN be removed 891 892 digitalWrite(LED_WriteWarning, LOW); 893 delay(1000); 894 895 // set blinking 896 behaviour of LED depending on result of SML data acquisition of eHZ1 897 if(SML_DataAcquisition_eHZ1 898 == true) { 899 eHZ1Blink_ON = 1000; 900 } 901 if(SML_DataAcquisition_eHZ1 902 == false) { 903 eHZ1Blink_ON = 250; 904 } 905 // set blinking behaviour of 906 LED depending on result of SML data acquisition of eHZ1 907 if(SML_DataAcquisition_eHZ2 908 == true) { 909 eHZ2Blink_ON = 1000; 910 } 911 if(SML_DataAcquisition_eHZ2 == 912 false) { 913 eHZ2Blink_ON = 250; 914 } 915 916 startTime = millis(); 917 // 918 show result of data acquisition by blinking slow (success) or fast (fail). slow/fast 919 defined above. 920 while(millis()-startTime < loggingInterval){ 921 for(int 922 i=0; i<3; i++) { 923 digitalWrite(LED_SML_Acquisition_eHZ1, HIGH); 924 delay(eHZ1Blink_ON); 925 926 digitalWrite(LED_SML_Acquisition_eHZ1, LOW); 927 delay(eHZ1Blink_OFF); 928 929 } 930 for(int i=0; i<3; i++) { 931 digitalWrite(LED_SML_Acquisition_eHZ2, 932 HIGH); 933 delay(eHZ2Blink_ON); 934 digitalWrite(LED_SML_Acquisition_eHZ2, 935 LOW); 936 delay(eHZ2Blink_OFF); 937 } 938 } 939 940} 941
eHZ-IW8E2A5_EMLogger_ArdNanoEvery_TMG_09.ino
arduino
Recording of 2 eHZ readings to an SD card incl. supply voltage monitoring and recording
1/* 2 * Read output of 2 electricity meters of type eHZ-IW8E2A5 and write 3 data to SD card. 4 * 5 * Written by Torsten Geppert (2020/2021) based on ideas/parts 6 of code from 7 * - https://www.volkszaehler.org/ 8 * - and hardware / software 9 by Volker Wegert https://sourceforge.net/p/ehzy/wiki/Hardware/ 10 * - and "set_clock.ino" 11 by Matt Sparks, 2009, http://quadpoint.org/projects/arduino-ds1302 (seems to be 12 one of the very few FUNCTIONAL DS1302 libraries out there in the www) 13 * 14 15 * Major changes/adjustments 16 * 1) Hardware modified. Replaced resistors 17 * 18 - between IR diode and GND: 15 kOhm --> 1 MOhm (470 kOhm also sufficient). Necessary 19 to bring BC337(-25) transistor to achieve the necessary gain (probably because of 20 lower gain of BC337-25 as compared to BC337-40 or maybe signal of IR sender of eHZ 21 too weak or alingment IR sender to IR receiver not precise enough 22 * - between 23 collector of transistor and VCC: 15 kOhm --> 22 kOhm 24 * 2) w/ resepect to Volker 25 Wegert's code: 26 * - Changes in syntax of PROGMEM related texts and corresponding 27 array necessary (probably due to other version of IDE as compared to Volker Wegert 28 back in in 2012) 29 * - in the end removal of parts of code (e.g piezo control),I 30 did not use in my final code (Nevertheless, those things were really helpfull for 31 debugging during development phase) 32 * 3) Change in Arduino related Hardware 33 34 * - Use Arduino Nano Every + DS1302 based RTC module and SD card shield 35 (instead of Arduino Uno + SD card shield incl. RTC). 36 * Reason: Limitation 37 of Arduino Uno's RAM, making code run unstable w/ already 1 electricity meter (of 38 type eHZ-IW8E2A5). It was not possible to realize readout of 2 eHZs using Arduino 39 UNO (at least my experience/impression) 40 * - HW used: 41 * - RTC: 42 VMA301 by Velleman (e.g., from https://www.conrad.de/de/p/makerfactory-echtzeit-uhr-vma301-passend-fuer-arduino-boards-arduino-arduino-uno-fayaduino-freeduino-seeeduino-1612764.html) 43 44 * - SD shield: VMA304 by Velleman (e.g., https://www.conrad.de/de/p/makerfactory-sd-karten-logging-shield-fuer-arduino-2-tlg-1612765.html) 45 46 * - Level shifter (needed to adjust output from Arduino (5V) to SD shield 47 (3.3V). In case NOT used the SD (shield ?) card might be irreversibly damaged !!! 48 49 * - added 3 LEDs to show SUCCESS or FAILURE of SD card initialization and 50 data aqcuisition 51 * - added piezo speaker (accoustic warning in case supply 52 voiltage - e.g., from battery) drops below defineable limit 53 * - additional 54 volatge display (manually triggered by push-button (NOT connected to/controlled 55 by Arduino) 56 * 4) Time of RTC shield can be set in section "RTC setup". Should 57 be sufficient to do this initially (i.e., when you record your data for the first 58 time and have to set the actual time) or in case RTC time deviates too much from 59 real time (e.g., due to drift of oscillator in RC module due to missing temperature 60 compensatioen or similar) 61 * see comment below "Uncomment in case current 62 time should be written to RTC shield" 63 * 5) Ouptut into 1 file, at defined interval, 64 for two meters type eHZ-IW8E2A5. (Unfortunately - and currently not understood - 65 sometimes data is written into several files. However, due to naming (increasing 66 number) it is stil possible to bring those files in (temporal) order. In addition 67 time stamp is written into file for each datapoint aacquired. 68 * Output data: 69 OBIS Kennzahl 1.8.0 (at Position 9x16+12 = 156 of SML protocol (In buffer w/ 1st 70 element being number 0: position 155). 4 bytes (right after HEX value "56" at 71 position 155 ## ## ## ## ##) 72 * To understand the SML protocol (and where 73 to find the OBIS data I was interested in) Stefan Weigerts explanation is highly 74 appreciated: http://www.stefan-weigert.de/php_loader/sml.php 75 */ 76 77/* ======================================================================================================= 78 79 * Include Libraries and alike 80 * ======================================================================================================= 81 */ 82 83#include <stdarg.h> 84#include <stdio.h> 85#include <avr/pgmspace.h> 86#include 87 <SoftwareSerial.h> // Serial interfaces realized by software (to allow, e.g., reading 88 of more than just 1 sensor/eHz) 89#include <SD.h> // e.g., to write 90 files to SD card etc. 91#include <SPI.h> // needed for SD.h 92#include 93 <DS1302.h> // needed for RTC shield 94 95/*************************************************************************************************** 96 97 * CONSTANTS and alike 98 ***************************************************************************************************/ 99 100// 101 Software Serial stuff ============================================================================================= 102// 103 Serial Data from source 1 (IR diode of 1st eHZ) 104#define IR_RX_PIN_1 A0 // 105 Pin 14 on Arduino Nano Every 106#define IR_TX_PIN_1 A2 // not 107 used, but required for SoftwareSerial; Pin 16 on Arduino Nano Every 108// Serial 109 Data from source 2 (IR diode of 2nd eHZ) 110#define IR_RX_PIN_2 A1 // 111 Pin 14 on Arduino Nano Every 112#define IR_TX_PIN_2 A2 // not 113 used, but required for SoftwareSerial; Pin 16 on Arduino Nano Every 114// Dummy 115 software serial 116#define DUMMY_RX_PIN 9 // not connected 117#define 118 DUMMY_TX_PIN A3 // not connected 119 120// SD card shield VMA304 121 stuff ============================================================================================= 122#define 123 SD_CS_PIN 8 // Pin for SD card selection.Typically use hardware 124 SS pin. (Pin 10 on most Arduino boards; however pin 8=A12 on Arduino Nano Every) 125#define 126 SD_SS_PIN 10 // ###really needed ? Check if can be deleted 127 ### 128 129// RTC stuff shield VMA301 stuff =========================================================================================== 130// 131 uses Pins A3, A4, A5 on Arduino Nano Every (Physical Pins 7, 8, 9; aka ???, SDA, 132 SCL) 133const int kCePin = 17; // Chip Enable 134const int 135 kIoPin = 18; // Input/Output 136const int kSclkPin = 19; // 137 Serial Clock 138 139// Other stuff ============================================================================================= 140#define 141 loggingInterval 3600000 // time between readings in milli-seconds, e.g., 142 143 // 5 min = 5 * 60 * 1000 = 300 000 144 145 // 60 min = 60 * 60 * 1000 = 3 600 000 146 147// supply voltage monitoring stuff 148 =========================================================================================== 149#define 150 buzzPin 3 // Buzzer connected to PWM Pin D3 151#define supplVoltPin A2 152 // center contact of voltage divider (GND <-> supply Voltage (NOT 5V ! but V_in)) 153#define 154 operatVolt 5 // operating voltage of Arduino (used as reference for ADC) 155#define 156 buzzTimeEarly 8 // earliest time (hour of day; e.g., 8 AM = 8; 10PM -> 22) buzzer 157 is triggered in case supply voltage below limit 158#define buzzTimeLate 20 // 159 latest time (hour of day) buzzer is triggered in case supply voltage below limit 160float 161 supplVoltLim = 5.6; // absolute supply voltage limit; serves as trigger for buzzer 162 163int 164 numberSMLBytesRead = 0; // used to restart reading of SML data in 165 case less than specified number of bytes (typically 397 for my electricity meter) 166 read 167 168const int LED_WriteWarning = 4; // Pin D4 (physical 169 pin no. 22): LED to signalize approaching or ongoing write to SD card 170const int 171 LED_SML_Acquisition_eHZ1 = 5; // Pin PD1 (phyiscal pin no. 10): LED to signalize 172 status of SML data acquisition for eHZ1 (OK: flashiung @ 1Hz; NOK: flashing @4 Hz*) 173const 174 int LED_SML_Acquisition_eHZ2 = 6; // Pin A13 (phyical pin no. 24): LED to signalize 175 status of SML data acquisition for eHZ1 (OK: flashiung @ 1Hz; NOK: flashing @4 Hz*); 176 for whatever reason does NOT work w/ originally intended Pin D7 (phys pin 3; AVR 177 Pin 39) 178// *frequency can be changed below to, e.g. 2 Hz by setting on time to 179 500 180int eHZ1Blink_ON=1000; // time LED for eHZ1 is turned 181 on 182int eHZ2Blink_ON=1000; // time LED for eHZ2 is turned 183 on 184int eHZ1Blink_OFF=250; // time LED for eHZ1 is turned 185 off 186int eHZ2Blink_OFF=250; // time LED for eHZ2 is turned 187 off 188bool SML_DataAcquisition_eHZ1 = false; // TRUE: SML data for eHZ1 189 acquired successfully; FALSE: error (e.g., buffer overflow, wrong header) during 190 SML data acquisition for eHZ1 191bool SML_DataAcquisition_eHZ2 = false; // 192 TRUE: SML data for eHZ2 acquired successfully; FALSE: error (e.g., buffer overflow, 193 wrong header) during SML data acquisition for eHZ2 194const int eHZ1_ID = 1; 195const 196 int eHZ2_ID = 2; 197unsigned long startTime; 198 199/** 200 * size of the SML message 201 buffer = meximum number of bytes that can be received 202 */ 203#define SML_MSG_BUFFER_SIZE 204 400 // eHZ-IW8E2A5 delivers SML telegram of 396 bytes 205 206/** 207 * 208 size of a filename (8.3 + terminating 0x00 = 13 chars) 209 */ 210#define FILENAME_SIZE 211 13 212 213/** 214 * maximum number of files that can be stored 215 */ 216#define 217 MAX_FILE_NUMBER 99999999 218 219/** 220 * maximum time to wait for the end of 221 a data packet received via IR 222 */ 223#define SERIAL_READ_TIMEOUT_MS 500 224 225/*************************************************************************************************** 226 227 * MESSAGES 228 ***************************************************************************************************/ 229 230/** 231 232 * maximum length of a message (entire text after variable substitution!) 233 */ 234#define 235 MAX_MESSAGE_LENGTH 50 236 237/** 238 * message numbers 239 */ 240#define 241 MSG_PROGRAM_STOPPED 0 242#define MSG_NEXT_FILENAME 1 243#define 244 MSG_NO_FILE_NAMES_LEFT 2 245#define MSG_INIT_HARDWARE 3 246#define 247 MSG_INIT_SD_CARD 4 248#define MSG_INIT_SD_CARD_ERROR 5 249#define 250 MSG_BYTE_READ 6 251#define MSG_BUFFER_OVERFLOW 7 252#define 253 MSG_SERIAL_OVERFLOW 8 254#define MSG_INVALID_HEADER 9 255#define 256 MSG_FILE_OPEN_FAILED 10 257#define MSG_FILE_WRITTEN 11 258#define 259 MSG_FREE_MEMORY 12 260#define MSG_NUM_BYTES_READ 13 261 262/** 263 264 * actual message texts - caution, adapt MAX_MESSAGE_LENGTH if required! 265 * ....+....1....+....2....+....3....+....4....+....5 266 267 */ 268const char msgText00[] PROGMEM = "Program stopped."; 269const char msgText01[] 270 PROGMEM = "Next output file name is '%s'"; 271const char msgText02[] PROGMEM = 272 "No more file names left"; 273const char msgText03[] PROGMEM = "Initializing 274 Hardware..."; 275const char msgText04[] PROGMEM = "Initializing SD Card..."; 276const 277 char msgText05[] PROGMEM = "SD Card initialization failed"; 278const char msgText06[] 279 PROGMEM = "Read byte %02hhu from IR receiver"; 280const char msgText07[] PROGMEM 281 = "Message buffer overflow"; 282const char msgText08[] PROGMEM = "Serial buffer 283 overflow"; 284const char msgText09[] PROGMEM = "Invalid escape sequence"; 285const 286 char msgText10[] PROGMEM = "Unable to open output file"; 287const char msgText11[] 288 PROGMEM = "%u bytes of data written to file '%s'"; 289const char msgText12[] PROGMEM 290 = "%u bytes of memory available"; 291const char msgText13[] PROGMEM = "%u bytes 292 read"; 293 294/** 295 * table for easier access to the message texts 296 */ 297const 298 char *const msgTextTable[] PROGMEM = { 299 msgText00, 300 msgText01, 301 msgText02, 302 303 msgText03, 304 msgText04, 305 msgText05, 306 msgText06, 307 msgText07, 308 309 msgText08, 310 msgText09, 311 msgText10, 312 msgText11, 313 msgText12, 314 315 msgText13 316}; 317 318/*************************************************************************************************** 319 320 * GLOBAL VARIABLES (YUCK!) 321 ***************************************************************************************************/ 322 323/** 324 325 * instance of the SoftwareSerial library to handle the IR communication 326 */ 327SoftwareSerial 328 mySerial_1(IR_RX_PIN_1, IR_TX_PIN_1); // used for readout of SML data 329 of eHZ1 330SoftwareSerial mySerial_2(IR_RX_PIN_2, IR_TX_PIN_2); // 331 used for readout of SML data of eHZ2 332SoftwareSerial dummySerial(DUMMY_RX_PIN, 333 DUMMY_TX_PIN); 334 335// Create a RTC DS1302 object. 336DS1302 rtc(kCePin, kIoPin, 337 kSclkPin); 338 339/** 340 * variables to keep track of the name of the next file 341 to be written 342 */ 343unsigned long nextFileNumber = 0; 344char nextFileName[FILENAME_SIZE]; 345 346/** 347 348 * the global buffer to store the SML message currently being read 349 */ 350unsigned 351 char buffer[SML_MSG_BUFFER_SIZE]; 352 353/** 354 * Readings of eHZ 1 and 2 (after 355 extraction from SML data) and corresponding timestamps 356 */ 357unsigned long eHZ_1_Reading; 358 359unsigned long eHZ_2_Reading; 360char eHZ_1_Reading_timeStamp[50]; 361char 362 eHZ_2_Reading_timeStamp[50]; 363 364/** 365 * Supply voltage 366 */ 367float supplVolt; 368 369/*************************************************************************************************** 370 371 * SUBROUTINES 372 ***************************************************************************************************/ 373 374/** 375 376 * printMessage - reads a message text from the PROGMEM, performs variable substitution 377 and 378 * writes the resulting text to the serial console. Use MSG_* 379 constants for 380 * messageNumber. 381 */ 382void printMessage(int 383 messageNumber, ...) { 384 va_list args; 385 char format[MAX_MESSAGE_LENGTH]; 386 387 char buffer[MAX_MESSAGE_LENGTH]; 388 va_start(args, messageNumber); 389 strncpy_P(format, 390 (char*)pgm_read_word(&(msgTextTable[messageNumber])), MAX_MESSAGE_LENGTH); 391 vsnprintf(buffer, 392 MAX_MESSAGE_LENGTH, format, args); 393 Serial.println(buffer); 394 va_end(args); 395} 396 397/** 398 399 * reportFreeMemory - determines the amount of free memory and logs it to the serial 400 console. 401 */ 402void reportFreeMemory() { 403 int freeMemory; 404 uint8_t * 405 heapptr, * stackptr; 406 stackptr = (uint8_t *) malloc(4); // use stackptr 407 temporarily 408 heapptr = stackptr; // save value of heap pointer 409 410 free(stackptr); // free up the memory again (sets stackptr 411 to 0) 412 stackptr = (uint8_t *) (SP); // save value of stack pointer 413 414 freeMemory = stackptr - heapptr; 415 printMessage(MSG_FREE_MEMORY, freeMemory); 416} 417 418 419/** 420 * stop - stops program execution in a controlled fashion (endless 421 loop). 422 */ 423void stop() { 424 printMessage(MSG_PROGRAM_STOPPED); 425 while(1); 426} 427 428/** 429 430 * findNextFileNumber - determines the number and name of the next file available. 431 This will change 432 * the global variables nextFileName and 433 nextFileNumber. This routine will 434 * stop the entire program 435 if no free filenames can be found. 436 */ 437void findNextFileNumber() { 438 do 439 { 440 sprintf(nextFileName, "%08lu.CSV", nextFileNumber); 441 if (!SD.exists(nextFileName)) 442 { 443 printMessage(MSG_NEXT_FILENAME, nextFileName); 444 return; 445 } 446 else { 447 nextFileNumber += 1; 448 if (nextFileNumber > MAX_FILE_NUMBER) 449 { 450 printMessage(MSG_NO_FILE_NAMES_LEFT); 451// errorSound(ERR_NO_FILE_NAMES_LEFT); 452 453 stop(); 454 } 455 } 456 } while (true); 457} 458 459void createTimeStamp(int 460 eHZ_Number) { 461 // Get the current time and date from the chip. 462 Time t = 463 rtc.time(); 464 // Format the time and date and insert into the temporary buffer. 465 466 char currDAndT_buf[50]; 467 snprintf(currDAndT_buf, sizeof(currDAndT_buf), "%04d-%02d-%02d 468 %02d:%02d:%02d;", 469 t.yr, t.mon, t.date, 470 t.hr, t.min, t.sec); 471 472 473 // assign determined timestamp to either eHZ1 or eHz2 474 if ( eHZ_Number == 475 eHZ1_ID ) { 476 for (int i = 0; i<50; i++) { 477 eHZ_1_Reading_timeStamp[i]=currDAndT_buf[i]; 478 479 } 480//Serial.println("eHZ_1 Timestamp copied"); 481 } 482 if ( eHZ_Number 483 == eHZ2_ID ) { 484 for (int i = 0; i<50; i++) { 485 eHZ_2_Reading_timeStamp[i]=currDAndT_buf[i]; 486 487 } 488//Serial.println("eHZ_2 Timestamp copied"); 489 } 490} 491 492void writeTimeAndReadingToFile() 493 { 494 Serial.println("Writing data to SD card..."); 495 496 File outputFile; 497 498 499 outputFile = SD.open(nextFileName, FILE_WRITE); 500 if (!outputFile) { 501 printMessage(MSG_FILE_OPEN_FAILED); 502 503 } 504 else { 505 outputFile.print(eHZ_1_Reading_timeStamp); 506 outputFile.print(eHZ_1_Reading); 507 508 outputFile.print(";"); 509 outputFile.print(eHZ_2_Reading_timeStamp); 510 511 outputFile.print(eHZ_2_Reading); 512 outputFile.print(";"); 513 outputFile.print(eHZ_2_Reading_timeStamp); 514 515 outputFile.println(supplVolt); 516 outputFile.close(); 517 Serial.println("...done..."); 518 519 } 520} 521 522void getSMLData(SoftwareSerial mySoftSer, int eHZ_Number) { 523 524 unsigned int nextBufferPosition = 0; 525 unsigned long lastReadTime = 0; 526 527 528 // clear the message buffer 529 memset(buffer, 0, sizeof(buffer)); 530 531 532 // initialize data acquisition LEDs assuming everything is fine 533 if ( eHZ_Number 534 == eHZ1_ID ) { // remember NO error during SML data acquisition for eHZ1 535 536 SML_DataAcquisition_eHZ1 = true; 537 } 538 if ( eHZ_Number == eHZ2_ID ) { 539 // remember NO error during SML data acquisition for eHZ2 540 SML_DataAcquisition_eHZ2 541 = true; 542 } 543 544 // remove a pending overflow flag (might have been left 545 over from a previous run) 546 // and ensure that the SoftwareSerial library is 547 listening 548 mySoftSer.overflow(); 549 mySoftSer.listen(); 550 551 // wait until 552 actual data is available 553 while (!mySoftSer.available()); 554 // Next 2 lines 555 ("Serial.print...") not necessary for algorithm, however might be useful for information 556 purpose on serial monitor. 557 // In addition reading of serial data SEEMs to be 558 more stable (less errors such as "invalid escape sequence"). Maybe 559 // something 560 like "delay(200);" might also work... 561 Serial.print("Number of bytes available 562 at SW serial:"); 563 Serial.println(mySoftSer.available()); 564 565 // keep reading 566 data until either the message buffer is filled or no more data was 567 // received 568 for SERIAL_READ_TIMEOUT_MS ms 569 570 lastReadTime = millis(); 571 while (millis() 572 - lastReadTime < SERIAL_READ_TIMEOUT_MS) { 573 if (mySoftSer.available()) { 574 575 buffer[nextBufferPosition] = mySoftSer.read(); 576 lastReadTime = millis(); 577 578 if (nextBufferPosition >= SML_MSG_BUFFER_SIZE) { 579 dummySerial.listen(); 580 // disable further IR input by switching software serial input 581 to a dummy instance 582 printMessage(MSG_BUFFER_OVERFLOW); 583 584 585 if ( eHZ_Number == eHZ1_ID ) { // remember error during SML data 586 acquisition for eHZ1 587 SML_DataAcquisition_eHZ1 = false; 588 } 589 590 if ( eHZ_Number == eHZ2_ID ) { // remember error during SML data 591 acquisition for eHZ2 592 SML_DataAcquisition_eHZ2 = false; 593 } 594 595 return; 596 } 597 nextBufferPosition += 1; 598 numberSMLBytesRead 599 = nextBufferPosition+1; // used to restart reading of SML data in case less than 600 specified number of bytes (typically 397 for my electricity meter) read 601 } 602 603 } 604 605 // report an error if an overflow condition was encountered - the data 606 received is useless 607 // in this case :-( 608 if (mySoftSer.overflow()) { 609 610 dummySerial.listen(); // disable further IR input 611 printMessage(MSG_SERIAL_OVERFLOW); 612 613 614 if ( eHZ_Number == eHZ1_ID ) { // remember error during SML 615 data acquisition for eHZ1 616 SML_DataAcquisition_eHZ1 = false; 617 } 618 619 if ( eHZ_Number == eHZ2_ID ) { // remember error during SML data acquisition 620 for eHZ2 621 SML_DataAcquisition_eHZ2 = false; 622 } 623 } 624 else { 625 626 dummySerial.listen(); // disable further IR input 627 // check the header 628 629 printMessage(MSG_NUM_BYTES_READ, nextBufferPosition + 1); 630 if (!isValidHeader()) 631 { 632 // not a valid header - notify the user... 633 printMessage(MSG_INVALID_HEADER); 634 635 // ...and empty the receiver buffer (wait for the end of the current data 636 stream 637 while (mySoftSer.available() > 0) { 638 mySoftSer.read(); 639 640 } 641 if ( eHZ_Number == eHZ1_ID ) { // remember error during 642 SML data acquisition for eHZ1 643 SML_DataAcquisition_eHZ1 = false; 644 } 645 646 if ( eHZ_Number == eHZ2_ID ) { // remember error during SML data 647 acquisition for eHZ2 648 SML_DataAcquisition_eHZ2 = false; 649 } 650 651 } 652 } 653} 654 655unsigned long getElectricityMeterReading(char SML_buff) 656 { 657 /* Returns electricity meter reading in kWh 658 * Electricity meter reading 659 (for eHZ-IW8E2A5) is represented by the five bytes at buffer positions 155-159. 660 661 * Make those 5 bytes to 1 HEX value, convert to DEC, divide by 10 (due to sacler 662 in eHZ), divide by 1000 to get kWh 663 * However, use only lowest 4 bytes (i.e., 664 meaningful data start at position 156). Highest number represented by these four 665 bytes is 666 * in HEX FF FF FF FF or in DEC 4294967295 (= 2^32). This would correspond 667 to approx. 429 496 kWh. 668 * Assuming an (realistic) average energy consumption 669 of about 5000 kWh per year for the heat pump, this would allow 670 * to store 671 the meter reading for about 85 years ! 672 * Therefore use data type unsigned 673 long (allows values from 0 ... 2^32-1 = 0 ... 4 294 967 295) 674 */ 675 unsigned 676 char EMReading_buf[4]; 677 unsigned long EMReading; 678 679 // clear the EMReading 680 buffer 681 memset(EMReading_buf, 0, sizeof(EMReading_buf)); 682 683 // get 4 bytes 684 representing the electricity meter reading and write them in opposite order to new 685 array. Then create unsigned long. 686 // Algorhithm see https://forum.arduino.cc/index.php?topic=108865.0 687 688 for(int i=3; i>=0; i--) { 689 EMReading_buf[3-i] = (buffer[156+i]); 690 } 691 692 EMReading = *((unsigned long *) (& EMReading_buf[0])); 693 EMReading = EMReading/10; 694 // consider scaler in SML data -> divide by 10 to 695 get Wh 696 return EMReading; 697} 698 699/** 700 * isValidHeader - returns true 701 if the global message buffer begins with a valid SML escape sequence. 702 */ 703inline 704 boolean isValidHeader() { 705 return ((buffer[0] == 0x1b) && 706 (buffer[1] 707 == 0x1b) && 708 (buffer[2] == 0x1b) && 709 (buffer[3] == 0x1b) 710 && 711 (buffer[4] == 0x01) && 712 (buffer[5] == 0x01) && 713 (buffer[6] 714 == 0x01) && 715 (buffer[7] == 0x01)); 716} 717 718/** 719 * for DS1302 720 based RTC time. Not necessarily (to be) used for 721 */ 722namespace { 723 724String 725 dayAsString(const Time::Day day) { 726 switch (day) { 727 case Time::kSunday: 728 return "Sunday"; 729 case Time::kMonday: return "Monday"; 730 case Time::kTuesday: 731 return "Tuesday"; 732 case Time::kWednesday: return "Wednesday"; 733 case 734 Time::kThursday: return "Thursday"; 735 case Time::kFriday: return "Friday"; 736 737 case Time::kSaturday: return "Saturday"; 738 } 739 return "(unknown day)"; 740} 741 742} 743 // namespace 744 745 746/*************************************************************************************************** 747 748 * MAIN ROUTINES 749 ***************************************************************************************************/ 750 751/** 752 ######################################################################################################## 753 754 * SETUP 755 ######################################################################################################## 756 757 */ 758void setup() { 759 // Serial communication 760 // ===================================================================================================== 761 762 Serial.begin(9600); 763 764 Serial.println("===== Starting setup ====="); 765 766 // Pin configuration 767 // ===================================================================================================== 768 769 printMessage(MSG_INIT_HARDWARE); 770 pinMode(IR_RX_PIN_1, INPUT); 771 pinMode(IR_TX_PIN_1, 772 OUTPUT); 773 pinMode(IR_RX_PIN_2, INPUT); 774 pinMode(IR_TX_PIN_2, OUTPUT); 775 776 pinMode(SD_CS_PIN, OUTPUT); 777 pinMode(SD_SS_PIN, OUTPUT); 778 pinMode(DUMMY_RX_PIN, 779 INPUT); 780 pinMode(DUMMY_TX_PIN, OUTPUT); 781 Serial.println("...done !... pinMode 782 set !"); 783 784 // RTC setup 785 // ===================================================================================================== 786 787 // Initialize a new chip by turning off write protection and clearing the clock 788 halt flag. These methods needn't always be called. See the DS1302 datasheet for 789 details. 790 rtc.writeProtect(false); 791 rtc.halt(false); 792/* // Uncomment 793 in case current time should be written to RTC shield 794 // Make a new time object 795 to set the date and time and set the time 796 // Format: year, moth, day, hour, 797 minute, second, day of the week (1=Sunday)) 798 Time t(2021, 3, 03, 21, 48, 0, 799 4); 800 rtc.time(t); 801*/ 802 803 // Setup of supply voltage monitoring 804 805 // ===================================================================================================== 806 807 Serial.println("Testing LEDs & piezo speaker..."); 808 pinMode(buzzPin, OUTPUT); 809 810 pinMode(supplVoltPin, INPUT); 811 // quick sound check to prove piezo speaker 812 works (only during day time...) 813 814 Time t2 = rtc.time(); 815 if( t2.hr 816 > buzzTimeEarly && t2.hr < buzzTimeLate ) { 817 for (int i = 0; i <5; i++) { 818 819 tone(buzzPin, 1000, 250); 820 delay(500); 821 tone(buzzPin, 5000, 822 250); 823 delay(500); 824 } 825 } 826 827 // Setup of data acquisition 828 and diagnosis / warning LEDs 829 // ===================================================================================================== 830 831 pinMode(LED_SML_Acquisition_eHZ1, OUTPUT); 832 digitalWrite(LED_SML_Acquisition_eHZ1, 833 LOW); 834 pinMode(LED_SML_Acquisition_eHZ2, OUTPUT); 835 digitalWrite(LED_SML_Acquisition_eHZ2, 836 LOW); 837 pinMode(LED_WriteWarning, OUTPUT); 838 digitalWrite(LED_WriteWarning, 839 LOW); 840 841 // ...let blink 5 times to proove the're working 842 //------------------------------------------------------------------------------------------------------- 843 844 Serial.println("Data acquisition LED 1 (BLUE): ...blink 5 times"); 845 for(byte 846 i=0; i<5; i++) { 847 digitalWrite(LED_SML_Acquisition_eHZ1, HIGH); 848 delay(250); 849 850 digitalWrite(LED_SML_Acquisition_eHZ1, LOW); 851 delay(250); 852 } 853 Serial.println("...done 854 !"); 855 Serial.println("Data acquisition LED 2 (YELLOW): ...blink 5 times"); 856 857 for(byte i=0; i<5; i++) { 858 digitalWrite(LED_SML_Acquisition_eHZ2, HIGH); 859 860 delay(250); 861 digitalWrite(LED_SML_Acquisition_eHZ2, LOW); 862 delay(250); 863 864 } 865 Serial.println("...done !"); 866 Serial.println("SD status LED (RED): 867 ...blink 5 times"); 868 for(byte i=0; i<5; i++) { 869 digitalWrite(LED_WriteWarning, 870 HIGH); 871 delay(250); 872 digitalWrite(LED_WriteWarning, LOW); 873 delay(250); 874 875 } 876 Serial.println("...done !"); 877 878 // SD card setup 879 // ===================================================================================================== 880 881 Serial.println("Initializing SD card..."); 882 if (!SD.begin(SD_CS_PIN)) { 883 884 printMessage(MSG_INIT_SD_CARD_ERROR); 885// errorSound(ERR_INIT_SD_CARD_ERROR); 886 887 stop(); 888 } 889 else { 890 Serial.println("...done !"); 891 Time 892 t2 = rtc.time(); 893 if( t2.hr > buzzTimeEarly && t2.hr < buzzTimeLate ) { 894 895 for (int i = 0; i <10; i++) { 896 tone(buzzPin, 250, 250); 897 delay(500); 898 899 } 900 } 901 } 902 903 // Sofware Serial setup 904 // ===================================================================================================== 905 906 dummySerial.begin(9600); 907 mySerial_1.begin(9600); // eHZ 908 sends at fix rate of 9600 Baud 909 mySerial_2.begin(9600); // 910 eHZ sends at fix rate of 9600 Baud 911 912 // determine the first file name to 913 use 914 findNextFileNumber(); 915 916} 917 918/** ######################################################################################################## 919 920 * END of SETUP 921 ######################################################################################################## 922 923 */ 924 925void loop() { 926 // turn off all LEDs 927 digitalWrite(LED_WriteWarning, 928 LOW); 929 digitalWrite(LED_SML_Acquisition_eHZ1, LOW); 930 digitalWrite(LED_SML_Acquisition_eHZ2, 931 LOW); 932// reportFreeMemory(); // for information 933 purposes only 934 935 numberSMLBytesRead = 0; 936 while(numberSMLBytesRead!=397 937 || SML_DataAcquisition_eHZ1 == false){ // in case SML data is plausible (i.e., 938 SML header is OK and also no buffer overflow) it still happens that eHZ reading 939 is derived from this data. Examining this showed that in those cases typically less 940 than 397 bytes SML data were read. This while loop/condition should prevent this. 941 942 getSMLData(mySerial_1, eHZ1_ID); // get SML data from 943 1st electricity meter (Input: A0), store it into (global) buffer 944 } 945 createTimeStamp(eHZ1_ID); 946 947 Serial.println("Time stamp eHZ_1 created..."); 948 eHZ_1_Reading = getElectricityMeterReading(buffer); 949 // extract OBIS 1.8.0. in Wh x 10 and store it in (global) variable 950 Serial.print("eHZ_1 951 Reading: "); 952 Serial.println(eHZ_1_Reading); 953 954 numberSMLBytesRead 955 = 0; 956 while(numberSMLBytesRead!=397 || SML_DataAcquisition_eHZ2 == false){ // 957 in case SML data is plausible (i.e., SML header is OK and also no buffer overflow) 958 it still happens that eHZ reading is derived from this data. Examining this showed 959 that in those cases typically less than 397 bytes SML data were read. This while 960 loop/condition should prevent this. 961 getSMLData(mySerial_2, eHZ2_ID); // 962 get SML data from 2nd electricity meter (Input: A1), store it into (global) buffer 963 964 } 965 createTimeStamp(eHZ2_ID); 966 Serial.println("Time stamp eHZ_2 created..."); 967 968 eHZ_2_Reading = getElectricityMeterReading(buffer); // extract OBIS 1.8.0. 969 in Wh x 10 and store it in (global) variable 970 Serial.print("eHZ_2 Reading: 971 "); 972 Serial.println(eHZ_2_Reading); 973 974 supplVolt = analogRead(supplVoltPin) 975 * 2 / 1024.0 * operatVolt; // get actual supply voltage in V (multiply 976 analog reading by 2 due to 1:1 voltage divider 977 978 Serial.println(); 979 980 981 // turn ON red LED to show that writing is beeing performed and SD card should 982 NOT be removed. Prewarn by blinking for about 3s. 983 // ----------------------------------------------------------------------------------------------------------------------------- 984 985 for(byte i=0; i<3; i++) { 986 digitalWrite(LED_WriteWarning, HIGH); 987 delay(500); 988 989 digitalWrite(LED_WriteWarning, LOW); 990 delay(500); 991 } 992 // Write 993 data to SD card 994 // --------------------- 995 digitalWrite(LED_WriteWarning, 996 HIGH); 997 // write data to file (time 1; reading 1; time 2; reading 2; time 2; 998 supply voltage) 999 writeTimeAndReadingToFile(); 1000 // turn OFF red LED to show 1001 that writing is FINISHED and SD card CAN be removed 1002 digitalWrite(LED_WriteWarning, 1003 LOW); 1004 delay(1000); 1005 1006 // Show result (SUCCESS/FAIL) of data acquisition 1007 for each channel 1008 // --------------------------------------------------------------- 1009 1010 // set blinking behaviour of LED depending on result of SML data acquisition of 1011 eHZ1 1012 if(SML_DataAcquisition_eHZ1 == true) { 1013 eHZ1Blink_ON = 1000; 1014 1015 } 1016 if(SML_DataAcquisition_eHZ1 == false) { 1017 eHZ1Blink_ON = 250; 1018 1019 } 1020 // set blinking behaviour of LED depending on result of SML data acquisition 1021 of eHZ1 1022 if(SML_DataAcquisition_eHZ2 == true) { 1023 eHZ2Blink_ON = 1000; 1024 1025 } 1026 if(SML_DataAcquisition_eHZ2 == false) { 1027 eHZ2Blink_ON = 250; 1028 1029 } 1030 1031 // wait for specified time interval until next data acquisition to 1032 be performed 1033 startTime = millis(); 1034 // show result of data acquisition 1035 by blinking slow (success) or fast (fail). slow/fast defined above. 1036 while(millis()-startTime 1037 < loggingInterval){ 1038 delay(60000); // only blink once 1039 a minute (to save battery energy) 1040 for(int i=0; i<3; i++) { 1041 digitalWrite(LED_SML_Acquisition_eHZ1, 1042 HIGH); 1043 delay(eHZ1Blink_ON); 1044 digitalWrite(LED_SML_Acquisition_eHZ1, 1045 LOW); 1046 delay(eHZ1Blink_OFF); 1047 } 1048 for(int i=0; i<3; i++) { 1049 1050 digitalWrite(LED_SML_Acquisition_eHZ2, HIGH); 1051 delay(eHZ2Blink_ON); 1052 1053 digitalWrite(LED_SML_Acquisition_eHZ2, LOW); 1054 delay(eHZ2Blink_OFF); 1055 1056 } 1057 // Sound buzzer in case supply voltage drops below defined 1058 limit; trigger alarm only during specified time slots. 1059 Time t2 = rtc.time(); 1060Serial.print("supplyVolt: 1061 "); 1062Serial.println(supplVolt); 1063 if( supplVolt < supplVoltLim && t2.hr 1064 > buzzTimeEarly && t2.hr < buzzTimeLate ) { 1065 for (int i = 0; i <5; i++) 1066 { 1067 tone(buzzPin, 1000, 250); 1068 delay(750); 1069 } 1070 } 1071 1072 } 1073 1074} 1075
Downloadable files
Schematics for reading 2 smart meters using Arduino Nano Every w/ manual supply voltage display.
Schematics for reading 2 smart meters using Arduino Nano Every w/ manual supply voltage display.
Schematics for reading 2 smart meters using Arduino Nano Every w/ supply voltage monitoring.
Schematics for reading 2 smart meters using Arduino Nano Every w/ supply voltage monitoring.
Schematics for reading 2 smart meters using Arduino Nano Every
Schematics for reading 2 smart meters using Arduino Nano Every
Schematics for reading 2 smart meters using Arduino Nano Every w/ manual supply voltage display.
Schematics for reading 2 smart meters using Arduino Nano Every w/ manual supply voltage display.
Schematics for reading 2 smart meters using Arduino Nano Every w/ supply voltage monitoring.
Schematics for reading 2 smart meters using Arduino Nano Every w/ supply voltage monitoring.
Schematics for reading 2 smart meters using Arduino Nano Every
Schematics for reading 2 smart meters using Arduino Nano Every
Comments