├── .gitignore ├── include ├── OTA.h ├── WiFi_Handler.h ├── LED.h ├── NTP.h ├── MQTT.h ├── Config.h ├── Receiver.h ├── Macros.h ├── WWW.h ├── APS_ECU.h └── HA.h ├── src ├── LED.cpp ├── OTA.cpp ├── Config.cpp ├── Sensor433Gateway.ino ├── Receiver.cpp ├── WiFi_Handler.cpp ├── NTP.cpp ├── MQTT.cpp ├── HA.cpp ├── APS_ECU.cpp └── WWW.cpp └── platformio.ini /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | -------------------------------------------------------------------------------- /include/OTA.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define OTA_LED 0 4 | 5 | void ota_setup(); 6 | void ota_enable(); 7 | bool ota_enabled(); 8 | bool ota_loop(); 9 | -------------------------------------------------------------------------------- /include/WiFi_Handler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #define WIFI_LED 1 6 | 7 | void wifi_setup(); 8 | void wifi_off(); 9 | void wifi_enter_captive(); 10 | bool wifi_loop(); 11 | -------------------------------------------------------------------------------- /include/LED.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | void led_setup(); 4 | void led_set_adv(uint8_t n, uint8_t r, uint8_t g, uint8_t b, bool commit); 5 | void led_set(uint8_t n, uint8_t r, uint8_t g, uint8_t b); 6 | void led_set_all(uint8_t r, uint8_t g, uint8_t b); 7 | void led_set_inhibit(bool state); 8 | bool led_loop(); 9 | -------------------------------------------------------------------------------- /include/NTP.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | enum statusType 7 | { 8 | Idle, 9 | Sent, 10 | Received, 11 | Pause 12 | }; 13 | 14 | void ntp_setup(); 15 | bool ntp_loop(); 16 | 17 | const char *Time_getStateString(); 18 | void getTimeAdv(struct tm *tm, unsigned long offset); 19 | void getTime(struct tm *tm); 20 | void getStartupTime(struct tm *tm); 21 | void sendNTPpacket(IPAddress &address); 22 | -------------------------------------------------------------------------------- /include/MQTT.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | void MQTT_connect(); 4 | void mqtt_setup(); 5 | bool mqtt_loop(); 6 | 7 | void mqtt_publish_string(const char *name, const char *value); 8 | void mqtt_publish_string_plain(const char *path_buffer, const char *value); 9 | void mqtt_publish_float(const char *name, float value); 10 | void mqtt_publish_float_plain(const char *path_buffer, float value); 11 | void mqtt_publish_int(const char *name, uint32_t value); 12 | void mqtt_publish_int_plain(const char *path_buffer, uint32_t value); 13 | -------------------------------------------------------------------------------- /src/LED.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | 5 | #define LED_GPIO ONBOARD_LED 6 | 7 | bool led_inhibit = false; 8 | 9 | void led_setup() 10 | { 11 | } 12 | 13 | void led_set_adv(uint8_t n, uint8_t r, uint8_t g, uint8_t b, bool commit) 14 | { 15 | } 16 | 17 | void led_set(uint8_t n, uint8_t r, uint8_t g, uint8_t b) 18 | { 19 | digitalWrite(LED_GPIO, ((r > 0) || (g > 0) || (b > 0)) ? HIGH : LOW); 20 | } 21 | 22 | void led_set_all(uint8_t r, uint8_t g, uint8_t b) 23 | { 24 | } 25 | 26 | void led_set_inhibit(bool state) 27 | { 28 | led_inhibit = state; 29 | } 30 | 31 | bool led_loop() 32 | { 33 | return false; 34 | } 35 | -------------------------------------------------------------------------------- /include/Config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define CONFIG_SOFTAPNAME "esp32-config" 4 | #define CONFIG_OTANAME "Sensor433Gateway" 5 | 6 | #define CONFIG_MAGIC 0xE1AAFF02 7 | typedef struct 8 | { 9 | uint32_t magic; 10 | uint32_t verbose; 11 | uint32_t mqtt_publish; 12 | char hostname[32]; 13 | char wifi_ssid[32]; 14 | char wifi_password[32]; 15 | char mqtt_server[32]; 16 | int mqtt_port; 17 | char mqtt_user[32]; 18 | char mqtt_password[32]; 19 | char mqtt_client[32]; 20 | char mqtt_filter[256]; 21 | char aps_hostname[64]; 22 | char aps_mqttpath[64]; 23 | } t_cfg; 24 | 25 | extern t_cfg current_config; 26 | 27 | void cfg_read(); 28 | void cfg_save(); 29 | -------------------------------------------------------------------------------- /include/Receiver.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | typedef struct map_entry 11 | { 12 | char *first; 13 | char *second; 14 | } t_map_entry; 15 | 16 | void rcv_addreceived(const char *path, const char *json); 17 | void logJson(JsonObject &jsondata); 18 | void publish_string(const char *path, const char *elem, const char *value); 19 | void publish_float(const char *path, const char *elem, float value); 20 | void publish_int(const char *path, const char *elem, int value); 21 | bool rcv_enabled(const char *path); 22 | void rtl_433_Callback(char *msg); 23 | void rcv_setup(); 24 | bool rcv_loop(); 25 | -------------------------------------------------------------------------------- /include/Macros.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // #define min(a, b) ({ __typeof__ (a) _a = (a); __typeof__ (b) _b = (b); _a < _b ? _a : _b; }) 4 | // #define max(a, b) ({ __typeof__ (a) _a = (a); __typeof__ (b) _b = (b); _a > _b ? _a : _b; }) 5 | #define coerce(val, min, max) \ 6 | do \ 7 | { \ 8 | if ((val) > (max)) \ 9 | { \ 10 | val = max; \ 11 | } \ 12 | else if ((val) < (min)) \ 13 | { \ 14 | val = min; \ 15 | } \ 16 | } while (0) 17 | #define xstr(s) str(s) 18 | #define str(s) #s 19 | -------------------------------------------------------------------------------- /include/WWW.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #define xstr(s) str(s) 11 | #define str(s) #s 12 | 13 | #define min(a, b) ({ __typeof__ (a) _a = (a); __typeof__ (b) _b = (b); _a < _b ? _a : _b; }) 14 | #define max(a, b) ({ __typeof__ (a) _a = (a); __typeof__ (b) _b = (b); _a > _b ? _a : _b; }) 15 | 16 | void www_setup(); 17 | bool www_loop(); 18 | void www_activity(); 19 | int www_is_captive_active(); 20 | void www_handle_404(); 21 | void www_handle_index(); 22 | void www_handle_root(); 23 | void www_handle_ota(); 24 | void www_handle_reset(); 25 | void www_handle_set_parm(); 26 | String www_send_html(); 27 | -------------------------------------------------------------------------------- /src/OTA.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | bool ota_active = false; 9 | bool ota_setup_done = false; 10 | uint32_t ota_offtime = 0; 11 | 12 | void ota_setup() 13 | { 14 | if (ota_setup_done) 15 | { 16 | ota_enable(); 17 | return; 18 | } 19 | Serial.printf("[OTA] setHostname\n"); 20 | ArduinoOTA.setHostname(CONFIG_OTANAME); 21 | 22 | Serial.printf("[OTA] onStart\n"); 23 | ArduinoOTA.onStart([]() 24 | { 25 | Serial.printf("[OTA] starting\n"); 26 | led_set(OTA_LED, 255, 0, 255); 27 | ota_active = true; 28 | ota_offtime = millis() + 600000; }) 29 | .onEnd([]() 30 | { ota_active = false; }) 31 | .onProgress([](unsigned int progress, unsigned int total) 32 | { led_set(OTA_LED, 255 - (progress / (total / 255)), 0, (progress / (total / 255))); }) 33 | .onError([](ota_error_t error) 34 | { 35 | Serial.printf("Error[%u]: ", error); 36 | if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); 37 | else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); 38 | else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); 39 | else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); 40 | else if (error == OTA_END_ERROR) Serial.println("End Failed"); }); 41 | 42 | Serial.printf("[OTA] begin\n"); 43 | ArduinoOTA.begin(); 44 | 45 | Serial.printf("[OTA] Setup finished\n"); 46 | 47 | ota_setup_done = true; 48 | ota_enable(); 49 | } 50 | 51 | void ota_enable() 52 | { 53 | Serial.printf("[OTA] Enabled\n"); 54 | ota_offtime = millis() + 600000; 55 | } 56 | 57 | bool ota_enabled() 58 | { 59 | return (ota_offtime > millis() || ota_active); 60 | } 61 | 62 | bool ota_loop() 63 | { 64 | if (ota_enabled()) 65 | { 66 | ArduinoOTA.handle(); 67 | } 68 | 69 | return ota_active; 70 | } 71 | -------------------------------------------------------------------------------- /include/APS_ECU.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* 4 | from https://github.com/HectorMalot/ecur/blob/f094b3ba8f7cbf15c183e7061e49ee96afbc9994/ecur.go 5 | 6 | NewECUInfo parses the ECUInfo call into a struct 7 | Decoded explanation 8 | --------------------- 9 | 0- 2 : APS = Mark start of datastream 10 | 3- 4 : 11 = Answer notation 11 | 4- 8 : 0094 = Datalength 12 | 9-12 : 0001 = commandnumber 13 | 13-24 : 216000026497 = ECU_R nummer 14 | 25-26 : 01 = number of inverters online 15 | 27-30 : 42896 = Lifetime energy (kWh)/10 16 | 31-34 : 00 00 00 00 = LastSystemPower kW/100 17 | 35-38 : 202 = CurrentDayEnergy (/100) 18 | 39-45 : 7xd0 = LastTimeConnectEMA 19 | 46-47 : 8 = number of inverters registered 20 | 48-49 : 0 = number of inverters online 21 | 50-51 : 10 = EcuChannel 22 | 52-54 : 014 = VersionLEN => VL 23 | 55-55+VL : ECU_R_1.2.13 = Version 24 | 56+VL-57+VL : 009 = TimeZoneLen => TL 25 | 58+VL-57+VL+TL : Etc/GMT-8 = Timezone server always indicated at -8 hours 26 | 58+VL+TL-63+VL+TL : 80 97 1b 01 5d 1e = EthernetMAC 27 | 64+VL+TL-69+VL+TL : 00 00 00 00 00 00 = WirelessMAC //Shoud be but there is a bugin firmware 28 | 70+VL+TL-73+VL+TL : END\n = SignatureStop Marks end of datastream 29 | */ 30 | typedef struct __attribute__((packed)) 31 | { 32 | uint8_t ecuid[12]; 33 | uint16_t inverters_online; 34 | uint32_t energy_lifetime; 35 | uint32_t power_current; 36 | uint32_t energy_day; 37 | uint8_t last_time_connected[7]; 38 | uint16_t inverters_registered; 39 | uint16_t inverters_online_; 40 | uint8_t ecu_channel[2]; 41 | } t_ecuinfo; 42 | 43 | typedef struct __attribute__((packed)) 44 | { 45 | uint8_t timestamp[7]; 46 | uint8_t uid[6]; 47 | uint8_t unknown[2]; 48 | uint8_t model; 49 | uint16_t frequency; 50 | uint16_t temperature; 51 | uint16_t power_a; 52 | uint16_t voltage_a; 53 | uint16_t power_b; 54 | uint16_t voltage_b; 55 | } t_ecuinfoinverter; 56 | 57 | typedef struct __attribute__((packed)) 58 | { 59 | uint8_t unk[4]; 60 | uint16_t packets; 61 | t_ecuinfoinverter inverter; 62 | } t_ecudetailed; 63 | 64 | void aps_publish_int(const char *name, uint32_t value); 65 | void aps_publish_float(const char *name, float value); 66 | void aps_publish_string(const char *name, const char *value); 67 | bool aps_loop(); 68 | void aps_setup(); 69 | -------------------------------------------------------------------------------- /src/Config.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | t_cfg current_config; 9 | bool config_valid = false; 10 | 11 | void cfg_save() 12 | { 13 | File file = SPIFFS.open("/config.dat", "w"); 14 | if (!file || file.isDirectory()) 15 | { 16 | return; 17 | } 18 | 19 | if (strlen(current_config.hostname) < 2) 20 | { 21 | strcpy(current_config.hostname, CONFIG_OTANAME); 22 | } 23 | 24 | file.write((uint8_t *)¤t_config, sizeof(current_config)); 25 | file.close(); 26 | } 27 | 28 | void cfg_reset() 29 | { 30 | memset(¤t_config, 0x00, sizeof(current_config)); 31 | 32 | current_config.magic = CONFIG_MAGIC; 33 | strcpy(current_config.hostname, CONFIG_OTANAME); 34 | strcpy(current_config.mqtt_server, ""); 35 | current_config.mqtt_port = 11883; 36 | strcpy(current_config.mqtt_user, ""); 37 | strcpy(current_config.mqtt_password, ""); 38 | strcpy(current_config.mqtt_client, CONFIG_OTANAME); 39 | strcpy(current_config.mqtt_filter, ""); 40 | strcpy(current_config.aps_hostname, ""); 41 | strcpy(current_config.aps_mqttpath, ""); 42 | current_config.mqtt_publish = 0; 43 | 44 | strcpy(current_config.wifi_ssid, ""); 45 | strcpy(current_config.wifi_password, ""); 46 | } 47 | 48 | void cfg_read() 49 | { 50 | File file = SPIFFS.open("/config.dat", "r"); 51 | 52 | config_valid = false; 53 | 54 | if (!file || file.isDirectory()) 55 | { 56 | cfg_reset(); 57 | } 58 | else 59 | { 60 | file.read((uint8_t *)¤t_config, sizeof(current_config)); 61 | file.close(); 62 | 63 | if (current_config.magic != CONFIG_MAGIC) 64 | { 65 | /* on a minor version change, just keep wifi settings and hostname */ 66 | if ((current_config.magic & ~0xF) == (CONFIG_MAGIC & ~0xF)) 67 | { 68 | char hostname[32]; 69 | char wifi_ssid[32]; 70 | char wifi_password[32]; 71 | 72 | strcpy(hostname, current_config.hostname); 73 | strcpy(wifi_ssid, current_config.wifi_ssid); 74 | strcpy(wifi_password, current_config.wifi_password); 75 | 76 | cfg_reset(); 77 | 78 | strcpy(current_config.hostname, hostname); 79 | strcpy(current_config.wifi_ssid, wifi_ssid); 80 | strcpy(current_config.wifi_password, wifi_password); 81 | config_valid = true; 82 | } 83 | else 84 | { 85 | cfg_reset(); 86 | } 87 | return; 88 | } 89 | config_valid = true; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | ;https://github.com/esphome/esp-web-tools/issues/278 12 | 13 | 14 | [libraries] 15 | arduinolog = https://github.com/1technophile/Arduino-Log.git#d13cd80 16 | arduinojson = ArduinoJson@5.13.4 17 | RadioLib = jgromes/RadioLib@^5.3.0 18 | rtl_433_ESP = https://github.com/g3gg0/rtl_433_ESP.git 19 | 20 | 21 | [env:Sensor433Gateway] 22 | platform = espressif32 23 | upload_protocol = espota 24 | upload_port = rf433.fritz.box 25 | board = pico32 26 | framework = arduino 27 | board_build.f_cpu = 240000000L 28 | monitor_speed = 115200 29 | build_flags = !bash -c "echo -DPIO_SRC_REVNUM=$(git rev-list --count HEAD) -DPIO_SRC_REV=$(git rev-parse --short HEAD)" 30 | '-Isrc' 31 | '-DDEBUG_ESP_HTTP_UPDATE' 32 | '-DDEBUG_ESP_PORT=Serial ' 33 | '-DLOG_LEVEL=LOG_LEVEL_WARNING' 34 | '-DONBOARD_LED=25' ; Onboard LED is GPIO 25 on the Heltec Board 35 | ; *** rtl_433_ESP Options *** 36 | ; '-DRTL_DEBUG=4' ; rtl_433 verbose mode 37 | ; '-DRTL_VERBOSE=58' ; LaCrosse TX141-Bv2, TX141TH-Bv2, TX141-Bv3, TX141W, TX145wsdth sensor 38 | ; '-DRAW_SIGNAL_DEBUG=true' ; display raw received messages 39 | ; '-DMEMORY_DEBUG=true' ; display memory usage information 40 | '-DDEMOD_DEBUG=true' ; display signal debug info 41 | ; '-DMY_DEVICES=true' ; subset of devices 42 | ; '-DPUBLISH_UNPARSED=true' ; publish unparsed signal details 43 | '-DRSSI_THRESHOLD=8' ; Apply a delta of 12 44 | '-DOOK_FIXED_THRESHOLD=0x50' ; Inital OOK Threhold - Only for SX127X 45 | ; '-DAVERAGE_RSSI=5000' ; Display RSSI floor ( Average of 5000 samples ) 46 | '-DSIGNAL_RSSI=true' ; Display during signal receive 47 | ; *** RF Module Options *** 48 | '-DRF_SX1278="SX1278"' ; Heltec ESP 32 Module 49 | '-DRF_MODULE_DIO0=26' ; SX1276 pin DIO0 50 | '-DRF_MODULE_DIO1=35' ; SX1276 pin DIO1 51 | '-DRF_MODULE_DIO2=34' ; SX1276 pin DIO2 52 | '-DRF_MODULE_RST=14' ; pin to be used as hardware reset 53 | '-DRF_MODULE_INIT_STATUS=true' ; Display transceiver config during startupmi 54 | ; *** Heltec module requires non-standard SPI Config *** 55 | '-DRF_MODULE_CS=18' ; pin to be used as chip select 56 | '-DRF_MODULE_MOSI=27' 57 | '-DRF_MODULE_MISO=19' 58 | '-DRF_MODULE_SCK=5' 59 | ;'-DRF_SX1276' 60 | ; *** RadioLib Options *** 61 | '-DRADIOLIB_DEBUG=true' 62 | '-DTFA_TWINPLUS_RAIN=true' 63 | ; '-DRADIOLIB_VERBOSE=true' 64 | 65 | board_build.flash_mode = qio 66 | lib_deps = 67 | ${libraries.arduinolog} 68 | ${libraries.arduinojson} 69 | ${libraries.RadioLib} 70 | ${libraries.rtl_433_ESP} 71 | knolleary/PubSubClient@^2.8 72 | suculent/ESP32httpUpdate@^2.1.145 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /include/HA.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define MAX_LEN 32 4 | #define MAX_ENTITIES 48 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/Sensor433Gateway.ino: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #define WDT_TIMEOUT 180 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | int loopCount = 0; 23 | 24 | float main_duration_avg = 0; 25 | float main_duration = 0; 26 | float main_duration_max = 0; 27 | float main_duration_min = 1000000; 28 | 29 | extern bool config_valid; 30 | 31 | void setup() 32 | { 33 | Serial.begin(115200); 34 | Serial.printf("\n\n\n"); 35 | 36 | esp_task_wdt_reset(); 37 | 38 | Serial.printf("[i] SDK: '%s'\n", ESP.getSdkVersion()); 39 | Serial.printf("[i] CPU Speed: %d MHz\n", ESP.getCpuFreqMHz()); 40 | Serial.printf("[i] Chip Id: %06llX\n", ESP.getEfuseMac()); 41 | Serial.printf("[i] Flash Mode: %08X\n", ESP.getFlashChipMode()); 42 | Serial.printf("[i] Flash Size: %08X\n", ESP.getFlashChipSize()); 43 | Serial.printf("[i] Flash Speed: %d MHz\n", ESP.getFlashChipSpeed() / 1000000); 44 | Serial.printf("[i] Heap %d/%d\n", ESP.getFreeHeap(), ESP.getHeapSize()); 45 | Serial.printf("[i] SPIRam %d/%d\n", ESP.getFreePsram(), ESP.getPsramSize()); 46 | Serial.printf("\n"); 47 | Serial.printf("[i] Starting\n"); 48 | 49 | Serial.printf("[i] Setup WDT\n"); 50 | esp_task_wdt_init(WDT_TIMEOUT, true); 51 | esp_task_wdt_add(NULL); 52 | 53 | Serial.printf("[i] Setup LEDs\n"); 54 | led_setup(); 55 | Serial.printf("[i] Setup SPIFFS\n"); 56 | if (!SPIFFS.begin(true)) 57 | { 58 | Serial.println("[E] SPIFFS Mount Failed"); 59 | } 60 | cfg_read(); 61 | Serial.printf("[i] Setup WiFi\n"); 62 | wifi_setup(); 63 | Serial.printf("[i] Setup Webserver\n"); 64 | www_setup(); 65 | Serial.printf("[i] Setup Time\n"); 66 | ntp_setup(); 67 | Serial.printf("[i] Setup MQTT\n"); 68 | mqtt_setup(); 69 | Serial.printf("[i] Setup rtl433\n"); 70 | rcv_setup(); 71 | Serial.printf("[i] Setup APS ECU\n"); 72 | aps_setup(); 73 | 74 | Serial.println("Setup done"); 75 | } 76 | 77 | void loop() 78 | { 79 | bool hasWork = false; 80 | 81 | uint64_t startTime = micros(); 82 | 83 | hasWork |= led_loop(); 84 | hasWork |= wifi_loop(); 85 | hasWork |= www_loop(); 86 | hasWork |= ntp_loop(); 87 | hasWork |= mqtt_loop(); 88 | hasWork |= ota_loop(); 89 | hasWork |= rcv_loop(); 90 | hasWork |= aps_loop(); 91 | 92 | uint64_t duration = micros() - startTime; 93 | 94 | main_duration = duration; 95 | main_duration_avg = (15 * main_duration_avg + duration) / 16.0f; 96 | 97 | if (main_duration < main_duration_min) 98 | { 99 | main_duration_min = main_duration; 100 | } 101 | if (main_duration > main_duration_max) 102 | { 103 | main_duration_max = main_duration; 104 | } 105 | 106 | loopCount++; 107 | 108 | if (!hasWork) 109 | { 110 | delay(100); 111 | } 112 | else 113 | { 114 | delay(10); 115 | } 116 | esp_task_wdt_reset(); 117 | } 118 | -------------------------------------------------------------------------------- /src/Receiver.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #ifndef RF_MODULE_FREQUENCY 12 | #define RF_MODULE_FREQUENCY 433.92 13 | #endif 14 | 15 | #define JSON_MSG_BUFFER 512 16 | 17 | std::list ReceiverLastReceived; 18 | 19 | rtl_433_ESP rf(-1); // use -1 to disable transmitter 20 | 21 | void rcv_addreceived(const char *path, const char *json) 22 | { 23 | for (auto it = ReceiverLastReceived.begin(); it != ReceiverLastReceived.end(); it++) 24 | { 25 | /* entry with that path already found, just update json */ 26 | if (!strcmp(it->first, path)) 27 | { 28 | free(it->second); 29 | it->second = strdup(json); 30 | return; 31 | } 32 | } 33 | 34 | /* non found, add new one */ 35 | t_map_entry entry; 36 | entry.first = strdup(path); 37 | entry.second = strdup(json); 38 | ReceiverLastReceived.push_back(entry); 39 | } 40 | 41 | void rcv_publish_string(const char *path, const char *elem, const char *value) 42 | { 43 | char tmp[128]; 44 | char topic[128]; 45 | 46 | sprintf(tmp, "%s/%s", path, elem); 47 | strcpy(topic, "feeds/string/%s/"); 48 | strcat(topic, tmp); 49 | 50 | mqtt_publish_string(topic, value); 51 | } 52 | 53 | void rcv_publish_float(const char *path, const char *elem, float value) 54 | { 55 | char tmp[128]; 56 | char topic[128]; 57 | 58 | sprintf(tmp, "%s/%s", path, elem); 59 | strcpy(topic, "feeds/float/%s/"); 60 | strcat(topic, tmp); 61 | 62 | mqtt_publish_float(topic, value); 63 | } 64 | 65 | void rcv_publish_int(const char *path, const char *elem, int value) 66 | { 67 | char tmp[128]; 68 | char topic[128]; 69 | 70 | sprintf(tmp, "%s/%s", path, elem); 71 | strcpy(topic, "feeds/integer/%s/"); 72 | strcat(topic, tmp); 73 | 74 | mqtt_publish_int(topic, value); 75 | } 76 | 77 | bool rcv_enabled(const char *path) 78 | { 79 | char *fields = current_config.mqtt_filter; 80 | int pos = 0; 81 | 82 | while (fields[pos]) 83 | { 84 | if (fields[pos] == ' ') 85 | { 86 | pos++; 87 | } 88 | else 89 | { 90 | const char *cur_str = &fields[pos]; 91 | 92 | const char *end = strchr(cur_str, ' '); 93 | int length = strlen(cur_str); 94 | 95 | if (end) 96 | { 97 | length = (int)(end - cur_str); 98 | } 99 | 100 | if (!strncmp(cur_str, path, length)) 101 | { 102 | return true; 103 | } 104 | 105 | pos += length; 106 | } 107 | } 108 | 109 | return false; 110 | } 111 | 112 | void rcv_callback(char *msg) 113 | { 114 | const char *message = strdup(msg); 115 | 116 | DynamicJsonBuffer json(JSON_MSG_BUFFER); 117 | JsonObject &data = json.parseObject(message); 118 | 119 | char path[128]; 120 | const char *model = data["model"]; 121 | int channel = data["channel"]; 122 | 123 | bool publish = false; 124 | 125 | if (!strcmp(model, "status")) 126 | { 127 | sprintf(path, "status"); 128 | publish = (current_config.mqtt_publish & 2); 129 | } 130 | else 131 | { 132 | sprintf(path, "%s-%d", model, channel); 133 | for (int pos = 0; pos < strlen(path); pos++) 134 | { 135 | if (path[pos] == ' ') 136 | { 137 | path[pos] = '_'; 138 | } 139 | } 140 | 141 | rcv_addreceived(path, msg); 142 | 143 | publish = rcv_enabled(path) && (current_config.mqtt_publish & 1); 144 | } 145 | 146 | if (publish) 147 | { 148 | for (auto dataobj : data) 149 | { 150 | if (dataobj.value.is()) 151 | { 152 | rcv_publish_int(path, dataobj.key, dataobj.value); 153 | } 154 | else if (dataobj.value.is()) 155 | { 156 | rcv_publish_float(path, dataobj.key, dataobj.value); 157 | } 158 | else if (dataobj.value.is()) 159 | { 160 | rcv_publish_int(path, dataobj.key, dataobj.value ? 1 : 0); 161 | } 162 | else if (dataobj.value.is()) 163 | { 164 | rcv_publish_string(path, dataobj.key, dataobj.value); 165 | } 166 | } 167 | } 168 | 169 | free((void *)message); 170 | } 171 | 172 | void rcv_setup() 173 | { 174 | static char messageBuffer[JSON_MSG_BUFFER]; 175 | 176 | rf.initReceiver(RF_MODULE_RECEIVER_GPIO, RF_MODULE_FREQUENCY); 177 | rf.setCallback(rcv_callback, messageBuffer, JSON_MSG_BUFFER); 178 | rf.enableReceiver(RF_MODULE_RECEIVER_GPIO); 179 | } 180 | 181 | bool rcv_loop() 182 | { 183 | uint32_t time = millis(); 184 | static int nextTime = 0; 185 | 186 | rf.loop(); 187 | 188 | if (time >= nextTime) 189 | { 190 | nextTime = time + 60000; 191 | //rf.getStatus(0); 192 | } 193 | return false; 194 | } 195 | -------------------------------------------------------------------------------- /src/WiFi_Handler.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | DNSServer dnsServer; 11 | 12 | bool connecting = false; 13 | bool wifi_captive = false; 14 | char wifi_error[64]; 15 | int wifi_rssi = 0; 16 | 17 | #define WIFI_LED 1 18 | 19 | void wifi_setup() 20 | { 21 | Serial.printf("[WiFi] Connecting to '%s', password '%s'...\n", current_config.wifi_ssid, current_config.wifi_password); 22 | sprintf(wifi_error, ""); 23 | WiFi.begin(current_config.wifi_ssid, current_config.wifi_password); 24 | connecting = true; 25 | led_set(WIFI_LED, 8, 8, 0); 26 | } 27 | 28 | void wifi_off() 29 | { 30 | connecting = false; 31 | WiFi.disconnect(); 32 | WiFi.mode(WIFI_OFF); 33 | } 34 | 35 | void wifi_enter_captive() 36 | { 37 | wifi_off(); 38 | WiFi.softAP(CONFIG_SOFTAPNAME); 39 | dnsServer.start(53, "*", WiFi.softAPIP()); 40 | Serial.printf("[WiFi] Local IP: %s\n", WiFi.softAPIP().toString().c_str()); 41 | 42 | wifi_captive = true; 43 | 44 | /* reset captive idle timer */ 45 | www_activity(); 46 | } 47 | 48 | bool wifi_loop(void) 49 | { 50 | int status = WiFi.status(); 51 | uint32_t curTime = millis(); 52 | static uint32_t nextTime = 0; 53 | static uint32_t stateCounter = 0; 54 | 55 | if (wifi_captive) 56 | { 57 | dnsServer.processNextRequest(); 58 | led_set(WIFI_LED, 0, ((millis() % 250) > 125) ? 0 : 255, 0); 59 | 60 | /* captive mode, but noone cares */ 61 | if (!www_is_captive_active()) 62 | { 63 | Serial.printf("[WiFi] Timeout in captive, trying known networks again\n"); 64 | sprintf(wifi_error, "Timeout in captive, trying known networks again"); 65 | dnsServer.stop(); 66 | wifi_off(); 67 | wifi_captive = false; 68 | stateCounter = 0; 69 | sprintf(wifi_error, ""); 70 | } 71 | return true; 72 | } 73 | 74 | if (nextTime > curTime) 75 | { 76 | return false; 77 | } 78 | 79 | /* standard refresh time */ 80 | nextTime = curTime + 500; 81 | 82 | /* when stuck at a state, disconnect */ 83 | if (++stateCounter > 20) 84 | { 85 | Serial.printf("[WiFi] Timeout connecting\n"); 86 | sprintf(wifi_error, "Timeout - incorrect password?"); 87 | wifi_off(); 88 | } 89 | 90 | if (strcmp(wifi_error, "")) 91 | { 92 | Serial.printf("[WiFi] Entering captive mode. Reason: '%s'\n", wifi_error); 93 | 94 | wifi_enter_captive(); 95 | 96 | stateCounter = 0; 97 | return false; 98 | } 99 | 100 | switch (status) 101 | { 102 | case WL_CONNECTED: 103 | if (connecting) 104 | { 105 | led_set(WIFI_LED, 0, 4, 0); 106 | connecting = false; 107 | Serial.print("[WiFi] Connected, IP address: "); 108 | Serial.println(WiFi.localIP()); 109 | stateCounter = 0; 110 | sprintf(wifi_error, ""); 111 | } 112 | else 113 | { 114 | static int last_rssi = -1; 115 | wifi_rssi = WiFi.RSSI(); 116 | 117 | if (last_rssi != wifi_rssi) 118 | { 119 | float maxRssi = -70; 120 | float minRssi = -90; 121 | float strRatio = (wifi_rssi - minRssi) / (maxRssi - minRssi); 122 | float strength = min(1, max(0, strRatio)); 123 | float brightness = 0.05f; 124 | int r = brightness * 255.0f * (1.0f - strength); 125 | int g = brightness * 255.0f * strength; 126 | 127 | led_set(WIFI_LED, r, g, 0); 128 | 129 | if (current_config.verbose & 1) 130 | { 131 | Serial.printf("[WiFi] RSSI %d, strength: %1.2f, r: %d, g: %d\n", wifi_rssi, strength, r, g); 132 | } 133 | 134 | last_rssi = wifi_rssi; 135 | } 136 | 137 | /* happy with this state, reset counter */ 138 | stateCounter = 0; 139 | } 140 | break; 141 | 142 | case WL_CONNECTION_LOST: 143 | Serial.printf("[WiFi] Connection lost\n"); 144 | sprintf(wifi_error, "Network found, but connection lost"); 145 | led_set(WIFI_LED, 32, 8, 0); 146 | wifi_off(); 147 | break; 148 | 149 | case WL_CONNECT_FAILED: 150 | Serial.printf("[WiFi] Connection failed\n"); 151 | sprintf(wifi_error, "Network found, but connection failed"); 152 | wifi_off(); 153 | break; 154 | 155 | case WL_NO_SSID_AVAIL: 156 | Serial.printf("[WiFi] No SSID with that name\n"); 157 | sprintf(wifi_error, "Network not found"); 158 | wifi_off(); 159 | break; 160 | 161 | case WL_SCAN_COMPLETED: 162 | Serial.printf("[WiFi] Scan completed\n"); 163 | wifi_off(); 164 | break; 165 | 166 | case WL_DISCONNECTED: 167 | if (!connecting) 168 | { 169 | Serial.printf("[WiFi] Disconnected\n"); 170 | led_set(WIFI_LED, 255, 0, 255); 171 | wifi_off(); 172 | } 173 | break; 174 | 175 | case WL_IDLE_STATUS: 176 | if (!connecting) 177 | { 178 | connecting = true; 179 | Serial.printf("[WiFi] Idle, connect to '%s'\n", current_config.wifi_ssid); 180 | WiFi.mode(WIFI_STA); 181 | WiFi.begin(current_config.wifi_ssid, current_config.wifi_password); 182 | } 183 | else 184 | { 185 | Serial.printf("[WiFi] Idle, connecting...\n"); 186 | } 187 | break; 188 | 189 | case WL_NO_SHIELD: 190 | if (!connecting) 191 | { 192 | connecting = true; 193 | Serial.printf("[WiFi] Disabled (%d), connecting to '%s'\n", status, current_config.wifi_ssid); 194 | WiFi.mode(WIFI_STA); 195 | WiFi.begin(current_config.wifi_ssid, current_config.wifi_password); 196 | } 197 | break; 198 | 199 | default: 200 | Serial.printf("[WiFi] unknown (%d), disable\n", status); 201 | wifi_off(); 202 | break; 203 | } 204 | 205 | return false; 206 | } 207 | -------------------------------------------------------------------------------- /src/NTP.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | IPAddress timeServerIP; // time.nist.gov NTP server address 9 | const char *ntpServerName = "time.nist.gov"; 10 | unsigned int localPort = 2390; // local port to listen for UDP packets 11 | const int NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message 12 | byte packetBuffer[NTP_PACKET_SIZE]; // buffer to hold incoming and outgoing packets 13 | WiFiUDP Udp; // A UDP instance to let us send and receive packets over UDP 14 | 15 | uint32_t retries = 0; 16 | unsigned long lastSent = 0; 17 | unsigned long timeReference = 0; 18 | bool time_valid = false; 19 | unsigned long secsSince1900 = 0; 20 | unsigned long setup_time_offset = 2; 21 | 22 | /* 2000-03-01 (mod 400 year, immediately after feb29 */ 23 | #define LEAPOCH (946684800LL + 86400 * (31 + 29)) 24 | #define DAYS_PER_400Y (365 * 400 + 97) 25 | #define DAYS_PER_100Y (365 * 100 + 24) 26 | #define DAYS_PER_4Y (365 * 4 + 1) 27 | 28 | statusType currentStatus = Idle; 29 | 30 | void printTime() 31 | { 32 | struct tm tm; 33 | getTime(&tm); 34 | 35 | Serial.printf("[NTP] The time is: %02d:%02d:%02d\n", tm.tm_hour, tm.tm_min, tm.tm_sec); 36 | } 37 | 38 | int secs_to_tm(long long t, struct tm *tm) 39 | { 40 | long long days, secs; 41 | int remdays, remsecs, remyears; 42 | int qc_cycles, c_cycles, q_cycles; 43 | int years, months; 44 | int wday, yday, leap; 45 | static const char days_in_month[] = {31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 29}; 46 | 47 | secs = t - LEAPOCH; 48 | days = secs / 86400; 49 | remsecs = secs % 86400; 50 | if (remsecs < 0) 51 | { 52 | remsecs += 86400; 53 | days--; 54 | } 55 | 56 | wday = (3 + days) % 7; 57 | if (wday < 0) 58 | wday += 7; 59 | 60 | qc_cycles = days / DAYS_PER_400Y; 61 | remdays = days % DAYS_PER_400Y; 62 | if (remdays < 0) 63 | { 64 | remdays += DAYS_PER_400Y; 65 | qc_cycles--; 66 | } 67 | 68 | c_cycles = remdays / DAYS_PER_100Y; 69 | if (c_cycles == 4) 70 | c_cycles--; 71 | remdays -= c_cycles * DAYS_PER_100Y; 72 | 73 | q_cycles = remdays / DAYS_PER_4Y; 74 | if (q_cycles == 25) 75 | q_cycles--; 76 | remdays -= q_cycles * DAYS_PER_4Y; 77 | 78 | remyears = remdays / 365; 79 | if (remyears == 4) 80 | remyears--; 81 | remdays -= remyears * 365; 82 | 83 | leap = !remyears && (q_cycles || !c_cycles); 84 | yday = remdays + 31 + 28 + leap; 85 | if (yday >= 365 + leap) 86 | yday -= 365 + leap; 87 | 88 | years = remyears + 4 * q_cycles + 100 * c_cycles + 400 * qc_cycles; 89 | 90 | for (months = 0; days_in_month[months] <= remdays; months++) 91 | remdays -= days_in_month[months]; 92 | 93 | tm->tm_year = years + 100; 94 | tm->tm_mon = months + 2; 95 | if (tm->tm_mon >= 12) 96 | { 97 | tm->tm_mon -= 12; 98 | tm->tm_year++; 99 | } 100 | tm->tm_mday = remdays; 101 | tm->tm_wday = wday; 102 | tm->tm_yday = yday; 103 | 104 | tm->tm_hour = remsecs / 3600; 105 | tm->tm_min = remsecs / 60 % 60; 106 | tm->tm_sec = remsecs % 60; 107 | 108 | return 0; 109 | } 110 | 111 | void getTimeAdv(struct tm *tm, unsigned long offset) 112 | { 113 | unsigned long epoch = secsSince1900 - 2208988800UL; 114 | 115 | long secs = ((long)offset - (long)timeReference) / 1000; 116 | epoch += 60 * 60 * setup_time_offset; 117 | epoch += secs; 118 | 119 | secs_to_tm(epoch, tm); 120 | } 121 | 122 | void getTime(struct tm *tm) 123 | { 124 | getTimeAdv(tm, millis()); 125 | } 126 | 127 | void getStartupTime(struct tm *tm) 128 | { 129 | getTimeAdv(tm, 0); 130 | } 131 | 132 | // send an NTP request to the time server at the given address 133 | void sendNTPpacket(IPAddress &address) 134 | { 135 | memset(packetBuffer, 0, NTP_PACKET_SIZE); 136 | 137 | // Initialize values needed to form NTP request 138 | packetBuffer[0] = 0b11100011; // LI, Version, Mode 139 | packetBuffer[1] = 0; // Stratum, or type of clock 140 | packetBuffer[2] = 6; // Polling Interval 141 | packetBuffer[3] = 0xEC; // Peer Clock Precision 142 | // 8 bytes of zero for Root Delay & Root Dispersion 143 | packetBuffer[12] = 49; 144 | packetBuffer[13] = 0x4E; 145 | packetBuffer[14] = 49; 146 | packetBuffer[15] = 52; 147 | 148 | Udp.beginPacket(address, 123); // NTP requests are to port 123 149 | Udp.write(packetBuffer, NTP_PACKET_SIZE); 150 | Udp.endPacket(); 151 | } 152 | 153 | const char *Time_getStateString() 154 | { 155 | static char retString[64]; 156 | const char *state = ""; 157 | 158 | switch (currentStatus) 159 | { 160 | default: 161 | state = "Unknown state"; 162 | break; 163 | 164 | case Idle: 165 | state = "Idle"; 166 | break; 167 | 168 | case Sent: 169 | state = "Sent"; 170 | break; 171 | 172 | case Received: 173 | state = "Received"; 174 | break; 175 | 176 | case Pause: 177 | state = "Pause"; 178 | break; 179 | } 180 | 181 | snprintf(retString, sizeof(retString), "%s, ref: %lu, last: %lu, retries: %u, millis(): %lu", state, timeReference, lastSent, retries, millis()); 182 | 183 | return retString; 184 | } 185 | 186 | void ntp_setup() 187 | { 188 | Udp.begin(localPort); 189 | currentStatus = Idle; 190 | lastSent = 0; 191 | timeReference = 0; 192 | memset(packetBuffer, 0x00, sizeof(packetBuffer)); 193 | } 194 | 195 | bool ntp_loop() 196 | { 197 | if (WiFi.status() != WL_CONNECTED) 198 | { 199 | return false; 200 | } 201 | 202 | switch (currentStatus) 203 | { 204 | default: 205 | Serial.println("[NTP] Unknown state"); 206 | currentStatus = Idle; 207 | break; 208 | 209 | case Idle: 210 | if (!time_valid || millis() - lastSent > 1000 * 60 * 60) 211 | { 212 | Serial.println("[NTP] Sending request"); 213 | 214 | lastSent = millis(); 215 | currentStatus = Sent; 216 | WiFi.hostByName(ntpServerName, timeServerIP); 217 | sendNTPpacket(timeServerIP); // send an NTP packet to a time server 218 | } 219 | break; 220 | 221 | case Sent: 222 | if (millis() - lastSent > 1000 * 10) 223 | { 224 | Serial.println("[NTP] No reply, resend"); 225 | if (retries < 10) 226 | { 227 | retries++; 228 | currentStatus = Idle; 229 | } 230 | else 231 | { 232 | currentStatus = Pause; 233 | } 234 | } 235 | else if (Udp.parsePacket()) 236 | { 237 | timeReference = millis(); 238 | currentStatus = Received; 239 | Udp.read(packetBuffer, NTP_PACKET_SIZE); // read the packet into the buffer 240 | } 241 | break; 242 | 243 | case Received: 244 | { 245 | unsigned long highWord = word(packetBuffer[40], packetBuffer[41]); 246 | unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]); 247 | secsSince1900 = highWord << 16 | lowWord; 248 | 249 | printTime(); 250 | 251 | if (!time_valid) 252 | { 253 | time_valid = true; 254 | } 255 | 256 | retries = 0; 257 | currentStatus = Idle; 258 | break; 259 | } 260 | 261 | case Pause: 262 | if (millis() - lastSent > 1000 * 60 * 2) 263 | { 264 | currentStatus = Idle; 265 | } 266 | break; 267 | } 268 | 269 | return false; 270 | } 271 | -------------------------------------------------------------------------------- /src/MQTT.cpp: -------------------------------------------------------------------------------- 1 | 2 | // #define TESTMODE 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | WiFiClient client; 14 | PubSubClient mqtt(client); 15 | 16 | extern int wifi_rssi; 17 | 18 | extern float main_duration_avg; 19 | extern float main_duration_max; 20 | extern float main_duration_min; 21 | extern float main_duration; 22 | 23 | uint32_t mqtt_last_publish_time = 0; 24 | uint32_t mqtt_lastConnect = 0; 25 | uint32_t mqtt_retries = 0; 26 | bool mqtt_fail = false; 27 | 28 | char command_topic[64]; 29 | char response_topic[64]; 30 | 31 | void ota_setup(); 32 | 33 | void callback(char *topic, byte *payload, unsigned int length) 34 | { 35 | Serial.print("Message arrived ["); 36 | Serial.print(topic); 37 | Serial.print("] "); 38 | Serial.print("'"); 39 | for (int i = 0; i < length; i++) 40 | { 41 | Serial.print((char)payload[i]); 42 | } 43 | Serial.print("'"); 44 | Serial.println(); 45 | 46 | payload[length] = 0; 47 | 48 | ha_received(topic, (const char *)payload); 49 | 50 | if (!strcmp(topic, command_topic)) 51 | { 52 | char *command = (char *)payload; 53 | char buf[1024]; 54 | 55 | if (!strncmp(command, "http", 4)) 56 | { 57 | snprintf(buf, sizeof(buf) - 1, "updating from: '%s'", command); 58 | Serial.printf("%s\n", buf); 59 | 60 | mqtt.publish(response_topic, buf); 61 | ESPhttpUpdate.rebootOnUpdate(false); 62 | t_httpUpdate_return ret = ESPhttpUpdate.update(command); 63 | 64 | switch (ret) 65 | { 66 | case HTTP_UPDATE_FAILED: 67 | snprintf(buf, sizeof(buf) - 1, "HTTP_UPDATE_FAILED Error (%d): %s", ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str()); 68 | mqtt.publish(response_topic, buf); 69 | Serial.printf("%s\n", buf); 70 | break; 71 | 72 | case HTTP_UPDATE_NO_UPDATES: 73 | snprintf(buf, sizeof(buf) - 1, "HTTP_UPDATE_NO_UPDATES"); 74 | mqtt.publish(response_topic, buf); 75 | Serial.printf("%s\n", buf); 76 | break; 77 | 78 | case HTTP_UPDATE_OK: 79 | snprintf(buf, sizeof(buf) - 1, "HTTP_UPDATE_OK"); 80 | mqtt.publish(response_topic, buf); 81 | Serial.printf("%s\n", buf); 82 | delay(500); 83 | ESP.restart(); 84 | break; 85 | 86 | default: 87 | snprintf(buf, sizeof(buf) - 1, "update failed"); 88 | mqtt.publish(response_topic, buf); 89 | Serial.printf("%s\n", buf); 90 | break; 91 | } 92 | } 93 | else 94 | { 95 | snprintf(buf, sizeof(buf) - 1, "unknown command: '%s'", command); 96 | mqtt.publish(response_topic, buf); 97 | Serial.printf("%s\n", buf); 98 | } 99 | } 100 | } 101 | 102 | void mqtt_ota_received(const t_ha_entity *entity, void *ctx, const char *message) 103 | { 104 | ota_setup(); 105 | } 106 | 107 | void mqtt_setup() 108 | { 109 | mqtt.setCallback(callback); 110 | 111 | ha_setup(); 112 | 113 | t_ha_entity entity; 114 | 115 | memset(&entity, 0x00, sizeof(entity)); 116 | entity.id = "ota"; 117 | entity.name = "Enable OTA"; 118 | entity.type = ha_button; 119 | entity.cmd_t = "command/%s/ota"; 120 | entity.received = &mqtt_ota_received; 121 | ha_add(&entity); 122 | 123 | memset(&entity, 0x00, sizeof(entity)); 124 | entity.id = "rssi"; 125 | entity.name = "WiFi RSSI"; 126 | entity.type = ha_sensor; 127 | entity.stat_t = "feeds/integer/%s/rssi"; 128 | entity.unit_of_meas = "dBm"; 129 | ha_add(&entity); 130 | 131 | memset(&entity, 0x00, sizeof(entity)); 132 | entity.id = "error"; 133 | entity.name = "Error message"; 134 | entity.stat_t = "feeds/string/%s/error"; 135 | entity.cmd_t = "feeds/string/%s/error"; 136 | ha_add(&entity); 137 | } 138 | 139 | void mqtt_publish_string(const char *name, const char *value) 140 | { 141 | char path_buffer[128]; 142 | 143 | sprintf(path_buffer, name, current_config.mqtt_client); 144 | 145 | if (!mqtt.publish(path_buffer, value)) 146 | { 147 | mqtt_fail = true; 148 | } 149 | Serial.printf("Published %s : %s\n", path_buffer, value); 150 | } 151 | 152 | void mqtt_publish_string_plain(const char *path_buffer, const char *value) 153 | { 154 | if (!mqtt.publish(path_buffer, value)) 155 | { 156 | mqtt_fail = true; 157 | } 158 | Serial.printf("Published %s : %s\n", path_buffer, value); 159 | } 160 | 161 | void mqtt_publish_float(const char *name, float value) 162 | { 163 | char path_buffer[128]; 164 | char buffer[32]; 165 | 166 | sprintf(path_buffer, name, current_config.mqtt_client); 167 | sprintf(buffer, "%0.2f", value); 168 | 169 | if (!mqtt.publish(path_buffer, buffer)) 170 | { 171 | mqtt_fail = true; 172 | } 173 | Serial.printf("Published %s : %s\n", path_buffer, buffer); 174 | } 175 | 176 | void mqtt_publish_float_plain(const char *path_buffer, float value) 177 | { 178 | char buffer[32]; 179 | 180 | sprintf(buffer, "%0.2f", value); 181 | 182 | if (!mqtt.publish(path_buffer, buffer)) 183 | { 184 | mqtt_fail = true; 185 | } 186 | Serial.printf("Published %s : %s\n", path_buffer, buffer); 187 | } 188 | 189 | void mqtt_publish_int(const char *name, uint32_t value) 190 | { 191 | char path_buffer[128]; 192 | char buffer[32]; 193 | 194 | if (value == 0x7FFFFFFF) 195 | { 196 | return; 197 | } 198 | sprintf(path_buffer, name, current_config.mqtt_client); 199 | sprintf(buffer, "%d", value); 200 | 201 | if (!mqtt.publish(path_buffer, buffer)) 202 | { 203 | mqtt_fail = true; 204 | } 205 | Serial.printf("Published %s : %s\n", path_buffer, buffer); 206 | } 207 | 208 | void mqtt_publish_int_plain(const char *path_buffer, uint32_t value) 209 | { 210 | char buffer[32]; 211 | 212 | sprintf(buffer, "%d", value); 213 | 214 | if (!mqtt.publish(path_buffer, buffer)) 215 | { 216 | mqtt_fail = true; 217 | } 218 | Serial.printf("Published %s : %s\n", path_buffer, buffer); 219 | } 220 | 221 | bool mqtt_loop() 222 | { 223 | uint32_t time = millis(); 224 | static uint32_t nextTime = 0; 225 | 226 | #ifdef TESTMODE 227 | return false; 228 | #endif 229 | if (mqtt_fail) 230 | { 231 | mqtt_fail = false; 232 | mqtt.disconnect(); 233 | } 234 | 235 | MQTT_connect(); 236 | 237 | if (!mqtt.connected()) 238 | { 239 | return false; 240 | } 241 | 242 | mqtt.loop(); 243 | 244 | ha_loop(); 245 | 246 | if (time >= nextTime) 247 | { 248 | bool do_publish = false; 249 | 250 | if ((time - mqtt_last_publish_time) > 60000) 251 | { 252 | do_publish = true; 253 | } 254 | 255 | if (do_publish) 256 | { 257 | /* debug */ 258 | mqtt_publish_int("feeds/integer/%s/rssi", wifi_rssi); 259 | 260 | mqtt_last_publish_time = time; 261 | 262 | if (current_config.mqtt_publish & 1) 263 | { 264 | } 265 | if (current_config.mqtt_publish & 2) 266 | { 267 | mqtt_publish_int((char *)"feeds/integer/%s/version", PIO_SRC_REVNUM); 268 | 269 | if ((main_duration_max > 0) && (main_duration_max < 1000000) && (main_duration_min > 0) && (main_duration_min < 1000000)) 270 | { 271 | mqtt_publish_float((char *)"test/float/%s/debug/main_duration", main_duration); 272 | mqtt_publish_float((char *)"feeds/float/%s/debug/main_duration_min", main_duration_min); 273 | mqtt_publish_float((char *)"feeds/float/%s/debug/main_duration_max", main_duration_max); 274 | mqtt_publish_float((char *)"feeds/float/%s/debug/main_duration_avg", main_duration_avg); 275 | } 276 | main_duration_max = 0; 277 | main_duration_min = 1000000; 278 | } 279 | } 280 | nextTime = time + 1000; 281 | } 282 | 283 | return false; 284 | } 285 | 286 | void MQTT_connect() 287 | { 288 | uint32_t curTime = millis(); 289 | int8_t ret; 290 | 291 | if (strlen(current_config.mqtt_server) == 0) 292 | { 293 | return; 294 | } 295 | 296 | mqtt.setServer(current_config.mqtt_server, current_config.mqtt_port); 297 | 298 | if (WiFi.status() != WL_CONNECTED) 299 | { 300 | return; 301 | } 302 | 303 | if (mqtt.connected()) 304 | { 305 | return; 306 | } 307 | 308 | if ((mqtt_lastConnect != 0) && (curTime - mqtt_lastConnect < (1000 << mqtt_retries))) 309 | { 310 | return; 311 | } 312 | 313 | mqtt_lastConnect = curTime; 314 | 315 | Serial.println("MQTT: Connecting to MQTT... "); 316 | 317 | sprintf(command_topic, "tele/%s/command", current_config.mqtt_client); 318 | sprintf(response_topic, "tele/%s/response", current_config.mqtt_client); 319 | 320 | ret = mqtt.connect(current_config.mqtt_client, current_config.mqtt_user, current_config.mqtt_password); 321 | 322 | if (ret == 0) 323 | { 324 | mqtt_retries++; 325 | if (mqtt_retries > 8) 326 | { 327 | mqtt_retries = 8; 328 | } 329 | Serial.printf("MQTT: (%d) ", mqtt.state()); 330 | Serial.println("MQTT: Retrying MQTT connection"); 331 | mqtt.disconnect(); 332 | } 333 | else 334 | { 335 | Serial.println("MQTT Connected!"); 336 | mqtt.subscribe(command_topic); 337 | ha_connected(); 338 | mqtt_publish_string((char *)"feeds/string/%s/error", ""); 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /src/HA.cpp: -------------------------------------------------------------------------------- 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/APS_ECU.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | float aps_energy_lifetime = 0; 9 | float aps_power_current = 0; 10 | float aps_energy_day = 0; 11 | float aps_frequency = 0; 12 | float aps_temperature = 0; 13 | float aps_power_a = 0; 14 | float aps_voltage_a = 0; 15 | float aps_power_b = 0; 16 | float aps_voltage_b = 0; 17 | char aps_channel[3]; 18 | char aps_ecu_serial[32]; 19 | char aps_ecu_firmware[32]; 20 | char aps_inv_serial[32]; 21 | char aps_inv_unk[16]; 22 | char aps_inv_model[16]; 23 | char aps_timestamp[32]; 24 | char aps_timestamp_mqtt[32]; 25 | 26 | void mqtt_publish_string_plain(const char *path_buffer, const char *value); 27 | void mqtt_publish_float_plain(const char *path_buffer, float value); 28 | void mqtt_publish_int_plain(const char *path_buffer, uint32_t value); 29 | 30 | void aps_setup() 31 | { 32 | t_ha_entity entity; 33 | 34 | memset(&entity, 0x00, sizeof(entity)); 35 | entity.id = "channel"; 36 | entity.name = "Channel"; 37 | entity.type = ha_sensor; 38 | entity.stat_t = "feeds/string/%s/channel"; 39 | entity.alt_name = current_config.aps_mqttpath; 40 | ha_add(&entity); 41 | 42 | memset(&entity, 0x00, sizeof(entity)); 43 | entity.id = "power_current"; 44 | entity.name = "Solar power"; 45 | entity.type = ha_sensor; 46 | entity.stat_t = "feeds/float/%s/power_current"; 47 | entity.alt_name = current_config.aps_mqttpath; 48 | entity.unit_of_meas = "W"; 49 | entity.dev_class = "power"; 50 | entity.state_class = "measurement"; 51 | ha_add(&entity); 52 | 53 | memset(&entity, 0x00, sizeof(entity)); 54 | entity.id = "energy_day"; 55 | entity.name = "Solar energy daily"; 56 | entity.type = ha_sensor; 57 | entity.stat_t = "feeds/float/%s/energy_day"; 58 | entity.alt_name = current_config.aps_mqttpath; 59 | entity.unit_of_meas = "Wh"; 60 | entity.dev_class = "energy"; 61 | entity.state_class = "total_increasing"; 62 | ha_add(&entity); 63 | 64 | memset(&entity, 0x00, sizeof(entity)); 65 | entity.id = "energy_lifetime"; 66 | entity.name = "Solar energy lifetime"; 67 | entity.type = ha_sensor; 68 | entity.stat_t = "feeds/float/%s/energy_lifetime"; 69 | entity.alt_name = current_config.aps_mqttpath; 70 | entity.unit_of_meas = "Wh"; 71 | entity.dev_class = "energy"; 72 | entity.state_class = "total_increasing"; 73 | ha_add(&entity); 74 | 75 | memset(&entity, 0x00, sizeof(entity)); 76 | entity.id = "power_a"; 77 | entity.name = "Solar power channel A"; 78 | entity.type = ha_sensor; 79 | entity.stat_t = "feeds/float/%s/power_a"; 80 | entity.alt_name = current_config.aps_mqttpath; 81 | entity.unit_of_meas = "W"; 82 | entity.dev_class = "power"; 83 | ha_add(&entity); 84 | 85 | memset(&entity, 0x00, sizeof(entity)); 86 | entity.id = "power_b"; 87 | entity.name = "Solar power channel B"; 88 | entity.type = ha_sensor; 89 | entity.stat_t = "feeds/float/%s/power_b"; 90 | entity.alt_name = current_config.aps_mqttpath; 91 | entity.unit_of_meas = "W"; 92 | entity.dev_class = "power"; 93 | ha_add(&entity); 94 | 95 | memset(&entity, 0x00, sizeof(entity)); 96 | entity.id = "voltage_a"; 97 | entity.name = "Solar voltage channel A"; 98 | entity.type = ha_sensor; 99 | entity.stat_t = "feeds/float/%s/voltage_a"; 100 | entity.alt_name = current_config.aps_mqttpath; 101 | entity.unit_of_meas = "V"; 102 | entity.dev_class = "voltage"; 103 | ha_add(&entity); 104 | 105 | memset(&entity, 0x00, sizeof(entity)); 106 | entity.id = "voltage_b"; 107 | entity.name = "Solar voltage channel B"; 108 | entity.type = ha_sensor; 109 | entity.stat_t = "feeds/float/%s/voltage_b"; 110 | entity.alt_name = current_config.aps_mqttpath; 111 | entity.unit_of_meas = "V"; 112 | entity.dev_class = "voltage"; 113 | ha_add(&entity); 114 | 115 | memset(&entity, 0x00, sizeof(entity)); 116 | entity.id = "frequency"; 117 | entity.name = "Solar mains frequency"; 118 | entity.type = ha_sensor; 119 | entity.stat_t = "feeds/float/%s/frequency"; 120 | entity.alt_name = current_config.aps_mqttpath; 121 | entity.unit_of_meas = "Hz"; 122 | entity.dev_class = "frequency"; 123 | ha_add(&entity); 124 | 125 | memset(&entity, 0x00, sizeof(entity)); 126 | entity.id = "temperature"; 127 | entity.name = "Solar inverter temperature"; 128 | entity.type = ha_sensor; 129 | entity.stat_t = "feeds/float/%s/temperature"; 130 | entity.alt_name = current_config.aps_mqttpath; 131 | entity.unit_of_meas = "°C"; 132 | entity.dev_class = "temperature"; 133 | ha_add(&entity); 134 | 135 | memset(&entity, 0x00, sizeof(entity)); 136 | entity.id = "ecu_serial"; 137 | entity.name = "Solar gateway serial number"; 138 | entity.type = ha_sensor; 139 | entity.stat_t = "feeds/string/%s/ecu_serial"; 140 | entity.alt_name = current_config.aps_mqttpath; 141 | ha_add(&entity); 142 | 143 | memset(&entity, 0x00, sizeof(entity)); 144 | entity.id = "ecu_firmware"; 145 | entity.name = "Solar gateway firmware"; 146 | entity.type = ha_sensor; 147 | entity.stat_t = "feeds/string/%s/ecu_firmware"; 148 | entity.alt_name = current_config.aps_mqttpath; 149 | ha_add(&entity); 150 | 151 | memset(&entity, 0x00, sizeof(entity)); 152 | entity.id = "inverter_serial"; 153 | entity.name = "Solar inverter serial number"; 154 | entity.type = ha_sensor; 155 | entity.stat_t = "feeds/string/%s/inverter_serial"; 156 | entity.alt_name = current_config.aps_mqttpath; 157 | ha_add(&entity); 158 | 159 | memset(&entity, 0x00, sizeof(entity)); 160 | entity.id = "inverter_unknown"; 161 | entity.name = "Solar inverter unknown info"; 162 | entity.type = ha_sensor; 163 | entity.stat_t = "feeds/string/%s/inverter_unknown"; 164 | entity.alt_name = current_config.aps_mqttpath; 165 | ha_add(&entity); 166 | 167 | memset(&entity, 0x00, sizeof(entity)); 168 | entity.id = "inverter_model"; 169 | entity.name = "Solar inverter model"; 170 | entity.type = ha_sensor; 171 | entity.stat_t = "feeds/string/%s/inverter_model"; 172 | entity.alt_name = current_config.aps_mqttpath; 173 | ha_add(&entity); 174 | 175 | memset(&entity, 0x00, sizeof(entity)); 176 | entity.id = "timestamp"; 177 | entity.name = "Solar gateway sync timestamp"; 178 | entity.type = ha_sensor; 179 | entity.stat_t = "feeds/string/%s/timestamp"; 180 | entity.alt_name = current_config.aps_mqttpath; 181 | ha_add(&entity); 182 | } 183 | 184 | bool aps_request(uint32_t command, uint8_t *payload, uint32_t payload_length, uint8_t *response, uint32_t *length) 185 | { 186 | uint32_t time = millis(); 187 | WiFiClient aps_client; 188 | 189 | aps_publish_string((char *)"status", "connecting"); 190 | Serial.printf("[APS] connecting to %s\n", current_config.aps_hostname); 191 | if (!aps_client.connect(current_config.aps_hostname, 8899)) 192 | { 193 | aps_publish_string((char *)"status", "connection failed"); 194 | Serial.println("[APS] connection failed"); 195 | return false; 196 | } 197 | Serial.printf("[APS] connected\n"); 198 | aps_client.setTimeout(100); 199 | aps_client.setNoDelay(true); 200 | 201 | uint8_t buffer[128]; 202 | uint32_t request_len = 3 + 2 + 4 + 4 + payload_length + 3; 203 | 204 | sprintf((char *)buffer, "APS%02d%04d%04d", 11, request_len, command); 205 | if (payload && payload_length > 0) 206 | { 207 | memcpy(&buffer[13], payload, payload_length); 208 | } 209 | memcpy(&buffer[13 + payload_length], "END", 3); 210 | 211 | buffer[request_len] = 0; 212 | 213 | Serial.printf("[APS] sending: '%s'\n", (char *)buffer); 214 | aps_client.write(buffer, request_len); 215 | aps_client.flush(); 216 | 217 | while (aps_client.available() < 5) 218 | { 219 | if (millis() - time > 1000) 220 | { 221 | aps_publish_string((char *)"status", "timeout in header receive"); 222 | Serial.println("[APS] timeout in header receive"); 223 | aps_client.stop(); 224 | return false; 225 | } 226 | } 227 | 228 | memset(buffer, 0x00, sizeof(buffer)); 229 | 230 | if (aps_client.read(buffer, 5) != 5) 231 | { 232 | aps_publish_string((char *)"status", "no header received"); 233 | Serial.println("[APS] no header received"); 234 | aps_client.stop(); 235 | return false; 236 | } 237 | 238 | if (memcmp(buffer, "APS11", 5)) 239 | { 240 | aps_publish_string((char *)"status", "no APS11 header"); 241 | Serial.println("[APS] no APS11 header"); 242 | aps_client.stop(); 243 | return false; 244 | } 245 | 246 | uint32_t response_length = 0; 247 | uint32_t response_command = 0; 248 | 249 | if (aps_client.read(buffer, 4) != 4) 250 | { 251 | aps_publish_string((char *)"status", "no length received"); 252 | Serial.println("[APS] no length received"); 253 | aps_client.stop(); 254 | return false; 255 | } 256 | buffer[4] = 0; 257 | sscanf((char *)buffer, "%04lu", &response_length); 258 | 259 | if (aps_client.read(buffer, 4) != 4) 260 | { 261 | aps_publish_string((char *)"status", "no command received"); 262 | Serial.println("[APS] no command received"); 263 | aps_client.stop(); 264 | return false; 265 | } 266 | buffer[4] = 0; 267 | sscanf((char *)buffer, "%04lu", &response_command); 268 | 269 | *length = response_length - 16; 270 | 271 | if (aps_client.read(buffer, *length + 3) != *length + 3) 272 | { 273 | aps_publish_string((char *)"status", "no payload received"); 274 | Serial.println("[APS] no payload received"); 275 | aps_client.stop(); 276 | return false; 277 | } 278 | 279 | memcpy(response, buffer, *length); 280 | 281 | aps_client.stop(); 282 | 283 | return true; 284 | } 285 | 286 | void aps_publish_int(const char *name, uint32_t value) 287 | { 288 | char path_buffer[128]; 289 | 290 | if (strlen(current_config.aps_mqttpath) > 0) 291 | { 292 | sprintf(path_buffer, "feeds/integer/%s/%s", current_config.aps_mqttpath, name); 293 | 294 | mqtt_publish_int_plain(path_buffer, value); 295 | } 296 | } 297 | 298 | void aps_publish_float(const char *name, float value) 299 | { 300 | char path_buffer[128]; 301 | 302 | if (strlen(current_config.aps_mqttpath) > 0) 303 | { 304 | sprintf(path_buffer, "feeds/float/%s/%s", current_config.aps_mqttpath, name); 305 | 306 | mqtt_publish_float_plain(path_buffer, value); 307 | } 308 | } 309 | 310 | void aps_publish_string(const char *name, const char *value) 311 | { 312 | char path_buffer[128]; 313 | 314 | if (strlen(current_config.aps_mqttpath) > 0) 315 | { 316 | sprintf(path_buffer, "feeds/string/%s/%s", current_config.aps_mqttpath, name); 317 | 318 | mqtt_publish_string_plain(path_buffer, value); 319 | } 320 | } 321 | 322 | bool aps_fetch() 323 | { 324 | Serial.println("[APS] requesting general info"); 325 | uint8_t buffer[128]; 326 | uint32_t length = 0; 327 | 328 | if (!aps_request(1, NULL, 0, buffer, &length)) 329 | { 330 | Serial.println("[APS] failed"); 331 | return false; 332 | } 333 | 334 | if (length < 78) 335 | { 336 | aps_publish_string((char *)"status", "small payload received 1"); 337 | Serial.printf("[APS] payload with only %d bytes\n", length); 338 | return false; 339 | } 340 | 341 | t_ecuinfo *infos = (t_ecuinfo *)buffer; 342 | 343 | aps_energy_lifetime = htonl(infos->energy_lifetime) * 100.0f; 344 | aps_power_current = htonl(infos->power_current); 345 | aps_energy_day = htonl(infos->energy_day) * 10.0f; 346 | sprintf(aps_channel, "%c%c", infos->ecu_channel[0], infos->ecu_channel[1]); 347 | 348 | memcpy(aps_ecu_serial, infos->ecuid, 12); 349 | aps_ecu_serial[12] = 0; 350 | 351 | Serial.printf(" ECU-ID: %s\n", aps_ecu_serial); 352 | Serial.printf(" energy_lifetime: %f Wh\n", aps_energy_lifetime); 353 | Serial.printf(" power_current: %f W\n", aps_power_current); 354 | Serial.printf(" energy_day: %f Wh\n", aps_energy_day); 355 | 356 | char *strings = (char *)&infos[1]; 357 | int version_len = 0; 358 | char buf[4]; 359 | 360 | strncpy(buf, strings, 3); 361 | buf[3] = 0; 362 | version_len = atoi(buf); 363 | strncpy(aps_ecu_firmware, &strings[3], version_len); 364 | aps_ecu_firmware[version_len] = 0; 365 | Serial.printf(" ECU-Version: %s\n", aps_ecu_firmware); 366 | 367 | /* detailed request */ 368 | Serial.println("[APS] requesting detailed info"); 369 | 370 | if (!aps_request(2, (uint8_t *)aps_ecu_serial, 12, buffer, &length)) 371 | { 372 | Serial.println("[APS] failed"); 373 | return false; 374 | } 375 | if (length < 34) 376 | { 377 | aps_publish_string((char *)"status", "small payload received 2"); 378 | Serial.printf("[APS] payload with only %d bytes\n", length); 379 | return false; 380 | } 381 | 382 | t_ecudetailed *detailed = (t_ecudetailed *)buffer; 383 | 384 | aps_frequency = htons(detailed->inverter.frequency) / 10.0f; 385 | aps_temperature = htons(detailed->inverter.temperature) - 100.0f; 386 | aps_power_a = htons(detailed->inverter.power_a); 387 | aps_voltage_a = htons(detailed->inverter.voltage_a) / 10.0f; 388 | aps_power_b = htons(detailed->inverter.power_b); 389 | aps_voltage_b = htons(detailed->inverter.voltage_b) / 10.0f; 390 | 391 | Serial.printf(" frequency: %f Hz\n", aps_frequency); 392 | Serial.printf(" temperature: %f °C\n", aps_temperature); 393 | Serial.printf(" power_a: %f W\n", aps_power_a); 394 | Serial.printf(" voltage_a: %f V\n", aps_voltage_a); 395 | Serial.printf(" power_b: %f W\n", aps_power_b); 396 | Serial.printf(" voltage_b: %f V\n", aps_voltage_b); 397 | 398 | strcpy(aps_inv_unk, ""); 399 | for (int pos = 0; pos < sizeof(detailed->inverter.unknown); pos++) 400 | { 401 | char buf[3]; 402 | sprintf(buf, "%02X", detailed->inverter.unknown[pos]); 403 | strcat(aps_inv_unk, buf); 404 | } 405 | Serial.printf(" unknown: %s\n", aps_inv_unk); 406 | 407 | sprintf(aps_inv_model, "%02X", detailed->inverter.model); 408 | Serial.printf(" model: 0x%02X\n", detailed->inverter.model); 409 | 410 | strcpy(aps_timestamp, ""); 411 | for (int pos = 0; pos < sizeof(detailed->inverter.timestamp); pos++) 412 | { 413 | char buf[3]; 414 | sprintf(buf, "%02X", detailed->inverter.timestamp[pos]); 415 | strcat(aps_timestamp, buf); 416 | } 417 | 418 | strcpy(aps_timestamp_mqtt, ""); 419 | strncat(aps_timestamp_mqtt, &aps_timestamp[0], 4); 420 | strcat(aps_timestamp_mqtt, "-"); 421 | strncat(aps_timestamp_mqtt, &aps_timestamp[4], 2); 422 | strcat(aps_timestamp_mqtt, "-"); 423 | strncat(aps_timestamp_mqtt, &aps_timestamp[6], 2); 424 | strcat(aps_timestamp_mqtt, " "); 425 | strncat(aps_timestamp_mqtt, &aps_timestamp[8], 2); 426 | strcat(aps_timestamp_mqtt, ":"); 427 | strncat(aps_timestamp_mqtt, &aps_timestamp[10], 2); 428 | strcat(aps_timestamp_mqtt, ":"); 429 | strncat(aps_timestamp_mqtt, &aps_timestamp[12], 2); 430 | 431 | Serial.printf(" timestamp: %s\n", aps_timestamp_mqtt); 432 | 433 | strcpy(aps_inv_serial, ""); 434 | for (int pos = 0; pos < sizeof(detailed->inverter.uid); pos++) 435 | { 436 | char buf[3]; 437 | sprintf(buf, "%02X", detailed->inverter.uid[pos]); 438 | strcat(aps_inv_serial, buf); 439 | } 440 | Serial.printf(" uid: %s\n", aps_inv_serial); 441 | 442 | return true; 443 | } 444 | 445 | bool aps_loop() 446 | { 447 | uint32_t time = millis(); 448 | static int nextTime = 5000; 449 | 450 | if (time >= nextTime) 451 | { 452 | if (aps_fetch()) 453 | { 454 | aps_publish_float((char *)"energy_lifetime", aps_energy_lifetime); 455 | aps_publish_float((char *)"power_current", aps_power_current); 456 | aps_publish_float((char *)"energy_day", aps_energy_day); 457 | aps_publish_float((char *)"frequency", aps_frequency); 458 | aps_publish_float((char *)"temperature", aps_temperature); 459 | aps_publish_float((char *)"power_a", aps_power_a); 460 | aps_publish_float((char *)"voltage_a", aps_voltage_a); 461 | aps_publish_float((char *)"power_b", aps_power_b); 462 | aps_publish_float((char *)"voltage_b", aps_voltage_b); 463 | aps_publish_string((char *)"ecu_serial", aps_ecu_serial); 464 | aps_publish_string((char *)"ecu_firmware", aps_ecu_firmware); 465 | aps_publish_string((char *)"inverter_serial", aps_inv_serial); 466 | aps_publish_string((char *)"inverter_unknown", aps_inv_unk); 467 | aps_publish_string((char *)"inverter_model", aps_inv_model); 468 | aps_publish_string((char *)"timestamp", aps_timestamp_mqtt); 469 | aps_publish_string((char *)"channel", aps_channel); 470 | } 471 | nextTime = time + 5 * 60000; 472 | } 473 | 474 | return false; 475 | } 476 | -------------------------------------------------------------------------------- /src/WWW.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | extern std::list ReceiverLastReceived; 10 | extern char wifi_error[]; 11 | extern bool wifi_captive; 12 | 13 | WebServer webserver(80); 14 | 15 | int www_wifi_scanned = -1; 16 | uint32_t www_last_captive = 0; 17 | 18 | void www_setup() 19 | { 20 | webserver.on("/", www_handle_root); 21 | webserver.on("/index.html", www_handle_index); 22 | webserver.on("/set_parm", www_handle_set_parm); 23 | webserver.on("/ota", www_handle_ota); 24 | webserver.on("/reset", www_handle_reset); 25 | webserver.onNotFound(www_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 www_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 www_handle_index() 128 | { 129 | webserver.send(200, "text/html", www_send_html()); 130 | } 131 | 132 | bool www_loop() 133 | { 134 | webserver.handleClient(); 135 | return false; 136 | } 137 | 138 | void www_handle_root() 139 | { 140 | webserver.send(200, "text/html", www_send_html()); 141 | } 142 | 143 | void www_handle_ota() 144 | { 145 | ota_setup(); 146 | webserver.send(200, "text/html", www_send_html()); 147 | } 148 | 149 | void www_handle_reset() 150 | { 151 | webserver.send(200, "text/html", www_send_html()); 152 | ESP.restart(); 153 | } 154 | 155 | void www_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 | return; 263 | } 264 | 265 | current_config.verbose = 0; 266 | current_config.verbose |= (webserver.arg("verbose_c0") != "") ? 1 : 0; 267 | current_config.verbose |= (webserver.arg("verbose_c1") != "") ? 2 : 0; 268 | current_config.verbose |= (webserver.arg("verbose_c2") != "") ? 4 : 0; 269 | current_config.verbose |= (webserver.arg("verbose_c3") != "") ? 8 : 0; 270 | current_config.verbose |= (webserver.arg("verbose_c4") != "") ? 16 : 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 | strncpy(current_config.mqtt_filter, webserver.arg("mqtt_filter").c_str(), sizeof(current_config.mqtt_filter)); 287 | 288 | strncpy(current_config.aps_hostname, webserver.arg("aps_hostname").c_str(), sizeof(current_config.aps_hostname)); 289 | strncpy(current_config.aps_mqttpath, webserver.arg("aps_mqttpath").c_str(), sizeof(current_config.aps_mqttpath)); 290 | 291 | cfg_save(); 292 | 293 | if (current_config.verbose) 294 | { 295 | Serial.printf("Config:\n"); 296 | Serial.printf(" mqtt_publish: %d %%\n", current_config.mqtt_publish); 297 | Serial.printf(" verbose: %d\n", current_config.verbose); 298 | } 299 | 300 | if (webserver.arg("reboot") == "true") 301 | { 302 | webserver.send(200, "text/html", "

Saved. Rebooting...

(will refresh page in 5 seconds)"); 303 | delay(500); 304 | ESP.restart(); 305 | return; 306 | } 307 | 308 | if (webserver.arg("scan") == "true") 309 | { 310 | www_wifi_scanned = WiFi.scanNetworks(); 311 | } 312 | webserver.send(200, "text/html", www_send_html()); 313 | www_wifi_scanned = -1; 314 | } 315 | 316 | String www_send_html() 317 | { 318 | char buf[1024]; 319 | 320 | www_activity(); 321 | 322 | String ptr = " \n"; 323 | ptr += "\n"; 324 | 325 | sprintf(buf, "" CONFIG_OTANAME " Control\n"); 326 | 327 | ptr += buf; 328 | ptr += "\n"; 352 | ptr += "\n"; 353 | ptr += "\n"; 354 | ptr += "\n"; 355 | 356 | sprintf(buf, "

" CONFIG_OTANAME "

\n"); 357 | ptr += buf; 358 | 359 | sprintf(buf, "

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

\n"); 360 | ptr += buf; 361 | 362 | if (strlen(wifi_error) != 0) 363 | { 364 | sprintf(buf, "

WiFi Error: %s

\n", wifi_error); 365 | ptr += buf; 366 | } 367 | 368 | if (!ota_enabled()) 369 | { 370 | ptr += "[Enable OTA] "; 371 | } 372 | sprintf(buf, ""); 373 | ptr += buf; 374 | ptr += "

