Compare commits

..

1 Commits

2 changed files with 439 additions and 797 deletions

View File

@@ -0,0 +1,439 @@
/**
* FortrezZ Flow Meter Interface
*
* Copyright 2016 FortrezZ, LLC
*
* 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.
*
*/
metadata {
definition (name: "FortrezZ Flow Meter Interface", namespace: "fortrezz", author: "Daniel Kurin") {
capability "Battery"
capability "Energy Meter"
capability "Image Capture"
capability "Temperature Measurement"
capability "Sensor"
capability "Water Sensor"
attribute "gpm", "number"
attribute "cumulative", "number"
attribute "alarmState", "string"
attribute "chartMode", "string"
attribute "lastThreshhold", "number"
command "chartMode"
command "zero"
command "setHighFlowLevel", ["number"]
fingerprint deviceId: "0x2101", inClusters: "0x5E, 0x86, 0x72, 0x5A, 0x73, 0x71, 0x85, 0x59, 0x32, 0x31, 0x70, 0x80, 0x7A"
}
simulator {
// TODO: define status and reply messages here
}
preferences {
input "gallonThreshhold", "decimal", title: "High Flow Rate Threshhold", description: "Flow rate (in gpm) that will trigger a notification.", defaultValue: 5, required: false, displayDuringSetup: true
input("registerEmail", type: "email", required: false, title: "Email Address", description: "Register your device with FortrezZ", displayDuringSetup: true)
}
tiles(scale: 2) {
carouselTile("flowHistory", "device.image", width: 6, height: 3) { }
valueTile("battery", "device.battery", inactiveLabel: false, width: 2, height: 2) {
state "battery", label:'${currentValue}%\nBattery', unit:""
}
valueTile("temperature", "device.temperature", width: 2, height: 2) {
state("temperature", label:'${currentValue}°',
backgroundColors:[
[value: 31, color: "#153591"],
[value: 44, color: "#1e9cbb"],
[value: 59, color: "#90d2a7"],
[value: 74, color: "#44b621"],
[value: 84, color: "#f1d801"],
[value: 95, color: "#d04e00"],
[value: 96, color: "#bc2323"]
]
)
}
valueTile("gpm", "device.gpm", inactiveLabel: false, width: 2, height: 2) {
state "gpm", label:'${currentValue}gpm', unit:""
}
standardTile("powerState", "device.powerState", width: 2, height: 2) {
state "reconnected", icon:"http://swiftlet.technology/wp-content/uploads/2016/02/Connected-64.png", backgroundColor:"#cccccc"
state "disconnected", icon:"http://swiftlet.technology/wp-content/uploads/2016/02/Disconnected-64.png", backgroundColor:"#cc0000"
state "batteryReplaced", icon:"http://swiftlet.technology/wp-content/uploads/2016/04/Full-Battery-96.png", backgroundColor:"#cccccc"
state "noBattery", icon:"http://swiftlet.technology/wp-content/uploads/2016/04/No-Battery-96.png", backgroundColor:"#cc0000"
}
standardTile("waterState", "device.waterState", width: 2, height: 2, canChangeIcon: true) {
state "none", icon:"http://cdn.device-icons.smartthings.com/Weather/weather12-icn@2x.png", backgroundColor:"#cccccc", label: "No Flow"
state "flow", icon:"http://cdn.device-icons.smartthings.com/Weather/weather12-icn@2x.png", backgroundColor:"#53a7c0", label: "Flow"
state "overflow", icon:"http://cdn.device-icons.smartthings.com/Weather/weather12-icn@2x.png", backgroundColor:"#cc0000", label: "High Flow"
}
standardTile("heatState", "device.heatState", width: 2, height: 2) {
state "normal", label:'Normal', icon:"st.alarm.temperature.normal", backgroundColor:"#ffffff"
state "freezing", label:'Freezing', icon:"st.alarm.temperature.freeze", backgroundColor:"#2eb82e"
state "overheated", label:'Overheated', icon:"st.alarm.temperature.overheat", backgroundColor:"#F80000"
}
standardTile("take1", "device.image", width: 2, height: 2, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false, decoration: "flat") {
state "take", label: "", action: "Image Capture.take", nextState:"taking", icon: "st.secondary.refresh"
}
standardTile("chartMode", "device.chartMode", width: 2, height: 2, canChangeIcon: false, canChangeBackground: false, decoration: "flat") {
state "day", label:'24 Hours\n(press to change)', nextState: "week", action: 'chartMode'
state "week", label:'7 Days\n(press to change)', nextState: "month", action: 'chartMode'
state "month", label:'4 Weeks\n(press to change)', nextState: "day", action: 'chartMode'
}
valueTile("zeroTile", "device.zero", width: 2, height: 2, canChangeIcon: false, canChangeBackground: false, decoration: "flat") {
state "zero", label:'Zero', action: 'zero'
}
main (["waterState"])
details(["flowHistory", "chartMode", "take1", "temperature", "gpm", "waterState", "battery"])
}
}
// parse events into attributes
def parse(String description) {
def results = []
if (description.startsWith("Err")) {
results << createEvent(descriptionText:description, displayed:true)
} else {
def cmd = zwave.parse(description, [ 0x80: 1, 0x84: 1, 0x71: 2, 0x72: 1 ])
if (cmd) {
results << createEvent( zwaveEvent(cmd) )
}
}
//log.debug "\"$description\" parsed to ${results.inspect()}"
if(gallonThreshhold != device.currentValue("lastThreshhold"))
{
results << setThreshhold(gallonThreshhold)
}
log.debug "zwave parsed to ${results.inspect()}"
return results
}
def updated()
{
log.debug("Updated")
}
def setHighFlowLevel(level)
{
setThreshhold(level)
}
def take() {
def mode = device.currentValue("chartMode")
if(mode == "day")
{
take1()
}
else if(mode == "week")
{
take7()
}
else if(mode == "month")
{
take28()
}
}
def chartMode(string) {
log.debug("ChartMode")
def state = device.currentValue("chartMode")
def tempValue = ""
switch(state)
{
case "day":
tempValue = "week"
break
case "week":
tempValue = "month"
break
case "month":
tempValue = "day"
break
default:
tempValue = "day"
break
}
sendEvent(name: "chartMode", value: tempValue)
take()
}
def take1() {
api("24hrs", "") {
log.debug("Image captured")
if(it.headers.'Content-Type'.contains("image/png")) {
if(it.data) {
storeImage(getPictureName("24hrs"), it.data)
}
}
}
}
def take7() {
api("7days", "") {
log.debug("Image captured")
if(it.headers.'Content-Type'.contains("image/png")) {
if(it.data) {
storeImage(getPictureName("7days"), it.data)
}
}
}
}
def take28() {
api("4weeks", "") {
log.debug("Image captured")
if(it.headers.'Content-Type'.contains("image/png")) {
if(it.data) {
storeImage(getPictureName("4weeks"), it.data)
}
}
}
}
def zero()
{
delayBetween([
zwave.meterV3.meterReset().format(),
zwave.meterV3.meterGet().format(),
zwave.firmwareUpdateMdV2.firmwareMdGet().format(),
], 100)
}
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd)
{
log.debug cmd
def map = [:]
if(cmd.sensorType == 1) {
map = [name: "temperature"]
if(cmd.scale == 0) {
map.value = getTemperature(cmd.scaledSensorValue)
} else {
map.value = cmd.scaledSensorValue
}
map.unit = location.temperatureScale
} /* else if(cmd.sensorType == 2) {
map = [name: "waterState"]
if(cmd.sensorValue[0] == 0x80) {
map.value = "flow"
sendEvent(name: "water", value: "dry")
} else if(cmd.sensorValue[0] == 0x00) {
map.value = "none"
sendEvent(name: "water", value: "dry")
} else if(cmd.sensorValue[0] == 0xFF) {
map.value = "overflow"
sendEvent(name: "water", value: "wet")
sendAlarm("waterOverflow")
}
} */
return map
}
def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd)
{
def map = [:]
map.name = "gpm"
def delta = cmd.scaledMeterValue - cmd.scaledPreviousMeterValue
if (delta < 0 || delta > 10000) {
log.error(cmd)
delta = 0
}
map.value = delta
map.unit = "gpm"
sendDataToCloud(delta)
sendEvent(name: "cumulative", value: cmd.scaledMeterValue, displayed: false, unit: "gal")
return map
}
def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd)
{
def map = [:]
if (cmd.zwaveAlarmType == 8) // Power Alarm
{
map.name = "powerState" // For Tile (shows in "Recently")
if (cmd.zwaveAlarmEvent == 2) // AC Mains Disconnected
{
map.value = "disconnected"
sendAlarm("acMainsDisconnected")
}
else if (cmd.zwaveAlarmEvent == 3) // AC Mains Reconnected
{
map.value = "reconnected"
sendAlarm("acMainsReconnected")
}
else if (cmd.zwaveAlarmEvent == 0x0B) // Replace Battery Now
{
map.value = "noBattery"
sendAlarm("replaceBatteryNow")
}
else if (cmd.zwaveAlarmEvent == 0x00) // Battery Replaced
{
map.value = "batteryReplaced"
sendAlarm("batteryReplaced")
}
}
else if (cmd.zwaveAlarmType == 4) // Heat Alarm
{
map.name = "heatState"
if (cmd.zwaveAlarmEvent == 0) // Normal
{
map.value = "normal"
}
else if (cmd.zwaveAlarmEvent == 1) // Overheat
{
map.value = "overheated"
sendAlarm("tempOverheated")
}
else if (cmd.zwaveAlarmEvent == 5) // Underheat
{
map.value = "freezing"
sendAlarm("tempFreezing")
}
}
else if (cmd.zwaveAlarmType == 5) // Water Alarm
{
map.name = "waterState"
if (cmd.zwaveAlarmEvent == 0) // Normal
{
map.value = "none"
sendEvent(name: "water", value: "dry")
}
else if (cmd.zwaveAlarmEvent == 6) // Flow Detected
{
if(cmd.eventParameter[0] == 2)
{
map.value = "flow"
sendEvent(name: "water", value: "dry")
}
else if(cmd.eventParameter[0] == 3)
{
map.value = "overflow"
sendAlarm("waterOverflow")
sendEvent(name: "water", value: "wet")
}
}
}
//log.debug "alarmV2: $cmd"
return map
}
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
def map = [:]
if(cmd.batteryLevel == 0xFF) {
map.name = "battery"
map.value = 1
map.descriptionText = "${device.displayName} has a low battery"
map.displayed = true
} else {
map.name = "battery"
map.value = cmd.batteryLevel > 0 ? cmd.batteryLevel.toString() : 1
map.unit = "%"
map.displayed = false
}
return map
}
def zwaveEvent(physicalgraph.zwave.Command cmd)
{
log.debug "COMMAND CLASS: $cmd"
}
def sendDataToCloud(double data)
{
def params = [
uri: "https://iot.swiftlet.technology",
path: "/fortrezz/post.php",
body: [
id: device.id,
value: data,
email: registerEmail
]
]
//log.debug("POST parameters: ${params}")
try {
httpPostJson(params) { resp ->
resp.headers.each {
//log.debug "${it.name} : ${it.value}"
}
log.debug "sendDataToCloud query response: ${resp.data}"
}
} catch (e) {
log.debug "something went wrong: $e"
}
}
def getTemperature(value) {
if(location.temperatureScale == "C"){
return value
} else {
return Math.round(celsiusToFahrenheit(value))
}
}
private getPictureName(category) {
//def pictureUuid = device.id.toString().replaceAll('-', '')
def pictureUuid = java.util.UUID.randomUUID().toString().replaceAll('-', '')
def name = "image" + "_$pictureUuid" + "_" + category + ".png"
return name
}
def api(method, args = [], success = {}) {
def methods = [
//"snapshot": [uri: "http://${ip}:${port}/snapshot.cgi${login()}&${args}", type: "post"],
"24hrs": [uri: "https://iot.swiftlet.technology/fortrezz/chart.php?uuid=${device.id}&tz=${location.timeZone.ID}&type=1", type: "get"],
"7days": [uri: "https://iot.swiftlet.technology/fortrezz/chart.php?uuid=${device.id}&tz=${location.timeZone.ID}&type=2", type: "get"],
"4weeks": [uri: "https://iot.swiftlet.technology/fortrezz/chart.php?uuid=${device.id}&tz=${location.timeZone.ID}&type=3", type: "get"],
]
def request = methods.getAt(method)
return doRequest(request.uri, request.type, success)
}
private doRequest(uri, type, success) {
log.debug(uri)
if(type == "post") {
httpPost(uri , "", success)
}
else if(type == "get") {
httpGet(uri, success)
}
}
def sendAlarm(text)
{
sendEvent(name: "alarmState", value: text, descriptionText: text, displayed: false)
}
def setThreshhold(rate)
{
log.debug "Setting Threshhold to ${rate}"
def event = createEvent(name: "lastThreshhold", value: rate, displayed: false)
def cmds = []
cmds << zwave.configurationV2.configurationSet(configurationValue: [(int)Math.round(rate*10)], parameterNumber: 5, size: 1).format()
sendEvent(event)
return response(cmds) // return a list containing the event and the result of response()
}

