├── .gitignore ├── ESP-WaterMonitor.ino ├── LICENSE ├── README.md ├── TDS检测专用芯片BA01-V1.0.pdf ├── libraries └── EasyMqtt │ ├── README.md │ ├── ToDo.md │ ├── examples │ ├── basic │ │ └── basic.ino │ ├── subscribe │ │ └── subscribe.ino │ └── temperature │ │ └── temperature.ino │ ├── html │ ├── generateC │ ├── linegraph.txt │ ├── template.html │ └── test.html │ ├── keywords.txt │ ├── library.json │ ├── library.properties │ ├── read.txt │ └── src │ ├── Config.cpp │ ├── Config.h │ ├── EasyMqtt.cpp │ ├── EasyMqtt.h │ ├── Entry.cpp │ ├── Entry.h │ ├── NTPClient.cpp │ ├── NTPClient.h │ ├── WebPortal.cpp │ ├── WebPortal.h │ └── html.h └── packages └── ESP-WaterMonitor.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Shared objects (inc. Windows DLLs) 26 | *.dll 27 | *.so 28 | *.so.* 29 | *.dylib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.i*86 36 | *.x86_64 37 | *.hex 38 | 39 | # Debug files 40 | *.dSYM/ 41 | *.su 42 | *.idb 43 | *.pdb 44 | 45 | # Kernel Module Compile Results 46 | *.mod* 47 | *.cmd 48 | .tmp_versions/ 49 | modules.order 50 | Module.symvers 51 | Mkfile.old 52 | dkms.conf 53 | -------------------------------------------------------------------------------- /ESP-WaterMonitor.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "SoftwareSerial.h" 8 | 9 | //WIFI设置 10 | #define wifi_ssid "ASUS" // WIFI名称 11 | #define wifi_pass "123456" // WIFI密码 12 | 13 | //MQTT设置 14 | #define mqtt_server "192.168.123.100" // MQTT服务器 15 | #define mqtt_port 1883 // MQTT端口 16 | #define mqtt_user "homeassistant" // MQTT用户名 17 | #define mqtt_pass "123456" // MQTT密码 18 | 19 | //OTA设置 20 | const char* host = "ESP-WaterMonitor"; 21 | const char* update_path = "/"; // OTA页面地址 22 | const char* update_username = "admin"; // OTA用户名 23 | const char* update_password = "123456"; // OTA密码 24 | 25 | SoftwareSerial mytds(5, 4); // RX, TX (D1, D2)接tds模块的TX和RX 26 | 27 | byte flowSensorPin1 = 12; // D6 接流量计1黄色针脚 28 | byte flowSensorPin2 = 14; // D5 接流量计2黄色针脚 29 | byte ledPin = 1; // 状态指示灯 30 | 31 | ESP8266WebServer httpServer(8266); 32 | ESP8266HTTPUpdateServer httpUpdater; 33 | 34 | RemoteDebug Debug; 35 | EasyMqtt mqtt; 36 | 37 | const int ProtocolHeaderByteTDS = 0xAA; // TDS协议头 38 | const int ProtocolHeaderByteTemp = 0xAB; // 温度协议头 39 | const int ProtocolHeaderByteError = 0xAC; // 异常消息协议头 40 | const int ProtocolHeaderLength = 1; // TDS协议头长度 41 | const int ProtocolBodyLength = 4; // TDS协议体长度 42 | const int ProtocolChecksumLength = 1; // 校验值长度 43 | 44 | bool appearToHaveValidMessage; 45 | byte command[]={0xA0,0x00,0x00,0x00,0x00,0xA0}; // 查询指令 46 | byte receivedMessage[ProtocolBodyLength]; 47 | 48 | int tds1 = 0; 49 | int tds2 = 0; 50 | float temp1 = 0.00; 51 | float temp2 = 0.00; 52 | 53 | // 流量计相关 54 | bool reset1 = false; 55 | bool reset2 = false; 56 | 57 | // YF-S402 频率:F=73*Q(Q 为流量 L/min) 误差:±10 流完一升水输出 4380 个脉冲 58 | float calibrationFactor1 = 4380; 59 | float calibrationFactor2 = 4380; 60 | 61 | float frequencyFactor = 73; 62 | 63 | float flowRate1 = 0; 64 | float flowRate2 = 0; 65 | 66 | float totalFlow1 = 0.00; 67 | float totalFlow2 = 0.00; 68 | 69 | volatile byte pulseCount1 = 0; 70 | volatile byte pulseCount2 = 0; 71 | 72 | unsigned long oldTime = 0; 73 | unsigned long previousTime = 0; 74 | 75 | void setup() { 76 | // Serial.begin(115200); 77 | mytds.begin(9600); 78 | 79 | Debug.begin("Telnet_HostName"); 80 | Debug.setResetCmdEnabled(true); 81 | 82 | mqtt.config().set("device.name", "ESP-WaterMonitor"); 83 | mqtt.wifi(wifi_ssid, wifi_pass); 84 | mqtt.mqtt(mqtt_server, mqtt_port, mqtt_user, mqtt_pass); 85 | 86 | //OTA 87 | MDNS.begin(host); 88 | httpUpdater.setup(&httpServer, update_path, update_username, update_password); 89 | httpServer.begin(); 90 | MDNS.addService("http", "tcp", 8266); 91 | Debug.printf("HTTPUpdateServer ready! Open http://%s.local%s in your browser and login with username '%s' and password '%s'\n", host, update_path, update_username, update_password); 92 | EEPROM.begin(512); 93 | 94 | if (!reset1) { 95 | EEPROM.get(100, totalFlow1); 96 | // 初始化EEPROM 97 | if (isnan(totalFlow1)) { 98 | totalFlow1 = 0.00; 99 | } 100 | } 101 | 102 | if (!reset2) { 103 | EEPROM.get(200, totalFlow2); 104 | if (isnan(totalFlow2)) { 105 | totalFlow2 = 0.00; 106 | } 107 | } 108 | 109 | pinMode(flowSensorPin1, INPUT); 110 | pinMode(flowSensorPin2, INPUT); 111 | 112 | pinMode(ledPin, OUTPUT); 113 | 114 | digitalWrite(flowSensorPin1, HIGH); 115 | digitalWrite(flowSensorPin2, HIGH); 116 | 117 | digitalWrite(ledPin, HIGH); 118 | 119 | attachInterrupt(flowSensorPin1, pulseCounter1, FALLING); 120 | attachInterrupt(flowSensorPin2, pulseCounter2, FALLING); 121 | 122 | mqtt.setInterval(3); 123 | mqtt["totalFlow2"] << [](){ return totalFlow2; }; 124 | mqtt["totalFlow1"] << [](){ return totalFlow1; }; 125 | mqtt["flowRate2"] << [](){ return flowRate2; }; 126 | mqtt["flowRate1"] << [](){ return flowRate1; }; 127 | mqtt["temp2"] << [](){ return temp2; }; 128 | mqtt["temp1"] << [](){ return temp1; }; 129 | mqtt["tds2"] << [](){ return tds2; }; 130 | mqtt["tds1"] << [](){ return tds1; }; 131 | } 132 | 133 | // 把 2 byte数据转换为int 134 | int hexToDec (int high, int low) { 135 | int z = 0; 136 | z = (high<<8)|low; 137 | return z; 138 | } 139 | 140 | void checkTDS() { 141 | 142 | if ((millis() - oldTime) > 3000) { 143 | 144 | byte ProtocolHeader = 0x00; 145 | // 发送查询指令 146 | mytds.write(command,6); 147 | int availableBytes = mytds.available(); 148 | while (availableBytes>0) { 149 | 150 | if (!appearToHaveValidMessage) { 151 | 152 | // 寻找协议头 153 | if (availableBytes >= ProtocolHeaderLength) { 154 | 155 | // 读取1byte数据 156 | byte firstByte = mytds.read(); 157 | 158 | if (firstByte == ProtocolHeaderByteTDS || 159 | firstByte == ProtocolHeaderByteTemp || 160 | firstByte == ProtocolHeaderByteError) { 161 | // 发现有效数据 162 | appearToHaveValidMessage = true; 163 | ProtocolHeader = firstByte; 164 | availableBytes = mytds.available(); 165 | } 166 | } 167 | else 168 | { 169 | Debug.print("not found protocol header!"); 170 | } 171 | } 172 | 173 | if (availableBytes >= (ProtocolBodyLength + ProtocolChecksumLength) && appearToHaveValidMessage) { 174 | 175 | // 读取协议体,添加协议头并计算校验和 176 | byte calculatedChecksum = 0x00; 177 | calculatedChecksum += ProtocolHeader; 178 | 179 | for (int i = 0; i < ProtocolBodyLength; i++) { 180 | receivedMessage[i] = mytds.read(); 181 | calculatedChecksum += receivedMessage[i]; 182 | } 183 | // 读取校验和 184 | byte receivedChecksum = mytds.read(); 185 | // 对比校验和 186 | if (receivedChecksum == calculatedChecksum) { 187 | // 解析TDS数据 188 | if (ProtocolHeader == ProtocolHeaderByteTDS) 189 | { 190 | tds1 = hexToDec(receivedMessage[0],receivedMessage[1]); 191 | tds2 = hexToDec(receivedMessage[2],receivedMessage[3]); 192 | Debug.print("tds1:"); 193 | Debug.println(tds1); 194 | Debug.print("tds2:"); 195 | Debug.println(tds2); 196 | } 197 | // 解析温度数据数据 198 | if (ProtocolHeader == ProtocolHeaderByteTemp) 199 | { 200 | temp1 = hexToDec(receivedMessage[0],receivedMessage[1])/100.00; 201 | temp2 = hexToDec(receivedMessage[2],receivedMessage[3])/100.00; 202 | Debug.print("temp1:"); 203 | Debug.println(temp1); 204 | Debug.print("temp2:"); 205 | Debug.println(temp2); 206 | } 207 | // 异常代码: 01:命令帧异常 208 | // 02:忙碌中 209 | // 03:校正失败 210 | // 04:检测温度超出范围 211 | if (ProtocolHeader == ProtocolHeaderByteError) 212 | { 213 | Debug.print("error:"); 214 | Debug.println(receivedMessage[0]); 215 | } 216 | } else { 217 | Debug.print("checksum error!receivedMessage:"); 218 | Debug.println(receivedMessage[0]); 219 | Debug.println(receivedMessage[1]); 220 | Debug.println(receivedMessage[2]); 221 | Debug.println(receivedMessage[3]); 222 | } 223 | appearToHaveValidMessage = false; 224 | } 225 | // 模块每次发送12byte数据,包含 6 byte TDS 数据和 6 byte 温度数据,需要循环两次分别校验 226 | availableBytes -= 6; 227 | } 228 | oldTime = millis(); 229 | } 230 | } 231 | 232 | void checkFlow() { 233 | 234 | unsigned long elapsedTime = millis() - previousTime; 235 | 236 | if (elapsedTime >= 1000) { 237 | // 计算前禁用中断 238 | detachInterrupt(flowSensorPin1); 239 | detachInterrupt(flowSensorPin2); 240 | 241 | // 计算流量 242 | float litresFlowed1 = pulseCount1 / calibrationFactor1; 243 | float litresFlowed2 = pulseCount2 / calibrationFactor2; 244 | 245 | // 计算脉冲频率 246 | float pulseFrequency1 = pulseCount1 / (elapsedTime / 1000.0); 247 | float pulseFrequency2 = pulseCount2 / (elapsedTime / 1000.0); 248 | 249 | // 计算流速(单位:L/min) 250 | flowRate1 = pulseFrequency1 / frequencyFactor; 251 | flowRate2 = pulseFrequency2 / frequencyFactor; 252 | 253 | // 把本次流量累加到总流量 254 | totalFlow1 += litresFlowed1; 255 | totalFlow2 += litresFlowed2; 256 | 257 | // 把总流量存入EEPROM 258 | EEPROM.put(100, totalFlow1); 259 | EEPROM.put(200, totalFlow2); 260 | EEPROM.commit(); 261 | 262 | Debug.print("flowRate1:"); 263 | Debug.println(flowRate1); 264 | 265 | Debug.print("flowRate2:"); 266 | Debug.println(flowRate2); 267 | 268 | Debug.print("totalFlow1:"); 269 | Debug.println(totalFlow1); 270 | 271 | Debug.print("totalFlow2:"); 272 | Debug.println(totalFlow2); 273 | 274 | // 重置脉冲计数器 275 | pulseCount1 = 0; 276 | pulseCount2 = 0; 277 | 278 | previousTime = millis(); 279 | 280 | // 恢复中断 281 | attachInterrupt(flowSensorPin1, pulseCounter1, FALLING); 282 | attachInterrupt(flowSensorPin2, pulseCounter2, FALLING); 283 | } 284 | } 285 | 286 | void pulseCounter1() { 287 | pulseCount1++; 288 | } 289 | 290 | void pulseCounter2() { 291 | pulseCount2++; 292 | } 293 | 294 | void loop() { 295 | Debug.handle(); 296 | httpServer.handleClient(); 297 | checkTDS(); 298 | checkFlow(); 299 | mqtt.loop(); 300 | wdt_reset(); 301 | } 302 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP-WaterMonitor 2 | 基于esp8266的TDS、流量监测系统 3 | 4 | ![](http://img.killadm.com/18-11-19/92132411.jpg) 5 | 6 | 7 | 8 | ## 特性 9 | 10 | - 双路 TDS/温度监测 11 | - 双路流速/流量监测 12 | - WEB界面展示当前值/更新时间 13 | - MQTT/RESTful API支持 14 | - Arduino OTA/WEB OTA支持 15 | - 基于telnet的远程debug输出 16 | - 提供HomeAassistant配置范例 17 | 18 | 19 | 20 | ## 依赖 21 | 22 | - 此项目依赖EasyMqtt库 https://github.com/bloft/EasyMqtt 23 | 24 | - 在libraries/EasyMqtt目录下有一个汉化版本 25 | 26 | 27 | 28 | ## 硬件 29 | 30 | - **BA01 双通道TDS检测模块** x 1 31 | 32 | ![](http://img.killadm.com/18-11-18/41525627.jpg) 33 | 34 | | 检测参数 | 误差 | 工作电压 | 接口 | 检测通道 | 35 | | :------: | :------------: | :------: | :--: | :------: | 36 | | TDS/水温 | <2%F.S./±0.5°C | 3.3 V | UART | 双通道 | 37 | 38 | - **232三通** x 2 39 | 40 | ![](http://img.killadm.com/18-11-20/61480188.jpg) 41 | 42 | 43 | 44 | - **YF-S402 霍尔流量计** x 2 45 | 46 | ![](http://img.killadm.com/18-11-18/40809858.jpg) 47 | 48 | | 工作电压 | 允许耐压 | 流量范围 | 输出波形 | 接口 | 49 | | :------: | :-------: | :------: | :------: | :--: | 50 | | 5V | < 1.75Mpa | 0.3-5L | 方波 | 2分 | 51 | 52 | 频率:F=73*Q(Q 为流量 L/min) 流完一升水输出 4380 个脉冲 53 | 54 | - **ESP8266** x 1 55 | 56 | ![](http://img.killadm.com/18-11-19/64619394.jpg) 57 | 58 | 59 | 60 | ## 电路连接 61 | 62 | | ESP8266 | TDS模块 | 原水流量计 | 纯水流量计 | 63 | | :-----: | :-----: | :--------: | :--------: | 64 | | GND | GND | 黑线 | 黑线 | 65 | | 3V3 | VCC | | | 66 | | 5V | | 红线 | 红线 | 67 | | D1 | TX | | | 68 | | D2 | RX | | | 69 | | D5 | | | 黄线 | 70 | | D6 | | 黄线 | | 71 | 72 | 73 | 74 | ## 传感器连接 75 | 76 | ![](http://img.killadm.com/18-11-20/85375575.jpg) 77 | 78 | 79 | 80 | ## 接入HomeAssistant 81 | 82 | ![](http://img.killadm.com/18-11-19/34953802.jpg) 83 | 84 | - package用法https://bbs.hassbian.com/forum.php?mod=viewthread&tid=1114 85 | 86 | - 配置范例 [packages/ESP-WaterMonitor.yaml](https://raw.githubusercontent.com/killadm/ESP-WaterMonitor/master/packages/ESP-WaterMonitor.yaml) (注意:把范例中的2681212替换成你自己的Device ID!) 87 | -------------------------------------------------------------------------------- /TDS检测专用芯片BA01-V1.0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/killadm/ESP-WaterMonitor/2a4f5d4b96eaccf398f69dd094f52cf4665c6288/TDS检测专用芯片BA01-V1.0.pdf -------------------------------------------------------------------------------- /libraries/EasyMqtt/README.md: -------------------------------------------------------------------------------- 1 | # EasyMqtt 2 | Easy handling of Mqtt on esp8266 3 | 4 | * Easy wifi configuration using web interface 5 | * Easy mqtt configuration using web interface 6 | * Easy configuration of mqtt endpoints 7 | * Web based UI to see current values 8 | 9 | ## Examble usage EasyMqtt 10 | ```C++ 11 | #include 12 | 13 | EasyMqtt mqtt; 14 | 15 | void setup() { 16 | mqtt.config().set("foo", "My Foo"); 17 | 18 | mqtt["foo"] << [&](){ 19 | return String(mqtt.config().get("foo", "default")); 20 | }; 21 | } 22 | 23 | void loop() { 24 | mqtt.loop(); 25 | } 26 | 27 | ``` 28 | -------------------------------------------------------------------------------- /libraries/EasyMqtt/ToDo.md: -------------------------------------------------------------------------------- 1 | # ToDo: 2 | 3 | ## Improvements 4 | 5 | ### Add easyMqtt/$id/system/connected + will to indicate if device is online 6 | 7 | ### OTA support 8 | Add support for OTA using ArduinoOTA 9 | 10 | ## Future 11 | 12 | ### Support writing to a topic with a subscriber 13 | Support both << and >> on the same topic with out read out own published values 14 | 15 | ### Add support for float type (extend Entry) 16 | This will make it possible to generate graphs in UI and ease implementations 17 | 18 | ### Add publish configured endpoints, to support openhab2 auto configure 19 | something like 20 | easyMqtt/$id/system/config 21 | { 22 | "id"="1dfasfa", 23 | "ip"="192.168.1.79", 24 | "endpoints"=["/temparatur", "/humidity"] 25 | } 26 | 27 | Read / Write / String / Number / ... 28 | 29 | -------------------------------------------------------------------------------- /libraries/EasyMqtt/examples/basic/basic.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | EasyMqtt mqtt; 4 | 5 | void setup() { 6 | mqtt.wifi("ssid", "pass"); 7 | mqtt.mqtt("server", 1883, "user", "password"); 8 | 9 | mqtt["foo"] << []() { return String("bar"); }; 10 | } 11 | 12 | void loop() { 13 | mqtt.loop(); 14 | } 15 | -------------------------------------------------------------------------------- /libraries/EasyMqtt/examples/subscribe/subscribe.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | EasyMqtt mqtt; 4 | 5 | void setup() { 6 | Serial.begin(115200); 7 | Serial.println(); 8 | 9 | mqtt.wifi("ssid", "pass"); 10 | mqtt.mqtt("server", 1883, "user", "password"); 11 | 12 | mqtt["write"] >> [](String value) { 13 | Serial.print("Incomming: "); 14 | Serial.println(value); 15 | }; 16 | } 17 | 18 | void loop() { 19 | mqtt.loop(); 20 | } 21 | -------------------------------------------------------------------------------- /libraries/EasyMqtt/examples/temperature/temperature.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | EasyMqtt mqtt; 5 | DHT dht(D7, DHT22); 6 | 7 | void setup() { 8 | dht.begin(); 9 | mqtt.wifi("ssid", "pass"); 10 | mqtt.mqtt("server", 1883, "user", "password"); 11 | 12 | mqtt["temperature"] << [&]() { 13 | return dht.readTemperature(); 14 | }; 15 | 16 | mqtt["humidity"] << [&]() { 17 | return dht.readHumidity(); 18 | }; 19 | 20 | } 21 | 22 | void loop() { 23 | mqtt.loop(); 24 | } 25 | -------------------------------------------------------------------------------- /libraries/EasyMqtt/html/generateC: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | |U"; 15 | 16 | preg_match_all($pattern, $html, $matches, PREG_SET_ORDER); 17 | 18 | $split = preg_split($pattern, $html); 19 | 20 | $tmp = array(); 21 | foreach($matches as $key => $value) { 22 | if(!array_key_exists($value[1], $tmp)) { 23 | $tmp[$value[1]] = addslashes(trimContent($split[$key + 1])); 24 | } 25 | } 26 | 27 | $fp = fopen($output, "w"); 28 | fputs($fp, "#pragma once\n"); 29 | fputs($fp, "\n"); 30 | foreach($tmp as $header => $content) { 31 | $header = strtoupper($header); 32 | fputs ($fp, "const char HTML_${header}[] PROGMEM = \"$content\";\n"); 33 | } 34 | fclose ($fp); 35 | } 36 | 37 | function trimContent($str) { 38 | $arr = explode("\n",$str); 39 | $output = ""; 40 | foreach($arr as $line) { 41 | $output .= trim($line); 42 | } 43 | return $output; 44 | } 45 | -------------------------------------------------------------------------------- /libraries/EasyMqtt/html/linegraph.txt: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 29 | -------------------------------------------------------------------------------- /libraries/EasyMqtt/html/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {device_id} 9 | 10 | 11 | 12 | 13 | 35 | 62 |
63 | 89 |
90 |
91 |
92 |
93 |
94 | 95 | 96 | 97 |
98 |
99 |
100 | 101 | {name} 102 | 103 |
104 |
    105 | {input} 106 | {output} 107 |
