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 1ae6ba6..3d90fa3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,30 @@ -# ESP-NOW-Window-Door-Sensor +# ESP-NOW window/door sensor for ESP8266 +ESP-NOW based window/door sensor for ESP8266. Alternate firmware for Tuya/SmartLife WiFi window/door sensors. + +## Features + +1. When triggered transmits system information, battery status and sensor status. +2. Average response time of 1 second (depends on the MCU of the sensor). +3. In setup/update mode creates an access point named "ESP-NOW Window XXXXXXXXXXXX" with password "12345678" (IP 192.168.4.1). +4. Automatically adds sensor and battery configuration to Home Assistan via MQTT discovery as a binary_sensors (2 different binary_sensor). +5. Possibility firmware update over OTA (if is allows the size of the flash memory). +6. Web interface for settings. + +## Notes + +1. ESP-NOW mesh network based on the library [ZHNetwork](https://github.com/aZholtikov/ZHNetwork). +2. For enter to setup/update mode press the button for > 5 seconds. The LED will illuminate. Access point will be available during 120 seconds before the module is powered off. + +## Tested on + +See [here](https://github.com/aZholtikov/ESP-NOW-Window-Door-Sensor/tree/main/hardware). + +## Attention + +1. A gateway is required. For details see [ESP-NOW Gateway](https://github.com/aZholtikov/ESP-NOW-Gateway). +2. ESP-NOW network name 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. For using this firmware on Tuya/SmartLife WiFi window/door sensors, the WiFi module must be replaced with an ESP8266 compatible module (if necessary). +5. Highly recommended connect an external power supply during setup/upgrade. +6. Because this sensor is battery operated, it has an additional controller (MCU) that controls the power of the WiFi module (Module) and transmits data to it for transmission to the network. The communication is done via UART at 9600 speed. Make sure that the protocol is correct before flashing. Details [here](https://github.com/aZholtikov/ESP-NOW-Window-Door-Sensor/tree/main/doc). diff --git a/data/function.js b/data/function.js new file mode 100755 index 0000000..edd2130 --- /dev/null +++ b/data/function.js @@ -0,0 +1,74 @@ +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'); + setGpioValue('deviceSensorClassSelect', 'deviceSensorClass'); + handleServerResponse(); +} + +function getValue(id) { + var value = document.getElementById(id).value; + return value; +} + +function getSelectValue(id) { + var select = document.getElementById(id); + var value = select.value; + return value; +} + +function sendRequest(submit, server) { + request = new XMLHttpRequest(); + request.open("GET", server, true); + request.send(); +} + +function saveSetting(submit) { + server = "/setting?deviceSensorName=" + getValue('deviceSensorName') + + "&deviceBatteryName=" + getValue('deviceBatteryName') + + "&espnowNetName=" + getValue('espnowNetName') + + "&deviceSensorClass=" + getSelectValue('deviceSensorClassSelect'); + 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; +} + +function setGpioValue(id, value) { + var select = document.getElementById(id); + select.value = document.getElementById(value).value; +} diff --git a/data/index.htm b/data/index.htm new file mode 100644 index 0000000..90f9727 --- /dev/null +++ b/data/index.htm @@ -0,0 +1,54 @@ + + + + + + + + ESP-NOW Window/Door Sensor + + + +
+

ESP-NOW Window/Door Sensor

+
+

Firmware:

+

+ +
+ +
+

Sensor name:

+ +
+ +
+

Battery name:

+ +
+ +
+

ESP-NOW network name:

+ +
+ +
+

Sensor type:

+ +

