16 Commits
v1.0 ... v1.26

Author SHA1 Message Date
24ba0a09f4 Version 1.26
Changed FS from SPIFFS to LittleFS.
2023-02-04 10:52:52 +03:00
eebaca38b7 Minor changes 2023-01-28 14:07:38 +03:00
8e9082f05e Version 1.25
Fixed bug with ESP-NOW sensors not being added to HA via MQTT discovery.
2023-01-28 12:15:09 +03:00
1c306e031b Version 1.24
Added encrypting messages.
2023-01-22 12:46:58 +03:00
74c6279f50 Version 1.23
Added value template to config message.
2023-01-15 11:58:40 +03:00
8c3b51304e Version 1.22
Web interface minor redesign.
Main code minor changes.
2023-01-12 12:24:53 +03:00
7b60f9efb3 Readme update 2023-01-08 10:49:50 +03:00
06487167f4 Version 1.21
Fixed one minor bug.
2023-01-08 10:14:51 +03:00
219cf17855 Version 1.2
Fixed some minor bugs.
Added window/door sensor support.
2023-01-06 13:09:21 +03:00
3aca3b7bbd Minor changes 2023-01-05 12:34:42 +03:00
9a86f806e4 Version 1.1
To Do item 1 performed.
2023-01-04 14:02:05 +03:00
aeaf9ad12c Version 1.01
Minor code optimization.
2023-01-03 21:23:43 +03:00
70181f56f0 Minor changes 2022-12-29 20:25:45 +03:00
028894a8cb Minor changes 2022-12-29 19:00:26 +03:00
409cdf88b8 Minor changes 2022-12-29 16:40:12 +03:00
280198da3a Minor changes 2022-12-28 21:49:07 +03:00
6 changed files with 241 additions and 519 deletions

View File

