torstengeppert
Published © GPL3+

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.

IntermediateFull instructions provided12,123
Logging 2 Electricity Smart Meters Using Arduino Nano Every

Things used in this project

Hardware components

Arduino Nano Every
Arduino Nano Every
×1
VMA 301 RTC shield
×1
VMA 304 SD card shield
×1
Level shifter, bidirectional, 5V 3.3V
×1
IR Phototransistor, NPN, SFH309FA
×2
BC337-25 Transistor, NPN
×2
Through Hole Resistor, 220 ohm
Through Hole Resistor, 220 ohm
×3
Resistor 1M ohm
Resistor 1M ohm
×2
Resistor 22.1k ohm
Resistor 22.1k ohm
×2
5 mm LED: Red
5 mm LED: Red
×1
5 mm LED: Yellow
5 mm LED: Yellow
×1
LED, Blue
LED, Blue
×1
Custom PCB
Custom PCB
×1
2 wire cable (phototransistor PCB)
×2
DC Power Connector, Jack
DC Power Connector, Jack
×1

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
Solder Wire, Lead Free
Solder Wire, Lead Free

Story

Read more

Schematics

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/ supply voltage monitoring.

Code

eHZ-IW8E2A5_EMLogger_ArdNanoEvery_TMG_06.ino

Arduino
Recording of 2 eHZ readings to an SD card.
/*
 * Read output of 2 electricity meters of type eHZ-IW8E2A5 and write data to SD card.
 * 
 * Written by Torsten Geppert (2020) based on ideas/parts of code from
 *   - https://www.volkszaehler.org/
 *   - and hardware / software by Volker Wegert https://sourceforge.net/p/ehzy/wiki/Hardware/
 *   - 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)
 *
 * Major changes/adjustments
 * 1) Hardware modified. Replaced resistors
 *    - 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
 *    - between collector of transistor and VCC: 15 kOhm --> 22 kOhm 
 * 2) w/ resepect to Volker Wegert's code:
 *   - 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)
 *   - 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)
 * 3) Change in Arduino related Hardware  
 *   - Use Arduino Nano Every + DS1302 based RTC module and SD card shield (instead of Arduino Uno + SD card shield incl. RTC).
 *     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)
 *     -  HW used:
 *       - 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)
 *       - SD shield: VMA304 by Velleman (e.g., https://www.conrad.de/de/p/makerfactory-sd-karten-logging-shield-fuer-arduino-2-tlg-1612765.html)
 *       - 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 !!!
 * 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)
 *    see comment below "Uncomment in case current time should be written to RTC shield"
 * 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.
 *    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 ## ## ## ## ##)
 *    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
 */

/* ======================================================================================================= 
 * Include Libraries and alike
 * ======================================================================================================= */

#include <stdarg.h>
#include <stdio.h>
#include <avr/pgmspace.h>
#include <SoftwareSerial.h> // Serial interfaces realized by software (to allow, e.g., reading of more than just 1 sensor/eHz)
#include <SD.h>             // e.g., to write files to SD card etc.
#include <DS1302.h>

/***************************************************************************************************
 * CONSTANTS and alike
 ***************************************************************************************************/

// Software Serial stuff =============================================================================================
// Serial Data from source 1 (IR diode of 1st eHZ)
#define IR_RX_PIN_1     A0                  // Pin 14 on Arduino Nano Every
#define IR_TX_PIN_1     A2                  // not used, but required for SoftwareSerial; Pin 16 on Arduino Nano Every
// Serial Data from source 2 (IR diode of 2nd eHZ)
#define IR_RX_PIN_2     A1                  // Pin 14 on Arduino Nano Every
#define IR_TX_PIN_2     A2                  // not used, but required for SoftwareSerial; Pin 16 on Arduino Nano Every
// Dummy software serial
#define DUMMY_RX_PIN  9                     // not connected
#define DUMMY_TX_PIN  A3                    // not connected

// SD card shield VMA304 stuff =============================================================================================
#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)
#define SD_SS_PIN    10                     // ###really needed ? Check if can be deleted ###  

// RTC stuff shield VMA301 stuff ===========================================================================================
// uses Pins A3, A4, A5 on Arduino Nano Every (Physical Pins 7, 8, 9; aka ???, SDA, SCL)
const int kCePin   = 17;                    // Chip Enable
const int kIoPin   = 18;                    // Input/Output
const int kSclkPin = 19;                    // Serial Clock

// Other stuff =============================================================================================
#define loggingInterval 300000              // time between readings in milli-seconds (e.g., 5 min = 5 * 60 * 1000 = 300 000)

int numberSMLBytesRead = 0;                 // used to restart reading of SML data in case less than specified number of bytes (typically 397 for my electricity meter) read         

const int LED_WriteWarning = 4;              // Pin D4 (physical pin no. 22): LED to signalize approaching or ongoing write to SD card
const 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*)
const 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)
// *frequency can be changed below to, e.g. 2 Hz by setting on time to 500
int eHZ1Blink_ON=1000;                       // time LED for eHZ1 is turned on
int eHZ2Blink_ON=1000;                       // time LED for eHZ2 is turned on 
int eHZ1Blink_OFF=250;                       // time LED for eHZ1 is turned off
int eHZ2Blink_OFF=250;                       // time LED for eHZ2 is turned off  
bool 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
bool 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
const int eHZ1_ID = 1;
const int eHZ2_ID = 2;
unsigned long startTime;

/**
 * size of the SML message buffer = meximum number of bytes that can be received
 */
#define SML_MSG_BUFFER_SIZE    400      // eHZ-IW8E2A5 delivers SML telegram of 396 bytes

/**
 * size of a filename (8.3 + terminating 0x00 = 13 chars)
 */
#define FILENAME_SIZE           13 

/**
 * maximum number of files that can be stored
 */
#define MAX_FILE_NUMBER   99999999

/**
 * maximum time to wait for the end of a data packet received via IR
 */
#define SERIAL_READ_TIMEOUT_MS 500

/***************************************************************************************************
 * MESSAGES
 ***************************************************************************************************/

/**
 * maximum length of a message (entire text after variable substitution!)
 */
#define MAX_MESSAGE_LENGTH        50

/**
 * message numbers
 */
#define MSG_PROGRAM_STOPPED        0
#define MSG_NEXT_FILENAME          1
#define MSG_NO_FILE_NAMES_LEFT     2
#define MSG_INIT_HARDWARE          3
#define MSG_INIT_SD_CARD           4
#define MSG_INIT_SD_CARD_ERROR     5
#define MSG_BYTE_READ              6
#define MSG_BUFFER_OVERFLOW        7
#define MSG_SERIAL_OVERFLOW        8  
#define MSG_INVALID_HEADER         9
#define MSG_FILE_OPEN_FAILED      10
#define MSG_FILE_WRITTEN          11
#define MSG_FREE_MEMORY           12
#define MSG_NUM_BYTES_READ        13

/**
 * actual message texts - caution, adapt MAX_MESSAGE_LENGTH if required!
 *                               ....+....1....+....2....+....3....+....4....+....5
 */
