From 898d719ac8449a889f129df40dd0acf2aa3e3dda Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 10 Feb 2018 22:08:36 -0600 Subject: [PATCH 1/4] Support LANnouncer&VLCThing and presence without announcements Fix missing announcement options --- .../rooms-manager.src/rooms-manager.groovy | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/smartapps/bangali/rooms-manager.src/rooms-manager.groovy b/smartapps/bangali/rooms-manager.src/rooms-manager.groovy index 0c55136..1bd6311 100644 --- a/smartapps/bangali/rooms-manager.src/rooms-manager.groovy +++ b/smartapps/bangali/rooms-manager.src/rooms-manager.groovy @@ -381,19 +381,25 @@ def pageSpeakerSettings() { def i = (presenceSensors ? presenceSensors.size() : 0) def str = (presenceNames ? presenceNames.split(',') : []) def j = str.size() - if (i != j) sendNotification("Count of presense sensors and names do not match!", [method: "push"]); - dynamicPage(name: "pageSpeakerSettings", title: "Speaker Settings", install: true, uninstall: true) { + if (presenceNames && i != j) sendNotification("Count of presense sensors and names do not match!", [method: "push"]); + dynamicPage(name: "pageSpeakerSettings", title: "Presence & Announcement Settings", install: true, uninstall: true) { section { - input "speakerDevices", "capability.audioNotification", title: "Which speakers?", required: false, multiple: true, submitOnChange: true + input "presenceSensors", "capability.presenceSensor", title: "Which presence sensors?", required: true, multiple: true + input "speakerDevices", "capability.audioNotification", title: "Which speakers?", required: false, multiple: true, submitOnChange: true + input "speechSynthesizers", "capability.speechSynthesis", title: "Which synthesizers?", required: false, multiple: true, submitOnChange: true + if (speakerDevices) { - input "speakerVolume", "number", title: "Speaker volume?", required: false, multiple: false, defaultValue: 33, range: "1..100" - input "speakerAnnounce", "bool", title: "Announce when presence sensors arrive or depart?", required: false, multiple: false, defaultValue: false, submitOnChange: true + input "speakerVolume", "number", title: "Speaker volume?", required: false, multiple: false, defaultValue: 33, range: "1..100" } else { paragraph "Speaker volume?\nselect speaker(s) to set." - paragraph "Announce when presence sensors arrive or depart?\nselect speaker(s) to set." } - if (speakerDevices && speakerAnnounce) { + if (speakerDevices || speechSynthesizers) + input "speakerAnnounce", "bool", title: "Announce when presence sensors arrive or depart?", required: false, multiple: false, defaultValue: false, submitOnChange: true + else + paragraph "Announce when presence sensors arrive or depart?\nselect speaker/synths to set." + + if ((speakerDevices || speechSynthesizers) && speakerAnnounce) { input "presenceSensors", "capability.presenceSensor", title: "Which presence snesors?", required: true, multiple: true input "presenceNames", "text", title: "Comma delmited names? (in sequence of presence sensors)", required: true, multiple: false, submitOnChange: true input "contactSensors", "capability.contactSensor", title: "Which contact sensors? (welcome home greeting is played after this contact sensor closes.)", required: true, multiple: true @@ -411,11 +417,11 @@ def pageSpeakerSettings() { paragraph "Seconds after?\nselect announce to set." paragraph "Left home announcement?\nselect announce to set." } - if (speakerDevices) + if (speakerDevices || speechSynthesizers) input "timeAnnounce", "enum", title: "Announce time?", required: false, multiple: false, defaultValue: 4, options: [[1:"Every 15 minutes"], [2:"Every 30 minutes"], [3:"Every hour"], [4:"No"]], submitOnChange: true else - paragraph "Announce time?\nselect speaker devices to set." + paragraph "Announce time?\nselect speakers/synths to set." } section { if (speakerAnnounce || ['1', '2', '3'].contains(timeAnnounce)) { @@ -531,7 +537,12 @@ def contactClosedEventHandler(evt = null) { def nowDate = new Date(now()) def intCurrentHH = nowDate.format("HH", location.timeZone) as Integer if (intCurrentHH >= startHH && intCurrentHH <= endHH) - speakerDevices.playTextAndResume(persons, speakerVolume) + { + if (speakerDevices) + speakerDevices.playTextAndResume(persons, speakerVolume) + if (speechSynthesizers) + speechSynthesizers.speak(persons) + } if (evt) state.whoCameHome.personsIn = []; else state.whoCameHome.personsOut = []; } @@ -723,7 +734,10 @@ def tellTime() { // speakerDevices.playTrackAndResume("http://s3.amazonaws.com/smartapp-media/sonos/bell1.mp3", // (intCurrentMM == 0 ? 10 : (intCurrentMM == 15 ? 4 : (intCurrentMM == 30 ? 6 : 8))), speakerVolume) // pause(1000) - speakerDevices.playTextAndResume(timeString, speakerVolume) + if (speakerDevices) + speakerDevices.playTextAndResume(timeString, speakerVolume) + if (speechSynthesizers) + speechSynthesizers.speak(timeString) } } From 1ab5ce05a3b2e72a359aae7b5cb8feac5a56d1ad Mon Sep 17 00:00:00 2001 From: Mark Date: Tue, 13 Feb 2018 23:10:57 -0600 Subject: [PATCH 2/4] Fix room temperature bugs and add optional thermostat offset Extract rule retrieval and hvac operation into own methods And fix device off bug, fine tune Bug fixes, simplify and add thermostat/sensor delta *Just quit if thermostat is already running *store only a single setpoint *assume thermostat is always shared, regardless of whether there is an offset *NEVER turn off a thermostat (could be catastrophic) Remove restore points 2/13: fixed page settings Prevent clobbering setpoint if thermostat is turned off manually --- .../rooms-child-app.groovy | 295 +++++++++++------- 1 file changed, 190 insertions(+), 105 deletions(-) diff --git a/smartapps/bangali/rooms-child-app.src/rooms-child-app.groovy b/smartapps/bangali/rooms-child-app.src/rooms-child-app.groovy index f47c4a4..4720ceb 100644 --- a/smartapps/bangali/rooms-child-app.src/rooms-child-app.groovy +++ b/smartapps/bangali/rooms-child-app.src/rooms-child-app.groovy @@ -1241,18 +1241,21 @@ private pageRoomTemperature() { input "maintainRoomTemp", "enum", title: "Maintain room temperature?", required: false, multiple: false, defaultValue: 4, options: [[1:"Cool"], [2:"Heat"], [3:"Both"], [4:"Neither"]], submitOnChange: true if (['1', '2', '3'].contains(maintainRoomTemp)) { + if (personsPresence) + input "checkPresence", "bool", title: "Check presence before maintaining temperature?", required: true, multiple: false, defaultValue: false + else + paragraph "Check presence before maintaining temperature?\nselect presence sensor(s) to set" input "useThermostat", "bool", title: "Use thermostat? (otherwise uses room ac and/or heater)", required: true, multiple: false, defaultValue: false, submitOnChange: true - if (useThermostat) { + //input "outTempSensor", "capability.temperatureMeasurement", title: "Which outdoor temperature sensor?", required: false, multiple: false + if (useThermostat) { input "roomThermostat", "capability.thermostat", title: "Which thermostat?", required: true, multiple: false - input "thermoToTempSensor", "number", title: "Delta (room temperature - thermostat temperature)?", - description: "if room sensor reads 2° lower than thermostat set this to -2 and so on.", - required: false, multiple: false, defaultValue: 0, range: "-15..15" + input "thermoToTempSensor", "number", title: "Room sensor temperature - thermostat temperature = ? (typical delta)", + description: "Use to compensate for differences ", + required: true, multiple: false, defaultValue: 0, range: "-15..15" + input "thermostatOffset", "number", title: "Temporary extra thermostat offset to force HVAC activation?", + description: "Adds/subtracts to/from thermostat setting", + required: true, multiple: false, range: "0..9", defaultValue: 3 } - if (personsPresence) - input "checkPresence", "bool", title: "Check presence before maintaining temperature?", required: true, multiple: false, defaultValue: false - else - paragraph "Check presence before maintaining temperature?\nselect presence sensor(s) to set" - input "outTempSensor", "capability.temperatureMeasurement", title: "Which outdoor temperature sensor?", required: false, multiple: false } if (!useThermostat && ['1', '3'].contains(maintainRoomTemp)) { input "roomCoolSwitch", "capability.switch", title: "Which switch to turn on AC?", required: true, multiple: false, range: "32..99" @@ -2410,27 +2413,165 @@ def processCoolHeat() { def processCoolHeat() { ifDebug("processCoolHeat") - def temp = -1 - def child = getChildDevice(getRoom()) + + //Need to stop things even if nobody's in def isHere = (personsPresence ? personsPresence.currentValue("presence").contains('present') : false) if ((checkPresence && !isHere) || maintainRoomTemp == '4') { - if (checkPresence && !isHere) { - if (['1', '3'].contains(maintainRoomTemp)) - (useThermostat ? roomThermostat.off() : roomCoolSwitch.off()) - if (['2', '3'].contains(maintainRoomTemp)) - (useThermostat ? roomThermostat.off() : roomHeatSwitch.off()) - } updateMaintainIndP(temp) updateThermostatIndP(isHere) - return } - def roomState = child?.currentValue('occupancy') + if (maintainRoomTemp == '4') + return + def temperature = getAvgTemperature() def updateMaintainIndicator = true - def turnOn = null - def thisRule = [:] - if (state.rules) { + + def turnOn = getActiveTemperatureRule() + ifDebug("processCoolHeat: rule: $turnOn") + if (turnOn) { + def thisRule = getRule(turnOn, 't') + def tempRange = thisRule.tempRange + def setpoint + //reduced the off commands and fan commands to prevent spamming the likes of Nest, which have an API rate limiter + + if (['1', '3'].contains(maintainRoomTemp)) { + def coolHigh = thisRule.coolTemp + tempRange + def coolLow = thisRule.coolTemp - tempRange + ifDebug ("Temperature: ${temperature} Lower limit: ${coolLow}, Upper Limit: ${coolHigh}, cooling: ${state.coolingActive}") + if (temperature >= coolHigh && (!checkPresence || (checkPresence && isHere)) && (!state.coolingActive)) + setTemp("coolon", thisRule.coolTemp) + else + if (temperature <= coolLow && state.coolingActive) + setTemp("cooloff") + } + + if (['2', '3'].contains(maintainRoomTemp)) { + def heatHigh = thisRule.heatTemp + tempRange + def heatLow = thisRule.heatTemp - tempRange + ifDebug ("Temperature: ${temperature} Lower limit: ${heatLow}, Upper Limit: ${heatHigh}, heating: ${state.heatingActive}") + if (temperature >= heatHigh && state.heatingActive) + setTemp("heatoff") + else + if (temperature <= heatLow && (!checkPresence || (checkPresence && isHere)) && !(state.heatingActive)) + setTemp("heaton", thisRule.heatTemp) + } + + updateThermostatIndP(isHere) + if (updateMaintainIndicator) { + if (maintainRoomTemp == '1') + updateMaintainIndP(thisRule.coolTemp) + else if (maintainRoomTemp == '2') + updateMaintainIndP(thisRule.heatTemp) + else if (maintainRoomTemp == '3') { + def x = Math.abs(temperature - thisRule.coolTemp) + def y = Math.abs(temperature - thisRule.heatTemp) + if (x >= y) + updateMaintainIndP(thisRule.heatTemp) + else + updateMaintainIndP(thisRule.coolTemp) + } + } + } + + else if (state.heatingActive) + setTemp("heatoff") + else if (state.coolingActive) + setTemp("cooloff") +} + +private setTemp(command, temp = null) +{ + def setpoint + switch (command) + { + case "heaton": + if (useThermostat) { + if (thermostatActive()) + break + setpoint = temp - thermoToTempSensor + thermostatOffset + ifDebug("On - New setpoint: ${setpoint}") + roomThermostat.setHeatingSetpoint(setpoint) + //roomThermostat.fanAuto() + roomThermostat.heat() + } + else + if (roomHeatSwitch.currentValue("switch") == 'off') { + roomHeatSwitch.on() + updateMaintainIndP(roomHeatTemp) + updateMaintainIndicator = false + } + state.heatingActive = true + break + + case "heatoff": + state.heatingActive = false + if (useThermostat) { + currentTemp = roomThermostat.currentTemperature + if (roomThermostat.currentHeatingSetpoint <= currentTemp) + break + roomThermostat.setHeatingSetpoint(currentTemp) + ifDebug("Off - New setpoint: $currentTemp") + } + else + roomHeatSwitch.off() + break + + case "coolon": + ifDebug("Cooling on") + if (useThermostat) { + if (thermostatActive()) + break + setpoint = temp - thermoToTempSensor + thermostatOffset + roomThermostat.setCoolingSetpoint(setpoint) + //roomThermostat.setThermostatFanMode() + roomThermostat.cool() + } + else + if (roomCoolSwitch.currentValue("switch") == 'off') { + roomCoolSwitch.on() + updateMaintainIndP(roomCoolTemp) + updateMaintainIndicator = false + } + state.coolingActive = true + break + + case "cooloff": + ifDebug("Cooling off") + state.coolingActive = false + if (useThermostat) { + currentTemp = roomThermostat.currentTemperature + if (roomThermostat.currentCoolingSetpoint >= currentTemp) + break + roomThermostat.setCoolingSetpoint(currentTemp) + } + else + roomCoolSwitch.off() + break + } +} + +private thermostatActive() +{ + def status = roomThermostat.currentThermostatOperatingState + ifDebug("Thermostat status: $status") + if (status == "heating" || status == "cooling") + return true + return false +} + +def getStateValue(key) { + return state[key] +} + +private getActiveTemperatureRule() +{ + if (state.rules) + { + def thisRule = [:] + def result = null def currentMode = String.valueOf(location.currentMode) + def child = getChildDevice(getRoom()) + def roomState = child?.currentValue('occupancy') def nowTime = now() + 1000 def nowDate = new Date(nowTime) def sunriseAndSunset = getSunriseAndSunset() @@ -2439,7 +2580,9 @@ def processCoolHeat() { def timedRulesOnly = false def sunriseTimeWithOff, sunsetTimeWithOff def i = 1 - for (; i < 11; i++) { + + for (; i < 11; i++) + { def ruleHasTime = false def ruleNo = String.valueOf(i) thisRule = getNextRule(ruleNo, 't', true) @@ -2448,23 +2591,24 @@ def processCoolHeat() { if (thisRule.mode && !thisRule.mode.contains(currentMode)) continue; if (thisRule.state && !thisRule.state.contains(roomState)) continue; if (thisRule.dayOfWeek && !(checkRunDay(thisRule.dayOfWeek))) continue; -// saved old time comparison while adding offset to sunrise / sunset -/* if ((thisRule.fromTimeType && (thisRule.fromTimeType != timeTime() || thisRule.fromTime)) && - (thisRule.toTimeType && (thisRule.toTimeType != timeTime() || thisRule.toTime))) { - def fTime = ( thisRule.fromTimeType == timeSunrise() ? sunriseTime : ( thisRule.fromTimeType == timeSunset() ? sunsetTime : timeToday(thisRule.fromTime, location.timeZone))) - def tTime = ( thisRule.toTimeType == timeSunrise() ? sunriseTime : ( thisRule.toTimeType == timeSunset() ? sunsetTime : timeToday(thisRule.toTime, location.timeZone))) -// ifDebug("ruleNo: $ruleNo | fTime: $fTime | tTime: $tTime | nowDate: $nowDate | timeOfDayIsBetween: ${timeOfDayIsBetween(fTime, tTime, nowDate, location.timeZone)}") - if (!(timeOfDayIsBetween(fTime, tTime, nowDate, location.timeZone))) continue; - if (!timedRulesOnly) { - turnOn = null - timedRulesOnly = true - i = 0 - continue - } - ruleHasTime = true + // saved old time comparison while adding offset to sunrise / sunset + /* if ((thisRule.fromTimeType && (thisRule.fromTimeType != timeTime() || thisRule.fromTime)) && + (thisRule.toTimeType && (thisRule.toTimeType != timeTime() || thisRule.toTime))) { + def fTime = ( thisRule.fromTimeType == timeSunrise() ? sunriseTime : ( thisRule.fromTimeType == timeSunset() ? sunsetTime : timeToday(thisRule.fromTime, location.timeZone))) + def tTime = ( thisRule.toTimeType == timeSunrise() ? sunriseTime : ( thisRule.toTimeType == timeSunset() ? sunsetTime : timeToday(thisRule.toTime, location.timeZone))) + // ifDebug("ruleNo: $ruleNo | fTime: $fTime | tTime: $tTime | nowDate: $nowDate | timeOfDayIsBetween: ${timeOfDayIsBetween(fTime, tTime, nowDate, location.timeZone)}") + if (!(timeOfDayIsBetween(fTime, tTime, nowDate, location.timeZone))) continue; + if (!timedRulesOnly) { + turnOn = null + timedRulesOnly = true + i = 0 + continue + } + ruleHasTime = true }*/ if ((thisRule.fromTimeType && (thisRule.fromTimeType != timeTime() || thisRule.fromTime)) && - (thisRule.toTimeType && (thisRule.toTimeType != timeTime() || thisRule.toTime))) { + (thisRule.toTimeType && (thisRule.toTimeType != timeTime() || thisRule.toTime))) + { if (thisRule.fromTimeType == timeSunrise()) sunriseTimeWithOff = (thisRule.fromTimeOffset ? new Date(sunriseTime.getTime() + (thisRule.fromTimeOffset * 60000L)) : sunriseTime) else if (thisRule.fromTimeType == timeSunset()) @@ -2475,83 +2619,24 @@ def processCoolHeat() { else if (thisRule.toTimeType == timeSunset()) sunsetTimeWithOff = (thisRule.toTimeOffset ? new Date(sunsetTime.getTime() + (thisRule.toTimeOffset * 60000L)) : sunsetTime) def tTime = ( thisRule.toTimeType == timeSunrise() ? sunriseTimeWithOff : ( thisRule.toTimeType == timeSunset() ? sunsetTimeWithOff : timeToday(thisRule.toTime, location.timeZone))) -// ifDebug("ruleNo: $ruleNo | fTime: $fTime | tTime: $tTime | nowDate: $nowDate | timeOfDayIsBetween: ${timeOfDayIsBetween(fTime, tTime, nowDate, location.timeZone)}") + // ifDebug("ruleNo: $ruleNo | fTime: $fTime | tTime: $tTime | nowDate: $nowDate | timeOfDayIsBetween: ${timeOfDayIsBetween(fTime, tTime, nowDate, location.timeZone)}") if (!(timeOfDayIsBetween(fTime, tTime, nowDate, location.timeZone))) continue; if (!timedRulesOnly) { - turnOn = null + result = null timedRulesOnly = true i = 0 continue } ruleHasTime = true } -// ifDebug("ruleNo: $thisRule.ruleNo | thisRule.luxThreshold: $thisRule.luxThreshold | turnOn: $turnOn | previousRuleLux: $previousRuleLux") -// ifDebug("timedRulesOnly: $timedRulesOnly | ruleHasTime: $ruleHasTime") + ifDebug("${i} ${thisRule.ruleNo}") + // ifDebug("ruleNo: $thisRule.ruleNo | thisRule.luxThreshold: $thisRule.luxThreshold | turnOn: $turnOn | previousRuleLux: $previousRuleLux") + // ifDebug("timedRulesOnly: $timedRulesOnly | ruleHasTime: $ruleHasTime") if (timedRulesOnly && !ruleHasTime) continue; - turnOn = thisRule.ruleNo - } - } - ifDebug("processCoolHeat: rule: $turnOn") - if (turnOn) { - thisRule = getRule(turnOn, 't') - def tempRange = thisRule.tempRange - if (['1', '3'].contains(maintainRoomTemp)) { - def coolHigh = thisRule.coolTemp + (tempRange / 2f).round(1) - def coolLow = thisRule.coolTemp - (tempRange / 2f).round(1) - if (temperature >= coolHigh) { - if (useThermostat) { - roomThermostat.setCoolingSetpoint(thisRule.coolTemp - thermoToTempSensor) - roomThermostat.setThermostatFanMode() - roomThermostat.cool() - } - else { - if (roomCoolSwitch.currentValue("switch") == 'off') { - roomCoolSwitch.on() - updateMaintainIndP(roomCoolTemp) - updateMaintainIndicator = false - } - } - } - else if (temperature <= coolLow) - (useThermostat ? roomThermostat.off() : roomCoolSwitch.off()) - } - if (['2', '3'].contains(maintainRoomTemp)) { - def heatHigh = thisRule.heatTemp + (tempRange / 2f).round(1) - def heatLow = thisRule.heatTemp - (tempRange / 2f).round(1) - if (temperature >= heatHigh) - (useThermostat ? roomThermostat.off() : roomHeatSwitch.off()) - else { - if (temperature <= heatLow) { - if (useThermostat) { - roomThermostat.setHeatingSetpoint(thisRule.heatTemp - thermoToTempSensor) - roomThermostat.fanAuto() - roomThermostat.heat() - } - else { - if (roomHeatSwitch.currentValue("switch") == 'off') { - roomHeatSwitch.on() - updateMaintainIndP(roomHeatTemp) - updateMaintainIndicator = false - } - } - } - } - } - updateThermostatIndP(isHere) - if (updateMaintainIndicator) { - if (maintainRoomTemp == '1') - updateMaintainIndP(thisRule.coolTemp) - else if (maintainRoomTemp == '2') - updateMaintainIndP(thisRule.heatTemp) - else if (maintainRoomTemp == '3') { - def x = Math.abs(temperature - thisRule.coolTemp) - def y = Math.abs(temperature - thisRule.heatTemp) - if (x >= y) - updateMaintainIndP(thisRule.heatTemp) - else - updateMaintainIndP(thisRule.coolTemp) - } + ifDebug("${i} ${thisRule.ruleNo}") + result = thisRule.ruleNo } + return result } } From b998195474e12396c49c034e3e7c87e94689c5e1 Mon Sep 17 00:00:00 2001 From: Mark Date: Tue, 13 Feb 2018 23:53:00 -0600 Subject: [PATCH 3/4] Fix thermostat offset typo --- smartapps/bangali/rooms-child-app.src/rooms-child-app.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smartapps/bangali/rooms-child-app.src/rooms-child-app.groovy b/smartapps/bangali/rooms-child-app.src/rooms-child-app.groovy index 4720ceb..5212536 100644 --- a/smartapps/bangali/rooms-child-app.src/rooms-child-app.groovy +++ b/smartapps/bangali/rooms-child-app.src/rooms-child-app.groovy @@ -2521,7 +2521,7 @@ private setTemp(command, temp = null) if (useThermostat) { if (thermostatActive()) break - setpoint = temp - thermoToTempSensor + thermostatOffset + setpoint = temp - thermoToTempSensor - thermostatOffset roomThermostat.setCoolingSetpoint(setpoint) //roomThermostat.setThermostatFanMode() roomThermostat.cool() From 3897bfb48486c99db8da1e454710e82123e64dad Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 14 Feb 2018 08:11:03 -0600 Subject: [PATCH 4/4] Set thermostat to swing temperature, not desired temperature --- smartapps/bangali/rooms-child-app.src/rooms-child-app.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smartapps/bangali/rooms-child-app.src/rooms-child-app.groovy b/smartapps/bangali/rooms-child-app.src/rooms-child-app.groovy index 5212536..c021b75 100644 --- a/smartapps/bangali/rooms-child-app.src/rooms-child-app.groovy +++ b/smartapps/bangali/rooms-child-app.src/rooms-child-app.groovy @@ -2439,7 +2439,7 @@ def processCoolHeat() { def coolLow = thisRule.coolTemp - tempRange ifDebug ("Temperature: ${temperature} Lower limit: ${coolLow}, Upper Limit: ${coolHigh}, cooling: ${state.coolingActive}") if (temperature >= coolHigh && (!checkPresence || (checkPresence && isHere)) && (!state.coolingActive)) - setTemp("coolon", thisRule.coolTemp) + setTemp("coolon", coolLow) else if (temperature <= coolLow && state.coolingActive) setTemp("cooloff") @@ -2453,7 +2453,7 @@ def processCoolHeat() { setTemp("heatoff") else if (temperature <= heatLow && (!checkPresence || (checkPresence && isHere)) && !(state.heatingActive)) - setTemp("heaton", thisRule.heatTemp) + setTemp("heaton", heatHigh) } updateThermostatIndP(isHere)