├── assets ├── main_page.png ├── schematics.png ├── ferraris_meter.png ├── main_settings.png ├── expert_settings.png └── influxdb_sensor_data.png ├── include ├── influx.h ├── utils.h ├── web.h ├── wlan.h ├── ferraris.h ├── nvs.h ├── mqtt.h ├── config.h ├── index_en.h └── index_de.h ├── LICENSE ├── platformio.ini ├── src ├── influx.cpp ├── utils.cpp ├── main.cpp ├── wlan.cpp ├── nvs.cpp ├── ferraris.cpp ├── web.cpp └── mqtt.cpp ├── restful.html └── README.md /assets/main_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lrswss/esp8266-wifi-power-meter/HEAD/assets/main_page.png -------------------------------------------------------------------------------- /assets/schematics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lrswss/esp8266-wifi-power-meter/HEAD/assets/schematics.png -------------------------------------------------------------------------------- /assets/ferraris_meter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lrswss/esp8266-wifi-power-meter/HEAD/assets/ferraris_meter.png -------------------------------------------------------------------------------- /assets/main_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lrswss/esp8266-wifi-power-meter/HEAD/assets/main_settings.png -------------------------------------------------------------------------------- /assets/expert_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lrswss/esp8266-wifi-power-meter/HEAD/assets/expert_settings.png -------------------------------------------------------------------------------- /assets/influxdb_sensor_data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lrswss/esp8266-wifi-power-meter/HEAD/assets/influxdb_sensor_data.png -------------------------------------------------------------------------------- /include/influx.h: -------------------------------------------------------------------------------- 1 | /*************************************************************************** 2 | Copyright (c) 2019-2022 Lars Wessels 3 | 4 | This file a part of the "ESP8266 Wifi Power Meter" source code. 5 | https://github.com/lrswss/esp8266-wifi-power-meter 6 | 7 | Licensed under the MIT License. You may not use this file except in 8 | compliance with the License. You may obtain a copy of the License at 9 | 10 | https://opensource.org/licenses/MIT 11 | 12 | ***************************************************************************/ 13 | 14 | #ifndef _INFLUX_H 15 | #define _INFLUX_H 16 | 17 | #include 18 | #include 19 | 20 | void send2influx_udp(uint16_t counter, uint16_t threshold, uint16_t pulse); 21 | 22 | #endif 23 | -------------------------------------------------------------------------------- /include/utils.h: -------------------------------------------------------------------------------- 1 | /*************************************************************************** 2 | Copyright (c) 2019-2023 Lars Wessels 3 | 4 | This file a part of the "ESP8266 Wifi Power Meter" source code. 5 | https://github.com/lrswss/esp8266-wifi-power-meter 6 | 7 | Licensed under the MIT License. You may not use this file except in 8 | compliance with the License. You may obtain a copy of the License at 9 | 10 | https://opensource.org/licenses/MIT 11 | 12 | ***************************************************************************/ 13 | 14 | #ifndef _UTILS_H 15 | #define _UTILS_H 16 | 17 | #include 18 | 19 | //#define DEBUG_HEAP 20 | 21 | String systemID(); 22 | int32_t tsDiff(uint32_t tsMillis); 23 | char* getRuntime(bool minutesOnly); 24 | void blinkLED(uint8_t repeat, uint16_t pause); 25 | void restartSystem(); 26 | void toggleLED(); 27 | void switchLED(bool state); 28 | 29 | #endif -------------------------------------------------------------------------------- /include/web.h: -------------------------------------------------------------------------------- 1 | /*************************************************************************** 2 | Copyright (c) 2019-2023 Lars Wessels 3 | 4 | This file a part of the "ESP8266 Wifi Power Meter" source code. 5 | https://github.com/lrswss/esp8266-wifi-power-meter 6 | 7 | Licensed under the MIT License. You may not use this file except in 8 | compliance with the License. You may obtain a copy of the License at 9 | 10 | https://opensource.org/licenses/MIT 11 | 12 | ***************************************************************************/ 13 | 14 | #ifndef _WEB_H 15 | #define _WEB_H 16 | 17 | #include 18 | #include 19 | 20 | #ifdef LANGUAGE_EN 21 | #include "index_en.h" // HTML-Page (english) 22 | #else 23 | #include "index_de.h" // HTML-Page (german) 24 | #endif 25 | 26 | void startWebserver(); 27 | void stopWebserver(); 28 | void handleWebrequest(); 29 | void setMessage(const char *msg, uint8_t secs); 30 | 31 | #endif -------------------------------------------------------------------------------- /include/wlan.h: -------------------------------------------------------------------------------- 1 | /*************************************************************************** 2 | Copyright (c) 2019-2023 Lars Wessels 3 | 4 | This file a part of the "ESP8266 Wifi Power Meter" source code. 5 | https://github.com/lrswss/esp8266-wifi-power-meter 6 | 7 | Licensed under the MIT License. You may not use this file except in 8 | compliance with the License. You may obtain a copy of the License at 9 | 10 | https://opensource.org/licenses/MIT 11 | 12 | ***************************************************************************/ 13 | 14 | #ifndef _WLAN_H 15 | #define _WLAN_H 16 | 17 | #include 18 | #include 19 | 20 | extern uint16_t wifiReconnectCounter; 21 | extern uint32_t wifiOnlineTenthSecs; 22 | extern int8_t wifiStatus; 23 | 24 | #define WIFI_AP_SSID "WifiPowerMeter" 25 | #define WIFI_MIN_RSSI 25 26 | #define WIFI_CONFIG_TIMEOUT_SECS 300 27 | #define WIFI_CONNECT_TIMEOUT 30 28 | 29 | void startWifi(); 30 | void restartWifi(); 31 | void reconnectWifi(); 32 | void stopWifi(uint32_t currTime); 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2019-2022 Lars Wessels 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; Copyright (c) 2022-2023 Lars Wessels 2 | ; 3 | ; This file a part of the "ESP8266 Wifi Power Meter" source code. 4 | ; https://github.com/lrswss/esp8266-wifi-power-meter 5 | 6 | ; PlatformIO Project Configuration File 7 | ; Please visit documentation for the other options and examples 8 | ; https://docs.platformio.org/page/projectconf.html 9 | 10 | [platformio] 11 | default_envs = d1_mini 12 | description = Firmware for ESP8266 power meter 13 | 14 | [common] 15 | firmware_version = 242 16 | upload_speed = 460800 17 | monitor_speed = 115200 18 | port = /dev/tty.usbserial-1420 19 | lib_deps_builtin = 20 | lib_deps_all = 21 | arduinojson = ArduinoJson@>=6 22 | mqtt = PubSubClient 23 | eeprom = EEPROM_Rotate 24 | webserver = ESP8266WebServer 25 | wifimanager = WiFiManager 26 | mavg = movingAvg 27 | build_flags = 28 | '-DFIRMWARE_VERSION=${common.firmware_version}' 29 | 30 | [env:d1_mini] 31 | platform = espressif8266 32 | board = d1_mini 33 | framework = arduino 34 | board_build.f_cpu = 80000000L 35 | build_flags = ${common.build_flags} 36 | lib_deps = ${common.lib_deps_all} 37 | upload_speed = ${common.upload_speed} 38 | monitor_speed = ${common.monitor_speed} 39 | monitor_port = ${common.port} 40 | upload_port = ${common.port} 41 | monitor_filters = esp8266_exception_decoder 42 | -------------------------------------------------------------------------------- /src/influx.cpp: -------------------------------------------------------------------------------- 1 | /*************************************************************************** 2 | Copyright (c) 2019-2022 Lars Wessels 3 | 4 | This file a part of the "ESP8266 Wifi Power Meter" source code. 5 | https://github.com/lrswss/esp8266-wifi-power-meter 6 | 7 | Licensed under the MIT License. You may not use this file except in 8 | compliance with the License. You may obtain a copy of the License at 9 | 10 | https://opensource.org/licenses/MIT 11 | 12 | ***************************************************************************/ 13 | 14 | #include "config.h" 15 | #include "influx.h" 16 | 17 | WiFiUDP udp; 18 | 19 | // For debugging purposes analog readings can be send to an InfluxDB via UDP 20 | // Visualizing the data with Grafana might help to determine the right threshold value 21 | void send2influx_udp(uint16_t counter, uint16_t threshold, uint16_t pulse) { 22 | static char measurement[128]; 23 | uint32_t requestTimer = 0; 24 | 25 | // create udp packet containing raw values according to influxdb line protocol 26 | // https://docs.influxdata.com/influxdb/v2.4/reference/syntax/line-protocol/ 27 | sprintf(measurement, "esp8266_power_meter,device=%s counter=%d,threshold=%d,pulse=%d\n", 28 | INFLUXDB_DEVICE_TAG, counter, threshold, pulse); 29 | 30 | // send udp packet 31 | Serial.printf("UDP (%s:%d): %s", INFLUXDB_HOST, INFLUXDB_UDP_PORT, measurement); 32 | requestTimer = millis(); 33 | udp.beginPacket(INFLUXDB_HOST, INFLUXDB_UDP_PORT); 34 | udp.print(String(measurement)); 35 | udp.endPacket(); 36 | Serial.printf(" (%ld ms)", millis() - requestTimer); 37 | } -------------------------------------------------------------------------------- /include/ferraris.h: -------------------------------------------------------------------------------- 1 | /*************************************************************************** 2 | Copyright (c) 2019-2023 Lars Wessels 3 | 4 | This file a part of the "ESP8266 Wifi Power Meter" source code. 5 | https://github.com/lrswss/esp8266-wifi-power-meter 6 | 7 | Licensed under the MIT License. You may not use this file except in 8 | compliance with the License. You may obtain a copy of the License at 9 | 10 | https://opensource.org/licenses/MIT 11 | 12 | ***************************************************************************/ 13 | 14 | #ifndef _FERRARIS_H 15 | #define _FERRARIS_H 16 | 17 | #include 18 | #include 19 | 20 | // sanity checks for web ui and settings import 21 | #define KWH_TURNS_MIN 75 22 | #define KWH_TURNS_MAX 800 23 | #define METER_ID_LEN_MIN 2 24 | #define POWER_AVG_SECS_MIN 60 25 | #define POWER_AVG_SECS_MAX 300 26 | #define PULSE_HISTORY_SIZE 64 27 | #define PULSE_THRESHOLD_MIN 10 28 | #define PULSE_THRESHOLD_MAX 1023 29 | #define READINGS_SPREAD_MIN 3 30 | #define READINGS_SPREAD_MAX 30 31 | #define READINGS_INTERVAL_MS_MIN 15 32 | #define READINGS_INTERVAL_MS_MAX 50 33 | #define READINGS_BUFFER_SECS_MIN 30 34 | #define READINGS_BUFFER_SECS_MAX 120 35 | #define THRESHOLD_TRIGGER_MIN 3 36 | #define THRESHOLD_TRIGGER_MAX 8 37 | #define DEBOUNCE_TIME_MS_MIN 1000 38 | #define DEBOUNCE_TIME_MS_MAX 3000 39 | 40 | typedef struct { 41 | float consumption; 42 | int16_t power; 43 | uint16_t size; 44 | uint16_t index; 45 | uint16_t spread; 46 | uint16_t max; 47 | uint16_t min; 48 | uint16_t offsetNoWifi; 49 | } ferrarisReadings_t; 50 | 51 | extern ferrarisReadings_t ferraris; 52 | extern bool thresholdCalculation; 53 | 54 | void initFerraris(); 55 | bool readFerraris(); 56 | void calibrateFerraris(); 57 | void resetWifiOffset(); 58 | 59 | #endif 60 | -------------------------------------------------------------------------------- /include/nvs.h: -------------------------------------------------------------------------------- 1 | /*************************************************************************** 2 | Copyright (c) 2019-2023 Lars Wessels 3 | 4 | This file a part of the "ESP8266 Wifi Power Meter" source code. 5 | https://github.com/lrswss/esp8266-wifi-power-meter 6 | 7 | Licensed under the MIT License. You may not use this file except in 8 | compliance with the License. You may obtain a copy of the License at 9 | 10 | https://opensource.org/licenses/MIT 11 | 12 | ***************************************************************************/ 13 | 14 | #ifndef _NVS_H 15 | #define _NVS_H 16 | 17 | #include 18 | #include 19 | #include 20 | 21 | #define EEPROM_ADDR 10 22 | #define BACKUP_CYCLE_MIN 60 23 | #define BACKUP_CYCLE_MAX 180 24 | 25 | typedef struct { 26 | uint32_t counterTotal; 27 | uint32_t counterOffset; 28 | uint16_t pulseThreshold; 29 | uint16_t turnsPerKwh; 30 | uint16_t backupCycleMin; 31 | bool calculateCurrentPower; 32 | bool calculatePowerMvgAvg; 33 | uint16_t powerAvgSecs; 34 | uint8_t readingsBufferSec; 35 | uint8_t readingsIntervalMs; 36 | uint8_t readingsSpreadMin; 37 | uint8_t aboveThresholdTrigger; 38 | uint16_t pulseDebounceMs; 39 | bool enableMQTT; 40 | char mqttBroker[65]; 41 | uint16_t mqttBrokerPort; 42 | char mqttBaseTopic[65]; 43 | uint16_t mqttIntervalSecs; 44 | bool mqttEnableAuth; 45 | char mqttUsername[33]; 46 | char mqttPassword[33]; 47 | bool mqttJSON; 48 | bool enableHADiscovery; 49 | bool mqttSecure; 50 | bool enablePowerSavingMode; 51 | bool enableInflux; 52 | char systemID[17]; 53 | uint8_t magic; 54 | } settings_t; // (252*8) 2012 bytes (must be less than EEP's size, see nvs.c) 55 | 56 | // use rotating pseudo EEPROM (actually ESP8266 flash) 57 | extern settings_t settings; 58 | 59 | void initNVS(); 60 | void saveNVS(bool rotate); 61 | void resetNVS(); 62 | const char* nvs2json(); 63 | bool json2nvs(const char* buf, size_t size); 64 | 65 | #endif -------------------------------------------------------------------------------- /restful.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | RESTful 7 | 14 | 36 | 37 |
38 |

Wifi Power Meter

39 |

Enter IP-Address: 40 |
41 |

42 |
43 |

TotalCounter:---

44 |

TotalConsumption:--- kWh

45 |

CurrentConsumption:--- Watt

46 |

Uptime:-- minutes

47 |

RSSI:-- dBm

