Created
April 26, 2017 20:49
-
-
Save mgranberryto/dbabfc2ac118ba5841db62d425fb70b9 to your computer and use it in GitHub Desktop.
NS-Connect
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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() | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}" | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@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.