\n"; 375 | 376 | ptr += "
\n"; 377 | ptr += ""; 378 | 379 | #define ADD_CONFIG(name, value, fmt, desc) \ 380 | do \ 381 | { \ 382 | ptr += ""; \ 383 | sprintf(buf, "\n", value); \ 384 | ptr += buf; \ 385 | } while (0) 386 | 387 | #define ADD_CONFIG_CHECK4(name, value, fmt, desc, text0, text1, text2, text3) \ 388 | do \ 389 | { \ 390 | ptr += "\n"); \ 408 | ptr += buf; \ 409 | } while (0) 410 | 411 | #define ADD_CONFIG_CHECK5(name, value, fmt, desc, text0, text1, text2, text3, text4) \ 412 | do \ 413 | { \ 414 | 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("mqtt_filter", current_config.mqtt_filter, "%s", "Sensors to publish (space separated list of MQTT IDs)"); 489 | ADD_CONFIG_CHECK5("verbose", current_config.verbose, "%d", "Verbosity", "Serial", "_", "_", "_", "_"); 490 | ADD_CONFIG_CHECK4("mqtt_publish", current_config.mqtt_publish, "%d", "MQTT publishes", "RF messages", "Debug", "_", "_"); 491 | ADD_CONFIG("http_update", "", "%s", "Update URL (Release)"); 492 | 493 | ADD_CONFIG("aps_hostname", current_config.aps_hostname, "%s", "APS ECU address"); 494 | ADD_CONFIG("aps_mqttpath", current_config.aps_mqttpath, "%s", "MQTT path for APS ECU reports"); 495 | 496 | ptr += "
" desc ":
"; \ 391 | sprintf(buf, "\n", (value & 1) ? "checked" : ""); \ 392 | ptr += buf; \ 393 | sprintf(buf, "\n"); \ 394 | ptr += buf; \ 395 | sprintf(buf, "\n", (value & 2) ? "checked" : ""); \ 396 | ptr += buf; \ 397 | sprintf(buf, "\n"); \ 398 | ptr += buf; \ 399 | sprintf(buf, "\n", (value & 4) ? "checked" : ""); \ 400 | ptr += buf; \ 401 | sprintf(buf, "\n"); \ 402 | ptr += buf; \ 403 | sprintf(buf, "\n", (value & 8) ? "checked" : ""); \ 404 | ptr += buf; \ 405 | sprintf(buf, "\n"); \ 406 | ptr += buf; \ 407 | sprintf(buf, "
" desc ":
"; \ 415 | sprintf(buf, "\n", (value & 1) ? "checked" : ""); \ 416 | ptr += buf; \ 417 | sprintf(buf, "\n"); \ 418 | ptr += buf; \ 419 | sprintf(buf, "\n", (value & 2) ? "checked" : ""); \ 420 | ptr += buf; \ 421 | sprintf(buf, "\n"); \ 422 | ptr += buf; \ 423 | sprintf(buf, "\n", (value & 4) ? "checked" : ""); \ 424 | ptr += buf; \ 425 | sprintf(buf, "\n"); \ 426 | ptr += buf; \ 427 | sprintf(buf, "\n", (value & 8) ? "checked" : ""); \ 428 | ptr += buf; \ 429 | sprintf(buf, "\n"); \ 430 | ptr += buf; \ 431 | sprintf(buf, "\n", (value & 16) ? "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"; 497 | 498 | ptr += "