108 | 109 |
110 |
111 | 112 | 113 |
114 |
115 |
{name}
116 |
    117 | 118 |
  • 119 |
    120 |
    121 | 122 | 123 | 126 | 127 |
    128 |
    129 |
  • 130 | 131 | 132 |
  • 133 | {value} 134 |
  • 135 | 136 |
137 | 138 |
139 |
140 | 141 | 142 |
143 |
144 |
audio/mute
145 |
    146 |
  • 147 | 148 | {value} 149 | 150 | 151 | {value} 152 | 153 |
  • 154 |
155 | 156 |
157 |
158 | 159 | 160 |
161 |
162 |
model
163 |
    164 |
  • 165 | W1110S 166 |
  • 167 |
168 | 169 |
170 |
171 | 172 | 173 |
174 |
175 |
model
176 |
    177 |
  • 178 |
    179 |
    180 | 181 | 182 | 185 | 186 |
    187 |
    188 |
  • 189 |
190 | 191 |
192 |
193 | 194 | 195 |
196 |
197 |
source
198 |
RGB
199 | 200 |
201 |
202 | 203 | 204 |
205 |
206 |
lamp/mode
207 |
LNOR
208 | 209 |
210 |
211 | 212 | 213 |
214 |
215 |
lamp/time
216 |
84
217 | 218 |
219 |
220 | 221 | 222 |
223 |
224 |
audio/volume
225 |
#FF0000
226 | 227 |
228 |
229 | 230 | 231 |
232 |
233 |
picture/aspect
234 |
AUTO
235 | 236 |
237 |
238 | 239 | 240 |
241 |
242 |
picture/sharpness
243 |
15
244 | 245 |
246 |
247 | 248 | 249 |
250 |
251 |
picture/brightness
252 |
50
253 | 254 |
255 |
256 | 257 | 258 |
259 |
260 |
picture/contrast
261 |
50
262 | 263 |
264 |
265 | 266 | 267 |
268 |
269 |
picture/mode
270 |
FOOTBALL
271 | 272 |
273 |
274 | 275 | 276 |
277 |
278 |
279 | 280 |
281 |
282 |
283 |
284 |
285 | 286 |
287 |

