diff --git a/.gitignore b/.gitignore index 9ac56ed..bca76e6 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,7 @@ # Pycharm .idea + +*.h.lv + +settings.h diff --git a/README.md b/README.md index b07c8d4..6e95f6b 100644 --- a/README.md +++ b/README.md @@ -1,122 +1,153 @@ # esp32_p1meter -Software for the ESP32 (DoIT ESP DEVKIT v1/NodeMcu 32s etc.) that decodes and sends P1 smart meter (DSMR) data to a MQTT broker, with the possibility for Over The Air (OTA) firmware updates. +Software for the ESP32 (uPesy ESP32 Wroom Devkit) that decodes and sends P1 smart meter (DSMR) data to a MQTT broker, with the possibility for Over The Air (OTA) firmware updates. ## About this fork -This fork was based on a ESP8266 and I only had a ESP32 laying around so I'm trying to make this work on my ESP32 DoIT board. +This fork is based on work from [bartwo](https://github.com/bartwo/esp32_p1meter). -The original project of [fliphess](https://github.com/fliphess/esp8266_p1meter) has issues with DSMR 5.0 meters, which send telegrams every 1 second at a high 115200 baud rate. -This causes the used SoftwareSerial to struggle to keep up and thus only receives corrupted messages. - -The project of [daniel-jong](https://github.com/daniel-jong/esp8266_p1meter) switches to using the main hardware serial port (RX) for communication with the p1 meter and is tested on the `Landys and Gyr E360` smartmeter (DSMR 5.0). - -Then I noticed the project of [WhoSayIn](https://github.com/WhoSayIn/esp8266_dsmr2mqtt), that takes a much more minimalistic approach, which I liked. However, I discovered this project was also designed for the DSMR 4.0 meters. - -With this fork, I want to accomplish the following: -- Combine the projects mentioned above in a minimalistic setup for the newer DSMR 5.0 smart meters. -- Separate code in multiple files for readability. -- Add solar panel meter: read out delivered energy. -- Easy to read and add new readouts from a telegram. Used a struct to accomplish this. -- Generate the full MQTT topics based on the array of telegram decode structs. -- Easy to debug the software and able to compile without the debug for a more compact compiled code base. - -I noticed that the other repositories use SoftwareSerial library to readout the P1 port but the ESP32 has multiple RX and TX port to read en write serial streams. This made it easy to debug the code and have the full speed of the hardware serial. Also the ESP32 is a bit faster so it doesn't crash as fast as a ESP8266 when you want to readout every second. +How is this fork different from base: +- Make MQTT sensors configured using JSON map with all available data and get MQTT sensors created dynamically. +- Add email sending for debugging purposes. +- Added support for string readings not just numbers. +- Even more refactored code for readability. ## Setup This setup requires: -- An ESP32 (DoIT DEVKIT v1 has been tested) -- Small breadboard -- A 10k ohm resistor -- A 4 pin (RJ11) or [6 pin (RJ12) cable](https://www.tinytronics.nl/shop/nl/kabels/adapters/rj12-naar-6-pins-dupont-jumper-adapter). Both cables work great, but a 6 pin cable can also power the ESP8266 on most DSMR5+ meters. +- An ESP32 (I have used [ESP32-WROOM-32U](https://www.aliexpress.com/item/32864722159.html) because of external antenna - my meter is in metal case ~15m outside from the access point with thick wall) +- IPEX-SMA cable of appropriate length to have antenna outside and put it through some hole in the meter metal case. For me 50cm length is more than enough. +- A 10k ohm resistor. +- A simple LED for debugging and appropriate resistor for it for more information read [here](https://kitronik.co.uk/blogs/resources/which-resistor-should-i-use-with-my-led). +- A 6 pin RJ12 cable created from UTP cable and soldered directly to the kit. 4 pin version should work too but in Lithuania it's hard to have external power for the device in the meter box. Setting up your Arduino IDE: - Ensure you have selected the right board (you might need to install your esp32board in the Arduino IDE). -- I have tested this on the 80 MHz and 160 MHz CPU frequency mode, pick either one. -- Using the Tools->Manage Libraries... install `PubSubClient`. +- I have tested this on the 240 MHz and 160 MHz CPU frequency mode, pick either one. +- Using the Tools->Manage Libraries... install `PubSubClient`, `ArduinoJson` and `tiny-collections`. For email debugging install `EmailSender` library. - In the file `Settings.h` change all values accordingly - Write to your device via USB the first time, you can do it OTA all times thereafter. ### Circuit diagram -_Note: I have only tested this on the `ISKRA AM550`._ -I have used the RX02 pin on the ESP32 so u can still use the USB port for debugging you ESP32. -Connect the ESP32 to an RJ11 cable/connector following the diagram. +I have used the RX02 pin on the ESP32 to still use the USB port on devkit for debugging your ESP32. You could use any other pin however if you use standard RX and try debugging using USB, conflicts will happen. +Connect the ESP32 to an RJ12 cable/connector following the diagram. | P1 pin | ESP32 Pin | | ---- | ---- | +| 1 - 5v out | 5v or Vin | | 2 - RTS | 3.3v | | 3 - GND | GND | | 4 - | | | 5 - RXD (data) | RX02 (gpio16) | +| 6 - GND | GND | -On most models a 10K resistor should be used between the ESP's 3.3v and the p1's DATA (RXD) pin. Many howto's mention RTS requires 5V (VIN) to activate the P1 port, but for me 3V3 suffices. +On most models a 10K resistor should be used between the ESP's 3.3v and the P1's DATA (RXD) pin. Many howto's mention RTS requires 5V (VIN) to activate the P1 port, but for me 3V3 suffices. -
Optional: Powering the ESP8266 using your DSMR5+ meter +
Optional: Simpler 4 pin cable without power from your DSMR5+ meter

-When using a 6 pin cable you can use the power source provided by the meter. - +If you have how to power ESP32, a 4 pin cable is OK. + | P1 pin | ESP32 Pin | | ---- | ---- | -| 1 - 5v out | 5v or Vin | | 2 - RTS | 3.3v | | 3 - GND | GND | | 4 - | | | 5 - RXD (data) | RX02 (gpio16) | -| 6 - GND | GND | - +