Captured signals

\n"; 499 | ptr += ""; 518 | 519 | for (auto it = ReceiverLastReceived.begin(); it != ReceiverLastReceived.end(); it++) 520 | { 521 | const char *tmp_str = strdup(it->second); 522 | 523 | DynamicJsonBuffer json(512); 524 | JsonObject &data = json.parseObject(tmp_str); 525 | 526 | const char *model = data["model"]; 527 | const char *protocol = data["protocol"]; 528 | int id = data["id"]; 529 | int channel = data["channel"]; 530 | float temp = data["temperature_C"]; 531 | float humidity = data["humidity"]; 532 | float rain = data["rain"]; 533 | int rssi = data["rssi"]; 534 | 535 | ptr += ""; 556 | 557 | free((void *)tmp_str); 558 | } 559 | ptr += "
"; 500 | ptr += "MQTT ID"; 501 | ptr += ""; 502 | ptr += "Model"; 503 | ptr += ""; 504 | ptr += "ID"; 505 | ptr += ""; 506 | ptr += "Channel"; 507 | ptr += ""; 508 | ptr += "Temperature"; 509 | ptr += ""; 510 | ptr += "Humidity"; 511 | ptr += ""; 512 | ptr += "Rain"; 513 | ptr += ""; 514 | ptr += "Protocol"; 515 | ptr += ""; 516 | ptr += "RSSI"; 517 | ptr += "
first; 537 | ptr += "');\">"; 538 | ptr += it->first; 539 | ptr += ""; 540 | ptr += model; 541 | ptr += ""; 542 | ptr += id; 543 | ptr += ""; 544 | ptr += channel; 545 | ptr += ""; 546 | ptr += temp; 547 | ptr += ""; 548 | ptr += humidity; 549 | ptr += ""; 550 | ptr += rain; 551 | ptr += ""; 552 | ptr += protocol; 553 | ptr += ""; 554 | ptr += rssi; 555 | ptr += " dBm
"; 560 | 561 | ptr += "\n"; 562 | ptr += "\n"; 563 | return ptr; 564 | } 565 | --------------------------------------------------------------------------------