From 80ec2c20b88611f9b2a42c38e98d3dca56596f8b Mon Sep 17 00:00:00 2001 From: Alexey Zholtikov Date: Wed, 28 Dec 2022 21:44:16 +0300 Subject: [PATCH] Version 1.0 Initial version. --- .gitignore | 3 + README.md | 40 +++- data/function.js | 64 ++++++ data/index.htm | 66 ++++++ data/style.css | 70 +++++++ main.cpp | 356 ++++++++++++++++++++++++++++++++ platformio.ini | 49 +++++ src/main.cpp | 520 +++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 1167 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100755 data/function.js create mode 100644 data/index.htm create mode 100644 data/style.css create mode 100644 main.cpp create mode 100644 platformio.ini create mode 100644 src/main.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..40433be --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.pio +.vscode +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index 2b3135d..95e4b07 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,40 @@ -# ESP-NOW-Gateway +# ESP-NOW gateway for ESP8266/ESP32 +Gateway for data exchange between ESP-NOW devices and MQTT broker via WiFi. + +## Features + +1. The first time turn on (or after rebooting) creates an access point named "ESP-NOW Gateway XXXXXXXXXXXX" with password "12345678" (IP 192.168.4.1) if fails to connect to WiFi. In case of lost a WiFi connection after successfuly connection search the required WiFi SSID availability every 30 seconds. +2. Possibility a device search through the Windows Network Environment via SSDP. +3. Periodically transmission of system information to the MQTT broker (every 60 seconds) and availability status to the ESP-NOW network and to the MQTT broker (every 10 seconds). +4. Automatically adds gateway configuration to Home Assistan via MQTT discovery as a binary_sensor. +5. Possibility firmware update over OTA. +6. Web interface for settings. + +## Notes + +1. ESP-NOW mesh network based on the library [ZHNetwork](https://github.com/aZholtikov/ZHNetwork). +2. Regardless of the status of connections to WiFi or MQTT the device perform ESP-NOW node function. +3. For restart the device (without using the Web interface and only if MQTT connection established) send an "restart" command to the device's root topic (example - "homeassistant/gateway/70039F44BEF7"). + +## Attention + +1. ESP-NOW network name must be set same of all another ESP-NOW devices in network. +2. Upload the "data" folder (with web interface) into the filesystem before flashing. +3. WiFi router must be set on channel 1. + +## Supported devices + +1. [RF - Gateway](https://github.com/aZholtikov/RF-Gateway) +2. [ESP-NOW Switch](https://github.com/aZholtikov/ESP-NOW-Switch) +3. [ESP-NOW Led Light/Strip](https://github.com/aZholtikov/ESP-NOW-Led-Light-Strip) +4. [ESP-NOW Window/Door Sensor](https://github.com/aZholtikov/ESP-NOW-Window-Door-Sensor) +5. [ESP-NOW Water Leakage Sensor](https://github.com/aZholtikov/ESP-NOW-Water-Leakage-Sensor) + +## To Do + +- [ ] Automatically add ESP-NOW devices configurations to Home Assistan via MQTT discovery. +- [ ] LAN connection support. +- [ ] nRF24 device support (in current time uses "RF Gateway"). +- [ ] BLE device support (for ESP32). +- [ ] LoRa device support. diff --git a/data/function.js b/data/function.js new file mode 100755 index 0000000..221ec8a --- /dev/null +++ b/data/function.js @@ -0,0 +1,64 @@ +var xmlHttp = createXmlHttpObject(); +function createXmlHttpObject() { + if (window.XMLHttpRequest) { + xmlHttp = new XMLHttpRequest(); + } else { + xmlHttp = new ActiveXObject('Microsoft.XMLHTTP'); + } + return xmlHttp; +} + +function load() { + if (xmlHttp.readyState == 0 || xmlHttp.readyState == 4) { + xmlHttp.open('PUT', '/config.json', true); + xmlHttp.send(null); + xmlHttp.onload = function () { + jsonResponse = JSON.parse(xmlHttp.responseText); + loadBlock(); + } + } +} + +function loadBlock() { + newData = JSON.parse(xmlHttp.responseText); + data = document.getElementsByTagName('body')[0].innerHTML; + var newString; + for (var key in newData) { + newString = data.replace(new RegExp('{{' + key + '}}', 'g'), newData[key]); + data = newString; + } + document.getElementsByTagName('body')[0].innerHTML = newString; + setFirmvareValue('version', 'firmware'); + handleServerResponse(); +} + +function getValue(id) { + var value = document.getElementById(id).value; + return value; +} + +function sendRequest(submit, server) { + request = new XMLHttpRequest(); + request.open("GET", server, true); + request.send(); +} + +function saveSetting(submit) { + server = "/setting?ssid=" + getValue('ssid') + "&password=" + encodeURIComponent(getValue('password')) + + "&host=" + getValue('mqttHostName') + "&port=" + getValue('mqttHostPort') + + "&login=" + getValue('mqttUserLogin') + "&pass=" + encodeURIComponent(getValue('mqttUserPassword')) + + "&prefix=" + getValue('topicPrefix') + + "&name=" + getValue('deviceName') + + "&net=" + getValue('espnowNetName'); + sendRequest(submit, server); + alert("Please restart device for changes apply."); +} + +function restart(submit) { + server = "/restart"; + sendRequest(submit, server); +} + +function setFirmvareValue(id, value) { + document.getElementById(id).innerHTML = document.getElementById(value).value; +} diff --git a/data/index.htm b/data/index.htm new file mode 100644 index 0000000..e428703 --- /dev/null +++ b/data/index.htm @@ -0,0 +1,66 @@ + + + + + + + + ESP-NOW Gateway + + + +
+