48 | 49 |
50 |
51 | 52 | 53 | -------------------------------------------------------------------------------- /include/mqtt.h: -------------------------------------------------------------------------------- 1 | /*************************************************************************** 2 | Copyright (c) 2019-2023 Lars Wessels 3 | 4 | This file a part of the "ESP8266 Wifi Power Meter" source code. 5 | https://github.com/lrswss/esp8266-wifi-power-meter 6 | 7 | Licensed under the MIT License. You may not use this file except in 8 | compliance with the License. You may obtain a copy of the License at 9 | 10 | https://opensource.org/licenses/MIT 11 | 12 | ***************************************************************************/ 13 | 14 | #ifndef _MQTT_H 15 | #define _MQTT_H 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | #define MQTT_CLIENT_ID "WifiPowerMeter_%x" 24 | #define MQTT_SUBTOPIC_CNT "counter" 25 | #define MQTT_SUBTOPIC_CONS "consumption" 26 | #define MQTT_SUBTOPIC_PWR "power" 27 | #define MQTT_SUBTOPIC_RUNT "runtime" 28 | #define MQTT_SUBTOPIC_RSSI "rssi" 29 | #define MQTT_SUBTOPIC_HEAP "freeheap" 30 | #define MQTT_SUBTOPIC_WIFI "wificounter" 31 | #define MQTT_SUBTOPIC_TXINT "mqttinterval" 32 | #define MQTT_SUBTOPIC_ONAIR "wifisecs" 33 | #define MQTT_SUBTOPIC_PSAVE "powersave" 34 | #define MQTT_SUBTOPIC_RST "restart" 35 | #define MQTT_TOPIC_DISCOVER "homeassistant/sensor/wifipowermeter-" 36 | 37 | #define MQTT_BROKER_LEN_MIN 4 38 | #define MQTT_BROKER_PORT_MIN 1023 39 | #define MQTT_BROKER_PORT_MAX 65535 40 | #define MQTT_BASETOPIC_LEN_MIN 4 41 | #define MQTT_INTERVAL_MIN 30 42 | #define MQTT_INTERVAL_MAX 1800 43 | #define MQTT_USER_LEN_MIN 4 44 | #define MQTT_PASS_LEN_MIN 4 45 | 46 | // minimum mqtt message interval (secs) if power saving mode is enabled 47 | // wifi is switched off at least for given number of seconds 48 | #define MQTT_INTERVAL_MIN_POWERSAVING 180 49 | 50 | // power saving mode enables moving average for current power reading 51 | // to return reasonable values 52 | #define POWER_AVG_SECS_POWERSAVING 90 53 | 54 | // retry connecting to MQTT broker after given number of seconds 55 | #define MQTT_CONN_RETRY_SECS 15 56 | 57 | void mqttPublish(); 58 | void mqttDisconnect(bool unsetHAdiscovery); 59 | void mqttLoop(); 60 | void mqttUnsetTopic(const char* topic); 61 | 62 | #endif 63 | -------------------------------------------------------------------------------- /src/utils.cpp: -------------------------------------------------------------------------------- 1 | /*************************************************************************** 2 | Copyright (c) 2019-2023 Lars Wessels 3 | 4 | This file a part of the "ESP8266 Wifi Power Meter" source code. 5 | https://github.com/lrswss/esp8266-wifi-power-meter 6 | 7 | Licensed under the MIT License. You may not use this file except in 8 | compliance with the License. You may obtain a copy of the License at 9 | 10 | https://opensource.org/licenses/MIT 11 | 12 | ***************************************************************************/ 13 | 14 | #include "utils.h" 15 | #include "wlan.h" 16 | #include "nvs.h" 17 | #include "mqtt.h" 18 | 19 | 20 | // rollover safe comparison for given timestamp with millis() 21 | int32_t tsDiff(uint32_t tsMillis) { 22 | int32_t diff = millis() - tsMillis; 23 | if (diff < 0) 24 | return abs(diff); 25 | else 26 | return diff; 27 | } 28 | 29 | 30 | // returns total runtime (day/hours/minutes) as a string 31 | // has internal time keeping to cope with millis() rollover after 49 days 32 | // should be called from time to time to update interal counter 33 | // if minutesOnly is set return local runtime in minutes (for display in HA) 34 | char* getRuntime(bool minutesOnly) { 35 | static uint32_t lastMillis = 0; 36 | static uint32_t seconds = 0; 37 | static char runtime[16]; 38 | 39 | seconds += tsDiff(lastMillis) / 1000; 40 | lastMillis = millis(); 41 | 42 | if (minutesOnly) { 43 | sprintf(runtime, "%d", seconds/60); 44 | } else { 45 | uint16_t days = seconds / 86400 ; 46 | uint8_t hours = (seconds % 86400) / 3600; 47 | uint8_t minutes = ((seconds % 86400) % 3600) / 60; 48 | sprintf(runtime, "%dd %dh %dm", days, hours, minutes); 49 | } 50 | 51 | return runtime; 52 | } 53 | 54 | 55 | // returns hardware system id (ESP's chip id) 56 | // or value set in web ui as 'power meter id' 57 | // used for MQTT topic 58 | String systemID() { 59 | if (strlen(settings.systemID) < 1) { 60 | snprintf(settings.systemID, sizeof(settings.systemID)-1, "%06X",ESP.getChipId()); 61 | } 62 | return String(settings.systemID); 63 | } 64 | 65 | 66 | // save current counter value to eeprom and restart ESP 67 | void restartSystem() { 68 | mqttDisconnect(true); 69 | saveNVS(false); 70 | Serial.println(F("Restart...")); 71 | delay(1000); 72 | ESP.restart(); 73 | } 74 | 75 | 76 | // blinking led 77 | // Wemos D1 mini: LOW = on, HIGH = off 78 | void blinkLED(uint8_t repeat, uint16_t pause) { 79 | for (uint8_t i = 0; i < repeat; i++) { 80 | digitalWrite(LED_BUILTIN, LOW); 81 | delay(pause); 82 | digitalWrite(LED_BUILTIN, HIGH); 83 | if (repeat > 1) 84 | delay(pause); 85 | } 86 | } 87 | 88 | 89 | // toggle state of LED on each call 90 | void toggleLED() { 91 | static byte ledState = LOW; 92 | ledState = !ledState; 93 | digitalWrite(LED_BUILTIN, ledState); 94 | } 95 | 96 | 97 | // turn LED on or off 98 | // Wemos D1 mini: LOW = on, HIGH = off 99 | void switchLED(bool state) { 100 | digitalWrite(LED_BUILTIN, state ? LOW : HIGH); 101 | } 102 | 103 | 104 | -------------------------------------------------------------------------------- /include/config.h: -------------------------------------------------------------------------------- 1 | 2 | /*************************************************************************** 3 | Copyright (c) 2019-2023 Lars Wessels 4 | 5 | This file a part of the "ESP8266 Wifi Power Meter" source code. 6 | https://github.com/lrswss/esp8266-wifi-power-meter 7 | 8 | Licensed under the MIT License. You may not use this file except in 9 | compliance with the License. You may obtain a copy of the License at 10 | 11 | https://opensource.org/licenses/MIT 12 | 13 | ***************************************************************************/ 14 | 15 | #ifndef _CONFIG_H 16 | #define _CONFIG_H 17 | 18 | // uncomment for german ui, defaults to english 19 | #define LANGUAGE_EN 20 | 21 | // 22 | // the following options can also be set in web ui 23 | // 24 | 25 | // this value has to be set according 26 | // to the specs of your ferraris meter 27 | #define TURNS_PER_KWH 75 28 | 29 | // calculate current power consumption (watts) from previous marker interval; 30 | // when the ferraris disk rotates rather slowly on low power consumption 31 | // this value can only be updated about once every 1-2 minutes 32 | #define CALCULATE_CURRENT_POWER 33 | 34 | // optionally apply a moving average to power consumption calculations 35 | // interval used for average calculation, set to 0 to disable 36 | #define POWER_AVG_SECS 120 37 | 38 | // publish power meter readings via MQTT (optional) 39 | //#define MQTT_ENABLE 40 | #define MQTT_PUBLISH_JSON 41 | #define MQTT_BROKER_HOSTNAME "__mqtt_broker__" 42 | #define MQTT_BROKER_PORT 1883 43 | #define MQTT_BASE_TOPIC "__mqtt_topic__" 44 | #define MQTT_PUBLISH_INTERVAL_SEC 60 45 | 46 | // uncomment to enable MQTT authentication 47 | //#define MQTT_USERNAME "admin" 48 | //#define MQTT_PASSWORD "secret" 49 | 50 | // uncomment to enable encrypted MQTT connection 51 | // MQTT must have TLS enabled on port 8883 52 | //#define MQTT_USE_TLS 53 | 54 | // uncomment to enable Home Assistant MQTT auto discovery 55 | //#define MQTT_HA_AUTO_DISCOVERY 56 | 57 | // switch off Wifi inbetween MQTT messages (95mA -> 35mA) 58 | // If enabled, you cannot set the MQTT publish interval below 59 | // MQTT_INTERVAL_MIN_POWERSAVING_SECS (see mqtt.h). 60 | // Since Wifi is switched on only for 2 seconds to publish meter 61 | // readings the web ui will be inaccessible. To regain access to 62 | // the web ui you need to power down the Wifi power meter or publish 63 | // a MQTT message to (cmd/powersave 0) to disable power saving mode 64 | // internal web server will run for 5 min. before switching back 65 | // to power saving mode unless you disable the option under settings 66 | //#define POWER_SAVING_MODE 67 | 68 | // optional preset power meter's id (e.g. number of ferraris meter) 69 | // defaults to last 3 octets of ESP8266's MAC address 70 | // (string with max. 16 characters) 71 | //#define SYSTEM_ID "12345678" 72 | 73 | // the following settings should be changed with care 74 | // better use web ui (expert settings) for fine-tuning 75 | #define READINGS_BUFFER_SEC 90 76 | #define READINGS_INTERVAL_MS 25 77 | #define READINGS_SPREAD_MIN 3 78 | #define ABOVE_THRESHOLD_TRIGGER 3 79 | #define PULSE_DEBOUNCE_MS 2000 80 | #define BACKUP_CYCLE_MIN 60 81 | 82 | // For debugging purposes only 83 | // Raw analog readings (READINGS_INTERVAL_MS) from the IR sensor are 84 | // send to an InfluxDB which has to configured to accept data on a 85 | // dedicated UDP port. Can also be enabled/disabled in web ui but HOST, 86 | // PORT, TAG need to preset here. Use this if threshold auto-detection 87 | // fails or you want to fine-tune it 88 | //#define INFLUXDB_ENABLE 89 | #define INFLUXDB_HOST "__influxdb_server__" 90 | #define INFLUXDB_UDP_PORT 8089 91 | #define INFLUXDB_DEVICE_TAG "__wifipowermeter__" 92 | 93 | // to make Arduino IDE happy 94 | // version number is set in platformio.ini 95 | #ifndef FIRMWARE_VERSION 96 | #define FIRMWARE_VERSION 242 97 | #endif 98 | 99 | // set default port for MQTT over TLS 100 | #ifdef MQTT_USE_TLS 101 | #undef MQTT_BROKER_PORT 102 | #define MQTT_BROKER_PORT 8883 103 | #endif 104 | 105 | // enable MQTT and JSON if HA discovery is set 106 | #ifdef MQTT_HA_AUTO_DISCOVERY 107 | #ifndef MQTT_ENABLE 108 | #define MQTT_ENABLE 109 | #endif 110 | #ifndef MQTT_PUBLISH_JSON 111 | #define MQTT_PUBLISH_JSON 112 | #endif 113 | #endif 114 | 115 | #ifndef MQTT_ENABLE 116 | #undef POWER_SAVING_MODE 117 | #endif 118 | 119 | #endif 120 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | /*************************************************************************** 2 | ESP8266 Wifi Power Meter (Ferraris Counter) 3 | 4 | Hardware: Wemos D1 mini board + TCRT5000 IR sensor 5 | https://github.com/lrswss/esp8266-wifi-power-meter/ 6 | 7 | (c) 2019-2023 Lars Wessels 8 | 9 | Permission is hereby granted, free of charge, to any person 10 | obtaining a copy of this software and associated documentation 11 | files (the "Software"), to deal in the Software without restriction, 12 | including without limitation the rights to use, copy, modify, 13 | merge, publish, distribute, sublicense, and/or sell copies of the 14 | Software, and to permit persons to whom the Software is furnished 15 | to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be 18 | included in all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 23 | IN NO EVENT SHALL THEAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 24 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 25 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 26 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 27 | 28 | ***************************************************************************/ 29 | 30 | #include "config.h" 31 | #include "mqtt.h" 32 | #include "utils.h" 33 | #include "wlan.h" 34 | #include "ferraris.h" 35 | #include "web.h" 36 | #include "nvs.h" 37 | 38 | 39 | void setup() { 40 | Serial.begin(115200); 41 | delay(1000); 42 | 43 | Serial.printf("\n\nStarting WifiPowerMeter v%d\n", FIRMWARE_VERSION); 44 | Serial.println(F("https://github.com/lrswss/esp8266-wifi-power-meter")); 45 | Serial.printf("Compiled on %s, %s\n\n",__DATE__, __TIME__); 46 | 47 | initNVS(); 48 | if (!settings.enableMQTT) 49 | Serial.println(F("Publishing of meter readings via MQTT disabled")); 50 | if (!settings.enableInflux) 51 | Serial.println(F("Streaming of raw sensor readings to InfluxDB disabled")); 52 | 53 | // setup LED on Wemos D1 mini 54 | // Wemos D1 mini: LOW = on, HIGH = off 55 | pinMode(LED_BUILTIN, OUTPUT); 56 | digitalWrite(LED_BUILTIN, HIGH); 57 | 58 | initFerraris(); 59 | startWifi(); 60 | startWebserver(); 61 | getRuntime(false); // start timer 62 | } 63 | 64 | 65 | void loop() { 66 | static uint32_t previousMeasurementMillis = 0; 67 | static uint32_t prevLoopTimer = 0; 68 | static uint32_t busyTime = 0; 69 | 70 | // scan ferraris disk for red marker 71 | if (tsDiff(previousMeasurementMillis) > settings.readingsIntervalMs) { 72 | previousMeasurementMillis = millis(); 73 | if (readFerraris()) { 74 | blinkLED(2, 200); 75 | if (settings.enableMQTT && wifiStatus == 1) { 76 | reconnectWifi(); 77 | mqttPublish(); 78 | if (settings.enablePowerSavingMode) 79 | stopWifi(busyTime); 80 | } 81 | } 82 | } 83 | 84 | // run tasks once every second 85 | if (tsDiff(prevLoopTimer) >= 1000) { 86 | prevLoopTimer = millis(); 87 | busyTime += 1; 88 | 89 | // regular MQTT publish interval (if enabled) 90 | // if power saving is enabled, start/stop Wifi before/after MQTT message 91 | if (settings.enableMQTT) { 92 | if (!(busyTime % settings.mqttIntervalSecs)) { 93 | if (settings.enablePowerSavingMode) 94 | startWifi(); 95 | mqttPublish(); 96 | } 97 | // in power saving mode turn off Wifi 2 seconds after publishing data 98 | // needed to trigger mqttLoop() below before going offline again 99 | if (busyTime > 2 && !((busyTime-2) % settings.mqttIntervalSecs)) 100 | stopWifi(settings.enablePowerSavingMode ? busyTime : 0); 101 | } 102 | 103 | // check Wifi uplink and try to reconnect (every 30 sec.) if 104 | // not in power saving mode; LED is on if Wifi uplink is down 105 | if (wifiStatus == 1) { 106 | reconnectWifi(); 107 | 108 | // flash LED every 5 seconds if in power saving mode 109 | } else if (!(busyTime % 5)) { 110 | blinkLED(1, 50); 111 | } 112 | 113 | // frequently save counter readings and threshold to EEPROM 114 | if (!(busyTime % (settings.backupCycleMin * 60))) 115 | saveNVS(true); 116 | } 117 | 118 | if (wifiStatus == 1) { 119 | handleWebrequest(); 120 | if (settings.enableMQTT) 121 | mqttLoop(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/wlan.cpp: -------------------------------------------------------------------------------- 1 | /*************************************************************************** 2 | Copyright (c) 2019-2023 Lars Wessels 3 | 4 | This file a part of the "ESP8266 Wifi Power Meter" source code. 5 | https://github.com/lrswss/esp8266-wifi-power-meter 6 | 7 | Licensed under the MIT License. You may not use this file except in 8 | compliance with the License. You may obtain a copy of the License at 9 | 10 | https://opensource.org/licenses/MIT 11 | 12 | ***************************************************************************/ 13 | 14 | #include "config.h" 15 | #include "wlan.h" 16 | #include "utils.h" 17 | #include "web.h" 18 | #include "nvs.h" 19 | 20 | uint32_t wifiOnlineTenthSecs = 0; 21 | uint16_t wifiReconnectCounter = 0; 22 | int8_t wifiStatus = -1; 23 | 24 | static WiFiManager wm; 25 | static String apname = String(WIFI_AP_SSID) + "-" + systemID(); 26 | static uint32_t wifiStartMillis; 27 | 28 | 29 | // start access point to configure WiFi or 30 | // restart WiFi is recently disabled in power saving mode 31 | void startWifi() { 32 | static uint8_t connectionFailed = 0; 33 | 34 | // WiFi is configured/online 35 | if (wifiStatus == 1) { 36 | return; 37 | 38 | } else if (wifiStatus == -1) { // first Wifi connection attempt after power up 39 | Serial.println(F("\nStart WiFiManager...")); 40 | Serial.printf("Check for SSID >> %s << if system does not connect to your WiFi network\n", apname.c_str()); 41 | wm.setDebugOutput(false); 42 | wm.setMinimumSignalQuality(WIFI_MIN_RSSI); 43 | wm.setConfigPortalTimeout(WIFI_CONFIG_TIMEOUT_SECS); 44 | wm.setConnectTimeout(WIFI_CONNECT_TIMEOUT); 45 | 46 | } else { 47 | // only triggered on WiFi restart in power saving mode (wifiStatus = 0) 48 | wifiStartMillis = millis(); 49 | Serial.printf("Restart WiFi uplink to %s...\n", WiFi.SSID().c_str()); 50 | WiFi.forceSleepWake(); 51 | WiFi.mode(WIFI_STA); 52 | wm.setConfigPortalTimeout(10); // use shorter timeouts to avoid red marker misses 53 | wm.setConnectTimeout(20); 54 | } 55 | 56 | switchLED(true); 57 | if (!wm.autoConnect(apname.c_str())) { 58 | // just return if previously working Wifi doesn't repeatly fail 59 | if (wifiStatus == 1 && ++connectionFailed <= 5) 60 | return; 61 | else if (wifiStatus == 0 && ++connectionFailed <= 2) 62 | return; 63 | Serial.println(F("Failed to connect to a WiFi network...restarting!")); 64 | 65 | blinkLED(20, 100); 66 | saveNVS(true); 67 | ESP.restart(); 68 | } 69 | Serial.printf("Connected to SSID %s with RSSI %d dBm on IP ", WiFi.SSID().c_str(), WiFi.RSSI()); 70 | Serial.println(WiFi.localIP()); 71 | blinkLED(4, 100); 72 | connectionFailed = 0; 73 | wifiStatus = 1; 74 | } 75 | 76 | 77 | // reconnect to Wifi if at least 30 secs have past since last retry 78 | // turn on system LED if Wifi uplink is down 79 | void reconnectWifi() { 80 | static uint32_t lastReconnect = 0; 81 | 82 | // no need to reconnect... 83 | if (WiFi.isConnected()) 84 | return; 85 | 86 | switchLED(true); 87 | if ((millis() - lastReconnect) > 30000) { 88 | lastReconnect = millis(); 89 | 90 | // count and publish Wifi reconnects unless in power saving mode 91 | // could help identify an unstable uplink 92 | if (!settings.enablePowerSavingMode) 93 | wifiReconnectCounter++; 94 | else 95 | wifiReconnectCounter = 0; 96 | 97 | Serial.printf("Trying to reconnect to SSID %s...", WiFi.SSID().c_str()); 98 | if (WiFi.reconnect()) { 99 | Serial.println(F("OK")); 100 | switchLED(false); 101 | } else { 102 | Serial.println(F("failed!")); 103 | } 104 | } 105 | } 106 | 107 | 108 | // turn off Wifi in power saving mode 109 | // otherwise reset Wifi timeout counter 110 | void stopWifi(uint32_t currTime) { 111 | static uint32_t initTime = 0; 112 | uint32_t wifiOnlineMs = 0; 113 | 114 | if (currTime == 0) { 115 | initTime = 0; // reset Wifi timeout 116 | return; 117 | } else if (!initTime) { 118 | // start Wifi timeout timer 119 | initTime = currTime; 120 | } 121 | 122 | // switch off webserver 5 minutes after power saving mode 123 | // was enabled or if active after system reset/restart 124 | if (wifiStatus == 1 && (currTime - initTime) > 300) { 125 | wifiOnlineMs = millis() - wifiStartMillis; 126 | if (wifiOnlineMs < 60000) 127 | Serial.printf("Stopping WiFi to save power (after %d ms)\n", wifiOnlineMs); 128 | else 129 | Serial.println(F("Stopping WiFi to save power")); 130 | WiFi.mode(WIFI_OFF); 131 | WiFi.forceSleepBegin(); 132 | if (wifiStartMillis > 0) 133 | wifiOnlineTenthSecs += wifiOnlineMs/100; 134 | wifiStatus = 0; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/nvs.cpp: -------------------------------------------------------------------------------- 1 | /*************************************************************************** 2 | Copyright (c) 2019-2023 Lars Wessels 3 | 4 | This file a part of the "ESP8266 Wifi Power Meter" source code. 5 | https://github.com/lrswss/esp8266-wifi-power-meter 6 | 7 | Licensed under the MIT License. You may not use this file except in 8 | compliance with the License. You may obtain a copy of the License at 9 | 10 | https://opensource.org/licenses/MIT 11 | 12 | ***************************************************************************/ 13 | 14 | #include "config.h" 15 | #include "nvs.h" 16 | #include "ferraris.h" 17 | #include "mqtt.h" 18 | 19 | EEPROM_Rotate EEP; 20 | settings_t settings; 21 | settings_t defaultSettings = { 22 | 0, 23 | 0, 24 | 0, 25 | TURNS_PER_KWH, 26 | BACKUP_CYCLE_MIN, 27 | #ifdef CALCULATE_CURRENT_POWER 28 | true, 29 | #else 30 | false, 31 | #endif 32 | #if POWER_AVG_SECS > 0 33 | true, 34 | #else 35 | false, 36 | #endif 37 | POWER_AVG_SECS, 38 | READINGS_BUFFER_SEC, 39 | READINGS_INTERVAL_MS, 40 | READINGS_SPREAD_MIN, 41 | ABOVE_THRESHOLD_TRIGGER, 42 | PULSE_DEBOUNCE_MS, 43 | #ifdef MQTT_ENABLE 44 | true, 45 | #else 46 | false, 47 | #endif 48 | MQTT_BROKER_HOSTNAME, 49 | MQTT_BROKER_PORT, 50 | MQTT_BASE_TOPIC, 51 | MQTT_PUBLISH_INTERVAL_SEC, 52 | #if defined(MQTT_USERNAME) && defined(MQTT_PASSWORD) 53 | true, 54 | MQTT_USERNAME, 55 | MQTT_PASSWORD, 56 | #else 57 | false, 58 | "none", 59 | "none", 60 | #endif 61 | #ifdef MQTT_PUBLISH_JSON 62 | true, 63 | #else 64 | false, 65 | #endif 66 | #ifdef MQTT_HA_AUTO_DISCOVERY 67 | true, 68 | #else 69 | false, 70 | #endif 71 | #ifdef MQTT_USE_TLS 72 | true, 73 | #else 74 | false, 75 | #endif 76 | #ifdef POWER_SAVING_MODE 77 | true, 78 | #else 79 | false, 80 | #endif 81 | #ifdef INFLUXDB_ENABLE 82 | true, 83 | #else 84 | false, 85 | #endif 86 | #ifdef SYSTEM_ID 87 | SYSTEM_ID, 88 | #else 89 | "", 90 | #endif 91 | 0x77 92 | }; 93 | 94 | static void readNVS() { 95 | settings_t settingsNVS; 96 | EEP.get(EEPROM_ADDR, settingsNVS); 97 | if (settingsNVS.magic == 0x77) { 98 | memcpy(&settings, &settingsNVS, sizeof(settings_t)); 99 | Serial.printf("Restored system settings from NVS (%d bytes)\n", sizeof(settings)*8); 100 | Serial.printf("Counter(%d), Offset(", settings.counterTotal); 101 | Serial.print(settings.counterOffset / 10.0); 102 | Serial.printf("), Threshold(%d)\n", settings.pulseThreshold); 103 | } else { 104 | memcpy(&settings, &defaultSettings, sizeof(settings_t)); 105 | Serial.printf("Initialized NVS (%d bytes) with default setttings\n", sizeof(settings)*8); 106 | saveNVS(true); 107 | } 108 | Serial.printf("Backup settings to NVS every %d minutes\n", settings.backupCycleMin); 109 | } 110 | 111 | 112 | void initNVS() { 113 | EEP.size(3); 114 | EEP.begin(2048); // must be larger than size of settings_t in nvs.h 115 | readNVS(); 116 | } 117 | 118 | 119 | // restore default settings 120 | void resetNVS() { 121 | Serial.println(F("Reset settings to default values")); 122 | memcpy(&settings, &defaultSettings, sizeof(settings_t)); 123 | saveNVS(false); 124 | } 125 | 126 | 127 | // save current reading to pseudo eeprom 128 | void saveNVS(bool rotate) { 129 | Serial.println(F("Save settings to NVS")); 130 | EEP.put(EEPROM_ADDR, settings); 131 | EEP.rotate(rotate); 132 | EEP.commit(); 133 | } 134 | 135 | 136 | // export system settings as JSON string 137 | const char* nvs2json() { 138 | DynamicJsonDocument JSON(768); 139 | static char buf[896]; 140 | 141 | JSON["pulseThreshold"] = settings.pulseThreshold; 142 | JSON["turnsPerKwh"] = settings.turnsPerKwh; 143 | JSON["backupCycleMin"] = settings.backupCycleMin; 144 | JSON["calculateCurrentPower"] = settings.calculateCurrentPower; 145 | JSON["calculatePowerMvgAvg"] = settings.calculatePowerMvgAvg; 146 | JSON["powerAvgSecs"] = settings.powerAvgSecs; 147 | JSON["readingsBufferSec"] = settings.readingsBufferSec; 148 | JSON["readingsIntervalMs"] = settings.readingsIntervalMs; 149 | JSON["readingsSpreadMin"] = settings.readingsSpreadMin; 150 | JSON["aboveThresholdTrigger"] = settings.aboveThresholdTrigger; 151 | JSON["pulseDebounceMs"] = settings.pulseDebounceMs; 152 | JSON["enableMQTT"] = settings.enableMQTT; 153 | JSON["mqttBroker"] = settings.mqttBroker; 154 | JSON["mqttBrokerPort"] = settings.mqttBrokerPort; 155 | JSON["mqttBaseTopic"] = settings.mqttBaseTopic; 156 | JSON["mqttIntervalSecs"] = settings.mqttIntervalSecs; 157 | JSON["mqttEnableAuth"] = settings.mqttEnableAuth; 158 | if (settings.mqttEnableAuth) { 159 | JSON["mqttUsername"] = settings.mqttUsername; 160 | JSON["mqttPassword"] = settings.mqttPassword; 161 | } 162 | JSON["mqttJSON"] = settings.mqttJSON; 163 | JSON["mqttSecure"] = settings.mqttSecure; 164 | JSON["enableHADiscovery"] = settings.enableHADiscovery; 165 | JSON["enablePowerSavingMode"] = settings.enablePowerSavingMode; 166 | JSON["enableInflux"] = settings.enableInflux; 167 | JSON["systemID"] = settings.systemID; 168 | JSON["version"] = FIRMWARE_VERSION; 169 | 170 | memset(buf, 0, sizeof(buf)); 171 | serializeJsonPretty(JSON, buf, sizeof(buf)-1); 172 | if (JSON.overflowed()) 173 | return NULL; 174 | return buf; 175 | } 176 | 177 | 178 | // restore system settings from uploaded JSON file 179 | bool json2nvs(const char* buf, size_t size) { 180 | DynamicJsonDocument JSON(1024); 181 | uint16_t mqttIntervalMinSecs; 182 | 183 | DeserializationError error = deserializeJson(JSON, buf, size); 184 | if (error) { 185 | Serial.printf("Failed to import settings, deserialize error %s!\n", error.c_str()); 186 | return false; 187 | } else if (JSON["version"] > FIRMWARE_VERSION) { 188 | Serial.printf("Failed to import settings, invalid configuration v%d!\n", int(JSON["version"])); 189 | return false; 190 | } 191 | 192 | if (JSON["pulseThreshold"] >= PULSE_THRESHOLD_MIN && JSON["pulseThreshold"] <= PULSE_THRESHOLD_MAX) 193 | settings.pulseThreshold = JSON["pulseThreshold"]; 194 | if (JSON["turnsPerKwh"] >= KWH_TURNS_MIN && JSON["turnsPerKwh"] <= KWH_TURNS_MAX) 195 | settings.turnsPerKwh = JSON["turnsPerKwh"]; 196 | if (JSON["backupCycleMin"] >= BACKUP_CYCLE_MIN && JSON["backupCycleMin"] <= BACKUP_CYCLE_MAX) 197 | settings.backupCycleMin = JSON["backupCycleMin"]; 198 | settings.calculateCurrentPower = JSON["calculateCurrentPower"]; 199 | settings.calculatePowerMvgAvg = JSON["calculatePowerMvgAvg"]; 200 | if (JSON["powerAvgSecs"] >= POWER_AVG_SECS_MIN && JSON["powerAvgSecs"] <= POWER_AVG_SECS_MAX) 201 | settings.powerAvgSecs = JSON["powerAvgSecs"]; 202 | if (JSON["readingsBufferSec"] >= READINGS_BUFFER_SECS_MIN && JSON["readingsBufferSec"] <= READINGS_BUFFER_SECS_MAX) 203 | settings.readingsBufferSec = JSON["readingsBufferSec"]; 204 | if (JSON["readingsIntervalMs"] >= READINGS_INTERVAL_MS_MIN && JSON["readingsIntervalMs"] <= READINGS_INTERVAL_MS_MAX) 205 | settings.readingsIntervalMs = JSON["readingsIntervalMs"]; 206 | if (JSON["readingsSpreadMin"] >= READINGS_SPREAD_MIN && JSON["readingsSpreadMin"] <= READINGS_SPREAD_MAX) 207 | settings.readingsSpreadMin = JSON["readingsSpreadMin"]; 208 | if (JSON["aboveThresholdTrigger"] >= THRESHOLD_TRIGGER_MIN && JSON["aboveThresholdTrigger"] <= THRESHOLD_TRIGGER_MAX) 209 | settings.aboveThresholdTrigger = JSON["aboveThresholdTrigger"]; 210 | if (JSON["pulseDebounceMs"] >= DEBOUNCE_TIME_MS_MIN && JSON["pulseDebounceMs"] <= DEBOUNCE_TIME_MS_MAX) 211 | settings.pulseDebounceMs = JSON["pulseDebounceMs"]; 212 | 213 | settings.enableMQTT = JSON["enableMQTT"]; 214 | if (strlen(JSON["mqttBroker"]) >= MQTT_BROKER_LEN_MIN) 215 | strlcpy(settings.mqttBroker, JSON["mqttBroker"], sizeof(settings.mqttBroker)); 216 | if (JSON["mqttBrokerPort"] > 1023) 217 | settings.mqttBrokerPort = JSON["mqttBrokerPort"]; 218 | if (strlen(JSON["mqttBaseTopic"]) >= MQTT_BASETOPIC_LEN_MIN) 219 | strlcpy(settings.mqttBaseTopic, JSON["mqttBaseTopic"], sizeof(settings.mqttBaseTopic)); 220 | 221 | settings.enablePowerSavingMode = JSON["enablePowerSavingMode"]; 222 | if (settings.enablePowerSavingMode) 223 | mqttIntervalMinSecs = MQTT_INTERVAL_MIN_POWERSAVING; 224 | else 225 | mqttIntervalMinSecs = MQTT_INTERVAL_MIN; 226 | if (JSON["mqttIntervalSecs"] >= mqttIntervalMinSecs && JSON["mqttIntervalSecs"] <= MQTT_INTERVAL_MAX) 227 | settings.mqttIntervalSecs = JSON["mqttIntervalSecs"]; 228 | 229 | settings.mqttEnableAuth = JSON["mqttEnableAuth"]; 230 | if (settings.mqttEnableAuth) { 231 | if (strlen(JSON["mqttUsername"]) >= MQTT_USER_LEN_MIN) 232 | strlcpy(settings.mqttUsername, JSON["mqttUsername"], sizeof(settings.mqttUsername)); 233 | if (strlen(JSON["mqttPassword"]) >= MQTT_PASS_LEN_MIN) 234 | strlcpy(settings.mqttPassword, JSON["mqttPassword"], sizeof(settings.mqttPassword)); 235 | } 236 | settings.mqttJSON = JSON["mqttJSON"]; 237 | settings.enableHADiscovery = JSON["enableHADiscovery"]; 238 | settings.mqttSecure = JSON["mqttSecure"]; 239 | 240 | settings.enableInflux = JSON["enableInflux"]; 241 | if (strlen(JSON["systemID"]) >= METER_ID_LEN_MIN) 242 | strlcpy(settings.systemID, JSON["systemID"], sizeof(settings.systemID)); 243 | 244 | Serial.println(F("Successfully imported settings")); 245 | saveNVS(true); 246 | 247 | return true; 248 | } 249 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP8266 Wifi Power Meter (Ferraris) 2 | 3 | ## Description 4 | 5 |

