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, ZWave, …)
 InfluxDB
 Grafana (optional)
 dynamically priced energy contract, e.g. Tibber
 Lua and
jsonlua
(sudo luarocks install jsonlua
).
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 >= '20230317T21:59:24+01:00' AND time <= '20230317T23: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 "ContentType: application/json" \
X POST \
d '{ "query": "{ viewer { homes { currentSubscription{ priceInfo{ today { total } tomorrow { total } } } } } }" }' https://api.tibber.com/v1beta/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 ( 'maxmin: ', to_eur( max_price  min_price ) )
Result:
hour total cost
max: 18.9 0.15
min: +13 0.11
maxmin: 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 nth 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, noneco):
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
maxmin: 0.1