├── .gitignore ├── Configuration.md ├── Flashing.md ├── README.md ├── WThermostat_1.38.bin ├── docs ├── BAC-002 │ ├── MCU-Commands_own_overview.odt │ ├── MCU-Commands_own_overview.pdf │ ├── bac-002-wifi-inside.png │ └── front_bac-002.jpg ├── CompilerOptions.PNG ├── DLX-LH01 │ └── MCU commands.txt ├── Flashing_Tywe3S_Detail.jpg ├── HY08WE │ ├── Display_HY02B05.png │ ├── Front_HY02B05.jpg │ ├── Guide-to-Interworking-with-the-Tuya-MCU.pdf │ ├── Manual_HY02B05.pdf │ ├── Manual_HY08WE.pdf │ ├── PCB layout.jpg │ └── PCB layout_2.jpg ├── MCU-Commands ET-81.txt ├── MCU-Commands HY08WE ├── MCU-Manual-ENG_Tuya.pdf ├── ME102H │ ├── MCU-Commands_ME102H.odt │ ├── PCB layout.jpg │ └── front_me102h.png ├── ME81H │ ├── ME81H_manual.jpg │ └── ME81H_pcb_layout.jpg ├── Manual BAC-002.pdf ├── Manual ET-81.pdf ├── Manual HY03.pdf ├── Setup_AP.png ├── Setup_Clock.png ├── Setup_Firmware.png ├── Setup_Main.png ├── Setup_Network_0.png ├── Setup_Network_1.png ├── Setup_Thermostat.png ├── Setup_Wifi_MQTT.png ├── StackTrace.PNG ├── Webthing_Complete.png ├── Webthing_Icon.png └── originalFirmwareME102H.bin ├── platformio.ini └── src ├── WClock.h ├── WThermostat.cpp ├── WThermostat.h ├── WThermostat_BAC_002_ALW.h ├── WThermostat_BHT_002_GBLW.h ├── WThermostat_CalypsoW.h ├── WThermostat_DLX_LH01.h ├── WThermostat_ET81W.h ├── WThermostat_HY08WE.h ├── WThermostat_ME102H.h ├── WThermostat_ME81H.h ├── WThermostat_MK70GBH.h ├── WThermostat_TEMPLATE.h └── WTuyaDevice.h /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | .project 3 | .pio 4 | .vscode 5 | .settings 6 | include 7 | test 8 | .gcc-flags.json 9 | .travis.yml 10 | .clang_complete 11 | .gcc-flags.json 12 | -------------------------------------------------------------------------------- /Configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration of Thermostat 2 | This document describes the first configuration steps after flashing of firmware. The firmware supports MQTT messaging and 3 | Mozilla Webthings. Both can be running parallel. 4 | 5 | Steps are in general: 6 | 1. Configure thermostat device (model selection) 7 | 2. Configure network access 8 | 3. Configure MQTT (optional) 9 | 4. Configure clock settings 10 | 11 | 12 | ## 1. Configure thermostat device (model selection) 13 | * The thermostat opens an Access Point when it's started first time after flashing. 14 | * The AccessPoint is named `Thermostat-Beca_xxxxxx`. Default password is `12345678` 15 | * After connection open `http://192.168.4.1` in a web browser 16 | * Goto 'Configure device' 17 | * Choose your thermostat model 18 | * Choose, if heating relay monitor is supported, hw modification need to work, see https://github.com/klausahrenberg/WThermostatBeca/issues/17#issuecomment-552078026 19 | * Choose work day and weekend start in your region 20 | * Press 'Save Configuration' and wait for reboot of device. 21 | 22 | ## 2. Configure Network access 23 | * Goto 'Configure network' 24 | * Fill out 'Things IDX' (unique id of your choice), 'SSID' (only 2G network), 'Password' for Wifi 25 | * Leave 'Support Mozilla WebThings' checked (recommended). If this is checked, the thermostat will always run the web interface. 26 | You don't need to use Webthings itself. 27 | * If you don't want to use MQTT, press 'Save Configuration' and wait for reboot of device. 28 | 29 | ## 3. Configure MQTT (optional) 30 | * Stay at page 'Network configuration' 31 | * Select checkbox 'Support MQTT', web page will extend 32 | * Fill out 'MQTT Server', 'MQTT User' (optional), 'MQTT password' (optional) and 'MQTT topic' 33 | * Press 'Save Configuration' and wait for reboot of device. 34 | * After restart the thermostat sends 2 MQTT messages to topics 'devices/thermostat' and 'devices/clock' to let you know the IP and MQTT topic of the device. The json message looks like: 35 | ```json 36 | { 37 | "url":"http://192.168.0.xxx/things/thermostat", 38 | "ip":"192.168.0.xxx", 39 | "topic":"/things/thermostat" 40 | } 41 | ``` 42 | ## 4. Configure clock settings 43 | Normally you don't need to change options here. 44 | * Open configuration page at `http:///config` 45 | * Goto 'Configure clock' 46 | * Modify 'NTP server' for time synchronisation 47 | * Modify 'Time zone request' for time offset synchronisation depending on your location 48 | -------------------------------------------------------------------------------- /Flashing.md: -------------------------------------------------------------------------------- 1 | ## Notice 2 | Modifying and flashing of devices is at your own risk. I'm not responsible for bricked or damaged devices. I strongly recommend a backup of original firmware before installing any other software. 3 | The thermostat is working independent from the Wifi-Module. That means, functionality of the thermostat itself will not and can't be changed. This firmware replaces only the communication part of the thermostat, which is handled by the ESP module. The firmware will partially work with other devices too. The Tuya devices has a serial communication standard (MCU commands) which is only different in parameters. Unknown commands will be forwarded to the MQTT server. 4 | 5 | ## Choose your way 6 | There are 2 options to flash the firmware on device: 7 | 1. Flashing device manually: Unplug and open the device, wire 4 cables and connect it to a programmer for ESP8266 8 | 2. Use tuya-convert for flashing. Not tested from my side, tested and committed by another user 9 | 10 | # Option 1: Flashing device manually 11 | ## 1. Check your device 12 | Compatible devices looks inside like this. On the right you can see the ESP8266 module (TYWE3S) 13 | 14 | ![thermostat inside](https://raw.githubusercontent.com/klausahrenberg/ThermostatBecaWifi/master/docs/bac-002-wifi-inside.png) 15 | 16 | ## 2. Connection to device for flashing 17 | There are many ways to get the physical connection to ESP module. I soldered the connections on the device for flashing. Maybe there is a more elegant way to do that. It's quite the same, if you try to flash any other Sonoff devices to Tasmota. So get the inspiration for flashing there: https://github.com/arendst/Sonoff-Tasmota/wiki 18 | 19 | Following connections were working for me (refer to ESP-12E pinout): 20 | - Red: ESP-VCC and ESP-EN connected to Programmer-VCC (3.3V) 21 | - Black: ESP-GND and ESP-GPIO15 connected to Programmer-GND 22 | - Green: ESP-RX connected to Programmer-TX 23 | - Yellow: ESP-TX connected to Programmer-RX 24 | - Blue right: ESP-GPIO0, must be connected with GND during power up 25 | - Blue left: ESP-Reset, connect to GND to restart the ESP 26 | 27 | ![Flashing connection](https://raw.githubusercontent.com/klausahrenberg/ThermostatBecaWifi/master/docs/Flashing_Tywe3S_Detail.jpg) 28 | 29 | ## 3. Remove the power supply from thermostat during all flashing steps 30 | Flasing will fail, if the thermostat is still powered during this operation. 31 | ## 4. Backup the original firmware 32 | Don't skip this. In case of malfunction you need the original firmware. Tasmota has also a great tutorial for the right esptool commands: https://github.com/arendst/Sonoff-Tasmota/wiki/Esptool. So the backup command is: 33 | 34 | ```esptool.py -p -b 460800 read_flash 0x00000 0x100000 originalFirmware1M.bin``` 35 | 36 | for example: 37 | 38 | ```esptool.py -p /dev/ttyUSB0 -b 460800 read_flash 0x00000 0x100000 originalFirmware1M.bin``` 39 | 40 | ## 5. Upload new firmware 41 | Get the ESP in programming mode first. 42 | Erase flash: 43 | 44 | ```esptool.py -p /dev/ttyUSB0 erase_flash``` 45 | 46 | After erasing the flash, get the ESP in programming mode again. 47 | Write firmware (1MB) 48 | 49 | ```esptool.py -p /dev/ttyUSB0 write_flash -fs 1MB 0x0 WThermostat_x.xx.bin``` 50 | 51 | ## 6. First run after uploading 52 | After you have successfully flashed the firmware to your thermostat you need to fully assemble it to proceed with its configuration. Disconnect all wires from your programmer and run the thermostat via normal power supply. The web page for configuration will not be shown, if the ESP is only connected to 3.3V of the programmer. Just a warning: Do NOT run the thermostat with 220V while being not fully assembled! 53 | 54 | # Option 2: Use tuya-convert 55 | This method does not require any kind of soldering or disassembly of the device. 56 | Some users were able to flash Beca Thermostats (BHT-002 and BHT-6000 also) with [tuya-convert](https://github.com/ct-Open-Source/tuya-convert). 57 | Follow the steps [here](https://github.com/ct-Open-Source/tuya-convert#procedure) to upload the firmware to your thermostat. 58 | You should download the binary from here and place it in the ```/files/``` folder before starting the flash procedure. 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ThermostatBecaWifi 2 | 3 | [![GitHub version](https://img.shields.io/github/release/klausahrenberg/WThermostatBeca.svg)](https://github.com/klausahrenberg/WThermostatBeca/releases/latest) 4 | 5 | Replaces original Tuya firmware on Beca thermostat with ESP8266 wifi module. The firmware is tested with following devices: 6 | * BHT-002, BHT-6000, BHT-3000 (floor heating) 7 | * AVATTO ME102H (Thermostat with LCD touch screen) 8 | * BAC-002 (heating, cooling, ventilation) 9 | * ET-81W 10 | * Floureon HY08WE 11 | * AVATTO ME81AH (floor heating, thanks to @lozb36 for implementation) 12 | * Minco Heat MK70GB-H (floor heating, thanks to @indimouse for implementation) 13 | * VH Control Calypso-W 14 | 15 | ![BAC-002](docs/BAC-002/front_bac-002.jpg) ![ME102H](docs/ME102H/front_me102h.png) 16 | 17 | ## Function support since 1.20 (beta status) 18 | For productive use only stable version 1.19. Version 1.20x_beta is rewritten to make the support of different models more easily in future. For model BHT-002 and ME102H the new version is tested and should work reliable already. 19 | 20 | | Function | BHT-002 BHT-6000 BHT-3000 | ME102H | BAC-002 | ET-81W | HY08WE | ME81AH | MK70GBH | Calypso-W* | 21 | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | 22 | | actual temperature | yes | yes | yes | yes | yes | yes | yes | yes | 23 | | target temperature | yes | yes | yes | yes | yes | yes | yes | yes | 24 | | floor temperature | yes | yes | no | yes | yes | yes | no | yes | 25 | | schedules mode | auto off | auto off | auto off | holiday auto hold | off auto holiday hold | auto off | off auto hold | holiday auto hold | 26 | | locked | yes | yes | yes | yes | yes | yes | yes | yes 27 | | schedules | 18 | 8 | 18 | no | no | 8 | 8 | no 28 | | system mode | no | no | cool, heat, fan | no | no | heat, cool, fan | no | no 29 | | fan mode | no | no | auto, high, med, low | no | no | no | no | no 30 | 31 | '* needs testing 32 | 33 | ## Features 34 | * No Tuya cloud connection anymore 35 | * Enables thermostat to communicate via MQTT and/or Mozilla Webthings 36 | * Configuration of connection and device parameters via web interface 37 | * NTP and time zone synchronisation to set the clock of thermostat 38 | * Reading and setting of all parameters via MQTT 39 | * Reading and setting of main parameters via Webthing 40 | * Only BHT-002-GBLW: actualFloorTemperature (external temperature sensor) 41 | * Only BAC-002-ALW: fanSpeed:auto|low|medium|high; systemMode:cooling|heating|ventilation 42 | * Reading and setting of time schedules via MQTT 43 | ## Installation 44 | To install the firmware, follow instructions here: 45 | https://github.com/klausahrenberg/WThermostatBeca/blob/master/Flashing.md 46 | ## Initial configuration 47 | To setup the device model, network options and other parameters, follow instrcution here: 48 | https://github.com/klausahrenberg/WThermostatBeca/blob/master/Configuration.md 49 | After initial setup, the device configuration is still available via `http:///config` 50 | ## Integration in Webthings 51 | This firmware supports Mozilla Webthings directly. With Webthings you can control the device via the Gateway - inside and also outside of your home network. No clunky VPN, dynDNS solutions needed to access your home devices. I recommend to run the gateway in parallel to an MQTT server and for example Node-Red. Via MQTT you can control the device completely and logic can be done by Node-Red. Webthings is used for outside control of main parameters. 52 | Add the device to the gateway via '+' icon. After that you have the new nice and shiny icon in the dashboard: 53 | ![webthing_icon](https://github.com/klausahrenberg/WThermostatBeca/blob/master/docs/Webthing_Icon.png) 54 | The icon shows the actual temperature and heating state. 55 | There is also a detailed view available: 56 | 57 | 58 | ## Json structure 59 | Firmware provides 3 different json messages: 60 | 1. State report 61 | 2. Schedules 62 | 3. Device information (at start of device to let you know the topics and ip) 63 | ### 1. State report 64 | **MQTT:** State report is provided every 5 minutes, at change of a parameter or at request via message with empty payload to `/thermostat/` 65 | **Webthing:** State report can be requested by: `http:///things/thermostat/properties` 66 | ```json 67 | { 68 | "idx":"thermostat_beca", 69 | "ip":"192.168.x.x", 70 | "firmware":"x.xx", 71 | "temperature":21.5, 72 | "targetTemperature":23, 73 | "deviceOn":true, 74 | "schedulesMode":"off|auto", 75 | "ecoMode":false, 76 | "locked":false, 77 | "state":"off|heating", //only_available,_if_hardware_modified 78 | "floorTemperature":20, //only_BHT-002-GBLW 79 | "fanMode":"auto|low|medium|high", //only_BAC-002-ALW 80 | "systemMode":"cool|heat|fan_only" //only_BAC-002-ALW 81 | } 82 | ``` 83 | ### 2. Schedules 84 | **MQTT:** Request actual schedules via message with empty payload to `/thermostat//schedules` 85 | **Webthing:** State report can be requested by: `http:///things/thermostat/schedules` 86 | ```json 87 | { 88 | "w1h":"06:00", 89 | "w1t":20, 90 | "w2h":"08:00", 91 | "w2t":15, 92 | ... 93 | "w6h":"22:00", 94 | "w6t":15, 95 | "a1h":"06:00", 96 | ... 97 | "a6t":15, 98 | "u1h":"06:00", 99 | ... 100 | "u6t":15 101 | } 102 | ``` 103 | Schedules can be modified via MQTT. Send a payload structure with all schedules or only the parts you want to modify to 104 | `/thermostat//schedules`. 105 | ### 3. Device information 106 | **MQTT:** At start of device to let you know the topics and ip to `devices/thermostat` 107 | **Webthing:** n.a. 108 | ```json 109 | { 110 | "url":"http://192.168.x.x/things/thermostat", 111 | "ip":"192.168.x.x", 112 | "stateTopic":"/thermostat/" 113 | "setTopic":"/thermostat/" 114 | } 115 | ``` 116 | ## Modifying parameters via MQTT 117 | Send a complete json structure with changed parameters to `/thermostat/`, e.g. `beca/thermostat/set`. Alternatively you can set single values, modify the topic to `/thermostat//`, e.g. `beca/thermostat/set/deviceOn`. The payload contains the new value. 118 | Send a json with changed schedules to `/things/thermostat/schedules`. 119 | 120 | ### Build this firmware from source 121 | For build from sources you can use the Atom IDE (recommended), Arduino IDE or other. All sources needed are inside the folder 'WThermostat' and my other library: 122 | https://github.com/klausahrenberg/WAdapter. 123 | Additionally you will need some other libraries, which you can find listed here: 124 | https://github.com/klausahrenberg/WThermostatBeca/blob/master/WThermostat/platformio.ini 125 | -------------------------------------------------------------------------------- /WThermostat_1.38.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/WThermostat_1.38.bin -------------------------------------------------------------------------------- /docs/BAC-002/MCU-Commands_own_overview.odt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/BAC-002/MCU-Commands_own_overview.odt -------------------------------------------------------------------------------- /docs/BAC-002/MCU-Commands_own_overview.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/BAC-002/MCU-Commands_own_overview.pdf -------------------------------------------------------------------------------- /docs/BAC-002/bac-002-wifi-inside.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/BAC-002/bac-002-wifi-inside.png -------------------------------------------------------------------------------- /docs/BAC-002/front_bac-002.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/BAC-002/front_bac-002.jpg -------------------------------------------------------------------------------- /docs/CompilerOptions.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/CompilerOptions.PNG -------------------------------------------------------------------------------- /docs/DLX-LH01/MCU commands.txt: -------------------------------------------------------------------------------- 1 | 2 | //device On 3 | 55 aa 03 07 00 05 01 01 00 01 00 4 | 55 aa 03 07 00 05 01 01 00 01 00 5 | 6 | 7 | verbose: MCU: targetTemperature_x02:5.00/5.00; 55 aa 03 07 00 08 >02 02 00 04 00 00 00 0a 8 | 9 | verbose: MCU: manualMode_x04; 55 aa 03 07 00 05 >04 04 00 01 00 10 | 11 | warning: MCU: unknown; 55 aa 03 07 00 05 >13 04 00 01 00 12 | 13 | warning: MCU: unknown; 55 aa 03 07 00 05 >07 01 00 01 00 14 | 15 | warning: MCU: unknown; 55 aa 03 07 00 05 >0b 04 00 01 00 16 | 17 | warning: MCU: unknown; 55 aa 03 07 00 08 >0c 02 00 04 00 00 00 00 18 | 19 | warning: MCU: unknown; 55 aa 03 07 00 05 >0d 05 00 01 00 20 | 21 | warning: MCU: unknown; 55 aa 03 07 00 05 >0e 04 00 01 00 22 | 23 | verbose: MCU: actualTemperature_x03; 55 aa 03 07 00 08 >03 02 00 04 00 00 00 11 24 | 25 | trace: MCU: deviceOn_x01; 55 aa 03 07 00 05 >01 01 00 01 01 26 | 27 | verbose: MCU: targetTemperature_x02:5.00/5.00; 55 aa 03 07 00 08 >02 02 00 04 00 00 00 0a 28 | 29 | verbose: MCU: manualMode_x04; 55 aa 03 07 00 05 >04 04 00 01 00 30 | 31 | warning: MCU: unknown; 55 aa 03 07 00 05 >13 04 00 01 00 32 | 33 | warning: MCU: unknown; 55 aa 03 07 00 05 >07 01 00 01 00 34 | 35 | warning: MCU: unknown;55 aa 03 07 00 05 0b 04 00 01 00 36 | 37 | warning: MCU: unknown;55 aa 03 07 00 08 0c 02 00 04 00 00 00 00 38 | 39 | warning: MCU: unknown;55 aa 03 07 00 05 0d 05 00 01 00 40 | 41 | warning: MCU: unknown;55 aa 03 07 00 05 0e 04 00 01 01 42 | 43 | verbose: MCU: actualTemperature_x03;55 aa 03 07 00 08 03 02 00 04 00 00 00 11 44 | 45 | trace: MCU: deviceOn_x01;55 aa 03 07 00 05 01 01 00 01 00 46 | 47 | 48 | verbose: MCU: targetTemperature_x02:5.00/5.00;55 aa 03 07 00 08 02 02 00 04 00 00 00 0a 49 | 50 | verbose: MCU: manualMode_x04;55 aa 03 07 00 05 04 04 00 01 00 51 | 52 | warning: MCU: unknown;55 aa 03 07 00 05 13 04 00 01 00 53 | 54 | warning: MCU: unknown;55 aa 03 07 00 05 07 01 00 01 00 55 | 56 | warning: MCU: unknown;55 aa 03 07 00 05 0b 04 00 01 00 57 | 58 | warning: MCU: unknown;55 aa 03 07 00 08 0c 02 00 04 00 00 00 00 59 | 60 | warning: MCU: unknown;55 aa 03 07 00 05 0d 05 00 01 00 61 | 62 | warning: MCU: unknown;55 aa 03 07 00 05 0e 04 00 01 00 63 | 64 | verbose: MCU: actualTemperature_x03;55 aa 03 07 00 08 03 02 00 04 00 00 00 11 65 | 66 | trace: MCU: deviceOn_x01;55 aa 03 07 00 05 01 01 00 01 01 67 | 68 | 69 | verbose: MCU: targetTemperature_x02:5.00/5.00;55 aa 03 07 00 08 02 02 00 04 00 00 00 0a 70 | 71 | verbose: MCU: manualMode_x04;55 aa 03 07 00 05 04 04 00 01 00 72 | 73 | warning: MCU: unknown;55 aa 03 07 00 05 13 04 00 01 00 74 | 75 | warning: MCU: unknown;55 aa 03 07 00 05 07 01 00 01 00 76 | 77 | warning: MCU: unknown;55 aa 03 07 00 05 0b 04 00 01 00 78 | 79 | warning: MCU: unknown;55 aa 03 07 00 08 0c 02 00 04 00 00 00 00 80 | 81 | warning: MCU: unknown;55 aa 03 07 00 05 0d 05 00 01 00 82 | 83 | warning: MCU: unknown;55 aa 03 07 00 05 0e 04 00 01 01 84 | 85 | verbose: MCU: actualTemperature_x03;55 aa 03 07 00 08 03 02 00 04 00 00 00 11 86 | 87 | 88 | trace: MCU: targetTemperature_x02:4.50/4.50;55 aa 03 07 00 08 02 02 00 04 00 00 00 09 89 | 90 | trace: MCU: targetTemperature_x02:4.00/4.00;55 aa 03 07 00 08 02 02 00 04 00 00 00 08 91 | 92 | 93 | trace: MCU: targetTemperature_x02:3.50/3.50;55 aa 03 07 00 08 02 02 00 04 00 00 00 07 94 | 95 | verbose: MCU: processSerial;55 aa 03 07 00 08 02 02 00 04 00 00 00 08 96 | 97 | trace: MCU: targetTemperature_x02:4.00/4.00;55 aa 03 07 00 08 02 02 00 04 00 00 00 08 98 | 99 | verbose: MCU: processSerial;55 aa 03 07 00 08 02 02 00 04 00 00 00 09 100 | 101 | trace: MCU: targetTemperature_x02:4.50/4.50;55 aa 03 07 00 08 02 02 00 04 00 00 00 09 102 | 103 | verbose: MCU: processSerial;55 aa 03 07 00 08 02 02 00 04 00 00 00 0a 104 | 105 | trace: MCU: targetTemperature_x02:5.00/5.00;55 aa 03 07 00 08 02 02 00 04 00 00 00 0a 106 | 107 | verbose: MCU: processSerial;55 aa 03 07 00 08 02 02 00 04 00 00 00 0b 108 | 109 | 110 | trace: MCU: targetTemperature_x02:5.50/5.50;55 aa 03 07 00 08 02 02 00 04 00 00 00 0b 111 | 112 | verbose: MCU: processSerial;55 aa 03 00 00 01 01 113 | verbose: MCU: processSerial;55 aa 03 07 00 05 04 04 00 01 01 114 | verbose: MCU: manualMode_x04;55 aa 03 07 00 05 04 04 00 01 01 115 | verbose: MCU: processSerial;55 aa 03 07 00 05 0e 04 00 01 00 116 | warning: MCU: unknown;55 aa 03 07 00 05 0e 04 00 01 00 117 | verbose: MCU: processSerial;55 aa 03 07 00 05 04 04 00 01 00 118 | trace: Manual Mode newChanged to off 119 | trace: MCU: manualMode_x04;55 aa 03 07 00 05 04 04 00 01 00 120 | notice: ReceivedSerial/Changed 121 | trace: sending heartBeatCommand 122 | trace: Heap Info HeartBeat: MaxFree: 20016 123 | trace: commandCharsToSerial: 55 aa 00 00 00 00, ChckSum 0xff 124 | verbose: MCU: processSerial;55 aa 03 07 00 05 0e 04 00 01 01 125 | warning: MCU: unknown;55 aa 03 07 00 05 0e 04 00 01 01 126 | verbose: MCU: processSerial;55 aa 03 00 00 01 01 127 | trace: sending heartBeatCommand 128 | trace: Heap Info HeartBeat: MaxFree: 20016 129 | trace: commandCharsToSerial: 55 aa 00 00 00 00, ChckSum 0xff 130 | verbose: MCU: processSerial;55 aa 03 00 00 01 01 131 | notice: Notify interval is up -> Device state changed... 132 | notice: Device state changed -> send device state... home/soggiorno/stat/things/network/properties 133 | notice: Send actual device state via MQTT home/soggiorno/stat/things/network/properties 134 | verbose: MQTT sent 135 | trace: Received MQTT callback: 'home/soggiorno/cmnd/things/thermostat/properties/mode'->'off' 136 | trace: look for device id 'thermostat' 137 | notice: Set property 'mode' for device thermostat 138 | trace: modeToMcu off 139 | trace: commandCharsToSerial: 55 aa 00 06 00 05 01 01 00 01 00, ChckSum 0x0d 140 | trace: schedulesModeToMcu off 141 | trace: commandCharsToSerial: 55 aa 00 06 00 05 04 04 00 01 01, ChckSum 0x14 142 | trace: Property updated. 143 | trace: sending heartBeatCommand 144 | trace: Heap Info HeartBeat: MaxFree: 19784 145 | trace: commandCharsToSerial: 55 aa 00 00 00 00, ChckSum 0xff 146 | verbose: MCU: processSerial;55 aa 03 00 00 01 01 147 | trace: Received MQTT callback: 'home/soggiorno/cmnd/things/thermostat/properties/mode'->'off' 148 | trace: look for device id 'thermostat' 149 | notice: Set property 'mode' for device thermostat 150 | trace: Property updated. 151 | verbose: MQTT... 'home/soggiorno/stat/things/thermostat/mode' 152 | verbose: MQTT .. 'off' 153 | verbose: MQTT connected... 154 | verbose: MQTT sent. Topic: 'home/soggiorno/stat/things/thermostat/mode' 155 | trace: sending heartBeatCommand 156 | trace: Heap Info HeartBeat: MaxFree: 19784 157 | trace: commandCharsToSerial: 55 aa 00 00 00 00, ChckSum 0xff 158 | verbose: MCU: processSerial;55 aa 03 00 00 01 01 159 | trace: Received MQTT callback: 'home/soggiorno/cmnd/things/thermostat/properties/mode'->'heat' 160 | trace: look for device id 'thermostat' 161 | notice: Set property 'mode' for device thermostat 162 | trace: modeToMcu heat 163 | trace: commandCharsToSerial: 55 aa 00 06 00 05 01 01 00 01 01, ChckSum 0x0e 164 | trace: Property updated. 165 | verbose: MQTT... 'home/soggiorno/stat/things/thermostat/mode' 166 | verbose: MQTT .. 'heat' 167 | verbose: MCU: processSerial;55 aa 03 07 00 05 01 01 00 01 01 168 | verbose: MCU: processSerial;55 aa 03 07 00 08 02 02 00 04 00 00 00 0b 169 | verbose: MCU: targetTemperature_x02:5.50/5.50;55 aa 03 07 00 08 02 02 00 04 00 00 00 0b 170 | verbose: MCU: processSerial;55 aa 03 07 00 05 04 04 00 01 00 171 | trace: Manual Mode newChanged to off 172 | trace: modeToMcu auto 173 | verbose: MCU: processSerial;55 aa 03 07 00 05 13 04 00 01 00 174 | warning: MCU: unknown;55 aa 03 07 00 05 13 04 00 01 00 175 | verbose: MCU: processSerial;55 aa 03 07 00 05 07 01 00 01 00 176 | warning: MCU: unknown;55 aa 03 07 00 05 07 01 00 01 00 177 | verbose: MCU: processSerial;55 aa 03 07 00 05 0b 04 00 01 00 178 | warning: MCU: unknown;55 aa 03 07 00 05 0b 04 00 01 00 179 | verbose: MCU: processSerial;55 aa 03 07 00 08 0c 02 00 04 00 00 00 00 180 | warning: MCU: unknown;55 aa 03 07 00 08 0c 02 00 04 00 00 00 00 181 | verbose: MCU: processSerial;55 aa 03 07 00 05 0d 05 00 01 00 182 | warning: MCU: unknown;55 aa 03 07 00 05 0d 05 00 01 00 183 | verbose: MCU: processSerial;55 aa 03 07 00 05 0e 04 00 01 01 184 | warning: MCU: unknown;55 aa 03 07 00 05 0e 04 00 01 01 185 | verbose: MCU: processSerial;55 aa 03 07 00 08 03 02 00 04 00 00 00 11 186 | verbose: MCU: actualTemperature_x03;55 aa 03 07 00 08 03 02 00 04 00 00 00 11 187 | trace: sending heartBeatCommand 188 | trace: Heap Info HeartBeat: MaxFree: 19784 189 | trace: commandCharsToSerial: 55 aa 00 00 00 00, ChckSum 0xff 190 | verbose: MCU: processSerial;55 aa 03 00 00 01 01 191 | notice: Notify interval is up -> Device state changed... 192 | notice: Device state changed -> send device state... home/soggiorno/stat/things/network/properties 193 | notice: Send actual device state via MQTT home/soggiorno/stat/things/network/properties 194 | verbose: MQTT sent 195 | trace: Received MQTT callback: 'home/soggiorno/cmnd/things/thermostat/properties/mode'->'off' 196 | trace: look for device id 'thermostat' 197 | notice: Set property 'mode' for device thermostat 198 | trace: modeToMcu off 199 | trace: commandCharsToSerial: 55 aa 00 06 00 05 01 01 00 01 00, ChckSum 0x0d 200 | trace: schedulesModeToMcu off 201 | trace: commandCharsToSerial: 55 aa 00 06 00 05 04 04 00 01 01, ChckSum 0x14 202 | trace: Property updated. 203 | trace: sending heartBeatCommand 204 | trace: Heap Info HeartBeat: MaxFree: 19784 205 | trace: commandCharsToSerial: 55 aa 00 00 00 00, ChckSum 0xff 206 | verbose: MCU: processSerial;55 aa 03 00 00 01 01 207 | trace: Received MQTT callback: 'home/soggiorno/cmnd/things/thermostat/properties/mode'->'off' 208 | trace: look for device id 'thermostat' 209 | notice: Set property 'mode' for device thermostat 210 | trace: Property updated. 211 | verbose: MQTT... 'home/soggiorno/stat/things/thermostat/mode' 212 | verbose: MQTT .. 'off' 213 | verbose: MQTT connected... 214 | verbose: MQTT sent. Topic: 'home/soggiorno/stat/things/thermostat/mode' 215 | trace: sending heartBeatCommand 216 | trace: Heap Info HeartBeat: MaxFree: 19784 217 | trace: commandCharsToSerial: 55 aa 00 00 00 00, ChckSum 0xff 218 | verbose: MCU: processSerial;55 aa 03 00 00 01 01 219 | trace: sending heartBeatCommand 220 | trace: Heap Info HeartBeat: MaxFree: 19784 221 | trace: commandCharsToSerial: 55 aa 00 00 00 00, ChckSum 0xff 222 | verbose: MCU: processSerial;55 aa 03 00 00 01 01 223 | notice: Notify interval is up -> Device state changed... 224 | notice: Device state changed -> send device state... home/soggiorno/stat/things/network/properties 225 | notice: Send actual device state via MQTT home/soggiorno/stat/things/network/properties 226 | verbose: MQTT sent 227 | trace: sending heartBeatCommand 228 | trace: Heap Info HeartBeat: MaxFree: 19784 229 | trace: commandCharsToSerial: 55 aa 00 00 00 00, ChckSum 0xff 230 | verbose: MCU: processSerial;55 aa 03 00 00 01 01 231 | trace: sending heartBeatCommand 232 | trace: Heap Info HeartBeat: MaxFree: 19784 233 | trace: commandCharsToSerial: 55 aa 00 00 00 00, ChckSum 0xff 234 | verbose: MCU: processSerial;55 aa 03 00 00 01 01 235 | trace: Received MQTT callback: 'home/soggiorno/cmnd/things/thermostat/properties/targetTemperature'->'6.5' 236 | trace: look for device id 'thermostat' 237 | notice: Set property 'targetTemperature' for device thermostat 238 | trace: Got new targetTemperature 239 | trace: onChangeTargetTemperature, temp: 6.50 240 | notice: Set target Temperature (manual mode) to 6.50 241 | trace: commandCharsToSerial: 55 aa 00 06 00 08 02 02 00 04 00 00 00 0d, ChckSum 0x22 242 | trace: Property updated. 243 | verbose: MCU: processSerial;55 aa 03 07 00 08 02 02 00 04 00 00 00 0d 244 | verbose: MCU: targetTemperature_x02:6.50/6.50;55 aa 03 07 00 08 02 02 00 04 00 00 00 0d 245 | verbose: MCU: processSerial;55 aa 03 07 00 05 04 04 00 01 00 246 | trace: Manual Mode newChanged to off 247 | trace: sending heartBeatCommand 248 | trace: Heap Info HeartBeat: MaxFree: 19784 249 | trace: commandCharsToSerial: 55 aa 00 00 00 00, ChckSum 0xff 250 | verbose: MCU: processSerial;55 aa 03 00 00 01 01 251 | notice: Notify interval is up -> Device state changed... 252 | notice: Device state changed -> send device state... home/soggiorno/stat/things/network/properties 253 | notice: Send actual device state via MQTT home/soggiorno/stat/things/network/properties 254 | verbose: MQTT sent 255 | trace: Received MQTT callback: 'home/soggiorno/cmnd/things/thermostat/properties/targetTemperature'->'5.0' 256 | trace: look for device id 'thermostat' 257 | notice: Set property 'targetTemperature' for device thermostat 258 | trace: Got new targetTemperature 259 | trace: Got new targetTemperature: Switching from Schdule to Manual 260 | trace: onChangeTargetTemperature, temp: 5.00 261 | notice: Set target Temperature (manual mode) to 5.00 262 | trace: commandCharsToSerial: 55 aa 00 06 00 08 02 02 00 04 00 00 00 0a, ChckSum 0x1f 263 | verbose: MCU: processSerial;55 aa 03 07 00 05 04 04 00 01 00 264 | trace: Manual Mode newChanged to off 265 | trace: sending heartBeatCommand 266 | trace: Heap Info HeartBeat: MaxFree: 19784 267 | trace: commandCharsToSerial: 55 aa 00 00 00 00, ChckSum 0xff 268 | verbose: MCU: processSerial;55 aa 03 00 00 01 01 269 | verbose: MCU: processSerial;55 aa 03 07 00 08 03 02 00 04 00 00 00 12 270 | trace: MCU: actualTemperature_x03;55 aa 03 07 00 08 03 02 00 04 00 00 00 12 271 | notice: ReceivedSerial/Changed 272 | trace: sending heartBeatCommand 273 | trace: Heap Info HeartBeat: MaxFree: 19784 274 | trace: commandCharsToSerial: 55 aa 00 00 00 00, ChckSum 0xff 275 | verbose: MCU: processSerial;55 aa 03 00 00 01 01 276 | trace: sending heartBeatCommand 277 | trace: Heap Info HeartBeat: MaxFree: 19784 278 | trace: commandCharsToSerial: 55 aa 00 00 00 00, ChckSum 0xff 279 | verbose: MCU: processSerial;55 aa 03 00 00 01 01 280 | notice: Notify interval is up -> Device state changed... 281 | notice: Device state changed -> send device state... home/soggiorno/stat/things/network/properties 282 | notice: Send actual device state via MQTT home/soggiorno/stat/things/network/properties 283 | verbose: MQTT sent 284 | trace: sending heartBeatCommand 285 | trace: Heap Info HeartBeat: MaxFree: 19784 286 | trace: commandCharsToSerial: 55 aa 00 00 00 00, ChckSum 0xff 287 | verbose: MCU: processSerial;55 aa 03 00 00 01 01 288 | WebSocket Connection Closed! 289 | -------------------------------------------------------------------------------- /docs/Flashing_Tywe3S_Detail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/Flashing_Tywe3S_Detail.jpg -------------------------------------------------------------------------------- /docs/HY08WE/Display_HY02B05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/HY08WE/Display_HY02B05.png -------------------------------------------------------------------------------- /docs/HY08WE/Front_HY02B05.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/HY08WE/Front_HY02B05.jpg -------------------------------------------------------------------------------- /docs/HY08WE/Guide-to-Interworking-with-the-Tuya-MCU.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/HY08WE/Guide-to-Interworking-with-the-Tuya-MCU.pdf -------------------------------------------------------------------------------- /docs/HY08WE/Manual_HY02B05.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/HY08WE/Manual_HY02B05.pdf -------------------------------------------------------------------------------- /docs/HY08WE/Manual_HY08WE.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/HY08WE/Manual_HY08WE.pdf -------------------------------------------------------------------------------- /docs/HY08WE/PCB layout.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/HY08WE/PCB layout.jpg -------------------------------------------------------------------------------- /docs/HY08WE/PCB layout_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/HY08WE/PCB layout_2.jpg -------------------------------------------------------------------------------- /docs/MCU-Commands ET-81.txt: -------------------------------------------------------------------------------- 1 | 2 | 0x01 deviceOn false:0x00|true:0x01 3 | 0x02 targetTemperature /10 4 | 0x03 schedulesMode holiday:0x00|auto:0x01|hold:0x02 5 | 0x05 floorTemperature °C /10 6 | 0x06 + floorTemperature F 7 | 0x07 ? 3b 59 minutes? 8 | 0x08 temperature °C /10 9 | 0x09 + temperature F 10 | 0x0a + °C to F selection °C:0x00|F:0x01 11 | 0x0b ? 00 byte value 12 | 0x0c + holidayDays in days 13 | 0x0d + holdTime in minutes 14 | 0x0e + sensor selection room:0x00|floor:0x01|both:0x02 15 | 0x0f ? 0 int value 16 | 0x10 ? 0 int value 17 | 0x11 ? 0 int value 18 | 0x12 ? 0 int value 19 | 0x13 ? 101 257 int value / control backlash C? 20 | 0x14 ? 101 257 int value / control backlash F? 21 | 0x15 ? 100 256 int value 22 | 0x16 + power rating watt int value 23 | 0x17 ? 8 int value 24 | 0x19 ? 100 256 int value something at C and F change 101 - 100 25 | 26 | floor limit temperature 27 | delay output (seconds) 28 | control backlash (temp tolerance) in C 29 | backlight brightness 30 | 31 | {"unknownMCU":"55 aa 01 07 00 08 06 02 00 04 00 00 01 09"} 32 | {"unknownMCU":"55 aa 01 07 00 08 07 02 00 04 00 00 00 3b"} 33 | {"unknownMCU":"55 aa 01 07 00 08 08 02 00 04 00 00 00 c8"} 34 | {"unknownMCU":"55 aa 01 07 00 08 09 02 00 04 00 00 00 c8"} 35 | {"unknownMCU":"55 aa 01 07 00 05 0a 01 00 01 00"} 36 | {"unknownMCU":"55 aa 01 07 00 05 0b 01 00 01 00"} 37 | {"unknownMCU":"55 aa 01 07 00 08 0c 02 00 04 00 00 00 00"} 38 | {"unknownMCU":"55 aa 01 07 00 08 0d 02 00 04 00 00 00 00"} 39 | {"unknownMCU":"55 aa 01 07 00 05 0e 04 00 01 02"} 40 | {"unknownMCU":"55 aa 01 07 00 08 0f 02 00 04 00 00 00 00"} 41 | {"unknownMCU":"55 aa 01 07 00 08 10 02 00 04 00 00 00 00"} 42 | {"unknownMCU":"55 aa 01 07 00 08 11 02 00 04 00 00 00 00"} 43 | {"unknownMCU":"55 aa 01 07 00 08 12 02 00 04 00 00 00 00"} 44 | {"unknownMCU":"55 aa 01 07 00 05 13 04 00 01 01"} 45 | {"unknownMCU":"55 aa 01 07 00 05 14 04 00 01 01"} 46 | {"unknownMCU":"55 aa 01 07 00 05 15 01 00 01 00"} 47 | {"unknownMCU":"55 aa 01 07 00 08 16 02 00 04 00 00 00 00"} 48 | {"unknownMCU":"55 aa 01 07 00 08 17 02 00 04 00 00 00 02"} 49 | {"unknownMCU":"55 aa 01 07 00 05 19 04 00 01 00"} 50 | {"unknownMCU":"55 aa 01 07 00 08 08 02 00 04 00 00 00 cd"} 51 | {"unknownMCU":"55 aa 01 07 00 08 08 02 00 04 00 00 00 c8"} 52 | {"unknownMCU":"55 aa 01 07 00 08 05 02 00 04 00 00 01 04"} 53 | {"unknownMCU":"55 aa 01 07 00 08 08 02 00 04 00 00 00 cd"} 54 | {"unknownMCU":"55 aa 01 07 00 08 08 02 00 04 00 00 00 c8"} 55 | {"unknownMCU":"55 aa 01 07 00 08 05 02 00 04 00 00 00 ff"} 56 | {"unknownMCU":"55 aa 01 07 00 08 08 02 00 04 00 00 00 cd"} 57 | {"unknownMCU":"55 aa 01 07 00 08 08 02 00 04 00 00 00 c8"} 58 | {"unknownMCU":"55 aa 01 07 00 08 05 02 00 04 00 00 00 fa"} 59 | {"unknownMCU":"55 aa 01 07 00 08 08 02 00 04 00 00 00 cd"} 60 | {"unknownMCU":"55 aa 01 07 00 08 08 02 00 04 00 00 00 c8"} 61 | 62 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 91"} 63 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 8c"} 64 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 87"} 65 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 82"} 66 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 7d"} 67 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 82"} 68 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 87"} 69 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 8c"} 70 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 91"} 71 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 96"} 72 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 9b"} 73 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 a0"} 74 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 a5"} 75 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 aa"} 76 | {"unknownMCU":"55 aa 01 07 00 08 08 02 00 04 00 00 00 cd"} 77 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 af"} 78 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 b4"} 79 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 b9"} 80 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 be"} 81 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 c3"} 82 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 c8"} 83 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 cd"} 84 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 d2"} 85 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 d7"} 86 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 dc"} 87 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 e1"} 88 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 e6"} 89 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 eb"} 90 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 f0"} 91 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 f5"} 92 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 fa"} 93 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 ff"} 94 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 01 04"} 95 | 96 | Turning power off and on: 97 | 98 | {"unknownMCU":"55 aa 01 07 00 08 08 02 00 04 00 00 00 c8"} 99 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 32"} 100 | {"deviceOn_x01":"55 aa 01 07 00 05 01 01 00 01 00"} 101 | {"unknownMCU":"55 aa 01 07 00 05 19 04 00 01 00"} 102 | {"targetTemperature_x02":"55 aa 01 07 00 08 02 02 00 04 00 00 00 96"} 103 | {"deviceOn_x01":"55 aa 01 07 00 05 01 01 00 01 01"} 104 | {"unknownMCU":"55 aa 01 07 00 08 08 02 00 04 00 00 00 cd"} 105 | {"unknownMCU":"55 aa 01 07 00 08 05 02 00 04 00 00 00 f5"} 106 | {"unknownMCU":"55 aa 01 07 00 08 05 02 00 04 00 00 00 f0"} 107 | {"unknownMCU":"55 aa 01 07 00 08 05 02 00 04 00 00 00 f5"} 108 | -------------------------------------------------------------------------------- /docs/MCU-Commands HY08WE: -------------------------------------------------------------------------------- 1 | 0x01 deviceOn false:0x00|true:0x01 2 | 0x02 targetTemperature /10 3 | 0x67 4 | 0x68 5 | 0x69 6 | 0x0c 7 | 0x6a 8 | 0x6b 9 | 0x6c 10 | 0x6d 11 | 0x6e 12 | 0x6f 13 | 0x70 14 | 0x71 15 | 0x72 16 | 0x73 17 | 0x74 18 | 0x75 19 | 0x76 20 | 21 | "{"unknown":"55 aa 01 07 00 05 0c 05 00 01 00"}" 22 | "{"unknown":"55 aa 01 07 00 08 67 02 00 04 00 00 00 00"}" 23 | "{"unknown":"55 aa 01 07 00 08 68 02 00 04 00 00 00 03"}" 24 | "{"unknown":"55 aa 01 07 00 08 69 02 00 04 00 00 00 14"}" //20 25 | "{"unknown":"55 aa 01 07 00 05 6a 01 00 01 01"}" 26 | "{"unknown":"55 aa 01 07 00 05 6b 01 00 01 01"}" 27 | "{"unknown":"55 aa 01 07 00 05 6c 01 00 01 01"}" 28 | "{"unknown":"55 aa 01 07 00 08 6d 02 00 04 ff ff ff f6"}" 29 | "{"unknown":"55 aa 01 07 00 08 6e 02 00 04 00 00 00 0a"}" //10 30 | "{"unknown":"55 aa 01 07 00 08 6f 02 00 04 00 00 00 02"}" //02 31 | "{"unknown":"55 aa 01 07 00 08 70 02 00 04 00 00 00 2d"}" //45 32 | "{"unknown":"55 aa 01 07 00 08 71 02 00 04 00 00 00 05"}" //05 33 | "{"unknown":"55 aa 01 07 00 08 72 02 00 04 00 00 00 23"}" //35 34 | "{"unknown":"55 aa 01 07 00 08 73 02 00 04 00 00 00 05"}" //05 35 | "{"unknown":"55 aa 01 07 00 05 74 04 00 01 00"}" 36 | "{"unknown":"55 aa 01 07 00 05 75 04 00 01 00"}" 37 | "{"unknown":"55 aa 01 07 00 05 76 04 00 01 00"}" 38 | -------------------------------------------------------------------------------- /docs/MCU-Manual-ENG_Tuya.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/MCU-Manual-ENG_Tuya.pdf -------------------------------------------------------------------------------- /docs/ME102H/MCU-Commands_ME102H.odt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/ME102H/MCU-Commands_ME102H.odt -------------------------------------------------------------------------------- /docs/ME102H/PCB layout.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/ME102H/PCB layout.jpg -------------------------------------------------------------------------------- /docs/ME102H/front_me102h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/ME102H/front_me102h.png -------------------------------------------------------------------------------- /docs/ME81H/ME81H_manual.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/ME81H/ME81H_manual.jpg -------------------------------------------------------------------------------- /docs/ME81H/ME81H_pcb_layout.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/ME81H/ME81H_pcb_layout.jpg -------------------------------------------------------------------------------- /docs/Manual BAC-002.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/Manual BAC-002.pdf -------------------------------------------------------------------------------- /docs/Manual ET-81.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/Manual ET-81.pdf -------------------------------------------------------------------------------- /docs/Manual HY03.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/Manual HY03.pdf -------------------------------------------------------------------------------- /docs/Setup_AP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/Setup_AP.png -------------------------------------------------------------------------------- /docs/Setup_Clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/Setup_Clock.png -------------------------------------------------------------------------------- /docs/Setup_Firmware.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/Setup_Firmware.png -------------------------------------------------------------------------------- /docs/Setup_Main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/Setup_Main.png -------------------------------------------------------------------------------- /docs/Setup_Network_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/Setup_Network_0.png -------------------------------------------------------------------------------- /docs/Setup_Network_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/Setup_Network_1.png -------------------------------------------------------------------------------- /docs/Setup_Thermostat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/Setup_Thermostat.png -------------------------------------------------------------------------------- /docs/Setup_Wifi_MQTT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/Setup_Wifi_MQTT.png -------------------------------------------------------------------------------- /docs/StackTrace.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/StackTrace.PNG -------------------------------------------------------------------------------- /docs/Webthing_Complete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/Webthing_Complete.png -------------------------------------------------------------------------------- /docs/Webthing_Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/Webthing_Icon.png -------------------------------------------------------------------------------- /docs/originalFirmwareME102H.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klausahrenberg/WThermostatBeca/425ab6139d67bc52b137cca177412fbc6ba5020b/docs/originalFirmwareME102H.bin -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [env:esp01_1m] 12 | platform = espressif8266 13 | board = esp01_1m 14 | framework = arduino 15 | board_build.flash_mode = dout 16 | board_build.ldscript = eagle.flash.1m.ld 17 | upload_resetmethod = nodemcu 18 | upload_speed = 921600 19 | build_flags = 20 | -I ../WAdapter/src 21 | lib_deps = 22 | ESP8266WiFi 23 | https://github.com/me-no-dev/ESPAsyncWebServer 24 | AsyncTCP 25 | ESP8266mDNS 26 | PubSubClient 27 | DNSServer 28 | EEPROM 29 | NTPClient 30 | Time 31 | Hash -------------------------------------------------------------------------------- /src/WClock.h: -------------------------------------------------------------------------------- 1 | #ifndef __WCLOCK_H__ 2 | #define __WCLOCK_H__ 3 | 4 | #include "Arduino.h" 5 | #ifdef ESP8266 6 | #include 7 | #include 8 | #elif ESP32 9 | #include 10 | #include 11 | #include 12 | #endif 13 | #include 14 | #include 15 | #include "TimeLib.h" 16 | #include "WDevice.h" 17 | #include "WNetwork.h" 18 | 19 | const char* DEFAULT_NTP_SERVER = "pool.ntp.org"; 20 | const char* DEFAULT_TIME_ZONE_SERVER = "http://worldtimeapi.org/api/ip"; 21 | const byte STD_MONTH = 0; 22 | const byte STD_WEEK = 1; 23 | const byte STD_WEEKDAY = 2; 24 | const byte STD_HOUR = 3; 25 | const byte DST_MONTH = 4; 26 | const byte DST_WEEK = 5; 27 | const byte DST_WEEKDAY = 6; 28 | const byte DST_HOUR = 7; 29 | const byte *DEFAULT_DST_RULE = (const byte[]){10, 0, 0, 3, 3, 0, 0, 2}; 30 | const byte *DEFAULT_NIGHT_SWITCHES = (const byte[]){22, 00, 7, 00}; 31 | 32 | class WClock: public WDevice { 33 | public: 34 | typedef std::function THandlerFunction; 35 | 36 | WClock(WNetwork* network, bool supportNightMode) 37 | : WDevice(network, "clock", "clock", DEVICE_TYPE_TEXT_DISPLAY) { 38 | setMainDevice(false); 39 | setVisibility(MQTT); 40 | this->ntpServer = network->settings()->setString("ntpServer", DEFAULT_NTP_SERVER); 41 | this->ntpServer->readOnly(true); 42 | this->ntpServer->visibility(MQTT); 43 | this->addProperty(ntpServer); 44 | this->useTimeZoneServer = network->settings()->setBoolean("useTimeZoneServer", true); 45 | this->useTimeZoneServer->readOnly(true); 46 | this->useTimeZoneServer->visibility(NONE); 47 | this->addProperty(useTimeZoneServer); 48 | this->timeZoneServer = network->settings()->setString("timeZoneServer", DEFAULT_TIME_ZONE_SERVER); 49 | this->timeZoneServer->readOnly(true); 50 | this->timeZoneServer->visibility(this->useTimeZoneServer->asBool() ? MQTT : NONE); 51 | //this->ntpServer->setVisibility(MQTT); 52 | this->addProperty(timeZoneServer); 53 | _epochTimeFormatted = WProps::createStringProperty("epochTimeFormatted", "epochTimeFormatted"); 54 | _epochTimeFormatted->readOnly(true); 55 | _epochTimeFormatted->onValueRequest([this](WProperty* p) {updateFormattedTime();}); 56 | this->addProperty(_epochTimeFormatted); 57 | this->validTime = WProps::createOnOffProperty("validTime", "validTime"); 58 | this->validTime->asBool(false); 59 | this->validTime->readOnly(true); 60 | this->addProperty(validTime); 61 | if (this->useTimeZoneServer->asBool()) { 62 | this->timeZone = WProps::createStringProperty("timezone", "timeZone"); 63 | this->timeZone->readOnly(true); 64 | this->addProperty(timeZone); 65 | } else { 66 | this->timeZone = nullptr; 67 | } 68 | this->rawOffset = WProps::createIntegerProperty("raw_offset", "rawOffset"); 69 | this->rawOffset->asInt(3600); 70 | this->rawOffset->visibility(NONE); 71 | network->settings()->add(this->rawOffset); 72 | this->rawOffset->readOnly(true); 73 | this->addProperty(rawOffset); 74 | this->dstOffset = WProps::createIntegerProperty("dst_offset", "dstOffset"); 75 | this->dstOffset->asInt(3600); 76 | this->dstOffset->visibility(NONE); 77 | network->settings()->add(this->dstOffset); 78 | this->dstOffset->readOnly(true); 79 | this->addProperty(dstOffset); 80 | this->useDaySavingTimes = network->settings()->setBoolean("useDaySavingTimes", false); 81 | this->useDaySavingTimes->visibility(NONE); 82 | this->dstRule = network->settings()->setByteArray("dstRule", 8, DEFAULT_DST_RULE); 83 | //HtmlPages 84 | WPage* configPage = new WPage(this->id(), "Configure clock"); 85 | configPage->setPrintPage(std::bind(&WClock::printConfigPage, this, std::placeholders::_1, std::placeholders::_2)); 86 | configPage->setSubmittedPage(std::bind(&WClock::saveConfigPage, this, std::placeholders::_1, std::placeholders::_2)); 87 | network->addCustomPage(configPage); 88 | 89 | lastTry = lastNtpSync = lastTimeZoneSync = ntpTime = dstStart = dstEnd = 0; 90 | failedTimeZoneSync = 0; 91 | //enableNightMode 92 | this->enableNightMode = nullptr; 93 | this->nightMode = nullptr; 94 | this->nightSwitches = nullptr; 95 | if (supportNightMode) { 96 | this->enableNightMode = network->settings()->setBoolean("enableNightMode", true); 97 | this->nightMode = WProps::createBooleanProperty("nightMode", "nightMode"); 98 | this->addProperty(this->nightMode); 99 | this->nightSwitches = network->settings()->setByteArray("nightSwitches", 4, DEFAULT_NIGHT_SWITCHES); 100 | } 101 | this->wifiClient = nullptr; 102 | } 103 | 104 | void loop(unsigned long now) { 105 | //Invalid after 3 hours 106 | validTime->asBool((lastNtpSync > 0) && ((!this->useTimeZoneServer->asBool()) || (lastTimeZoneSync > 0)) && (now - lastTry < (3 * 60 * 60000))); 107 | if (((lastTry == 0) || (now - lastTry > 10000)) && (WiFi.status() == WL_CONNECTED)) { 108 | bool timeUpdated = false; 109 | //1. Sync ntp 110 | if ((!isValidTime()) 111 | && ((lastNtpSync == 0) || (now - lastNtpSync > 60000))) { 112 | network()->debug(F("Time via NTP server '%s'"), ntpServer->c_str()); 113 | WiFiUDP ntpUDP; 114 | NTPClient ntpClient(ntpUDP, ntpServer->c_str()); 115 | if (ntpClient.update()) { 116 | lastNtpSync = millis(); 117 | ntpTime = ntpClient.getEpochTime(); 118 | this->calculateDstStartAndEnd(); 119 | validTime->asBool(!this->useTimeZoneServer->asBool()); 120 | network()->debug(F("NTP time synced: %s"), _epochTimeFormatted->c_str()); 121 | timeUpdated = true; 122 | } else { 123 | network()->error(F("NTP sync failed. ")); 124 | } 125 | } 126 | //2. Sync time zone 127 | if ((!isValidTime()) 128 | && ((lastNtpSync > 0) && ((lastTimeZoneSync == 0) || (now - lastTimeZoneSync > 60000))) 129 | && (useTimeZoneServer->asBool()) 130 | && (!timeZoneServer->equalsString(""))) { 131 | String request = timeZoneServer->c_str(); 132 | network()->debug(F("Time zone update via '%s'"), request.c_str()); 133 | HTTPClient http; 134 | if (this->wifiClient == nullptr) { 135 | this->wifiClient = new WiFiClient(); 136 | } 137 | http.begin(*wifiClient, request); 138 | int httpCode = http.GET(); 139 | if (httpCode > 0) { 140 | WJsonParser parser; 141 | this->timeZone->readOnly(false); 142 | this->rawOffset->readOnly(false); 143 | this->dstOffset->readOnly(false); 144 | WProperty* property = parser.parse(http.getString().c_str(), this); 145 | this->timeZone->readOnly(true); 146 | this->rawOffset->readOnly(true); 147 | this->dstOffset->readOnly(true); 148 | if (property != nullptr) { 149 | failedTimeZoneSync = 0; 150 | lastTimeZoneSync = millis(); 151 | validTime->asBool(true); 152 | network()->debug(F("Time zone evaluated. Current local time: %s"), _epochTimeFormatted->c_str()); 153 | timeUpdated = true; 154 | } else { 155 | failedTimeZoneSync++; 156 | network()->error(F("Time zone update failed. (%d. attempt): Wrong html response."), failedTimeZoneSync); 157 | } 158 | } else { 159 | failedTimeZoneSync++; 160 | network()->error(F("Time zone update failed (%d. attempt): http code %d"), failedTimeZoneSync, httpCode); 161 | } 162 | http.end(); 163 | if (failedTimeZoneSync == 3) { 164 | failedTimeZoneSync = 0; 165 | lastTimeZoneSync = millis(); 166 | } 167 | } 168 | //check nightMode 169 | if ((validTime) && (this->enableNightMode) && (this->enableNightMode->asBool())) { 170 | this->nightMode->asBool(this->isTimeBetween(this->nightSwitches->byteArrayValue(0), this->nightSwitches->byteArrayValue(1), 171 | this->nightSwitches->byteArrayValue(2), this->nightSwitches->byteArrayValue(3))); 172 | } 173 | if (timeUpdated) { 174 | _notifyOnTimeUpdate(); 175 | } else { 176 | _notifyOnMinuteUpdate(); 177 | } 178 | lastTry = millis(); 179 | } 180 | } 181 | 182 | void setOnTimeUpdate(THandlerFunction onTimeUpdate) { 183 | _onTimeUpdate = onTimeUpdate; 184 | } 185 | 186 | void setOnMinuteTrigger(THandlerFunction onMinuteTrigger) { 187 | _onMinuteTrigger = onMinuteTrigger; 188 | } 189 | 190 | unsigned long epochTime() { 191 | return _epochTime(true); 192 | } 193 | 194 | byte weekDay() { 195 | return weekDayOf(epochTime()); 196 | } 197 | 198 | static byte weekDayOf(unsigned long epochTime) { 199 | //weekday from 0 to 6, 0 is Sunday 200 | return (((epochTime / 86400L) + 4) % 7); 201 | } 202 | 203 | const char* weekDayNameShort() { 204 | return weekDayNameShortOf(weekDay()); 205 | } 206 | 207 | const char* weekDayNameShortOf(byte weekDay) { 208 | switch (weekDay) { 209 | case 1: return PSTR("Mo"); 210 | case 2: return PSTR("Di"); 211 | case 3: return PSTR("Mi"); 212 | case 4: return PSTR("Do"); 213 | case 5: return PSTR("Fr"); 214 | case 6: return PSTR("Sa"); 215 | case 0: return PSTR("So"); 216 | default: return PSTR("n.a."); 217 | } 218 | } 219 | 220 | byte hours() { 221 | return hoursOf(epochTime()); 222 | } 223 | 224 | static byte hoursOf(unsigned long epochTime) { 225 | return ((epochTime % 86400L) / 3600); 226 | } 227 | 228 | byte minutes() { 229 | return minutesOf(epochTime()); 230 | } 231 | 232 | static byte minutesOf(unsigned long epochTime) { 233 | return ((epochTime % 3600) / 60); 234 | } 235 | 236 | byte seconds() { 237 | return secondsOf(epochTime()); 238 | } 239 | 240 | static byte secondsOf(unsigned long epochTime) { 241 | return (epochTime % 60); 242 | } 243 | 244 | int yearOf() { 245 | return yearOf(epochTime()); 246 | } 247 | 248 | static int yearOf(unsigned long epochTime) { 249 | return year(epochTime); 250 | } 251 | 252 | byte monthOf() { 253 | return monthOf(epochTime()); 254 | } 255 | 256 | static byte monthOf(unsigned long epochTime) { 257 | //month from 1 to 12 258 | return month(epochTime); 259 | } 260 | 261 | byte dayOf() { 262 | return dayOf(epochTime()); 263 | } 264 | 265 | static byte dayOf(unsigned long epochTime) { 266 | //day from 1 to 31 267 | return day(epochTime); 268 | } 269 | 270 | static bool isTimeLaterThan(byte epochTimeHours, byte epochTimeMinutes, byte hours, byte minutes) { 271 | return ((epochTimeHours > hours) || ((epochTimeHours == hours) && (epochTimeMinutes >= minutes))); 272 | } 273 | 274 | bool isTimeLaterThan(byte hours, byte minutes) { 275 | return isTimeLaterThan(this->hours(), this->minutes(), hours, minutes); 276 | } 277 | 278 | bool isTimeEarlierThan(byte hours, byte minutes) { 279 | return ((this->hours() < hours) || ((this->hours() == hours) && (this->minutes() < minutes))); 280 | } 281 | 282 | bool isTimeBetween(byte fromHours, byte fromMinutes, byte toHours, byte toMinutes) { 283 | if (isTimeLaterThan(fromHours, fromMinutes, toHours, toMinutes)) { 284 | //e.g. 22:00-06:00 285 | return ((isTimeLaterThan(fromHours, fromMinutes)) || (isTimeEarlierThan(toHours, toMinutes))); 286 | } else { 287 | //e.g. 06:00-22:00 288 | return ((isTimeLaterThan(fromHours, fromMinutes)) && (isTimeEarlierThan(toHours, toMinutes))); 289 | } 290 | } 291 | 292 | void updateFormattedTime() { 293 | WStringStream* stream = updateFormattedTime(epochTime()); 294 | _epochTimeFormatted->asString(stream->c_str()); 295 | delete stream; 296 | } 297 | 298 | static WStringStream* updateFormattedTime(unsigned long rawTime) { 299 | // Format YY-MM-DD hh:mm:ss 300 | WStringStream* stream = new WStringStream(19); 301 | char buffer[20]; 302 | snprintf(buffer, 20, "%02d-%02d-%02d %02d:%02d:%02d", 303 | yearOf(rawTime), monthOf(rawTime), dayOf(rawTime), 304 | ((rawTime % 86400L) / 3600), ((rawTime % 3600) / 60), rawTime % 60); 305 | stream->print(buffer); 306 | return stream; 307 | } 308 | 309 | bool isValidTime() { 310 | return validTime->asBool(); 311 | } 312 | 313 | bool isClockSynced() { 314 | return ((lastNtpSync > 0) && (lastTimeZoneSync > 0)); 315 | } 316 | 317 | int getRawOffset() { 318 | return rawOffset->asInt(); 319 | } 320 | 321 | int getDstOffset() { 322 | return (useTimeZoneServer->asBool() || isDaySavingTime() ? dstOffset->asInt() : 0); 323 | } 324 | 325 | void printConfigPage(AsyncWebServerRequest* request, Print* page) { 326 | page->printf(HTTP_CONFIG_PAGE_BEGIN, id()); 327 | page->printf(HTTP_TOGGLE_GROUP_STYLE, "ga", (useTimeZoneServer->asBool() ? HTTP_BLOCK : HTTP_NONE), "gb", (useTimeZoneServer->asBool() ? HTTP_NONE : HTTP_BLOCK)); 328 | page->printf(HTTP_TOGGLE_GROUP_STYLE, "gd", (useDaySavingTimes->asBool() ? HTTP_BLOCK : HTTP_NONE), "ge", HTTP_NONE); 329 | if (this->enableNightMode) { 330 | page->printf(HTTP_TOGGLE_GROUP_STYLE, "gn", (enableNightMode->asBool() ? HTTP_BLOCK : HTTP_NONE), "gm", HTTP_NONE); 331 | } 332 | //NTP Server 333 | page->printf(HTTP_TEXT_FIELD, "NTP server:", "ntp", "32", ntpServer->c_str()); 334 | 335 | page->print(FPSTR(HTTP_DIV_BEGIN)); 336 | page->printf(HTTP_RADIO_OPTION, "sa", "sa", HTTP_TRUE, (useTimeZoneServer->asBool() ? HTTP_CHECKED : ""), "tg()", "Get time zone via internet"); 337 | page->printf(HTTP_RADIO_OPTION, "sb", "sa", HTTP_FALSE, (useTimeZoneServer->asBool() ? "" : HTTP_CHECKED), "tg()", "Use fixed offset to UTC time"); 338 | page->print(FPSTR(HTTP_DIV_END)); 339 | 340 | page->printf(HTTP_DIV_ID_BEGIN, "ga"); 341 | page->printf(HTTP_TEXT_FIELD, "Time zone server:", "tz", "64", timeZoneServer->c_str()); 342 | page->print(FPSTR(HTTP_DIV_END)); 343 | page->printf(HTTP_DIV_ID_BEGIN, "gb"); 344 | page->printf(HTTP_TEXT_FIELD, "Fixed offset to UTC in minutes:", "ro", "5", String(rawOffset->asInt() / 60).c_str()); 345 | 346 | page->printf(HTTP_CHECKBOX_OPTION, "sd", "sd", (useDaySavingTimes->asBool() ? HTTP_CHECKED : ""), "td()", "Calculate day saving time (summer time)"); 347 | page->printf(HTTP_DIV_ID_BEGIN, "gd"); 348 | page->print(F("")); 349 | page->print(F("")); 350 | page->print(F("")); 351 | page->print(F("")); 352 | page->print(F("")); 353 | page->print(F("")); 354 | page->print(F("")); 355 | page->print(F("")); 356 | page->print(F("")); 357 | page->print(F("")); 360 | page->print(F("")); 361 | page->print(F("")); 362 | page->print(F("")); 363 | page->print(F("")); 366 | page->print(F("")); 369 | page->print(F("")); 370 | page->print(F("")); 371 | page->print(F("")); 372 | page->print(F("")); 375 | page->print(F("")); 378 | page->print(F("")); 379 | page->print(F("")); 380 | page->print(F("")); 381 | page->print(F("")); 384 | page->print(F("")); 387 | page->print(F("")); 388 | page->print(F("")); 389 | page->print(F("")); 390 | page->print(F("")); 393 | page->print(F("")); 396 | page->print(F("")); 397 | page->print(F("
Standard timeDay saving time
(summer time)
Offset to standard time in minutes")); 358 | page->printf(HTTP_INPUT_FIELD, "do", "5", String(dstOffset->asInt() / 60).c_str()); 359 | page->print(F("
Month [1..12]")); 364 | page->printf(HTTP_INPUT_FIELD, "rm", "2", String(dstRule->byteArrayValue(STD_MONTH)).c_str()); 365 | page->print(F("")); 367 | page->printf(HTTP_INPUT_FIELD, "dm", "2", String(dstRule->byteArrayValue(DST_MONTH)).c_str()); 368 | page->print(F("
Week [0: last week of month; 1..4]")); 373 | page->printf(HTTP_INPUT_FIELD, "rw", "1", String(dstRule->byteArrayValue(STD_WEEK)).c_str()); 374 | page->print(F("")); 376 | page->printf(HTTP_INPUT_FIELD, "dw", "1", String(dstRule->byteArrayValue(DST_WEEK)).c_str()); 377 | page->print(F("
Weekday [0:sunday .. 6:saturday]")); 382 | page->printf(HTTP_INPUT_FIELD, "rd", "1", String(dstRule->byteArrayValue(STD_WEEKDAY)).c_str()); 383 | page->print(F("")); 385 | page->printf(HTTP_INPUT_FIELD, "dd", "1", String(dstRule->byteArrayValue(DST_WEEKDAY)).c_str()); 386 | page->print(F("
Hour [0..23]")); 391 | page->printf(HTTP_INPUT_FIELD, "rh", "2", String(dstRule->byteArrayValue(STD_HOUR)).c_str()); 392 | page->print(F("")); 394 | page->printf(HTTP_INPUT_FIELD, "dh", "2", String(dstRule->byteArrayValue(DST_HOUR)).c_str()); 395 | page->print(F("
")); 398 | page->print(FPSTR(HTTP_DIV_END)); 399 | page->print(FPSTR(HTTP_DIV_END)); 400 | if (this->enableNightMode) { 401 | //nightMode 402 | page->printf(HTTP_CHECKBOX_OPTION, "sn", "sn", (enableNightMode->asBool() ? HTTP_CHECKED : ""), "tn()", "Enable support for night mode"); 403 | page->printf(HTTP_DIV_ID_BEGIN, "gn"); 404 | page->print(F("")); 405 | page->print(F("")); 406 | char timeFrom[6]; 407 | snprintf(timeFrom, 6, "%02d:%02d", this->nightSwitches->byteArrayValue(0), this->nightSwitches->byteArrayValue(1)); 408 | page->print(F("")); 411 | char timeTo[6]; 412 | snprintf(timeTo, 6, "%02d:%02d", this->nightSwitches->byteArrayValue(2), this->nightSwitches->byteArrayValue(3)); 413 | page->print(F("")); 416 | page->print(F("")); 417 | page->print(F("
from")); 409 | page->printf(HTTP_INPUT_FIELD, "nf", "5", timeFrom); 410 | page->print(F("to")); 414 | page->printf(HTTP_INPUT_FIELD, "nt", "5", timeTo); 415 | page->print(F("
")); 418 | page->print(FPSTR(HTTP_DIV_END)); 419 | page->printf(HTTP_TOGGLE_FUNCTION_SCRIPT, "tn()", "sn", "gn", "gm"); 420 | } 421 | page->printf(HTTP_TOGGLE_FUNCTION_SCRIPT, "tg()", "sa", "ga", "gb"); 422 | page->printf(HTTP_TOGGLE_FUNCTION_SCRIPT, "td()", "sd", "gd", "ge"); 423 | page->print(FPSTR(HTTP_CONFIG_SAVE_BUTTON)); 424 | } 425 | 426 | void saveConfigPage(AsyncWebServerRequest* request, Print* page) { 427 | this->ntpServer->asString(request->arg("ntp").c_str()); 428 | this->timeZoneServer->asString(request->arg("tz").c_str()); 429 | this->useTimeZoneServer->asBool(request->arg("sa") == HTTP_TRUE); 430 | this->useDaySavingTimes->asBool(request->arg("sd") == HTTP_TRUE); 431 | this->rawOffset->asInt(atol(request->arg("ro").c_str()) * 60); 432 | this->dstOffset->asInt(atol(request->arg("do").c_str()) * 60); 433 | this->dstRule->byteArrayValue(STD_MONTH, atoi(request->arg("rm").c_str())); 434 | this->dstRule->byteArrayValue(STD_WEEK, atoi(request->arg("rw").c_str())); 435 | this->dstRule->byteArrayValue(STD_WEEKDAY, atoi(request->arg("rd").c_str())); 436 | this->dstRule->byteArrayValue(STD_HOUR, atoi(request->arg("rh").c_str())); 437 | this->dstRule->byteArrayValue(DST_MONTH, atoi(request->arg("dm").c_str())); 438 | this->dstRule->byteArrayValue(DST_WEEK, atoi(request->arg("dw").c_str())); 439 | this->dstRule->byteArrayValue(DST_WEEKDAY, atoi(request->arg("dd").c_str())); 440 | this->dstRule->byteArrayValue(DST_HOUR, atoi(request->arg("dh").c_str())); 441 | if (this->enableNightMode) { 442 | this->enableNightMode->asBool(request->arg("sn") == HTTP_TRUE); 443 | processNightModeTime(0, request->arg("nf").c_str()); 444 | processNightModeTime(2, request->arg("nt").c_str()); 445 | } 446 | } 447 | 448 | WProperty* epochTimeFormatted() { 449 | return _epochTimeFormatted; 450 | } 451 | 452 | WProperty* nightMode; 453 | 454 | private: 455 | THandlerFunction _onTimeUpdate, _onMinuteTrigger; 456 | unsigned long lastTry, lastNtpSync, lastTimeZoneSync, ntpTime; 457 | unsigned long dstStart, dstEnd; 458 | byte failedTimeZoneSync; 459 | WProperty* _epochTimeFormatted; 460 | WProperty* validTime; 461 | WProperty* ntpServer; 462 | WProperty* useTimeZoneServer; 463 | WProperty* timeZoneServer; 464 | WProperty* timeZone; 465 | WProperty* rawOffset; 466 | WProperty* dstOffset; 467 | WProperty* useDaySavingTimes; 468 | WProperty* dstRule; 469 | WProperty* enableNightMode; 470 | WProperty* nightSwitches; 471 | WiFiClient* wifiClient; 472 | 473 | void _notifyOnTimeUpdate() { 474 | if (_onTimeUpdate) { 475 | _onTimeUpdate(); 476 | } 477 | _notifyOnMinuteUpdate(); 478 | } 479 | 480 | void _notifyOnMinuteUpdate() { 481 | if (_onMinuteTrigger) { 482 | _onMinuteTrigger(); 483 | } 484 | } 485 | 486 | unsigned long _epochTime(bool useDstOffset) { 487 | return (lastNtpSync > 0 ? ntpTime + getRawOffset() + (useDstOffset ? getDstOffset() : 0) + ((millis() - lastNtpSync) / 1000) : 0); 488 | } 489 | 490 | unsigned long getEpochTime(int year, byte month, byte week, byte weekday, byte hour) { 491 | tmElements_t ds; 492 | ds.Year = (week == 0 && month == 12 ? year + 1 : year) - 1970; 493 | ds.Month = (week == 0 ? (month == 12 ? 1 : month + 1) : month); 494 | ds.Day = 1; 495 | ds.Hour = hour; 496 | ds.Minute = 0; 497 | ds.Second = 0; 498 | unsigned long tt = makeTime(ds); 499 | byte iwd = weekDayOf(tt); 500 | if (week == 0) { 501 | //last week of last month 502 | short diffwd = iwd - weekday; 503 | diffwd = (diffwd <= 0 ? diffwd + 7 : diffwd); 504 | tt = tt - (diffwd * 60 * 60 * 24); 505 | } else { 506 | short diffwd = weekday - iwd; 507 | diffwd = (diffwd < 0 ? diffwd + 7 : diffwd); 508 | tt = tt + (((7 * (week - 1)) + diffwd) * 60 * 60 * 24); 509 | } 510 | return tt; 511 | } 512 | 513 | void calculateDstStartAndEnd() { 514 | if ((!this->useTimeZoneServer->asBool()) && (this->useDaySavingTimes->asBool())) { 515 | int year = WClock::yearOf(_epochTime(false)); 516 | dstStart = getEpochTime(year, dstRule->byteArrayValue(DST_MONTH), dstRule->byteArrayValue(DST_WEEK), dstRule->byteArrayValue(DST_WEEKDAY), dstRule->byteArrayValue(DST_HOUR)); 517 | WStringStream* stream = updateFormattedTime(dstStart); 518 | //network()->debug(F("DST start is: %s"), stream->c_str()); 519 | delete stream; 520 | dstEnd = getEpochTime(year, dstRule->byteArrayValue(STD_MONTH), dstRule->byteArrayValue(STD_WEEK), dstRule->byteArrayValue(STD_WEEKDAY), dstRule->byteArrayValue(STD_HOUR)); 521 | stream = updateFormattedTime(dstEnd); 522 | //network()->debug(F("STD start is: %s"), stream->c_str()); 523 | delete stream; 524 | } 525 | } 526 | 527 | bool isDaySavingTime() { 528 | if ((!this->useTimeZoneServer->asBool()) && (this->useDaySavingTimes->asBool())) { 529 | if ((this->dstStart != 0) && (this->dstEnd != 0)) { 530 | unsigned long now = _epochTime(false); 531 | if (yearOf(now) != yearOf(dstStart)) { 532 | calculateDstStartAndEnd(); 533 | } 534 | if (dstStart < dstEnd) { 535 | return ((now >= dstStart) && (now < dstEnd)); 536 | } else { 537 | return ((now < dstEnd) || (now >= dstStart)); 538 | } 539 | } else { 540 | return false; 541 | } 542 | } else { 543 | return (dstOffset->asInt() != 0); 544 | } 545 | } 546 | 547 | void processNightModeTime(byte arrayIndex, String timeStr) { 548 | timeStr = (timeStr.length() == 4 ? "0" + timeStr : timeStr); 549 | if (timeStr.length() == 5) { 550 | byte hh = timeStr.substring(0, 2).toInt(); 551 | byte mm = timeStr.substring(3, 5).toInt(); 552 | this->nightSwitches->byteArrayValue(arrayIndex, hh); 553 | this->nightSwitches->byteArrayValue(arrayIndex + 1, mm); 554 | } 555 | } 556 | 557 | }; 558 | 559 | #endif 560 | -------------------------------------------------------------------------------- /src/WThermostat.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "WNetwork.h" 3 | #include "WClock.h" 4 | #include "WThermostat.h" 5 | #include "WThermostat_BHT_002_GBLW.h" 6 | #include "WThermostat_BAC_002_ALW.h" 7 | #include "WThermostat_ET81W.h" 8 | #include "WThermostat_HY08WE.h" 9 | #include "WThermostat_ME81H.h" 10 | #include "WThermostat_MK70GBH.h" 11 | #include "WThermostat_ME102H.h" 12 | #include "WThermostat_CalypsoW.h" 13 | #include "WThermostat_DLX_LH01.h" 14 | 15 | #define APPLICATION "Thermostat" 16 | #define VERSION "1.38" 17 | #define FLAG_SETTINGS 0x12 18 | #define DEBUG false 19 | 20 | WNetwork* network; 21 | WProperty* thermostatModel; 22 | WThermostat* device; 23 | WClock* wClock; 24 | 25 | void setup() { 26 | Serial.begin(9600); 27 | //Wifi and Mqtt connection 28 | network = new WNetwork(DEBUG, APPLICATION, VERSION, NO_LED, FLAG_SETTINGS, nullptr); 29 | network->setOnConfigurationFinished([]() { 30 | //Switch blinking thermostat in normal operating mode back 31 | device->cancelConfiguration(); 32 | }); 33 | //WClock - time sync 34 | wClock = new WClock(network, false); 35 | network->addDevice(wClock); 36 | //Model 37 | thermostatModel = network->settings()->setByte("thermostatModel", MODEL_BHT_002_GBLW); 38 | //Thermostat device 39 | device = nullptr; 40 | switch (thermostatModel->asByte()) { 41 | case MODEL_BHT_002_GBLW : 42 | device = new WThermostat_BHT_002_GBLW(network, thermostatModel, wClock); 43 | break; 44 | case MODEL_BAC_002_ALW : 45 | device = new WThermostat_BAC_002_ALW(network, thermostatModel, wClock); 46 | break; 47 | case MODEL_ET81W : 48 | device = new WThermostat_ET81W(network, thermostatModel, wClock); 49 | break; 50 | case MODEL_HY08WE : 51 | device = new WThermostat_HY08WE(network, thermostatModel, wClock); 52 | break; 53 | case MODEL_ME81H : 54 | device = new WThermostat_ME81H(network, thermostatModel, wClock); 55 | break; 56 | case MODEL_MK70GBH : 57 | device = new WThermostat_MK70GBH(network, thermostatModel, wClock); 58 | break; 59 | case MODEL_ME102H : 60 | device = new WThermostat_ME102H(network, thermostatModel, wClock); 61 | break; 62 | case MODEL_CALYPSOW : 63 | device = new WThermostat_CalypsoW(network, thermostatModel, wClock); 64 | break; 65 | case MODEL_DLX_LH01 : 66 | device = new WThermostat_DLX_LH01(network, thermostatModel, wClock); 67 | break; 68 | default : 69 | network->error(F("Can't start device. Wrong thermostatModel (%d)"), thermostatModel->asByte()); 70 | } 71 | if (device != nullptr) { 72 | device->configureCommandBytes(); 73 | device->initializeProperties(); 74 | } 75 | network->addDevice(device); 76 | } 77 | 78 | void loop() { 79 | network->loop(millis()); 80 | delay(50); 81 | } 82 | -------------------------------------------------------------------------------- /src/WThermostat.h: -------------------------------------------------------------------------------- 1 | #ifndef THERMOSTAT_H 2 | #define THERMOSTAT_H 3 | 4 | #include 5 | #include 6 | #include "WTuyaDevice.h" 7 | #include "WClock.h" 8 | 9 | #define COUNT_DEVICE_MODELS 6 10 | #define MODEL_BHT_002_GBLW 0 11 | #define MODEL_BAC_002_ALW 1 12 | #define MODEL_ET81W 2 13 | #define MODEL_HY08WE 3 14 | #define MODEL_ME81H 4 15 | #define MODEL_MK70GBH 5 16 | #define MODEL_ME102H 6 17 | #define MODEL_CALYPSOW 7 18 | #define MODEL_DLX_LH01 8 19 | #define PIN_STATE_HEATING_RELAY 5 20 | #define NOT_SUPPORTED 0x00 21 | 22 | const char* SCHEDULES = "schedules"; 23 | const char* SCHEDULES_MODE_OFF = "off"; 24 | const char* SCHEDULES_MODE_AUTO = "auto"; 25 | const char* SCHEDULES_MODE_HOLD = "hold"; 26 | const char* STATE_OFF = SCHEDULES_MODE_OFF; 27 | const char* STATE_HEATING = "heating"; 28 | const char SCHEDULES_PERIODS[] = "123456"; 29 | const char SCHEDULES_DAYS[] = "wau"; 30 | 31 | class WThermostat : public WTuyaDevice { 32 | public : 33 | WThermostat(WNetwork* network, WProperty* thermostatModel, WClock* wClock) 34 | : WTuyaDevice(network, "thermostat", "thermostat", DEVICE_TYPE_THERMOSTAT) { 35 | this->thermostatModel = thermostatModel; 36 | this->wClock = wClock; 37 | this->wClock->setOnTimeUpdate([this]() { 38 | this->sendActualTimeToBeca(); 39 | }); 40 | lastNotify = lastScheduleNotify = 0; 41 | this->schedulesChanged = false; 42 | this->schedulesReceived = false; 43 | this->targetTemperatureManualMode = 0.0; 44 | this->currentSchedulePeriod = -1; 45 | //HtmlPages 46 | WPage* configPage = new WPage(this->id(), "Configure thermostat"); 47 | configPage->setPrintPage(std::bind(&WThermostat::printConfigPage, this, std::placeholders::_1, std::placeholders::_2)); 48 | configPage->setSubmittedPage(std::bind(&WThermostat::submitConfigPage, this, std::placeholders::_1, std::placeholders::_2)); 49 | network->addCustomPage(configPage); 50 | WPage* schedulesPage = new WPage(SCHEDULES, "Configure schedules"); 51 | schedulesPage->setPrintPage(std::bind(&WThermostat::printConfigSchedulesPage, this, std::placeholders::_1, std::placeholders::_2)); 52 | schedulesPage->setSubmittedPage(std::bind(&WThermostat::submitConfigSchedulesPage, this, std::placeholders::_1, std::placeholders::_2)); 53 | network->addCustomPage(schedulesPage); 54 | } 55 | 56 | virtual void configureCommandBytes() { 57 | //command bytes 58 | this->byteDeviceOn = 0x01; 59 | this->byteTemperatureActual = NOT_SUPPORTED; 60 | this->byteTemperatureTarget = NOT_SUPPORTED; 61 | this->byteTemperatureFloor = NOT_SUPPORTED; 62 | this->temperatureFactor = 2.0f; 63 | this->byteSchedulesMode = NOT_SUPPORTED; 64 | this->byteLocked = NOT_SUPPORTED; 65 | this->byteSchedules = NOT_SUPPORTED; 66 | this->byteSchedulingPosHour = 1; 67 | this->byteSchedulingPosMinute = 0; 68 | this->byteSchedulingDays = 18; 69 | } 70 | 71 | virtual bool isDeviceStateComplete() { 72 | return (((this->byteDeviceOn == NOT_SUPPORTED) || (!this->deviceOn->isNull())) && 73 | ((this->byteTemperatureActual == NOT_SUPPORTED) || (!this->actualTemperature->isNull())) && 74 | ((this->byteTemperatureTarget == NOT_SUPPORTED) || (this->targetTemperatureManualMode != 0.0)) && 75 | ((this->byteTemperatureFloor == NOT_SUPPORTED) || (!this->actualFloorTemperature->isNull())) && 76 | ((this->byteSchedulesMode == NOT_SUPPORTED) || (!this->schedulesMode->isNull())) 77 | ); 78 | } 79 | 80 | virtual void initializeProperties() { 81 | //schedulesDayOffset 82 | this->schedulesDayOffset = network()->settings()->setByte("schedulesDayOffset", 0); 83 | //standard properties 84 | this->actualTemperature = WProps::createTemperatureProperty("temperature", "Actual"); 85 | this->actualTemperature->readOnly(true); 86 | this->addProperty(actualTemperature); 87 | this->targetTemperature = WProps::createTargetTemperatureProperty("targetTemperature", "Target"); 88 | this->targetTemperature->multipleOf(1.0f / this->temperatureFactor); 89 | this->targetTemperature->addListener(std::bind(&WThermostat::setTargetTemperature, this, std::placeholders::_1)); 90 | this->targetTemperature->onValueRequest([this](WProperty* p) {updateTargetTemperature();}); 91 | this->addProperty(targetTemperature); 92 | if (byteTemperatureFloor != NOT_SUPPORTED) { 93 | this->actualFloorTemperature = WProps::createTargetTemperatureProperty("floorTemperature", "Floor"); 94 | this->actualFloorTemperature->readOnly(true); 95 | this->actualFloorTemperature->visibility(MQTT); 96 | this->addProperty(actualFloorTemperature); 97 | } else { 98 | this->actualFloorTemperature = nullptr; 99 | } 100 | this->deviceOn = WProps::createOnOffProperty("deviceOn", "Power"); 101 | //2021-01-24 test bht-002 bug 102 | network()->settings()->add(this->deviceOn); 103 | this->deviceOn->addListener(std::bind(&WThermostat::deviceOnToMcu, this, std::placeholders::_1)); 104 | this->addProperty(deviceOn); 105 | this->schedulesMode = new WProperty("schedulesMode", "Schedules", STRING, TYPE_THERMOSTAT_MODE_PROPERTY); 106 | this->schedulesMode->addEnumString(SCHEDULES_MODE_AUTO); 107 | this->schedulesMode->addEnumString(SCHEDULES_MODE_OFF); 108 | this->schedulesMode->addListener(std::bind(&WThermostat::schedulesModeToMcu, this, std::placeholders::_1)); 109 | this->addProperty(schedulesMode); 110 | this->switchBackToAuto = network()->settings()->setBoolean("switchBackToAuto", true); 111 | this->locked = WProps::createOnOffProperty("locked", "Lock"); 112 | this->locked->addListener(std::bind(&WThermostat::lockedToMcu, this, std::placeholders::_1)); 113 | this->locked->visibility(MQTT); 114 | this->addProperty(locked); 115 | this->completeDeviceState = network()->settings()->setBoolean("sendCompleteDeviceState", true); 116 | //Heating Relay and State property 117 | this->state = nullptr; 118 | this->supportingHeatingRelay = network()->settings()->setBoolean("supportingHeatingRelay", true); 119 | if (this->supportingHeatingRelay->asBool()) { 120 | pinMode(PIN_STATE_HEATING_RELAY, INPUT); 121 | this->state = new WProperty("state", "State", STRING, TYPE_HEATING_COOLING_PROPERTY); 122 | this->state->readOnly(true); 123 | this->state->addEnumString(STATE_OFF); 124 | this->state->addEnumString(STATE_HEATING); 125 | this->addProperty(state); 126 | } 127 | } 128 | 129 | virtual void printConfigPage(AsyncWebServerRequest* request, Print* page) { 130 | page->printf(HTTP_CONFIG_PAGE_BEGIN, id()); 131 | //ComboBox with model selection 132 | page->printf(HTTP_COMBOBOX_BEGIN, "Thermostat model:", "tm"); 133 | page->printf(HTTP_COMBOBOX_ITEM, "0", (this->thermostatModel->asByte() == 0 ? HTTP_SELECTED : ""), "BHT-002, BHT-6000, BHT-3000 (floor heating)"); 134 | page->printf(HTTP_COMBOBOX_ITEM, "6", (this->thermostatModel->asByte() == 6 ? HTTP_SELECTED : ""), "AVATTO ME102H (Touch screen)"); 135 | page->printf(HTTP_COMBOBOX_ITEM, "1", (this->thermostatModel->asByte() == 1 ? HTTP_SELECTED : ""), "BAC-002, BAC-1000 (heating, cooling, ventilation)"); 136 | page->printf(HTTP_COMBOBOX_ITEM, "2", (this->thermostatModel->asByte() == 2 ? HTTP_SELECTED : ""), "ET-81W"); 137 | page->printf(HTTP_COMBOBOX_ITEM, "3", (this->thermostatModel->asByte() == 3 ? HTTP_SELECTED : ""), "Floureon HY08WE"); 138 | page->printf(HTTP_COMBOBOX_ITEM, "4", (this->thermostatModel->asByte() == 4 ? HTTP_SELECTED : ""), "AVATTO ME81AH"); 139 | page->printf(HTTP_COMBOBOX_ITEM, "5", (this->thermostatModel->asByte() == 5 ? HTTP_SELECTED : ""), "Minco Heat MK70GB-H"); 140 | page->printf(HTTP_COMBOBOX_ITEM, "7", (this->thermostatModel->asByte() == 7 ? HTTP_SELECTED : ""), "VH Control Calypso-W"); 141 | page->printf(HTTP_COMBOBOX_ITEM, "8", (this->thermostatModel->asByte() == 8 ? HTTP_SELECTED : ""), "DLX-LH01"); 142 | page->print(FPSTR(HTTP_COMBOBOX_END)); 143 | //Checkbox 144 | page->printf(HTTP_CHECKBOX_OPTION, "sb", "sb", (this->switchBackToAuto->asBool() ? HTTP_CHECKED : ""), "", "Auto mode from manual mode at next schedule period change
(not at model ET-81W and ME81AH)"); 145 | //ComboBox with weekday 146 | page->printf(HTTP_COMBOBOX_BEGIN, "Workday schedules:", "ws"); 147 | page->printf(HTTP_COMBOBOX_ITEM, "0", (getSchedulesDayOffset() == 0 ? HTTP_SELECTED : ""), "Workday Mon-Fri; Weekend Sat-Sun"); 148 | page->printf(HTTP_COMBOBOX_ITEM, "1", (getSchedulesDayOffset() == 1 ? HTTP_SELECTED : ""), "Workday Sun-Thu; Weekend Fri-Sat"); 149 | page->printf(HTTP_COMBOBOX_ITEM, "2", (getSchedulesDayOffset() == 2 ? HTTP_SELECTED : ""), "Workday Sat-Wed; Weekend Thu-Fri"); 150 | page->printf(HTTP_COMBOBOX_ITEM, "3", (getSchedulesDayOffset() == 3 ? HTTP_SELECTED : ""), "Workday Fri-Tue; Weekend Wed-Thu"); 151 | page->printf(HTTP_COMBOBOX_ITEM, "4", (getSchedulesDayOffset() == 4 ? HTTP_SELECTED : ""), "Workday Thu-Mon; Weekend Tue-Wed"); 152 | page->printf(HTTP_COMBOBOX_ITEM, "5", (getSchedulesDayOffset() == 5 ? HTTP_SELECTED : ""), "Workday Wed-Sun; Weekend Mon-Tue"); 153 | page->printf(HTTP_COMBOBOX_ITEM, "6", (getSchedulesDayOffset() == 6 ? HTTP_SELECTED : ""), "Workday Tue-Sat; Weekend Sun-Mon"); 154 | page->print(FPSTR(HTTP_COMBOBOX_END)); 155 | page->printf(HTTP_CHECKBOX_OPTION, "cr", "cr", (this->sendCompleteDeviceState() ? "" : HTTP_CHECKED), "", "Send changes in separate MQTT messages"); 156 | //notifyAllMcuCommands 157 | page->printf(HTTP_CHECKBOX_OPTION, "am", "am", (this->notifyAllMcuCommands->asBool() ? HTTP_CHECKED : ""), "", "Send all MCU commands via MQTT"); 158 | //Checkbox with support for relay 159 | page->printf(HTTP_CHECKBOX_OPTION, "rs", "rs", (this->supportingHeatingRelay->asBool() ? HTTP_CHECKED : ""), "", "Relay at GPIO 5 (not working without hw mod)"); 160 | 161 | printConfigPageCustomParameters(request, page); 162 | 163 | page->print(FPSTR(HTTP_CONFIG_SAVE_BUTTON)); 164 | } 165 | 166 | virtual void printConfigPageCustomParameters(AsyncWebServerRequest* request, Print* page) { 167 | 168 | } 169 | 170 | virtual void submitConfigPage(AsyncWebServerRequest* request, Print* page) { 171 | this->thermostatModel->asByte(request->arg("tm").toInt()); 172 | this->schedulesDayOffset->asByte(request->arg("ws").toInt()); 173 | this->switchBackToAuto->asBool(request->arg("sb") == HTTP_TRUE); 174 | this->completeDeviceState->asBool(request->arg("cr") != HTTP_TRUE); 175 | this->notifyAllMcuCommands->asBool(request->arg("am") == HTTP_TRUE); 176 | this->supportingHeatingRelay->asBool(request->arg("rs") == HTTP_TRUE); 177 | submitConfigPageCustomParameters(request, page); 178 | } 179 | 180 | virtual void submitConfigPageCustomParameters(AsyncWebServerRequest* request, Print* page) { 181 | 182 | } 183 | 184 | void handleUnknownMqttCallback(bool getState, String completeTopic, String partialTopic, char *payload, unsigned int length) { 185 | if (partialTopic.startsWith(SCHEDULES)) { 186 | if (byteSchedules != NOT_SUPPORTED) { 187 | partialTopic = partialTopic.substring(strlen(SCHEDULES) + 1); 188 | if (getState) { 189 | //Send actual schedules 190 | handleSchedulesChange(completeTopic); 191 | } else if (length > 0) { 192 | //Set schedules 193 | network()->debug(F("Payload for schedules -> set schedules...")); 194 | WJsonParser* parser = new WJsonParser(); 195 | schedulesChanged = false; 196 | parser->parse(payload, std::bind(&WThermostat::processSchedulesKeyValue, this, 197 | std::placeholders::_1, std::placeholders::_2)); 198 | delete parser; 199 | if (schedulesChanged) { 200 | network()->debug(F("Some schedules changed. Write to MCU...")); 201 | this->schedulesToMcu(); 202 | } 203 | } 204 | } 205 | } 206 | } 207 | 208 | void processSchedulesKeyValue(const char* key, const char* value) { 209 | byte hh_Offset = byteSchedulingPosHour; 210 | byte mm_Offset = byteSchedulingPosMinute; 211 | if (strlen(key) == 3) { 212 | byte startAddr = 255; 213 | byte period = 255; 214 | for (int i = 0; i < 6; i++) { 215 | if (SCHEDULES_PERIODS[i] == key[1]) { 216 | period = i; 217 | break; 218 | } 219 | } 220 | if (key[0] == SCHEDULES_DAYS[0]) { 221 | startAddr = 0; 222 | } else if (key[0] == SCHEDULES_DAYS[1]) { 223 | startAddr = 18; 224 | } else if (key[0] == SCHEDULES_DAYS[2]) { 225 | startAddr = 36; 226 | } 227 | if ((startAddr != 255) && (period != 255)) { 228 | byte index = startAddr + period * 3; 229 | if (index < this->byteSchedulingDays * 3) { 230 | if (key[2] == 'h') { 231 | //hour 232 | String timeStr = String(value); 233 | timeStr = (timeStr.length() == 4 ? "0" + timeStr : timeStr); 234 | if (timeStr.length() == 5) { 235 | byte hh = timeStr.substring(0, 2).toInt(); 236 | byte mm = timeStr.substring(3, 5).toInt(); 237 | schedulesChanged = schedulesChanged || (schedules[index + hh_Offset] != hh); 238 | schedules[index + hh_Offset] = hh; 239 | schedulesChanged = schedulesChanged || (schedules[index + mm_Offset] != mm); 240 | schedules[index + mm_Offset] = mm; 241 | } 242 | } else if (key[2] == 't') { 243 | //temperature 244 | //it will fail, when temperature needs 2 bytes 245 | int tt = (int) (atof(value) * this->temperatureFactor); 246 | if (tt < 0xFF) { 247 | schedulesChanged = schedulesChanged || (schedules[index + 2] != tt); 248 | schedules[index + 2] = tt; 249 | } 250 | } 251 | } 252 | } 253 | } 254 | } 255 | 256 | void sendSchedules(AsyncWebServerRequest* request) { 257 | WStringStream* response = network()->getResponseStream(); 258 | WJson json(response); 259 | json.beginObject(); 260 | this->toJsonSchedules(&json, 0);// SCHEDULE_WORKDAY); 261 | this->toJsonSchedules(&json, 1);// SCHEDULE_WEEKEND_1); 262 | this->toJsonSchedules(&json, 2);// SCHEDULE_WEEKEND_2); 263 | json.endObject(); 264 | request->send(200, APPLICATION_JSON, response->c_str()); 265 | } 266 | 267 | virtual void toJsonSchedules(WJson* json, byte schedulesDay) { 268 | byte startAddr = 0; 269 | char dayChar = SCHEDULES_DAYS[0]; 270 | byte hh_Offset = byteSchedulingPosHour; 271 | byte mm_Offset = byteSchedulingPosMinute; 272 | switch (schedulesDay) { 273 | case 1 : 274 | startAddr = 18; 275 | dayChar = SCHEDULES_DAYS[1]; 276 | break; 277 | case 2 : 278 | startAddr = 36; 279 | dayChar = SCHEDULES_DAYS[2]; 280 | break; 281 | } 282 | char timeStr[6]; 283 | timeStr[5] = '\0'; 284 | char* buffer = new char[4]; 285 | buffer[0] = dayChar; 286 | buffer[3] = '\0'; 287 | for (int i = 0; i < 6; i++) { 288 | byte index = startAddr + i * 3; 289 | if (index < this->byteSchedulingDays * 3) { 290 | buffer[1] = SCHEDULES_PERIODS[i]; 291 | sprintf(timeStr, "%02d:%02d", schedules[index + hh_Offset], schedules[index + mm_Offset]); 292 | buffer[2] = 'h'; 293 | json->propertyString(buffer, timeStr); 294 | buffer[2] = 't'; 295 | json->propertyDouble(buffer, (double) schedules[index + 2] / this->temperatureFactor); 296 | } else { 297 | break; 298 | } 299 | } 300 | delete[] buffer; 301 | } 302 | 303 | virtual void loop(unsigned long now) { 304 | if (state != nullptr) { 305 | bool heating = false; 306 | if ((this->supportingHeatingRelay->asBool()) && (state != nullptr)) { 307 | heating = digitalRead(PIN_STATE_HEATING_RELAY); 308 | } 309 | this->state->asString(heating ? STATE_HEATING : STATE_OFF); 310 | } 311 | WTuyaDevice::loop(now); 312 | updateCurrentSchedulePeriod(); 313 | if (receivedSchedules()) { 314 | //Notify schedules 315 | if ((network()->isMqttConnected()) && (lastScheduleNotify == 0) && (now - lastScheduleNotify > MINIMUM_INTERVAL)) { 316 | handleSchedulesChange(""); 317 | schedulesChanged = false; 318 | lastScheduleNotify = now; 319 | } 320 | } 321 | } 322 | 323 | virtual bool sendCompleteDeviceState() { 324 | return this->completeDeviceState->asBool(); 325 | } 326 | 327 | protected : 328 | WClock *wClock; 329 | WProperty* schedulesDayOffset; 330 | byte byteDeviceOn; 331 | byte byteTemperatureActual; 332 | byte byteTemperatureTarget; 333 | byte byteTemperatureFloor; 334 | byte byteSchedulesMode; 335 | byte byteLocked; 336 | byte byteSchedules; 337 | byte byteSchedulingPosHour; 338 | byte byteSchedulingPosMinute; 339 | byte byteSchedulingDays; 340 | byte byteSchedulingFunctionL; 341 | byte byteSchedulingDataL; 342 | float temperatureFactor; 343 | WProperty* thermostatModel; 344 | WProperty* actualTemperature; 345 | WProperty* targetTemperature; 346 | WProperty* actualFloorTemperature; 347 | double targetTemperatureManualMode; 348 | WProperty* deviceOn; 349 | WProperty* schedulesMode; 350 | WProperty *completeDeviceState; 351 | WProperty* switchBackToAuto; 352 | WProperty* locked; 353 | WProperty* state; 354 | WProperty* supportingHeatingRelay; 355 | byte schedules[54]; 356 | 357 | void sendActualTimeToBeca() { 358 | //Command: Set date and time 359 | // ?? YY MM DD HH MM SS Weekday 360 | //DEC: 01 19 02 15 16 04 18 05 361 | //HEX: 55 AA 00 1C 00 08 01 13 02 0F 10 04 12 05 362 | //DEC: 01 19 02 20 17 51 44 03 363 | //HEX: 55 AA 00 1C 00 08 01 13 02 14 11 33 2C 03 364 | unsigned long epochTime = wClock->epochTime(); 365 | epochTime = epochTime + (getSchedulesDayOffset() * 86400); 366 | byte year = wClock->yearOf(epochTime) % 100; 367 | byte month = wClock->monthOf(epochTime); 368 | byte dayOfMonth = wClock->dayOf(epochTime); 369 | byte hours = wClock->hoursOf(epochTime) ; 370 | byte minutes = wClock->minutesOf(epochTime); 371 | byte seconds = wClock->secondsOf(epochTime); 372 | byte dayOfWeek = getDayOfWeek(); 373 | unsigned char cancelConfigCommand[] = { 0x55, 0xaa, 0x00, 0x1c, 0x00, 0x08, 374 | 0x01, year, month, dayOfMonth, 375 | hours, minutes, seconds, dayOfWeek}; 376 | commandCharsToSerial(14, cancelConfigCommand); 377 | } 378 | 379 | byte getDayOfWeek() { 380 | unsigned long epochTime = wClock->epochTime(); 381 | epochTime = epochTime + (getSchedulesDayOffset() * 86400); 382 | byte dayOfWeek = wClock->weekDayOf(epochTime); 383 | //make sunday a seven 384 | dayOfWeek = (dayOfWeek ==0 ? 7 : dayOfWeek); 385 | return dayOfWeek; 386 | } 387 | 388 | bool isWeekend() { 389 | byte dayOfWeek = getDayOfWeek(); 390 | return ((dayOfWeek == 6) || (dayOfWeek == 7)); 391 | } 392 | 393 | byte getSchedulesDayOffset() { 394 | return schedulesDayOffset->asByte(); 395 | } 396 | 397 | virtual bool processCommand(byte commandByte, byte length) { 398 | bool knownCommand = WTuyaDevice::processCommand(commandByte, length); 399 | switch (commandByte) { 400 | case 0x03: { 401 | //ignore, MCU response to wifi state 402 | //55 aa 01 03 00 00 403 | knownCommand = (length == 0); 404 | break; 405 | } 406 | case 0x04: { 407 | //Setup initialization request 408 | //received: 55 aa 01 04 00 00 409 | //send answer: 55 aa 00 03 00 01 00 410 | unsigned char configCommand[] = { 0x55, 0xAA, 0x00, 0x03, 0x00, 0x01, 0x00 }; 411 | commandCharsToSerial(7, configCommand); 412 | network()->startWebServer(); 413 | knownCommand = true; 414 | break; 415 | } 416 | case 0x1C: { 417 | //Request for time sync from MCU : 55 aa 01 1c 00 00 418 | this->sendActualTimeToBeca(); 419 | knownCommand = true; 420 | break; 421 | } 422 | } 423 | return knownCommand; 424 | } 425 | 426 | virtual bool processStatusCommand(byte cByte, byte commandLength) { 427 | //Status report from MCU 428 | bool changed = false; 429 | bool newB; 430 | float newValue; 431 | const char* newS; 432 | bool knownCommand = false; 433 | if (cByte == byteDeviceOn ) { 434 | if (commandLength == 0x05) { 435 | //device On/Off 436 | //55 aa 00 06 00 05 01 01 00 01 00|01 437 | //2021-01-24 test for bht-002 438 | if (!this->mcuRestarted) { 439 | newB = (receivedCommand[10] == 0x01); 440 | changed = ((changed) || (newB != deviceOn->asBool())); 441 | deviceOn->asBool(newB); 442 | } else if (!this->deviceOn->isNull()) { 443 | deviceOnToMcu(this->deviceOn); 444 | this->mcuRestarted = false; 445 | } 446 | knownCommand = true; 447 | } 448 | } else if (cByte == byteTemperatureTarget) { 449 | if (commandLength == 0x08) { 450 | //target Temperature for manual mode 451 | //e.g. 24.5C: 55 aa 01 07 00 08 02 02 00 04 00 00 00 31 452 | 453 | unsigned long rawValue = WSettings::getUnsignedLong(receivedCommand[10], receivedCommand[11], receivedCommand[12], receivedCommand[13]); 454 | newValue = (float) rawValue / this->temperatureFactor; 455 | changed = ((changed) || (!WProperty::isEqual(targetTemperatureManualMode, newValue, 0.01))); 456 | targetTemperatureManualMode = newValue; 457 | if (changed) updateTargetTemperature(); 458 | knownCommand = true; 459 | } 460 | } else if ((byteTemperatureActual != NOT_SUPPORTED) && (cByte == byteTemperatureActual)) { 461 | if (commandLength == 0x08) { 462 | //actual Temperature 463 | //e.g. 23C: 55 aa 01 07 00 08 03 02 00 04 00 00 00 2e 464 | unsigned long rawValue = WSettings::getUnsignedLong(receivedCommand[10], receivedCommand[11], receivedCommand[12], receivedCommand[13]); 465 | newValue = (float) rawValue / this->temperatureFactor; 466 | changed = ((changed) || (!actualTemperature->equalsDouble(newValue))); 467 | actualTemperature->asDouble(newValue); 468 | knownCommand = true; 469 | } 470 | } else if ((byteTemperatureFloor != NOT_SUPPORTED) && (cByte == byteTemperatureFloor)) { 471 | if (commandLength == 0x08) { 472 | //MODEL_BHT_002_GBLW - actualFloorTemperature 473 | //55 aa 01 07 00 08 66 02 00 04 00 00 00 00 474 | unsigned long rawValue = WSettings::getUnsignedLong(receivedCommand[10], receivedCommand[11], receivedCommand[12], receivedCommand[13]); 475 | newValue = (float) rawValue / this->temperatureFactor; 476 | changed = ((changed) || (!actualFloorTemperature->equalsDouble(newValue))); 477 | actualFloorTemperature->asDouble(newValue); 478 | knownCommand = true; 479 | } 480 | } else if (cByte == byteSchedulesMode) { 481 | if (commandLength == 0x05) { 482 | //schedulesMode 483 | newS = schedulesMode->enumString(receivedCommand[10]); 484 | if (newS != nullptr) { 485 | changed = ((changed) || (schedulesMode->asString(newS))); 486 | if (changed) updateTargetTemperature(); 487 | knownCommand = true; 488 | } 489 | } 490 | } else if (cByte == byteLocked) { 491 | if (commandLength == 0x05) { 492 | //locked 493 | newB = (receivedCommand[10] == 0x01); 494 | changed = ((changed) || (newB != locked->asBool())); 495 | locked->asBool(newB); 496 | knownCommand = true; 497 | } 498 | } else if (cByte == byteSchedules) { 499 | this->schedulesReceived = this->processStatusSchedules(commandLength); 500 | knownCommand = this->schedulesReceived; 501 | } 502 | if (changed) { 503 | notifyState(); 504 | } 505 | return knownCommand; 506 | } 507 | 508 | virtual bool processStatusSchedules(byte commandLength) { 509 | bool result = (commandLength == (this->byteSchedulingDays * 3 + 4)); 510 | if (result) { 511 | bool changed = false; 512 | //schedules 0x65 at heater model, 0x68 at fan model, example 513 | //55 AA 00 06 00 3A 65 00 00 36 514 | //00 07 28 00 08 1E 1E 0B 1E 1E 0D 1E 00 11 2C 00 16 1E 515 | //00 06 28 00 08 28 1E 0B 28 1E 0D 28 00 11 28 00 16 1E 516 | //00 06 28 00 08 28 1E 0B 28 1E 0D 28 00 11 28 00 16 1E 517 | for (int i = 0; i < this->byteSchedulingDays * 3; i++) { 518 | byte newByte = receivedCommand[i + 10]; 519 | changed = ((changed) || (newByte != schedules[i])); 520 | schedules[i] = newByte; 521 | } 522 | if (changed) { 523 | notifySchedules(); 524 | } 525 | } 526 | return result; 527 | } 528 | 529 | void updateCurrentSchedulePeriod() { 530 | if ((receivedSchedules()) && (wClock->isValidTime())) { 531 | byte hh_Offset = byteSchedulingPosHour; 532 | byte mm_Offset = byteSchedulingPosMinute; 533 | byte weekDay = wClock->weekDay(); 534 | weekDay += getSchedulesDayOffset(); 535 | weekDay = weekDay % 7; 536 | int startAddr = (this->byteSchedulingDays == 18 ? (weekDay == 0 ? 36 : (weekDay == 6 ? 18 : 0)) : ((weekDay == 0) || (weekDay == 6) ? 18 : 0)); 537 | int period = 0; 538 | if (wClock->isTimeEarlierThan(schedules[startAddr + period * 3 + hh_Offset], schedules[startAddr + period * 3 + mm_Offset])) { 539 | //Jump back to day before and last schedule of day 540 | weekDay = weekDay - 1; 541 | weekDay = weekDay % 7; 542 | startAddr = (this->byteSchedulingDays == 18 ? (weekDay == 0 ? 36 : (weekDay == 6 ? 18 : 0)) : ((weekDay == 0) || (weekDay == 6) ? 18 : 0)); 543 | period = 5; 544 | } else { 545 | //check the schedules in same day 546 | for (int i = 1; i < 6; i++) { 547 | int index = startAddr + i * 3; 548 | if (index < this->byteSchedulingDays * 3) { 549 | if ((i < 5) && (index + 1 < this->byteSchedulingDays * 3)) { 550 | if (wClock->isTimeBetween(schedules[index + hh_Offset], schedules[index + mm_Offset], 551 | schedules[startAddr + (i + 1) * 3 + hh_Offset], schedules[startAddr + (i + 1) * 3 + mm_Offset])) { 552 | period = i; 553 | break; 554 | } 555 | } else if (wClock->isTimeLaterThan(schedules[index + hh_Offset], schedules[index + mm_Offset])) { 556 | period = (i == 5 ? 5 : 1); 557 | } 558 | } else { 559 | break; 560 | } 561 | } 562 | } 563 | int newPeriod = startAddr + period * 3; 564 | if ((this->switchBackToAuto->asBool()) && 565 | (this->currentSchedulePeriod > -1) && (newPeriod != this->currentSchedulePeriod) && 566 | (this->schedulesMode->equalsString(SCHEDULES_MODE_OFF))) { 567 | this->schedulesMode->asString(SCHEDULES_MODE_AUTO); 568 | } 569 | this->currentSchedulePeriod = newPeriod; 570 | } else { 571 | this->currentSchedulePeriod = -1; 572 | } 573 | } 574 | 575 | virtual void deviceOnToMcu(WProperty* property) { 576 | if (!isReceivingDataFromMcu()) { 577 | //55 AA 00 06 00 05 01 01 00 01 01 578 | byte dt = (this->deviceOn->asBool() ? 0x01 : 0x00); 579 | unsigned char deviceOnCommand[] = { 0x55, 0xAA, 0x00, 0x06, 0x00, 0x05, 580 | byteDeviceOn, 0x01, 0x00, 0x01, dt}; 581 | commandCharsToSerial(11, deviceOnCommand); 582 | } 583 | } 584 | 585 | void targetTemperatureManualModeToMcu() { 586 | if ((!isReceivingDataFromMcu()) && (schedulesMode->equalsString(SCHEDULES_MODE_OFF))) { 587 | network()->debug(F("Set target Temperature (manual mode) to %D"), targetTemperatureManualMode); 588 | //55 AA 00 06 00 08 02 02 00 04 00 00 00 2C 589 | byte ulValues[4]; 590 | WSettings::getUnsignedLongBytes((targetTemperatureManualMode * this->temperatureFactor), ulValues); 591 | unsigned char setTemperatureCommand[] = { 0x55, 0xAA, 0x00, 0x06, 0x00, 0x08, 592 | byteTemperatureTarget, 0x02, 0x00, 0x04, ulValues[0], ulValues[1], ulValues[2], ulValues[3]}; 593 | commandCharsToSerial(14, setTemperatureCommand); 594 | } 595 | } 596 | 597 | void schedulesModeToMcu(WProperty* property) { 598 | if ((!isReceivingDataFromMcu()) && (schedulesMode != nullptr)) { 599 | //55 AA 00 06 00 05 04 04 00 01 01 600 | byte sm = schedulesMode->enumIndex(); 601 | if (sm != 0xFF) { 602 | unsigned char deviceOnCommand[] = { 0x55, 0xAA, 0x00, 0x06, 0x00, 0x05, 603 | byteSchedulesMode, 0x04, 0x00, 0x01, sm}; 604 | commandCharsToSerial(11, deviceOnCommand); 605 | } 606 | } 607 | } 608 | 609 | void lockedToMcu(WProperty* property) { 610 | if (!isReceivingDataFromMcu()) { 611 | byte dt = (this->locked->asBool() ? 0x01 : 0x00); 612 | unsigned char deviceOnCommand[] = { 0x55, 0xAA, 0x00, 0x06, 0x00, 0x05, 613 | byteLocked, 0x01, 0x00, 0x01, dt}; 614 | commandCharsToSerial(11, deviceOnCommand); 615 | } 616 | } 617 | 618 | virtual void schedulesToMcu() { 619 | if (receivedSchedules()) { 620 | //Changed schedules from MQTT server, send to mcu 621 | //send the changed array to MCU 622 | //per unit |MM HH TT| 623 | //55 AA 00 06 00 3A 65 00 00 36| 624 | //00 06 28|00 08 1E|1E 0B 1E|1E 0D 1E|00 11 2C|00 16 1E| 625 | //00 06 28|00 08 28|1E 0B 28|1E 0D 28|00 11 28|00 16 1E| 626 | //00 06 28|00 08 28|1E 0B 28|1E 0D 28|00 11 28|00 16 1E| 627 | int daysToSend = this->byteSchedulingDays; 628 | int functionLength = (daysToSend * 3); 629 | unsigned char scheduleCommand[functionLength]; 630 | scheduleCommand[0] = 0x55; 631 | scheduleCommand[1] = 0xaa; 632 | scheduleCommand[2] = 0x00; 633 | scheduleCommand[3] = 0x06; 634 | scheduleCommand[4] = 0x00; 635 | scheduleCommand[5] = (functionLength + 4); 636 | scheduleCommand[6] = byteSchedules; 637 | scheduleCommand[7] = 0x00; 638 | scheduleCommand[8] = 0x00; 639 | scheduleCommand[9] = functionLength; 640 | for (int i = 0; i < functionLength; i++) { 641 | scheduleCommand[i + 10] = schedules[i]; 642 | } 643 | commandCharsToSerial(functionLength + 10, scheduleCommand); 644 | //notify change 645 | this->notifySchedules(); 646 | } 647 | } 648 | 649 | void handleSchedulesChange(String completeTopic) { 650 | network()->debug(F("Send Schedules state...")); 651 | if (completeTopic == "") { 652 | completeTopic = String(network()->mqttBaseTopic()) + SLASH + String(this->id()) + SLASH + String(network()->mqttStateTopic()) + SLASH + SCHEDULES; 653 | } 654 | WStringStream* response = network()->getResponseStream(); 655 | WJson json(response); 656 | json.beginObject(); 657 | this->toJsonSchedules(&json, 0);// SCHEDULE_WORKDAY); 658 | this->toJsonSchedules(&json, 1);// SCHEDULE_SATURDAY); 659 | this->toJsonSchedules(&json, 2);// SCHEDULE_SUNDAY); 660 | json.endObject(); 661 | network()->publishMqtt(completeTopic.c_str(), response); 662 | } 663 | 664 | void printConfigSchedulesPage(AsyncWebServerRequest* request, Print* page) { 665 | byte hh_Offset = this->byteSchedulingPosHour; 666 | byte mm_Offset = this->byteSchedulingPosMinute; 667 | page->printf(HTTP_CONFIG_PAGE_BEGIN, SCHEDULES); 668 | page->print(F("")); 669 | page->print(F("")); 670 | page->print(F("")); 671 | page->print(F("")); 672 | if (this->byteSchedulingDays > 6) { 673 | page->print(F("")); 674 | } 675 | if (this->byteSchedulingDays > 12) { 676 | page->print(F("")); 677 | } 678 | page->print(F("")); 679 | for (byte period = 0; period < 6; period++) { 680 | page->print(F("")); 681 | page->printf("", String(period + 1).c_str()); 682 | for (byte sd = 0; sd < 3; sd++) { 683 | int index = sd * 18 + period * 3; 684 | if (index < this->byteSchedulingDays * 3) { 685 | char timeStr[6]; 686 | char keyH[4]; 687 | char keyT[4]; 688 | snprintf(keyH, 4, "%c%ch", SCHEDULES_DAYS[sd], SCHEDULES_PERIODS[period]); 689 | snprintf(keyT, 4, "%c%ct", SCHEDULES_DAYS[sd], SCHEDULES_PERIODS[period]); 690 | //hour 691 | snprintf(timeStr, 6, "%02d:%02d", schedules[index + hh_Offset], schedules[index + mm_Offset]); 692 | 693 | page->print(F("")); 701 | } else { 702 | break; 703 | } 704 | } 705 | page->print(F("")); 706 | } 707 | page->print(F("
WeekdayWeekend 1Weekend 2
Period %s")); 694 | page->print(F("Time:")); 695 | page->printf(HTTP_INPUT_FIELD, keyH, "5", timeStr); 696 | //temp 697 | String tempStr((double) schedules[index + 2] / this->temperatureFactor, 1); 698 | page->print(F("Temp:")); 699 | page->printf(HTTP_INPUT_FIELD, keyT, "4", tempStr.c_str()); 700 | page->print(F("
")); 708 | page->print(FPSTR(HTTP_CONFIG_SAVE_BUTTON)); 709 | } 710 | 711 | void submitConfigSchedulesPage(AsyncWebServerRequest* request, Print* page) { 712 | schedulesChanged = false; 713 | for (int period = 0; period < 6; period++) { 714 | for (int sd = 0; sd < 3; sd++) { 715 | char keyH[4]; 716 | char keyT[4]; 717 | snprintf(keyH, 4, "%c%ch", SCHEDULES_DAYS[sd], SCHEDULES_PERIODS[period]); 718 | snprintf(keyT, 4, "%c%ct", SCHEDULES_DAYS[sd], SCHEDULES_PERIODS[period]); 719 | if ((request->hasArg(keyH)) && (request->hasArg(keyT))) { 720 | processSchedulesKeyValue(keyH, request->arg(keyH).c_str()); 721 | processSchedulesKeyValue(keyT, request->arg(keyT).c_str()); 722 | } 723 | } 724 | } 725 | if (schedulesChanged) { 726 | network()->debug(F("Some schedules changed. Write to MCU...")); 727 | this->schedulesToMcu(); 728 | page->print(F("Changed schedules have been saved.")); 729 | } else { 730 | page->print(F("Schedules have not changed.")); 731 | } 732 | } 733 | 734 | void setTargetTemperature(WProperty* property) { 735 | if (!WProperty::isEqual(targetTemperatureManualMode, this->targetTemperature->asDouble(), 0.01)) { 736 | targetTemperatureManualMode = this->targetTemperature->asDouble(); 737 | targetTemperatureManualModeToMcu(); 738 | //schedulesMode->setString(SCHEDULES_MODE_OFF); 739 | } 740 | } 741 | 742 | void updateTargetTemperature() { 743 | if ((this->currentSchedulePeriod != -1) && (schedulesMode->equalsString(SCHEDULES_MODE_AUTO))) { 744 | double temp = (double) schedules[this->currentSchedulePeriod + 2] / this->temperatureFactor; 745 | targetTemperature->asDouble(temp); 746 | } else { 747 | targetTemperature->asDouble(targetTemperatureManualMode); 748 | } 749 | } 750 | 751 | bool receivedSchedules() { 752 | return ((network()->isDebugging()) || (this->schedulesReceived)); 753 | } 754 | 755 | void notifyState() { 756 | lastNotify = 0; 757 | } 758 | 759 | void notifySchedules() { 760 | lastScheduleNotify = 0; 761 | } 762 | 763 | private : 764 | unsigned long lastNotify, lastScheduleNotify; 765 | bool schedulesChanged, schedulesReceived; 766 | int currentSchedulePeriod; 767 | 768 | 769 | }; 770 | 771 | #endif 772 | -------------------------------------------------------------------------------- /src/WThermostat_BAC_002_ALW.h: -------------------------------------------------------------------------------- 1 | #ifndef THERMOSTAT_BAC_002_ALW_H 2 | #define THERMOSTAT_BAC_002_ALW_H 3 | 4 | #include 5 | #include 6 | #include "WThermostat.h" 7 | 8 | const char* SYSTEM_MODE_COOL = "cool"; 9 | const char* SYSTEM_MODE_HEAT = "heat"; 10 | const char* SYSTEM_MODE_FAN = "fan_only"; 11 | const char* FAN_MODE_AUTO = SCHEDULES_MODE_AUTO; 12 | const char* FAN_MODE_LOW = "low"; 13 | const char* FAN_MODE_MEDIUM = "medium"; 14 | const char* FAN_MODE_HIGH = "high"; 15 | 16 | class WThermostat_BAC_002_ALW : public WThermostat { 17 | public : 18 | WThermostat_BAC_002_ALW(WNetwork* network, WProperty* thermostatModel, WClock* wClock) 19 | : WThermostat(network, thermostatModel, wClock) { 20 | network->debug(F("WThermostat_BAC_002_ALW created")); 21 | } 22 | 23 | virtual void configureCommandBytes() { 24 | this->byteDeviceOn = 0x01; 25 | this->byteTemperatureActual = 0x03; 26 | this->byteTemperatureTarget = 0x02; 27 | this->byteTemperatureFloor = NOT_SUPPORTED; 28 | this->temperatureFactor = 2.0f; 29 | this->byteSchedulesMode = 0x04; 30 | this->byteLocked = 0x06; 31 | this->byteSchedules = 0x68; 32 | this->byteSchedulingPosHour = 1; 33 | this->byteSchedulingPosMinute = 0; 34 | this->byteSchedulingDays = 18; 35 | //custom 36 | this->byteSystemMode = 0x66; 37 | this->byteFanMode = 0x67; 38 | } 39 | 40 | virtual void initializeProperties() { 41 | WThermostat::initializeProperties(); 42 | //systemMode 43 | this->systemMode = new WProperty("systemMode", "System Mode", STRING, TYPE_THERMOSTAT_MODE_PROPERTY); 44 | this->systemMode->addEnumString(SYSTEM_MODE_COOL); 45 | this->systemMode->addEnumString(SYSTEM_MODE_HEAT); 46 | this->systemMode->addEnumString(SYSTEM_MODE_FAN); 47 | this->systemMode->addListener(std::bind(&WThermostat_BAC_002_ALW::systemModeToMcu, this, std::placeholders::_1)); 48 | this->addProperty(systemMode); 49 | //fanMode 50 | this->fanMode = new WProperty("fanMode", "Fan", STRING, TYPE_FAN_MODE_PROPERTY); 51 | this->fanMode->addEnumString(FAN_MODE_AUTO); 52 | this->fanMode->addEnumString(FAN_MODE_HIGH); 53 | this->fanMode->addEnumString(FAN_MODE_MEDIUM); 54 | this->fanMode->addEnumString(FAN_MODE_LOW); 55 | this->fanMode->addListener(std::bind(&WThermostat_BAC_002_ALW::fanModeToMcu, this, std::placeholders::_1)); 56 | this->addProperty(fanMode); 57 | } 58 | 59 | protected : 60 | 61 | virtual bool processStatusCommand(byte cByte, byte commandLength) { 62 | //Status report from MCU 63 | bool changed = false; 64 | bool knownCommand = WThermostat::processStatusCommand(cByte, commandLength); 65 | 66 | if (!knownCommand) { 67 | const char* newS; 68 | if (cByte == this->byteSystemMode) { 69 | if (commandLength == 0x05) { 70 | //MODEL_BAC_002_ALW - systemMode 71 | //cooling: 55 AA 00 06 00 05 66 04 00 01 00 72 | //heating: 55 AA 00 06 00 05 66 04 00 01 01 73 | //ventilation: 55 AA 00 06 00 05 66 04 00 01 02 74 | newS = systemMode->enumString(receivedCommand[10]); 75 | if (newS != nullptr) { 76 | changed = ((changed) || (systemMode->asString(newS))); 77 | knownCommand = true; 78 | } 79 | } 80 | } else if (cByte == this->byteFanMode) { 81 | if (commandLength == 0x05) { 82 | //fanMode 83 | //auto - 55 aa 01 07 00 05 67 04 00 01 00 84 | //high - 55 aa 01 07 00 05 67 04 00 01 01 85 | //medium - 55 aa 01 07 00 05 67 04 00 01 02 86 | //low - 55 aa 01 07 00 05 67 04 00 01 03 87 | newS = fanMode->enumString(receivedCommand[10]); 88 | if (newS != nullptr) { 89 | changed = ((changed) || (fanMode->asString(newS))); 90 | knownCommand = true; 91 | } 92 | } 93 | } else if (cByte == 0x05) { 94 | if (commandLength == 0x05) { 95 | //ecoMode -> ignore 96 | knownCommand = true; 97 | } 98 | } 99 | } 100 | if (changed) { 101 | notifyState(); 102 | } 103 | return knownCommand; 104 | } 105 | 106 | void systemModeToMcu(WProperty* property) { 107 | if (!isReceivingDataFromMcu()) { 108 | byte sm = property->enumIndex(); 109 | if (sm != 0xFF) { 110 | //send to device 111 | //cooling: 55 AA 00 06 00 05 66 04 00 01 00 112 | //heating: 55 AA 00 06 00 05 66 04 00 01 01 113 | //ventilation: 55 AA 00 06 00 05 66 04 00 01 02 114 | unsigned char cm[] = { 0x55, 0xAA, 0x00, 0x06, 0x00, 0x05, 115 | this->byteSystemMode, 0x04, 0x00, 0x01, sm}; 116 | commandCharsToSerial(11, cm); 117 | } 118 | } 119 | } 120 | 121 | void fanModeToMcu(WProperty* property) { 122 | if (!isReceivingDataFromMcu()) { 123 | byte fm = fanMode->enumIndex(); 124 | if (fm != 0xFF) { 125 | //send to device 126 | //auto: 55 aa 00 06 00 05 67 04 00 01 00 127 | //high: 55 aa 00 06 00 05 67 04 00 01 01 128 | //medium: 55 aa 00 06 00 05 67 04 00 01 02 129 | //low: 55 aa 00 06 00 05 67 04 00 01 03 130 | unsigned char deviceOnCommand[] = { 0x55, 0xAA, 0x00, 0x06, 0x00, 0x05, 131 | this->byteFanMode, 0x04, 0x00, 0x01, fm}; 132 | commandCharsToSerial(11, deviceOnCommand); 133 | } 134 | } 135 | } 136 | 137 | private : 138 | WProperty* systemMode; 139 | byte byteSystemMode; 140 | WProperty* fanMode; 141 | byte byteFanMode; 142 | 143 | }; 144 | 145 | #endif 146 | -------------------------------------------------------------------------------- /src/WThermostat_BHT_002_GBLW.h: -------------------------------------------------------------------------------- 1 | #ifndef THERMOSTAT_BHT_002_GBLW_H 2 | #define THERMOSTAT_BHT_002_GBLW_H 3 | 4 | #include 5 | #include 6 | #include "WThermostat.h" 7 | 8 | class WThermostat_BHT_002_GBLW : public WThermostat { 9 | public : 10 | WThermostat_BHT_002_GBLW(WNetwork* network, WProperty* thermostatModel, WClock* wClock) 11 | : WThermostat(network, thermostatModel, wClock) { 12 | network->debug(F("WThermostat_BHT_002_GBLW created")); 13 | } 14 | 15 | virtual void configureCommandBytes() { 16 | this->byteDeviceOn = 0x01; 17 | this->byteTemperatureActual = 0x03; 18 | this->byteTemperatureTarget = 0x02; 19 | this->byteTemperatureFloor = 0x66; 20 | this->temperatureFactor = 2.0f; 21 | this->byteSchedulesMode = 0x04; 22 | this->byteLocked = 0x06; 23 | this->byteSchedules = 0x65; 24 | this->byteSchedulingPosHour = 1; 25 | this->byteSchedulingPosMinute = 0; 26 | this->byteSchedulingDays = 18; 27 | //custom 28 | this->byteEcoMode = 0x05; 29 | } 30 | 31 | virtual void initializeProperties() { 32 | WThermostat::initializeProperties(); 33 | } 34 | 35 | protected : 36 | 37 | virtual bool processStatusCommand(byte cByte, byte commandLength) { 38 | //Status report from MCU 39 | bool changed = false; 40 | bool knownCommand = WThermostat::processStatusCommand(cByte, commandLength); 41 | 42 | if (!knownCommand) { 43 | if (cByte == 0x68) { 44 | if (receivedCommand[5] == 0x05) { 45 | //Unknown permanently sent from MCU 46 | //55 aa 01 07 00 05 68 01 00 01 01 47 | knownCommand = true; 48 | } 49 | } else if (cByte == this->byteEcoMode) { 50 | if (commandLength == 0x05) { 51 | //ecoMode -> ignore 52 | knownCommand = true; 53 | } 54 | } 55 | } 56 | if (changed) { 57 | notifyState(); 58 | } 59 | return knownCommand; 60 | } 61 | 62 | void ecoModeToMcu(WProperty* property) { 63 | if (!isReceivingDataFromMcu()) { 64 | //55 AA 00 06 00 05 05 01 00 01 01 65 | byte dt = 0x00; //(this->ecoMode->getBoolean() ? 0x01 : 0x00); 66 | unsigned char deviceOnCommand[] = { 0x55, 0xAA, 0x00, 0x06, 0x00, 0x05, 67 | this->byteEcoMode, 0x01, 0x00, 0x01, dt}; 68 | commandCharsToSerial(11, deviceOnCommand); 69 | } 70 | } 71 | 72 | private : 73 | byte byteEcoMode; 74 | }; 75 | 76 | #endif 77 | -------------------------------------------------------------------------------- /src/WThermostat_CalypsoW.h: -------------------------------------------------------------------------------- 1 | #ifndef THERMOSTAT_CALYPSOW_H 2 | #define THERMOSTAT_CALYPSOW_H 3 | 4 | #include 5 | #include 6 | #include "WThermostat.h" 7 | 8 | class WThermostat_CalypsoW : public WThermostat { 9 | public : 10 | WThermostat_CalypsoW(WNetwork* network, WProperty* thermostatModel, WClock* wClock) 11 | : WThermostat(network, thermostatModel, wClock) { 12 | network->debug(F("WThermostat_CalypsoW created")); 13 | } 14 | 15 | virtual void configureCommandBytes() { 16 | this->byteDeviceOn = 0x01; 17 | this->byteTemperatureActual = 0x05; 18 | this->byteTemperatureTarget = 0x02; 19 | this->byteTemperatureFloor = 0x08; 20 | this->temperatureFactor = 10.0f; 21 | this->byteSchedulesMode = 0x03; 22 | this->byteLocked = 0x06; 23 | this->byteSchedules = NOT_SUPPORTED; 24 | this->byteSchedulingPosHour = 1; 25 | this->byteSchedulingPosMinute = 0; 26 | this->byteSchedulingDays = 18; 27 | } 28 | 29 | virtual void initializeProperties() { 30 | WThermostat::initializeProperties(); 31 | //schedulesMode 32 | this->schedulesMode->clearEnums(); 33 | this->schedulesMode->addEnumString(SCHEDULES_MODE_HOLIDAY); 34 | this->schedulesMode->addEnumString(SCHEDULES_MODE_AUTO); 35 | this->schedulesMode->addEnumString(SCHEDULES_MODE_HOLD); 36 | } 37 | 38 | protected : 39 | 40 | private : 41 | 42 | }; 43 | 44 | #endif 45 | -------------------------------------------------------------------------------- /src/WThermostat_DLX_LH01.h: -------------------------------------------------------------------------------- 1 | #ifndef THERMOSTAT_DLX_LH01_H 2 | #define THERMOSTAT_DLX_LH01_H 3 | 4 | #include 5 | #include 6 | #include "WThermostat.h" 7 | 8 | const char* SCHEDULES_MODE_ECO = "eco"; 9 | 10 | class WThermostat_DLX_LH01 : public WThermostat { 11 | public : 12 | WThermostat_DLX_LH01(WNetwork* network, WProperty* thermostatModel, WClock* wClock) 13 | : WThermostat(network, thermostatModel, wClock) { 14 | network->debug(F("WThermostat_DLX_LH01 created")); 15 | } 16 | 17 | virtual void configureCommandBytes() { 18 | this->byteDeviceOn = 0x01; 19 | this->byteTemperatureActual = 0x03; 20 | this->byteTemperatureTarget = 0x02; 21 | this->byteTemperatureFloor = NOT_SUPPORTED; 22 | this->temperatureFactor = 1.0f; 23 | this->byteSchedulesMode = 0x04; 24 | this->byteLocked = 0x07; 25 | this->byteSchedules = NOT_SUPPORTED; 26 | this->byteSchedulingPosHour = 1; 27 | this->byteSchedulingPosMinute = 0; 28 | this->byteSchedulingDays = 18; 29 | //custom 30 | } 31 | 32 | virtual void initializeProperties() { 33 | WThermostat::initializeProperties(); 34 | //schedulesMode 35 | this->schedulesMode->clearEnums(); 36 | this->schedulesMode->addEnumString(SCHEDULES_MODE_AUTO); 37 | this->schedulesMode->addEnumString(SCHEDULES_MODE_OFF); 38 | this->schedulesMode->addEnumString(SCHEDULES_MODE_ECO); 39 | } 40 | 41 | protected : 42 | 43 | virtual bool processStatusCommand(byte cByte, byte commandLength) { 44 | //Status report from MCU 45 | bool changed = false; 46 | bool knownCommand = WThermostat::processStatusCommand(cByte, commandLength); 47 | 48 | if (!knownCommand) { 49 | /*if (cByte == this->byteEcoMode) { 50 | if (commandLength == 0x05) { 51 | //ecoMode -> ignore 52 | knownCommand = true; 53 | } 54 | }*/ 55 | } 56 | if (changed) { 57 | notifyState(); 58 | } 59 | return knownCommand; 60 | } 61 | 62 | private : 63 | 64 | }; 65 | 66 | #endif 67 | -------------------------------------------------------------------------------- /src/WThermostat_ET81W.h: -------------------------------------------------------------------------------- 1 | #ifndef THERMOSTAT_ET81W_H 2 | #define THERMOSTAT_ET81W_H 3 | 4 | #include 5 | #include 6 | #include "WThermostat.h" 7 | 8 | const char* SCHEDULES_MODE_HOLIDAY = "holiday"; 9 | 10 | class WThermostat_ET81W : public WThermostat { 11 | public : 12 | WThermostat_ET81W(WNetwork* network, WProperty* thermostatModel, WClock* wClock) 13 | : WThermostat(network, thermostatModel, wClock) { 14 | network->debug(F("WThermostat_ET_81_W created")); 15 | } 16 | 17 | virtual void configureCommandBytes() { 18 | this->byteDeviceOn = 0x01; 19 | this->byteTemperatureActual = 0x08; 20 | this->byteTemperatureTarget = 0x02; 21 | this->byteTemperatureFloor = 0x05; 22 | this->temperatureFactor = 10.0f; 23 | this->byteSchedulesMode = 0x03; 24 | this->byteLocked = 0x06; 25 | this->byteSchedules = NOT_SUPPORTED; 26 | this->byteSchedulingPosHour = 1; 27 | this->byteSchedulingPosMinute = 0; 28 | this->byteSchedulingDays = 18; 29 | } 30 | 31 | virtual void initializeProperties() { 32 | WThermostat::initializeProperties(); 33 | //schedulesMode 34 | this->schedulesMode->clearEnums(); 35 | this->schedulesMode->addEnumString(SCHEDULES_MODE_HOLIDAY); 36 | this->schedulesMode->addEnumString(SCHEDULES_MODE_AUTO); 37 | this->schedulesMode->addEnumString(SCHEDULES_MODE_HOLD); 38 | } 39 | 40 | protected : 41 | 42 | private : 43 | 44 | }; 45 | 46 | #endif 47 | -------------------------------------------------------------------------------- /src/WThermostat_HY08WE.h: -------------------------------------------------------------------------------- 1 | #ifndef THERMOSTAT_HY08WE_H 2 | #define THERMOSTAT_HY08WE_H 3 | 4 | #include 5 | #include 6 | #include "WThermostat.h" 7 | 8 | class WThermostat_HY08WE : public WThermostat { 9 | public : 10 | WThermostat_HY08WE(WNetwork* network, WProperty* thermostatModel, WClock* wClock) 11 | : WThermostat(network, thermostatModel, wClock) { 12 | network->debug(F("WThermostat_HY08WE created")); 13 | } 14 | 15 | virtual void configureCommandBytes() { 16 | this->byteDeviceOn = 0x01; 17 | this->byteTemperatureActual = 0x03; 18 | this->byteTemperatureTarget = 0x02; 19 | this->byteTemperatureFloor = 0x66; 20 | this->temperatureFactor = 10.0f; 21 | this->byteSchedulesMode = 0x04; 22 | this->byteLocked = 0x06; 23 | this->byteSchedules = NOT_SUPPORTED; 24 | this->byteSchedulingPosHour = 1; 25 | this->byteSchedulingPosMinute = 0; 26 | this->byteSchedulingDays = 18; 27 | } 28 | 29 | virtual void initializeProperties() { 30 | WThermostat::initializeProperties(); 31 | //2021-01-14 - schedulesMode 32 | this->schedulesMode->clearEnums(); 33 | this->schedulesMode->addEnumString(SCHEDULES_MODE_OFF); 34 | this->schedulesMode->addEnumString(SCHEDULES_MODE_AUTO); 35 | this->schedulesMode->addEnumString(SCHEDULES_MODE_HOLIDAY); 36 | this->schedulesMode->addEnumString(SCHEDULES_MODE_HOLD); 37 | } 38 | 39 | protected : 40 | 41 | virtual bool processStatusCommand(byte cByte, byte commandLength) { 42 | //Status report from MCU 43 | bool changed = false; 44 | bool knownCommand = WThermostat::processStatusCommand(cByte, commandLength); 45 | 46 | if (!knownCommand) { 47 | //const char* newS; 48 | /*if (cByte == this->byteXXX) { 49 | if (commandLength == 0xXX) { 50 | newS = systemMode->getEnumString(receivedCommand[10]); 51 | if (newS != nullptr) { 52 | changed = ((changed) || (systemMode->setString(newS))); 53 | knownCommand = true; 54 | } 55 | } 56 | }*/ 57 | } 58 | if (changed) { 59 | notifyState(); 60 | } 61 | return knownCommand; 62 | } 63 | 64 | private : 65 | 66 | }; 67 | 68 | #endif 69 | -------------------------------------------------------------------------------- /src/WThermostat_ME102H.h: -------------------------------------------------------------------------------- 1 | #ifndef THERMOSTAT_ME102H_H 2 | #define THERMOSTAT_ME102H_H 3 | 4 | #include 5 | #include 6 | #include "WThermostat.h" 7 | 8 | const char* SENSOR_SELECTION_INTERNAL = "internal"; 9 | const char* SENSOR_SELECTION_FLOOR = "floor"; 10 | const char* SENSOR_SELECTION_BOTH = "both"; 11 | 12 | class WThermostat_ME102H : public WThermostat { 13 | public : 14 | WThermostat_ME102H(WNetwork* network, WProperty* thermostatModel, WClock* wClock) 15 | : WThermostat(network, thermostatModel, wClock) { 16 | network->debug(F("WThermostat_ME102H created")); 17 | } 18 | 19 | virtual void configureCommandBytes() { 20 | this->byteDeviceOn = 0x01; 21 | this->byteTemperatureActual = 0x18; 22 | this->byteTemperatureTarget = 0x10; 23 | this->byteTemperatureFloor = 0x65; 24 | this->temperatureFactor = 1.0f; 25 | this->byteSchedulesMode = 0x02; 26 | this->byteLocked = 0x28; 27 | this->byteSchedules = 0x6c; 28 | this->byteSchedulingPosHour = 0; 29 | this->byteSchedulingPosMinute = 1; 30 | this->byteSchedulingDays = 8; 31 | //custom 32 | this->byteSensorSelection = 0x2b; 33 | } 34 | 35 | virtual void initializeProperties() { 36 | WThermostat::initializeProperties(); 37 | //schedules mode 38 | this->schedulesMode->clearEnums(); 39 | this->schedulesMode->addEnumString(SCHEDULES_MODE_AUTO); 40 | this->schedulesMode->addEnumString(SCHEDULES_MODE_OFF); 41 | //sensorSelection 42 | this->sensorSelection = new WProperty("sensorSelection", "Sensor Selection", STRING, TYPE_THERMOSTAT_MODE_PROPERTY); 43 | this->sensorSelection->addEnumString(SENSOR_SELECTION_INTERNAL); 44 | this->sensorSelection->addEnumString(SENSOR_SELECTION_FLOOR); 45 | this->sensorSelection->addEnumString(SENSOR_SELECTION_BOTH); 46 | this->sensorSelection->visibility(MQTT); 47 | this->sensorSelection->addListener(std::bind(&WThermostat_ME102H::sensorSelectionToMcu, this, std::placeholders::_1)); 48 | this->addProperty(this->sensorSelection); 49 | } 50 | 51 | protected : 52 | 53 | virtual bool processStatusCommand(byte cByte, byte commandLength) { 54 | //Status report from MCU 55 | bool changed = false; 56 | bool knownCommand = WThermostat::processStatusCommand(cByte, commandLength); 57 | 58 | if (!knownCommand) { 59 | const char* newS; 60 | if (cByte == this->byteSensorSelection) { 61 | if (commandLength == 0x05) { 62 | //sensor selection - 63 | //internal: 55 aa 03 07 00 05 2b 04 00 01 00 64 | //floor: 55 aa 03 07 00 05 2b 04 00 01 01 65 | //both: 55 aa 03 07 00 05 2b 04 00 01 02 66 | newS = this->sensorSelection->enumString(receivedCommand[10]); 67 | if (newS != nullptr) { 68 | changed = ((changed) || (this->sensorSelection->asString(newS))); 69 | knownCommand = true; 70 | } 71 | } 72 | } else { 73 | //consume some unsupported commands 74 | switch (cByte) { 75 | case 0x17 : 76 | //Temperature Scale C / 77 | //MCU: 55 aa 03 07 00 05 17 04 00 01 00 78 | knownCommand = true; 79 | break; 80 | case 0x13 : 81 | //Temperature ceiling - 35C / MCU: 55 aa 03 07 00 08 13 02 00 04 00 00 00 23 82 | knownCommand = true; 83 | break; 84 | case 0x1a : 85 | //Lower limit of temperature - 5C / MCU: 55 aa 03 07 00 08 1a 02 00 04 00 00 00 05 86 | knownCommand = true; 87 | break; 88 | case 0x6a : 89 | //temp_differ_on - 1C / MCU: 55 aa 03 07 00 08 6a 02 00 04 00 00 00 01 90 | knownCommand = true; 91 | break; 92 | case 0x1b : 93 | //Temperature correction - 0C / MCU: 55 aa 03 07 00 08 1b 02 00 04 00 00 00 00 94 | knownCommand = true; 95 | break; 96 | case 0x67 : 97 | //freeze / MCU: 55 aa 03 07 00 05 67 01 00 01 00 98 | knownCommand = true; 99 | break; 100 | case 0x68 : 101 | // programming_mode - weekend (2 days off) / MCU: 55 aa 03 07 00 05 68 04 00 01 01 102 | knownCommand = true; 103 | break; 104 | case 0x2d : 105 | //unknown Wifi state? / 106 | //MCU: 55 aa 03 07 00 05 2d 05 00 01 00 107 | knownCommand = true; 108 | break; 109 | case 0x24 : 110 | //unknown Wifi state? / MCU: 55 aa 03 07 00 05 24 04 00 01 00 111 | knownCommand = true; 112 | break; 113 | } 114 | } 115 | } 116 | if (changed) { 117 | notifyState(); 118 | } 119 | return knownCommand; 120 | } 121 | 122 | void sensorSelectionToMcu(WProperty* property) { 123 | if (!isReceivingDataFromMcu()) { 124 | byte sm = property->enumIndex(); 125 | if (sm != 0xFF) { 126 | //send to device 127 | //internal: 55 aa 03 07 00 05 2b 04 00 01 00 128 | //floor: 55 aa 03 07 00 05 2b 04 00 01 01 129 | //both: 55 aa 03 07 00 05 2b 04 00 01 02 130 | unsigned char cm[] = { 0x55, 0xAA, 0x00, 0x06, 0x00, 0x05, 131 | this->byteSensorSelection, 0x04, 0x00, 0x01, sm}; 132 | commandCharsToSerial(11, cm); 133 | } 134 | } 135 | } 136 | 137 | private : 138 | WProperty* sensorSelection; 139 | byte byteSensorSelection; 140 | 141 | }; 142 | 143 | #endif 144 | -------------------------------------------------------------------------------- /src/WThermostat_ME81H.h: -------------------------------------------------------------------------------- 1 | #ifndef THERMOSTAT_ME81H_H 2 | #define THERMOSTAT_ME81H_H 3 | 4 | #include 5 | #include 6 | #include "WThermostat.h" 7 | #include "WThermostat_BAC_002_ALW.h" 8 | #include "WThermostat_ME102H.h" 9 | 10 | class WThermostat_ME81H : public WThermostat { 11 | public : 12 | WThermostat_ME81H(WNetwork* network, WProperty* thermostatModel, WClock* wClock) 13 | : WThermostat(network, thermostatModel, wClock) { 14 | network->debug(F("WThermostat_ME81H created")); 15 | } 16 | 17 | virtual void configureCommandBytes() { 18 | this->byteDeviceOn = 0x01; 19 | //actual temperature must be handled in processStatusCommand (byte 0x08) 20 | this->byteTemperatureActual = NOT_SUPPORTED; 21 | this->byteTemperatureTarget = 0x10; 22 | this->byteTemperatureFloor = 0x00; 23 | this->temperatureFactor = 1.0f; 24 | this->byteSchedulesMode = 0x02; 25 | this->byteLocked = 0x28; 26 | this->byteSchedules = 0x26; 27 | this->byteSchedulingPosHour = 0; 28 | this->byteSchedulingPosMinute = 1; 29 | this->byteSchedulingDays = 8; 30 | //custom 31 | this->byteSystemMode = 0x24; 32 | this->byteSensorSelection = 0x2b; 33 | } 34 | 35 | virtual void initializeProperties() { 36 | WThermostat::initializeProperties(); 37 | //systemMode 38 | this->systemMode = new WProperty("systemMode", "System Mode", STRING, TYPE_THERMOSTAT_MODE_PROPERTY); 39 | this->systemMode->addEnumString(SYSTEM_MODE_HEAT); 40 | this->systemMode->addEnumString(SYSTEM_MODE_COOL); 41 | this->systemMode->addEnumString(SYSTEM_MODE_FAN); 42 | this->systemMode->addListener(std::bind(&WThermostat_ME81H::systemModeToMcu, this, std::placeholders::_1)); 43 | this->addProperty(systemMode); 44 | //sensorSelection 45 | this->sensorSelection = new WProperty("sensorSelection", "Sensor Selection", STRING, TYPE_THERMOSTAT_MODE_PROPERTY); 46 | this->sensorSelection->addEnumString(SENSOR_SELECTION_INTERNAL); 47 | this->sensorSelection->addEnumString(SENSOR_SELECTION_FLOOR); 48 | this->sensorSelection->addEnumString(SENSOR_SELECTION_BOTH); 49 | this->sensorSelection->visibility(MQTT); 50 | this->sensorSelection->addListener(std::bind(&WThermostat_ME81H::sensorSelectionToMcu, this, std::placeholders::_1)); 51 | this->addProperty(this->sensorSelection); 52 | } 53 | 54 | protected : 55 | 56 | virtual bool processStatusCommand(byte cByte, byte commandLength) { 57 | //Status report from MCU 58 | bool changed = false; 59 | bool knownCommand = WThermostat::processStatusCommand(cByte, commandLength); 60 | 61 | if (!knownCommand) { 62 | const char* newS; 63 | if (cByte == 0x18) { 64 | if (commandLength == 0x08) { 65 | //actual Temperature at this model has a different divider of 10 66 | unsigned long rawValue = WSettings::getUnsignedLong(receivedCommand[10], receivedCommand[11], receivedCommand[12], receivedCommand[13]); 67 | float newValue = (float) rawValue / 10.0f; 68 | changed = ((changed) || (!actualTemperature->equalsDouble(newValue))); 69 | actualTemperature->asDouble(newValue); 70 | knownCommand = true; 71 | } 72 | } else if (cByte == this->byteSystemMode) { 73 | if (commandLength == 0x05) { 74 | //MODEL_BAC_002_ALW - systemMode 75 | //cooling: 55 AA 00 06 00 05 66 04 00 01 00 76 | //heating: 55 AA 00 06 00 05 66 04 00 01 01 77 | //ventilation: 55 AA 00 06 00 05 66 04 00 01 02 78 | newS = systemMode->enumString(receivedCommand[10]); 79 | if (newS != nullptr) { 80 | changed = ((changed) || (systemMode->asString(newS))); 81 | knownCommand = true; 82 | } 83 | } 84 | } else if (cByte == this->byteSensorSelection) { 85 | if (commandLength == 0x05) { 86 | //sensor selection - 87 | //internal: 55 aa 03 07 00 05 2b 04 00 01 00 88 | //floor: 55 aa 03 07 00 05 2b 04 00 01 01 89 | //both: 55 aa 03 07 00 05 2b 04 00 01 02 90 | newS = this->sensorSelection->enumString(receivedCommand[10]); 91 | if (newS != nullptr) { 92 | changed = ((changed) || (this->sensorSelection->asString(newS))); 93 | knownCommand = true; 94 | } 95 | } 96 | } else { 97 | //consume some unsupported commands 98 | switch (cByte) { 99 | /*case 0x66 : 100 | //Temperature Scale C / 101 | //MCU: C / 55 aa 03 07 00 05 66 04 00 01 00 102 | //MCU: F / 55 aa 03 07 00 05 66 04 00 01 01 103 | knownCommand = true; 104 | break;*/ 105 | case 0x13 : 106 | //Temperature ceiling 107 | //MCU: 35C / 55 aa 03 07 00 08 13 02 00 04 00 00 00 23 108 | //MCU: 40C / 55 aa 03 07 00 08 13 02 00 04 00 00 00 28 109 | knownCommand = true; 110 | break; 111 | case 0x1a : 112 | //Lower limit of temperature 113 | //MCU: 5C / 55 aa 03 07 00 08 1a 02 00 04 00 00 00 05 114 | //MCU: 10C / 55 aa 03 07 00 08 1a 02 00 04 00 00 00 0a 115 | knownCommand = true; 116 | break; 117 | case 0x1b : 118 | //Temperature correction 119 | //MCU: 0C / 55 aa 03 07 00 08 1b 02 00 04 00 00 00 00 120 | //MCU: -2C / 55 aa 03 07 00 08 1b 02 00 04 ff ff ff fe 121 | knownCommand = true; 122 | break; 123 | case 0x0a : 124 | //freeze / 125 | //MCU: off / 55 aa 03 07 00 05 0a 01 00 01 00 126 | //MCU: on / 55 aa 03 07 00 05 0a 01 00 01 01 127 | knownCommand = true; 128 | break; 129 | case 0x65 : 130 | //temp_differ_on - 1C / 131 | //MCU: 55 aa 03 07 00 08 65 02 00 04 00 00 00 01 132 | knownCommand = true; 133 | break; 134 | case 0x66 : 135 | // programming_mode - weekend (2 days off) / MCU: 55 aa 03 07 00 05 68 04 00 01 01 136 | //01: 5+2 / 02: 6+1 / 03 7+0 day mode 137 | knownCommand = true; 138 | break; 139 | case 0x2d : 140 | //unknown Wifi state? / MCU: 55 aa 03 07 00 05 2d 05 00 01 00 141 | knownCommand = true; 142 | break; 143 | /*case 0x24 : 144 | //unknown Wifi state? / MCU: 55 aa 03 07 00 05 24 04 00 01 00 145 | knownCommand = true; 146 | break;*/ 147 | } 148 | } 149 | } 150 | if (changed) { 151 | notifyState(); 152 | } 153 | return knownCommand; 154 | } 155 | 156 | void systemModeToMcu(WProperty* property) { 157 | if (!isReceivingDataFromMcu()) { 158 | byte sm = property->enumIndex(); 159 | if (sm != 0xFF) { 160 | //send to device 161 | //cooling: 55 AA 00 06 00 05 66 04 00 01 00 162 | //heating: 55 AA 00 06 00 05 66 04 00 01 01 163 | //ventilation: 55 AA 00 06 00 05 66 04 00 01 02 164 | unsigned char cm[] = { 0x55, 0xAA, 0x00, 0x06, 0x00, 0x05, 165 | this->byteSystemMode, 0x04, 0x00, 0x01, sm}; 166 | commandCharsToSerial(11, cm); 167 | } 168 | } 169 | } 170 | 171 | void sensorSelectionToMcu(WProperty* property) { 172 | if (!isReceivingDataFromMcu()) { 173 | byte sm = property->enumIndex(); 174 | if (sm != 0xFF) { 175 | //send to device 176 | //internal: 55 aa 03 07 00 05 2d 05 00 01 00 177 | //floor: 55 aa 03 07 00 05 2d 05 00 01 01 178 | //both: 55 aa 03 07 00 05 2d 05 00 01 02 179 | unsigned char cm[] = { 0x55, 0xAA, 0x00, 0x06, 0x00, 0x05, 180 | this->byteSensorSelection, 0x05, 0x00, 0x01, sm}; 181 | commandCharsToSerial(11, cm); 182 | } 183 | } 184 | } 185 | 186 | private : 187 | WProperty* systemMode; 188 | byte byteSystemMode; 189 | WProperty* sensorSelection; 190 | byte byteSensorSelection; 191 | }; 192 | 193 | #endif 194 | -------------------------------------------------------------------------------- /src/WThermostat_MK70GBH.h: -------------------------------------------------------------------------------- 1 | #ifndef THERMOSTAT_MK70GBH_H 2 | #define THERMOSTAT_MK70GBH_H 3 | 4 | #include 5 | #include 6 | #include "WThermostat.h" 7 | 8 | class WThermostat_MK70GBH : public WThermostat { 9 | public : 10 | WThermostat_MK70GBH(WNetwork* network, WProperty* thermostatModel, WClock* wClock) 11 | : WThermostat(network, thermostatModel, wClock) { 12 | network->debug(F("WThermostat_MK70GBH created")); 13 | } 14 | 15 | virtual void configureCommandBytes() { 16 | this->byteDeviceOn = 0x01; 17 | this->byteTemperatureActual = 0x03; 18 | this->byteTemperatureTarget = 0x02; 19 | this->byteTemperatureFloor = NOT_SUPPORTED; 20 | this->temperatureFactor = 10.0f; 21 | this->byteSchedulesMode = 0x04; 22 | this->byteLocked = 0x08; 23 | this->byteSchedules = 0x2b; 24 | this->byteSchedulingPosHour = 0; 25 | this->byteSchedulingPosMinute = 1; 26 | this->byteSchedulingDays = 8; 27 | //custom parameters 28 | this->byteStatusMode = 0x05; 29 | } 30 | 31 | virtual void initializeProperties() { 32 | WThermostat::initializeProperties(); 33 | //schedulesMode 34 | this->schedulesMode->clearEnums(); 35 | this->schedulesMode->addEnumString(SCHEDULES_MODE_OFF); 36 | this->schedulesMode->addEnumString(SCHEDULES_MODE_AUTO); 37 | this->schedulesMode->addEnumString(SCHEDULES_MODE_HOLD); 38 | //statusMode 39 | this->statusMode = new WProperty("statusMode", "Status", STRING, TYPE_HEATING_COOLING_PROPERTY); 40 | this->statusMode->addEnumString(STATE_OFF); 41 | this->statusMode->addEnumString(STATE_HEATING); 42 | this->statusMode->readOnly(true); 43 | this->statusMode->visibility(MQTT); 44 | this->addProperty(statusMode); 45 | } 46 | 47 | protected : 48 | 49 | virtual bool processStatusCommand(byte cByte, byte commandLength) { 50 | //Status report from MCU 51 | bool changed = false; 52 | bool knownCommand = WThermostat::processStatusCommand(cByte, commandLength); 53 | 54 | if (!knownCommand) { 55 | const char* newS; 56 | if (cByte == byteStatusMode) { 57 | if (commandLength == 0x05) { 58 | //status 59 | newS = statusMode->enumString(receivedCommand[10]); 60 | if (newS != nullptr) { 61 | changed = ((changed) || (statusMode->asString(newS))); 62 | knownCommand = true; 63 | } 64 | } 65 | } 66 | } 67 | if (changed) { 68 | notifyState(); 69 | } 70 | return knownCommand; 71 | } 72 | 73 | virtual bool processStatusSchedules(byte commandLength) { 74 | bool result = (commandLength == 0x24); 75 | if (result) { 76 | bool changed = false; 77 | //schedules for model MK70GB-H 78 | int res = 1; 79 | int ii = 0; 80 | for (int i = 0; i < 32; i++) { 81 | byte newByte = receivedCommand[i + 10]; 82 | if (i != 2) { 83 | if (i > 2) 84 | res = (i+2) % 4; 85 | if (res != 0) { 86 | changed = ((changed) || (newByte != schedules[ii])); 87 | schedules[ii] = newByte; 88 | ii++; 89 | } 90 | } 91 | } 92 | if (changed) { 93 | notifySchedules(); 94 | } 95 | } 96 | return result; 97 | } 98 | 99 | virtual void schedulesToMcu() { 100 | if (receivedSchedules()) { 101 | int daysToSend = this->byteSchedulingDays; 102 | int functionLengthInt = (daysToSend * 3); 103 | char functionL = 0x20; 104 | char dataL = 0x24; 105 | unsigned char scheduleCommand[functionLengthInt+10]; 106 | scheduleCommand[0] = 0x55; 107 | scheduleCommand[1] = 0xaa; 108 | scheduleCommand[2] = 0x00; 109 | scheduleCommand[3] = 0x06; 110 | scheduleCommand[4] = 0x00; 111 | scheduleCommand[5] = dataL; //0x3a; // dataLength 112 | scheduleCommand[6] = byteSchedules; 113 | scheduleCommand[7] = 0x00; 114 | scheduleCommand[8] = 0x00; 115 | scheduleCommand[9] = functionL; 116 | 117 | int res = 1; 118 | functionLengthInt = functionLengthInt + 8; 119 | int ii = 0; 120 | for (int i = 0; i 2) { 124 | res = (i+2) % 4; 125 | if (res != 0) { 126 | scheduleCommand[i + 10] = schedules[ii]; 127 | ii++; 128 | } else { 129 | scheduleCommand[i + 10] = 0x00; 130 | } 131 | } else { 132 | scheduleCommand[i + 10] = schedules[ii]; 133 | ii++; 134 | } 135 | } 136 | 137 | commandCharsToSerial(functionLengthInt+10, scheduleCommand); 138 | //notify change 139 | this->notifySchedules(); 140 | } 141 | } 142 | 143 | private : 144 | WProperty* statusMode; 145 | byte byteStatusMode; 146 | 147 | }; 148 | 149 | #endif 150 | -------------------------------------------------------------------------------- /src/WThermostat_TEMPLATE.h: -------------------------------------------------------------------------------- 1 | #ifndef THERMOSTAT_TEMPLATE_H 2 | #define THERMOSTAT_TEMPLATE_H 3 | 4 | #include 5 | #include 6 | #include "WThermostat.h" 7 | 8 | class WThermostat_TEMPLATE : public WThermostat { 9 | public : 10 | WThermostat_TEMPLATE(WNetwork* network, WProperty* thermostatModel, WClock* wClock) 11 | : WThermostat(network, thermostatModel, wClock) { 12 | network->debug(F("WThermostat_TEMPLATE created")); 13 | } 14 | 15 | virtual void configureCommandBytes() { 16 | this->byteDeviceOn = 0x01; 17 | this->byteTemperatureActual = NOT_SUPPORTED; 18 | this->byteTemperatureTarget = NOT_SUPPORTED; 19 | this->byteTemperatureFloor = NOT_SUPPORTED; 20 | this->temperatureFactor = 10.0f; 21 | this->byteSchedulesMode = NOT_SUPPORTED; 22 | this->byteLocked = NOT_SUPPORTED; 23 | this->byteSchedules = NOT_SUPPORTED; 24 | this->byteSchedulingPosHour = 0; 25 | this->byteSchedulingPosMinute = 1; 26 | this->byteSchedulingDays = 8; 27 | } 28 | 29 | virtual void initializeProperties() { 30 | WThermostat::initializeProperties(); 31 | } 32 | 33 | protected : 34 | 35 | virtual bool processStatusCommand(byte cByte, byte commandLength) { 36 | //Status report from MCU 37 | bool changed = false; 38 | bool knownCommand = WThermostat::processStatusCommand(cByte, commandLength); 39 | 40 | if (!knownCommand) { 41 | //const char* newS; 42 | if (cByte == this->byteXXX) { 43 | if (commandLength == 0xXX) { 44 | /*newS = systemMode->getEnumString(receivedCommand[10]); 45 | if (newS != nullptr) { 46 | changed = ((changed) || (systemMode->setString(newS))); 47 | knownCommand = true; 48 | }*/ 49 | } 50 | } 51 | } 52 | if (changed) { 53 | notifyState(); 54 | } 55 | return knownCommand; 56 | } 57 | 58 | private : 59 | 60 | }; 61 | 62 | #endif 63 | -------------------------------------------------------------------------------- /src/WTuyaDevice.h: -------------------------------------------------------------------------------- 1 | #ifndef TUYA_DEVICE_H 2 | #define TUYA_DEVICE_H 3 | 4 | #include 5 | #include 6 | #include "WDevice.h" 7 | 8 | #define HEARTBEAT_INTERVAL 10000 9 | #define MINIMUM_INTERVAL 2000 10 | #define QUERY_INTERVAL 2000 11 | #define CMD_RESP_TIMEOUT 1500 12 | 13 | const unsigned char COMMAND_START[] = {0x55, 0xAA}; 14 | 15 | enum WTuyaDeviceState { 16 | STATE_INIT, 17 | STATE_PRODUCT_INFO_WAIT, 18 | STATE_PRODUCT_INFO_DONE, 19 | STATE_WIFI_WORKING_MODE_WAIT, 20 | STATE_WIFI_WORKING_MODE_DONE, 21 | STATE_COMMAND_WAIT, 22 | STATE_COMMAND_DONE, 23 | STATE_IDLE, 24 | }; 25 | 26 | #define COMMAND_WRITE_Q_LENGTH 8 27 | 28 | class WTuyaDevice : public WDevice { 29 | public : 30 | WTuyaDevice(WNetwork* network, const char* id, const char* name, const char* type) 31 | : WDevice(network, id, name, type) { 32 | resetAll(); 33 | this->receivingDataFromMcu = false; 34 | lastHeartBeat = lastQueryStatus = 0; 35 | //notifyAllMcuCommands 36 | this->notifyAllMcuCommands = network->settings()->setBoolean("notifyAllMcuCommands", false); 37 | // JY 38 | gpioStatus = -1; 39 | gpioReset = -1; 40 | processingState = STATE_INIT; 41 | commandWriteQDepth = 0; 42 | m_iCommandRetry = 0; 43 | iResetState = 0; 44 | lastCommandSent = 0; 45 | usingCommandQueue = false; 46 | } 47 | 48 | virtual void queryProductInfo() { 49 | unsigned char queryStateCommand[] = { 0x55, 0xAA, 0x00, 0x01, 0x00, 0x00 }; 50 | commandCharsToSerial(6, queryStateCommand, false, STATE_PRODUCT_INFO_WAIT); 51 | } 52 | 53 | virtual void queryWorkingModeWiFi() { 54 | unsigned char queryStateCommand[] = { 0x55, 0xAA, 0x00, 0x02, 0x00, 0x00 }; 55 | commandCharsToSerial(6, queryStateCommand, false, STATE_WIFI_WORKING_MODE_WAIT); 56 | } 57 | 58 | virtual void queryDeviceState() { 59 | //55 AA 00 08 00 00 60 | unsigned char queryStateCommand[] = { 0x55, 0xAA, 0x00, 0x08, 0x00, 0x00 }; 61 | commandCharsToSerial(6, queryStateCommand); 62 | } 63 | 64 | virtual void cancelConfiguration() { 65 | if (gpioStatus != -1) { 66 | pinMode(gpioStatus, OUTPUT); 67 | digitalWrite(gpioStatus, LOW); 68 | } else { 69 | unsigned char cancelConfigCommand[] = { 0x55, 0xaa, 0x00, 0x03, 0x00, 0x01, 0x02 }; 70 | commandCharsToSerial(7, cancelConfigCommand); 71 | } 72 | delay(1000); 73 | } 74 | 75 | virtual void loop(unsigned long now) { 76 | while (Serial.available() > 0) { 77 | receiveIndex++; 78 | unsigned char inChar = Serial.read(); 79 | receivedCommand[receiveIndex] = inChar; 80 | if (receiveIndex < 2) { 81 | //Check command start 82 | if (COMMAND_START[receiveIndex] != receivedCommand[receiveIndex]) { 83 | resetAll(); 84 | } 85 | } else if (receiveIndex == 5) { 86 | //length information now available 87 | commandLength = receivedCommand[4] * 0x100 + receivedCommand[5]; 88 | } else if ((commandLength > -1) 89 | && (receiveIndex == (6 + commandLength))) { 90 | //verify checksum 91 | int expChecksum = 0; 92 | for (int i = 0; i < receiveIndex; i++) { 93 | expChecksum += receivedCommand[i]; 94 | } 95 | expChecksum = expChecksum % 0x100; 96 | if (expChecksum == receivedCommand[receiveIndex]) { 97 | processSerialCommand(); 98 | } 99 | resetAll(); 100 | } 101 | } 102 | // 103 | if (gpioReset != -1) { 104 | int iNewResetState = digitalRead(gpioReset); 105 | if (iResetState != iNewResetState) { 106 | network()->debug(F("Wifi Reset State change, new state = %d"), iNewResetState); 107 | iResetState = iNewResetState; 108 | if (!iResetState) { 109 | //tbi 110 | /*if (onConfigurationRequest) { 111 | network->debug("MCU has signalled Reset, new state = %d", iNewResetState); 112 | // set special property to force AP mode and reconfig 113 | // even if connection to configured SSID succeeds 114 | WProperty* propForce = network->getSettings()->setBoolean("forceConfig", true); 115 | propForce->setBoolean(true); 116 | network->getSettings()->save(); 117 | onConfigurationRequest(); 118 | }*/ 119 | } 120 | } 121 | } 122 | 123 | switch (processingState) { 124 | case STATE_INIT: { 125 | queryProductInfo(); 126 | break; 127 | } 128 | case STATE_PRODUCT_INFO_WAIT: { 129 | if ((now - lastCommandSent) > CMD_RESP_TIMEOUT) { 130 | setProcessingState(STATE_PRODUCT_INFO_DONE); 131 | network()->error(F("Timeout: waiting for Product Info response")); 132 | } 133 | break; 134 | } 135 | case STATE_PRODUCT_INFO_DONE: { 136 | queryWorkingModeWiFi(); 137 | break; 138 | } 139 | case STATE_WIFI_WORKING_MODE_WAIT: { 140 | if ((now - lastCommandSent) > CMD_RESP_TIMEOUT) { 141 | setProcessingState(STATE_WIFI_WORKING_MODE_DONE); 142 | network()->error(F("Timeout: waiting for Wifi Working Mode response")); 143 | } 144 | break; 145 | } 146 | case STATE_WIFI_WORKING_MODE_DONE: { 147 | //Note: if wanting other MCU commands, change the state 148 | setProcessingState(STATE_IDLE); 149 | break; 150 | } 151 | case STATE_COMMAND_WAIT: { 152 | // check for response 153 | if ((now - lastCommandSent) > CMD_RESP_TIMEOUT) { 154 | // let send the latest command again.... hope for the best 155 | if (m_iCommandRetry < 1) { 156 | //network->debug("Timeout: waiting for thermostat command response, try again"); 157 | if (commandWriteQDepth != 0) { 158 | m_iCommandRetry++; 159 | byte* pBuffer = (byte*)commandWriteQueue[0]; 160 | commandCharsToSerial(*((unsigned int*)(pBuffer + 0)), pBuffer + sizeof(int), true, STATE_COMMAND_WAIT); 161 | } 162 | } else { 163 | setProcessingState(STATE_COMMAND_DONE); 164 | //network->debug("Timeout: waiting for thermostat command response, oh well"); 165 | } 166 | } 167 | break; 168 | } 169 | case STATE_COMMAND_DONE: { 170 | m_iCommandRetry = 0; 171 | // dequeue top commanf, shift and send next 172 | // if we have queued commands..... execute them 173 | if (commandWriteQDepth != 0) { 174 | //network->debug(F("loop: command complete.... dequeue top element in Q")); 175 | // dequeue first in queue 176 | byte * pBuffer = (byte *)commandWriteQueue[0]; 177 | // release memory 178 | free(pBuffer); 179 | // shift all down 180 | for (int i = 0; i < (commandWriteQDepth-1); i++) { 181 | commandWriteQueue[i] = commandWriteQueue[i + 1]; 182 | } 183 | // readjust index 184 | commandWriteQDepth--; 185 | // NEXT 186 | 187 | // if stuff on queue, sent it 188 | if (commandWriteQDepth != 0) { 189 | //network->debug(F("loop: command complete.... send next element in Q")); 190 | byte* pBuffer = (byte*)commandWriteQueue[0]; 191 | commandCharsToSerial(*((unsigned int*)(pBuffer + 0)), pBuffer + sizeof(int), true, STATE_COMMAND_WAIT); 192 | } 193 | } else { 194 | setProcessingState(STATE_IDLE); 195 | } 196 | break; 197 | } 198 | case STATE_IDLE: { 199 | //Heartbeat 200 | if ((HEARTBEAT_INTERVAL > 0) && 201 | ((lastHeartBeat == 0) || (now - lastHeartBeat > HEARTBEAT_INTERVAL))) { 202 | unsigned char heartBeatCommand[] = { 0x55, 0xAA, 0x00, 0x00, 0x00, 0x00 }; 203 | commandCharsToSerial(6, heartBeatCommand); 204 | //commandHexStrToSerial("55 aa 00 00 00 00"); 205 | lastHeartBeat = now; 206 | } 207 | //Query 208 | if (((now - lastHeartBeat) > MINIMUM_INTERVAL) 209 | && ((lastQueryStatus == 0) || (now - lastQueryStatus > QUERY_INTERVAL))) { 210 | queryDeviceState(); 211 | lastQueryStatus = now; 212 | } 213 | break; 214 | } 215 | } 216 | 217 | /*//Heartbeat 218 | if ((HEARTBEAT_INTERVAL > 0) 219 | && ((lastHeartBeat == 0) 220 | || (now - lastHeartBeat > HEARTBEAT_INTERVAL))) { 221 | unsigned char heartBeatCommand[] = 222 | { 0x55, 0xAA, 0x00, 0x00, 0x00, 0x00 }; 223 | commandCharsToSerial(6, heartBeatCommand); 224 | //commandHexStrToSerial("55 aa 00 00 00 00"); 225 | lastHeartBeat = now; 226 | } 227 | //Query 228 | if ((lastHeartBeat > 0) && 229 | (now - lastQueryStatus > MINIMUM_INTERVAL) && 230 | (now - lastQueryStatus > QUERY_INTERVAL)) { 231 | this->queryDeviceState(); 232 | lastQueryStatus = now; 233 | }*/ 234 | } 235 | 236 | protected : 237 | unsigned char receivedCommand[1024]; 238 | WProperty* notifyAllMcuCommands; 239 | bool receivingDataFromMcu; 240 | int commandLength; 241 | int receiveIndex; 242 | bool firstHeartBeatReceived; 243 | //2021-01-24 test for bht-002 244 | bool mcuRestarted; 245 | unsigned long lastHeartBeat; 246 | unsigned long lastQueryStatus; 247 | int iResetState; // JY 248 | unsigned long lastCommandSent; // JY 249 | WTuyaDeviceState processingState; // JY 250 | void* commandWriteQueue[COMMAND_WRITE_Q_LENGTH]; // JY 251 | int commandWriteQDepth; // JY 252 | int m_iCommandRetry; // JY 253 | int8_t gpioStatus; // JY 254 | int8_t gpioReset; // JY 255 | bool usingCommandQueue; 256 | 257 | void resetAll() { 258 | receiveIndex = -1; 259 | commandLength = -1; 260 | } 261 | 262 | int getIndex(unsigned char c) { 263 | const char HEX_DIGITS[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', 264 | '9', 'a', 'b', 'c', 'd', 'e', 'f' }; 265 | int result = -1; 266 | for (int i = 0; i < 16; i++) { 267 | if (c == HEX_DIGITS[i]) { 268 | result = i; 269 | break; 270 | } 271 | } 272 | return result; 273 | } 274 | 275 | unsigned char* getCommand() { 276 | return receivedCommand; 277 | } 278 | 279 | int getCommandLength() { 280 | return commandLength; 281 | } 282 | 283 | String getCommandAsString() { 284 | return getBufferAsString(commandLength, receivedCommand); 285 | } 286 | 287 | String getBufferAsString(int length, unsigned char* command) { 288 | String result = ""; 289 | bool fSpace = false; 290 | if (length > -1) { 291 | for (int i = 0; i < length; i++) { 292 | unsigned char ch = command[i]; 293 | if (fSpace) 294 | result = result + " "; 295 | result = result + (ch < 16 ? "0" : "") + String(ch, HEX);// charToHexStr(ch); 296 | fSpace = true; 297 | } 298 | } 299 | return result; 300 | } 301 | 302 | void commandHexStrToSerial(String command) { 303 | command.trim(); 304 | command.replace(" ", ""); 305 | command.toLowerCase(); 306 | int chkSum = 0; 307 | if ((command.length() > 1) && (command.length() % 2 == 0)) { 308 | for (int i = 0; i < (command.length() / 2); i++) { 309 | unsigned char chValue = getIndex(command.charAt(i * 2)) * 0x10 310 | + getIndex(command.charAt(i * 2 + 1)); 311 | chkSum += chValue; 312 | Serial.print((char) chValue); 313 | } 314 | unsigned char chValue = chkSum % 0x100; 315 | Serial.print((char) chValue); 316 | } 317 | } 318 | 319 | virtual bool processCommand(byte commandByte, byte length) { 320 | bool knownCommand = false; 321 | switch (commandByte) { 322 | case 0x00: { 323 | //heartbeat signal from MCU 324 | switch (receivedCommand[6]) { 325 | case 0x00 : //55 aa 01 00 00 01 00: first heartbeat 326 | case 0x01 : //55 aa 01 00 00 01 01: every heartbeat after 327 | knownCommand = true; 328 | break; 329 | } 330 | if ((knownCommand) && ((this->processingState == STATE_INIT) || (receivedCommand[6] == 0x00))) { 331 | //At first packet from MCU or first heart received by ESP, query queryProductInfo 332 | queryProductInfo(); 333 | this->processingState = STATE_PRODUCT_INFO_WAIT; 334 | } 335 | knownCommand = true; 336 | break; 337 | } 338 | case 0x01: { 339 | network()->debug(F("MCU: Product info received: %s"), this->getCommandAsString().c_str()); 340 | //queryWorkingModeWiFi(); 341 | if (processingState == STATE_PRODUCT_INFO_WAIT) { 342 | setProcessingState(STATE_PRODUCT_INFO_DONE); 343 | } 344 | break; 345 | } 346 | case 0x02: { 347 | network()->debug(F("MCU: Working mode of Wifi: %s"), this->getCommandAsString().c_str()); 348 | //Working mode of Wifi 349 | if (receivedCommand[5] == 0x00) { 350 | knownCommand = true; 351 | //nothing to do 352 | } else if (receivedCommand[5] == 0x02) { 353 | //ME102H resonds: 55 AA 03 02 00 02 0E 00 14; GPIO 0E - Wifi status; 00 - Reset button 354 | network()->setStatusLedPin(receivedCommand[6], true); 355 | this->gpioReset = receivedCommand[7]; 356 | knownCommand = true; 357 | } 358 | //after that, query the product info from MCU 359 | //..skipped 360 | //finally query the current state of device 361 | if (processingState == STATE_WIFI_WORKING_MODE_WAIT) { 362 | setProcessingState(STATE_WIFI_WORKING_MODE_DONE); 363 | } 364 | break; 365 | } 366 | case 0x03: { 367 | //ignore, MCU response to wifi state 368 | //55 aa 01 03 00 00 369 | network()->debug(F("WiFi state: %s"), this->getCommandAsString().c_str()); 370 | break; 371 | } 372 | case 0x04: { 373 | //Setup initialization request 374 | //received: 55 aa 01 04 00 00 375 | network()->debug(F("WiFi reset: %s"), this->getCommandAsString().c_str()); 376 | //send answer: 55 aa 00 03 00 01 00 377 | unsigned char configCommand[] = { 0x55, 0xAA, 0x00, 0x03, 0x00, 0x01, 0x00 }; 378 | commandCharsToSerial(7, configCommand); 379 | onConfigurationRequest(); 380 | break; 381 | } 382 | case 0x05: { 383 | //ignore, MCU response to wifi state 384 | //55 aa 01 03 00 00 385 | network()->debug(F("Reset WiFi selection: %s"), this->getCommandAsString().c_str()); 386 | break; 387 | } 388 | case 0x07: { 389 | knownCommand = processStatusCommand(receivedCommand[6], length); 390 | if (processingState == STATE_COMMAND_WAIT) { 391 | setProcessingState(STATE_COMMAND_DONE); 392 | } 393 | break; 394 | } 395 | } 396 | return knownCommand; 397 | } 398 | 399 | virtual bool processStatusCommand(byte statusCommandByte, byte length) { 400 | return false; 401 | } 402 | 403 | virtual void processSerialCommand() { 404 | if (commandLength > -1) { 405 | //unknown 406 | //55 aa 00 00 00 00 407 | this->receivingDataFromMcu = true; 408 | if (notifyAllMcuCommands->asBool()) { 409 | network()->debug(F("MCU: %s"), this->getCommandAsString().c_str()); 410 | } 411 | bool knownCommand = false; 412 | if (receivedCommand[3] == 0x07) { 413 | knownCommand = processStatusCommand(receivedCommand[6], receivedCommand[5]); 414 | } else { 415 | knownCommand = processCommand(receivedCommand[3], receivedCommand[5]); 416 | } 417 | if (!knownCommand) { 418 | notifyUnknownCommand(); 419 | } 420 | this->receivingDataFromMcu = false; 421 | } 422 | } 423 | 424 | bool isReceivingDataFromMcu() { 425 | return this->receivingDataFromMcu; 426 | } 427 | 428 | void notifyUnknownCommand() { 429 | network()->error(F("Unknown MCU command: %s"), this->getCommandAsString().c_str()); 430 | } 431 | 432 | void onConfigurationRequest() { 433 | //tbi 434 | } 435 | 436 | void commandCharsToSerial(unsigned int length, unsigned char* command, bool fNoQ = false, WTuyaDeviceState nextState = STATE_COMMAND_WAIT) { 437 | int chkSum = 0; 438 | bool fReady = ((!usingCommandQueue) || 439 | ((processingState != STATE_PRODUCT_INFO_WAIT) && 440 | (processingState != STATE_WIFI_WORKING_MODE_WAIT) && 441 | (processingState != STATE_COMMAND_WAIT && !fNoQ))); 442 | bool fAddQueue = false; 443 | if (fReady) { 444 | if (length > 2) { 445 | for (int i = 0; i < length; i++) { 446 | unsigned char chValue = command[i]; 447 | chkSum += chValue; 448 | Serial.print((char)chValue); 449 | } 450 | unsigned char chValue = chkSum % 0x100; 451 | Serial.print((char)chValue); 452 | } 453 | setProcessingState(nextState); 454 | lastCommandSent = millis(); 455 | //network->debug("commandCharsToSerial: %s", getBufferAsString(length, command).c_str()); 456 | } else { 457 | fAddQueue = true; 458 | //network->debug("commandCharsToSerial: Command write in progress.... will Q"); 459 | } 460 | if (usingCommandQueue) { 461 | // we have now sent something (if we can) 462 | // if we are in wait mode, then queue the command so that retry logic can be invoked 463 | if (processingState == STATE_COMMAND_WAIT) 464 | fAddQueue = true; 465 | if (fNoQ) 466 | fAddQueue = false; 467 | if (fAddQueue) { 468 | // queue the command for later 469 | //network->debug("commandCharsToSerial: Command Q'd"); 470 | if (commandWriteQDepth < COMMAND_WRITE_Q_LENGTH) { 471 | byte* pBuffer = (byte *)malloc(sizeof(int) + length); 472 | memcpy(pBuffer+0, &length, sizeof(int)); 473 | memcpy(pBuffer+sizeof(int), command, length); 474 | commandWriteQueue[commandWriteQDepth] = pBuffer; 475 | commandWriteQDepth++; 476 | } else { 477 | network()->error(F("commandCharsToSerial: no space left in command write queue")); 478 | } 479 | } 480 | } else { 481 | //Don't use queue 482 | if (nextState == STATE_COMMAND_WAIT) { 483 | setProcessingState(STATE_IDLE); 484 | } 485 | } 486 | } 487 | 488 | private : 489 | 490 | void setProcessingState(WTuyaDeviceState processingState) { 491 | if (this->processingState != processingState) { 492 | this->processingState = processingState; 493 | //network->debug(F("Thermostat processing state change. Old = %d, New = %d"), processingStateOld, processingState); 494 | } 495 | } 496 | 497 | }; 498 | 499 | #endif 500 | --------------------------------------------------------------------------------