const char msgText00[] PROGMEM = "Program stopped.";
const char msgText01[] PROGMEM = "Next output file name is '%s'";
const char msgText02[] PROGMEM = "No more file names left";
const char msgText03[] PROGMEM = "Initializing Hardware...";
const char msgText04[] PROGMEM = "Initializing SD Card...";
const char msgText05[] PROGMEM = "SD Card initialization failed";
const char msgText06[] PROGMEM = "Read byte %02hhu from IR receiver";
const char msgText07[] PROGMEM = "Message buffer overflow";
const char msgText08[] PROGMEM = "Serial buffer overflow";
const char msgText09[] PROGMEM = "Invalid escape sequence";
const char msgText10[] PROGMEM = "Unable to open output file";
const char msgText11[] PROGMEM = "%u bytes of data written to file '%s'";
const char msgText12[] PROGMEM = "%u bytes of memory available";
const char msgText13[] PROGMEM = "%u bytes read";

/**
 * table for easier access to the message texts
 */
const char *const msgTextTable[] PROGMEM = {   
  msgText00,
  msgText01,
  msgText02,
  msgText03,
  msgText04,
  msgText05,
  msgText06,
  msgText07,
  msgText08,
  msgText09,
  msgText10,
  msgText11,
  msgText12,
  msgText13
};

/***************************************************************************************************
 * GLOBAL VARIABLES (YUCK!)
 ***************************************************************************************************/

/**
 * instance of the SoftwareSerial library to handle the IR communication
 */
SoftwareSerial mySerial_1(IR_RX_PIN_1, IR_TX_PIN_1);              // used for readout of SML data of eHZ1
SoftwareSerial mySerial_2(IR_RX_PIN_2, IR_TX_PIN_2);              // used for readout of SML data of eHZ2
SoftwareSerial dummySerial(DUMMY_RX_PIN, DUMMY_TX_PIN);

// Create a RTC DS1302 object.
DS1302 rtc(kCePin, kIoPin, kSclkPin);

/**
 * variables to keep track of the name of the next file to be written
 */
unsigned long nextFileNumber = 0;
char nextFileName[FILENAME_SIZE];

/**
 * the global buffer to store the SML message currently being read
 */
unsigned char buffer[SML_MSG_BUFFER_SIZE];

/**
 * Readings of eHZ 1 and 2 (after extraction from SML data) and corresponding timestamps
 */
unsigned long eHZ_1_Reading;                
unsigned long eHZ_2_Reading;
char eHZ_1_Reading_timeStamp[50];
char eHZ_2_Reading_timeStamp[50];

/***************************************************************************************************
 * SUBROUTINES
 ***************************************************************************************************/

/** 
 * printMessage - reads a message text from the PROGMEM, performs variable substitution and 
 *                writes the resulting text to the serial console. Use MSG_* constants for
 *                messageNumber.
 */
void printMessage(int messageNumber, ...) {
  va_list args;
  char format[MAX_MESSAGE_LENGTH];
  char buffer[MAX_MESSAGE_LENGTH];
  va_start(args, messageNumber);
  strncpy_P(format, (char*)pgm_read_word(&(msgTextTable[messageNumber])), MAX_MESSAGE_LENGTH);
  vsnprintf(buffer, MAX_MESSAGE_LENGTH, format, args);
  Serial.println(buffer);
  va_end(args);
}

/**
 * reportFreeMemory - determines the amount of free memory and logs it to the serial console.
 */
void reportFreeMemory() {
  int freeMemory;
  uint8_t * heapptr, * stackptr;
  stackptr = (uint8_t *) malloc(4);     // use stackptr temporarily
  heapptr = stackptr;                   // save value of heap pointer
  free(stackptr);                       // free up the memory again (sets stackptr to 0)
  stackptr = (uint8_t *) (SP);          // save value of stack pointer
  freeMemory = stackptr - heapptr;
  printMessage(MSG_FREE_MEMORY, freeMemory);
}  

/**
 * stop - stops program execution in a controlled fashion (endless loop).
 */
void stop() {
  printMessage(MSG_PROGRAM_STOPPED);
  while(1);
}

/**
 * findNextFileNumber - determines the number and name of the next file available. This will change
 *                      the global variables nextFileName and nextFileNumber. This routine will
 *                      stop the entire program if no free filenames can be found.
 */
void findNextFileNumber() {
  do {
    sprintf(nextFileName, "%08lu.CSV", nextFileNumber);
    if (!SD.exists(nextFileName)) {
      printMessage(MSG_NEXT_FILENAME, nextFileName);
      return;
    } else {
      nextFileNumber += 1;
      if (nextFileNumber > MAX_FILE_NUMBER) {
        printMessage(MSG_NO_FILE_NAMES_LEFT);
//        errorSound(ERR_NO_FILE_NAMES_LEFT);
        stop();
      }
    }
  } while (true); 
}

void createTimeStamp(int eHZ_Number) {
  // Get the current time and date from the chip.
  Time t = rtc.time();
  // Format the time and date and insert into the temporary buffer.
  char currDAndT_buf[50];
  snprintf(currDAndT_buf, sizeof(currDAndT_buf), "%04d-%02d-%02d %02d:%02d:%02d;",
         t.yr, t.mon, t.date,
         t.hr, t.min, t.sec);

   // assign determined timestamp to either eHZ1 or eHz2
   if ( eHZ_Number == eHZ1_ID ) {           
    for (int i = 0; i<50; i++) {
      eHZ_1_Reading_timeStamp[i]=currDAndT_buf[i];
    }
//Serial.println("eHZ_1 Timestamp copied");
   }
  if ( eHZ_Number == eHZ2_ID ) {           
    for (int i = 0; i<50; i++) {
      eHZ_2_Reading_timeStamp[i]=currDAndT_buf[i];
    }
//Serial.println("eHZ_2 Timestamp copied");
  }
}

void writeTimeAndReadingToFile() {
  File outputFile;

  outputFile = SD.open(nextFileName, FILE_WRITE); 
  if (!outputFile) {
    printMessage(MSG_FILE_OPEN_FAILED);
  }
  else {
    outputFile.print(eHZ_1_Reading_timeStamp);
    outputFile.print(eHZ_1_Reading);
    outputFile.print(";");
    outputFile.print(eHZ_2_Reading_timeStamp);
    outputFile.println(eHZ_2_Reading);

    outputFile.close();
  }
}

