├── LICENSE ├── README.md └── flora ├── .gitignore ├── config.h.example └── flora.ino /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Sven Henkel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ARCHIVED 2 | 3 | This repository has been archived due to lack of time to properly maintain the code. 4 | 5 | # flora 6 | 7 | This arduino sketch implements an ESP32 BLE client for XIaomi Mi Flora Plant sensors, pushing the meaasurements to an MQTT server. 8 | 9 | ## Technical requirements 10 | 11 | Hardware: 12 | - ESP32 13 | - Xiaomi Mi Plant Sensor (firmware revision >= 2.6.6) 14 | 15 | Software: 16 | - MQTT server 17 | 18 | ## Setup instructions 19 | 20 | 1) Copy config.h.example into config.h and update seetings according to your environment: 21 | - WLAN SSID and password 22 | - MQTT Server address 23 | - MAC address(es) of your Xiaomi Mi Plant sensor(s) 24 | 25 | 2) Open ino sketch in Arduino, compile & upload. 26 | 27 | ## Measuring interval 28 | 29 | The ESP32 will perform a single connection attempt to the Xiaomi Mi Plant sensor, read the sensor data & push it to the MQTT server. The ESP32 will enter deep sleep mode after all sensors have been read and sleep for X minutes before repeating the exercise... 30 | Battery level is read every Xth wakeup. 31 | Up to X attempst per sensor are performed when reading the data fails. 32 | 33 | ## Configuration 34 | 35 | - SLEEP_DURATION - how long should the device sleep between sensor reads? 36 | - EMERGENCY_HIBERNATE - how long after wakeup should the device forcefully go to sleep (e.g. when something gets stuck)? 37 | - BATTERY_INTERVAL - how ofter should the battery status be read? 38 | - RETRY - how ofter should a single device be tried on each run? 39 | 40 | ## Constraints 41 | 42 | Some "nice to have" features are not yet implemented or cannot be implemented: 43 | - OTA updates: I didn't manage to implement OTA update capabilities due to program size constraints: BLE and WLAN brings the sketch up to 90% of the size limit, so I decided to use the remaining 10% for something more useful than OTA... 44 | 45 | ## Sketch size issues 46 | 47 | The sketch does not fit into the default arduino parition size of around 1.3MB. You'll need to change your default parition table and increase maximum build size to at least 1.6MB. 48 | On Arduino IDE this can be achieved using "Tools -> Partition Scheme -> No OTA" 49 | 50 | ## Credits 51 | 52 | Many thanks go to the guys at https://github.com/open-homeautomation/miflora for figuring out the sensor protocol. 53 | -------------------------------------------------------------------------------- /flora/.gitignore: -------------------------------------------------------------------------------- 1 | config.h 2 | -------------------------------------------------------------------------------- /flora/config.h.example: -------------------------------------------------------------------------------- 1 | 2 | // array of different xiaomi flora MAC addresses 3 | char* FLORA_DEVICES[] = { 4 | "C4:7C:8D:67:11:11", 5 | "C4:7C:8D:67:22:22", 6 | "C4:7C:8D:67:33:33" 7 | }; 8 | 9 | // sleep between to runs in seconds 10 | #define SLEEP_DURATION 30 * 60 11 | // emergency hibernate countdown in seconds 12 | #define EMERGENCY_HIBERNATE 3 * 60 13 | // how often should the battery be read - in run count 14 | #define BATTERY_INTERVAL 6 15 | // how often should a device be retried in a run when something fails 16 | #define RETRY 3 17 | 18 | const char* WIFI_SSID = "ssid"; 19 | const char* WIFI_PASSWORD = "password"; 20 | 21 | // MQTT topic gets defined by "//" 22 | // where MAC_ADDRESS is one of the values from FLORA_DEVICES array 23 | // property is either temperature, moisture, conductivity, light or battery 24 | 25 | const char* MQTT_HOST = "10.10.10.1"; 26 | const int MQTT_PORT = 1883; 27 | const char* MQTT_CLIENTID = "miflora-client"; 28 | const char* MQTT_USERNAME = "username"; 29 | const char* MQTT_PASSWORD = "password"; 30 | const String MQTT_BASE_TOPIC = "flora"; 31 | const int MQTT_RETRY_WAIT = 5000; 32 | -------------------------------------------------------------------------------- /flora/flora.ino: -------------------------------------------------------------------------------- 1 | /** 2 | A BLE client for the Xiaomi Mi Plant Sensor, pushing measurements to an MQTT server. 3 | 4 | See https://github.com/nkolban/esp32-snippets/blob/master/Documentation/BLE%20C%2B%2B%20Guide.pdf 5 | on how bluetooth low energy and the library used are working. 6 | 7 | See https://github.com/ChrisScheffler/miflora/wiki/The-Basics for details on how the 8 | protocol is working. 9 | 10 | MIT License 11 | 12 | Copyright (c) 2017 Sven Henkel 13 | Multiple units reading by Grega Lebar 2018 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | */ 33 | 34 | #include "BLEDevice.h" 35 | #include 36 | #include 37 | 38 | #include "config.h" 39 | 40 | // boot count used to check if battery status should be read 41 | RTC_DATA_ATTR int bootCount = 0; 42 | 43 | // device count 44 | static int deviceCount = sizeof FLORA_DEVICES / sizeof FLORA_DEVICES[0]; 45 | 46 | // the remote service we wish to connect to 47 | static BLEUUID serviceUUID("00001204-0000-1000-8000-00805f9b34fb"); 48 | 49 | // the characteristic of the remote service we are interested in 50 | static BLEUUID uuid_version_battery("00001a02-0000-1000-8000-00805f9b34fb"); 51 | static BLEUUID uuid_sensor_data("00001a01-0000-1000-8000-00805f9b34fb"); 52 | static BLEUUID uuid_write_mode("00001a00-0000-1000-8000-00805f9b34fb"); 53 | 54 | TaskHandle_t hibernateTaskHandle = NULL; 55 | 56 | WiFiClient espClient; 57 | PubSubClient client(espClient); 58 | 59 | void connectWifi() { 60 | Serial.println("Connecting to WiFi..."); 61 | WiFi.begin(WIFI_SSID, WIFI_PASSWORD); 62 | 63 | while (WiFi.status() != WL_CONNECTED) { 64 | delay(500); 65 | Serial.print("."); 66 | } 67 | 68 | Serial.println(""); 69 | Serial.println("WiFi connected"); 70 | Serial.println(""); 71 | } 72 | 73 | void disconnectWifi() { 74 | WiFi.disconnect(true); 75 | Serial.println("WiFi disonnected"); 76 | } 77 | 78 | void connectMqtt() { 79 | Serial.println("Connecting to MQTT..."); 80 | client.setServer(MQTT_HOST, MQTT_PORT); 81 | 82 | while (!client.connected()) { 83 | if (!client.connect(MQTT_CLIENTID, MQTT_USERNAME, MQTT_PASSWORD)) { 84 | Serial.print("MQTT connection failed:"); 85 | Serial.print(client.state()); 86 | Serial.println("Retrying..."); 87 | delay(MQTT_RETRY_WAIT); 88 | } 89 | } 90 | 91 | Serial.println("MQTT connected"); 92 | Serial.println(""); 93 | } 94 | 95 | void disconnectMqtt() { 96 | client.disconnect(); 97 | Serial.println("MQTT disconnected"); 98 | } 99 | 100 | BLEClient* getFloraClient(BLEAddress floraAddress) { 101 | BLEClient* floraClient = BLEDevice::createClient(); 102 | 103 | if (!floraClient->connect(floraAddress)) { 104 | Serial.println("- Connection failed, skipping"); 105 | return nullptr; 106 | } 107 | 108 | Serial.println("- Connection successful"); 109 | return floraClient; 110 | } 111 | 112 | BLERemoteService* getFloraService(BLEClient* floraClient) { 113 | BLERemoteService* floraService = nullptr; 114 | 115 | try { 116 | floraService = floraClient->getService(serviceUUID); 117 | } 118 | catch (...) { 119 | // something went wrong 120 | } 121 | if (floraService == nullptr) { 122 | Serial.println("- Failed to find data service"); 123 | } 124 | else { 125 | Serial.println("- Found data service"); 126 | } 127 | 128 | return floraService; 129 | } 130 | 131 | bool forceFloraServiceDataMode(BLERemoteService* floraService) { 132 | BLERemoteCharacteristic* floraCharacteristic; 133 | 134 | // get device mode characteristic, needs to be changed to read data 135 | Serial.println("- Force device in data mode"); 136 | floraCharacteristic = nullptr; 137 | try { 138 | floraCharacteristic = floraService->getCharacteristic(uuid_write_mode); 139 | } 140 | catch (...) { 141 | // something went wrong 142 | } 143 | if (floraCharacteristic == nullptr) { 144 | Serial.println("-- Failed, skipping device"); 145 | return false; 146 | } 147 | 148 | // write the magic data 149 | uint8_t buf[2] = {0xA0, 0x1F}; 150 | floraCharacteristic->writeValue(buf, 2, true); 151 | 152 | delay(500); 153 | return true; 154 | } 155 | 156 | bool readFloraDataCharacteristic(BLERemoteService* floraService, String baseTopic) { 157 | BLERemoteCharacteristic* floraCharacteristic = nullptr; 158 | 159 | // get the main device data characteristic 160 | Serial.println("- Access characteristic from device"); 161 | try { 162 | floraCharacteristic = floraService->getCharacteristic(uuid_sensor_data); 163 | } 164 | catch (...) { 165 | // something went wrong 166 | } 167 | if (floraCharacteristic == nullptr) { 168 | Serial.println("-- Failed, skipping device"); 169 | return false; 170 | } 171 | 172 | // read characteristic value 173 | Serial.println("- Read value from characteristic"); 174 | std::string value; 175 | try{ 176 | value = floraCharacteristic->readValue(); 177 | } 178 | catch (...) { 179 | // something went wrong 180 | Serial.println("-- Failed, skipping device"); 181 | return false; 182 | } 183 | const char *val = value.c_str(); 184 | 185 | Serial.print("Hex: "); 186 | for (int i = 0; i < 16; i++) { 187 | Serial.print((int)val[i], HEX); 188 | Serial.print(" "); 189 | } 190 | Serial.println(" "); 191 | 192 | int16_t* temp_raw = (int16_t*)val; 193 | float temperature = (*temp_raw) / ((float)10.0); 194 | Serial.print("-- Temperature: "); 195 | Serial.println(temperature); 196 | 197 | int moisture = val[7]; 198 | Serial.print("-- Moisture: "); 199 | Serial.println(moisture); 200 | 201 | int light = val[3] + val[4] * 256; 202 | Serial.print("-- Light: "); 203 | Serial.println(light); 204 | 205 | int conductivity = val[8] + val[9] * 256; 206 | Serial.print("-- Conductivity: "); 207 | Serial.println(conductivity); 208 | 209 | if (temperature > 200) { 210 | Serial.println("-- Unreasonable values received, skip publish"); 211 | return false; 212 | } 213 | 214 | char buffer[64]; 215 | 216 | snprintf(buffer, 64, "%f", temperature); 217 | client.publish((baseTopic + "temperature").c_str(), buffer); 218 | snprintf(buffer, 64, "%d", moisture); 219 | client.publish((baseTopic + "moisture").c_str(), buffer); 220 | snprintf(buffer, 64, "%d", light); 221 | client.publish((baseTopic + "light").c_str(), buffer); 222 | snprintf(buffer, 64, "%d", conductivity); 223 | client.publish((baseTopic + "conductivity").c_str(), buffer); 224 | 225 | return true; 226 | } 227 | 228 | bool readFloraBatteryCharacteristic(BLERemoteService* floraService, String baseTopic) { 229 | BLERemoteCharacteristic* floraCharacteristic = nullptr; 230 | 231 | // get the device battery characteristic 232 | Serial.println("- Access battery characteristic from device"); 233 | try { 234 | floraCharacteristic = floraService->getCharacteristic(uuid_version_battery); 235 | } 236 | catch (...) { 237 | // something went wrong 238 | } 239 | if (floraCharacteristic == nullptr) { 240 | Serial.println("-- Failed, skipping battery level"); 241 | return false; 242 | } 243 | 244 | // read characteristic value 245 | Serial.println("- Read value from characteristic"); 246 | std::string value; 247 | try{ 248 | value = floraCharacteristic->readValue(); 249 | } 250 | catch (...) { 251 | // something went wrong 252 | Serial.println("-- Failed, skipping battery level"); 253 | return false; 254 | } 255 | const char *val2 = value.c_str(); 256 | int battery = val2[0]; 257 | 258 | char buffer[64]; 259 | 260 | Serial.print("-- Battery: "); 261 | Serial.println(battery); 262 | snprintf(buffer, 64, "%d", battery); 263 | client.publish((baseTopic + "battery").c_str(), buffer); 264 | 265 | return true; 266 | } 267 | 268 | bool processFloraService(BLERemoteService* floraService, char* deviceMacAddress, bool readBattery) { 269 | // set device in data mode 270 | if (!forceFloraServiceDataMode(floraService)) { 271 | return false; 272 | } 273 | 274 | String baseTopic = MQTT_BASE_TOPIC + "/" + deviceMacAddress + "/"; 275 | bool dataSuccess = readFloraDataCharacteristic(floraService, baseTopic); 276 | 277 | bool batterySuccess = true; 278 | if (readBattery) { 279 | batterySuccess = readFloraBatteryCharacteristic(floraService, baseTopic); 280 | } 281 | 282 | return dataSuccess && batterySuccess; 283 | } 284 | 285 | bool processFloraDevice(BLEAddress floraAddress, char* deviceMacAddress, bool getBattery, int tryCount) { 286 | Serial.print("Processing Flora device at "); 287 | Serial.print(floraAddress.toString().c_str()); 288 | Serial.print(" (try "); 289 | Serial.print(tryCount); 290 | Serial.println(")"); 291 | 292 | // connect to flora ble server 293 | BLEClient* floraClient = getFloraClient(floraAddress); 294 | if (floraClient == nullptr) { 295 | return false; 296 | } 297 | 298 | // connect data service 299 | BLERemoteService* floraService = getFloraService(floraClient); 300 | if (floraService == nullptr) { 301 | floraClient->disconnect(); 302 | return false; 303 | } 304 | 305 | // process devices data 306 | bool success = processFloraService(floraService, deviceMacAddress, getBattery); 307 | 308 | // disconnect from device 309 | floraClient->disconnect(); 310 | 311 | return success; 312 | } 313 | 314 | void hibernate() { 315 | esp_sleep_enable_timer_wakeup(SLEEP_DURATION * 1000000ll); 316 | Serial.println("Going to sleep now."); 317 | delay(100); 318 | esp_deep_sleep_start(); 319 | } 320 | 321 | void delayedHibernate(void *parameter) { 322 | delay(EMERGENCY_HIBERNATE*1000); // delay for five minutes 323 | Serial.println("Something got stuck, entering emergency hibernate..."); 324 | hibernate(); 325 | } 326 | 327 | void setup() { 328 | // all action is done when device is woken up 329 | Serial.begin(115200); 330 | delay(1000); 331 | 332 | // increase boot count 333 | bootCount++; 334 | 335 | // create a hibernate task in case something gets stuck 336 | xTaskCreate(delayedHibernate, "hibernate", 4096, NULL, 1, &hibernateTaskHandle); 337 | 338 | Serial.println("Initialize BLE client..."); 339 | BLEDevice::init(""); 340 | BLEDevice::setPower(ESP_PWR_LVL_P7); 341 | 342 | // connecting wifi and mqtt server 343 | connectWifi(); 344 | connectMqtt(); 345 | 346 | // check if battery status should be read - based on boot count 347 | bool readBattery = ((bootCount % BATTERY_INTERVAL) == 0); 348 | 349 | // process devices 350 | for (int i=0; i