diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..697b909 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +All notable changes to this project will be documented in this file. + + Added : for new features. + Changed : for changes in existing functionality. + Deprecated: for soon-to-be removed features. + Removed : for now removed features. + Fixed : for any bug fixes. + Security : in case of vulnerabilities. + +## [1.0.0] - 2022-11-29 +First working version. diff --git a/MQTT_energymeter.ino b/MQTT_energymeter.ino new file mode 100644 index 0000000..06c6b2e --- /dev/null +++ b/MQTT_energymeter.ino @@ -0,0 +1,284 @@ +/**********************************************************************************/ +/* */ +/* MQTT_energymeter counts light pulses from an energy meter, converts it to Wh */ +/* and publishes it to an MQTT broker via ethernet. Every 1000Wh the counter is */ +/* saved to EEPROM. Every power up this value is read from EERPOM as the starting */ +/* value for the counter, preventing data loss after a power outage. */ +/* */ +/* (C)2022 M.T. Konstapel https://meezenest.nl/mees */ +/* */ +/* This file is part of MQTT_energymeter. */ +/* */ +/* MQTT_energymeter is free software: you can redistribute it and/or modify */ +/* it under the terms of the GNU General Public License as published by */ +/* the Free Software Foundation, either version 3 of the License, or */ +/* (at your option) any later version. */ +/* */ +/* MQTT_energymeter is distributed in the hope that it will be useful, */ +/* but WITHOUT ANY WARRANTY; without even the implied warranty of */ +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ +/* GNU General Public License for more details. */ +/* */ +/* You should have received a copy of the GNU General Public License */ +/* along with MQTT_energymeter. If not, see . */ +/* */ +/**********************************************************************************/ + + #include +// The connection_data struct needs to be defined in an external file. +#include +#include +//#include "utility/logging.h" +#include "PubSubClient.h" +#include //Internal EEPROM + +#define MACADDRESS 0x12,0x7C,0x54,0x33,0xB4,0xA2 +#define MYIPADDR 192,168,88,120 +#define MYIPMASK 255,255,255,0 +#define MYDNS 192,168,88,1 +#define MYGW 192,168,88,1 +#define LISTENPORT 1000 +#define UARTBAUD 115200 + +/* CHANGE THESE VALUES TO CUSTOMIZE DATA LOGGER */ +#define CLIENT_ID "MeesElectronics" // Our MQTT client ID +#define CLIENT_TOPIC "energy/solar" // Name of the topic we publish +//IPAddress IP_MQTT_broker(192, 168, 89, 10); // IP address of the MQTT broker +//String string_ip_mqtt_broker = "192.168.88.10"; +char IP_string_MQTT_broker[20] = "192.168.88.11"; +uint16_t MQTT_port = 1884; +IPAddress IP_MQTT_broker; + +#define PULSES_PER_KWH 500 // 500 pulses from the energy meter is one kWh +/* END USER DEFINABLE VALUES */ + +uint8_t mac[6] = {MACADDRESS}; +uint8_t myIP[4] = {MYIPADDR}; +uint8_t myMASK[4] = {MYIPMASK}; +uint8_t myDNS[4] = {MYDNS}; +uint8_t myGW[4] = {MYGW}; + +EthernetServer server = EthernetServer(LISTENPORT); +EthernetClient ethClient; +PubSubClient mqttClient; + +#define PULSES_PER_WH 1000/PULSES_PER_KWH // Calculates how many pulses corresponds to a Wh +#define INTERVAL 10000 // 10 sec delay between MQTT publishings +long previousMillis; + +char numberArray[20]; +uint32_t PulseCount=0; // This variable holds the pulse count from the energy meter +uint32_t EnergyReading=0; // This variable holds the number of Watt-hours +uint32_t stored_kwh_count=0; // Counted whole kWh value sored in EEPROM + +void onDetectInterrupt() +{ + // The Arduino calls this function when it detects a falling edge on pin 2. + // Received a pulse from the energy meter: add one to the counter + PulseCount++; +} + +void setup() { + // setup serial communication + Serial.begin(UARTBAUD); + Serial.println(F("Energy meter with MQTT client.")); + + // Read EEPROM address 0. If it is 0xFF the EEPROM is probaly empty and we set EEPROM address 0 to 0x5A + if (EEPROM.read(0) == 0xFF) { + Serial.println(F("Powered up for the first time. Saving the default settings to the EEPROM.")); + EEPROM.write(0, 0x5A); + EEPROM.put(1, stored_kwh_count); // Save zero to EEPROM + EEPROM.put(5, MQTT_port); // Save MQTT broker port + EEPROM.put(9,IP_string_MQTT_broker); //Save default IP address of MQTT broker + } + // Now we test if the first address of the EEPROM is 0x5A. If so, te EEPROM is correctly initialized. We can assume that the stored + // stored_kwh_count at address 1-4 is valid. + if (EEPROM.read(0) == 0x5A) { + Serial.print(F("Valid configuration found in EEPROM.\nLast stored kWh value: ")); + EEPROM.get(1,stored_kwh_count); + Serial.println(stored_kwh_count); + EEPROM.get(9,IP_string_MQTT_broker); //Save default IP address of MQTT broker + Serial.print(F("IP of MQTT broker: ")); + Serial.println(IP_string_MQTT_broker); + EEPROM.get(5, MQTT_port); + Serial.print(F("Port of MQTT broker: ")); + Serial.println(MQTT_port); + + } else { + // There was a problem reading the EEPROM. Try resetting the EEPROM settings and halt. A reboot of the device might solve the issue. + Serial.println(F("Error reading the EEPROM. Try rebooting the device.")); + EEPROM.write(0, 0x5A); + EEPROM.put(1, stored_kwh_count); // Save zero to EEPROM + EEPROM.put(5, MQTT_port); // Save MQTT broker port + EEPROM.put(9,IP_string_MQTT_broker); //Save default IP address of MQTT broker + while(1); + } + + // Attach interrupt to input pin connected to pulse input + pinMode(2, INPUT_PULLUP); + attachInterrupt(digitalPinToInterrupt(2), onDetectInterrupt, FALLING); + + // initialize the ethernet device + //Ethernet.begin(mac,myIP,myDNS,myGW,myMASK); + Serial.print(F("Configuring ethernet...")); + //Ethernet.begin(mac,myIP,myDNS,myGW,myMASK); + // setup ethernet communication using DHCP + if (Ethernet.begin(mac) == 0) { + Serial.println("[FAIL]"); + while(1); + }; + Serial.println(F("[OK]")); + // start listening for clients + server.begin(); + + // setup mqtt client + IP_MQTT_broker.fromString(IP_string_MQTT_broker); + mqttClient.setClient(ethClient); + mqttClient.setServer(IP_MQTT_broker,MQTT_port); + Serial.println(F("MQTT client configured")); + + previousMillis = millis(); +} + +void loop() { + size_t size; + uint16_t cnt; + char tmp_string[6]; + + // Telnet loop + if (EthernetClient client = server.available()) + { + if (client) + { + while((size = client.available()) > 0) + { + uint8_t* msg = (uint8_t*)malloc(size+1); //make memory allocation one byte larger to accomodate for the NULL string terminator. + size = client.read(msg,size); + + msg[size-1] = 0; //Add NULL to terminate the string + + // Set IP of MQTT broker + if (msg[0] == ':' && size>9 && size<20) + { + // Extract IP address from command + for (cnt=2; cnt < (size-2); cnt++) + { + IP_string_MQTT_broker[cnt-2] = msg[cnt]; + } + IP_string_MQTT_broker[cnt-2] = 0; // Add NULL to string + + if (IP_MQTT_broker.fromString((char*)IP_string_MQTT_broker)) + { + client.write("MQTT server set to ",19); + Serial.print(F("MQTT server set to : ")); + Serial.println(IP_string_MQTT_broker); + client.write(msg,size); + } + else + { + client.write("?",1); + Serial.println(F("?")); + } + } + // Set port of MQTT broker + else if (msg[0] == 'P' && size>3 && size<10) + { + // Clear temporary string + for (cnt=0; cnt < sizeof(tmp_string); cnt++) + tmp_string[cnt]=0; + + // Extract port number from command + uint16_t port=0; + for (cnt=2; cnt < (size-2); cnt++) + { + if (msg[cnt]>=48 && msg[cnt]<=57) + { + port = 10*port + (msg[cnt]-48); + tmp_string[cnt-2] = msg[cnt]; + } + } + tmp_string[cnt-2] = 0; // Add NULL to string + + if (port>0 && port<=0xFFFF) + { + MQTT_port=port; + client.write("MQTT port set to ",17); + Serial.print(F("Set port : ")); + Serial.println(MQTT_port); + client.write(tmp_string,cnt-2); + } + else + { + client.write("?",1); + Serial.println(F("?")); + } + } + // Print IP and port of MQTT broker + else if (msg[0] == 'I') + { + client.write("MQTT server set to : ",21); + Serial.print(F("MQTT server set to : ")); + Serial.print(IP_string_MQTT_broker); + Serial.print(F(" port : ")); + Serial.println(MQTT_port); + client.write(IP_string_MQTT_broker,sizeof(IP_string_MQTT_broker)); + client.write(" port : ",8); + itoa(MQTT_port,tmp_string,10); //(integer, yourBuffer, base) + client.write(tmp_string,sizeof(tmp_string)); + + } + // Save IP of MQTT broker to EEPROM + else if (msg[0] == 'S') + { + EEPROM.put(5, MQTT_port); //Save port address of MQTT broker + EEPROM.put(9,IP_string_MQTT_broker); //Save IP address of MQTT broker + client.write("SAVED",5); + Serial.println(F("SAVED")); + + } + // Unknown command + else + { + client.write("?",1); + Serial.println(F("?")); + } + client.write("\n",1); + + free(msg); + } + } + } + + // check interval + if(millis() - previousMillis > INTERVAL) + { + sendData(); + Serial.print(F(CLIENT_TOPIC));Serial.print(F(" : "));Serial.println(EnergyReading); + previousMillis = millis(); + } + mqttClient.loop(); + readEnergymeter(); +} + +void sendData() { + if(mqttClient.connect(CLIENT_ID)) { + //Convert unsigned long to string (base 10) and send it to our MQTT broker + mqttClient.publish(CLIENT_TOPIC, ultoa(EnergyReading, numberArray, 10)); + } +} + +// Counts pulses from energy meter. Every pulse is equivalent to a certain amount of energy in Wh. This is almost always stated on te front of the meter. +// With variable PULSES_PER_KWH you can define this value. +// Every PULSES_PER_KWH pulses is saved to EEPROM. After a powercycle this value is read from EEPROM. +void readEnergymeter(){ + // Every kWh (PULSES_PER_KWH) we increment and save kWh value to EEPROM + if (PulseCount >= PULSES_PER_KWH) { + PulseCount=0; + //Serial.println(F("Store kWh counter to EEPROM.")); + stored_kwh_count++; + EEPROM.put(1,stored_kwh_count); + } + + // Convert pulses to Wh for sending to the MQTT broker + EnergyReading=1000*stored_kwh_count + PulseCount*PULSES_PER_WH; +} diff --git a/README.md b/README.md index 77627cf..e71d0ea 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,29 @@ -# atmega328p_mqtt_energy_meter +# Datalogger reading kWh pulses from a utility grid meter and publising it via MQTT +(C) 2022 M. Konstapel https://meezenest.nl/mees + +Inspired by https://www.instructables.com/A-Simple-MQTT-PubSub-Node-With-Arduino-UNO-and-ENC/ + +## Features + +- Runs on low spec microcontroller (Uno, Nano, Mini, and other ATMega328-based boards) +- Wired ethernet via ENC28J60 Ethernet module +- Gets ip address va DHCP +- Stores data in EEPROM to prevent data loss +- Easy to configure by telnet port 1000 + +## Configure + +The Device gets its IP address via DHCP. Make sure you have a working DHCP server on the network. + +The ip address and port of the MQTT broker can be set by connecting to the device via telnet on port 1000. + +To set the ip address of the MQTT broker use the command ": ". For example, to set the ip address to 192.168.1.10 type ": 192.168.1.10" and hit enter. + +To set the port number of the MQTT broker use the command "P ". For example, to set the port number to 1883 type "P 1883" and hit enter. + +To show the current settings type "I". + +To save the settings to EEPROM type "S" and hit enter. + +IMPORTANT: after making changes, power cycle the device.