mirror of
https://github.com/UtilitechAS/amsreader-firmware.git
synced 2026-02-08 09:22:12 +00:00
1075 lines
33 KiB
C++
1075 lines
33 KiB
C++
/**
|
|
* @brief ESP8266 based program to receive data from AMS electric meters and send to MQTT
|
|
*
|
|
* @details Originally developed by Roar Fredriksen, this program was created to receive data from
|
|
* AMS electric meters via M-Bus, decode and send to a MQTT broker. The data packet structure
|
|
* supported by this software is specific to Norwegian meters, but may also support data from
|
|
* electricity providers in other countries. It was originally based on ESP8266, but have also been
|
|
* adapted to work with ESP32.
|
|
*
|
|
* @author Roar Fredriksen (@roarfred)
|
|
* The original developer for this project
|
|
* https://github.com/roarfred/AmsToMqttBridge
|
|
*
|
|
* @author Gunnar Skjold (@gskjold)
|
|
* Maintainer of current code
|
|
* https://github.com/gskjold/AmsToMqttBridge
|
|
*/
|
|
|
|
#include "AmsToMqttBridge.h"
|
|
#include "AmsStorage.h"
|
|
#define ARDUINOJSON_POSITIVE_EXPONENTIATION_THRESHOLD 1e9
|
|
#include <ArduinoJson.h>
|
|
#include <MQTT.h>
|
|
#include <DNSServer.h>
|
|
#include <lwip/apps/sntp.h>
|
|
|
|
#if defined(ESP8266)
|
|
ADC_MODE(ADC_VCC);
|
|
#endif
|
|
|
|
#include "HwTools.h"
|
|
|
|
#include "web/AmsWebServer.h"
|
|
#include "AmsConfiguration.h"
|
|
#include "HanReader.h"
|
|
#include "HanToJson.h"
|
|
|
|
#include "Aidon.h"
|
|
#include "Kaifa.h"
|
|
#include "Kamstrup.h"
|
|
#include "Omnipower.h"
|
|
|
|
#include "Uptime.h"
|
|
|
|
#define WEBSOCKET_DISABLED true
|
|
#include "RemoteDebug.h"
|
|
|
|
#define DEBUG_ESP_HTTP_CLIENT 1
|
|
#define DEBUG_ESP_PORT Serial
|
|
|
|
HwTools hw;
|
|
|
|
DNSServer dnsServer;
|
|
|
|
AmsConfiguration config;
|
|
|
|
RemoteDebug Debug;
|
|
|
|
AmsWebServer ws(&Debug, &hw);
|
|
|
|
MQTTClient mqtt(512);
|
|
|
|
HanReader hanReader;
|
|
|
|
Stream *hanSerial;
|
|
|
|
void setup() {
|
|
if(config.hasConfig()) {
|
|
config.load();
|
|
}
|
|
|
|
if(!config.hasConfig() || config.getConfigVersion() < 81) {
|
|
debugI("Setting default hostname");
|
|
uint16_t chipId;
|
|
#if defined(ESP32)
|
|
chipId = ESP.getEfuseMac();
|
|
#else
|
|
chipId = ESP.getChipId();
|
|
#endif
|
|
config.setWifiHostname((String("ams-") + String(chipId, HEX)).c_str());
|
|
}
|
|
|
|
if(!config.hasConfig() || config.getConfigVersion() < 82) {
|
|
config.setVccMultiplier(1.0);
|
|
config.setVccBootLimit(0);
|
|
#if HW_ROARFRED
|
|
config.setHanPin(3);
|
|
config.setApPin(0);
|
|
config.setLedPin(2);
|
|
config.setLedInverted(true);
|
|
config.setTempSensorPin(5);
|
|
#elif defined(ARDUINO_ESP8266_WEMOS_D1MINI)
|
|
config.setHanPin(5);
|
|
config.setApPin(4);
|
|
config.setLedPin(2);
|
|
config.setLedInverted(true);
|
|
config.setTempSensorPin(14);
|
|
config.setVccMultiplier(1.1);
|
|
#elif defined(ARDUINO_LOLIN_D32)
|
|
config.setHanPin(16);
|
|
config.setLedPin(5);
|
|
config.setLedInverted(true);
|
|
config.setTempSensorPin(14);
|
|
config.setVccPin(35);
|
|
config.setVccMultiplier(2.25);
|
|
#elif defined(ARDUINO_FEATHER_ESP32)
|
|
config.setHanPin(16);
|
|
config.setLedPin(2);
|
|
config.setTempSensorPin(14);
|
|
#elif defined(ARDUINO_ESP32_DEV)
|
|
config.setHanPin(16);
|
|
config.setLedPin(2);
|
|
config.setLedInverted(false);
|
|
#elif defined(ESP8266)
|
|
config.setHanPin(3);
|
|
config.setLedPin(2);
|
|
config.setLedInverted(true);
|
|
#elif defined(ESP32)
|
|
config.setHanPin(16);
|
|
config.setLedPin(2);
|
|
config.setLedInverted(true);
|
|
config.setTempSensorPin(14);
|
|
#endif
|
|
}
|
|
|
|
hw.setLed(config.getLedPin(), config.isLedInverted());
|
|
hw.setLedRgb(config.getLedPinRed(), config.getLedPinGreen(), config.getLedPinBlue(), config.isLedRgbInverted());
|
|
hw.setTempSensorPin(config.getTempSensorPin());
|
|
hw.setTempAnalogSensorPin(config.getTempAnalogSensorPin());
|
|
hw.setVccPin(config.getVccPin());
|
|
hw.setVccMultiplier(config.getVccMultiplier());
|
|
hw.setVccOffset(config.getVccOffset());
|
|
hw.ledBlink(LED_INTERNAL, 1);
|
|
hw.ledBlink(LED_RED, 1);
|
|
hw.ledBlink(LED_YELLOW, 1);
|
|
hw.ledBlink(LED_GREEN, 1);
|
|
hw.ledBlink(LED_BLUE, 1);
|
|
|
|
if(config.getHanPin() == 3) {
|
|
switch(config.getMeterType()) {
|
|
case METER_TYPE_KAMSTRUP:
|
|
case METER_TYPE_OMNIPOWER:
|
|
Serial.begin(2400, SERIAL_8N1);
|
|
break;
|
|
default:
|
|
Serial.begin(2400, SERIAL_8E1);
|
|
break;
|
|
}
|
|
} else {
|
|
Serial.begin(115200);
|
|
}
|
|
|
|
if(config.hasConfig() && config.isDebugSerial()) {
|
|
Debug.setSerialEnabled(config.isDebugSerial());
|
|
} else {
|
|
#if DEBUG_MODE
|
|
Debug.setSerialEnabled(true);
|
|
#endif
|
|
}
|
|
|
|
double vcc = hw.getVcc();
|
|
|
|
if (Debug.isActive(RemoteDebug::INFO)) {
|
|
debugI("AMS bridge started");
|
|
debugI("Voltage: %.2fV", vcc);
|
|
}
|
|
|
|
double vccBootLimit = config.getVccBootLimit();
|
|
if(vccBootLimit > 0 && (config.getApPin() == 0xFF || digitalRead(config.getApPin()) == HIGH)) { // Skip if user is holding AP button while booting (HIGH = button is released)
|
|
if (vcc < vccBootLimit) {
|
|
if(Debug.isActive(RemoteDebug::INFO)) {
|
|
debugI("Voltage is too low, sleeping");
|
|
Serial.flush();
|
|
}
|
|
ESP.deepSleep(10000000); //Deep sleep to allow output cap to charge up
|
|
}
|
|
}
|
|
|
|
WiFi.disconnect(true);
|
|
WiFi.softAPdisconnect(true);
|
|
WiFi.mode(WIFI_OFF);
|
|
|
|
bool spiffs = false;
|
|
#if defined(ESP32)
|
|
debugD("ESP32 SPIFFS");
|
|
spiffs = SPIFFS.begin(true);
|
|
#else
|
|
debugD("ESP8266 SPIFFS");
|
|
spiffs = SPIFFS.begin();
|
|
#endif
|
|
|
|
if(spiffs) {
|
|
bool flashed = false;
|
|
if(SPIFFS.exists(FILE_FIRMWARE)) {
|
|
if(Debug.isActive(RemoteDebug::INFO)) debugI("Found firmware");
|
|
#if defined(ESP8266)
|
|
WiFi.setSleepMode(WIFI_LIGHT_SLEEP);
|
|
WiFi.forceSleepBegin();
|
|
#endif
|
|
int i = 0;
|
|
while(hw.getVcc() < 3.2 && i < 3) {
|
|
if(Debug.isActive(RemoteDebug::INFO)) debugI(" vcc not optimal, light sleep 10s");
|
|
#if defined(ESP8266)
|
|
delay(10000);
|
|
#elif defined(ESP32)
|
|
esp_sleep_enable_timer_wakeup(10000000);
|
|
esp_light_sleep_start();
|
|
#endif
|
|
i++;
|
|
}
|
|
|
|
debugI(" flashing");
|
|
File firmwareFile = SPIFFS.open(FILE_FIRMWARE, "r");
|
|
debugD(" firmware size: %d", firmwareFile.size());
|
|
uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;
|
|
if (!Update.begin(maxSketchSpace, U_FLASH)) {
|
|
if(Debug.isActive(RemoteDebug::ERROR)) {
|
|
debugE("Unable to start firmware update");
|
|
Update.printError(Serial);
|
|
}
|
|
} else {
|
|
while (firmwareFile.available()) {
|
|
uint8_t ibuffer[128];
|
|
firmwareFile.read((uint8_t *)ibuffer, 128);
|
|
Update.write(ibuffer, sizeof(ibuffer));
|
|
}
|
|
flashed = Update.end(true);
|
|
}
|
|
firmwareFile.close();
|
|
SPIFFS.remove(FILE_FIRMWARE);
|
|
}
|
|
SPIFFS.end();
|
|
if(flashed) {
|
|
if(Debug.isActive(RemoteDebug::INFO)) {
|
|
debugI("Firmware update complete, restarting");
|
|
Serial.flush();
|
|
}
|
|
delay(250);
|
|
#if defined(ESP8266)
|
|
ESP.reset();
|
|
#elif defined(ESP32)
|
|
ESP.restart();
|
|
#endif
|
|
return;
|
|
}
|
|
}
|
|
|
|
if(config.hasConfig()) {
|
|
if(Debug.isActive(RemoteDebug::INFO)) config.print(&Debug);
|
|
WiFi_connect();
|
|
if(config.isNtpEnable()) {
|
|
configTime(config.getNtpOffset(), config.getNtpSummerOffset(), config.getNtpServer());
|
|
sntp_servermode_dhcp(config.isNtpDhcp() ? 1 : 0);
|
|
}
|
|
} else {
|
|
if(Debug.isActive(RemoteDebug::INFO)) {
|
|
debugI("No configuration, booting AP");
|
|
}
|
|
swapWifiMode();
|
|
}
|
|
|
|
ws.setup(&config, &mqtt);
|
|
}
|
|
|
|
|
|
int buttonTimer = 0;
|
|
bool buttonActive = false;
|
|
unsigned long longPressTime = 5000;
|
|
bool longPressActive = false;
|
|
|
|
bool wifiConnected = false;
|
|
|
|
unsigned long lastTemperatureRead = 0;
|
|
float temperatures[32];
|
|
|
|
unsigned long lastRead = 0;
|
|
unsigned long lastSuccessfulRead = 0;
|
|
|
|
unsigned long lastErrorBlink = 0;
|
|
int lastError = 0;
|
|
|
|
// domoticz energy init
|
|
double energy = -1.0;
|
|
|
|
String toHex(uint8_t* in) {
|
|
String hex;
|
|
for(int i = 0; i < sizeof(in)*2; i++) {
|
|
if(in[i] < 0x10) {
|
|
hex += '0';
|
|
}
|
|
hex += String(in[i], HEX);
|
|
}
|
|
hex.toUpperCase();
|
|
return hex;
|
|
}
|
|
|
|
|
|
void loop() {
|
|
Debug.handle();
|
|
unsigned long now = millis();
|
|
if(config.getApPin() != 0xFF) {
|
|
if (digitalRead(config.getApPin()) == LOW) {
|
|
if (buttonActive == false) {
|
|
buttonActive = true;
|
|
buttonTimer = now;
|
|
}
|
|
|
|
if ((now - buttonTimer > longPressTime) && (longPressActive == false)) {
|
|
longPressActive = true;
|
|
swapWifiMode();
|
|
}
|
|
} else {
|
|
if (buttonActive == true) {
|
|
if (longPressActive == true) {
|
|
longPressActive = false;
|
|
} else {
|
|
// Single press action
|
|
}
|
|
buttonActive = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(now - lastTemperatureRead > 15000) {
|
|
hw.updateTemperatures();
|
|
lastTemperatureRead = now;
|
|
|
|
if(strlen(config.getMqttHost()) > 0) {
|
|
bool anyChanged = false;
|
|
uint8_t c = hw.getTempSensorCount();
|
|
for(int i = 0; i < c; i++) {
|
|
bool changed = false;
|
|
TempSensorData* data = hw.getTempSensorData(i);
|
|
if(data->lastValidRead > -85) {
|
|
changed = data->lastValidRead != temperatures[i];
|
|
temperatures[i] = data->lastValidRead;
|
|
}
|
|
|
|
if((changed && config.getMqttPayloadFormat() == 1) || config.getMqttPayloadFormat() == 2) {
|
|
mqtt.publish(String(config.getMqttPublishTopic()) + "/temperature/" + toHex(data->address), String(temperatures[i], 2));
|
|
}
|
|
|
|
anyChanged |= changed;
|
|
}
|
|
|
|
if(anyChanged && config.getMqttPayloadFormat() == 0) {
|
|
StaticJsonDocument<512> json;
|
|
JsonObject temps = json.createNestedObject("temperatures");
|
|
for(int i = 0; i < c; i++) {
|
|
TempSensorData* data = hw.getTempSensorData(i);
|
|
JsonObject obj = temps.createNestedObject(toHex(data->address));
|
|
obj["name"] = data->name;
|
|
obj["value"] = serialized(String(temperatures[i], 2));
|
|
}
|
|
String msg;
|
|
serializeJson(json, msg);
|
|
mqtt.publish(config.getMqttPublishTopic(), msg.c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Only do normal stuff if we're not booted as AP
|
|
if (WiFi.getMode() != WIFI_AP) {
|
|
if (WiFi.status() != WL_CONNECTED) {
|
|
wifiConnected = false;
|
|
Debug.stop();
|
|
WiFi_connect();
|
|
} else {
|
|
if(!wifiConnected) {
|
|
wifiConnected = true;
|
|
if(config.getAuthSecurity() > 0) {
|
|
Debug.setPassword(config.getAuthPassword());
|
|
}
|
|
Debug.begin(config.getWifiHostname(), (uint8_t) config.getDebugLevel());
|
|
Debug.setSerialEnabled(config.isDebugSerial());
|
|
if(!config.isDebugTelnet()) {
|
|
Debug.stop();
|
|
}
|
|
if(Debug.isActive(RemoteDebug::INFO)) {
|
|
debugI("Successfully connected to WiFi!");
|
|
debugI("IP: %s", WiFi.localIP().toString().c_str());
|
|
}
|
|
if(strlen(config.getWifiHostname()) > 0 && config.isMdnsEnable()) {
|
|
debugD("mDNS is enabled, using host: %s", config.getWifiHostname());
|
|
if(MDNS.begin(config.getWifiHostname())) {
|
|
MDNS.addService("http", "tcp", 80);
|
|
} else {
|
|
debugE("Failed to set up mDNS!");
|
|
}
|
|
}
|
|
}
|
|
if(config.isNtpChanged()) {
|
|
if(config.isNtpEnable()) {
|
|
configTime(config.getNtpOffset(), config.getNtpSummerOffset(), config.getNtpServer());
|
|
sntp_servermode_dhcp(config.isNtpDhcp() ? 1 : 0);
|
|
}
|
|
config.ackNtpChange();
|
|
}
|
|
#if defined ESP8266
|
|
MDNS.update();
|
|
#endif
|
|
|
|
if(now > 10000 && now - lastErrorBlink > 3000) {
|
|
errorBlink();
|
|
}
|
|
|
|
if (strlen(config.getMqttHost()) > 0) {
|
|
mqtt.loop();
|
|
delay(10); // Needed to preserve power. After adding this, the voltage is super smooth on a HAN powered device
|
|
if(!mqtt.connected() || config.isMqttChanged()) {
|
|
MQTT_connect();
|
|
}
|
|
if(config.getMqttPayloadFormat() == 1) {
|
|
sendSystemStatusToMqtt();
|
|
}
|
|
} else if(mqtt.connected()) {
|
|
mqtt.disconnect();
|
|
}
|
|
}
|
|
} else {
|
|
dnsServer.processNextRequest();
|
|
// Continously flash the LED when AP mode
|
|
if (now / 50 % 64 == 0) {
|
|
if(!hw.ledBlink(LED_YELLOW, 1)) {
|
|
hw.ledBlink(LED_INTERNAL, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
if(config.isMeterChanged()) {
|
|
setupHanPort(config.getHanPin(), config.getMeterType());
|
|
config.ackMeterChanged();
|
|
}
|
|
|
|
if(now - lastRead > 100) {
|
|
yield();
|
|
readHanPort();
|
|
lastRead = now;
|
|
}
|
|
ws.loop();
|
|
delay(1); // Needed for auto modem sleep
|
|
}
|
|
|
|
void setupHanPort(int pin, int newMeterType) {
|
|
debugI("Setting up HAN on pin %d for meter type %d", pin, newMeterType);
|
|
|
|
HardwareSerial *hwSerial = NULL;
|
|
if(pin == 3) {
|
|
hwSerial = &Serial;
|
|
}
|
|
#if defined(ESP32)
|
|
if(pin == 9) {
|
|
hwSerial = &Serial1;
|
|
}
|
|
if(pin == 16) {
|
|
hwSerial = &Serial2;
|
|
}
|
|
#endif
|
|
|
|
if(pin == 0) {
|
|
debugE("Invalid GPIO configured for HAN");
|
|
return;
|
|
}
|
|
|
|
if(hwSerial != NULL) {
|
|
debugD("Hardware serial");
|
|
Serial.flush();
|
|
switch(newMeterType) {
|
|
case METER_TYPE_KAMSTRUP:
|
|
case METER_TYPE_OMNIPOWER:
|
|
hwSerial->begin(2400, SERIAL_8N1);
|
|
break;
|
|
default:
|
|
hwSerial->begin(2400, SERIAL_8E1);
|
|
break;
|
|
}
|
|
hanSerial = hwSerial;
|
|
} else {
|
|
debugD("Software serial");
|
|
Serial.flush();
|
|
SoftwareSerial *swSerial = new SoftwareSerial(pin);
|
|
|
|
switch(newMeterType) {
|
|
case METER_TYPE_KAMSTRUP:
|
|
case METER_TYPE_OMNIPOWER:
|
|
swSerial->begin(2400, SWSERIAL_8N1);
|
|
break;
|
|
default:
|
|
swSerial->begin(2400, SWSERIAL_8E1);
|
|
break;
|
|
}
|
|
hanSerial = swSerial;
|
|
|
|
Serial.begin(115200);
|
|
}
|
|
|
|
hanReader.setup(hanSerial, &Debug);
|
|
hanReader.setEncryptionKey(config.getMeterEncryptionKey());
|
|
hanReader.setAuthenticationKey(config.getMeterAuthenticationKey());
|
|
|
|
// Compensate for the known Kaifa bug
|
|
hanReader.compensateFor09HeaderBug = (newMeterType == 1);
|
|
|
|
// Empty buffer before starting
|
|
while (hanSerial->available() > 0) {
|
|
hanSerial->read();
|
|
}
|
|
|
|
if(config.hasConfig() && config.isDebugSerial()) {
|
|
if(WiFi.status() == WL_CONNECTED) {
|
|
Debug.begin(config.getWifiHostname(), (uint8_t) config.getDebugLevel());
|
|
}
|
|
Debug.setSerialEnabled(config.isDebugSerial());
|
|
if(!config.isDebugTelnet()) {
|
|
Debug.stop();
|
|
}
|
|
}
|
|
}
|
|
|
|
void errorBlink() {
|
|
if(lastError == 3)
|
|
lastError = 0;
|
|
lastErrorBlink = millis();
|
|
for(;lastError < 3;lastError++) {
|
|
switch(lastError) {
|
|
case 0:
|
|
if(lastErrorBlink - lastSuccessfulRead > 30000) {
|
|
hw.ledBlink(LED_RED, 1); // If no message received from AMS in 30 sec, blink once
|
|
return;
|
|
}
|
|
break;
|
|
case 1:
|
|
if(strlen(config.getMqttHost()) > 0 && mqtt.lastError() != 0) {
|
|
hw.ledBlink(LED_RED, 2); // If MQTT error, blink twice
|
|
return;
|
|
}
|
|
break;
|
|
case 2:
|
|
if(WiFi.getMode() != WIFI_AP && WiFi.status() != WL_CONNECTED) {
|
|
hw.ledBlink(LED_RED, 3); // If WiFi not connected, blink three times
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void swapWifiMode() {
|
|
if(!hw.ledOn(LED_YELLOW)) {
|
|
hw.ledOn(LED_INTERNAL);
|
|
}
|
|
WiFiMode_t mode = WiFi.getMode();
|
|
dnsServer.stop();
|
|
WiFi.disconnect(true);
|
|
WiFi.softAPdisconnect(true);
|
|
WiFi.mode(WIFI_OFF);
|
|
yield();
|
|
|
|
if (mode != WIFI_AP || !config.hasConfig()) {
|
|
if(Debug.isActive(RemoteDebug::INFO)) debugI("Swapping to AP mode");
|
|
WiFi.softAP("AMS2MQTT");
|
|
WiFi.mode(WIFI_AP);
|
|
|
|
dnsServer.setErrorReplyCode(DNSReplyCode::NoError);
|
|
dnsServer.start(53, "*", WiFi.softAPIP());
|
|
} else {
|
|
if(Debug.isActive(RemoteDebug::INFO)) debugI("Swapping to STA mode");
|
|
WiFi_connect();
|
|
}
|
|
delay(500);
|
|
if(!hw.ledOff(LED_YELLOW)) {
|
|
hw.ledOff(LED_INTERNAL);
|
|
}
|
|
}
|
|
|
|
void mqttMessageReceived(String &topic, String &payload)
|
|
{
|
|
|
|
if (Debug.isActive(RemoteDebug::DEBUG)) {
|
|
debugD("Incoming MQTT message: [%s] %s", topic.c_str(), payload.c_str());
|
|
}
|
|
|
|
// Do whatever needed here...
|
|
// Ideas could be to query for values or to initiate OTA firmware update
|
|
}
|
|
|
|
int currentMeterType = 0;
|
|
AmsData lastMqttData;
|
|
void readHanPort() {
|
|
if (hanReader.read()) {
|
|
// Empty serial buffer. For some reason this seems to make a difference. Some garbage on the wire after package?
|
|
while(hanSerial->available()) {
|
|
hanSerial->read();
|
|
}
|
|
|
|
lastSuccessfulRead = millis();
|
|
|
|
if(config.getMeterType() > 0) {
|
|
if(!hw.ledBlink(LED_GREEN, 1))
|
|
hw.ledBlink(LED_INTERNAL, 1);
|
|
|
|
AmsData data(config.getMeterType(), config.isSubstituteMissing(), hanReader);
|
|
if(data.getListType() > 0) {
|
|
ws.setData(data);
|
|
|
|
if(strlen(config.getMqttHost()) > 0 && strlen(config.getMqttPublishTopic()) > 0) {
|
|
if(config.getMqttPayloadFormat() == 0) {
|
|
StaticJsonDocument<512> json;
|
|
hanToJson(json, data, hw, hw.getTemperature(), config.getMqttClientId());
|
|
if (Debug.isActive(RemoteDebug::INFO)) {
|
|
debugI("Sending data to MQTT");
|
|
if (Debug.isActive(RemoteDebug::DEBUG)) {
|
|
serializeJsonPretty(json, Debug);
|
|
}
|
|
}
|
|
|
|
String msg;
|
|
serializeJson(json, msg);
|
|
mqtt.publish(config.getMqttPublishTopic(), msg.c_str());
|
|
//
|
|
// Start DOMOTICZ
|
|
//
|
|
} else if(config.getMqttPayloadFormat() == 3) {
|
|
debugI("Sending data to MQTT");
|
|
//
|
|
// Special MQTT messages for DOMOTIZ (https://www.domoticz.com/wiki/MQTT)
|
|
// -All messages should be published to topic "domoticz/in"
|
|
//
|
|
// message msg_PE : send active power and and cumulative energy consuption to virtual meter "Electricity (instant and counter)"
|
|
//
|
|
// /json.htm?type=command¶m=udevice&idx=IDX&nvalue=0&svalue=POWER;ENERGY
|
|
//
|
|
// MQTT sample message: {"command": "udevice", "idx" : IDX , "nvalue" : 0, "svalue" : "POWER;ENERGY"}
|
|
// IDX = id of your device (This number can be found in the devices tab in the column "IDX")
|
|
// POWER = current power (Watt)
|
|
// ENERGY = cumulative energy in Watt-hours (Wh) This is an incrementing counter.
|
|
// (if you choose as type "Energy read : Computed", this is just a "dummy" counter, not updatable because it's the result of DomoticZ calculs from POWER)
|
|
//
|
|
// message msg_V1 : send Voltage of L1 to virtual Voltage meter
|
|
//
|
|
// /json.htm?type=command¶m=udevice&idx=IDX&nvalue=0&svalue=VOLTAGE
|
|
//
|
|
// MQTT sample message: {"command": "udevice", "idx" : IDX , "nvalue" : 0, "svalue" : "VOLTAGE"}
|
|
// IDX = id of your device (This number can be found in the devices tab in the column "IDX")
|
|
// VOLTAGE = Voltage (V)
|
|
//
|
|
|
|
int idx1 = config.getDomoELIDX();
|
|
if (idx1 > 0) {
|
|
String PowerEnergy;
|
|
int p;
|
|
// double energy = config.getDomoEnergy();
|
|
double tmp_energy;
|
|
StaticJsonDocument<200> json_PE;
|
|
p = data.getActiveImportPower();
|
|
// cumulative energy is given only once pr hour. check if value is different from 0 and store last valid value on global variable.
|
|
tmp_energy = data.getActiveImportCounter();
|
|
if (tmp_energy > 1.0) energy = tmp_energy;
|
|
// power_unit: watt, energy_unit: watt*h. Stored as kwh, need watth
|
|
PowerEnergy = String((double) p/1.0) + ";" + String((double) energy*1000.0) ;
|
|
json_PE["command"] = "udevice";
|
|
json_PE["idx"] = idx1;
|
|
json_PE["nvalue"] = 0;
|
|
json_PE["svalue"] = PowerEnergy;
|
|
// Stringify the json
|
|
String msg_PE;
|
|
serializeJson(json_PE, msg_PE);
|
|
// publish power data directly to domoticz/in, but only after first reading of total power, once an hour... . (otherwise total consumtion will be wrong.)
|
|
if (energy > 0.0 ) mqtt.publish("domoticz/in", msg_PE.c_str());
|
|
}
|
|
int idxu1 =config.getDomoVL1IDX();
|
|
if (idxu1 > 0){
|
|
StaticJsonDocument<200> json_u1;
|
|
double u1;
|
|
//
|
|
// prepare message msg_u1 for virtual Voltage meter"
|
|
//
|
|
u1 = data.getL1Voltage();
|
|
if (u1 > 0.1){
|
|
json_u1["command"] = "udevice";
|
|
json_u1["idx"] = idxu1;
|
|
json_u1["nvalue"] = 0;
|
|
json_u1["svalue"] = String(u1);
|
|
// Stringify the json
|
|
String msg_u1;
|
|
serializeJson(json_u1, msg_u1);
|
|
// publish power data directly to domoticz/in
|
|
mqtt.publish("domoticz/in", msg_u1.c_str());
|
|
}
|
|
}
|
|
int idxu2 =config.getDomoVL2IDX();
|
|
if (idxu2 > 0){
|
|
StaticJsonDocument<200> json_u2;
|
|
double u2;
|
|
//
|
|
// prepare message msg_u2 for virtual Voltage meter"
|
|
//
|
|
u2 = data.getL2Voltage();
|
|
if (u2 > 0.1){
|
|
json_u2["command"] = "udevice";
|
|
json_u2["idx"] = idxu2;
|
|
json_u2["nvalue"] = 0;
|
|
json_u2["svalue"] = String(u2);
|
|
// Stringify the json
|
|
String msg_u2;
|
|
serializeJson(json_u2, msg_u2);
|
|
// publish power data directly to domoticz/in
|
|
mqtt.publish("domoticz/in", msg_u2.c_str());
|
|
}
|
|
}
|
|
int idxu3 =config.getDomoVL3IDX();
|
|
if (idxu3 > 0){
|
|
StaticJsonDocument<200> json_u3;
|
|
double u3;
|
|
//
|
|
// prepare message msg_u3 for virtual Voltage meter"
|
|
//
|
|
u3 = data.getL3Voltage();
|
|
if (u3 > 0.1){
|
|
json_u3["command"] = "udevice";
|
|
json_u3["idx"] = idxu3;
|
|
json_u3["nvalue"] = 0;
|
|
json_u3["svalue"] = String(u3);
|
|
// Stringify the json
|
|
String msg_u3;
|
|
serializeJson(json_u3, msg_u3);
|
|
// publish power data directly to domoticz/in
|
|
mqtt.publish("domoticz/in", msg_u3.c_str());
|
|
}
|
|
}
|
|
|
|
int idxi1 =config.getDomoCL1IDX();
|
|
if (idxi1 > 0){
|
|
StaticJsonDocument<200> json_i1;
|
|
double i1, i2, i3;
|
|
String Ampere3;
|
|
//
|
|
// prepare message msg_i1 for virtual Current/Ampere 3phase mater"
|
|
//
|
|
i1 = data.getL1Current();
|
|
i2 = data.getL2Current();
|
|
i3 = data.getL3Current();
|
|
Ampere3 = String(i1) + ";" + String(i2) + ";" + String(i3) ;
|
|
json_i1["command"] = "udevice";
|
|
json_i1["idx"] = idxi1;
|
|
json_i1["nvalue"] = 0;
|
|
json_i1["svalue"] = Ampere3;
|
|
// Stringify the json
|
|
String msg_i1;
|
|
serializeJson(json_i1, msg_i1);
|
|
// publish power data directly to domoticz/in
|
|
if (i1 > 0.0) mqtt.publish("domoticz/in", msg_i1.c_str());
|
|
}
|
|
//
|
|
// End DOMOTICZ
|
|
//
|
|
} else if(config.getMqttPayloadFormat() == 1 || config.getMqttPayloadFormat() == 2) {
|
|
mqtt.publish(String(config.getMqttPublishTopic()) + "/meter/dlms/timestamp", String(data.getPackageTimestamp()));
|
|
switch(data.getListType()) {
|
|
case 3:
|
|
// ID and type belongs to List 2, but I see no need to send that every 10s
|
|
mqtt.publish(String(config.getMqttPublishTopic()) + "/meter/id", data.getMeterId());
|
|
mqtt.publish(String(config.getMqttPublishTopic()) + "/meter/type", data.getMeterType());
|
|
mqtt.publish(String(config.getMqttPublishTopic()) + "/meter/clock", String(data.getMeterTimestamp()));
|
|
mqtt.publish(String(config.getMqttPublishTopic()) + "/meter/import/reactive/accumulated", String(data.getReactiveImportCounter(), 2));
|
|
mqtt.publish(String(config.getMqttPublishTopic()) + "/meter/import/active/accumulated", String(data.getActiveImportCounter(), 2));
|
|
mqtt.publish(String(config.getMqttPublishTopic()) + "/meter/export/reactive/accumulated", String(data.getReactiveExportCounter(), 2));
|
|
mqtt.publish(String(config.getMqttPublishTopic()) + "/meter/export/active/accumulated", String(data.getActiveExportCounter(), 2));
|
|
case 2:
|
|
// Only send data if changed. ID and Type is sent on the 10s interval only if changed
|
|
if(lastMqttData.getMeterId() != data.getMeterId() || config.getMqttPayloadFormat() == 2) {
|
|
mqtt.publish(String(config.getMqttPublishTopic()) + "/meter/id", data.getMeterId());
|
|
}
|
|
if(lastMqttData.getMeterType() != data.getMeterType() || config.getMqttPayloadFormat() == 2) {
|
|
mqtt.publish(String(config.getMqttPublishTopic()) + "/meter/type", data.getMeterType());
|
|
}
|
|
if(lastMqttData.getL1Current() != data.getL1Current() || config.getMqttPayloadFormat() == 2) {
|
|
mqtt.publish(String(config.getMqttPublishTopic()) + "/meter/l1/current", String(data.getL1Current(), 2));
|
|
}
|
|
if(lastMqttData.getL1Voltage() != data.getL1Voltage() || config.getMqttPayloadFormat() == 2) {
|
|
mqtt.publish(String(config.getMqttPublishTopic()) + "/meter/l1/voltage", String(data.getL1Voltage(), 2));
|
|
}
|
|
if(lastMqttData.getL2Current() != data.getL2Current() || config.getMqttPayloadFormat() == 2) {
|
|
mqtt.publish(String(config.getMqttPublishTopic()) + "/meter/l2/current", String(data.getL2Current(), 2));
|
|
}
|
|
if(lastMqttData.getL2Voltage() != data.getL2Voltage() || config.getMqttPayloadFormat() == 2) {
|
|
mqtt.publish(String(config.getMqttPublishTopic()) + "/meter/l2/voltage", String(data.getL2Voltage(), 2));
|
|
}
|
|
if(lastMqttData.getL3Current() != data.getL3Current() || config.getMqttPayloadFormat() == 2) {
|
|
mqtt.publish(String(config.getMqttPublishTopic()) + "/meter/l3/current", String(data.getL3Current(), 2));
|
|
}
|
|
if(lastMqttData.getL3Voltage() != data.getL3Voltage() || config.getMqttPayloadFormat() == 2) {
|
|
mqtt.publish(String(config.getMqttPublishTopic()) + "/meter/l3/voltage", String(data.getL3Voltage(), 2));
|
|
}
|
|
if(lastMqttData.getReactiveExportPower() != data.getReactiveExportPower() || config.getMqttPayloadFormat() == 2) {
|
|
mqtt.publish(String(config.getMqttPublishTopic()) + "/meter/export/reactive", String(data.getReactiveExportPower()));
|
|
}
|
|
if(lastMqttData.getActiveExportPower() != data.getActiveExportPower() || config.getMqttPayloadFormat() == 2) {
|
|
mqtt.publish(String(config.getMqttPublishTopic()) + "/meter/export/active", String(data.getActiveExportPower()));
|
|
}
|
|
if(lastMqttData.getReactiveImportPower() != data.getReactiveImportPower() || config.getMqttPayloadFormat() == 2) {
|
|
mqtt.publish(String(config.getMqttPublishTopic()) + "/meter/import/reactive", String(data.getReactiveImportPower()));
|
|
}
|
|
case 1:
|
|
if(lastMqttData.getActiveImportPower() != data.getActiveImportPower() || config.getMqttPayloadFormat() == 2) {
|
|
mqtt.publish(String(config.getMqttPublishTopic()) + "/meter/import/active", String(data.getActiveImportPower()));
|
|
}
|
|
}
|
|
}
|
|
lastMqttData.apply(data);
|
|
mqtt.loop();
|
|
delay(10);
|
|
}
|
|
} else {
|
|
if(config.isSendUnknown() && strlen(config.getMqttHost()) > 0 && strlen(config.getMqttPublishTopic()) > 0) {
|
|
byte buf[512];
|
|
int length = hanReader.getBuffer(buf);
|
|
String hexstring = "";
|
|
|
|
for(int i = 0; i < length; i++) {
|
|
if(buf[i] < 0x10) {
|
|
hexstring += '0';
|
|
}
|
|
|
|
hexstring += String(buf[i], HEX);
|
|
}
|
|
mqtt.publish(String(config.getMqttPublishTopic()), hexstring);
|
|
}
|
|
}
|
|
} else {
|
|
// Auto detect meter if not set
|
|
for(int i = 1; i <= 4; i++) {
|
|
String list;
|
|
switch(i) {
|
|
case METER_TYPE_KAIFA:
|
|
list = hanReader.getString((int) Kaifa_List1Phase::ListVersionIdentifier);
|
|
break;
|
|
case METER_TYPE_AIDON:
|
|
list = hanReader.getString((int) Aidon_List1Phase::ListVersionIdentifier);
|
|
break;
|
|
case METER_TYPE_KAMSTRUP:
|
|
list = hanReader.getString((int) Kamstrup_List1Phase::ListVersionIdentifier);
|
|
break;
|
|
case METER_TYPE_OMNIPOWER:
|
|
list = hanReader.getString((int) Omnipower_DLMS::ListVersionIdentifier);
|
|
break;
|
|
}
|
|
if(!list.isEmpty()) {
|
|
list.toLowerCase();
|
|
if(list.startsWith("kfm")) {
|
|
config.setMeterType(METER_TYPE_KAIFA);
|
|
if(Debug.isActive(RemoteDebug::INFO)) debugI("Detected Kaifa meter");
|
|
} else if(list.startsWith("aidon")) {
|
|
config.setMeterType(METER_TYPE_AIDON);
|
|
if(Debug.isActive(RemoteDebug::INFO)) debugI("Detected Aidon meter");
|
|
} else if(list.startsWith("kamstrup")) {
|
|
switch(i) {
|
|
case METER_TYPE_KAMSTRUP:
|
|
config.setMeterType(METER_TYPE_KAMSTRUP);
|
|
if(Debug.isActive(RemoteDebug::INFO)) debugI("Detected Kamstrup meter");
|
|
break;
|
|
case METER_TYPE_OMNIPOWER:
|
|
config.setMeterType(METER_TYPE_OMNIPOWER);
|
|
if(Debug.isActive(RemoteDebug::INFO)) debugI("Detected Kamstrup meter");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
hanReader.compensateFor09HeaderBug = (config.getMeterType() == 1);
|
|
}
|
|
}
|
|
|
|
// Switch parity if meter is still not detected
|
|
if(config.getMeterType() == 0 && millis() - lastSuccessfulRead > 10000) {
|
|
lastSuccessfulRead = millis();
|
|
debugD("No data for current setting, switching parity");
|
|
Serial.flush();
|
|
if(++currentMeterType == 4) currentMeterType = 1;
|
|
setupHanPort(config.getHanPin(), currentMeterType);
|
|
}
|
|
}
|
|
|
|
unsigned long wifiTimeout = WIFI_CONNECTION_TIMEOUT;
|
|
unsigned long lastWifiRetry = -WIFI_CONNECTION_TIMEOUT;
|
|
void WiFi_connect() {
|
|
if(millis() - lastWifiRetry < wifiTimeout) {
|
|
delay(50);
|
|
return;
|
|
}
|
|
lastWifiRetry = millis();
|
|
|
|
if (Debug.isActive(RemoteDebug::INFO)) debugI("Connecting to WiFi network: %s", config.getWifiSsid());
|
|
|
|
if (WiFi.status() != WL_CONNECTED) {
|
|
MDNS.end();
|
|
WiFi.disconnect();
|
|
yield();
|
|
|
|
WiFi.enableAP(false);
|
|
WiFi.mode(WIFI_STA);
|
|
if(strlen(config.getWifiIp()) > 0) {
|
|
IPAddress ip, gw, sn(255,255,255,0), dns1, dns2;
|
|
ip.fromString(config.getWifiIp());
|
|
gw.fromString(config.getWifiGw());
|
|
sn.fromString(config.getWifiSubnet());
|
|
dns1.fromString(config.getWifiDns1());
|
|
dns2.fromString(config.getWifiDns2());
|
|
WiFi.config(ip, gw, sn, dns1, dns2);
|
|
} else {
|
|
#if defined(ESP32)
|
|
WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE); // Workaround to make DHCP hostname work for ESP32. See: https://github.com/espressif/arduino-esp32/issues/2537
|
|
#endif
|
|
}
|
|
if(strlen(config.getWifiHostname()) > 0) {
|
|
#if defined(ESP8266)
|
|
WiFi.hostname(config.getWifiHostname());
|
|
#elif defined(ESP32)
|
|
WiFi.setHostname(config.getWifiHostname());
|
|
#endif
|
|
}
|
|
WiFi.begin(config.getWifiSsid(), config.getWifiPassword());
|
|
yield();
|
|
}
|
|
}
|
|
|
|
unsigned long lastMqttRetry = -10000;
|
|
void MQTT_connect() {
|
|
if(strlen(config.getMqttHost()) == 0) {
|
|
if(Debug.isActive(RemoteDebug::WARNING)) debugW("No MQTT config");
|
|
return;
|
|
}
|
|
if(millis() - lastMqttRetry < (mqtt.lastError() == 0 ? 5000 : 60000)) {
|
|
yield();
|
|
return;
|
|
}
|
|
lastMqttRetry = millis();
|
|
if(Debug.isActive(RemoteDebug::INFO)) {
|
|
debugD("Disconnecting MQTT before connecting");
|
|
}
|
|
|
|
mqtt.disconnect();
|
|
yield();
|
|
|
|
WiFiClientSecure *secureClient = NULL;
|
|
Client *client = NULL;
|
|
if(config.isMqttSsl()) {
|
|
debugI("MQTT SSL is configured");
|
|
|
|
secureClient = new WiFiClientSecure();
|
|
#if defined(ESP8266)
|
|
secureClient->setBufferSizes(512, 512);
|
|
#endif
|
|
|
|
if(SPIFFS.begin()) {
|
|
char *ca = NULL;
|
|
char *cert = NULL;
|
|
char *key = NULL;
|
|
|
|
if(SPIFFS.exists(FILE_MQTT_CA)) {
|
|
debugI("Found MQTT CA file");
|
|
File file = SPIFFS.open(FILE_MQTT_CA, "r");
|
|
secureClient->loadCACert(file, file.size());
|
|
}
|
|
if(SPIFFS.exists(FILE_MQTT_CERT)) {
|
|
debugI("Found MQTT certificate file");
|
|
File file = SPIFFS.open(FILE_MQTT_CERT, "r");
|
|
secureClient->loadCertificate(file, file.size());
|
|
}
|
|
if(SPIFFS.exists(FILE_MQTT_KEY)) {
|
|
debugI("Found MQTT key file");
|
|
File file = SPIFFS.open(FILE_MQTT_KEY, "r");
|
|
secureClient->loadPrivateKey(file, file.size());
|
|
}
|
|
SPIFFS.end();
|
|
}
|
|
client = secureClient;
|
|
} else {
|
|
client = new WiFiClient();
|
|
}
|
|
|
|
if(Debug.isActive(RemoteDebug::INFO)) {
|
|
debugI("Connecting to MQTT %s:%d", config.getMqttHost(), config.getMqttPort());
|
|
}
|
|
mqtt.begin(config.getMqttHost(), config.getMqttPort(), *client);
|
|
|
|
#if defined(ESP8266)
|
|
if(secureClient) {
|
|
time_t epoch = time(nullptr);
|
|
debugD("Setting NTP time %i for secure MQTT connection", epoch);
|
|
secureClient->setX509Time(epoch);
|
|
}
|
|
#endif
|
|
|
|
// Connect to a unsecure or secure MQTT server
|
|
if ((strlen(config.getMqttUser()) == 0 && mqtt.connect(config.getMqttClientId())) ||
|
|
(strlen(config.getMqttUser()) > 0 && mqtt.connect(config.getMqttClientId(), config.getMqttUser(), config.getMqttPassword()))) {
|
|
if (Debug.isActive(RemoteDebug::INFO)) debugI("Successfully connected to MQTT!");
|
|
config.ackMqttChange();
|
|
|
|
// Subscribe to the chosen MQTT topic, if set in configuration
|
|
if (strlen(config.getMqttSubscribeTopic()) > 0) {
|
|
mqtt.subscribe(config.getMqttSubscribeTopic());
|
|
if (Debug.isActive(RemoteDebug::INFO)) debugI(" Subscribing to [%s]\r\n", config.getMqttSubscribeTopic());
|
|
}
|
|
|
|
if(config.getMqttPayloadFormat() == 0) {
|
|
sendMqttData("Connected!");
|
|
} else if(config.getMqttPayloadFormat() == 1) {
|
|
sendSystemStatusToMqtt();
|
|
}
|
|
} else {
|
|
if (Debug.isActive(RemoteDebug::ERROR)) {
|
|
debugE("Failed to connect to MQTT");
|
|
#if defined(ESP8266)
|
|
if(secureClient) {
|
|
char buf[256];
|
|
secureClient->getLastSSLError(buf,256);
|
|
Debug.println(buf);
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
yield();
|
|
}
|
|
|
|
// Send a simple string embedded in json over MQTT
|
|
void sendMqttData(String data)
|
|
{
|
|
// Make sure we have configured a publish topic
|
|
if (strlen(config.getMqttPublishTopic()) == 0)
|
|
return;
|
|
|
|
// Build a json with the message in a "data" attribute
|
|
StaticJsonDocument<128> json;
|
|
json["id"] = WiFi.macAddress();
|
|
json["up"] = millis64()/1000;
|
|
json["data"] = data;
|
|
double vcc = hw.getVcc();
|
|
if(vcc > 0) {
|
|
json["vcc"] = vcc;
|
|
}
|
|
json["rssi"] = hw.getWifiRssi();
|
|
|
|
// Stringify the json
|
|
String msg;
|
|
serializeJson(json, msg);
|
|
|
|
// Send the json over MQTT
|
|
mqtt.publish(config.getMqttPublishTopic(), msg.c_str());
|
|
|
|
if (Debug.isActive(RemoteDebug::INFO)) debugI("Sending MQTT data");
|
|
if (Debug.isActive(RemoteDebug::DEBUG)) debugD("[%s]", data.c_str());
|
|
}
|
|
|
|
unsigned long lastSystemDataSent = -10000;
|
|
void sendSystemStatusToMqtt() {
|
|
if (strlen(config.getMqttPublishTopic()) == 0)
|
|
return;
|
|
if(millis() - lastSystemDataSent < 10000)
|
|
return;
|
|
lastSystemDataSent = millis();
|
|
|
|
mqtt.publish(String(config.getMqttPublishTopic()) + "/id", WiFi.macAddress());
|
|
mqtt.publish(String(config.getMqttPublishTopic()) + "/uptime", String((unsigned long) millis64()/1000));
|
|
double vcc = hw.getVcc();
|
|
if(vcc > 0) {
|
|
mqtt.publish(String(config.getMqttPublishTopic()) + "/vcc", String(vcc, 2));
|
|
}
|
|
mqtt.publish(String(config.getMqttPublishTopic()) + "/rssi", String(hw.getWifiRssi()));
|
|
if(hw.getTemperature() > -85) {
|
|
mqtt.publish(String(config.getMqttPublishTopic()) + "/temperature", String(hw.getTemperature(), 2));
|
|
}
|
|
}
|