void getSMLData(SoftwareSerial mySoftSer, int eHZ_Number) {
  unsigned int nextBufferPosition = 0;
  unsigned long lastReadTime = 0;

   // clear the message buffer
  memset(buffer, 0, sizeof(buffer));
  
  // initialize data acquisition LEDs assuming everything is fine
  if ( eHZ_Number == eHZ1_ID ) {           // remember NO error during SML data acquisition for eHZ1
    SML_DataAcquisition_eHZ1 = true;
  }
  if ( eHZ_Number == eHZ2_ID ) {           // remember NO error during SML data acquisition for eHZ2
    SML_DataAcquisition_eHZ2 = true;
  }
 
  // remove a pending overflow flag (might have been left over from a previous run) 
  // and ensure that the SoftwareSerial library is listening
  mySoftSer.overflow();
  mySoftSer.listen();

  // wait until actual data is available
  while (!mySoftSer.available());
  // Next 2 lines ("Serial.print...") not necessary for algorithm, however might be useful for information purpose on serial monitor.
  // In addition reading of serial data SEEMs to be more stable (less errors such as "invalid escape sequence"). Maybe 
  // something like "delay(200);" might also work...
  Serial.print("Number of bytes available at SW serial:");
  Serial.println(mySoftSer.available());

  // keep reading data until either the message buffer is filled or no more data was
  // received for SERIAL_READ_TIMEOUT_MS ms
  
  lastReadTime = millis();
  while (millis() - lastReadTime < SERIAL_READ_TIMEOUT_MS) {
    if (mySoftSer.available()) {
      buffer[nextBufferPosition] = mySoftSer.read();
      lastReadTime = millis();
      if (nextBufferPosition >= SML_MSG_BUFFER_SIZE) {
        dummySerial.listen();                     // disable further IR input by switching software serial input to a dummy instance
        printMessage(MSG_BUFFER_OVERFLOW);
        
        if ( eHZ_Number == eHZ1_ID ) {           // remember error during SML data acquisition for eHZ1
          SML_DataAcquisition_eHZ1 = false;
        }
        if ( eHZ_Number == eHZ2_ID ) {           // remember error during SML data acquisition for eHZ2
          SML_DataAcquisition_eHZ2 = false;
        }       
        return;
      }
      nextBufferPosition += 1;
      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
    }
  }

  // report an error if an overflow condition was encountered - the data received is useless
  // in this case :-(
  if (mySoftSer.overflow()) {
    dummySerial.listen(); // disable further IR input
    printMessage(MSG_SERIAL_OVERFLOW);
    
    if ( eHZ_Number == eHZ1_ID ) {           // remember error during SML data acquisition for eHZ1
      SML_DataAcquisition_eHZ1 = false;
    }
    if ( eHZ_Number == eHZ2_ID ) {           // remember error during SML data acquisition for eHZ2
      SML_DataAcquisition_eHZ2 = false;
    }
  }
  else {
    dummySerial.listen(); // disable further IR input
    // check the header
    printMessage(MSG_NUM_BYTES_READ, nextBufferPosition + 1);
    if (!isValidHeader()) {
      // not a valid header - notify the user...
      printMessage(MSG_INVALID_HEADER);
      // ...and empty the receiver buffer (wait for the end of the current data stream
      while (mySoftSer.available() > 0) {
        mySoftSer.read();
      }
      if ( eHZ_Number == eHZ1_ID ) {           // remember error during SML data acquisition for eHZ1
        SML_DataAcquisition_eHZ1 = false;
      }
      if ( eHZ_Number == eHZ2_ID ) {           // remember error during SML data acquisition for eHZ2
        SML_DataAcquisition_eHZ2 = false;
      }    
    }
  }
}

unsigned long getElectricityMeterReading(char SML_buff) {
  /* Returns electricity meter reading in kWh
   * Electricity meter reading (for eHZ-IW8E2A5) is represented by the five bytes at buffer positions 155-159.
   * 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
   * However, use only lowest 4 bytes (i.e., meaningful data start at position 156). Highest number represented by these four bytes is
   * in HEX FF FF FF FF or in DEC 4294967295 (= 2^32). This would correspond to approx. 429 496 kWh.
   * Assuming an (realistic) average energy consumption of about 5000 kWh per year for the heat pump, this would allow
   * to store the meter reading for about 85 years !
   * Therefore use data type unsigned long (allows values from 0 ... 2^32-1 = 0 ... 4 294 967 295)
  */
  unsigned char EMReading_buf[4];
  unsigned long EMReading;

   // clear the EMReading buffer
  memset(EMReading_buf, 0, sizeof(EMReading_buf));

  // get 4 bytes representing the electricity meter reading and write them in opposite order to new array. Then create unsigned long.
  // Algorhithm see https://forum.arduino.cc/index.php?topic=108865.0
  for(int i=3; i>=0; i--) {
    EMReading_buf[3-i] = (buffer[156+i]);
  }
  EMReading = *((unsigned long *) (& EMReading_buf[0]));
  EMReading = EMReading/10;                               // consider scaler in SML data -> divide by 10 to get Wh
  return EMReading;
}

/**
 * isValidHeader - returns true if the global message buffer begins with a valid SML escape sequence.
 */
inline boolean isValidHeader() {
  return ((buffer[0] == 0x1b) &&
          (buffer[1] == 0x1b) &&
          (buffer[2] == 0x1b) &&
          (buffer[3] == 0x1b) &&
          (buffer[4] == 0x01) &&
          (buffer[5] == 0x01) &&
          (buffer[6] == 0x01) &&
          (buffer[7] == 0x01));
}

/**
 * for DS1302 based RTC time. Not necessarily (to be) used for 
 */
namespace {

String dayAsString(const Time::Day day) {
  switch (day) {
    case Time::kSunday: return "Sunday";
    case Time::kMonday: return "Monday";
    case Time::kTuesday: return "Tuesday";
    case Time::kWednesday: return "Wednesday";
    case Time::kThursday: return "Thursday";
    case Time::kFriday: return "Friday";
    case Time::kSaturday: return "Saturday";
  }
  return "(unknown day)";
}

}  // namespace


/***************************************************************************************************
 * MAIN ROUTINES
 ***************************************************************************************************/

/**
 * SETUP
 */
