├── .gitignore ├── src ├── IO.ino ├── LED.ino ├── RTTTL.ino ├── Buzzer.ino ├── OTA.ino ├── Config.ino ├── ESP32_VuePower.ino ├── WiFi.ino ├── NTPTime.ino ├── Sensors.ino ├── HA.ino ├── MQTT.ino └── Webserver.ino ├── include ├── NTPTime.h ├── Pins.h ├── Macros.h ├── Config.h ├── Sensors.h └── HA.h ├── flash-command.sh ├── LICENSE ├── platformio.ini └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | -------------------------------------------------------------------------------- /src/IO.ino: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | 4 | void io_setup() 5 | { 6 | } 7 | 8 | bool io_loop() 9 | { 10 | return false; 11 | } 12 | -------------------------------------------------------------------------------- /include/NTPTime.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | void time_setup(); 6 | const char *Time_getStateString(); 7 | bool time_loop(); 8 | void printTime(); 9 | int secs_to_tm(long long t, struct tm *tm); 10 | void getTimeAdv(struct tm *tm, unsigned long offset); 11 | void getTime(struct tm *tm); 12 | void getStartupTime(struct tm *tm); 13 | void sendNTPpacket(IPAddress &address); 14 | -------------------------------------------------------------------------------- /flash-command.sh: -------------------------------------------------------------------------------- 1 | 2 | ./esptool.py -b 921600 --port /dev/ttyS3 write_flash \ 3 | 0x1000 /mnt/c/Users/g3gg0/Documents/PlatformIO/Projects/ESP32_Vue/.pio/build/VuePower/bootloader.bin \ 4 | 0x8000 /mnt/c/Users/g3gg0/Documents/PlatformIO/Projects/ESP32_Vue/.pio/build/VuePower/partitions.bin \ 5 | 0xe000 /mnt/c/Users/g3gg0/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin \ 6 | 0x10000 /mnt/c/Users/g3gg0/Documents/PlatformIO/Projects/ESP32_Vue/.pio/build/VuePower/firmware.bin 7 | -------------------------------------------------------------------------------- /include/Pins.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define DIO_BUZZER 12 4 | #define DIO_BUZZER_GND 27 5 | #define DIO_SDA 21 6 | #define DIO_SCL 22 7 | #define DIO_LED 23 8 | 9 | /* from https://github.com/emporia-vue-local/esphome/discussions/173 */ 10 | #define ATMEL_SWDIO 13 11 | #define ATMEL_SWCLK 14 12 | #define ATMEL_RST 26 13 | 14 | /* hardware config 15 | 0x4 - Emporia Vue 2 - "VUE Smart Home Energy Meter" - REV-4/REV-5 16 | */ 17 | #define HWCFG_0 34 0 18 | #define HWCFG_1 35 0 19 | #define HWCFG_2 32 1 20 | #define HWCFG_3 33 0 21 | -------------------------------------------------------------------------------- /src/LED.ino: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include 4 | 5 | bool led_inhibit = false; 6 | 7 | void led_setup() 8 | { 9 | pinMode(DIO_LED, OUTPUT); 10 | } 11 | 12 | void led_set_adv(uint8_t n, uint8_t r, uint8_t g, uint8_t b, bool commit) 13 | { 14 | } 15 | 16 | void led_set(uint8_t n, uint8_t r, uint8_t g, uint8_t b) 17 | { 18 | if (n == 1) 19 | { 20 | digitalWrite(DIO_LED, !(r || g || b)); 21 | } 22 | } 23 | 24 | void led_set_inhibit(bool state) 25 | { 26 | } 27 | 28 | bool led_loop() 29 | { 30 | return false; 31 | } 32 | -------------------------------------------------------------------------------- /src/RTTTL.ino: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | 5 | MD_RTTTLParser Tone; 6 | MD_MusicTable Table; 7 | 8 | void rtttl_cb (uint8_t octave, uint8_t noteId, uint32_t duration, bool activate) 9 | { 10 | if(activate) 11 | { 12 | if(Table.findNoteOctave(noteId, octave)) 13 | { 14 | buzz_on(Table.getFrequency()); 15 | } 16 | } 17 | else 18 | { 19 | buzz_off(); 20 | } 21 | } 22 | 23 | void rtttl_setup() 24 | { 25 | Tone.begin(); 26 | Tone.setCallback(&rtttl_cb); 27 | } 28 | 29 | bool rtttl_loop() 30 | { 31 | Tone.run(); 32 | 33 | return false; 34 | } 35 | 36 | void rtttl_play(const char *rtttl) 37 | { 38 | Tone.setTune(rtttl); 39 | } 40 | -------------------------------------------------------------------------------- /include/Macros.h: -------------------------------------------------------------------------------- 1 | #ifndef __MACROS_H__ 2 | #define __MACROS_H__ 3 | 4 | // #define min(a, b) ({ __typeof__ (a) _a = (a); __typeof__ (b) _b = (b); _a < _b ? _a : _b; }) 5 | // #define max(a, b) ({ __typeof__ (a) _a = (a); __typeof__ (b) _b = (b); _a > _b ? _a : _b; }) 6 | #define coerce(val, min, max) \ 7 | do \ 8 | { \ 9 | if ((val) > (max)) \ 10 | { \ 11 | val = max; \ 12 | } \ 13 | else if ((val) < (min)) \ 14 | { \ 15 | val = min; \ 16 | } \ 17 | } while (0) 18 | #define xstr(s) str(s) 19 | #define str(s) #s 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /src/Buzzer.ino: -------------------------------------------------------------------------------- 1 | 2 | #include "Config.h" 3 | #include 4 | 5 | #define BUZZER_LEDC 0 6 | 7 | #define PWM_BITS 9 8 | #define PWM_PCT(x) ((uint32_t)((100.0f - x) * ((1UL << (PWM_BITS)) - 1) / 100.0f)) 9 | 10 | void buzz_setup() 11 | { 12 | pinMode(DIO_BUZZER, OUTPUT); 13 | pinMode(DIO_BUZZER_GND, OUTPUT); 14 | digitalWrite(DIO_BUZZER_GND, LOW); 15 | } 16 | 17 | bool buzz_loop() 18 | { 19 | return false; 20 | } 21 | 22 | void buzz_on(uint32_t freq) 23 | { 24 | ledcSetup(BUZZER_LEDC, freq, PWM_BITS); 25 | ledcAttachPin(DIO_BUZZER, BUZZER_LEDC); 26 | ledcWrite(BUZZER_LEDC, PWM_PCT(50)); 27 | } 28 | 29 | void buzz_off() 30 | { 31 | ledcWrite(BUZZER_LEDC, 0); 32 | } 33 | 34 | void buzz_beep(uint32_t freq, uint32_t duration) 35 | { 36 | buzz_on(freq); 37 | delay(duration); 38 | buzz_off(); 39 | } 40 | -------------------------------------------------------------------------------- /include/Config.h: -------------------------------------------------------------------------------- 1 | #ifndef __CONFIG_H__ 2 | #define __CONFIG_H__ 3 | 4 | #define CONFIG_SOFTAPNAME "esp32-config" 5 | #define CONFIG_OTANAME "VuePower" 6 | #define CONFIG_MAXFAILS 10 7 | 8 | #define CONFIG_MAGIC 0xE1AAFF01 9 | 10 | #define CONFIG_PUBLISH_MQTT 1 11 | #define CONFIG_PUBLISH_HA 2 12 | #define CONFIG_PUBLISH_DEBUG 4 13 | 14 | #define CONFIG_VERBOSE_SERIAL 1 15 | 16 | typedef struct 17 | { 18 | uint32_t magic; 19 | 20 | char hostname[32]; 21 | char wifi_ssid[32]; 22 | char wifi_password[32]; 23 | 24 | char mqtt_server[32]; 25 | int mqtt_port; 26 | char mqtt_user[32]; 27 | char mqtt_password[32]; 28 | char mqtt_client[32]; 29 | 30 | uint16_t verbose; 31 | uint16_t boot_count; 32 | uint32_t mqtt_publish; 33 | 34 | float frequency_calib; 35 | float sensor_calib_phase[3]; 36 | float sensor_calib_phase_voltage[3]; 37 | float sensor_calib_channel[16]; 38 | int sensor_phase[16]; 39 | char channel_name[16][32]; 40 | } t_cfg; 41 | 42 | extern t_cfg current_config; 43 | 44 | #endif 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 g3gg0.de 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 all 13 | 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 THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /include/Sensors.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define CHANNEL_STATUS_OK 0 4 | #define CHANNEL_STATUS_NOTCONNECTED 1 5 | 6 | #define PHASE_STATUS_OK 0 7 | #define PHASE_STATUS_NOTCONNECTED 1 8 | 9 | #define POWER_PT1 8 10 | 11 | typedef struct 12 | { 13 | float average; 14 | float per_second[60]; 15 | uint32_t counter[60]; 16 | uint32_t tm_min_last; 17 | } sensor_minute_stat_t; 18 | 19 | typedef struct 20 | { 21 | float current; 22 | float power; 23 | float power_filtered; 24 | float voltage; 25 | float angle; 26 | sensor_minute_stat_t minute_stats; 27 | float power_total; 28 | float power_draw_total; 29 | float power_inject_total; 30 | float power_daily; 31 | uint32_t status; 32 | } sensor_phase_data_t; 33 | 34 | typedef struct 35 | { 36 | float current; 37 | float power_real; 38 | float power_filtered; 39 | float power[3]; 40 | float power_calc[3]; 41 | float power_phase_match[3]; 42 | sensor_minute_stat_t minute_stats; 43 | float power_total; 44 | float power_draw_total; 45 | float power_inject_total; 46 | float power_daily; 47 | int32_t phase_match; 48 | uint32_t status; 49 | } sensor_ch_data_t; 50 | 51 | typedef struct 52 | { 53 | char state[64]; 54 | float frequency; 55 | sensor_phase_data_t phases[3]; 56 | sensor_ch_data_t channels[16]; 57 | } sensor_data_t; 58 | 59 | extern sensor_data_t sensor_data; 60 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | 2 | 3 | [env:VuePower] 4 | platform = espressif32 5 | board = esp32dev 6 | framework = arduino 7 | monitor_speed = 115200 8 | build_flags = !bash -c "echo -Isrc -DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_PORT=Serial -DPIO_SRC_REVNUM=$(git rev-list --count HEAD) -DPIO_SRC_REV=$(git rev-parse --short HEAD)" 9 | board_build.flash_mode = dio 10 | lib_deps = knolleary/PubSubClient @ ^2.8 11 | suculent/ESP32httpUpdate@^2.1.145 12 | https://github.com/MajicDesigns/MD_MusicTable 13 | https://github.com/MajicDesigns/MD_RTTTLParser 14 | 15 | 16 | [env:VuePower_OTA] 17 | platform = espressif32 18 | board = esp32dev 19 | framework = arduino 20 | monitor_speed = 115200 21 | build_flags = !bash -c "echo -Isrc -DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_PORT=Serial -DPIO_SRC_REVNUM=$(git rev-list --count HEAD) -DPIO_SRC_REV=$(git rev-parse --short HEAD)" 22 | lib_deps = knolleary/PubSubClient @ ^2.8 23 | suculent/ESP32httpUpdate@^2.1.145 24 | https://github.com/MajicDesigns/MD_MusicTable 25 | https://github.com/MajicDesigns/MD_RTTTLParser 26 | monitor_filters = esp32_exception_decoder 27 | upload_protocol = espota 28 | upload_port = 192.168.1.38 29 | 30 | [env:VuePower_OTAdev] 31 | platform = espressif32 32 | board = esp32dev 33 | framework = arduino 34 | monitor_speed = 115200 35 | build_flags = !bash -c "echo -Isrc -DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_PORT=Serial -DPIO_SRC_REVNUM=$(git rev-list --count HEAD) -DPIO_SRC_REV=$(git rev-parse --short HEAD)" 36 | lib_deps = knolleary/PubSubClient @ ^2.8 37 | suculent/ESP32httpUpdate@^2.1.145 38 | https://github.com/MajicDesigns/MD_MusicTable 39 | https://github.com/MajicDesigns/MD_RTTTLParser 40 | monitor_filters = esp32_exception_decoder 41 | upload_protocol = espota 42 | upload_port = 192.168.1.91 43 | -------------------------------------------------------------------------------- /src/OTA.ino: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | 4 | bool ota_active = false; 5 | bool ota_setup_done = false; 6 | uint32_t ota_offtime = 0; 7 | 8 | void ota_setup() 9 | { 10 | if (ota_setup_done) 11 | { 12 | ota_enable(); 13 | return; 14 | } 15 | Serial.printf("[OTA] setHostname\n"); 16 | ArduinoOTA.setHostname(CONFIG_OTANAME); 17 | 18 | Serial.printf("[OTA] onStart\n"); 19 | ArduinoOTA.onStart([]() 20 | { 21 | Serial.printf("[OTA] starting\n"); 22 | led_set(0, 255, 0, 255); 23 | ota_active = true; 24 | ota_offtime = millis() + 600000; }) 25 | .onEnd([]() 26 | { ota_active = false; }) 27 | .onProgress([](unsigned int progress, unsigned int total) 28 | { led_set(0, 255 - (progress / (total / 255)), 0, (progress / (total / 255))); }) 29 | .onError([](ota_error_t error) 30 | { 31 | Serial.printf("Error[%u]: ", error); 32 | if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); 33 | else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); 34 | else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); 35 | else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); 36 | else if (error == OTA_END_ERROR) Serial.println("End Failed"); }); 37 | 38 | Serial.printf("[OTA] begin\n"); 39 | ArduinoOTA.begin(); 40 | 41 | Serial.printf("[OTA] Setup finished\n"); 42 | 43 | ota_setup_done = true; 44 | ota_enable(); 45 | } 46 | 47 | void ota_enable() 48 | { 49 | Serial.printf("[OTA] Enabled\n"); 50 | ota_offtime = millis() + 600000; 51 | } 52 | 53 | bool ota_enabled() 54 | { 55 | return (ota_offtime > millis() || ota_active); 56 | } 57 | 58 | bool ota_loop() 59 | { 60 | if (ota_enabled()) 61 | { 62 | ArduinoOTA.handle(); 63 | } 64 | 65 | return ota_active; 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP32_VuePower 2 | 3 | See: https://www.g3gg0.de/wordpress/uncategorized/emporia-vue-2-custom-firmware/ 4 | 5 | Excerpt: 6 | 7 | After a few hours, with the ESPHome fork as a great reference, I built a custom firmware for the Vue with Home Assistant support and bilancing counters to properly track how much power you get from the grid and how much you inject back. Tracking all the measurements per phase of course. 8 | 9 | While I have still some problems understanding the exact way it measures current and the power in reference to the three phases, I still have some primitive autodetection, trying to find the phase which has the most correlation with the current signal. However the current I measure is sometimes far beyond what the power measurements show. This can not be only due to the power factor. I guess there is a current offset, that would have to be trained as well. But the current measurement isn’t really interesting, so I’d probably skip that. 10 | 11 | For the case when the autodetection does not reliable detect the right phase – which is ithe case for e.g. solar inverters, driving energy into the grid – you can manually specify the correct phase (0, 1, 2) via the very simple web interface, so there is no doubt we are using the correct values. Init values are -1 for “autodetect”. In doubt, set manually, which channel is powered by which phase. 12 | Screenshot of the web interface 13 | 14 | Also the channels names can be configured in the web interface, so they show up in your Home Assistant with that name if you enabled that option. But make sure you do not use any special characters, as they might (actually will…) cause trouble in Home Assistant. Guess I should encode them, but… meh… some other day. 15 | 16 | Flashing the firmware from PlatformIO did not work for me. I always got an non-explanatory error when esptool tried to flash the device. 17 | So I ran it manually and that worked. Here is the command i used: 18 | 19 | ./esptool.py -b 921600 --port /dev/ttyS3 write_flash \ 20 | 0x1000 ESP32_Vue/.pio/build/VuePower/bootloader.bin \ 21 | 0x8000 ESP32_Vue/.pio/build/VuePower/partitions.bin \ 22 | 0xe000 .platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin \ 23 | 0x10000 ESP32_Vue/.pio/build/VuePower/firmware.bin 24 | 25 | After flashing, the firmware will start an access point with the name esp32-config, waiting for you to connect, open up http://192.168.4.1 and configure WiFi. Then you can configure it from a proper web browser. 26 | -------------------------------------------------------------------------------- /src/Config.ino: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | 5 | #include "Config.h" 6 | 7 | t_cfg current_config; 8 | bool config_valid = false; 9 | 10 | void cfg_save() 11 | { 12 | File file = SPIFFS.open("/config.dat", "w"); 13 | if (!file || file.isDirectory()) 14 | { 15 | return; 16 | } 17 | 18 | if (strlen(current_config.hostname) < 2) 19 | { 20 | strcpy(current_config.hostname, CONFIG_OTANAME); 21 | } 22 | 23 | file.write((uint8_t *)¤t_config, sizeof(current_config)); 24 | file.close(); 25 | } 26 | 27 | void cfg_reset() 28 | { 29 | memset(¤t_config, 0x00, sizeof(current_config)); 30 | 31 | current_config.magic = CONFIG_MAGIC; 32 | strcpy(current_config.hostname, CONFIG_OTANAME); 33 | strcpy(current_config.mqtt_server, ""); 34 | current_config.mqtt_port = 11883; 35 | strcpy(current_config.mqtt_user, ""); 36 | strcpy(current_config.mqtt_password, ""); 37 | strcpy(current_config.mqtt_client, CONFIG_OTANAME); 38 | current_config.mqtt_publish = 0; 39 | current_config.verbose = 0; 40 | current_config.boot_count = 0; 41 | 42 | strcpy(current_config.wifi_ssid, ""); 43 | strcpy(current_config.wifi_password, ""); 44 | 45 | current_config.frequency_calib = 1.0f; 46 | current_config.sensor_calib_phase[0] = 0.022f; 47 | current_config.sensor_calib_phase[1] = 0.022f; 48 | current_config.sensor_calib_phase[2] = 0.022f; 49 | current_config.sensor_calib_phase_voltage[0] = 0.022f; 50 | current_config.sensor_calib_phase_voltage[1] = 0.022f; 51 | current_config.sensor_calib_phase_voltage[2] = 0.022f; 52 | for (int ch = 0; ch < 16; ch++) 53 | { 54 | current_config.sensor_calib_channel[ch] = 0.022f; 55 | current_config.sensor_phase[ch] = -1; 56 | sprintf(current_config.channel_name[ch], "Channel #%d", ch + 1); 57 | } 58 | } 59 | 60 | void cfg_read() 61 | { 62 | File file = SPIFFS.open("/config.dat", "r"); 63 | 64 | config_valid = false; 65 | 66 | if (!file || file.isDirectory()) 67 | { 68 | cfg_reset(); 69 | } 70 | else 71 | { 72 | file.read((uint8_t *)¤t_config, sizeof(current_config)); 73 | file.close(); 74 | 75 | if (current_config.magic != CONFIG_MAGIC) 76 | { 77 | /* on a minor version change, just keep wifi settings and hostname */ 78 | if ((current_config.magic & ~0xF) == (CONFIG_MAGIC & ~0xF)) 79 | { 80 | char hostname[32]; 81 | char wifi_ssid[32]; 82 | char wifi_password[32]; 83 | 84 | strcpy(hostname, current_config.hostname); 85 | strcpy(wifi_ssid, current_config.wifi_ssid); 86 | strcpy(wifi_password, current_config.wifi_password); 87 | 88 | cfg_reset(); 89 | 90 | strcpy(current_config.hostname, hostname); 91 | strcpy(current_config.wifi_ssid, wifi_ssid); 92 | strcpy(current_config.wifi_password, wifi_password); 93 | config_valid = true; 94 | } 95 | else 96 | { 97 | cfg_reset(); 98 | } 99 | return; 100 | } 101 | config_valid = true; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /include/HA.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define MAX_LEN 32 4 | #define MAX_ENTITIES (3 * 9 + 16 * 7 + 32) 5 | 6 | typedef enum 7 | { 8 | ha_unused = 0, 9 | /* https://www.home-assistant.io/integrations/text.mqtt/ */ 10 | ha_text, 11 | /* https://www.home-assistant.io/integrations/sensor.mqtt/ */ 12 | ha_sensor, 13 | /* https://www.home-assistant.io/integrations/number.mqtt/ */ 14 | ha_number, 15 | /* https://www.home-assistant.io/integrations/button.mqtt/ */ 16 | ha_button, 17 | /* https://www.home-assistant.io/integrations/select.mqtt/ */ 18 | ha_select, 19 | /* https://www.home-assistant.io/integrations/binary_sensor.mqtt/ */ 20 | ha_binary_sensor, 21 | /* https://www.home-assistant.io/integrations/ha_light.mqtt/ */ 22 | ha_light 23 | } t_ha_device_type; 24 | 25 | typedef struct s_ha_entity t_ha_entity; 26 | 27 | struct s_ha_entity 28 | { 29 | t_ha_device_type type; 30 | 31 | const char *name; 32 | const char *id; 33 | 34 | /* used by: sensor */ 35 | const char *unit_of_meas; 36 | /* used by: sensor */ 37 | const char *val_tpl; 38 | /* used by: sensor */ 39 | const char *dev_class; 40 | /* used by: sensor */ 41 | const char *state_class; 42 | /* used by: button, number, text */ 43 | const char *cmd_t; 44 | /* used by: sensor, binary_sensor, number, text */ 45 | const char *stat_t; 46 | /* used by: light */ 47 | const char *rgb_t; 48 | const char *rgbw_t; 49 | /* used by: switch, comma separated */ 50 | const char *options; 51 | /* used by: number */ 52 | float min; 53 | /* used by: number */ 54 | float max; 55 | /* used by: number */ 56 | const char *mode; 57 | /* icon */ 58 | const char *ic; 59 | /* entity_category */ 60 | const char *ent_cat; 61 | 62 | /* used by: light */ 63 | const char *fx_cmd_t; 64 | /* used by: light */ 65 | const char *fx_stat_t; 66 | /* used by: light, comma separated */ 67 | const char *fx_list; 68 | 69 | /* alternative client name */ 70 | const char *alt_name; 71 | 72 | void (*received)(const t_ha_entity *, void *, const char *); 73 | void *received_ctx; 74 | void (*rgb_received)(const t_ha_entity *, void *, const char *); 75 | void *rgb_received_ctx; 76 | void (*fx_received)(const t_ha_entity *, void *, const char *); 77 | void *fx_received_ctx; 78 | void (*transmit)(const t_ha_entity *, void *); 79 | void *transmit_ctx; 80 | }; 81 | 82 | typedef struct 83 | { 84 | char name[MAX_LEN]; 85 | char id[MAX_LEN]; 86 | char cu[MAX_LEN]; 87 | char mf[MAX_LEN]; 88 | char mdl[MAX_LEN]; 89 | char sw[MAX_LEN]; 90 | t_ha_entity entities[MAX_ENTITIES]; 91 | int entitiy_count; 92 | } t_ha_info; 93 | 94 | void ha_setup(); 95 | void ha_connected(); 96 | bool ha_loop(); 97 | void ha_transmit_all(); 98 | void ha_publish(); 99 | void ha_add(t_ha_entity *entity); 100 | void ha_addmqtt(char *json_str, const char *name, const char *value, t_ha_entity *entity, bool last); 101 | void ha_received(char *topic, const char *payload); 102 | void ha_transmit(const t_ha_entity *entity, const char *value); 103 | void ha_transmit_topic(const char *stat_t, const char *value); 104 | int ha_parse_index(const char *options, const char *message); 105 | void ha_get_index(const char *options, int index, char *text); 106 | -------------------------------------------------------------------------------- /src/ESP32_VuePower.ino: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "Sensors.h" 10 | #include "HA.h" 11 | #include "Config.h" 12 | #include 13 | #include 14 | 15 | extern bool ota_active; 16 | bool safemode = false; 17 | 18 | const char *wakeup_reason() 19 | { 20 | esp_sleep_wakeup_cause_t wakeup_reason; 21 | 22 | wakeup_reason = esp_sleep_get_wakeup_cause(); 23 | 24 | switch (wakeup_reason) 25 | { 26 | case ESP_SLEEP_WAKEUP_EXT0: 27 | return "external signal using RTC_IO"; 28 | case ESP_SLEEP_WAKEUP_EXT1: 29 | return "external signal using RTC_CNTL"; 30 | case ESP_SLEEP_WAKEUP_TIMER: 31 | return "timer"; 32 | case ESP_SLEEP_WAKEUP_TOUCHPAD: 33 | return "touchpad"; 34 | case ESP_SLEEP_WAKEUP_ULP: 35 | return "ULP program"; 36 | case ESP_SLEEP_WAKEUP_UNDEFINED: 37 | return "undefined"; 38 | default: 39 | return "unknown reason"; 40 | } 41 | } 42 | 43 | void setup() 44 | { 45 | Serial.begin(115200); 46 | Serial.printf("\n\n\n"); 47 | 48 | Serial.printf("[i] Wakeup '%s'\n", wakeup_reason()); 49 | Serial.printf("[i] SDK: '%s'\n", ESP.getSdkVersion()); 50 | Serial.printf("[i] CPU Speed: %d MHz\n", ESP.getCpuFreqMHz()); 51 | Serial.printf("[i] Chip Id: %06X\n", ESP.getEfuseMac()); 52 | Serial.printf("[i] Flash Mode: %08X\n", ESP.getFlashChipMode()); 53 | Serial.printf("[i] Flash Size: %08X\n", ESP.getFlashChipSize()); 54 | Serial.printf("[i] Flash Speed: %d MHz\n", ESP.getFlashChipSpeed() / 1000000); 55 | Serial.printf("[i] Heap %d/%d\n", ESP.getFreeHeap(), ESP.getHeapSize()); 56 | Serial.printf("[i] SPIRam %d/%d\n", ESP.getFreePsram(), ESP.getPsramSize()); 57 | Serial.printf("\n"); 58 | Serial.printf("[i] Starting\n"); 59 | 60 | led_setup(); 61 | rtttl_setup(); 62 | 63 | Serial.printf("[i] Setup SPIFFS\n"); 64 | if (!SPIFFS.begin(true)) 65 | { 66 | Serial.println("[E] SPIFFS Mount Failed"); 67 | } 68 | 69 | cfg_read(); 70 | safemode_startup(); 71 | 72 | Serial.printf("[i] Setup WiFi\n"); 73 | wifi_setup(); 74 | Serial.printf("[i] Setup OTA\n"); 75 | ota_setup(); 76 | 77 | if (safemode) 78 | { 79 | Serial.printf("[E] ENTER SAFE MODE. No further initialization.\n"); 80 | ota_enable(); 81 | rtttl_play("Halloween:d=4, o=5, b=180:8d6, 8g, 8g, 8d6, 8g, 8g, 8d6, 8g, 8d#6, 8g, 8d6, 8g, 8g, 8d6, 8g, 8g, 8d6, 8g, 8d#6, 8g, 8c#6, 8f#, 8f#, 8c#6, 8f#, 8f#, 8c#6, 8f#, 8d6, 8f#, 8c#6, 8f#, 8f#, 8c#6, 8f#, 8f#, 8c#6, 8f#, 8d6, 8f#"); 82 | return; 83 | } 84 | 85 | Serial.printf("[i] Setup Time\n"); 86 | time_setup(); 87 | Serial.printf("[i] Setup Webserver\n"); 88 | www_setup(); 89 | Serial.printf("[i] Setup MQTT\n"); 90 | mqtt_setup(); 91 | Serial.printf("[i] Setup I/O\n"); 92 | io_setup(); 93 | Serial.printf("[i] Setup Sensors\n"); 94 | sensors_setup(); 95 | Serial.printf("[i] Setup Buzzer\n"); 96 | buzz_setup(); 97 | 98 | Serial.println("Setup done"); 99 | 100 | buzz_beep(3200, 150); 101 | buzz_beep(3700, 150); 102 | buzz_beep(4200, 500); 103 | } 104 | 105 | void safemode_startup() 106 | { 107 | current_config.boot_count++; 108 | cfg_save(); 109 | Serial.printf("[i] Boot count: %d\n", current_config.boot_count); 110 | 111 | if (current_config.boot_count > CONFIG_MAXFAILS) 112 | { 113 | safemode = true; 114 | } 115 | } 116 | 117 | bool safemode_loop() 118 | { 119 | if (current_config.boot_count) 120 | { 121 | if (millis() > 30000) 122 | { 123 | Serial.printf("[i] Successfully booted\n"); 124 | current_config.boot_count = 0; 125 | cfg_save(); 126 | } 127 | } 128 | return false; 129 | } 130 | 131 | void loop() 132 | { 133 | bool hasWork = false; 134 | 135 | if (!ota_active) 136 | { 137 | hasWork |= wifi_loop(); 138 | 139 | if (!safemode) 140 | { 141 | hasWork |= time_loop(); 142 | hasWork |= mqtt_loop(); 143 | hasWork |= www_loop(); 144 | hasWork |= sensors_loop(); 145 | } 146 | } 147 | hasWork |= rtttl_loop(); 148 | hasWork |= ota_loop(); 149 | safemode_loop(); 150 | 151 | if (!hasWork) 152 | { 153 | delay(5); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/WiFi.ino: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | 4 | DNSServer dnsServer; 5 | 6 | bool connecting = false; 7 | bool wifi_captive = false; 8 | char wifi_error[64]; 9 | int wifi_rssi = 0; 10 | 11 | void wifi_setup() 12 | { 13 | Serial.printf("[WiFi] Connecting to '%s', password '%s'...\n", current_config.wifi_ssid, current_config.wifi_password); 14 | sprintf(wifi_error, ""); 15 | WiFi.begin(current_config.wifi_ssid, current_config.wifi_password); 16 | connecting = true; 17 | led_set(1, 8, 8, 0); 18 | } 19 | 20 | void wifi_off() 21 | { 22 | connecting = false; 23 | WiFi.disconnect(); 24 | WiFi.mode(WIFI_OFF); 25 | } 26 | 27 | void wifi_enter_captive() 28 | { 29 | wifi_off(); 30 | WiFi.softAP(CONFIG_SOFTAPNAME); 31 | dnsServer.start(53, "*", WiFi.softAPIP()); 32 | Serial.printf("[WiFi] Local IP: %s\n", WiFi.softAPIP().toString().c_str()); 33 | 34 | wifi_captive = true; 35 | 36 | /* reset captive idle timer */ 37 | www_activity(); 38 | } 39 | 40 | bool wifi_loop(void) 41 | { 42 | int status = WiFi.status(); 43 | uint32_t curTime = millis(); 44 | static uint32_t nextTime = 0; 45 | static uint32_t stateCounter = 0; 46 | 47 | if (wifi_captive) 48 | { 49 | dnsServer.processNextRequest(); 50 | led_set(1, 0, ((millis() % 250) > 125) ? 0 : 255, 0); 51 | 52 | /* captive mode, but noone cares */ 53 | if (!www_is_captive_active()) 54 | { 55 | Serial.printf("[WiFi] Timeout in captive, trying known networks again\n"); 56 | sprintf(wifi_error, "Timeout in captive, trying known networks again"); 57 | dnsServer.stop(); 58 | wifi_off(); 59 | wifi_captive = false; 60 | stateCounter = 0; 61 | sprintf(wifi_error, ""); 62 | } 63 | return true; 64 | } 65 | 66 | if (nextTime > curTime) 67 | { 68 | return false; 69 | } 70 | 71 | /* standard refresh time */ 72 | nextTime = curTime + 500; 73 | 74 | /* when stuck at a state, disconnect */ 75 | if (++stateCounter > 20) 76 | { 77 | Serial.printf("[WiFi] Timeout connecting\n"); 78 | sprintf(wifi_error, "Timeout - incorrect password?"); 79 | wifi_off(); 80 | } 81 | 82 | if (strcmp(wifi_error, "")) 83 | { 84 | Serial.printf("[WiFi] Entering captive mode. Reason: '%s'\n", wifi_error); 85 | 86 | wifi_enter_captive(); 87 | 88 | stateCounter = 0; 89 | return false; 90 | } 91 | 92 | switch (status) 93 | { 94 | case WL_CONNECTED: 95 | if (connecting) 96 | { 97 | led_set(1, 0, 4, 0); 98 | connecting = false; 99 | Serial.print("[WiFi] Connected, IP address: "); 100 | Serial.println(WiFi.localIP()); 101 | stateCounter = 0; 102 | sprintf(wifi_error, ""); 103 | } 104 | else 105 | { 106 | static int last_rssi = -1; 107 | wifi_rssi = WiFi.RSSI(); 108 | 109 | if (last_rssi != wifi_rssi) 110 | { 111 | float maxRssi = -70; 112 | float minRssi = -90; 113 | float strRatio = (wifi_rssi - minRssi) / (maxRssi - minRssi); 114 | float strength = min(1, max(0, strRatio)); 115 | float brightness = 0.05f; 116 | int r = brightness * 255.0f * (1.0f - strength); 117 | int g = brightness * 255.0f * strength; 118 | 119 | led_set(1, r, g, 0); 120 | 121 | if (current_config.verbose & 1) 122 | { 123 | Serial.printf("[WiFi] RSSI %d, strength: %1.2f, r: %d, g: %d\n", wifi_rssi, strength, r, g); 124 | } 125 | 126 | last_rssi = wifi_rssi; 127 | } 128 | 129 | /* happy with this state, reset counter */ 130 | stateCounter = 0; 131 | } 132 | break; 133 | 134 | case WL_CONNECTION_LOST: 135 | Serial.printf("[WiFi] Connection lost\n"); 136 | sprintf(wifi_error, "Network found, but connection lost"); 137 | led_set(1, 32, 8, 0); 138 | wifi_off(); 139 | break; 140 | 141 | case WL_CONNECT_FAILED: 142 | Serial.printf("[WiFi] Connection failed\n"); 143 | sprintf(wifi_error, "Network found, but connection failed"); 144 | wifi_off(); 145 | break; 146 | 147 | case WL_NO_SSID_AVAIL: 148 | Serial.printf("[WiFi] No SSID with that name\n"); 149 | sprintf(wifi_error, "Network not found"); 150 | wifi_off(); 151 | break; 152 | 153 | case WL_SCAN_COMPLETED: 154 | Serial.printf("[WiFi] Scan completed\n"); 155 | wifi_off(); 156 | break; 157 | 158 | case WL_DISCONNECTED: 159 | if (!connecting) 160 | { 161 | Serial.printf("[WiFi] Disconnected\n"); 162 | led_set(1, 255, 0, 255); 163 | wifi_off(); 164 | } 165 | break; 166 | 167 | case WL_IDLE_STATUS: 168 | if (!connecting) 169 | { 170 | connecting = true; 171 | Serial.printf("[WiFi] Idle, connect to '%s'\n", current_config.wifi_ssid); 172 | WiFi.mode(WIFI_STA); 173 | WiFi.begin(current_config.wifi_ssid, current_config.wifi_password); 174 | } 175 | else 176 | { 177 | Serial.printf("[WiFi] Idle, connecting...\n"); 178 | } 179 | break; 180 | 181 | case WL_NO_SHIELD: 182 | if (!connecting) 183 | { 184 | connecting = true; 185 | Serial.printf("[WiFi] Disabled (%d), connecting to '%s'\n", status, current_config.wifi_ssid); 186 | WiFi.mode(WIFI_STA); 187 | WiFi.begin(current_config.wifi_ssid, current_config.wifi_password); 188 | } 189 | break; 190 | 191 | default: 192 | Serial.printf("[WiFi] unknown (%d), disable\n", status); 193 | wifi_off(); 194 | break; 195 | } 196 | 197 | return false; 198 | } 199 | -------------------------------------------------------------------------------- /src/NTPTime.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "NTPTime.h" 4 | 5 | enum statusType 6 | { 7 | Idle, 8 | Sent, 9 | Received, 10 | Pause 11 | }; 12 | 13 | IPAddress timeServerIP; // time.nist.gov NTP server address 14 | const char *ntpServerName = "time.nist.gov"; 15 | unsigned int localPort = 2390; // local port to listen for UDP packets 16 | const int NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message 17 | byte packetBuffer[NTP_PACKET_SIZE]; // buffer to hold incoming and outgoing packets 18 | WiFiUDP Udp; // A UDP instance to let us send and receive packets over UDP 19 | 20 | uint32_t retries = 0; 21 | unsigned long lastSent = 0; 22 | unsigned long timeReference = 0; 23 | bool time_valid = false; 24 | unsigned long secsSince1900 = 0; 25 | unsigned long setup_time_offset = 2; 26 | 27 | /* 2000-03-01 (mod 400 year, immediately after feb29 */ 28 | #define LEAPOCH (946684800LL + 86400 * (31 + 29)) 29 | #define DAYS_PER_400Y (365 * 400 + 97) 30 | #define DAYS_PER_100Y (365 * 100 + 24) 31 | #define DAYS_PER_4Y (365 * 4 + 1) 32 | 33 | statusType currentStatus = Idle; 34 | 35 | void time_setup() 36 | { 37 | Udp.begin(localPort); 38 | currentStatus = Idle; 39 | lastSent = 0; 40 | timeReference = 0; 41 | memset(packetBuffer, 0x00, sizeof(packetBuffer)); 42 | } 43 | 44 | const char *Time_getStateString() 45 | { 46 | static char retString[64]; 47 | const char *state = ""; 48 | 49 | switch (currentStatus) 50 | { 51 | default: 52 | state = "Unknown state"; 53 | break; 54 | 55 | case Idle: 56 | state = "Idle"; 57 | break; 58 | 59 | case Sent: 60 | state = "Sent"; 61 | break; 62 | 63 | case Received: 64 | state = "Received"; 65 | break; 66 | 67 | case Pause: 68 | state = "Pause"; 69 | break; 70 | } 71 | 72 | snprintf(retString, sizeof(retString), "%s, ref: %lu, last: %lu, retries: %u, millis(): %lu", state, timeReference, lastSent, retries, millis()); 73 | 74 | return retString; 75 | } 76 | 77 | bool time_loop() 78 | { 79 | if (WiFi.status() != WL_CONNECTED) 80 | { 81 | return false; 82 | } 83 | 84 | switch (currentStatus) 85 | { 86 | default: 87 | Serial.println("[NTP] Unknown state"); 88 | currentStatus = Idle; 89 | break; 90 | 91 | case Idle: 92 | if (!time_valid || millis() - lastSent > 1000 * 60 * 60) 93 | { 94 | Serial.println("[NTP] Sending request"); 95 | 96 | lastSent = millis(); 97 | currentStatus = Sent; 98 | WiFi.hostByName(ntpServerName, timeServerIP); 99 | sendNTPpacket(timeServerIP); // send an NTP packet to a time server 100 | } 101 | break; 102 | 103 | case Sent: 104 | if (millis() - lastSent > 1000 * 10) 105 | { 106 | Serial.println("[NTP] No reply, resend"); 107 | if (retries < 10) 108 | { 109 | retries++; 110 | currentStatus = Idle; 111 | } 112 | else 113 | { 114 | currentStatus = Pause; 115 | } 116 | } 117 | else if (Udp.parsePacket()) 118 | { 119 | timeReference = millis(); 120 | currentStatus = Received; 121 | Udp.read(packetBuffer, NTP_PACKET_SIZE); // read the packet into the buffer 122 | } 123 | break; 124 | 125 | case Received: 126 | { 127 | unsigned long highWord = word(packetBuffer[40], packetBuffer[41]); 128 | unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]); 129 | secsSince1900 = highWord << 16 | lowWord; 130 | 131 | printTime(); 132 | 133 | if (!time_valid) 134 | { 135 | time_valid = true; 136 | } 137 | 138 | retries = 0; 139 | currentStatus = Idle; 140 | break; 141 | } 142 | 143 | case Pause: 144 | if (millis() - lastSent > 1000 * 60 * 2) 145 | { 146 | currentStatus = Idle; 147 | } 148 | break; 149 | } 150 | 151 | return false; 152 | } 153 | 154 | void printTime() 155 | { 156 | struct tm tm; 157 | getTime(&tm); 158 | 159 | Serial.printf("[NTP] The time is: %02d:%02d:%02d\n", tm.tm_hour, tm.tm_min, tm.tm_sec); 160 | } 161 | 162 | int secs_to_tm(long long t, struct tm *tm) 163 | { 164 | long long days, secs; 165 | int remdays, remsecs, remyears; 166 | int qc_cycles, c_cycles, q_cycles; 167 | int years, months; 168 | int wday, yday, leap; 169 | static const char days_in_month[] = {31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 29}; 170 | 171 | secs = t - LEAPOCH; 172 | days = secs / 86400; 173 | remsecs = secs % 86400; 174 | if (remsecs < 0) 175 | { 176 | remsecs += 86400; 177 | days--; 178 | } 179 | 180 | wday = (3 + days) % 7; 181 | if (wday < 0) 182 | wday += 7; 183 | 184 | qc_cycles = days / DAYS_PER_400Y; 185 | remdays = days % DAYS_PER_400Y; 186 | if (remdays < 0) 187 | { 188 | remdays += DAYS_PER_400Y; 189 | qc_cycles--; 190 | } 191 | 192 | c_cycles = remdays / DAYS_PER_100Y; 193 | if (c_cycles == 4) 194 | c_cycles--; 195 | remdays -= c_cycles * DAYS_PER_100Y; 196 | 197 | q_cycles = remdays / DAYS_PER_4Y; 198 | if (q_cycles == 25) 199 | q_cycles--; 200 | remdays -= q_cycles * DAYS_PER_4Y; 201 | 202 | remyears = remdays / 365; 203 | if (remyears == 4) 204 | remyears--; 205 | remdays -= remyears * 365; 206 | 207 | leap = !remyears && (q_cycles || !c_cycles); 208 | yday = remdays + 31 + 28 + leap; 209 | if (yday >= 365 + leap) 210 | yday -= 365 + leap; 211 | 212 | years = remyears + 4 * q_cycles + 100 * c_cycles + 400 * qc_cycles; 213 | 214 | for (months = 0; days_in_month[months] <= remdays; months++) 215 | remdays -= days_in_month[months]; 216 | 217 | tm->tm_year = years + 100; 218 | tm->tm_mon = months + 2; 219 | if (tm->tm_mon >= 12) 220 | { 221 | tm->tm_mon -= 12; 222 | tm->tm_year++; 223 | } 224 | tm->tm_mday = remdays; 225 | tm->tm_wday = wday; 226 | tm->tm_yday = yday; 227 | 228 | tm->tm_hour = remsecs / 3600; 229 | tm->tm_min = remsecs / 60 % 60; 230 | tm->tm_sec = remsecs % 60; 231 | 232 | return 0; 233 | } 234 | 235 | void getTimeAdv(struct tm *tm, unsigned long offset) 236 | { 237 | unsigned long epoch = secsSince1900 - 2208988800UL; 238 | 239 | long secs = ((long)offset - (long)timeReference) / 1000; 240 | epoch += 60 * 60 * setup_time_offset; 241 | epoch += secs; 242 | 243 | secs_to_tm(epoch, tm); 244 | } 245 | 246 | void getTime(struct tm *tm) 247 | { 248 | getTimeAdv(tm, millis()); 249 | } 250 | 251 | void getStartupTime(struct tm *tm) 252 | { 253 | getTimeAdv(tm, 0); 254 | } 255 | 256 | // send an NTP request to the time server at the given address 257 | void sendNTPpacket(IPAddress &address) 258 | { 259 | memset(packetBuffer, 0, NTP_PACKET_SIZE); 260 | 261 | // Initialize values needed to form NTP request 262 | packetBuffer[0] = 0b11100011; // LI, Version, Mode 263 | packetBuffer[1] = 0; // Stratum, or type of clock 264 | packetBuffer[2] = 6; // Polling Interval 265 | packetBuffer[3] = 0xEC; // Peer Clock Precision 266 | // 8 bytes of zero for Root Delay & Root Dispersion 267 | packetBuffer[12] = 49; 268 | packetBuffer[13] = 0x4E; 269 | packetBuffer[14] = 49; 270 | packetBuffer[15] = 52; 271 | 272 | Udp.beginPacket(address, 123); // NTP requests are to port 123 273 | Udp.write(packetBuffer, NTP_PACKET_SIZE); 274 | Udp.endPacket(); 275 | } 276 | -------------------------------------------------------------------------------- /src/Sensors.ino: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "NTPTime.h" 8 | 9 | #include "esp32-hal-i2c.h" 10 | 11 | /* parts of it were taken from https://github.com/emporia-vue-local/esphome */ 12 | 13 | #define I2C_EMPORIA 0x64 14 | 15 | struct __attribute__((__packed__)) ReadingPowerEntry 16 | { 17 | int32_t phase[3]; 18 | }; 19 | 20 | struct __attribute__((__packed__)) SensorReading 21 | { 22 | bool is_unread; 23 | uint8_t checksum; 24 | uint8_t unknown; 25 | uint8_t sequence_num; 26 | 27 | ReadingPowerEntry power[19]; 28 | 29 | uint16_t voltage[3]; 30 | uint16_t frequency; 31 | uint16_t angle[2]; 32 | uint16_t current[19]; 33 | 34 | uint16_t end; 35 | }; 36 | 37 | sensor_data_t sensor_data; 38 | 39 | SensorReading sensor_reading; 40 | 41 | void sensors_setup() 42 | { 43 | i2cInit(0, DIO_SDA, DIO_SCL, 800000); 44 | sprintf(sensor_data.state, "(init)"); 45 | } 46 | 47 | void minute_stat_update(sensor_minute_stat_t *stat, float value) 48 | { 49 | char msg[128]; 50 | char buf[128]; 51 | struct tm tm; 52 | getTime(&tm); 53 | 54 | if (stat->tm_min_last != tm.tm_min) 55 | { 56 | float sum = 0; 57 | int entries = 0; 58 | 59 | for (int pos = 0; pos < 60; pos++) 60 | { 61 | if (stat->counter[pos]) 62 | { 63 | sum += stat->per_second[pos] / stat->counter[pos]; 64 | entries++; 65 | } 66 | stat->per_second[pos] = 0; 67 | stat->counter[pos] = 0; 68 | } 69 | 70 | if (!entries) 71 | { 72 | entries = 1; 73 | sum = 0; 74 | } 75 | stat->average = sum / entries; 76 | stat->tm_min_last = tm.tm_min; 77 | } 78 | 79 | stat->per_second[tm.tm_sec] += value; 80 | stat->counter[tm.tm_sec]++; 81 | } 82 | 83 | bool sensors_loop() 84 | { 85 | uint32_t time = millis(); 86 | static int last_tm_yday = -1; 87 | static int last_tm_min = -1; 88 | static uint32_t nextTime = 0; 89 | const uint32_t delta = 500; 90 | struct tm tm; 91 | 92 | if (time >= nextTime) 93 | { 94 | uint32_t delta_real = time - nextTime; 95 | float hour_fract = (delta_real / 1000.0f) / 3600.0f; 96 | 97 | nextTime = time + delta; 98 | 99 | size_t rxLength; 100 | led_set(1, 1, 0, 0); 101 | int err = i2cRead(0, I2C_EMPORIA, (uint8_t *)&sensor_reading, sizeof(sensor_reading), 50, &rxLength); 102 | led_set(1, 0, 0, 0); 103 | 104 | if (err || rxLength != sizeof(sensor_reading)) 105 | { 106 | sprintf(sensor_data.state, "I2C Read failure: %d, %d", err, rxLength); 107 | return false; 108 | } 109 | 110 | if (sensor_reading.end != 0) 111 | { 112 | sprintf(sensor_data.state, "I2C Read failure: incorrect magic"); 113 | return false; 114 | } 115 | 116 | if (!sensor_reading.is_unread) 117 | { 118 | sprintf(sensor_data.state, "Ignoring sensor reading that is marked as read"); 119 | return false; 120 | } 121 | 122 | if (sensor_reading.frequency == 0) 123 | { 124 | sprintf(sensor_data.state, "Ignoring sensor reading, frequency is zero"); 125 | return false; 126 | } 127 | sprintf(sensor_data.state, "OK"); 128 | 129 | sensor_data.frequency = 25310.0f / (float)sensor_reading.frequency * current_config.frequency_calib; 130 | 131 | for (int phase = 0; phase < 3; phase++) 132 | { 133 | sensor_phase_data_t *ph = &sensor_data.phases[phase]; 134 | 135 | ph->power = sensor_reading.power[phase].phase[phase] * current_config.sensor_calib_phase[phase] / 5.5f; 136 | ph->voltage = sensor_reading.voltage[phase] * fabsf(current_config.sensor_calib_phase_voltage[phase]); 137 | ph->current = sensor_reading.current[phase] * 775.0 / 42624.0; 138 | ph->angle = (phase > 0) ? sensor_reading.angle[phase - 1] * 360.0f / (float)sensor_reading.frequency : 0.0f; 139 | 140 | /* when no sensor connected, the current is beyond */ 141 | if (ph->current > 150) 142 | { 143 | ph->status = PHASE_STATUS_NOTCONNECTED; 144 | ph->power = 0; 145 | ph->voltage = 0; 146 | ph->current = 0; 147 | ph->angle = 0; 148 | continue; 149 | } 150 | ph->power_filtered = ((POWER_PT1 - 1) * ph->power_filtered + ph->power) / POWER_PT1; 151 | ph->status = PHASE_STATUS_OK; 152 | } 153 | 154 | for (int ch = 0; ch < 16; ch++) 155 | { 156 | sensor_ch_data_t *cur_ch = &sensor_data.channels[ch]; 157 | 158 | cur_ch->current = sensor_reading.current[3 + ch] * 775.0 / 170496.0; 159 | 160 | /* when no sensor connected, the current is beyond 93A */ 161 | if (cur_ch->current > 90) 162 | { 163 | cur_ch->status = CHANNEL_STATUS_NOTCONNECTED; 164 | cur_ch->power_real = 0; 165 | cur_ch->current = 0; 166 | continue; 167 | } 168 | cur_ch->status = CHANNEL_STATUS_OK; 169 | 170 | cur_ch->phase_match = 0; 171 | float phase_match_value = -1; 172 | 173 | for (int phase = 0; phase < 3; phase++) 174 | { 175 | cur_ch->power[phase] = sensor_reading.power[3 + ch].phase[phase] * current_config.sensor_calib_channel[ch] / 22.0f; 176 | 177 | /* ty to detect to which phase the current sensor is connected to */ 178 | cur_ch->power_calc[phase] = cur_ch->current * sensor_data.phases[phase].voltage; 179 | 180 | if (fabsf(cur_ch->power_calc[phase]) > 0) 181 | { 182 | const float pt1_value = 8; 183 | float match = cur_ch->power[phase] / cur_ch->power_calc[phase]; 184 | cur_ch->power_phase_match[phase] = ((pt1_value - 1) * cur_ch->power_phase_match[phase] + match) / pt1_value; 185 | } 186 | else 187 | { 188 | cur_ch->power_phase_match[phase] = 0; 189 | } 190 | 191 | if (cur_ch->power_phase_match[phase] > phase_match_value) 192 | { 193 | phase_match_value = cur_ch->power_phase_match[phase]; 194 | cur_ch->phase_match = phase; 195 | } 196 | } 197 | 198 | /* autodetect phase or use overridden? */ 199 | if (current_config.sensor_phase[ch] >= 0 && current_config.sensor_phase[ch] < 3) 200 | { 201 | cur_ch->phase_match = current_config.sensor_phase[ch]; 202 | } 203 | cur_ch->power_real = cur_ch->power[cur_ch->phase_match]; 204 | 205 | cur_ch->power_filtered = ((POWER_PT1 - 1) * cur_ch->power_filtered + cur_ch->power_real) / POWER_PT1; 206 | } 207 | 208 | /* statistics */ 209 | getTime(&tm); 210 | 211 | if (tm.tm_yday != last_tm_yday) 212 | { 213 | /* reset statistics */ 214 | for (int phase = 0; phase < 3; phase++) 215 | { 216 | sensor_data.phases[phase].power_daily = 0; 217 | } 218 | 219 | for (int ch = 0; ch < 16; ch++) 220 | { 221 | sensor_ch_data_t *cur_ch = &sensor_data.channels[ch]; 222 | 223 | cur_ch->power_daily = 0; 224 | } 225 | last_tm_yday = tm.tm_yday; 226 | } 227 | 228 | for (int phase = 0; phase < 3; phase++) 229 | { 230 | minute_stat_update(&sensor_data.phases[phase].minute_stats, sensor_data.phases[phase].power); 231 | } 232 | 233 | for (int ch = 0; ch < 16; ch++) 234 | { 235 | sensor_ch_data_t *cur_ch = &sensor_data.channels[ch]; 236 | minute_stat_update(&cur_ch->minute_stats, cur_ch->power_real); 237 | } 238 | 239 | /* summing up fractions of the power would end up in throwing away the value. 240 | adding 0.00xx to a number high as 100000 would have no effect. 241 | so average a minute and add every minute instead of multiple times a second. 242 | maybe later add multiple layers6 */ 243 | if (tm.tm_min != last_tm_min) 244 | { 245 | last_tm_min = tm.tm_min; 246 | for (int phase = 0; phase < 3; phase++) 247 | { 248 | float energy = sensor_data.phases[phase].minute_stats.average / 60.0f; 249 | 250 | sensor_data.phases[phase].power_total += energy; 251 | sensor_data.phases[phase].power_daily += energy; 252 | 253 | if (energy >= 0) 254 | { 255 | sensor_data.phases[phase].power_draw_total += energy; 256 | } 257 | else 258 | { 259 | sensor_data.phases[phase].power_inject_total += -energy; 260 | } 261 | } 262 | 263 | for (int ch = 0; ch < 16; ch++) 264 | { 265 | sensor_ch_data_t *cur_ch = &sensor_data.channels[ch]; 266 | float energy = cur_ch->minute_stats.average / 60.0f; 267 | cur_ch->power_total += energy / 60.0f; 268 | cur_ch->power_daily += energy / 60.0f; 269 | 270 | if (energy >= 0) 271 | { 272 | cur_ch->power_draw_total += energy; 273 | } 274 | else 275 | { 276 | cur_ch->power_inject_total += -energy; 277 | } 278 | } 279 | } 280 | } 281 | return true; 282 | } 283 | -------------------------------------------------------------------------------- /src/HA.ino: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | t_ha_info ha_info; 11 | extern PubSubClient mqtt; 12 | 13 | void ha_addstrarray(char *json_str, const char *name, const char *value, bool last = false) 14 | { 15 | char tmp_buf[128]; 16 | 17 | if (value && strlen(value) > 0) 18 | { 19 | int pos = 0; 20 | char values_buf[128]; 21 | int out_pos = 0; 22 | 23 | values_buf[out_pos++] = '"'; 24 | 25 | bool done = false; 26 | while (!done && out_pos < sizeof(values_buf)) 27 | { 28 | switch (value[pos]) 29 | { 30 | case ';': 31 | values_buf[out_pos++] = '"'; 32 | if (value[pos + 1]) 33 | { 34 | values_buf[out_pos++] = ','; 35 | values_buf[out_pos++] = '"'; 36 | } 37 | break; 38 | 39 | case 0: 40 | values_buf[out_pos++] = '"'; 41 | done = true; 42 | break; 43 | 44 | default: 45 | values_buf[out_pos++] = value[pos]; 46 | break; 47 | } 48 | pos++; 49 | } 50 | values_buf[out_pos++] = '\000'; 51 | 52 | snprintf(tmp_buf, sizeof(tmp_buf), "\"%s\": [%s]%c ", name, values_buf, (last ? ' ' : ',')); 53 | strcat(json_str, tmp_buf); 54 | } 55 | } 56 | 57 | void ha_addstr(char *json_str, const char *name, const char *value, bool last = false) 58 | { 59 | char tmp_buf[128]; 60 | 61 | if (value && strlen(value) > 0) 62 | { 63 | snprintf(tmp_buf, sizeof(tmp_buf), "\"%s\": \"%s\"%c ", name, value, (last ? ' ' : ',')); 64 | strcat(json_str, tmp_buf); 65 | } 66 | } 67 | 68 | void ha_addmqtt(char *json_str, const char *name, const char *value, t_ha_entity *entity, bool last = false) 69 | { 70 | char tmp_buf[128]; 71 | 72 | if (value && strlen(value) > 0) 73 | { 74 | char path_buffer[64]; 75 | 76 | if (entity && entity->alt_name) 77 | { 78 | sprintf(path_buffer, value, entity->alt_name); 79 | } 80 | else 81 | { 82 | sprintf(path_buffer, value, current_config.mqtt_client); 83 | } 84 | snprintf(tmp_buf, sizeof(tmp_buf), "\"%s\": \"%s\"%c ", name, path_buffer, (last ? ' ' : ',')); 85 | strcat(json_str, tmp_buf); 86 | } 87 | } 88 | 89 | void ha_addfloat(char *json_str, const char *name, float value, bool last = false) 90 | { 91 | char tmp_buf[64]; 92 | 93 | snprintf(tmp_buf, sizeof(tmp_buf), "\"%s\": \"%f\"%c ", name, value, (last ? ' ' : ',')); 94 | strcat(json_str, tmp_buf); 95 | } 96 | 97 | void ha_addint(char *json_str, const char *name, int value, bool last = false) 98 | { 99 | char tmp_buf[64]; 100 | 101 | snprintf(tmp_buf, sizeof(tmp_buf), "\"%s\": \"%d\"%c ", name, value, (last ? ' ' : ',')); 102 | strcat(json_str, tmp_buf); 103 | } 104 | 105 | void ha_publish() 106 | { 107 | char *json_str = (char *)malloc(1024); 108 | char mqtt_path[128]; 109 | char uniq_id[128]; 110 | 111 | Serial.printf("[HA] Publish\n"); 112 | 113 | sprintf(ha_info.cu, "http://%s/", WiFi.localIP().toString().c_str()); 114 | 115 | for (int pos = 0; pos < ha_info.entitiy_count; pos++) 116 | { 117 | const char *type = NULL; 118 | 119 | switch (ha_info.entities[pos].type) 120 | { 121 | case ha_sensor: 122 | // Serial.printf("[HA] sensor\n"); 123 | type = "sensor"; 124 | break; 125 | case ha_text: 126 | // Serial.printf("[HA] text\n"); 127 | type = "text"; 128 | break; 129 | case ha_number: 130 | // Serial.printf("[HA] number\n"); 131 | type = "number"; 132 | break; 133 | case ha_button: 134 | // Serial.printf("[HA] button\n"); 135 | type = "button"; 136 | break; 137 | case ha_binary_sensor: 138 | // Serial.printf("[HA] binary_sensor\n"); 139 | type = "binary_sensor"; 140 | break; 141 | case ha_select: 142 | // Serial.printf("[HA] select\n"); 143 | type = "select"; 144 | break; 145 | case ha_light: 146 | // Serial.printf("[HA] light\n"); 147 | type = "light"; 148 | break; 149 | default: 150 | // Serial.printf("[HA] last one\n"); 151 | break; 152 | } 153 | 154 | if (!type) 155 | { 156 | break; 157 | } 158 | 159 | sprintf(uniq_id, "%s_%s", ha_info.id, ha_info.entities[pos].id); 160 | 161 | // Serial.printf("[HA] uniq_id %s\n", uniq_id); 162 | sprintf(mqtt_path, "homeassistant/%s/%s/%s/config", type, ha_info.id, ha_info.entities[pos].id); 163 | 164 | // Serial.printf("[HA] mqtt_path %s\n", mqtt_path); 165 | 166 | strcpy(json_str, "{"); 167 | ha_addstr(json_str, "name", ha_info.entities[pos].name); 168 | ha_addstr(json_str, "uniq_id", uniq_id); 169 | ha_addstr(json_str, "dev_cla", ha_info.entities[pos].dev_class); 170 | ha_addstr(json_str, "stat_cla", ha_info.entities[pos].state_class); 171 | ha_addstr(json_str, "ic", ha_info.entities[pos].ic); 172 | ha_addstr(json_str, "mode", ha_info.entities[pos].mode); 173 | ha_addstr(json_str, "ent_cat", ha_info.entities[pos].ent_cat); 174 | ha_addmqtt(json_str, "cmd_t", ha_info.entities[pos].cmd_t, &ha_info.entities[pos]); 175 | ha_addmqtt(json_str, "stat_t", ha_info.entities[pos].stat_t, &ha_info.entities[pos]); 176 | ha_addmqtt(json_str, "rgbw_cmd_t", ha_info.entities[pos].rgbw_t, &ha_info.entities[pos]); 177 | ha_addmqtt(json_str, "rgb_cmd_t", ha_info.entities[pos].rgb_t, &ha_info.entities[pos]); 178 | ha_addmqtt(json_str, "fx_cmd_t", ha_info.entities[pos].fx_cmd_t, &ha_info.entities[pos]); 179 | ha_addmqtt(json_str, "fx_stat_t", ha_info.entities[pos].fx_stat_t, &ha_info.entities[pos]); 180 | ha_addstrarray(json_str, "fx_list", ha_info.entities[pos].fx_list); 181 | ha_addmqtt(json_str, "val_tpl", ha_info.entities[pos].val_tpl, &ha_info.entities[pos]); 182 | ha_addstrarray(json_str, "options", ha_info.entities[pos].options); 183 | ha_addstr(json_str, "unit_of_meas", ha_info.entities[pos].unit_of_meas); 184 | 185 | switch (ha_info.entities[pos].type) 186 | { 187 | case ha_number: 188 | ha_addint(json_str, "min", ha_info.entities[pos].min); 189 | ha_addint(json_str, "max", ha_info.entities[pos].max); 190 | break; 191 | default: 192 | break; 193 | } 194 | 195 | strcat(json_str, "\"dev\": {"); 196 | ha_addstr(json_str, "name", ha_info.name); 197 | ha_addstr(json_str, "ids", ha_info.id); 198 | ha_addstr(json_str, "cu", ha_info.cu); 199 | ha_addstr(json_str, "mf", ha_info.mf); 200 | ha_addstr(json_str, "mdl", ha_info.mdl); 201 | ha_addstr(json_str, "sw", ha_info.sw, true); 202 | strcat(json_str, "}}"); 203 | 204 | // Serial.printf("[HA] topic '%s'\n", mqtt_path); 205 | // Serial.printf("[HA] content '%s'\n", json_str); 206 | 207 | if (!mqtt.publish(mqtt_path, json_str)) 208 | { 209 | Serial.printf("[HA] publish failed\n"); 210 | } 211 | } 212 | 213 | Serial.printf("[HA] done\n"); 214 | free(json_str); 215 | } 216 | 217 | void ha_received(char *topic, const char *payload) 218 | { 219 | for (int pos = 0; pos < ha_info.entitiy_count; pos++) 220 | { 221 | char item_topic[128]; 222 | 223 | if (ha_info.entities[pos].cmd_t && ha_info.entities[pos].received) 224 | { 225 | sprintf(item_topic, ha_info.entities[pos].cmd_t, current_config.mqtt_client); 226 | if (!strcmp(topic, item_topic)) 227 | { 228 | ha_info.entities[pos].received(&ha_info.entities[pos], ha_info.entities[pos].received_ctx, payload); 229 | 230 | if (ha_info.entities[pos].transmit) 231 | { 232 | ha_info.entities[pos].transmit(&ha_info.entities[pos], ha_info.entities[pos].transmit_ctx); 233 | } 234 | } 235 | } 236 | 237 | if (ha_info.entities[pos].rgb_t && ha_info.entities[pos].rgb_received) 238 | { 239 | sprintf(item_topic, ha_info.entities[pos].rgb_t, current_config.mqtt_client); 240 | if (!strcmp(topic, item_topic)) 241 | { 242 | ha_info.entities[pos].rgb_received(&ha_info.entities[pos], ha_info.entities[pos].rgb_received_ctx, payload); 243 | 244 | if (ha_info.entities[pos].transmit) 245 | { 246 | ha_info.entities[pos].transmit(&ha_info.entities[pos], ha_info.entities[pos].transmit_ctx); 247 | } 248 | } 249 | } 250 | 251 | if (ha_info.entities[pos].fx_cmd_t && ha_info.entities[pos].fx_received) 252 | { 253 | sprintf(item_topic, ha_info.entities[pos].fx_cmd_t, current_config.mqtt_client); 254 | if (!strcmp(topic, item_topic)) 255 | { 256 | ha_info.entities[pos].fx_received(&ha_info.entities[pos], ha_info.entities[pos].fx_received_ctx, payload); 257 | 258 | if (ha_info.entities[pos].transmit) 259 | { 260 | ha_info.entities[pos].transmit(&ha_info.entities[pos], ha_info.entities[pos].transmit_ctx); 261 | } 262 | } 263 | } 264 | } 265 | } 266 | 267 | void ha_transmit(const t_ha_entity *entity, const char *value) 268 | { 269 | if (!entity) 270 | { 271 | return; 272 | } 273 | 274 | if (!entity->stat_t) 275 | { 276 | return; 277 | } 278 | char item_topic[128]; 279 | sprintf(item_topic, entity->stat_t, current_config.mqtt_client); 280 | 281 | if (!mqtt.publish(item_topic, value)) 282 | { 283 | Serial.printf("[HA] publish failed\n"); 284 | } 285 | } 286 | 287 | void ha_transmit_topic(const char *stat_t, const char *value) 288 | { 289 | if (!stat_t) 290 | { 291 | return; 292 | } 293 | 294 | char item_topic[128]; 295 | sprintf(item_topic, stat_t, current_config.mqtt_client); 296 | 297 | if (!mqtt.publish(item_topic, value)) 298 | { 299 | Serial.printf("[HA] publish failed\n"); 300 | } 301 | } 302 | 303 | void ha_transmit_all() 304 | { 305 | for (int pos = 0; pos < ha_info.entitiy_count; pos++) 306 | { 307 | if (ha_info.entities[pos].transmit) 308 | { 309 | ha_info.entities[pos].transmit(&ha_info.entities[pos], ha_info.entities[pos].transmit_ctx); 310 | } 311 | } 312 | } 313 | 314 | void ha_setup() 315 | { 316 | memset(&ha_info, 0x00, sizeof(ha_info)); 317 | 318 | sprintf(ha_info.name, "%s", current_config.mqtt_client); 319 | sprintf(ha_info.id, "%06llX", ESP.getEfuseMac()); 320 | sprintf(ha_info.cu, "http://%s/", WiFi.localIP().toString().c_str()); 321 | sprintf(ha_info.mf, "g3gg0.de"); 322 | sprintf(ha_info.mdl, ""); 323 | sprintf(ha_info.sw, "v1." xstr(PIO_SRC_REVNUM) " (" xstr(PIO_SRC_REV) ")"); 324 | ha_info.entitiy_count = 0; 325 | 326 | mqtt.setBufferSize(512); 327 | } 328 | 329 | void ha_connected() 330 | { 331 | for (int pos = 0; pos < ha_info.entitiy_count; pos++) 332 | { 333 | char item_topic[128]; 334 | if (ha_info.entities[pos].cmd_t && ha_info.entities[pos].received) 335 | { 336 | sprintf(item_topic, ha_info.entities[pos].cmd_t, current_config.mqtt_client); 337 | mqtt.subscribe(item_topic); 338 | } 339 | if (ha_info.entities[pos].rgb_t && ha_info.entities[pos].rgb_received) 340 | { 341 | sprintf(item_topic, ha_info.entities[pos].rgb_t, current_config.mqtt_client); 342 | mqtt.subscribe(item_topic); 343 | } 344 | if (ha_info.entities[pos].fx_cmd_t && ha_info.entities[pos].fx_received) 345 | { 346 | sprintf(item_topic, ha_info.entities[pos].fx_cmd_t, current_config.mqtt_client); 347 | mqtt.subscribe(item_topic); 348 | } 349 | } 350 | ha_publish(); 351 | ha_transmit_all(); 352 | } 353 | 354 | bool ha_loop() 355 | { 356 | uint32_t time = millis(); 357 | static uint32_t nextTime = 0; 358 | 359 | if (time >= nextTime) 360 | { 361 | ha_publish(); 362 | ha_transmit_all(); 363 | nextTime = time + 60000; 364 | } 365 | 366 | return false; 367 | } 368 | 369 | void ha_add(t_ha_entity *entity) 370 | { 371 | if (!entity) 372 | { 373 | return; 374 | } 375 | 376 | if (ha_info.entitiy_count >= MAX_ENTITIES) 377 | { 378 | return; 379 | } 380 | memcpy(&ha_info.entities[ha_info.entitiy_count++], entity, sizeof(t_ha_entity)); 381 | } 382 | 383 | int ha_parse_index(const char *options, const char *message) 384 | { 385 | if (!options) 386 | { 387 | return -1; 388 | } 389 | 390 | int pos = 0; 391 | char tmp_buf[128]; 392 | char *cur_elem = tmp_buf; 393 | 394 | strncpy(tmp_buf, options, sizeof(tmp_buf)); 395 | 396 | while (true) 397 | { 398 | char *next_elem = strchr(cur_elem, ';'); 399 | if (next_elem) 400 | { 401 | *next_elem = '\000'; 402 | } 403 | if (!strcmp(cur_elem, message)) 404 | { 405 | return pos; 406 | } 407 | 408 | if (!next_elem) 409 | { 410 | return -1; 411 | } 412 | 413 | cur_elem = next_elem + 1; 414 | pos++; 415 | } 416 | } 417 | 418 | void ha_get_index(const char *options, int index, char *text) 419 | { 420 | if (!options || !text) 421 | { 422 | return; 423 | } 424 | 425 | int pos = 0; 426 | char tmp_buf[128]; 427 | char *cur_elem = tmp_buf; 428 | 429 | strncpy(tmp_buf, options, sizeof(tmp_buf)); 430 | 431 | while (true) 432 | { 433 | char *next_elem = strchr(cur_elem, ';'); 434 | if (next_elem) 435 | { 436 | *next_elem = '\000'; 437 | } 438 | if (pos == index) 439 | { 440 | strcpy(text, cur_elem); 441 | return; 442 | } 443 | 444 | if (!next_elem) 445 | { 446 | return; 447 | } 448 | 449 | cur_elem = next_elem + 1; 450 | pos++; 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /src/MQTT.ino: -------------------------------------------------------------------------------- 1 | #define MQTT_DEBUG 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "HA.h" 9 | 10 | WiFiClient client; 11 | PubSubClient mqtt(client); 12 | 13 | extern int wifi_rssi; 14 | 15 | uint32_t mqtt_last_publish_time = 0; 16 | uint32_t mqtt_lastConnect = 0; 17 | uint32_t mqtt_retries = 0; 18 | bool mqtt_fail = false; 19 | 20 | char command_topic[64]; 21 | char response_topic[64]; 22 | 23 | void callback(char *topic, byte *payload, unsigned int length) 24 | { 25 | Serial.print("Message arrived ["); 26 | Serial.print(topic); 27 | Serial.print("] "); 28 | Serial.print("'"); 29 | for (int i = 0; i < length; i++) 30 | { 31 | Serial.print((char)payload[i]); 32 | } 33 | Serial.print("'"); 34 | Serial.println(); 35 | 36 | payload[length] = 0; 37 | 38 | if (current_config.mqtt_publish & CONFIG_PUBLISH_HA) 39 | { 40 | ha_received(topic, (const char *)payload); 41 | } 42 | 43 | if (!strcmp(topic, command_topic)) 44 | { 45 | char *command = (char *)payload; 46 | char buf[1024]; 47 | 48 | if (!strncmp(command, "http", 4)) 49 | { 50 | snprintf(buf, sizeof(buf) - 1, "updating from: '%s'", command); 51 | Serial.printf("%s\n", buf); 52 | 53 | mqtt.publish(response_topic, buf); 54 | ESPhttpUpdate.rebootOnUpdate(false); 55 | t_httpUpdate_return ret = ESPhttpUpdate.update(command); 56 | 57 | switch (ret) 58 | { 59 | case HTTP_UPDATE_FAILED: 60 | snprintf(buf, sizeof(buf) - 1, "HTTP_UPDATE_FAILED Error (%d): %s", ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str()); 61 | mqtt.publish(response_topic, buf); 62 | Serial.printf("%s\n", buf); 63 | break; 64 | 65 | case HTTP_UPDATE_NO_UPDATES: 66 | snprintf(buf, sizeof(buf) - 1, "HTTP_UPDATE_NO_UPDATES"); 67 | mqtt.publish(response_topic, buf); 68 | Serial.printf("%s\n", buf); 69 | break; 70 | 71 | case HTTP_UPDATE_OK: 72 | snprintf(buf, sizeof(buf) - 1, "HTTP_UPDATE_OK"); 73 | mqtt.publish(response_topic, buf); 74 | Serial.printf("%s\n", buf); 75 | delay(500); 76 | ESP.restart(); 77 | break; 78 | 79 | default: 80 | snprintf(buf, sizeof(buf) - 1, "update failed"); 81 | mqtt.publish(response_topic, buf); 82 | Serial.printf("%s\n", buf); 83 | break; 84 | } 85 | } 86 | else 87 | { 88 | snprintf(buf, sizeof(buf) - 1, "unknown command: '%s'", command); 89 | mqtt.publish(response_topic, buf); 90 | Serial.printf("%s\n", buf); 91 | } 92 | } 93 | } 94 | 95 | void mqtt_ota_received(const t_ha_entity *entity, void *ctx, const char *message) 96 | { 97 | ota_setup(); 98 | } 99 | 100 | void mqtt_setup() 101 | { 102 | mqtt.setCallback(callback); 103 | 104 | if (current_config.mqtt_publish & CONFIG_PUBLISH_HA) 105 | { 106 | ha_setup(); 107 | 108 | t_ha_entity entity; 109 | 110 | memset(&entity, 0x00, sizeof(entity)); 111 | entity.id = "ota"; 112 | entity.name = "Enable OTA"; 113 | entity.type = ha_button; 114 | entity.cmd_t = "command/%s/ota"; 115 | entity.received = &mqtt_ota_received; 116 | ha_add(&entity); 117 | 118 | memset(&entity, 0x00, sizeof(entity)); 119 | entity.id = "rssi"; 120 | entity.name = "WiFi RSSI"; 121 | entity.type = ha_sensor; 122 | entity.stat_t = "feeds/integer/%s/rssi"; 123 | entity.unit_of_meas = "dBm"; 124 | ha_add(&entity); 125 | 126 | memset(&entity, 0x00, sizeof(entity)); 127 | entity.id = "status"; 128 | entity.name = "Status message"; 129 | entity.type = ha_sensor; 130 | entity.stat_t = "live/%s/status"; 131 | ha_add(&entity); 132 | 133 | memset(&entity, 0x00, sizeof(entity)); 134 | entity.id = "frequency"; 135 | entity.name = "Mains Frequency"; 136 | entity.type = ha_sensor; 137 | entity.stat_t = "live/%s/frequency"; 138 | entity.unit_of_meas = "Hz"; 139 | entity.dev_class = "frequency"; 140 | entity.state_class = "measurement"; 141 | ha_add(&entity); 142 | 143 | for (int phase = 0; phase < 3; phase++) 144 | { 145 | char buf[32]; 146 | 147 | memset(&entity, 0x00, sizeof(entity)); 148 | sprintf(buf, "ph%d_angle", phase + 1); 149 | entity.id = strdup(buf); 150 | sprintf(buf, "Phase #%d Angle", phase + 1); 151 | entity.name = strdup(buf); 152 | entity.type = ha_sensor; 153 | sprintf(buf, "live/%%s/phase_%d/angle", phase + 1); 154 | entity.stat_t = strdup(buf); 155 | entity.unit_of_meas = "°"; 156 | entity.state_class = "measurement"; 157 | 158 | ha_add(&entity); 159 | 160 | memset(&entity, 0x00, sizeof(entity)); 161 | sprintf(buf, "ph%d_voltage", phase + 1); 162 | entity.id = strdup(buf); 163 | sprintf(buf, "Phase #%d Voltage", phase + 1); 164 | entity.name = strdup(buf); 165 | entity.type = ha_sensor; 166 | sprintf(buf, "live/%%s/phase_%d/voltage", phase + 1); 167 | entity.stat_t = strdup(buf); 168 | entity.unit_of_meas = "V"; 169 | entity.dev_class = "voltage"; 170 | entity.state_class = "measurement"; 171 | 172 | ha_add(&entity); 173 | 174 | memset(&entity, 0x00, sizeof(entity)); 175 | sprintf(buf, "ph%d_power", phase + 1); 176 | entity.id = strdup(buf); 177 | sprintf(buf, "Phase #%d Power", phase + 1); 178 | entity.name = strdup(buf); 179 | entity.type = ha_sensor; 180 | sprintf(buf, "live/%%s/phase_%d/power", phase + 1); 181 | entity.stat_t = strdup(buf); 182 | entity.unit_of_meas = "W"; 183 | entity.dev_class = "power"; 184 | entity.state_class = "measurement"; 185 | 186 | ha_add(&entity); 187 | 188 | memset(&entity, 0x00, sizeof(entity)); 189 | sprintf(buf, "ph%d_power_total", phase + 1); 190 | entity.id = strdup(buf); 191 | sprintf(buf, "Phase #%d Power Total", phase + 1); 192 | entity.name = strdup(buf); 193 | entity.type = ha_sensor; 194 | sprintf(buf, "live/%%s/phase_%d/power_total", phase + 1); 195 | entity.stat_t = strdup(buf); 196 | entity.unit_of_meas = "Wh"; 197 | entity.dev_class = "energy"; 198 | entity.state_class = "total"; 199 | 200 | ha_add(&entity); 201 | 202 | memset(&entity, 0x00, sizeof(entity)); 203 | sprintf(buf, "ph%d_power_draw_total", phase + 1); 204 | entity.id = strdup(buf); 205 | sprintf(buf, "Phase #%d Drawn", phase + 1); 206 | entity.name = strdup(buf); 207 | entity.type = ha_sensor; 208 | sprintf(buf, "live/%%s/phase_%d/power_draw_total", phase + 1); 209 | entity.stat_t = strdup(buf); 210 | entity.unit_of_meas = "Wh"; 211 | entity.dev_class = "energy"; 212 | entity.state_class = "total_increasing"; 213 | 214 | ha_add(&entity); 215 | 216 | memset(&entity, 0x00, sizeof(entity)); 217 | sprintf(buf, "ph%d_power_inject_total", phase + 1); 218 | entity.id = strdup(buf); 219 | sprintf(buf, "Phase #%d Injected", phase + 1); 220 | entity.name = strdup(buf); 221 | entity.type = ha_sensor; 222 | sprintf(buf, "live/%%s/phase_%d/power_inject_total", phase + 1); 223 | entity.stat_t = strdup(buf); 224 | entity.unit_of_meas = "Wh"; 225 | entity.dev_class = "energy"; 226 | entity.state_class = "total_increasing"; 227 | 228 | ha_add(&entity); 229 | 230 | memset(&entity, 0x00, sizeof(entity)); 231 | sprintf(buf, "ph%d_power_daily", phase + 1); 232 | entity.id = strdup(buf); 233 | sprintf(buf, "Phase #%d Power Daily", phase + 1); 234 | entity.name = strdup(buf); 235 | entity.type = ha_sensor; 236 | sprintf(buf, "live/%%s/phase_%d/power_daily", phase + 1); 237 | entity.stat_t = strdup(buf); 238 | entity.unit_of_meas = "Wh"; 239 | entity.dev_class = "energy"; 240 | entity.state_class = "total_increasing"; 241 | 242 | ha_add(&entity); 243 | 244 | memset(&entity, 0x00, sizeof(entity)); 245 | sprintf(buf, "power_daily", phase + 1); 246 | entity.id = strdup(buf); 247 | sprintf(buf, "Power Daily", phase + 1); 248 | entity.name = strdup(buf); 249 | entity.type = ha_sensor; 250 | sprintf(buf, "live/%%s/power_daily"); 251 | entity.stat_t = strdup(buf); 252 | entity.unit_of_meas = "Wh"; 253 | entity.dev_class = "energy"; 254 | entity.state_class = "total_increasing"; 255 | 256 | ha_add(&entity); 257 | 258 | memset(&entity, 0x00, sizeof(entity)); 259 | sprintf(buf, "ph%d_current", phase + 1); 260 | entity.id = strdup(buf); 261 | sprintf(buf, "Phase #%d Current", phase + 1); 262 | entity.name = strdup(buf); 263 | entity.type = ha_sensor; 264 | sprintf(buf, "live/%%s/phase_%d/current", phase + 1); 265 | entity.stat_t = strdup(buf); 266 | entity.unit_of_meas = "A"; 267 | entity.dev_class = "current"; 268 | entity.state_class = "measurement"; 269 | 270 | ha_add(&entity); 271 | 272 | memset(&entity, 0x00, sizeof(entity)); 273 | sprintf(buf, "ph%d_status", phase + 1); 274 | entity.id = strdup(buf); 275 | sprintf(buf, "Phase #%d Status", phase + 1); 276 | entity.name = strdup(buf); 277 | entity.type = ha_sensor; 278 | sprintf(buf, "live/%%s/phase_%d/status", phase + 1); 279 | entity.stat_t = strdup(buf); 280 | 281 | ha_add(&entity); 282 | } 283 | 284 | for (int channel = 0; channel < 16; channel++) 285 | { 286 | char buf[64]; 287 | 288 | memset(&entity, 0x00, sizeof(entity)); 289 | sprintf(buf, "ch%d_power", channel + 1); 290 | entity.id = strdup(buf); 291 | sprintf(buf, "%s Power", current_config.channel_name[channel]); 292 | entity.name = strdup(buf); 293 | entity.type = ha_sensor; 294 | sprintf(buf, "live/%%s/ch%d/power", channel + 1); 295 | entity.stat_t = strdup(buf); 296 | entity.unit_of_meas = "W"; 297 | entity.dev_class = "power"; 298 | entity.state_class = "measurement"; 299 | 300 | ha_add(&entity); 301 | 302 | memset(&entity, 0x00, sizeof(entity)); 303 | sprintf(buf, "ch%d_power_total", channel + 1); 304 | entity.id = strdup(buf); 305 | sprintf(buf, "%s Power Total", current_config.channel_name[channel]); 306 | entity.name = strdup(buf); 307 | entity.type = ha_sensor; 308 | sprintf(buf, "live/%%s/ch%d/power_total", channel + 1); 309 | entity.stat_t = strdup(buf); 310 | entity.unit_of_meas = "Wh"; 311 | entity.dev_class = "energy"; 312 | entity.state_class = "total"; 313 | 314 | ha_add(&entity); 315 | 316 | memset(&entity, 0x00, sizeof(entity)); 317 | sprintf(buf, "ch%d_power_draw_total", channel + 1); 318 | entity.id = strdup(buf); 319 | sprintf(buf, "%s Drawn", current_config.channel_name[channel]); 320 | entity.name = strdup(buf); 321 | entity.type = ha_sensor; 322 | sprintf(buf, "live/%%s/ch%d/power_draw_total", channel + 1); 323 | entity.stat_t = strdup(buf); 324 | entity.unit_of_meas = "Wh"; 325 | entity.dev_class = "energy"; 326 | entity.state_class = "total_increasing"; 327 | 328 | ha_add(&entity); 329 | 330 | memset(&entity, 0x00, sizeof(entity)); 331 | sprintf(buf, "ch%d_power_inject_total", channel + 1); 332 | entity.id = strdup(buf); 333 | sprintf(buf, "%s Injected", current_config.channel_name[channel]); 334 | entity.name = strdup(buf); 335 | entity.type = ha_sensor; 336 | sprintf(buf, "live/%%s/ch%d/power_inject_total", channel + 1); 337 | entity.stat_t = strdup(buf); 338 | entity.unit_of_meas = "Wh"; 339 | entity.dev_class = "energy"; 340 | entity.state_class = "total_increasing"; 341 | 342 | ha_add(&entity); 343 | 344 | memset(&entity, 0x00, sizeof(entity)); 345 | sprintf(buf, "ch%d_power_daily", channel + 1); 346 | entity.id = strdup(buf); 347 | sprintf(buf, "%s Power Daily", current_config.channel_name[channel]); 348 | entity.name = strdup(buf); 349 | entity.type = ha_sensor; 350 | sprintf(buf, "live/%%s/ch%d/power_daily", channel + 1); 351 | entity.stat_t = strdup(buf); 352 | entity.unit_of_meas = "Wh"; 353 | entity.dev_class = "energy"; 354 | entity.state_class = "total_increasing"; 355 | 356 | ha_add(&entity); 357 | 358 | memset(&entity, 0x00, sizeof(entity)); 359 | sprintf(buf, "ch%d_current", channel + 1); 360 | entity.id = strdup(buf); 361 | sprintf(buf, "%s Current", current_config.channel_name[channel]); 362 | entity.name = strdup(buf); 363 | entity.type = ha_sensor; 364 | sprintf(buf, "live/%%s/ch%d/current", channel + 1); 365 | entity.stat_t = strdup(buf); 366 | entity.unit_of_meas = "A"; 367 | entity.dev_class = "current"; 368 | entity.state_class = "measurement"; 369 | 370 | ha_add(&entity); 371 | 372 | memset(&entity, 0x00, sizeof(entity)); 373 | sprintf(buf, "ch%d_status", channel + 1); 374 | entity.id = strdup(buf); 375 | sprintf(buf, "%s Status", current_config.channel_name[channel]); 376 | entity.name = strdup(buf); 377 | entity.type = ha_sensor; 378 | sprintf(buf, "live/%%s/ch%d/status", channel + 1); 379 | entity.stat_t = strdup(buf); 380 | 381 | ha_add(&entity); 382 | } 383 | } 384 | } 385 | 386 | void mqtt_publish_string(const char *name, const char *value) 387 | { 388 | char path_buffer[128]; 389 | 390 | sprintf(path_buffer, name, current_config.mqtt_client); 391 | 392 | if (!mqtt.publish(path_buffer, value)) 393 | { 394 | mqtt_fail = true; 395 | } 396 | if (current_config.verbose & CONFIG_VERBOSE_SERIAL) 397 | { 398 | Serial.printf("Published %s : %s\n", path_buffer, value); 399 | } 400 | } 401 | 402 | void mqtt_publish_float(const char *name, float value) 403 | { 404 | char path_buffer[128]; 405 | char buffer[32]; 406 | 407 | sprintf(path_buffer, name, current_config.mqtt_client); 408 | sprintf(buffer, "%0.4f", value); 409 | 410 | if (!mqtt.publish(path_buffer, buffer)) 411 | { 412 | mqtt_fail = true; 413 | } 414 | if (current_config.verbose & CONFIG_VERBOSE_SERIAL) 415 | { 416 | Serial.printf("Published %s : %s\n", path_buffer, buffer); 417 | } 418 | } 419 | 420 | void mqtt_publish_int(const char *name, uint32_t value) 421 | { 422 | char path_buffer[128]; 423 | char buffer[32]; 424 | 425 | if (value == 0x7FFFFFFF) 426 | { 427 | return; 428 | } 429 | sprintf(path_buffer, name, current_config.mqtt_client); 430 | sprintf(buffer, "%d", value); 431 | 432 | if (!mqtt.publish(path_buffer, buffer)) 433 | { 434 | mqtt_fail = true; 435 | } 436 | if (current_config.verbose & CONFIG_VERBOSE_SERIAL) 437 | { 438 | Serial.printf("Published %s : %s\n", path_buffer, buffer); 439 | } 440 | } 441 | 442 | bool mqtt_loop() 443 | { 444 | uint32_t time = millis(); 445 | static uint32_t nextTime = 0; 446 | 447 | #ifdef TESTMODE 448 | return false; 449 | #endif 450 | if (mqtt_fail) 451 | { 452 | mqtt_fail = false; 453 | mqtt.disconnect(); 454 | } 455 | 456 | MQTT_connect(); 457 | 458 | if (!mqtt.connected()) 459 | { 460 | return false; 461 | } 462 | 463 | mqtt.loop(); 464 | 465 | if (current_config.mqtt_publish & CONFIG_PUBLISH_HA) 466 | { 467 | ha_loop(); 468 | } 469 | 470 | if (time >= nextTime) 471 | { 472 | bool do_publish = false; 473 | nextTime = time + 5000; 474 | 475 | if ((time - mqtt_last_publish_time) > 5000) 476 | { 477 | do_publish = true; 478 | } 479 | 480 | if (do_publish) 481 | { 482 | char buf[64]; 483 | Serial.printf("[MQTT] Publishing\n"); 484 | 485 | /* debug */ 486 | if (current_config.mqtt_publish & CONFIG_PUBLISH_DEBUG) 487 | { 488 | mqtt_publish_int("feeds/integer/%s/rssi", wifi_rssi); 489 | } 490 | 491 | /* publish */ 492 | if (current_config.mqtt_publish & CONFIG_PUBLISH_MQTT) 493 | { 494 | mqtt_publish_string("live/%s/status", sensor_data.state); 495 | mqtt_publish_float("live/%s/frequency", sensor_data.frequency); 496 | 497 | for (int phase = 0; phase < 3; phase++) 498 | { 499 | sensor_phase_data_t *ph = &sensor_data.phases[phase]; 500 | 501 | sprintf(buf, "live/%%s/phase_%d/status", phase + 1); 502 | switch (ph->status) 503 | { 504 | case PHASE_STATUS_NOTCONNECTED: 505 | mqtt_publish_string(buf, "Sensor not connected properly"); 506 | continue; 507 | 508 | case PHASE_STATUS_OK: 509 | mqtt_publish_string(buf, "OK"); 510 | break; 511 | } 512 | sprintf(buf, "live/%%s/phase_%d/voltage", phase + 1); 513 | mqtt_publish_float(buf, ph->voltage); 514 | sprintf(buf, "live/%%s/phase_%d/current", phase + 1); 515 | mqtt_publish_float(buf, ph->current); 516 | sprintf(buf, "live/%%s/phase_%d/angle", phase + 1); 517 | mqtt_publish_float(buf, ph->angle); 518 | sprintf(buf, "live/%%s/phase_%d/power", phase + 1); 519 | mqtt_publish_float(buf, ph->power_filtered); 520 | sprintf(buf, "live/%%s/phase_%d/power_total", phase + 1); 521 | mqtt_publish_float(buf, ph->power_total); 522 | sprintf(buf, "live/%%s/phase_%d/power_draw_total", phase + 1); 523 | mqtt_publish_float(buf, ph->power_draw_total); 524 | sprintf(buf, "live/%%s/phase_%d/power_inject_total", phase + 1); 525 | mqtt_publish_float(buf, ph->power_inject_total); 526 | sprintf(buf, "live/%%s/phase_%d/power_daily", phase + 1); 527 | mqtt_publish_float(buf, ph->power_daily); 528 | sprintf(buf, "live/%%s/phase_%d/status", phase + 1); 529 | } 530 | 531 | float power_daily = 0; 532 | for (int phase = 0; phase < 3; phase++) 533 | { 534 | sensor_phase_data_t *ph = &sensor_data.phases[phase]; 535 | power_daily += ph->power_daily; 536 | } 537 | sprintf(buf, "live/%%s/power_daily"); 538 | mqtt_publish_float(buf, power_daily); 539 | 540 | for (int ch = 0; ch < 16; ch++) 541 | { 542 | sensor_ch_data_t *cur_ch = &sensor_data.channels[ch]; 543 | 544 | sprintf(buf, "live/%%s/ch%d/status", ch + 1); 545 | switch (cur_ch->status) 546 | { 547 | case CHANNEL_STATUS_NOTCONNECTED: 548 | mqtt_publish_string(buf, "Sensor not connected properly"); 549 | continue; 550 | case CHANNEL_STATUS_OK: 551 | mqtt_publish_string(buf, "OK"); 552 | break; 553 | } 554 | 555 | for (int phase = 0; phase < 3; phase++) 556 | { 557 | sprintf(buf, "live/%%s/ch%d/rel_phase_%d/power", ch + 1, phase + 1); 558 | mqtt_publish_float(buf, cur_ch->power[phase]); 559 | sprintf(buf, "live/%%s/ch%d/rel_phase_%d/power_calc", ch + 1, phase + 1); 560 | mqtt_publish_float(buf, cur_ch->power_calc[phase]); 561 | sprintf(buf, "live/%%s/ch%d/rel_phase_%d/match", ch + 1, phase + 1); 562 | mqtt_publish_float(buf, cur_ch->power_phase_match[phase]); 563 | } 564 | sprintf(buf, "live/%%s/ch%d/current", ch + 1); 565 | mqtt_publish_float(buf, cur_ch->current); 566 | sprintf(buf, "live/%%s/ch%d/power", ch + 1); 567 | mqtt_publish_float(buf, cur_ch->power_filtered); 568 | sprintf(buf, "live/%%s/ch%d/power_phase", ch + 1); 569 | mqtt_publish_int(buf, cur_ch->phase_match); 570 | sprintf(buf, "live/%%s/ch%d/power_total", ch + 1); 571 | mqtt_publish_float(buf, cur_ch->power_total); 572 | sprintf(buf, "live/%%s/ch%d/power_draw_total", ch + 1); 573 | mqtt_publish_float(buf, cur_ch->power_draw_total); 574 | sprintf(buf, "live/%%s/ch%d/power_inject_total", ch + 1); 575 | mqtt_publish_float(buf, cur_ch->power_inject_total); 576 | sprintf(buf, "live/%%s/ch%d/power_daily", ch + 1); 577 | mqtt_publish_float(buf, cur_ch->power_daily); 578 | sprintf(buf, "live/%%s/ch%d/status", ch + 1); 579 | } 580 | } 581 | mqtt_last_publish_time = time; 582 | } 583 | } 584 | 585 | return false; 586 | } 587 | 588 | void MQTT_connect() 589 | { 590 | uint32_t curTime = millis(); 591 | int8_t ret; 592 | 593 | if (strlen(current_config.mqtt_server) == 0) 594 | { 595 | return; 596 | } 597 | 598 | mqtt.setServer(current_config.mqtt_server, current_config.mqtt_port); 599 | 600 | if (WiFi.status() != WL_CONNECTED) 601 | { 602 | return; 603 | } 604 | 605 | if (mqtt.connected()) 606 | { 607 | return; 608 | } 609 | 610 | if ((mqtt_lastConnect != 0) && (curTime - mqtt_lastConnect < (1000 << mqtt_retries))) 611 | { 612 | return; 613 | } 614 | 615 | mqtt_lastConnect = curTime; 616 | 617 | Serial.println("[MQTT] Connecting to MQTT... "); 618 | 619 | sprintf(command_topic, "tele/%s/command", current_config.mqtt_client); 620 | sprintf(response_topic, "tele/%s/response", current_config.mqtt_client); 621 | 622 | ret = mqtt.connect(current_config.mqtt_client, current_config.mqtt_user, current_config.mqtt_password); 623 | 624 | if (ret == 0) 625 | { 626 | mqtt_retries++; 627 | if (mqtt_retries > 8) 628 | { 629 | mqtt_retries = 8; 630 | } 631 | Serial.println("[MQTT] Retrying MQTT connection"); 632 | mqtt.disconnect(); 633 | } 634 | else 635 | { 636 | Serial.println("[MQTT] Connected!"); 637 | mqtt.subscribe(command_topic); 638 | if (current_config.mqtt_publish & CONFIG_PUBLISH_HA) 639 | { 640 | ha_connected(); 641 | } 642 | } 643 | } 644 | -------------------------------------------------------------------------------- /src/Webserver.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "Config.h" 4 | 5 | #define xstr(s) str(s) 6 | #define str(s) #s 7 | 8 | WebServer webserver(80); 9 | extern char wifi_error[]; 10 | extern bool wifi_captive; 11 | 12 | int www_wifi_scanned = -1; 13 | uint32_t www_last_captive = 0; 14 | 15 | #define min(a, b) ({ __typeof__ (a) _a = (a); __typeof__ (b) _b = (b); _a < _b ? _a : _b; }) 16 | #define max(a, b) ({ __typeof__ (a) _a = (a); __typeof__ (b) _b = (b); _a > _b ? _a : _b; }) 17 | 18 | void www_setup() 19 | { 20 | webserver.on("/", handle_root); 21 | webserver.on("/index.html", handle_index); 22 | webserver.on("/set_parm", handle_set_parm); 23 | webserver.on("/ota", handle_ota); 24 | webserver.on("/reset", handle_reset); 25 | webserver.onNotFound(handle_404); 26 | 27 | webserver.begin(); 28 | Serial.println("HTTP server started"); 29 | 30 | if (!MDNS.begin(current_config.hostname)) 31 | { 32 | Serial.println("Error setting up MDNS responder!"); 33 | while (1) 34 | { 35 | delay(1000); 36 | } 37 | } 38 | MDNS.addService("http", "tcp", 80); 39 | MDNS.addService("telnet", "tcp", 23); 40 | } 41 | 42 | unsigned char h2int(char c) 43 | { 44 | if (c >= '0' && c <= '9') 45 | { 46 | return ((unsigned char)c - '0'); 47 | } 48 | if (c >= 'a' && c <= 'f') 49 | { 50 | return ((unsigned char)c - 'a' + 10); 51 | } 52 | if (c >= 'A' && c <= 'F') 53 | { 54 | return ((unsigned char)c - 'A' + 10); 55 | } 56 | return (0); 57 | } 58 | 59 | String urldecode(String str) 60 | { 61 | String encodedString = ""; 62 | char c; 63 | char code0; 64 | char code1; 65 | for (int i = 0; i < str.length(); i++) 66 | { 67 | c = str.charAt(i); 68 | if (c == '+') 69 | { 70 | encodedString += ' '; 71 | } 72 | else if (c == '%') 73 | { 74 | i++; 75 | code0 = str.charAt(i); 76 | i++; 77 | code1 = str.charAt(i); 78 | c = (h2int(code0) << 4) | h2int(code1); 79 | encodedString += c; 80 | } 81 | else 82 | { 83 | encodedString += c; 84 | } 85 | 86 | yield(); 87 | } 88 | 89 | return encodedString; 90 | } 91 | 92 | void www_activity() 93 | { 94 | if (wifi_captive) 95 | { 96 | www_last_captive = millis(); 97 | } 98 | } 99 | 100 | int www_is_captive_active() 101 | { 102 | if (wifi_captive && millis() - www_last_captive < 30000) 103 | { 104 | return 1; 105 | } 106 | return 0; 107 | } 108 | 109 | void handle_404() 110 | { 111 | www_activity(); 112 | 113 | if (wifi_captive) 114 | { 115 | char buf[128]; 116 | sprintf(buf, "HTTP/1.1 302 Found\r\nContent-Type: text/html\r\nContent-length: 0\r\nLocation: http://%s/\r\n\r\n", WiFi.softAPIP().toString().c_str()); 117 | webserver.sendContent(buf); 118 | Serial.printf("[WWW] 302 - http://%s%s/ -> http://%s/\n", webserver.hostHeader().c_str(), webserver.uri().c_str(), WiFi.softAPIP().toString().c_str()); 119 | } 120 | else 121 | { 122 | webserver.send(404, "text/plain", "So empty here"); 123 | Serial.printf("[WWW] 404 - http://%s%s/\n", webserver.hostHeader().c_str(), webserver.uri().c_str()); 124 | } 125 | } 126 | 127 | void handle_index() 128 | { 129 | webserver.send(200, "text/html", SendHTML()); 130 | } 131 | 132 | bool www_loop() 133 | { 134 | webserver.handleClient(); 135 | return false; 136 | } 137 | 138 | void handle_root() 139 | { 140 | webserver.send(200, "text/html", SendHTML()); 141 | } 142 | 143 | void handle_ota() 144 | { 145 | ota_setup(); 146 | webserver.send(200, "text/html", SendHTML()); 147 | } 148 | 149 | void handle_reset() 150 | { 151 | webserver.send(200, "text/html", SendHTML()); 152 | ESP.restart(); 153 | } 154 | 155 | void handle_set_parm() 156 | { 157 | if (webserver.arg("http_download") != "" && webserver.arg("http_name") != "") 158 | { 159 | String url = webserver.arg("http_download"); 160 | String filename = webserver.arg("http_name"); 161 | HTTPClient http; 162 | 163 | http.begin(url); 164 | 165 | int httpCode = http.GET(); 166 | 167 | Serial.printf("[HTTP] GET... code: %d\n", httpCode); 168 | 169 | switch (httpCode) 170 | { 171 | case HTTP_CODE_OK: 172 | { 173 | int len = http.getSize(); 174 | const int blocksize = 1024; 175 | uint8_t *buffer = (uint8_t *)malloc(blocksize); 176 | 177 | if (!buffer) 178 | { 179 | Serial.printf("[HTTP] Failed to alloc %d byte\n", blocksize); 180 | return; 181 | } 182 | 183 | WiFiClient *stream = http.getStreamPtr(); 184 | File file = SPIFFS.open("/" + filename, "w"); 185 | 186 | if (!file) 187 | { 188 | Serial.printf("[HTTP] Failed to open file\n", blocksize); 189 | return; 190 | } 191 | 192 | int written = 0; 193 | 194 | while (http.connected() && (written < len)) 195 | { 196 | size_t size = stream->available(); 197 | 198 | if (size) 199 | { 200 | int c = stream->readBytes(buffer, ((size > blocksize) ? blocksize : size)); 201 | 202 | if (c > 0) 203 | { 204 | file.write(buffer, c); 205 | written += c; 206 | } 207 | else 208 | { 209 | break; 210 | } 211 | } 212 | } 213 | 214 | free(buffer); 215 | file.close(); 216 | 217 | Serial.printf("[HTTP] Finished. Wrote %d byte to %s\n", written, filename.c_str()); 218 | webserver.send(200, "text/plain", "Downloaded " + url + " and wrote " + written + " byte to " + filename); 219 | break; 220 | } 221 | 222 | default: 223 | { 224 | Serial.print("[HTTP] unexpected response\n"); 225 | webserver.send(200, "text/plain", "Unexpected HTTP status code " + httpCode); 226 | break; 227 | } 228 | } 229 | 230 | return; 231 | } 232 | 233 | if (webserver.arg("http_update") != "") 234 | { 235 | String url = webserver.arg("http_update"); 236 | 237 | Serial.printf("Update from %s\n", url.c_str()); 238 | 239 | ESPhttpUpdate.rebootOnUpdate(false); 240 | t_httpUpdate_return ret = ESPhttpUpdate.update(url); 241 | 242 | switch (ret) 243 | { 244 | case HTTP_UPDATE_FAILED: 245 | webserver.send(200, "text/plain", "HTTP_UPDATE_FAILED while updating from " + url + " " + ESPhttpUpdate.getLastErrorString()); 246 | Serial.printf("HTTP_UPDATE_FAILED Error (%d): %s\n", ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str()); 247 | break; 248 | 249 | case HTTP_UPDATE_NO_UPDATES: 250 | webserver.send(200, "text/plain", "HTTP_UPDATE_NO_UPDATES: Updating from " + url); 251 | Serial.println("Update failed: HTTP_UPDATE_NO_UPDATES"); 252 | break; 253 | 254 | case HTTP_UPDATE_OK: 255 | webserver.send(200, "text/html", "

