I take no credit for the app, but I have used Paul's smartthings to homebridge to home kit app and it works pretty well.
There is a post going within the GitHub where they are editing the JSON Api and creating the hubitat app to bring Homekit integration into Hubitat courtesy of homebridge.
Check it out and maybe someone can support this further?
[UPDATE] Please use @tonesto7 Tony's much more polished version of the homebridge-hubitat plugin from the thread linked immediately below. Thank you Tony for your work on making this easy to install for users, and easier to use with Hubitat.
[Begin original message]
Yea, that is me doing that editing. I have the "JSON Complete API" app running on my Hubitat. One thing that I have noticed is that Hubitat is removing spaces in the JSON response, specifically in the names of the device capabilties. For example, "Temperature Measurement" is coming over as "TemperatureMeasurement", which then confuses the homebridge-smartthings plugin. I have even edited portions of the plugin to account for this, which has helped. But right now the direct updates from Hubitat to Homebridge are not working.
I hope to have more time to debug this and then try to get it released to the Hubitat community. I am totally open to any assistance from community members who are a little more skilled in Groovy and NodeJS though!
Using the code below, I have a working Hubitat Homebridge conficguration, but the DIRECT UPDATES are not working for some reason. Which means, you can use Homekit on your iOS device to control devices, but their status in Homekit is not being updated automatically. (UPDATE: Using the old polling method for updates does work, however it is not ideal as it places an unnecessary workload on the Hubitat Hub.)
I could really use some help from the community, if anyone is interested.
Here is my "JSON Complete API" app, updated for Hubitat.
/**
* JSON Complete API
*
* Copyright 2017 Paul Lovelace
*
* Modifications for Hubitat (in progress) - Dan Ogorchock 2/14/2018
*/
definition(
name: "JSON Complete API",
namespace: "pdlove",
author: "Paul Lovelace",
description: "API for JSON with complete set of devices",
category: "SmartThings Labs",
iconUrl: "https://raw.githubusercontent.com/pdlove/homebridge-smartthings/master/smartapps/JSON%401.png",
iconX2Url: "https://raw.githubusercontent.com/pdlove/homebridge-smartthings/master/smartapps/JSON%402.png",
iconX3Url: "https://raw.githubusercontent.com/pdlove/homebridge-smartthings/master/smartapps/JSON%403.png",
oauth: true)
preferences {
page(name: "copyConfig")
}
//When adding device groups, need to add here
def copyConfig() {
if (!state.accessToken) {
createAccessToken()
}
dynamicPage(name: "copyConfig", title: "Configure Devices", install:true, uninstall:true) {
section("Select devices to include in the /devices API call") {
paragraph "Version 0.5.5"
input "deviceList", "capability.refresh", title: "Most Devices", multiple: true, required: false
input "sensorList", "capability.sensor", title: "Sensor Devices", multiple: true, required: false
input "switchList", "capability.switch", title: "All Switches", multiple: true, required: false
//paragraph "Devices Selected: ${deviceList ? deviceList?.size() : 0}\nSensors Selected: ${sensorList ? sensorList?.size() : 0}\nSwitches Selected: ${switchList ? switchList?.size() : 0}"
}
section("Configure Pubnub") {
input "pubnubSubscribeKey", "text", title: "PubNub Subscription Key", multiple: false, required: false
input "pubnubPublishKey", "text", title: "PubNub Publish Key", multiple: false, required: false
input "subChannel", "text", title: "Channel (Can be anything)", multiple: false, required: false
}
section() {
paragraph "View this SmartApp's configuration to use it in other places."
//Original SmartThings cloud endpoint
//href url:"${apiServerUrl("/api/smartapps/installations/${app.id}/config?access_token=${state.accessToken}")}", style:"embedded", required:false, title:"Config", description:"Tap, select, copy, then click \"Done\""
//Hubitat cloud endpoint
//href url:"${getApiServerUrl()}/${hubUID}/apps/${app.id}/config?access_token=${state.accessToken}", style:"embedded", required:false, title:"Config", description:"Tap, select, copy, then click \"Done\""
//Hubitat local endpoint
href url:fullLocalApiServerUrl("config") + "?access_token=${state.accessToken}", style:"embedded", required:false, title:"Config", description:"Tap, select, copy, then click \"Done\""
}
section() {
paragraph "View the JSON generated from the installed devices."
//Original SmartThings cloud endpoint
//href url:"${apiServerUrl("/api/smartapps/installations/${app.id}/devices?access_token=${state.accessToken}")}", style:"embedded", required:false, title:"Device Results", description:"View accessories JSON"
//Hubitat cloud endpoint
//href url:"${getApiServerUrl()}/${hubUID}/apps/${app.id}/devices?access_token=${state.accessToken}", style:"embedded", required:false, title:"Device Results", description:"View accessories JSON"
//Hubitat local endpoint
href url:fullLocalApiServerUrl("devices") + "?access_token=${state.accessToken}", style:"embedded", required:false, title:"Device Results", description:"View accessories JSON"
}
section() {
paragraph "Enter the name you would like shown in the smart app list"
label title:"SmartApp Label (optional)", required: false
}
}
}
def renderDevices() {
def deviceData = []
deviceList.each {
try {
deviceData << [name: it.displayName,
basename: it.name,
deviceid: it.id,
status: it.status,
manufacturerName: it.getManufacturerName(),
modelName: it.getModelName(),
lastTime: it.getLastActivity(),
capabilities: deviceCapabilityList(it),
commands: deviceCommandList(it),
attributes: deviceAttributeList(it)
]
} catch (e) {
log.error("Error Occurred Parsing Device "+it.displayName+", Error " + e)
}
}
sensorList.each {
try {
deviceData << [name: it.displayName,
basename: it.name,
deviceid: it.id,
status: it.status,
manufacturerName: it.getManufacturerName(),
modelName: it.getModelName(),
lastTime: it.getLastActivity(),
capabilities: deviceCapabilityList(it),
commands: deviceCommandList(it),
attributes: deviceAttributeList(it)
]
} catch (e) {
log.error("Error Occurred Parsing Device "+it.displayName+", Error " + e)
}
}
switchList.each {
try {
deviceData << [name: it.displayName,
basename: it.name,
deviceid: it.id,
status: it.status,
manufacturerName: it.getManufacturerName(),
modelName: it.getModelName(),
lastTime: it.getLastActivity(),
capabilities: deviceCapabilityList(it),
commands: deviceCommandList(it),
attributes: deviceAttributeList(it)
]
} catch (e) {
log.error("Error Occurred Parsing Device "+it.displayName+", Error " + e)
}
}
return deviceData
}
def findDevice(paramid) {
def device = deviceList.find { it.id == paramid }
if (device) return device
device = sensorList.find { it.id == paramid }
if (device) return device
device = switchList.find { it.id == paramid }
return device
}
//No more individual device group definitions after here.
def installed() {
log.debug "Installed with settings: ${settings}"
initialize()
}
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
initialize()
}
def initialize() {
if(!state.accessToken) {
createAccessToken()
}
registerAll()
state.subscriptionRenewed = 0
subscribe(location, null, HubResponseEvent, [filterEvents:false])
log.debug "0.5.5"
}
def authError() {
[error: "Permission denied"]
}
def renderConfig() {
def configJson = new groovy.json.JsonOutput().toJson([
description: "JSON API",
platforms: [
[
platform: "SmartThings",
name: "SmartThings",
//
//Original SmartThings cloud endpoint
//app_url: apiServerUrl("/api/smartapps/installations/"),
//
//Hubitat cloud endpoint
//app_url: "${getApiServerUrl()}/${hubUID}/apps/",
//
//Hubitat local endpoint
app_url: getLocalApiServerUrl() + "/",
app_id: app.id,
access_token: state.accessToken
]
],
])
def configString = new groovy.json.JsonOutput().prettyPrint(configJson)
render contentType: "text/plain", data: configString
}
def renderLocation() {
[
latitude: location.latitude,
longitude: location.longitude,
mode: location.mode,
name: location.name,
temperature_scale: location.temperatureScale,
zip_code: location.zipCode,
hubIP: location.hubs[0].getDataValue("localIP"),
//hubIP: location.hubs[0].localIP,
smartapp_version: '0.5.5'
]
}
def CommandReply(statusOut, messageOut) {
def replyData =
[
status: statusOut,
message: messageOut
]
def replyJson = new groovy.json.JsonOutput().toJson(replyData)
render contentType: "application/json", data: replyJson
}
def deviceCommand() {
log.info("Command Request")
def device = findDevice(params.id)
def command = params.command
if (!device) {
log.error("Device Not Found")
CommandReply("Failure", "Device Not Found")
} else if (!device.hasCommand(command)) {
log.error("Device "+device.displayName+" does not have the command "+command)
CommandReply("Failure", "Device "+device.displayName+" does not have the command "+command)
} else {
def value1 = request.JSON?.value1
def value2 = request.JSON?.value2
try {
if (value2) {
device."$command"(value1,value2)
} else if (value1) {
device."$command"(value1)
} else {
device."$command"()
}
log.info("Command Successful for Device "+device.displayName+", Command "+command)
CommandReply("Success", "Device "+device.displayName+", Command "+command)
} catch (e) {
log.error("Error Occurred For Device "+device.displayName+", Command "+command)
CommandReply("Failure", "Error Occurred For Device "+device.displayName+", Command "+command)
}
}
}
def deviceAttribute() {
def device = findDevice(params.id)
def attribute = params.attribute
if (!device) {
httpError(404, "Device not found")
} else {
def currentValue = device.currentValue(attribute)
[currentValue: currentValue]
}
}
def deviceQuery() {
def device = findDevice(params.id)
if (!device) {
device = null
httpError(404, "Device not found")
}
if (result) {
def jsonData =
[
name: device.displayName,
deviceid: device.id,
capabilities: deviceCapabilityList(device),
commands: deviceCommandList(device),
attributes: deviceAttributeList(device)
]
def resultJson = new groovy.json.JsonOutput().toJson(jsonData)
render contentType: "application/json", data: resultJson
}
}
def deviceCapabilityList(device) {
def i=0
device.capabilities.collectEntries { capability->
[
(capability.name):1
]
}
}
def deviceCommandList(device) {
def i=0
device.supportedCommands.collectEntries { command->
[
(command.name): (command.arguments)
]
}
}
def deviceAttributeList(device) {
device.supportedAttributes.collectEntries { attribute->
try {
[
(attribute.name): device.currentValue(attribute.name)
]
} catch(e) {
[
(attribute.name): null
]
}
}
}
def getAllData() {
//Since we're about to send all of the data, we'll count this as a subscription renewal and clear out pending changes.
state.subscriptionRenewed = now()
state.devchanges = []
def deviceData =
[ location: renderLocation(),
deviceList: renderDevices() ]
def deviceJson = new groovy.json.JsonOutput().toJson(deviceData)
render contentType: "application/json", data: deviceJson
}
def startSubscription() {
//This simply registers the subscription.
state.subscriptionRenewed = now()
def deviceJson = new groovy.json.JsonOutput().toJson([status: "Success"])
render contentType: "application/json", data: deviceJson
}
def endSubscription() {
//Because it takes too long to register for an api command, we don't actually unregister.
//We simply blank the devchanges and change the subscription renewal to two hours ago.
state.devchanges = []
state.subscriptionRenewed = 0
def deviceJson = new groovy.json.JsonOutput().toJson([status: "Success"])
render contentType: "application/json", data: deviceJson
}
def registerAll() {
//This has to be done at startup because it takes too long for a normal command.
log.debug "Registering All Events"
state.devchanges = []
registerChangeHandler(deviceList)
registerChangeHandler(sensorList)
registerChangeHandler(switchList)
}
def registerChangeHandler(myList) {
myList.each { myDevice ->
def theAtts = myDevice.supportedAttributes
theAtts.each {att ->
subscribe(myDevice, att.name, changeHandler)
log.debug "Registering ${myDevice.displayName}.${att.name}"
}
}
}
def changeHandler(evt) {
//Send to Pubnub if we need to.
if (pubnubPublishKey!=null) {
def deviceData = [device: evt.deviceId, attribute: evt.name, value: evt.value, date: evt.date]
def changeJson = new groovy.json.JsonOutput().toJson(deviceData)
def changeData = URLEncoder.encode(changeJson)
def uri = "http://pubsub.pubnub.com/publish/${pubnubPublishKey}/${pubnubSubscribeKey}/0/${subChannel}/0/${changeData}"
log.debug "${uri}"
httpGet(uri)
}
if (state.directIP!="") {
//Send Using the Direct Mechanism
def deviceData = [device: evt.deviceId, attribute: evt.name, value: evt.value, date: evt.date]
//How do I control the port?!?
log.debug "Sending Update to ${state.directIP}:${state.directPort}"
def result = new hubitat.device.HubAction(
method: "GET",
path: "/update",
headers: [
HOST: "${state.directIP}:${state.directPort}",
change_device: evt.deviceId,
change_attribute: evt.name,
change_value: evt.value,
change_date: evt.date
]
)
//log.debug "changeHandler: sending result = ${result}"
sendHubCommand(result)
}
//Only add to the state's devchanges if the endpoint has renewed in the last 10 minutes.
if (state.subscriptionRenewed>(now()-(1000*60*10))) {
// if (evt.isStateChange()) {
log.debug "evt.getIsStateChange() = ${evt.getIsStateChange()}"
if (evt.getIsStateChange()) {
state.devchanges << [device: evt.deviceId, attribute: evt.name, value: evt.value, date: evt.date]
} else if (state.subscriptionRenewed>0) { //Otherwise, clear it
log.debug "Endpoint Subscription Expired. No longer storing changes for devices."
state.devchanges=[]
state.subscriptionRenewed=0
}
}
}
def getChangeEvents() {
//Store the changes so we can swap it out very quickly and eliminate the possibility of losing any.
//This is mainly to make this thread safe because I'm willing to bet that a change event can fire
//while generating/sending the JSON.
def oldchanges = state.devchanges
state.devchanges=[]
state.subscriptionRenewed = now()
if (oldchanges.size()==0) {
def deviceJson = new groovy.json.JsonOutput().toJson([status: "None"])
render contentType: "application/json", data: deviceJson
} else {
def changeJson = new groovy.json.JsonOutput().toJson([status: "Success", attributes:oldchanges])
render contentType: "application/json", data: changeJson
}
}
def enableDirectUpdates() {
log.debug("Command Request")
state.directIP = params.ip
state.directPort = params.port
log.debug("Trying ${state.directIP}:${state.directPort}")
def result = new hubitat.device.HubAction(
method: "GET",
path: "/initial",
headers: [
HOST: "${state.directIP}:${state.directPort}"
],
query: deviceData
)
sendHubCommand(result)
}
def HubResponseEvent(evt) {
log.debug(evt.description)
}
def locationHandler(evt) {
def description = evt.description
def hub = evt?.hubId
log.debug "cp desc: " + description
if (description.count(",") > 4)
{
def bodyString = new String(description.split(',')[5].split(":")[1].decodeBase64())
log.debug(bodyString)
}
}
def getSubscriptionService() {
def replyData =
[
pubnub_publishkey: pubnubPublishKey,
pubnub_subscribekey: pubnubSubscribeKey,
pubnub_channel: subChannel
]
def replyJson = new groovy.json.JsonOutput().toJson(replyData)
render contentType: "application/json", data: replyJson
}
mappings {
/* if (!params.access_token || (params.access_token && params.access_token != state.accessToken)) {
path("/devices") { action: [GET: "authError"] }
path("/config") { action: [GET: "authError"] }
path("/location") { action: [GET: "authError"] }
path("/:id/command/:command") { action: [POST: "authError"] }
path("/:id/query") { action: [GET: "authError"] }
path("/:id/attribute/:attribute") { action: [GET: "authError"] }
path("/subscribe") { action: [GET: "authError"] }
path("/getUpdates") { action: [GET: "authError"] }
path("/unsubscribe") { action: [GET: "authError"] }
path("/startDirect/:ip/:port") { action: [GET: "authError"] }
path("/getSubcriptionService") { action: [GET: "authError"] }
} else {
*/
path("/devices") { action: [GET: "getAllData"] }
path("/config") { action: [GET: "renderConfig"] }
path("/location") { action: [GET: "renderLocation"] }
path("/:id/command/:command") { action: [POST: "deviceCommand"] }
path("/:id/query") { action: [GET: "deviceQuery"] }
path("/:id/attribute/:attribute") { action: [GET: "deviceAttribute"] }
path("/subscribe") { action: [GET: "startSubscription"] }
path("/getUpdates") { action: [GET: "getChangeEvents"] }
path("/unsubscribe") { action: [GET: "endSubscription"] }
path("/startDirect/:ip/:port") { action: [GET: "enableDirectUpdates"] }
path("/getSubcriptionService") { action: [GET: "getSubscriptionService"] }
// }
}
And here is my edited version of smartthingsapi.js which runs on the homebridge app server (in my case, a raspberry pi).
var http = require('http');
var url = require('url');
var app_host;
var app_port;
var app_path;
var access_token;
function _http(data, callback) {
//console.log("Calling Smartthings");
var options = {
hostname: app_host,
port: app_port,
path: app_path + data.path + "?access_token=" + access_token,
method: data.method,
headers: {}
};
var that = this;
if (data.data) {
data.data = JSON.stringify(data.data);
options.headers['Content-Length'] = Buffer.byteLength(data.data);
options.headers['Content-Type'] = "application/json";
}
var str = '';
var req = http.request(options, function (response) {
response.on('data', function (chunk) {
str += chunk;
});
response.on('end', function () {
if (data.debug) console.log("response in http:", str);
try {
str = JSON.parse(str);
} catch (e) {
if (data.debug) {
console.log(e.stack);
console.log("raw message", str);
}
str = undefined;
}
if (callback) { callback(str); callback=undefined; };
});
});
if (data.data) {
req.write(data.data);
}
req.end();
req.on('error', function (e) {
console.log("error at req: ", e.message);
if (callback) { callback(); callback=undefined; };
});
}
function POST(data, callback) {
data.method = "POST";
_http(data, callback);
}
function PUT(data, callback) {
data.method = "PUT";
_http(data, callback);
}
function GET(data, callback) {
data.method = "GET";
_http(data, callback);
}
function DELETE(data, callback) {
data.method = "DELETE";
_http(data, callback);
}
var smartthings = {
init: function (inURL, inAppID, inAccess_Token) {
var appURL = url.parse(inURL);
app_host = appURL.hostname || "graph.api.smartthings.com";
app_port = appURL.port || "80";
app_path = (appURL.path || "/api/smartapps/installations/") + inAppID + "/";
access_token = inAccess_Token;
},
getDevices: function (callback) {
GET({ debug: false, path: 'devices' }, function (data) {
if (callback) { callback(data); callback=undefined; };
})
},
getDevice: function (deviceid, callback) {
GET({ debug: false, path: deviceid + '/query' }, function (data) {
if (data) {
if (callback) { callback(data); callback=undefined; };
} else {
if (callback) { callback(); callback=undefined; };;
}
})
},
getUpdates: function (callback) {
GET({ debug: false, path: 'getUpdates' }, function (data) {
if (callback) { callback(data); callback=undefined; };;
})
},
runCommand: function (callback, deviceid, command, values) {
POST({ debug: false, path: deviceid + '/command/' + command, data: values }, function (data) {
if (callback) { callback(); callback=undefined; };;
})
},
startDirect: function (callback, myIP, myPort) {
GET({ debug: false, path: 'startDirect/' + myIP + '/' + myPort }, function (data) {
if (callback) { callback(); callback=undefined; };;
})
},
getSubscriptionService: function (callback) {
GET({ debug: false, path: 'getSubcriptionService' }, function (data) {
if (callback) { callback(data); callback=undefined; };;
})
}
}
module.exports = smartthings;
And finally, here is my copy of smartthings.js which is modified to account for the lack of spaces in the device Capabilities names returned by Hubitat.
var inherits = require('util').inherits;
var Accessory, Service, Characteristic, uuid, EnergyCharacteristics;
/*
* SmartThings Accessory
*/
module.exports = function(oAccessory, oService, oCharacteristic, ouuid) {
if (oAccessory) {
Accessory = oAccessory;
Service = oService;
Characteristic = oCharacteristic;
EnergyCharacteristics = require('../lib/customCharacteristics').EnergyCharacteristics(Characteristic)
uuid = ouuid;
inherits(SmartThingsAccessory, Accessory);
SmartThingsAccessory.prototype.loadData = loadData;
SmartThingsAccessory.prototype.getServices = getServices;
}
return SmartThingsAccessory;
};
module.exports.SmartThingsAccessory = SmartThingsAccessory;
function SmartThingsAccessory(platform, device) {
this.deviceid = device.deviceid;
this.name = device.name;
this.platform = platform;
this.state = {};
this.device = device;
var idKey = 'hbdev:smartthings:' + this.deviceid;
var id = uuid.generate(idKey);
Accessory.call(this, this.name, id);
var that = this;
//Get the Capabilities List
for (var index in device.capabilities) {
if ((platform.knownCapabilities.indexOf(index) == -1) && (platform.unknownCapabilities.indexOf(index) == -1))
platform.unknownCapabilities.push(index);
}
this.getaddService = function(Service) {
var myService = this.getService(Service);
if (!myService) myService = this.addService(Service);
return myService
};
this.deviceGroup = "unknown"; //This way we can easily tell if we set a device group
var thisCharacteristic;
if (device.capabilities["SwitchLevel"] !== undefined) {
if (device.commands.levelOpenClose) {
//This is a Window Shade
this.deviceGroup = "shades"
thisCharacteristic = this.getaddService(Service.WindowCovering).getCharacteristic(Characteristic.TargetPosition)
thisCharacteristic.on('get', function(callback) { callback(null, parseInt(that.device.attributes.level)); });
thisCharacteristic.on('set', function(value, callback) { that.platform.api.runCommand(callback, that.deviceid, "setLevel", { value1: value }); });
that.platform.addAttributeUsage("level", this.deviceid, thisCharacteristic);
thisCharacteristic = this.getaddService(Service.WindowCovering).getCharacteristic(Characteristic.CurrentPosition)
thisCharacteristic.on('get', function(callback) { callback(null, parseInt(that.device.attributes.level)); });
that.platform.addAttributeUsage("level", this.deviceid, thisCharacteristic);
} else if (device.commands.lowSpeed) {
//This is a Ceiling Fan
this.deviceGroup = "fans"
thisCharacteristic = this.getaddService(Service.Fan).getCharacteristic(Characteristic.On)
thisCharacteristic.on('get', function(callback) { callback(null, that.device.attributes.switch == "on"); })
thisCharacteristic.on('set', function(value, callback) {
if (value)
that.platform.api.runCommand(callback, that.deviceid, "on");
else
that.platform.api.runCommand(callback, that.deviceid, "off"); });
that.platform.addAttributeUsage("switch", this.deviceid, thisCharacteristic);
thisCharacteristic = this.getaddService(Service.Fan).getCharacteristic(Characteristic.RotationSpeed)
thisCharacteristic.on('get', function(callback) { callback(null, parseInt(that.device.attributes.level)); });
thisCharacteristic.on('set', function(value, callback) {
if (value > 0)
that.platform.api.runCommand(callback, that.deviceid, "setLevel", {value1: value }); });
that.platform.addAttributeUsage("level", this.deviceid, thisCharacteristic);
} else if (device.commands.setLevel) {
this.deviceGroup = "lights";
thisCharacteristic = this.getaddService(Service.Lightbulb).getCharacteristic(Characteristic.On)
thisCharacteristic.on('get', function(callback) { callback(null, that.device.attributes.switch == "on"); });
thisCharacteristic.on('set', function(value, callback) {
if (value)
that.platform.api.runCommand(callback, that.deviceid, "on");
else
that.platform.api.runCommand(callback, that.deviceid, "off"); });
that.platform.addAttributeUsage("switch", this.deviceid, thisCharacteristic);
thisCharacteristic = this.getaddService(Service.Lightbulb).getCharacteristic(Characteristic.Brightness)
thisCharacteristic.on('get', function(callback) { callback(null, parseInt(that.device.attributes.level)); });
thisCharacteristic.on('set', function(value, callback) { that.platform.api.runCommand(callback, that.deviceid, "setLevel", { value1: value }); });
that.platform.addAttributeUsage("level", this.deviceid, thisCharacteristic);
if (device.capabilities["ColorControl"] !== undefined) {
thisCharacteristic = this.getaddService(Service.Lightbulb).getCharacteristic(Characteristic.Hue)
thisCharacteristic.on('get', function(callback) { callback(null, Math.round(that.device.attributes.hue*3.6)); });
thisCharacteristic.on('set', function(value, callback) { that.platform.api.runCommand(callback, that.deviceid, "setHue", { value1: Math.round(value/3.6) }); });
that.platform.addAttributeUsage("hue", this.deviceid, thisCharacteristic);
thisCharacteristic = this.getaddService(Service.Lightbulb).getCharacteristic(Characteristic.Saturation)
thisCharacteristic.on('get', function(callback) { callback(null, parseInt(that.device.attributes.saturation)); });
thisCharacteristic.on('set', function(value, callback) { that.platform.api.runCommand(callback, that.deviceid, "setSaturation", { value1: value }); });
that.platform.addAttributeUsage("saturation", this.deviceid, thisCharacteristic);
}
}
}
if (device.capabilities["GarageDoorControl"] !== undefined) {
this.deviceGroup = "garage_doors";
thisCharacteristic = this.getaddService(Service.GarageDoorOpener).getCharacteristic(Characteristic.TargetDoorState)
thisCharacteristic.on('get', function(callback) {
if (that.device.attributes.door == 'closed' || that.device.attributes.door == 'closing')
callback(null, Characteristic.TargetDoorState.CLOSED);
else if (that.device.attributes.door == 'open' || that.device.attributes.door == 'opening')
callback(null, Characteristic.TargetDoorState.OPEN); });
thisCharacteristic.on('set', function(value, callback) {
if (value == Characteristic.TargetDoorState.OPEN) {
that.platform.api.runCommand(callback, that.deviceid, "open");
that.device.attributes.door = "opening";
} else if (value == Characteristic.TargetDoorState.CLOSED) {
that.platform.api.runCommand(callback, that.deviceid, "close");
that.device.attributes.door = "closing";
} });
that.platform.addAttributeUsage("door", this.deviceid, thisCharacteristic);
thisCharacteristic = this.getaddService(Service.GarageDoorOpener).getCharacteristic(Characteristic.CurrentDoorState)
thisCharacteristic.on('get', function(callback) {
switch (that.device.attributes.door) {
case 'open':
callback(null, Characteristic.TargetDoorState.OPEN);
break;
case 'opening':
callback(null, Characteristic.TargetDoorState.OPENING);
break;
case 'closed':
callback(null, Characteristic.TargetDoorState.CLOSED);
break;
case 'closing':
callback(null, Characteristic.TargetDoorState.CLOSING);
break;
default:
callback(null, Characteristic.TargetDoorState.STOPPED);
break;
}
});
that.platform.addAttributeUsage("door", this.deviceid, thisCharacteristic);
this.getaddService(Service.GarageDoorOpener).setCharacteristic(Characteristic.ObstructionDetected, false);
}
if (device.capabilities["Lock"] !== undefined) {
this.deviceGroup = "locks";
thisCharacteristic = this.getaddService(Service.LockMechanism).getCharacteristic(Characteristic.LockCurrentState)
thisCharacteristic.on('get', function(callback) {
switch (that.device.attributes.lock) {
case 'locked':
callback(null, Characteristic.LockCurrentState.SECURED);
break;
case 'unlocked':
callback(null, Characteristic.LockCurrentState.UNSECURED);
break;
default:
callback(null, Characteristic.LockCurrentState.UNKNOWN);
break;
} });
that.platform.addAttributeUsage("lock", this.deviceid, thisCharacteristic);
thisCharacteristic = this.getaddService(Service.LockMechanism).getCharacteristic(Characteristic.LockTargetState)
thisCharacteristic.on('get', function(callback) {
switch (that.device.attributes.lock) {
case 'locked':
callback(null, Characteristic.LockCurrentState.SECURED);
break;
case 'unlocked':
callback(null, Characteristic.LockCurrentState.UNSECURED);
break;
default:
callback(null, Characteristic.LockCurrentState.UNKNOWN);
break;
} });
thisCharacteristic.on('set', function(value, callback) {
if (value === false) {
value = Characteristic.LockTargetState.UNSECURED;
} else if (value === true) {
value = Characteristic.LockTargetState.SECURED;
}
switch (value) {
case Characteristic.LockTargetState.SECURED:
that.platform.api.runCommand(callback, that.deviceid, "lock");
that.device.attributes.lock = "locked";
break;
case Characteristic.LockTargetState.UNSECURED:
that.platform.api.runCommand(callback, that.deviceid, "unlock");
that.device.attributes.lock = "unlocked";
break;
} });
that.platform.addAttributeUsage("lock", this.deviceid, thisCharacteristic);
}
// if (devices.capabilities["Valve"] !== undefined) {
// this.deviceGroup = "valve";
// Thinking of implementing this as a Door service.
// }
if (device.capabilities["Button"] !== undefined) {
this.deviceGroup = " button";
}
if (device.capabilities["Switch"] !== undefined && this.deviceGroup == "unknown") {
this.deviceGroup = "switch";
thisCharacteristic = this.getaddService(Service.Switch).getCharacteristic(Characteristic.On)
thisCharacteristic.on('get', function(callback) { callback(null, that.device.attributes.switch == "on"); })
thisCharacteristic.on('set', function(value, callback) {
if (value)
that.platform.api.runCommand(callback, that.deviceid, "on");
else
that.platform.api.runCommand(callback, that.deviceid, "off");
});
that.platform.addAttributeUsage("switch", this.deviceid, thisCharacteristic);
if (device.capabilities["SwitchLevel"] !== undefined) {
thisCharacteristic = this.getaddService(Service.Lightbulb).getCharacteristic(Characteristic.Brightness)
thisCharacteristic.on('get', function(callback) { callback(null, parseInt(that.device.attributes.level)); });
thisCharacteristic.on('set', function(value, callback) { that.platform.api.runCommand(callback, that.deviceid, "setLevel", { value1: value }); });
that.platform.addAttributeUsage("level", this.deviceid, thisCharacteristic);
}
}
if ((device.capabilities["SmokeDetector"] !== undefined) && (that.device.attributes.smoke)) {
this.deviceGroup = "detectors";
thisCharacteristic = this.getaddService(Service.SmokeSensor).getCharacteristic(Characteristic.SmokeDetected)
thisCharacteristic.on('get', function(callback) {
if (that.device.attributes.smoke == 'clear')
callback(null, Characteristic.SmokeDetected.SMOKE_NOT_DETECTED);
else
callback(null, Characteristic.SmokeDetected.SMOKE_DETECTED);
});
that.platform.addAttributeUsage("smoke", this.deviceid, thisCharacteristic);
}
if ((device.capabilities["CarbonMonoxideDetector"] !== undefined) && (that.device.attributes.carbonMonoxide)) {
this.deviceGroup = "detectors";
thisCharacteristic = this.getaddService(Service.CarbonMonoxideSensor).getCharacteristic(Characteristic.CarbonMonoxideDetected)
thisCharacteristic.on('get', function(callback) {
if (that.device.attributes.carbonMonoxide == 'clear')
callback(null, Characteristic.CarbonMonoxideDetected.CO_LEVELS_NORMAL);
else
callback(null, Characteristic.CarbonMonoxideDetected.CO_LEVELS_ABNORMAL);
});
that.platform.addAttributeUsage("carbonMonoxide", this.deviceid, thisCharacteristic);
}
if (device.capabilities["MotionSensor"] !== undefined) {
if (this.deviceGroup == 'unknown') this.deviceGroup = "sensor";
thisCharacteristic = this.getaddService(Service.MotionSensor).getCharacteristic(Characteristic.MotionDetected)
thisCharacteristic.on('get', function(callback) { callback(null, (that.device.attributes.motion == "active")); });
that.platform.addAttributeUsage("motion", this.deviceid, thisCharacteristic);
}
if (device.capabilities["WaterSensor"] !== undefined) {
if (this.deviceGroup == 'unknown') this.deviceGroup = "sensor";
thisCharacteristic = this.getaddService(Service.LeakSensor).getCharacteristic(Characteristic.LeakDetected)
thisCharacteristic.on('get', function(callback) {
var reply = Characteristic.LeakDetected.LEAK_DETECTED;
if (that.device.attributes.water == "dry") reply = Characteristic.LeakDetected.LEAK_NOT_DETECTED;
callback(null, reply); });
that.platform.addAttributeUsage("water", this.deviceid, thisCharacteristic);
}
if (device.capabilities["PresenceSensor"] !== undefined) {
if (this.deviceGroup == 'unknown') this.deviceGroup = "sensor";
thisCharacteristic = this.getaddService(Service.OccupancySensor).getCharacteristic(Characteristic.OccupancyDetected)
thisCharacteristic.on('get', function(callback) { callback(null, (that.device.attributes.presence == "present")); });
that.platform.addAttributeUsage("presence", this.deviceid, thisCharacteristic);
}
if (device.capabilities["RelativeHumidityMeasurement"] !== undefined) {
if (this.deviceGroup == 'unknown') this.deviceGroup = "sensor";
thisCharacteristic = this.getaddService(Service.HumiditySensor).getCharacteristic(Characteristic.CurrentRelativeHumidity)
thisCharacteristic.on('get', function(callback) { callback(null, Math.round(that.device.attributes.humidity)); });
that.platform.addAttributeUsage("humidity", this.deviceid, thisCharacteristic);
}
if (device.capabilities["TemperatureMeasurement"] !== undefined) {
if (this.deviceGroup == 'unknown') this.deviceGroup = "sensor";
thisCharacteristic = this.getaddService(Service.TemperatureSensor).getCharacteristic(Characteristic.CurrentTemperature)
thisCharacteristic.on('get', function(callback) {
if (that.platform.temperature_unit == 'C')
callback(null, Math.round(that.device.attributes.temperature*10)/10);
else
callback(null, Math.round(((that.device.attributes.temperature - 32) / 1.8)*10)/10);
});
that.platform.addAttributeUsage("temperature", this.deviceid, thisCharacteristic);
}
if (device.capabilities["ContactSensor"] !== undefined && device.capabilities["GarageDoorControl"] === undefined) {
if (this.deviceGroup == 'unknown') this.deviceGroup = "sensor";
thisCharacteristic = this.getaddService(Service.ContactSensor).getCharacteristic(Characteristic.ContactSensorState)
thisCharacteristic.on('get', function(callback) {
if (that.device.attributes.contact == "closed")
callback(null, Characteristic.ContactSensorState.CONTACT_DETECTED);
else
callback(null, Characteristic.ContactSensorState.CONTACT_NOT_DETECTED);
});
that.platform.addAttributeUsage("contact", this.deviceid, thisCharacteristic);
}
if (device.capabilities["Battery"] !== undefined) {
thisCharacteristic = this.getaddService(Service.BatteryService).getCharacteristic(Characteristic.BatteryLevel)
thisCharacteristic.on('get', function(callback) { callback(null, Math.round(that.device.attributes.battery)); });
that.platform.addAttributeUsage("battery", this.deviceid, thisCharacteristic);
thisCharacteristic = this.getaddService(Service.BatteryService).getCharacteristic(Characteristic.StatusLowBattery)
thisCharacteristic.on('get', function(callback) {
if (that.device.attributes.battery < 0.20)
callback(null, Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW);
else
callback(null, Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL);
});
this.getaddService(Service.BatteryService).setCharacteristic(Characteristic.ChargingState, Characteristic.ChargingState.NOT_CHARGING);
that.platform.addAttributeUsage("battery", this.deviceid, thisCharacteristic);
}
if (device.capabilities["EnergyMeter"] !== undefined) {
this.deviceGroup = 'EnergyMeter';
thisCharacteristic = this.getaddService(Service.Outlet).addCharacteristic(EnergyCharacteristics.TotalConsumption1)
thisCharacteristic.on('get', function(callback) { callback(null, Math.round(that.device.attributes.energy)); });
that.platform.addAttributeUsage("energy", this.deviceid, thisCharacteristic);
}
if (device.capabilities["PowerMeter"] !== undefined) {
thisCharacteristic = this.getaddService(Service.Outlet).addCharacteristic(EnergyCharacteristics.CurrentConsumption1)
thisCharacteristic.on('get', function(callback) { callback(null, Math.round(that.device.attributes.power)); });
that.platform.addAttributeUsage("power", this.deviceid, thisCharacteristic);
}
if (device.capabilities["AccelerationSensor"] !== undefined) {
if (this.deviceGroup == 'unknown') this.deviceGroup = "sensor";
}
if (device.capabilities["ThreeAxis"] !== undefined) {
if (this.deviceGroup == 'unknown') this.deviceGroup = "sensor";
}
if (device.capabilities["Thermostat"] !== undefined) {
this.deviceGroup = "thermostats";
thisCharacteristic = this.getaddService(Service.Thermostat).getCharacteristic(Characteristic.CurrentHeatingCoolingState)
thisCharacteristic.on('get', function(callback) {
switch (that.device.attributes.thermostatOperatingState) {
case "pending cool":
case "cooling":
callback(null, Characteristic.CurrentHeatingCoolingState.COOL);
break;
case "pending heat":
case "heating":
callback(null, Characteristic.CurrentHeatingCoolingState.HEAT);
break;
default: //The above list should be inclusive, but we need to return something if they change stuff.
//TODO: Double check if Smartthings can send "auto" as operatingstate. I don't think it can.
callback(null, Characteristic.CurrentHeatingCoolingState.OFF);
break;
}
});
that.platform.addAttributeUsage("thermostatOperatingState", this.deviceid, thisCharacteristic);
//Handle the Target State
thisCharacteristic = this.getaddService(Service.Thermostat).getCharacteristic(Characteristic.TargetHeatingCoolingState)
thisCharacteristic.on('get', function(callback) {
switch (that.device.attributes.thermostatMode) {
case "cool":
callback(null, Characteristic.TargetHeatingCoolingState.COOL);
break;
case "emergency heat":
case "heat":
callback(null, Characteristic.TargetHeatingCoolingState.HEAT);
break;
case "auto":
callback(null, Characteristic.TargetHeatingCoolingState.AUTO);
break;
default: //The above list should be inclusive, but we need to return something if they change stuff.
callback(null, Characteristic.TargetHeatingCoolingState.OFF);
break;
}
})
thisCharacteristic.on('set', function(value, callback) {
switch (value) {
case Characteristic.TargetHeatingCoolingState.COOL:
that.platform.api.runCommand(callback, that.deviceid, "cool");
that.device.attributes.thermostatMode = 'cool';
break;
case Characteristic.TargetHeatingCoolingState.HEAT:
that.platform.api.runCommand(callback, that.deviceid, "heat");
that.device.attributes.thermostatMode = 'heat';
break;
case Characteristic.TargetHeatingCoolingState.AUTO:
that.platform.api.runCommand(callback, that.deviceid, "auto");
that.device.attributes.thermostatMode = 'auto';
break;
case Characteristic.TargetHeatingCoolingState.OFF:
that.platform.api.runCommand(callback, that.deviceid, "off");
that.device.attributes.thermostatMode = 'off';
break;
}
});
that.platform.addAttributeUsage("thermostatMode", this.deviceid, thisCharacteristic);
if (device.capabilities["RelativeHumidityMeasurement"] !== undefined) {
thisCharacteristic = this.getaddService(Service.Thermostat).getCharacteristic(Characteristic.CurrentRelativeHumidity)
thisCharacteristic.on('get', function(callback) {
callback(null, parseInt(that.device.attributes.humidity));
});
that.platform.addAttributeUsage("humidity", this.deviceid, thisCharacteristic);
}
thisCharacteristic = this.getaddService(Service.Thermostat).getCharacteristic(Characteristic.CurrentTemperature)
thisCharacteristic.on('get', function(callback) {
if (that.platform.temperature_unit == 'C')
callback(null, Math.round(that.device.attributes.temperature*10)/10);
else
callback(null, Math.round(((that.device.attributes.temperature - 32) / 1.8)*10)/10);
});
that.platform.addAttributeUsage("temperature", this.deviceid, thisCharacteristic);
thisCharacteristic = this.getaddService(Service.Thermostat).getCharacteristic(Characteristic.TargetTemperature)
thisCharacteristic.on('get', function(callback) {
var temp = undefined;
switch (that.device.attributes.thermostatMode) {
case "cool":
temp = that.device.attributes.coolingSetpoint;
break;
case "emergency heat":
case "heat":
temp = that.device.attributes.heatingSetpoint;
break;
default: //This should only refer to auto
// Choose closest target as single target
var high = that.device.attributes.coolingSetpoint;
var low = that.device.attributes.heatingSetpoint;
var cur = that.device.attributes.temperature;
temp = Math.abs(high - cur) < Math.abs(cur - low) ? high : low;
break;
}
if (!temp)
callback('Unknown');
else if (that.platform.temperature_unit == 'C')
callback(null, Math.round(temp*10)/10);
else
callback(null, Math.round(((temp - 32) / 1.8)*10)/10);
})
thisCharacteristic.on('set', function(value, callback) {
//Convert the Celsius value to the appropriate unit for Smartthings
var temp = value;
if (that.platform.temperature_unit == 'C')
temp = value;
else
temp = ((value * 1.8) + 32);
//Set the appropriate temperature unit based on the mode
switch (that.device.attributes.thermostatMode) {
case "cool":
that.platform.api.runCommand(callback, that.deviceid, "setCoolingSetpoint", {
value1: temp
});
that.device.attributes.coolingSetpoint = temp;
break;
case "emergency heat":
case "heat":
that.platform.api.runCommand(callback, that.deviceid, "setHeatingSetpoint", {
value1: temp
});
that.device.attributes.heatingSetpoint = temp;
break;
default: //This should only refer to auto
// Choose closest target as single target
var high = that.device.attributes.coolingSetpoint;
var low = that.device.attributes.heatingSetpoint;
var cur = that.device.attributes.temperature;
var isHighTemp = Math.abs(high - cur) < Math.abs(cur - low);
if (isHighTemp) {
that.platform.api.runCommand(callback, that.deviceid, "setCoolingSetpoint", { value1: temp });
} else {
that.platform.api.runCommand(null, that.deviceid, "setHeatingSetpoint", { value1: temp });
}
break;
}
});
that.platform.addAttributeUsage("thermostatMode", this.deviceid, thisCharacteristic);
that.platform.addAttributeUsage("coolingSetpoint", this.deviceid, thisCharacteristic);
that.platform.addAttributeUsage("heatingSetpoint", this.deviceid, thisCharacteristic);
that.platform.addAttributeUsage("temperature", this.deviceid, thisCharacteristic);
thisCharacteristic = this.getaddService(Service.Thermostat).getCharacteristic(Characteristic.TemperatureDisplayUnits)
thisCharacteristic.on('get', function(callback) {
if (platform.temperature_unit == "C")
callback(null, Characteristic.TemperatureDisplayUnits.CELSIUS);
else
callback(null, Characteristic.TemperatureDisplayUnits.FAHRENHEIT);
});
//that.platform.addAttributeUsage("temperature_unit", "platform", thisCharacteristic);
thisCharacteristic = this.getaddService(Service.Thermostat).getCharacteristic(Characteristic.HeatingThresholdTemperature)
thisCharacteristic.on('get', function(callback) {
if (that.platform.temperature_unit == 'C')
callback(null, Math.round(that.device.attributes.heatingSetpoint*10)/10);
else
callback(null, Math.round(((that.device.attributes.heatingSetpoint - 32) / 1.8)*10)/10);
})
thisCharacteristic.on('set', function(value, callback) {
//Convert the Celsius value to the appropriate unit for Smartthings
var temp = value;
if (that.platform.temperature_unit == 'C')
temp = value;
else
temp = ((value * 1.8) + 32);
that.platform.api.runCommand(callback, that.deviceid, "setHeatingSetpoint", {
value1: temp
});
that.device.attributes.heatingSetpoint = temp;
});
that.platform.addAttributeUsage("heatingSetpoint", this.deviceid, thisCharacteristic);
thisCharacteristic = this.getaddService(Service.Thermostat).getCharacteristic(Characteristic.CoolingThresholdTemperature)
thisCharacteristic.on('get', function(callback) {
if (that.platform.temperature_unit == 'C')
callback(null, Math.round((that.device.attributes.coolingSetpoint*10))/10);
else
callback(null, Math.round(((that.device.attributes.coolingSetpoint - 32) / 1.8)*10)/10);
});
thisCharacteristic.on('set', function(value, callback) {
//Convert the Celsius value to the appropriate unit for Smartthings
var temp = value;
if (that.platform.temperature_unit == 'C')
temp = value;
else
temp = ((value * 1.8) + 32);
that.platform.api.runCommand(callback, that.deviceid, "setCoolingSetpoint", {
value1: temp
});
that.device.attributes.coolingSetpoint = temp;
});
that.platform.addAttributeUsage("coolingSetpoint", this.deviceid, thisCharacteristic);
}
this.loadData(device, this);
}
function loadData(data, myObject) {
var that = this;
if (myObject !== undefined) that = myObject;
if (data !== undefined) {
this.device = data;
for (var i = 0; i < that.services.length; i++) {
for (var j = 0; j < that.services[i].characteristics.length; j++) {
that.services[i].characteristics[j].getValue();
}
}
} else {
this.log.debug("Fetching Device Data")
this.platform.api.getDevice(this.deviceid, function(data) {
if (data === undefined) return;
this.device = data;
for (var i = 0; i < that.services.length; i++) {
for (var j = 0; j < that.services[i].characteristics.length; j++) {
that.services[i].characteristics[j].getValue();
}
}
});
}
}
function getServices() {
return this.services;
}
@justin.bennett, I did a hubitat bridge similar to the homebridge. The problem is I don’t have homebridge so there’s no way I can do any testing for it. I wouldn’t really know where to even start without having Homebridge and knowing how it works.
So, I am almost there… just need a little help… Maybe @chuck.schwer, @bravenel, @mike.maxwell, or @patrick can take a quick look, please? It appears that Hubitat is producing different results versus SmartThings when sending a HTTP request to my raspberry pi.
In SmartThings, the “JSON Complete API” SmartApp sends a hubaction to the HomeBridge server any time a device is updated. Here is an example from SmartThings that show everything working correctly.
(from ST Live Logging)
result = GET /update HTTP/1.1
Accept: */*
User-Agent: Linux UPnP/1.0 SmartThings
HOST: 192.168.1.145:8000
change_device: 29a0e0db-a238-4e3b-9f17-873b8510abc4
change_attribute: temperature
change_value: 65
change_date: Thu Feb 15 15:16:31 UTC 2018
And you can see the homebridge-smartthings plug-in on my raspberry pi receives the following:
[2018-2-15 10:26:26] [SmartThings] Direct Update request.url /update
[2018-2-15 10:26:26] [SmartThings] Direct Update request.headers["change_device"] 29a0e0db-a238-4e3b-9f17-873b8510abc4
[2018-2-15 10:26:26] [SmartThings] Direct Update request.headers["change_attribute"] temperature
[2018-2-15 10:26:26] [SmartThings] Direct Update request.headers["change_value"] 66
[2018-2-15 10:26:26] [SmartThings] Direct Update request.headers["change_date"] Thu Feb 15 15:26:25 UTC 2018
However, the same code running in Hubitat produces the following
(from Hubitat Live Logging)
result = hubitat.device.HubAction(LAN, GET, /update, [ACCEPT:*/*, USER-AGENT:Linux UPnP/1.0 Hubitat, change_device:457, change_attribute:switch, change_date:Thu Feb 15 10:33:44 EST 2018, CONTENT-TYPE:text/xml; charset="utf-8", HOST:192.168.1.145:8000, change_value:off], null, 192.168.1.145:8000, null, null, null)
resulting in the following in the homebridge-smartthings plug-in on my raspberry pi
[2018-2-15 10:27:06] [SmartThings] Direct Update request.url /update
[2018-2-15 10:27:06] [SmartThings] Direct Update request.headers["change_device"] undefined
[2018-2-15 10:27:06] [SmartThings] Direct Update request.headers["change_attribute"] undefined
[2018-2-15 10:27:06] [SmartThings] Direct Update request.headers["change_value"] undefined
[2018-2-15 10:27:06] [SmartThings] Direct Update request.headers["change_date"] undefined
So, the NodeJS server is able to correct parse the .url field, but not the .headers field
Here is the snippet of groovy code which is producing these hub actions
//Send Using the Direct Mechanism
def deviceData = [device: evt.deviceId, attribute: evt.name, value: evt.value, date: evt.date]
//How do I control the port?!?
log.debug "Sending Update to ${state.directIP}:${state.directPort}"
def result = new hubitat.device.HubAction(
method: "GET",
path: "/update",
headers: [
HOST: "${state.directIP}:${state.directPort}",
change_device: evt.deviceId,
change_attribute: evt.name,
change_value: evt.value,
change_date: evt.date ]
)
log.debug "changeHandler: result = ${result}"
sendHubCommand(result)
and here is the NodeJS code that is trying to parse the incoming request
From a very high level, homebridge is a neat piece of software that sits in a docker container on a server or on a raspberry pi device (costs $30). Home bridge allows a user to build a "hub" that can be seen by iOS devices as a home kit solution.
You can manually add some LAN devices to the setup (such as Netatmo) and they will be seen as home kit devices on an iOS device and can be controlled through Siri.
Why Hubitat is so important to the equation is because the Hubitat bridge can house, detect and automate z-wave and zigbee devices so your GE Switches/Bulbs and other less expensive home automation devices can be automated through Hubitat (and Rule Machine), but you can enjoy the UI and Siri input.
Check out this site. I probably have the programming experience of a novice compared to others on this site and it took me about 90 minutes to setup a Raspberry Pi and configure homebridge to work with ST.
OK - It has taken me a while to hack together a solution, but I seem to have a working Hubitat-to-Homebridge-to-HomeKit solution. I finally was able to get the direct updates to work by passing a “body:” with the event data, instead of trying to make custom headers: work. I am really not sure what the “correct way” is to do this, but I am trying to learn.
It is not probably the prettiest code, and it is really not ready for release to most users due to the significant number of manual steps to get it working. Currently, it cannot coexist on the same Raspberry PI as the normal homebridge-smartthings plug-in.
Fortunately, the original author (@pdlovelace) of homebridge-smartthings has a Hubitat Elevation hub on order. Looks like he joined the Hubitat community a few hours ago! Welcome @pdlovelace!
I will be happy to work with @pdlovelace to get him up to speed on my changes that were necessary to get things to work. Hopefully he’ll take up the challenge to create a npm installable version of ‘homebridge-hubitat’ for the community.
Here are my current edited files from homebridge-smartthings. @pdlovelace, let me know if you have any questions. I am looking forward to having a more industrialized version! I don’t really know how to thoroughly test everything, but I do know basic devices seem to be working.
json-complete-api.groovy (installed as a Hubitat App with OAUTH enabled)
/**
* JSON Complete API
*
* Copyright 2017 Paul Lovelace
*
* Modifications for Hubitat (in progress) - Dan Ogorchock 2/15/2018
*/
definition(
name: "JSON Complete API",
namespace: "pdlove",
author: "Paul Lovelace",
description: "API for JSON with complete set of devices",
category: "SmartThings Labs",
iconUrl: "https://raw.githubusercontent.com/pdlove/homebridge-smartthings/master/smartapps/JSON%401.png",
iconX2Url: "https://raw.githubusercontent.com/pdlove/homebridge-smartthings/master/smartapps/JSON%402.png",
iconX3Url: "https://raw.githubusercontent.com/pdlove/homebridge-smartthings/master/smartapps/JSON%403.png",
oauth: true)
preferences {
page(name: "copyConfig")
}
//When adding device groups, need to add here
def copyConfig() {
if (!state.accessToken) {
createAccessToken()
}
dynamicPage(name: "copyConfig", title: "Configure Devices", install:true, uninstall:true) {
section("Select devices to include in the /devices API call") {
paragraph "Version 0.5.5"
input "deviceList", "capability.refresh", title: "Most Devices", multiple: true, required: false
input "sensorList", "capability.sensor", title: "Sensor Devices", multiple: true, required: false
input "switchList", "capability.switch", title: "All Switches", multiple: true, required: false
//paragraph "Devices Selected: ${deviceList ? deviceList?.size() : 0}\nSensors Selected: ${sensorList ? sensorList?.size() : 0}\nSwitches Selected: ${switchList ? switchList?.size() : 0}"
}
section("Configure Pubnub") {
input "pubnubSubscribeKey", "text", title: "PubNub Subscription Key", multiple: false, required: false
input "pubnubPublishKey", "text", title: "PubNub Publish Key", multiple: false, required: false
input "subChannel", "text", title: "Channel (Can be anything)", multiple: false, required: false
}
section() {
paragraph "View this SmartApp's configuration to use it in other places."
//Original SmartThings cloud endpoint
//href url:"${apiServerUrl("/api/smartapps/installations/${app.id}/config?access_token=${state.accessToken}")}", style:"embedded", required:false, title:"Config", description:"Tap, select, copy, then click \"Done\""
//Hubitat cloud endpoint
//href url:"${getApiServerUrl()}/${hubUID}/apps/${app.id}/config?access_token=${state.accessToken}", style:"embedded", required:false, title:"Config", description:"Tap, select, copy, then click \"Done\""
//Hubitat local endpoint
href url:fullLocalApiServerUrl("config") + "?access_token=${state.accessToken}", style:"embedded", required:false, title:"Config", description:"Tap, select, copy, then click \"Done\""
}
section() {
paragraph "View the JSON generated from the installed devices."
//Original SmartThings cloud endpoint
//href url:"${apiServerUrl("/api/smartapps/installations/${app.id}/devices?access_token=${state.accessToken}")}", style:"embedded", required:false, title:"Device Results", description:"View accessories JSON"
//Hubitat cloud endpoint
//href url:"${getApiServerUrl()}/${hubUID}/apps/${app.id}/devices?access_token=${state.accessToken}", style:"embedded", required:false, title:"Device Results", description:"View accessories JSON"
//Hubitat local endpoint
href url:fullLocalApiServerUrl("devices") + "?access_token=${state.accessToken}", style:"embedded", required:false, title:"Device Results", description:"View accessories JSON"
}
section() {
paragraph "Enter the name you would like shown in the smart app list"
label title:"SmartApp Label (optional)", required: false
}
}
}
def renderDevices() {
def deviceData = []
deviceList.each {
try {
deviceData << [name: it.displayName,
basename: it.name,
deviceid: it.id,
status: it.status,
manufacturerName: it.getManufacturerName(),
modelName: it.getModelName(),
lastTime: it.getLastActivity(),
capabilities: deviceCapabilityList(it),
commands: deviceCommandList(it),
attributes: deviceAttributeList(it)
]
} catch (e) {
log.error("Error Occurred Parsing Device "+it.displayName+", Error " + e)
}
}
sensorList.each {
try {
deviceData << [name: it.displayName,
basename: it.name,
deviceid: it.id,
status: it.status,
manufacturerName: it.getManufacturerName(),
modelName: it.getModelName(),
lastTime: it.getLastActivity(),
capabilities: deviceCapabilityList(it),
commands: deviceCommandList(it),
attributes: deviceAttributeList(it)
]
} catch (e) {
log.error("Error Occurred Parsing Device "+it.displayName+", Error " + e)
}
}
switchList.each {
try {
deviceData << [name: it.displayName,
basename: it.name,
deviceid: it.id,
status: it.status,
manufacturerName: it.getManufacturerName(),
modelName: it.getModelName(),
lastTime: it.getLastActivity(),
capabilities: deviceCapabilityList(it),
commands: deviceCommandList(it),
attributes: deviceAttributeList(it)
]
} catch (e) {
log.error("Error Occurred Parsing Device "+it.displayName+", Error " + e)
}
}
return deviceData
}
def findDevice(paramid) {
def device = deviceList.find { it.id == paramid }
if (device) return device
device = sensorList.find { it.id == paramid }
if (device) return device
device = switchList.find { it.id == paramid }
return device
}
//No more individual device group definitions after here.
def installed() {
log.debug "Installed with settings: ${settings}"
initialize()
}
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
initialize()
}
def initialize() {
if(!state.accessToken) {
createAccessToken()
}
registerAll()
state.subscriptionRenewed = 0
subscribe(location, null, HubResponseEvent, [filterEvents:false])
log.debug "0.5.5"
}
def authError() {
[error: "Permission denied"]
}
def renderConfig() {
def configJson = new groovy.json.JsonOutput().toJson([
description: "JSON API",
platforms: [
[
platform: "SmartThings",
name: "SmartThings",
//
//Original SmartThings cloud endpoint
//app_url: apiServerUrl("/api/smartapps/installations/"),
//
//Hubitat cloud endpoint
//app_url: "${getApiServerUrl()}/${hubUID}/apps/",
//
//Hubitat local endpoint
app_url: getLocalApiServerUrl() + "/",
app_id: app.id,
access_token: state.accessToken
]
],
])
def configString = new groovy.json.JsonOutput().prettyPrint(configJson)
render contentType: "text/plain", data: configString
}
def renderLocation() {
[
latitude: location.latitude,
longitude: location.longitude,
mode: location.mode,
name: location.name,
temperature_scale: location.temperatureScale,
zip_code: location.zipCode,
hubIP: location.hubs[0].getDataValue("localIP"),
//hubIP: location.hubs[0].localIP,
smartapp_version: '0.5.5'
]
}
def CommandReply(statusOut, messageOut) {
def replyData =
[
status: statusOut,
message: messageOut
]
def replyJson = new groovy.json.JsonOutput().toJson(replyData)
render contentType: "application/json", data: replyJson
}
def deviceCommand() {
log.info("Command Request")
def device = findDevice(params.id)
def command = params.command
if (!device) {
log.error("Device Not Found")
CommandReply("Failure", "Device Not Found")
} else if (!device.hasCommand(command)) {
log.error("Device "+device.displayName+" does not have the command "+command)
CommandReply("Failure", "Device "+device.displayName+" does not have the command "+command)
} else {
def value1 = request.JSON?.value1
def value2 = request.JSON?.value2
try {
if (value2) {
device."$command"(value1,value2)
} else if (value1) {
device."$command"(value1)
} else {
device."$command"()
}
log.info("Command Successful for Device "+device.displayName+", Command "+command)
CommandReply("Success", "Device "+device.displayName+", Command "+command)
} catch (e) {
log.error("Error Occurred For Device "+device.displayName+", Command "+command)
CommandReply("Failure", "Error Occurred For Device "+device.displayName+", Command "+command)
}
}
}
def deviceAttribute() {
def device = findDevice(params.id)
def attribute = params.attribute
if (!device) {
httpError(404, "Device not found")
} else {
def currentValue = device.currentValue(attribute)
[currentValue: currentValue]
}
}
def deviceQuery() {
def device = findDevice(params.id)
if (!device) {
device = null
httpError(404, "Device not found")
}
if (result) {
def jsonData =
[
name: device.displayName,
deviceid: device.id,
capabilities: deviceCapabilityList(device),
commands: deviceCommandList(device),
attributes: deviceAttributeList(device)
]
def resultJson = new groovy.json.JsonOutput().toJson(jsonData)
render contentType: "application/json", data: resultJson
}
}
def deviceCapabilityList(device) {
def i=0
device.capabilities.collectEntries { capability->
[
(capability.name):1
]
}
}
def deviceCommandList(device) {
def i=0
device.supportedCommands.collectEntries { command->
[
(command.name): (command.arguments)
]
}
}
def deviceAttributeList(device) {
device.supportedAttributes.collectEntries { attribute->
try {
[
(attribute.name): device.currentValue(attribute.name)
]
} catch(e) {
[
(attribute.name): null
]
}
}
}
def getAllData() {
//Since we're about to send all of the data, we'll count this as a subscription renewal and clear out pending changes.
state.subscriptionRenewed = now()
state.devchanges = []
def deviceData =
[ location: renderLocation(),
deviceList: renderDevices() ]
def deviceJson = new groovy.json.JsonOutput().toJson(deviceData)
render contentType: "application/json", data: deviceJson
}
def startSubscription() {
//This simply registers the subscription.
state.subscriptionRenewed = now()
def deviceJson = new groovy.json.JsonOutput().toJson([status: "Success"])
render contentType: "application/json", data: deviceJson
}
def endSubscription() {
//Because it takes too long to register for an api command, we don't actually unregister.
//We simply blank the devchanges and change the subscription renewal to two hours ago.
state.devchanges = []
state.subscriptionRenewed = 0
def deviceJson = new groovy.json.JsonOutput().toJson([status: "Success"])
render contentType: "application/json", data: deviceJson
}
def registerAll() {
//This has to be done at startup because it takes too long for a normal command.
log.debug "Registering All Events"
state.devchanges = []
registerChangeHandler(deviceList)
registerChangeHandler(sensorList)
registerChangeHandler(switchList)
}
def registerChangeHandler(myList) {
myList.each { myDevice ->
def theAtts = myDevice.supportedAttributes
theAtts.each {att ->
subscribe(myDevice, att.name, changeHandler)
log.debug "Registering ${myDevice.displayName}.${att.name}"
}
}
}
def changeHandler(evt) {
//Send to Pubnub if we need to.
if (pubnubPublishKey!=null) {
def deviceData = [device: evt.deviceId, attribute: evt.name, value: evt.value, date: evt.date]
def changeJson = new groovy.json.JsonOutput().toJson(deviceData)
def changeData = URLEncoder.encode(changeJson)
def uri = "http://pubsub.pubnub.com/publish/${pubnubPublishKey}/${pubnubSubscribeKey}/0/${subChannel}/0/${changeData}"
log.debug "${uri}"
httpGet(uri)
}
if (state.directIP!="") {
//Send Using the Direct Mechanism
def deviceData = [device: evt.deviceId, attribute: evt.name, value: evt.value, date: evt.date]
def json = new groovy.json.JsonBuilder(deviceData)
//log.debug "JSON = " + json.toString()
//How do I control the port?!?
log.debug "Sending Update to ${state.directIP}:${state.directPort}"
def result = new hubitat.device.HubAction(
method: "POST",
path: "/update",
headers: [ HOST: "${state.directIP}:${state.directPort}" ],
//changed the event data from headers: to body:
body: json.toString()
)
log.debug "changeHandler: result = ${result}"
sendHubCommand(result)
}
//Only add to the state's devchanges if the endpoint has renewed in the last 10 minutes.
if (state.subscriptionRenewed>(now()-(1000*60*10))) {
// if (evt.isStateChange()) { //Hubitat function name change
if (evt.getIsStateChange()) {
state.devchanges << [device: evt.deviceId, attribute: evt.name, value: evt.value, date: evt.date]
}
} else if (state.subscriptionRenewed>0) { //Otherwise, clear it
log.debug "Endpoint Subscription Expired. No longer storing changes for devices."
state.devchanges=[]
state.subscriptionRenewed=0
}
}
def getChangeEvents() {
//Store the changes so we can swap it out very quickly and eliminate the possibility of losing any.
//This is mainly to make this thread safe because I'm willing to bet that a change event can fire
//while generating/sending the JSON.
def oldchanges = state.devchanges
state.devchanges=[]
state.subscriptionRenewed = now()
if (oldchanges.size()==0) {
def deviceJson = new groovy.json.JsonOutput().toJson([status: "None"])
render contentType: "application/json", data: deviceJson
} else {
def changeJson = new groovy.json.JsonOutput().toJson([status: "Success", attributes:oldchanges])
render contentType: "application/json", data: changeJson
}
}
def enableDirectUpdates() {
log.debug("Command Request")
state.directIP = params.ip
state.directPort = params.port
log.debug("Trying ${state.directIP}:${state.directPort}")
def result = new hubitat.device.HubAction(
method: "GET",
path: "/initial",
headers: [
HOST: "${state.directIP}:${state.directPort}"
],
query: deviceData
)
log.debug "enableDirectUpdates: result = ${result}"
sendHubCommand(result)
}
def HubResponseEvent(evt) {
log.debug(evt.description)
}
def locationHandler(evt) {
def description = evt.description
def hub = evt?.hubId
log.debug "cp desc: " + description
if (description.count(",") > 4)
{
def bodyString = new String(description.split(',')[5].split(":")[1].decodeBase64())
log.debug(bodyString)
}
}
def getSubscriptionService() {
def replyData =
[
pubnub_publishkey: pubnubPublishKey,
pubnub_subscribekey: pubnubSubscribeKey,
pubnub_channel: subChannel
]
def replyJson = new groovy.json.JsonOutput().toJson(replyData)
render contentType: "application/json", data: replyJson
}
mappings {
//commented out due to Hubitat error (don't think the check is really necessary...)
/* if (!params.access_token || (params.access_token && params.access_token != state.accessToken)) {
path("/devices") { action: [GET: "authError"] }
path("/config") { action: [GET: "authError"] }
path("/location") { action: [GET: "authError"] }
path("/:id/command/:command") { action: [POST: "authError"] }
path("/:id/query") { action: [GET: "authError"] }
path("/:id/attribute/:attribute") { action: [GET: "authError"] }
path("/subscribe") { action: [GET: "authError"] }
path("/getUpdates") { action: [GET: "authError"] }
path("/unsubscribe") { action: [GET: "authError"] }
path("/startDirect/:ip/:port") { action: [GET: "authError"] }
path("/getSubcriptionService") { action: [GET: "authError"] }
} else {
*/
path("/devices") { action: [GET: "getAllData"] }
path("/config") { action: [GET: "renderConfig"] }
path("/location") { action: [GET: "renderLocation"] }
path("/:id/command/:command") { action: [POST: "deviceCommand"] }
path("/:id/query") { action: [GET: "deviceQuery"] }
path("/:id/attribute/:attribute") { action: [GET: "deviceAttribute"] }
path("/subscribe") { action: [GET: "startSubscription"] }
path("/getUpdates") { action: [GET: "getChangeEvents"] }
path("/unsubscribe") { action: [GET: "endSubscription"] }
path("/startDirect/:ip/:port") { action: [GET: "enableDirectUpdates"] }
path("/getSubcriptionService") { action: [GET: "getSubscriptionService"] }
// }
}
homebridge-smatthings plug-in files (installed on the raspberrypi)
index.js
var PubNub = require('pubnub')
var smartthings = require('./lib/smartthingsapi');
var http = require('http')
var os = require('os');
var Service, Characteristic, Accessory, uuid, EnergyCharacteristics;
var SmartThingsAccessory;
module.exports = function (homebridge) {
Service = homebridge.hap.Service;
Characteristic = homebridge.hap.Characteristic;
Accessory = homebridge.hap.Accessory;
uuid = homebridge.hap.uuid;
SmartThingsAccessory = require('./accessories/smartthings')(Accessory, Service, Characteristic, uuid);
homebridge.registerPlatform("homebridge-smartthings", "SmartThings", SmartThingsPlatform);
};
function SmartThingsPlatform(log, config) {
// Load Wink Authentication From Config File
this.app_url = config["app_url"];
this.app_id = config["app_id"];
this.access_token = config["access_token"];
//This is how often it does a full refresh
this.polling_seconds = config["polling_seconds"];
if (!this.polling_seconds) this.polling_seconds = 3600; //Get a full refresh every hour.
//This is how often it polls for subscription data.
this.update_method = config["update_method"];
if (!this.update_method) this.update_method='direct';
this.update_seconds = config["update_seconds"];
if (!this.update_seconds) this.update_seconds = 30; //30 seconds is the new default
if (this.update_method==='api' && this.update_seconds<30)
that.log("The setting for update_seconds is lower than the SmartThings recommended value. Please switch to direct or PubNub using a free subscription for real-time updates.");
this.direct_port = config["direct_port"];
if (!this.direct_port) this.direct_port = 8000;
this.direct_ip = config["direct_ip"];
if (!this.direct_ip) this.direct_ip = smartthing_getIP();
this.api = smartthings;
this.log = log;
this.deviceLookup = {};
this.firstpoll = true;
this.attributeLookup = {}
}
SmartThingsPlatform.prototype = {
reloadData: function (callback) {
var that = this;
var foundAccessories = [];
this.log.debug("Refreshing All Device Data");
smartthings.getDevices(function (myList) {
that.log.debug("Received All Device Data");
// success
if (myList && myList.deviceList && myList.deviceList instanceof Array) {
var populateDevices = function (devices) {
for (var i = 0; i < devices.length; i++) {
var device = devices[i];
var accessory = undefined;
if (that.deviceLookup[device.deviceid]) {
accessory = that.deviceLookup[device.deviceid];
accessory.loadData(devices[i]);
} else {
accessory = new SmartThingsAccessory(that, device);
if (accessory != undefined) {
if ((accessory.services.length <= 1) || (accessory.deviceGroup == "unknown")) {
if (that.firstpoll) that.log("Device Skipped - Group " + accessory.deviceGroup + ", Name " + accessory.name + ", ID " + accessory.deviceid + ", JSON: " + JSON.stringify(device));
} else {
that.log("Device Added - Group " + accessory.deviceGroup + ", Name " + accessory.name + ", ID " + accessory.deviceid)//+", JSON: "+ JSON.stringify(device));
that.deviceLookup[accessory.deviceid] = accessory;
foundAccessories.push(accessory);
}
}
}
}
}
if (myList && myList.location) {
that.temperature_unit = myList.location.temperature_scale;
}
populateDevices(myList.deviceList);
} else if ((!myList) || (!myList.error)) {
that.log("Invalid Response from API call");
} else if (myList.error) {
that.log("Error received type " + myList.type + ' - ' + myList.message);
} else {
that.log("Invalid Response from API call");
}
if (callback)
callback(foundAccessories)
that.firstpoll = false;
});
},
accessories: function (callback) {
this.log("Fetching Smart Things devices.");
var that = this;
var foundAccessories = [];
this.deviceLookup = [];
this.unknownCapabilities = [];
this.knownCapabilities = ["Switch", "ColorControl", "Battery", "Polling", "Lock", "Refresh", "LockCodes", "Sensor", "Actuator",
"Configuration", "SwitchLevel", "TemperatureMeasurement", "MotionSensor", "ColorTemperature",
"ContactSensor", "ThreeAxis", "AccelerationSensor", "Momentary", "DoorControl", "GarageDoorControl",
"RelativeHumidityMeasurement", "PresenceSensor", "Thermostat", "EnergyMeter", "PowerMeter",
"ThermostatCoolingSetpoint", "ThermostatMode", "ThermostatFanMode", "ThermostatOperatingState",
"ThermostatHeatingSetpoint", "ThermostatSetpoint", "Indicator"];
this.temperature_unit = 'F';
smartthings.init(this.app_url, this.app_id, this.access_token);
this.reloadData(function (foundAccessories) {
that.log("Unknown Capabilities: " + JSON.stringify(that.unknownCapabilities));
callback(foundAccessories);
setInterval(that.reloadData.bind(that), that.polling_seconds * 1000);
//Initialize Update Mechanism for realtime-ish updates.
if (that.update_method==='api') //Legacy API method.
setInterval(that.doIncrementalUpdate.bind(that), that.update_seconds * 1000);
else if (that.update_method==='pubnub') { //Uses user's PubNub account
that.api.getSubscriptionService(function(data) {
pubnub = new PubNub({ subscribeKey : data.pubnub_subscribekey });
pubnub.addListener({
status: function(statusEvent) { if (statusEvent.category==='PNReconnectedCategory') that.reloadData(null); },
message: function(message) { that.processFieldUpdate(message.message, that); } });
pubnub.subscribe({ channels: [ that.pubnub_channel ] });
});
}
else if (that.update_method=='direct') { //The Hub sends updates to this module using http
smartthings_SetupHTTPServer(that);
smartthings.startDirect(null,that.direct_ip, that.direct_port);
}
});
},
addAttributeUsage: function(attribute, deviceid, mycharacteristic) {
if (!this.attributeLookup[attribute])
this.attributeLookup[attribute] = {};
if (!this.attributeLookup[attribute][deviceid])
this.attributeLookup[attribute][deviceid] = [];
this.attributeLookup[attribute][deviceid].push(mycharacteristic);
},
doIncrementalUpdate: function() {
var that=this;
smartthings.getUpdates(function(data) { that.processIncrementalUpdate(data, that)});
},
processIncrementalUpdate: function(data, that) {
if (data && data.attributes && data.attributes instanceof Array) {
for (var i = 0; i < data.attributes.length; i++) {
that.processFieldUpdate(data.attributes[i], that);
}
}
},
processFieldUpdate: function(attributeSet, that) {
//that.log("Processing Update");
if (!((that.attributeLookup[attributeSet.attribute]) && (that.attributeLookup[attributeSet.attribute][attributeSet.device]))) return;
var myUsage = that.attributeLookup[attributeSet.attribute][attributeSet.device];
if (myUsage instanceof Array) {
for (var j = 0; j < myUsage.length; j++) {
var accessory = that.deviceLookup[attributeSet.device];
if (accessory) {
accessory.device.attributes[attributeSet.attribute] = attributeSet.value;
myUsage[j].getValue();
}
}
}
}
};
function smartthing_getIP() {
var myIP = '';
var ifaces = os.networkInterfaces();
Object.keys(ifaces).forEach(function (ifname) {
var alias = 0;
ifaces[ifname].forEach(function (iface) {
if ('IPv4' !== iface.family || iface.internal !== false) {
// skip over internal (i.e. 127.0.0.1) and non-ipv4 addresses
return;
}
myIP = iface.address;
});
});
return myIP;
}
function smartthings_SetupHTTPServer(mySmartThings) {
//Get the IP address that we will send to the SmartApp. This can be overridden in the config file.
//Start the HTTP Server
const server = http.createServer(function(request,response) {
smartthings_HandleHTTPResponse(request, response, mySmartThings)});
server.listen(mySmartThings.direct_port, (err) => {
if (err) {
mySmartThings.log('something bad happened', err);
return '';
}
mySmartThings.log(`Direct Connect Is Listening On ${mySmartThings.direct_ip}:${mySmartThings.direct_port}`);
})
return 'good';
}
function smartthings_HandleHTTPResponse(request, response, mySmartThings) {
if (request.url == '/update') {
var jsonString = '';
request.on('data', function (data) {
jsonString += data;
});
request.on('end', function () {
var tmp = JSON.parse(jsonString.trim());
//console.log(tmp);
var newChange = { device: tmp.device,
attribute: tmp.attribute,
value: tmp.value,
date: tmp.date};
mySmartThings.processFieldUpdate(newChange, mySmartThings);
//console.log(`Direct Update tmp.device ${tmp.device}`);
});
}
else {
if (request.url=='/initial')
mySmartThings.log("SmartThings Hub Communication Established");
}
response.end('OK');
}
smartthingsapi.js
var http = require('http');
var url = require('url');
var app_host;
var app_port;
var app_path;
var access_token;
function _http(data, callback) {
//console.log("Calling Smartthings");
var options = {
hostname: app_host,
port: app_port,
path: app_path + data.path + "?access_token=" + access_token,
method: data.method,
headers: {}
};
var that = this;
if (data.data) {
data.data = JSON.stringify(data.data);
options.headers['Content-Length'] = Buffer.byteLength(data.data);
options.headers['Content-Type'] = "application/json";
}
var str = '';
var req = http.request(options, function (response) {
response.on('data', function (chunk) {
str += chunk;
});
response.on('end', function () {
if (data.debug) console.log("response in http:", str);
try {
str = JSON.parse(str);
} catch (e) {
if (data.debug) {
console.log(e.stack);
console.log("raw message", str);
}
str = undefined;
}
if (callback) { callback(str); callback=undefined; };
});
});
if (data.data) {
req.write(data.data);
}
req.end();
req.on('error', function (e) {
console.log("error at req: ", e.message);
if (callback) { callback(); callback=undefined; };
});
}
function POST(data, callback) {
data.method = "POST";
_http(data, callback);
}
function PUT(data, callback) {
data.method = "PUT";
_http(data, callback);
}
function GET(data, callback) {
data.method = "GET";
_http(data, callback);
}
function DELETE(data, callback) {
data.method = "DELETE";
_http(data, callback);
}
var smartthings = {
init: function (inURL, inAppID, inAccess_Token) {
var appURL = url.parse(inURL);
app_host = appURL.hostname || "graph.api.smartthings.com";
app_port = appURL.port || "80";
app_path = (appURL.path || "/api/smartapps/installations/") + inAppID + "/";
access_token = inAccess_Token;
},
getDevices: function (callback) {
GET({ debug: true, path: 'devices' }, function (data) {
if (callback) { callback(data); callback=undefined; };
})
},
getDevice: function (deviceid, callback) {
GET({ debug: true, path: deviceid + '/query' }, function (data) {
if (data) {
if (callback) { callback(data); callback=undefined; };
} else {
if (callback) { callback(); callback=undefined; };;
}
})
},
getUpdates: function (callback) {
GET({ debug: true, path: 'getUpdates' }, function (data) {
if (callback) { callback(data); callback=undefined; };;
})
},
runCommand: function (callback, deviceid, command, values) {
POST({ debug: true, path: deviceid + '/command/' + command, data: values }, function (data) {
if (callback) { callback(); callback=undefined; };;
})
},
startDirect: function (callback, myIP, myPort) {
GET({ debug: true, path: 'startDirect/' + myIP + '/' + myPort }, function (data) {
if (callback) { callback(); callback=undefined; };;
})
},
getSubscriptionService: function (callback) {
GET({ debug: true, path: 'getSubcriptionService' }, function (data) {
if (callback) { callback(data); callback=undefined; };;
})
}
}
module.exports = smartthings;
So installed a docker version of homebridge and installed the JSON app into hubitat.
Edited the config.json and got to the following:
I think this is due to the fact that I did not edit the index.js and smartthingsapi.js files. Reason being, I can't find them. Where are they to edit?__
I just used nano to edit it. Are you still having trouble finding the files? I had to google how to use the linux “find” command to find them. I do not recall their locations off the top of my head.
If you want to simply copy the pre-edited files I posted above from a windows pc to your raspberry pi (after figuring out where these files are located, of course), you might want to use WinSCP via a SSH connection. You’ll need to enable SSH in the raspberry pi first, of course.
Hopefully Paul (@pdlovelace) is making some progress on a Hubitat specific homebridge plug-in.
It is in the root of this GitHub repository... I did not use Docker. I just followed the installation instructions and then started hacking on the files until I got something to work. The files I posted above are the ones that I hacked up!
I guess that I made the assumption that anyone trying this had already been through the exercise of installing Homebridge + the Homebridge-SmartThings plug-in already. That was my starting point. Probably a good idea to get it working with SmartThings first to know the basics are in place before starting to edit things.