Wemos D1 mini
  6 | with TCRT5000 IR sensor mounted on ferraris electricity meter


7 | 8 | Using the analog output of an IR sensor, the rotation of a Ferraris 9 | electricity meter are detected. The total meter reading (KWh) and current 10 | consumption (Watt) are displayed on an embedded web page and optionally published 11 | via MQTT. As visual feedback the blue LED on the Wemos D1 mini board blinks if 12 | the red marker has been detected. An embedded web server helps to adjust 13 | various parameters to fine-tune the sensor setup and also offers a simple 14 | RESTful interface to retrieve the meter reading. The total consumption 15 | reading and sensor settings are frequently saved to (pseudo) EEPROM to 16 | migate counter resets due to possible intermediate power failures. 17 | 18 | ## Shopping list 19 | 20 | * [Wemos D1 mini](https://arduino-projekte.info/wemos-d1-mini/) development board 21 | * [TCRT5000](https://www.google.com/search?q=TCRT5000) IR sensor board 22 | * 5V (250mA) Power supply with micro USB plug 23 | 24 | You don't have to use a Wemos D1 mini board. A NodeMCU or any other ESP8266 25 | based development board will work just fine. 26 | 27 | ## Using the TCRT5000's analog output (A0) instead of D0 28 | 29 | Since the low/high pulses generated by the widely used TCRT5000 sensor on D0 are 30 | not very reliable (e.g. multiple counts for single pass of the "red marker" on 31 | the ferraris disk) its analog output (A0) is used to automatically determine a 32 | suitable threshold value to detect the marker. This way you don't need to fiddle 33 | around with the small potentiometer on the sensor board to find the "right" trigger 34 | threshold for your specific sensor mounting setup. 35 | 36 | ## Compiling the firmware for the Wemos D1 mini 37 | 38 | While version 1.x was written and compiled with the Arduino IDE, version 2.x 39 | was developed with [Visual Studio Code](https://code.visualstudio.com/) and 40 | the [PlatformIO add-on](https://platformio.org/install/ide?install=vscode). 41 | The later is more convenient for larger projects especially when you split 42 | your code across multiple files and are used to syntax highlighting and 43 | seamless Git integration. But of course you can still compile it with the 44 | Arduino IDE (see instructions below). 45 | 46 | After you've downloaded Visual Studio Code and installed the PlatformIO add-on, 47 | clone or download this repository to your pc or laptop, open the project folder 48 | in VSC and edit the file `include/config.h`. Starting with version 2.x all settings 49 | found in this file can also be set in the web interface of the Wifi Power Meter. 50 | The only thing you need to set before you compile the firmware is the language 51 | of the web interface which defaults to English. For a German user interface just 52 | comment the line with `LANGUAGE_EN` at the beginning of the file. 53 | 54 | To build and upload the firmware via USB to your Wemos D1 mini or NodeMCU just 55 | click on the right arrow in the blue bar on the bottom of VSC. All necessary libraries 56 | (see `platformio.ini`) will be downloaded automatically. Eventually you'll see 57 | the upload starting in a terminal window. If it fails you probably need to adjust 58 | the settings for `port` in `plattformi.ini`. The project was developed on MacOS 59 | which uses UNIX style devices names. On Windows you need to specify a `COM` port. 60 | 61 | To compile and install the firmware with the Arduino IDE, you have to 62 | [add support for ESP8266 based boards](https://github.com/esp8266/Arduino) first 63 | using the boards manager. Then make sure, that the following libraries have been 64 | installed with the [Arduino IDE library manager](https://www.arduino.cc/en/Guide/Libraries): 65 | 66 | * [EPS8266Wifi](https://github.com/esp8266/Arduino/tree/master/libraries/ESP8266WiFi) 67 | * [ESP8266WebServer](https://github.com/esp8266/Arduino/tree/master/libraries/ESP8266WebServer) 68 | * [ArduinoJson](https://arduinojson.org/) 69 | * [PubSubClient](https://github.com/knolleary/pubsubclient/releases) 70 | * [EEPROM_Rotate](https://github.com/xoseperez/eeprom_rotate) 71 | * [WiFiManager](https://github.com/tzapu/WiFiManager) 72 | * [movingAvg](https://github.com/JChristensen/movingAvg) 73 | 74 | Since the Arduino IDE requires all files to be in one folder, you need to move 75 | the header files from `include` to `src`. Then rename `main.cpp` to `powermeter.ino` 76 | and finally the directory `src` to `powermeter` or the Arduino IDE will complain 77 | that the name of the main source file does not match the directory where it is located. 78 | Before you hit compile and upload, make sure that you've selected `Wemos D1 R2 & mini` 79 | or whatever ESP8266 board you are using as the target platform under `Tools`. 80 | 81 | ## Hardware Schematics 82 | 83 |