Firmware updated. Rebooting...

(will refresh page in 5 seconds)"); 256 | webserver.close(); 257 | Serial.println("Update successful"); 258 | delay(500); 259 | ESP.restart(); 260 | return; 261 | } 262 | 263 | return; 264 | } 265 | 266 | current_config.verbose = 0; 267 | current_config.verbose |= (webserver.arg("verbose_c0") != "") ? 1 : 0; 268 | current_config.verbose |= (webserver.arg("verbose_c1") != "") ? 2 : 0; 269 | current_config.verbose |= (webserver.arg("verbose_c2") != "") ? 4 : 0; 270 | current_config.verbose |= (webserver.arg("verbose_c3") != "") ? 8 : 0; 271 | current_config.mqtt_publish = 0; 272 | current_config.mqtt_publish |= (webserver.arg("mqtt_publish_c0") != "") ? 1 : 0; 273 | current_config.mqtt_publish |= (webserver.arg("mqtt_publish_c1") != "") ? 2 : 0; 274 | current_config.mqtt_publish |= (webserver.arg("mqtt_publish_c2") != "") ? 4 : 0; 275 | current_config.mqtt_publish |= (webserver.arg("mqtt_publish_c3") != "") ? 8 : 0; 276 | 277 | strncpy(current_config.hostname, webserver.arg("hostname").c_str(), sizeof(current_config.hostname)); 278 | strncpy(current_config.wifi_ssid, webserver.arg("wifi_ssid").c_str(), sizeof(current_config.wifi_ssid)); 279 | strncpy(current_config.wifi_password, webserver.arg("wifi_password").c_str(), sizeof(current_config.wifi_password)); 280 | 281 | strncpy(current_config.mqtt_server, webserver.arg("mqtt_server").c_str(), sizeof(current_config.mqtt_server)); 282 | current_config.mqtt_port = max(1, min(65535, webserver.arg("mqtt_port").toInt())); 283 | strncpy(current_config.mqtt_user, webserver.arg("mqtt_user").c_str(), sizeof(current_config.mqtt_user)); 284 | strncpy(current_config.mqtt_password, webserver.arg("mqtt_password").c_str(), sizeof(current_config.mqtt_password)); 285 | strncpy(current_config.mqtt_client, webserver.arg("mqtt_client").c_str(), sizeof(current_config.mqtt_client)); 286 | 287 | current_config.frequency_calib = max(0, min(2, webserver.arg("cal_freq").toFloat())); 288 | 289 | for (int ch = 0; ch < 16; ch++) 290 | { 291 | char sym[32]; 292 | sprintf(sym, "cal_ch_%d", ch); 293 | current_config.sensor_calib_channel[ch] = max(-2, min(2, webserver.arg(sym).toFloat())); 294 | sprintf(sym, "sel_ch_%d", ch); 295 | current_config.sensor_phase[ch] = max(-1, min(2, webserver.arg(sym).toInt())); 296 | sprintf(sym, "name_ch_%d", ch); 297 | strncpy(current_config.channel_name[ch], webserver.arg(sym).c_str(), sizeof(current_config.channel_name[ch])); 298 | } 299 | for (int phase = 0; phase < 3; phase++) 300 | { 301 | char sym[32]; 302 | sprintf(sym, "cal_ph_%d", phase); 303 | current_config.sensor_calib_phase[phase] = max(-2, min(2, webserver.arg(sym).toFloat())); 304 | } 305 | for (int phase = 0; phase < 3; phase++) 306 | { 307 | char sym[32]; 308 | sprintf(sym, "cal_pv_%d", phase); 309 | current_config.sensor_calib_phase_voltage[phase] = max(-2, min(2, webserver.arg(sym).toFloat())); 310 | } 311 | 312 | cfg_save(); 313 | 314 | if (current_config.verbose) 315 | { 316 | Serial.printf("Config:\n"); 317 | Serial.printf(" mqtt_publish: %d %%\n", current_config.mqtt_publish); 318 | Serial.printf(" verbose: %d\n", current_config.verbose); 319 | } 320 | 321 | if (webserver.arg("reboot") == "true") 322 | { 323 | webserver.send(200, "text/html", "

