mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-15 13:10:51 +00:00
Compare commits
1 Commits
MSA-2119-9
...
MSA-2122-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e332a7c43 |
797
smartapps/codersaur/influxdb-logger.src/influxdb-logger.groovy
Normal file
797
smartapps/codersaur/influxdb-logger.src/influxdb-logger.groovy
Normal file
@@ -0,0 +1,797 @@
|
||||
/*****************************************************************************************************************
|
||||
* 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'}
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
/**
|
||||
* Consumption Metering
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "Consumption Metering",
|
||||
namespace: "FortrezZ",
|
||||
author: "FortrezZ, LLC",
|
||||
description: "Child SmartApp for Consumption Metering rules",
|
||||
category: "Green Living",
|
||||
parent: "FortrezZ:FortrezZ Water Consumption Metering",
|
||||
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 {
|
||||
page(name: "prefsPage", title: "Choose the detector behavior", install: true, uninstall: true)
|
||||
|
||||
// Do something here like update a message on the screen,
|
||||
// or introduce more inputs. submitOnChange will refresh
|
||||
// the page and allow the user to see the changes immediately.
|
||||
// For example, you could prompt for the level of the dimmers
|
||||
// if dimmers have been selected:
|
||||
//log.debug "Child Settings: ${settings}"
|
||||
}
|
||||
|
||||
def prefsPage() {
|
||||
def dailySchedule = 0
|
||||
def daysOfTheWeek = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
|
||||
dynamicPage(name: "prefsPage") {
|
||||
section("Set Water Usage Goals") {
|
||||
input(name: "type", type: "enum", title: "Set a new goal?", submitOnChange: true, options: ruleTypes())
|
||||
}
|
||||
def measurementType = "water"
|
||||
if(type)
|
||||
{
|
||||
switch (type) {
|
||||
case "Daily Goal":
|
||||
section("Water Measurement Preference"){
|
||||
input(name: "measurementType", type: "enum", title: "Press to change water measurement options", submitOnChange: true, options: waterTypes())}
|
||||
|
||||
section("Threshold settings") {
|
||||
input(name: "waterGoal", type: "decimal", title: "Daily ${measurementType} Goal", required: true, defaultValue: 0.5)
|
||||
}
|
||||
|
||||
|
||||
break
|
||||
|
||||
case "Weekly Goal":
|
||||
section("Water Measurement Preference"){
|
||||
input(name: "measurementType", type: "enum", title: "Press to change water measurement options", submitOnChange: true, options: waterTypes())}
|
||||
section("Threshold settings") {
|
||||
input(name: "waterGoal", type: "decimal", title: "Weekly ${measurementType} Goal", required: true, defaultValue: 0.1)
|
||||
}
|
||||
|
||||
|
||||
break
|
||||
|
||||
case "Monthly Goal":
|
||||
section("Water Measurement Preference"){
|
||||
input(name: "measurementType", type: "enum", title: "Press to change water measurement options", submitOnChange: true, options: waterTypes())}
|
||||
section("Threshold settings") {
|
||||
input(name: "waterGoal", type: "decimal", title: "Monthly ${measurementType} Goal", required: true, defaultValue: 0.1)
|
||||
}
|
||||
|
||||
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def ruleTypes() {
|
||||
def types = []
|
||||
types << "Daily Goal"
|
||||
types << "Weekly Goal"
|
||||
types << "Monthly Goal"
|
||||
|
||||
return types
|
||||
}
|
||||
|
||||
def waterTypes()
|
||||
{
|
||||
def watertype = []
|
||||
|
||||
watertype << "Gallons"
|
||||
watertype << "Cubic Feet"
|
||||
watertype << "Liters"
|
||||
watertype << "Cubic Meters"
|
||||
return watertype
|
||||
}
|
||||
/*
|
||||
def setDailyGoal(measurementType3)
|
||||
{
|
||||
return parent.setDailyGoal(measurementType3)
|
||||
}
|
||||
def setWeeklyGoal()
|
||||
{
|
||||
return parent.setWeeklyGoal(measurementType)
|
||||
}
|
||||
def setMonthlyGoal()
|
||||
{
|
||||
return parent.setMonthlyGoal(measurementType)
|
||||
}
|
||||
*/
|
||||
|
||||
def actionTypes() {
|
||||
def types = []
|
||||
types << [name: "Switch", capability: "capabilty.switch"]
|
||||
types << [name: "Water Valve", capability: "capability.valve"]
|
||||
|
||||
return types
|
||||
}
|
||||
|
||||
def deviceCommands(dev)
|
||||
{
|
||||
def cmds = []
|
||||
dev.supportedCommands.each { command ->
|
||||
cmds << command.name
|
||||
}
|
||||
|
||||
return cmds
|
||||
}
|
||||
|
||||
def installed() {
|
||||
state.Daily = 0
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
app.updateLabel("${type} - ${waterGoal} ${measurementType}")
|
||||
//schedule(" 0 0/1 * 1/1 * ? *", setDailyGoal())
|
||||
initialize()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
app.updateLabel("${type} - ${waterGoal} ${measurementType}")
|
||||
|
||||
|
||||
unsubscribe()
|
||||
initialize()
|
||||
//unschedule()
|
||||
}
|
||||
|
||||
def settings() {
|
||||
def set = settings
|
||||
if (set["dev"] != null)
|
||||
{
|
||||
log.debug("dev set: ${set.dev}")
|
||||
set.dev = set.dev.id
|
||||
}
|
||||
if (set["valve"] != null)
|
||||
{
|
||||
log.debug("valve set: ${set.valve}")
|
||||
set.valve = set.valve.id
|
||||
}
|
||||
|
||||
log.debug(set)
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
def devAction(action)
|
||||
{
|
||||
if(dev)
|
||||
{
|
||||
log.debug("device: ${dev}, action: ${action}")
|
||||
dev."${action}"()
|
||||
}
|
||||
}
|
||||
|
||||
def isValveStatus(status)
|
||||
{
|
||||
def result = false
|
||||
log.debug("Water Valve ${valve} has status ${valve.currentState("contact").value}, compared to ${status.toLowerCase()}")
|
||||
if(valve)
|
||||
{
|
||||
if(valve.currentState("contact").value == status.toLowerCase())
|
||||
{
|
||||
result = true
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
|
||||
|
||||
// TODO: subscribe to attributes, devices, locations, etc.
|
||||
}
|
||||
def uninstalled() {
|
||||
// external cleanup. No need to unsubscribe or remove scheduled jobs
|
||||
}
|
||||
// TODO: implement event handlers
|
||||
@@ -1,388 +0,0 @@
|
||||
/**
|
||||
* FortrezZ Water Consumption Metering
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "FortrezZ Water Consumption Metering",
|
||||
namespace: "FortrezZ",
|
||||
author: "FortrezZ, LLC",
|
||||
description: "Use the FortrezZ Water Meter to efficiently use your homes water system.",
|
||||
category: "Green Living",
|
||||
iconUrl: "http://swiftlet.technology/wp-content/uploads/2016/05/logo-square-200-1.png",
|
||||
iconX2Url: "http://swiftlet.technology/wp-content/uploads/2016/05/logo-square-500.png",
|
||||
iconX3Url: "http://swiftlet.technology/wp-content/uploads/2016/05/logo-square.png")
|
||||
|
||||
|
||||
preferences {
|
||||
page(name: "page2", title: "Select device and actions", install: true, uninstall: true)
|
||||
}
|
||||
|
||||
def page2() {
|
||||
dynamicPage(name: "page2") {
|
||||
section("Choose a water meter to monitor:") {
|
||||
input(name: "meter", type: "capability.energyMeter", title: "Water Meter", description: null, required: true, submitOnChange: true)
|
||||
}
|
||||
|
||||
if (meter) {
|
||||
section {
|
||||
app(name: "childRules", appName: "Consumption Metering", namespace: "FortrezZ", title: "Create New Water Consumption Goal", multiple: true)
|
||||
}
|
||||
}
|
||||
|
||||
section("Start/End time of all water usage goal periods") {
|
||||
input(name: "alertTime", type: "time", required: true)
|
||||
}
|
||||
|
||||
section("Billing info") {
|
||||
input(name: "unitType", type: "enum", title: "Water unit used in billing", description: null, defaultValue: "Gallons", required: true, submitOnChange: true, options: waterTypes())
|
||||
input(name: "costPerUnit", type: "decimal", title: "Cost of water unit in billing", description: null, defaultValue: 0, required: true, submitOnChange: true)
|
||||
input(name: "fixedFee", type: "decimal", title: "Add a Fixed Fee?", description: null, defaultValue: 0, submitOnChange: true)}
|
||||
|
||||
section("Send notifications through...") {
|
||||
input(name: "pushNotification", type: "bool", title: "SmartThings App", required: false)
|
||||
input(name: "smsNotification", type: "bool", title: "Text Message (SMS)", submitOnChange: true, required: false)
|
||||
if (smsNotification)
|
||||
{
|
||||
input(name: "phone", type: "phone", title: "Phone number?", required: true)
|
||||
}
|
||||
//input(name: "hoursBetweenNotifications", type: "number", title: "Hours between notifications", required: false)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
log.debug "there are ${childApps.size()} child smartapps"
|
||||
|
||||
|
||||
def childRules = []
|
||||
childApps.each {child ->
|
||||
log.debug "child ${child.id}: ${child.settings()}"
|
||||
childRules << [id: child.id, rules: child.settings()] //this section of code stores the long ID and settings (which contains several variables of the individual goal such as measurement type, water consumption goal, start cumulation, current cumulation.) into an array
|
||||
}
|
||||
|
||||
def match = false
|
||||
def changeOfSettings = false
|
||||
for (myItem in childRules) {
|
||||
def q = myItem.rules
|
||||
for (item2 in state.rules) {
|
||||
def r = item2.rules
|
||||
log.debug(r.alertType)
|
||||
if (myItem.id == item2.id) { //I am comparing the previous array to current array and checking to see if any new goals have been made.
|
||||
match = true
|
||||
if (q.type == r.type){
|
||||
changeOfSettings = true}
|
||||
}
|
||||
}
|
||||
if (match == false) { // if a new goal has been made, i need to do some first time things like set up a recurring schedule depending on goal duration
|
||||
state["NewApp${myItem.id}"] = true
|
||||
log.debug "Created a new ${q.type} with an ID of ${myItem.id}"}
|
||||
|
||||
match = false
|
||||
}
|
||||
|
||||
for (myItem in childRules) {
|
||||
if (state["NewApp${myItem.id}"] == true){
|
||||
state["NewApp${myItem.id}"] = false
|
||||
state["currentCumulation${myItem.id}"] = 0 // we create another object attached to our new goal called 'currentCumulation' which should hold the value for how much water has been used since the goal period has started
|
||||
state["oneHundred${myItem.id}"] = false
|
||||
state["ninety${myItem.id}"] = false
|
||||
state["seventyFive${myItem.id}"] = false
|
||||
state["fifty${myItem.id}"] = false
|
||||
state["endOfGoalPeriod${myItem.id}"] = false
|
||||
}
|
||||
}
|
||||
|
||||
state.rules = childRules // storing the array we just made to state makes it persistent across the instances this smart app is used and global across the app ( this value cannot be implicitely shared to any child app unfortunately without making it a local variable FYI
|
||||
log.debug "Parent Settings: ${settings}"
|
||||
|
||||
if (costPerUnit != 0 && costPerUnit != null){//we ask the user in the main page for billing info which includes the price of the water and what water measurement unit is used. we combine convert the unit to gallons (since that is what the FMI uses to tick water usage) and then create a ratio that can be converted to any water measurement type
|
||||
state.costRatio = costPerUnit/(convertToGallons(unitType))
|
||||
state.fixedFee = fixedFee
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def parseAlerTimeAndStartNewSchedule(myAlert)
|
||||
{
|
||||
def endTime = myAlert.split("T")
|
||||
def endHour = endTime[1].split(":")[0] // parsing the time stamp which is of this format: 2016-12-13T16:25:00.000-0500
|
||||
def endMinute = endTime[1].split(":")[1]
|
||||
schedule("0 ${endMinute} ${endHour} 1/1 * ? *", goalSearch) // creating a schedule to launch goalSearch every day at a user defined time - default is at midnight
|
||||
log.debug("new schedule created at ${endHour} : ${endMinute}")
|
||||
}
|
||||
|
||||
def convertToGallons(myUnit) // does what title says - takes whatever unit in string form and converts it to gallons to create a ratio. the result is returned
|
||||
{
|
||||
switch (myUnit){
|
||||
case "Gallons":
|
||||
return 1
|
||||
break
|
||||
case "Cubic Feet":
|
||||
return 7.48052
|
||||
break
|
||||
case "Liters":
|
||||
return 0.264172
|
||||
break
|
||||
case "Cubic Meters":
|
||||
return 264.172
|
||||
break
|
||||
default:
|
||||
log.debug "value for water measurement doesn't fit into the 4 water measurement categories"
|
||||
return 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def goalSearch(){
|
||||
|
||||
def dateTime = new Date() // this section is where we get date in time within our timezone and split it into 2 arrays which carry the date and time
|
||||
def fullDateTime = dateTime.format("yyyy-MM-dd HH:mm:ss", location.timeZone)
|
||||
def mySplit = fullDateTime.split()
|
||||
|
||||
log.debug("goalSearch: ${fullDateTime}") // 2016-12-09 14:59:56
|
||||
|
||||
// ok, so I ran into a problem here. I wanted to simply do | state.dateSplit = mySplit[0].split("-") | but I kept getting this error in the log "java.lang.UnsupportedOperationException" So I split it to variables and then individually placed them into the state array
|
||||
def dateSplit = mySplit[0].split("-")
|
||||
def timeSplit = mySplit[1].split(":")
|
||||
state.dateSplit = []
|
||||
state.timeSplit = []
|
||||
for (i in dateSplit){
|
||||
state.dateSplit << i} // unnecessary?
|
||||
for (g in timeSplit){
|
||||
state.timeSplit << g}
|
||||
def dayOfWeek = Date.parse("yyyy-MM-dd", mySplit[0]).format("EEEE")
|
||||
state.debug = false
|
||||
dailyGoalSearch(dateSplit, timeSplit)
|
||||
weeklyGoalSearch(dateSplit, timeSplit, dayOfWeek)
|
||||
monthlyGoalSearch(dateSplit, timeSplit)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
def dailyGoalSearch(dateSplit, timeSplit){ // because of our limitations of schedule() we had to create 3 separate methods for the existing goal period of day, month, and year. they are identical other than their time periods.
|
||||
def myRules = state.rules // also, these methods are called when our goal period ends. we filter out the goals that we want and then invoke a separate method called schedulGoal to inform the user that the goal ended and produce some results based on their water usage.
|
||||
for (it in myRules){
|
||||
def r = it.rules
|
||||
if (r.type == "Daily Goal") {
|
||||
scheduleGoal(r.measurementType, it.id, r.waterGoal, r.type, 0.03333)
|
||||
}
|
||||
}
|
||||
}
|
||||
def weeklyGoalSearch(dateSplit, timeSplit, dayOfWeek){
|
||||
def myRules = state.rules // also, these methods are called when our goal period ends. we filter out the goals that we want and then invoke a separate method called schedulGoal to inform the user that the goal ended and produce some results based on their water usage.
|
||||
for (it in myRules){
|
||||
def r = it.rules
|
||||
if (r.type == "Weekly Goal") {
|
||||
if (dayOfWeek == "Sunday" || state.debug == true){
|
||||
scheduleGoal(r.measurementType, it.id, r.waterGoal, r.type, 0.23333)}
|
||||
}
|
||||
}
|
||||
}
|
||||
def monthlyGoalSearch(dateSplit, timeSplit){
|
||||
def myRules = state.rules // also, these methods are called when our goal period ends. we filter out the goals that we want and then invoke a separate method called schedulGoal to inform the user that the goal ended and produce some results based on their water usage.
|
||||
for (it in myRules){
|
||||
def r = it.rules
|
||||
if (r.type == "Monthly Goal") {
|
||||
if (dateSplit[2] == "01" || state.debug == true){
|
||||
scheduleGoal(r.measurementType, it.id, r.waterGoal, r.type, 0.23333)}
|
||||
}
|
||||
}
|
||||
}
|
||||
def scheduleGoal(measureType, goalID, wGoal, goalType, fixedFeeRatio){ // this is where the magic happens. after a goal period has finished this method is invoked and the user gets a notification of the results of the water usage over their period.
|
||||
def cost = 0
|
||||
def f = 1.0f
|
||||
def topCumulative = meter.latestValue("cumulative") // pulling the current cumulative value from the FMI for calculating how much water we have used since starting the goal.
|
||||
if (state["Start${goalID}"] == null){state["Start${goalID}"] = topCumulative} // we create another object attached to our goal called 'start' and store the existing cumulation on the FMI device so we know at what mileage we are starting at for this goal. this is useful for determining how much water is used during the goal period.
|
||||
def curCumulation = waterConversionPreference(topCumulative, measureType) - waterConversionPreference(state["Start${goalID}"], measureType)
|
||||
|
||||
|
||||
if (state.costRatio){
|
||||
cost = costConversionPreference(state.costRatio,measureType) * curCumulation * f + (state.fixedFee * fixedFeeRatio)// determining the cost of the water that they have used over the period ( i had to create a variable 'f' and make it a float and multiply it to make the result a float. this is because the method .round() requires it to be a float for some reasons and it was easier than typecasting the result to a float.
|
||||
}
|
||||
def percentage = (curCumulation / wGoal) * 100 * f
|
||||
if (costPerUnit != 0) {
|
||||
notify("Your ${goalType} period has ended. You have used ${(curCumulation * f).round(2)} ${measureType} of your goal of ${wGoal} ${measureType} (${(percentage * f).round(1)}%). Costing \$${cost.round(2)}")// notifies user of the type of goal that finished, the amount of water they used versus the goal of water they used, and the cost of the water used
|
||||
log.debug "Your ${goalType} period has ended. You have used ${(curCumulation * f).round(2)} ${measureType} of your goal of ${wGoal} ${measureType} (${(percentage * f).round(1)}%). Costing \$${cost.round(2)}"
|
||||
|
||||
}
|
||||
if (costPerUnit == 0) // just in case the user didn't add any billing info, i created a second set of notification code to not include any billing info.
|
||||
{
|
||||
notify("Your ${goalType} period has ended. You have you have used ${(curCumulation * f).round(2)} ${measureType} of your goal of ${wGoal} ${measureType} (${percentage.round(1)}%).")
|
||||
log.debug "Your ${goalType} period has ended. You have you have used ${(curCumulation * f).round(2)} ${measureType} of your goal of ${wGoal} ${measureType} (${percentage.round(1)}%)."
|
||||
}
|
||||
state["Start${goalID}"] = topCumulative;
|
||||
state["oneHundred${goalID}"] = false
|
||||
state["ninety${goalID}"] = false
|
||||
state["seventyFive${goalID}"] = false
|
||||
state["fifty${goalID}"] = false
|
||||
state["endOfGoalPeriod${goalID}"] = true // telling the app that the goal period is over.
|
||||
}
|
||||
|
||||
|
||||
|
||||
def waterTypes() // holds the types of water measurement used in the main smartapp page for billing info and for setting goals
|
||||
{
|
||||
def watertype = []
|
||||
|
||||
watertype << "Gallons"
|
||||
watertype << "Cubic Feet"
|
||||
watertype << "Liters"
|
||||
watertype << "Cubic Meters"
|
||||
return watertype
|
||||
}
|
||||
|
||||
def installed() { // when the app is first installed - do something
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() { // whevenever the app is updated in any way by the user and you press the 'done' button on the top right of the app - do something
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
|
||||
if (alertTime != state.alertTime) // we created this 'if' statement to prevent another schedule being made whenever the user opens the smartapp
|
||||
{
|
||||
unschedule() //unscheduling is a good idea here because we don't want multiple schedules happening and this function cancles all schedules
|
||||
parseAlerTimeAndStartNewSchedule(alertTime) // we use cron scheduling to use the function 'goalSearch' every minute
|
||||
state.alarmTime = alarmTime // setting state.alarmTime prevents a new schedule being made whenever the user opens the smartapp
|
||||
}
|
||||
|
||||
unsubscribe()
|
||||
initialize()
|
||||
//unschedule()
|
||||
}
|
||||
|
||||
def initialize() { // whenever you open the smart app - do something
|
||||
subscribe(meter, "cumulative", cumulativeHandler)
|
||||
//subscribe(meter, "gpm", gpmHandler)
|
||||
log.debug("Subscribing to events")
|
||||
}
|
||||
|
||||
def cumulativeHandler(evt) { // every time a tick on the FMI happens this method is called. 'evt' contains the cumulative value of every tick that has happened on the FMI since it was last reset. each tick represents 1/10 of a gallon
|
||||
def f = 1.0f
|
||||
def gpm = meter.latestValue("gpm") // storing the current gallons per minute value
|
||||
def cumulative = new BigDecimal(evt.value) // storing the current cumulation value
|
||||
log.debug "Cumulative Handler: [gpm: ${gpm}, cumulative: ${cumulative}]"
|
||||
def rules = state.rules //storing the array of child apps to 'rules'
|
||||
rules.each { it -> // looping through each app in the array but storing each app into the variable 'it'
|
||||
def r = it.rules // each child app has a 2 immediate properties, one called 'id' and one called 'rules' - so 'r' contains the values of 'rules' in the child app
|
||||
def childAppID = it.id // storing the child app ID to a variable
|
||||
if (state["Start${childAppID}"] == null) {state["Start${childAppID}"] = cumulative}// just for the first run of the app... start should be null. so we have to change that for the logic to work.
|
||||
|
||||
|
||||
def newCumulative = waterConversionPreference(cumulative, r.measurementType) //each goal allows the user to choose a water measurement type. here we convert the value of 'cumulative' to whatever the user prefers for display and logic purposes
|
||||
def goalStartCumulative = waterConversionPreference(state["Start${childAppID}"], r.measurementType)
|
||||
|
||||
|
||||
def DailyGallonGoal = r.waterGoal // 'r.waterGoal' contains the number of units of water the user set as a goal. we then save that to 'DailyGallonGoal'
|
||||
state.DailyGallonGoal = DailyGallonGoal // and then we make that value global and persistent for logic reasons
|
||||
def currentCumulation = newCumulative - goalStartCumulative // earlier we created the value 'currentCumulation' and set it to 0, now we are converting both the 'cumulative' value and what 'cumulative' was when the goal perio was made and subtracting them to discover how much water has been used since the creation of the goal in the users prefered water measurement unit.
|
||||
state["currentCumulation${childAppID}"] = currentCumulation
|
||||
log.debug("Threshold:${DailyGallonGoal}, Value:${(currentCumulation * f).round(2)}")
|
||||
|
||||
if ( currentCumulation >= (0.5 * DailyGallonGoal) && currentCumulation < (0.75 * DailyGallonGoal) && state["fifty${childAppID}"] == false) // tell the user if they break certain use thresholds
|
||||
{
|
||||
notify("You have reached 50% of your ${r.type} use limit. (${(currentCumulation * f).round(2)} of ${DailyGallonGoal} ${r.measurementType})")
|
||||
log.debug "You have reached 50% of your ${r.type} use limit. (${(currentCumulation * f).round(2)} of ${DailyGallonGoal} ${r.measurementType})"
|
||||
state["fifty${childAppID}"] = true
|
||||
}
|
||||
if ( currentCumulation >= (0.75 * DailyGallonGoal) && currentCumulation < (0.9 * DailyGallonGoal) && state["seventyFive${childAppID}"] == false)
|
||||
{
|
||||
notify("You have reached 75% of your ${r.type} use limit. (${(currentCumulation * f).round(2)} of ${DailyGallonGoal} ${r.measurementType})")
|
||||
log.debug "You have reached 75% of your ${r.type} use limit. (${(currentCumulation * f).round(2)} of ${DailyGallonGoal} ${r.measurementType})"
|
||||
state["seventyFive${childAppID}"] = true
|
||||
}
|
||||
if ( currentCumulation >= (0.9 * DailyGallonGoal) && currentCumulation < (DailyGallonGoal) && state["ninety${childAppID}"] == false)
|
||||
{
|
||||
notify("You have reached 90% of your ${r.type} use limit. (${(currentCumulation * f).round(2)} of ${DailyGallonGoal} ${r.measurementType})")
|
||||
log.debug "You have reached 90% of your ${r.type} use limit. (${(currentCumulation * f).round(2)} of ${DailyGallonGoal} ${r.measurementType})"
|
||||
state["ninety${childAppID}"] = true
|
||||
}
|
||||
if (currentCumulation >= DailyGallonGoal && state["oneHundred${childAppID}"] == false)
|
||||
{
|
||||
notify("You have reached 100% of your ${r.type} use limit. (${(currentCumulation * f).round(2)} of ${DailyGallonGoal} ${r.measurementType})")
|
||||
log.debug "You have reached 100% of your ${r.type} use limit. (${(currentCumulation * f).round(2)} of ${DailyGallonGoal} ${r.measurementType})"
|
||||
state["oneHundred${childAppID}"] = true
|
||||
//send command here like shut off the water
|
||||
|
||||
|
||||
|
||||
}
|
||||
if (state["endOfGoalPeriod${childAppID}"] == true) // changing the start value to the most recent cumulative value for goal reset.
|
||||
{state["Start${childAppID}"] = cumulative
|
||||
state["endOfGoalPeriod${childAppID}"] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def waterConversionPreference(cumul, measurementType1) // convert the current cumulation to one of the four options below - since cumulative is initially in gallons, then the options to change them is easy
|
||||
{
|
||||
switch (measurementType1)
|
||||
{
|
||||
case "Cubic Feet":
|
||||
return (cumul * 0.133681)
|
||||
break
|
||||
|
||||
case "Liters":
|
||||
return (cumul * 3.78541)
|
||||
break
|
||||
|
||||
case "Cubic Meters":
|
||||
return (cumul * 0.00378541)
|
||||
break
|
||||
|
||||
case "Gallons":
|
||||
return cumul
|
||||
break
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
def costConversionPreference(cumul, measurementType1) // convert the current cumulation to one of the four options below - since cumulative is initially in gallons, then the options to change them is easy
|
||||
{
|
||||
switch (measurementType1)
|
||||
{
|
||||
case "Cubic Feet":
|
||||
return (cumul / 0.133681)
|
||||
break
|
||||
|
||||
case "Liters":
|
||||
return (cumul / 3.78541)
|
||||
break
|
||||
|
||||
case "Cubic Meters":
|
||||
return (cumul / 0.00378541)
|
||||
break
|
||||
|
||||
case "Gallons":
|
||||
return cumul
|
||||
break
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
def notify(myMsg) // method for both push notifications and for text messaging.
|
||||
{
|
||||
log.debug("Sending Notification")
|
||||
if (pushNotification) {sendPush(myMsg)} else {sendNotificationEvent(myMsg)}
|
||||
if (smsNotification) {sendSms(phone, myMsg)}
|
||||
}
|
||||
|
||||
def uninstalled() {
|
||||
// external cleanup. No need to unsubscribe or remove scheduled jobs
|
||||
unsubscribe()
|
||||
unschedule()
|
||||
}
|
||||
Reference in New Issue
Block a user