void setup() {
  // Serial communication
  // =====================================================================================================
  Serial.begin(9600);
  
  // Pin configuration
  // =====================================================================================================
  printMessage(MSG_INIT_HARDWARE);
  pinMode(IR_RX_PIN_1, INPUT);
  pinMode(IR_TX_PIN_1, OUTPUT);
  pinMode(IR_RX_PIN_2, INPUT);
  pinMode(IR_TX_PIN_2, OUTPUT);
  pinMode(SD_CS_PIN, OUTPUT);
  pinMode(SD_SS_PIN, OUTPUT);
  pinMode(DUMMY_RX_PIN, INPUT);
  pinMode(DUMMY_TX_PIN, OUTPUT);
  Serial.println("...done !");

  // Setup of diagnosis / warning LEDs
  // =====================================================================================================
  // Data acquisition diagnosis LED ( let blink 5 times to prove it's working)
  //-------------------------------------------------------------------------------------------------------
  pinMode(LED_SML_Acquisition_eHZ1, OUTPUT);
  digitalWrite(LED_SML_Acquisition_eHZ1, LOW);
  pinMode(LED_SML_Acquisition_eHZ2, OUTPUT);
  digitalWrite(LED_SML_Acquisition_eHZ2, LOW);


  // SD card setup 
  // =====================================================================================================
  Serial.println("Initializing SD card...");
  if (!SD.begin(SD_CS_PIN)) {
    printMessage(MSG_INIT_SD_CARD_ERROR);
//    errorSound(ERR_INIT_SD_CARD_ERROR);
    stop();
  }
  else {
    Serial.println("...done !");
  }

  // RTC setup
  // =====================================================================================================
  // 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.
  rtc.writeProtect(false);
  rtc.halt(false);
/*  // Uncomment in case current time should be written to RTC shield
  // Make a new time object to set the date and time and set the time
  // Format: year, moth, day, hour, minute, second, day of the week (1=Sunday))
  Time t(2020, 3, 14, 9, 12, 0, 7);
  rtc.time(t);
*/

  // Sofware Serial setup
  // =====================================================================================================
  dummySerial.begin(9600);
  mySerial_1.begin(9600);                    // eHZ sends at fix rate of 9600 Baud
  mySerial_2.begin(9600);                    // eHZ sends at fix rate of 9600 Baud
  
  // Warning LED ( let blink 5 times to proove it's working)
  //-------------------------------------------------------------------------------------------------------
  pinMode(LED_WriteWarning, OUTPUT);
  for(byte i=0; i<5; i++) {
    digitalWrite(LED_WriteWarning, HIGH);
    delay(250);
    digitalWrite(LED_WriteWarning, LOW);
    delay(250);
  }

  // determine the first file name to use
  findNextFileNumber();

}

/**
 * END of SETUP
 */

void loop() {
  // turn off all LEDs
  digitalWrite(LED_WriteWarning, LOW);
  digitalWrite(LED_SML_Acquisition_eHZ1, LOW);
  digitalWrite(LED_SML_Acquisition_eHZ2, LOW);
//  reportFreeMemory();                                       // for information purposes only

  numberSMLBytesRead = 0;
  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.
    getSMLData(mySerial_1, eHZ1_ID);                          // get SML data from 1st electricity meter (Input: A0), store it into (global) buffer
  }
  createTimeStamp(eHZ1_ID);
  Serial.println("Time stamp eHZ_1 created...");
  eHZ_1_Reading = getElectricityMeterReading(buffer);     // extract OBIS 1.8.0. in Wh x 10 and store it in (global) variable
  Serial.print("eHZ_1 Reading: ");
  Serial.println(eHZ_1_Reading);
  
  numberSMLBytesRead = 0;
  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.
    getSMLData(mySerial_2, eHZ2_ID);                          // get SML data from 2nd electricity meter (Input: A1), store it into (global) buffer
  }
  createTimeStamp(eHZ2_ID);
  Serial.println("Time stamp eHZ_2 created...");
  eHZ_2_Reading = getElectricityMeterReading(buffer);     // extract OBIS 1.8.0. in Wh x 10 and store it in (global) variable 
  Serial.print("eHZ_2 Reading: ");
  Serial.println(eHZ_2_Reading);

  Serial.println();
  
  // turn ON red LED to show that writing is beeing performed and SD card should NOT be removed. Prewarn by blinking for about 3s.
  for(byte i=0; i<3; i++) {
    digitalWrite(LED_WriteWarning, HIGH);
    delay(500);
    digitalWrite(LED_WriteWarning, LOW);
    delay(500);
  }
  digitalWrite(LED_WriteWarning, HIGH);
  // write data to file (time + 2 readings)
  writeTimeAndReadingToFile();
  // turn OFF red LED to show that writing is FINISHED and SD card CAN be removed
  digitalWrite(LED_WriteWarning, LOW);
  delay(1000);
   
  // set blinking behaviour of LED depending on result of SML data acquisition of eHZ1
  if(SML_DataAcquisition_eHZ1 == true) {
     eHZ1Blink_ON = 1000;
  }
  if(SML_DataAcquisition_eHZ1 == false) {
    eHZ1Blink_ON = 250;
  }
  // set blinking behaviour of LED depending on result of SML data acquisition of eHZ1
  if(SML_DataAcquisition_eHZ2 == true) {
    eHZ2Blink_ON = 1000;
  }
  if(SML_DataAcquisition_eHZ2 == false) {
    eHZ2Blink_ON = 250;
  }

  startTime = millis();
  // show result of data acquisition by blinking slow (success) or fast (fail). slow/fast defined above.
  while(millis()-startTime < loggingInterval){
    for(int i=0; i<3; i++) {
      digitalWrite(LED_SML_Acquisition_eHZ1, HIGH);
      delay(eHZ1Blink_ON);
      digitalWrite(LED_SML_Acquisition_eHZ1, LOW);
      delay(eHZ1Blink_OFF);
    }
    for(int i=0; i<3; i++) {
      digitalWrite(LED_SML_Acquisition_eHZ2, HIGH);
      delay(eHZ2Blink_ON);
      digitalWrite(LED_SML_Acquisition_eHZ2, LOW);
      delay(eHZ2Blink_OFF);
    }              
  }

}

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.
// A0: amplified signal of 1st IR diode (output=collector of 1st transistor)
// A1: amplified signal of 2nd IR diode (output=collector of 2nd transistor) (in case 2 IR diodes fo 2 eHZs used)
// A6: raw signal of 1st IR photo diode (base of 1st transistor)
// A7: raw signal of 2nd IR photo diode (base of 2nd transistor) (in case 2 IR diodes fo 2 eHZs used) 


void setup() {
  
  //for 1st IR diode  
  pinMode(A0, INPUT);
  pinMode(A6, INPUT);
  
  //for 2nd IR diode
  pinMode(A1, INPUT);
  pinMode(A7, INPUT);
  Serial.begin(9600);  
}

void loop() {
  //for 1st IR diode

  Serial.print("A0: ");
  Serial.print(analogRead(A0));
  Serial.print(" ;  A6: ");
  Serial.print(analogRead(A6));


  //for 2nd IR diode
  Serial.print(" ; A1: ");
  Serial.print(analogRead(A1));
  Serial.print(" ;  A7: ");
  Serial.println(analogRead(A7));


}

eHZ-IW8E2A5_EMLogger_ArdNanoEvery_TMG_09.ino

Arduino
Recording of 2 eHZ readings to an SD card incl. supply voltage monitoring and recording
/*
 * Read output of 2 electricity meters of type eHZ-IW8E2A5 and write data to SD card.
 * 
 * Written by Torsten Geppert (2020/2021) based on ideas/parts of code from
 *   - https://www.volkszaehler.org/
 *   - and hardware / software by Volker Wegert https://sourceforge.net/p/ehzy/wiki/Hardware/
 *   - 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)
 *
 * Major changes/adjustments
 * 1) Hardware modified. Replaced resistors
 *    - 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
 *    - between collector of transistor and VCC: 15 kOhm --> 22 kOhm 
 * 2) w/ resepect to Volker Wegert's code:
 *   - 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)
 *   - 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)
 * 3) Change in Arduino related Hardware  
 *   - Use Arduino Nano Every + DS1302 based RTC module and SD card shield (instead of Arduino Uno + SD card shield incl. RTC).
 *     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)
 *     -  HW used:
 *       - 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)
 *       - SD shield: VMA304 by Velleman (e.g., https://www.conrad.de/de/p/makerfactory-sd-karten-logging-shield-fuer-arduino-2-tlg-1612765.html)
 *       - 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 !!!
 *       - added 3 LEDs to show SUCCESS or FAILURE of SD card initialization and data aqcuisition
 *       - added piezo speaker (accoustic warning in case supply voiltage - e.g., from battery) drops below defineable limit
 *       - additional volatge display (manually triggered by push-button (NOT connected to/controlled by Arduino)
 * 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)
 *    see comment below "Uncomment in case current time should be written to RTC shield"
 * 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.
 *    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 ## ## ## ## ##)
 *    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
 */

