├── package.json ├── sample-config.json ├── README.md └── index.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebridge-particle", 3 | "version": "0.0.4", 4 | "description": "Particle plugin for homebridge: https://github.com/nfarina/homebridge", 5 | "license": "ISC", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/krvarma/homebridge-particle" 9 | }, 10 | "keywords": [ 11 | "homebridge-plugin" 12 | ], 13 | "engines": { 14 | "node": ">=0.12.0", 15 | "homebridge": ">=0.2.0" 16 | }, 17 | "dependencies": { 18 | "request": "^2.65.0", 19 | "eventsource": "" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sample-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bridge": { 3 | "name": "Homebridge", 4 | "username": "CC:22:3D:E3:CE:39", 5 | "port": 51826, 6 | "pin": "031-45-154" 7 | }, 8 | 9 | "description": "This is an example configuration file with one Particle platform. It contians 3 accessories, two lights and a temperature sensor. You should replace the access token and device id placeholder with your access token and device id", 10 | 11 | "platforms": [ 12 | { 13 | "platform": "Particle", 14 | "name": "Particle Devices", 15 | "access_token": "<>", 16 | "cloudurl": "https://api.spark.io/v1/devices/", 17 | "devices": [ 18 | { 19 | "accessory": "ParticleLight", 20 | "name": "Bedroom Light", 21 | "deviceid": "<>", 22 | "type": "LIGHT", 23 | "function_name": "onoff", 24 | "args": "0={STATE}" 25 | }, 26 | { 27 | "accessory": "ParticleLight", 28 | "name": "Kitchen Light", 29 | "deviceid": "<>", 30 | "type": "LIGHT", 31 | "function_name": "onoff", 32 | "args": "1={STATE}" 33 | }, 34 | { 35 | "accessory": "ParticleTemperature", 36 | "name": "Kitchen Temperature", 37 | "deviceid": "<>", 38 | "type": "SENSOR", 39 | "sensorType": "temperature", 40 | "key": "temperature", 41 | "event_name": "tvalue" 42 | } 43 | ] 44 | } 45 | ] 46 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Particle device plugin for Homebridge** 2 | ------------------------------------- 3 | 4 | As you all know in the new version of [Homebridge](https://github.com/nfarina/homebridge), the plugin architecture is changed. In new Homebridge, plugins are published through NPM with name starts with *homebridge-*. Users can install the plugin using NPM. 5 | 6 | My previous example of Particle and Homebridge uses old plugin architecture. I have been thinking for a long time to upgrade my previous plugin to the new architecture. But because of many reasons it is delayed. Luckily last week I was able to complete and publish to NPM. 7 | 8 | You can install it using NPM like all other modules, you can install using: 9 | 10 | `npm install -g homebridge-particle`. 11 | 12 | In this version, I have made some changes from the older version. Mainly the plugin is now a Homebridge Platform. Also in this version accessories are defined in `config.json` file. The plugin loads the accessories from the `config.json` file and create accessory dynamically. A sample configuration file is like: 13 | 14 | { 15 | "bridge": { 16 | "name": "Homebridge", 17 | "username": "CC:22:3D:E3:CE:39", 18 | "port": 51826, 19 | "pin": "031-45-154" 20 | }, 21 | 22 | "description": "This is an example configuration file with one Particle platform and 3 accessories, two lights and a temperature sensor. You should replace the access token and device id placeholder with your access token and device id", 23 | 24 | "platforms": [ 25 | { 26 | "platform": "Particle", 27 | "name": "Particle Devices", 28 | "access_token": "<>", 29 | "cloudurl": "https://api.spark.io/v1/devices/", 30 | "devices": [ 31 | { 32 | "accessory": "BedroomLight", 33 | "name": "Bedroom Light", 34 | "deviceid": "<>", 35 | "type": "LIGHT", 36 | "function_name": "onoff", 37 | "args": "0={STATE}" 38 | }, 39 | { 40 | "accessory": "KitchenLight", 41 | "name": "Kitchen Light", 42 | "deviceid": "<>", 43 | "type": "LIGHT", 44 | "function_name": "onoff", 45 | "args": "1={STATE}" 46 | }, 47 | { 48 | "accessory": "KitchenTemperature", 49 | "name": "Kitchen Temperature", 50 | "deviceid": "<>", 51 | "type": "SENSOR", 52 | "sensorType": "temperature", 53 | "key": "temperature", 54 | "event_name": "HKSValues" 55 | }, 56 | { 57 | "accessory": "Motion Sensor", 58 | "name": "Motion", 59 | "deviceid": "<>", 60 | "type": "SENSOR", 61 | "sensorType": "motion", 62 | "key": "motion", 63 | "event_name": "HKSValues" 64 | }, 65 | { 66 | "accessory": "Contact Sensor", 67 | "name": "Contact", 68 | "deviceid": "<>", 69 | "type": "SENSOR", 70 | "sensorType": "contact", 71 | "key": "contact", 72 | "event_name": "HKSValues" 73 | } 74 | ] 75 | } 76 | ] 77 | } 78 | 79 | As you can see from the above example this `config.json` file defines 3 accessories. 2 Lights and one Temperature Sensor. The **access_token** defines the Particle Access Token and **cloudurl** defines the base Particle API url. If you are using the Particle Cloud, then the value of *cloudurl* should be https://api.spark.io/v1/devices/. If you are using local cloud, then replace with your sensor address. 80 | 81 | The `devices` array contains all the accessories. You can see the accessory object defines following string objects: 82 | 83 | - ***accessory*** - Accessory name, this is the name of the accessory. 84 | - ***name*** - Display name, this is the name to be displayed on the HomeKit app. 85 | - ***deviceid*** - Device ID of the Particle Device (Core, Photon or Electron). It is defined in accessory so that you can use different Particle Devices for different accessory. 86 | - ***type*** - Type of the accessoy. As of now, the plugin supports 2 type, LIGHT and SENSOR. Type LIGHT represents a light, such as bedroom light, kitchen light, living room light, etc... Type SENSOR represents sensor accessory such as Temperature sensor, Humidity sensor, Light sensor, etc... 87 | - ***sensorType*** - Optional Sensor Type, this string object is optional. This is only valid when the accessory type is SENSOR. As of now the plugin supports 3 types of sensors, Temperature Sensor, Humidity Sensor and Light Sensor. More sensor will be supports in future versions. 88 | - ***event_name*** - The name of the event to listen for sensor value update. This is only valid if the accessory type is SENSOR. If the accessory is a type of SENSOR, then the plugin listens for events published from Particle Device (using `Particle.publish`). The device firmware should publish the sensor values in the format `key=value`. The key identifies the sensor value. For a temperature sensor the key should be ***temperature***. For a humidity sensor the key should be ***humidity***. For light sensor it should be ***light***. 89 | - ***key*** - Name of the key, this is not used in this version of the plugin. This is included for future purpose. 90 | 91 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var request = require("request"); 2 | var eventSource = require('eventsource'); 3 | var Service, Characteristic; 4 | 5 | module.exports = function(homebridge) { 6 | Service = homebridge.hap.Service; 7 | Characteristic = homebridge.hap.Characteristic; 8 | 9 | homebridge.registerPlatform("homebridge-particle", "Particle", ParticlePlatform); 10 | } 11 | 12 | function ParticlePlatform(log, config){ 13 | this.log = log; 14 | this.accessToken = config["access_token"]; 15 | this.deviceId = config["deviceid"]; 16 | this.url = config["cloudurl"]; 17 | this.devices = config["devices"]; 18 | } 19 | 20 | ParticlePlatform.prototype = { 21 | accessories: function(callback){ 22 | var foundAccessories = []; 23 | 24 | var count = this.devices.length; 25 | 26 | for(index=0; index< count; ++index){ 27 | var accessory = new ParticleAccessory( 28 | this.log, 29 | this.url, 30 | this.accessToken, 31 | this.devices[index]); 32 | 33 | foundAccessories.push(accessory); 34 | } 35 | 36 | callback(foundAccessories); 37 | } 38 | }; 39 | 40 | function ParticleAccessory(log, url, access_token, device) { 41 | this.log = log; 42 | this.name = device["name"], 43 | this.args = device["args"]; 44 | this.deviceId = device["deviceid"]; 45 | this.type = device["type"]; 46 | this.functionName = device["function_name"]; 47 | this.eventName = device["event_name"]; 48 | this.sensorType = device["sensorType"]; 49 | this.key = device["key"]; 50 | this.accessToken = access_token; 51 | this.url = url; 52 | this.value = 20; 53 | 54 | console.log(this.name + " = " + this.sensorType); 55 | 56 | this.services = []; 57 | 58 | this.informationService = new Service.AccessoryInformation(); 59 | 60 | this.informationService 61 | .setCharacteristic(Characteristic.Manufacturer, "Particle") 62 | .setCharacteristic(Characteristic.Model, "Photon") 63 | .setCharacteristic(Characteristic.SerialNumber, "AA098BB09"); 64 | 65 | this.services.push(this.informationService); 66 | 67 | if(this.type === "LIGHT"){ 68 | this.lightService = new Service.Lightbulb(this.name); 69 | 70 | this.lightService 71 | .getCharacteristic(Characteristic.On) 72 | .on('set', this.setState.bind(this)); 73 | 74 | this.services.push(this.lightService); 75 | } 76 | else if(this.type === "SWITCH"){ 77 | this.switchService = new Service.Switch(this.name); 78 | 79 | this.switchService 80 | .getCharacteristic(Characteristic.On) 81 | .on('set', this.setState.bind(this)); 82 | 83 | this.services.push(this.switchService); 84 | } 85 | else if(this.type === "SENSOR"){ 86 | var service; 87 | 88 | console.log("Sensor Type: " + this.sensorType.toLowerCase()); 89 | 90 | if(this.sensorType.toLowerCase() === "temperature"){ 91 | console.log("Temperature Sensor"); 92 | 93 | service = new Service.TemperatureSensor(this.name); 94 | 95 | service 96 | .getCharacteristic(Characteristic.CurrentTemperature) 97 | .on('get', this.getDefaultValue.bind(this)); 98 | } 99 | else if(this.sensorType.toLowerCase() === "humidity"){ 100 | console.log("Humidity Sensor"); 101 | 102 | service = new Service.HumiditySensor(this.name); 103 | 104 | service 105 | .getCharacteristic(Characteristic.CurrentRelativeHumidity) 106 | .on('get', this.getDefaultValue.bind(this)); 107 | } 108 | else if(this.sensorType.toLowerCase() === "light"){ 109 | console.log("Light Sensor"); 110 | 111 | service = new Service.LightSensor(this.name); 112 | 113 | service 114 | .getCharacteristic(Characteristic.CurrentAmbientLightLevel) 115 | .on('get', this.getDefaultValue.bind(this)); 116 | } 117 | else if(this.sensorType.toLowerCase() === "motion"){ 118 | console.log("Motion Sensor"); 119 | 120 | service = new Service.MotionSensor(this.name); 121 | 122 | service 123 | .getCharacteristic(Characteristic.MotionDetected) 124 | .on('get', this.getDefaultValue.bind(this)); 125 | } 126 | else if(this.sensorType.toLowerCase() === "contact"){ 127 | console.log("Contact Sensor"); 128 | 129 | service = new Service.ContactSensor(this.name); 130 | 131 | service 132 | .getCharacteristic(Characteristic.MotionDetected) 133 | .on('get', this.getDefaultValue.bind(this)); 134 | } 135 | 136 | if(service != undefined){ 137 | console.log("Initializing " + service.displayName + ", " + this.sensorType); 138 | 139 | var eventUrl = this.url + this.deviceId + "/events/" + this.eventName + "?access_token=" + this.accessToken; 140 | var es = new eventSource(eventUrl); 141 | 142 | console.log(eventUrl); 143 | 144 | es.onerror = function() { 145 | console.log('ERROR!'); 146 | }; 147 | 148 | es.addEventListener(this.eventName, 149 | this.processEventData.bind(this), false); 150 | 151 | this.services.push(service); 152 | } 153 | 154 | console.log("Service Count: " + this.services.length); 155 | } 156 | } 157 | 158 | ParticleAccessory.prototype.setState = function(state, callback) { 159 | this.log.info("Getting current state..."); 160 | 161 | this.log.info("URL: " + this.url); 162 | this.log.info("Device ID: " + this.deviceId); 163 | 164 | var onUrl = this.url + this.deviceId + "/" + this.functionName; 165 | 166 | var argument = this.args.replace("{STATE}", (state ? "1" : "0")); 167 | 168 | this.log.info("Calling function: " + onUrl + "?" + argument); 169 | 170 | request.post( 171 | onUrl, { 172 | form: { 173 | access_token: this.accessToken, 174 | args: argument 175 | } 176 | }, 177 | function(error, response, body) { 178 | //console.log(response); 179 | 180 | if (!error) { 181 | callback(); 182 | } else { 183 | callback(error); 184 | } 185 | } 186 | ); 187 | } 188 | 189 | ParticleAccessory.prototype.processEventData = function(e){ 190 | var data = JSON.parse(e.data); 191 | var tokens = data.data.split('='); 192 | 193 | console.log(tokens[0] + " = " + tokens[1] + ", " + this.services[1].displayName + ", " + this.sensorType + ", " + this.key.toLowerCase() + ", " + tokens[0].toLowerCase()); 194 | console.log(this.services[1] != undefined && this.key.toLowerCase() === tokens[0].toLowerCase()); 195 | 196 | if(this.services[1] != undefined && this.key.toLowerCase() === tokens[0].toLowerCase()){ 197 | if (tokens[0].toLowerCase() === "temperature") { 198 | this.value = parseFloat(tokens[1]); 199 | 200 | this.services[1] 201 | .getCharacteristic(Characteristic.CurrentTemperature) 202 | .setValue(parseFloat(tokens[1])); 203 | } 204 | else if (tokens[0].toLowerCase() === "humidity") { 205 | this.value = parseFloat(tokens[1]); 206 | 207 | this.services[1] 208 | .getCharacteristic(Characteristic.CurrentRelativeHumidity) 209 | .setValue(parseFloat(tokens[1])); 210 | } 211 | else if (tokens[0].toLowerCase() === "light") { 212 | this.value = parseFloat(tokens[1]); 213 | 214 | this.services[1] 215 | .getCharacteristic(Characteristic.CurrentAmbientLightLevel) 216 | .setValue(parseFloat(tokens[1])); 217 | } 218 | else if (tokens[0].toLowerCase() === "switch") { 219 | this.value = parseFloat(tokens[1]); 220 | 221 | this.services[1] 222 | .getCharacteristic(Characteristic.On) 223 | .setValue(parseFloat(tokens[1])); 224 | } 225 | else if (tokens[0].toLowerCase() === "motion") { 226 | this.value = parseFloat(tokens[1]); 227 | this.log('Received ' + this.value); 228 | if (this.value === '1.00' || this.value === 1.00 || this.value === 'true' || this.value === 'TRUE') this.value = true; 229 | else if (this.value === '0.00' || this.value === 0.00 || this.value === 'false' || this.value === 'FALSE') this.value = false; 230 | if (this.value !== true && this.value !== false) { 231 | this.log('Received value is not valid.'); 232 | } else { 233 | this.services[1] 234 | .getCharacteristic(Characteristic.MotionDetected) 235 | .setValue(this.value); 236 | } 237 | else if (tokens[0].toLowerCase() === "contact") { 238 | this.value = parseFloat(tokens[1]); 239 | this.log('Received ' + this.value); 240 | if (this.value === '1.00' || this.value === 1.00 || this.value === 'true' || this.value === 'TRUE') this.value = true; 241 | else if (this.value === '0.00' || this.value === 0.00 || this.value === 'false' || this.value === 'FALSE') this.value = false; 242 | if (this.value !== true && this.value !== false) { 243 | this.log('Received value is not valid.'); 244 | } else { 245 | this.services[1] 246 | .getCharacteristic(Characteristic.ContactDetected) 247 | .setValue(this.value); 248 | } 249 | } 250 | } 251 | } 252 | } 253 | 254 | ParticleAccessory.prototype.getDefaultValue = function(callback) { 255 | callback(null, this.value); 256 | } 257 | 258 | ParticleAccessory.prototype.setCurrentValue = function(value, callback) { 259 | console.log("Value: " + value); 260 | 261 | callback(null, value); 262 | } 263 | 264 | ParticleAccessory.prototype.getServices = function() { 265 | return this.services; 266 | } --------------------------------------------------------------------------------