84 | Schematics für Wifi Power Meter 85 |

86 | 87 | Connect the 5V pin of the TCRT5000 sensor to 3.3V pin (not 5V!) of the Wemos 88 | board and its ground pin to GND. The analog output of the TCRT5000 sensor (A0) 89 | must be connected to the corresponding analog input (A0) of the Wemos. The Wemos 90 | can be powered with a small USB power supply (5V, around 250mA). 91 | 92 | ## Initial setup of WiFi connection 93 | 94 | After the first power-up, the [WifiManager](https://github.com/tzapu/WiFiManager) 95 | will create the access point `WifiPowerMeter-XXXXXX` (with no password) to connect 96 | the Wemos D1 to your local WiFi network. If you have set your WiFi credentials 97 | the ESP8266 will received an IP via DHCP (check serial debug message or your router). 98 | Point your favourite browser to the IP. If you can establish a connection to the 99 | embebbed web server, you can proceed mounting the sensor and the controller board 100 | on your ferraris meter. 101 | 102 | ## Mounting the sensor on the ferraris meter 103 | 104 | For mounting the TCRT5000 on your ferraris meter a little 3D printed case 105 | comes in handy. Thingiverse offers quite a few options like 106 | [this one](https://www.thingiverse.com/thing:2668168). 107 | 108 | Since the firmware also supports OTA updates, you only need to flash your ESP8266 109 | once before mounting it in your meter cabinet. Please note that all settings might 110 | be reset to their default values if the structure of the system settings stored in 111 | ESP8266's flash changes with a new firmware. To avoid having to start from scratch 112 | after a firmware update, **all system settings can be exported to a JSON file**, 113 | which you can upload after an update to restore your configuration. 114 | 115 | ## Setting the threshold to detect the red marker 116 | 117 | Make sure that during the initial calibration phase (default 90 secs.), which is 118 | triggered by `Calculate Threshold`, the ferraris disk is actually spinning fast 119 | enough. The calculation of the IR sensor pulse threshold should be based on 120 | at least two full rotations with the red marker passing the IR sensor twice. 121 | Turning on your oven or water kettle should help to speed up things. ;-) 122 | 123 | After the initial and hopefully successful calibration cycle, you only have to 124 | save the calculated threshold value with `Save Threshold` to switch the WiFi 125 | Power Meter to normal operation. You can tune the threshold value and other 126 | parameters under `Expert settings`, but you should be OK with the default settings. 127 | 128 | In a last configuration step you have to set `Rotations per kWh` under `Settings` 129 | to the value of the Ferraris meter (the default value is 75) and sync the 130 | current kWh reading of the meter with the setting `Current Consumption` of 131 | the Wifi Power Meter. 132 | 133 |

main page of
134 | embedded web server   settings of the wifi power meter   expert settings to tune the detection of
137 | the red marker


138 | 139 | ## Receive meter readings via MQTT or RESTful 140 | 141 | I'm using MQTT (enabled under `Settings`) to pass the meter readings to [Home 142 | Assistant](https://www.home-assistant.io) using its [discovery 143 | function](https://www.home-assistant.io/docs/mqtt/discovery/). Sensor readings 144 | are published to single topics or as JSON at `//state`. 145 | The address of the MQTT Broker, publishing interval, optional TLS encryption, 146 | `maintopic` and `sensorid` can be configured under `Settings`. 147 | 148 | The Wifi Power Meter also offers support for RESTful HTTP requests. Readings are 149 | available as JSON under `http:///readings`. See `restful.html` as an example. 150 | 151 | ## Power Saving Mode (experimental) 152 | 153 | If you have enabled `MQTT` and `Power Saving Mode` under `Settings`, the Wifi 154 | Power Meter will turn off WiFi between MQTT messages. This reduces the current 155 | consumption from 95mA to about 35mA (20mA ESP8266 and 15mA TCRT5000) which 156 | might be low enough to run the device from a single 18650 cell for a few days. 157 | The `Power Saving Mode` is experimental and was not tested over long period of 158 | time. When the ESP8266 reconnects to your WiFi network, which takes a few seconds, 159 | the IR sensor may miss the red marker resulting in slight errors in the total 160 | consumption readings. 161 | 162 | Please note that the web interface will be inaccessible after 5 minutes and 163 | thereafter if `Power Saving Mode` was enabled. The Wemos D1's blue LED 164 | will flash every 5 seconds. To regain access to the web interface you will need 165 | to power down the Wifi Power Meter or send the retained MQTT message `0` to 166 | `//cmd/powersave`. The MQTT publish interval which also 167 | determines the time for intermediate WiFi wakeups can be configured (in seconds) 168 | using the topic `//cmd/mqttinterval`. 169 | 170 | ## Debug readings with InfluxDB and Grafana 171 | 172 | For debugging purposes you can activate UDP streaming of raw A/D readings from the 173 | IR sensor to an [InfluxDB](https://www.influxdata.com/products/influxdb-overview/) 174 | under `Expert settings`. Contiously visualizing the readings with [Grafana](https://grafana.com/oss/grafana/) 175 | might help placing the TCRT5000 on your electricity meter to get better readings. 176 | 177 | Note that InfluxDB version >=2 requires the Telegraf 178 | [Socket Listener](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/socket_listener/README.md) 179 | plugin to process UDP line protocol streams. 180 | 181 |

182 | InfluxDB raw sensor data example 183 |