/* ======================================================================================================= 
 * Include Libraries and alike
 * ======================================================================================================= */

#include <stdarg.h>
#include <stdio.h>
#include <avr/pgmspace.h>
#include <SoftwareSerial.h> // Serial interfaces realized by software (to allow, e.g., reading of more than just 1 sensor/eHz)
#include <SD.h>             // e.g., to write files to SD card etc.
#include <SPI.h>            // needed for SD.h
#include <DS1302.h>         // needed for RTC shield

/***************************************************************************************************
 * CONSTANTS and alike
 ***************************************************************************************************/

// Software Serial stuff =============================================================================================
// Serial Data from source 1 (IR diode of 1st eHZ)
#define IR_RX_PIN_1     A0                  // Pin 14 on Arduino Nano Every
#define IR_TX_PIN_1     A2                  // not used, but required for SoftwareSerial; Pin 16 on Arduino Nano Every
// Serial Data from source 2 (IR diode of 2nd eHZ)
#define IR_RX_PIN_2     A1                  // Pin 14 on Arduino Nano Every
#define IR_TX_PIN_2     A2                  // not used, but required for SoftwareSerial; Pin 16 on Arduino Nano Every
// Dummy software serial
#define DUMMY_RX_PIN  9                     // not connected
#define DUMMY_TX_PIN  A3                    // not connected

// SD card shield VMA304 stuff =============================================================================================
#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)
#define SD_SS_PIN    10                     // ###really needed ? Check if can be deleted ###  

// RTC stuff shield VMA301 stuff ===========================================================================================
// uses Pins A3, A4, A5 on Arduino Nano Every (Physical Pins 7, 8, 9; aka ???, SDA, SCL)
const int kCePin   = 17;                    // Chip Enable
const int kIoPin   = 18;                    // Input/Output
const int kSclkPin = 19;                    // Serial Clock

// Other stuff =============================================================================================
#define loggingInterval 3600000             // time between readings in milli-seconds, e.g.,
                                            // 5 min = 5 * 60 * 1000 = 300 000
                                            // 60 min = 60 * 60 * 1000 = 3 600 000                                            
// supply voltage monitoring stuff ===========================================================================================
#define buzzPin 3          // Buzzer connected to PWM Pin D3
#define supplVoltPin A2    // center contact of voltage divider (GND <-> supply Voltage (NOT 5V ! but V_in))
#define operatVolt 5     // operating voltage of Arduino (used as reference for ADC)
#define buzzTimeEarly 8    // earliest time (hour of day; e.g., 8 AM = 8; 10PM -> 22) buzzer is triggered in case supply voltage below limit
#define buzzTimeLate 20    // latest time (hour of day) buzzer is triggered in case supply voltage below limit
float supplVoltLim = 5.6;  // absolute supply voltage limit; serves as trigger for buzzer

int numberSMLBytesRead = 0;                 // used to restart reading of SML data in case less than specified number of bytes (typically 397 for my electricity meter) read         

const int LED_WriteWarning = 4;             // Pin D4 (physical pin no. 22): LED to signalize approaching or ongoing write to SD card
const 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*)
const 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)
// *frequency can be changed below to, e.g. 2 Hz by setting on time to 500
int eHZ1Blink_ON=1000;                       // time LED for eHZ1 is turned on
int eHZ2Blink_ON=1000;                       // time LED for eHZ2 is turned on 
int eHZ1Blink_OFF=250;                       // time LED for eHZ1 is turned off
int eHZ2Blink_OFF=250;                       // time LED for eHZ2 is turned off  
bool 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
bool 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
const int eHZ1_ID = 1;
const int eHZ2_ID = 2;
unsigned long startTime;

/**
 * size of the SML message buffer = meximum number of bytes that can be received
 */
#define SML_MSG_BUFFER_SIZE    400      // eHZ-IW8E2A5 delivers SML telegram of 396 bytes

/**
 * size of a filename (8.3 + terminating 0x00 = 13 chars)
 */
#define FILENAME_SIZE           13 

/**
 * maximum number of files that can be stored
 */
#define MAX_FILE_NUMBER   99999999

/**
 * maximum time to wait for the end of a data packet received via IR
 */
#define SERIAL_READ_TIMEOUT_MS 500

/***************************************************************************************************
 * MESSAGES
 ***************************************************************************************************/

/**
 * maximum length of a message (entire text after variable substitution!)
 */
#define MAX_MESSAGE_LENGTH        50

/**
 * message numbers
 */
#define MSG_PROGRAM_STOPPED        0
#define MSG_NEXT_FILENAME          1
#define MSG_NO_FILE_NAMES_LEFT     2
#define MSG_INIT_HARDWARE          3
#define MSG_INIT_SD_CARD           4
#define MSG_INIT_SD_CARD_ERROR     5
#define MSG_BYTE_READ              6
#define MSG_BUFFER_OVERFLOW        7
#define MSG_SERIAL_OVERFLOW        8  
#define MSG_INVALID_HEADER         9
#define MSG_FILE_OPEN_FAILED      10
#define MSG_FILE_WRITTEN          11
#define MSG_FREE_MEMORY           12
#define MSG_NUM_BYTES_READ        13

/**
 * actual message texts - caution, adapt MAX_MESSAGE_LENGTH if required!
 *                               ....+....1....+....2....+....3....+....4....+....5
 */
const char msgText00[] PROGMEM = "Program stopped.";
const char msgText01[] PROGMEM = "Next output file name is '%s'";
const char msgText02[] PROGMEM = "No more file names left";
const char msgText03[] PROGMEM = "Initializing Hardware...";
const char msgText04[] PROGMEM = "Initializing SD Card...";
const char msgText05[] PROGMEM = "SD Card initialization failed";
const char msgText06[] PROGMEM = "Read byte %02hhu from IR receiver";
const char msgText07[] PROGMEM = "Message buffer overflow";
const char msgText08[] PROGMEM = "Serial buffer overflow";
const char msgText09[] PROGMEM = "Invalid escape sequence";
const char msgText10[] PROGMEM = "Unable to open output file";
const char msgText11[] PROGMEM = "%u bytes of data written to file '%s'";
const char msgText12[] PROGMEM = "%u bytes of memory available";
const char msgText13[] PROGMEM = "%u bytes read";