View File

@@ -1,797 +0,0 @@
/*****************************************************************************************************************
* Copyright David Lomas (codersaur)
*
* Name: InfluxDB Logger
*
* Date: 2017-04-03
*
* Version: 1.11
*
* Source: https://github.com/codersaur/SmartThings/tree/master/smartapps/influxdb-logger
*
* Author: David Lomas (codersaur)
*
* Description: A SmartApp to log SmartThings device states to an InfluxDB database.
*
* For full information, including installation instructions, exmples, and version history, see:
* https://github.com/codersaur/SmartThings/tree/master/smartapps/influxdb-logger
*
* IMPORTANT - To enable the resolution of groupNames (i.e. room names), you must manually insert the group IDs
* into the getGroupName() command code at the end of this file.
*
* License:
* 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.
*****************************************************************************************************************/
definition(
name: "InfluxDB Logger",
namespace: "codersaur",
author: "David Lomas (codersaur)",
description: "Log SmartThings device states to InfluxDB",
category: "My Apps",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")
preferences {
section("General:") {
//input "prefDebugMode", "bool", title: "Enable debug logging?", defaultValue: true, displayDuringSetup: true
input (
name: "configLoggingLevelIDE",
title: "IDE Live Logging Level:\nMessages with this level and higher will be logged to the IDE.",
type: "enum",
options: [
"0" : "None",
"1" : "Error",
"2" : "Warning",
"3" : "Info",
"4" : "Debug",
"5" : "Trace"
],
defaultValue: "3",
displayDuringSetup: true,
required: false
)
}
section ("InfluxDB Database:") {
input "prefDatabaseHost", "text", title: "Host", defaultValue: "10.10.10.10", required: true
input "prefDatabasePort", "text", title: "Port", defaultValue: "8086", required: true
input "prefDatabaseName", "text", title: "Database Name", defaultValue: "", required: true
input "prefDatabaseUser", "text", title: "Username", required: false
input "prefDatabasePass", "text", title: "Password", required: false
}
section("Polling:") {
input "prefSoftPollingInterval", "number", title:"Soft-Polling interval (minutes)", defaultValue: 10, required: true
}
section("System Monitoring:") {
input "prefLogModeEvents", "bool", title:"Log Mode Events?", defaultValue: true, required: true
input "prefLogHubProperties", "bool", title:"Log Hub Properties?", defaultValue: true, required: true
input "prefLogLocationProperties", "bool", title:"Log Location Properties?", defaultValue: true, required: true
}
section("Devices To Monitor:") {
input "accelerometers", "capability.accelerationSensor", title: "Accelerometers", multiple: true, required: false
input "alarms", "capability.alarm", title: "Alarms", multiple: true, required: false
input "batteries", "capability.battery", title: "Batteries", multiple: true, required: false
input "beacons", "capability.beacon", title: "Beacons", multiple: true, required: false
input "buttons", "capability.button", title: "Buttons", multiple: true, required: false
input "cos", "capability.carbonMonoxideDetector", title: "Carbon Monoxide Detectors", multiple: true, required: false
input "co2s", "capability.carbonDioxideMeasurement", title: "Carbon Dioxide Detectors", multiple: true, required: false
input "colors", "capability.colorControl", title: "Color Controllers", multiple: true, required: false
input "consumables", "capability.consumable", title: "Consumables", multiple: true, required: false
input "contacts", "capability.contactSensor", title: "Contact Sensors", multiple: true, required: false
input "doorsControllers", "capability.doorControl", title: "Door Controllers", multiple: true, required: false
input "energyMeters", "capability.energyMeter", title: "Energy Meters", multiple: true, required: false
input "humidities", "capability.relativeHumidityMeasurement", title: "Humidity Meters", multiple: true, required: false
input "illuminances", "capability.illuminanceMeasurement", title: "Illuminance Meters", multiple: true, required: false
input "locks", "capability.lock", title: "Locks", multiple: true, required: false
input "motions", "capability.motionSensor", title: "Motion Sensors", multiple: true, required: false
input "musicPlayers", "capability.musicPlayer", title: "Music Players", multiple: true, required: false
input "peds", "capability.stepSensor", title: "Pedometers", multiple: true, required: false
input "phMeters", "capability.pHMeasurement", title: "pH Meters", multiple: true, required: false
input "powerMeters", "capability.powerMeter", title: "Power Meters", multiple: true, required: false
input "presences", "capability.presenceSensor", title: "Presence Sensors", multiple: true, required: false
input "pressures", "capability.sensor", title: "Pressure Sensors", multiple: true, required: false
input "shockSensors", "capability.shockSensor", title: "Shock Sensors", multiple: true, required: false
input "signalStrengthMeters", "capability.signalStrength", title: "Signal Strength Meters", multiple: true, required: false
input "sleepSensors", "capability.sleepSensor", title: "Sleep Sensors", multiple: true, required: false
input "smokeDetectors", "capability.smokeDetector", title: "Smoke Detectors", multiple: true, required: false
input "soundSensors", "capability.soundSensor", title: "Sound Sensors", multiple: true, required: false
input "spls", "capability.soundPressureLevel", title: "Sound Pressure Level Sensors", multiple: true, required: false
input "switches", "capability.switch", title: "Switches", multiple: true, required: false
input "switchLevels", "capability.switchLevel", title: "Switch Levels", multiple: true, required: false
input "tamperAlerts", "capability.tamperAlert", title: "Tamper Alerts", multiple: true, required: false
input "temperatures", "capability.temperatureMeasurement", title: "Temperature Sensors", multiple: true, required: false
input "thermostats", "capability.thermostat", title: "Thermostats", multiple: true, required: false
input "threeAxis", "capability.threeAxis", title: "Three-axis (Orientation) Sensors", multiple: true, required: false
input "touchs", "capability.touchSensor", title: "Touch Sensors", multiple: true, required: false
input "uvs", "capability.ultravioletIndex", title: "UV Sensors", multiple: true, required: false
input "valves", "capability.valve", title: "Valves", multiple: true, required: false
input "volts", "capability.voltageMeasurement", title: "Voltage Meters", multiple: true, required: false
input "waterSensors", "capability.waterSensor", title: "Water Sensors", multiple: true, required: false
input "windowShades", "capability.windowShade", title: "Window Shades", multiple: true, required: false
}
}
/*****************************************************************************************************************
* SmartThings System Commands:
*****************************************************************************************************************/
/**
* installed()
*
* Runs when the app is first installed.
**/
def installed() {
state.installedAt = now()
state.loggingLevelIDE = 5
log.debug "${app.label}: Installed with settings: ${settings}"
}
/**
* uninstalled()
*
* Runs when the app is uninstalled.
**/
def uninstalled() {
logger("uninstalled()","trace")
}
/**
* updated()
*
* Runs when app settings are changed.
*
* Updates device.state with input values and other hard-coded values.
* Builds state.deviceAttributes which describes the attributes that will be monitored for each device collection
* (used by manageSubscriptions() and softPoll()).
* Refreshes scheduling and subscriptions.
**/
def updated() {
logger("updated()","trace")
// Update internal state:
state.loggingLevelIDE = (settings.configLoggingLevelIDE) ? settings.configLoggingLevelIDE.toInteger() : 3
// Database config:
state.databaseHost = settings.prefDatabaseHost
state.databasePort = settings.prefDatabasePort
state.databaseName = settings.prefDatabaseName
state.databaseUser = settings.prefDatabaseUser
state.databasePass = settings.prefDatabasePass
state.path = "/write?db=${state.databaseName}"
state.headers = [:]
state.headers.put("HOST", "${state.databaseHost}:${state.databasePort}")
state.headers.put("Content-Type", "application/x-www-form-urlencoded")
if (state.databaseUser && state.databasePass) {
state.headers.put("Authorization", encodeCredentialsBasic(state.databaseUser, state.databasePass))
}
// Build array of device collections and the attributes we want to report on for that collection:
// Note, the collection names are stored as strings. Adding references to the actual collection
// objects causes major issues (possibly memory issues?).
state.deviceAttributes = []
state.deviceAttributes << [ devices: 'accelerometers', attributes: ['acceleration']]
state.deviceAttributes << [ devices: 'alarms', attributes: ['alarm']]
state.deviceAttributes << [ devices: 'batteries', attributes: ['battery']]
state.deviceAttributes << [ devices: 'beacons', attributes: ['presence']]
state.deviceAttributes << [ devices: 'buttons', attributes: ['button']]
state.deviceAttributes << [ devices: 'cos', attributes: ['carbonMonoxide']]
state.deviceAttributes << [ devices: 'co2s', attributes: ['carbonDioxide']]
state.deviceAttributes << [ devices: 'colors', attributes: ['hue','saturation','color']]
state.deviceAttributes << [ devices: 'consumables', attributes: ['consumableStatus']]
state.deviceAttributes << [ devices: 'contacts', attributes: ['contact']]
state.deviceAttributes << [ devices: 'doorsControllers', attributes: ['door']]
state.deviceAttributes << [ devices: 'energyMeters', attributes: ['energy']]
state.deviceAttributes << [ devices: 'humidities', attributes: ['humidity']]
state.deviceAttributes << [ devices: 'illuminances', attributes: ['illuminance']]
state.deviceAttributes << [ devices: 'locks', attributes: ['lock']]
state.deviceAttributes << [ devices: 'motions', attributes: ['motion']]
state.deviceAttributes << [ devices: 'musicPlayers', attributes: ['status','level','trackDescription','trackData','mute']]
state.deviceAttributes << [ devices: 'peds', attributes: ['steps','goal']]
state.deviceAttributes << [ devices: 'phMeters', attributes: ['pH']]
state.deviceAttributes << [ devices: 'powerMeters', attributes: ['power','voltage','current','powerFactor']]
state.deviceAttributes << [ devices: 'presences', attributes: ['presence']]
state.deviceAttributes << [ devices: 'pressures', attributes: ['pressure']]
state.deviceAttributes << [ devices: 'shockSensors', attributes: ['shock']]
state.deviceAttributes << [ devices: 'signalStrengthMeters', attributes: ['lqi','rssi']]
state.deviceAttributes << [ devices: 'sleepSensors', attributes: ['sleeping']]
state.deviceAttributes << [ devices: 'smokeDetectors', attributes: ['smoke']]
state.deviceAttributes << [ devices: 'soundSensors', attributes: ['sound']]
state.deviceAttributes << [ devices: 'spls', attributes: ['soundPressureLevel']]
state.deviceAttributes << [ devices: 'switches', attributes: ['switch']]
state.deviceAttributes << [ devices: 'switchLevels', attributes: ['level']]
state.deviceAttributes << [ devices: 'tamperAlerts', attributes: ['tamper']]
state.deviceAttributes << [ devices: 'temperatures', attributes: ['temperature']]
state.deviceAttributes << [ devices: 'thermostats', attributes: ['temperature','heatingSetpoint','coolingSetpoint','thermostatSetpoint','thermostatMode','thermostatFanMode','thermostatOperatingState','thermostatSetpointMode','scheduledSetpoint','optimisation','windowFunction']]
state.deviceAttributes << [ devices: 'threeAxis', attributes: ['threeAxis']]
state.deviceAttributes << [ devices: 'touchs', attributes: ['touch']]
state.deviceAttributes << [ devices: 'uvs', attributes: ['ultravioletIndex']]
state.deviceAttributes << [ devices: 'valves', attributes: ['contact']]
state.deviceAttributes << [ devices: 'volts', attributes: ['voltage']]
state.deviceAttributes << [ devices: 'waterSensors', attributes: ['water']]
state.deviceAttributes << [ devices: 'windowShades', attributes: ['windowShade']]
// Configure Scheduling:
state.softPollingInterval = settings.prefSoftPollingInterval.toInteger()
manageSchedules()
// Configure Subscriptions:
manageSubscriptions()
}
/*****************************************************************************************************************
* Event Handlers:
*****************************************************************************************************************/
/**
* handleAppTouch(evt)
*
* Used for testing.
**/
def handleAppTouch(evt) {
logger("handleAppTouch()","trace")
softPoll()
}
/**
* handleModeEvent(evt)
*
* Log Mode changes.
**/
def handleModeEvent(evt) {
logger("handleModeEvent(): Mode changed to: ${evt.value}","info")
def locationId = escapeStringForInfluxDB(location.id)
def locationName = escapeStringForInfluxDB(location.name)
def mode = '"' + escapeStringForInfluxDB(evt.value) + '"'
def data = "_stMode,locationId=${locationId},locationName=${locationName} mode=${mode}"
postToInfluxDB(data)
}
/**
* handleEvent(evt)
*
* Builds data to send to InfluxDB.
* - Escapes and quotes string values.
* - Calculates logical binary values where string values can be
* represented as binary values (e.g. contact: closed = 1, open = 0)
*
* Useful references:
* - http://docs.smartthings.com/en/latest/capabilities-reference.html
* - https://docs.influxdata.com/influxdb/v0.10/guides/writing_data/
**/
def handleEvent(evt) {
logger("handleEvent(): $evt.displayName($evt.name:$evt.unit) $evt.value","info")
// Build data string to send to InfluxDB:
// Format: <measurement>[,<tag_name>=<tag_value>] field=<field_value>
// If value is an integer, it must have a trailing "i"
// If value is a string, it must be enclosed in double quotes.
def measurement = evt.name
// tags:
def deviceId = escapeStringForInfluxDB(evt.deviceId)
def deviceName = escapeStringForInfluxDB(evt.displayName)
def groupId = escapeStringForInfluxDB(evt?.device.device.groupId)
def groupName = escapeStringForInfluxDB(getGroupName(evt?.device.device.groupId))
def hubId = escapeStringForInfluxDB(evt?.device.device.hubId)
def hubName = escapeStringForInfluxDB(evt?.device.device.hub.toString())
// Don't pull these from the evt.device as the app itself will be associated with one location.
def locationId = escapeStringForInfluxDB(location.id)
def locationName = escapeStringForInfluxDB(location.name)
def unit = escapeStringForInfluxDB(evt.unit)
def value = escapeStringForInfluxDB(evt.value)
def valueBinary = ''
def data = "${measurement},deviceId=${deviceId},deviceName=${deviceName},groupId=${groupId},groupName=${groupName},hubId=${hubId},hubName=${hubName},locationId=${locationId},locationName=${locationName}"
// Unit tag and fields depend on the event type:
// Most string-valued attributes can be translated to a binary value too.
if ('acceleration' == evt.name) { // acceleration: Calculate a binary value (active = 1, inactive = 0)
unit = 'acceleration'
value = '"' + value + '"'
valueBinary = ('active' == evt.value) ? '1i' : '0i'
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
}
else if ('alarm' == evt.name) { // alarm: Calculate a binary value (strobe/siren/both = 1, off = 0)
unit = 'alarm'
value = '"' + value + '"'
valueBinary = ('off' == evt.value) ? '0i' : '1i'
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
}
else if ('button' == evt.name) { // button: Calculate a binary value (held = 1, pushed = 0)
unit = 'button'
value = '"' + value + '"'
valueBinary = ('pushed' == evt.value) ? '0i' : '1i'
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
}
else if ('carbonMonoxide' == evt.name) { // carbonMonoxide: Calculate a binary value (detected = 1, clear/tested = 0)
unit = 'carbonMonoxide'
value = '"' + value + '"'
valueBinary = ('detected' == evt.value) ? '1i' : '0i'
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
}
else if ('consumableStatus' == evt.name) { // consumableStatus: Calculate a binary value ("good" = 1, "missing"/"replace"/"maintenance_required"/"order" = 0)
unit = 'consumableStatus'
value = '"' + value + '"'
valueBinary = ('good' == evt.value) ? '1i' : '0i'
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
}
else if ('contact' == evt.name) { // contact: Calculate a binary value (closed = 1, open = 0)
unit = 'contact'
value = '"' + value + '"'
valueBinary = ('closed' == evt.value) ? '1i' : '0i'
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
}
else if ('door' == evt.name) { // door: Calculate a binary value (closed = 1, open/opening/closing/unknown = 0)
unit = 'door'
value = '"' + value + '"'
valueBinary = ('closed' == evt.value) ? '1i' : '0i'
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
}
else if ('lock' == evt.name) { // door: Calculate a binary value (locked = 1, unlocked = 0)
unit = 'lock'
value = '"' + value + '"'
valueBinary = ('locked' == evt.value) ? '1i' : '0i'
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
}
else if ('motion' == evt.name) { // Motion: Calculate a binary value (active = 1, inactive = 0)
unit = 'motion'
value = '"' + value + '"'
valueBinary = ('active' == evt.value) ? '1i' : '0i'
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
}
else if ('mute' == evt.name) { // mute: Calculate a binary value (muted = 1, unmuted = 0)
unit = 'mute'
value = '"' + value + '"'
valueBinary = ('muted' == evt.value) ? '1i' : '0i'
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
}
else if ('presence' == evt.name) { // presence: Calculate a binary value (present = 1, not present = 0)
unit = 'presence'
value = '"' + value + '"'
valueBinary = ('present' == evt.value) ? '1i' : '0i'
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
}
else if ('shock' == evt.name) { // shock: Calculate a binary value (detected = 1, clear = 0)
unit = 'shock'
value = '"' + value + '"'
valueBinary = ('detected' == evt.value) ? '1i' : '0i'
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
}
else if ('sleeping' == evt.name) { // sleeping: Calculate a binary value (sleeping = 1, not sleeping = 0)
unit = 'sleeping'
value = '"' + value + '"'
valueBinary = ('sleeping' == evt.value) ? '1i' : '0i'
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
}
else if ('smoke' == evt.name) { // smoke: Calculate a binary value (detected = 1, clear/tested = 0)
unit = 'smoke'
value = '"' + value + '"'
valueBinary = ('detected' == evt.value) ? '1i' : '0i'
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
}
else if ('sound' == evt.name) { // sound: Calculate a binary value (detected = 1, not detected = 0)
unit = 'sound'
value = '"' + value + '"'
valueBinary = ('detected' == evt.value) ? '1i' : '0i'
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
}
else if ('switch' == evt.name) { // switch: Calculate a binary value (on = 1, off = 0)
unit = 'switch'
value = '"' + value + '"'
valueBinary = ('on' == evt.value) ? '1i' : '0i'
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
}
else if ('tamper' == evt.name) { // tamper: Calculate a binary value (detected = 1, clear = 0)
unit = 'tamper'
value = '"' + value + '"'
valueBinary = ('detected' == evt.value) ? '1i' : '0i'
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
}
else if ('thermostatMode' == evt.name) { // thermostatMode: Calculate a binary value (<any other value> = 1, off = 0)
unit = 'thermostatMode'
value = '"' + value + '"'
valueBinary = ('off' == evt.value) ? '0i' : '1i'
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
}
else if ('thermostatFanMode' == evt.name) { // thermostatFanMode: Calculate a binary value (<any other value> = 1, off = 0)
unit = 'thermostatFanMode'
value = '"' + value + '"'
valueBinary = ('off' == evt.value) ? '0i' : '1i'
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
}
else if ('thermostatOperatingState' == evt.name) { // thermostatOperatingState: Calculate a binary value (heating = 1, <any other value> = 0)
unit = 'thermostatOperatingState'
value = '"' + value + '"'
valueBinary = ('heating' == evt.value) ? '1i' : '0i'
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
}
else if ('thermostatSetpointMode' == evt.name) { // thermostatSetpointMode: Calculate a binary value (followSchedule = 0, <any other value> = 1)
unit = 'thermostatSetpointMode'
value = '"' + value + '"'
valueBinary = ('followSchedule' == evt.value) ? '0i' : '1i'
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
}
else if ('threeAxis' == evt.name) { // threeAxis: Format to x,y,z values.
unit = 'threeAxis'
def valueXYZ = evt.value.split(",")
def valueX = valueXYZ[0]
def valueY = valueXYZ[1]
def valueZ = valueXYZ[2]
data += ",unit=${unit} valueX=${valueX}i,valueY=${valueY}i,valueZ=${valueZ}i" // values are integers.
}
else if ('touch' == evt.name) { // touch: Calculate a binary value (touched = 1, "" = 0)
unit = 'touch'
value = '"' + value + '"'
valueBinary = ('touched' == evt.value) ? '1i' : '0i'
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
}
else if ('optimisation' == evt.name) { // optimisation: Calculate a binary value (active = 1, inactive = 0)
unit = 'optimisation'
value = '"' + value + '"'
valueBinary = ('active' == evt.value) ? '1i' : '0i'
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
}
else if ('windowFunction' == evt.name) { // windowFunction: Calculate a binary value (active = 1, inactive = 0)
unit = 'windowFunction'
value = '"' + value + '"'
valueBinary = ('active' == evt.value) ? '1i' : '0i'
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
}
else if ('touch' == evt.name) { // touch: Calculate a binary value (touched = 1, <any other value> = 0)
unit = 'touch'
value = '"' + value + '"'
valueBinary = ('touched' == evt.value) ? '1i' : '0i'
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
}
else if ('water' == evt.name) { // water: Calculate a binary value (wet = 1, dry = 0)
unit = 'water'
value = '"' + value + '"'
valueBinary = ('wet' == evt.value) ? '1i' : '0i'
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
}
else if ('windowShade' == evt.name) { // windowShade: Calculate a binary value (closed = 1, <any other value> = 0)
unit = 'windowShade'
value = '"' + value + '"'
valueBinary = ('closed' == evt.value) ? '1i' : '0i'
data += ",unit=${unit} value=${value},valueBinary=${valueBinary}"
}
// Catch any other event with a string value that hasn't been handled:
else if (evt.value ==~ /.*[^0-9\.,-].*/) { // match if any characters are not digits, period, comma, or hyphen.
logger("handleEvent(): Found a string value that's not explicitly handled: Device Name: ${deviceName}, Event Name: ${evt.name}, Value: ${evt.value}","warn")
value = '"' + value + '"'
data += ",unit=${unit} value=${value}"
}
// Catch any other general numerical event (carbonDioxide, power, energy, humidity, level, temperature, ultravioletIndex, voltage, etc).
else {
data += ",unit=${unit} value=${value}"
}
// Post data to InfluxDB:
postToInfluxDB(data)
}
/*****************************************************************************************************************
* Main Commands:
*****************************************************************************************************************/
/**
* softPoll()
*
* Executed by schedule.
*
* Forces data to be posted to InfluxDB (even if an event has not been triggered).
* Doesn't poll devices, just builds a fake event to pass to handleEvent().
*
* Also calls LogSystemProperties().
**/
def softPoll() {
logger("softPoll()","trace")
logSystemProperties()
// Iterate over each attribute for each device, in each device collection in deviceAttributes:
def devs // temp variable to hold device collection.
state.deviceAttributes.each { da ->
devs = settings."${da.devices}"
if (devs && (da.attributes)) {
devs.each { d ->
da.attributes.each { attr ->
if (d.hasAttribute(attr) && d.latestState(attr)?.value != null) {
logger("softPoll(): Softpolling device ${d} for attribute: ${attr}","info")
// Send fake event to handleEvent():
handleEvent([
name: attr,
value: d.latestState(attr)?.value,
unit: d.latestState(attr)?.unit,
device: d,
deviceId: d.id,
displayName: d.displayName
])
}
}
}
}
}
}
/**
* logSystemProperties()
*
* Generates measurements for SmartThings system (hubs and locations) properties.
**/
def logSystemProperties() {
logger("logSystemProperties()","trace")
def locationId = '"' + escapeStringForInfluxDB(location.id) + '"'
def locationName = '"' + escapeStringForInfluxDB(location.name) + '"'
// Location Properties:
if (prefLogLocationProperties) {
try {
def tz = '"' + escapeStringForInfluxDB(location.timeZone.ID) + '"'
def mode = '"' + escapeStringForInfluxDB(location.mode) + '"'
def hubCount = location.hubs.size()
def times = getSunriseAndSunset()
def srt = '"' + times.sunrise.format("HH:mm", location.timeZone) + '"'
def sst = '"' + times.sunset.format("HH:mm", location.timeZone) + '"'
def data = "_stLocation,locationId=${locationId},locationName=${locationName},latitude=${location.latitude},longitude=${location.longitude},timeZone=${tz} mode=${mode},hubCount=${hubCount}i,sunriseTime=${srt},sunsetTime=${sst}"
postToInfluxDB(data)
} catch (e) {
logger("logSystemProperties(): Unable to log Location properties: ${e}","error")
}
}
// Hub Properties:
if (prefLogHubProperties) {
location.hubs.each { h ->
try {
def hubId = '"' + escapeStringForInfluxDB(h.id) + '"'
def hubName = '"' + escapeStringForInfluxDB(h.name) + '"'
def hubIP = '"' + escapeStringForInfluxDB(h.localIP) + '"'
def hubStatus = '"' + escapeStringForInfluxDB(h.status) + '"'
def batteryInUse = ("false" == h.hub.getDataValue("batteryInUse")) ? "0i" : "1i"
def hubUptime = h.hub.getDataValue("uptime") + 'i'
def zigbeePowerLevel = h.hub.getDataValue("zigbeePowerLevel") + 'i'
def zwavePowerLevel = '"' + escapeStringForInfluxDB(h.hub.getDataValue("zwavePowerLevel")) + '"'
def firmwareVersion = '"' + escapeStringForInfluxDB(h.firmwareVersionString) + '"'
def data = "_stHub,locationId=${locationId},locationName=${locationName},hubId=${hubId},hubName=${hubName},hubIP=${hubIP} "
data += "status=${hubStatus},batteryInUse=${batteryInUse},uptime=${hubUptime},zigbeePowerLevel=${zigbeePowerLevel},zwavePowerLevel=${zwavePowerLevel},firmwareVersion=${firmwareVersion}"
postToInfluxDB(data)
} catch (e) {
logger("logSystemProperties(): Unable to log Hub properties: ${e}","error")
}
}
}
}
/**
* postToInfluxDB()
*
* Posts data to InfluxDB.
*
* Uses hubAction instead of httpPost() in case InfluxDB server is on the same LAN as the Smartthings Hub.
**/
def postToInfluxDB(data) {
logger("postToInfluxDB(): Posting data to InfluxDB: Host: ${state.databaseHost}, Port: ${state.databasePort}, Database: ${state.databaseName}, Data: [${data}]","debug")
try {
def hubAction = new physicalgraph.device.HubAction(
[
method: "POST",
path: state.path,
body: data,
headers: state.headers
],
null,
[ callback: handleInfluxResponse ]
)
sendHubCommand(hubAction)
}
catch (Exception e) {
logger("postToInfluxDB(): Exception ${e} on ${hubAction}","error")
}
// For reference, code that could be used for WAN hosts:
// def url = "http://${state.databaseHost}:${state.databasePort}/write?db=${state.databaseName}"
// try {
// httpPost(url, data) { response ->
// if (response.status != 999 ) {
// log.debug "Response Status: ${response.status}"
// log.debug "Response data: ${response.data}"
// log.debug "Response contentType: ${response.contentType}"
// }
// }
// } catch (e) {
// logger("postToInfluxDB(): Something went wrong when posting: ${e}","error")
// }
}
/**
* handleInfluxResponse()
*
* Handles response from post made in postToInfluxDB().
**/
def handleInfluxResponse(physicalgraph.device.HubResponse hubResponse) {
if(hubResponse.status >= 400) {
logger("postToInfluxDB(): Something went wrong! Response from InfluxDB: Headers: ${hubResponse.headers}, Body: ${hubResponse.body}","error")
}
}
/*****************************************************************************************************************
* Private Helper Functions:
*****************************************************************************************************************/
/**
* manageSchedules()
*
* Configures/restarts scheduled tasks:
* softPoll() - Run every {state.softPollingInterval} minutes.
**/
private manageSchedules() {
logger("manageSchedules()","trace")
// Generate a random offset (1-60):
Random rand = new Random(now())
def randomOffset = 0
// softPoll:
try {
unschedule(softPoll)
}
catch(e) {
// logger("manageSchedules(): Unschedule failed!","error")
}
if (state.softPollingInterval > 0) {
randomOffset = rand.nextInt(60)
logger("manageSchedules(): Scheduling softpoll to run every ${state.softPollingInterval} minutes (offset of ${randomOffset} seconds).","trace")
schedule("${randomOffset} 0/${state.softPollingInterval} * * * ?", "softPoll")
}
}
/**
* manageSubscriptions()
*
* Configures subscriptions.
**/
private manageSubscriptions() {
logger("manageSubscriptions()","trace")
// Unsubscribe:
unsubscribe()
// Subscribe to App Touch events:
subscribe(app,handleAppTouch)
// Subscribe to mode events:
if (prefLogModeEvents) subscribe(location, "mode", handleModeEvent)
// Subscribe to device attributes (iterate over each attribute for each device collection in state.deviceAttributes):
def devs // dynamic variable holding device collection.
state.deviceAttributes.each { da ->
devs = settings."${da.devices}"
if (devs && (da.attributes)) {
da.attributes.each { attr ->
logger("manageSubscriptions(): Subscribing to attribute: ${attr}, for devices: ${da.devices}","info")
// There is no need to check if all devices in the collection have the attribute.
subscribe(devs, attr, handleEvent)
}
}
}
}
/**
* logger()
*
* Wrapper function for all logging.
**/
private logger(msg, level = "debug") {
switch(level) {
case "error":
if (state.loggingLevelIDE >= 1) log.error msg
break
case "warn":
if (state.loggingLevelIDE >= 2) log.warn msg
break
case "info":
if (state.loggingLevelIDE >= 3) log.info msg
break
case "debug":
if (state.loggingLevelIDE >= 4) log.debug msg
break
case "trace":
if (state.loggingLevelIDE >= 5) log.trace msg
break
default:
log.debug msg
break
}
}
/**
* encodeCredentialsBasic()
*
* Encode credentials for HTTP Basic authentication.
**/
private encodeCredentialsBasic(username, password) {
return "Basic " + "${username}:${password}".encodeAsBase64().toString()
}
/**
* escapeStringForInfluxDB()
*
* Escape values to InfluxDB.
*
* If a tag key, tag value, or field key contains a space, comma, or an equals sign = it must
* be escaped using the backslash character \. Backslash characters do not need to be escaped.
* Commas and spaces will also need to be escaped for measurements, though equals signs = do not.
*
* Further info: https://docs.influxdata.com/influxdb/v0.10/write_protocols/write_syntax/
**/
private escapeStringForInfluxDB(str) {
if (str) {
str = str.replaceAll(" ", "\\\\ ") // Escape spaces.
str = str.replaceAll(",", "\\\\,") // Escape commas.
str = str.replaceAll("=", "\\\\=") // Escape equal signs.
str = str.replaceAll("\"", "\\\\\"") // Escape double quotes.
//str = str.replaceAll("'", "_") // Replace apostrophes with underscores.
}
else {
str = 'null'
}
return str
}
/**
* getGroupName()
*
* Get the name of a 'Group' (i.e. Room) from its ID.
*
* This is done manually as there does not appear to be a way to enumerate
* groups from a SmartApp currently.
*
* GroupIds can be obtained from the SmartThings IDE under 'My Locations'.
*
* See: https://community.smartthings.com/t/accessing-group-within-a-smartapp/6830
**/
private getGroupName(id) {
if (id == null) {return 'Home'}
else if (id == 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX') {return 'Kitchen'}
else if (id == 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX') {return 'Lounge'}
else if (id == 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX') {return 'Office'}
else {return 'Unknown'}
}