diff --git a/tasmota/berry/modules/cheap_power/README.md b/tasmota/berry/modules/cheap_power/README.md new file mode 100644 index 000000000000..9be12936f8f2 --- /dev/null +++ b/tasmota/berry/modules/cheap_power/README.md @@ -0,0 +1,105 @@ +## CheapPower Module + +This fetches electricity prices and chooses the cheapest future time slots. +Currently, the following price data sources have been implemented: +* `cheap_power_dk_no.tapp`: Denmark, Norway (Nord Pool) +* `cheap_power_elering.tapp`: Estonia, Finland, Latvia, Lithuania (Nord Pool via [Elering](https://elering.ee/en), ex VAT) +* `cheap_power_fi.tapp`: Finland (Nord Pool) +* `cheap_power_se.tapp`: Sweden (Nord Pool); assuming Swedish time zone +* `cheap_power_smard.tapp`: https://smard.de (Central Europe; local time) +* `cheap_power_uk_octopus.tapp`: United Kingdom (Octopus Energy) + +### Usage: + +* copy the `cheap_power_*.tapp` for your data source to the file system +* Invoke the Tasmota command `BrRestart` or restart the entire firmware +* Invoke the Tasmota command `CheapPower1`, `CheapPower2`, … to + * download prices for some time into the future + * automatically choose the cheapest future time slots (default: 1) + * to schedule `Power1 ON`, `Power2 ON`, … at the chosen slots + * to install a Web UI in the main menu + +### Timer Installation: +``` +# Europe/Helsinki time zone; see https://tasmota.github.io/docs/Timezone-Table/ +Backlog0 Timezone 99; TimeStd 0,0,10,1,4,120; TimeDst 0,0,3,1,3,180 + +# Detach Switch1 from Power1 +Backlog0 SwitchMode1 15; SwitchTopic1 0 +Backlog0 WebButton1 boiler; WebButton2 heat +## Power off after 900 seconds (15 minutes) +#PulseTime1 1000 +# Power off after 3600 seconds (60 minutes, 1 hour) +PulseTime1 3700 + +Rule1 ON Clock#Timer DO CheapPower1 … ENDON +Timer {"Enable":1,"Mode":0,"Time":"18:00","Window":0,"Days":"1111111","Repeat":1,"Output":1,"Action":3} +Rule1 1 +Timers 1 +``` +The download schedule can be adjusted in the timer configuration menu. +The prices for the next day will typically be updated in the afternoon +or evening of the previous day. + +In case the prices cannot be downloaded, the download will be retried +in 1, 2, 4, 8, 16, 32, 64, 64, 64, … minutes until it succeeds. + +### Additional Parameters + +* `cheap_power_fi.tapp` (Finland): none +* `cheap_power_elering.tapp`: price zone FI, EE, LT, LV +* `cheap_power_dk_no.tapp` (Denmark, Norway): price zone DK1, DK2, NO1 to NO5 +* `cheap_power_se.tapp` (Sweden): price zone SE1 to SE4 +* `cheap_power_smard.tapp`: zone AT, BE, CH, CZ, DE, FR, HU, IT, LU, NL, PL, SI +``` +CheapPower1 DE +``` +* `cheap_power_uk_octopus.tapp` (United Kingdom): tariff name and price zone: +``` +CheapPower AGILE-24-10-01 B +``` +The default number of active price slots per day is 1. It can be set in the +web user interface. + +### Web User Interface + +The user interface in the main menu consists of the following buttons: +* ⏮ moves the first time slot earlier (or wraps from the beginning to the end) +* ⏭ moves the first time slot later (or wraps from the end to the beginning) +* ⏯ pauses (switches off) or chooses the optimal slots +* 🔄 requests the prices to be downloaded and the optimal slots to be chosen +* ➖ decreases the number of slots (minimum: 1) +* ➕ increases the number of slots (maximum: currently available price slots) + +The status output above the buttons may indicate that the output +is paused until further command or price update: +``` +(0≤1)0.299‥2.38 ¢/kWh ⭘ +``` +Or it may indicate the price and duration until the next active slot: +``` +(1≤1)0.299‥2.38 ¢/kWh (0.299) ⭙ 5:05 +``` +Or it may indicate the price of the currently active time slot: +``` +(1≤1)0.299‥2.38 ¢/kWh (0.299) ⭙ +``` +The first number indicates the number of remaining scheduled time slots, +and the second number indicates the maximum number of time slots per day +(default 1). + +The two quantities around the ‥ are the minimum and maximum known prices +from the current time onwards. + +The scheduled slots are also indicated by red bars in the line graph +of the available current and future prices. + +### Application Contents + +```bash +for i in */cheap_power.be +do + zip ../cheap_power_${i%/*}.tapp -j -0 autoexec.be \ + cheap_power_base.be binary_heap.be "$i" +done +``` diff --git a/tasmota/berry/modules/cheap_power/autoexec.be b/tasmota/berry/modules/cheap_power/autoexec.be new file mode 100644 index 000000000000..63e71dd11db2 --- /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, payload) + import sys + var path = sys.path() + path.push(wd) + import cheap_power + path.pop() + cheap_power.start(idx, payload) + end +) diff --git a/tasmota/berry/modules/cheap_power/binary_heap.be b/tasmota/berry/modules/cheap_power/binary_heap.be new file mode 100644 index 000000000000..2f95bdb5115f --- /dev/null +++ b/tasmota/berry/modules/cheap_power/binary_heap.be @@ -0,0 +1,54 @@ +# https://en.wikipedia.org/wiki/Binary_heap +# This allows to choose the M first elements of an N-sized array +# with respect to a comparison predicate cmp that defines a total order. +# This avoids the overhead of sorting the entire array and then picking +# the first elements. This is related to a priority queue. +# We also define a binary heap based sort() of an entire array. + +var binary_heap = module("binary_heap") + +binary_heap._heapify = def(array, cmp, i) + var m = i, child, e, am, ac + while true + child = 2 * i + 1 + if child >= array.size() return end + ac = array[child] + am = array[m] + if cmp(ac, am) m = child am = ac end + child += 1 + if child < array.size() + ac = array[child] + if cmp(ac, am) m = child am = ac end + end + if m == i break end + array[m] = array[i] + array[i] = am + i = m + end +end + +# similar to C++11 std::make_heap +binary_heap.make_heap = def(array, cmp) + var i = size(array) / 2 + while i >= 0 binary_heap._heapify(array, cmp, i) i -= 1 end +end + +# similar to C++11 std::pop_heap, but removes and returns the element +binary_heap.remove_heap = def(array, cmp) + var m = array.size() + if m < 2 return m == 1 ? array.pop() : nil end + m = array[0] + array[0] = array.pop() + binary_heap._heapify(array, cmp, 0) + return m +end + +# https://en.wikipedia.org/wiki/Heapsort +binary_heap.sort = def(array, cmp) + var i = array.size(), heap = array.copy() + binary_heap.make_heap(heap, cmp) + array.clear() + while i > 0 array.push(binary_heap.remove_heap(heap, cmp)) i -= 1 end +end + +return binary_heap diff --git a/tasmota/berry/modules/cheap_power/cheap_power_base.be b/tasmota/berry/modules/cheap_power/cheap_power_base.be new file mode 100644 index 000000000000..ff35ed87ad05 --- /dev/null +++ b/tasmota/berry/modules/cheap_power/cheap_power_base.be @@ -0,0 +1,237 @@ +var cheap_power_base = module("cheap_power_base") + +cheap_power_base.init = def (m) + +import binary_heap +var make_heap = binary_heap.make_heap, remove_heap = binary_heap.remove_heap +import webserver + +class CheapPowerBase + var plugin # the data source + 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 slots + var slots # the maximum number of time slots to choose + var channel# the channel to control + var tz # the current time zone offset from UTC + var p_kWh # currency unit/kWh + var past # minimum timer start age (one time slot duration) + static PREV = 0, NEXT = 1, PAUSE = 2, UPDATE = 3, LESS = 4, MORE = 5 + static UI = "
| " + " | " + " | " + " | " + " | " + " | " + " |