// Some useful websites that I used for writing this sketch:
//
// DSMR parser library: https://github.com/matthijskooijman/arduino-dsmr
// MQTT example code: https://www.instructables.com/MQTT-Bare-Minimum-Sketch/
// Circuit that inspired mine: https://klushok.etv.tudelft.nl/projects/view?id=8

#include <Arduino.h>
#include <PubSubClient.h>
#include <ESP8266WiFi.h>
#include "dsmr.h"

// -----------------------------------------------------------------------
// Update these for your configuration

#define wifi_ssid "****"
#define wifi_password "*****"
#define mqtt_server "*****"
#define mqtt_port 1883
#define mqtt_user "*****"
#define mqtt_password "*****"

// When defined, no serial data are read, but instead the stub
// data from below is used. This is useful to test the WiFi and
// MQTT connection, while not being connected to an actual
// smart meter.
//#define TEST_DATA

// -----------------------------------------------------------------------

#ifdef TEST_DATA
const char test_msg[] =
  "/KFM5KAIFA-METER\r\n"
  "\r\n"
  "1-3:0.2.8(40)\r\n"
  "0-0:1.0.0(150117185916W)\r\n"
  "0-0:96.1.1(0000000000000000000000000000000000)\r\n"
  "1-0:1.8.1(000671.578*kWh)\r\n"
  "1-0:1.8.2(000842.472*kWh)\r\n"
  "1-0:2.8.1(000000.000*kWh)\r\n"
  "1-0:2.8.2(000000.000*kWh)\r\n"
  "0-0:96.14.0(0001)\r\n"
  "1-0:1.7.0(00.333*kW)\r\n"
  "1-0:2.7.0(00.000*kW)\r\n"
  "0-0:17.0.0(999.9*kW)\r\n"
  "0-0:96.3.10(1)\r\n"
  "0-0:96.7.21(00008)\r\n"
  "0-0:96.7.9(00007)\r\n"
  "1-0:99.97.0(1)(0-0:96.7.19)(000101000001W)(2147483647*s)\r\n"
  "1-0:32.32.0(00000)\r\n"
  "1-0:32.36.0(00000)\r\n"
  "0-0:96.13.1()\r\n"
  "0-0:96.13.0()\r\n"
  "1-0:31.7.0(001*A)\r\n"
  "1-0:21.7.0(00.332*kW)\r\n"
  "1-0:22.7.0(00.000*kW)\r\n"
  "0-1:24.1.0(003)\r\n"
  "0-1:96.1.0(0000000000000000000000000000000000)\r\n"
  "0-1:24.2.1(150117180000W)(00473.789*m3)\r\n"
  "0-1:24.4.0(1)\r\n"
  "!6F4A\r\n";
#endif

// All possible items that can be received from the P1 port,
// depending on the DSMR version used. This sketch tries to
// fetch all of them, and publishes the items that actually
// were received. When desired, you can safely remove unneeded
// items from this definition by simply deleting their lines.
using MeterData = ParsedData<
  /* String */ identification,
  /* String */ p1_version,
  /* String */ timestamp,
  /* String */ equipment_id,
  /* FixedValue */ energy_delivered_tariff1,
  /* FixedValue */ energy_delivered_tariff2,
  /* FixedValue */ energy_returned_tariff1,
  /* FixedValue */ energy_returned_tariff2,
  /* String */ electricity_tariff,
  /* FixedValue */ power_delivered,
  /* FixedValue */ power_returned,
  /* FixedValue */ electricity_threshold,
  /* uint8_t */ electricity_switch_position,
  /* uint32_t */ electricity_failures,
  /* uint32_t */ electricity_long_failures,
  /* String */ electricity_failure_log,
  /* uint32_t */ electricity_sags_l1,
  /* uint32_t */ electricity_sags_l2,
  /* uint32_t */ electricity_sags_l3,
  /* uint32_t */ electricity_swells_l1,
  /* uint32_t */ electricity_swells_l2,
  /* uint32_t */ electricity_swells_l3,
  /* String */ message_short,
  /* String */ message_long,
  /* FixedValue */ voltage_l1,
  /* FixedValue */ voltage_l2,
  /* FixedValue */ voltage_l3,
  /* FixedValue */ current_l1,
  /* FixedValue */ current_l2,
  /* FixedValue */ current_l3,
  /* FixedValue */ power_delivered_l1,
  /* FixedValue */ power_delivered_l2,
  /* FixedValue */ power_delivered_l3,
  /* FixedValue */ power_returned_l1,
  /* FixedValue */ power_returned_l2,
  /* FixedValue */ power_returned_l3,
  /* uint16_t */ gas_device_type,
  /* String */ gas_equipment_id,
  /* uint8_t */ gas_valve_position,
  /* TimestampedFixedValue */ gas_delivered,
  /* uint16_t */ thermal_device_type,
  /* String */ thermal_equipment_id,
  /* uint8_t */ thermal_valve_position,
  /* TimestampedFixedValue */ thermal_delivered,
  /* uint16_t */ water_device_type,
  /* String */ water_equipment_id,
  /* uint8_t */ water_valve_position,
  /* TimestampedFixedValue */ water_delivered,
  /* uint16_t */ slave_device_type,
  /* String */ slave_equipment_id,
  /* uint8_t */ slave_valve_position,
  /* TimestampedFixedValue */ slave_delivered