+
+ +
+ + +
+
+ + + \ No newline at end of file diff --git a/data/style.css b/data/style.css new file mode 100644 index 0000000..b4e4f72 --- /dev/null +++ b/data/style.css @@ -0,0 +1,95 @@ +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; +} + +.text-select { + width: 35%; + 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; +} + +select { + width: 110px; + 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; +} + +select: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); +} + +#deviceSensorName, +#deviceBatteryName, +#espnowNetName { + width: 73%; +} + +.wrapper.wrapper--end { + align-items: baseline; +} \ No newline at end of file diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..0d37a72 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,45 @@ +# Communication protocol + +The protocol can be read with 2 USB-TTL converters. RX of the first one is connected to the RX of the WiFi module to read the MCU data. Connect the RX of the second to the TX of the WiFi module to read the module's data. Don't forget to connect all GND (of both converters and module). + +Communication protocol used in the firmware (only necessary "cuts" from the original protocol): + +```text +1. Normal mode. Sensor triggering. + + Module power is on + Module sends 55 AA 00 01 00 00 00 (Initial message) + MCU returns 55 AA 00 01 00 ............ (MCU system information) + Module sends 55 AA 00 02 00 01 04 06 (Network connection established) + MCU returns 55 AA 00 02 00 00 01 (Confirmation message) + The MCU sends 55 AA 00 08 00 0C 00 01 01 01 01 01 03 04 00 01 02 23 (Battery status. 02 23 - high, 01 22 - medium, 00 21 - low) + Module returns 55 AA 00 08 00 01 00 08 (Confirmation message) + The MCU sends 55 AA 00 08 00 0C 00 02 02 02 02 02 01 01 01 00 22 (Sensor position. 01 23 - open, 00 22 - closed) + Module returns 55 AA 00 08 00 01 00 08 (Confirmation message) + Module power off + +2. Sending the battery status. Pressing the button. Not used in the firmware. + + Module power is on + Module sends 55 AA 00 01 00 00 00 (Initial message) + MCU returns 55 AA 00 01 00 00 ............ (MCU system information) + Module sends 55 AA 00 02 00 01 04 06 (Network connection established) + MCU returns 55 AA 00 02 00 00 01 (Confirmation message) + The MCU sends 55 AA 00 08 00 0C 00 01 01 01 01 01 03 04 00 01 02 23 (Battery status. 02 23 - high, 01 22 - medium, 00 21 - low) + Module returns 55 AA 00 08 00 01 00 08 (Confirmation message) + Module power off + +3. Update mode. Pressing the button for > 5 seconds - the LED light on. + + Module power on + Module sends 55 AA 00 01 00 00 00 (Initial message) + MCU returns 55 AA 00 01 00 00 ............ (MCU system information) + Module sends 55 AA 00 02 00 01 04 06 (Network connection established) + MCU returns 55 AA 00 02 00 00 01 (Confirmation message) + The MCU sends 55 AA 00 03 00 00 02 (Message for switching to setting mode) + Module returns 55 AA 00 03 00 00 02 (Confirmation message) + + Update mode has started. Will be available during 120 seconds until the module is powered off. + After updating and rebooting the module will return to normal mode - the LED will go off. + Highly recommended connect an external power supply during update. +``` diff --git a/doc/Serial Communication Protocol v20220706.pdf b/doc/Serial Communication Protocol v20220706.pdf new file mode 100644 index 0000000..a0ad0d0 Binary files /dev/null and b/doc/Serial Communication Protocol v20220706.pdf differ diff --git a/doc/Serial Port Protocol Door Sensor.pdf b/doc/Serial Port Protocol Door Sensor.pdf new file mode 100644 index 0000000..ffb6ef5 Binary files /dev/null and b/doc/Serial Port Protocol Door Sensor.pdf differ diff --git a/doc/Serial Port Protocol Wi-Fi Common Solution v20210412.pdf b/doc/Serial Port Protocol Wi-Fi Common Solution v20210412.pdf new file mode 100644 index 0000000..0a5fe84 Binary files /dev/null and b/doc/Serial Port Protocol Wi-Fi Common Solution v20210412.pdf differ diff --git a/doc/Serial Port Protocol Wi-Fi Low-Power Device Solution v20210125.pdf b/doc/Serial Port Protocol Wi-Fi Low-Power Device Solution v20210125.pdf new file mode 100644 index 0000000..9fe408c Binary files /dev/null and b/doc/Serial Port Protocol Wi-Fi Low-Power Device Solution v20210125.pdf differ diff --git a/hardware/Model_D06_Type_1/TYWE3S.pdf b/hardware/Model_D06_Type_1/TYWE3S.pdf new file mode 100644 index 0000000..819e634 Binary files /dev/null and b/hardware/Model_D06_Type_1/TYWE3S.pdf differ diff --git a/hardware/Model_D06_Type_1/inside1.jpeg b/hardware/Model_D06_Type_1/inside1.jpeg new file mode 100644 index 0000000..39314ce Binary files /dev/null and b/hardware/Model_D06_Type_1/inside1.jpeg differ diff --git a/hardware/Model_D06_Type_1/inside2.jpeg b/hardware/Model_D06_Type_1/inside2.jpeg new file mode 100644 index 0000000..e525964 Binary files /dev/null and b/hardware/Model_D06_Type_1/inside2.jpeg differ diff --git a/hardware/Model_D06_Type_1/main.jpeg b/hardware/Model_D06_Type_1/main.jpeg new file mode 100644 index 0000000..9f80b5c Binary files /dev/null and b/hardware/Model_D06_Type_1/main.jpeg differ diff --git a/hardware/Model_D06_Type_2/CBU.pdf b/hardware/Model_D06_Type_2/CBU.pdf new file mode 100644 index 0000000..57978a3 Binary files /dev/null and b/hardware/Model_D06_Type_2/CBU.pdf differ diff --git a/hardware/Model_D06_Type_2/ESP-M2.pdf b/hardware/Model_D06_Type_2/ESP-M2.pdf new file mode 100644 index 0000000..b69a8dd Binary files /dev/null and b/hardware/Model_D06_Type_2/ESP-M2.pdf differ diff --git a/hardware/Model_D06_Type_2/inside1.jpeg b/hardware/Model_D06_Type_2/inside1.jpeg new file mode 100644 index 0000000..d99b8e8 Binary files /dev/null and b/hardware/Model_D06_Type_2/inside1.jpeg differ diff --git a/hardware/Model_D06_Type_2/inside2.jpeg b/hardware/Model_D06_Type_2/inside2.jpeg new file mode 100644 index 0000000..2d7b524 Binary files /dev/null and b/hardware/Model_D06_Type_2/inside2.jpeg differ diff --git a/hardware/Model_D06_Type_2/inside3.jpeg b/hardware/Model_D06_Type_2/inside3.jpeg new file mode 100644 index 0000000..079da3b Binary files /dev/null and b/hardware/Model_D06_Type_2/inside3.jpeg differ diff --git a/hardware/Model_D06_Type_2/inside4.jpeg b/hardware/Model_D06_Type_2/inside4.jpeg new file mode 100644 index 0000000..0bccef7 Binary files /dev/null and b/hardware/Model_D06_Type_2/inside4.jpeg differ diff --git a/hardware/Model_D06_Type_2/main.jpeg b/hardware/Model_D06_Type_2/main.jpeg new file mode 100644 index 0000000..9f80b5c Binary files /dev/null and b/hardware/Model_D06_Type_2/main.jpeg differ diff --git a/hardware/README.md b/hardware/README.md new file mode 100644 index 0000000..6520221 --- /dev/null +++ b/hardware/README.md @@ -0,0 +1,4 @@ +# Tested on + +1. Model D06 Type 1. Built on Tuya WiFi module TYWE3S (ESP8266 chip). Analogue of ESP-01. Does not require replacement. Total triggering time about 1.5 sec. [Photo](https://github.com/aZholtikov/ESP-NOW-Window-Door-Sensor/tree/main/hardware/Model_D06_Type_1). +2. Model D06 Type 2. Built on Tuya WiFi module CBU (BK7231N chip). Replacement required. No physical equivalent (as of this writing). Performed replacement with ESP-M2 (ESP8285 chip) because it was in stock. It can be replaced with any ESP8266 compatible. Total response time about 0.5 sec. [Photo](https://github.com/aZholtikov/ESP-NOW-Window-Door-Sensor/tree/main/hardware/Model_D06_Type_2). diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..79af914 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,19 @@ +[env:TYWE3S] +platform = espressif8266 +board = esp01_1m +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 + +[env:ESP-M2] +platform = espressif8266 +board = esp8285 +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 diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..6d9010f --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,264 @@ +#include "ArduinoJson.h" +#include "ArduinoOTA.h" +#include "ESPAsyncWebServer.h" +#include "ZHNetwork.h" +#include "ZHConfig.h" + +void onConfirmReceiving(const uint8_t *target, const bool status); + +void loadConfig(void); +void saveConfig(void); +void setupWebServer(void); + +void sendSensorConfigMessage(void); +void sendBatteryConfigMessage(void); +void sendAttributesMessage(void); + +const String firmware{"1.0"}; + +String espnowNetName{"DEFAULT"}; + +String deviceSensorName{"ESP-NOW window sensor"}; +uint8_t deviceSensorClass{HABSDC_WINDOW}; +String deviceBatteryName{"ESP-NOW window sensor battery"}; + +char receivedBytes[128]{0}; +byte counter{0}; +byte messageLenght{0}; +bool dataReceiving{false}; +bool dataReceived{false}; +bool semaphore{false}; + +esp_now_payload_data_t outgoingData{ENDT_SENSOR, ENPT_STATE}; +StaticJsonDocument json; +char buffer[sizeof(esp_now_payload_data_t::message)]{0}; +char temp[sizeof(esp_now_payload_data_t)]{0}; +const char initialMessage[] = {0x55, 0xAA, 0x00, 0x01, 0x00, 0x00, 0x00}; +const char connectedMessage[] = {0x55, 0xAA, 0x00, 0x02, 0x00, 0x01, 0x04, 0x06}; +const char settingMessage[] = {0x55, 0xAA, 0x00, 0x03, 0x00, 0x00, 0x02}; +const char confirmationMessage[] = {0x55, 0xAA, 0x00, 0x08, 0x00, 0x01, 0x00, 0x08}; + +ZHNetwork myNet; +AsyncWebServer webServer(80); + +void setup() +{ + Serial.begin(9600); + + SPIFFS.begin(); + + loadConfig(); + + myNet.begin(espnowNetName.c_str()); + + myNet.setOnConfirmReceivingCallback(onConfirmReceiving); + + sendSensorConfigMessage(); + sendBatteryConfigMessage(); + sendAttributesMessage(); + + Serial.write(initialMessage, sizeof(initialMessage)); + Serial.flush(); +} + +void loop() +{ + if (Serial.available() > 0 && !dataReceived) + { + char receivedByte[1]; + Serial.readBytes(receivedByte, 1); + if (receivedByte[0] == 0x55) + { + dataReceiving = true; + receivedBytes[counter++] = receivedByte[0]; + return; + } + if (dataReceiving) + { + if (counter == 5) + messageLenght = 6 + int(receivedByte[0]); + if (counter == messageLenght) + { + receivedBytes[counter] = receivedByte[0]; + counter = 0; + dataReceiving = false; + dataReceived = true; + return; + } + receivedBytes[counter++] = receivedByte[0]; + } + } + if (dataReceived) + { + if (receivedBytes[3] == 0x01) + { + Serial.write(connectedMessage, sizeof(connectedMessage)); + Serial.flush(); + dataReceived = false; + } + if (receivedBytes[3] == 0x02) + dataReceived = false; + if (receivedBytes[3] == 0x03) + { + Serial.write(settingMessage, sizeof(settingMessage)); + Serial.flush(); + Serial.end(); + dataReceived = false; + WiFi.softAP(("ESP-NOW Window " + myNet.getNodeMac()).c_str(), "12345678", 1, 0); + setupWebServer(); + ArduinoOTA.begin(); + } + if (receivedBytes[3] == 0x08) + { + if (receivedBytes[7] == 0x01) + { + if (receivedBytes[17] == 0x02) + json["battery"] = "HIGH"; + if (receivedBytes[17] == 0x01) + json["battery"] = "MID"; + if (receivedBytes[17] == 0x00) + json["battery"] = "LOW"; + dataReceived = false; + Serial.write(confirmationMessage, sizeof(confirmationMessage)); + Serial.flush(); + } + if (receivedBytes[7] == 0x02) + { + if (receivedBytes[17] == 0x01) + json["state"] = "OPEN"; + if (receivedBytes[17] == 0x00) + json["state"] = "CLOSED"; + dataReceived = false; + serializeJsonPretty(json, buffer); + memcpy(outgoingData.message, buffer, sizeof(esp_now_payload_data_t::message)); + memcpy(temp, &outgoingData, sizeof(esp_now_payload_data_t)); + myNet.sendBroadcastMessage(temp); + semaphore = true; + } + } + } + myNet.maintenance(); + ArduinoOTA.handle(); +} + +void onConfirmReceiving(const uint8_t *target, const bool status) +{ + if (semaphore) + { + Serial.write(confirmationMessage, sizeof(confirmationMessage)); + Serial.flush(); + } +} + +void loadConfig() +{ + if (!SPIFFS.exists("/config.json")) + saveConfig(); + File file = SPIFFS.open("/config.json", "r"); + String jsonFile = file.readString(); + StaticJsonDocument<512> json; + deserializeJson(json, jsonFile); + espnowNetName = json["espnowNetName"].as(); + deviceSensorName = json["deviceSensorName"].as(); + deviceBatteryName = json["deviceBatteryName"].as(); + deviceSensorClass = json["deviceSensorClass"]; + file.close(); +} + +void saveConfig() +{ + StaticJsonDocument<512> json; + json["firmware"] = firmware; + json["espnowNetName"] = espnowNetName; + json["deviceSensorName"] = deviceSensorName; + json["deviceBatteryName"] = deviceBatteryName; + json["deviceSensorClass"] = deviceSensorClass; + json["system"] = "empty"; + File file = SPIFFS.open("/config.json", "w"); + serializeJsonPretty(json, file); + file.close(); +} + +void setupWebServer() +{ + webServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request) + { request->send(SPIFFS, "/index.htm"); }); + + webServer.on("/setting", HTTP_GET, [](AsyncWebServerRequest *request) + { + deviceSensorName = request->getParam("deviceSensorName")->value(); + deviceBatteryName = request->getParam("deviceBatteryName")->value(); + deviceSensorClass = request->getParam("deviceSensorClass")->value().toInt(); + espnowNetName = request->getParam("espnowNetName")->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"); + } }); + + webServer.begin(); +} + +void sendSensorConfigMessage() +{ + esp_now_payload_data_t outgoingData{ENDT_SENSOR, ENPT_CONFIG}; + StaticJsonDocument json; + json["name"] = deviceSensorName; + json["unit"] = 1; + json["type"] = HACT_BINARY_SENSOR; + json["class"] = deviceSensorClass; + json["payload_on"] = "OPEN"; + json["payload_off"] = "CLOSED"; + 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 sendBatteryConfigMessage() +{ + esp_now_payload_data_t outgoingData{ENDT_SENSOR, ENPT_CONFIG}; + StaticJsonDocument json; + json["name"] = deviceBatteryName; + json["unit"] = 2; + json["type"] = HACT_BINARY_SENSOR; + json["class"] = HABSDC_BATTERY; + json["payload_on"] = "MID"; + json["payload_off"] = "HIGH"; + 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() +{ + esp_now_payload_data_t outgoingData{ENDT_SENSOR, ENPT_ATTRIBUTES}; + StaticJsonDocument json; + json["Type"] = "ESP-NOW Window Sensor"; + json["MCU"] = "ESP8266"; + json["MAC"] = myNet.getNodeMac(); + json["Firmware"] = firmware; + json["Library"] = myNet.getFirmwareVersion(); + 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); +}