wifi


288 |
289 | 290 |
291 | 292 |
293 |
294 |
295 | 296 |
297 | 298 |
299 |
300 |
301 |

mqtt


302 |
303 | 304 |
305 | 306 |
307 |
308 |
309 | 310 |
311 | 312 |
313 |
314 |
315 | 316 |
317 | 318 |
319 |
320 |
321 | 322 |
323 | 324 |
325 |
326 | 327 |
328 |

General


329 | 330 | 331 |
332 | 333 |
334 | 335 |
336 |
337 | 338 | 339 |
340 |
341 |
342 | 343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 | 351 |
352 |
353 |
354 |
355 |
356 |

About

357 |

Device is using EasyMqtt version {version}

358 |
359 |
Device Id
{device_id}
360 |
Topic
{topic}
361 |
362 |
363 |

MQTT API:

364 |
    365 | 366 |
  • 367 | {path} 368 | Set 369 | Get 370 |
  • 371 |
  • 372 | EasyMqtt/123456/model 373 | Get 374 |
  • 375 | 376 |
377 |
378 |

Rest API:

379 |
    380 | 381 | 382 |
  • 383 | {path} 384 |
  • 385 | 386 |
  • 387 | /rest/model 388 | GET 389 |
  • 390 |
  • 391 | /rest/source 392 | GET 393 | POST 394 |
  • 395 |
  • 396 | /rest/lamp/mode 397 | GET 398 | POST 399 |
  • 400 | 401 |
402 |
403 |
404 |
405 |
406 | 407 | 408 |
409 |
410 | 411 | 412 | 413 | 414 | 415 | 416 | -------------------------------------------------------------------------------- /libraries/EasyMqtt/html/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Device Info - {device_id} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 62 |
63 |

Device Info - {device_id}

69 | 80 |
81 |
82 |
83 |
84 |
85 | 86 | 87 | 88 |
89 |
90 |
91 | 92 | {name} 93 |
94 |
    95 |
  • 96 |
  • Current:{value}
  • 97 |
  • Avg:{value}
  • 98 |
99 | 100 |
101 |
102 | 103 | 104 | 105 |
106 |
107 |
{name}
108 |
109 |
110 |
111 | 112 | 113 | 116 | 117 |
118 |
119 |
120 |
121 |
122 | 123 | 124 |
125 |
126 |
127 | 128 | /sensors/temperature 129 |
130 |
    131 |
  • 132 |
  • Current:21.4
  • 133 |
  • Avg:21.2
  • 134 |