### Data Sent -All metrics are send to their own MQTT topic. -The software generates all the topic through the Serial monitor when starting up -Example topics are: +All metrics are send to their own MQTT topic. The software outputs all the topics through the Serial monitor when starting up +Example: ``` -sensors/power/p1meter/consumption_low_tarif -sensors/power/p1meter/consumption_high_tarif -sensors/power/p1meter/actual_received -sensors/power/p1meter/instant_power_usage_l1 -sensors/power/p1meter/instant_power_usage_l2 -sensors/power/p1meter/instant_power_usage_l3 -sensors/power/p1meter/instant_power_current_l1 -sensors/power/p1meter/instant_power_current_l2 -sensors/power/p1meter/instant_power_current_l3 -sensors/power/p1meter/instant_voltage_l1 -sensors/power/p1meter/instant_voltage_l2 -sensors/power/p1meter/instant_voltage_l3 -sensors/power/p1meter/actual_tarif_group -sensors/power/p1meter/short_power_outages -sensors/power/p1meter/long_power_outages -sensors/power/p1meter/short_power_drops -sensors/power/p1meter/short_power_peaks +p1_meter/sensor/active_energy_import +p1_meter/sensor/clock +p1_meter/sensor/reactive_energy_import +p1_meter/sensor/reactive_energy_export +p1_meter/sensor/active_energy_import_rate_1 +p1_meter/sensor/active_energy_import_rate_2 +p1_meter/sensor/active_energy_export_rate_1 +p1_meter/sensor/active_energy_export_rate_2 +p1_meter/sensor/reactive_energy_rate_1 +p1_meter/sensor/reactive_energy_rate_2 +p1_meter/sensor/reactive_energy_minusr_rate_1 +p1_meter/sensor/reactive_energy_minusr_rate_2 +p1_meter/sensor/instantaneous_voltage_l1 +p1_meter/sensor/average_voltage_l1 +p1_meter/sensor/instantaneous_current_l1 +p1_meter/sensor/sliding_average_current_l1 +p1_meter/sensor/instantaneous_voltage_l2 +p1_meter/sensor/average_voltage_l2 +p1_meter/sensor/instantaneous_current_l2 +p1_meter/sensor/sliding_average_current_l2 +p1_meter/sensor/instantaneous_voltage_l3 +p1_meter/sensor/average_voltage_l3 +p1_meter/sensor/instantaneous_current_l3 +p1_meter/sensor/sliding_average_current_l3 +p1_meter/sensor/instantaneous_voltage +p1_meter/sensor/instantaneous_current +p1_meter/sensor/instantaneous_current_in_neutral +p1_meter/sensor/instantaneous_current_sum_over_all_phases +p1_meter/sensor/instantaneous_net_frequency_any_phase +p1_meter/sensor/instantaneous_active_power +p1_meter/sensor/instantaneous_active_import_power_in_phase_l1 +p1_meter/sensor/instantaneous_active_import_power_in_phase_l2 +p1_meter/sensor/instantaneous_active_import_power_in_phase_l3 +p1_meter/sensor/instantaneous_active_export_power_in_phase_l1 +p1_meter/sensor/instantaneous_active_export_power_in_phase_l2 +p1_meter/sensor/instantaneous_active_export_power_in_phase_l3 +p1_meter/sensor/instantaneous_reactive_import_power_in_phase_l1 +p1_meter/sensor/instantaneous_reactive_import_power_in_phase_l2 +p1_meter/sensor/instantaneous_reactive_import_power_in_phase_l3 +p1_meter/sensor/instantaneous_reactive_export_power_in_phase_l1 +p1_meter/sensor/instantaneous_reactive_export_power_in_phase_l2 +p1_meter/sensor/instantaneous_reactive_export_power_in_phase_l3 +p1_meter/sensor/instantaneous_apparent_import_power +p1_meter/sensor/instantaneous_apparent_import_power_in_phase_l1 +p1_meter/sensor/instantaneous_apparent_import_power_in_phase_l2 +p1_meter/sensor/instantaneous_apparent_import_power_in_phase_l3 +p1_meter/sensor/instantaneous_apparent_export_power +p1_meter/sensor/instantaneous_apparent_export_power_in_phase_l1 +p1_meter/sensor/instantaneous_apparent_export_power_in_phase_l2 +p1_meter/sensor/instantaneous_apparent_export_power_in_phase_l3 +p1_meter/sensor/average_import_power +p1_meter/sensor/average_net_power +p1_meter/sensor/average_total_power +p1_meter/sensor/instantaneous_power_factor +p1_meter/sensor/instantaneous_power_factor_in_phase_l1 +p1_meter/sensor/instantaneous_power_factor_in_phase_l2 +p1_meter/sensor/instantaneous_power_factor_in_phase_l3 +p1_meter/sensor/minimum_power_factor +p1_meter/sensor/demand_register_1_active_energy_import +p1_meter/sensor/demand_register_2_active_energy_export +p1_meter/sensor/demand_register_3_reactive_energy_import +p1_meter/sensor/demand_register_4_reactive_energy_export +p1_meter/sensor/demand_register_5_apparent_energy_import +p1_meter/sensor/demand_register_6_apparent_energy_export +p1_meter/sensor/last_average_demand_register_1_active_energy_import +p1_meter/sensor/last_average_demand_register_2_active_energy_export +p1_meter/sensor/last_average_demand_register_3_reactive_energy_import +p1_meter/sensor/last_average_demand_register_4_reactive_energy_export +p1_meter/sensor/last_average_demand_register_5_apparent_energy_import +p1_meter/sensor/last_average_demand_register_6_apparent_energy_export +p1_meter/sensor/number_of_power_failures_in_any_phase +p1_meter/sensor/duration_of_last_voltage_sag_in_phase_l1 +p1_meter/sensor/duration_of_last_voltage_sag_in_phase_l2 +p1_meter/sensor/duration_of_last_voltage_sag_in_phase_l3 +p1_meter/sensor/magnitude_of_last_voltage_sag_in_phase_l1 +p1_meter/sensor/magnitude_of_last_voltage_sag_in_phase_l2 +p1_meter/sensor/magnitude_of_last_voltage_sag_in_phase_l3 +p1_meter/sensor/duration_of_last_voltage_swell_in_phase_l1 +p1_meter/sensor/duration_of_last_voltage_swell_in_phase_l2 +p1_meter/sensor/duration_of_last_voltage_swell_in_phase_l3 +p1_meter/sensor/magnitude_of_last_voltage_swell_in_phase_l1 +p1_meter/sensor/magnitude_of_last_voltage_swell_in_phase_l2 +p1_meter/sensor/magnitude_of_last_voltage_swell_in_phase_l3 ``` -But all the metrics you need are easily added using the `setupDataReadout()` method. With the DEBUG mode it is easy to see all the topics you add/create by the serial monitor. To see what your telegram is outputting in the Netherlands see: https://www.netbeheernederland.nl/_upload/Files/Slimme_meter_15_a727fce1f1.pdf for the dutch codes pag. 19 -23 - +All the metrics you need are easily added in `DSMR_MAP` variable in `dsmr_map.h` file. With the DEBUG mode it's possible to see all the topics you add/create in the serial monitor. Also, it's possible to configure topic structure by changing `MQTT_ROOT_TOPIC` value in `settings.h` file. +There is additional TEST mode to try your setup with test telegram and actual MQTT message send while your adapter is not connected to P1 port. +EMAIL_DEBUGGING is used to send debug messages to any email, e.g. GMail address. This is might be usefull to trace what's going on when device is connected to P1 port and actual debugging using USB port is impossible. ### Home Assistant Configuration Use this [example](https://raw.githubusercontent.com/daniel-jong/esp8266_p1meter/master/assets/p1_sensors.yaml) for home assistant's `sensor.yaml` - -## Known limitations and issues -My ESP32 can use the 5v from the `ISKRA AM550` but you first need to power it on via USB else it will bootloop. After it's booted and connected with the 5v port on the P1 connection you can unplug the ESP32 and it will stay on. - -## Thanks to -I want to Thank [JHockx](https://github.com/jhockx/esp8266_p1meter) because he told me he was working on a project reading out his P1 Meter. It sounded like a fun project but I had somewhat different hardware laying around so I started working with that. - -I also want to thank all the people he mentions in his project: -- https://github.com/fliphess/esp8266_p1meter -- https://github.com/jantenhove/P1-Meter-ESP8266 -- https://github.com/neographikal/P1-Meter-ESP8266-MQTT -- http://gejanssen.com/howto/Slimme-meter-uitlezen/ -- https://github.com/rroethof/p1reader/ -- http://romix.macuser.nl/software.html -- http://blog.regout.info/category/slimmeter/ -- http://domoticx.com/p1-poort-slimme-meter-hardware/ - -In addition, I'd like thank and refer to the following projects which served as a source of information: -- [https://github.com/daniel-jong/esp8266_p1meter](https://github.com/daniel-jong/esp8266_p1meter) -- [https://github.com/WhoSayIn/esp8266_dsmr2mqtt](https://github.com/WhoSayIn/esp8266_dsmr2mqtt) -- [https://github.com/jhockx/esp8266_p1meter](https://github.com/jhockx/esp8266_p1meter) - -Other sources: -- [DSMR 5.0 documentation](https://www.netbeheernederland.nl/_upload/Files/Slimme_meter_15_a727fce1f1.pdf) diff --git "a/assets/p1 s\304\205sajos duomen\305\263 modelis.pdf" "b/assets/p1 s\304\205sajos duomen\305\263 modelis.pdf" new file mode 100644 index 0000000..ec61d60 Binary files /dev/null and "b/assets/p1 s\304\205sajos duomen\305\263 modelis.pdf" differ diff --git a/dsmr_map.h b/dsmr_map.h new file mode 100644 index 0000000..89325fc --- /dev/null +++ b/dsmr_map.h @@ -0,0 +1,395 @@ +/* + The main readout JSON file. Adopt it for your needs. Items can be removed from it. + OBIS represents code and Name is name of MQTT topic. + The code for finding this in the telegram see + https://www.eso.lt/download/523006/p1%20s%C4%85sajos%20duomen%C5%B3%20modelis.pdf +*/ +const char* DSMR_MAP = R""""( +[ + { + "OBIS": "0-0:1.0.0", + "Name": "clock" + }, + { + "OBIS": "1-0:1.8.0", + "Name": "active_energy_import" + }, + { + "OBIS": "1-0:3.8.0", + "Name": "reactive_energy_import" + }, + { + "OBIS": "1-0:4.8.0", + "Name": "reactive_energy_export" + }, + { + "OBIS": "1-0:1.8.1", + "Name": "active_energy_import_rate_1" + }, + { + "OBIS": "1-0:1.8.2", + "Name": "active_energy_import_rate_2" + }, + { + "OBIS": "1-0:2.8.1", + "Name": "active_energy_export_rate_1" + }, + { + "OBIS": "1-0:2.8.2", + "Name": "active_energy_export_rate_2" + }, + { + "OBIS": "1-0:3.8.1", + "Name": "reactive_energy_rate_1" + }, + { + "OBIS": "1-0:3.8.2", + "Name": "reactive_energy_rate_2" + }, + { + "OBIS": "1-0:4.8.1", + "Name": "reactive_energy_minusr_rate_1" + }, + { + "OBIS": "1-0:4.8.2", + "Name": "reactive_energy_minusr_rate_2" + }, + { + "OBIS": "1-0:32.7.0", + "Name": "instantaneous_voltage_l1" + }, + { + "OBIS": "1-0:32.24.0", + "Name": "average_voltage_l1" + }, + { + "OBIS": "1-0:31.7.0", + "Name": "instantaneous_current_l1" + }, + { + "OBIS": "1-0:31.4.0", + "Name": "sliding_average_current_l1" + }, + { + "OBIS": "1-0:52.7.0", + "Name": "instantaneous_voltage_l2" + }, + { + "OBIS": "1-0:52.24.0", + "Name": "average_voltage_l2" + }, + { + "OBIS": "1-0:51.7.0", + "Name": "instantaneous_current_l2" + }, + { + "OBIS": "1-0:51.4.0", + "Name": "sliding_average_current_l2" + }, + { + "OBIS": "1-0:72.7.0", + "Name": "instantaneous_voltage_l3" + }, + { + "OBIS": "1-0:72.24.0", + "Name": "average_voltage_l3" + }, + { + "OBIS": "1-0:71.7.0", + "Name": "instantaneous_current_l3" + }, + { + "OBIS": "1-0:71.4.0", + "Name": "sliding_average_current_l3" + }, + { + "OBIS": "1-0:12.7.0", + "Name": "instantaneous_voltage" + }, + { + "OBIS": "1-0:11.7.0", + "Name": "instantaneous_current" + }, + { + "OBIS": "1-0:91.7.0", + "Name": "instantaneous_current_in_neutral" + }, + { + "OBIS": "1-0:90.7.0", + "Name": "instantaneous_current_sum_over_all_phases" + }, + { + "OBIS": "1-0:14.7.0", + "Name": "instantaneous_net_frequency_any_phase" + }, + { + "OBIS": "1-0:15.7.0", + "Name": "instantaneous_active_power" + }, + { + "OBIS": "1-0:21.7.0", + "Name": "instantaneous_active_import_power_in_phase_l1" + }, + { + "OBIS": "1-0:41.7.0", + "Name": "instantaneous_active_import_power_in_phase_l2" + }, + { + "OBIS": "1-0:61.7.0", + "Name": "instantaneous_active_import_power_in_phase_l3" + }, + { + "OBIS": "1-0:22.7.0", + "Name": "instantaneous_active_export_power_in_phase_l1" + }, + { + "OBIS": "1-0:42.7.0", + "Name": "instantaneous_active_export_power_in_phase_l2" + }, + { + "OBIS": "1-0:62.7.0", + "Name": "instantaneous_active_export_power_in_phase_l3" + }, + { + "OBIS": "1-0:23.7.0", + "Name": "instantaneous_reactive_import_power_in_phase_l1" + }, + { + "OBIS": "1-0:43.7.0", + "Name": "instantaneous_reactive_import_power_in_phase_l2" + }, + { + "OBIS": "1-0:63.7.0", + "Name": "instantaneous_reactive_import_power_in_phase_l3" + }, + { + "OBIS": "1-0:24.7.0", + "Name": "instantaneous_reactive_export_power_in_phase_l1" + }, + { + "OBIS": "1-0:44.7.0", + "Name": "instantaneous_reactive_export_power_in_phase_l2" + }, + { + "OBIS": "1-0:64.7.0", + "Name": "instantaneous_reactive_export_power_in_phase_l3" + }, + { + "OBIS": "1-0:9.7.0", + "Name": "instantaneous_apparent_import_power" + }, + { + "OBIS": "1-0:29.7.0", + "Name": "instantaneous_apparent_import_power_in_phase_l1" + }, + { + "OBIS": "1-0:49.7.0", + "Name": "instantaneous_apparent_import_power_in_phase_l2" + }, + { + "OBIS": "1-0:69.7.0", + "Name": "instantaneous_apparent_import_power_in_phase_l3" + }, + { + "OBIS": "1-0:10.7.0", + "Name": "instantaneous_apparent_export_power" + }, + { + "OBIS": "1-0:30.7.0", + "Name": "instantaneous_apparent_export_power_in_phase_l1" + }, + { + "OBIS": "1-0:50.7.0", + "Name": "instantaneous_apparent_export_power_in_phase_l2" + }, + { + "OBIS": "1-0:70.7.0", + "Name": "instantaneous_apparent_export_power_in_phase_l3" + }, + { + "OBIS": "1-0:1.24.0", + "Name": "average_import_power" + }, + { + "OBIS": "1-0:16.24.0", + "Name": "average_net_power" + }, + { + "OBIS": "1-0:15.24.0", + "Name": "average_total_power" + }, + { + "OBIS": "1-0:13.7.0", + "Name": "instantaneous_power_factor" + }, + { + "OBIS": "1-0:33.7.0", + "Name": "instantaneous_power_factor_in_phase_l1" + }, + { + "OBIS": "1-0:53.7.0", + "Name": "instantaneous_power_factor_in_phase_l2" + }, + { + "OBIS": "1-0:73.7.0", + "Name": "instantaneous_power_factor_in_phase_l3" + }, + { + "OBIS": "1-0:13.3.0", + "Name": "minimum_power_factor" + }, + { + "OBIS": "1-0:1.4.0", + "Name": "demand_register_1_active_energy_import" + }, + { + "OBIS": "1-0:2.4.0", + "Name": "demand_register_2_active_energy_export" + }, + { + "OBIS": "1-0:3.4.0", + "Name": "demand_register_3_reactive_energy_import" + }, + { + "OBIS": "1-0:4.4.0", + "Name": "demand_register_4_reactive_energy_export" + }, + { + "OBIS": "1-0:9.4.0", + "Name": "demand_register_5_apparent_energy_import" + }, + { + "OBIS": "1-0:10.4.0", + "Name": "demand_register_6_apparent_energy_export" + }, + { + "OBIS": "1-0:1.5.0", + "Name": "last_average_demand_register_1_active_energy_import" + }, + { + "OBIS": "1-0:2.5.0", + "Name": "last_average_demand_register_2_active_energy_export" + }, + { + "OBIS": "1-0:3.5.0", + "Name": "last_average_demand_register_3_reactive_energy_import" + }, + { + "OBIS": "1-0:4.5.0", + "Name": "last_average_demand_register_4_reactive_energy_export" + }, + { + "OBIS": "1-0:9.5.0", + "Name": "last_average_demand_register_5_apparent_energy_import" + }, + { + "OBIS": "1-0:10.5.0", + "Name": "last_average_demand_register_6_apparent_energy_export" + }, + { + "OBIS": "0-0:96.7.21", + "Name": "number_of_power_failures_in_any_phase" + }, + { + "OBIS": "1-0:32.33.0", + "Name": "duration_of_last_voltage_sag_in_phase_l1" + }, + { + "OBIS": "1-0:52.33.0", + "Name": "duration_of_last_voltage_sag_in_phase_l2" + }, + { + "OBIS": "1-0:72.33.0", + "Name": "duration_of_last_voltage_sag_in_phase_l3" + }, + { + "OBIS": "1-0:32.34.0", + "Name": "magnitude_of_last_voltage_sag_in_phase_l1" + }, + { + "OBIS": "1-0:52.34.0", + "Name": "magnitude_of_last_voltage_sag_in_phase_l2" + }, + { + "OBIS": "1-0:72.34.0", + "Name": "magnitude_of_last_voltage_sag_in_phase_l3" + }, + { + "OBIS": "1-0:32.37.0", + "Name": "duration_of_last_voltage_swell_in_phase_l1" + }, + { + "OBIS": "1-0:52.37.0", + "Name": "duration_of_last_voltage_swell_in_phase_l2" + }, + { + "OBIS": "1-0:72.37.0", + "Name": "duration_of_last_voltage_swell_in_phase_l3" + }, + { + "OBIS": "1-0:32.38.0", + "Name": "magnitude_of_last_voltage_swell_in_phase_l1" + }, + { + "OBIS": "1-0:52.38.0", + "Name": "magnitude_of_last_voltage_swell_in_phase_l2" + }, + { + "OBIS": "1-0:72.38.0", + "Name": "magnitude_of_last_voltage_swell_in_phase_l3" + } +] +)""""; + +/* + Removed irrelavant readings (missing rate 3-4 related and meter firmware) + { + "OBIS": "1-0:1.8.3", + "Name": "active_energy_import_rate_3" + }, + { + "OBIS": "1-0:1.8.4", + "Name": "active_energy_import_rate_4" + }, + { + "OBIS": "1-0:2.8.3", + "Name": "active_energy_export_rate_3" + }, + { + "OBIS": "1-0:2.8.4", + "Name": "active_energy_export_rate_4" + }, + { + "OBIS": "1-0:3.8.3", + "Name": "reactive_energy_rate_3" + }, + { + "OBIS": "1-0:3.8.4", + "Name": "reactive_energy_rate_4" + }, + { + "OBIS": "1-0:4.8.3", + "Name": "reactive_energy_minusr_rate_3" + }, + { + "OBIS": "1-0:4.8.4", + "Name": "reactive_energy_minusr_rate_4" + }, + { + "OBIS": "1-0:0.2.0", + "Name": "active_firmware_identifier" + }, + { + "OBIS": "1-0:0.2.8", + "Name": "active_firmware_signature" + }, + { + "OBIS": "1-1:0.2.0", + "Name": "active_firmware_identifier_1" + }, + { + "OBIS": "1-1:0.2.8", + "Name": "active_firmware_signature_1" + } + +*/ \ No newline at end of file diff --git a/esp32_p1meter.ino b/esp32_p1meter.ino index 92b5d52..8479858 100644 --- a/esp32_p1meter.ino +++ b/esp32_p1meter.ino @@ -1,297 +1,128 @@ #include #include #include +#include +#include +#include #include "settings.h" +#include "dsmr_map.h" WiFiClient espClient; PubSubClient mqttClient(espClient); +#ifdef EMAIL_DEBUGGING +#include +EMailSender emailSend(EMAIL_ADDRESS, EMAIL_PASSWORD); +#endif + /*********************************** Main Setup ***********************************/ -void setup() -{ - // Initialize pins - pinMode(LED_BUILTIN, OUTPUT); - digitalWrite(LED_BUILTIN, LOW); - Serial.begin(BAUD_RATE); - Serial2.begin(BAUD_RATE, SERIAL_8N1, RXD2, TXD2, true); +void setup() { + // Initialize pins and blink once for setup start + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LOW); + blinkLed(1, 500); -#ifdef DEBUG - Serial.println("Booting - DEBUG mode on"); - blinkLed(2, 500); - delay(500); - blinkLed(2, 2000); - // Blinking 2 times fast and two times slower to indicate DEBUG mode -#endif - WiFi.mode(WIFI_STA); - WiFi.begin(WIFI_SSID, WIFI_PASS); - while (WiFi.waitForConnectResult() != WL_CONNECTED) - { -#ifdef DEBUG - Serial.println("Connection Failed! Rebooting..."); -#endif - delay(5000); - ESP.restart(); - } - delay(3000); - setupDataReadout(); - setupOTA(); + Serial.begin(BAUD_RATE); - mqttClient.setServer(MQTT_HOST, atoi(MQTT_PORT)); - blinkLed(5, 500); // Blink 5 times to indicate end of setup #ifdef DEBUG - Serial.println("Ready"); - Serial.print("IP address: "); - Serial.println(WiFi.localIP()); + // Blinking 2 times long to indicate DEBUG mode + debug("Booting - DEBUG mode on"); + blinkLed(2, 2000); #endif -} -/*********************************** - Main Loop - ***********************************/ -void loop() -{ - long now = millis(); - if (WiFi.status() != WL_CONNECTED) - { - blinkLed(20, 50); // Blink fast to indicate failed WiFi connection - WiFi.begin(WIFI_SSID, WIFI_PASS); - while (WiFi.waitForConnectResult() != WL_CONNECTED) - { -#ifdef DEBUG - Serial.println("Connection Failed! Rebooting..."); -#endif - delay(5000); - ESP.restart(); - } - } + makeSureWiFiConnected(true); - ArduinoOTA.handle(); + MDNS.begin(String(HOSTNAME).c_str()); + delay(1000); - if (!mqttClient.connected()) - { - if (now - LAST_RECONNECT_ATTEMPT > 5000) - { - LAST_RECONNECT_ATTEMPT = now; + setupDataReadout(); + setupOTA(); - if (!mqttReconnect()) - { -#ifdef DEBUG - Serial.println("Connection to MQTT Failed! Rebooting..."); + mqttClient.setServer(MQTT_HOST, atoi(MQTT_PORT)); + makeSureMqttConnected(); + +#ifndef TEST + Serial2.begin(BAUD_RATE, SERIAL_8N1, RXD2, TXD2, true); #endif - delay(5000); - ESP.restart(); - } - else - { - LAST_RECONNECT_ATTEMPT = 0; - } - } - } - else - { - mqttClient.loop(); - } - // Check if we want a full update of all the data including the unchanged data. - if (now - LAST_FULL_UPDATE_SENT > UPDATE_FULL_INTERVAL) - { - for (int i = 0; i < NUMBER_OF_READOUTS; i++) - { - telegramObjects[i].sendData = true; - LAST_FULL_UPDATE_SENT = millis(); - } - } + debug("System initialised successfully!"); - if (now - LAST_UPDATE_SENT > UPDATE_INTERVAL) - { - if (readP1Serial()) - { - LAST_UPDATE_SENT = millis(); - sendDataToBroker(); - } - } + blinkLed(5, 500); // Blink 5 times to indicate end of setup } /*********************************** - Setup Methods + Main Loop ***********************************/ +void loop() { + long now = millis(); -/** - setupDataReadout() - - This method can be used to create more data readout to mqtt topic. - Use the name for the mqtt topic. - The code for finding this in the telegram see - https://www.netbeheernederland.nl/_upload/Files/Slimme_meter_15_a727fce1f1.pdf for the dutch codes pag. 19 -23 - Use startChar and endChar for setting the boundies where the value is in between. - Default startChar and endChar is '(' and ')' - Note: Make sure when you add or remove telegramObject to update the NUMBER_OF_READOUTS accordingly. -*/ -void setupDataReadout() -{ - // 1-0:1.8.1(000992.992*kWh) - // 1-0:1.8.1 = Elektra verbruik laag tarief (DSMR v5.0) - telegramObjects[0].name = "consumption_tarif_1"; - strcpy(telegramObjects[0].code, "1-0:1.8.1"); - telegramObjects[0].endChar = '*'; - - // 1-0:1.8.2(000560.157*kWh) - // 1-0:1.8.2 = Elektra verbruik hoog tarief (DSMR v5.0) - telegramObjects[1].name = "consumption_tarif_2"; - strcpy(telegramObjects[1].code, "1-0:1.8.2"); - telegramObjects[1].endChar = '*'; - - telegramObjects[0].name = "received_tarif_1"; - strcpy(telegramObjects[0].code, "1-0:2.8.1"); - telegramObjects[0].endChar = '*'; - - // 1-0:1.8.2(000560.157*kWh) - // 1-0:1.8.2 = Elektra verbruik hoog tarief (DSMR v5.0) - telegramObjects[1].name = "received_tarif_2"; - strcpy(telegramObjects[1].code, "1-0:2.8.2"); - telegramObjects[1].endChar = '*'; - - // 1-0:1.7.0(00.424*kW) Actueel verbruik - // 1-0:1.7.x = Electricity consumption actual usage (DSMR v5.0) - telegramObjects[2].name = "actual_consumption"; - strcpy(telegramObjects[2].code, "1-0:1.7.0"); - telegramObjects[2].endChar = '*'; - - // 1-0:2.7.0(00.000*kW) Actuele teruglevering (-P) in 1 Watt resolution - telegramObjects[3].name = "actual_received"; - strcpy(telegramObjects[3].code, "1-0:2.7.0"); - telegramObjects[3].endChar = '*'; - - // 1-0:21.7.0(00.378*kW) - // 1-0:21.7.0 = Instantaan vermogen Elektriciteit levering L1 - telegramObjects[4].name = "instant_power_usage_l1"; - strcpy(telegramObjects[4].code, "1-0:21.7.0"); - telegramObjects[4].endChar = '*'; - - // 1-0:41.7.0(00.378*kW) - // 1-0:41.7.0 = Instantaan vermogen Elektriciteit levering L2 - telegramObjects[5].name = "instant_power_usage_l2"; - strcpy(telegramObjects[5].code, "1-0:41.7.0"); - telegramObjects[5].endChar = '*'; - - // 1-0:61.7.0(00.378*kW) - // 1-0:61.7.0 = Instantaan vermogen Elektriciteit levering L3 - telegramObjects[6].name = "instant_power_usage_l3"; - strcpy(telegramObjects[6].code, "1-0:61.7.0"); - telegramObjects[6].endChar = '*'; - - // 1-0:22.7.0(00.378*kW) - // 1-0:22.7.0 = Instantaan vermogen Elektriciteit teruglevering L1 - telegramObjects[4].name = "instant_power_return_l1"; - strcpy(telegramObjects[4].code, "1-0:22.7.0"); - telegramObjects[4].endChar = '*'; - - // 1-0:42.7.0(00.378*kW) - // 1-0:42.7.0 = Instantaan vermogen Elektriciteit teruglevering L2 - telegramObjects[5].name = "instant_power_return_l2"; - strcpy(telegramObjects[5].code, "1-0:42.7.0"); - telegramObjects[5].endChar = '*'; - - // 1-0:62.7.0(00.378*kW) - // 1-0:62.7.0 = Instantaan vermogen Elektriciteit teruglevering L3 - telegramObjects[6].name = "instant_power_return_l3"; - strcpy(telegramObjects[6].code, "1-0:62.7.0"); - telegramObjects[6].endChar = '*'; - - // 1-0:31.7.0(002*A) - // 1-0:31.7.0 = Instantane stroom Elektriciteit L1 - telegramObjects[7].name = "instant_power_current_l1"; - strcpy(telegramObjects[7].code, "1-0:31.7.0"); - telegramObjects[7].endChar = '*'; - - // 1-0:51.7.0(002*A) - // 1-0:51.7.0 = Instantane stroom Elektriciteit L2 - telegramObjects[8].name = "instant_power_current_l2"; - strcpy(telegramObjects[8].code, "1-0:51.7.0"); - telegramObjects[8].endChar = '*'; + makeSureWiFiConnected(false); - // 1-0:71.7.0(002*A) - // 1-0:71.7.0 = Instantane stroom Elektriciteit L3 - telegramObjects[9].name = "instant_power_current_l3"; - strcpy(telegramObjects[9].code, "1-0:71.7.0"); - telegramObjects[9].endChar = '*'; + ArduinoOTA.handle(); - // 1-0:32.7.0(232.0*V) - // 1-0:32.7.0 = Voltage L1 - telegramObjects[10].name = "instant_voltage_l1"; - strcpy(telegramObjects[10].code, "1-0:32.7.0"); - telegramObjects[10].endChar = '*'; - - // 1-0:52.7.0(232.0*V) - // 1-0:52.7.0 = Voltage L2 - telegramObjects[11].name = "instant_voltage_l2"; - strcpy(telegramObjects[11].code, "1-0:52.7.0"); - telegramObjects[11].endChar = '*'; - - // 1-0:72.7.0(232.0*V) - // 1-0:72.7.0 = Voltage L3 - telegramObjects[12].name = "instant_voltage_l3"; - strcpy(telegramObjects[12].code, "1-0:72.7.0"); - telegramObjects[12].endChar = '*'; - - // 0-0:96.14.0(0001) - // 0-0:96.14.0 = Actual Tarif - telegramObjects[13].name = "actual_tarif_group"; - strcpy(telegramObjects[13].code, "0-0:96.14.0"); - - // 0-1:24.2.3(150531200000S)(00811.923*m3) - // 0-1:24.2.3 = Gas (DSMR v5.0) on Belgian meters - telegramObjects[18].name = "gas_meter_m3"; - strcpy(telegramObjects[18].code, "0-1:24.2.3"); - telegramObjects[12].endChar = '*'; - -#ifdef DEBUG - Serial.println("MQTT Topics initialized:"); - for (int i = 0; i < NUMBER_OF_READOUTS; i++) - { - Serial.println(String(MQTT_ROOT_TOPIC) + "/" + telegramObjects[i].name); + makeSureMqttConnected(); + mqttClient.loop(); + + // Check if we want a full update of all the data including the unchanged data. + if (now - LAST_FULL_UPDATE_SENT > UPDATE_FULL_INTERVAL) { + for (int i = 0; i < telegramObjects.size(); i++) { + telegramObjects[i].sendData = true; + LAST_FULL_UPDATE_SENT = millis(); } + } + + if (now - LAST_UPDATE_SENT > UPDATE_INTERVAL) { +#ifdef TEST + if (readTestSerial()) { +#else + if (readP1Serial()) { #endif + LAST_UPDATE_SENT = millis(); + sendDataToBroker(); + } + } } +/*********************************** + Setup Methods + ***********************************/ /** Over the Air update setup */ -void setupOTA() -{ - ArduinoOTA - .onStart([]() { - String type; - if (ArduinoOTA.getCommand() == U_FLASH) +void setupOTA() { + ArduinoOTA.onStart([]() { + String type; + if (ArduinoOTA.getCommand() == U_FLASH) type = "sketch"; - else // U_SPIFFS + else // U_SPIFFS type = "filesystem"; - // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() - Serial.println("Start updating " + type); - }) - .onEnd([]() { - Serial.println("\nEnd"); - }) - .onProgress([](unsigned int progress, unsigned int total) { - Serial.printf("Progress: %u%%\r", (progress / (total / 100))); - }) - .onError([](ota_error_t error) { - 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(); + // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() + Serial.println("Start updating " + type); + }) + .onEnd([]() { + Serial.println("\nEnd"); + }) + .onProgress([](unsigned int progress, unsigned int total) { + Serial.printf("Progress: %u%%\r", (progress / (total / 100))); + }) + .onError([](ota_error_t error) { + 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.setHostname(String(HOSTNAME).c_str()); + ArduinoOTA.setPasswordHash(String(OTA_PASSWORD_HASH).c_str()); + ArduinoOTA.begin(); } diff --git a/mqtt.ino b/mqtt.ino index dbdec83..337b7c6 100644 --- a/mqtt.ino +++ b/mqtt.ino @@ -1,63 +1,45 @@ -void sendMQTTMessage(const char *topic, char *payload) -{ - bool result = mqttClient.publish(topic, payload, false); +void sendMQTTMessage(const char *topic, const char *payload) { + bool result = mqttClient.publish(topic, payload, false); } -bool mqttReconnect() -{ - int MQTT_RECONNECT_RETRIES = 0; +void makeSureMqttConnected() { + uint _retries = MQTT_MAX_RECONNECT_TRIES; - while (!mqttClient.connected() && MQTT_RECONNECT_RETRIES < MQTT_MAX_RECONNECT_TRIES) - { - MQTT_RECONNECT_RETRIES++; - - if (mqttClient.connect(HOSTNAME, MQTT_USER, MQTT_PASS)) - { - char *message = new char[16 + strlen(HOSTNAME) + 1]; - strcpy(message, "p1 meter alive: "); - strcat(message, HOSTNAME); - mqttClient.publish("hass/status", message); - } - else - { - delay(5000); - } + while (!mqttClient.connected() && _retries > 0) { + if (mqttClient.connect(String(HOSTNAME).c_str(), MQTT_USER, MQTT_PASS)) { + char *message = new char[16 + strlen(HOSTNAME) + 1]; + strcpy(message, "p1 meter alive: "); + strcat(message, HOSTNAME); + mqttClient.publish(String(MQTT_STATUS_TOPIC).c_str(), message); } - - if (MQTT_RECONNECT_RETRIES >= MQTT_MAX_RECONNECT_TRIES) - { - return false; + else { + debug("MQTT Connection Failed! Retries left: " + String(_retries)); + _retries--; + delay(5000); } + } - return true; + if (!mqttClient.connected()) { + blinkLed(20, 200); // Blink moderately fast to indicate failed connection + debug ("Connection to MQTT Failed! Rebooting..."); + ESP.restart(); + } } -void sendMetric(String name, long metric) -{ - //if (metric > 0) - //{ - char output[10]; - ltoa(metric, output, sizeof(output)); - - String topic = String(MQTT_ROOT_TOPIC) + "/" + name; +void sendMetric(String name, String metric) { + String topic = String(MQTT_ROOT_TOPIC) + "/" + name; #ifdef DEBUG - Serial.println(topic); + Serial.println(topic); #endif - sendMQTTMessage(topic.c_str(), output); - //} + sendMQTTMessage(topic.c_str(), metric.c_str()); } -void sendDataToBroker() -{ - for (int i = 0; i < NUMBER_OF_READOUTS; i++) - { -#ifdef DEBUG - Serial.println((String) "Sending: " + telegramObjects[i].name + " value: " + telegramObjects[i].value); -#endif - if (telegramObjects[i].sendData) - { - sendMetric(telegramObjects[i].name, telegramObjects[i].value); - telegramObjects[i].sendData = false; - } +void sendDataToBroker() { + for (int i = 0; i < telegramObjects.size(); i++) { + if (telegramObjects[i].sendData) { + debug("Sending: " + telegramObjects[i].name + " value: " + telegramObjects[i].value); + sendMetric(telegramObjects[i].name, telegramObjects[i].value); + telegramObjects[i].sendData = false; } + } } diff --git a/read_p1.ino b/read_p1.ino index 0abd0a5..7b8d286 100644 --- a/read_p1.ino +++ b/read_p1.ino @@ -1,170 +1,143 @@ -unsigned int crc16(unsigned int crc, unsigned char *buf, int len) -{ - for (int pos = 0; pos < len; pos++) - { - crc ^= (unsigned int)buf[pos]; - - for (int i = 8; i != 0; i--) - { - if ((crc & 0x0001) != 0) - { - crc >>= 1; - crc ^= 0xA001; - } - else - { - crc >>= 1; - } - } +unsigned int crc16(unsigned int crc, unsigned char *buf, int len) { + for (int pos = 0; pos < len; pos++) { + crc ^= (unsigned int)buf[pos]; + + for (int i = 8; i != 0; i--) { + if ((crc & 0x0001) != 0) { + crc >>= 1; + crc ^= 0xA001; + } else { + crc >>= 1; + } } - return crc; + } + return crc; } -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; - } +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; + } + return true; } -int findCharInArrayRev(char array[], char c, int len) -{ - for (int i = len - 1; i >= 0; i--) - { - if (array[i] == c) - { - return i; - } +int findCharInArrayRev(char array[], char c, int len) { + for (int i = len - 1; i >= 0; i--) { + if (array[i] == c) { + return i; } + } - return -1; + return -1; } -long getValue(char *buffer, int maxlen, char startchar, char endchar) -{ - int s = findCharInArrayRev(buffer, startchar, maxlen - 2); - int l = findCharInArrayRev(buffer, endchar, maxlen - 2) - s - 1; - - char res[16]; - memset(res, 0, sizeof(res)); - - if (strncpy(res, buffer + s + 1, l)) - { - if (endchar == '*') - { - if (isNumber(res, l)) - return (1000 * atof(res)); - } - else if (endchar == ')') - { - if (isNumber(res, l)) - return atof(res); - } - } +String getValue(char *buffer, int maxlen, char startchar, char endchar) { + int s = findCharInArrayRev(buffer, startchar, maxlen - 2); + int l = findCharInArrayRev(buffer, endchar, maxlen - 2) - s - 1; + + if (l < 0) // end char not found + return String(NA); + + char res[16]; + memset(res, 0, sizeof(res)); + + if (strncpy(res, buffer + s + 1, l)) { + if (isNumber(res, l)) + return String((endchar == VALUE_END_CHAR ? VALUE_NUMERIC_MULTIPLIER : 1) * atof(res)); + else + return String(res); + } - return 0; + return ""; } /** * Decodes the telegram PER line. Not the complete message. */ -bool decodeTelegram(int len) -{ - int startChar = findCharInArrayRev(telegram, '/', len); - int endChar = findCharInArrayRev(telegram, '!', len); - bool validCRCFound = false; +bool decodeTelegram(int len) { + int startChar = findCharInArrayRev(telegram, '/', len); + int endChar = findCharInArrayRev(telegram, '!', len); + bool validCRCFound = false; #ifdef DEBUG - for (int cnt = 0; cnt < len; cnt++) - { - Serial.print(telegram[cnt]); - } - Serial.print("\n"); + for (int cnt = 0; cnt < len; cnt++) { + Serial.print(telegram[cnt]); + } + Serial.println(""); #endif - if (startChar >= 0) - { - // * Start found. Reset CRC calculation - currentCRC = crc16(0x0000, (unsigned char *)telegram + startChar, len - startChar); - } - else if (endChar >= 0) - { - // * Add to crc calc - currentCRC = crc16(currentCRC, (unsigned char *)telegram + endChar, 1); + if (startChar >= 0) { + // * Start found. Reset CRC calculation + currentCRC = crc16(0x0000, (unsigned char *)telegram + startChar, len - startChar); + } 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); + char messageCRC[5]; + strncpy(messageCRC, telegram + endChar + 1, 4); - messageCRC[4] = 0; // * Thanks to HarmOtten (issue 5) - validCRCFound = (strtol(messageCRC, NULL, 16) == currentCRC); + messageCRC[4] = 0; // * Thanks to HarmOtten (issue 5) + validCRCFound = (strtol(messageCRC, NULL, 16) == currentCRC); #ifdef DEBUG - if (validCRCFound) - Serial.println(F("CRC Valid!")); - else - Serial.println(F("CRC Invalid!")); -#endif - currentCRC = 0; - } + if (validCRCFound) + Serial.println(F("CRC Valid!")); else - { - currentCRC = crc16(currentCRC, (unsigned char *)telegram, len); - } - - // Loops throug all the telegramObjects to find the code in the telegram line - // If it finds the code the value will be stored in the object so it can later be send to the mqtt broker - for (int i = 0; i < NUMBER_OF_READOUTS; i++) - { - if (strncmp(telegram, telegramObjects[i].code, strlen(telegramObjects[i].code)) == 0) - { - long newValue = getValue(telegram, len, telegramObjects[i].startChar, telegramObjects[i].endChar); - if (newValue != telegramObjects[i].value) - { - telegramObjects[i].value = newValue; - telegramObjects[i].sendData = true; - } - break; - + Serial.println(F("CRC Invalid!")); +#endif + currentCRC = 0; + } else { + currentCRC = crc16(currentCRC, (unsigned char *)telegram, len); + } + + // Loops throug all the telegramObjects to find the code in the telegram line + // If it finds the code the value will be stored in the object so it can later be send to the mqtt broker + for (int i = 0; i < telegramObjects.size(); i++) { + if (strncmp(telegram, telegramObjects[i].code, strlen(telegramObjects[i].code)) == 0) { + String newValue = getValue(telegram, len, VALUE_START_CHAR, VALUE_END_CHAR); + if (newValue == String(NA)) { + newValue = getValue(telegram, len, VALUE_START_CHAR, VALUE_NO_UNITS_END_CHAR); + } + if (newValue != telegramObjects[i].value) { + telegramObjects[i].value = newValue; + telegramObjects[i].sendData = true; + } #ifdef DEBUG - Serial.println((String) "Found a Telegram object: " + telegramObjects[i].name + " value: " + telegramObjects[i].value); + Serial.println((String) "Found a Telegram object: " + telegramObjects[i].name + " value: " + telegramObjects[i].value); #endif - } + break; } + } - return validCRCFound; + return validCRCFound; } -bool readP1Serial() -{ - if (Serial2.available()) - { +bool readP1Serial() { + if (Serial2.available()) { #ifdef DEBUG - Serial.println("Serial2 is available"); - Serial.println("Memset telegram"); + Serial.println("Serial2 is available"); + Serial.println("Memset telegram"); #endif - memset(telegram, 0, sizeof(telegram)); - while (Serial2.available()) - { - // Reads the telegram untill it finds a return character - // That is after each line in the telegram - int len = Serial2.readBytesUntil('\n', telegram, P1_MAXLINELENGTH); - - telegram[len] = '\n'; - telegram[len + 1] = 0; - - bool result = decodeTelegram(len + 1); - // When the CRC is check which is also the end of the telegram - // if valid decode return true - if (result) - { - return true; - } - } + memset(telegram, 0, sizeof(telegram)); + while (Serial2.available()) { + // Reads the telegram until it finds a return character + // That is after each line in the telegram + int len = Serial2.readBytesUntil('\n', telegram, P1_MAXLINELENGTH); + + telegram[len] = '\n'; + telegram[len + 1] = 0; + + bool result = decodeTelegram(len + 1); + // When the CRC is check which is also the end of the telegram + // if valid decode return true + if (result) { + return true; + } } - return false; + } + return false; } + + diff --git a/read_test.ino b/read_test.ino new file mode 100644 index 0000000..ba39ff1 --- /dev/null +++ b/read_test.ino @@ -0,0 +1,132 @@ +#ifdef TEST +String testTelegram = R""""( +/ESO5\253880853_A +0-0:1.0.0(231030174109W) +1-0:1.8.0(00002168.313*kWh) +1-0:3.8.0(00000135.946*kvarh) +1-0:4.8.0(00001457.806*kvarh) +1-0:1.8.1(00000901.998*kWh) +1-0:1.8.2(00001266.315*kWh) +1-0:1.8.3(00000000.000*kWh) +1-0:1.8.4(00000000.000*kWh) +1-0:2.8.1(00005240.589*kWh) +1-0:2.8.2(00002078.629*kWh) +1-0:2.8.3(00000000.000*kWh) +1-0:2.8.4(00000000.000*kWh) +1-0:3.8.1(00000069.426*kvarh) +1-0:3.8.2(00000066.520*kvarh) +1-0:3.8.3(00000000.000*kvarh) +1-0:3.8.4(00000000.000*kvarh) +1-0:4.8.1(00000655.658*kvarh) +1-0:4.8.2(00000802.148*kvarh) +1-0:4.8.3(00000000.000*kvarh) +1-0:4.8.4(00000000.000*kvarh) +1-0:32.7.0(0000.230*kV) +1-0:32.24.0(0000.229*kV) +1-0:31.7.0(0000.000*kA) +1-0:31.4.0(0000.000*kA) +1-0:52.7.0(0000.227*kV) +1-0:52.24.0(0000.226*kV) +1-0:51.7.0(0000.000*kA) +1-0:51.4.0(0000.000*kA) +1-0:72.7.0(0000.228*kV) +1-0:72.24.0(0000.227*kV) +1-0:71.7.0(0000.003*kA) +1-0:71.4.0(0000.000*kA) +1-0:12.7.0(0000.230*kV) +1-0:11.7.0(0000.003*kA) +1-0:91.7.0(0000.000*kA) +1-0:90.7.0(0000.003*kA) +1-0:14.7.0(0000.050*kHz) +1-0:15.7.0(00000000.742*kW) +1-0:21.7.0(00000000.020*kW) +1-0:41.7.0(00000000.142*kW) +1-0:61.7.0(00000000.579*kW) +1-0:22.7.0(00000000.000*kW) +1-0:42.7.0(00000000.000*kW) +1-0:62.7.0(00000000.000*kW) +1-0:23.7.0(00000000.000*kvar) +1-0:43.7.0(00000000.000*kvar) +1-0:24.7.0(00000000.013*kvar) +1-0:44.7.0(00000000.053*kvar) +1-0:64.7.0(00000000.250*kvar) +1-0:9.7.0(00000000.890*kVA) +1-0:29.7.0(00000000.024*kVA) +1-0:49.7.0(00000000.166*kVA) +1-0:10.7.0(00000000.000*kVA) +1-0:30.7.0(00000000.000*kVA) +1-0:50.7.0(00000000.000*kVA) +1-0:70.7.0(00000000.000*kVA) +1-0:1.24.0(00000000.090*kW) +1-0:16.24.0(00000000.008*kW) +1-0:15.24.0(00000000.733*kW) +1-0:13.7.0(00919) +1-0:33.7.0(00667) +1-0:53.7.0(00857) +1-0:73.7.0(00827) +1-0:13.3.0(00000) +1-0:0.8.2(00900*s) +1-0:1.4.0(00000000.586*kW) +1-0:2.4.0(00000000.000*kW) +1-0:3.4.0(00000000.000*kvar) +1-0:4.4.0(00000000.235*kvar) +1-0:9.4.0(00000000.696*kVA) +1-0:10.4.0(00000000.000*kVA) +1-0:1.5.0(00000000.678*kW) +1-0:2.5.0(00000000.000*kW) +1-0:3.5.0(00000000.005*kvar) +1-0:4.5.0(00000000.335*kvar) +1-0:9.5.0(00000000.909*kVA) +1-0:10.5.0(00000000.000*kVA) +0-0:96.7.21(00032) +1-0:32.33.0(04880*s) +1-0:52.33.0(04880*s) +1-0:72.33.0(04880*s) +1-0:32.34.0(00000*V) +1-0:52.34.0(00000*V) +1-0:72.34.0(00000*V) +1-0:32.37.0(00000*s) +1-0:52.37.0(00000*s) +1-0:72.37.0(00000*s) +1-0:32.38.0(00000*V) +1-0:52.38.0(00000*V) +1-0:72.38.0(00000*V) +1-0:0.2.0(02.21) +1-0:0.2.8(1EA43311) +1-1:0.2.0(01.97) +1-1:0.2.8(68860C25) +!C471 +)""""; + +bool readTestSerial() { + int _index = 1; + while (true) { + String _line = getTestLine(testTelegram, '\n', _index); + if (_line.isEmpty()) { + return true; + } + int _len = _line.length(); + strcpy(telegram, _line.c_str()); + telegram[_len] = '\n'; + telegram[_len + 1] = 0; + + bool result = decodeTelegram(_len + 2); + _index++; + } +} + +String getTestLine(String data, char separator, int index) { + int _found = 0; + int _strIndex[] = { + 0, -1 }; + int _maxIndex = data.length()-1; + for(int i=0; i<=_maxIndex && _found<=index; i++){ + if(data.charAt(i) == separator || i == _maxIndex) { + _found++; + _strIndex[0] = _strIndex[1]+1; + _strIndex[1] = (i == _maxIndex) ? i+1 : i; + } + } + return _found > index ? data.substring(_strIndex[0], _strIndex[1]) : ""; +} +#endif \ No newline at end of file diff --git a/readout.ino b/readout.ino new file mode 100644 index 0000000..cf8f63a --- /dev/null +++ b/readout.ino @@ -0,0 +1,32 @@ +/** + setupDataReadout() + + This method reads JSON string specified in DsmrMap variable in dsmr_map.h file + and converts to telegramDecodedObjects MQTT vector. +*/ +void setupDataReadout() { + DynamicJsonDocument _dsmrMapDocument(DYNAMIC_JSON_DOCUMENT_SIZE); + DeserializationError _error = deserializeJson(_dsmrMapDocument, DSMR_MAP); + if (_error) { + Serial.print("deserializeJson() failed: "); + Serial.println(_error.c_str()); + return; + } + + // Create TelegramObject vector which will be used to send MQTT messages + const uint _count = _dsmrMapDocument.as().size(); + for (int i = 0; i < _count; i++) { + TelegramDecodedObject _tdo; + strcpy(_tdo.code, _dsmrMapDocument.as()[i]["OBIS"]); + _tdo.name = _dsmrMapDocument.as()[i]["Name"].as(); + + telegramObjects.push_back(_tdo); + } + +#ifdef DEBUG + Serial.println("MQTT Topics initialized (" + String(_count) + "):"); + for (int i = 0; i < _count; i++) { + Serial.println(String(MQTT_ROOT_TOPIC) + "/" + telegramObjects[i].name); + } +#endif +} \ No newline at end of file diff --git a/settings.h b/settings.h index bc497f3..36a110e 100644 --- a/settings.h +++ b/settings.h @@ -1,7 +1,13 @@ -#define DEBUG +// Uncomment for debugging +//#define DEBUG +// Uncomment for debug messages sent to gmail +//#define EMAIL_DEBUGGING +// Uncomment for testing +//#define TEST + // Update treshold in milliseconds, messages will only be sent on this interval -#define UPDATE_INTERVAL 1000 // 1 second -//#define UPDATE_INTERVAL 10000 // 10 seconds +//#define UPDATE_INTERVAL 1000 // 1 second +#define UPDATE_INTERVAL 30000 // 30 seconds //#define UPDATE_INTERVAL 60000 // 1 minute //#define UPDATE_INTERVAL 300000 // 5 minutes @@ -11,43 +17,69 @@ // #define UPDATE_FULL_INTERVAL 1800000 // 30 minutes // #define UPDATE_FULL_INTERVAL 3600000 // 1 Hour -#define HOSTNAME "p1meter" -#define OTA_PASSWORD "admin" +#define NA "N/A" + +#define HOSTNAME "p1_meter" +#define OTA_PASSWORD_HASH "MD5 hash of my password" #define BAUD_RATE 115200 #define RXD2 16 #define TXD2 17 + +//default +//#define LED_PIN LED_BUILTIN +#define LED_PIN 32 + #define P1_MAXLINELENGTH 1050 -#define MQTT_MAX_RECONNECT_TRIES 100 -#define MQTT_ROOT_TOPIC "sensors/power/p1meter" +// To calculate this use https://arduinojson.org/v6/assistant/ +// This is for 84 readouts with some slack +#define DYNAMIC_JSON_DOCUMENT_SIZE 12288 -#define NUMBER_OF_READOUTS 19 +#define MQTT_MAX_RECONNECT_TRIES 10 +#define MQTT_ROOT_TOPIC "p1_meter/sensor" +#define MQTT_STATUS_TOPIC "p1_meter/status" -long LAST_RECONNECT_ATTEMPT = 0; -long LAST_UPDATE_SENT = 0; -long LAST_FULL_UPDATE_SENT = 0; +#define WIFI_MAX_RECONNECT_TRIES 5 + +char WIFI_SSID[32] = "MyWifi"; +char WIFI_PASS[32] = "MyWifiPassword"; + +char MQTT_HOST[64] = "homeassistant.local"; +char MQTT_PORT[6] = "1883"; +char MQTT_USER[32] = "mqtt"; +char MQTT_PASS[32] = "mqttpass"; -char WIFI_SSID[32] = ""; -char WIFI_PASS[32] = ""; +#ifdef EMAIL_DEBUGGING +char EMAIL_ADDRESS[32] = "name.surname@gmail.com"; +char EMAIL_PASSWORD[32] = "mygmailappkey"; +String emailMessageDump; +#endif -char MQTT_HOST[64] = ""; -char MQTT_PORT[6] = ""; -char MQTT_USER[32] = ""; -char MQTT_PASS[32] = ""; +// if your P1 values with units need to be multiplied by any multiplier, specify it here +long VALUE_NUMERIC_MULTIPLIER = 1; +// Nothing to change below - globals to make it all work as simple as possible char telegram[P1_MAXLINELENGTH]; +long LAST_UPDATE_SENT = 0; +long LAST_FULL_UPDATE_SENT = 0; + +const char VALUE_START_CHAR = '('; +const char VALUE_END_CHAR = '*'; +const char VALUE_NO_UNITS_END_CHAR = ')'; + struct TelegramDecodedObject { String name; - long value; + String value; char code[16]; - char startChar = '('; - char endChar = ')'; bool sendData = true; }; -struct TelegramDecodedObject telegramObjects[NUMBER_OF_READOUTS]; +tc::vector telegramObjects; unsigned int currentCRC = 0; + + + diff --git a/telegram b/telegram index 4dc1baa..db2c186 100644 --- a/telegram +++ b/telegram @@ -1,33 +1,96 @@ -/FLU5\253769484_A -0-0:96.1.4(50215) -0-0:96.1.1() -0-0:1.0.0(210410103019S) -1-0:1.8.1(001869.223*kWh) -1-0:1.8.2(002598.088*kWh) -1-0:2.8.1(000535.014*kWh) -1-0:2.8.2(000175.049*kWh) -0-0:96.14.0(0002) -1-0:1.7.0(00.052*kW) -1-0:2.7.0(00.000*kW) -1-0:21.7.0(00.000*kW) -1-0:41.7.0(00.000*kW) -1-0:61.7.0(00.081*kW) -1-0:22.7.0(00.004*kW) -1-0:42.7.0(00.023*kW) -1-0:62.7.0(00.000*kW) -1-0:32.7.0(237.8*V) -1-0:52.7.0(238.1*V) -1-0:72.7.0(241.1*V) -1-0:31.7.0(000.74*A) -1-0:51.7.0(000.52*A) -1-0:71.7.0(000.69*A) -0-0:96.3.10(1) -0-0:17.0.0(999.9*kW) -1-0:31.4.0(999*A) -0-0:96.13.0() -0-1:24.1.0(003) -0-1:96.1.1() -0-1:24.4.0(1) -0-1:24.2.3(210410102502S)(00012.445*m3) -!1CCE - +/ESO5\253880853_A +0-0:1.0.0(231030174109W) +1-0:1.8.0(00002168.313*kWh) +1-0:3.8.0(00000135.946*kvarh) +1-0:4.8.0(00001457.806*kvarh) +1-0:1.8.1(00000901.998*kWh) +1-0:1.8.2(00001266.315*kWh) +1-0:1.8.3(00000000.000*kWh) +1-0:1.8.4(00000000.000*kWh) +1-0:2.8.1(00005240.589*kWh) +1-0:2.8.2(00002078.629*kWh) +1-0:2.8.3(00000000.000*kWh) +1-0:2.8.4(00000000.000*kWh) +1-0:3.8.1(00000069.426*kvarh) +1-0:3.8.2(00000066.520*kvarh) +1-0:3.8.3(00000000.000*kvarh) +1-0:3.8.4(00000000.000*kvarh) +1-0:4.8.1(00000655.658*kvarh) +1-0:4.8.2(00000802.148*kvarh) +1-0:4.8.3(00000000.000*kvarh) +1-0:4.8.4(00000000.000*kvarh) +1-0:32.7.0(0000.230*kV) +1-0:32.24.0(0000.229*kV) +1-0:31.7.0(0000.000*kA) +1-0:31.4.0(0000.000*kA) +1-0:52.7.0(0000.227*kV) +1-0:52.24.0(0000.226*kV) +1-0:51.7.0(0000.000*kA) +1-0:51.4.0(0000.000*kA) +1-0:72.7.0(0000.228*kV) +1-0:72.24.0(0000.227*kV) +1-0:71.7.0(0000.003*kA) +1-0:71.4.0(0000.000*kA) +1-0:12.7.0(0000.230*kV) +1-0:11.7.0(0000.003*kA) +1-0:91.7.0(0000.000*kA) +1-0:90.7.0(0000.003*kA) +1-0:14.7.0(0000.050*kHz) +1-0:15.7.0(00000000.742*kW) +1-0:21.7.0(00000000.020*kW) +1-0:41.7.0(00000000.142*kW) +1-0:61.7.0(00000000.579*kW) +1-0:22.7.0(00000000.000*kW) +1-0:42.7.0(00000000.000*kW) +1-0:62.7.0(00000000.000*kW) +1-0:23.7.0(00000000.000*kvar) +1-0:43.7.0(00000000.000*kvar) +1-0:24.7.0(00000000.013*kvar) +1-0:44.7.0(00000000.053*kvar) +1-0:64.7.0(00000000.250*kvar) +1-0:9.7.0(00000000.890*kVA) +1-0:29.7.0(00000000.024*kVA) +1-0:49.7.0(00000000.166*kVA) +1-0:10.7.0(00000000.000*kVA) +1-0:30.7.0(00000000.000*kVA) +1-0:50.7.0(00000000.000*kVA) +1-0:70.7.0(00000000.000*kVA) +1-0:1.24.0(00000000.090*kW) +1-0:16.24.0(00000000.008*kW) +1-0:15.24.0(00000000.733*kW) +1-0:13.7.0(00919) +1-0:33.7.0(00667) +1-0:53.7.0(00857) +1-0:73.7.0(00827) +1-0:13.3.0(00000) +1-0:0.8.2(00900*s) +1-0:1.4.0(00000000.586*kW) +1-0:2.4.0(00000000.000*kW) +1-0:3.4.0(00000000.000*kvar) +1-0:4.4.0(00000000.235*kvar) +1-0:9.4.0(00000000.696*kVA) +1-0:10.4.0(00000000.000*kVA) +1-0:1.5.0(00000000.678*kW) +1-0:2.5.0(00000000.000*kW) +1-0:3.5.0(00000000.005*kvar) +1-0:4.5.0(00000000.335*kvar) +1-0:9.5.0(00000000.909*kVA) +1-0:10.5.0(00000000.000*kVA) +0-0:96.7.21(00032) +1-0:32.33.0(04880*s) +1-0:52.33.0(04880*s) +1-0:72.33.0(04880*s) +1-0:32.34.0(00000*V) +1-0:52.34.0(00000*V) +1-0:72.34.0(00000*V) +1-0:32.37.0(00000*s) +1-0:52.37.0(00000*s) +1-0:72.37.0(00000*s) +1-0:32.38.0(00000*V) +1-0:52.38.0(00000*V) +1-0:72.38.0(00000*V) +1-0:0.2.0(02.21) +1-0:0.2.8(1EA43311) +1-1:0.2.0(01.97) +1-1:0.2.8(68860C25) +!C471 \ No newline at end of file diff --git a/utils.ino b/utils.ino index 1998516..e912c6e 100644 --- a/utils.ino +++ b/utils.ino @@ -1,13 +1,77 @@ -void blinkLed(int numberOfBlinks, int msBetweenBlinks) -{ - for (int i = 0; i < numberOfBlinks; i++) - { - digitalWrite(LED_BUILTIN, HIGH); +void blinkLed(int numberOfBlinks, int msBetweenBlinks) { + int msDurationForLow=200; + for (int i = 0; i < numberOfBlinks; i++) { + if (i > 0) { + delay(msDurationForLow); + } + digitalWrite(LED_PIN, HIGH); delay(msBetweenBlinks); - digitalWrite(LED_BUILTIN, LOW); - if (i != numberOfBlinks - 1) - { - delay(msBetweenBlinks); + digitalWrite(LED_PIN, LOW); + } +} + +void makeSureWiFiConnected(bool setupMode) { + if (setupMode) { + WiFi.mode(WIFI_STA); + WiFi.begin(WIFI_SSID, WIFI_PASS); + delay(3000); + } + + if (WiFi.status() != WL_CONNECTED) { + blinkLed(20, 50); // Blink fast to indicate failed WiFi connection + WiFi.begin(WIFI_SSID, WIFI_PASS); + + int _retries = WIFI_MAX_RECONNECT_TRIES; + while (WiFi.waitForConnectResult() != WL_CONNECTED && _retries > 0) { +#ifdef DEBUG + Serial.print("WiFi Connection Failed! Retries left: "); + Serial.println(_retries); +#endif + _retries--; + delay(3000); + } + if (WiFi.status() != WL_CONNECTED) { + blinkLed(10, 50); // Blink fast to indicate failed WiFi connection +#ifdef DEBUG + Serial.println("Connection Failed! Rebooting..."); +#endif + ESP.restart(); } } + if (setupMode) { + debug ("WiFi Ready. IP: " + WiFi.localIP().toString() + ", RSSI: " + String(WiFi.RSSI())); + } +} + +void sendEmailMessage(String subject, String message) { +#ifdef EMAIL_DEBUGGING + EMailSender::EMailMessage _eMailMessage; + _eMailMessage.subject = subject; + _eMailMessage.message = message; + + EMailSender::Response _resp = emailSend.send(EMAIL_ADDRESS, _eMailMessage); + if (!_resp.status) { + Serial.println("Sending status: "); + Serial.println(_resp.status); + Serial.println(_resp.code); + Serial.println(_resp.desc); + } +#endif + ; +} + +void debug(String msg) { +#ifdef DEBUG + Serial.println(msg); + + #ifdef EMAIL_DEBUGGING + emailMessageDump += msg + "\r\n"; + + if (emailMessageDump.length() > 5000) { + sendEmailMessage(String(HOSTNAME) + " message", emailMessageDump); + emailMessageDump = ""; + } + #endif +#endif + ; }