ESP-NOW Gateway

+
+

Firmware:

+

+ +
+ +
+

Device name:

+ +
+ +
+

ESP-NOW network name:

+ +
+ +

WiFi settings

+
+ + +
+ +

MQTT settings

+
+ + +
+ +
+ + +
+ +
+

MQTT topic prefix:

+ +
+ +
+ + +
+
+ + + \ No newline at end of file diff --git a/data/style.css b/data/style.css new file mode 100644 index 0000000..7a231e5 --- /dev/null +++ b/data/style.css @@ -0,0 +1,70 @@ +body { + font-family: "Gill Sans", sans-serif; + background: rgb(255, 255, 255); +} + +.box { + width: 400px; + padding: 20px 20px; + margin: 20px auto; + background: #e0f5fb; + box-shadow: 4px 4px 30px rgba(0, 0, 0, 0.2); + border-radius: 10px; +} + +h1 { + color: rgb(65, 125, 238); + text-align: center; +} + +.text { + font-weight: 600; + flex-shrink: 0; + margin-right: 10px; +} + +.wrapper { + display: flex; + justify-content: space-between; + align-items: baseline; +} + +input { + width: 48%; + min-height: 30px; + border-radius: 5px; + border: none; + margin-bottom: 10px; + padding: 0 10px; + color: rgb(0, 0, 0); + background: #a3e0f1; + transition: .5s; +} + +input:hover { + background: white; + cursor: pointer; +} + +.btn { + width: 48%; + background: rgb(65, 125, 238); + color: white; + transition: .5s; +} + +.btn:hover { + background: rgb(65, 125, 238); + opacity: 0.5; + transform: translatey(-3px); +} + +#deviceName, +#espnowNetName, +#topicPrefix { + width: 100%; +} + +.wrapper.wrapper--end { + align-items: baseline; +} \ No newline at end of file diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..4962dbf --- /dev/null +++ b/main.cpp @@ -0,0 +1,356 @@ +#include +#include +#include +#include +#include +#include "ArduinoJson.h" // Версия 5. С другой не работает. +#include +#include +#include +#include +#include +#include "AsyncTelegram.h" + +//*******************************************************************************************************************************// +String ssidStaName = "ZH-SMART"; // Имя Wi-Fi сети. +String passwordSta = "Firan1978"; // Пароль Wi-Fi сети. +String ssidApName = "ESP32"; // Имя точки доступа. +String passwordAp = ""; // Пароль точки доступа. + +String deviceName = "Smart Home Controller"; +String modelName = "Smart Home Controller"; +String modelNumber = "00000003"; +String serialNumber = "00000001"; +String uuid = "3c1b475a-e586-40e9-8605-f818f0ad5891"; + +IPAddress IP(192, 168, 4, 1); // IP адрес точки доступа. +String mqttHostName = "mqtt.zh.com.ru"; // Адрес MQTT сервера. +uint mqttHostPort = 1883; // Порт MQTT сервера. +String mqttUserLogin = ""; +String mqttUserPassword = ""; + +String token = "1471595796:AAGZPvrk8fa6-KaV0oTaveuPXMzA-_3Ql9U"; // Telegram токен. +ulong userID = 1472083376; + +String controllerTopicHead = "Квартира/Контроллеры/Шлюз"; + +bool countertop_lighting_status_Kitchen; + +bool heating_battery_1_valve_status_Living_Room; +int heating_battery_1_temperature_Living_Room; +//*******************************************************************************************************************************// + +//*******************************************************************************************************************************// +void loadNetConfigFile(void); // Функция загрузки конфигурации из файла netconfig.json. +void saveNetConfigFile(void); // Функция записи конфигурации в файл netconfig.json. +void setupSsdp(void); // Функция настройки протокола SSDP. +void setupWebServer(void); // Функция настройки WEB сервера. +void connectToWiFi(void); // Функция подключения к Wi-Fi сети. +void connectToMqtt(void); // Функция подключения к MQTT серверу. +void reboot(void); // Функция перезагрузки при работе в режиме точки доступа. + +String processor(const String &var); // Функция обработки HTTP запросов. +String xmlNode(String tags, String data); // Функция "сборки" информационного SSDP файла. +String decToHex(uint32_t decValue, byte desiredStringLength); // Функция перевода в шестнадцатеричную систему. + +void onMqttConnect(bool sessionPresent); // Событие. Если выполнено подключение к MQTT серверу. +void onMqttDisconnect(AsyncMqttClientDisconnectReason reason); // Событие. Если произошло отключение от MQTT сервера. +void onMqttMessage(char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total); // Событие. Если получен топик. +//*******************************************************************************************************************************// + +//*******************************************************************************************************************************// +AsyncWebServer server(80); // Создаём объект server для работы с библиотекой ESPAsyncWebServer. +AsyncMqttClient mqttClient; // Создаём объект mqttClient для работы с библиотекой AsyncMqttClient. +WiFiClient client; // Создаём объект client для работы с библиотекой WiFi. +AsyncTelegram myBot; // Создаём объект myBot для работы с библиотекой AsyncTelegram. + +Ticker mqttReconnectTimer; // Создаём таймер переподключения к MQTT серверу (при потере соединения). +Ticker apModeRebootTimer; // Создаём таймер перезагрузки при работе в режиме точки доступа. +//*******************************************************************************************************************************// + +void setup() +{ + SPIFFS.begin(); // Инициируем работу файловой системы. + Serial.begin(115200); // Инициируем работу SERIAL. + + mqttClient.onConnect(onMqttConnect); // Включаем обработчик события подключения к MQTT серверу. + mqttClient.onDisconnect(onMqttDisconnect); // Включаем обработчик события отключения от MQTT сервера. + mqttClient.onMessage(onMqttMessage); // Включаем обработчик события получения топика. + mqttClient.setServer(mqttHostName.c_str(), mqttHostPort); // Устанавливаем параметры подключения к MQTT серверу. + mqttClient.setCredentials(mqttUserLogin.c_str(), mqttUserPassword.c_str()); + + saveNetConfigFile(); + loadNetConfigFile(); // Загружаем конфигурацию из файла netconfig.json. + connectToWiFi(); // Подключаемся к Wi-Fi. + setupSsdp(); // Настраиваем протокол SSDP. + setupWebServer(); // Настраиваем WEB сервер. + connectToMqtt(); // Подключаемся к MQTT серверу. + + myBot.setTelegramToken(token.c_str()); + myBot.begin(); + + ArduinoOTA.begin(); // Запускаем сервер обновления "по воздуху". + + apModeRebootTimer.attach(300, reboot); // Запускаем таймер перезагрузки при работе в режиме точки доступа. +} + +void loop() +{ + ArduinoOTA.handle(); +} + +void loadNetConfigFile(void) // Функция загрузки конфигурации из файла netconfig.json. +{ + if (!SPIFFS.exists("/netconfig.json")) // Если файл не существует: + saveNetConfigFile(); // Создаем файл, записав в него данные по умолчанию. + File file = SPIFFS.open("/netconfig.json", "r"); // Открываем файл для чтения. + String jsonFile = file.readString(); // Читаем файл в переменную. + DynamicJsonDocument json(1024); // Резервируем память для JSON объекта. + deserializeJson(json, jsonFile); + ssidStaName = json["ssidStaName"].as(); // Читаем поля JSON. + passwordSta = json["passwordSta"].as(); + ssidApName = json["ssidApName"].as(); + passwordAp = json["passwordAp"].as(); + deviceName = json["deviceName"].as(); + mqttHostName = json["mqttHostName"].as(); + mqttHostPort = json["mqttHostPort"]; + mqttUserLogin = json["mqttUserLogin"].as(); + mqttUserPassword = json["mqttUserPassword"].as(); + token = json["token"].as(); + userID = json["userID"]; + file.close(); // Закрываем файл. +} + +void saveNetConfigFile(void) // Функция записи конфигурации в файл netconfig.json. +{ + DynamicJsonDocument json(1024); // Резервируем память для JSON объекта. + json["ssidStaName"] = ssidStaName; // Заполняем поля JSON. + json["passwordSta"] = passwordSta; + json["ssidApName"] = ssidApName; + json["passwordAp"] = passwordAp; + json["deviceName"] = deviceName; + json["mqttHostName"] = mqttHostName; + json["mqttHostPort"] = mqttHostPort; + json["mqttUserLogin"] = mqttUserLogin; + json["mqttUserPassword"] = mqttUserPassword; + json["token"] = token; + json["userID"] = userID; + File file = SPIFFS.open("/netconfig.json", "w"); // Открываем файл для записи. + serializeJsonPretty(json, file); // Записываем строку JSON в файл. + file.close(); // Закрываем файл. +} + +void setupSsdp(void) // Функция настройки протокола SSDP. +{ + SSDP.setSchemaURL("description.xml"); + SSDP.setDeviceType("upnp:rootdevice"); + + server.on("/description.xml", HTTP_GET, [](AsyncWebServerRequest *request) { + String ssdpSend = ""; + String ssdpHeder = xmlNode("major", "1"); + ssdpHeder += xmlNode("minor", "0"); + ssdpHeder = xmlNode("specVersion", ssdpHeder); + ssdpHeder += xmlNode("URLBase", "http://" + WiFi.localIP().toString()); + String ssdpDescription = xmlNode("deviceType", "upnp:rootdevice"); + ssdpDescription += xmlNode("friendlyName", deviceName); + ssdpDescription += xmlNode("presentationURL", "/"); + ssdpDescription += xmlNode("serialNumber", serialNumber); + ssdpDescription += xmlNode("modelName", modelName); + ssdpDescription += xmlNode("modelNumber", modelNumber); + ssdpDescription += xmlNode("modelURL", "http://zh.com.ru"); + ssdpDescription += xmlNode("manufacturer", "Alexey Zholtikov"); + ssdpDescription += xmlNode("manufacturerURL", "http://zh.com.ru"); + ssdpDescription += xmlNode("UDN", uuid); + ssdpDescription = xmlNode("device", ssdpDescription); + ssdpHeder += ssdpDescription; + ssdpSend += ssdpHeder; + ssdpSend += ""; + request->send(200, "text/xml", ssdpSend); + }); + + SSDP.begin(); // Запускаем протокол SSDP. +} + +void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final){ + if(!index){ + Serial.printf("UploadStart: %s\n", filename.c_str()); + } + for(size_t i=0; isend(SPIFFS, "/index.htm", String(), false, processor); + }); + + server.on("/heap", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", String(ESP.getFreeHeap())); + }); + + server.on( + "/upload", HTTP_POST, [](AsyncWebServerRequest *request) { + request->send(200); + }, + handleUpload); + + server.on("/restart", HTTP_GET, [](AsyncWebServerRequest *request) { // Перезагрузка модуля по запросу вида /restart?device=ok. + if (request->getParam("device")->value() == "ok") + { + request->send(200, "text/plain", "Reset OK"); + ESP.restart(); + } + else + { + request->send(200, "text/plain", "No Reset"); + } + }); + + server.on("/status", HTTP_GET, [](AsyncWebServerRequest *request) { // Получение статуса модуля. + request->send(200, "text/plain", "OK"); + }); + + server.on("/countertop_lighting_status_Kitchen", HTTP_GET, [](AsyncWebServerRequest *request) { // Получение статуса подсветки на кухне. + StaticJsonDocument<100> json; + json["value"] = countertop_lighting_status_Kitchen; + char buffer[100]; + serializeJson(json, buffer); + request->send(200, "text/json", buffer); + + }); + + server.on("/heating_battery_1_valve_status_Living_Room", HTTP_GET, [](AsyncWebServerRequest *request) { // Получение статуса клапана батареи №1 в гостиной. + StaticJsonDocument<100> json; + json["value"] = heating_battery_1_valve_status_Living_Room; + char buffer[100]; + serializeJson(json, buffer); + request->send(200, "text/json", buffer); + }); + + server.on("/heating_battery_1_temperature_Living_Room", HTTP_GET, [](AsyncWebServerRequest *request) { // Получение температуры батареи №1 в гостиной. + StaticJsonDocument<100> json; + json["value"] = heating_battery_1_temperature_Living_Room; + char buffer[100]; + serializeJson(json, buffer); + request->send(200, "text/json", buffer); + }); + + server.onNotFound([](AsyncWebServerRequest *request) { // Передача файлов на страницу. + if (SPIFFS.exists(request->url())) + request->send(SPIFFS, request->url(), String(), false); + else + { + request->send(404, "text/plain", "File Not Found"); + } + }); + server.onFileUpload(handleUpload); + server.begin(); // Запускаем WEB сервер. +} + +void connectToWiFi(void) // Функция подключения к Wi-Fi сети. +{ + WiFi.mode(WIFI_STA); // Устанавливаем режим работы (WIFI_STA - подключение к сети Wi-Fi). + byte tries = 10; // Счетчик количества попыток подключения. + WiFi.begin(ssidStaName.c_str(), passwordSta.c_str()); // Подключаемся к Wi-Fi сети. + while (tries-- && WiFi.status() != WL_CONNECTED) // Пытаемся подключиться к Wi-Fi сети. + { + delay(1000); // Пауза между попытками подключения. + } + if (WiFi.status() != WL_CONNECTED) // Если подключение не удалось: + { + WiFi.disconnect(); // Отключаем Wi-Fi. + WiFi.mode(WIFI_AP); // Устанавливаем режим работы (WIFI_AP - точка доступа). + WiFi.softAPConfig(IP, IP, IPAddress(255, 255, 255, 0)); // Задаем настройки сети. + WiFi.softAP(ssidApName.c_str(), passwordAp.c_str()); // Включаем Wi-Fi в режиме точки доступа. + } +} + +void connectToMqtt() // Функция подключения к MQTT серверу. +{ + mqttClient.connect(); +} + +void reboot(void) // Функция перезагрузки при работе в режиме точки доступа. +{ + if (WiFi.getMode() == WIFI_AP) + { + ESP.restart(); + } +} + +String processor(const String &var) // Функция обработки HTTP запросов. +{ + return String(); +} + +String xmlNode(String tags, String data) // Функция "сборки" информационного SSDP файла. +{ + String temp = "<" + tags + ">" + data + ""; + return temp; +} + +String decToHex(uint32_t decValue, byte desiredStringLength) // Функция перевода в шестнадцатеричную систему. +{ + String hexString = String(decValue, HEX); + while (hexString.length() < desiredStringLength) + hexString = "0" + hexString; + return hexString; +} + +void onMqttConnect(bool sessionPresent) // Если выполнено подключение к MQTT серверу: +{ + mqttReconnectTimer.detach(); // Отключаем таймер переподключения к MQTT серверу. + + mqttClient.publish(String(controllerTopicHead + "/IP").c_str(), 2, true, String(WiFi.localIP().toString()).c_str()); // Публикуем IP. + mqttClient.publish(String(controllerTopicHead + "/ID").c_str(), 2, true, decToHex(ESP.getEfuseMac(), 6).c_str()); // Публикуем Chip ID. + mqttClient.publish(String(controllerTopicHead + "/Статус").c_str(), 2, true, "Работает"); // Публикуем статус. + + mqttClient.subscribe("#", 2); // Подписываемся на все топики. +} + +void onMqttDisconnect(AsyncMqttClientDisconnectReason reason) // Если произошло отключение от MQTT сервера: +{ + mqttReconnectTimer.attach(10, connectToMqtt); // Включаем таймер переподключения к MQTT серверу. +} + +void onMqttMessage(char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) // Если получен топик: +{ + if (String(topic) == "Квартира/Освещение/Кухня/Подсветка столешницы/Состояние/Реле") + { + if (String(payload).substring(0, len) == "Вкл") + { + countertop_lighting_status_Kitchen = true; + return; + } + if (String(payload).substring(0, len) == "Выкл") + { + countertop_lighting_status_Kitchen = false; + return; + } + } + + if (String(topic) == "Квартира/Отопление/Гостиная/Батарея №1/Состояние/Клапан") + { + if (String(payload).substring(0, len) == "Открыт") + { + heating_battery_1_valve_status_Living_Room = true; + return; + } + if (String(payload).substring(0, len) == "Закрыт") + { + heating_battery_1_valve_status_Living_Room = false; + return; + } + } + + if (String(topic) == "Квартира/Отопление/Гостиная/Батарея №1/Состояние/Температура") + { + heating_battery_1_temperature_Living_Room = String(payload).substring(0, len).toInt(); + return; + } +} \ No newline at end of file diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..f48b1e3 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,49 @@ +[env:esp8266] +platform = espressif8266 +board = nodemcuv2 +framework = arduino +lib_deps = + https://github.com/aZholtikov/ZHNetwork + https://github.com/aZholtikov/ZHConfig + bblanchon/ArduinoJson@^6.19.4 + me-no-dev/ESP Async WebServer@^1.2.3 + marvinroger/AsyncMqttClient@^0.9.0 + +[env:esp8266-ota] +platform = espressif8266 +board = nodemcuv2 +framework = arduino +upload_port = 192.168.1.113 +upload_protocol = espota +lib_deps = + https://github.com/aZholtikov/ZHNetwork + https://github.com/aZholtikov/ZHConfig + bblanchon/ArduinoJson@^6.19.4 + me-no-dev/ESP Async WebServer@^1.2.3 + marvinroger/AsyncMqttClient@^0.9.0 + +[env:esp32] +platform = espressif32 +board = az-delivery-devkit-v4 +framework = arduino +lib_deps = + https://github.com/aZholtikov/ZHNetwork + https://github.com/aZholtikov/ZHConfig + bblanchon/ArduinoJson@^6.19.4 + me-no-dev/ESP Async WebServer@^1.2.3 + marvinroger/AsyncMqttClient@^0.9.0 + luc-github/ESP32SSDP@^1.2.0 + +[env:esp32-ota] +platform = espressif32 +board = az-delivery-devkit-v4 +framework = arduino +upload_port = 192.168.1.144 +upload_protocol = espota +lib_deps = + https://github.com/aZholtikov/ZHNetwork + https://github.com/aZholtikov/ZHConfig + bblanchon/ArduinoJson@^6.19.4 + me-no-dev/ESP Async WebServer@^1.2.3 + marvinroger/AsyncMqttClient@^0.9.0 + luc-github/ESP32SSDP@^1.2.0 diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..72d9cdd --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,520 @@ +#include "ArduinoJson.h" +#include "ArduinoOTA.h" +#include "ESPAsyncWebServer.h" +#include "AsyncMQTTClient.h" +#include "Ticker.h" +#include "ZHNetwork.h" +#include "ZHConfig.h" +#if defined(ESP8266) +#include "ESP8266SSDP.h" +#endif +#if defined(ESP32) +#include "SPIFFS.h" +#include "ESP32SSDP.h" +#endif + +void onWifiEvent(WiFiEvent_t event); + +void onEspnowMessage(const char *data, const uint8_t *sender); + +void onMqttConnect(bool sessionPresent); +void onMqttDisconnect(AsyncMqttClientDisconnectReason reason); +void onMqttMessage(char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total); + +void sendKeepAliveMessage(void); +void sendAttributesMessage(void); + +String getValue(String data, char separator, uint8_t index); + +void loadConfig(void); +void saveConfig(void); + +String xmlNode(String tags, String data); +void setupWebServer(void); + +void connectToMqtt(void); +void connectToWifi(void); + +const String firmware{"1.0"}; + +String espnowNetName{"DEFAULT"}; + +String deviceName{"ESP-NOW gateway"}; + +String ssid{"SSID"}; +String password{"PASSWORD"}; + +String mqttHostName{"MQTT"}; +uint16_t mqttHostPort{1883}; +String mqttUserLogin{""}; +String mqttUserPassword{""}; +String topicPrefix{"homeassistant"}; + +bool isWasConnectionToWifi{false}; + +ZHNetwork myNet; +AsyncWebServer webServer(80); +AsyncMqttClient mqttClient; + +Ticker wifiReconnectTimer; +bool wifiReconnectTimerSemaphore{false}; +void wifiReconnectTimerCallback(void); + +Ticker mqttReconnectTimer; +bool mqttReconnectTimerSemaphore{false}; +void mqttReconnectTimerCallback(void); + +Ticker keepAliveMessageTimer; +bool keepAliveMessageTimerSemaphore{false}; +void keepAliveMessageTimerCallback(void); + +Ticker attributesMessageTimer; +bool attributesMessageTimerSemaphore{false}; +void attributesMessageTimerCallback(void); + +void setup() +{ + WiFi.onEvent(onWifiEvent); + SPIFFS.begin(); + loadConfig(); +#if defined(ESP8266) + WiFi.setSleepMode(WIFI_NONE_SLEEP); +#endif +#if defined(ESP32) + WiFi.setSleep(WIFI_PS_NONE); +#endif + + WiFi.persistent(false); + WiFi.setAutoConnect(false); + WiFi.setAutoReconnect(false); + + myNet.begin(espnowNetName.c_str()); + + myNet.setOnBroadcastReceivingCallback(onEspnowMessage); + myNet.setOnUnicastReceivingCallback(onEspnowMessage); + + mqttClient.onConnect(onMqttConnect); + mqttClient.onDisconnect(onMqttDisconnect); + mqttClient.onMessage(onMqttMessage); + mqttClient.setServer(mqttHostName.c_str(), mqttHostPort); + mqttClient.setCredentials(mqttUserLogin.c_str(), mqttUserPassword.c_str()); + + connectToWifi(); + setupWebServer(); + + ArduinoOTA.begin(); + + sendKeepAliveMessage(); + keepAliveMessageTimer.attach(10, keepAliveMessageTimerCallback); +} + +void loop() +{ + if (wifiReconnectTimerSemaphore) + connectToWifi(); + if (mqttReconnectTimerSemaphore) + connectToMqtt(); + if (keepAliveMessageTimerSemaphore) + sendKeepAliveMessage(); + if (attributesMessageTimerSemaphore) + sendAttributesMessage(); + myNet.maintenance(); + ArduinoOTA.handle(); +} + +void onWifiEvent(WiFiEvent_t event) +{ + switch (event) + { +#if defined(ESP8266) + case WIFI_EVENT_STAMODE_DISCONNECTED: +#endif +#if defined(ESP32) + case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: +#endif + WiFi.mode(WIFI_OFF); // Without rebooting WiFi stops working ESP-NOW. + myNet.begin(espnowNetName.c_str()); + wifiReconnectTimer.attach(30, wifiReconnectTimerCallback); + break; + } +} + +void onEspnowMessage(const char *data, const uint8_t *sender) +{ + if (!mqttClient.connected()) + return; + esp_now_payload_data_t incomingData; + memcpy(&incomingData, data, sizeof(esp_now_payload_data_t)); + esp_now_payload_data_t jsonData; + memcpy(&jsonData.message, &incomingData.message, sizeof(esp_now_payload_data_t::message)); + StaticJsonDocument json; + switch (incomingData.payloadsType) + { + case ENPT_ATTRIBUTES: + mqttClient.publish((topicPrefix + "/" + getValueName(incomingData.deviceType) + "/" + myNet.macToString(sender) + "/" + getValueName(incomingData.payloadsType)).c_str(), 2, true, incomingData.message); + break; + case ENPT_KEEP_ALIVE: + mqttClient.publish((topicPrefix + "/" + getValueName(incomingData.deviceType) + "/" + myNet.macToString(sender) + "/" + getValueName(incomingData.payloadsType)).c_str(), 2, true, "online"); + break; + case ENPT_SET: + break; + case ENPT_STATE: + mqttClient.publish((topicPrefix + "/" + getValueName(incomingData.deviceType) + "/" + myNet.macToString(sender) + "/" + getValueName(incomingData.payloadsType)).c_str(), 2, true, incomingData.message); + break; + case ENPT_UPDATE: + break; + case ENPT_RESTART: + break; + case ENPT_SYSTEM: + break; + case ENPT_CONFIG: + break; + case ENPT_FORWARD: + deserializeJson(json, jsonData.message); + mqttClient.publish((topicPrefix + "/rf_sensor/" + getValueName(json["type"].as()) + "/" + json["id"].as()).c_str(), 2, false, incomingData.message); + break; + default: + break; + } +} + +void onMqttConnect(bool sessionPresent) +{ + mqttClient.subscribe((topicPrefix + "/gateway/#").c_str(), 2); + mqttClient.subscribe((topicPrefix + "/espnow_switch/#").c_str(), 2); + mqttClient.subscribe((topicPrefix + "/espnow_led/#").c_str(), 2); + + StaticJsonDocument<1024> json; + json["platform"] = "mqtt"; + json["name"] = deviceName; + json["unique_id"] = myNet.getNodeMac() + "-1"; + json["device_class"] = "connectivity"; + json["state_topic"] = topicPrefix + "/gateway/" + myNet.getNodeMac() + "/status"; + json["json_attributes_topic"] = topicPrefix + "/gateway/" + myNet.getNodeMac() + "/attributes"; + json["payload_on"] = "online"; + json["expire_after"] = 30; + json["force_update"] = "true"; + json["qos"] = 2; + json["retain"] = "true"; + char buffer[1024]{0}; + serializeJsonPretty(json, buffer); + mqttClient.publish((topicPrefix + "/binary_sensor/" + myNet.getNodeMac() + "-1" + "/config").c_str(), 2, true, buffer); + + sendKeepAliveMessage(); + sendAttributesMessage(); + attributesMessageTimer.attach(60, attributesMessageTimerCallback); +} + +void onMqttDisconnect(AsyncMqttClientDisconnectReason reason) +{ + mqttReconnectTimer.once(5, mqttReconnectTimerCallback); + sendKeepAliveMessage(); + attributesMessageTimer.detach(); +} + +void onMqttMessage(char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) +{ + String mac = getValue(String(topic).substring(0, String(topic).length()), '/', 2); + String message; + bool flag{false}; + for (uint16_t i = 0; i < len; ++i) + { + message += (char)payload[i]; + } + esp_now_payload_data_t outgoingData; + outgoingData.deviceType = ENDT_GATEWAY; + StaticJsonDocument json; + if (message == "update" || message == "restart") + { + mqttClient.publish(topic, 2, true, ""); + mqttClient.publish((String(topic) + "/status").c_str(), 2, true, "offline"); + if (mac == myNet.getNodeMac() && message == "restart") + ESP.restart(); + flag = true; + } + if (String(topic) == topicPrefix + "/espnow_switch/" + mac + "/set" || String(topic) == topicPrefix + "/espnow_led/" + mac + "/set") + { + flag = true; + json["set"] = message == "ON" ? "ON" : "OFF"; + } + if (String(topic) == topicPrefix + "/espnow_led/" + mac + "/brightness") + { + flag = true; + json["brightness"] = message; + } + if (String(topic) == topicPrefix + "/espnow_led/" + mac + "/temperature") + { + flag = true; + json["temperature"] = message; + } + if (String(topic) == topicPrefix + "/espnow_led/" + mac + "/rgb") + { + flag = true; + json["rgb"] = message; + } + if (flag) + { + if (message == "restart") + outgoingData.payloadsType = ENPT_RESTART; + else + outgoingData.payloadsType = message == "update" ? ENPT_UPDATE : ENPT_SET; + char buffer[sizeof(esp_now_payload_data_t::message)]{0}; + serializeJsonPretty(json, buffer); + memcpy(&outgoingData.message, &buffer, sizeof(esp_now_payload_data_t::message)); + char temp[sizeof(esp_now_payload_data_t)]{0}; + memcpy(&temp, &outgoingData, sizeof(esp_now_payload_data_t)); + uint8_t target[6]; + myNet.stringToMac(mac, target); + myNet.sendUnicastMessage(temp, target); + } +} + +void sendKeepAliveMessage() +{ + keepAliveMessageTimerSemaphore = false; + if (mqttClient.connected()) + mqttClient.publish((topicPrefix + "/gateway/" + myNet.getNodeMac() + "/status").c_str(), 2, true, "online"); + esp_now_payload_data_t outgoingData; + outgoingData.deviceType = ENDT_GATEWAY; + outgoingData.payloadsType = ENPT_KEEP_ALIVE; + StaticJsonDocument json; + json["MQTT"] = mqttClient.connected() ? "online" : "offline"; + char buffer[sizeof(esp_now_payload_data_t::message)]{0}; + serializeJsonPretty(json, buffer); + memcpy(&outgoingData.message, &buffer, sizeof(esp_now_payload_data_t::message)); + char temp[sizeof(esp_now_payload_data_t)]{0}; + memcpy(&temp, &outgoingData, sizeof(esp_now_payload_data_t)); + myNet.sendBroadcastMessage(temp); +} + +void sendAttributesMessage() +{ + attributesMessageTimerSemaphore = false; + uint32_t secs = millis() / 1000; + uint32_t mins = secs / 60; + uint32_t hours = mins / 60; + uint32_t days = hours / 24; + StaticJsonDocument json; + json["Type"] = "ESP-NOW Gateway"; +#if defined(ESP8266) + json["MCU"] = "ESP8266"; +#endif +#if defined(ESP32) + json["MCU"] = "ESP32"; +#endif + json["MAC"] = myNet.getNodeMac(); + json["Firmware"] = firmware; + json["Library"] = myNet.getFirmwareVersion(); + json["IP"] = WiFi.localIP().toString(); + json["Uptime"] = "Days:" + String(days) + " Hours:" + String(hours - (days * 24)) + " Mins:" + String(mins - (hours * 60)); + char buffer[sizeof(esp_now_payload_data_t::message)]{0}; + serializeJsonPretty(json, buffer); + mqttClient.publish((topicPrefix + "/gateway/" + myNet.getNodeMac() + "/attributes").c_str(), 2, true, buffer); +} + +String getValue(String data, char separator, uint8_t index) +{ + uint8_t found{0}; + int8_t strIndex[]{0, -1}; + uint8_t maxIndex = data.length() - 1; + for (uint8_t i{0}; i <= maxIndex && found <= index; i++) + if (data.charAt(i) == separator || i == maxIndex) + { + found++; + strIndex[0] = strIndex[1] + 1; + strIndex[1] = (i == maxIndex) ? i + 1 : i; + } + return found > index ? data.substring(strIndex[0], strIndex[1]) : ""; +} + +void loadConfig() +{ + if (!SPIFFS.exists("/config.json")) + saveConfig(); + File file = SPIFFS.open("/config.json", "r"); + String jsonFile = file.readString(); + StaticJsonDocument<1024> json; + deserializeJson(json, jsonFile); + espnowNetName = json["espnowNetName"].as(); + deviceName = json["deviceName"].as(); + ssid = json["ssid"].as(); + password = json["password"].as(); + mqttHostName = json["mqttHostName"].as(); + mqttHostPort = json["mqttHostPort"]; + mqttUserLogin = json["mqttUserLogin"].as(); + mqttUserPassword = json["mqttUserPassword"].as(); + topicPrefix = json["topicPrefix"].as(); + file.close(); +} + +void saveConfig() +{ + StaticJsonDocument<1024> json; + json["firmware"] = firmware; + json["espnowNetName"] = espnowNetName; + json["deviceName"] = deviceName; + json["ssid"] = ssid; + json["password"] = password; + json["mqttHostName"] = mqttHostName; + json["mqttHostPort"] = mqttHostPort; + json["mqttUserLogin"] = mqttUserLogin; + json["mqttUserPassword"] = mqttUserPassword; + json["topicPrefix"] = topicPrefix; + json["system"] = "empty"; + File file = SPIFFS.open("/config.json", "w"); + serializeJsonPretty(json, file); + file.close(); +} + +String xmlNode(String tags, String data) +{ + String temp = "<" + tags + ">" + data + ""; + return temp; +} + +void setupWebServer() +{ + SSDP.setSchemaURL("description.xml"); + SSDP.setDeviceType("upnp:rootdevice"); + + webServer.on("/description.xml", HTTP_GET, [](AsyncWebServerRequest *request) + { + String ssdpSend = ""; + String ssdpHeader = xmlNode("major", "1"); + ssdpHeader += xmlNode("minor", "0"); + ssdpHeader = xmlNode("specVersion", ssdpHeader); + ssdpHeader += xmlNode("URLBase", "http://" + WiFi.localIP().toString()); + String ssdpDescription = xmlNode("deviceType", "upnp:rootdevice"); + ssdpDescription += xmlNode("friendlyName", deviceName); + ssdpDescription += xmlNode("presentationURL", "/"); + ssdpDescription += xmlNode("serialNumber", "0000000" + String(random(1000))); + ssdpDescription += xmlNode("modelName", "ESP-NOW Gateway"); + ssdpDescription += xmlNode("modelNumber", firmware); + ssdpDescription += xmlNode("modelURL", "https://github.com/aZholtikov/ESP-NOW-Gateway"); + ssdpDescription += xmlNode("manufacturer", "Alexey Zholtikov"); + ssdpDescription += xmlNode("manufacturerURL", "https://github.com/aZholtikov"); + ssdpDescription += xmlNode("UDN", "DAA26FA3-D2D4-4072-BC7A-" + myNet.getNodeMac()); + ssdpDescription = xmlNode("device", ssdpDescription); + ssdpHeader += ssdpDescription; + ssdpSend += ssdpHeader; + ssdpSend += ""; + request->send(200, "text/xml", ssdpSend); }); + + webServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request) + { request->send(SPIFFS, "/index.htm"); }); + + webServer.on("/setting", HTTP_GET, [](AsyncWebServerRequest *request) + { + ssid = request->getParam("ssid")->value(); + password = request->getParam("password")->value(); + mqttHostName = request->getParam("host")->value(); + mqttHostPort = request->getParam("port")->value().toInt(); + mqttUserLogin = request->getParam("login")->value(); + mqttUserPassword = request->getParam("pass")->value(); + topicPrefix = request->getParam("prefix")->value(); + deviceName = request->getParam("name")->value(); + espnowNetName = request->getParam("net")->value(); + request->send(200); + saveConfig(); }); + + webServer.on("/restart", HTTP_GET, [](AsyncWebServerRequest *request) + { + request->send(200); + ESP.restart(); }); + + webServer.onNotFound([](AsyncWebServerRequest *request) + { + if (SPIFFS.exists(request->url())) + request->send(SPIFFS, request->url()); + else + { + request->send(404, "text/plain", "File Not Found"); + } }); + + SSDP.begin(); + webServer.begin(); +} + +void connectToMqtt() +{ + mqttReconnectTimerSemaphore = false; + mqttClient.connect(); +} + +void connectToWifi() +{ + wifiReconnectTimerSemaphore = false; + uint8_t scan = WiFi.scanNetworks(false, false, 1); + String name; + int32_t rssi; + uint8_t encryption; + uint8_t *bssid; + int32_t channel; + bool hidden; + bool flag{false}; + for (int8_t i = 0; i < scan; i++) + { +#if defined(ESP8266) + WiFi.getNetworkInfo(i, name, encryption, rssi, bssid, channel, hidden); +#endif +#if defined(ESP32) + WiFi.getNetworkInfo(i, name, encryption, rssi, bssid, channel); +#endif + if (name == ssid) + flag = true; + } + if (flag) + { + WiFi.begin(ssid.c_str(), password.c_str()); + while (true) + { + if (WiFi.status() == WL_CONNECTED) + { + isWasConnectionToWifi = true; + wifiReconnectTimer.detach(); + mqttClient.connect(); + return; + } + if (WiFi.status() == WL_CONNECT_FAILED) + { + if (!isWasConnectionToWifi) + { + WiFi.mode(WIFI_AP); + WiFi.softAP(String("ESP-NOW Gateway " + myNet.getNodeMac()).c_str(), "12345678"); + } + return; + } + delay(100); + } + } + else + { + WiFi.mode(WIFI_OFF); // Without rebooting WiFi stops working ESP-NOW. + myNet.begin(espnowNetName.c_str()); + } + if (!isWasConnectionToWifi) + { + WiFi.mode(WIFI_AP); + WiFi.softAP(String("ESP-NOW Gateway " + myNet.getNodeMac()).c_str(), "12345678"); + } +} + +void wifiReconnectTimerCallback() +{ + wifiReconnectTimerSemaphore = true; +} + +void mqttReconnectTimerCallback() +{ + mqttReconnectTimerSemaphore = true; +} + +void keepAliveMessageTimerCallback() +{ + keepAliveMessageTimerSemaphore = true; +} + +void attributesMessageTimerCallback() +{ + attributesMessageTimerSemaphore = true; +} \ No newline at end of file