Saved. Rebooting...

(will refresh page in 5 seconds)"); 324 | delay(500); 325 | ESP.restart(); 326 | return; 327 | } 328 | 329 | if (webserver.arg("scan") == "true") 330 | { 331 | www_wifi_scanned = WiFi.scanNetworks(); 332 | } 333 | webserver.send(200, "text/html", SendHTML()); 334 | www_wifi_scanned = -1; 335 | } 336 | 337 | void handle_NotFound() 338 | { 339 | webserver.send(404, "text/plain", "Not found"); 340 | } 341 | 342 | String SendHTML() 343 | { 344 | char buf[1024]; 345 | 346 | www_activity(); 347 | 348 | String ptr = " \n"; 349 | ptr += "\n"; 350 | 351 | sprintf(buf, "" CONFIG_OTANAME " Control\n"); 352 | 353 | ptr += buf; 354 | ptr += "\n"; 378 | /* https://github.com/mdbassit/Coloris */ 379 | ptr += "\n"; 380 | ptr += "\n"; 381 | ptr += "\n"; 382 | ptr += "\n"; 383 | 384 | sprintf(buf, "

" CONFIG_OTANAME "

\n"); 385 | ptr += buf; 386 | 387 | sprintf(buf, "

v1." xstr(PIO_SRC_REVNUM) " - " xstr(PIO_SRC_REV) "

