Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
295 changes: 190 additions & 105 deletions smartapps/bangali/rooms-child-app.src/rooms-child-app.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just housekeeping and thermostatOffset, which is the "ensure trigger" offset.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trying to understand thermostatOffset since i dont use a central thermostat. does setting the thermostat to a lower or higher value then issuing a thermostat.cool() or thermostat.heat() not turn on the thermostat?

thanks.

Copy link
Copy Markdown
Author

@dday4thedeceiver dday4thedeceiver Feb 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To simplify, let's assume all sensors read the same at the same actual temperature or it's not possible to discern a delta as they seem to be all over the place, that's a separate concern. Let's also assume we want the area around the thermostat and the room at the same temperature, 70F. Finally, the overall assumption is, when we are heating this is the minimum temperature we want, we don't mind being a little warmer but certainly not colder.

Here's the scenario: The thermostat is at 71F and not heating but the room is at 68F. We want to heat the room +2F. With no sensor offset and no thermostat offset, we would make the setpoint 71F (with a swing of +-1), but that would do nothing.

With a thermostat offset of, say, 4F, we would make it 75F instead which would ensure triggering the thermostat. When the room reaches 71F, we turn off the thermostat by setting it to the current thermostat temperature (which would presumably be ~73F but could be whatever) unless it's already off (manually etc)

BTW, I just realized we turn off at the swing temperature (that's how most thermostats behave, although not all) but setting the thermostat to desired temperature, which is inconsistent. Committing a fix.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok. do we really need the thermostat offset then? more settings usually tend to confuse users. why dont we use a static offset of 5 degrees and update the text on that page to note that.

also, we keep talking about this as a central thermostat but the handling seems to be on a per room basis. heres an example:
room A maintain: 68F room sensor: 65F temperature offset: 2F
room B maintain: 68F room sensor: 72F temperature offset: 5F
room C maintain: 68F room sensor: 70F temperature offset: 3F

this will keep toggling the thermostat ... that wont be good.

thanks.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, it doesn't really make sense on a per room basis. Sure, hard coded 5F sounds fine, since we don't really have a suitable spot for it in the parent app.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Busy with moving :) The plan sounds good. I'll probably get started on Sunday. I may submit a couple of other PRs first actually (adjacency & auto level) they are already running on my system but gotta clean up the commits...

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh ... that can be consuming but hopefully not stressful. :-)

👍

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

guessing you are still tied up with the after move effects? :-) do you want me to get the thermostat related changes started? i also have an user asking for fan support for some time now so want to get that done as well for him. but was holding off till the thermostat changes were final.

sorry this is not meant to bug you.

thanks.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok. let me start with #1 for thermostat.

right. since with any typical thermostat there is really no way to just heat or cool a room. might as well look at all rooms with that thermostat, find the averages of the rules and temperatures and use these average numbers to manage the thermostat.

one thing i am debating about this is whether i should transparently override user intent ... generally i am not for doing that. instead of finding all the rules and averaging them out would it better if and when they add the same thermostat more than one time to different rooms warn them that this may have undesirable effect ... blah ... blah ... and not transparently override it in the background.

any thoughts on this?

thanks.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A notification is needed but it can be subtle: "Shared thermostat, temperature averaging enabled"
They can figure it out from there.

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"
Expand Down Expand Up @@ -2410,27 +2413,165 @@ def processCoolHeat() {

def processCoolHeat() {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Split into three methods, setTemp() which does the actual heating/cooling, processCoolHeat for the logic and getActiveTemperatureRule() which is unchanged otherwise.

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())
}
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

turning off cool/heat with non-presence is required - no?

thanks.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. It still does that, just had to move it down since we don't just turn the thermostat off and do more processing.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like it only does that now when it finds a rule? if yes, that wont work.

or am i misreading?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lines 2476-2479 take care of it when no rules match

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i might be missing something but why process the rules if checkpresence is on and the person is not present?

thanks.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, no reason. I just wanted to get to the full "off" routine and overlooked that. Will fix, along with making the offset constant.

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
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

state variables added to prevent messing with the thermostat if not needed

Copy link
Copy Markdown
Owner

@adey adey Feb 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the state variable for heating active or cooling active instead of checking the current state of the thermostat to determine if heating or cooling is active?

thanks.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For activating, the state variables are redundant since we quit if HVAC is running (and I should remove the checks really), but for cooling it stops us from turning off activations from other rooms and from the thermostat itself.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will make the response to this a part of the centralized model discussion above.


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", coolLow)
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", heatHigh)
}

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)
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prevents changing setpoint if thermostat is manually turned off

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()
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No logic changes

{
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()
Expand All @@ -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)
Expand All @@ -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())
Expand All @@ -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())
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explicitly not turning thermostats off to prevent unintentional freezes

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

saw your notes up top on turning off thermostat being possibly catastrophic. could you expand on that?

thanks.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we turn the thermostat off, and ST gets disconnected or something else happens, the thermostat could stay off - if unnoticed that could lead to pipes freezing etc. Turning off via lowering the setpoint instead is safe and works well.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh. you can probably tell i live in california 😄

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha! Ahh, I wish I could say the same...

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

Expand Down
Loading