184 | 185 | ## Contributing 186 | 187 | Pull requests are welcome! For major changes, please open an issue first to discuss 188 | what you would like to change. 189 | 190 | ## License 191 | 192 | Copyright (c) 2019-2023 Lars Wessels 193 | This software was published under the MIT license. 194 | Please check the [license file](LICENSE). 195 | -------------------------------------------------------------------------------- /src/ferraris.cpp: -------------------------------------------------------------------------------- 1 | /*************************************************************************** 2 | Copyright (c) 2019-2023 Lars Wessels 3 | 4 | This file a part of the "ESP8266 Wifi Power Meter" source code. 5 | https://github.com/lrswss/esp8266-wifi-power-meter 6 | 7 | Licensed under the MIT License. You may not use this file except in 8 | compliance with the License. You may obtain a copy of the License at 9 | 10 | https://opensource.org/licenses/MIT 11 | 12 | ***************************************************************************/ 13 | 14 | #include "config.h" 15 | #include "ferraris.h" 16 | #include "web.h" 17 | #include "utils.h" 18 | #include "influx.h" 19 | #include "nvs.h" 20 | #include "wlan.h" 21 | 22 | 23 | static int16_t *pulseReadings; 24 | static movingAvg pulseInterval(PULSE_HISTORY_SIZE); 25 | bool thresholdCalculation = false; 26 | ferrarisReadings_t ferraris; 27 | 28 | 29 | // helper function for qsort() to sort the array with analog readings 30 | static int sortAsc(const void *val1, const void *val2) { 31 | uint16_t a = *((uint16_t *)val1); 32 | uint16_t b = *((uint16_t *)val2); 33 | return a - b; 34 | } 35 | 36 | 37 | // determine average pulse reading in the past 'count' number of ADC readings 38 | static uint16_t findPastAverage(uint16_t count) { 39 | uint32_t average = 0, index = count; 40 | 41 | for (uint16_t i = ferraris.index; i > 0; i--) { 42 | average += pulseReadings[i]; 43 | if (--index == 0) 44 | break; 45 | } 46 | if (index > 0) { 47 | for (uint16_t i = ferraris.size-1; i > ferraris.index; i--) { 48 | average += pulseReadings[i]; 49 | if (--index == 0) 50 | break; 51 | } 52 | } 53 | return ((average + (count / 2)) / count); 54 | } 55 | 56 | 57 | // reset sensor readings 58 | static void resetReadings() { 59 | memset(pulseReadings, 0, ferraris.size * sizeof(int16_t)); 60 | ferraris.consumption = (settings.counterTotal / (settings.turnsPerKwh * 1.0)) 61 | + (settings.counterOffset / 100.0); 62 | ferraris.power = settings.calculateCurrentPower ? -1 : -2; 63 | ferraris.index = 0; 64 | ferraris.spread = 0; 65 | ferraris.max = 0; 66 | ferraris.min = 0; 67 | ferraris.offsetNoWifi = 0; 68 | } 69 | 70 | 71 | // read an averaged value from the TCRT5000 IR sensor (5ms) 72 | static int16_t readIRSensor() { 73 | int16_t pulseReading; // 0-1023 74 | int16_t sumReadings = 0; 75 | 76 | for (uint8_t i = 0; i < 10; i++) { // average over 10 readings 77 | sumReadings += analogRead(A0); // about 100us 78 | delayMicroseconds(400); 79 | } 80 | pulseReading = int(sumReadings/10); 81 | 82 | if (settings.enableInflux) 83 | send2influx_udp(settings.counterTotal, 84 | (settings.pulseThreshold + ferraris.offsetNoWifi), pulseReading); 85 | return pulseReading; 86 | } 87 | 88 | 89 | // calculate valid threshold to detect the red marker 90 | // on ferraris disk from analog sensor readings 91 | static void calculateThreshold() { 92 | 93 | // sort array with analog readings 94 | qsort(pulseReadings, ferraris.size, sizeof(pulseReadings[0]), sortAsc); 95 | 96 | // min, max and slightly corrected spreadw of all readings 97 | ferraris.min = pulseReadings[0]; 98 | ferraris.max = pulseReadings[ferraris.size - 1]; 99 | ferraris.spread = pulseReadings[(int)(ferraris.size * 0.99)] - pulseReadings[(int)(ferraris.size * 0.01)]; 100 | 101 | if (ferraris.spread >= settings.readingsSpreadMin) { 102 | // My Ferraris disk has a diameter of approx. 9cm => circumference about 28.3cm 103 | // Length of marker on the disk is more or less 1cm => fraction of circumference about 1/30 => 3% 104 | // Thus after at least(!) one full rotation of the ferraris disk all analog sensor 105 | // readings above the 97% percentile should qualify as a suitable threshold values 106 | settings.pulseThreshold = pulseReadings[(int)(ferraris.size * 0.98)]; 107 | Serial.println(F("Calculation of new threshold for red marker succeeded.")); 108 | Serial.printf("Threshold (%d), ", settings.pulseThreshold); 109 | setMessage("thresholdFound", 5); 110 | } else { 111 | settings.pulseThreshold = 0; 112 | Serial.println(F("Spread of sensor readings not sufficient for threshold calculation!")); 113 | setMessage("thresholdFailed", 5); 114 | } 115 | Serial.printf("Minimum(%d), Maximum(%d)\n", ferraris.min, ferraris.max); 116 | } 117 | 118 | 119 | // try to find a rising edge in previous analog readings 120 | static bool findRisingEdge() { 121 | uint16_t i, belowThresholdTrigger; 122 | uint16_t belowTh = 0; 123 | uint16_t aboveTh = 0; 124 | 125 | // min. number of pulses below threshold required to detect a rising edge 126 | belowThresholdTrigger = (settings.pulseDebounceMs / 2) / settings.readingsIntervalMs; 127 | 128 | i = ferraris.index > 0 ? ferraris.index : ferraris.size - 1; 129 | while (i - 1 >= 0 && belowTh <= belowThresholdTrigger && 130 | aboveTh <= settings.aboveThresholdTrigger) { 131 | i -= 1; 132 | if (pulseReadings[i] < (settings.pulseThreshold + ferraris.offsetNoWifi)) 133 | belowTh++; 134 | else 135 | aboveTh++; 136 | } 137 | 138 | // round robin... 139 | i = ferraris.size; 140 | while (i - 1 > ferraris.index && belowTh <= belowThresholdTrigger && 141 | aboveTh <= settings.aboveThresholdTrigger) { 142 | i -= 1; 143 | if (pulseReadings[i] < (settings.pulseThreshold + ferraris.offsetNoWifi)) 144 | belowTh++; 145 | else 146 | aboveTh++; 147 | } 148 | 149 | return (aboveTh >= settings.aboveThresholdTrigger && belowTh >= belowThresholdTrigger); 150 | } 151 | 152 | 153 | // Calcultate current power consumption based on moving average 154 | // over given number of seconds. If secs is zero, calculate power 155 | // only based on the most recent pulse interval of the red marker. 156 | // When the ferraris disk rotates rather slowly on low power 157 | // consumption this value can only be updated about once every 158 | // 1-2 minutes on a meter with 75 kwh/turn. 159 | static int16_t calculateCurrentPower(uint16_t secs) { 160 | uint32_t sumAvg = 0; 161 | uint8_t pulses = 0; 162 | 163 | if (secs > 0) { 164 | for (uint8_t i = 1; i <= pulseInterval.getCount(); i++) { 165 | sumAvg += pulseInterval.getAvg(i); // tenth of sec 166 | if (sumAvg > 0) 167 | pulses++; 168 | if (sumAvg > (secs * 100)) 169 | break; 170 | } 171 | } 172 | 173 | if (!settings.calculateCurrentPower) { 174 | return -2; 175 | } else if (pulseInterval.getCount() > 0) { 176 | pulses = (secs == 0) ? 1 : pulses; // only consider last pulse interval (no moving avg) 177 | return int(3600000 / (settings.turnsPerKwh * (pulseInterval.getAvg(pulses)/10.0))); 178 | } else { 179 | return -1; 180 | } 181 | } 182 | 183 | 184 | // since ADC readings seem to increase a little bit, if Wifi was 185 | // switched off, calculate an offset for the pulseThreshold 186 | static void setPulseThresholdOffset(bool reset) { 187 | static uint32_t wifiOffMillis = 0; 188 | static uint32_t wifiOnMillis = 0; 189 | static uint16_t noPulseLevelWifiOn = 0; 190 | 191 | if (reset) { 192 | wifiOnMillis = 0; 193 | wifiOffMillis = 0; 194 | ferraris.offsetNoWifi = 0; 195 | return; 196 | } 197 | 198 | // remember time when Wifi connection was first 199 | // available and when it was switched off 200 | if (!wifiOnMillis && (wifiStatus == 1)) 201 | wifiOnMillis = millis(); 202 | if (!wifiOffMillis && (wifiStatus == 0)) 203 | wifiOffMillis = millis(); 204 | 205 | // determine the average baseline pulse reading within the 206 | // last 30 sec. at least 60 sec. after system startup 207 | if (!noPulseLevelWifiOn && (wifiOnMillis > 0) && ((millis() - wifiOnMillis) > 60000)) { 208 | noPulseLevelWifiOn = findPastAverage(30000/settings.readingsIntervalMs); 209 | Serial.printf("Set average ADC no pulse level to %d\n", noPulseLevelWifiOn); 210 | } 211 | 212 | // determine the offset for pulse readings if Wifi is off based on 213 | // the last 20 sec. at least 30 sec. after Wifi was switched off 214 | if (!ferraris.offsetNoWifi && (wifiOffMillis > 0) && ((millis() - wifiOffMillis) > 30000)) { 215 | ferraris.offsetNoWifi = findPastAverage(20000/settings.readingsIntervalMs) - noPulseLevelWifiOn; 216 | Serial.printf("Set ADC offset for inactive Wifi to %d\n", ferraris.offsetNoWifi); 217 | } 218 | } 219 | 220 | 221 | // setup pin for IR sensor and allocate array for pulse readings 222 | void initFerraris() { 223 | ferraris.size = (int)(settings.readingsBufferSec * 1000 / settings.readingsIntervalMs); 224 | pulseReadings = (int16_t*)malloc(ferraris.size * sizeof(int16_t)); 225 | if (pulseReadings == NULL) { 226 | Serial.println(F("malloc() failed")); 227 | while (true) { 228 | toggleLED(); 229 | delay(100); 230 | } 231 | } 232 | pinMode(A0, INPUT); 233 | pulseInterval.begin(); 234 | resetReadings(); 235 | } 236 | 237 | 238 | // scan ferraris disk for red marker and calibrate threshold 239 | // returns true if system is calibrated and red marker was identified 240 | bool readFerraris() { 241 | static uint32_t previousCountMillis = 0; 242 | static uint8_t cutDwnCnt = 0; 243 | static uint8_t aboveThreshold = 0; 244 | uint16_t pulseReading; 245 | int16_t currentPower; 246 | 247 | pulseReading = readIRSensor(); 248 | pulseReadings[ferraris.index++] = pulseReading; 249 | 250 | // calibration is triggered in web ui 251 | if (thresholdCalculation) { 252 | toggleLED(); 253 | // fill up array with analog sensors readings then 254 | // try to find valid threshold value for red marker 255 | if (ferraris.index >= ferraris.size) { 256 | switchLED(false); 257 | thresholdCalculation = false; 258 | calculateThreshold(); 259 | } 260 | return false; 261 | 262 | } else { 263 | // save readings in a round-robin array to be able 264 | // to search for a rising edge in recent readings 265 | if (ferraris.index >= ferraris.size) 266 | ferraris.index = 0; 267 | } 268 | 269 | // only count a rotation if a valid threshold value has been set, since last 270 | // count at least pulseDebounceMs seconds have passed, the readings have 271 | // been above the threshold at least aboveThresholdTrigger consecutive times 272 | // and a rising edge was identified in recents readings 273 | if (settings.pulseThreshold > 0 && 274 | (tsDiff(previousCountMillis) > settings.pulseDebounceMs) && 275 | pulseReading >= (settings.pulseThreshold + ferraris.offsetNoWifi) && 276 | ++aboveThreshold >= settings.aboveThresholdTrigger && 277 | findRisingEdge()) { 278 | 279 | // if Wifi is off but ADC offset is not yet set, 280 | // ignore possibly false pulse counts 281 | if (wifiStatus == 0 && !ferraris.offsetNoWifi) 282 | return false; 283 | 284 | // keep history of recents pulse intervals (saved as tenth of second) 285 | // for optional moving average, see calculateCurrentPower() below 286 | if (previousCountMillis > 0) 287 | pulseInterval.reading(tsDiff(previousCountMillis) / 100); 288 | 289 | settings.counterTotal++; 290 | previousCountMillis = millis(); 291 | aboveThreshold = 0; 292 | cutDwnCnt = 0; 293 | 294 | // (re)calculate total consumption (kwh) and current power consumption (watt) 295 | ferraris.consumption = (settings.counterTotal / (settings.turnsPerKwh * 1.0)) 296 | + (settings.counterOffset / 100.0); 297 | currentPower = calculateCurrentPower(0); 298 | if (settings.calculatePowerMvgAvg) { 299 | ferraris.power = calculateCurrentPower(settings.powerAvgSecs); 300 | Serial.printf("Red marker detected (%d rotations), averaged/current power consumption %d/%d W\n", 301 | settings.counterTotal, ferraris.power, currentPower); 302 | } else { 303 | ferraris.power = currentPower; 304 | Serial.printf("Red marker detected (%d rotations), current power consumption %d W\n", 305 | settings.counterTotal, ferraris.power); 306 | } 307 | 308 | return true; 309 | } 310 | 311 | // after a peak in consumption, this helps to bring 312 | // down the current (averaged) power reading more quickly 313 | if (settings.calculatePowerMvgAvg && pulseInterval.getAvg(1) > 0 && 314 | (tsDiff(previousCountMillis)/100 > (pulseInterval.getAvg(cutDwnCnt+1)*(cutDwnCnt+1)))) { 315 | pulseInterval.reading(tsDiff(previousCountMillis)/100); 316 | cutDwnCnt++; 317 | } else if (!settings.calculatePowerMvgAvg && ferraris.power > 1000 && 318 | (3600000/(ferraris.power * 75)) < (tsDiff(previousCountMillis)/1000 * 0.5)) { 319 | Serial.printf("Red marker not yet detected, lower current power to %d\n", int(ferraris.power * 0.7)); 320 | ferraris.power = int(ferraris.power * 0.7); 321 | } 322 | 323 | // since switching off Wifi has an effect on ADC readings a (positiv) 324 | // offset has to be calculated and added to pulse threshold value 325 | if (settings.enablePowerSavingMode && !ferraris.offsetNoWifi) 326 | setPulseThresholdOffset(false); 327 | 328 | return false; 329 | } 330 | 331 | 332 | // trigger calibration of threshold value for red marker on ferraris disk 333 | void calibrateFerraris() { 334 | Serial.println(F("Trying to identify threshold value for red marker...")); 335 | thresholdCalculation = true; 336 | settings.pulseThreshold = 0; 337 | resetReadings(); 338 | } 339 | 340 | 341 | // if system switches from power saving mode back to online 342 | // mode (Wifi always on) the ADC offset needs to be reset to 0 343 | void resetWifiOffset() { 344 | Serial.println(F("Reset ADC offset")); 345 | setPulseThresholdOffset(true); 346 | } 347 | -------------------------------------------------------------------------------- /src/web.cpp: -------------------------------------------------------------------------------- 1 | /*************************************************************************** 2 | Copyright (c) 2019-2023 Lars Wessels 3 | 4 | This file a part of the "ESP8266 Wifi Power Meter" source code. 5 | https://github.com/lrswss/esp8266-wifi-power-meter 6 | 7 | Licensed under the MIT License. You may not use this file except in 8 | compliance with the License. You may obtain a copy of the License at 9 | 10 | https://opensource.org/licenses/MIT 11 | 12 | ***************************************************************************/ 13 | 14 | #include "config.h" 15 | #include "mqtt.h" 16 | #include "utils.h" 17 | #include "ferraris.h" 18 | #include "web.h" 19 | #include "nvs.h" 20 | #include "wlan.h" 21 | 22 | // local webserver on port 80 with OTA-Option 23 | ESP8266WebServer httpServer(80); 24 | 25 | static char msgType[32]; 26 | static uint8_t msgTimeout; 27 | 28 | 29 | // required for RESTful api 30 | static void setCrossOrigin() { 31 | httpServer.sendHeader(F("Access-Control-Allow-Origin"), F("*")); 32 | httpServer.sendHeader(F("Access-Control-Max-Age"), F("600")); 33 | httpServer.sendHeader(F("Access-Control-Allow-Methods"), F("GET,OPTIONS")); 34 | httpServer.sendHeader(F("Access-Control-Allow-Headers"), F("*")); 35 | } 36 | 37 | 38 | // response for REST preflight request 39 | static void sendCORS() { 40 | setCrossOrigin(); 41 | httpServer.sendHeader(F("Access-Control-Allow-Credentials"), F("false")); 42 | httpServer.send(204); 43 | } 44 | 45 | 46 | // passes updated value to web ui as JSON on AJAX call once a second 47 | // can also be used for (remote) RESTful request 48 | static void handleGetReadings() { 49 | StaticJsonDocument<384> JSON; 50 | static char reply[288]; 51 | 52 | JSON.clear(); 53 | JSON["totalCounter"] = settings.counterTotal; 54 | JSON["totalConsumption"] = String(ferraris.consumption, 2); 55 | JSON["currentPower"] = ferraris.power; 56 | JSON["runtime"] = getRuntime(false); 57 | JSON["rssi"] = WiFi.RSSI(); 58 | 59 | // only relevant for power meter's web ui 60 | if (httpServer.arg("local").length() >= 1) { 61 | JSON["thresholdCalculation"] = thresholdCalculation ? 1 : 0; 62 | JSON["pulseThreshold"] = settings.pulseThreshold; 63 | JSON["currentReadings"] = ferraris.index; 64 | JSON["totalReadings"] = ferraris.size; 65 | JSON["pulseMin"] = ferraris.min; 66 | JSON["pulseMax"] = ferraris.max; 67 | 68 | if (strlen(msgType) > 0) { 69 | JSON["msgType"] = msgType; 70 | JSON["msgTimeout"] = msgTimeout; 71 | memset(msgType, 0, sizeof(msgType)); 72 | msgTimeout = 0; 73 | } 74 | #ifdef DEBUG_HEAP 75 | JSON["freeheap"] = ESP.getFreeHeap(); 76 | #endif 77 | } 78 | memset(reply, 0, sizeof(reply)); 79 | size_t s = serializeJson(JSON, reply); 80 | setCrossOrigin(); // required for remote REST queries 81 | httpServer.send(200, "text/plain", reply, s); 82 | } 83 | 84 | 85 | // display message for given time in web ui 86 | void setMessage(const char *msg, uint8_t timeSecs) { 87 | strlcpy(msgType, msg, sizeof(msgType)); 88 | msgTimeout = timeSecs; 89 | } 90 | 91 | 92 | // terminate webserver (power saving mode) 93 | void stopWebserver() { 94 | httpServer.stop(); 95 | Serial.println(F("Webserver stopped")); 96 | } 97 | 98 | 99 | // handle and dispatch http request 100 | void handleWebrequest() { 101 | httpServer.handleClient(); 102 | } 103 | 104 | 105 | // configure url handler and start web server 106 | void startWebserver() { 107 | 108 | // send main page 109 | httpServer.on("/", HTTP_GET, []() { 110 | String html = FPSTR(HEADER_html); 111 | html += FPSTR(MAIN_html); 112 | html += FPSTR(FOOTER_html); 113 | html.replace("__SYSTEMID__", systemID()); 114 | html.replace("__FIRMWARE__", String(FIRMWARE_VERSION)); 115 | html.replace("__BUILD__", String(__DATE__)+" "+String(__TIME__)); 116 | Serial.println(F("Show main page")); 117 | httpServer.send(200, "text/html", html); 118 | }); 119 | 120 | // handler for AJAX requests 121 | httpServer.on("/readings", HTTP_GET, handleGetReadings); 122 | httpServer.on("/readings", HTTP_OPTIONS, sendCORS); 123 | 124 | // restart ESP8266 125 | httpServer.on("/restart", HTTP_GET, []() { 126 | httpServer.send(200, "text/plain", "OK", 2); 127 | restartSystem(); 128 | }); 129 | 130 | // save default settings to NVS 131 | httpServer.on("/reset", HTTP_GET, []() { 132 | httpServer.send(200, "text/plain", "OK", 2); 133 | mqttDisconnect(true); 134 | WiFi.disconnect(true); 135 | resetNVS(); 136 | ESP.eraseConfig(); 137 | ESP.restart(); 138 | }); 139 | 140 | // trigger calculation of new threshold value 141 | httpServer.on("/calcThreshold", HTTP_GET, []() { 142 | httpServer.send(200, "text/plain", "OK", 2); 143 | calibrateFerraris(); 144 | }); 145 | 146 | // save measured threshold value to NVS 147 | httpServer.on("/saveThreshold", HTTP_GET, []() { 148 | saveNVS(true); 149 | Serial.printf("Saved %d as new threshold for marker detection\n", settings.pulseThreshold); 150 | httpServer.send(200, "text/plain", "OK", 2); 151 | }); 152 | 153 | // reset all counters to zero 154 | httpServer.on("/resetCounter", HTTP_GET, []() { 155 | settings.counterTotal = 0; 156 | settings.counterOffset = 0; 157 | saveNVS(true); 158 | Serial.println(F("Reset counter and offset")); 159 | httpServer.send(200, "text/plain", "OK", 2); 160 | }); 161 | 162 | // show upload form for firmware update 163 | httpServer.on("/update", HTTP_GET, []() { 164 | String html = FPSTR(HEADER_html); 165 | if (httpServer.arg("res") == "ok") { 166 | html += FPSTR(UPDATE_OK_html); 167 | Serial.println(F("Show firmware upload success")); 168 | } else if (httpServer.arg("res") == "err") { 169 | html += FPSTR(UPDATE_ERR_html); 170 | Serial.println(F("Show firmware upload failed")); 171 | } else { 172 | html += FPSTR(UPDATE_html); 173 | Serial.println(F("Show firmware upload form")); 174 | } 175 | html += FPSTR(FOOTER_html); 176 | html.replace("__SYSTEMID__", systemID()); 177 | html.replace("__FIRMWARE__", String(FIRMWARE_VERSION)); 178 | html.replace("__BUILD__", String(__DATE__)+" "+String(__TIME__)); 179 | httpServer.send(200, "text/html", html); 180 | }); 181 | 182 | // show main configuration 183 | httpServer.on("/config", HTTP_GET, []() { 184 | String html = FPSTR(HEADER_html); 185 | html += FPSTR(CONFIG_html); 186 | html += FPSTR(FOOTER_html); 187 | 188 | html.replace("__TURNS_KWH__", String(settings.turnsPerKwh)); 189 | html.replace("__KWH_TURNS_MIN__", String(KWH_TURNS_MIN)); 190 | html.replace("__KWH_TURNS_MAX__", String(KWH_TURNS_MAX)); 191 | html.replace("__CONSUMPTION_KWH__", String(ferraris.consumption, 2)); 192 | html.replace("__BACKUP_CYCLE__", String(settings.backupCycleMin)); 193 | html.replace("__BACKUP_CYCLE_MIN__", String(BACKUP_CYCLE_MIN)); 194 | html.replace("__BACKUP_CYCLE_MAX__", String(BACKUP_CYCLE_MAX)); 195 | html.replace("__METER_ID__", systemID()); 196 | if (settings.calculateCurrentPower) 197 | html.replace("__CURRENT_POWER__", "checked"); 198 | else 199 | html.replace("__CURRENT_POWER__", ""); 200 | if (settings.calculatePowerMvgAvg && settings.powerAvgSecs > 0) 201 | html.replace("__POWER_AVG__", "checked"); 202 | else 203 | html.replace("__POWER_AVG__", ""); 204 | html.replace("__POWER_AVG_SECS__", String(settings.powerAvgSecs)); 205 | html.replace("__POWER_AVG_SECS_MIN__", String(POWER_AVG_SECS_MIN)); 206 | html.replace("__POWER_AVG_SECS_MAX__", String(POWER_AVG_SECS_MAX)); 207 | html.replace("__POWER_AVG_SECS_POWERSAVING__", String(POWER_AVG_SECS_POWERSAVING)); 208 | 209 | if (settings.enableMQTT) 210 | html.replace("__MQTT__", "checked"); 211 | else 212 | html.replace("__MQTT__", ""); 213 | html.replace("__MQTT_BROKER__", String(settings.mqttBroker)); 214 | html.replace("__MQTT_PORT__", String(settings.mqttBrokerPort)); 215 | html.replace("__MQTT_BASE_TOPIC__", String(settings.mqttBaseTopic)); 216 | if (settings.mqttJSON) 217 | html.replace("__MQTT_JSON__", "checked"); 218 | else 219 | html.replace("__MQTT_JSON__", ""); 220 | if (settings.enableHADiscovery) 221 | html.replace("__MQTT_HA_DISCOVERY__", "checked"); 222 | else 223 | html.replace("__MQTT_HA_DISCOVERY__", ""); 224 | html.replace("__MQTT_INTERVAL__", String(settings.mqttIntervalSecs)); 225 | if (settings.mqttEnableAuth) 226 | html.replace("__MQTT_AUTH__", "checked"); 227 | else 228 | html.replace("__MQTT_AUTH__", ""); 229 | html.replace("__MQTT_USERNAME__", String(settings.mqttUsername)); 230 | html.replace("__MQTT_PASSWORD__", String(settings.mqttPassword)); 231 | if (settings.mqttSecure) 232 | html.replace("__MQTT_SECURE__", "checked"); 233 | else 234 | html.replace("__MQTT_SECURE__", ""); 235 | if (settings.enablePowerSavingMode) { 236 | html.replace("__POWER_SAVING_MODE__", "checked"); 237 | } else { 238 | html.replace("__POWER_SAVING_MODE__", ""); 239 | } 240 | html.replace("__MQTT_INTERVAL_MIN_POWERSAVING__", String(MQTT_INTERVAL_MIN_POWERSAVING)); 241 | 242 | html.replace("__SYSTEMID__", systemID()); 243 | html.replace("__FIRMWARE__", String(FIRMWARE_VERSION)); 244 | html.replace("__BUILD__", String(__DATE__)+" "+String(__TIME__)); 245 | Serial.println(F("Show main configuration")); 246 | httpServer.send(200, "text/html", html); 247 | }); 248 | 249 | // save general settings 250 | httpServer.on("/config", HTTP_POST, []() { 251 | uint16_t mqttIntervalMinSecs; 252 | 253 | if (httpServer.arg("kwh_turns").toInt() >= KWH_TURNS_MIN && 254 | httpServer.arg("kwh_turns").toInt() <= KWH_TURNS_MAX) 255 | settings.turnsPerKwh = httpServer.arg("kwh_turns").toInt(); 256 | if (httpServer.arg("consumption_kwh").toFloat() >= 1 && httpServer.arg("consumption_kwh").toFloat() <= 999999) { 257 | ferraris.consumption = httpServer.arg("consumption_kwh").toFloat(); 258 | settings.counterOffset = (httpServer.arg("consumption_kwh").toFloat() * 100) - 259 | lround(settings.counterTotal * 100 / settings.turnsPerKwh); 260 | } 261 | if (httpServer.arg("meter_id").length() >= METER_ID_LEN_MIN && httpServer.arg("meter_id").length() <= 16) 262 | strlcpy(settings.systemID, httpServer.arg("meter_id").c_str(), 16); 263 | else 264 | memset(settings.systemID, 0, sizeof(settings.systemID)); 265 | if (httpServer.arg("backup_cycle").toInt() >= BACKUP_CYCLE_MIN && 266 | httpServer.arg("backup_cycle").toInt() <= BACKUP_CYCLE_MAX) 267 | settings.backupCycleMin = httpServer.arg("backup_cycle").toInt(); 268 | if (httpServer.arg("current_power") == "on") { 269 | settings.calculateCurrentPower = true; 270 | if (httpServer.arg("current_power_avg") == "on") { 271 | settings.calculatePowerMvgAvg = true; 272 | if (httpServer.arg("power_avg_secs").toInt() >= POWER_AVG_SECS_MIN && 273 | httpServer.arg("power_avg_secs").toInt() <= POWER_AVG_SECS_MAX) 274 | settings.powerAvgSecs = httpServer.arg("power_avg_secs").toInt(); 275 | } else { 276 | settings.calculatePowerMvgAvg = false; 277 | } 278 | } else { 279 | settings.calculateCurrentPower = false; 280 | } 281 | 282 | if (httpServer.arg("mqtt") == "on") { 283 | settings.enableMQTT = true; 284 | if (httpServer.arg("powersavingmode") == "on") { 285 | settings.enablePowerSavingMode = true; 286 | mqttIntervalMinSecs = MQTT_INTERVAL_MIN_POWERSAVING; // to actually save power... 287 | } else { 288 | settings.enablePowerSavingMode = false; 289 | mqttIntervalMinSecs = MQTT_INTERVAL_MIN; 290 | } 291 | if (httpServer.arg("mqttbroker").length() >= MQTT_BROKER_LEN_MIN && 292 | httpServer.arg("mqttbroker").length() <= 64) 293 | strlcpy(settings.mqttBroker, httpServer.arg("mqttbroker").c_str(), 64); 294 | if (httpServer.arg("mqttport").toInt() >= MQTT_BROKER_PORT_MIN && 295 | httpServer.arg("mqttport").toInt() <= MQTT_BROKER_PORT_MAX) 296 | settings.mqttBrokerPort = httpServer.arg("mqttport").toInt(); 297 | if (httpServer.arg("mqttbasetopic").length() >= MQTT_BASETOPIC_LEN_MIN && 298 | httpServer.arg("mqttbasetopic").length() <= 64) 299 | strlcpy(settings.mqttBaseTopic, httpServer.arg("mqttbasetopic").c_str(), 64); 300 | if (httpServer.arg("mqttinterval").toInt() >= mqttIntervalMinSecs && 301 | httpServer.arg("mqttinterval").toInt() <= MQTT_INTERVAL_MAX) { 302 | if (httpServer.arg("mqttinterval").toInt() != settings.mqttIntervalSecs) 303 | settings.mqttIntervalSecs = httpServer.arg("mqttinterval").toInt(); 304 | } else if (httpServer.arg("mqttinterval").toInt() < mqttIntervalMinSecs) 305 | settings.mqttIntervalSecs = mqttIntervalMinSecs; 306 | else if (httpServer.arg("mqttinterval").toInt() > MQTT_INTERVAL_MAX) 307 | settings.mqttIntervalSecs = MQTT_INTERVAL_MAX; 308 | if (httpServer.arg("mqtt_json") == "on") 309 | settings.mqttJSON = true; 310 | else 311 | settings.mqttJSON = false; 312 | if (httpServer.arg("mqtt_ha_discovery") == "on") 313 | settings.enableHADiscovery = true; 314 | else 315 | settings.enableHADiscovery = false; 316 | if (httpServer.arg("mqttauth") == "on") { 317 | settings.mqttEnableAuth = true; 318 | if (httpServer.arg("mqttuser").length() >= MQTT_USER_LEN_MIN && 319 | httpServer.arg("mqttuser").length() <= 32) 320 | strlcpy(settings.mqttUsername, httpServer.arg("mqttuser").c_str(), 32); 321 | if (httpServer.arg("mqttpassword").length() >= MQTT_PASS_LEN_MIN && 322 | httpServer.arg("mqttpassword").length() <= 32) 323 | strlcpy(settings.mqttPassword, httpServer.arg("mqttpassword").c_str(), 32); 324 | } else { 325 | settings.mqttEnableAuth = false; 326 | } 327 | if (httpServer.arg("mqtt_secure") == "on") 328 | settings.mqttSecure = true; 329 | else 330 | settings.mqttSecure = false; 331 | } else { 332 | settings.enableMQTT = false; 333 | settings.enablePowerSavingMode = false; 334 | } 335 | 336 | mqttDisconnect(false); 337 | saveNVS(true); 338 | mqttPublish(); 339 | httpServer.sendHeader("Location", "/config?saved", true); 340 | httpServer.send(302, "text/plain", ""); 341 | Serial.println(F("Updated main configuration")); 342 | }); 343 | 344 | // show expert settings page 345 | httpServer.on("/expert", HTTP_GET, []() { 346 | String html = FPSTR(HEADER_html); 347 | html += FPSTR(EXPERT_html); 348 | html += FPSTR(FOOTER_html); 349 | 350 | html.replace("__PULSE_THRESHOLD__", String(settings.pulseThreshold)); 351 | html.replace("__PULSE_THRESHOLD_MIN__", String(PULSE_THRESHOLD_MIN)); 352 | html.replace("__PULSE_THRESHOLD_MAX__", String(PULSE_THRESHOLD_MAX)); 353 | html.replace("__READINGS_SPREAD__", String(settings.readingsSpreadMin)); 354 | html.replace("__READINGS_SPREAD_MIN__", String(READINGS_SPREAD_MIN)); 355 | html.replace("__READINGS_SPREAD_MAX__", String(READINGS_SPREAD_MAX)); 356 | html.replace("__READINGS_INTERVAL_MS__", String(settings.readingsIntervalMs)); 357 | html.replace("__READINGS_INTERVAL_MS_MIN__", String(READINGS_INTERVAL_MS_MIN)); 358 | html.replace("__READINGS_INTERVAL_MS_MAX__", String(READINGS_INTERVAL_MS_MAX)); 359 | html.replace("__READINGS_BUFFER_SECS__", String(settings.readingsBufferSec)); 360 | html.replace("__READINGS_BUFFER_SECS_MIN__", String(READINGS_BUFFER_SECS_MIN)); 361 | html.replace("__READINGS_BUFFER_SECS_MAX__", String(READINGS_BUFFER_SECS_MAX)); 362 | html.replace("__THRESHOLD_TRIGGER__", String(settings.aboveThresholdTrigger)); 363 | html.replace("__THRESHOLD_TRIGGER_MIN__", String(THRESHOLD_TRIGGER_MIN)); 364 | html.replace("__THRESHOLD_TRIGGER_MAX__", String(THRESHOLD_TRIGGER_MAX)); 365 | html.replace("__DEBOUNCE_TIME_MS__", String(settings.pulseDebounceMs)); 366 | html.replace("__DEBOUNCE_TIME_MS_MIN__", String(DEBOUNCE_TIME_MS_MIN)); 367 | html.replace("__DEBOUNCE_TIME_MS_MAX__", String(DEBOUNCE_TIME_MS_MAX)); 368 | if (settings.enableInflux) 369 | html.replace("__INFLUXDB__", "checked"); 370 | else 371 | html.replace("__INFLUXDB__", ""); 372 | 373 | html.replace("__SYSTEMID__", systemID()); 374 | html.replace("__FIRMWARE__", String(FIRMWARE_VERSION)); 375 | html.replace("__BUILD__", String(__DATE__)+" "+String(__TIME__)); 376 | Serial.println(F("Show expert settings")); 377 | httpServer.send(200, "text/html", html); 378 | }); 379 | 380 | // save general settings 381 | httpServer.on("/expert", HTTP_POST, []() { 382 | if (httpServer.arg("pulse_threshold").toInt() >= PULSE_THRESHOLD_MIN && 383 | httpServer.arg("pulse_threshold").toInt() <= PULSE_THRESHOLD_MAX) 384 | settings.pulseThreshold = httpServer.arg("pulse_threshold").toInt(); 385 | if (httpServer.arg("readings_spread").toInt() >= READINGS_SPREAD_MIN && 386 | httpServer.arg("readings_spread").toInt() <= READINGS_SPREAD_MAX) 387 | settings.readingsSpreadMin = httpServer.arg("readings_spread").toInt(); 388 | if (httpServer.arg("readings_interval").toInt() >= READINGS_INTERVAL_MS_MIN && 389 | httpServer.arg("readings_interval").toInt() <= READINGS_INTERVAL_MS_MAX) 390 | settings.readingsIntervalMs = httpServer.arg("readings_interval").toInt(); 391 | if (httpServer.arg("readings_buffer").toInt() >= READINGS_BUFFER_SECS_MIN && 392 | httpServer.arg("readings_buffer").toInt() <= READINGS_BUFFER_SECS_MAX) 393 | settings.readingsBufferSec = httpServer.arg("readings_buffer").toInt(); 394 | if (httpServer.arg("threshold_trigger").toInt() >= THRESHOLD_TRIGGER_MIN && 395 | httpServer.arg("threshold_trigger").toInt() <= THRESHOLD_TRIGGER_MAX) 396 | settings.aboveThresholdTrigger = httpServer.arg("threshold_trigger").toInt(); 397 | if (httpServer.arg("debounce_time").toInt() >= DEBOUNCE_TIME_MS_MIN && 398 | httpServer.arg("debounce_time").toInt() <= DEBOUNCE_TIME_MS_MAX) 399 | settings.pulseDebounceMs = httpServer.arg("debounce_time").toInt(); 400 | if (httpServer.arg("influxdb") == "on") 401 | settings.enableInflux = true; 402 | else 403 | settings.enableInflux = false; 404 | 405 | saveNVS(true); 406 | httpServer.sendHeader("Location", "/expert?saved", true); 407 | httpServer.send(302, "text/plain", ""); 408 | Serial.println(F("Update expert configuration")); 409 | }); 410 | 411 | // handle firmware upload 412 | httpServer.on("/update", HTTP_POST, []() { 413 | if (Update.hasError()) { 414 | Serial.println(F("OTA failed")); 415 | httpServer.send(500, "text/plain", "ERROR"); 416 | blinkLED(4, 50); 417 | } else { 418 | Serial.println(F("OTA successful")); 419 | httpServer.send(200, "text/plain", "OK"); 420 | blinkLED(2, 250); 421 | } 422 | }, []() { 423 | HTTPUpload& upload = httpServer.upload(); 424 | if (upload.status == UPLOAD_FILE_START) { 425 | saveNVS(false); 426 | Serial.println(F("Starting OTA update...")); 427 | uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000; 428 | if (!Update.begin(maxSketchSpace)) { 429 | Update.printError(Serial); 430 | } 431 | } else if (upload.status == UPLOAD_FILE_WRITE) { 432 | if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { 433 | Update.printError(Serial); 434 | saveNVS(true); 435 | } 436 | } else if (upload.status == UPLOAD_FILE_END) { 437 | if (!Update.end(true)) { 438 | Update.printError(Serial); 439 | saveNVS(true); 440 | } 441 | } 442 | }); 443 | 444 | // send configuration as JSON file 445 | httpServer.on("/nvsbackup", HTTP_GET, []() { 446 | String configfile; 447 | const char* configJSON; 448 | 449 | configJSON = nvs2json(); 450 | if (configJSON != NULL) { 451 | Serial.printf("Sending configuration data as JSON (%d bytes)...\n", strlen(configJSON)); 452 | configfile = "WifiPowerMeter_" + systemID() + "_v" + String(FIRMWARE_VERSION) + ".json"; 453 | httpServer.sendHeader("Content-Type", "text/plain"); 454 | httpServer.sendHeader("Content-Disposition", "attachment; filename="+configfile); 455 | httpServer.setContentLength(strlen(configJSON)); 456 | httpServer.sendHeader("Connection", "close"); 457 | httpServer.send(200, "application/octet-stream", configJSON); 458 | } else { 459 | Serial.println(F("Failed to export configuration data!")); 460 | httpServer.send(500, "text/plain", "ERROR"); 461 | } 462 | }); 463 | 464 | 465 | // show upload form for firmware update 466 | httpServer.on("/nvsimport", HTTP_GET, []() { 467 | String html = FPSTR(HEADER_html); 468 | if (httpServer.arg("res") == "ok") { 469 | html += FPSTR(IMPORT_OK_html); 470 | Serial.println(F("Show configuration import success")); 471 | } else if (httpServer.arg("res") == "err") { 472 | html += FPSTR(IMPORT_ERR_html); 473 | Serial.println(F("Show configuration import failed")); 474 | } else { 475 | html += FPSTR(IMPORT_html); 476 | Serial.println(F("Show configuration import form")); 477 | } 478 | html += FPSTR(FOOTER_html); 479 | html.replace("__SYSTEMID__", systemID()); 480 | html.replace("__FIRMWARE__", String(FIRMWARE_VERSION)); 481 | html.replace("__BUILD__", String(__DATE__)+" "+String(__TIME__)); 482 | httpServer.send(200, "text/html", html); 483 | }); 484 | 485 | 486 | httpServer.on("/nvsimport", HTTP_POST, []() { 487 | HTTPUpload& upload = httpServer.upload(); 488 | if (upload.status == UPLOAD_FILE_START) { 489 | Serial.println(F("Importing configuration data...")); 490 | } else if (upload.status == UPLOAD_FILE_END) { 491 | if (!json2nvs((const char*)upload.buf, upload.currentSize)) 492 | httpServer.send(500, "text/plain", "ERROR"); 493 | else 494 | httpServer.send(200, "text/plain", "OK"); 495 | } else { 496 | Serial.println(F("Failed to import settings, upload error!")); 497 | httpServer.send(500, "text/plain", "ERROR"); 498 | } 499 | }); 500 | 501 | httpServer.begin(); 502 | Serial.println(F("Webserver started")); 503 | } 504 | -------------------------------------------------------------------------------- /src/mqtt.cpp: -------------------------------------------------------------------------------- 1 | /*************************************************************************** 2 | Copyright (c) 2019-2023 Lars Wessels 3 | 4 | This file a part of the "ESP8266 Wifi Power Meter" source code. 5 | https://github.com/lrswss/esp8266-wifi-power-meter 6 | 7 | Licensed under the MIT License. You may not use this file except in 8 | compliance with the License. You may obtain a copy of the License at 9 | 10 | https://opensource.org/licenses/MIT 11 | 12 | ***************************************************************************/ 13 | 14 | #include "config.h" 15 | #include "wlan.h" 16 | #include "mqtt.h" 17 | #include "web.h" 18 | #include "utils.h" 19 | #include "nvs.h" 20 | #include "ferraris.h" 21 | 22 | static WiFiClient espClient; 23 | static WiFiClientSecure espClientSecure; 24 | static PubSubClient *mqtt = NULL; 25 | 26 | 27 | // publish JSON on given MQTT topic 28 | static bool publishJSON(JsonDocument& json, char *topic, bool retain, bool verbose) { 29 | static char buf[592]; 30 | bool rc = false; 31 | size_t bytes; 32 | 33 | memset(buf, 0, sizeof(buf)); 34 | bytes = serializeJson(json, buf, sizeof(buf)-1); 35 | if (json.overflowed()) { 36 | Serial.printf("MQTT %s aborted, JSON overflow!\n", topic); 37 | } else { 38 | if (mqtt->publish(topic, buf, retain)) { 39 | rc = true; 40 | if (verbose) 41 | Serial.printf("MQTT %s %s\n", topic, buf); 42 | else 43 | Serial.printf("MQTT %s (%d bytes)\n", topic, bytes); 44 | } else { 45 | Serial.printf("MQTT %s failed!\n", topic); 46 | } 47 | } 48 | json.clear(); 49 | delay(50); 50 | return rc; 51 | } 52 | 53 | 54 | // add device description to HA discovery sensor topic 55 | static void addDeviceDescription(JsonDocument& json) { 56 | JsonObject dev = json.createNestedObject("dev"); 57 | dev["name"] = "WiFi Power Meter " + String(settings.systemID); 58 | dev["ids"].add(String(settings.systemID)); 59 | dev["cu"] = "http://" + WiFi.localIP().toString(); 60 | dev["mdl"] = "ESP8266"; 61 | dev["mf"] = "https://github.com/lrswss"; 62 | dev["sw"] = FIRMWARE_VERSION; 63 | } 64 | 65 | 66 | // published or deletes Home Assistant auto discovery message 67 | // https://www.home-assistant.io/docs/mqtt/discovery/ 68 | static void publishHADiscoveryMessage(bool publish) { 69 | DynamicJsonDocument JSON(640); 70 | static char systemID[17], mqttBaseTopic[65]; 71 | static bool discoveryPublished = false; 72 | char devTopic[96], topicTotalCon[80], topicPower[80], topicRSSI[80]; 73 | char topicRuntime[80], topicCount[80], topicWifiCnt[96], topicWifiOnAir[96]; 74 | 75 | // message topic didn't change skip HA discovery message 76 | if ((discoveryPublished == publish) && !strncmp(systemID, settings.systemID, 64) && 77 | !strncmp(mqttBaseTopic, settings.mqttBaseTopic, 64)) 78 | return; 79 | 80 | if (publish) { 81 | // if message topic has changed update HA discovery message 82 | if ((strlen(systemID) > 0) && (strlen(mqttBaseTopic) > 0) && 83 | ((strncmp(systemID, settings.systemID, 64) != 0) || 84 | (strncmp(mqttBaseTopic, settings.mqttBaseTopic, 64) != 0))) { 85 | publishHADiscoveryMessage(false); 86 | delay(250); 87 | } 88 | strlcpy(systemID, settings.systemID, 16); 89 | strlcpy(mqttBaseTopic, settings.mqttBaseTopic, 64); 90 | } 91 | 92 | snprintf(devTopic, sizeof(devTopic), "%s/%s/state", mqttBaseTopic, systemID); 93 | snprintf(topicCount, sizeof(topicCount), 94 | "%s%s/pulse_count/config", MQTT_TOPIC_DISCOVER, systemID); 95 | snprintf(topicTotalCon, sizeof(topicTotalCon), 96 | "%s%s/total_consumption/config", MQTT_TOPIC_DISCOVER, systemID); 97 | snprintf(topicPower, sizeof(topicPower), 98 | "%s%s/current_power/config", MQTT_TOPIC_DISCOVER, systemID); 99 | snprintf(topicRSSI, sizeof(topicRSSI), 100 | "%s%s/signal_strength/config", MQTT_TOPIC_DISCOVER, systemID); 101 | snprintf(topicWifiCnt, sizeof(topicWifiCnt), 102 | "%s%s/wifi_reconnect_counter/config", MQTT_TOPIC_DISCOVER, systemID); 103 | snprintf(topicWifiOnAir, sizeof(topicWifiOnAir), 104 | "%s%s/wifi_powersaving_uptime/config", MQTT_TOPIC_DISCOVER, systemID); 105 | snprintf(topicRuntime, sizeof(topicRuntime), 106 | "%s%s/runtime/config", MQTT_TOPIC_DISCOVER, systemID); 107 | 108 | if (publish) { 109 | Serial.printf("Sending Home Assistant MQTT discovery message for %s...\n", devTopic); 110 | 111 | JSON["name"] = "Ferraris Impuls Counter"; 112 | JSON["unique_id"] = "wifipowermeter-" + String(settings.systemID)+ "-impuls-counter"; 113 | JSON["ic"] = "mdi:rotate-360"; 114 | JSON["stat_t"] = devTopic; 115 | JSON["val_tpl"] = "{{ value_json."+ String(MQTT_SUBTOPIC_CNT) +" }}"; 116 | addDeviceDescription(JSON); 117 | publishJSON(JSON, topicCount, true, false); 118 | 119 | JSON["name"] = "Total Consumption"; 120 | JSON["unique_id"] = "wifipowermeter-" + String(settings.systemID)+ "-total-consumption"; 121 | JSON["ic"] = "mdi:counter"; 122 | JSON["unit_of_meas"] = "kWh"; 123 | JSON["dev_cla"] = "energy"; 124 | JSON["stat_cla"] = "total_increasing"; 125 | JSON["stat_t"] = devTopic; 126 | JSON["val_tpl"] = "{{ value_json."+ String(MQTT_SUBTOPIC_CONS) +" }}"; 127 | addDeviceDescription(JSON); 128 | publishJSON(JSON, topicTotalCon, true, false); 129 | 130 | JSON["name"] = "Current Power Consumption"; 131 | JSON["unique_id"] = "wifipowermeter-" + String(settings.systemID)+ "-current-power"; 132 | JSON["ic"] = "mdi:lightning-bolt"; 133 | JSON["unit_of_meas"] = "W"; 134 | JSON["dev_cla"] = "power"; 135 | JSON["stat_cla"] = "measurement"; 136 | JSON["stat_t"] = devTopic; 137 | JSON["val_tpl"] = "{{ value_json."+ String(MQTT_SUBTOPIC_PWR) +" }}"; 138 | addDeviceDescription(JSON); 139 | publishJSON(JSON, topicPower, true, false); 140 | 141 | JSON["name"] = "WiFi Signal Strength"; 142 | JSON["unique_id"] = "wifipowermeter-" + String(settings.systemID)+ "-rssi"; 143 | JSON["unit_of_meas"] = "dBm"; 144 | JSON["dev_cla"] = "signal_strength"; 145 | JSON["stat_t"] = devTopic; 146 | JSON["val_tpl"] = "{{ value_json."+ String(MQTT_SUBTOPIC_RSSI) +" }}"; 147 | addDeviceDescription(JSON); 148 | publishJSON(JSON, topicRSSI, true, false); 149 | 150 | if (settings.enablePowerSavingMode) { 151 | JSON["name"] = "WiFi Power Saving Uptime"; 152 | JSON["unique_id"] = "wifipowermeter-" + String(settings.systemID)+ "-wifi-powersaving-uptime"; 153 | JSON["unit_of_meas"] = "s"; // seconds 154 | JSON["ic"] = "mdi:wifi-arrow-up-down"; 155 | JSON["dev_cla"] = "duration"; 156 | JSON["stat_t"] = devTopic; 157 | JSON["val_tpl"] = "{{ value_json."+ String(MQTT_SUBTOPIC_ONAIR) +" }}"; 158 | addDeviceDescription(JSON); 159 | publishJSON(JSON, topicWifiOnAir, true, false); 160 | mqtt->publish(topicWifiCnt, "", true); // remove WiFi reconnect counter topic 161 | } else { 162 | JSON["name"] = "WiFi Reconnect Counter"; 163 | JSON["unique_id"] = "wifipowermeter-" + String(settings.systemID)+ "-wifi-reconnect-counter"; 164 | JSON["ic"] = "mdi:wifi-alert"; 165 | JSON["stat_t"] = devTopic; 166 | JSON["val_tpl"] = "{{ value_json."+ String(MQTT_SUBTOPIC_WIFI) +" }}"; 167 | addDeviceDescription(JSON); 168 | publishJSON(JSON, topicWifiCnt, true, false); 169 | mqtt->publish(topicWifiOnAir, "", true); // remove WiFi total connection time topic 170 | } 171 | 172 | JSON["name"] = "Uptime"; 173 | JSON["unique_id"] = "wifipowermeter-" + String(settings.systemID)+ "-uptime"; 174 | JSON["unit_of_meas"] = "min"; // minutes 175 | JSON["ic"] = "mdi:clock-outline"; 176 | JSON["dev_cla"] = "duration"; 177 | JSON["stat_t"] = devTopic; 178 | JSON["val_tpl"] = "{{ value_json."+ String(MQTT_SUBTOPIC_RUNT) +" }}"; 179 | addDeviceDescription(JSON); 180 | publishJSON(JSON, topicRuntime, true, false); 181 | discoveryPublished = true; 182 | 183 | } else if (strlen(devTopic) > 8) { 184 | // send empty (retained) message to delete sensor autoconfiguration 185 | Serial.printf("Removing Home Assistant MQTT discovery message for %s...\n", devTopic); 186 | 187 | mqtt->publish(topicCount, "", true); 188 | delay(50); 189 | mqtt->publish(topicTotalCon, "", true); 190 | delay(50); 191 | mqtt->publish(topicPower, "", true); 192 | delay(50); 193 | mqtt->publish(topicRSSI, "", true); 194 | delay(50); 195 | mqtt->publish(topicWifiCnt, "", true); 196 | delay(50); 197 | mqtt->publish(topicWifiOnAir, "", true); 198 | delay(50); 199 | mqtt->publish(topicRuntime, "", true); 200 | 201 | memset(devTopic, 0, sizeof(devTopic)); 202 | discoveryPublished = false; 203 | } 204 | } 205 | 206 | 207 | // checks for remote commands (powersave mode, mqttinterval) 208 | static void mqttCallback(char* topic, byte* payload, unsigned int length) { 209 | uint16_t valInt; 210 | char valStr[8]; 211 | 212 | if (!length) // avoid loop on mqttUnsetTopic() 213 | return; 214 | 215 | // convert string payload to integer 216 | strncpy(valStr, (const char*)payload, length); 217 | valStr[length] = '\0'; 218 | valInt = atoi(valStr); 219 | 220 | // enable/disable power saving mode 221 | if (strstr(topic, MQTT_SUBTOPIC_PSAVE) != NULL) { 222 | if (valInt != 0) { 223 | settings.enablePowerSavingMode = true; 224 | settings.calculatePowerMvgAvg = true; 225 | settings.powerAvgSecs = POWER_AVG_SECS_POWERSAVING; 226 | if (settings.mqttIntervalSecs < MQTT_INTERVAL_MIN_POWERSAVING) 227 | settings.mqttIntervalSecs = MQTT_INTERVAL_MIN_POWERSAVING; 228 | } else { 229 | settings.enablePowerSavingMode = false; 230 | settings.mqttIntervalSecs = MQTT_PUBLISH_INTERVAL_SEC; 231 | resetWifiOffset(); // unset ADC offset used when Wifi is offline 232 | } 233 | Serial.printf("MQTT %s: %sable power saving mode (MQTT publish interval %d seconds)\n", 234 | topic, settings.enablePowerSavingMode ? "en" : "dis", settings.mqttIntervalSecs); 235 | mqttUnsetTopic(MQTT_SUBTOPIC_PSAVE); 236 | 237 | // set mqtt publish interval 238 | } else if (strstr(topic, MQTT_SUBTOPIC_TXINT) != NULL) { 239 | if (settings.enablePowerSavingMode && valInt < MQTT_INTERVAL_MIN_POWERSAVING) { 240 | settings.mqttIntervalSecs = MQTT_INTERVAL_MIN_POWERSAVING; 241 | } else if (valInt < MQTT_INTERVAL_MIN) { 242 | settings.mqttIntervalSecs = MQTT_INTERVAL_MIN; 243 | } else if (valInt > MQTT_INTERVAL_MAX) { 244 | valInt = MQTT_INTERVAL_MAX; 245 | } else { 246 | settings.mqttIntervalSecs = valInt; 247 | } 248 | Serial.printf("MQTT command %s: set MQTT publish interval to %d seconds\n", 249 | topic, settings.mqttIntervalSecs); 250 | mqttUnsetTopic(MQTT_SUBTOPIC_TXINT); 251 | 252 | } else if (strstr(topic, MQTT_SUBTOPIC_RST) != NULL && valInt > 0) { 253 | Serial.printf("MQTT command %s: restart system\n", topic); 254 | mqttUnsetTopic(MQTT_SUBTOPIC_RST); 255 | restartSystem(); 256 | } 257 | } 258 | 259 | 260 | static void mqttInit() { 261 | if (mqtt != NULL) 262 | return; 263 | 264 | if (settings.mqttSecure) { 265 | espClientSecure.setInsecure(); 266 | // must reduce memory usage with Maximum Fragment Length Negotiation (supported by mosquitto) 267 | espClientSecure.probeMaxFragmentLength(settings.mqttBroker, settings.mqttBrokerPort, 1024); 268 | espClientSecure.setBufferSizes(1024, 1024); 269 | mqtt = new PubSubClient(espClientSecure); 270 | } else { 271 | mqtt = new PubSubClient(espClient); 272 | } 273 | mqtt->setServer(settings.mqttBroker, settings.mqttBrokerPort); 274 | mqtt->setBufferSize(672); // for home assistant MQTT device discovery 275 | mqtt->setSocketTimeout(2); // keep web ui responsive 276 | mqtt->setKeepAlive(settings.mqttIntervalSecs + 10); 277 | mqtt->setCallback(mqttCallback); 278 | } 279 | 280 | 281 | // subscribe to command topics 282 | static void subCmdTopics() { 283 | static char topic[96]; 284 | 285 | snprintf(topic, sizeof(topic)-1, "%s/%s/cmd/%s", 286 | settings.mqttBaseTopic, systemID().c_str(), MQTT_SUBTOPIC_PSAVE); 287 | mqtt->subscribe(topic); 288 | delay(50); 289 | 290 | snprintf(topic, sizeof(topic)-1, "%s/%s/cmd/%s", 291 | settings.mqttBaseTopic, systemID().c_str(), MQTT_SUBTOPIC_TXINT); 292 | mqtt->subscribe(topic); 293 | delay(50); 294 | 295 | snprintf(topic, sizeof(topic)-1, "%s/%s/cmd/%s", 296 | settings.mqttBaseTopic, systemID().c_str(), MQTT_SUBTOPIC_RST); 297 | mqtt->subscribe(topic); 298 | delay(50); 299 | } 300 | 301 | 302 | // connect to MQTT server (with changing id on every attempt) 303 | // give up after three consecutive connection failures and 304 | // retry after MQTT_CONN_RETRY_SECS seconds 305 | static bool mqttConnect() { 306 | static char clientid[32]; 307 | static uint32 mqttErrorMillis = 0; 308 | uint8_t mqttError = 0; 309 | 310 | if (mqttErrorMillis > 0 && tsDiff(mqttErrorMillis) < (MQTT_CONN_RETRY_SECS * 1000)) 311 | return false; 312 | 313 | if (!WiFi.isConnected()) { 314 | Serial.printf("WiFi not available, cannot connect to MQTT broker %s\n", settings.mqttBroker); 315 | mqttErrorMillis = millis(); 316 | return false; 317 | } 318 | 319 | mqttInit(); 320 | if (mqtt->connected()) { 321 | publishHADiscoveryMessage(settings.enableHADiscovery); 322 | mqttErrorMillis = 0; 323 | return true; 324 | } 325 | 326 | while (!mqtt->connected() && mqttError < 3) { 327 | snprintf(clientid, sizeof(clientid), MQTT_CLIENT_ID, (int)random(0xfffff)); 328 | Serial.printf("Connecting to MQTT broker %s as %s", settings.mqttBroker, clientid); 329 | if (settings.mqttEnableAuth) 330 | Serial.printf(" with username %s", settings.mqttUsername); 331 | Serial.printf(" on port %d%s...", settings.mqttBrokerPort, settings.mqttSecure ? " (TLS)" : ""); 332 | if (settings.mqttEnableAuth && mqtt->connect(clientid, settings.mqttUsername, settings.mqttPassword)) { 333 | Serial.println(F("OK")); 334 | subCmdTopics(); 335 | publishHADiscoveryMessage(settings.enableHADiscovery); 336 | mqttErrorMillis = 0; 337 | mqttError = 0; 338 | return true; 339 | } else if (!settings.mqttEnableAuth && mqtt->connect(clientid)) { 340 | Serial.println(F("OK")); 341 | subCmdTopics(); 342 | publishHADiscoveryMessage(settings.enableHADiscovery); 343 | mqttErrorMillis = 0; 344 | mqttError = 0; 345 | return true; 346 | } else { 347 | mqttError++; 348 | Serial.printf("failed (error %d)\n", mqtt->state()); 349 | delay(250); 350 | } 351 | } 352 | 353 | if (mqttError >= 3) { 354 | setMessage("mqttConnFailed", 3); 355 | Serial.printf("Retry MQTT connection in %d seconds...\n", MQTT_CONN_RETRY_SECS); 356 | mqttErrorMillis = millis(); 357 | mqttError = 0; 358 | return false; 359 | } 360 | 361 | return true; 362 | } 363 | 364 | 365 | // publish empty message to unset given cmd topic (retained) 366 | void mqttUnsetTopic(const char* topic) { 367 | char topicStr[128]; 368 | 369 | if (mqttConnect()) { 370 | snprintf(topicStr, sizeof(topicStr), "%s/%s/cmd/%s", 371 | settings.mqttBaseTopic, systemID().c_str(), topic); 372 | if (mqtt->publish(topicStr, new byte[0], 0, true)) { 373 | Serial.printf("MQTT unset %s\n", topicStr); 374 | } else { 375 | Serial.printf("MQTT unset %s failed!\n", topicStr); 376 | } 377 | } 378 | } 379 | 380 | 381 | // publish data on multiple topics 382 | static void publishDataSingle() { 383 | static char topicStr[128]; 384 | uint8_t mqttError = 0; 385 | 386 | if (mqttConnect()) { 387 | snprintf(topicStr, sizeof(topicStr), "%s/%s/state/%s", 388 | settings.mqttBaseTopic, systemID().c_str(), MQTT_SUBTOPIC_CNT); 389 | if (mqtt->publish(topicStr, String(settings.counterTotal).c_str(), false)) 390 | Serial.printf("MQTT %s %d\n", topicStr, settings.counterTotal); 391 | else { 392 | Serial.printf("MQTT %s failed!\n", topicStr); 393 | mqttError++; 394 | } 395 | delay(50); 396 | 397 | // need counter offset to publish total consumption (kwh) 398 | if (ferraris.consumption > 0) { 399 | snprintf(topicStr, sizeof(topicStr), "%s/%s/state/%s", 400 | settings.mqttBaseTopic, systemID().c_str(), MQTT_SUBTOPIC_CONS); 401 | if (mqtt->publish(topicStr, String(ferraris.consumption, 2).c_str(), false)) { 402 | Serial.printf("MQTT %s ", topicStr); 403 | Serial.println(ferraris.consumption, 2); // float! 404 | } else { 405 | Serial.printf("MQTT %s failed!\n", topicStr); 406 | mqttError++; 407 | } 408 | delay(50); 409 | } 410 | 411 | if (ferraris.power > -1) { 412 | snprintf(topicStr, sizeof(topicStr), "%s/%s/state/%s", 413 | settings.mqttBaseTopic, systemID().c_str(), MQTT_SUBTOPIC_PWR); 414 | if (mqtt->publish(topicStr, String(ferraris.power).c_str(), false)) 415 | Serial.printf("MQTT %s %d\n", topicStr, ferraris.power); 416 | else { 417 | Serial.printf("MQTT %s failed!\n", topicStr); 418 | mqttError++; 419 | } 420 | delay(50); 421 | } 422 | 423 | snprintf(topicStr, sizeof(topicStr), "%s/%s/state/%s", 424 | settings.mqttBaseTopic, systemID().c_str(), MQTT_SUBTOPIC_TXINT); 425 | if (mqtt->publish(topicStr, String(settings.mqttIntervalSecs).c_str(), false)) 426 | Serial.printf("MQTT %s %d\n", topicStr, settings.mqttIntervalSecs); 427 | else { 428 | Serial.printf("MQTT %s failed!\n", topicStr); 429 | mqttError++; 430 | } 431 | delay(50); 432 | 433 | snprintf(topicStr, sizeof(topicStr), "%s/%s/state/%s", 434 | settings.mqttBaseTopic, systemID().c_str(), MQTT_SUBTOPIC_RUNT); 435 | if (mqtt->publish(topicStr, getRuntime(true), true)) 436 | Serial.printf("MQTT %s %s\n", topicStr, getRuntime(true)); 437 | else { 438 | Serial.printf("MQTT %s failed!\n", topicStr); 439 | mqttError++; 440 | } 441 | delay(50); 442 | 443 | snprintf(topicStr, sizeof(topicStr), "%s/%s/state/%s", 444 | settings.mqttBaseTopic, systemID().c_str(), MQTT_SUBTOPIC_RSSI); 445 | if (mqtt->publish(topicStr, String(WiFi.RSSI()).c_str(), false)) 446 | Serial.printf("MQTT %s %d\n", topicStr, WiFi.RSSI()); 447 | else { 448 | Serial.printf("MQTT %s failed!\n", topicStr); 449 | mqttError++; 450 | } 451 | delay(50); 452 | 453 | snprintf(topicStr, sizeof(topicStr), "%s/%s/state/%s", 454 | settings.mqttBaseTopic, systemID().c_str(), MQTT_SUBTOPIC_PSAVE); 455 | if (mqtt->publish(topicStr, String(settings.enablePowerSavingMode ? 1 : 0).c_str(), false)) 456 | Serial.printf("MQTT %s %d\n", topicStr, settings.enablePowerSavingMode ? 1 : 0); 457 | else { 458 | Serial.printf("MQTT %s failed!\n", topicStr); 459 | mqttError++; 460 | } 461 | delay(50); 462 | 463 | // in power saving mode publish total number of seconds connected to WiFi 464 | // if continuously conntected to WiFi publish number of WiFi reconnects 465 | if (settings.enablePowerSavingMode) { 466 | snprintf(topicStr, sizeof(topicStr), "%s/%s/state/%s", 467 | settings.mqttBaseTopic, systemID().c_str(), MQTT_SUBTOPIC_ONAIR); 468 | if (mqtt->publish(topicStr, String(wifiOnlineTenthSecs/10).c_str(), false)) 469 | Serial.printf("MQTT %s %d\n", topicStr, wifiOnlineTenthSecs/10); 470 | else { 471 | Serial.printf("MQTT %s failed!\n", topicStr); 472 | mqttError++; 473 | } 474 | delay(50); 475 | } else { 476 | snprintf(topicStr, sizeof(topicStr), "%s/%s/state/%s", 477 | settings.mqttBaseTopic, systemID().c_str(), MQTT_SUBTOPIC_WIFI); 478 | if (mqtt->publish(topicStr, String(wifiReconnectCounter).c_str(), false)) 479 | Serial.printf("MQTT %s %d\n", topicStr, wifiReconnectCounter); 480 | else { 481 | Serial.printf("MQTT %s failed!\n", topicStr); 482 | mqttError++; 483 | } 484 | delay(50); 485 | } 486 | 487 | snprintf(topicStr, sizeof(topicStr), "%s/%s/state/version", 488 | settings.mqttBaseTopic, systemID().c_str()); 489 | if (mqtt->publish(topicStr, String(FIRMWARE_VERSION).c_str(), false)) 490 | Serial.printf("MQTT %s %d\n", topicStr, FIRMWARE_VERSION); 491 | else { 492 | Serial.printf("MQTT %s failed!\n", topicStr); 493 | mqttError++; 494 | } 495 | delay(50); 496 | 497 | #ifdef DEBUG_HEAP 498 | snprintf(topicStr, sizeof(topicStr), "%s/%s/state/%s", 499 | settings.mqttBroker, systemID().c_str(), MQTT_SUBTOPIC_HEAP); 500 | if (mqtt->publish(topicStr, String(ESP.getFreeHeap()).c_str(), false)) 501 | Serial.printf("%s %d\n", topicStr, ESP.getFreeHeap()); 502 | else { 503 | Serial.printf("MQTT %s failed!\n", topicStr); 504 | mqttError++; 505 | } 506 | #endif 507 | } 508 | if (!mqttError) 509 | setMessage("publishData", 3); 510 | else 511 | setMessage("publishFailed", 3); 512 | } 513 | 514 | 515 | // publish data on base topic as JSON 516 | static void publishDataJSON() { 517 | StaticJsonDocument<192> JSON; 518 | static char topicStr[128]; 519 | 520 | JSON.clear(); 521 | if (mqttConnect()) { 522 | JSON[MQTT_SUBTOPIC_CNT] = settings.counterTotal; 523 | if (ferraris.consumption > 0) 524 | JSON[MQTT_SUBTOPIC_CONS] = int(ferraris.consumption * 100) / 100.0; 525 | if (ferraris.power > -1) 526 | JSON[MQTT_SUBTOPIC_PWR] = ferraris.power; 527 | JSON[MQTT_SUBTOPIC_TXINT] = settings.mqttIntervalSecs; 528 | JSON[MQTT_SUBTOPIC_RUNT] = atoi(getRuntime(true)); 529 | 530 | // in power saving mode publish total number of seconds connected to WiFi 531 | // if continuously conntected to WiFi publish number of WiFi reconnects 532 | if (!settings.enablePowerSavingMode) { 533 | JSON[MQTT_SUBTOPIC_PSAVE] = 0; 534 | JSON[MQTT_SUBTOPIC_WIFI] = wifiReconnectCounter; 535 | } else { 536 | JSON[MQTT_SUBTOPIC_PSAVE] = 1; 537 | JSON[MQTT_SUBTOPIC_ONAIR] = wifiOnlineTenthSecs/10; 538 | } 539 | 540 | JSON[MQTT_SUBTOPIC_RSSI] = WiFi.RSSI(); 541 | JSON["version"] = FIRMWARE_VERSION; 542 | #ifdef DEBUG_HEAP 543 | JSON[MQTT_SUBTOPIC_HEAP] = ESP.getFreeHeap(); 544 | #endif 545 | snprintf(topicStr, sizeof(topicStr), "%s/%s/state", 546 | settings.mqttBaseTopic, systemID().c_str()); 547 | if (publishJSON(JSON, topicStr, false, true)) 548 | setMessage("publishData", 3); 549 | else 550 | setMessage("publishFailed", 3); 551 | } 552 | } 553 | 554 | 555 | // publish meter reading updates on single 556 | // topic as JSON or on multiple topics 557 | void mqttPublish() { 558 | if (settings.mqttJSON) 559 | publishDataJSON(); 560 | else 561 | publishDataSingle(); 562 | } 563 | 564 | 565 | void mqttDisconnect(bool unsetHAdiscovery) { 566 | if (unsetHAdiscovery) 567 | publishHADiscoveryMessage(false); 568 | if (mqtt != NULL) { 569 | mqtt->disconnect(); 570 | mqtt->~PubSubClient(); 571 | mqtt = NULL; 572 | } 573 | } 574 | 575 | 576 | // check for remote commands 577 | void mqttLoop() { 578 | if (mqttConnect()) 579 | mqtt->loop(); 580 | } 581 | -------------------------------------------------------------------------------- /include/index_en.h: -------------------------------------------------------------------------------- 1 | /*************************************************************************** 2 | Copyright (c) 2019-2023 Lars Wessels 3 | 4 | This file a part of the "ESP8266 Wifi Power Meter" source code. 5 | https://github.com/lrswss/esp8266-wifi-power-meter 6 | 7 | Licensed under the MIT License. You may not use this file except in 8 | compliance with the License. You may obtain a copy of the License at 9 | 10 | https://opensource.org/licenses/MIT 11 | 12 | ***************************************************************************/ 13 | 14 | const char HEADER_html[] PROGMEM = R"=====( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Ferraris Meter __SYSTEMID__ 23 | 45 | )====="; 46 | 47 | 48 | const char MAIN_html[] PROGMEM = R"=====( 49 | 192 | 193 | 194 | 195 |
196 |
197 |