\n"); 388 | ptr += buf; 389 | 390 | if (strlen(wifi_error) != 0) 391 | { 392 | sprintf(buf, "

WiFi Error: %s

\n", wifi_error); 393 | ptr += buf; 394 | } 395 | 396 | if (!ota_enabled()) 397 | { 398 | ptr += "[Enable OTA] "; 399 | } 400 | sprintf(buf, "
\n"); 401 | ptr += buf; 402 | ptr += "

\n"; 403 | 404 | ptr += "
\n"; 405 | ptr += ""; 406 | 407 | #define ADD_CONFIG(name, value, fmt, desc) \ 408 | do \ 409 | { \ 410 | ptr += ""; \ 411 | sprintf(buf, "\n", value); \ 412 | ptr += buf; \ 413 | } while (0) 414 | 415 | #define ADD_CONFIG_CHECK4(name, value, fmt, desc, text0, text1, text2, text3) \ 416 | do \ 417 | { \ 418 | ptr += "\n"); \ 436 | ptr += buf; \ 437 | } while (0) 438 | 439 | #define ADD_CONFIG_COLOR(name, value, fmt, desc) \ 440 | do \ 441 | { \ 442 | ptr += ""; \ 443 | sprintf(buf, "\n", value); \ 444 | ptr += buf; \ 445 | } while (0) 446 | 447 | ADD_CONFIG("hostname", current_config.hostname, "%s", "Hostname"); 448 | ADD_CONFIG("wifi_ssid", current_config.wifi_ssid, "%s", "WiFi SSID"); 449 | ADD_CONFIG("wifi_password", current_config.wifi_password, "%s", "WiFi Password"); 450 | 451 | ptr += ""; 482 | 483 | ADD_CONFIG("mqtt_server", current_config.mqtt_server, "%s", "MQTT Server"); 484 | ADD_CONFIG("mqtt_port", current_config.mqtt_port, "%d", "MQTT Port"); 485 | ADD_CONFIG("mqtt_user", current_config.mqtt_user, "%s", "MQTT Username"); 486 | ADD_CONFIG("mqtt_password", current_config.mqtt_password, "%s", "MQTT Password"); 487 | ADD_CONFIG("mqtt_client", current_config.mqtt_client, "%s", "MQTT Client Identification"); 488 | ADD_CONFIG_CHECK4("verbose", current_config.verbose, "%d", "Verbosity", "Serial", "-", "-", "-"); 489 | ADD_CONFIG_CHECK4("mqtt_publish", current_config.mqtt_publish, "%d", "MQTT publishes", "Live data", "Home Assistant", "Debug", "-"); 490 | ADD_CONFIG("http_update", "", "%s", "Update URL (Release)"); 491 | 492 | ADD_CONFIG("cal_freq", current_config.frequency_calib, "%f", "Calib value frequency"); 493 | 494 | ADD_CONFIG("cal_ph_0", current_config.sensor_calib_phase[0], "%f", "Calib value phase 1"); 495 | ADD_CONFIG("cal_ph_1", current_config.sensor_calib_phase[1], "%f", "Calib value phase 2"); 496 | ADD_CONFIG("cal_ph_2", current_config.sensor_calib_phase[2], "%f", "Calib value phase 3"); 497 | ADD_CONFIG("cal_pv_0", current_config.sensor_calib_phase_voltage[0], "%f", "Calib value phase voltage 1"); 498 | ADD_CONFIG("cal_pv_1", current_config.sensor_calib_phase_voltage[1], "%f", "Calib value phase voltage 2"); 499 | ADD_CONFIG("cal_pv_2", current_config.sensor_calib_phase_voltage[2], "%f", "Calib value phase voltage 3"); 500 | 501 | ADD_CONFIG("cal_ch_0", current_config.sensor_calib_channel[0], "%f", "Calib value channel 1"); 502 | ADD_CONFIG("cal_ch_1", current_config.sensor_calib_channel[1], "%f", "Calib value channel 2"); 503 | ADD_CONFIG("cal_ch_2", current_config.sensor_calib_channel[2], "%f", "Calib value channel 3"); 504 | ADD_CONFIG("cal_ch_3", current_config.sensor_calib_channel[3], "%f", "Calib value channel 4"); 505 | ADD_CONFIG("cal_ch_4", current_config.sensor_calib_channel[4], "%f", "Calib value channel 5"); 506 | ADD_CONFIG("cal_ch_5", current_config.sensor_calib_channel[5], "%f", "Calib value channel 6"); 507 | ADD_CONFIG("cal_ch_6", current_config.sensor_calib_channel[6], "%f", "Calib value channel 7"); 508 | ADD_CONFIG("cal_ch_7", current_config.sensor_calib_channel[7], "%f", "Calib value channel 8"); 509 | ADD_CONFIG("cal_ch_8", current_config.sensor_calib_channel[8], "%f", "Calib value channel 9"); 510 | ADD_CONFIG("cal_ch_9", current_config.sensor_calib_channel[9], "%f", "Calib value channel 10"); 511 | ADD_CONFIG("cal_ch_10", current_config.sensor_calib_channel[10], "%f", "Calib value channel 11"); 512 | ADD_CONFIG("cal_ch_11", current_config.sensor_calib_channel[11], "%f", "Calib value channel 12"); 513 | ADD_CONFIG("cal_ch_12", current_config.sensor_calib_channel[12], "%f", "Calib value channel 13"); 514 | ADD_CONFIG("cal_ch_13", current_config.sensor_calib_channel[13], "%f", "Calib value channel 14"); 515 | ADD_CONFIG("cal_ch_14", current_config.sensor_calib_channel[14], "%f", "Calib value channel 15"); 516 | ADD_CONFIG("cal_ch_15", current_config.sensor_calib_channel[15], "%f", "Calib value channel 16"); 517 | 518 | ADD_CONFIG("sel_ch_0", current_config.sensor_phase[0], "%d", "Reference phase channel 1"); 519 | ADD_CONFIG("sel_ch_1", current_config.sensor_phase[1], "%d", "Reference phase channel 2"); 520 | ADD_CONFIG("sel_ch_2", current_config.sensor_phase[2], "%d", "Reference phase channel 3"); 521 | ADD_CONFIG("sel_ch_3", current_config.sensor_phase[3], "%d", "Reference phase channel 4"); 522 | ADD_CONFIG("sel_ch_4", current_config.sensor_phase[4], "%d", "Reference phase channel 5"); 523 | ADD_CONFIG("sel_ch_5", current_config.sensor_phase[5], "%d", "Reference phase channel 6"); 524 | ADD_CONFIG("sel_ch_6", current_config.sensor_phase[6], "%d", "Reference phase channel 7"); 525 | ADD_CONFIG("sel_ch_7", current_config.sensor_phase[7], "%d", "Reference phase channel 8"); 526 | ADD_CONFIG("sel_ch_8", current_config.sensor_phase[8], "%d", "Reference phase channel 9"); 527 | ADD_CONFIG("sel_ch_9", current_config.sensor_phase[9], "%d", "Reference phase channel 10"); 528 | ADD_CONFIG("sel_ch_10", current_config.sensor_phase[10], "%d", "Reference phase channel 11"); 529 | ADD_CONFIG("sel_ch_11", current_config.sensor_phase[11], "%d", "Reference phase channel 12"); 530 | ADD_CONFIG("sel_ch_12", current_config.sensor_phase[12], "%d", "Reference phase channel 13"); 531 | ADD_CONFIG("sel_ch_13", current_config.sensor_phase[13], "%d", "Reference phase channel 14"); 532 | ADD_CONFIG("sel_ch_14", current_config.sensor_phase[14], "%d", "Reference phase channel 15"); 533 | ADD_CONFIG("sel_ch_15", current_config.sensor_phase[15], "%d", "Reference phase channel 16"); 534 | 535 | ADD_CONFIG("name_ch_0", current_config.channel_name[0], "%s", "Name of channel 1"); 536 | ADD_CONFIG("name_ch_1", current_config.channel_name[1], "%s", "Name of channel 2"); 537 | ADD_CONFIG("name_ch_2", current_config.channel_name[2], "%s", "Name of channel 3"); 538 | ADD_CONFIG("name_ch_3", current_config.channel_name[3], "%s", "Name of channel 4"); 539 | ADD_CONFIG("name_ch_4", current_config.channel_name[4], "%s", "Name of channel 5"); 540 | ADD_CONFIG("name_ch_5", current_config.channel_name[5], "%s", "Name of channel 6"); 541 | ADD_CONFIG("name_ch_6", current_config.channel_name[6], "%s", "Name of channel 7"); 542 | ADD_CONFIG("name_ch_7", current_config.channel_name[7], "%s", "Name of channel 8"); 543 | ADD_CONFIG("name_ch_8", current_config.channel_name[8], "%s", "Name of channel 9"); 544 | ADD_CONFIG("name_ch_9", current_config.channel_name[9], "%s", "Name of channel 10"); 545 | ADD_CONFIG("name_ch_10", current_config.channel_name[10], "%s", "Name of channel 11"); 546 | ADD_CONFIG("name_ch_11", current_config.channel_name[11], "%s", "Name of channel 12"); 547 | ADD_CONFIG("name_ch_12", current_config.channel_name[12], "%s", "Name of channel 13"); 548 | ADD_CONFIG("name_ch_13", current_config.channel_name[13], "%s", "Name of channel 14"); 549 | ADD_CONFIG("name_ch_14", current_config.channel_name[14], "%s", "Name of channel 15"); 550 | ADD_CONFIG("name_ch_15", current_config.channel_name[15], "%s", "Name of channel 16"); 551 | 552 | ptr += "
" desc ":
"; \ 419 | sprintf(buf, "\n", (value & 1) ? "checked" : ""); \ 420 | ptr += buf; \ 421 | sprintf(buf, "\n"); \ 422 | ptr += buf; \ 423 | sprintf(buf, "\n", (value & 2) ? "checked" : ""); \ 424 | ptr += buf; \ 425 | sprintf(buf, "\n"); \ 426 | ptr += buf; \ 427 | sprintf(buf, "\n", (value & 4) ? "checked" : ""); \ 428 | ptr += buf; \ 429 | sprintf(buf, "\n"); \ 430 | ptr += buf; \ 431 | sprintf(buf, "\n", (value & 8) ? "checked" : ""); \ 432 | ptr += buf; \ 433 | sprintf(buf, "\n"); \ 434 | ptr += buf; \ 435 | sprintf(buf, "
WiFi networks:"; 452 | 453 | if (www_wifi_scanned == -1) 454 | { 455 | ptr += ""; 456 | } 457 | else if (www_wifi_scanned == 0) 458 | { 459 | ptr += "No networks found, "; 460 | } 461 | else 462 | { 463 | ptr += ""; 464 | ptr += ""; 465 | for (int i = 0; i < www_wifi_scanned; ++i) 466 | { 467 | if (WiFi.SSID(i) != "") 468 | { 469 | ptr += ""; 476 | } 477 | } 478 | ptr += "
"; 472 | ptr += WiFi.SSID(i); 473 | ptr += ""; 474 | ptr += WiFi.RSSI(i); 475 | ptr += " dBm
"; 479 | } 480 | 481 | ptr += "
\n"; 553 | 554 | ptr += "\n"; 555 | ptr += "\n"; 556 | return ptr; 557 | } 558 | --------------------------------------------------------------------------------