@ -4,36 +4,43 @@ 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.
1. Creates an access point named "ESP-NOW gateway XXXXXXXXXXXX" with password "12345678" (IP 192.168.4.1).
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.
5. Automatically adds supported ESP-NOW devices configurations to Home Assistan via MQTT discovery.
6. Possibility firmware update over OTA.
7. 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").
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/espnow_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.
2. If encryption is used, the key must be set same of all another ESP-NOW devices in network.
3. Upload the "data" folder (with web interface) into the filesystem before flashing.
4. WiFi router must be set on channel 1.
## Tested on
1. NodeMCU 1.0 (ESP-12E Module). ESP-NOW + WiFi mode. Unstable work.
2. AZ-Delivery ESP-32 Dev Kit C V4. ESP-NOW + WiFi mode. Stable work.
## Supported devices
1. [RF - Gateway](https://github.com/aZholtikov/RF-Gateway)
1. [RF Gateway](https://github.com/aZholtikov/RF-Gateway) (coming soon)
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)
3. [ESP-NOW Light/Led Strip](https://github.com/aZholtikov/ESP-NOW-Light-Led-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.
- [X] 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).

View File

@ -31,20 +31,20 @@
<p class="text">WiFi settings</p>
<div class="wrapper">
<input id="ssid" value="{{ssid}}" placeholder="SSID" label title="WiFi network name" />
<input class="text-inp" id="ssid" value="{{ssid}}" placeholder="SSID" label title="WiFi network name" />
<input id="password" value="{{password}}" onfocus="this.type='text'" type="password" placeholder="Password"
autocomplete="off" label title="WiFi password" />
</div>
<p class="text">MQTT settings</p>
<div class="wrapper">
<input id="mqttHostName" value="{{mqttHostName}}" placeholder="URL or IP" label
<input class="text-inp" id="mqttHostName" value="{{mqttHostName}}" placeholder="URL or IP" label
title="MQTT server URL or IP" />
<input id="mqttHostPort" value="{{mqttHostPort}}" placeholder="Port" label title="MQTT server port" />
</div>
<div class="wrapper">
<input id="mqttUserLogin" value="{{mqttUserLogin}}" placeholder="Login" label
<input class="text-inp" id="mqttUserLogin" value="{{mqttUserLogin}}" placeholder="Login" label
title="MQTT server user login" />
<input id="mqttUserPassword" value="{{mqttUserPassword}}" onfocus="this.type='text'" type="password"
placeholder="Password" autocomplete="off" label title="MQTT server user password" />

View File

@ -1,3 +1,7 @@
p{
margin: 0 0;
}
body {
font-family: "Gill Sans", sans-serif;
background: rgb(255, 255, 255);
@ -21,6 +25,21 @@ h1 {
font-weight: 600;
flex-shrink: 0;
margin-right: 10px;
margin-left: 10px;
margin: 10px 0;
}
.text-inp {
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;
margin-left: 0;
}
.wrapper {
@ -39,6 +58,7 @@ input {
color: rgb(0, 0, 0);
background: #a3e0f1;
transition: .5s;
margin-left: 10px;
}
input:hover {
@ -51,6 +71,8 @@ input:hover {
background: rgb(65, 125, 238);
color: white;
transition: .5s;
margin-left: 0;
margin-top: 8px;
}
.btn:hover {
@ -65,6 +87,10 @@ input:hover {
width: 100%;
}
#espnowNetName {
margin-bottom: 10px;
}
.wrapper.wrapper--end {
align-items: baseline;
}

356
main.cpp
View File

@ -1,356 +0,0 @@
#include <Arduino.h>
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <AsyncMqttClient.h>
#include <ESP32SSDP.h>
#include "ArduinoJson.h" // Версия 5. С другой не работает.
#include <SPIFFS.h>
#include <FS.h>
#include <time.h>
#include <Ticker.h>
#include <ArduinoOTA.h>
#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<String>(); // Читаем поля JSON.
passwordSta = json["passwordSta"].as<String>();
ssidApName = json["ssidApName"].as<String>();
passwordAp = json["passwordAp"].as<String>();
deviceName = json["deviceName"].as<String>();
mqttHostName = json["mqttHostName"].as<String>();
mqttHostPort = json["mqttHostPort"];
mqttUserLogin = json["mqttUserLogin"].as<String>();
mqttUserPassword = json["mqttUserPassword"].as<String>();
token = json["token"].as<String>();
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 = "<root xmlns=\"urn:schemas-upnp-org:device-1-0\">";
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 += "</root>";
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; i<len; i++){
Serial.write(data[i]);
}
if(final){
Serial.printf("UploadEnd: %s, %u B\n", filename.c_str(), index+len);
}
}
void setupWebServer(void) // Функция настройки WEB сервера.
{
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(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 + "</" + tags + ">";
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;
}
}

View File

@ -1,49 +1,53 @@
[env:esp8266]
[env:ESP8266]
platform = espressif8266
board = nodemcuv2
framework = arduino
board_build.filesystem = littlefs
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
https://github.com/aZholtikov/Async-Web-Server
https://github.com/bblanchon/ArduinoJson
https://github.com/marvinroger/async-mqtt-client
[env:esp8266-ota]
[env:ESP8266-OTA]
platform = espressif8266
board = nodemcuv2
framework = arduino
board_build.filesystem = littlefs
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
https://github.com/aZholtikov/Async-Web-Server
https://github.com/bblanchon/ArduinoJson
https://github.com/marvinroger/async-mqtt-client
[env:esp32]
[env:ESP32]
platform = espressif32
board = az-delivery-devkit-v4
framework = arduino
board_build.filesystem = littlefs
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
https://github.com/aZholtikov/Async-Web-Server
https://github.com/bblanchon/ArduinoJson
https://github.com/marvinroger/async-mqtt-client
https://github.com/luc-github/ESP32SSDP
[env:esp32-ota]
[env:ESP32-OTA]
platform = espressif32
board = az-delivery-devkit-v4
framework = arduino
upload_port = 192.168.1.144
board_build.filesystem = littlefs
upload_port = 192.168.1.110
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
https://github.com/aZholtikov/Async-Web-Server
https://github.com/bblanchon/ArduinoJson
https://github.com/marvinroger/async-mqtt-client
https://github.com/luc-github/ESP32SSDP

View File

@ -1,7 +1,8 @@
#include "ArduinoJson.h"
#include "ArduinoOTA.h"
#include "ESPAsyncWebServer.h"
#include "ESPAsyncWebServer.h" // https://github.com/aZholtikov/Async-Web-Server
#include "AsyncMQTTClient.h"
#include "LittleFS.h"
#include "Ticker.h"
#include "ZHNetwork.h"
#include "ZHConfig.h"
@ -9,7 +10,6 @@
#include "ESP8266SSDP.h"
#endif
#if defined(ESP32)
#include "SPIFFS.h"
#include "ESP32SSDP.h"
#endif
@ -33,13 +33,17 @@ String xmlNode(String tags, String data);
void setupWebServer(void);
void connectToMqtt(void);
void connectToWifi(void);
const String firmware{"1.0"};
const String firmware{"1.26"};
String espnowNetName{"DEFAULT"};
String deviceName{"ESP-NOW gateway"};
#if defined(ESP8266)
String deviceName = "ESP-NOW gateway " + String(ESP.getChipId(), HEX);
#endif
#if defined(ESP32)
String deviceName = "ESP-NOW gateway " + String(ESP.getEfuseMac(), HEX);
#endif
String ssid{"SSID"};
String password{"PASSWORD"};
@ -50,22 +54,16 @@ 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};
bool keepAliveMessageTimerSemaphore{true};
void keepAliveMessageTimerCallback(void);
Ticker attributesMessageTimer;
@ -74,9 +72,12 @@ void attributesMessageTimerCallback(void);
void setup()
{
WiFi.onEvent(onWifiEvent);
SPIFFS.begin();
LittleFS.begin();
loadConfig();
WiFi.onEvent(onWifiEvent);
#if defined(ESP8266)
WiFi.setSleepMode(WIFI_NONE_SLEEP);
#endif
@ -86,32 +87,55 @@ void setup()
WiFi.persistent(false);
WiFi.setAutoConnect(false);
WiFi.setAutoReconnect(false);
WiFi.setAutoReconnect(true);
myNet.begin(espnowNetName.c_str());
myNet.begin(espnowNetName.c_str(), true);
// myNet.setCryptKey("VERY_LONG_CRYPT_KEY"); // If encryption is used, the key must be set same of all another ESP-NOW devices in network.
myNet.setOnBroadcastReceivingCallback(onEspnowMessage);
myNet.setOnUnicastReceivingCallback(onEspnowMessage);
#if defined(ESP8266)
WiFi.softAP(("ESP-NOW gateway " + String(ESP.getChipId(), HEX)).c_str(), "12345678");
#endif
#if defined(ESP32)
WiFi.softAP(("ESP-NOW gateway " + String(ESP.getEfuseMac(), HEX)).c_str(), "12345678");
#endif
uint8_t scan = WiFi.scanNetworks(false, false, 1);
String name;
int32_t rssi;
uint8_t encryption;
uint8_t *bssid;
int32_t channel;
bool hidden;
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)
WiFi.begin(ssid.c_str(), password.c_str());
}
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)
@ -124,19 +148,13 @@ void loop()
void onWifiEvent(WiFiEvent_t event)
{
switch (event)
{
#if defined(ESP8266)
case WIFI_EVENT_STAMODE_DISCONNECTED:
if (event == WIFI_EVENT_STAMODE_GOT_IP)
#endif
#if defined(ESP32)
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
if (event == ARDUINO_EVENT_WIFI_STA_GOT_IP)
#endif
WiFi.mode(WIFI_OFF); // Without rebooting WiFi stops working ESP-NOW.
myNet.begin(espnowNetName.c_str());
wifiReconnectTimer.attach(30, wifiReconnectTimerCallback);
break;
}
mqttClient.connect();
}
void onEspnowMessage(const char *data, const uint8_t *sender)
@ -145,42 +163,127 @@ void onEspnowMessage(const char *data, const uint8_t *sender)
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<sizeof(esp_now_payload_data_t::message)> json;
switch (incomingData.payloadsType)
{
case ENPT_ATTRIBUTES:
if (incomingData.payloadsType == 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:
if (incomingData.payloadsType == 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:
if (incomingData.payloadsType == 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);
if (incomingData.payloadsType == ENPT_CONFIG)
{
if (incomingData.deviceType == ENDT_SWITCH)
{
esp_now_payload_data_t configData;
memcpy(&configData.message, &incomingData.message, sizeof(esp_now_payload_data_t::message));
StaticJsonDocument<sizeof(esp_now_payload_data_t::message)> json;
deserializeJson(json, configData.message);
uint8_t unit = json["unit"].as<uint8_t>();
ha_component_type_t type = json["type"].as<ha_component_type_t>();
StaticJsonDocument<2048> jsonConfig;
jsonConfig["platform"] = "mqtt";
jsonConfig["name"] = json["name"];
jsonConfig["unique_id"] = myNet.macToString(sender) + "-" + unit;
jsonConfig["device_class"] = getValueName(json["class"].as<ha_switch_device_class_t>());
jsonConfig["state_topic"] = topicPrefix + "/" + getValueName(incomingData.deviceType) + "/" + myNet.macToString(sender) + "/state";
jsonConfig["value_template"] = "{{ value_json." + json["template"].as<String>() + " }}";
jsonConfig["command_topic"] = topicPrefix + "/" + getValueName(incomingData.deviceType) + "/" + myNet.macToString(sender) + "/set";
jsonConfig["json_attributes_topic"] = topicPrefix + "/" + getValueName(incomingData.deviceType) + "/" + myNet.macToString(sender) + "/attributes";
jsonConfig["availability_topic"] = topicPrefix + "/" + getValueName(incomingData.deviceType) + "/" + myNet.macToString(sender) + "/status";
jsonConfig["payload_on"] = json["payload_on"];
jsonConfig["payload_off"] = json["payload_off"];
jsonConfig["optimistic"] = "false";
jsonConfig["qos"] = 2;
jsonConfig["retain"] = "true";
char buffer[2048]{0};
serializeJsonPretty(jsonConfig, buffer);
mqttClient.publish((topicPrefix + "/" + getValueName(type) + "/" + myNet.macToString(sender) + "-" + unit + "/config").c_str(), 2, true, buffer);
}
if (incomingData.deviceType == ENDT_LED)
{
esp_now_payload_data_t configData;
memcpy(&configData.message, &incomingData.message, sizeof(esp_now_payload_data_t::message));
StaticJsonDocument<sizeof(esp_now_payload_data_t::message)> json;
deserializeJson(json, configData.message);
uint8_t unit = json["unit"].as<uint8_t>();
ha_component_type_t type = json["type"].as<ha_component_type_t>();
esp_now_led_type_t ledClass = json["class"];
StaticJsonDocument<2048> jsonConfig;
jsonConfig["platform"] = "mqtt";
jsonConfig["name"] = json["name"];
jsonConfig["unique_id"] = myNet.macToString(sender) + "-" + unit;
jsonConfig["state_topic"] = topicPrefix + "/" + getValueName(incomingData.deviceType) + "/" + myNet.macToString(sender) + "/state";
jsonConfig["state_value_template"] = "{{ value_json.state }}";
jsonConfig["command_topic"] = topicPrefix + "/" + getValueName(incomingData.deviceType) + "/" + myNet.macToString(sender) + "/set";
jsonConfig["brightness_state_topic"] = topicPrefix + "/" + getValueName(incomingData.deviceType) + "/" + myNet.macToString(sender) + "/state";
jsonConfig["brightness_value_template"] = "{{ value_json.brightness }}";
jsonConfig["brightness_command_topic"] = topicPrefix + "/" + getValueName(incomingData.deviceType) + "/" + myNet.macToString(sender) + "/brightness";
if (ledClass == ENLT_RGB || ledClass == ENLT_RGBW || ledClass == ENLT_RGBWW)
{
jsonConfig["rgb_state_topic"] = topicPrefix + "/" + getValueName(incomingData.deviceType) + "/" + myNet.macToString(sender) + "/state";
jsonConfig["rgb_value_template"] = "{{ value_json.rgb | join(',') }}";
jsonConfig["rgb_command_topic"] = topicPrefix + "/" + getValueName(incomingData.deviceType) + "/" + myNet.macToString(sender) + "/rgb";
}
if (ledClass == ENLT_WW || ledClass == ENLT_RGBWW)
{
jsonConfig["color_temp_state_topic"] = topicPrefix + "/" + getValueName(incomingData.deviceType) + "/" + myNet.macToString(sender) + "/state";
jsonConfig["color_temp_value_template"] = "{{ value_json.temperature }}";
jsonConfig["color_temp_command_topic"] = topicPrefix + "/" + getValueName(incomingData.deviceType) + "/" + myNet.macToString(sender) + "/temperature";
}
jsonConfig["json_attributes_topic"] = topicPrefix + "/" + getValueName(incomingData.deviceType) + "/" + myNet.macToString(sender) + "/attributes";
jsonConfig["availability_topic"] = topicPrefix + "/" + getValueName(incomingData.deviceType) + "/" + myNet.macToString(sender) + "/status";
jsonConfig["payload_on"] = json["payload_on"];
jsonConfig["payload_off"] = json["payload_off"];
jsonConfig["optimistic"] = "false";
jsonConfig["qos"] = 2;
jsonConfig["retain"] = "true";
char buffer[2048]{0};
serializeJsonPretty(jsonConfig, buffer);
mqttClient.publish((topicPrefix + "/" + getValueName(type) + "/" + myNet.macToString(sender) + "-" + unit + "/config").c_str(), 2, true, buffer);
}
if (incomingData.deviceType == ENDT_SENSOR)
{
esp_now_payload_data_t configData;
memcpy(&configData.message, &incomingData.message, sizeof(esp_now_payload_data_t::message));
StaticJsonDocument<sizeof(esp_now_payload_data_t::message)> json;
deserializeJson(json, configData.message);
uint8_t unit = json["unit"].as<uint8_t>();
ha_component_type_t type = json["type"].as<ha_component_type_t>();
StaticJsonDocument<2048> jsonConfig;
jsonConfig["platform"] = "mqtt";
jsonConfig["name"] = json["name"];
jsonConfig["unique_id"] = myNet.macToString(sender) + "-" + unit;
jsonConfig["state_topic"] = topicPrefix + "/" + getValueName(incomingData.deviceType) + "/" + myNet.macToString(sender) + "/state";
jsonConfig["value_template"] = "{{ value_json." + json["template"].as<String>() + " }}";
jsonConfig["json_attributes_topic"] = topicPrefix + "/" + getValueName(incomingData.deviceType) + "/" + myNet.macToString(sender) + "/attributes";
jsonConfig["force_update"] = "true";
jsonConfig["qos"] = 2;
jsonConfig["retain"] = "true";
if (type == HACT_SENSOR)
jsonConfig["device_class"] = getValueName(json["class"].as<ha_sensor_device_class_t>());
if (type == HACT_BINARY_SENSOR)
{
jsonConfig["device_class"] = getValueName(json["class"].as<ha_binary_sensor_device_class_t>());
jsonConfig["payload_on"] = json["payload_on"];
jsonConfig["payload_off"] = json["payload_off"];
}
char buffer[2048]{0};
serializeJsonPretty(jsonConfig, buffer);
mqttClient.publish((topicPrefix + "/" + getValueName(type) + "/" + myNet.macToString(sender) + "-" + unit + "/config").c_str(), 2, true, buffer);
}
}
if (incomingData.payloadsType == ENPT_FORWARD)
{
esp_now_payload_data_t forwardData;
memcpy(&forwardData.message, &incomingData.message, sizeof(esp_now_payload_data_t::message));
StaticJsonDocument<sizeof(esp_now_payload_data_t::message)> json;
deserializeJson(json, forwardData.message);
mqttClient.publish((topicPrefix + "/rf_sensor/" + getValueName(json["type"].as<rf_sensor_type_t>()) + "/" + json["id"].as<uint16_t>()).c_str(), 2, false, incomingData.message);
break;
default:
break;
}
}
void onMqttConnect(bool sessionPresent)
{
mqttClient.subscribe((topicPrefix + "/gateway/#").c_str(), 2);
mqttClient.subscribe((topicPrefix + "/espnow_gateway/#").c_str(), 2);
mqttClient.subscribe((topicPrefix + "/espnow_switch/#").c_str(), 2);
mqttClient.subscribe((topicPrefix + "/espnow_led/#").c_str(), 2);
@ -189,8 +292,8 @@ void onMqttConnect(bool sessionPresent)
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["state_topic"] = topicPrefix + "/espnow_gateway/" + myNet.getNodeMac() + "/status";
json["json_attributes_topic"] = topicPrefix + "/espnow_gateway/" + myNet.getNodeMac() + "/attributes";
json["payload_on"] = "online";
json["expire_after"] = 30;
json["force_update"] = "true";
@ -235,7 +338,7 @@ void onMqttMessage(char *topic, char *payload, AsyncMqttClientMessageProperties
if (String(topic) == topicPrefix + "/espnow_switch/" + mac + "/set" || String(topic) == topicPrefix + "/espnow_led/" + mac + "/set")
{
flag = true;
json["set"] = message == "ON" ? "ON" : "OFF";
json["set"] = message;
}
if (String(topic) == topicPrefix + "/espnow_led/" + mac + "/brightness")
{
@ -273,12 +376,13 @@ void sendKeepAliveMessage()
{
keepAliveMessageTimerSemaphore = false;
if (mqttClient.connected())
mqttClient.publish((topicPrefix + "/gateway/" + myNet.getNodeMac() + "/status").c_str(), 2, true, "online");
mqttClient.publish((topicPrefix + "/espnow_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<sizeof(esp_now_payload_data_t::message)> json;
json["MQTT"] = mqttClient.connected() ? "online" : "offline";
json["frequency"] = 10; // For compatibility with the previous version. Will be removed in future releases.
char buffer[sizeof(esp_now_payload_data_t::message)]{0};
serializeJsonPretty(json, buffer);
memcpy(&outgoingData.message, &buffer, sizeof(esp_now_payload_data_t::message));
@ -295,7 +399,7 @@ void sendAttributesMessage()
uint32_t hours = mins / 60;
uint32_t days = hours / 24;
StaticJsonDocument<sizeof(esp_now_payload_data_t::message)> json;
json["Type"] = "ESP-NOW Gateway";
json["Type"] = "ESP-NOW gateway";
#if defined(ESP8266)
json["MCU"] = "ESP8266";
#endif
@ -309,7 +413,7 @@ void sendAttributesMessage()
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);
mqttClient.publish((topicPrefix + "/espnow_gateway/" + myNet.getNodeMac() + "/attributes").c_str(), 2, true, buffer);
}
String getValue(String data, char separator, uint8_t index)
@ -329,9 +433,9 @@ String getValue(String data, char separator, uint8_t index)
void loadConfig()
{
if (!SPIFFS.exists("/config.json"))
if (!LittleFS.exists("/config.json"))
saveConfig();
File file = SPIFFS.open("/config.json", "r");
File file = LittleFS.open("/config.json", "r");
String jsonFile = file.readString();
StaticJsonDocument<1024> json;
deserializeJson(json, jsonFile);
@ -361,7 +465,7 @@ void saveConfig()
json["mqttUserPassword"] = mqttUserPassword;
json["topicPrefix"] = topicPrefix;
json["system"] = "empty";
File file = SPIFFS.open("/config.json", "w");
File file = LittleFS.open("/config.json", "w");
serializeJsonPretty(json, file);
file.close();
}
@ -388,7 +492,7 @@ void setupWebServer()
ssdpDescription += xmlNode("friendlyName", deviceName);
ssdpDescription += xmlNode("presentationURL", "/");
ssdpDescription += xmlNode("serialNumber", "0000000" + String(random(1000)));
ssdpDescription += xmlNode("modelName", "ESP-NOW Gateway");
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");
@ -401,7 +505,7 @@ void setupWebServer()
request->send(200, "text/xml", ssdpSend); });
webServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
{ request->send(SPIFFS, "/index.htm"); });
{ request->send(LittleFS, "/index.htm"); });
webServer.on("/setting", HTTP_GET, [](AsyncWebServerRequest *request)
{
@ -424,8 +528,8 @@ void setupWebServer()
webServer.onNotFound([](AsyncWebServerRequest *request)
{
if (SPIFFS.exists(request->url()))
request->send(SPIFFS, request->url());
if (LittleFS.exists(request->url()))
request->send(LittleFS, request->url());
else
{
request->send(404, "text/plain", "File Not Found");
@ -441,69 +545,6 @@ void connectToMqtt()
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;