/**
 * table for easier access to the message texts
 */
const char *const msgTextTable[] PROGMEM = {   
  msgText00,
  msgText01,
  msgText02,
  msgText03,
  msgText04,
  msgText05,
  msgText06,
  msgText07,
  msgText08,
  msgText09,
  msgText10,
  msgText11,
  msgText12,
  msgText13
};

/***************************************************************************************************
 * GLOBAL VARIABLES (YUCK!)
 ***************************************************************************************************/

/**
 * instance of the SoftwareSerial library to handle the IR communication
 */
SoftwareSerial mySerial_1(IR_RX_PIN_1, IR_TX_PIN_1);              // used for readout of SML data of eHZ1
SoftwareSerial mySerial_2(IR_RX_PIN_2, IR_TX_PIN_2);              // used for readout of SML data of eHZ2
SoftwareSerial dummySerial(DUMMY_RX_PIN, DUMMY_TX_PIN);

// Create a RTC DS1302 object.
DS1302 rtc(kCePin, kIoPin, kSclkPin);

/**
 * variables to keep track of the name of the next file to be written
 */
unsigned long nextFileNumber = 0;
char nextFileName[FILENAME_SIZE];

/**
 * the global buffer to store the SML message currently being read
 */
unsigned char buffer[SML_MSG_BUFFER_SIZE];

/**
 * Readings of eHZ 1 and 2 (after extraction from SML data) and corresponding timestamps
 */
unsigned long eHZ_1_Reading;                
unsigned long eHZ_2_Reading;
char eHZ_1_Reading_timeStamp[50];
char eHZ_2_Reading_timeStamp[50];

/**
 * Supply voltage
 */
float supplVolt;

/***************************************************************************************************
 * SUBROUTINES
 ***************************************************************************************************/

/** 
 * printMessage - reads a message text from the PROGMEM, performs variable substitution and 
 *                writes the resulting text to the serial console. Use MSG_* constants for
 *                messageNumber.
 */
void printMessage(int messageNumber, ...) {
  va_list args;
  char format[MAX_MESSAGE_LENGTH];
  char buffer[MAX_MESSAGE_LENGTH];
  va_start(args, messageNumber);
  strncpy_P(format, (char*)pgm_read_word(&(msgTextTable[messageNumber])), MAX_MESSAGE_LENGTH);
  vsnprintf(buffer, MAX_MESSAGE_LENGTH, format, args);
  Serial.println(buffer);
  va_end(args);
}

/**
 * reportFreeMemory - determines the amount of free memory and logs it to the serial console.
 */
void reportFreeMemory() {
  int freeMemory;
  uint8_t * heapptr, * stackptr;
  stackptr = (uint8_t *) malloc(4);     // use stackptr temporarily
  heapptr = stackptr;                   // save value of heap pointer
  free(stackptr);                       // free up the memory again (sets stackptr to 0)
  stackptr = (uint8_t *) (SP);          // save value of stack pointer
  freeMemory = stackptr - heapptr;
  printMessage(MSG_FREE_MEMORY, freeMemory);
}  

/**
 * stop - stops program execution in a controlled fashion (endless loop).
 */
void stop() {
  printMessage(MSG_PROGRAM_STOPPED);
  while(1);
}

/**
 * findNextFileNumber - determines the number and name of the next file available. This will change
 *                      the global variables nextFileName and nextFileNumber. This routine will
 *                      stop the entire program if no free filenames can be found.
 */
void findNextFileNumber() {
  do {
    sprintf(nextFileName, "%08lu.CSV", nextFileNumber);
    if (!SD.exists(nextFileName)) {
      printMessage(MSG_NEXT_FILENAME, nextFileName);
      return;
    } else {
      nextFileNumber += 1;
      if (nextFileNumber > MAX_FILE_NUMBER) {
        printMessage(MSG_NO_FILE_NAMES_LEFT);
//        errorSound(ERR_NO_FILE_NAMES_LEFT);
        stop();
      }
    }
  } while (true); 
}

void createTimeStamp(int eHZ_Number) {
  // Get the current time and date from the chip.
  Time t = rtc.time();
  // Format the time and date and insert into the temporary buffer.
  char currDAndT_buf[50];
  snprintf(currDAndT_buf, sizeof(currDAndT_buf), "%04d-%02d-%02d %02d:%02d:%02d;",
         t.yr, t.mon, t.date,
         t.hr, t.min, t.sec);

   // assign determined timestamp to either eHZ1 or eHz2
   if ( eHZ_Number == eHZ1_ID ) {           
    for (int i = 0; i<50; i++) {
      eHZ_1_Reading_timeStamp[i]=currDAndT_buf[i];
    }
//Serial.println("eHZ_1 Timestamp copied");
   }
  if ( eHZ_Number == eHZ2_ID ) {           
    for (int i = 0; i<50; i++) {
      eHZ_2_Reading_timeStamp[i]=currDAndT_buf[i];
    }
//Serial.println("eHZ_2 Timestamp copied");
  }
}

void writeTimeAndReadingToFile() {
  Serial.println("Writing data to SD card...");
  
  File outputFile;

  outputFile = SD.open(nextFileName, FILE_WRITE); 
  if (!outputFile) {
    printMessage(MSG_FILE_OPEN_FAILED);
  }
  else {
    outputFile.print(eHZ_1_Reading_timeStamp);
    outputFile.print(eHZ_1_Reading);
    outputFile.print(";");
    outputFile.print(eHZ_2_Reading_timeStamp);
    outputFile.print(eHZ_2_Reading);
    outputFile.print(";");
    outputFile.print(eHZ_2_Reading_timeStamp);
    outputFile.println(supplVolt);
    outputFile.close();
    Serial.println("...done...");
  }
}

