Skip to content

Instantly share code, notes, and snippets.

@mgranberryto
Created April 26, 2017 20:49
Show Gist options
  • Save mgranberryto/dbabfc2ac118ba5841db62d425fb70b9 to your computer and use it in GitHub Desktop.
Save mgranberryto/dbabfc2ac118ba5841db62d425fb70b9 to your computer and use it in GitHub Desktop.
NS-Connect
/**
* NightscoutPump
*
* Copyright 2017 Matthias Granberry
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
*/
import java.text.SimpleDateFormat
metadata {
definition (name: "Nightscout Uploader Device", namespace: "mgranberry", author: "Matthias Granberry") {
capability "Battery"
capability "Health Check"
capability "Refresh"
capability "Sensor"
capability "Temperature Measurement"
attribute "clock", "string"
attribute "glucose", "number"
}
simulator {
// TODO: define status and reply messages here
}
tiles(scale:2) {
valueTile("battery", "device.battery", decoration: "flat", width: 2, height: 2){
state "battery", label:'${currentValue}%\nBattery', unit:"%"
}
valueTile("clock", "device.clock", decoration: "flat", width: 2, height: 2){
state "clock", label:'Updated\n\n${currentValue}', unit:""
}
valueTile("glucose", "device.glucose", decoration:"flat", width: 2, height: 2) {
state "glucose", label: '${currentValue}\nGlucose', unit: "mg/dL"
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
}
}
}
def parse(org.codehaus.groovy.grails.web.json.JSONObject data) {
log.debug "Parsing JSON $data"
def results = []
if ('sgv' in data && data['sgv'] >= 11) {
results << createEvent("name": "glucose", "value": "${data['sgv']}", unit: "mg/dL")
results << createEvent("name": "temperature", "value": "${data['sgv']}", unit: "F")
}
if ('uploaderBattery' in data)
results << createEvent("name": "battery", "value": data['uploaderBattery'])
if ('uploader' in data)
results << createEvent("name": "battery", "value": data['uploader']['battery'])
if ('created_at' in data)
results << createEvent("name": "clock", "value": parseTime(data['created_at']))
return results
}
def parseTime(time) {
def tz = TimeZone.getTimeZone(location.timeZone.ID)
def t
if (time.contains('.'))
t = Date.parse("yyyy-MM-dd'T'HH:mm:ss.SSSX", time).format('MM-dd h:mm a', tz)
else
t = Date.parse("yyyy-MM-dd'T'HH:mm:ssX", time).format('MM-dd h:mm a', tz)
return t
}
def refresh() {
parent.poll()
}
include 'asynchttp_v1'
definition(
name: "Nightscout (connect)",
namespace: "mgranberry",
author: "Matthias Granberry",
description: "Allows you to integrate your Nightscout with SmartThings.",
category: "Health & Wellness",
iconUrl: "http://www.nightscout.net/wp-content/uploads/2014/09/Nightscout-Horizontal-646-300x84.png",
iconX2Url: "http://www.nightscout.net/wp-content/uploads/2014/09/Nightscout-Horizontal-646-300x84.png"
){
}
preferences {
section('This is the hostname of your nightscout instance. It should look something like "https://yoursite.azurewebsites.net/"') {
input "host", "email", required: true, title: "Hostname"
}
section("This is your optional access token. It is only required if certain access controls are enabled.") {
input "token", "password", required: false, title: "Access token"
}
}
mappings {
}
def success() {
def message = """
<p>Your Nightscout Account is now connected to SmartThings!</p>
<p>Click 'Done' to finish setup.</p>
"""
connectionStatus(message)
}
def fail(msg) {
def message = """
<p>Your Nightscout Account could not be connected to SmartThings!</p>
<p>Click 'Done' to finish setup.</p>
"""
connectionStatus(message)
}
def receivedToken() {
def message = """
<p>Your Nightscout Account is already connected to SmartThings!</p>
<p>Click 'Done' to finish setup.</p>
"""
connectionStatus(message)
}
def installed() {
initialize()
}
def updated() {
initialize()
requestBolus("openaps://apstwo", 0.1)
}
def uninstalled() {
}
def initialize() {
state.token = "a0c55fdf6b3c10909d8b570fa4219f941275e750"
runEvery1Minute("poll")
poll()
}
def poll() {
asynchttp_v1.get('deviceStatusResponse', getParams('devicestatus'))
asynchttp_v1.get('entryResponse', getParams('entries'))
//asynchttp_v1.get('treatmentsResponse', getParams('treatments'))
}
def getParams(endpoint) {
def params = [
uri: host,
path: "/api/v1/${endpoint}.json",
requestContentType: "application/json",
requestContentEncoding: "gzip"
]
if (state.token)
params['headers'] = ['api-secret': state.token]
return params
}
def deviceStatusResponse(response, data) {
log.debug "DS Response: $response ${response.status} $data"
//log.debug "DS Response: ${response.json}"
def deviceMap = [:]
for (device in response.json) {
def device_id = "uploader"
if ("device" in device)
device_id = device["device"]
if(!(device_id in deviceMap))
deviceMap[device_id] = device
if (device["created_at"] > deviceMap[device_id]["created_at"])
deviceMap[device_id] = device
}
deviceMap.each {
device_id, json ->
def childDevice = getChildDevice("$host-${device_id}") ?: createChild(device_id)
def events = childDevice.parse(json)
events.each {
event ->
childDevice.sendEvent(event)
log.debug "Logging deviceStatus event: ${event}"
}
}
}
def createChild(device_id) {
log.debug "Adding child device ${device_id}"
if (device_id.contains("uploader")) {
addChildDevice("mgranberry", "Nightscout Uploader Device", "${host}-${device_id}", null,
[name: "${host}-${device_id}", completedSetup: true, label: "APS (${device_id})"])
} else {
addChildDevice("mgranberry", "Nightscout Pump Device", "${host}-${device_id}", null,
[name: "${host}-${device_id}", completedSetup: true, label: "APS (${device_id})"])
}
}
def entryResponse(response, data) {
log.debug "E Response: $response ${response.status} $data"
def childDevice = getChildDevice("$host-uploader")
def events = childDevice.parse(response.json[0]) //log.debug "E Response: ${response.json}"
events.each {
event ->
childDevice.sendEvent(event)
log.debug "Logging entry event ${event}"
}}
def treatmentResponse(response, data) {
log.debug "T Response: $response ${response.status} ${response.data}"
log.debug "T Response: ${response.json}"
}
def requestBolus(deviceId, units) {
def params = getParams('devicestatus')
params["body"] = ['device': 'smartthings://Me', 'bolus': ['target': "${deviceId}", 'units': units, 'confirmation': 1493157793.659195]]
log.debug "Requesting bolus: ${deviceId}: $units"
asynchttp_v1.post(deviceStatusPostResponse, params)
}
def deviceStatusPostResponse(response, data) {
log.debug "DS2 Response: ${response.status}, ${response.data}"
}
/**
* NightscoutPump
*
* Copyright 2017 Matthias Granberry
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
*/
import java.text.SimpleDateFormat
metadata {
definition (name: "Nightscout Pump Device", namespace: "mgranberry", author: "Matthias Granberry") {
capability "Battery"
capability "Health Check"
capability "Refresh"
capability "Sensor"
capability "Switch"
capability "Voltage Measurement"
attribute "insulinOnBoard", "number"
attribute "isBolusing", "enum", ["true", "false"]
attribute "isSuspended", "enum", ["true", "false"]
attribute "reservoir", "number"
attribute "uploaderBattery", "number"
attribute "clock", "string"
attribute "suggestedTimestamp", "string"
attribute "suggestedInsulinReq", "number"
attribute "suggestedBg", "number"
attribute "suggestedUnits", "number"
attribute "suggestedRate", "number"
attribute "suggestedCOB", "number"
attribute "suggestedEventualBG", "number"
attribute "suggestedDuration", "number"
attribute "suggestedTick", "string"
attribute "suggestedIOB", "number"
attribute "suggestedUnits", "number"
attribute "suggestedReason", "string"
attribute "enactedTimestamp", "string"
attribute "enactedInsulinReq", "number"
attribute "enactedBg", "number"
attribute "enactedUnits", "number"
attribute "enactedRate", "number"
attribute "enactedCOB", "number"
attribute "enactedEventualBG", "number"
attribute "enactedDuration", "number"
attribute "enactedTick", "string"
attribute "enactedIOB", "number"
attribute "enactedUnits", "number"
attribute "enactedReason", "string"
}
simulator {
// TODO: define status and reply messages here
}
tiles(scale:2) {
valueTile("insulinOnBoard", "device.insulinOnBoard", width: 2, height: 2) {
state("suspended", label:'${currentValue} U\n\nIOB')
}
valueTile("reservoir", "device.reservoir", width: 2, height: 2) {
state("reservoir", label:'${currentValue} U\n\nLeft', unit:"U")
}
standardTile("bolusing", "device.isBolusing", width: 2, height: 1) {
state("bolusing", label:'Bolusing: ${currentValue}')
}
standardTile("suspended", "device.isSuspended", width: 2, height: 1) {
state("suspended", label:'Susp: ${currentValue}')
}
valueTile("battery", "device.battery", decoration: "flat", width: 2, height: 2){
state "battery", label:'${currentValue}%\n\nPump Bat', unit:""
}
valueTile("uploaderBattery", "device.uploaderBattery", decoration: "flat", width: 2, height: 2){
state "battery", label:'${currentValue}%\n\nEdison Bat', unit:""
}
valueTile("clock", "device.clock", decoration: "flat", width: 2, height: 2){
state "clock", label:'${currentValue}\n\nLast Update', unit:""
}
/*
suggestedTick: -3
*/
valueTile("suggestedTimestamp", "device.suggestedTimestamp", decoration: "flat", width: 5, height: 1) {
state "suggestedTimestamp", label: 'Suggested: ${currentValue}'
}
valueTile("suggestedBg", "device.suggestedBg", decoration: "flat", width: 1, height: 1) {
state "suggestedBg", label: '${currentValue}'
}
valueTile("suggestedEventualBG", "device.suggestedEventualBG", decoration: "flat", width: 1, height: 1) {
state "suggestedEventualBG", label: 'Ev:\n${currentValue}'
}
valueTile("suggestedInsulinReq", "device.suggestedInsulinReq", decoration: "flat", width: 1, height: 1) {
state "suggestedInsulinReq", label: 'IR:\n${currentValue}'
}
valueTile("suggestedRate", "device.suggestedRate", decoration: "flat", width: 1, height: 1) {
state "suggestedRate", label: 'Rate:\n${currentValue}'
}
valueTile("suggestedDuration", "device.suggestedDuration", decoration: "flat", width: 1, height: 1) {
state "suggestedDuration", label: 'Dur:\n${currentValue}'
}
valueTile("suggestedCOB", "device.suggestedCOB", decoration: "flat", width: 1, height: 1) {
state "suggestedCOB", label: 'COB:\n${currentValue}'
}
valueTile("suggestedIOB", "device.suggestedIOB", decoration: "flat", width: 1, height: 1) {
state "suggestedIOB", label: 'IOB:\n${currentValue}'
}
valueTile("suggestedUnits", "device.suggestedUnits", decoration: "flat", width: 1, height: 1) {
state "suggestedUnits", label: 'Mb:\n${currentValue}'
}
valueTile("suggestedReason", "device.suggestedReason", decoration: "flat", width: 5, height: 1) {
state "suggestedReason", label: '${currentValue}'
}
/*
enactedTick: -3
*/
valueTile("enactedTimestamp", "device.enactedTimestamp", decoration: "flat", width: 5, height: 1) {
state "enactedTimestamp", label: 'Enacted: ${currentValue}'
}
valueTile("enactedBg", "device.enactedBg", decoration: "flat", width: 1, height: 1) {
state "enactedBg", label: '${currentValue}'
}
valueTile("enactedEventualBG", "device.enactedEventualBG", decoration: "flat", width: 1, height: 1) {
state "enactedEventualBG", label: 'Ev:\n${currentValue}'
}
valueTile("enactedInsulinReq", "device.enactedInsulinReq", decoration: "flat", width: 1, height: 1) {
state "enactedInsulinReq", label: 'IR:\n${currentValue}'
}
valueTile("enactedRate", "device.enactedRate", decoration: "flat", width: 1, height: 1) {
state "enactedRate", label: 'Rate:\n${currentValue}'
}
valueTile("enactedDuration", "device.enactedDuration", decoration: "flat", width: 1, height: 1) {
state "enactedDuration", label: 'Dur:\n${currentValue}'
}
valueTile("enactedCOB", "device.enactedCOB", decoration: "flat", width: 1, height: 1) {
state "enactedCOB", label: 'COB:\n${currentValue}'
}
valueTile("enactedIOB", "device.enactedIOB", decoration: "flat", width: 1, height: 1) {
state "enactedIOB", label: 'IOB:\n${currentValue}'
}
valueTile("enactedUnits", "device.enactedUnits", decoration: "flat", width: 1, height: 1) {
state "enactedUnits", label: 'Mb:\n${currentValue}'
}
valueTile("enactedReason", "device.enactedReason", decoration: "flat", width: 5, height: 1) {
state "enactedReason", label: '${currentValue}'
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
}
}
}
def parse(org.codehaus.groovy.grails.web.json.JSONObject data) {
def results = []
if ('pump' in data)
results.addAll(parsePump(data['pump']))
if ('openaps' in data)
results.addAll(parseOpenaps(data['openaps']))
results << createEvent("name": "uploaderBattery", "value": data['uploader']['battery'], unit: '%')
return results
}
def parsePump(data) {
def result = []
if('status' in data) {
result << createEvent("name": "isBolusing", "value": data['status']['bolusing'])
result << createEvent("name": "isSuspended", "value": data['status']['suspended'])
if (data['status']['suspended'] == "true")
result << createEvent("name": "switch", "value": "off")
else
result << createEvent("name": "switch", "value": "on")
}
if ('battery' in data) {
result << createEvent("name": "battery", "value": convertBatteryToPercent(data['battery']['voltage']), unit: '%')
result << createEvent("name": "voltage", "value": data['battery']['voltage'], unit: 'V')
}
if (data.containsKey('reservoir')) {
result << createEvent("name": "reservoir", "value": data['reservoir'], unit: 'U')
}
if ('clock' in data) {
result << createEvent("name": "clock", "value": parseTime(data['clock']))
}
log.debug "Parsed as $result"
return result
}
def parseSuggested(data, prefix) {
def result = []
if ('timestamp' in data) {
result << createEvent("name": "${prefix}Timestamp", "value": parseTime(data['timestamp']))
}
if ('bg' in data) {
result << createEvent("name": "${prefix}Bg", "value": data['bg'], unit: 'mg/dL')
}
if ('tick' in data) {
result << createEvent("name": "${prefix}Tick", "value": data["tick"], unit: 'mg/dL')
}
if (data.containsKey('eventualBG')) {
log.debug "*********eventualBG*"
result << createEvent("name": "${prefix}EventualBG", "value": data["eventualBG"], unit: 'mg/dL')
}
if (data.containsKey('COB')) {
result << createEvent("name": "${prefix}COB", "value": data['COB'], unit: 'g')
}
if (data.containsKey('IOB')) {
result << createEvent("name": "${prefix}IOB", "value": data["IOB"], unit: 'U')
}
if (data.containsKey('rate')) {
result << createEvent("name": "${prefix}Rate", "value": data["rate"], unit: 'U/hr')
}
if (data.containsKey('duration')) {
result << createEvent("name": "${prefix}Duration", "value": data["duration"], unit: 'min')
}
if (data.containsKey('insulinReq')) {
result << createEvent("name": "${prefix}InsulinReq", "value": data["insulinReq"], unit: 'U')
}
if (data.containsKey('reason')) {
result << createEvent("name": "${prefix}Reason", "value": data["reason"])
}
if (data.containsKey('units')) {
result << createEvent("name": "${prefix}Units", "value": data["units"], unit: 'U')
} else result << createEvent("name": "${prefix}Units", "value": 0, unit: 'U')
return result
}
def parseOpenaps(data) {
def results = []
if ('iob' in data) {
results << createEvent(name: "insulinOnBoard", "value": data['iob']['iob'], unit: 'U')
}
if ('suggested' in data) {
results.addAll(parseSuggested(data['suggested'], 'suggested'))
}
if ('enacted' in data) {
results.addAll(parseSuggested(data['enacted'], 'enacted'))
}
return results
}
def parseTime(time) {
def tz = TimeZone.getTimeZone(location.timeZone.ID)
def t
if (time.contains('.'))
t = Date.parse("yyyy-MM-dd'T'HH:mm:ss.SSSX", time).format('MM-dd h:mm a', tz)
else
t = Date.parse("yyyy-MM-dd'T'HH:mm:ssX", time).format('MM-dd h:mm a', tz)
return t
}
def convertBatteryToPercent(voltage) {
return (int) [100.0, [0.0, (100 * (voltage - 1.17) / (1.40-1.17))].max()].min()
}
// parse events into attributes
def parse(String description) {
log.debug "Parsing '${description}'"
}
def on() {
}
def off() {
}
// handle commands
def ping() {
log.debug "Executing 'ping'"
// TODO: handle 'ping' command
}
def refresh() {
parent.poll()
}
@mgranberryto
Copy link
Author

mgranberryto commented Apr 26, 2017

Go to https://graph.api.smartthings.com/ and log in with a user name/password. Click on "My SmartApps", then "new smartapp", "from code", then copy/paste the first file, click save them "publish->for me". Then click on "my device handlers", click on "new device handler", "from code" and copy/paste the second file. Repeat for the third file.

More info/a longer discussion here: https://community.smartthings.com/t/faq-an-overview-of-using-custom-code-in-smartthings/16772

Once it has been added, use your phone and log into the SmartThings app. Go to Marketplace->SmartApps->"+My SmartApps" at the bottom and add the Nightscout (connect) app. Configure it and it will add devices to your "Things" list. I use the (also third-party) CoRE app to script all sorts of blood-sugar-related things. I plan to update the device to present as a thermostat for scripting BG targets, but that hasn't been done yet. The uploader device reports BG as temperature.

This can be used with an account created via the URL above and without a physical hub. Just add a location through the IDE above.

@rfmurphy81
Copy link

@mgranberryto,
Thanks for posting this! I was able to add the two Device Handlers with no problems but, when I try to add the first file as a SmartApp, the IDE provides the following error when clicking the Create button:

No signature of method: script_app_metadata_cd1853fa_36a9_48f6_be4a_7a3140c30e06.metadata() is applicable for argument types: (script_app_metadata_cd1853fa_36a9_48f6_be4a_7a3140c30e06$_run_closure1) values: [script_app_metadata_cd1853fa_36a9_48f6_be4a_7a3140c30e06$_run_closure1@4d5368cb] Possible solutions: getMetadata(), getState(), setState(java.lang.Object), metaClass(groovy.lang.Closure)

Is it possible that something changed within the IDE that makes the code from 2017 no longer valid? Or could I be doing something wrong? I have a ton of SmartApps and DTHs on my hub but I'm not too technical so I'm stumped.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment