diff --git a/.gitignore b/.gitignore index 89cc49c..9b8093f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .vscode/c_cpp_properties.json .vscode/launch.json .vscode/ipch +**/config*.json \ No newline at end of file diff --git a/Readme.md b/Readme.md index c0d492a..db34d03 100644 --- a/Readme.md +++ b/Readme.md @@ -3,7 +3,7 @@ located in IKEAs Vindriktning after this upgrade it will measure: * air quality -* temperatur +* temperature * pressure * altitude @@ -23,6 +23,7 @@ Upload this new generated filesystem with: Can be found at ```~/.platformio/penv/bin/pio``` # Hardware +## Core ESP8266 version ESP12 was used. The prototype was based on the Witty board @@ -39,20 +40,40 @@ VCC | GND ``` The following pins are used: -* GPIO4 PM1006 particle sensor -* GPIO2 WS2812 stripe out of three LEDs, replacing the orignal LEDs at front +* GPIO4 PM1006 particle sensor PIN REST on Vindriktning board +* GPIO2 WS2812 stripe out of three LEDs, replacing the original LEDs at front * GPIO15 Red LED (optional) -* GPIO12 Green LED (optional) +* GPIO12 Green LED (optional) Used as 3.3V Supply for the I2C sensor * GPIO13 Blue LED (optional) * GPIO13 VCC of I2C (3.3 V) * GPIO14 I2C clock * GPIO5 I2C data pin +* RXD Victron MPPT + +## Victron + +An *ADUM 1201* should be used for galvanic isolation. +The following wiring will be setup: +``` +Ve.Direct | Purpose | connect | ADUM1201 #2 | ADUM1201 #1 | connect | ESP8266 +1 | GND | <-> | GND2 | GND1 | <-> | GND +2 | RX | <-> | VOB | VIB | | +3 | TX | <-> | VIA | VOA | <-> | RX +4 | 5V | <-> | VDD2 | VDD1 | <-> | 3V3 +``` # Bill of materials +## Core * IKEA Vindriktning * ESP8266 (e.g. Witty board) * BMP280 sensor * some wire +## Victron +* ADUM 1201 + # Sources -* [https://github.com/amkuipers/witty Witty pinout] \ No newline at end of file +For the Witty board +* [https://github.com/amkuipers/witty Witty pinout] +* [https://arduino.ua/products_pictures/large_AOC361-5.jpg Schematics] +* [https://github.com/KinDR007/VictronMPPT-ESPHOME/ Victron MPPT ESP8266 Library] \ No newline at end of file diff --git a/host/Readme.md b/host/Readme.md index 5303e87..e94a0ac 100644 --- a/host/Readme.md +++ b/host/Readme.md @@ -27,7 +27,7 @@ usage: ota_updater.py [-h] -l BROKER_HOST -p BROKER_PORT [-u BROKER_USERNAME] [-d BROKER_PASSWORD] [-t BASE_TOPIC] -i DEVICE_ID firmware -ota firmware update scirpt for ESP8226 implemenation of the Homie mqtt IoT +ota firmware update script for ESP8226 implementation of the Homie mqtt IoT convention. positional arguments: diff --git a/host/VictronDummyData.txt b/host/VictronDummyData.txt new file mode 100644 index 0000000..352b51f --- /dev/null +++ b/host/VictronDummyData.txt @@ -0,0 +1,37 @@ +SER# HQ2202K3VD9 +V 26310 +I 0 +VPV 0 +PPV 0 +CS 0 +MPPT 0 +ERR 0 +LOAD ON +IL 0 +H19 0 +H20 0 +H21 0 +H22 0 +H23 0 +HSDS 0 +Checksum f +PID 0xA04C +FW 159 +SER# HQ2202K3VD9 +V 26310 +I 1 +VPV 2 +PPV 3 +CS 4 +MPPT 5 +ERR 6 +LOAD ON +IL 7 +H19 8 +H20 9 +H21 10 +H22 11 +H23 12 +HSDS 13 +Checksum f + diff --git a/host/simulateMPPT75-10.py b/host/simulateMPPT75-10.py new file mode 100755 index 0000000..28f256e --- /dev/null +++ b/host/simulateMPPT75-10.py @@ -0,0 +1,57 @@ +#!/usr/bin/python3 +import serial +import time +import argparse +import re +import random +from termcolor import colored +from time import localtime, strftime, gmtime +from datetime import datetime, timezone + +parser = argparse.ArgumentParser() +parser.add_argument('-d', '--device', help='ttyUSB device. By default, /dev/ttyUSB0 is used.') +args = parser.parse_args() + +if (args.device): + serialDevice=args.device +else: + serialDevice="/dev/ttyUSB0" + +with serial.Serial(serialDevice, 19200, timeout=1) as ser: + print(ser.name) # check which port was really used + + lastUpdate=0 + + # some dummy values + battery_volt=12000 + battery_current = 1 + panel_volt = 13000 + panel_power = 2 + + # Main Loop + while (True): + s=ser.readline() + now=int(time.time()) + updateSerial=((now - lastUpdate) > 2) + + if ((s is not None) and (len(s) > 0)): + print(colored(strftime("%Y-%m-%d %H:%M:%S", gmtime()) + " " + str(s.decode('utf-8', errors='ignore')).rstrip(), "green")) + + if (updateSerial): + lastUpdate=int(time.time()) + # Send the status string + + outStr="FW\t159\nSER#\tHQ2202K3VD9\nV\t{0:d}\nI\t{1:d}\nVPV\t{2:d}\nPPV\t{3:d}\nChecksum f\r\n".format(battery_volt, battery_current, panel_volt, panel_power) + # Other values + # CS\t{4:d}\nMPPT\t{5:d}\nERR\t{6:d}\nLOAD\t{7}\nIL\t{8:d}\nH19\t{9:d}\nH20\t{10:d}\nH21\t{11:d}\nH22\t{12:d}\nH23\t{13:d}\nHSDS\t{14:d}\n + + # write a string + ser.write(outStr.encode(encoding='UTF-8')) + print(colored(outStr.rstrip(), "white")) + time.sleep(0.2) + panel_volt = panel_volt + 1 + panel_power = panel_power + 1 + + ser.close() # close port + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/host/spelling.sh b/host/spelling.sh index ec98d73..fa2ecfd 100755 --- a/host/spelling.sh +++ b/host/spelling.sh @@ -7,5 +7,6 @@ if [ $? -ne 0 ]; then fi codespell -w ../src/* codespell -w ../include/* -codespell ../Readme.md +codespell -w ../*.md +codespell -w *.md exit 0 diff --git a/host/upload-via-mqtt.sh b/host/upload-via-mqtt.sh index 949eda2..5789b13 100755 --- a/host/upload-via-mqtt.sh +++ b/host/upload-via-mqtt.sh @@ -20,6 +20,6 @@ fi echo "Waiting for $homieId ..." mosquitto_sub -h $mqttHost -t "${mqttPrefix}${homieId}/#" -R -C 1 -python ota_updater.py -l $mqttHost -t "$mqttPrefix" -i "$homieId" $firmwareFile +python3 ota_updater.py -l $mqttHost -t "$mqttPrefix" -i "$homieId" $firmwareFile exit 0 diff --git a/include/HomieSettings.h b/include/HomieSettings.h index 3e1667a..5f67333 100644 --- a/include/HomieSettings.h +++ b/include/HomieSettings.h @@ -12,7 +12,21 @@ #ifndef HOMIE_SETTINGS #define HOMIE_SETTINGS -#define HOMIE_FIRMWARE_NAME "Vindriktning" -#define HOMIE_FIRMWARE_VERSION "1.2.1" +#if BME680 +#define FIRMWARE_FEATURE1 "_Bme680" +#else +#define FIRMWARE_FEATURE1 "" +#endif + +#if VICTRON +#define FIRMWARE_FEATURE2 "_Victron" +#else +#define FIRMWARE_FEATURE2 "" +#endif + +#define HOMIE_FIRMWARE_NAME "RoomSensor" FIRMWARE_FEATURE1 FIRMWARE_FEATURE2 +#define HOMIE_FIRMWARE_VERSION "2.4.5" + +#define SERIAL_BAUDRATE 19200 #endif diff --git a/include/MqttLog.h b/include/MqttLog.h new file mode 100644 index 0000000..a4a1cf6 --- /dev/null +++ b/include/MqttLog.h @@ -0,0 +1,31 @@ +/** + * @file MqttLog.h + * @author Ollo + * @brief Wrapper for Logger to Mqtt + * @version 0.1 + * + */ + +#ifndef MQTT_LOGGER +#define MQTT_LOGGER + +#include + +#define LOG_TOPIC "log\0" +#define MQTT_LEVEL_ERROR 1 +#define MQTT_LEVEL_WARNING 10 +#define MQTT_LEVEL_INFO 20 +#define MQTT_LEVEL_DEBUG 90 + +#define MQTT_LOG_PM1006 10 +#define MQTT_LOG_I2CINIT 100 +#define MQTT_LOG_I2READ 101 +#define MQTT_LOG_RGB 200 + +#define MQTT_LOG_VICTRON 400 + +extern bool mConnected; + +void log(int level, String message, int statusCode); + +#endif /* end of MQTT_LOGGER */ \ No newline at end of file diff --git a/include/VictronTexts.h b/include/VictronTexts.h new file mode 100644 index 0000000..47d9b15 --- /dev/null +++ b/include/VictronTexts.h @@ -0,0 +1,399 @@ +#pragma once + + + + std::string tracking_mode_text(int value) { + switch (value) { + case 0: + return "Off"; + case 1: + return "Limited"; + case 2: + return "Active"; + default: + return "Unknown"; + } + } + + std::string error_code_text(int value) { + switch (value) { + case 0: + return "No error"; + case 2: + return "Battery voltage too high"; + case 17: + return "Charger temperature too high"; + case 18: + return "Charger over current"; + case 19: + return "Charger current reversed"; + case 20: + return "Bulk time limit exceeded"; + case 21: + return "Current sensor issue"; + case 26: + return "Terminals overheated"; + case 28: + return "Converter issue"; + case 33: + return "Input voltage too high (solar panel)"; + case 34: + return "Input current too high (solar panel)"; + case 38: + return "Input shutdown (excessive battery voltage)"; + case 39: + return "Input shutdown (due to current flow during off mode)"; + case 65: + return "Lost communication with one of devices"; + case 66: + return "Synchronised charging device configuration issue"; + case 67: + return "BMS connection lost"; + case 68: + return "Network misconfigured"; + case 116: + return "Factory calibration data lost"; + case 117: + return "Invalid/incompatible firmware"; + case 119: + return "User settings invalid"; + default: + return "Unknown"; + } + } + + + + std::string charging_mode_text(int value) { + switch (value) { + case 0: + return "Off"; + case 1: + return "Low power"; + case 2: + return "Fault"; + case 3: + return "Bulk"; + case 4: + return "Absorption"; + case 5: + return "Float"; + case 6: + return "Storage"; + case 7: + return "Equalize (manual)"; + case 9: + return "Inverting"; + case 11: + return "Power supply"; + case 245: + return "Starting-up"; + case 246: + return "Repeated absorption"; + case 247: + return "Auto equalize / Recondition"; + case 248: + return "BatterySafe"; + case 252: + return "External control"; + default: + return "Unknown"; + } + } + + + + std::string device_type_text(long value) + { + switch (value) { + case 0x203: + return "BMV-700"; + case 0x204: + return "BMV-702"; + case 0x205: + return "BMV-700H"; + case 0x0300: + return "BlueSolar MPPT 70|15"; + case 0xA040: + return "BlueSolar MPPT 75|50"; + case 0xA041: + return "BlueSolar MPPT 150|35"; + case 0xA042: + return "BlueSolar MPPT 75|15"; + case 0xA043: + return "BlueSolar MPPT 100|15"; + case 0xA044: + return "BlueSolar MPPT 100|30"; + case 0xA045: + return "BlueSolar MPPT 100|50"; + case 0xA046: + return "BlueSolar MPPT 150|70"; + case 0xA047: + return "BlueSolar MPPT 150|100"; + case 0xA049: + return "BlueSolar MPPT 100|50 rev2"; + case 0xA04A: + return "BlueSolar MPPT 100|30 rev2"; + case 0xA04B: + return "BlueSolar MPPT 150|35 rev2"; + case 0xA04C: + return "BlueSolar MPPT 75|10"; + case 0xA04D: + return "BlueSolar MPPT 150|45"; + case 0xA04E: + return "BlueSolar MPPT 150|60"; + case 0xA04F: + return "BlueSolar MPPT 150|85"; + case 0xA050: + return "SmartSolar MPPT 250|100"; + case 0xA051: + return "SmartSolar MPPT 150|100"; + case 0xA052: + return "SmartSolar MPPT 150|85"; + case 0xA053: + return "SmartSolar MPPT 75|15"; + case 0xA075: + return "SmartSolar MPPT 75|15 rev2"; + case 0xA054: + return "SmartSolar MPPT 75|10"; + case 0xA074: + return "SmartSolar MPPT 75|10 rev2"; + case 0xA055: + return "SmartSolar MPPT 100|15"; + case 0xA056: + return "SmartSolar MPPT 100|30"; + case 0xA073: + return "SmartSolar MPPT 150|45 rev3"; + case 0xA057: + return "SmartSolar MPPT 100|50"; + case 0xA058: + return "SmartSolar MPPT 150|35"; + case 0xA059: + return "SmartSolar MPPT 150|100 rev2"; + case 0xA05A: + return "SmartSolar MPPT 150|85 rev2"; + case 0xA05B: + return "SmartSolar MPPT 250|70"; + case 0xA05C: + return "SmartSolar MPPT 250|85"; + case 0xA05D: + return "SmartSolar MPPT 250|60"; + case 0xA05E: + return "SmartSolar MPPT 250|45"; + case 0xA05F: + return "SmartSolar MPPT 100|20"; + case 0xA060: + return "SmartSolar MPPT 100|20 48V"; + case 0xA061: + return "SmartSolar MPPT 150|45"; + case 0xA062: + return "SmartSolar MPPT 150|60"; + case 0xA063: + return "SmartSolar MPPT 150|70"; + case 0xA064: + return "SmartSolar MPPT 250|85 rev2"; + case 0xA065: + return "SmartSolar MPPT 250|100 rev2"; + case 0xA066: + return "BlueSolar MPPT 100|20"; + case 0xA067: + return "BlueSolar MPPT 100|20 48V"; + case 0xA068: + return "SmartSolar MPPT 250|60 rev2"; + case 0xA069: + return "SmartSolar MPPT 250|70 rev2"; + case 0xA06A: + return "SmartSolar MPPT 150|45 rev2"; + case 0xA06B: + return "SmartSolar MPPT 150|60 rev2"; + case 0xA06C: + return "SmartSolar MPPT 150|70 rev2"; + case 0xA06D: + return "SmartSolar MPPT 150|85 rev3"; + case 0xA06E: + return "SmartSolar MPPT 150|100 rev3"; + case 0xA06F: + return "BlueSolar MPPT 150|45 rev2"; + case 0xA070: + return "BlueSolar MPPT 150|60 rev2"; + case 0xA071: + return "BlueSolar MPPT 150|70 rev2"; + case 0xA07D: + return "BlueSolar MPPT 75|15 rev3"; + case 0xA102: + return "SmartSolar MPPT VE.Can 150/70"; + case 0xA103: + return "SmartSolar MPPT VE.Can 150/45"; + case 0xA104: + return "SmartSolar MPPT VE.Can 150/60"; + case 0xA105: + return "SmartSolar MPPT VE.Can 150/85"; + case 0xA106: + return "SmartSolar MPPT VE.Can 150/100"; + case 0xA107: + return "SmartSolar MPPT VE.Can 250/45"; + case 0xA108: + return "SmartSolar MPPT VE.Can 250/60"; + case 0xA109: + return "SmartSolar MPPT VE.Can 250/70"; + case 0xA10A: + return "SmartSolar MPPT VE.Can 250/85"; + case 0xA10B: + return "SmartSolar MPPT VE.Can 250/100"; + case 0xA10C: + return "SmartSolar MPPT VE.Can 150/70 rev2"; + case 0xA10D: + return "SmartSolar MPPT VE.Can 150/85 rev2"; + case 0xA10E: + return "SmartSolar MPPT VE.Can 150/100 rev2"; + case 0xA10F: + return "BlueSolar MPPT VE.Can 150/100"; + case 0xA112: + return "BlueSolar MPPT VE.Can 250/70"; + case 0xA113: + return "BlueSolar MPPT VE.Can 250/100"; + case 0xA114: + return "SmartSolar MPPT VE.Can 250/70 rev2"; + case 0xA115: + return "SmartSolar MPPT VE.Can 250/100 rev2"; + case 0xA116: + return "SmartSolar MPPT VE.Can 250/85 rev2"; + case 0xA201: + return "Phoenix Inverter 12V 250VA 230V"; + case 0xA202: + return "Phoenix Inverter 24V 250VA 230V"; + case 0xA204: + return "Phoenix Inverter 48V 250VA 230V"; + case 0xA211: + return "Phoenix Inverter 12V 375VA 230V"; + case 0xA212: + return "Phoenix Inverter 24V 375VA 230V"; + case 0xA214: + return "Phoenix Inverter 48V 375VA 230V"; + case 0xA221: + return "Phoenix Inverter 12V 500VA 230V"; + case 0xA222: + return "Phoenix Inverter 24V 500VA 230V"; + case 0xA224: + return "Phoenix Inverter 48V 500VA 230V"; + case 0xA231: + return "Phoenix Inverter 12V 250VA 230V"; + case 0xA232: + return "Phoenix Inverter 24V 250VA 230V"; + case 0xA234: + return "Phoenix Inverter 48V 250VA 230V"; + case 0xA239: + return "Phoenix Inverter 12V 250VA 120V"; + case 0xA23A: + return "Phoenix Inverter 24V 250VA 120V"; + case 0xA23C: + return "Phoenix Inverter 48V 250VA 120V"; + case 0xA241: + return "Phoenix Inverter 12V 375VA 230V"; + case 0xA242: + return "Phoenix Inverter 24V 375VA 230V"; + case 0xA244: + return "Phoenix Inverter 48V 375VA 230V"; + case 0xA249: + return "Phoenix Inverter 12V 375VA 120V"; + case 0xA24A: + return "Phoenix Inverter 24V 375VA 120V"; + case 0xA24C: + return "Phoenix Inverter 48V 375VA 120V"; + case 0xA251: + return "Phoenix Inverter 12V 500VA 230V"; + case 0xA252: + return "Phoenix Inverter 24V 500VA 230V"; + case 0xA254: + return "Phoenix Inverter 48V 500VA 230V"; + case 0xA259: + return "Phoenix Inverter 12V 500VA 120V"; + case 0xA25A: + return "Phoenix Inverter 24V 500VA 120V"; + case 0xA25C: + return "Phoenix Inverter 48V 500VA 120V"; + case 0xA261: + return "Phoenix Inverter 12V 800VA 230V"; + case 0xA262: + return "Phoenix Inverter 24V 800VA 230V"; + case 0xA264: + return "Phoenix Inverter 48V 800VA 230V"; + case 0xA269: + return "Phoenix Inverter 12V 800VA 120V"; + case 0xA26A: + return "Phoenix Inverter 24V 800VA 120V"; + case 0xA26C: + return "Phoenix Inverter 48V 800VA 120V"; + case 0xA271: + return "Phoenix Inverter 12V 1200VA 230V"; + case 0xA272: + return "Phoenix Inverter 24V 1200VA 230V"; + case 0xA274: + return "Phoenix Inverter 48V 1200VA 230V"; + case 0xA279: + case 0xA2F9: + return "Phoenix Inverter 12V 1200VA 120V"; + case 0xA27A: + return "Phoenix Inverter 24V 1200VA 120V"; + case 0xA27C: + return "Phoenix Inverter 48V 1200VA 120V"; + case 0xA281: + return "Phoenix Inverter 12V 1600VA 230V"; + case 0xA282: + return "Phoenix Inverter 24V 1600VA 230V"; + case 0xA284: + return "Phoenix Inverter 48V 1600VA 230V"; + case 0xA291: + return "Phoenix Inverter 12V 2000VA 230V"; + case 0xA292: + return "Phoenix Inverter 24V 2000VA 230V"; + case 0xA294: + return "Phoenix Inverter 48V 2000VA 230V"; + case 0xA2A1: + return "Phoenix Inverter 12V 3000VA 230V"; + case 0xA2A2: + return "Phoenix Inverter 24V 3000VA 230V"; + case 0xA2A4: + return "Phoenix Inverter 48V 3000VA 230V"; + case 0xA30A: + return "Blue Smart IP65 Charger 12|25"; + case 0xA332: + return "Blue Smart IP22 Charger 24|8"; + case 0xA334: + return "Blue Smart IP22 Charger 24|12"; + case 0xA336: + return "Blue Smart IP22 Charger 24|16"; + case 0xA340: + return "Phoenix Smart IP43 Charger 12|50 (1+1)"; + case 0xA341: + return "Phoenix Smart IP43 Charger 12|50 (3)"; + case 0xA342: + return "Phoenix Smart IP43 Charger 24|25 (1+1)"; + case 0xA343: + return "Phoenix Smart IP43 Charger 24|25 (3)"; + case 0xA344: + return "Phoenix Smart IP43 Charger 12|30 (1+1)"; + case 0xA345: + return "Phoenix Smart IP43 Charger 12|30 (3)"; + case 0xA346: + return "Phoenix Smart IP43 Charger 24|16 (1+1)"; + case 0xA347: + return "Phoenix Smart IP43 Charger 24|16 (3)"; + case 0xA381: + return "BMV-712 Smart"; + case 0xA382: + return "BMV-710H Smart"; + case 0xA383: + return "BMV-712 Smart Rev2"; + case 0xA389: + return "SmartShunt 500A/50mV"; + case 0xA38A: + return "SmartShunt 1000A/50mV"; + case 0xA38B: + return "SmartShunt 2000A/50mV"; + case 0xA442: + return "Multi RS Solar 48V 6000VA 230V"; + default: + return "Unknown"; + } + } \ No newline at end of file diff --git a/include/victron.h b/include/victron.h new file mode 100644 index 0000000..610b60a --- /dev/null +++ b/include/victron.h @@ -0,0 +1,92 @@ +/** + * @file victron.h + * @author Icefest + * @brief Wrapper for Victron MPPT + * @version 0.1 + * + * Inspired by: + * https://github.com/KinDR007/VictronMPPT-ESPHOME/blob/main/components/victron/victron.h + */ + +#ifndef VICTRON_MPPT +#define VICTRON_MPPT + +#include +#include + +#define VICTRON_THROTTLE 100 + +typedef void (*debug_serialcommunication) (std::string); + +namespace victron +{ + + class VictronComponent + { + public: + VictronComponent(int initialstate); + VictronComponent(); + ~VictronComponent(); + void loop(void); + String toJson(void); + + int getBatteryVoltage() { + return (battery_voltage_sensor_ / 1000); + } + + int getPanelVoltage() { + return (panel_voltage_sensor_ / 1000); + } + + int getPanelPower() { + return panel_power_sensor_; + } + + bool hasData() { + return (battery_voltage_sensor_ > 0); + } + + void activateDebugging(debug_serialcommunication debugFunction); + + private: + void handle_value_(); + void logTextSensor(String tag, String message, std::string text); + void logBinarySensor(String tag, String message, bool flag); + void logSensor(String tag, String message, int number); + + /* States during serial parsing */ + bool publishing_; + int state_; + std::string label_; + std::string value_; + std::string complete_line_; + uint32_t last_transmission_; + uint32_t last_publish_; + + debug_serialcommunication fdebugSerial = NULL; + + /* All Settings */ + int max_power_yesterday_sensor_ = 0; + int max_power_today_sensor_ = 0; + float yield_total_sensor_ = 0; + float yield_yesterday_sensor_ = 0; + float yield_today_sensor_ = 0; + int panel_voltage_sensor_ = 0; + int panel_power_sensor_ = 0; + int battery_voltage_sensor_ = 0; + int battery_current_sensor_ = 0; + int load_current_sensor_ = 0; + int day_number_sensor_ = 0; + int charging_mode_id_sensor_ = 0; + int error_code_sensor_ = 0; + int tracking_mode_id_sensor_ = 0; + + + bool load_state_binary_sensor_; + + long device_type_text_sensor_; + }; + +} // namespace victron + +#endif /* End of VICTRON_MPPT */ \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 06e0c4a..236f19d 100644 --- a/platformio.ini +++ b/platformio.ini @@ -12,11 +12,18 @@ platform = espressif8266 board = d1_mini framework = arduino -build_flags = -D PIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY +build_flags = -D PIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY -D BME680 +; build_flag needs define the Bosch sensor... +; -D BMP280 +;or +; -D BME680 +; Optinal Paramter to read Victron MPPT: -D VICTRON + ; the latest development branch (convention V3.0.x) lib_deps = https://github.com/homieiot/homie-esp8266.git#develop EspSoftwareSerial NeoPixel + adafruit/Adafruit BMP280 Library @ ^2.4.2 adafruit/Adafruit BME680 Library @ ^2.0.1 -upload_port = /dev/ttyUSB1 \ No newline at end of file +upload_port = /dev/ttyUSB0 \ No newline at end of file diff --git a/src/MqttLog.cpp b/src/MqttLog.cpp new file mode 100644 index 0000000..0db3e16 --- /dev/null +++ b/src/MqttLog.cpp @@ -0,0 +1,38 @@ +/** + * @file MqttLog.cpp + * @author Ollo + * @brief Wrapper for Logger to Mqtt + * @version 0.1 + * + */ + +#include "MqttLog.h" + +bool mConnected = false; + +#define getTopic(test, topic) \ + char *topic = new char[strlen(Homie.getConfiguration().mqtt.baseTopic) + strlen(Homie.getConfiguration().deviceId) + 1 + strlen(test) + 1]; \ + strcpy(topic, Homie.getConfiguration().mqtt.baseTopic); \ + strcat(topic, Homie.getConfiguration().deviceId); \ + strcat(topic, "/"); \ + strcat(topic, test); + + +void log(int level, String message, int statusCode) +{ + String buffer; + StaticJsonDocument<200> doc; + doc["level"] = level; + doc["uptime"] = String(millis()); + doc["message"] = message; + doc["statusCode"] = statusCode; + serializeJson(doc, buffer); + if (mConnected) + { + getTopic(LOG_TOPIC, logTopic) + + Homie.getMqttClient().publish(logTopic, 2, false, buffer.c_str()); + delete logTopic; + } + Homie.getLogger() << (level) << "@" << (statusCode) << " " << (message) << endl; +} diff --git a/src/main.cpp b/src/main.cpp index 5aac240..5b9b136 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -15,52 +15,52 @@ #include #include #include "HomieSettings.h" +#include "MqttLog.h" #include #include #include +#ifdef VICTRON +#include +#endif +#ifdef BME680 #include "Adafruit_BME680.h" +#else +#ifdef BMP280 +#include "Adafruit_BMP280.h" +#else +#error "Decition, which BMx??? is used missing" +#endif +#endif /****************************************************************************** * DEFINES ******************************************************************************/ #define GPIO_WS2812 D4 /**< GPIO2 */ -#define SENSOR_PM1006_RX D2 /**< GPIO4 */ +#define SENSOR_PM1006_RX D2 /**< GPIO4 */ #define SENSOR_PM1006_TX -1 /**< Unused */ #define WITTY_RGB_R D8 /**< GPIO15 */ -#define WITTY_RGB_G D6 /**< GPIO12 */ +#define WITTY_RGB_G D6 /**< GPIO12 Used as 3.3V Power supply for the I2C Sensor */ #define WITTY_RGB_B D7 /**< GPIO13 */ #define PM1006_BIT_RATE 9600 -#define PM1006_MQTT_UPDATE 5000 /**< Check the sensor every 10 seconds; New measurement is done every 20seconds by the sensor */ +#define PM1006_MQTT_UPDATE 30000 /**< Check the sensor every 30 seconds; New measurement is done every 20seconds by the PM1006 sensor */ #define PIXEL_COUNT 3 #define GPIO_BUTTON SENSOR_PM1006_RX /**< Button and software serial share one pin on Witty board */ -#define SENSOR_I2C_SCK D1 /**< GPIO14 - I2C clock pin */ -#define SENSOR_I2C_SDI D5 /**< GPIO5 - I2C data pin */ - -#define PM1006_BIT_RATE 9600 -#define PM1006_MQTT_UPDATE 5000 /**< Check the sensor every 10 seconds; New measurement is done every 20seconds by the sensor */ +#define SENSOR_I2C_SCK D5 /**< GPIO14 - I2C clock pin */ +#define SENSOR_I2C_SDI D1 /**< GPIO5 - I2C data pin */ #define SEALEVELPRESSURE_HPA (1013.25) -#define LOG_TOPIC "log\0" -#define MQTT_LEVEL_ERROR 1 -#define MQTT_LEVEL_WARNING 10 -#define MQTT_LEVEL_INFO 20 -#define MQTT_LEVEL_DEBUG 90 +#define BUTTON_MAX_CYCLE 10000U /**< Action: Reset configuration */ +#define BUTTON_MIN_ACTION_CYCLE 55U /**< Minimum cycle to react on the button (e.g. 5 second) */ +#define BUTTON_CHECK_INTERVALL 100U /**< Check every 100 ms the button state */ -#define MQTT_LOG_PM1006 10 -#define MQTT_LOG_I2CINIT 100 -#define MQTT_LOG_I2READ 101 -#define MQTT_LOG_RGB 200 +#define MIN_MEASURED_CYCLES 2 +#define PM_MAX 1001 /**< According datasheet https://en.gassensor.com.cn/ParticulateMatterSensor/info_itemid_105.html 1000 is the maximum */ #define TEMPBORDER 20 -#define getTopic(test, topic) \ - char *topic = new char[strlen(Homie.getConfiguration().mqtt.baseTopic) + strlen(Homie.getConfiguration().deviceId) + 1 + strlen(test) + 1]; \ - strcpy(topic, Homie.getConfiguration().mqtt.baseTopic); \ - strcat(topic, Homie.getConfiguration().deviceId); \ - strcat(topic, "/"); \ - strcat(topic, test); +#define PERCENT2FACTOR(b, a) ((b * a.get()) / 100) #define NUMBER_TYPE "Number" #define NODE_PARTICLE "particle" @@ -70,6 +70,13 @@ #define NODE_GAS "gas" #define NODE_HUMIDITY "humidity" #define NODE_AMBIENT "ambient" +#define NODE_BUTTON "button" +#define NODE_MPPT "mppt" +#define NODE_SOLAR "solar" +#define NODE_SOLAR_BATTERYVOLT "batteryV" +#define NODE_SOLAR_PANELPOWER "panelP" +#define NODE_SOLAR_PANELVOLT "panelV" +#define SERIAL_RCEVBUF_MAX 80 /**< Maximum 80 characters can be received from the PM1006 sensor */ /****************************************************************************** * TYPE DEFS ******************************************************************************/ @@ -85,41 +92,87 @@ void log(int level, String message, int code); ******************************************************************************/ bool mConfigured = false; -bool mConnected = false; + +bool mOTAactive = false; /**< Stop sleeping, if OTA is running */ bool mFailedI2Cinitialization = false; +long mLastButtonAction = 0; /******************************* Sensor data **************************/ HomieNode particle(NODE_PARTICLE, "particle", "number"); /**< Measuret in micro gram per quibik meter air volume */ -HomieNode temperatureNode(NODE_TEMPERATUR, "Room Temperature", "number"); +HomieNode temperaturNode(NODE_TEMPERATUR, "Room Temperature", "number"); HomieNode pressureNode(NODE_PRESSURE, "Pressure", "number"); HomieNode altitudeNode(NODE_ALTITUDE, "Altitude", "number"); +#ifdef BME680 HomieNode gasNode(NODE_GAS, "Gas", "number"); HomieNode humidityNode(NODE_HUMIDITY, "Humidity", "number"); +#endif +HomieNode buttonNode(NODE_BUTTON, "Button", "number"); + +#ifdef VICTRON +HomieNode mpptNode(NODE_MPPT, "MPPT", "json"); +HomieNode solarNode(NODE_SOLAR, "Solar", "number"); +#endif /****************************** Output control ***********************/ HomieNode ledStripNode /* to rule them all */("led", "RGB led", "color"); /************************** Settings ******************************/ -HomieSetting i2cEnable("i2c", "BME280 sensor present"); -HomieSetting rgbTemp("rgbTemp", "Show temperatur via red (>20 °C) and blue (< 20°C)"); +HomieSetting i2cEnable("i2c", +#ifdef BME680 +"BME680 sensor present" +#else +#ifdef BMP280 +"BMP280 sensor present" +#else +"No I2C sensor specified in the project" +#endif +#endif + +); +HomieSetting rgbTemp("rgbTemp", "Show temperature via red (>20 °C) and blue (< 20°C)"); +HomieSetting rgbDim("rgbDim", "Factor (1 to 200%) of the status LEDs"); +HomieSetting deepsleep("deepsleep", "Amount of seconds to sleep (default 0 - always online, maximum 4294 - 71 minutes)"); static SoftwareSerial pmSerial(SENSOR_PM1006_RX, SENSOR_PM1006_TX); -Adafruit_BME680 bme(&Wire); // connected via I2C +#ifdef BME680 +Adafruit_BME680 bmx(&Wire); // connected via I2C +#else +#ifdef BMP280 +Adafruit_BMP280 bmx; // connected via I2C +#endif +#endif Adafruit_NeoPixel strip(PIXEL_COUNT, GPIO_WS2812, NEO_GRB + NEO_KHZ800); +#ifdef VICTRON +HomieSetting deepsleepMppt("dsleepMppt", "Deep sleep only after MPPT comminication (default 0 / false: sleep without any info from Victron)"); +victron::VictronComponent mppt(0); +#endif + // Variablen -uint8_t serialRxBuf[80]; +uint8_t serialRxBuf[SERIAL_RCEVBUF_MAX]; uint8_t rxBufIdx = 0; -int spm25 = 0; +int mParticle_pM25 = 0; int last = 0; unsigned int mButtonPressed = 0; bool mSomethingReceived = false; +uint32_t mMeasureIndex = 0; + /****************************************************************************** * LOCAL FUNCTIONS *****************************************************************************/ +/** + * @brief Log Victron communication plain to MQTT + * + * @param uartLine one complete line, received on VC.Direct Bus + */ +void mqttLog_callback(std::string uartLine) +{ + log(MQTT_LEVEL_DEBUG, String(uartLine.c_str()), MQTT_LOG_VICTRON); +} + /** * @brief Get the Sensor Data from software serial * @@ -154,7 +207,12 @@ int getSensorData() { // Header und Prüfsumme checken if (serialRxBuf[0] == 0x16 && serialRxBuf[1] == 0x11 && serialRxBuf[2] == 0x0B /* && checksum == 0 */) { - return (serialRxBuf[5] << 8 | serialRxBuf[6]); + int pmValue = (serialRxBuf[5] << 8 | serialRxBuf[6]); + if (pmValue > PM_MAX) { + return (-1); + } else { + return pmValue; + } } else { @@ -170,58 +228,93 @@ void onHomieEvent(const HomieEvent &event) { switch (event.type) { + case HomieEventType::READY_TO_SLEEP: + if (mOTAactive) { + Homie.getLogger() << "Skip sleeping, as OTA was started" << endl; + return; + } else if (deepsleep.get() > 0) { + long sleepInSeconds = deepsleep.get(); + Homie.doDeepSleep(sleepInSeconds * 1000000, RF_NO_CAL); + } + break; case HomieEventType::MQTT_READY: mConnected=true; +#ifdef VICTRON + mppt.activateDebugging(mqttLog_callback); +#endif digitalWrite(WITTY_RGB_R, LOW); if (!i2cEnable.get()) { /** keep green LED activated to power I2C sensor */ digitalWrite(WITTY_RGB_G, LOW); + log(MQTT_LEVEL_INFO, F("I2C powersupply deactivated"), MQTT_LOG_I2CINIT); } digitalWrite(WITTY_RGB_B, LOW); - strip.fill(strip.Color(0,0,128)); - strip.show(); + /* Update LED only, if not sleeping */ + if (deepsleep.get() <= 0) { + strip.fill(strip.Color(0,0,PERCENT2FACTOR(127, rgbDim))); + strip.show(); + } + if (mFailedI2Cinitialization) { - log(MQTT_LEVEL_DEBUG, F("Could not find a valid BME680 sensor, check wiring or " - "try a different address!"), MQTT_LOG_I2CINIT); + log(MQTT_LEVEL_DEBUG, +#ifdef BME680 + "Could not find a valid BME680 sensor, check wiring or try a different address!" +#else +#ifdef BMP280 + "Could not find a valid BMP280 sensor, check wiring or try a different address!" +#else + "no I2C sensor defined" +#endif +#endif + , MQTT_LOG_I2CINIT); } else { log(MQTT_LEVEL_INFO, F("BME680 sensor found"), MQTT_LOG_I2CINIT); } break; - case HomieEventType::READY_TO_SLEEP: - break; case HomieEventType::OTA_STARTED: + mOTAactive = true; break; case HomieEventType::OTA_SUCCESSFUL: ESP.restart(); break; + case HomieEventType::WIFI_CONNECTED: + digitalWrite(WITTY_RGB_B, HIGH); + break; default: break; } } void bmpPublishValues() { +#ifdef BME680 // Tell BME680 to begin measurement. - unsigned long endTime = bme.beginReading(); + unsigned long endTime = bmx.beginReading(); if (endTime == 0) { - log(MQTT_LEVEL_ERROR, F("BME680 not accessable"), MQTT_LOG_I2READ); + log(MQTT_LEVEL_ERROR, "BMX not accessible", MQTT_LOG_I2READ); return; } - temperatureNode.setProperty(NODE_TEMPERATUR).send(String(bme.readTemperature())); - pressureNode.setProperty(NODE_PRESSURE).send(String(bme.readPressure() / 100.0F)); - altitudeNode.setProperty(NODE_ALTITUDE).send(String(bme.readAltitude(SEALEVELPRESSURE_HPA))); - gasNode.setProperty(NODE_GAS).send(String((bme.gas_resistance / 1000.0))); - - humidityNode.setProperty(NODE_HUMIDITY).send(String(bme.humidity)); - +#endif + // Publish the values + temperaturNode.setProperty(NODE_TEMPERATUR).send(String(bmx.readTemperature())); + pressureNode.setProperty(NODE_PRESSURE).send(String(bmx.readPressure() / 100.0F)); + altitudeNode.setProperty(NODE_ALTITUDE).send(String(bmx.readAltitude(SEALEVELPRESSURE_HPA))); +#ifdef BME680 + gasNode.setProperty(NODE_GAS).send(String((bmx.gas_resistance / 1000.0))); + humidityNode.setProperty(NODE_HUMIDITY).send(String(bmx.humidity)); +#endif + log(MQTT_LEVEL_DEBUG, String("Temp" + String(bmx.readTemperature()) + "\tPressure:" + + String(bmx.readPressure() / 100.0F) + "\t Altitude:"+ + String(bmx.readAltitude(SEALEVELPRESSURE_HPA))), MQTT_LOG_I2READ); if ( (rgbTemp.get()) && (!mSomethingReceived) ) { - if (bme.readTemperature() < TEMPBORDER) { - strip.setPixelColor(0, strip.Color(0,0,255)); + if (bmx.readTemperature() < TEMPBORDER) { + strip.setPixelColor(0, strip.Color(0,0,PERCENT2FACTOR(127, rgbDim))); } else { - strip.setPixelColor(0, strip.Color(255,0,0)); + strip.setPixelColor(0, strip.Color(PERCENT2FACTOR(127, rgbDim),0,0)); } strip.show(); } } + /** * @brief Main loop, triggered by the Homie API * All logic needs to be done here. @@ -237,16 +330,16 @@ void loopHandler() { static long lastRead = 0; if ((millis() - lastRead) > PM1006_MQTT_UPDATE) { - int pM25 = getSensorData(); - if (pM25 >= 0) { - particle.setProperty(NODE_PARTICLE).send(String(pM25)); + mParticle_pM25 = getSensorData(); + if (mParticle_pM25 >= 0) { + particle.setProperty(NODE_PARTICLE).send(String(mParticle_pM25)); if (!mSomethingReceived) { - if (pM25 < 35) { - strip.fill(strip.Color(0, 255, 0)); /* green */ - } else if (pM25 < 85) { - strip.fill(strip.Color(255, 127, 0)); /* orange */ + if (mParticle_pM25 < 35) { + strip.fill(strip.Color(0, PERCENT2FACTOR(127, rgbDim), 0)); /* green */ + } else if (mParticle_pM25 < 85) { + strip.fill(strip.Color(PERCENT2FACTOR(127, rgbDim), PERCENT2FACTOR(64, rgbDim), 0)); /* orange */ } else { - strip.fill(strip.Color(255, 0, 0)); /* red */ + strip.fill(strip.Color(PERCENT2FACTOR(127, rgbDim), 0, 0)); /* red */ } strip.show(); } @@ -256,9 +349,38 @@ void loopHandler() if (i2cEnable.get() && (!mFailedI2Cinitialization)) { bmpPublishValues(); } - + + mMeasureIndex++; + + /* Clean cycles buttons */ + if (mButtonPressed <= BUTTON_MIN_ACTION_CYCLE) { + buttonNode.setProperty(NODE_BUTTON).send("0"); + } lastRead = millis(); + + /* If nothing needs to be done, sleep and the time is ready for sleeping */ + if ((mMeasureIndex > MIN_MEASURED_CYCLES) && (deepsleep.get() > 0) +#ifdef VICTRON + && (mppt.hasData() || (deepsleepMppt.get() == 0)) +#endif + ) { + Homie.prepareToSleep(); + delay(100); + } +#ifdef VICTRON + mpptNode.setProperty(NODE_MPPT).send(mppt.toJson()); + solarNode.setProperty(NODE_SOLAR_BATTERYVOLT).send(String(mppt.getBatteryVoltage())); + solarNode.setProperty(NODE_SOLAR_PANELVOLT).send(String(mppt.getPanelVoltage())); + solarNode.setProperty(NODE_SOLAR_PANELPOWER).send(String(mppt.getPanelPower())); + +#endif + } + + /* if the user sees something via the LEDs, inform MQTT, too */ + if (mButtonPressed > BUTTON_MIN_ACTION_CYCLE) { + buttonNode.setProperty(NODE_BUTTON).send(String(mButtonPressed)); } + // Feed the dog -> ESP stay alive ESP.wdtFeed(); } @@ -268,23 +390,30 @@ void loopHandler() bool ledHandler(const HomieRange& range, const String& value) { if (range.isRange) return false; // only one switch is present - mSomethingReceived = true; // Stop animation - - int sep1 = value.indexOf(','); - int sep2 = value.indexOf(',', sep1 + 1); - if ((sep1 > 0) && (sep2 > 0)) { - int red = value.substring(0,sep1).toInt(); /* OpenHAB hue (0-360°) */ - int green = value.substring(sep1 + 1, sep2).toInt(); /* OpenHAB saturation (0-100%) */ - int blue = value.substring(sep2 + 1, value.length()).toInt(); /* brightness (0-100%) */ - - uint8_t r = (red * 255) / 250; - uint8_t g = (green *255) / 250; - uint8_t b = (blue *255) / 250; - uint32_t c = strip.Color(r,g,b); - strip.fill(c); - strip.show(); - ledStripNode.setProperty(NODE_AMBIENT).send(String(r) + "," + String(g) + "," + String(b)); + Homie.getLogger() << "Received: " << (value) << endl; + if (value.equals("250,250,250")) { + mSomethingReceived = false; // enable animation again + ledStripNode.setProperty(NODE_AMBIENT).send(value); return true; + } else { + mSomethingReceived = true; // Stop animation + + int sep1 = value.indexOf(','); + int sep2 = value.indexOf(',', sep1 + 1); + if ((sep1 > 0) && (sep2 > 0)) { + int red = value.substring(0,sep1).toInt(); + int green = value.substring(sep1 + 1, sep2).toInt(); + int blue = value.substring(sep2 + 1, value.length()).toInt(); + + uint8_t r = (red * 255) / 250; + uint8_t g = (green *255) / 250; + uint8_t b = (blue *255) / 250; + uint32_t c = strip.Color(r,g,b); + strip.fill(c); + strip.show(); + ledStripNode.setProperty(NODE_AMBIENT).send(value); + return true; + } } return false; } @@ -296,10 +425,11 @@ bool ledHandler(const HomieRange& range, const String& value) { *****************************************************************************/ void setup() -{ +{ SPIFFS.begin(); - Serial.begin(115200); + Serial.begin(SERIAL_BAUDRATE); Serial.setTimeout(2000); + pinMode(WITTY_RGB_R, OUTPUT); pinMode(WITTY_RGB_G, OUTPUT); pinMode(WITTY_RGB_B, OUTPUT); @@ -311,14 +441,24 @@ void setup() Homie.setLoopFunction(loopHandler); Homie.onEvent(onHomieEvent); i2cEnable.setDefaultValue(false); +#if VICTRON + deepsleepMppt.setDefaultValue(false); +#endif rgbTemp.setDefaultValue(false); - memset(serialRxBuf, 0, 80); + + rgbDim.setDefaultValue(100).setValidator([] (long candidate) { + return (candidate > 1) && (candidate <= 200); + }); + deepsleep.setDefaultValue(0).setValidator([] (long candidate) { + return ((candidate >= 0) && (candidate < 4294)); /* between 0 (deactivated) and 71 minutes */ + }); + memset(serialRxBuf, 0, SERIAL_RCEVBUF_MAX); pmSerial.begin(PM1006_BIT_RATE); Homie.setup(); particle.advertise(NODE_PARTICLE).setName("Particle").setDatatype(NUMBER_TYPE).setUnit("micro gram per quibik"); - temperatureNode.advertise(NODE_TEMPERATUR).setName("Degrees") + temperaturNode.advertise(NODE_TEMPERATUR).setName("Degrees") .setDatatype("float") .setUnit("ºC"); pressureNode.advertise(NODE_PRESSURE).setName("Pressure") @@ -327,47 +467,82 @@ void setup() altitudeNode.advertise(NODE_ALTITUDE).setName("Altitude") .setDatatype("float") .setUnit("m"); +#ifdef BME680 gasNode.advertise(NODE_GAS).setName("Gas") .setDatatype("float") .setUnit(" KOhms"); humidityNode.advertise(NODE_HUMIDITY).setName("Humidity") .setDatatype("float") .setUnit("%"); +#endif ledStripNode.advertise(NODE_AMBIENT).setName("All Leds") .setDatatype("color").setFormat("rgb") .settable(ledHandler); - - + buttonNode.advertise(NODE_BUTTON).setName("Button pressed") + .setDatatype("integer"); +#if VICTRON + mpptNode.advertise(NODE_MPPT).setName("MPPT") + .setDatatype("json"); + solarNode.advertise(NODE_SOLAR).setName("Solar") + .setDatatype("integer"); + + solarNode.advertise(NODE_SOLAR_BATTERYVOLT).setName("Solar") + .setDatatype("integer").setUnit("mV"); + solarNode.advertise(NODE_SOLAR_PANELPOWER).setName("Panel") + .setDatatype("integer").setUnit("W"); + solarNode.advertise(NODE_SOLAR_PANELVOLT).setName("Panel") + .setDatatype("integer").setUnit("mV"); +#endif strip.begin(); - /* activate I2C for BOSCH sensor */ - Wire.begin(SENSOR_I2C_SDI, SENSOR_I2C_SCK); mConfigured = Homie.isConfigured(); digitalWrite(WITTY_RGB_G, HIGH); if (mConfigured) { if (i2cEnable.get()) { - strip.fill(strip.Color(0,128,0)); - strip.show(); +#ifdef BME680 + printf("Wait 1 second...\r\n"); + delay(1000); +#endif + /* activate I2C for BOSCH sensor */ + Wire.begin(SENSOR_I2C_SDI, SENSOR_I2C_SCK); + printf("Wait 50 milliseconds...\r\n"); + delay(50); /* Extracted from library's example */ - mFailedI2Cinitialization = !bme.begin(); + mFailedI2Cinitialization = !bmx.begin(); if (!mFailedI2Cinitialization) { - bme.setTemperatureOversampling(BME680_OS_8X); - bme.setHumidityOversampling(BME680_OS_2X); - bme.setPressureOversampling(BME680_OS_4X); - bme.setIIRFilterSize(BME680_FILTER_SIZE_3); - bme.setGasHeater(320, 150); // 320*C for 150 ms + strip.fill(strip.Color(0,PERCENT2FACTOR(64, rgbDim),0)); + strip.show(); +#ifdef BME680 + bmx.setTemperatureOversampling(BME680_OS_8X); + bmx.setHumidityOversampling(BME680_OS_2X); + bmx.setPressureOversampling(BME680_OS_4X); + bmx.setIIRFilterSize(BME680_FILTER_SIZE_3); + bmx.setGasHeater(320, 150); // 320*C for 150 ms +#endif +#ifdef BMP280 + /* Default settings from datasheet. */ + bmx.setSampling(Adafruit_BMP280::MODE_NORMAL, /* Operating Mode. */ + Adafruit_BMP280::SAMPLING_X2, /* Temp. oversampling */ + Adafruit_BMP280::SAMPLING_X16, /* Pressure oversampling */ + Adafruit_BMP280::FILTER_X16, /* Filtering. */ + Adafruit_BMP280::STANDBY_MS_500); /* Standby time. */ +#endif + printf("Sensor found on I2C bus\r\n"); } else { - printf("Faild to initialize I2C bus\r\n"); + printf("Failed to initialize I2C bus\r\n"); } } - strip.fill(strip.Color(0,0,0)); - for (int i=0;i < (PIXEL_COUNT / 2); i++) { - strip.setPixelColor(0, strip.Color(0,0,128)); + /* Nothing when sleeping */ + if (deepsleep.get() <= 0) { + strip.fill(strip.Color(0,0,0)); + for (int i=0;i < (PIXEL_COUNT / 2); i++) { + strip.setPixelColor(0, strip.Color(0,0,128 * rgbDim.get())); + } + strip.show(); } - strip.show(); - digitalWrite(WITTY_RGB_B, HIGH); } else { + digitalWrite(WITTY_RGB_R, HIGH); strip.fill(strip.Color(128,0,0)); for (int i=0;i < (PIXEL_COUNT / 2); i++) { strip.setPixelColor(0, strip.Color(0,0,128)); @@ -381,37 +556,41 @@ void loop() Homie.loop(); /* use the pin, receiving the soft serial additionally as button */ if (digitalRead(GPIO_BUTTON) == LOW) { - mButtonPressed++; + if ((millis() - mLastButtonAction) > BUTTON_CHECK_INTERVALL) { + mButtonPressed++; + } + if (mButtonPressed > BUTTON_MIN_ACTION_CYCLE) { + digitalWrite(WITTY_RGB_R, HIGH); + digitalWrite(WITTY_RGB_B, LOW); + strip.fill(strip.Color(0,0,0)); + strip.setPixelColor(0, strip.Color((mButtonPressed % 100),0,0)); + strip.setPixelColor(1, strip.Color((mButtonPressed / 100),0,0)); + strip.setPixelColor(2, strip.Color((mButtonPressed / 100),0,0)); + strip.show(); + } } else { mButtonPressed=0U; + digitalWrite(WITTY_RGB_R, LOW); } - if (mButtonPressed > 10000U) { - mButtonPressed=0U; + if (mButtonPressed > BUTTON_MAX_CYCLE) { if (SPIFFS.exists("/homie/config.json")) { + strip.fill(strip.Color(0,PERCENT2FACTOR(127, rgbDim),0)); + strip.show(); printf("Resetting config\r\n"); SPIFFS.remove("/homie/config.json"); SPIFFS.end(); + delay(50); + Homie.reboot(); } else { printf("No config present\r\n"); + strip.fill(strip.Color(0,0,128)); + strip.show(); } } -} - -void log(int level, String message, int statusCode) -{ - String buffer; - StaticJsonDocument<200> doc; - doc["level"] = level; - doc["message"] = message; - doc["statusCode"] = statusCode; - serializeJson(doc, buffer); - if (mConnected) - { - getTopic(LOG_TOPIC, logTopic) - - Homie.getMqttClient().publish(logTopic, 2, false, buffer.c_str()); - delete logTopic; - } +#ifdef VICTRON + // Read victron MPPT + mppt.loop(); +#endif } diff --git a/src/victron.cpp b/src/victron.cpp new file mode 100644 index 0000000..5887341 --- /dev/null +++ b/src/victron.cpp @@ -0,0 +1,287 @@ +/** + * @file victron.cpp + * @author Icefest + * @brief Wrapper for Victron MPPT + * @version 0.1 + * + * Inspired by: + * https://github.com/KinDR007/VictronMPPT-ESPHOME/blob/main/components/victron/victron.cpp + */ + +#ifdef VICTRON +#include "victron.h" +#include "MqttLog.h" +#include "VictronTexts.h" + +namespace victron { + + static const char *const TAG = "victron"; + + VictronComponent::~VictronComponent() + { + + } + + VictronComponent::VictronComponent(int initialstate) + { + this->state_ = initialstate; + } + + void VictronComponent::activateDebugging(debug_serialcommunication debugFunction) + { + this->fdebugSerial = debugFunction; + } + + void VictronComponent::logTextSensor(String tag, String message, std::string text) + { + String complete = message + " : " + String(text.c_str()); + log(MQTT_LEVEL_INFO, complete, MQTT_LOG_VICTRON); + } + + void VictronComponent::logBinarySensor(String tag, String message, bool flag) + { + String complete = message + " : " + String(flag); + log(MQTT_LEVEL_INFO, complete, MQTT_LOG_VICTRON); + } + + void VictronComponent::logSensor(String tag, String message, int number) + { + String complete = message + " : " + String(number); + log(MQTT_LEVEL_INFO, complete, MQTT_LOG_VICTRON); + } + + void VictronComponent::loop() + { + const uint32_t now = millis(); + if ((state_ > 0) && (now - last_transmission_ >= 200)) { + // last transmission too long ago. Reset RX index. + log(MQTT_LEVEL_INFO, "Last transmission too long ago", MQTT_LOG_VICTRON); + state_ = 0; + } + + if (!Serial.available()) + return; + + last_transmission_ = now; + while (Serial.available()) { + uint8_t c; + c = Serial.read(); + + if (fdebugSerial) /* debugging enabled */ + { + /* always store the incoming data */ + complete_line_.push_back(c); + } + + if (state_ == 0) { + if (c == '\r' || c == '\n') { + if ( (fdebugSerial) /* debugging enabled */ && (complete_line_.length() > 0) ) + { + fdebugSerial(complete_line_); + complete_line_.clear(); + } + continue; + } + label_.clear(); + value_.clear(); + state_ = 1; + } + if (state_ == 1) { + // Start of a ve.direct hex frame + if (c == ':') { + state_ = 3; + continue; + } + if (c == '\t') { + state_ = 2; + } else { + label_.push_back(c); + } + continue; + } + if (state_ == 2) + { + if (label_ == "Checksum") { + state_ = 0; + // The checksum is used as end of frame indicator + if ((now - this->last_publish_) >= VICTRON_THROTTLE) { + this->last_publish_ = now; + this->publishing_ = true; + } else { + this->publishing_ = false; + } + continue; + } + if (c == '\r' || c == '\n') { + if (this->publishing_) { + handle_value_(); + } + state_ = 0; + } else { + value_.push_back(c); + } + } + // Discard ve.direct hex frame + if (state_ == 3) { + if (c == '\r' || c == '\n') { + state_ = 0; + } + } + } + } + + void VictronComponent::handle_value_() + { + int value; + + if (label_ == "V") { + battery_voltage_sensor_ = atoi(value_.c_str()); /* mV */ + return; + } + + if (label_ == "VPV") { + // mV to V + panel_voltage_sensor_ = atoi(value_.c_str()); /* mV */ + return; + } + + if (label_ == "PPV") { + panel_power_sensor_ = atoi(value_.c_str()); + return; + } + + if (label_ == "I") { + // mA to A + battery_current_sensor_ = atoi(value_.c_str()); /* mA */ + return; + } + + if (label_ == "IL") { + load_current_sensor_ = atoi(value_.c_str()); /* mA */ + return; + } + + if (label_ == "LOAD") { + load_state_binary_sensor_= (value_ == "ON" || value_ == "On"); + return; + } + + if (label_ == "Alarm") { + /* Skip Alarm */ + return; + } + + if (label_ == "H19") { + yield_total_sensor_ = (atoi(value_.c_str()) * 10.0f); // NOLINT(cert-err34-c) + return; + } + + if (label_ == "H20") { + yield_today_sensor_ = (atoi(value_.c_str()) * 10.0f); // NOLINT(cert-err34-c) + return; + } + + if (label_ == "H21") { + max_power_today_sensor_ = (atoi(value_.c_str())); // NOLINT(cert-err34-c) + return; + } + + if (label_ == "H22") { + yield_yesterday_sensor_ = (atoi(value_.c_str()) * 10.0f); // NOLINT(cert-err34-c) + return; + } + + if (label_ == "H23") { + max_power_yesterday_sensor_ = atoi(value_.c_str()); + return; + } + + if (label_ == "ERR") { + value = atoi(value_.c_str()); // NOLINT(cert-err34-c) + error_code_sensor_ = value; + return; + } + + if (label_ == "CS") { + value = atoi(value_.c_str()); // NOLINT(cert-err34-c) + charging_mode_id_sensor_ = value; + return; + } + + + if (label_ == "FW") { + /* Skip firmware */ + return; + } + + + if (label_ == "PID") { + device_type_text_sensor_ = strtol(value_.c_str(), nullptr, 0); + return; + } + + if (label_ == "HSDS") { + day_number_sensor_ = atoi(value_.c_str()); + return; + } + + if (label_ == "MPPT") { + value = atoi(value_.c_str()); // NOLINT(cert-err34-c) + tracking_mode_id_sensor_ = value; + return; + } + + String message= "Unhandled property:" + String(label_.c_str()) + " : " + String(value_.c_str()); + log(MQTT_LEVEL_ERROR, message, MQTT_LOG_VICTRON); + } + + String VictronComponent::toJson(void) + { + String buffer; + if (this->last_publish_ <= 0) + { + buffer += "{ "; + buffer += "\"mode\": \"nodata\",\n"; + buffer += "\"state\":" + String(state_) + ",\n"; + buffer += "\"transmission\":" + String(last_transmission_) + ",\n"; + buffer += "\"publish\":" + String(last_publish_) + "\n"; + buffer += "}"; + return buffer; + } + else + { + buffer += "{ "; + buffer += "\"mode\": \"newdata\",\n"; + buffer += "\"load\":" + String(load_state_binary_sensor_) + ",\n"; + buffer += "\"MaxPower\":{\n"; + buffer += "\"yesterday\":" + String(max_power_yesterday_sensor_) + ",\n"; + buffer += "\"today\":" + String(max_power_today_sensor_) + "\n"; + buffer += "},\n"; + buffer += "\"Yield\":{\n"; + buffer += "\"Total\":" + String(yield_total_sensor_) + ",\n"; + buffer += "\"Yesterday\":" + String(yield_yesterday_sensor_) + ",\n"; + buffer += "\"Today\":" + String(yield_today_sensor_) + "\n"; + buffer += "},\n"; + buffer += "\"Panel\":{\n"; + buffer += "\"Voltage\":" + String(panel_voltage_sensor_) + ",\n"; + buffer += "\"Power\":" + String(panel_power_sensor_) + "\n"; + buffer += "},\n"; + buffer += "\"Bat\":{\n"; + buffer += "\"Voltage\":" + String(battery_voltage_sensor_) + ",\n"; + buffer += "\"Current\":" + String(battery_current_sensor_) + "\n"; + buffer += "},\n"; + buffer += "\"LoadCurrent\":" + String(load_current_sensor_) + ",\n"; + buffer += "\"DayNumber\":" + String(day_number_sensor_) + ",\n"; + buffer += "\"ChargingModeID\":" + String(charging_mode_id_sensor_) + ",\n"; + buffer += "\"ErrorCode\":" + String(error_code_sensor_) + ",\n"; + buffer += "\"TrackingModeID\":" + String(tracking_mode_id_sensor_) + ",\n"; + buffer += "\"ErrorText\": \"" + String(error_code_text(error_code_sensor_).c_str()) + "\",\n"; + buffer += "\"TrackingMode\": \"" + String(tracking_mode_text(tracking_mode_id_sensor_).c_str()) + "\",\n"; + buffer += "\"ChargingMode\": \"" + String(charging_mode_text(charging_mode_id_sensor_).c_str()) + "\",\n"; + buffer += "\"DeviceType\": \"" + String(device_type_text(device_type_text_sensor_).c_str() ) + "\",\n"; + buffer += "}"; + return buffer; + } + } +} +#endif /* VICTRON */