├── FortrezZ Flood and Temperature Sensor Gen 3 └── devicehandler.groovy ├── README ├── flow meter (fmi) ├── Consumption Metering │ ├── smartapp-consumption-child.groovy │ └── smartapp-consumption-parent.groovy ├── Leak Detector SmartApp │ ├── smartapp-leakdetector-child.groovy │ └── smartapp-leakdetector-parent.groovy ├── devicehandler-largePipes.groovy └── devicehandler.groovy ├── mimo2 ├── devicehandler-a.groovy ├── devicehandler-b.groovy └── smartapp.groovy ├── mimolite └── devicehandler.groovy └── smartsense moisture └── devicehandler.groovy /FortrezZ Flood and Temperature Sensor Gen 3/devicehandler.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 SmartThings 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 11 | * for the specific language governing permissions and limitations under the License. 12 | * 13 | */ 14 | metadata { 15 | definition (name: "Wireless Flood / Temperature Sensor", namespace: "fortrezz", author: "FortrezZ, LLC") { 16 | capability "Water Sensor" 17 | capability "Sensor" 18 | capability "Battery" 19 | capability "Temperature Measurement" 20 | capability "Polling" 21 | 22 | fingerprint mfr: "0084", prod: "0073" 23 | fingerprint mfr: "0072", prod: "0500" 24 | //zw:S type:0701 mfr:0084 prod:0073 model:0005 ver:0.05 zwv:4.38 lib:06 cc:5E,86,72,5A,73,20,80,71,85,59,84,31,70 role:06 ff:8C05 ui:8C05 25 | } 26 | preferences { 27 | input ("version", "text", title: "Plugin Version 1.5", description:"", required: false, displayDuringSetup: true) 28 | } 29 | 30 | simulator { 31 | status "dry": "command: 7105, payload: 00 00 00 FF 05 FE 00 00" 32 | status "wet": "command: 7105, payload: 00 FF 00 FF 05 02 00 00" 33 | status "overheated": "command: 7105, payload: 00 00 00 FF 04 02 00 00" 34 | status "freezing": "command: 7105, payload: 00 00 00 FF 04 05 00 00" 35 | status "normal": "command: 7105, payload: 00 00 00 FF 04 FE 00 00" 36 | for (int i = 0; i <= 100; i += 20) { 37 | status "battery ${i}%": new physicalgraph.zwave.Zwave().batteryV1.batteryReport(batteryLevel: i).incomingMessage() 38 | } 39 | } 40 | 41 | tiles(scale: 2) { 42 | multiAttributeTile(name:"water", type: "generic", width: 6, height: 4){ 43 | tileAttribute ("device.water", key: "PRIMARY_CONTROL") { 44 | attributeState "dry", label: "Dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff" 45 | attributeState "wet", label: "Wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0" 46 | } 47 | } 48 | standardTile("temperatureState", "device.temperature", width: 2, height: 2) { 49 | state "normal", icon:"st.alarm.temperature.normal", backgroundColor:"#ffffff" 50 | state "freezing", icon:"st.alarm.temperature.freeze", backgroundColor:"#53a7c0" 51 | state "overheated", icon:"st.alarm.temperature.overheat", backgroundColor:"#F80000" 52 | } 53 | valueTile("temperature", "device.temperature", width: 2, height: 2) { 54 | state("temperature", label:'${currentValue}°', 55 | backgroundColors:[ 56 | [value: 31, color: "#153591"], 57 | [value: 44, color: "#1e9cbb"], 58 | [value: 59, color: "#90d2a7"], 59 | [value: 74, color: "#44b621"], 60 | [value: 84, color: "#f1d801"], 61 | [value: 95, color: "#d04e00"], 62 | [value: 96, color: "#bc2323"] 63 | ] 64 | ) 65 | } 66 | valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { 67 | state "battery", label:'${currentValue}% battery', unit:"" 68 | } 69 | standardTile("powered", "device.powered", width: 2, height: 2, inactiveLabel: false) { 70 | // state blank for non-mains 71 | state "powerOn", label: "Power On", icon: "st.switches.switch.on", backgroundColor: "#79b821" 72 | state "powerOff", label: "Power Off", icon: "st.switches.switch.off", backgroundColor: "#ffa81e" 73 | } 74 | valueTile("poll", "device.poll", width: 2, height: 2, canChangeIcon: false, canChangeBackground: false, decoration: "flat") { 75 | state "blank", label:'' 76 | // state "zero", label:'Poll', action: 'poll' 77 | } 78 | valueTile("systemStatus", "device.systemStatus", width: 2, height: 2, canChangeIcon: false, canChangeBackground: false, decoration: "flat") { 79 | state "blank", label:'' 80 | } 81 | main (["water", "temperatureState"]) 82 | details(["water", "temperature", "temperatureState", "battery", "systemStatus", "poll", "powered"]) 83 | } 84 | } 85 | 86 | def poll() { 87 | // Get Temperature 88 | // Get Wet Status 89 | return delayBetween([ 90 | zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1).format(), 91 | zwave.notificationV3.notificationGet().format(), 92 | zwave.batteryV1.batteryGet().format() 93 | ], 200) 94 | } 95 | 96 | def installed() { 97 | log.debug "Device Installed..." 98 | return response(configure()) 99 | } 100 | 101 | def updated() { // neat built-in smartThings function which automatically runs whenever any setting inputs are changed in the preferences menu of the device handler 102 | 103 | log.debug "Settings Updated..." 104 | return response(delayBetween([ 105 | secure(configure()), // the response() function is used for sending commands in reponse to an event, without it, no zWave commands will work for contained function 106 | secure(zwave.associationV2.associationGet(groupingIdentifier:8)) 107 | ], 200)) 108 | 109 | } 110 | 111 | def parse(String description) { 112 | def result = [] 113 | def parsedZwEvent = zwave.parse(description, [0x30: 1, 0x71: 3, 0x84: 1]) 114 | 115 | log.debug("Raw Event: ${parsedZwEvent}") 116 | if(parsedZwEvent) { 117 | if(parsedZwEvent.CMD == "8407") { 118 | def lastStatus = device.currentState("battery") 119 | def ageInMinutes = lastStatus ? (new Date().time - lastStatus.date.time)/60000 : 600 120 | log.debug "Battery status was last checked ${ageInMinutes} minutes ago" 121 | 122 | if (ageInMinutes >= 600) { 123 | log.debug "Battery status is outdated, requesting battery report" 124 | result << new physicalgraph.device.HubAction(zwave.batteryV1.batteryGet().format()) 125 | } 126 | result << new physicalgraph.device.HubAction(zwave.wakeUpV1.wakeUpNoMoreInformation().format()) 127 | } 128 | result << createEvent( zwaveEvent(parsedZwEvent) ) 129 | } 130 | if(!result) result = [ descriptionText: parsedZwEvent, displayed: false ] 131 | log.debug "Parse returned ${result}" 132 | return result 133 | } 134 | 135 | def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { //standard security encapsulation event code (should be the same on all device handlers) 136 | def encapsulatedCommand = cmd.encapsulatedCommand() 137 | // can specify command class versions here like in zwave.parse 138 | if (encapsulatedCommand) { 139 | return zwaveEvent(encapsulatedCommand) 140 | } 141 | } 142 | 143 | def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) 144 | { 145 | [descriptionText: "${device.displayName} woke up", isStateChange: false] 146 | } 147 | 148 | def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) 149 | { 150 | def map = [:] 151 | map.name = "water" 152 | map.value = cmd.sensorValue ? "wet" : "dry" 153 | map.descriptionText = "${device.displayName} is ${map.value}" 154 | map 155 | } 156 | 157 | def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { 158 | def map = [:] 159 | if(cmd.batteryLevel == 0xFF) { 160 | map.name = "battery" 161 | map.value = 1 162 | map.descriptionText = "${device.displayName} has a low battery" 163 | map.displayed = true 164 | } else { 165 | map.name = "battery" 166 | map.value = cmd.batteryLevel > 0 ? cmd.batteryLevel.toString() : 1 167 | map.unit = "%" 168 | map.displayed = false 169 | } 170 | map 171 | } 172 | 173 | def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) 174 | { 175 | def map = [:] 176 | if (cmd.notificationType == physicalgraph.zwave.commands.notificationv3.NotificationReport.NOTIFICATION_TYPE_WATER) { 177 | map.name = "water" 178 | if(cmd.event == 1 || cmd.event == 2) { 179 | map.value = "wet" 180 | } 181 | else if (cmd.event == 0) { 182 | map.value = "dry" 183 | } 184 | map.descriptionText = "${device.displayName} is ${map.value}" 185 | } 186 | if(cmd.notificationType == physicalgraph.zwave.commands.notificationv3.NotificationReport.NOTIFICATION_TYPE_HEAT) { 187 | map.name = "temperatureState" 188 | if(cmd.event == 1 || cmd.event == 2) { map.value = "overheated"} 189 | if(cmd.event == 3 || cmd.event == 4) { map.value = "changing temperature rapidly"} 190 | if(cmd.event == 5 || cmd.event == 5) { map.value = "freezing"} 191 | if(cmd.event == 0 || cmd.event == 254) { map.value = "normal"} 192 | map.descriptionText = "${device.displayName} is ${map.value}" 193 | } 194 | if (cmd.notificationType == physicalgraph.zwave.commands.notificationv3.NotificationReport.NOTIFICATION_TYPE_POWER_MANAGEMENT) { 195 | map.name = "powered" 196 | if(cmd.event == 2 || cmd.event == 0x0B) { 197 | map.value = "powerOff" 198 | } 199 | else if (cmd.event == 3 || cmd.event == 0x0D) { 200 | map.value = "powerOn" 201 | } 202 | map.descriptionText = "${device.displayName} is ${map.value}" 203 | } 204 | 205 | map 206 | } 207 | 208 | def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) 209 | { 210 | def map = [:] 211 | if(cmd.sensorType == 1) { 212 | map.name = "temperature" 213 | if(cmd.scale == 0) { 214 | map.value = getTemperature(cmd.scaledSensorValue) 215 | } else { 216 | map.value = cmd.scaledSensorValue 217 | } 218 | map.unit = location.temperatureScale 219 | } 220 | map 221 | } 222 | 223 | def configure() { 224 | log.debug "Configuring...." 225 | return zwave.associationV2.associationSet(groupingIdentifier:8, nodeId:[zwaveHubNodeId]) 226 | } 227 | 228 | private secure(physicalgraph.zwave.Command cmd) { //take multiChannel message and securely encrypts the message so the device can read it 229 | return zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() 230 | } 231 | 232 | def getTemperature(value) { 233 | if(location.temperatureScale == "C"){ 234 | return value 235 | } else { 236 | return Math.round(celsiusToFahrenheit(value)) 237 | } 238 | } 239 | 240 | def zwaveEvent(physicalgraph.zwave.Command cmd) 241 | { 242 | log.debug "COMMAND CLASS: $cmd" 243 | } -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Public Files for manual install for FortrezZ products. For more information, visit http://www.fortrezz.com -------------------------------------------------------------------------------- /flow meter (fmi)/Consumption Metering/smartapp-consumption-child.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Consumption Metering 3 | * 4 | * Copyright 2016 FortrezZ, LLC 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | definition( 17 | name: "Consumption Metering", 18 | namespace: "FortrezZ", 19 | author: "FortrezZ, LLC", 20 | description: "Child SmartApp for Consumption Metering rules", 21 | category: "Green Living", 22 | parent: "FortrezZ:FortrezZ Water Consumption Metering", 23 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", 24 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", 25 | iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") 26 | 27 | preferences { 28 | page(name: "prefsPage", title: "Plugin version 1.5\nChoose the detector behavior", install: true, uninstall: true) 29 | 30 | // Do something here like update a message on the screen, 31 | // or introduce more inputs. submitOnChange will refresh 32 | // the page and allow the user to see the changes immediately. 33 | // For example, you could prompt for the level of the dimmers 34 | // if dimmers have been selected: 35 | //log.debug "Child Settings: ${settings}" 36 | } 37 | 38 | def prefsPage() { 39 | def dailySchedule = 0 40 | def daysOfTheWeek = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] 41 | dynamicPage(name: "prefsPage") { 42 | section("Set Water Usage Goals") { 43 | input(name: "type", type: "enum", title: "Set a new goal?", submitOnChange: true, options: ruleTypes()) 44 | } 45 | def measurementType = "water" 46 | if(type) 47 | { 48 | switch (type) { 49 | case "Daily Goal": 50 | section("Water Measurement Preference"){ 51 | input(name: "measurementType", type: "enum", title: "Press to change water measurement options", submitOnChange: true, options: waterTypes())} 52 | 53 | section("Threshold settings") { 54 | input(name: "waterGoal", type: "decimal", title: "Daily ${measurementType} Goal", required: true, defaultValue: 0.5) 55 | } 56 | 57 | 58 | break 59 | 60 | case "Weekly Goal": 61 | section("Water Measurement Preference"){ 62 | input(name: "measurementType", type: "enum", title: "Press to change water measurement options", submitOnChange: true, options: waterTypes())} 63 | section("Threshold settings") { 64 | input(name: "waterGoal", type: "decimal", title: "Weekly ${measurementType} Goal", required: true, defaultValue: 0.1) 65 | } 66 | 67 | 68 | break 69 | 70 | case "Monthly Goal": 71 | section("Water Measurement Preference"){ 72 | input(name: "measurementType", type: "enum", title: "Press to change water measurement options", submitOnChange: true, options: waterTypes())} 73 | section("Threshold settings") { 74 | input(name: "waterGoal", type: "decimal", title: "Monthly ${measurementType} Goal", required: true, defaultValue: 0.1) 75 | } 76 | 77 | 78 | break 79 | 80 | default: 81 | break 82 | } 83 | } 84 | } 85 | } 86 | 87 | def ruleTypes() { 88 | def types = [] 89 | types << "Daily Goal" 90 | types << "Weekly Goal" 91 | types << "Monthly Goal" 92 | 93 | return types 94 | } 95 | 96 | def waterTypes() 97 | { 98 | def watertype = [] 99 | 100 | watertype << "Gallons" 101 | watertype << "Cubic Feet" 102 | watertype << "Liters" 103 | watertype << "Cubic Meters" 104 | return watertype 105 | } 106 | /* 107 | def setDailyGoal(measurementType3) 108 | { 109 | return parent.setDailyGoal(measurementType3) 110 | } 111 | def setWeeklyGoal() 112 | { 113 | return parent.setWeeklyGoal(measurementType) 114 | } 115 | def setMonthlyGoal() 116 | { 117 | return parent.setMonthlyGoal(measurementType) 118 | } 119 | */ 120 | 121 | def actionTypes() { 122 | def types = [] 123 | types << [name: "Switch", capability: "capabilty.switch"] 124 | types << [name: "Water Valve", capability: "capability.valve"] 125 | 126 | return types 127 | } 128 | 129 | def deviceCommands(dev) 130 | { 131 | def cmds = [] 132 | dev.supportedCommands.each { command -> 133 | cmds << command.name 134 | } 135 | 136 | return cmds 137 | } 138 | 139 | def installed() { 140 | state.Daily = 0 141 | log.debug "Installed with settings: ${settings}" 142 | app.updateLabel("${type} - ${waterGoal} ${measurementType}") 143 | //schedule(" 0 0/1 * 1/1 * ? *", setDailyGoal()) 144 | initialize() 145 | } 146 | 147 | 148 | 149 | 150 | 151 | def updated() { 152 | log.debug "Updated with settings: ${settings}" 153 | app.updateLabel("${type} - ${waterGoal} ${measurementType}") 154 | 155 | 156 | unsubscribe() 157 | initialize() 158 | //unschedule() 159 | } 160 | 161 | def settings() { 162 | def set = settings 163 | if (set["dev"] != null) 164 | { 165 | log.debug("dev set: ${set.dev}") 166 | set.dev = set.dev.id 167 | } 168 | if (set["valve"] != null) 169 | { 170 | log.debug("valve set: ${set.valve}") 171 | set.valve = set.valve.id 172 | } 173 | 174 | log.debug(set) 175 | 176 | return set 177 | } 178 | 179 | def devAction(action) 180 | { 181 | if(dev) 182 | { 183 | log.debug("device: ${dev}, action: ${action}") 184 | dev."${action}"() 185 | } 186 | } 187 | 188 | def isValveStatus(status) 189 | { 190 | def result = false 191 | log.debug("Water Valve ${valve} has status ${valve.currentState("contact").value}, compared to ${status.toLowerCase()}") 192 | if(valve) 193 | { 194 | if(valve.currentState("contact").value == status.toLowerCase()) 195 | { 196 | result = true 197 | } 198 | } 199 | return result 200 | } 201 | 202 | def initialize() { 203 | 204 | 205 | // TODO: subscribe to attributes, devices, locations, etc. 206 | } 207 | def uninstalled() { 208 | // external cleanup. No need to unsubscribe or remove scheduled jobs 209 | } 210 | // TODO: implement event handlers -------------------------------------------------------------------------------- /flow meter (fmi)/Consumption Metering/smartapp-consumption-parent.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * FortrezZ Water Consumption Metering 3 | * 4 | * Copyright 2016 FortrezZ, LLC 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | definition( 17 | name: "FortrezZ Water Consumption Metering", 18 | namespace: "FortrezZ", 19 | author: "FortrezZ, LLC", 20 | description: "Use the FortrezZ Water Meter to efficiently use your homes water system.", 21 | category: "Green Living", 22 | iconUrl: "http://swiftlet.technology/wp-content/uploads/2016/05/logo-square-200-1.png", 23 | iconX2Url: "http://swiftlet.technology/wp-content/uploads/2016/05/logo-square-500.png", 24 | iconX3Url: "http://swiftlet.technology/wp-content/uploads/2016/05/logo-square.png") 25 | 26 | 27 | preferences { 28 | page(name: "page2", title: "Plugin version 1.5\nSelect device and actions", install: true, uninstall: true) 29 | } 30 | def page2() { 31 | dynamicPage(name: "page2") { 32 | section("Choose a water meter to monitor:") { 33 | input(name: "meter", type: "capability.energyMeter", title: "Water Meter", description: null, required: true, submitOnChange: true) 34 | } 35 | 36 | if (meter) { 37 | section { 38 | app(name: "childRules", appName: "Consumption Metering", namespace: "FortrezZ", title: "Create New Water Consumption Goal", multiple: true) 39 | } 40 | } 41 | 42 | section("Start/End time of all water usage goal periods") { 43 | input(name: "alertTime", type: "time", required: true) 44 | } 45 | 46 | section("Billing info") { 47 | input(name: "unitType", type: "enum", title: "Water unit used in billing", description: null, defaultValue: "Gallons", required: true, submitOnChange: true, options: waterTypes()) 48 | input(name: "costPerUnit", type: "decimal", title: "Cost of water unit in billing", description: null, defaultValue: 0, required: true, submitOnChange: true) 49 | input(name: "fixedFee", type: "decimal", title: "Add a Fixed Fee?", description: null, defaultValue: 0, submitOnChange: true)} 50 | 51 | section("Send notifications through...") { 52 | input(name: "pushNotification", type: "bool", title: "SmartThings App", required: false) 53 | input(name: "smsNotification", type: "bool", title: "Text Message (SMS)", submitOnChange: true, required: false) 54 | if (smsNotification) 55 | { 56 | input(name: "phone", type: "phone", title: "Phone number?", required: true) 57 | } 58 | //input(name: "hoursBetweenNotifications", type: "number", title: "Hours between notifications", required: false) 59 | } 60 | 61 | 62 | 63 | 64 | log.debug "there are ${childApps.size()} child smartapps" 65 | 66 | 67 | def childRules = [] 68 | childApps.each {child -> 69 | log.debug "child ${child.id}: ${child.settings()}" 70 | 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 71 | } 72 | 73 | def match = false 74 | def changeOfSettings = false 75 | for (myItem in childRules) { 76 | def q = myItem.rules 77 | for (item2 in state.rules) { 78 | def r = item2.rules 79 | log.debug(r.alertType) 80 | 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. 81 | match = true 82 | if (q.type == r.type){ 83 | changeOfSettings = true} 84 | } 85 | } 86 | 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 87 | state["NewApp${myItem.id}"] = true 88 | log.debug "Created a new ${q.type} with an ID of ${myItem.id}"} 89 | 90 | match = false 91 | } 92 | 93 | for (myItem in childRules) { 94 | if (state["NewApp${myItem.id}"] == true){ 95 | state["NewApp${myItem.id}"] = false 96 | 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 97 | state["oneHundred${myItem.id}"] = false 98 | state["ninety${myItem.id}"] = false 99 | state["seventyFive${myItem.id}"] = false 100 | state["fifty${myItem.id}"] = false 101 | state["endOfGoalPeriod${myItem.id}"] = false 102 | } 103 | } 104 | 105 | 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 106 | log.debug "Parent Settings: ${settings}" 107 | 108 | 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 109 | state.costRatio = costPerUnit/(convertToGallons(unitType)) 110 | state.fixedFee = fixedFee 111 | } 112 | } 113 | } 114 | 115 | def parseAlerTimeAndStartNewSchedule(myAlert) 116 | { 117 | def endTime = myAlert.split("T") 118 | def endHour = endTime[1].split(":")[0] // parsing the time stamp which is of this format: 2016-12-13T16:25:00.000-0500 119 | def endMinute = endTime[1].split(":")[1] 120 | schedule("0 ${endMinute} ${endHour} 1/1 * ? *", goalSearch) // creating a schedule to launch goalSearch every day at a user defined time - default is at midnight 121 | log.debug("new schedule created at ${endHour} : ${endMinute}") 122 | } 123 | 124 | 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 125 | { 126 | switch (myUnit){ 127 | case "Gallons": 128 | return 1 129 | break 130 | case "Cubic Feet": 131 | return 7.48052 132 | break 133 | case "Liters": 134 | return 0.264172 135 | break 136 | case "Cubic Meters": 137 | return 264.172 138 | break 139 | default: 140 | log.debug "value for water measurement doesn't fit into the 4 water measurement categories" 141 | return 1 142 | break 143 | } 144 | } 145 | 146 | 147 | def goalSearch(){ 148 | 149 | 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 150 | def fullDateTime = dateTime.format("yyyy-MM-dd HH:mm:ss", location.timeZone) 151 | def mySplit = fullDateTime.split() 152 | 153 | log.debug("goalSearch: ${fullDateTime}") // 2016-12-09 14:59:56 154 | 155 | // 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 156 | def dateSplit = mySplit[0].split("-") 157 | def timeSplit = mySplit[1].split(":") 158 | state.dateSplit = [] 159 | state.timeSplit = [] 160 | for (i in dateSplit){ 161 | state.dateSplit << i} // unnecessary? 162 | for (g in timeSplit){ 163 | state.timeSplit << g} 164 | def dayOfWeek = Date.parse("yyyy-MM-dd", mySplit[0]).format("EEEE") 165 | state.debug = false 166 | dailyGoalSearch(dateSplit, timeSplit) 167 | weeklyGoalSearch(dateSplit, timeSplit, dayOfWeek) 168 | monthlyGoalSearch(dateSplit, timeSplit) 169 | 170 | } 171 | 172 | 173 | 174 | 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. 175 | 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. 176 | for (it in myRules){ 177 | def r = it.rules 178 | if (r.type == "Daily Goal") { 179 | scheduleGoal(r.measurementType, it.id, r.waterGoal, r.type, 0.03333) 180 | } 181 | } 182 | } 183 | def weeklyGoalSearch(dateSplit, timeSplit, dayOfWeek){ 184 | 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. 185 | for (it in myRules){ 186 | def r = it.rules 187 | if (r.type == "Weekly Goal") { 188 | if (dayOfWeek == "Sunday" || state.debug == true){ 189 | scheduleGoal(r.measurementType, it.id, r.waterGoal, r.type, 0.23333)} 190 | } 191 | } 192 | } 193 | def monthlyGoalSearch(dateSplit, timeSplit){ 194 | 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. 195 | for (it in myRules){ 196 | def r = it.rules 197 | if (r.type == "Monthly Goal") { 198 | if (dateSplit[2] == "01" || state.debug == true){ 199 | scheduleGoal(r.measurementType, it.id, r.waterGoal, r.type, 0.23333)} 200 | } 201 | } 202 | } 203 | 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. 204 | def cost = 0 205 | def f = 1.0f 206 | 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. 207 | 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. 208 | def curCumulation = waterConversionPreference(topCumulative, measureType) - waterConversionPreference(state["Start${goalID}"], measureType) 209 | 210 | 211 | if (state.costRatio){ 212 | 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. 213 | } 214 | def percentage = (curCumulation / wGoal) * 100 * f 215 | if (costPerUnit != 0) { 216 | 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 217 | 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)}" 218 | 219 | } 220 | 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. 221 | { 222 | 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)}%).") 223 | 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)}%)." 224 | } 225 | state["Start${goalID}"] = topCumulative; 226 | state["oneHundred${goalID}"] = false 227 | state["ninety${goalID}"] = false 228 | state["seventyFive${goalID}"] = false 229 | state["fifty${goalID}"] = false 230 | state["endOfGoalPeriod${goalID}"] = true // telling the app that the goal period is over. 231 | } 232 | 233 | 234 | 235 | def waterTypes() // holds the types of water measurement used in the main smartapp page for billing info and for setting goals 236 | { 237 | def watertype = [] 238 | 239 | watertype << "Gallons" 240 | watertype << "Cubic Feet" 241 | watertype << "Liters" 242 | watertype << "Cubic Meters" 243 | return watertype 244 | } 245 | 246 | def installed() { // when the app is first installed - do something 247 | log.debug "Installed with settings: ${settings}" 248 | 249 | initialize() 250 | } 251 | 252 | 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 253 | log.debug "Updated with settings: ${settings}" 254 | 255 | if (alertTime != state.alertTime) // we created this 'if' statement to prevent another schedule being made whenever the user opens the smartapp 256 | { 257 | unschedule() //unscheduling is a good idea here because we don't want multiple schedules happening and this function cancles all schedules 258 | parseAlerTimeAndStartNewSchedule(alertTime) // we use cron scheduling to use the function 'goalSearch' every minute 259 | state.alarmTime = alarmTime // setting state.alarmTime prevents a new schedule being made whenever the user opens the smartapp 260 | } 261 | 262 | unsubscribe() 263 | initialize() 264 | //unschedule() 265 | } 266 | 267 | def initialize() { // whenever you open the smart app - do something 268 | subscribe(meter, "cumulative", cumulativeHandler) 269 | //subscribe(meter, "gpm", gpmHandler) 270 | log.debug("Subscribing to events") 271 | } 272 | 273 | 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 274 | def f = 1.0f 275 | def gpm = meter.latestValue("gpm") // storing the current gallons per minute value 276 | def cumulative = new BigDecimal(evt.value) // storing the current cumulation value 277 | log.debug "Cumulative Handler: [gpm: ${gpm}, cumulative: ${cumulative}]" 278 | def rules = state.rules //storing the array of child apps to 'rules' 279 | rules.each { it -> // looping through each app in the array but storing each app into the variable 'it' 280 | 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 281 | def childAppID = it.id // storing the child app ID to a variable 282 | 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. 283 | 284 | 285 | 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 286 | def goalStartCumulative = waterConversionPreference(state["Start${childAppID}"], r.measurementType) 287 | 288 | 289 | 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' 290 | state.DailyGallonGoal = DailyGallonGoal // and then we make that value global and persistent for logic reasons 291 | 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. 292 | state["currentCumulation${childAppID}"] = currentCumulation 293 | log.debug("Threshold:${DailyGallonGoal}, Value:${(currentCumulation * f).round(2)}") 294 | 295 | if ( currentCumulation >= (0.5 * DailyGallonGoal) && currentCumulation < (0.75 * DailyGallonGoal) && state["fifty${childAppID}"] == false) // tell the user if they break certain use thresholds 296 | { 297 | notify("You have reached 50% of your ${r.type} use limit. (${(currentCumulation * f).round(2)} of ${DailyGallonGoal} ${r.measurementType})") 298 | log.debug "You have reached 50% of your ${r.type} use limit. (${(currentCumulation * f).round(2)} of ${DailyGallonGoal} ${r.measurementType})" 299 | state["fifty${childAppID}"] = true 300 | } 301 | if ( currentCumulation >= (0.75 * DailyGallonGoal) && currentCumulation < (0.9 * DailyGallonGoal) && state["seventyFive${childAppID}"] == false) 302 | { 303 | notify("You have reached 75% of your ${r.type} use limit. (${(currentCumulation * f).round(2)} of ${DailyGallonGoal} ${r.measurementType})") 304 | log.debug "You have reached 75% of your ${r.type} use limit. (${(currentCumulation * f).round(2)} of ${DailyGallonGoal} ${r.measurementType})" 305 | state["seventyFive${childAppID}"] = true 306 | } 307 | if ( currentCumulation >= (0.9 * DailyGallonGoal) && currentCumulation < (DailyGallonGoal) && state["ninety${childAppID}"] == false) 308 | { 309 | notify("You have reached 90% of your ${r.type} use limit. (${(currentCumulation * f).round(2)} of ${DailyGallonGoal} ${r.measurementType})") 310 | log.debug "You have reached 90% of your ${r.type} use limit. (${(currentCumulation * f).round(2)} of ${DailyGallonGoal} ${r.measurementType})" 311 | state["ninety${childAppID}"] = true 312 | } 313 | if (currentCumulation >= DailyGallonGoal && state["oneHundred${childAppID}"] == false) 314 | { 315 | notify("You have reached 100% of your ${r.type} use limit. (${(currentCumulation * f).round(2)} of ${DailyGallonGoal} ${r.measurementType})") 316 | log.debug "You have reached 100% of your ${r.type} use limit. (${(currentCumulation * f).round(2)} of ${DailyGallonGoal} ${r.measurementType})" 317 | state["oneHundred${childAppID}"] = true 318 | //send command here like shut off the water 319 | 320 | 321 | 322 | } 323 | if (state["endOfGoalPeriod${childAppID}"] == true) // changing the start value to the most recent cumulative value for goal reset. 324 | {state["Start${childAppID}"] = cumulative 325 | state["endOfGoalPeriod${childAppID}"] = false 326 | } 327 | } 328 | } 329 | 330 | 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 331 | { 332 | switch (measurementType1) 333 | { 334 | case "Cubic Feet": 335 | return (cumul * 0.133681) 336 | break 337 | 338 | case "Liters": 339 | return (cumul * 3.78541) 340 | break 341 | 342 | case "Cubic Meters": 343 | return (cumul * 0.00378541) 344 | break 345 | 346 | case "Gallons": 347 | return cumul 348 | break 349 | 350 | } 351 | } 352 | 353 | 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 354 | { 355 | switch (measurementType1) 356 | { 357 | case "Cubic Feet": 358 | return (cumul / 0.133681) 359 | break 360 | 361 | case "Liters": 362 | return (cumul / 3.78541) 363 | break 364 | 365 | case "Cubic Meters": 366 | return (cumul / 0.00378541) 367 | break 368 | 369 | case "Gallons": 370 | return cumul 371 | break 372 | 373 | } 374 | } 375 | 376 | def notify(myMsg) // method for both push notifications and for text messaging. 377 | { 378 | log.debug("Sending Notification") 379 | if (pushNotification) {sendPush(myMsg)} else {sendNotificationEvent(myMsg)} 380 | if (smsNotification) {sendSms(phone, myMsg)} 381 | } 382 | 383 | def uninstalled() { 384 | // external cleanup. No need to unsubscribe or remove scheduled jobs 385 | unsubscribe() 386 | unschedule() 387 | } -------------------------------------------------------------------------------- /flow meter (fmi)/Leak Detector SmartApp/smartapp-leakdetector-child.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Leak Detector 3 | * 4 | * Copyright 2016 Daniel Kurin 5 | * 6 | */ 7 | definition( 8 | name: "Leak Detector", 9 | namespace: "fortrezz", 10 | author: "FortrezZ, LLC", 11 | description: "Child SmartApp for leak detector rules", 12 | category: "Green Living", 13 | parent: "fortrezz:FortrezZ Leak Detector", 14 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", 15 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", 16 | iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") 17 | 18 | 19 | preferences { 20 | page(name: "prefsPage", title: "Plugin version 1.5\nChoose the detector behavior", install: true, uninstall: true) 21 | 22 | // Do something here like update a message on the screen, 23 | // or introduce more inputs. submitOnChange will refresh 24 | // the page and allow the user to see the changes immediately. 25 | // For example, you could prompt for the level of the dimmers 26 | // if dimmers have been selected: 27 | //log.debug "Child Settings: ${settings}" 28 | } 29 | 30 | def prefsPage() { 31 | def daysOfTheWeek = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] 32 | dynamicPage(name: "prefsPage") { 33 | 34 | 35 | section("Set Leak Threshold by...") { 36 | input(name: "type", type: "enum", title: "Type...", submitOnChange: true, options: ruleTypes()) 37 | } 38 | 39 | if(type) 40 | { 41 | switch (type) { 42 | case "Mode": 43 | section("Threshold settings") { 44 | input(name: "ruleName", type: "text", title: "Rule Name", required: true) 45 | input(name: "gpm", type: "decimal", title: "GPM exceeds", required: true, defaultValue: 0.1) 46 | } 47 | section("Only in these modes") { 48 | input(name: "modes", type: "mode", title: "select a mode(s)", multiple: true, required: true) 49 | } 50 | section ("Action") { 51 | input(name: "dev", type: "capability.actuator", title: "Choose a device to perform the action", required: false, submitOnChange: true) 52 | if (dev) { 53 | input(name: "command", type: "enum", title: "Command...", submitOnChange: true, options: deviceCommands(dev)) 54 | } 55 | } 56 | break 57 | 58 | case "Time Period": 59 | section("Threshold settings") { 60 | input(name: "ruleName", type: "text", title: "Rule Name", required: true) 61 | input(name: "gpm", type: "decimal", title: "GPM exceeds", required: true) 62 | } 63 | section("Between...") { 64 | input(name: "startTime", type: "time", title: "Start Time", required: true) 65 | } 66 | section("...and...") { 67 | input(name: "endTime", type: "time", title: "End Time", required: true) 68 | } 69 | section("Only on these days") { 70 | input(name: "days", type: "enum", title: "Days of the week", required: false, options: daysOfTheWeek, multiple: true) 71 | } 72 | section("Only in these modes") { 73 | input(name: "modes", type: "mode", title: "System Modes", required: false, multiple: true) 74 | } 75 | section ("Action") { 76 | input(name: "dev", type: "capability.actuator", title: "Choose a device to perform the action", required: false, submitOnChange: true) 77 | if (dev) { 78 | input(name: "command", type: "enum", title: "Command...", submitOnChange: true, options: deviceCommands(dev)) 79 | } 80 | } 81 | break 82 | 83 | case "Accumulated Flow": 84 | section("Threshold settings") { 85 | input(name: "ruleName", type: "text", title: "Rule Name", required: true) 86 | input(name: "gallons", type: "number", title: "Total Gallons exceeds", required: true) 87 | } 88 | section("Between...") { 89 | input(name: "startTime", type: "time", title: "Start Time", required: true) 90 | } 91 | section("...and...") { 92 | input(name: "endTime", type: "time", title: "End Time", required: true) 93 | } 94 | section("Only on these days") { 95 | input(name: "days", type: "enum", title: "Days of the week", required: false, options: daysOfTheWeek, multiple: true) 96 | } 97 | section("Only in these modes") { 98 | input(name: "modes", type: "mode", title: "System Modes", required: false, multiple: true) 99 | } 100 | section ("Action") { 101 | input(name: "dev", type: "capability.actuator", title: "Choose a device to perform the action", required: false, submitOnChange: true) 102 | if (dev) { 103 | input(name: "command", type: "enum", title: "Command...", submitOnChange: true, options: deviceCommands(dev)) 104 | } 105 | } 106 | break 107 | 108 | case "Continuous Flow": 109 | section("Threshold settings") { 110 | input(name: "ruleName", type: "text", title: "Rule Name", required: true) 111 | input(name: "flowMinutes", type: "number", title: "Minutes of constant flow", required: true, defaultValue: 60) 112 | } 113 | section("Only in these modes") { 114 | input(name: "modes", type: "mode", title: "System Modes", required: false, multiple: true) 115 | } 116 | section ("Action") { 117 | input(name: "dev", type: "capability.actuator", title: "Choose a device to perform the action", required: false, submitOnChange: true) 118 | if (dev) { 119 | input(name: "command", type: "enum", title: "Command...", submitOnChange: true, options: deviceCommands(dev)) 120 | } 121 | } 122 | break 123 | 124 | case "Water Valve Status": 125 | section("Threshold settings") { 126 | input(name: "ruleName", type: "text", title: "Rule Name", required: true) 127 | input(name: "gpm", type: "decimal", title: "GPM exceeds", required: true, defaultValue: 0.1) 128 | } 129 | section ("While...") { 130 | input(name: "valve", type: "capability.valve", title: "Choose a valve", required: true) 131 | } 132 | section ("...is...") { 133 | input(name: "valveStatus", type: "enum", title: "Status", options: ["Open","Closed"], required: true) 134 | } 135 | break 136 | 137 | case "Switch Status": 138 | section("Threshold settings") { 139 | input(name: "ruleName", type: "text", title: "Rule Name", required: true) 140 | input(name: "gpm", type: "decimal", title: "GPM exceeds", required: true, defaultValue: 0.1) 141 | } 142 | section ("If...") { 143 | input(name: "valve", type: "capability.switch", title: "Choose a switch", required: true) 144 | } 145 | section ("...is...") { 146 | input(name: "switchStatus", type: "enum", title: "Status", options: ["On","Off"], required: true) 147 | } 148 | break 149 | 150 | default: 151 | break 152 | } 153 | } 154 | } 155 | } 156 | 157 | def ruleTypes() { 158 | def types = [] 159 | types << "Mode" 160 | types << "Time Period" 161 | types << "Accumulated Flow" 162 | types << "Continuous Flow" 163 | types << "Water Valve Status" 164 | //types << "Switch Status" 165 | 166 | return types 167 | } 168 | 169 | def actionTypes() { 170 | def types = [] 171 | types << [name: "Switch", capability: "capabilty.switch"] 172 | types << [name: "Water Valve", capability: "capability.valve"] 173 | 174 | return types 175 | } 176 | 177 | def deviceCommands(dev) 178 | { 179 | def cmds = [] 180 | dev.supportedCommands.each { command -> 181 | cmds << command.name 182 | } 183 | 184 | return cmds 185 | } 186 | 187 | def installed() { 188 | log.debug "Installed with settings: ${settings}" 189 | app.updateLabel("${ruleName ? ruleName : ""} - ${type}") 190 | 191 | initialize() 192 | } 193 | 194 | def updated() { 195 | log.debug "Updated with settings: ${settings}" 196 | app.updateLabel("${ruleName ? ruleName : ""} - ${type}") 197 | 198 | unsubscribe() 199 | initialize() 200 | } 201 | 202 | def settings() { 203 | def set = settings 204 | if (set["dev"] != null) 205 | { 206 | log.debug("dev set: ${set.dev}") 207 | set.dev = set.dev.id 208 | } 209 | if (set["valve"] != null) 210 | { 211 | log.debug("valve set: ${set.valve}") 212 | set.valve = set.valve.id 213 | } 214 | log.debug(set) 215 | return set 216 | } 217 | 218 | def devAction(action) 219 | { 220 | if(dev) 221 | { 222 | log.debug("device: ${dev}, action: ${action}") 223 | dev."${action}"() 224 | } 225 | } 226 | 227 | def isValveStatus(status) 228 | { 229 | def result = false 230 | log.debug("Water Valve ${valve} has status ${valve.currentState("contact").value}, compared to ${status.toLowerCase()}") 231 | if(valve) 232 | { 233 | if(valve.currentState("contact").value == status.toLowerCase()) 234 | { 235 | result = true 236 | } 237 | } 238 | return result 239 | } 240 | 241 | def initialize() { 242 | // TODO: subscribe to attributes, devices, locations, etc. 243 | } 244 | 245 | // TODO: implement event handlers -------------------------------------------------------------------------------- /flow meter (fmi)/Leak Detector SmartApp/smartapp-leakdetector-parent.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Leak Detector for FortrezZ Water Meter 3 | * 4 | * Copyright 2016 Daniel Kurin 5 | * 6 | */ 7 | definition( 8 | name: "FortrezZ Leak Detector", 9 | namespace: "fortrezz", 10 | author: "FortrezZ, LLC", 11 | description: "Use the FortrezZ Water Meter to identify leaks in your home's water system.", 12 | category: "Green Living", 13 | iconUrl: "http://swiftlet.technology/wp-content/uploads/2016/05/logo-square-200-1.png", 14 | iconX2Url: "http://swiftlet.technology/wp-content/uploads/2016/05/logo-square-500.png", 15 | iconX3Url: "http://swiftlet.technology/wp-content/uploads/2016/05/logo-square.png") 16 | 17 | 18 | preferences { 19 | page(name: "page2", title: "Plugin version 1.5\nSelect device and actions", install: true, uninstall: true) 20 | } 21 | 22 | def page2() { 23 | dynamicPage(name: "page2") { 24 | section("Choose a water meter to monitor:") { 25 | input(name: "meter", type: "capability.energyMeter", title: "Water Meter", description: null, required: true, submitOnChange: true) 26 | } 27 | 28 | if (meter) { 29 | section { 30 | app(name: "childRules", appName: "Leak Detector", namespace: "fortrezz", title: "Create New Leak Detector...", multiple: true) 31 | } 32 | } 33 | 34 | section("Send notifications through...") { 35 | input(name: "pushNotification", type: "bool", title: "SmartThings App", required: false) 36 | input(name: "smsNotification", type: "bool", title: "Text Message (SMS)", submitOnChange: true, required: false) 37 | if (smsNotification) 38 | { 39 | input(name: "phone", type: "phone", title: "Phone number?", required: true) 40 | } 41 | input(name: "minutesBetweenNotifications", type: "number", title: "Minutes between notifications", required: true, defaultValue: 60) 42 | } 43 | 44 | log.debug "there are ${childApps.size()} child smartapps" 45 | def childRules = [] 46 | childApps.each {child -> 47 | //log.debug "child ${child.id}: ${child.settings()}" 48 | childRules << [id: child.id, rules: child.settings()] 49 | } 50 | state.rules = childRules 51 | //log.debug("Child Rules: ${state.rules} w/ length ${state.rules.toString().length()}") 52 | log.debug "Parent Settings: ${settings}" 53 | } 54 | } 55 | 56 | def installed() { 57 | log.debug "Installed with settings: ${settings}" 58 | 59 | initialize() 60 | } 61 | 62 | def updated() { 63 | log.debug "Updated with settings: ${settings}" 64 | 65 | unsubscribe() 66 | initialize() 67 | } 68 | 69 | def initialize() { 70 | subscribe(meter, "cumulative", cumulativeHandler) 71 | subscribe(meter, "gpm", gpmHandler) 72 | log.debug("Subscribing to events") 73 | } 74 | 75 | def cumulativeHandler(evt) { 76 | //Date Stuff 77 | def daysOfTheWeek = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] 78 | def today = new Date() 79 | today.clearTime() 80 | Calendar c = Calendar.getInstance(); 81 | c.setTime(today); 82 | int dow = c.get(Calendar.DAY_OF_WEEK); 83 | def dowName = daysOfTheWeek[dow-1] 84 | 85 | def gpm = meter.latestValue("gpm") 86 | def cumulative = new BigDecimal(evt.value) 87 | log.debug "Cumulative Handler: [gpm: ${gpm}, cumulative: ${cumulative}]" 88 | def rules = state.rules 89 | rules.each { it -> 90 | def r = it.rules 91 | def childAppID = it.id 92 | //log.debug("Rule: ${r}") 93 | switch (r.type) { 94 | case "Mode": 95 | log.debug("Mode Test: ${location.currentMode} in ${r.modes}... ${findIn(r.modes, location.currentMode)}") 96 | if (findIn(r.modes, location.currentMode)) 97 | { 98 | log.debug("Threshold:${r.gpm}, Value:${gpm}") 99 | if(gpm > r.gpm) 100 | { 101 | sendNotification(childAppID, gpm) 102 | if(r.dev) 103 | { 104 | //log.debug("Child App: ${childAppID}") 105 | def activityApp = getChildById(childAppID) 106 | activityApp.devAction(r.command) 107 | } 108 | } 109 | } 110 | break 111 | 112 | case "Time Period": 113 | log.debug("Time Period Test: ${r}") 114 | def boolTime = timeOfDayIsBetween(r.startTime, r.endTime, new Date(), location.timeZone) 115 | def boolDay = !r.days || findIn(r.days, dowName) // Truth Table of this mess: http://swiftlet.technology/wp-content/uploads/2016/05/IMG_20160523_150600.jpg 116 | def boolMode = !r.modes || findIn(r.modes, location.currentMode) 117 | 118 | if(boolTime && boolDay && boolMode) 119 | { 120 | if(gpm > r.gpm) 121 | { 122 | sendNotification(childAppID, gpm) 123 | if(r.dev) 124 | { 125 | def activityApp = getChildById(childAppID) 126 | activityApp.devAction(r.command) 127 | } 128 | } 129 | } 130 | break 131 | 132 | case "Accumulated Flow": 133 | log.debug("Accumulated Flow Test: ${r}") 134 | def boolTime = timeOfDayIsBetween(r.startTime, r.endTime, new Date(), location.timeZone) 135 | def boolDay = !r.days || findIn(r.days, dowName) // Truth Table of this mess: http://swiftlet.technology/wp-content/uploads/2016/05/IMG_20160523_150600.jpg 136 | def boolMode = !r.modes || findIn(r.modes, location.currentMode) 137 | 138 | if(boolTime && boolDay && boolMode) 139 | { 140 | def delta = 0 141 | if(state["accHistory${childAppID}"] != null) 142 | { 143 | delta = cumulative - state["accHistory${childAppID}"] 144 | } 145 | else 146 | { 147 | state["accHistory${childAppID}"] = cumulative 148 | } 149 | log.debug("Currently in specified time, delta from beginning of time period: ${delta}") 150 | 151 | if(delta > r.gallons) 152 | { 153 | sendNotification(childAppID, delta) 154 | if(r.dev) 155 | { 156 | def activityApp = getChildById(childAppID) 157 | activityApp.devAction(r.command) 158 | } 159 | } 160 | } 161 | else 162 | { 163 | log.debug("Outside specified time, saving value") 164 | state["accHistory${childAppID}"] = cumulative 165 | } 166 | break 167 | 168 | case "Continuous Flow": 169 | log.debug("Continuous Flow Test: ${r}") 170 | def contMinutes = 0 171 | def boolMode = !r.modes || findIn(r.modes, location.currentMode) 172 | 173 | if(gpm != 0) 174 | { 175 | if(state["contHistory${childAppID}"] == []) 176 | { 177 | state["contHistory${childAppID}"] = new Date() 178 | } 179 | else 180 | { 181 | //def td = now() - Date.parse("yyyy-MM-dd'T'HH:mm:ss'Z'", state["contHistory${childAppID}"]).getTime() 182 | //log.debug(state["contHistory${childAppID}"]) 183 | //def historyDate = new Date(state["contHistory${childAppID}"]) 184 | def historyDate = new Date().parse("yyyy-MM-dd'T'HH:mm:ssZ", state["contHistory${childAppID}"]) 185 | def td = now() - historyDate.getTime() 186 | //log.debug("Now minus then: ${td}") 187 | contMinutes = td/60000 188 | log.debug("Minutes of constant flow: ${contMinutes}, since ${state["contHistory${childAppID}"]}") 189 | } 190 | } 191 | 192 | if(contMinutes > r.flowMinutes && boolMode) 193 | { 194 | sendNotification(childAppID, Math.round(contMinutes)) 195 | if(r.dev) 196 | { 197 | def activityApp = getChildById(childAppID) 198 | activityApp.devAction(r.command) 199 | } 200 | } 201 | break 202 | 203 | case "Water Valve Status": 204 | log.debug("Water Valve Test: ${r}") 205 | def child = getChildById(childAppID) 206 | //log.debug("Water Valve Child App: ${child.id}") 207 | if(child.isValveStatus(r.valveStatus)) 208 | { 209 | if(gpm > r.gpm) 210 | { 211 | sendNotification(childAppID, gpm) 212 | } 213 | } 214 | break 215 | 216 | case "Switch Status": 217 | break 218 | 219 | default: 220 | break 221 | } 222 | } 223 | } 224 | 225 | def gpmHandler(evt) { 226 | //Date Stuff 227 | def daysOfTheWeek = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] 228 | def today = new Date() 229 | today.clearTime() 230 | Calendar c = Calendar.getInstance(); 231 | c.setTime(today); 232 | int dow = c.get(Calendar.DAY_OF_WEEK); 233 | def dowName = daysOfTheWeek[dow-1] 234 | 235 | def gpm = evt.value 236 | def cumulative = meter.latestValue("cumulative") 237 | log.debug "GPM Handler: [gpm: ${gpm}, cumulative: ${cumulative}]" 238 | def rules = state.rules 239 | rules.each { it -> 240 | def r = it.rules 241 | def childAppID = it.id 242 | switch (r.type) { 243 | 244 | // This is down here because "cumulative" never gets sent in the case of 0 change between messages 245 | case "Continuous Flow": 246 | log.debug("Continuous Flow Test (GPM): ${r}") 247 | def contMinutes = 0 248 | 249 | if(gpm == "0.0") 250 | { 251 | state["contHistory${childAppID}"] = [] 252 | } 253 | //log.debug("contHistory${childAppID} is ${state["contHistory${childAppID}"]}") 254 | break 255 | 256 | default: 257 | break 258 | } 259 | } 260 | } 261 | def sendNotification(device, gpm) 262 | { 263 | def set = getChildById(device).settings() 264 | def msg = "" 265 | if(set.type == "Accumulated Flow") 266 | { 267 | msg = "Water Flow Warning: \"${set.ruleName}\" is over threshold at ${gpm} gallons" 268 | } 269 | else if(set.type == "Continuous Flow") 270 | { 271 | msg = "Water Flow Warning: \"${set.ruleName}\" is over threshold at ${gpm} minutes" 272 | } 273 | else 274 | { 275 | msg = "Water Flow Warning: \"${set.ruleName}\" is over threshold at ${gpm}gpm" 276 | } 277 | log.debug(msg) 278 | 279 | // Only send notifications as often as the user specifies 280 | def lastNotification = 0 281 | if(state["notificationHistory${device}"]) 282 | { 283 | lastNotification = Date.parse("yyyy-MM-dd'T'HH:mm:ssZ", state["notificationHistory${device}"]).getTime() 284 | } 285 | def td = now() - lastNotification 286 | log.debug("Last Notification at ${state["notificationHistory${device}"]}... ${td/(60*1000)} minutes") 287 | if(td/(60*1000) > minutesBetweenNotifications.value) 288 | { 289 | log.debug("Sending Notification") 290 | if (pushNotification) 291 | { 292 | sendPush(msg) 293 | state["notificationHistory${device}"] = new Date() 294 | } 295 | if (smsNotification) 296 | { 297 | sendSms(phone, msg) 298 | state["notificationHistory${device}"] = new Date() 299 | } 300 | } 301 | } 302 | 303 | def getChildById(app) 304 | { 305 | return childApps.find{ it.id == app } 306 | } 307 | 308 | def findIn(haystack, needle) 309 | { 310 | def result = false 311 | haystack.each { it -> 312 | //log.debug("findIn: ${it} <- ${needle}") 313 | if (needle == it) 314 | { 315 | //log.debug("Found needle in haystack") 316 | result = true 317 | } 318 | } 319 | return result 320 | } -------------------------------------------------------------------------------- /flow meter (fmi)/devicehandler-largePipes.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * FortrezZ Flow Meter Interface 3 | * 4 | * Copyright 2016 FortrezZ, LLC 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | metadata { 17 | definition (name: "FortrezZ Flow Meter Interface", namespace: "fortrezz", author: "Daniel Kurin") { 18 | capability "Battery" 19 | capability "Energy Meter" 20 | capability "Image Capture" 21 | capability "Temperature Measurement" 22 | capability "Sensor" 23 | capability "Water Sensor" 24 | 25 | attribute "gpm", "number" 26 | attribute "cumulative", "number" 27 | attribute "alarmState", "string" 28 | attribute "chartMode", "string" 29 | attribute "lastThreshhold", "number" 30 | 31 | 32 | command "chartMode" 33 | command "zero" 34 | command "setHighFlowLevel", ["number"] 35 | 36 | fingerprint deviceId: "0x2101", inClusters: "0x5E, 0x86, 0x72, 0x5A, 0x73, 0x71, 0x85, 0x59, 0x32, 0x31, 0x70, 0x80, 0x7A" 37 | } 38 | 39 | simulator { 40 | // TODO: define status and reply messages here 41 | } 42 | 43 | preferences { 44 | input "gallonThreshhold", "decimal", title: "High Flow Rate Threshhold", description: "Flow rate (in gpm) that will trigger a notification.", defaultValue: 5, required: false, displayDuringSetup: true 45 | input("registerEmail", type: "email", required: false, title: "Email Address", description: "Register your device with FortrezZ", displayDuringSetup: true) 46 | } 47 | 48 | tiles(scale: 2) { 49 | carouselTile("flowHistory", "device.image", width: 6, height: 3) { } 50 | valueTile("battery", "device.battery", inactiveLabel: false, width: 2, height: 2) { 51 | state "battery", label:'${currentValue}%\nBattery', unit:"" 52 | } 53 | valueTile("temperature", "device.temperature", width: 2, height: 2) { 54 | state("temperature", label:'${currentValue}°', 55 | backgroundColors:[ 56 | [value: 31, color: "#153591"], 57 | [value: 44, color: "#1e9cbb"], 58 | [value: 59, color: "#90d2a7"], 59 | [value: 74, color: "#44b621"], 60 | [value: 84, color: "#f1d801"], 61 | [value: 95, color: "#d04e00"], 62 | [value: 96, color: "#bc2323"] 63 | ] 64 | ) 65 | } 66 | valueTile("gpm", "device.gpm", inactiveLabel: false, width: 2, height: 2) { 67 | state "gpm", label:'${currentValue}gpm', unit:"" 68 | } 69 | standardTile("powerState", "device.powerState", width: 2, height: 2) { 70 | state "reconnected", icon:"http://swiftlet.technology/wp-content/uploads/2016/02/Connected-64.png", backgroundColor:"#cccccc" 71 | state "disconnected", icon:"http://swiftlet.technology/wp-content/uploads/2016/02/Disconnected-64.png", backgroundColor:"#cc0000" 72 | state "batteryReplaced", icon:"http://swiftlet.technology/wp-content/uploads/2016/04/Full-Battery-96.png", backgroundColor:"#cccccc" 73 | state "noBattery", icon:"http://swiftlet.technology/wp-content/uploads/2016/04/No-Battery-96.png", backgroundColor:"#cc0000" 74 | } 75 | standardTile("waterState", "device.waterState", width: 2, height: 2, canChangeIcon: true) { 76 | state "none", icon:"http://cdn.device-icons.smartthings.com/Weather/weather12-icn@2x.png", backgroundColor:"#cccccc", label: "No Flow" 77 | state "flow", icon:"http://cdn.device-icons.smartthings.com/Weather/weather12-icn@2x.png", backgroundColor:"#53a7c0", label: "Flow" 78 | state "overflow", icon:"http://cdn.device-icons.smartthings.com/Weather/weather12-icn@2x.png", backgroundColor:"#cc0000", label: "High Flow" 79 | } 80 | standardTile("heatState", "device.heatState", width: 2, height: 2) { 81 | state "normal", label:'Normal', icon:"st.alarm.temperature.normal", backgroundColor:"#ffffff" 82 | state "freezing", label:'Freezing', icon:"st.alarm.temperature.freeze", backgroundColor:"#2eb82e" 83 | state "overheated", label:'Overheated', icon:"st.alarm.temperature.overheat", backgroundColor:"#F80000" 84 | } 85 | standardTile("take1", "device.image", width: 2, height: 2, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false, decoration: "flat") { 86 | state "take", label: "", action: "Image Capture.take", nextState:"taking", icon: "st.secondary.refresh" 87 | } 88 | standardTile("chartMode", "device.chartMode", width: 2, height: 2, canChangeIcon: false, canChangeBackground: false, decoration: "flat") { 89 | state "day", label:'24 Hours\n(press to change)', nextState: "week", action: 'chartMode' 90 | state "week", label:'7 Days\n(press to change)', nextState: "month", action: 'chartMode' 91 | state "month", label:'4 Weeks\n(press to change)', nextState: "day", action: 'chartMode' 92 | } 93 | valueTile("zeroTile", "device.zero", width: 2, height: 2, canChangeIcon: false, canChangeBackground: false, decoration: "flat") { 94 | state "zero", label:'Zero', action: 'zero' 95 | } 96 | main (["waterState"]) 97 | details(["flowHistory", "chartMode", "take1", "temperature", "gpm", "waterState", "battery"]) 98 | } 99 | 100 | } 101 | 102 | // parse events into attributes 103 | def parse(String description) { 104 | def results = [] 105 | if (description.startsWith("Err")) { 106 | results << createEvent(descriptionText:description, displayed:true) 107 | } else { 108 | def cmd = zwave.parse(description, [ 0x80: 1, 0x84: 1, 0x71: 2, 0x72: 1 ]) 109 | if (cmd) { 110 | results << createEvent( zwaveEvent(cmd) ) 111 | } 112 | } 113 | //log.debug "\"$description\" parsed to ${results.inspect()}" 114 | if(gallonThreshhold != device.currentValue("lastThreshhold")) 115 | { 116 | results << setThreshhold(gallonThreshhold) 117 | } 118 | log.debug "zwave parsed to ${results.inspect()}" 119 | return results 120 | } 121 | 122 | def updated() 123 | { 124 | log.debug("Updated") 125 | } 126 | 127 | def setHighFlowLevel(level) 128 | { 129 | setThreshhold(level) 130 | } 131 | 132 | def take() { 133 | def mode = device.currentValue("chartMode") 134 | if(mode == "day") 135 | { 136 | take1() 137 | } 138 | else if(mode == "week") 139 | { 140 | take7() 141 | } 142 | else if(mode == "month") 143 | { 144 | take28() 145 | } 146 | } 147 | 148 | def chartMode(string) { 149 | log.debug("ChartMode") 150 | def state = device.currentValue("chartMode") 151 | def tempValue = "" 152 | switch(state) 153 | { 154 | case "day": 155 | tempValue = "week" 156 | break 157 | 158 | case "week": 159 | tempValue = "month" 160 | break 161 | 162 | case "month": 163 | tempValue = "day" 164 | break 165 | 166 | default: 167 | tempValue = "day" 168 | break 169 | } 170 | sendEvent(name: "chartMode", value: tempValue) 171 | take() 172 | } 173 | 174 | def take1() { 175 | api("24hrs", "") { 176 | log.debug("Image captured") 177 | 178 | if(it.headers.'Content-Type'.contains("image/png")) { 179 | if(it.data) { 180 | storeImage(getPictureName("24hrs"), it.data) 181 | } 182 | } 183 | } 184 | } 185 | 186 | def take7() { 187 | api("7days", "") { 188 | log.debug("Image captured") 189 | 190 | if(it.headers.'Content-Type'.contains("image/png")) { 191 | if(it.data) { 192 | storeImage(getPictureName("7days"), it.data) 193 | } 194 | } 195 | } 196 | } 197 | 198 | def take28() { 199 | api("4weeks", "") { 200 | log.debug("Image captured") 201 | 202 | if(it.headers.'Content-Type'.contains("image/png")) { 203 | if(it.data) { 204 | storeImage(getPictureName("4weeks"), it.data) 205 | } 206 | } 207 | } 208 | } 209 | 210 | def zero() 211 | { 212 | delayBetween([ 213 | zwave.meterV3.meterReset().format(), 214 | zwave.meterV3.meterGet().format(), 215 | zwave.firmwareUpdateMdV2.firmwareMdGet().format(), 216 | ], 100) 217 | } 218 | 219 | def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) 220 | { 221 | log.debug cmd 222 | def map = [:] 223 | if(cmd.sensorType == 1) { 224 | map = [name: "temperature"] 225 | if(cmd.scale == 0) { 226 | map.value = getTemperature(cmd.scaledSensorValue) 227 | } else { 228 | map.value = cmd.scaledSensorValue 229 | } 230 | map.unit = location.temperatureScale 231 | } /* else if(cmd.sensorType == 2) { 232 | map = [name: "waterState"] 233 | if(cmd.sensorValue[0] == 0x80) { 234 | map.value = "flow" 235 | sendEvent(name: "water", value: "dry") 236 | } else if(cmd.sensorValue[0] == 0x00) { 237 | map.value = "none" 238 | sendEvent(name: "water", value: "dry") 239 | } else if(cmd.sensorValue[0] == 0xFF) { 240 | map.value = "overflow" 241 | sendEvent(name: "water", value: "wet") 242 | sendAlarm("waterOverflow") 243 | } 244 | } */ 245 | return map 246 | } 247 | 248 | def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) 249 | { 250 | def map = [:] 251 | map.name = "gpm" 252 | def delta = cmd.scaledMeterValue - cmd.scaledPreviousMeterValue 253 | if (delta < 0 || delta > 10000) { 254 | log.error(cmd) 255 | delta = 0 256 | } 257 | 258 | map.value = delta * 10 259 | map.unit = "gpm" 260 | sendDataToCloud(delta) 261 | sendEvent(name: "cumulative", value: cmd.scaledMeterValue, displayed: false, unit: "gal") 262 | return map 263 | } 264 | 265 | def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) 266 | { 267 | def map = [:] 268 | if (cmd.zwaveAlarmType == 8) // Power Alarm 269 | { 270 | map.name = "powerState" // For Tile (shows in "Recently") 271 | if (cmd.zwaveAlarmEvent == 2) // AC Mains Disconnected 272 | { 273 | map.value = "disconnected" 274 | sendAlarm("acMainsDisconnected") 275 | } 276 | else if (cmd.zwaveAlarmEvent == 3) // AC Mains Reconnected 277 | { 278 | map.value = "reconnected" 279 | sendAlarm("acMainsReconnected") 280 | } 281 | else if (cmd.zwaveAlarmEvent == 0x0B) // Replace Battery Now 282 | { 283 | map.value = "noBattery" 284 | sendAlarm("replaceBatteryNow") 285 | } 286 | else if (cmd.zwaveAlarmEvent == 0x00) // Battery Replaced 287 | { 288 | map.value = "batteryReplaced" 289 | sendAlarm("batteryReplaced") 290 | } 291 | } 292 | else if (cmd.zwaveAlarmType == 4) // Heat Alarm 293 | { 294 | map.name = "heatState" 295 | if (cmd.zwaveAlarmEvent == 0) // Normal 296 | { 297 | map.value = "normal" 298 | } 299 | else if (cmd.zwaveAlarmEvent == 1) // Overheat 300 | { 301 | map.value = "overheated" 302 | sendAlarm("tempOverheated") 303 | } 304 | else if (cmd.zwaveAlarmEvent == 5) // Underheat 305 | { 306 | map.value = "freezing" 307 | sendAlarm("tempFreezing") 308 | } 309 | } 310 | else if (cmd.zwaveAlarmType == 5) // Water Alarm 311 | { 312 | map.name = "waterState" 313 | if (cmd.zwaveAlarmEvent == 0) // Normal 314 | { 315 | map.value = "none" 316 | sendEvent(name: "water", value: "dry") 317 | } 318 | else if (cmd.zwaveAlarmEvent == 6) // Flow Detected 319 | { 320 | if(cmd.eventParameter[0] == 2) 321 | { 322 | map.value = "flow" 323 | sendEvent(name: "water", value: "dry") 324 | } 325 | else if(cmd.eventParameter[0] == 3) 326 | { 327 | map.value = "overflow" 328 | sendAlarm("waterOverflow") 329 | sendEvent(name: "water", value: "wet") 330 | } 331 | } 332 | } 333 | //log.debug "alarmV2: $cmd" 334 | 335 | return map 336 | } 337 | 338 | def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { 339 | def map = [:] 340 | if(cmd.batteryLevel == 0xFF) { 341 | map.name = "battery" 342 | map.value = 1 343 | map.descriptionText = "${device.displayName} has a low battery" 344 | map.displayed = true 345 | } else { 346 | map.name = "battery" 347 | map.value = cmd.batteryLevel > 0 ? cmd.batteryLevel.toString() : 1 348 | map.unit = "%" 349 | map.displayed = false 350 | } 351 | return map 352 | } 353 | 354 | def zwaveEvent(physicalgraph.zwave.Command cmd) 355 | { 356 | log.debug "COMMAND CLASS: $cmd" 357 | } 358 | 359 | def sendDataToCloud(double data) 360 | { 361 | def params = [ 362 | uri: "https://iot.swiftlet.technology", 363 | path: "/fortrezz/post.php", 364 | body: [ 365 | id: device.id, 366 | value: data, 367 | email: registerEmail 368 | ] 369 | ] 370 | 371 | //log.debug("POST parameters: ${params}") 372 | try { 373 | httpPostJson(params) { resp -> 374 | resp.headers.each { 375 | //log.debug "${it.name} : ${it.value}" 376 | } 377 | log.debug "sendDataToCloud query response: ${resp.data}" 378 | } 379 | } catch (e) { 380 | log.debug "something went wrong: $e" 381 | } 382 | } 383 | 384 | def getTemperature(value) { 385 | if(location.temperatureScale == "C"){ 386 | return value 387 | } else { 388 | return Math.round(celsiusToFahrenheit(value)) 389 | } 390 | } 391 | 392 | private getPictureName(category) { 393 | //def pictureUuid = device.id.toString().replaceAll('-', '') 394 | def pictureUuid = java.util.UUID.randomUUID().toString().replaceAll('-', '') 395 | 396 | def name = "image" + "_$pictureUuid" + "_" + category + ".png" 397 | return name 398 | } 399 | 400 | def api(method, args = [], success = {}) { 401 | def methods = [ 402 | //"snapshot": [uri: "http://${ip}:${port}/snapshot.cgi${login()}&${args}", type: "post"], 403 | "24hrs": [uri: "https://iot.swiftlet.technology/fortrezz/chart.php?uuid=${device.id}&tz=${location.timeZone.ID}&type=1", type: "get"], 404 | "7days": [uri: "https://iot.swiftlet.technology/fortrezz/chart.php?uuid=${device.id}&tz=${location.timeZone.ID}&type=2", type: "get"], 405 | "4weeks": [uri: "https://iot.swiftlet.technology/fortrezz/chart.php?uuid=${device.id}&tz=${location.timeZone.ID}&type=3", type: "get"], 406 | ] 407 | 408 | def request = methods.getAt(method) 409 | 410 | return doRequest(request.uri, request.type, success) 411 | } 412 | 413 | private doRequest(uri, type, success) { 414 | log.debug(uri) 415 | 416 | if(type == "post") { 417 | httpPost(uri , "", success) 418 | } 419 | 420 | else if(type == "get") { 421 | httpGet(uri, success) 422 | } 423 | } 424 | 425 | def sendAlarm(text) 426 | { 427 | sendEvent(name: "alarmState", value: text, descriptionText: text, displayed: false) 428 | } 429 | 430 | def setThreshhold(rate) 431 | { 432 | log.debug "Setting Threshhold to ${rate}" 433 | 434 | def event = createEvent(name: "lastThreshhold", value: rate, displayed: false) 435 | def cmds = [] 436 | cmds << zwave.configurationV2.configurationSet(configurationValue: [(int)Math.round(rate*10)], parameterNumber: 5, size: 1).format() 437 | sendEvent(event) 438 | return response(cmds) // return a list containing the event and the result of response() 439 | } -------------------------------------------------------------------------------- /flow meter (fmi)/devicehandler.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * FortrezZ Flow Meter Interface 3 | * 4 | * Copyright 2016 FortrezZ, LLC 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | metadata { 17 | definition (name: "FortrezZ Flow Meter Interface", namespace: "fortrezz", author: "Daniel Kurin") { 18 | capability "Battery" 19 | capability "Energy Meter" 20 | capability "Image Capture" 21 | capability "Temperature Measurement" 22 | capability "Sensor" 23 | capability "Water Sensor" 24 | 25 | attribute "gpm", "number" 26 | attribute "cumulative", "number" 27 | attribute "alarmState", "string" 28 | attribute "chartMode", "string" 29 | attribute "lastThreshhold", "number" 30 | 31 | 32 | command "chartMode" 33 | command "zero" 34 | command "setHighFlowLevel", ["number"] 35 | 36 | fingerprint deviceId: "0x2101", inClusters: "0x5E, 0x86, 0x72, 0x5A, 0x73, 0x71, 0x85, 0x59, 0x32, 0x31, 0x70, 0x80, 0x7A" 37 | } 38 | 39 | simulator { 40 | // TODO: define status and reply messages here 41 | } 42 | 43 | preferences { 44 | input ("version", "text", title: "Plugin Version 1.5", description:"", required: false, displayDuringSetup: true) 45 | input "gallonThreshhold", "decimal", title: "High Flow Rate Threshhold", description: "Flow rate (in gpm) that will trigger a notification.", defaultValue: 5, required: false, displayDuringSetup: true 46 | input("registerEmail", type: "email", required: false, title: "Email Address", description: "Register your device with FortrezZ", displayDuringSetup: true) 47 | } 48 | 49 | tiles(scale: 2) { 50 | carouselTile("flowHistory", "device.image", width: 6, height: 3) { } 51 | valueTile("battery", "device.battery", inactiveLabel: false, width: 2, height: 2) { 52 | state "battery", label:'${currentValue}%\nBattery', unit:"" 53 | } 54 | valueTile("temperature", "device.temperature", width: 2, height: 2) { 55 | state("temperature", label:'${currentValue}°', 56 | backgroundColors:[ 57 | [value: 31, color: "#153591"], 58 | [value: 44, color: "#1e9cbb"], 59 | [value: 59, color: "#90d2a7"], 60 | [value: 74, color: "#44b621"], 61 | [value: 84, color: "#f1d801"], 62 | [value: 95, color: "#d04e00"], 63 | [value: 96, color: "#bc2323"] 64 | ] 65 | ) 66 | } 67 | valueTile("gpm", "device.gpm", inactiveLabel: false, width: 2, height: 2) { 68 | state "gpm", label:'${currentValue}gpm', unit:"" 69 | } 70 | standardTile("powerState", "device.powerState", width: 2, height: 2) { 71 | state "reconnected", icon:"http://swiftlet.technology/wp-content/uploads/2016/02/Connected-64.png", backgroundColor:"#cccccc" 72 | state "disconnected", icon:"http://swiftlet.technology/wp-content/uploads/2016/02/Disconnected-64.png", backgroundColor:"#cc0000" 73 | state "batteryReplaced", icon:"http://swiftlet.technology/wp-content/uploads/2016/04/Full-Battery-96.png", backgroundColor:"#cccccc" 74 | state "noBattery", icon:"http://swiftlet.technology/wp-content/uploads/2016/04/No-Battery-96.png", backgroundColor:"#cc0000" 75 | } 76 | standardTile("waterState", "device.waterState", width: 2, height: 2, canChangeIcon: true) { 77 | state "none", icon:"http://cdn.device-icons.smartthings.com/Weather/weather12-icn@2x.png", backgroundColor:"#cccccc", label: "No Flow" 78 | state "flow", icon:"http://cdn.device-icons.smartthings.com/Weather/weather12-icn@2x.png", backgroundColor:"#53a7c0", label: "Flow" 79 | state "overflow", icon:"http://cdn.device-icons.smartthings.com/Weather/weather12-icn@2x.png", backgroundColor:"#cc0000", label: "High Flow" 80 | } 81 | standardTile("heatState", "device.heatState", width: 2, height: 2) { 82 | state "normal", label:'Normal', icon:"st.alarm.temperature.normal", backgroundColor:"#ffffff" 83 | state "freezing", label:'Freezing', icon:"st.alarm.temperature.freeze", backgroundColor:"#2eb82e" 84 | state "overheated", label:'Overheated', icon:"st.alarm.temperature.overheat", backgroundColor:"#F80000" 85 | } 86 | standardTile("take1", "device.image", width: 2, height: 2, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false, decoration: "flat") { 87 | state "take", label: "", action: "Image Capture.take", nextState:"taking", icon: "st.secondary.refresh" 88 | } 89 | standardTile("chartMode", "device.chartMode", width: 2, height: 2, canChangeIcon: false, canChangeBackground: false, decoration: "flat") { 90 | state "day", label:'24 Hours\n(press to change)', nextState: "week", action: 'chartMode' 91 | state "week", label:'7 Days\n(press to change)', nextState: "month", action: 'chartMode' 92 | state "month", label:'4 Weeks\n(press to change)', nextState: "day", action: 'chartMode' 93 | } 94 | valueTile("zeroTile", "device.zero", width: 2, height: 2, canChangeIcon: false, canChangeBackground: false, decoration: "flat") { 95 | state "zero", label:'Zero', action: 'zero' 96 | } 97 | main (["waterState"]) 98 | details(["flowHistory", "chartMode", "take1", "temperature", "gpm", "waterState", "battery"]) 99 | } 100 | 101 | } 102 | 103 | // parse events into attributes 104 | def parse(String description) { 105 | def results = [] 106 | if (description.startsWith("Err")) { 107 | results << createEvent(descriptionText:description, displayed:true) 108 | } else { 109 | def cmd = zwave.parse(description, [ 0x80: 1, 0x84: 1, 0x71: 2, 0x72: 1 ]) 110 | if (cmd) { 111 | results << createEvent( zwaveEvent(cmd) ) 112 | } 113 | } 114 | //log.debug "\"$description\" parsed to ${results.inspect()}" 115 | if(gallonThreshhold != device.currentValue("lastThreshhold")) 116 | { 117 | results << setThreshhold(gallonThreshhold) 118 | } 119 | log.debug "zwave parsed to ${results.inspect()}" 120 | return results 121 | } 122 | 123 | def updated() 124 | { 125 | log.debug("Updated") 126 | } 127 | 128 | def setHighFlowLevel(level) 129 | { 130 | setThreshhold(level) 131 | } 132 | 133 | def take() { 134 | def mode = device.currentValue("chartMode") 135 | if(mode == "day") 136 | { 137 | take1() 138 | } 139 | else if(mode == "week") 140 | { 141 | take7() 142 | } 143 | else if(mode == "month") 144 | { 145 | take28() 146 | } 147 | } 148 | 149 | def chartMode(string) { 150 | log.debug("ChartMode") 151 | def state = device.currentValue("chartMode") 152 | def tempValue = "" 153 | switch(state) 154 | { 155 | case "day": 156 | tempValue = "week" 157 | break 158 | 159 | case "week": 160 | tempValue = "month" 161 | break 162 | 163 | case "month": 164 | tempValue = "day" 165 | break 166 | 167 | default: 168 | tempValue = "day" 169 | break 170 | } 171 | sendEvent(name: "chartMode", value: tempValue) 172 | take() 173 | } 174 | 175 | def take1() { 176 | api("24hrs", "") { 177 | log.debug("Image captured") 178 | 179 | if(it.headers.'Content-Type'.contains("image/png")) { 180 | if(it.data) { 181 | storeImage(getPictureName("24hrs"), it.data) 182 | } 183 | } 184 | } 185 | } 186 | 187 | def take7() { 188 | api("7days", "") { 189 | log.debug("Image captured") 190 | 191 | if(it.headers.'Content-Type'.contains("image/png")) { 192 | if(it.data) { 193 | storeImage(getPictureName("7days"), it.data) 194 | } 195 | } 196 | } 197 | } 198 | 199 | def take28() { 200 | api("4weeks", "") { 201 | log.debug("Image captured") 202 | 203 | if(it.headers.'Content-Type'.contains("image/png")) { 204 | if(it.data) { 205 | storeImage(getPictureName("4weeks"), it.data) 206 | } 207 | } 208 | } 209 | } 210 | 211 | def zero() 212 | { 213 | delayBetween([ 214 | zwave.meterV3.meterReset().format(), 215 | zwave.meterV3.meterGet().format(), 216 | zwave.firmwareUpdateMdV2.firmwareMdGet().format(), 217 | ], 100) 218 | } 219 | 220 | def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) 221 | { 222 | log.debug cmd 223 | def map = [:] 224 | if(cmd.sensorType == 1) { 225 | map = [name: "temperature"] 226 | if(cmd.scale == 0) { 227 | map.value = getTemperature(cmd.scaledSensorValue) 228 | } else { 229 | map.value = cmd.scaledSensorValue 230 | } 231 | map.unit = location.temperatureScale 232 | } /* else if(cmd.sensorType == 2) { 233 | map = [name: "waterState"] 234 | if(cmd.sensorValue[0] == 0x80) { 235 | map.value = "flow" 236 | sendEvent(name: "water", value: "dry") 237 | } else if(cmd.sensorValue[0] == 0x00) { 238 | map.value = "none" 239 | sendEvent(name: "water", value: "dry") 240 | } else if(cmd.sensorValue[0] == 0xFF) { 241 | map.value = "overflow" 242 | sendEvent(name: "water", value: "wet") 243 | sendAlarm("waterOverflow") 244 | } 245 | } */ 246 | return map 247 | } 248 | 249 | def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) 250 | { 251 | def map = [:] 252 | map.name = "gpm" 253 | def delta = cmd.scaledMeterValue - cmd.scaledPreviousMeterValue 254 | if (delta < 0 || delta > 10000) { 255 | log.error(cmd) 256 | delta = 0 257 | } 258 | 259 | map.value = delta 260 | map.unit = "gpm" 261 | sendDataToCloud(delta) 262 | sendEvent(name: "cumulative", value: cmd.scaledMeterValue, displayed: false, unit: "gal") 263 | return map 264 | } 265 | 266 | def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) 267 | { 268 | def map = [:] 269 | if (cmd.zwaveAlarmType == 8) // Power Alarm 270 | { 271 | map.name = "powerState" // For Tile (shows in "Recently") 272 | if (cmd.zwaveAlarmEvent == 2) // AC Mains Disconnected 273 | { 274 | map.value = "disconnected" 275 | sendAlarm("acMainsDisconnected") 276 | } 277 | else if (cmd.zwaveAlarmEvent == 3) // AC Mains Reconnected 278 | { 279 | map.value = "reconnected" 280 | sendAlarm("acMainsReconnected") 281 | } 282 | else if (cmd.zwaveAlarmEvent == 0x0B) // Replace Battery Now 283 | { 284 | map.value = "noBattery" 285 | sendAlarm("replaceBatteryNow") 286 | } 287 | else if (cmd.zwaveAlarmEvent == 0x00) // Battery Replaced 288 | { 289 | map.value = "batteryReplaced" 290 | sendAlarm("batteryReplaced") 291 | } 292 | } 293 | else if (cmd.zwaveAlarmType == 4) // Heat Alarm 294 | { 295 | map.name = "heatState" 296 | if (cmd.zwaveAlarmEvent == 0) // Normal 297 | { 298 | map.value = "normal" 299 | } 300 | else if (cmd.zwaveAlarmEvent == 1) // Overheat 301 | { 302 | map.value = "overheated" 303 | sendAlarm("tempOverheated") 304 | } 305 | else if (cmd.zwaveAlarmEvent == 5) // Underheat 306 | { 307 | map.value = "freezing" 308 | sendAlarm("tempFreezing") 309 | } 310 | } 311 | else if (cmd.zwaveAlarmType == 5) // Water Alarm 312 | { 313 | map.name = "waterState" 314 | if (cmd.zwaveAlarmEvent == 0) // Normal 315 | { 316 | map.value = "none" 317 | sendEvent(name: "water", value: "dry") 318 | } 319 | else if (cmd.zwaveAlarmEvent == 6) // Flow Detected 320 | { 321 | if(cmd.eventParameter[0] == 2) 322 | { 323 | map.value = "flow" 324 | sendEvent(name: "water", value: "dry") 325 | } 326 | else if(cmd.eventParameter[0] == 3) 327 | { 328 | map.value = "overflow" 329 | sendAlarm("waterOverflow") 330 | sendEvent(name: "water", value: "wet") 331 | } 332 | } 333 | } 334 | //log.debug "alarmV2: $cmd" 335 | 336 | return map 337 | } 338 | 339 | def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { 340 | def map = [:] 341 | if(cmd.batteryLevel == 0xFF) { 342 | map.name = "battery" 343 | map.value = 1 344 | map.descriptionText = "${device.displayName} has a low battery" 345 | map.displayed = true 346 | } else { 347 | map.name = "battery" 348 | map.value = cmd.batteryLevel > 0 ? cmd.batteryLevel.toString() : 1 349 | map.unit = "%" 350 | map.displayed = false 351 | } 352 | return map 353 | } 354 | 355 | def zwaveEvent(physicalgraph.zwave.Command cmd) 356 | { 357 | log.debug "COMMAND CLASS: $cmd" 358 | } 359 | 360 | def sendDataToCloud(double data) 361 | { 362 | def params = [ 363 | uri: "https://iot.swiftlet.technology", 364 | path: "/fortrezz/post.php", 365 | body: [ 366 | id: device.id, 367 | value: data, 368 | email: registerEmail 369 | ] 370 | ] 371 | 372 | //log.debug("POST parameters: ${params}") 373 | try { 374 | httpPostJson(params) { resp -> 375 | resp.headers.each { 376 | //log.debug "${it.name} : ${it.value}" 377 | } 378 | log.debug "sendDataToCloud query response: ${resp.data}" 379 | } 380 | } catch (e) { 381 | log.debug "something went wrong: $e" 382 | } 383 | } 384 | 385 | def getTemperature(value) { 386 | if(location.temperatureScale == "C"){ 387 | return value 388 | } else { 389 | return Math.round(celsiusToFahrenheit(value)) 390 | } 391 | } 392 | 393 | private getPictureName(category) { 394 | //def pictureUuid = device.id.toString().replaceAll('-', '') 395 | def pictureUuid = java.util.UUID.randomUUID().toString().replaceAll('-', '') 396 | 397 | def name = "image" + "_$pictureUuid" + "_" + category + ".png" 398 | return name 399 | } 400 | 401 | def api(method, args = [], success = {}) { 402 | def methods = [ 403 | //"snapshot": [uri: "http://${ip}:${port}/snapshot.cgi${login()}&${args}", type: "post"], 404 | "24hrs": [uri: "https://iot.swiftlet.technology/fortrezz/chart.php?uuid=${device.id}&tz=${location.timeZone.ID}&type=1", type: "get"], 405 | "7days": [uri: "https://iot.swiftlet.technology/fortrezz/chart.php?uuid=${device.id}&tz=${location.timeZone.ID}&type=2", type: "get"], 406 | "4weeks": [uri: "https://iot.swiftlet.technology/fortrezz/chart.php?uuid=${device.id}&tz=${location.timeZone.ID}&type=3", type: "get"], 407 | ] 408 | 409 | def request = methods.getAt(method) 410 | 411 | return doRequest(request.uri, request.type, success) 412 | } 413 | 414 | private doRequest(uri, type, success) { 415 | log.debug(uri) 416 | 417 | if(type == "post") { 418 | httpPost(uri , "", success) 419 | } 420 | 421 | else if(type == "get") { 422 | httpGet(uri, success) 423 | } 424 | } 425 | 426 | def sendAlarm(text) 427 | { 428 | sendEvent(name: "alarmState", value: text, descriptionText: text, displayed: false) 429 | } 430 | 431 | def setThreshhold(rate) 432 | { 433 | log.debug "Setting Threshhold to ${rate}" 434 | 435 | def event = createEvent(name: "lastThreshhold", value: rate, displayed: false) 436 | def cmds = [] 437 | cmds << zwave.configurationV2.configurationSet(configurationValue: [(int)Math.round(rate*10)], parameterNumber: 5, size: 1).format() 438 | sendEvent(event) 439 | return response(cmds) // return a list containing the event and the result of response() 440 | } -------------------------------------------------------------------------------- /mimo2/devicehandler-a.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * MIMO2 Device Handler 3 | * 4 | * Copyright 2016 FortrezZ, LLC 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | metadata { 17 | definition (name: "FortrezZ MIMO2+", namespace: "fortrezz", author: "FortrezZ, LLC") { 18 | capability "Alarm" 19 | capability "Contact Sensor" 20 | capability "Switch" 21 | capability "Voltage Measurement" 22 | capability "Configuration" 23 | capability "Refresh" 24 | 25 | attribute "powered", "string" 26 | attribute "relay", "string" 27 | 28 | attribute "relay2", "string" 29 | attribute "contact2", "string" 30 | attribute "voltage2", "string" 31 | 32 | command "on" 33 | command "off" 34 | command "on2" 35 | command "off2" 36 | 37 | fingerprint deviceId: "0x2100", inClusters: "0x5E,0x86,0x72,0x5A,0x59,0x71,0x98,0x7A" 38 | preferences { 39 | 40 | input ("version", "text", title: "Plugin Version 1.5", description:"", required: false, displayDuringSetup: true) 41 | 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) 42 | input ("RelaySwitchDelay2", "decimal", title: "Relay 2", description: "Numbers 0 to 3.1 allowed.", defaultValue: 0, required: false, displayDuringSetup: true) 43 | input ("Sig1AD", "bool", title: "Switch off for digital, on for analog:\nSIG1", required: false, displayDuringSetup: true , defaultValue: false) 44 | input ("Sig2AD", "bool", title: "SIG2", required: false, displayDuringSetup: true, defaultValue: false) 45 | } // 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. :( ) 46 | } 47 | 48 | 49 | 50 | 51 | tiles { 52 | standardTile("switch", "device.switch", width: 2, height: 2) { 53 | state "on", label: "Relay 1 On", action: "off", icon: "http://swiftlet.technology/wp-content/uploads/2016/06/Switch-On-104-edit.png", backgroundColor: "#53a7c0" 54 | state "off", label: "Relay 1 Off", action: "on", icon: "http://swiftlet.technology/wp-content/uploads/2016/06/Switch-Off-104-edit.png", backgroundColor: "#ffffff" 55 | 56 | } 57 | standardTile("switch2", "device.switch2", width: 2, height: 2, inactiveLabel: false) { 58 | state "on", label: "Relay 2 On", action: "off2", icon: "http://swiftlet.technology/wp-content/uploads/2016/06/Switch-On-104-edit.png", backgroundColor: "#53a7c0" 59 | state "off", label: 'Relay 2 Off', action: "on2", icon: "http://swiftlet.technology/wp-content/uploads/2016/06/Switch-Off-104-edit.png", backgroundColor: "#ffffff" 60 | } 61 | standardTile("anaDig1", "device.anaDig1", inactiveLabel: false) { 62 | state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#ffa81e" 63 | state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#79b821" 64 | state "val", label:'${currentValue}v', unit:"", defaultState: true 65 | } 66 | standardTile("anaDig2", "device.anaDig2", inactiveLabel: false) { 67 | state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#ffa81e" 68 | state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#79b821" 69 | state "val", label:'${currentValue}v', unit:"", defaultState: true 70 | } 71 | standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { 72 | state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" 73 | } 74 | standardTile("powered", "device.powered", inactiveLabel: false) { 75 | state "powerOn", label: "Power On", icon: "st.switches.switch.on", backgroundColor: "#79b821" 76 | state "powerOff", label: "Power Off", icon: "st.switches.switch.off", backgroundColor: "#ffa81e" 77 | } 78 | standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") { 79 | state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" 80 | } 81 | standardTile("blank", "device.blank", inactiveLabel: true, decoration: "flat") { 82 | state("blank", label: '') 83 | } 84 | main (["switch"]) 85 | details(["switch", "anaDig1", "blank", "switch2", "anaDig2", "blank", "configure", "refresh", "powered"]) 86 | } 87 | } 88 | 89 | // parse events into attributes 90 | def parse(String description) { 91 | def result = null 92 | def cmd = zwave.parse(description) 93 | 94 | if (cmd.CMD == "7105") { //Mimo sent a power loss report 95 | log.debug "Device lost power" 96 | sendEvent(name: "powered", value: "powerOff", descriptionText: "$device.displayName lost power") 97 | } else { 98 | sendEvent(name: "powered", value: "powerOn", descriptionText: "$device.displayName regained power") 99 | } 100 | if (cmd) { 101 | def eventReturn = zwaveEvent(cmd) 102 | if(eventReturn in physicalgraph.device.HubMultiAction) { 103 | result = eventReturn 104 | } 105 | else { 106 | result = createEvent(eventReturn) 107 | } 108 | } 109 | log.debug "Parse returned ${result} $cmd.CMD" 110 | return result 111 | } 112 | 113 | def updated() { // neat built-in smartThings function which automatically runs whenever any setting inputs are changed in the preferences menu of the device handler 114 | 115 | 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. 116 | { 117 | state.count = 0 118 | log.debug "Settings Updated..." 119 | return response(delayBetween([ 120 | configure(), // the response() function is used for sending commands in reponse to an event, without it, no zWave commands will work for contained function 121 | refresh() 122 | ], 200)) 123 | } 124 | else {state.count = 1} 125 | } 126 | 127 | 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. 128 | { 129 | log.debug "sent a BasicSet command" 130 | return response(refresh()) 131 | } 132 | 133 | def zwaveEvent(int endPoint, physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) // event to get the state of the digital sensor SIG1 and SIG2 134 | { 135 | log.debug "sent a sensorBinaryReport command" 136 | return response(refresh()) 137 | } 138 | 139 | def zwaveEvent(int endPoint, physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) // event for seeing the states of relay 1 and relay 2 140 | { 141 | def map = [:] // map for containing the name and state fo the specified relay 142 | if (endPoint == 3) 143 | { 144 | if (cmd.value) // possible values are 255 and 0 (0 is false) 145 | {map.value = "on"} 146 | else 147 | {map.value = "off"} 148 | map.name = "switch" 149 | 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 150 | return [name: "switch", value: cmd.value ? "on" : "off"] 151 | } 152 | else if (endPoint == 4) 153 | { 154 | if (cmd.value) 155 | {map.value = "on"} 156 | else 157 | {map.value = "off"} 158 | map.name = "switch2" 159 | sendEvent(name: "relay2", value: "$map.value") 160 | 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 161 | return [name: "switch2", value: cmd.value ? "on" : "off"] 162 | } 163 | } 164 | 165 | def zwaveEvent (int endPoint, physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) // sensorMultilevelReport is used to report the value of the analog voltage for SIG1 166 | { 167 | def map = [:] 168 | def stdEvent = [:] 169 | def voltageVal = CalculateVoltage(cmd.scaledSensorValue) // saving the scaled Sensor Value used to enter into a large formula to determine actual voltage value 170 | if (endPoint == 1) //endPoint 1 is for SIG1 171 | { 172 | if (state.AD1 == false) // state.AD1 is to determine which state the anaDig1 tile should be in (either analogue or digital mode) 173 | { 174 | map.name = "anaDig1" 175 | stdEvent.name = "contact" 176 | if (voltageVal < 2) { // DK changed to 2v to follow LED behavior 177 | map.value = "closed" 178 | stdEvent.value = "closed" 179 | } 180 | else 181 | { 182 | map.value = "open" 183 | stdEvent.value = "open" 184 | } 185 | } 186 | else //or state.AD1 is true for analogue mode 187 | { 188 | map.name = "anaDig1" 189 | stdEvent.name = "voltage" 190 | map.value = voltageVal 191 | stdEvent.value = voltageVal 192 | map.unit = "v" 193 | stdEvent.unit = "v" 194 | } 195 | } 196 | else if (endPoint == 2) // endpoint 2 is for SIG2 197 | { 198 | if (state.AD2 == false) 199 | { 200 | map.name = "anaDig2" 201 | stdEvent.name = "contact2" 202 | if (voltageVal < 2) { 203 | map.value = "closed" 204 | stdEvent.value = "closed" 205 | } 206 | else 207 | { 208 | map.value = "open" 209 | stdEvent.value = "open" 210 | } 211 | } 212 | else 213 | { 214 | map.name = "anaDig2" 215 | stdEvent.name = "voltage2" 216 | map.value = voltageVal 217 | stdEvent.value = voltageVal 218 | map.unit = "v" 219 | stdEvent.unit = "v" 220 | } 221 | } 222 | log.debug "sent a SensorMultilevelReport $map.name $map.value" 223 | sendEvent(stdEvent) 224 | return map 225 | } 226 | 227 | def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { //standard security encapsulation event code (should be the same on all device handlers) 228 | def encapsulatedCommand = cmd.encapsulatedCommand() 229 | // can specify command class versions here like in zwave.parse 230 | if (encapsulatedCommand) { 231 | return zwaveEvent(encapsulatedCommand) 232 | } 233 | } 234 | 235 | // MultiChannelCmdEncap and MultiInstanceCmdEncap are ways that devices 236 | // can indicate that a message is coming from one of multiple subdevices 237 | // or "endpoints" that would otherwise be indistinguishable 238 | def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { 239 | def encapsulatedCommand = cmd.encapsulatedCommand() 240 | log.debug ("Command from endpoint ${cmd.sourceEndPoint}: ${encapsulatedCommand}") 241 | 242 | if (encapsulatedCommand) { 243 | return zwaveEvent(cmd.sourceEndPoint, encapsulatedCommand) 244 | } 245 | } 246 | 247 | def zwaveEvent(int endPoint, physicalgraph.zwave.commands.multichannelassociationv2.MultiChannelAssociationReport cmd) { 248 | 249 | log.debug "sent an Association Report" 250 | log.debug " ${cmd.groupingIdentifier}" 251 | //return [:] 252 | } 253 | 254 | def zwaveEvent(physicalgraph.zwave.Command cmd) { 255 | // Handles all Z-Wave commands we aren't interested in 256 | log.debug("Un-parsed Z-Wave message ${cmd}") 257 | return [:] 258 | } 259 | 260 | def CalculateVoltage(ADCvalue) // used to calculate the voltage based on the collected Scaled sensor value of the multilevel sensor event 261 | { 262 | 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))) 263 | return volt.round(1) 264 | } 265 | 266 | 267 | def configure() { 268 | log.debug "Configuring...." 269 | def sig1 270 | def sig2 271 | if (Sig1AD == true || Sig1AD == null) 272 | { sig1 = 0x01 273 | state.AD1 = true} 274 | else if (Sig1AD == false) 275 | { sig1 = 0x40 276 | state.AD1 = false} 277 | if (Sig2AD == true || Sig2AD == null) 278 | { sig2 = 0x01 279 | state.AD2 = true} 280 | else if (Sig2AD == false) 281 | { sig2 = 0x40 282 | state.AD2 = false} 283 | 284 | def delay = 0; 285 | def delay2 = 0; 286 | 287 | if (RelaySwitchDelay != null) {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 288 | 289 | if (RelaySwitchDelay2 != null) {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 290 | 291 | if (delay > 31) 292 | { 293 | log.debug "Relay 1 input ${delay / 10} set too high. Max value is 3.1" 294 | delay = 31 295 | } 296 | if (delay < 0) 297 | { 298 | log.debug "Relay 1 input ${delay / 10} set too low. Min value is 0" 299 | delay = 0 300 | } 301 | if (delay2 > 31) 302 | { 303 | log.debug "Relay 2 input ${delay2 / 10} set too high. Max value is 3.1" 304 | delay2 = 31 305 | } 306 | if (delay2 < 0) 307 | { 308 | log.debug "Relay 2 input ${delay2 / 10} set too low. Min value is 0" 309 | delay = 0 310 | } 311 | 312 | return delayBetween([ 313 | encap(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]), 0), 314 | encap(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier:2, nodeId:[zwaveHubNodeId]), 0), 315 | 316 | encap(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier:2, nodeId:[zwaveHubNodeId]), 1), 317 | encap(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier:2, nodeId:[zwaveHubNodeId]), 2), 318 | encap(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]), 3), 319 | encap(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]), 4), 320 | 321 | secure(zwave.configurationV1.configurationSet(configurationValue: [sig1], parameterNumber: 3, size: 1)), // sends a multiLevelSensor report every 30 seconds for SIG1 322 | secure(zwave.configurationV1.configurationSet(configurationValue: [sig2], parameterNumber: 9, size: 1)), // sends a multiLevelSensor report every 30 seconds for SIG2 323 | secure(zwave.configurationV1.configurationSet(configurationValue: [delay], parameterNumber: 1, size: 1)), // configurationValue for parameterNumber means how many 100ms do you want the relay 324 | // to wait before it cycles again / size should just be 1 (for 1 byte.) 325 | secure(zwave.configurationV1.configurationSet(configurationValue: [delay2], parameterNumber: 2, size: 1)), 326 | 327 | ], 200) 328 | } 329 | 330 | def on() { 331 | return encap(zwave.basicV1.basicSet(value: 0xff), 3) // physically changes the relay from on to off and requests a report of the relay 332 | // oddly, smartThings automatically sends a switchBinaryGet() command whenever the above basicSet command is sent, so we don't need to send one here. 333 | } 334 | 335 | def off() { 336 | return encap(zwave.basicV1.basicSet(value: 0x00), 3) // physically changes the relay from on to off and requests a report of the relay 337 | // oddly, smartThings automatically sends a switchBinaryGet() command whenever the above basicSet command is sent, so we don't need to send one here. 338 | } 339 | 340 | def on2() { 341 | return encap(zwave.basicV1.basicSet(value: 0xff), 4) 342 | // oddly, smartThings automatically sends a switchBinaryGet() command whenever the above basicSet command is sent, so we don't need to send one here. 343 | } 344 | 345 | def off2() { 346 | return encap(zwave.basicV1.basicSet(value: 0x00), 4) 347 | // oddly, smartThings automatically sends a switchBinaryGet() command whenever the above basicSet command is sent, so we don't need to send one here. 348 | } 349 | 350 | def refresh() { 351 | log.debug "Refresh" 352 | return delayBetween([ 353 | encap(zwave.sensorMultilevelV5.sensorMultilevelGet(), 1),// requests a report of the anologue input voltage for SIG1 354 | encap(zwave.sensorMultilevelV5.sensorMultilevelGet(), 2),// requests a report of the anologue input voltage for SIG2 355 | encap(zwave.switchBinaryV1.switchBinaryGet(), 3), //requests a report of the relay to make sure that it changed for Relay 1 356 | encap(zwave.switchBinaryV1.switchBinaryGet(), 4), //requests a report of the relay to make sure that it changed for Relay 2 357 | ],200) 358 | } 359 | 360 | def refreshZWave() { 361 | log.debug "Refresh (Z-Wave Response)" 362 | return delayBetween([ 363 | encap(zwave.sensorMultilevelV5.sensorMultilevelGet(), 1),// requests a report of the anologue input voltage for SIG1 364 | encap(zwave.sensorMultilevelV5.sensorMultilevelGet(), 2),// requests a report of the anologue input voltage for SIG2 365 | encap(zwave.switchBinaryV1.switchBinaryGet(), 3), //requests a report of the relay to make sure that it changed for Relay 1 366 | encap(zwave.switchBinaryV1.switchBinaryGet(), 4) //requests a report of the relay to make sure that it changed for Relay 2 367 | ],200) 368 | } 369 | 370 | private secureSequence(commands, delay=200) { // decided not to use this 371 | return delayBetween(commands.collect{ secure(it) }, delay) 372 | } 373 | 374 | private secure(physicalgraph.zwave.Command cmd) { //take multiChannel message and securely encrypts the message so the device can read it 375 | return zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() 376 | } 377 | 378 | 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 379 | if (endpoint) { 380 | return secure(zwave.multiChannelV3.multiChannelCmdEncap(bitAddress: false, sourceEndPoint:0, destinationEndPoint: endpoint).encapsulate(cmd)) 381 | } else { 382 | return secure(cmd) 383 | } 384 | } -------------------------------------------------------------------------------- /mimo2/devicehandler-b.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * FortrezZ MIMO2+ B-Side 3 | * 4 | * Copyright 2016 FortrezZ, LLC 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | metadata { 17 | definition (name: "FortrezZ MIMO2+ B-Side", namespace: "fortrezz", author: "FortrezZ, LLC") { 18 | capability "Contact Sensor" 19 | capability "Relay Switch" 20 | capability "Switch" 21 | capability "Voltage Measurement" 22 | capability "Refresh" 23 | } 24 | preferences { 25 | input ("version", "text", title: "Plugin Version 1.5", description:"", required: false, displayDuringSetup: true) 26 | } 27 | 28 | tiles { 29 | standardTile("switch", "device.switch", width: 2, height: 2) { 30 | state "on", label: "Relay 2 On", action: "off", icon: "http://swiftlet.technology/wp-content/uploads/2016/06/Switch-On-104-edit.png", backgroundColor: "#53a7c0" 31 | state "off", label: "Relay 2 Off", action: "on", icon: "http://swiftlet.technology/wp-content/uploads/2016/06/Switch-Off-104-edit.png", backgroundColor: "#ffffff" 32 | } 33 | standardTile("anaDig1", "device.anaDig1", inactiveLabel: false) { 34 | state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#ffa81e" 35 | state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#79b821" 36 | state "val", label:'${currentValue}v', unit:"", defaultState: true 37 | } 38 | standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { 39 | state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" 40 | } 41 | standardTile("powered", "device.powered", inactiveLabel: false) { 42 | state "powerOn", label: "Power On", icon: "st.switches.switch.on", backgroundColor: "#79b821" 43 | state "powerOff", label: "Power Off", icon: "st.switches.switch.off", backgroundColor: "#ffa81e" 44 | } 45 | standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") { 46 | state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" 47 | } 48 | standardTile("blank", "device.blank", inactiveLabel: true, decoration: "flat") { 49 | state("blank", label: '') 50 | } 51 | main (["switch"]) 52 | details(["switch", "anaDig1", "blank", "blank", "refresh", "powered"]) 53 | } 54 | } 55 | 56 | // parse events into attributes 57 | def parse(String description) { 58 | log.debug "Parsing '${description}'" 59 | // TODO: handle 'contact' attribute 60 | // TODO: handle 'switch' attribute 61 | // TODO: handle 'switch' attribute 62 | // TODO: handle 'voltage' attribute 63 | 64 | } 65 | 66 | def eventParse(evt) { 67 | log.debug("Event: ${evt.name}=${evt.value}") 68 | switch(evt.name) { 69 | case "powered": 70 | sendEvent(name: evt.name, value: evt.value) 71 | break 72 | case "switch2": 73 | sendEvent(name: "switch", value: evt.value) 74 | break 75 | case "contact2": 76 | sendEvent(name: "contact", value: evt.value) 77 | break 78 | case "voltage2": 79 | sendEvent(name: "voltage", value: evt.value) 80 | break 81 | case "relay2": 82 | sendEvent(name: evt.name, value: evt.value) 83 | break 84 | case "anaDig2": 85 | sendEvent(name: "anaDig1", value: evt.value) 86 | break 87 | } 88 | } 89 | 90 | // handle commands 91 | def on() { 92 | parent.on2(device.id) 93 | log.debug("Executing 'on'") 94 | // TODO: Send Event to parent device for "on2" 95 | } 96 | 97 | def off() { 98 | parent.off2(device.id) 99 | log.debug("Executing 'off'") 100 | // TODO: Send Event to parent device for "off2" 101 | } 102 | def refresh() { 103 | parent.refresh2(device.id) 104 | log.debug("Executing 'refresh'") 105 | } -------------------------------------------------------------------------------- /mimo2/smartapp.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * FortrezZ MIMO2+ B-Side 3 | * 4 | * Copyright 2016 FortrezZ, LLC 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | definition( 17 | name: "FortrezZ MIMO2+ B-Side", 18 | namespace: "fortrezz", 19 | author: "FortrezZ, LLC", 20 | description: "Breaks the MIMO2 into two separate devices to allow automation on SIG2 and Relay 2.", 21 | category: "Convenience", 22 | iconUrl: "http://swiftlet.technology/wp-content/uploads/2016/05/logo-square-200-1.png", 23 | iconX2Url: "http://swiftlet.technology/wp-content/uploads/2016/05/logo-square-500.png", 24 | iconX3Url: "http://swiftlet.technology/wp-content/uploads/2016/05/logo-square.png", 25 | singleInstance: true) 26 | 27 | 28 | preferences { 29 | section("Plugin Version 1.5") {} 30 | section("Title") { 31 | input(name: "devices", type: "capability.voltageMeasurement", title: "MIMO2 devices", description: null, required: true, submitOnChange: true, multiple: true) 32 | } 33 | } 34 | 35 | def installed() { 36 | log.debug "Installed with settings: ${settings}" 37 | 38 | initialize() 39 | } 40 | 41 | def updated() { 42 | log.debug "Updated with settings: ${settings}" 43 | 44 | unsubscribe() 45 | initialize() 46 | } 47 | 48 | def initialize(){ 49 | log.debug("Devices: ${settings.devices}") 50 | settings.devices.each {//deviceId -> 51 | subscribe(it, "powered", events) 52 | subscribe(it, "switch2", events) 53 | subscribe(it, "contact2", events) 54 | subscribe(it, "voltage2", events) 55 | subscribe(it, "relay2", events) 56 | subscribe(it, "anaDig2", events) 57 | 58 | try { 59 | def existingDevice = getChildDevice(it.id) 60 | if(!existingDevice) { 61 | log.debug("Device ID: ${existingDevice}") 62 | def childDevice = addChildDevice("fortrezz", "FortrezZ MIMO2+ B-Side", it.id, null, [name: "Device.${it.id}", label: "${it.name} B-Side", completedSetup: true]) 63 | } 64 | } catch (e) { 65 | log.error "Error creating device: ${e}" 66 | } 67 | } 68 | 69 | getChildDevices().each { 70 | def test = it 71 | def search = settings.devices.find { getChildDevice(it.id).id == test.id } 72 | if(!search) { 73 | removeChildDevices(test) 74 | } 75 | } 76 | } 77 | 78 | def uninstalled() { 79 | removeChildDevices(getChildDevices()) 80 | } 81 | 82 | private removeChildDevices(delete) { 83 | delete.each { 84 | deleteChildDevice(it.deviceNetworkId) 85 | } 86 | } 87 | 88 | def on2(child) { 89 | log.debug("on2") 90 | def ret = child 91 | settings.devices.each {//deviceId -> 92 | def ch = getChildDevice(it.id) 93 | if(child == ch.id) { 94 | ret = "${child}, ${it.id}" 95 | it.on2() 96 | } 97 | } 98 | return ret 99 | } 100 | 101 | def off2(child) { 102 | log.debug("off2") 103 | def ret = child 104 | settings.devices.each {//deviceId -> 105 | def ch = getChildDevice(it.id) 106 | if(child == ch.id) { 107 | ret = "${child}, ${it.id}" 108 | it.off2() 109 | } 110 | } 111 | return ret 112 | } 113 | 114 | def refresh2(child) { 115 | log.debug("refresh2") 116 | def ret = child 117 | settings.devices.each {//deviceId -> 118 | def ch = getChildDevice(it.id) 119 | if(child == ch.id) { 120 | ret = "${child}, ${it.id}" 121 | it.refresh() 122 | } 123 | } 124 | return ret 125 | } 126 | 127 | def events(evt) { 128 | def ch = getChildDevice(evt.device.id) 129 | ch.eventParse(evt); 130 | log.debug("${evt.device.id} triggered ${evt.name}") 131 | } 132 | // TODO: implement event handlers -------------------------------------------------------------------------------- /mimolite/devicehandler.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * FortrezZ Flow Meter Interface 3 | * 4 | * Copyright 2016 FortrezZ, LLC 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | * Based on Todd Wackford's MimoLite Garage Door Opener 16 | */ 17 | metadata { 18 | // Automatically generated. Make future change here. 19 | definition (name: "FortrezZ MIMOlite", namespace: "fortrezz", author: "FortrezZ, LLC") { 20 | capability "Configuration" 21 | capability "Switch" 22 | capability "Refresh" 23 | capability "Contact Sensor" 24 | capability "Voltage Measurement" 25 | 26 | attribute "powered", "string" 27 | 28 | command "on" 29 | command "off" 30 | 31 | fingerprint deviceId: "0x1000", inClusters: "0x72,0x86,0x71,0x30,0x31,0x35,0x70,0x85,0x25,0x03" 32 | } 33 | 34 | simulator { 35 | // Simulator stuff 36 | 37 | } 38 | 39 | preferences { 40 | input ("version", "text", title: "Plugin Version 1.5", description:"", required: false, displayDuringSetup: true) 41 | input "RelaySwitchDelay", "decimal", title: "Delay between relay switch on and off in seconds. Only Numbers 0 to 3.0 allowed. 0 value will remove delay and allow relay to function as a standard switch", description: "Numbers 0 to 3.1 allowed.", defaultValue: 0, required: false, displayDuringSetup: true 42 | } 43 | 44 | 45 | // UI tile definitions 46 | tiles (scale: 2) { 47 | standardTile("switch", "device.switch", width: 4, height: 4, canChangeIcon: false, decoration: "flat") { 48 | state "on", label: "On", action: "off", icon: "http://swiftlet.technology/wp-content/uploads/2016/06/Switch-On-104-edit.png", backgroundColor: "#53a7c0" 49 | state "off", label: 'Off', action: "on", icon: "http://swiftlet.technology/wp-content/uploads/2016/06/Switch-Off-104-edit.png", backgroundColor: "#ffffff" 50 | } 51 | standardTile("contact", "device.contact", width: 2, height: 2, inactiveLabel: false) { 52 | state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#ffa81e" 53 | state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#79b821" 54 | } 55 | standardTile("refresh", "device.switch", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { 56 | state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" 57 | } 58 | standardTile("powered", "device.powered", width: 2, height: 2, inactiveLabel: false) { 59 | state "powerOn", label: "Power On", icon: "st.switches.switch.on", backgroundColor: "#79b821" 60 | state "powerOff", label: "Power Off", icon: "st.switches.switch.off", backgroundColor: "#ffa81e" 61 | } 62 | standardTile("configure", "device.configure", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { 63 | state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" 64 | } 65 | valueTile("voltage", "device.voltage", width: 2, height: 2) { 66 | state "val", label:'${currentValue}v', unit:"", defaultState: true 67 | } 68 | valueTile("voltageCounts", "device.voltageCounts", width: 2, height: 2) { 69 | state "val", label:'${currentValue}', unit:"", defaultState: true 70 | } 71 | main (["switch"]) 72 | details(["switch", "contact", "voltage", "powered", "refresh","configure"]) 73 | } 74 | } 75 | 76 | def parse(String description) { 77 | //log.debug "description is: ${description}" 78 | 79 | def result = null 80 | def cmd = zwave.parse(description, [0x20: 1, 0x84: 1, 0x30: 1, 0x70: 1, 0x31: 5]) 81 | 82 | //log.debug "command value is: $cmd.CMD" 83 | 84 | if (cmd.CMD == "7105") { //Mimo sent a power loss report 85 | log.debug "Device lost power" 86 | sendEvent(name: "powered", value: "powerOff", descriptionText: "$device.displayName lost power") 87 | } else { 88 | sendEvent(name: "powered", value: "powerOn", descriptionText: "$device.displayName regained power") 89 | } 90 | //log.debug "${device.currentValue('contact')}" // debug message to make sure the contact tile is working 91 | if (cmd) { 92 | result = createEvent(zwaveEvent(cmd)) 93 | } 94 | log.debug "Parse returned ${result?.descriptionText} $cmd.CMD" 95 | return result 96 | } 97 | 98 | def updated() { 99 | log.debug "Settings Updated..." 100 | configure() 101 | } 102 | //notes about zwaveEvents: 103 | // these are special overloaded functions which MUST be returned with a map similar to (return [name: "switch", value: "on"]) 104 | // not doing so will produce a null on the parse function, this will mess you up in the future. 105 | // Perhaps can use 'createEvent()' and return that as long as a map is inside it. 106 | def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { 107 | log.debug "switchBinaryReport ${cmd}" 108 | if (cmd.value) // if the switch is on it will not be 0, so on = true 109 | { 110 | return [name: "switch", value: "on"] // change switch value to on 111 | } 112 | else // if the switch sensor report says its off then do... 113 | { 114 | return [name: "switch", value: "off"] // change switch value to off 115 | } 116 | 117 | } 118 | 119 | // working on next for the analogue and digital stuff. 120 | def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) // basic set is essentially our digital sensor for SIG1 121 | { 122 | log.debug "sent a BasicSet command" 123 | //refresh() 124 | delayBetween([zwave.sensorMultilevelV5.sensorMultilevelGet().format()])// requests a report of the anologue input voltage 125 | [name: "contact", value: cmd.value ? "open" : "closed"]} 126 | //[name: "contact", value: cmd.value ? "open" : "closed", type: "digital"]} 127 | 128 | def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) 129 | { 130 | log.debug "sent a sensorBinaryReport command" 131 | refresh() 132 | [name: "contact", value: cmd.value ? "open" : "closed"] 133 | } 134 | 135 | 136 | 137 | def zwaveEvent (physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) // sensorMultilevelReport is used to report the value of the analog voltage for SIG1 138 | { 139 | log.debug "sent a SensorMultilevelReport" 140 | def ADCvalue = cmd.scaledSensorValue 141 | sendEvent(name: "voltageCounts", value: ADCvalue) 142 | 143 | CalculateVoltage(cmd.scaledSensorValue) 144 | } 145 | 146 | def zwaveEvent(physicalgraph.zwave.Command cmd) { 147 | // Handles all Z-Wave commands we aren't interested in 148 | log.debug("Un-parsed Z-Wave message ${cmd}") 149 | [:] 150 | } 151 | 152 | def CalculateVoltage(ADCvalue) 153 | { 154 | def map = [:] 155 | 156 | def volt = (((1.5338*(10**-16))*(ADCvalue**5)) - ((1.2630*(10**-12))*(ADCvalue**4)) + ((3.8111*(10**-9))*(ADCvalue**3)) - ((4.7739*(10**-6))*(ADCvalue**2)) + ((2.8558*(10**-3))*(ADCvalue)) - (2.2721*(10**-2))) 157 | 158 | //def volt = (((3.19*(10**-16))*(ADCvalue**5)) - ((2.18*(10**-12))*(ADCvalue**4)) + ((5.47*(10**-9))*(ADCvalue**3)) - ((5.68*(10**-6))*(ADCvalue**2)) + (0.0028*ADCvalue) - (0.0293)) 159 | //log.debug "$cmd.scale $cmd.precision $cmd.size $cmd.sensorType $cmd.sensorValue $cmd.scaledSensorValue" 160 | def voltResult = volt.round(1)// + "v" 161 | 162 | map.name = "voltage" 163 | map.value = voltResult 164 | map.unit = "v" 165 | return map 166 | } 167 | 168 | 169 | def configure() { 170 | def x = 0; 171 | if (RelaySwitchDelay != null) {x = (RelaySwitchDelay*10).toInteger()} 172 | log.debug "Configuring.... " //setting up to monitor power alarm and actuator duration 173 | 174 | delayBetween([ 175 | zwave.associationV1.associationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]).format(), // FYI: Group 3: If a power dropout occurs, the MIMOlite will send an Alarm Command Class report 176 | // (if there is enough available residual power) 177 | zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:[zwaveHubNodeId]).format(), // periodically send a multilevel sensor report of the ADC analog voltage to the input 178 | zwave.associationV1.associationSet(groupingIdentifier:4, nodeId:[zwaveHubNodeId]).format(), // when the input is digitally triggered or untriggered, snd a binary sensor report 179 | zwave.configurationV1.configurationSet(configurationValue: [x], parameterNumber: 11, size: 1).format() // configurationValue for parameterNumber means how many 100ms do you want the relay 180 | // to wait before it cycles again / size should just be 1 (for 1 byte.) 181 | //zwave.configurationV1.configurationGet(parameterNumber: 11).format() // gets the new parameter changes. not currently needed. (forces a null return value without a zwaveEvent funciton 182 | ]) 183 | } 184 | 185 | def on() { 186 | delayBetween([ 187 | zwave.basicV1.basicSet(value: 0xFF).format(), // physically changes the relay from on to off and requests a report of the relay 188 | refresh()// to make sure that it changed (the report is used elsewhere, look for switchBinaryReport() 189 | ]) 190 | } 191 | 192 | def off() { 193 | delayBetween([ 194 | zwave.basicV1.basicSet(value: 0x00).format(), // physically changes the relay from on to off and requests a report of the relay 195 | refresh()// to make sure that it changed (the report is used elsewhere, look for switchBinaryReport() 196 | ]) 197 | } 198 | 199 | def refresh() { 200 | log.debug "REFRESH!" 201 | delayBetween([ 202 | zwave.switchBinaryV1.switchBinaryGet().format(), //requests a report of the relay to make sure that it changed (the report is used elsewhere, look for switchBinaryReport() 203 | zwave.sensorMultilevelV5.sensorMultilevelGet().format()// requests a report of the anologue input voltage 204 | 205 | ]) 206 | } -------------------------------------------------------------------------------- /smartsense moisture/devicehandler.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 SmartThings 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 11 | * for the specific language governing permissions and limitations under the License. 12 | * 13 | */ 14 | metadata { 15 | definition (name: "FortrezZ Water Sensor", namespace: "fortrezz", author: "SmartThings") { 16 | capability "Water Sensor" 17 | capability "Sensor" 18 | capability "Battery" 19 | capability "Temperature Measurement" 20 | 21 | fingerprint deviceId: "0x2001", inClusters: "0x30,0x9C,0x9D,0x85,0x80,0x72,0x31,0x84,0x86" 22 | fingerprint deviceId: "0x2101", inClusters: "0x71,0x70,0x85,0x80,0x72,0x31,0x84,0x86" 23 | } 24 | 25 | simulator { 26 | status "dry": "command: 7105, payload: 00 00 00 FF 05 FE 00 00" 27 | status "wet": "command: 7105, payload: 00 FF 00 FF 05 02 00 00" 28 | status "overheated": "command: 7105, payload: 00 00 00 FF 04 02 00 00" 29 | status "freezing": "command: 7105, payload: 00 00 00 FF 04 05 00 00" 30 | status "normal": "command: 7105, payload: 00 00 00 FF 04 FE 00 00" 31 | for (int i = 0; i <= 100; i += 20) { 32 | status "battery ${i}%": new physicalgraph.zwave.Zwave().batteryV1.batteryReport(batteryLevel: i).incomingMessage() 33 | } 34 | preferences { 35 | input ("version", "text", title: "Plugin Version 1.5", description:"", required: false, displayDuringSetup: true) 36 | } 37 | } 38 | 39 | tiles(scale: 2) { 40 | multiAttributeTile(name:"water", type: "generic", width: 6, height: 4){ 41 | tileAttribute ("device.water", key: "PRIMARY_CONTROL") { 42 | attributeState "dry", label: "Dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff" 43 | attributeState "wet", label: "Wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0" 44 | } 45 | } 46 | standardTile("temperatureState", "device.temperature", width: 2, height: 2) { 47 | state "normal", icon:"st.alarm.temperature.normal", backgroundColor:"#ffffff" 48 | state "freezing", icon:"st.alarm.temperature.freeze", backgroundColor:"#53a7c0" 49 | state "overheated", icon:"st.alarm.temperature.overheat", backgroundColor:"#F80000" 50 | } 51 | valueTile("temperature", "device.temperature", width: 2, height: 2) { 52 | state("temperature", label:'${currentValue}°', 53 | backgroundColors:[ 54 | [value: 31, color: "#153591"], 55 | [value: 44, color: "#1e9cbb"], 56 | [value: 59, color: "#90d2a7"], 57 | [value: 74, color: "#44b621"], 58 | [value: 84, color: "#f1d801"], 59 | [value: 95, color: "#d04e00"], 60 | [value: 96, color: "#bc2323"] 61 | ] 62 | ) 63 | } 64 | valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { 65 | state "battery", label:'${currentValue}% battery', unit:"" 66 | } 67 | main (["water", "temperatureState"]) 68 | details(["water", "temperatureState", "temperature", "battery"]) 69 | } 70 | } 71 | 72 | def parse(String description) { 73 | def result = [] 74 | def parsedZwEvent = zwave.parse(description, [0x30: 1, 0x71: 2, 0x84: 1]) 75 | 76 | if(parsedZwEvent) { 77 | if(parsedZwEvent.CMD == "8407") { 78 | def lastStatus = device.currentState("battery") 79 | def ageInMinutes = lastStatus ? (new Date().time - lastStatus.date.time)/60000 : 600 80 | log.debug "Battery status was last checked ${ageInMinutes} minutes ago" 81 | 82 | if (ageInMinutes >= 600) { 83 | log.debug "Battery status is outdated, requesting battery report" 84 | result << new physicalgraph.device.HubAction(zwave.batteryV1.batteryGet().format()) 85 | } 86 | result << new physicalgraph.device.HubAction(zwave.wakeUpV1.wakeUpNoMoreInformation().format()) 87 | } 88 | result << createEvent( zwaveEvent(parsedZwEvent) ) 89 | } 90 | if(!result) result = [ descriptionText: parsedZwEvent, displayed: false ] 91 | log.debug "Parse returned ${result}" 92 | return result 93 | } 94 | 95 | def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) 96 | { 97 | [descriptionText: "${device.displayName} woke up", isStateChange: false] 98 | } 99 | 100 | def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) 101 | { 102 | def map = [:] 103 | map.name = "water" 104 | map.value = cmd.sensorValue ? "wet" : "dry" 105 | map.descriptionText = "${device.displayName} is ${map.value}" 106 | map 107 | } 108 | 109 | def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { 110 | def map = [:] 111 | if(cmd.batteryLevel == 0xFF) { 112 | map.name = "battery" 113 | map.value = 1 114 | map.descriptionText = "${device.displayName} has a low battery" 115 | map.displayed = true 116 | } else { 117 | map.name = "battery" 118 | map.value = cmd.batteryLevel > 0 ? cmd.batteryLevel.toString() : 1 119 | map.unit = "%" 120 | map.displayed = false 121 | } 122 | map 123 | } 124 | 125 | def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) 126 | { 127 | def map = [:] 128 | if (cmd.zwaveAlarmType == physicalgraph.zwave.commands.alarmv2.AlarmReport.ZWAVE_ALARM_TYPE_WATER) { 129 | map.name = "water" 130 | map.value = cmd.alarmLevel ? "wet" : "dry" 131 | map.descriptionText = "${device.displayName} is ${map.value}" 132 | } 133 | if(cmd.zwaveAlarmType == physicalgraph.zwave.commands.alarmv2.AlarmReport.ZWAVE_ALARM_TYPE_HEAT) { 134 | map.name = "temperatureState" 135 | if(cmd.zwaveAlarmEvent == 1) { map.value = "overheated"} 136 | if(cmd.zwaveAlarmEvent == 2) { map.value = "overheated"} 137 | if(cmd.zwaveAlarmEvent == 3) { map.value = "changing temperature rapidly"} 138 | if(cmd.zwaveAlarmEvent == 4) { map.value = "changing temperature rapidly"} 139 | if(cmd.zwaveAlarmEvent == 5) { map.value = "freezing"} 140 | if(cmd.zwaveAlarmEvent == 6) { map.value = "freezing"} 141 | if(cmd.zwaveAlarmEvent == 254) { map.value = "normal"} 142 | map.descriptionText = "${device.displayName} is ${map.value}" 143 | } 144 | 145 | map 146 | } 147 | 148 | def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) 149 | { 150 | def map = [:] 151 | if(cmd.sensorType == 1) { 152 | map.name = "temperature" 153 | if(cmd.scale == 0) { 154 | map.value = getTemperature(cmd.scaledSensorValue) 155 | } else { 156 | map.value = cmd.scaledSensorValue 157 | } 158 | map.unit = location.temperatureScale 159 | } 160 | map 161 | } 162 | 163 | def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) 164 | { 165 | def map = [:] 166 | map.name = "water" 167 | map.value = cmd.value ? "wet" : "dry" 168 | map.descriptionText = "${device.displayName} is ${map.value}" 169 | map 170 | } 171 | 172 | def getTemperature(value) { 173 | if(location.temperatureScale == "C"){ 174 | return value 175 | } else { 176 | return Math.round(celsiusToFahrenheit(value)) 177 | } 178 | } 179 | 180 | def zwaveEvent(physicalgraph.zwave.Command cmd) 181 | { 182 | log.debug "COMMAND CLASS: $cmd" 183 | } --------------------------------------------------------------------------------