void getSMLData(SoftwareSerial mySoftSer, int eHZ_Number) {
  unsigned int nextBufferPosition = 0;
  unsigned long lastReadTime = 0;

   // clear the message buffer
  memset(buffer, 0, sizeof(buffer));
  
  // initialize data acquisition LEDs assuming everything is fine
  if ( eHZ_Number == eHZ1_ID ) {           // remember NO error during SML data acquisition for eHZ1
    SML_DataAcquisition_eHZ1 = true;
  }
  if ( eHZ_Number == eHZ2_ID ) {           // remember NO error during SML data acquisition for eHZ2
    SML_DataAcquisition_eHZ2 = true;
  }
 
  // remove a pending overflow flag (might have been left over from a previous run) 
  // and ensure that the SoftwareSerial library is listening
  mySoftSer.overflow();
  mySoftSer.listen();

  // wait until actual data is available
  while (!mySoftSer.available());
  // Next 2 lines ("Serial.print...") not necessary for algorithm, however might be useful for information purpose on serial monitor.
  // In addition reading of serial data SEEMs to be more stable (less errors such as "invalid escape sequence"). Maybe 
  // something like "delay(200);" might also work...
  Serial.print("Number of bytes available at SW serial:");
  Serial.println(mySoftSer.available());

  // keep reading data until either the message buffer is filled or no more data was
  // received for SERIAL_READ_TIMEOUT_MS ms
  
  lastReadTime = millis();
  while (millis() - lastReadTime < SERIAL_READ_TIMEOUT_MS) {
    if (mySoftSer.available()) {
      buffer[nextBufferPosition] = mySoftSer.read();
      lastReadTime = millis();
      if (nextBufferPosition >= SML_MSG_BUFFER_SIZE) {
        dummySerial.listen();                     // disable further IR input by switching software serial input to a dummy instance
        printMessage(MSG_BUFFER_OVERFLOW);
        
        if ( eHZ_Number == eHZ1_ID ) {           // remember error during SML data acquisition for eHZ1
          SML_DataAcquisition_eHZ1 = false;
        }
        if ( eHZ_Number == eHZ2_ID ) {           // remember error during SML data acquisition for eHZ2
          SML_DataAcquisition_eHZ2 = false;
        }       
        return;
      }
      nextBufferPosition += 1;
      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
    }
  }

  // report an error if an overflow condition was encountered - the data received is useless
  // in this case :-(
  if (mySoftSer.overflow()) {
    dummySerial.listen(); // disable further IR input
    printMessage(MSG_SERIAL_OVERFLOW);
    
    if ( eHZ_Number == eHZ1_ID ) {           // remember error during SML data acquisition for eHZ1
      SML_DataAcquisition_eHZ1 = false;
    }
    if ( eHZ_Number == eHZ2_ID ) {           // remember error during SML data acquisition for eHZ2
      SML_DataAcquisition_eHZ2 = false;
    }
  }
  else {
    dummySerial.listen(); // disable further IR input
    // check the header
    printMessage(MSG_NUM_BYTES_READ, nextBufferPosition + 1);
    if (!isValidHeader()) {
      // not a valid header - notify the user...
      printMessage(MSG_INVALID_HEADER);
      // ...and empty the receiver buffer (wait for the end of the current data stream
      while (mySoftSer.available() > 0) {
        mySoftSer.read();
      }
      if ( eHZ_Number == eHZ1_ID ) {           // remember error during SML data acquisition for eHZ1
        SML_DataAcquisition_eHZ1 = false;
      }
      if ( eHZ_Number == eHZ2_ID ) {           // remember error during SML data acquisition for eHZ2
        SML_DataAcquisition_eHZ2 = false;
      }    
    }
  }
}

unsigned long getElectricityMeterReading(char SML_buff) {
  /* Returns electricity meter reading in kWh
   * Electricity meter reading (for eHZ-IW8E2A5) is represented by the five bytes at buffer positions 155-159.
   * 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
   * However, use only lowest 4 bytes (i.e., meaningful data start at position 156). Highest number represented by these four bytes is
   * in HEX FF FF FF FF or in DEC 4294967295 (= 2^32). This would correspond to approx. 429 496 kWh.
   * Assuming an (realistic) average energy consumption of about 5000 kWh per year for the heat pump, this would allow
   * to store the meter reading for about 85 years !
   * Therefore use data type unsigned long (allows values from 0 ... 2^32-1 = 0 ... 4 294 967 295)
  */
  unsigned char EMReading_buf[4];
  unsigned long EMReading;

   // clear the EMReading buffer
  memset(EMReading_buf, 0, sizeof(EMReading_buf));

  // get 4 bytes representing the electricity meter reading and write them in opposite order to new array. Then create unsigned long.
  // Algorhithm see https://forum.arduino.cc/index.php?topic=108865.0
  for(int i=3; i>=0; i--) {
    EMReading_buf[3-i] = (buffer[156+i]);
  }
  EMReading = *((unsigned long *) (& EMReading_buf[0]));
  EMReading = EMReading/10;                               // consider scaler in SML data -> divide by 10 to get Wh
  return EMReading;
}

/**
 * isValidHeader - returns true if the global message buffer begins with a valid SML escape sequence.
 */
inline boolean isValidHeader() {
  return ((buffer[0] == 0x1b) &&
          (buffer[1] == 0x1b) &&
          (buffer[2] == 0x1b) &&
          (buffer[3] == 0x1b) &&
          (buffer[4] == 0x01) &&
          (buffer[5] == 0x01) &&
          (buffer[6] == 0x01) &&
          (buffer[7] == 0x01));
}

/**
 * for DS1302 based RTC time. Not necessarily (to be) used for 
 */
namespace {

String dayAsString(const Time::Day day) {
  switch (day) {
    case Time::kSunday: return "Sunday";
    case Time::kMonday: return "Monday";
    case Time::kTuesday: return "Tuesday";
    case Time::kWednesday: return "Wednesday";
    case Time::kThursday: return "Thursday";
    case Time::kFriday: return "Friday";
    case Time::kSaturday: return "Saturday";
  }
  return "(unknown day)";
}

}  // namespace


/***************************************************************************************************
 * MAIN ROUTINES
 ***************************************************************************************************/

/** ########################################################################################################
 * SETUP
    ########################################################################################################
 */