135 | 136 |
137 |
138 | 139 | 140 |
141 |
142 |
143 | 144 |
145 |
146 |
147 |
148 |
149 | 150 |
151 |

wifi


152 |
153 | 154 |
155 | 156 |
157 |
158 |
159 | 160 |
161 | 162 |
163 |
164 |
165 |

mqtt


166 |
167 | 168 |
169 | 170 |
171 |
172 |
173 | 174 |
175 | 176 |
177 |
178 |
179 | 180 |
181 | 182 |
183 |
184 |
185 | 186 |
187 | 188 |
189 |
190 | 191 |
192 |

{title}


193 | 194 | 195 |
196 | 197 |
198 | 199 |
200 |
201 | 202 | 203 |
204 |
205 |
206 | 207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 | 215 |
216 |
217 |
218 |
219 |
220 |

About

221 |

Device is using EasyMqtt version 0.3

222 |
223 |
Device Id
{device_id}
224 |
Topic
{topic}
225 |
226 |
227 |

MQTT API:

228 |
    229 | 230 |
  • 231 | {path} 232 | Set 233 | Get 234 |
  • 235 |
  • 236 | EasyMqtt/123456/model 237 | Get 238 |
  • 239 | 240 |
241 |
242 |

Rest API:

243 |
    244 | 245 | 246 |
  • 247 | {path} 248 |
  • 249 | 250 |
  • 251 | /rest/model 252 | GET 253 |
  • 254 |
  • 255 | /rest/source 256 | GET 257 | POST 258 |
  • 259 |
  • 260 | /rest/lamp/mode 261 | GET 262 | POST 263 |
  • 264 | 265 |
