Сегодня расскажу про реализацию идеи «Умного Дома» своими руками на примере своего дачного участка.
Зачем мне это надо? Изначально, только для одного — удаленного запуска газового котла Navien Deluxe, чтобы к приезду моей семьи дом был прогретым.
Т.к. штатный модуль WiFi у Navien довольно дорогой (также, как сторонний от Zont), а руки у меня не совсем кривые, то решено было изготовить контроллер самостоятельно при помощи модуля ESP32 и управляемого реле. Стоит отметить, что Navien запоминает последнее состояние, т.е. при подачи питания 220В котел запуститься с настройками, заданными перед пропаданием питания. На этом и основана логика работы контура ESP32-реле-стабилизатор-котёл.
На сегодняшний день, есть желание доработать проект, наделив его новым функционалом — полив огорода. Позже, эта штука будет обрастать ещё чем-то интересным, кмк.
Как я уже упомянул, за основу был взят модуль ESP32. Выбран он был, т.к. имелся опыт исользования ESP32-CAM и прошивки его кодом от s60sc. Ссылка на гитхабовский проект: https://github.com/s60sc/ESP32-CAM_MJPEG2SD. Сейчас, спустя время, правильней было бы его делать на ESP8266, потому что ESP32 глючноват и менее стабилен, но вроде я налавчился и останусь на нём.
На ESP32 поднят web-сервер (доступный только из внутренней vpn-сети) и крутится небольшой сайт. На нём есть страница управления котлом с показаниями датчиков температуры, страница просмотра видео с камер (пока только одной, той самой ESP32-CAM) и страница перезапуска узла питания. Последняя страница нужна для того, чтобы можно было удалённо оживить ESP32-CAM, когда он зависает, и сбросить 3G-подключение, когда и оно зависает без подключения к роутеру. Камера запитана от той же ветки, что узел связи. Подробнее на узле связи я останавливаться не буду, может потом как-нибудь.
Проект компилировался в Arduino IDE. Т.к. в языке C я не силён, за основу брался код из этих источников:
— работа с сайтом в памяти контроллера
— работа с EEPROM для хранения состояния реле
Основные комментарии приведены в листинге, если есть вопросы — отвечу отдельно, обращайтесь.
Шаблон сайта взят отсюда. Все необходимые файлы вытащены из папок в корень при заливке в память контроллера. Ненужное — удалено. Папка data проекта выглядит вот так:
HTML комментировать не буду, там и так всё очевидно, но если что-то будет не понятно, то опять же — обращайтесь.
Мой код прошивки контроллера:
// Подключаем используемые библиотеки #include// Считывание показаний датчиков по протоколу OneWire #include // Подключаем датчики Dallas #include // Библиотека для реализации WiFi-подключения #include // Хранение состояния реле #include // Асинхронный web-сервер #include // Хранение web-сайта #include // Контроль состояний ESP32 для автоматического перезапуска // Задаём задержку, после которой считаем, что модуль завис и перезагружаем его (таймер watchdog) #define WDT_TIMEOUT 30 // Используемый размер EEPROM для хранения состояний котла (ВКЛ/ВЫКЛ) (нужно при пропадании питания дома) #define EEPROM_SIZE 1 // Сенсоры-датчики температуры подключены к GPIO4 #define ONE_WIRE_BUS 4 // здесь пишем учетные данные своей сети: const char* ssid = "wifi_ap_name"; const char* password = "wifi_ap_password"; char ST_ip[16] = "192.168.0.11"; // Статический IP char ST_sn[16] = "255.255.255.0"; // маска char ST_gw[16] = "192.168.0.1"; // Шлюз // задаем номера для выходных GPIO-контактов: const int RelayPin = 27; // Управление реле питания котла const int NetworkPin = 14; // Управление реле питания узла связи // Создаем экземпляр класса «AsyncWebServer» // под названием «server» и задаем ему номер порта «80»: AsyncWebServer server(80); // ------Вспомогательные переменные------ // Переменная для хранения HTTP-запроса String header; // Переменная для хранения текущего состояния выходного контакта RelayPin int RelayState = HIGH; // Переменные для хранения показаний температуры String temperatureC1 = ""; String temperatureC2 = ""; float kitchenSensor, waterSensor; // Переменные для таймера unsigned long lastTime = 0; unsigned long timerDelay = 5000; // Запускаем обработчик устройств OneWire OneWire oneWire(ONE_WIRE_BUS); // Подключаем наш обработчик к Dallas-сенсорам DallasTemperature sensors(&oneWire); // Описываем наши датчики DeviceAddress sensor1 = { 0x28, 0xFF, 0x64, 0x02, 0xC9, 0xF6, 0x9A, 0xC0 }; // Кухня DeviceAddress sensor2 = { 0x28, 0xFF, 0x64, 0x02, 0xCD, 0x87, 0x40, 0x05 }; // Теплоноситель String readDSTemperatureC1() { // Опрашиваем датчик №1 sensors.requestTemperatures(); float tempC = sensors.getTempC(sensor1); if(tempC == -127.00) { Serial.println("Failed to read from sensor №1 (Kitchen)"); return "--"; } else { Serial.print("Kitchen Temperature Celsius: "); Serial.println(tempC); } return String(tempC); } String readDSTemperatureC2() { // Опрашиваем датчик №1 sensors.requestTemperatures(); float tempC = sensors.getTempC(sensor2); if(tempC == -127.00) { Serial.println("Failed to read from sensor №2 (Water)"); return "--"; } else { Serial.print("Water Temperature Celsius: "); Serial.println(tempC); } return String(tempC); } // Меняем заглушку на текущее состояние реле и температуры: String processor(const String& var){ // Создаем переменную для хранения состояния реле, у нас реле нормально закрытое, поэтому 0 - открыто, 1 - закрыто: String relayStateWeb; Serial.println(var); if(var == "STATE"){ if(digitalRead(RelayPin)){ relayStateWeb = "OFF"; } else{ relayStateWeb = "ON"; } Serial.println(relayStateWeb); return relayStateWeb; } if(var == "TEMPERATUREC1"){ return temperatureC1; } if(var == "TEMPERATUREC2"){ return temperatureC2; } return String(); } void WiFiStationConnected(WiFiEvent_t event, WiFiEventInfo_t info){ Serial.println("Connected to AP successfully!"); } void WiFiGotIP(WiFiEvent_t event, WiFiEventInfo_t info){ // печатаем в мониторе порта локальный IP-адрес // и запускаем веб-сервер: Serial.println(""); Serial.println("WiFi connected."); // "WiFi подключен." Serial.print("IP address: "); // "IP-адрес: " Serial.println(WiFi.localIP()); } void WiFiStationDisconnected(WiFiEvent_t event, WiFiEventInfo_t info){ Serial.println("Disconnected from WiFi access point"); Serial.print("WiFi lost connection. Reason: "); Serial.println(info.wifi_sta_disconnected.reason); Serial.println("Trying to Reconnect"); WiFi.begin(ssid, password); } void setup(void){ Serial.begin(115200); esp_task_wdt_init(WDT_TIMEOUT, true); // Инициализируем контроль состояния паники контроллера для перезагрузки esp_task_wdt_add(NULL); // Активируем контроль состояния sensors.begin(); // Инициализируем EEPROM с предопределенным размером EEPROM.begin(EEPROM_SIZE); // Определяем температуру temperatureC1 = readDSTemperatureC1(); temperatureC2 = readDSTemperatureC2(); // Задаем начальное состояние наших выходных контактов pinMode(RelayPin, OUTPUT); pinMode(NetworkPin, OUTPUT); digitalWrite(NetworkPin, HIGH); // Инициализируем SPIFFS: if(!SPIFFS.begin(true)){ Serial.println("An Error has occurred while mounting SPIFFS"); // "При монтировании SPIFFS произошла ошибка" return; } // прочитать последнее состояние реле из флэш-памяти RelayState = EEPROM.read(0); Serial.println((String) "Last state: " + RelayState); // Установить реле в последнее сохраненное состояние digitalWrite(RelayPin, RelayState); // Подключаемся к WiFi-сети при помощи заданных выше SSID и пароля: Serial.print("Connecting to "); // "Подключаемся к " Serial.println(ssid); // Устанавливаем статический IP-адрес IPAddress _ip, _gw, _sn; _ip.fromString(ST_ip); _gw.fromString(ST_gw); _sn.fromString(ST_sn); WiFi.config(_ip, _gw, _sn); WiFi.onEvent(WiFiStationConnected, ARDUINO_EVENT_WIFI_STA_CONNECTED); WiFi.onEvent(WiFiGotIP, ARDUINO_EVENT_WIFI_STA_GOT_IP); WiFi.onEvent(WiFiStationDisconnected, ARDUINO_EVENT_WIFI_STA_DISCONNECTED); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } // Ззапускаем веб-сервер: // URL для корневой страницы и прочих страниц веб-сервера: server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/index.html", String(), false, processor); }); server.on("/index.html", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/index.html", String(), false, processor); }); server.on("/cameras.html", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/cameras.html", String(), false, processor); }); server.on("/temp.html", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/temp.html", String(), false, processor); }); server.on("/network.html", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/network.html", String(), false, processor); }); // URL для файла «style.css»: server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/style.css", "text/css"); }); // Прочее server.on("/reset.css", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/reset.css", "text/css"); }); server.on("/logo.png", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/logo.png", "text/css"); }); server.on("/bg.jpg", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/bg.jpg", "text/css"); }); server.on("/jquery-1.7.min.js", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/jquery-1.7.min.js", "text/css"); }); server.on("/jquery.easing.1.3.js", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/jquery.easing.1.3.js", "text/css"); }); server.on("/FF-cash.js", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/FF-cash.js", "text/css"); }); server.on("/header.jpg", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/header.jpg", "text/css"); }); server.on("/nav-shadow.png", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/nav-shadow.png", "text/css"); }); server.on("/nav.jpg", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/nav.jpg", "text/css"); }); server.on("/menu-li.jpg", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/menu-li.jpg", "text/css"); }); server.on("/header-content.jpg", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/header-content.jpg", "text/css"); }); // URL для переключения GPIO-контакта на «LOW»: server.on("/on", HTTP_GET, [](AsyncWebServerRequest *request){ digitalWrite(RelayPin, LOW); Serial.println("GPIO off, Relay on"); // "GPIO включен" RelayState = LOW; EEPROM.write(0, RelayState); EEPROM.commit(); Serial.println("State saved in flash memory"); request->send(SPIFFS, "/index.html", String(), false, processor); }); // URL для переключения GPIO-контакта на «HIGH»: server.on("/off", HTTP_GET, [](AsyncWebServerRequest *request){ digitalWrite(RelayPin, HIGH); Serial.println("GPIO on, Relay off"); // "GPIO выключен" RelayState = HIGH; EEPROM.write(0, RelayState); EEPROM.commit(); Serial.println("State saved in flash memory"); request->send(SPIFFS, "/index.html", String(), false, processor); }); // URL для перезапуска узла связи: server.on("/reboot", HTTP_GET, [](AsyncWebServerRequest *request){ digitalWrite(NetworkPin, LOW); Serial.println("Network core power reset start"); delay(2000); digitalWrite(NetworkPin, HIGH); Serial.println("Network core power reset complete"); request->send(SPIFFS, "/network.html", String(), false, processor); }); server.on("/temperaturec1", HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, "text/plain", temperatureC1.c_str()); }); server.on("/temperaturec2", HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, "text/plain", temperatureC2.c_str()); }); server.begin(); } // В цикле опрашиваем сенсоры и следим за состоянием контроллера: void loop(void){ delay(1000); if ((millis() - lastTime) > timerDelay) { temperatureC1 = readDSTemperatureC1(); temperatureC2 = readDSTemperatureC2(); lastTime = millis(); } Serial.println("WDT RESET"); esp_task_wdt_reset(); }
HTML-код главной страницы (index.html):
П , 4 - сервер управления
HTML-код страницы просмотра камер (cameras.html):
П , 4 - сервер управления
Здесь мы просто выводим поток с адреса вещания камеры в локальной сети (http://192.168.0.10:81/stream)
HTML-код страницы управления узлом связи (network.html):
П , 4 - сервер управления
HTML-код блока отображения температуры (temp.html):
Температура на кухне %TEMPERATUREC1% °C
Температура теплоносителя %TEMPERATUREC2% °C