Ferraris Meter __SYSTEMID__

198 | 210 | 211 |
212 |
213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 |
Total rotations:--
Current Consumption:-- W
Total consumption:-- kWh
A/D readings saved:--/--
Minimum/Maximum:--/--
Threshold value:--
Runtime:-d -h -m
WiFi RSSI:-- dBm
223 |
224 |
225 |

226 |

227 |

228 |

229 |

230 |

231 |
232 | )====="; 233 | 234 | 235 | const char EXPERT_html[] PROGMEM = R"=====( 236 | 289 | 290 | 291 |
292 |
293 |

Expert Settings

294 | 299 |
300 | 301 |
302 |
303 |
 Ferraris marker scanner  304 |

Threshold for counter (__PULSE_THRESHOLD_MIN__-__PULSE_THRESHOLD_MAX__)
305 |

306 |

Variance für detection (__READINGS_SPREAD_MIN__-__READINGS_SPREAD_MAX__)
307 |

308 |

Sample rate sensor (__READINGS_INTERVAL_MS_MIN__-__READINGS_INTERVAL_MS_MAX__ ms)
309 |

310 |

Sensor ring buffer (__READINGS_BUFFER_SECS_MIN__-__READINGS_BUFFER_SECS_MAX__ sec.)
311 |

312 |

Pulses to increase counter (__THRESHOLD_TRIGGER_MIN__-__THRESHOLD_TRIGGER_MAX__)
313 |

314 |

Dead time counter (__DEBOUNCE_TIME_MS_MIN__-__DEBOUNCE_TIME_MS_MAX__ ms)
315 |

316 |
317 |
318 | 319 |
 InfluxDB  320 |

Stream raw sensor data

321 |
322 | 323 |
324 |

325 |
326 |

327 |

328 |

329 |
330 | )====="; 331 | 332 | 333 | const char CONFIG_html[] PROGMEM = R"=====( 334 | 507 | 508 | 509 |
510 |
511 |

Settings

512 | 520 |
521 | 522 |
523 |
524 |
 Ferraris Meter  525 |

Rotations per kWh (__KWH_TURNS_MIN__-__KWH_TURNS_MAX__)
526 |

527 |

Current meter reading (kWh)
528 |

529 |

Power meter ID (optional)
530 |

531 |

Auto-backup counter (__BACKUP_CYCLE_MIN__-__BACKUP_CYCLE_MAX__ min.)
532 |

533 |

Calculate current consumption

534 |

Enable moving average

535 |

Averaging interval (__POWER_AVG_SECS_MIN__-__POWER_AVG_SECS_MAX__ sec.)
536 |

537 |
538 |
539 |
540 | 541 |
 MQTT  542 |

Enable

543 | 564 |
565 |
566 | 567 |

568 |
569 |

570 |

571 |

572 |

573 |
574 | )====="; 575 | 576 | 577 | const char UPDATE_html[] PROGMEM = R"=====( 578 | 618 | 619 | 620 |
621 |
622 |

Firmware update

623 | 624 | 625 |
626 |
627 | 1. Select firmware file *.bin
628 | 2. Start update
629 | 3. Upload takes about 20 sec.
630 | 4. Restart system 631 |
632 |
633 |
634 | 635 |

636 |
637 |

638 |
639 | )====="; 640 | 641 | 642 | const char UPDATE_OK_html[] PROGMEM = R"=====( 643 | 672 | 673 | 674 |
675 |
676 |

Firmware update
successful

677 | 681 |
682 |
683 |

684 |

685 |
686 | )====="; 687 | 688 | 689 | const char UPDATE_ERR_html[] PROGMEM = R"=====( 690 | 691 | 692 |
693 |
694 |

Firmware update
failed

695 |
696 |
697 |

698 |

699 |
700 | )====="; 701 | 702 | 703 | const char IMPORT_html[] PROGMEM = R"=====( 704 | 744 | 745 | 746 |
747 |
748 |

Import system settings

749 | 750 | 751 |
752 |
753 | 1. Select JSON configuration file
754 | 2. Start upload
755 | 3. Restart system 756 |
757 |
758 |
759 | 760 |

761 |
762 |

763 |
764 | )====="; 765 | 766 | 767 | const char IMPORT_OK_html[] PROGMEM = R"=====( 768 | 780 | 781 | 782 |
783 |
784 |

Settings import
successful

785 | 788 |
789 |
790 |

791 |

792 |
793 | )====="; 794 | 795 | 796 | const char IMPORT_ERR_html[] PROGMEM = R"=====( 797 | 798 | 799 |
800 |
801 |

Settings import
failed

802 |
803 |
804 |

805 |

806 |
807 | )====="; 808 | 809 | 810 | const char FOOTER_html[] PROGMEM = R"=====( 811 | 820 |
821 | 822 | 823 | )====="; 824 | -------------------------------------------------------------------------------- /include/index_de.h: -------------------------------------------------------------------------------- 1 | /*************************************************************************** 2 | Copyright (c) 2019-2023 Lars Wessels 3 | 4 | This file a part of the "ESP8266 Wifi Power Meter" source code. 5 | https://github.com/lrswss/esp8266-wifi-power-meter 6 | 7 | Licensed under the MIT License. You may not use this file except in 8 | compliance with the License. You may obtain a copy of the License at 9 | 10 | https://opensource.org/licenses/MIT 11 | 12 | ***************************************************************************/ 13 | 14 | const char HEADER_html[] PROGMEM = R"=====( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Wifi Stromzähler __SYSTEMID__ 23 | 45 | )====="; 46 | 47 | 48 | const char MAIN_html[] PROGMEM = R"=====( 49 | 192 | 193 | 194 | 195 |
196 |
197 |

Stromzähler __SYSTEMID__

198 | 210 | 211 |
212 |
213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 |
Anzahl Umdrehungen:--
Momentanleistung:-- W
Gesamtverbrauch:-- kWh
A/D Messwerte:--/--
Minimum/Maximum:--/--
Impulsschwellwert:--
Laufzeit:--d --h --m
WLAN RSSI:-- dBm
223 |
224 |
225 |

226 |

227 |

228 |

229 |

230 |

231 |
232 | )====="; 233 | 234 | 235 | const char EXPERT_html[] PROGMEM = R"=====( 236 | 289 | 290 | 291 |
292 |
293 |

Experteneinstellungen

294 | 299 |
300 | 301 |
302 |
303 |
 Ferraris-Abtastung  304 |

Schwellwert für Zählung (__PULSE_THRESHOLD_MIN__-__PULSE_THRESHOLD_MAX__)
305 |

306 |

Varianz für Erkennung (__READINGS_SPREAD_MIN__-__READINGS_SPREAD_MAX__)
307 |

308 |

Abtastrate IR-Sensor (__READINGS_INTERVAL_MS_MIN__-__READINGS_INTERVAL_MS_MAX__)
309 |

310 |

Ringspeicher (__READINGS_BUFFER_SECS_MIN__-__READINGS_BUFFER_SECS_MAX__ Sek.)
311 |

312 |

Pulse für Zählung (__THRESHOLD_TRIGGER_MIN__-__THRESHOLD_TRIGGER_MAX__)
313 |

314 |

Totzeit Zählungen (__DEBOUNCE_TIME_MS_MIN__-__DEBOUNCE_TIME_MS_MAX__ ms)
315 |

316 |
317 |
318 | 319 |
 InfluxDB  320 |

Sensorrohdaten streamen

321 |
322 | 323 |
324 |

325 |
326 |

327 |

328 |

329 |
330 | )====="; 331 | 332 | 333 | const char CONFIG_html[] PROGMEM = R"=====( 334 | 508 | 509 | 510 |
511 |
512 |

Einstellungen

513 | 521 |
522 | 523 |
524 |
525 |
 Ferraris-Zähler  526 |

Umdrehungen pro kWh (__KWH_TURNS_MIN__-__KWH_TURNS_MAX__)
527 |

528 |

Aktueller Zählerstand (kWh)
529 |

530 |

Zählernummer (optional)
531 |

532 |

Backup Zählerstand (__BACKUP_CYCLE_MIN__-__BACKUP_CYCLE_MAX__ Min.)
533 |

534 |

Momentanleistung errechnen

535 |

Gleitender Durchschnitt

536 |

Zeitraum (__POWER_AVG_SECS_MIN__-__POWER_AVG_SECS_MAX__ Sek.)
537 |

538 |
539 |
540 |
541 | 542 |
 MQTT  543 |

Aktivieren

544 | 565 |
566 |
567 | 568 |

569 |
570 |

571 |

572 |

573 |

574 |
575 | )====="; 576 | 577 | 578 | const char UPDATE_html[] PROGMEM = R"=====( 579 | 619 | 620 | 621 |
622 |
623 |

Firmware-Update

624 | 625 | 626 |
627 |
628 | 1. Firmware-Datei *.bin auswählen
629 | 2. Aktualisierung starten
630 | 3. Upload dauert ca. 20 Sek.
631 | 4. System neu starten 632 |
633 |
634 |
635 | 636 |

637 |
638 |

639 |
640 | )====="; 641 | 642 | 643 | const char UPDATE_OK_html[] PROGMEM = R"=====( 644 | 673 | 674 | 675 |
676 |
677 |

Firmware-Update
erfolgreich

678 | 682 |
683 |
684 |

685 |

686 |
687 | )====="; 688 | 689 | 690 | const char UPDATE_ERR_html[] PROGMEM = R"=====( 691 | 692 | 693 |
694 |
695 |

Firmware-Update
fehlgeschlagen

696 |
697 |
698 |

699 |

700 |
701 | )====="; 702 | 703 | 704 | const char IMPORT_html[] PROGMEM = R"=====( 705 | 745 | 746 | 747 |
748 |
749 |

Einstellungen importieren

750 | 751 | 752 |
753 |
754 | 1. Konfigurationsdatei auswählen
755 | 2. Import der Datei starten
756 | 3. System neu starten 757 |
758 |
759 |
760 | 761 |

762 |
763 |

764 |
765 | )====="; 766 | 767 | 768 | const char IMPORT_OK_html[] PROGMEM = R"=====( 769 | 781 | 782 | 783 |
784 |
785 |

Einstellung importieren
erfolgreich

786 | 789 |
790 |
791 |

792 |

793 |
794 | )====="; 795 | 796 | 797 | const char IMPORT_ERR_html[] PROGMEM = R"=====( 798 | 799 | 800 |
801 |
802 |

Einstellungen importieren
fehlgeschlagen

803 |
804 |
805 |

806 |

807 |
808 | )====="; 809 | 810 | 811 | const char FOOTER_html[] PROGMEM = R"=====( 812 | 821 |
822 | 823 | 824 | )====="; 825 | --------------------------------------------------------------------------------