266 |
267 |
268 |
269 |
270 | 271 |
272 |
273 | 274 | 275 | 276 | 277 | -------------------------------------------------------------------------------- /libraries/EasyMqtt/keywords.txt: -------------------------------------------------------------------------------- 1 | ####################################### 2 | # Syntax Coloring Map For PubSubClient 3 | ####################################### 4 | 5 | ####################################### 6 | # Datatypes (KEYWORD1) 7 | ####################################### 8 | 9 | EasyMqtt KEYWORD1 10 | MqttEntry KEYWORD1 11 | 12 | ####################################### 13 | # Methods and Functions (KEYWORD2) 14 | ####################################### 15 | 16 | wifi KEYWORD2 17 | mqtt KEYWORD2 18 | loop KEYWORD2 19 | debug KEYWORD2 20 | getInterval KEYWORD2 21 | setInterval KEYWORD2 22 | publish KEYWORD2 23 | get KEYWORD2 24 | 25 | ####################################### 26 | # Constants (LITERAL1) 27 | ####################################### 28 | -------------------------------------------------------------------------------- /libraries/EasyMqtt/library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "EasyMqtt", 3 | "keywords": "mqtt, m2m, iot, esp8266", 4 | "description": "Makes it easier to get up and running with mqtt on the esp8266", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/bloft/EasyMqtt.git" 8 | }, 9 | "version": "0.4", 10 | "exclude": "tests", 11 | "examples": "examples/*/*.ino", 12 | "frameworks": "arduino", 13 | "platforms": [ 14 | "espressif" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /libraries/EasyMqtt/library.properties: -------------------------------------------------------------------------------- 1 | name=EasyMqtt 2 | version=0.4 3 | author=Bjarne Loft 4 | maintainer=Bjarne Loft 5 | sentence=A wrapper library for using mqtt on the ESP8266 6 | paragraph=Makes it easyer to get up and running with Mqtt on the esp8266 7 | category=Communication 8 | url=https://github.com/bloft/EasyMqtt 9 | architectures=* 10 | -------------------------------------------------------------------------------- /libraries/EasyMqtt/read.txt: -------------------------------------------------------------------------------- 1 | C++ 2 | https://hackaday.com/2017/05/05/using-modern-c-techniques-with-arduino/ 3 | 4 | OTA 5 | https://randomnerdtutorials.com/esp8266-ota-updates-with-arduino-ide-over-the-air/ 6 | -------------------------------------------------------------------------------- /libraries/EasyMqtt/src/Config.cpp: -------------------------------------------------------------------------------- 1 | #include "Config.h" 2 | #include 3 | #include 4 | #include 5 | #include "FS.h" 6 | 7 | Config::Config() { 8 | SPIFFS.begin(); 9 | } 10 | 11 | void Config::load() { 12 | File f = SPIFFS.open("/config.cfg", "r"); 13 | if (f) { 14 | while(f.available()) { 15 | String line = f.readStringUntil('\n'); 16 | int pos = line.indexOf("="); 17 | String key = line.substring(0, pos); 18 | String value = line.substring(pos+1, line.length()-1); 19 | set(key.c_str(), value.c_str()); 20 | } 21 | f.close(); 22 | } 23 | } 24 | 25 | void Config::save() { 26 | File f = SPIFFS.open("/config.cfg", "w"); 27 | if (f) { 28 | each([&](char *key, char *value) { 29 | f.print(key); 30 | f.print("="); 31 | f.println(value); 32 | }); 33 | f.close(); 34 | } 35 | } 36 | 37 | void Config::reset() { 38 | SPIFFS.remove("/config.cfg"); 39 | ESP.restart(); 40 | } 41 | 42 | void Config::each(std::function f) { 43 | struct element * ele = elements; 44 | while(ele) { 45 | f(ele->key, ele->value); 46 | ele = ele->next; 47 | } 48 | } 49 | 50 | int Config::getInt(const char *key, int defaultValue) { 51 | char defaultStr[16]; 52 | sprintf(defaultStr, "%i", defaultValue); 53 | return atoi(get(key, defaultStr)); 54 | } 55 | 56 | double Config::getDouble(const char *key, double defaultValue) { 57 | char defaultStr[16]; 58 | sprintf(defaultStr, "%f", defaultValue); 59 | return atof(get(key, defaultStr)); 60 | } 61 | 62 | long Config::getLong(const char *key, long defaultValue) { 63 | char defaultStr[16]; 64 | sprintf(defaultStr, "%ld", defaultValue); 65 | return atol(get(key, defaultStr)); 66 | } 67 | 68 | bool Config::getBool(const char *key, bool defaultValue) { 69 | return strcmp(get(key, defaultValue ? "true" : "false"), "true") == 0; 70 | } 71 | 72 | char * Config::get(const char *key, const char *defaultValue) { 73 | struct element * ele = elements; 74 | while(ele) { 75 | if(strcmp(key, ele->key) == 0) { 76 | return ele->value; 77 | } 78 | ele = ele->next; 79 | } 80 | return set(key, defaultValue); 81 | } 82 | 83 | char * Config::set(const char *key, const char *value){ 84 | struct element * ele = elements; 85 | while(ele) { 86 | if(strcmp(key, ele->key) == 0) { 87 | if(ele->value) { 88 | free(ele->value); 89 | } 90 | ele->value = (char*)malloc(strlen(value)+1); 91 | strcpy(ele->value, value); 92 | return ele->value; 93 | } 94 | ele = ele->next; 95 | } 96 | ele = elements; 97 | elements = new element(); 98 | elements->key = (char*)malloc(strlen(key)+1); 99 | strcpy(elements->key, key); 100 | elements->value = (char*)malloc(strlen(value)+1); 101 | strcpy(elements->value, value); 102 | elements->next = ele; 103 | return elements->value; 104 | } 105 | -------------------------------------------------------------------------------- /libraries/EasyMqtt/src/Config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class Config { 6 | private: 7 | struct element { 8 | char * key; 9 | char * value; 10 | struct element * next; 11 | }; 12 | struct element * elements = nullptr; 13 | 14 | public: 15 | Config(); 16 | void load(); 17 | void save(); 18 | void reset(); 19 | 20 | void each(std::function f); 21 | 22 | int getInt(const char *key, int defaultValue); 23 | double getDouble(const char *key, double defaultValue); 24 | long getLong(const char *key, long defaultValue); 25 | bool getBool(const char *key, bool defaultValue); 26 | char * get(const char *key, const char *defaultValue); 27 | char * set(const char *key, const char *value); 28 | }; 29 | -------------------------------------------------------------------------------- /libraries/EasyMqtt/src/EasyMqtt.cpp: -------------------------------------------------------------------------------- 1 | #include "EasyMqtt.h" 2 | 3 | #include 4 | #include 5 | #include "Entry.h" 6 | #include "Config.h" 7 | #include "WebPortal.h" 8 | #include 9 | 10 | 11 | /** 12 | * Handle connections to mqtt 13 | */ 14 | void EasyMqtt::connectWiFi() { 15 | if(WiFi.status() != WL_CONNECTED) { 16 | debug("Connecting to wifi"); 17 | WiFi.mode(WIFI_STA); 18 | WiFi.hostname("ESP-WaterMonitor"); 19 | WiFi.begin(config().get("wifi.ssid", ""), config().get("wifi.password", "")); 20 | 21 | #ifdef DEBUG 22 | WiFi.printDiag(Serial); 23 | #endif 24 | 25 | int timer = 0; 26 | while ((WiFi.status() == WL_DISCONNECTED) && timer < 60) { 27 | debug("Wifi Status", WiFi.status()); 28 | delay(500); 29 | timer++; 30 | } 31 | if(timer < 60 && WiFi.status() == WL_CONNECTED) { 32 | debug("WiFi connected"); 33 | debug("IP address", WiFi.localIP().toString()); 34 | } else { 35 | debug("WiFi connection timeout - Setup AP"); 36 | WiFi.mode(WIFI_AP); 37 | WiFi.softAP("EasyMqtt", "123456"); 38 | debug("IP address", WiFi.softAPIP().toString()); 39 | } 40 | debug("devideId", deviceId); 41 | webPortal.setup(this, &config(), &ntp()); 42 | } 43 | } 44 | 45 | void EasyMqtt::disconnectWiFi() { 46 | if(WiFi.status() == WL_CONNECTED) { 47 | WiFi.softAPdisconnect(); 48 | WiFi.disconnect(); 49 | int timeout = 0; 50 | while(WiFi.status() != WL_DISCONNECTED && timeout < 200){ 51 | delay(10); 52 | timeout++; 53 | } 54 | } 55 | } 56 | 57 | void EasyMqtt::connectMqtt() { 58 | if (!mqttClient.connected() && mqttDelay < millis()) { 59 | debug("Connecting to MQTT"); 60 | mqttClient.setClient(wifiClient); 61 | mqttClient.setCallback([&](const char* topic, uint8_t* payload, unsigned int length) { 62 | each([=](Entry* entry){ 63 | entry->callback(topic, payload, length); 64 | }); 65 | }); 66 | 67 | mqttClient.setServer(config().get("mqtt.host", ""), config().getInt("mqtt.port", 1883)); 68 | 69 | if (mqttClient.connect(config().get("device.name", String(ESP.getChipId()).c_str()), config().get("mqtt.username", ""), config().get("mqtt.password", ""), get("$system")["online"].getTopic().c_str(), 1, 1, "OFF")) { 70 | debug("Connected to MQTT"); 71 | 72 | setPublishFunction([&](Entry* entry, String message){ 73 | if(mqttClient.connected()) { 74 | mqttClient.publish(entry->getTopic().c_str(), message.c_str(), true); 75 | } 76 | }); 77 | 78 | debug("Topic", getTopic()); 79 | 80 | each([&](Entry* entry){ 81 | if (entry->isOut()) { 82 | mqttClient.subscribe(entry->getTopic().c_str()); 83 | } 84 | }); 85 | mqttDelay = 0; 86 | } else { 87 | debug("Connection to MQTT failed, rc", mqttClient.state()); 88 | 89 | mqttDelay = millis() + 5000; 90 | } 91 | } 92 | } 93 | 94 | EasyMqtt::EasyMqtt() : Entry("easyMqtt") { 95 | #ifdef DEBUG 96 | Serial.begin(115200); 97 | #endif 98 | 99 | // Add config entry 100 | cfg = new Config(); 101 | cfg->load(); 102 | 103 | // Add time(NTP) 104 | ntpClient = new NTPClient(); 105 | 106 | deviceId = config().get("device.name", String(ESP.getChipId()).c_str()); 107 | 108 | char * password = config().get("password", ""); 109 | if(strlen(password) > 0) { 110 | ArduinoOTA.setPassword(password); 111 | } 112 | ArduinoOTA.setHostname("ESP-WaterMoniter"); 113 | ArduinoOTA.begin(); 114 | 115 | setInterval(60, 10); 116 | 117 | get("$system").setInterval(300); // every 5 min 118 | get("$system")["deviceId"] << [this]() { 119 | return deviceId; 120 | }; 121 | get("$system")["mem"]["heap"] << []() { 122 | return ESP.getFreeHeap(); 123 | }; 124 | get("$system")["uptime"] << []() { 125 | return millis() / 1000; 126 | }; 127 | get("$system")["wifi"]["rssi"] << []() { 128 | return WiFi.RSSI(); 129 | }; 130 | get("$system")["wifi"]["quality"] << []() { 131 | int quality = (WiFi.RSSI()+100)*1.6667; 132 | if(quality < 0) quality = 0; 133 | if(quality > 100) quality = 100; 134 | return quality; 135 | }; 136 | get("$system")["wifi"]["ssid"] << []() { 137 | return WiFi.SSID(); 138 | }; 139 | get("$system")["wifi"]["ip"] << []() { 140 | return WiFi.localIP().toString(); 141 | }; 142 | get("$system")["online"] << []() { 143 | return "ON"; 144 | }; 145 | 146 | get("$system")["time"].setInterval(900); // every 15 min 147 | get("$system")["time"] << [this]() { 148 | ntp().update(); 149 | return ntp().getTime(); 150 | }; 151 | 152 | get("$system")["restart"] >> [this](String value) { 153 | if(value == config().get("password", "")) { 154 | debug("Restart"); 155 | ESP.restart(); 156 | } 157 | }; 158 | 159 | get("$system")["reset"] >> [this](String value) { 160 | if(value == config().get("password", "")) { 161 | config().reset(); 162 | } 163 | }; 164 | } 165 | 166 | String EasyMqtt::getDeviceId() { 167 | return config().get("device.name", String(ESP.getChipId()).c_str()); 168 | } 169 | 170 | String EasyMqtt::getTopic() { 171 | return config().get("device.name", String(ESP.getChipId()).c_str()) + String("/") +String(ESP.getChipId()); 172 | } 173 | 174 | Config & EasyMqtt::config() { 175 | return *cfg; 176 | } 177 | 178 | NTPClient & EasyMqtt::ntp() { 179 | return *ntpClient; 180 | } 181 | 182 | /** 183 | Configure wifi 184 | Deprecated 185 | */ 186 | void EasyMqtt::wifi(const char* ssid, const char* password) { 187 | config().get("wifi.ssid", ssid); 188 | config().get("wifi.password", password); 189 | } 190 | 191 | /** 192 | Configure mqtt 193 | Deprecated 194 | */ 195 | void EasyMqtt::mqtt(const char* host, int port, const char* username, const char* password) { 196 | config().get("mqtt.host", host); 197 | config().get("mqtt.port", String(port).c_str()); 198 | config().get("mqtt.username", username); 199 | config().get("mqtt.password", password); 200 | } 201 | 202 | /** 203 | Handle the normal loop 204 | */ 205 | void EasyMqtt::loop() { 206 | connectWiFi(); 207 | ArduinoOTA.handle(); 208 | if(WiFi.status() == WL_CONNECTED) { 209 | connectMqtt(); 210 | if(mqttClient.connected()) { 211 | mqttClient.loop(); 212 | } 213 | webPortal.loop(); 214 | each([](Entry* entry){ 215 | entry->update(); 216 | }); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /libraries/EasyMqtt/src/EasyMqtt.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "Entry.h" 6 | #include "Config.h" 7 | #include "WebPortal.h" 8 | #include "NTPClient.h" 9 | 10 | class EasyMqtt : public Entry { 11 | private: 12 | WiFiClient wifiClient; 13 | PubSubClient mqttClient; 14 | WebPortal webPortal; 15 | 16 | Config* cfg; 17 | 18 | NTPClient* ntpClient; 19 | 20 | String deviceId = "deviceId"; 21 | long mqttDelay = 0; 22 | 23 | protected: 24 | /** 25 | Handle connections to wifi 26 | */ 27 | void connectWiFi(); 28 | void disconnectWiFi(); 29 | 30 | /** 31 | Handle connections to mqtt 32 | */ 33 | void connectMqtt(); 34 | 35 | public: 36 | EasyMqtt(); 37 | 38 | String getDeviceId(); 39 | 40 | virtual String getTopic(); 41 | 42 | Config & config(); 43 | 44 | NTPClient & ntp(); 45 | 46 | /** 47 | Configure wifi 48 | Deprecated 49 | */ 50 | void wifi(const char* ssid, const char* password); 51 | 52 | /** 53 | Configure mqtt 54 | Deprecated 55 | */ 56 | void mqtt(const char* host, int port, const char* username, const char* password); 57 | 58 | /** 59 | Handle the normal loop 60 | */ 61 | void loop(); 62 | }; 63 | -------------------------------------------------------------------------------- /libraries/EasyMqtt/src/Entry.cpp: -------------------------------------------------------------------------------- 1 | #include "Entry.h" 2 | #include 3 | 4 | std::function Entry::getPublishFunction() { 5 | if(publishFunction == NULL && parent) { 6 | return parent->getPublishFunction(); 7 | } 8 | return publishFunction; 9 | } 10 | 11 | Entry::Entry(const char* name) { 12 | Entry::name = (char*)malloc(strlen(name)+1); 13 | strcpy(Entry::name, name); 14 | } 15 | 16 | Entry *Entry::getOrCreate(const char* name) { 17 | Entry *child = children; 18 | while (child != NULL) { 19 | if (strcmp(child->name, name) == 0) { 20 | return child; 21 | } 22 | child = child->next; 23 | } 24 | return addChild(new Entry(name)); 25 | } 26 | 27 | Entry *Entry::getRoot() { 28 | if(parent) { 29 | return parent->getRoot(); 30 | } else { 31 | return this; 32 | } 33 | } 34 | 35 | Entry *Entry::getParent() { 36 | return parent; 37 | } 38 | 39 | Entry *Entry::setParent(Entry& parent) { 40 | Entry::parent = &parent; 41 | return this; 42 | } 43 | 44 | Entry* Entry::addChild(Entry* child) { 45 | child->parent = this; 46 | child->next = children; 47 | children = child; 48 | return child; 49 | } 50 | 51 | void Entry::setPublishFunction(std::function function) { 52 | publishFunction = function; 53 | } 54 | 55 | void Entry::debug(String key, bool value) { 56 | #ifdef DEBUG 57 | debug(key + " = " + (value ? "true" : "false")); 58 | #endif 59 | } 60 | 61 | void Entry::debug(String key, int value) { 62 | #ifdef DEBUG 63 | debug(key + " = " + value); 64 | #endif 65 | } 66 | 67 | void Entry::debug(String key, String value) { 68 | #ifdef DEBUG 69 | debug(key + " = " + value); 70 | #endif 71 | } 72 | 73 | void Entry::debug(String msg) { 74 | #ifdef DEBUG 75 | Serial.println(msg); 76 | #endif 77 | } 78 | 79 | void Entry::callback(const char* topic, uint8_t* payload, unsigned int length) { 80 | if (strcmp(getTopic().c_str(), topic) == 0) { 81 | String _payload = ""; 82 | for (int i = 0; i < length; i++) { 83 | _payload += (char)payload[i]; 84 | } 85 | if(!isIn() || (millis() - lastPublish) > 1000 || strcmp(lastValue, _payload.c_str()) != 0) { 86 | update(_payload); 87 | } 88 | } 89 | } 90 | 91 | void Entry::update() { 92 | if (isIn()) { 93 | unsigned long time = millis(); 94 | if (time >= (lastUpdate + (getInterval() * 1000)) || lastUpdate == 0) { 95 | force++; 96 | lastUpdate = time; 97 | String value = inFunction(); 98 | if (value != "") { 99 | if(setValue(value.c_str(), force > getForce())) { 100 | force = 0; 101 | } 102 | } 103 | } 104 | } 105 | } 106 | 107 | void Entry::update(String payload) { 108 | if (isOut()) { 109 | outFunction(payload); 110 | } 111 | } 112 | 113 | bool Entry::isIn() { 114 | return inFunction != NULL; 115 | } 116 | 117 | bool Entry::isOut() { 118 | return outFunction != NULL; 119 | } 120 | 121 | bool Entry::isRoot() { 122 | return parent == NULL; 123 | } 124 | 125 | bool Entry::isInternal() { 126 | bool internal = name[0] == '$'; 127 | if (parent) { 128 | return internal || parent->isInternal(); 129 | } else { 130 | return internal; 131 | } 132 | } 133 | 134 | String Entry::getTopic() { 135 | if (parent) { 136 | return parent->getTopic() + "/" + name; 137 | } else { 138 | return String(name); 139 | } 140 | } 141 | 142 | int Entry::getInterval() { 143 | if (interval < 0) { 144 | return parent->getInterval(); 145 | } 146 | return interval; 147 | } 148 | 149 | void Entry::setInterval(int interval) { 150 | this->interval = interval; 151 | } 152 | 153 | void Entry::setInterval(int interval, int force) { 154 | this->interval = interval; 155 | forceUpdate = force; 156 | } 157 | 158 | void Entry::setPersist(bool persist) { 159 | this->persist = persist; 160 | if(persist) { 161 | File f = SPIFFS.open(getTopic(), "r"); 162 | if (f) { 163 | setValue(f.readStringUntil('\n').c_str(), true); 164 | f.close(); 165 | } 166 | } 167 | } 168 | 169 | int Entry::getForce() { 170 | if(forceUpdate < 0) { 171 | return parent->getForce(); 172 | } 173 | return forceUpdate; 174 | } 175 | 176 | char *Entry::getValue() { 177 | return lastValue; 178 | } 179 | 180 | bool Entry::setValue(const char *value, bool force) { 181 | if(force || !lastValue || strcmp(value, lastValue) != 0) { 182 | lastUpdate = millis(); 183 | if(lastValue) { 184 | free(lastValue); 185 | } 186 | lastValue = (char*)malloc(strlen(value)+1); 187 | strcpy(lastValue, value); 188 | publish(value); 189 | 190 | if(persist) { 191 | File f = SPIFFS.open(getTopic(), "w"); 192 | f.println(value); 193 | f.close(); 194 | } 195 | return true; 196 | } 197 | return false; 198 | } 199 | 200 | long Entry::getLastUpdate() { 201 | return lastUpdate; 202 | } 203 | 204 | void Entry::publish(const char *message) { 205 | auto function = getPublishFunction(); 206 | if(function) { 207 | lastPublish = millis(); 208 | function(this, String(message)); 209 | } 210 | } 211 | 212 | void Entry::each(std::function f) { 213 | f(this); 214 | Entry* child = children; 215 | while (child != NULL) { 216 | child->each(f); 217 | child = child->next; 218 | } 219 | } 220 | 221 | Entry & Entry::get(const char* name) { 222 | char *subName = strtok((char *)name, "/"); 223 | Entry *entry = this; 224 | while(subName != NULL) { 225 | entry = entry->getOrCreate(subName); 226 | subName = strtok(NULL, "/"); 227 | } 228 | return *entry; 229 | } 230 | 231 | Entry & Entry::operator[](int index) { 232 | int numOfDigits = log10(index) + 1; 233 | char* arr = (char*)calloc(numOfDigits, sizeof(char)); 234 | itoa(index, arr, 10); 235 | return *getOrCreate(arr); 236 | } 237 | 238 | Entry & Entry::operator[](const char* name) { 239 | return *getOrCreate(name); 240 | } 241 | 242 | void Entry::operator<<(std::function inFunction) { 243 | Entry::inFunction = inFunction; 244 | } 245 | 246 | void Entry::operator<<(std::function inFunction) { 247 | Entry::inFunction = [&, inFunction]() { 248 | return String(inFunction()); 249 | }; 250 | } 251 | 252 | void Entry::operator<<(std::function inFunction) { 253 | Entry::inFunction = [&, inFunction]() { 254 | float value = inFunction(); 255 | if(isnan(value)) { 256 | return String(""); 257 | } else { 258 | return String(value); 259 | } 260 | }; 261 | } 262 | 263 | void Entry::operator>>(std::function outFunction) { 264 | Entry::outFunction = outFunction; 265 | } 266 | -------------------------------------------------------------------------------- /libraries/EasyMqtt/src/Entry.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | 7 | class Entry { 8 | private: 9 | std::function outFunction = NULL; 10 | std::function inFunction = NULL; 11 | std::function publishFunction = NULL; 12 | 13 | char* name; 14 | int force = 0; 15 | int interval = -1; 16 | int forceUpdate = -1; 17 | unsigned long lastUpdate = 0; 18 | unsigned long lastPublish = 0; 19 | char* lastValue = NULL; 20 | bool persist = false; 21 | 22 | Entry* parent = NULL; 23 | Entry* next = NULL; 24 | Entry* children = NULL; 25 | 26 | std::function getPublishFunction(); 27 | 28 | protected: 29 | Entry(const char* name); 30 | Entry *getOrCreate(const char* name); 31 | Entry *getRoot(); 32 | Entry *getParent(); 33 | Entry *setParent(Entry& parent); 34 | Entry* addChild(Entry* child); 35 | void setPublishFunction(std::function function); 36 | 37 | public: 38 | void debug(String key, bool value); 39 | void debug(String key, int value); 40 | void debug(String key, String value); 41 | void debug(String msg); 42 | 43 | void callback(const char* topic, uint8_t* payload, unsigned int length); 44 | 45 | /** 46 | * Request a updated value if needed 47 | */ 48 | void update(); 49 | void update(String payload); 50 | 51 | bool isIn(); 52 | bool isOut(); 53 | bool isRoot(); 54 | bool isInternal(); 55 | 56 | virtual String getTopic(); 57 | 58 | int getInterval(); 59 | void setInterval(int interval); 60 | void setInterval(int interval, int force); 61 | 62 | void setPersist(bool persist); 63 | 64 | int getForce(); 65 | 66 | /** 67 | * Get last value 68 | */ 69 | char *getValue(); 70 | bool setValue(const char *value, bool force = false); 71 | 72 | /** 73 | * 74 | */ 75 | long getLastUpdate(); 76 | 77 | /** 78 | * Publish value to mqtt 79 | */ 80 | void publish(const char *message); 81 | 82 | /** 83 | * Iterate over each child, including sub children 84 | */ 85 | void each(std::function f); 86 | 87 | /** 88 | * Create or get the sub topic with the name {name} 89 | */ 90 | Entry & get(const char* name); 91 | 92 | /** 93 | * Create or get the sub topic with the name {name} 94 | */ 95 | Entry & operator[](int index); 96 | 97 | /** 98 | * Create or get the sub topic with the name {name} 99 | */ 100 | Entry & operator[](const char* name); 101 | 102 | /** 103 | * Read data from function and send it to mqtt 104 | */ 105 | void operator<<(std::function inFunction); 106 | void operator<<(std::function inFunction); 107 | void operator<<(std::function inFunction); 108 | 109 | /** 110 | * Handle data comming from mqtt 111 | */ 112 | void operator>>(std::function outFunction); 113 | }; 114 | -------------------------------------------------------------------------------- /libraries/EasyMqtt/src/NTPClient.cpp: -------------------------------------------------------------------------------- 1 | #include "NTPClient.h" 2 | #include 3 | 4 | // https://playground.arduino.cc/Code/NTPclient 5 | 6 | NTPClient::NTPClient() { 7 | WiFiUDP ntp(); 8 | this->udp.begin(123); 9 | } 10 | 11 | void NTPClient::update() { 12 | this->sendNTPPacket(); 13 | byte timeout = 0; 14 | int cb = 0; 15 | do { 16 | delay ( 10 ); 17 | cb = this->udp.parsePacket(); 18 | if (timeout > 100) return; 19 | timeout++; 20 | } while (cb == 0); 21 | 22 | localMillisAtUpdate = millis() - (10 * ((timeout + 1) / 2)); 23 | this->udp.read(this->packetBuffer, NTP_PACKET_SIZE); 24 | 25 | unsigned long highWord = word(this->packetBuffer[40], this->packetBuffer[41]); 26 | unsigned long lowWord = word(this->packetBuffer[42], this->packetBuffer[43]); 27 | 28 | unsigned long secsSince1900 = highWord << 16 | lowWord; 29 | 30 | localEpoc = secsSince1900 - SEVENZYYEARS; 31 | } 32 | 33 | void NTPClient::sendNTPPacket() { 34 | memset(this->packetBuffer, 0, NTP_PACKET_SIZE); 35 | this->packetBuffer[0] = 0b11100011; 36 | this->packetBuffer[1] = 0; 37 | this->packetBuffer[2] = 6; 38 | this->packetBuffer[3] = 0xEC; 39 | this->packetBuffer[12] = 49; 40 | this->packetBuffer[13] = 0x4E; 41 | this->packetBuffer[14] = 49; 42 | this->packetBuffer[15] = 52; 43 | 44 | this->udp.beginPacket(this->ntpServerName, 123); 45 | this->udp.write(this->packetBuffer, NTP_PACKET_SIZE); 46 | this->udp.endPacket(); 47 | } 48 | 49 | long NTPClient::getTime() { 50 | return getTime(millis()); 51 | } 52 | 53 | long NTPClient::getTime(long millis) { 54 | return localEpoc + ((millis - localMillisAtUpdate) / 1000); 55 | } 56 | -------------------------------------------------------------------------------- /libraries/EasyMqtt/src/NTPClient.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #define SEVENZYYEARS 2208988800UL 7 | #define NTP_PACKET_SIZE 48 8 | #define NTP_DEFAULT_LOCAL_PORT 1337 9 | 10 | class NTPClient { 11 | private: 12 | WiFiUDP udp; 13 | long localEpoc = 0; 14 | long localMillisAtUpdate; 15 | const char* ntpServerName = "time1.aliyun.com"; 16 | const int httpServerPort = NTP_DEFAULT_LOCAL_PORT; 17 | byte packetBuffer[NTP_PACKET_SIZE]; 18 | 19 | void sendNTPPacket(); 20 | 21 | public: 22 | NTPClient(); 23 | void update(); 24 | long getTime(); 25 | long getTime(long millis); 26 | }; 27 | -------------------------------------------------------------------------------- /libraries/EasyMqtt/src/WebPortal.cpp: -------------------------------------------------------------------------------- 1 | #include "WebPortal.h" 2 | #include "html.h" 3 | 4 | String WebPortal::getName(Entry* entry) { 5 | return getName(mqtt, entry); 6 | } 7 | 8 | String WebPortal::getName(Entry* root, Entry* entry) { 9 | String path = entry->getTopic(); 10 | path.replace(root->getTopic(), ""); 11 | return path; 12 | } 13 | 14 | String WebPortal::getRestPath(Entry* entry) { 15 | return "/rest" + getName(entry); 16 | } 17 | 18 | WebPortal::WebPortal() { 19 | } 20 | 21 | void WebPortal::setup(Entry *mqttEntry, Config *config, NTPClient *ntpClient) { 22 | WebPortal::mqtt = mqttEntry; 23 | WebPortal::config = config; 24 | WebPortal::ntp = ntpClient; 25 | mqtt->debug("Setup Web Portal"); 26 | webServer.reset(new ESP8266WebServer(80)); 27 | webServer->on("/", std::bind(&WebPortal::handleRoot, this)); 28 | webServer->on("/save", std::bind(&WebPortal::handleSaveConfig, this)); 29 | mqtt->each([&](Entry* entry) { 30 | webServer->on(getRestPath(entry).c_str(), std::bind(&WebPortal::handleRest, this)); 31 | }); 32 | webServer->onNotFound(std::bind(&WebPortal::handleNotFound, this)); 33 | webServer->begin(); 34 | } 35 | 36 | void WebPortal::handleRoot() { 37 | if(!auth()) return; 38 | 39 | String page = FPSTR(HTML_MAIN1); 40 | page.replace("{device_id}", "ESP-WaterMonitor"); 41 | webServer->sendContent(page); 42 | 43 | mqtt->each([&](Entry* entry) { 44 | if(!entry->isInternal() || webServer->arg("show").equals("all")) { 45 | sendSensor(entry); 46 | } 47 | }); 48 | 49 | webServer->sendContent_P(HTML_MAIN2); 50 | 51 | webServer->sendContent_P(HTML_CONFIG_HEADER); 52 | sendConfigs(); 53 | 54 | page = FPSTR(HTML_MAIN3); 55 | page.replace("{device_id}", String(mqtt->get("$system")["deviceId"].getValue())); 56 | page.replace("{topic}", mqtt->getTopic()); 57 | webServer->sendContent(page); 58 | 59 | mqtt->each([&](Entry* entry) { 60 | if(entry->isOut() || entry->isIn()) { 61 | sendMqttApi(entry); 62 | } 63 | }); 64 | 65 | webServer->sendContent_P(HTML_MAIN4); 66 | 67 | mqtt->each([&](Entry* entry) { 68 | if(entry->isOut() || entry->isIn()) { 69 | sendRestApi(entry); 70 | } 71 | }); 72 | 73 | webServer->sendContent_P(HTML_MAIN5); 74 | 75 | webServer->client().stop(); 76 | } 77 | 78 | void WebPortal::sendSensor(Entry* entry) { 79 | String page = FPSTR(HTML_SENSOR); 80 | page.replace("{color}", entry->isInternal() ? "warning" : "primary"); 81 | page.replace("{name}", getName(entry)); 82 | bool include = false; 83 | if(entry->isOut()) { 84 | include = true; 85 | page.replace("{input}", FPSTR(HTML_SENSOR_INPUT)); 86 | } else { 87 | page.replace("{input}", ""); 88 | } 89 | String value = entry->getValue(); 90 | if(value != NULL) { 91 | include = true; 92 | page.replace("{output}", FPSTR(HTML_SENSOR_OUTPUT)); 93 | if(getName(entry).endsWith("password")) { 94 | page.replace("{value}", "***"); 95 | } else { 96 | page.replace("{value}", value); 97 | } 98 | } else { 99 | page.replace("{output}", ""); 100 | } 101 | if(include) { 102 | page.replace("{path}", getRestPath(entry)); 103 | page.replace("{last_updated}", time(entry->getLastUpdate())); 104 | webServer->sendContent(page); 105 | } 106 | } 107 | 108 | void WebPortal::sendConfigs() { 109 | config->each([&](char *key, char *value) { 110 | String page = FPSTR(HTML_CONFIG_ENTRY); 111 | String name = String(key); 112 | page.replace("{key}", key); 113 | if(name.endsWith("password")) { 114 | page.replace("{type}", "password"); 115 | page.replace("{value}", ""); 116 | } else { 117 | page.replace("{type}", "text"); 118 | page.replace("{value}", String(value)); 119 | } 120 | webServer->sendContent(page); 121 | }); 122 | } 123 | 124 | void WebPortal::sendMqttApi(Entry* entry) { 125 | String page = FPSTR(HTML_API_DOC); 126 | String path = entry->getTopic(); 127 | if(entry->isOut()) path += "Set"; 128 | if(entry->isIn()) path += "Get"; 129 | page.replace("{path}", path); 130 | webServer->sendContent(page); 131 | } 132 | 133 | void WebPortal::sendRestApi(Entry* entry) { 134 | String page = FPSTR(HTML_API_DOC); 135 | String path = getRestPath(entry); 136 | if(entry->isOut()) path += "POST"; 137 | if(entry->isIn()) path += "GET"; 138 | page.replace("{path}", path); 139 | webServer->sendContent(page); 140 | } 141 | 142 | void WebPortal::handleRest() { 143 | if(!auth()) return; 144 | Entry* entry = &mqtt->get(webServer->uri().substring(6).c_str()); 145 | if(webServer->method() == HTTP_GET) { 146 | webServer->send(200, "application/json", "{\"value\":\"" + String(entry->getValue()) + "\",\"updated\":\"" + time(entry->getLastUpdate()) + "\"}"); 147 | } else if(webServer->method() == HTTP_POST && entry->isOut()) { 148 | entry->update(webServer->arg("plain")); 149 | webServer->send(200, "text/plain", webServer->uri() + " Update"); 150 | } else { 151 | webServer->send(404, "text/plain", "Unsupported"); 152 | } 153 | } 154 | 155 | void WebPortal::handleSaveConfig() { 156 | if(!auth()) return; 157 | config->each([&](char* key, char *value) { 158 | if(!(String(key)).endsWith("password") || strlen(value) > 0) { 159 | config->set(key, webServer->arg(key).c_str()); 160 | } 161 | }); 162 | config->save(); 163 | webServer->sendHeader("Location", String("/"), true); 164 | webServer->send(302, "text/plain", ""); 165 | ESP.restart(); 166 | } 167 | 168 | void WebPortal::handleNotFound() { 169 | if(!auth()) return; 170 | webServer->sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); 171 | webServer->sendHeader("Pragma", "no-cache"); 172 | webServer->sendHeader("Expires", "-1"); 173 | webServer->send(404, "text/plain", "Not Found"); 174 | } 175 | 176 | void WebPortal::loop() { 177 | webServer->handleClient(); 178 | } 179 | 180 | String WebPortal::time(long time) { 181 | double utcOffset = config->getDouble("time.offset", 8); 182 | 183 | long localTime = round(ntp->getTime(time) + 3600 * utcOffset); 184 | 185 | int seconds = localTime % 60; 186 | localTime /= 60; 187 | int minutes = localTime % 60; 188 | localTime /= 60; 189 | int hours = localTime % 24; 190 | 191 | char formated[9]; 192 | snprintf(formated, sizeof(formated), "%02d:%02d:%02d", hours, minutes, seconds); 193 | 194 | return String(formated); 195 | } 196 | 197 | bool WebPortal::auth() { 198 | char *pass = config->get("password", ""); 199 | if (strlen(pass) > 0 && !webServer->authenticate("admin", pass)) { 200 | webServer->requestAuthentication(); 201 | return false; 202 | } 203 | return true; 204 | } 205 | -------------------------------------------------------------------------------- /libraries/EasyMqtt/src/WebPortal.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "Entry.h" 5 | #include "Config.h" 6 | #include "NTPClient.h" 7 | 8 | // http://usemodj.com/2016/08/25/esp8266-arducam-5mp-ov5642-camera-wifi-video-streaming/ 9 | 10 | class WebPortal { 11 | private: 12 | std::unique_ptr webServer; 13 | Entry* mqtt; 14 | Config* config; 15 | NTPClient* ntp; 16 | 17 | String getName(Entry* entry); 18 | String getName(Entry* root, Entry* entry); 19 | 20 | String getRestPath(Entry* entry); 21 | 22 | String time(long time); 23 | bool auth(); 24 | 25 | void sendSensor(Entry* entry); 26 | void sendConfigs(); 27 | void sendMqttApi(Entry* entry); 28 | void sendRestApi(Entry* entry); 29 | 30 | public: 31 | WebPortal(); 32 | 33 | void setup(Entry *mqttEntry, Config *config, NTPClient *ntp); 34 | 35 | void handleRoot(); 36 | void handleRest(); 37 | void handleSaveConfig(); 38 | void handleNotFound(); 39 | 40 | void loop(); 41 | }; 42 | -------------------------------------------------------------------------------- /libraries/EasyMqtt/src/html.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | const char HTML_MAIN1[] PROGMEM = "{device_id}

