diff --git a/tasmota/berry/modules/cheap_power.tapp b/tasmota/berry/modules/cheap_power.tapp new file mode 100644 index 000000000..4235d50eb Binary files /dev/null and b/tasmota/berry/modules/cheap_power.tapp differ diff --git a/tasmota/berry/modules/cheap_power/autoexec.be b/tasmota/berry/modules/cheap_power/autoexec.be new file mode 100644 index 000000000..231c7fd4a --- /dev/null +++ b/tasmota/berry/modules/cheap_power/autoexec.be @@ -0,0 +1,11 @@ +var wd = tasmota.wd +tasmota.add_cmd("CheapPower", + def (cmd, idx) + import sys + var path = sys.path() + path.push(wd) + import cheap_power + path.pop() + cheap_power.start(idx) + end +) diff --git a/tasmota/berry/modules/cheap_power/cheap_power.be b/tasmota/berry/modules/cheap_power/cheap_power.be new file mode 100644 index 000000000..bf45f8bf0 --- /dev/null +++ b/tasmota/berry/modules/cheap_power/cheap_power.be @@ -0,0 +1,154 @@ +import webserver +import json + +var cheap_power = module("cheap_power") + +cheap_power.init = def (m) +class CheapPower + var prices # future prices for up to 48 hours + var times # start times of the prices + var timeout# timeout until retrying to update prices + var chosen # the chosen time slot + var channel# the channel to control + var tz # the current time zone offset from UTC + static var PAST = -3600 # minimum timer start age + static var MULT = .1255 # conversion to ¢/kWh including 25.5% VAT + static var PREV = 0, PAUSE = 1, UPDATE = 2, NEXT= 3 + static var UI = "" + "" + "" + "" + "" + "
" + static var URL0 = 'http://sahkotin.fi/prices?start=', URL1 = '&end=' + static var URLTIME = '%Y-%m-%dT%H:00:00.000Z' + + def init() + self.prices = [] + self.times = [] + end + + def start(idx) + if idx == nil || idx < 1 || idx > tasmota.global.devices_present + tasmota.log(f"CheapPower{idx} is not a valid Power output") + tasmota.resp_cmnd_failed() + else + self.channel = idx - 1 + tasmota.add_driver(self) + self.update() + tasmota.resp_cmnd_done() + end + end + + def power(on) tasmota.set_power(self.channel, on) end + + # fetch the prices for the next 24 to 48 hours + def update() + var wc = webclient() + var rtc = tasmota.rtc() + self.tz = rtc['timezone'] * 60 + var now = rtc['utc'] + var url = self.URL0 + + tasmota.strftime(self.URLTIME, now) + self.URL1 + + tasmota.strftime(self.URLTIME, now + 172800) + wc.begin(url) + var rc = wc.GET() + if rc == 200 + var data = json.load(wc.get_string()) + wc.close() + if data data = data.find('prices') end + var prices = [], times = [] + if data + for i: data.keys() + var datum = data[i] + prices.push(self.MULT * datum['value']) + times.push(tasmota.strptime(datum['date'], + '%Y-%m-%dT%H:%M:%S.000Z')['epoch']) + end + self.timeout = nil + self.prices = prices + self.times = times + self.schedule_chosen(self.find_cheapest(), now, self.PAST) + return + end + else + wc.close() + print(f'error {rc} for {url}') + end + # We failed to update the prices. Retry in 1, 2, 4, 8, …, 64 minutes. + if !self.timeout + self.timeout = 60000 + elif self.timeout < 3840000 + self.timeout = self.timeout * 2 + end + tasmota.set_timer(self.timeout, /->self.update()) + end + + # determine the cheapest slot + def find_cheapest() + var cheapest, N = size(self.prices) + if N + cheapest = 0 + for i: 1..N-1 + if self.prices[i] < self.prices[cheapest] cheapest = i end + end + end + return cheapest + end + + def date_from_now(chosen, now) return self.times[chosen] - now end + + # trigger the timer at the chosen hour + def schedule_chosen(chosen, now, old) + tasmota.remove_timer('power_on') + var d = chosen == nil ? self.PAST : self.date_from_now(chosen, now) + if d != old self.power(d > self.PAST && d <= 0) end + if d > 0 + tasmota.set_timer(d * 1000, def() self.power(true) end, 'power_on') + elif d <= self.PAST + chosen = nil + end + self.chosen = chosen + end + + def web_add_main_button() webserver.content_send(self.UI) end + + def web_sensor() + var ch, old = self.PAST, now = tasmota.rtc()['utc'] + var N = size(self.prices) + if N + ch = self.chosen + if ch != nil && ch < N old = self.date_from_now(ch, now) end + while N + if self.date_from_now(0, now) > self.PAST break end + ch = ch ? ch - 1 : nil + self.prices.pop(0) + self.times.pop(0) + N -= 1 + end + end + var op = webserver.has_arg('op') ? int(webserver.arg('op')) : nil + if op == self.UPDATE + self.update() + ch = self.chosen + end + if !N + elif op == self.PAUSE + ch = ch == nil ? self.find_cheapest() : nil + elif op == self.PREV + ch = (!ch ? N : ch) - 1 + elif op == self.NEXT + ch = ch != nil && ch + 1 < N ? ch + 1 : 0 + end + self.schedule_chosen(ch, now, old) + var status = ch == nil + ? '{s}⭘{m}{e}' + : format('{s}⭙ %s{m}%.3g ¢{e}', + tasmota.strftime('%Y-%m-%d %H:%M', self.tz + self.times[ch]), + self.prices[ch]) + tasmota.web_send_decimal(status) + end +end +return CheapPower() +end +return cheap_power