Compare commits

..

1 Commits

4 changed files with 481 additions and 598 deletions

View File

@@ -0,0 +1,102 @@
/**
* FortrezZ MIMO2+ B-Side
*
* 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 MIMO2+ B-Side", namespace: "fortrezz", author: "FortrezZ, LLC") {
capability "Contact Sensor"
capability "Relay Switch"
capability "Switch"
capability "Voltage Measurement"
capability "Refresh"
}
tiles {
standardTile("switch", "device.switch", width: 2, height: 2) {
state "on", label: "Relay 2 On", action: "off", icon: "http://swiftlet.technology/wp-content/uploads/2016/06/Switch-On-104-edit.png", backgroundColor: "#53a7c0"
state "off", label: "Relay 2 Off", action: "on", icon: "http://swiftlet.technology/wp-content/uploads/2016/06/Switch-Off-104-edit.png", backgroundColor: "#ffffff"
}
standardTile("anaDig1", "device.anaDig1", inactiveLabel: false) {
state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#ffa81e"
state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#79b821"
state "val", label:'${currentValue}v', unit:"", defaultState: true
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
}
standardTile("powered", "device.powered", inactiveLabel: false) {
state "powerOn", label: "Power On", icon: "st.switches.switch.on", backgroundColor: "#79b821"
state "powerOff", label: "Power Off", icon: "st.switches.switch.off", backgroundColor: "#ffa81e"
}
standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") {
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
}
standardTile("blank", "device.blank", inactiveLabel: true, decoration: "flat") {
state("blank", label: '')
}
main (["switch"])
details(["switch", "anaDig1", "blank", "blank", "refresh", "powered"])
}
}
// parse events into attributes
def parse(String description) {
log.debug "Parsing '${description}'"
// TODO: handle 'contact' attribute
// TODO: handle 'switch' attribute
// TODO: handle 'switch' attribute
// TODO: handle 'voltage' attribute
}
def eventParse(evt) {
log.debug("Event: ${evt.name}=${evt.value}")
switch(evt.name) {
case "powered":
sendEvent(name: evt.name, value: evt.value)
break
case "switch2":
sendEvent(name: "switch", value: evt.value)
break
case "contact2":
sendEvent(name: "contact", value: evt.value)
break
case "voltage2":
sendEvent(name: "voltage", value: evt.value)
break
case "relay2":
sendEvent(name: evt.name, value: evt.value)
break
case "anaDig2":
sendEvent(name: "anaDig1", value: evt.value)
break
}
}
// handle commands
def on() {
parent.on2(device.id)
log.debug("Executing 'on'")
// TODO: Send Event to parent device for "on2"
}
def off() {
parent.off2(device.id)
log.debug("Executing 'off'")
// TODO: Send Event to parent device for "off2"
}
def refresh() {
parent.refresh2(device.id)
log.debug("Executing 'refresh'")
}

View File

@@ -0,0 +1,379 @@
/**
* MIMO2 Device Handler
*
* 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 MIMO2+", namespace: "fortrezz", author: "FortrezZ, LLC") {
capability "Alarm"
capability "Contact Sensor"
capability "Switch"
capability "Voltage Measurement"
capability "Configuration"
capability "Refresh"
attribute "powered", "string"
attribute "relay", "string"
attribute "relay2", "string"
attribute "contact2", "string"
attribute "voltage2", "string"
command "on"
command "off"
command "on2"
command "off2"
fingerprint deviceId: "0x2100", inClusters: "0x5E,0x86,0x72,0x5A,0x59,0x71,0x98,0x7A"
}
preferences {
input ("RelaySwitchDelay", "decimal", title: "Delay between relay switch on and off in seconds. Only Numbers 0 to 3 allowed. 0 value will remove delay and allow relay to function as a standard switch:\nRelay 1", description: "Numbers 0 to 3.1 allowed.", defaultValue: 0, required: false, displayDuringSetup: true)
input ("RelaySwitchDelay2", "decimal", title: "Relay 2", description: "Numbers 0 to 3.1 allowed.", defaultValue: 0, required: false, displayDuringSetup: true)
input ("Sig1AD", "bool", title: "Switch off for digital, on for analog:\nSIG1", required: false, displayDuringSetup: true)
input ("Sig2AD", "bool", title: "SIG2", required: false, displayDuringSetup: true)
} // the range would be 0 to 3.1, but the range value would not accept 3.1, only whole numbers (i tried paranthesis and fractions too. :( )
tiles {
standardTile("switch", "device.switch", width: 2, height: 2) {
state "on", label: "Relay 1 On", action: "off", icon: "http://swiftlet.technology/wp-content/uploads/2016/06/Switch-On-104-edit.png", backgroundColor: "#53a7c0"
state "off", label: "Relay 1 Off", action: "on", icon: "http://swiftlet.technology/wp-content/uploads/2016/06/Switch-Off-104-edit.png", backgroundColor: "#ffffff"
}
standardTile("switch2", "device.switch2", width: 2, height: 2, inactiveLabel: false) {
state "on", label: "Relay 2 On", action: "off2", icon: "http://swiftlet.technology/wp-content/uploads/2016/06/Switch-On-104-edit.png", backgroundColor: "#53a7c0"
state "off", label: 'Relay 2 Off', action: "on2", icon: "http://swiftlet.technology/wp-content/uploads/2016/06/Switch-Off-104-edit.png", backgroundColor: "#ffffff"
}
standardTile("anaDig1", "device.anaDig1", inactiveLabel: false) {
state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#ffa81e"
state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#79b821"
state "val", label:'${currentValue}v', unit:"", defaultState: true
}
standardTile("anaDig2", "device.anaDig2", inactiveLabel: false) {
state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#ffa81e"
state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#79b821"
state "val", label:'${currentValue}v', unit:"", defaultState: true
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
}
standardTile("powered", "device.powered", inactiveLabel: false) {
state "powerOn", label: "Power On", icon: "st.switches.switch.on", backgroundColor: "#79b821"
state "powerOff", label: "Power Off", icon: "st.switches.switch.off", backgroundColor: "#ffa81e"
}
standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") {
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
}
standardTile("blank", "device.blank", inactiveLabel: true, decoration: "flat") {
state("blank", label: '')
}
main (["switch"])
details(["switch", "anaDig1", "blank", "switch2", "anaDig2", "blank", "configure", "refresh", "powered"])
}
}
// parse events into attributes
def parse(String description) {
def result = null
def cmd = zwave.parse(description)
if (cmd.CMD == "7105") { //Mimo sent a power loss report
log.debug "Device lost power"
sendEvent(name: "powered", value: "powerOff", descriptionText: "$device.displayName lost power")
} else {
sendEvent(name: "powered", value: "powerOn", descriptionText: "$device.displayName regained power")
}
if (cmd) {
def eventReturn = zwaveEvent(cmd)
if(eventReturn in physicalgraph.device.HubMultiAction) {
result = eventReturn
}
else {
result = createEvent(eventReturn)
}
}
log.debug "Parse returned ${result} $cmd.CMD"
return result
}
def updated() { // neat built-in smartThings function which automatically runs whenever any setting inputs are changed in the preferences menu of the device handler
if (state.count == 1) // this bit with state keeps the function from running twice ( which it always seems to want to do) (( oh, and state.count is a variable which is nonVolatile and doesn't change per every parse request.
{
state.count = 0
log.debug "Settings Updated..."
return response(delayBetween([
configure(), // the response() function is used for sending commands in reponse to an event, without it, no zWave commands will work for contained function
refresh()
], 200))
}
else {state.count = 1}
}
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) // basic set is essentially our digital sensor for SIG1 and SIG2 - it doesn't use an endpoint so we are having it send a multilevelGet() for SIG1 and SIG2 to see which one triggered.
{
log.debug "sent a BasicSet command"
return response(refresh())
}
def zwaveEvent(int endPoint, physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) // event to get the state of the digital sensor SIG1 and SIG2
{
log.debug "sent a sensorBinaryReport command"
return response(refresh())
}
def zwaveEvent(int endPoint, physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) // event for seeing the states of relay 1 and relay 2
{
def map = [:] // map for containing the name and state fo the specified relay
if (endPoint == 3)
{
if (cmd.value) // possible values are 255 and 0 (0 is false)
{map.value = "on"}
else
{map.value = "off"}
map.name = "switch"
log.debug "sent a SwitchBinary command $map.name $map.value" // the map is only used for debug messages. not for the return command to the device
return [name: "switch", value: cmd.value ? "on" : "off"]
}
else if (endPoint == 4)
{
if (cmd.value)
{map.value = "on"}
else
{map.value = "off"}
map.name = "switch2"
sendEvent(name: "relay2", value: "$map.value")
log.debug "sent a SwitchBinary command $map.name $map.value" // the map is only used for debug messages. not for the return command to the device
return [name: "switch2", value: cmd.value ? "on" : "off"]
}
}
def zwaveEvent (int endPoint, physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) // sensorMultilevelReport is used to report the value of the analog voltage for SIG1
{
def map = [:]
def stdEvent = [:]
def voltageVal = CalculateVoltage(cmd.scaledSensorValue) // saving the scaled Sensor Value used to enter into a large formula to determine actual voltage value
if (endPoint == 1) //endPoint 1 is for SIG1
{
if (state.AD1 == false) // state.AD1 is to determine which state the anaDig1 tile should be in (either analogue or digital mode)
{
map.name = "anaDig1"
stdEvent.name = "contact"
if (voltageVal < 2) { // DK changed to 2v to follow LED behavior
map.value = "closed"
stdEvent.value = "closed"
}
else
{
map.value = "open"
stdEvent.value = "open"
}
}
else //or state.AD1 is true for analogue mode
{
map.name = "anaDig1"
stdEvent.name = "voltage"
map.value = voltageVal
stdEvent.value = voltageVal
map.unit = "v"
stdEvent.unit = "v"
}
}
else if (endPoint == 2) // endpoint 2 is for SIG2
{
if (state.AD2 == false)
{
map.name = "anaDig2"
stdEvent.name = "contact2"
if (voltageVal < 2) {
map.value = "closed"
stdEvent.value = "closed"
}
else
{
map.value = "open"
stdEvent.value = "open"
}
}
else
{
map.name = "anaDig2"
stdEvent.name = "voltage2"
map.value = voltageVal
stdEvent.value = voltageVal
map.unit = "v"
stdEvent.unit = "v"
}
}
log.debug "sent a SensorMultilevelReport $map.name $map.value"
sendEvent(stdEvent)
return map
}
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { //standard security encapsulation event code (should be the same on all device handlers)
def encapsulatedCommand = cmd.encapsulatedCommand()
// can specify command class versions here like in zwave.parse
if (encapsulatedCommand) {
return zwaveEvent(encapsulatedCommand)
}
}
// MultiChannelCmdEncap and MultiInstanceCmdEncap are ways that devices
// can indicate that a message is coming from one of multiple subdevices
// or "endpoints" that would otherwise be indistinguishable
def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) {
def encapsulatedCommand = cmd.encapsulatedCommand()
log.debug ("Command from endpoint ${cmd.sourceEndPoint}: ${encapsulatedCommand}")
if (encapsulatedCommand) {
return zwaveEvent(cmd.sourceEndPoint, encapsulatedCommand)
}
}
def zwaveEvent(int endPoint, physicalgraph.zwave.commands.multichannelassociationv2.MultiChannelAssociationReport cmd) {
log.debug "sent an Association Report"
log.debug " ${cmd.groupingIdentifier}"
//return [:]
}
def zwaveEvent(physicalgraph.zwave.Command cmd) {
// Handles all Z-Wave commands we aren't interested in
log.debug("Un-parsed Z-Wave message ${cmd}")
return [:]
}
def CalculateVoltage(ADCvalue) // used to calculate the voltage based on the collected Scaled sensor value of the multilevel sensor event
{
def volt = (((2.396*(10**-17))*(ADCvalue**5)) - ((1.817*(10**-13))*(ADCvalue**4)) + ((5.087*(10**-10))*(ADCvalue**3)) - ((5.868*(10**-7))*(ADCvalue**2)) + ((9.967*(10**-4))*(ADCvalue)) - (1.367*(10**-2)))
return volt.round(1)
}
def configure() {
log.debug "Configuring...."
def sig1
def sig2
if (Sig1AD == true)
{ sig1 = 0x01
state.AD1 = true}
else if (Sig1AD == false)
{ sig1 = 0x40
state.AD1 = false}
if (Sig2AD == true)
{ sig2 = 0x01
state.AD2 = true}
else if (Sig2AD == false)
{ sig2 = 0x40
state.AD2 = false}
def delay = (RelaySwitchDelay*10).toInteger() // the input which we get from the user is a string and is in seconds while the MIMO2 configuration requires it in 100ms so - change to integer and multiply by 10
def delay2 = (RelaySwitchDelay2*10).toInteger() // the input which we get from the user is a string and is in seconds while the MIMO2 configuration requires it in 100ms so - change to integer and multiply by 10
if (delay > 31)
{
log.debug "Relay 1 input ${delay / 10} set too high. Max value is 3.1"
delay = 31
}
if (delay < 0)
{
log.debug "Relay 1 input ${delay / 10} set too low. Min value is 0"
delay = 0
}
if (delay2 > 31)
{
log.debug "Relay 2 input ${delay2 / 10} set too high. Max value is 3.1"
delay2 = 31
}
if (delay2 < 0)
{
log.debug "Relay 2 input ${delay2 / 10} set too low. Min value is 0"
delay = 0
}
return delayBetween([
encap(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]), 0),
encap(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier:2, nodeId:[zwaveHubNodeId]), 0),
encap(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier:2, nodeId:[zwaveHubNodeId]), 1),
encap(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier:2, nodeId:[zwaveHubNodeId]), 2),
encap(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]), 3),
encap(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]), 4),
secure(zwave.configurationV1.configurationSet(configurationValue: [sig1], parameterNumber: 3, size: 1)), // sends a multiLevelSensor report every 30 seconds for SIG1
secure(zwave.configurationV1.configurationSet(configurationValue: [sig2], parameterNumber: 9, size: 1)), // sends a multiLevelSensor report every 30 seconds for SIG2
secure(zwave.configurationV1.configurationSet(configurationValue: [delay], parameterNumber: 1, size: 1)), // configurationValue for parameterNumber means how many 100ms do you want the relay
// to wait before it cycles again / size should just be 1 (for 1 byte.)
secure(zwave.configurationV1.configurationSet(configurationValue: [delay2], parameterNumber: 2, size: 1)),
], 200)
}
def on() {
return encap(zwave.basicV1.basicSet(value: 0xff), 3) // physically changes the relay from on to off and requests a report of the relay
// oddly, smartThings automatically sends a switchBinaryGet() command whenever the above basicSet command is sent, so we don't need to send one here.
}
def off() {
return encap(zwave.basicV1.basicSet(value: 0x00), 3) // physically changes the relay from on to off and requests a report of the relay
// oddly, smartThings automatically sends a switchBinaryGet() command whenever the above basicSet command is sent, so we don't need to send one here.
}
def on2() {
return encap(zwave.basicV1.basicSet(value: 0xff), 4)
// oddly, smartThings automatically sends a switchBinaryGet() command whenever the above basicSet command is sent, so we don't need to send one here.
}
def off2() {
return encap(zwave.basicV1.basicSet(value: 0x00), 4)
// oddly, smartThings automatically sends a switchBinaryGet() command whenever the above basicSet command is sent, so we don't need to send one here.
}
def refresh() {
log.debug "Refresh"
return delayBetween([
encap(zwave.sensorMultilevelV5.sensorMultilevelGet(), 1),// requests a report of the anologue input voltage for SIG1
encap(zwave.sensorMultilevelV5.sensorMultilevelGet(), 2),// requests a report of the anologue input voltage for SIG2
encap(zwave.switchBinaryV1.switchBinaryGet(), 3), //requests a report of the relay to make sure that it changed for Relay 1
encap(zwave.switchBinaryV1.switchBinaryGet(), 4), //requests a report of the relay to make sure that it changed for Relay 2
],200)
}
def refreshZWave() {
log.debug "Refresh (Z-Wave Response)"
return delayBetween([
encap(zwave.sensorMultilevelV5.sensorMultilevelGet(), 1),// requests a report of the anologue input voltage for SIG1
encap(zwave.sensorMultilevelV5.sensorMultilevelGet(), 2),// requests a report of the anologue input voltage for SIG2
encap(zwave.switchBinaryV1.switchBinaryGet(), 3), //requests a report of the relay to make sure that it changed for Relay 1
encap(zwave.switchBinaryV1.switchBinaryGet(), 4) //requests a report of the relay to make sure that it changed for Relay 2
],200)
}
private secureSequence(commands, delay=200) { // decided not to use this
return delayBetween(commands.collect{ secure(it) }, delay)
}
private secure(physicalgraph.zwave.Command cmd) { //take multiChannel message and securely encrypts the message so the device can read it
return zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
}
private encap(cmd, endpoint) { // takes desired command and encapsulates it by multiChannel and then sends it to secure() to be wrapped with another encapsulation for secure encryption
if (endpoint) {
return secure(zwave.multiChannelV3.multiChannelCmdEncap(bitAddress: false, sourceEndPoint:0, destinationEndPoint: endpoint).encapsulate(cmd))
} else {
return secure(cmd)
}
}

View File

@@ -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

View File

@@ -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()
}