What follows is a sketch on how to optimize the energy costs of white goods (it might be extended to meet more complex boundary conditions as required for optimizing the energy costs for heat pumps, EVs, …). But please don’t get too excited re white goods: the savings on white goods are marginal compared to charging your car during cheap hours - but many a mickle makes a muckle.
BOM
- DUO (device under optimization)
- plug to measure power (Zigbee, Z-Wave, …)
- InfluxDB
- Grafana (optional)
- dynamically priced energy contract, e.g. Tibber
- Lua and
json-lua
(sudo luarocks install json-lua
).
Recipe:
-
Use Grafana to find a nice power profile of your DUO (e.g., Bosch washing machine):
-
Export the data from your InfluxDB (adjust the parameters accordingly, start time and stop time can be gleaned from the headline in Grafana:
use the correct timezone in your query):
influx -username <username> -password <password> -database 'openhab' -execute "select * from ZBSteckdosemessend2Waschmaschine_Power WHERE time >= '2023-03-17T21:59:24+01:00' AND time <= '2023-03-17T23:22:00+01:00'" > bosch_program_1.power_profile
- Get the hourly energy rates for today and tomorrow from Tibber (note that the rates for tomorrow are availabe about 10 minutes to 1 pm (CET/CEST), insert your personal Tibber API token):
curl \
-H "Authorization: Bearer <your personal Tibber API token>>" \
-H "Content-Type: application/json" \
-X POST \
-d '{ "query": "{ viewer { homes { currentSubscription{ priceInfo{ today { total } tomorrow { total } } } } } }" }' https://api.tibber.com/v1-beta/gql > tibber_rates_20230318.json
- Use the following Lua script to calculate the optimal starting time (simple mathematics and brute force to calculate the optimal starting time):
--json = require "json"
json = require "JSON"
tibber_rates = 'tibber_rates_20230320_21.json'
power_profile = 'bosch_dishwasher_70_non_eco.power_profile'
function fmt( t )
res = ''
if t >= 24
then
t = t - 24
res = '+'
end
tmp = math.ceil( ( t - math.floor( t ) ) * 60 )
if tmp == 60
then
t = t + 1
tmp = 0
end
return res .. math.floor( t ) .. ':' .. string.format( '%02d', tmp )
end -- function fmt
function to_eur( x )
return math.floor( x * 100 + 0.5 ) / 100
end -- function fmt
file = io.open( tibber_rates, 'rb' )
txt = file:read '*a'
--res = json.decode( txt )
res = json:decode( txt )
prices = {}
for x, y in pairs( res["data"]["viewer"]["homes"][1]["currentSubscription"]["priceInfo"][ "today" ] )
do
prices[ x - 1 ] = y[ "total" ]
end
for x, y in pairs( res["data"]["viewer"]["homes"][1]["currentSubscription"]["priceInfo"][ "tomorrow" ] )
do
prices[ x - 1 + 24 ] = y[ "total" ]
end
min_start = math.huge
min_price = math.huge
max_start = -math.huge
max_price = -math.huge
for h = ( os.date("*t").hour + os.date("*t").min / 60 + os.date("*t").sec / 3600 ) * 10, 480 -- search from now, interval: 1/10 h = 6 min
do
i=0
cost = 0
last_p = 0
last_t = 0
for l in io.lines( power_profile )
do
if i > 2 -- skip header
then
tx, sx, px = string.match( l, '(.*) (.*) (.*)' )
t = tonumber( tx ) / 1E9
p = tonumber( px )
if i == 3 then profile_start_time = t end -- shift power_profile to t = 0
t = t - profile_start_time + h / 10 * 3600
if last_t ~= 0
then
t_temp = math.floor( t / 3600 )
if t_temp >= 48
then
cost = math.huge * 0 -- nan; not computable: no rate for the day after tomorrow ...
else
cost = cost + ( p + last_p ) / 2 * ( t - last_t ) * prices[ t_temp ] / 1000 / 3600
end
end
last_p = p
last_t = t
end
i = i +1
end -- for l in io.lines
if cost < min_price
then
min_price = cost
min_start = h
end
if cost > max_price
then
max_price = cost
max_start = h
end
end -- for h
print( '', 'hour', 'total cost' )
print( 'max: ', fmt( max_start/10 ), to_eur( max_price ) )
print( 'min: ', fmt( min_start/10 ), to_eur( min_price ) )
print ( 'max-min: ', to_eur( max_price - min_price ) )
Result:
hour total cost
max: 18.9 0.15
min: +13 0.11
max-min: 0.04
Cost max for start at 18:54 today, cost min for start at 1 pm tomorrow. Maximal savings: 4 ct
- Repeat for different programs/different DUOs.
If you are worried about the energy required to brute force the optimization problem:
Reduce
- the data points in the power profile by averaging,
- the search interval from 1/10 h = 6 min to 1 h; it shouldn’t make a big difference if the power profile isn’t too pathological.
Limitations:
The more intelligent the DUO is, the more variability will be seen in the power profile. This may invalidate the optimal solution to some extent.
Downloads:
bosch_program_1.power_profile.txt (28.9 KB)
tibber_rates_20230318.json (908 Bytes)
save_money.lua.txt (2.0 KB)
Questions:
By adding user configurable boundary conditions to the calculations (start not earlier than, end not later than, at least m times a day and at least every n-th hour, …) the script could be converted into a universal cost optimizer (-> new Binding?). What do you think? I would be particularly interested in your opinion on whether a list of boundary conditions can be created that covers most user stories.
Edit #1:
Improvements to Lua script.
Power profile of my dishwasher (Bosch, 70°C, non-eco):
bosch_dishwasher_70_non_eco.power_profile.txt (84.9 KB)
tibber_rates_20230320_21.json (907 Bytes)
Result:
hour total cost
max: 19:33 0.47
min: +21:51 0.37
max-min: 0.1