"; 4 | const char HTML_SENSOR[] PROGMEM = "
{name}
    {input}{output}
上次更新: {last_updated}
"; 5 | const char HTML_SENSOR_INPUT[] PROGMEM = "
  • "; 6 | const char HTML_SENSNOR_INPUT[] PROGMEM = ""; 7 | const char HTML_SENSOR_OUTPUT[] PROGMEM = "
  • {value}
  • "; 8 | const char HTML_VALUE_ON[] PROGMEM = "{value}"; 9 | const char HTML_VALUE_OFF[] PROGMEM = "{value}"; 10 | const char HTML_MAIN2[] PROGMEM = "
    "; 11 | const char HTML_CONFIG_HEADER[] PROGMEM = "

    通用配置


    "; 12 | const char HTML_CONFIG_ENTRY[] PROGMEM = "
    "; 13 | const char HTML_MAIN3[] PROGMEM = "

    关于

    Device is using EasyMqtt version 0.4

    mod by killadm (luyeah521@gmail.com)

    固件更新

    Device Id
    {device_id}
    Topic
    {topic}

    MQTT API:

      "; 14 | const char HTML_MAIN4[] PROGMEM = "

    Rest API:

      "; 15 | const char HTML_API_DOC[] PROGMEM = "
    • {path}
    • "; 16 | const char HTML_MAIN5[] PROGMEM = "
    "; 17 | -------------------------------------------------------------------------------- /packages/ESP-WaterMonitor.yaml: -------------------------------------------------------------------------------- 1 | sensor: 2 | - platform: mqtt 3 | name: "tds1" 4 | state_topic: "ESP-WaterMonitor/2681212/tds1" 5 | unit_of_measurement: "PPM" 6 | qos: 1 7 | 8 | - platform: mqtt 9 | name: "tds2" 10 | state_topic: "ESP-WaterMonitor/2681212/tds2" 11 | unit_of_measurement: "PPM" 12 | qos: 1 13 | 14 | - platform: mqtt 15 | name: "temp1" 16 | state_topic: "ESP-WaterMonitor/2681212/temp1" 17 | unit_of_measurement: "°C" 18 | qos: 1 19 | 20 | - platform: mqtt 21 | name: "temp2" 22 | state_topic: "ESP-WaterMonitor/2681212/temp2" 23 | unit_of_measurement: "°C" 24 | qos: 1 25 | 26 | - platform: mqtt 27 | name: "totalFlow1" 28 | state_topic: "ESP-WaterMonitor/2681212/totalFlow1" 29 | unit_of_measurement: "L" 30 | qos: 1 31 | 32 | - platform: mqtt 33 | name: "totalFlow2" 34 | state_topic: "ESP-WaterMonitor/2681212/totalFlow2" 35 | unit_of_measurement: "L" 36 | qos: 1 37 | 38 | - platform: mqtt 39 | name: "flowRate1" 40 | state_topic: "ESP-WaterMonitor/2681212/flowRate1" 41 | unit_of_measurement: "L/min" 42 | qos: 1 43 | 44 | - platform: mqtt 45 | name: "flowRate2" 46 | state_topic: "ESP-WaterMonitor/2681212/flowRate2" 47 | unit_of_measurement: "L/min" 48 | qos: 1 49 | 50 | homeassistant: 51 | customize: 52 | sensor.tds1: 53 | friendly_name: 原水TDS 54 | icon: mdi:water 55 | sensor.tds2: 56 | friendly_name: 净水TDS 57 | icon: mdi:water 58 | sensor.temp1: 59 | friendly_name: 原水水温 60 | sensor.temp2: 61 | friendly_name: 净水水温 62 | sensor.totalFlow1: 63 | friendly_name: 原水流量 64 | icon: mdi:water-pump 65 | sensor.totalFlow2: 66 | friendly_name: 净水流量 67 | icon: mdi:water-pump 68 | sensor.flowRate1: 69 | friendly_name: 原水流速 70 | icon: mdi:help-network 71 | sensor.flowRate2: 72 | friendly_name: 净水流速 73 | icon: mdi:help-network 74 | 75 | group: 76 | water: 77 | name: 净水器 78 | view: no 79 | control: hidden 80 | entities: 81 | - sensor.tds1 82 | - sensor.tds2 83 | - sensor.temp1 84 | - sensor.temp2 85 | - sensor.flowRate1 86 | - sensor.flowRate2 87 | - sensor.totalFlow1 88 | - sensor.totalFlow2 89 | --------------------------------------------------------------------------------