├── images └── flower_care.jpg ├── .gitignore ├── package.json ├── README.md └── index.js /images/flower_care.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucavb/homebridge-mi-flora/HEAD/images/flower_care.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebridge-mi-flower-care", 3 | "version": "1.1.0", 4 | "description": "This is a homebridge plugin for the Xiaomi Mi Flora / Xiaomi Flower Care devices.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/honkmaster/homebridge-mi-flower-care.git" 12 | }, 13 | "keywords": [ 14 | "homebridge", 15 | "homebridge-plugin", 16 | "HomeKit", 17 | "xiaomi", 18 | "flower" 19 | ], 20 | "author": "Luca Becker, Tobias Tiemerding", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/honkmaster/homebridge-mi-flower-care/issues" 24 | }, 25 | "homepage": "https://github.com/honkmaster/homebridge-mi-flower-care#readme", 26 | "engines" : { 27 | "node": ">=0.12.0", 28 | "homebridge": ">=0.2.0" 29 | }, 30 | "dependencies" :{ 31 | "node-mi-flora" : "0.1.1", 32 | "fakegato-history": "^0.5.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # homebridge-mi-flower-care 2 | 3 | 4 | [![NPM version](https://badge.fury.io/js/homebridge-mi-flower-care.svg)](https://npmjs.org/package/homebridge-mi-flower-care) 5 | [![Dependency Status](https://david-dm.org/honkmaster/homebridge-mi-flower-care.svg)](https://david-dm.org/honkmaster/homebridge-mi-flower-care) 6 | ![License](https://img.shields.io/badge/license-ISC-lightgrey.svg) 7 | [![Downloads](https://img.shields.io/npm/dm/homebridge-mi-flower-care.svg)](https://npmjs.org/package/homebridge-mi-flower-care) 8 | 9 | This is a [Homebridge](https://github.com/nfarina/homebridge) plugin for exposing the Xiaomi Flower Care / Flower Mate / Flower Monitor / Mi Flora devices to HomeKit. Historical display of temperature / moisture data is available via HomeKit apps that support graphing (e.g. Elgato Eve). 10 | 11 | 12 | 13 | 14 | ## Installation 15 | 16 | ### Prerequisites 17 | 18 | #### System dependencies 19 | 20 | This plugin is using [node-mi-flora](https://github.com/demirhanaydin/node-mi-flora) / [Noble](https://github.com/noble/noble) in the background with the same package dependencies. You can install these dependencies using `apt-get`, if not already done. 21 | 22 | ``` 23 | (sudo) apt-get install bluetooth bluez libbluetooth-dev libudev-dev 24 | ``` 25 | 26 | For more details and descriptions for other platforms see the [Noble documentation](https://github.com/noble/noble#readme). 27 | 28 | #### MAC address 29 | 30 | Ensure you know the MAC address of your Xiaomi Flower Care. You can use `hcitool lescan` to scan for devices. The device will appear as `AA:BB:CC:DD:EE:FF Flower care` in the list. 31 | 32 | ### npm 33 | 34 | ``` 35 | (sudo) npm install -g --unsafe-perm homebridge-mi-flower-care 36 | ``` 37 | 38 | ## Example Configuration 39 | 40 | ``` 41 | { 42 | "accessory": "mi-flower-care", 43 | "name": "Golden cane palm", 44 | "deviceId": "AA:BB:CC:DD:EE:FF", 45 | "interval": 300 46 | } 47 | ``` 48 | 49 | | Key | Description | Optional / Required | 50 | |---------------|-------------|---------------------| 51 | | accessory     | Has to be `mi-flower-care`. | Required | 52 | | name | The name of this accessory. This will appear in your HomeKit app. | Required | 53 | | deviceId | The MAC address of your Xiaomi Flower Care device. | Required | 54 | | interval | Frequency of data refresh in seconds. Minimum: 1 (not recommended); Maximum: 600 (due to FakeGato). | Required | 55 | | humidityAlertLevel | Humidity level in percent used to trigger the humidity alert contact sensor. | Optional | 56 | | lowLightAlertLevel | Low light level in Lux used to trigger a low light alert contact sensor. | Optional | 57 | 58 | Typical values for `humidityAlertLevel`are 30 (%) and 2000 (Lux) for `lowLightAlertLevel`. 59 | 60 | ## Running 61 | 62 | Due to Bluetooth access, Homebridge **must** run with elevated privileges to work correctly i.e. sudo or root. 63 | 64 | ## Note 65 | 66 | The plugins is using Bluetooth LE (Low Energy) to connect to the Xiaomi Flower Care devices. Therefore, the first measured values are only visible after the first broadcast of the sensor. Up to this point the plugin is marked as inactive in HomeKit. In the worst case, the waiting time can last up to several minutes. Just have a little patience. 67 | 68 | ## Credits 69 | 70 | * lucavb - homebridge-mi-flora 71 | * demirhanaydin - node-mi-flora 72 | * simont77 - fakegato-history 73 | 74 | ## Legal 75 | 76 | Xiaomi and Mi are registered trademarks of BEIJING XIAOMI TECHNOLOGY CO., LTD. 77 | 78 | This project is in no way affiliated with, authorized, maintained, sponsored or endorsed by BEIJING XIAOMI TECHNOLOGY CO., LTD or any of its affiliates or subsidiaries. 79 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var MiFlora = require('node-mi-flora'); 2 | 3 | var Service, Characteristic, HomebridgeAPI, FakeGatoHistoryService; 4 | var inherits = require('util').inherits; 5 | var os = require("os"); 6 | var hostname = os.hostname(); 7 | 8 | module.exports = function (homebridge) { 9 | Service = homebridge.hap.Service; 10 | Characteristic = homebridge.hap.Characteristic; 11 | HomebridgeAPI = homebridge; 12 | FakeGatoHistoryService = require("fakegato-history")(homebridge); 13 | 14 | homebridge.registerAccessory("homebridge-mi-flower-care", "mi-flower-care", MiFlowerCarePlugin); 15 | }; 16 | 17 | 18 | function MiFlowerCarePlugin(log, config) { 19 | var that = this; 20 | this.log = log; 21 | this.name = config.name; 22 | this.displayName = this.name; 23 | this.deviceId = config.deviceId; 24 | this.interval = Math.min(Math.max(config.interval, 1), 600); 25 | 26 | this.config = config; 27 | 28 | this.storedData = {}; 29 | 30 | if (config.humidityAlertLevel != null) { 31 | this.humidityAlert = true; 32 | this.humidityAlertLevel = config.humidityAlertLevel; 33 | } else { 34 | this.humidityAlert = false; 35 | } 36 | 37 | if (config.lowLightAlertLevel != null) { 38 | this.lowLightAlert = true; 39 | this.lowLightAlertLevel = config.lowLightAlertLevel; 40 | } else { 41 | this.lowLightAlert = false; 42 | } 43 | 44 | // Setup services 45 | this.setUpServices(); 46 | 47 | // Setup MiFlora 48 | this.flora = new MiFlora(this.deviceId); 49 | 50 | this.flora.on('data', function (data) { 51 | if (data.deviceId = that.deviceId) { 52 | that.log("Lux: %s, Temperature: %s, Moisture: %s, Fertility: %s", data.lux, data.temperature, data.moisture, data.fertility); 53 | that.storedData.data = data; 54 | 55 | that.fakeGatoHistoryService.addEntry({ 56 | time: new Date().getTime() / 1000, 57 | temp: data.temperature, 58 | humidity: data.moisture 59 | }); 60 | 61 | that.lightService.getCharacteristic(Characteristic.CurrentAmbientLightLevel) 62 | .updateValue(data.lux); 63 | that.lightService.getCharacteristic(Characteristic.StatusActive) 64 | .updateValue(true); 65 | 66 | that.tempService.getCharacteristic(Characteristic.CurrentTemperature) 67 | .updateValue(data.temperature); 68 | that.tempService.getCharacteristic(Characteristic.StatusActive) 69 | .updateValue(true); 70 | 71 | that.humidityService.getCharacteristic(Characteristic.CurrentRelativeHumidity) 72 | .updateValue(data.moisture); 73 | that.humidityService.getCharacteristic(Characteristic.StatusActive) 74 | .updateValue(true); 75 | 76 | if (that.humidityAlert) { 77 | that.humidityAlertService.getCharacteristic(Characteristic.ContactSensorState) 78 | .updateValue(data.moisture <= that.humidityAlertLevel ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED); 79 | that.humidityAlertService.getCharacteristic(Characteristic.StatusActive) 80 | .updateValue(true); 81 | } 82 | 83 | if (that.lowLightAlert) { 84 | that.lowLightAlertService.getCharacteristic(Characteristic.ContactSensorState) 85 | .updateValue(data.lux <= that.lowLightAlertLevel ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED); 86 | that.lowLightAlertService.getCharacteristic(Characteristic.StatusActive) 87 | .updateValue(true); 88 | } 89 | } 90 | }); 91 | 92 | this.flora.on('firmware', function (data) { 93 | if (data.deviceId = that.deviceId) { 94 | that.log("Firmware: %s, Battery level: %s", data.firmwareVersion, data.batteryLevel); 95 | that.storedData.firmware = data; 96 | 97 | // Update values 98 | that.informationService.getCharacteristic(Characteristic.FirmwareRevision) 99 | .updateValue(data.firmwareVersion); 100 | 101 | that.batteryService.getCharacteristic(Characteristic.BatteryLevel) 102 | .updateValue(data.batteryLevel); 103 | that.batteryService.getCharacteristic(Characteristic.StatusLowBattery) 104 | .updateValue(data.batteryLevel <= 10 ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL); 105 | 106 | that.lightService.getCharacteristic(Characteristic.StatusLowBattery) 107 | .updateValue(data.batteryLevel <= 10 ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL); 108 | 109 | that.tempService.getCharacteristic(Characteristic.StatusLowBattery) 110 | .updateValue(data.batteryLevel <= 10 ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL); 111 | 112 | that.humidityService.getCharacteristic(Characteristic.StatusLowBattery) 113 | .updateValue(data.batteryLevel <= 10 ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL); 114 | 115 | if (that.humidityAlert) { 116 | that.humidityAlertService.getCharacteristic(Characteristic.StatusLowBattery) 117 | .updateValue(data.batteryLevel <= 10 ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL); 118 | } 119 | 120 | if (that.lowLightAlert) { 121 | that.lowLightAlertService.getCharacteristic(Characteristic.StatusLowBattery) 122 | .updateValue(data.batteryLevel <= 10 ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL); 123 | } 124 | } 125 | }); 126 | 127 | setInterval(function () { 128 | // Start scanning for updates, these will arrive in the corresponding callbacks 129 | that.flora.startScanning(); 130 | 131 | // Stop scanning 100ms before we start a new scan 132 | setTimeout(function () { 133 | that.flora.stopScanning(); 134 | }, (that.interval - 0.1) * 1000) 135 | }, this.interval * 1000); 136 | } 137 | 138 | 139 | MiFlowerCarePlugin.prototype.getFirmwareRevision = function (callback) { 140 | callback(null, this.storedData.firmware ? this.storedData.firmware.firmwareVersion : '0.0.0'); 141 | }; 142 | 143 | MiFlowerCarePlugin.prototype.getBatteryLevel = function (callback) { 144 | callback(null, this.storedData.firmware ? this.storedData.firmware.batteryLevel : 0); 145 | }; 146 | 147 | MiFlowerCarePlugin.prototype.getStatusActive = function (callback) { 148 | callback(null, this.storedData.data ? true : false); 149 | }; 150 | 151 | MiFlowerCarePlugin.prototype.getStatusLowBattery = function (callback) { 152 | if (this.storedData.firmware) { 153 | callback(null, this.storedData.firmware.batteryLevel <= 20 ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL); 154 | } else { 155 | callback(null, Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL); 156 | } 157 | }; 158 | 159 | MiFlowerCarePlugin.prototype.getStatusLowMoisture = function (callback) { 160 | if (this.storedData.data) { 161 | callback(null, this.storedData.data.moisture <= this.humidityAlertLevel ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED); 162 | } else { 163 | callback(null, Characteristic.ContactSensorState.CONTACT_DETECTED); 164 | } 165 | }; 166 | 167 | MiFlowerCarePlugin.prototype.getStatusLowLight = function (callback) { 168 | if (this.storedData.data) { 169 | callback(null, this.storedData.data.lux <= this.lowLightAlertLevel ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED); 170 | } else { 171 | callback(null, Characteristic.ContactSensorState.CONTACT_DETECTED); 172 | } 173 | }; 174 | 175 | MiFlowerCarePlugin.prototype.getCurrentAmbientLightLevel = function (callback) { 176 | callback(null, this.storedData.data ? this.storedData.data.lux : 0); 177 | }; 178 | 179 | MiFlowerCarePlugin.prototype.getCurrentTemperature = function (callback) { 180 | callback(null, this.storedData.data ? this.storedData.data.temperature : 0); 181 | }; 182 | 183 | MiFlowerCarePlugin.prototype.getCurrentMoisture = function (callback) { 184 | callback(null, this.storedData.data ? this.storedData.data.moisture : 0); 185 | }; 186 | 187 | MiFlowerCarePlugin.prototype.getCurrentFertility = function (callback) { 188 | callback(null, this.storedData.data ? this.storedData.data.fertility : 0); 189 | }; 190 | 191 | 192 | MiFlowerCarePlugin.prototype.setUpServices = function () { 193 | // info service 194 | this.informationService = new Service.AccessoryInformation(); 195 | 196 | this.informationService 197 | .setCharacteristic(Characteristic.Manufacturer, this.config.manufacturer || "Xiaomi") 198 | .setCharacteristic(Characteristic.Model, this.config.model || "Flower Care") 199 | .setCharacteristic(Characteristic.SerialNumber, this.config.serial || hostname + "-" + this.name); 200 | this.informationService.getCharacteristic(Characteristic.FirmwareRevision) 201 | .on('get', this.getFirmwareRevision.bind(this)); 202 | 203 | this.batteryService = new Service.BatteryService(this.name); 204 | this.batteryService.getCharacteristic(Characteristic.BatteryLevel) 205 | .on('get', this.getBatteryLevel.bind(this)); 206 | this.batteryService.setCharacteristic(Characteristic.ChargingState, Characteristic.ChargingState.NOT_CHARGEABLE); 207 | this.batteryService.getCharacteristic(Characteristic.StatusLowBattery) 208 | .on('get', this.getStatusLowBattery.bind(this)); 209 | 210 | this.lightService = new Service.LightSensor(this.name); 211 | this.lightService.getCharacteristic(Characteristic.CurrentAmbientLightLevel) 212 | .on('get', this.getCurrentAmbientLightLevel.bind(this)); 213 | this.lightService.getCharacteristic(Characteristic.StatusLowBattery) 214 | .on('get', this.getStatusLowBattery.bind(this)); 215 | this.lightService.getCharacteristic(Characteristic.StatusActive) 216 | .on('get', this.getStatusActive.bind(this)); 217 | 218 | this.tempService = new Service.TemperatureSensor(this.name); 219 | this.tempService.getCharacteristic(Characteristic.CurrentTemperature) 220 | .on('get', this.getCurrentTemperature.bind(this)); 221 | this.tempService.getCharacteristic(Characteristic.StatusLowBattery) 222 | .on('get', this.getStatusLowBattery.bind(this)); 223 | this.tempService.getCharacteristic(Characteristic.StatusActive) 224 | .on('get', this.getStatusActive.bind(this)); 225 | 226 | this.humidityService = new Service.HumiditySensor(this.name); 227 | this.humidityService.getCharacteristic(Characteristic.CurrentRelativeHumidity) 228 | .on('get', this.getCurrentMoisture.bind(this)); 229 | this.humidityService.getCharacteristic(Characteristic.StatusLowBattery) 230 | .on('get', this.getStatusLowBattery.bind(this)); 231 | this.humidityService.getCharacteristic(Characteristic.StatusActive) 232 | .on('get', this.getStatusActive.bind(this)); 233 | 234 | if (this.humidityAlert) { 235 | this.humidityAlertService = new Service.ContactSensor(this.name + " Low Humidity", "humidity"); 236 | this.humidityAlertService.getCharacteristic(Characteristic.ContactSensorState) 237 | .on('get', this.getStatusLowMoisture.bind(this)); 238 | this.humidityAlertService.getCharacteristic(Characteristic.StatusLowBattery) 239 | .on('get', this.getStatusLowBattery.bind(this)); 240 | this.humidityAlertService.getCharacteristic(Characteristic.StatusActive) 241 | .on('get', this.getStatusActive.bind(this)); 242 | } 243 | 244 | if (this.lowLightAlert) { 245 | this.lowLightAlertService = new Service.ContactSensor(this.name + " Low Light", "light"); 246 | this.lowLightAlertService.getCharacteristic(Characteristic.ContactSensorState) 247 | .on('get', this.getStatusLowLight.bind(this)); 248 | this.lowLightAlertService.getCharacteristic(Characteristic.StatusLowBattery) 249 | .on('get', this.getStatusLowBattery.bind(this)); 250 | this.lowLightAlertService.getCharacteristic(Characteristic.StatusActive) 251 | .on('get', this.getStatusActive.bind(this)); 252 | } 253 | 254 | this.fakeGatoHistoryService = new FakeGatoHistoryService("room", this, { storage: 'fs' }); 255 | 256 | /* 257 | own characteristics and services 258 | */ 259 | 260 | // moisture characteristic 261 | SoilMoisture = function () { 262 | Characteristic.call(this, 'Soil Moisture', 'C160D589-9510-4432-BAA6-5D9D77957138'); 263 | this.setProps({ 264 | format: Characteristic.Formats.UINT8, 265 | unit: Characteristic.Units.PERCENTAGE, 266 | maxValue: 100, 267 | minValue: 0, 268 | minStep: 0.1, 269 | perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY] 270 | }); 271 | this.value = this.getDefaultValue(); 272 | }; 273 | 274 | inherits(SoilMoisture, Characteristic); 275 | 276 | SoilMoisture.UUID = 'C160D589-9510-4432-BAA6-5D9D77957138'; 277 | 278 | 279 | // fertility characteristic 280 | SoilFertility = function () { 281 | Characteristic.call(this, 'Soil Fertility', '0029260E-B09C-4FD7-9E60-2C60F1250618'); 282 | this.setProps({ 283 | format: Characteristic.Formats.UINT8, 284 | maxValue: 10000, 285 | minValue: 0, 286 | minStep: 1, 287 | perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY] 288 | }); 289 | this.value = this.getDefaultValue(); 290 | }; 291 | 292 | inherits(SoilFertility, Characteristic); 293 | 294 | SoilFertility.UUID = '0029260E-B09C-4FD7-9E60-2C60F1250618'; 295 | 296 | 297 | // moisture sensor 298 | PlantSensor = function (displayName, subtype) { 299 | Service.call(this, displayName, '3C233958-B5C4-4218-A0CD-60B8B971AA0A', subtype); 300 | 301 | // Required Characteristics 302 | this.addCharacteristic(SoilMoisture); 303 | 304 | // Optional Characteristics 305 | this.addOptionalCharacteristic(Characteristic.CurrentTemperature); 306 | this.addOptionalCharacteristic(SoilFertility); 307 | }; 308 | 309 | inherits(PlantSensor, Service); 310 | 311 | PlantSensor.UUID = '3C233958-B5C4-4218-A0CD-60B8B971AA0A'; 312 | 313 | this.plantSensorService = new PlantSensor(this.name); 314 | this.plantSensorService.getCharacteristic(SoilMoisture) 315 | .on('get', this.getCurrentMoisture.bind(this)); 316 | this.plantSensorService.getCharacteristic(SoilFertility) 317 | .on('get', this.getCurrentFertility.bind(this)); 318 | }; 319 | 320 | 321 | MiFlowerCarePlugin.prototype.getServices = function () { 322 | var services = [this.informationService, this.batteryService, this.lightService, this.tempService, this.humidityService, this.plantSensorService, this.fakeGatoHistoryService]; 323 | if (this.humidityAlert) { 324 | services[services.length] = this.humidityAlertService; 325 | } 326 | if (this.lowLightAlert) { 327 | services[services.length] = this.lowLightAlertService; 328 | } 329 | return services; 330 | }; 331 | --------------------------------------------------------------------------------