P1-Meter-ESP8266/P1Meter.ino
2021-02-04 12:41:51 +01:00

583 lines
13 KiB
C++

#include <TimeLib.h>
#include <NTPClient.h>
#include <SoftwareSerial.h>
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <ESP8266HTTPClient.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
#include <Button2.h>
#include <Timer.h>
#include "CRC16.h"
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Fonts/Org_01.h>
#include <Adafruit_PCD8544.h>
// Function prototypes
void reboot();
time_t NTPgetTime();
void drawMenu();
// Time related settings
Timer t;
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "europe.pool.ntp.org", 3600, 10000);
// Display settings
//// Software SPI (slower updates, more flexible pin options):
//// pin 7 - Serial clock out (SCLK)
//// pin 6 - Serial data out (DIN)
//// pin 5 - Data/Command select (D/C)
//// pin 4 - LCD chip select (CS)
//// pin 3 - LCD reset (RST)
Adafruit_PCD8544 display = Adafruit_PCD8544(D7, D6, D5, D4, D3);
int tab = 0;
int tabCount = 3;
// LED settings
#define ledPin D8
int ledBrightness = 192;
void ledOff();
int ledTimeout;
// Button settings
#define buttonPin D2
Button2 button = Button2(buttonPin);
// Wifi Settings
const char *ssid = "*";
const char *password = "*";
const char *hostName = "ESPP1Meter";
// Server settings
const char *apiIP = "192.168.2.6";
const int apiPort = 3000;
const char *apiKey = "*";
const int gasIdx = 291;
const int energyIdx = 294;
const bool outputOnSerial = false;
// Statistics variables
unsigned long telegramsTotal = 0;
unsigned long telegramsInvalid = 0;
unsigned long postedTotal = 0;
unsigned long postedFailed = 0;
// Vars to store meter readings
long mEVLT = 0; //Meter reading Electrics - consumption low tariff
long mEVHT = 0; //Meter reading Electrics - consumption high tariff
long mEOLT = 0; //Meter reading Electrics - return low tariff
long mEOHT = 0; //Meter reading Electrics - return high tariff
long mEAV = 0; //Meter reading Electrics - Actual consumption
long mEAT = 0; //Meter reading Electrics - Actual return
long mGAS = 0; //Meter reading Gas
long prevGAS = 0;
#define MAXLINELENGTH 128 // longest normal line is 47 char (+3 for \r\n\0)
char telegram[MAXLINELENGTH];
unsigned int currentCRC = 0;
// P1 Serial settings
#define SERIAL_RX D1 // pin for SoftwareSerial RX
SoftwareSerial mySerial(SERIAL_RX, -1, false); // (RX, TX, inverted)
void setup()
{
// Init Serial
Serial.begin(115200);
delay(50);
// Init LED
pinMode(ledPin, OUTPUT);
analogWrite(ledPin, ledBrightness);
// Init Button
button.setReleasedHandler(press);
// Init Display
display.begin();
display.setContrast(50);
display.setFont(&Org_01);
drawMenu();
// Init WiFi
WiFi.mode(WIFI_STA);
display.println("Connecting to:");
display.print(" ");
display.println(ssid);
display.display();
WiFi.begin(ssid, password);
while (WiFi.waitForConnectResult() != WL_CONNECTED)
{
Serial.println("Connection Failed! Rebooting...");
drawMenu();
display.println("Connection Failed!");
display.display();
delay(5000);
ESP.restart();
}
// Init P1 Serial
mySerial.begin(115200);
// Init ArduinoOTA
//// Port defaults to 8266
ArduinoOTA.setPort(8266);
//// Hostname defaults to esp8266-[ChipID]
ArduinoOTA.setHostname(hostName);
//// No authentication by default
// ArduinoOTA.setPassword((const char *)"123");
ArduinoOTA.onStart([]() {
drawMenu();
display.println("OTA Update");
display.display();
Serial.println("Start");
});
ArduinoOTA.onEnd([]() {
Serial.println("\nEnd");
});
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
drawMenu();
display.println("OTA Update");
display.printf("Progress: %u%%\r", (progress / (total / 100)));
display.display();
Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
});
ArduinoOTA.onError([](ota_error_t error) {
drawMenu();
display.println("OTA Update");
display.printf("Error[%u]: ", error);
display.display();
Serial.printf("Error[%u]: ", error);
if (error == OTA_AUTH_ERROR)
Serial.println("Auth Failed");
else if (error == OTA_BEGIN_ERROR)
Serial.println("Begin Failed");
else if (error == OTA_CONNECT_ERROR)
Serial.println("Connect Failed");
else if (error == OTA_RECEIVE_ERROR)
Serial.println("Receive Failed");
else if (error == OTA_END_ERROR)
Serial.println("End Failed");
});
ArduinoOTA.begin();
// Init Time
timeClient.begin();
time_t test = NTPgetTime();
while (test < 10000)
{
test = NTPgetTime();
}
setTime(NTPgetTime());
getExternalTime NtpTime = &NTPgetTime;
setSyncProvider(NtpTime);
setSyncInterval(3600);
display.println("Time set");
display.display();
delay(1000);
// Exit setup
drawTab(2);
ledTimeout = t.after(5000, ledOff, (void *)0);
}
// Reboot the ESP
void reboot()
{
drawMenu();
display.println("Scheduled reboot");
display.display();
delay(5000);
ESP.restart();
}
// Update Time
time_t NTPgetTime()
{
timeClient.update();
return timeClient.getEpochTime();
}
// Turn of the backlight LEDs
void ledOff(void *context)
{
tab = 0;
drawTab(tab);
analogWrite(ledPin, 0);
}
// Button action
void press(Button2 &btn)
{
analogWrite(ledPin, ledBrightness);
t.stop(ledTimeout);
ledTimeout = t.after(10000, ledOff, (void *)0);
if ((tab + 1) == tabCount)
{
tab = 0;
}
else
{
tab++;
}
drawTab(tab);
}
// Draw top bar
void drawMenu()
{
display.clearDisplay();
display.setCursor(0, 4);
display.println("P1 Reader");
display.drawLine(0, 6, 84, 6, 1);
display.display();
display.setCursor(0, 12);
}
void drawTab(int no)
{
drawMenu();
switch (no)
{
// Network
case 0:
drawMenu();
// SSID
display.println("Connected to:");
display.print(" ");
display.println(ssid);
// Own IP
display.println("IP address: ");
display.print(" ");
display.println(WiFi.localIP());
// Target server
display.println("Target host: ");
display.print(" ");
display.println(String(apiIP) + ":" + String(apiPort));
break;
// Stats
case 1:
drawMenu();
// Telegrams
display.println("Telegrams");
display.print(" Total: ");
display.println(String(telegramsTotal));
display.print(" Invalid: ");
display.println(String(telegramsInvalid));
// Posted
display.println("Posted");
display.print(" Total: ");
display.println(String(postedTotal));
display.print(" Failed: ");
display.println(String(postedFailed));
break;
// Time
case 2:
display.println("Current time:");
// Time
display.print(" ");
if (hour() < 10)
{
display.print("0");
}
display.print(hour());
display.print(":");
if (minute() < 10)
{
display.print("0");
}
display.println(minute());
// Date
display.print(" ");
if (day() < 10)
{
display.print("0");
}
display.print(day());
display.print("-");
if (month() < 10)
{
display.print("0");
}
display.print(month());
display.print("-");
display.println(year());
break;
}
display.display();
}
// Send data to server
bool Send(int idx, int nValue, char *sValue)
{
HTTPClient http;
bool retVal = false;
char httpRequestData[255];
// "http://%s:%d/json.htm?type=command&param=udevice&idx=%d&nvalue=%d&svalue=%s"
// "api_key=tPmAT5Ab3j7F9&sensor=BME280&value1=24.25&value2=49.54&value3=1005.14"
sprintf(httpRequestData, "api_key=%s&type=%d&nval=%d&sval=%s", apiKey, idx, nValue, sValue);
Serial.printf("[HTTP] GET... URL: %s\n", httpRequestData);
http.begin(String("http://" + String(apiIP) + ":" + String(apiPort))); //HTTP
http.addHeader("Content-Type", "application/x-www-form-urlencoded");
postedTotal++;
int httpCode = http.POST(httpRequestData);
// httpCode will be negative on error
if (httpCode > 0)
{ // HTTP header has been send and Server response header has been handled
Serial.printf("[HTTP] GET... code: %d\n", httpCode);
// file found at server
if (httpCode == HTTP_CODE_OK)
{
String payload = http.getString();
retVal = true;
}
}
else
{
postedFailed++;
Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
}
http.end();
return retVal;
}
// Update Gas info
void UpdateGas()
{
if (prevGAS != mGAS)
{
char sValue[10];
sprintf(sValue, "%d", mGAS);
if (Send(gasIdx, 0, sValue))
prevGAS = mGAS;
}
}
// Update Electricity info
void UpdateElectricity()
{
char sValue[255];
sprintf(sValue, "%d;%d;%d;%d;%d;%d", mEVLT, mEVHT, mEOLT, mEOHT, mEAV, mEAT);
Send(energyIdx, 0, sValue);
}
// Check if value is number
bool isNumber(char *res, int len)
{
for (int i = 0; i < len; i++)
{
if (((res[i] < '0') || (res[i] > '9')) && (res[i] != '.' && res[i] != 0))
{
return false;
}
}
return true;
}
// Find char in array
int FindCharInArrayRev(char array[], char c, int len)
{
for (int i = len - 1; i >= 0; i--)
{
if (array[i] == c)
{
return i;
}
}
return -1;
}
// Check if value is valid
long getValidVal(long valNew, long valOld, long maxDiffer)
{
//check if the incoming value is valid
if (valOld > 0 && ((valNew - valOld > maxDiffer) && (valOld - valNew > maxDiffer)))
return valOld;
return valNew;
}
// Get value from char buffer
long getValue(char *buffer, int maxlen)
{
int s = FindCharInArrayRev(buffer, '(', maxlen - 2);
if (s < 8)
return 0;
if (s > 32)
s = 32;
int l = FindCharInArrayRev(buffer, '*', maxlen - 2) - s - 1;
if (l < 4)
return 0;
if (l > 12)
return 0;
char res[16];
memset(res, 0, sizeof(res));
if (strncpy(res, buffer + s + 1, l))
{
if (isNumber(res, l))
{
return (1000 * atof(res));
}
}
return 0;
}
// Decode P1 telegram
bool decodeTelegram(int len)
{
//need to check for start
int startChar = FindCharInArrayRev(telegram, '/', len);
int endChar = FindCharInArrayRev(telegram, '!', len);
bool validCRCFound = false;
if (startChar >= 0)
{
//start found. Reset CRC calculation
currentCRC = CRC16(0x0000, (unsigned char *)telegram + startChar, len - startChar);
if (outputOnSerial)
{
for (int cnt = startChar; cnt < len - startChar; cnt++)
Serial.print(telegram[cnt]);
}
Serial.println("Start found!");
telegramsTotal++;
}
else if (endChar >= 0)
{
//add to crc calc
currentCRC = CRC16(currentCRC, (unsigned char *)telegram + endChar, 1);
char messageCRC[5];
strncpy(messageCRC, telegram + endChar + 1, 4);
messageCRC[4] = 0; //thanks to HarmOtten (issue 5)
if (outputOnSerial)
{
for (int cnt = 0; cnt < len; cnt++)
Serial.print(telegram[cnt]);
}
validCRCFound = (strtol(messageCRC, NULL, 16) == currentCRC);
if (validCRCFound)
{
Serial.println("\nVALID CRC FOUND!");
}
else
{
Serial.println("\nINVALID CRC FOUND!");
telegramsInvalid++;
}
currentCRC = 0;
}
else
{
currentCRC = CRC16(currentCRC, (unsigned char *)telegram, len);
if (outputOnSerial)
{
for (int cnt = 0; cnt < len; cnt++)
Serial.print(telegram[cnt]);
}
}
long val = 0;
long val2 = 0;
// 1-0:1.8.1(000992.992*kWh)
// 1-0:1.8.1 = Elektra verbruik laag tarief (DSMR v4.0)
if (strncmp(telegram, "1-0:1.8.1", strlen("1-0:1.8.1")) == 0)
mEVLT = getValue(telegram, len);
// 1-0:1.8.2(000560.157*kWh)
// 1-0:1.8.2 = Elektra verbruik hoog tarief (DSMR v4.0)
if (strncmp(telegram, "1-0:1.8.2", strlen("1-0:1.8.2")) == 0)
mEVHT = getValue(telegram, len);
// 1-0:2.8.1(000348.890*kWh)
// 1-0:2.8.1 = Elektra opbrengst laag tarief (DSMR v4.0)
if (strncmp(telegram, "1-0:2.8.1", strlen("1-0:2.8.1")) == 0)
mEOLT = getValue(telegram, len);
// 1-0:2.8.2(000859.885*kWh)
// 1-0:2.8.2 = Elektra opbrengst hoog tarief (DSMR v4.0)
if (strncmp(telegram, "1-0:2.8.2", strlen("1-0:2.8.2")) == 0)
mEOHT = getValue(telegram, len);
// 1-0:1.7.0(00.424*kW) Actueel verbruik
// 1-0:2.7.0(00.000*kW) Actuele teruglevering
// 1-0:1.7.x = Electricity consumption actual usage (DSMR v4.0)
if (strncmp(telegram, "1-0:1.7.0", strlen("1-0:1.7.0")) == 0)
mEAV = getValue(telegram, len);
if (strncmp(telegram, "1-0:2.7.0", strlen("1-0:2.7.0")) == 0)
mEAT = getValue(telegram, len);
// 0-1:24.2.1(150531200000S)(00811.923*m3)
// 0-1:24.2.1 = Gas (DSMR v4.0) on Kaifa MA105 meter
if (strncmp(telegram, "0-1:24.2.1", strlen("0-1:24.2.1")) == 0)
mGAS = getValue(telegram, len);
return validCRCFound;
}
// Read P1 telegram
void readTelegram()
{
if (mySerial.available())
{
memset(telegram, 0, sizeof(telegram));
while (mySerial.available())
{
int len = mySerial.readBytesUntil('\n', telegram, MAXLINELENGTH);
telegram[len] = '\n';
telegram[len + 1] = 0;
yield();
if (decodeTelegram(len + 1))
{
UpdateElectricity();
UpdateGas();
}
}
}
}
void loop()
{
if (hour() == 0 && minute() == 0 && second() == 0)
{
reboot();
}
readTelegram();
button.loop();
t.update();
ArduinoOTA.handle();
}