>;

// My circuit has the request PIN always HIGH (because it is
// directly connected to the 5V power supply of the smart meter.
// PIN 3 (GPIO2) is not connected, so I'll feed that one as a
// stub here. Maybe it's an idea to hook up a LED + resistor
// to this pin for some visual feedback, making it an 'I am
// reading data' indicator.
P1Reader reader(&Serial, 3);

WiFiClient wifiClient;
PubSubClient pubsubClient;

void setup() {
  Serial.begin(115200, SERIAL_8N1);
  delay(1000); // Wait a bit for the serial port to wake up.
  Serial.println("Setting up device ...");
  setup_wifi();
  setup_pubsub();
  Serial.println("Starting the main loop ...");
}

void setup_wifi() {
  // We start by connecting to a WiFi network
  Serial.print("Setup connection to WiFi network '");
  Serial.print(wifi_ssid);
  Serial.print("' ");
  WiFi.begin(wifi_ssid, wifi_password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
}

void setup_pubsub() {
  Serial.println("Setup MQTT broker connection");
  pubsubClient.setClient(wifiClient);
  pubsubClient.setServer(mqtt_server, mqtt_port);
}

void reconnect() {
  // Loop until we're reconnected
  while (!pubsubClient.connected()) {
    Serial.print("Attempting MQTT connection ... ");
    if (pubsubClient.connect("ESP8266Client", mqtt_user, mqtt_password)) {
      Serial.println("connected");
    } else {
      Serial.print("failed, rc=");
      Serial.print(pubsubClient.state());
      Serial.println(", try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

struct Printer {
  template<typename Item>
  void apply(Item &i) {
    if (i.present()) {
      if (!pubsubClient.connected()) {
        reconnect();
      }
      String key = String("smartmeter/");
      key.concat(Item::name);
      
      String value = String(i.val());
      value.concat(" ");
      value.concat(Item::unit());

      Serial.print("> ");
      Serial.print(key);
      Serial.print(" = ");
      Serial.println(value);
      pubsubClient.publish(key.c_str(), value.c_str(), true);
    }
  }
};

// Implementation of the main loop using test data.
#ifdef TEST_DATA

void loop() {
  MeterData data;
  String err;
  
  Serial.println("Parsing test data");
  ParseResult<void> res = P1Parser::parse(&data, test_msg, lengthof(test_msg));
  if (res.err) {
    Serial.println(res.fullError(test_msg, test_msg + lengthof(test_msg)));
    if (!pubsubClient.connected()) {
      reconnect();
    }
    if (pubsubClient.connected()) {
      pubsubClient.publish("smartmeter/error", res.fullError(test_msg, test_msg + lengthof(test_msg)).c_str(), true);
    }
//  } else if (!data.all_present()) {
//    Serial.println("Some fields are missing");
  } else {
    data.applyEach(Printer());   
    // Succesfully parsed, print results:
    Serial.println(data.identification);
    Serial.print(data.power_delivered.int_val());
    Serial.println("W");
  }

  delay(10000);
}

// Implementation of the main loop using serial P1 data.
# else

long waitLoop = 0;

void loop() {
  // Builds a complete telegram.
  reader.loop();

  // Every 4 seconds, start a new telegram reading operation.
  // The smart meter will provide a telegram every 10 seconds.
  // By restarting every 4 seconds, we know that we will be
  // in sync with the P1 output at some point. A bit crude, but
  // I don't have a real RTS request line in my current
  // hardware design.
  if (millis() > waitLoop) {
      Serial.println("Start new telegram read operation");
      waitLoop = millis() + 4000;
      reader.enable(true);
  }

  // True when a new telegram is read.
  if (reader.available()) {
    MeterData data;
    String err;
        
    Serial.print("Parsing data from P1 ... ");
    if (reader.parse(&data, &err)) {
      Serial.println("OK");
      data.applyEach(Printer());
    } else {
      // When the parser fails, we log the error to console
      // and to an MQTT topic.
      Serial.println("ERROR");  
      Serial.println(err);

      if (!pubsubClient.connected()) {
        reconnect();
      }
      if (pubsubClient.connected()) {
        pubsubClient.publish("smartmeter/error", err.c_str(), true);
      }    
    }
  }
}

#endif // TEST OR PRODUCTION