void setup() {
  // Serial communication
  // =====================================================================================================
  Serial.begin(9600);

  Serial.println("===== Starting setup =====");
  // Pin configuration
  // =====================================================================================================
  printMessage(MSG_INIT_HARDWARE);
  pinMode(IR_RX_PIN_1, INPUT);
  pinMode(IR_TX_PIN_1, OUTPUT);
  pinMode(IR_RX_PIN_2, INPUT);
  pinMode(IR_TX_PIN_2, OUTPUT);
  pinMode(SD_CS_PIN, OUTPUT);
  pinMode(SD_SS_PIN, OUTPUT);
  pinMode(DUMMY_RX_PIN, INPUT);
  pinMode(DUMMY_TX_PIN, OUTPUT);
  Serial.println("...done !... pinMode set !");

  // RTC setup
  // =====================================================================================================
  // 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.
  rtc.writeProtect(false);
  rtc.halt(false);
/* // Uncomment in case current time should be written to RTC shield
  // Make a new time object to set the date and time and set the time
  // Format: year, moth, day, hour, minute, second, day of the week (1=Sunday))
  Time t(2021, 3, 03, 21, 48, 0, 4);
  rtc.time(t);
*/
  
  // Setup of supply voltage monitoring
  // =====================================================================================================
  Serial.println("Testing LEDs & piezo speaker...");
  pinMode(buzzPin, OUTPUT);
  pinMode(supplVoltPin, INPUT);
  // quick sound check to prove piezo speaker works (only during day time...)
  
  Time t2 = rtc.time();
  if( t2.hr > buzzTimeEarly && t2.hr < buzzTimeLate ) {
    for (int i = 0; i <5; i++) {
      tone(buzzPin, 1000, 250);
      delay(500);
      tone(buzzPin, 5000, 250);
      delay(500);
    }
  }

  // Setup of data acquisition and diagnosis / warning LEDs
  // =====================================================================================================
  pinMode(LED_SML_Acquisition_eHZ1, OUTPUT);
  digitalWrite(LED_SML_Acquisition_eHZ1, LOW);
  pinMode(LED_SML_Acquisition_eHZ2, OUTPUT);
  digitalWrite(LED_SML_Acquisition_eHZ2, LOW);
  pinMode(LED_WriteWarning, OUTPUT);
  digitalWrite(LED_WriteWarning, LOW);

  // ...let blink 5 times to proove the're working
  //-------------------------------------------------------------------------------------------------------
  Serial.println("Data acquisition LED 1 (BLUE): ...blink 5 times");
  for(byte i=0; i<5; i++) {
    digitalWrite(LED_SML_Acquisition_eHZ1, HIGH);
    delay(250);
    digitalWrite(LED_SML_Acquisition_eHZ1, LOW);
    delay(250);
  }
  Serial.println("...done !");
  Serial.println("Data acquisition LED 2 (YELLOW): ...blink 5 times");
  for(byte i=0; i<5; i++) {
    digitalWrite(LED_SML_Acquisition_eHZ2, HIGH);
    delay(250);
    digitalWrite(LED_SML_Acquisition_eHZ2, LOW);
    delay(250);
  }
  Serial.println("...done !");
  Serial.println("SD status LED (RED): ...blink 5 times");
  for(byte i=0; i<5; i++) {
    digitalWrite(LED_WriteWarning, HIGH);
    delay(250);
    digitalWrite(LED_WriteWarning, LOW);
    delay(250);
  }
  Serial.println("...done !");

  // SD card setup 
  // =====================================================================================================
  Serial.println("Initializing SD card...");
  if (!SD.begin(SD_CS_PIN)) {
    printMessage(MSG_INIT_SD_CARD_ERROR);
//    errorSound(ERR_INIT_SD_CARD_ERROR);
    stop();
  }
  else {
    Serial.println("...done !");
    Time t2 = rtc.time();
    if( t2.hr > buzzTimeEarly && t2.hr < buzzTimeLate ) {
      for (int i = 0; i <10; i++) {
        tone(buzzPin, 250, 250);
        delay(500);
      }
    }
  }

  // Sofware Serial setup
  // =====================================================================================================
  dummySerial.begin(9600);
  mySerial_1.begin(9600);                    // eHZ sends at fix rate of 9600 Baud
  mySerial_2.begin(9600);                    // eHZ sends at fix rate of 9600 Baud

  // determine the first file name to use
  findNextFileNumber();

}

/** ########################################################################################################
 * END of SETUP
    ########################################################################################################
 */

void loop() {
  // turn off all LEDs
  digitalWrite(LED_WriteWarning, LOW);
  digitalWrite(LED_SML_Acquisition_eHZ1, LOW);
  digitalWrite(LED_SML_Acquisition_eHZ2, LOW);
//  reportFreeMemory();                                       // for information purposes only

  numberSMLBytesRead = 0;
  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.
    getSMLData(mySerial_1, eHZ1_ID);                          // get SML data from 1st electricity meter (Input: A0), store it into (global) buffer
  }
  createTimeStamp(eHZ1_ID);
  Serial.println("Time stamp eHZ_1 created...");
  eHZ_1_Reading = getElectricityMeterReading(buffer);     // extract OBIS 1.8.0. in Wh x 10 and store it in (global) variable
  Serial.print("eHZ_1 Reading: ");
  Serial.println(eHZ_1_Reading);
  
  numberSMLBytesRead = 0;
  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.
    getSMLData(mySerial_2, eHZ2_ID);                          // get SML data from 2nd electricity meter (Input: A1), store it into (global) buffer
  }
  createTimeStamp(eHZ2_ID);
  Serial.println("Time stamp eHZ_2 created...");
  eHZ_2_Reading = getElectricityMeterReading(buffer);     // extract OBIS 1.8.0. in Wh x 10 and store it in (global) variable 
  Serial.print("eHZ_2 Reading: ");
  Serial.println(eHZ_2_Reading);

  supplVolt = analogRead(supplVoltPin) * 2 / 1024.0 * operatVolt;          // get actual supply voltage in V (multiply analog reading by 2 due to 1:1 voltage divider

  Serial.println();
  
  // turn ON red LED to show that writing is beeing performed and SD card should NOT be removed. Prewarn by blinking for about 3s.
  // -----------------------------------------------------------------------------------------------------------------------------
  for(byte i=0; i<3; i++) {
    digitalWrite(LED_WriteWarning, HIGH);
    delay(500);
    digitalWrite(LED_WriteWarning, LOW);
    delay(500);
  }
  // Write data to SD card
  // ---------------------
  digitalWrite(LED_WriteWarning, HIGH);
  // write data to file (time 1; reading 1; time 2; reading 2; time 2; supply voltage)
  writeTimeAndReadingToFile();
  // turn OFF red LED to show that writing is FINISHED and SD card CAN be removed
  digitalWrite(LED_WriteWarning, LOW);
  delay(1000);

  // Show result (SUCCESS/FAIL) of data acquisition for each channel
  // ---------------------------------------------------------------
  // set blinking behaviour of LED depending on result of SML data acquisition of eHZ1
  if(SML_DataAcquisition_eHZ1 == true) {
     eHZ1Blink_ON = 1000;
  }
  if(SML_DataAcquisition_eHZ1 == false) {
    eHZ1Blink_ON = 250;
  }
  // set blinking behaviour of LED depending on result of SML data acquisition of eHZ1
  if(SML_DataAcquisition_eHZ2 == true) {
    eHZ2Blink_ON = 1000;
  }
  if(SML_DataAcquisition_eHZ2 == false) {
    eHZ2Blink_ON = 250;
  }

  // wait for specified time interval until next data acquisition to be performed
  startTime = millis();
  // show result of data acquisition by blinking slow (success) or fast (fail). slow/fast defined above.
  while(millis()-startTime < loggingInterval){
    delay(60000);                      // only blink once a minute (to save battery energy)
    for(int i=0; i<3; i++) {
      digitalWrite(LED_SML_Acquisition_eHZ1, HIGH);
      delay(eHZ1Blink_ON);
      digitalWrite(LED_SML_Acquisition_eHZ1, LOW);
      delay(eHZ1Blink_OFF);
    }
    for(int i=0; i<3; i++) {
      digitalWrite(LED_SML_Acquisition_eHZ2, HIGH);
      delay(eHZ2Blink_ON);
      digitalWrite(LED_SML_Acquisition_eHZ2, LOW);
      delay(eHZ2Blink_OFF);
    }              
    // Sound buzzer in case supply voltage drops below defined limit; trigger alarm only during specified time slots.
    Time t2 = rtc.time();
Serial.print("supplyVolt: ");
Serial.println(supplVolt);
    if( supplVolt < supplVoltLim && t2.hr > buzzTimeEarly && t2.hr < buzzTimeLate ) { 
      for (int i = 0; i <5; i++) {
        tone(buzzPin, 1000, 250);
        delay(750);
      }
    }
  }

}

Credits

torstengeppert

torstengeppert

0 projects • 3 followers

Comments