I do something similar to this in NZ, but for my freezer and fridge (not the main one in the kitchen, the infrequently used ones). They can tolerate 6-8 hours of being off, but not a lot more than that.
I hold a running average of the prices, and they run when the price is below a threshold, and below both the current average and the forecast average for the next few hours (I can get forecast power prices from scraping a web page that our electricity commission run).
Basically I have it calculate when it thinks the price is about as low as it’ll be. And then I have a failsafe - if in any block of time it hasn’t run at least X hours of the last 24 hours, then it runs irrespective.
That exactly logic wouldn’t work for you, but similar logic could. And it’s been a long time since I wrote it…so that description is approximate. But my code is somewhat readable, as below.
For whatever reason, at the time I didn’t want to use a proper array type (might have been before I worked out how to, or might have been because I wanted it persisted across reboots). So I split the array of prices into Prices A, Prices B, Prices C, then I concatenate it back together to use it. It’s awfully ugly, so I presume I could refactor that now to be a lot smarter. Anyway, whatever, it’s working.
rule "fridges turn off"
when
Item swFridges changed to OFF
then
dtFridgesLastTurnOff.postUpdate( new DateTimeType( ZonedDateTime.now() ))
if( fridgeTimer !== null ){
fridgeTimer.cancel()
logError( 'plug', 'Fridges just turned off, but there was a fridge timer, so why didn\'t that get cancelled when fridges turned on?')
}
logDebug( 'plug', 'Fridges turned off, setting a 5 hour timer to turn them back on if power doesn\'t get cheap by then' )
fridgeTimer = createTimer( ZonedDateTime.now().plusSeconds( 5 * 60 * 60 + 300 ) ) [ |
logInfo( 'plug', 'Fridges have been off for 5 hours, turning back on on presumption we\'ve forgotten about them' )
swFridges.sendCommand( ON )
fridgeTimer = null
]
end
rule "fridges turn on"
when
Item swFridges changed to ON
then
dtFridgesLastTurnOn.postUpdate( new DateTimeType( ZonedDateTime.now() ) )
if( fridgeTimer !== null ){
fridgeTimer.cancel()
fridgeTimer = null
logDebug( 'plug', 'Cancelling timer, not needed as we turned on anyway' )
} else {
logError( 'plug', 'Fridges turned on and there wasn\'t a timer, that\'s a defect' )
}
end
rule "reset power settings"
when
Item swPowerReset changed to ON
then
fridgeTimer = null
swPowerReset.postUpdate( OFF )
end
rule "prices change"
when
Item numberPowerPriceNow received update
then
logDebug( 'plug', 'Running price calc' )
// Our logic is that we want to find a threshold where we run at least 1.5 hours in the next 5, and 4 hours in the next 10
// We calculate the average of the values, and then we iterate find a percentage adjustor where we will run at least that much
// We also check that we've run at least 1 hour in the last 5, if we haven't then we run anyway
// Get our prices into a number array by joining then splitting the strings, and casting to a number
var String priceString = stringPowerPricesA.state.toString() + ',' + stringPowerPricesB.state.toString() + ',' + stringPowerPricesC.state.toString()
val List<Number> numberPriceArray = newArrayList()
var int i
for( i = 0; i < priceString.split(",").length(); i++ ){
if( priceString.split(",").get(i) == "" ){
numberPriceArray.add(i, 0 )
} else {
numberPriceArray.add(i, Double::parseDouble(priceString.split(",").get(i)) )
}
}
// get the average
var Number priceAverage = 0
for( i = 0; i < numberPriceArray.size(); i++ ){
priceAverage = priceAverage + numberPriceArray.get(i)
}
priceAverage = priceAverage / numberPriceArray.size()
// iterate to find a percentage that gets us enough run time
var Number percentage = -0.6
var Number periodsIn5Hours = 0 // 5 hours from last turn off if we're off
var Number periodsIn10Hours = 0
var Number period5Hours = 10
if( swFridges.state == OFF ){
var Number secondsOff = ( new DateTimeType(ZonedDateTime.now() )).getZonedDateTime().toInstant.getEpochSecond() - (dtFridgesLastTurnOff.state as DateTimeType).getZonedDateTime().toInstant.getEpochSecond()
period5Hours = ( 5 * 60 * 60 - secondsOff ) / ( 30 * 60 )
}
while( percentage < 1 && ( periodsIn5Hours < 5 || periodsIn10Hours < 11 ) ){
percentage = percentage + 0.05
periodsIn5Hours = 0
periodsIn10Hours = 0
for( i = 0; i < numberPriceArray.size(); i++ ){
if( numberPriceArray.get(i) < priceAverage * ( 1 + percentage) ){
periodsIn10Hours = periodsIn10Hours + 1
if( i < period5Hours ){
periodsIn5Hours = periodsIn5Hours + 1
}
}
}
}
if( percentage >= 1 ){
logError( 'plug', 'Percentage is 1 or greater, which isn\'t expected. Input prices were ' + priceString + ' and priceAverage is ' + priceAverage )
}
// find out how many 10 minute periods we've run in last 5 hours
var Number runTimeHistory // number of ten minute periods in last 5 hours we've been on for
var String hist = stringPowerOnHistory.state.toString()
var String newHist
while( hist.length() > 0 ){
if( hist.substring(0,1) == '1' ){
runTimeHistory = runTimeHistory + 1
}
hist = hist.substring(1)
}
var Number priceNow = numberPowerPriceNow.state as Number
var Number priceTarget = priceAverage * (1 + percentage)
if( priceNow < numberPowerLowEnough.state as Number ){
if( swFridges.state == OFF ){
logInfo( 'plug', 'Turn fridges on, price of ' + priceNow + ' is low enough' )
swFridges.sendCommand( ON )
newHist = "1"
} else {
logDebug( 'plug', 'Leave fridges on, price of ' + priceNow + ' is low enough' )
newHist = "1"
}
}
else if( runTimeHistory < 13 ){
if( swFridges.state == OFF ){
logInfo( 'plug', 'Turn fridges on, they\'ve been on for less than two hours in the last 5 hours' )
swFridges.sendCommand( ON )
newHist = "1"
} else {
logDebug( 'plug', 'Leave fridges on, they\'ve been on for less than two hours in the last 5 hours' )
newHist = "1"
}
} else if( swFridges.state == OFF ){
if( priceNow < priceTarget ){
logInfo( 'plug', 'Turn fridges on, price of ' + priceNow + ' is lower than target price of ' + priceTarget + ' based on average of ' + priceAverage + ' and percentage of ' + percentage )
swFridges.sendCommand( ON )
newHist = "1"
} else {
logDebug( 'plug', 'Leave fridges off, price of ' + priceNow + ' is not lower than target price of ' + priceTarget + ' based on average of ' + priceAverage + ' and percentage of ' + percentage )
newHist = "0"
}
} else {
if( priceNow < priceTarget * 1.15 ){
logDebug( 'plug', 'Leave fridges on, price of ' + priceNow + ' is lower than target price x 1.15 of ' + priceTarget * 1.15 + ' based on average of ' + priceAverage + ' and percentage of ' + percentage )
newHist = "1"
} else {
logInfo( 'plug', 'Turn fridges off, price of ' + priceNow + ' is not lower than target price x 1.15 of ' + priceTarget * 1.15 + ' based on average of ' + priceAverage + ' and percentage of ' + percentage )
swFridges.sendCommand( OFF )
newHist = "0"
}
}
// update power history
if( stringPowerOnHistory.state.toString().length() < 1 ){
stringPowerOnHistory.postUpdate( newHist )
} else if ( stringPowerOnHistory.state.toString().length() < 30 ) {
stringPowerOnHistory.postUpdate( stringPowerOnHistory.state.toString().concat(newHist) )
} else {
stringPowerOnHistory.postUpdate( stringPowerOnHistory.state.toString().substring(1).concat(newHist) )
}
numberPowerPriceTarget.postUpdate( priceTarget )
numberPowerPriceAverage.postUpdate( priceAverage )
numberPowerPricePercentage.postUpdate( percentage * 100 )
logDebug( 'plug', 'Finished running price calc' )
end
Items file
Group Power (All)
Switch swFridges "Fridges [%s]" (Power, RestoreGroup, PowerGraphs) { channel="tplinksmarthome:hs110:FC1930:switch" }
Number numberFridgesPower "Fridges Power [%d watts]" (Power, PowerGraphs) { channel="tplinksmarthome:hs110:FC1930:power" }
Number numberFridgesEnergyUsage "Fridges Energy Usage [%.2f kWh]" (Power, PowerGraphs) { channel="tplinksmarthome:hs110:FC1930:energyUsage" }
Number numberPowerPriceNow "Price Now [$%.2f]" (Power, RestoreGroup, PowerGraphs) { channel="mqtt:topic:pi2:Power:priceNow" }
String stringPowerPricesA "Prices A [%s]" (Power, RestoreGroup) { channel="mqtt:topic:pi2:Power:pricesA" }
String stringPowerPricesB "Prices B [%s]" (Power, RestoreGroup) { channel="mqtt:topic:pi2:Power:pricesB" }
String stringPowerPricesC "Prices C [%s]" (Power, RestoreGroup) { channel="mqtt:topic:pi2:Power:pricesC" }
Number numberPowerPriceTarget "Target next 10 hours [$%.2f]" (Power, RestoreGroup)
Number numberPowerPriceAverage "Average next 10 hours [$%.2f]" (Power, RestoreGroup)
Number numberPowerPricePercentage "Percentage Adjustor [%d%%]" (Power, RestoreGroup)
String stringPowerOnHistory "History of Fridge Power [%s]" (Power, RestoreGroup)
DateTime dtFridgesLastTurnOn "Last Turn On [%1$td %1$tb %1$tH:%1$tM:%1$tS]" <clock> (Power, RestoreGroup)
DateTime dtFridgesLastTurnOff "Last Turn Off [%1$td %1$tb %1$tH:%1$tM:%1$tS]" <clock> (Power, RestoreGroup)
Number numberPowerLowEnough "Low enough to run [$%.2f]" (Power, RestoreGroup)
Switch swPowerReset "Reset power rules [%s]" (Power)
And a picture of what that looks like in my sitemap, to give you an idea of what the data looks like: