# @name Shelly 3EM Power (Gen1) # @desc Shelly 3EM Wirkleistung # @author Fukkel with KI help´s # @version 2.6_beta # @config shellyHost text "IP-Adresse Shelly 3EM" default=192.168.1.50 # @config interval number "Abfrage-Intervall (s)" default=5 min=2 max=60 step=1 # @config minP number "Anzeige ab Leistung (W)" default=0 min=0 max=5000 step=10 # @config tMed number "Schwelle mittel (W)" default=500 min=0 max=10000 step=10 # @config tHigh number "Schwelle hoch (W)" default=2000 min=0 max=10000 step=10 # @config iconId text "Eigenes Icon (ID, optional)" default= # @config maxPower number "Balkenskala Maximum (W)" default=4000 min=100 max=20000 step=100 # @config overload number "Überlastwarnung (W, 0=aus)" default=6000 min=0 max=30000 step=100 # @config fixedColor color "Feste Farbe (0=automatisch)" default=#000000 # @config inrushDelay number "Verzögerung Einschaltstrom (s)" default=5 min=0 max=30 step=1 # @config dayOffset number "Korrekturwert Tagesenergie (Wh)" default=0 min=0 max=100000 step=1 class ShellyPower var power, ticks, age, blink var wAbv, lvlT, cLvl var ovWAbv, ovT, cOv var failC var dWh, dayNum, viewMode var totals, dBase, eT, pSaveT, yWh, pMsk def init() self.power = store.get("power") self.ticks = 0 self.age = 9999 self.blink = 0 self.wAbv = false self.lvlT = 0 self.cLvl = nil self.ovWAbv = false self.ovT = 0 self.cOv = false self.failC = 0 self.dWh = self.cfg("dayEnergy", 0) self.dayNum = self.cfg("dayNum", -1) self.viewMode = 0 self.totals = [nil, nil, nil] self.dBase = store.get("dBase") self.eT = 0 self.pSaveT = 0 self.yWh = self.cfg("yesterdayEnergy", 0) self.pMsk = self.cfg("pMsk", 0) end def cfg(key, default) var v = store.get(key) if v == nil return default end return v end def cfg_min(key, default, minv) var v = self.cfg(key, default) if v < minv return minv end return v end def compute_level(p, tMed, tHigh) if p < 0 return -1 end if p < tMed return 0 end if p < tHigh return 1 end if p < tHigh * 2 return 2 end return 3 end def debounce(val, confirmed, wAbv, elapsed, delay, iv) if confirmed == nil || val <= confirmed return [val, false, 0] end elapsed = wAbv ? elapsed + iv : 0 if elapsed >= delay return [val, false, 0] end return [confirmed, true, elapsed] end def on_phase_total(idx, w, status) if w == nil return end var m = re.search("\"total\":([-0-9.]+)", w) if m == nil return end var v = num(m[1]) if v == nil return end self.totals[idx] = v self.recompute_energy() end def recompute_energy() var cur = 0 var ready = true var i = 0 while i < 3 if (self.pMsk >> i) & 1 == 1 if self.totals[i] == nil ready = false else cur += self.totals[i] end end i += 1 end if !ready return end i = 0 while i < 3 if self.totals[i] != nil && (self.pMsk >> i) & 1 == 0 if self.dBase != nil self.dBase += self.totals[i] end self.pMsk = self.pMsk | (1 << i) store.set("pMsk", self.pMsk) cur += self.totals[i] end i += 1 end if self.pMsk == 0 return end var d = day() var newDay = (d != self.dayNum) var meterReset = (self.dBase != nil && (cur < self.dBase || cur - self.dBase > 100000)) if newDay || self.dBase == nil || meterReset if newDay self.yWh = self.dWh + self.cfg("dayOffset", 0) store.set("yesterdayEnergy", self.yWh) store.set("dayOffset", 0) end self.dayNum = d self.dBase = cur store.set("dayNum", d) store.set("dBase", cur) end var e = cur - self.dBase if e < 0 e = 0 end self.dWh = e store.set("dayEnergy", e) end def on_body(w, status) if w == nil self.failC += 1 return end var m = re.search("\"total_power\":([-0-9.]+)", w) if m == nil self.failC += 1 return end var w_val = num(m[1]) if w_val == nil self.failC += 1 return end self.failC = 0 self.power = w_val self.age = 0 self.pSaveT -= 1 if self.pSaveT <= 0 store.set("power", w_val) self.pSaveT = 6 end var iv = self.cfg("interval", 5) var delay = self.cfg("inrushDelay", 5) var ap = w_val < 0 ? -w_val : w_val var lvl = self.compute_level(w_val, self.cfg("tMed", 500), self.cfg("tHigh", 2000)) var r1 = self.debounce(lvl, self.cLvl, self.wAbv, self.lvlT, delay, iv) self.cLvl = r1[0] self.wAbv = r1[1] self.lvlT = r1[2] var ov = store.get("overload") if ov != nil && ov > 0 var newOv = ap >= ov ? 1 : 0 var curOv = self.cOv ? 1 : 0 var r2 = self.debounce(newOv, curOv, self.ovWAbv, self.ovT, delay, iv) self.cOv = (r2[0] == 1) self.ovWAbv = r2[1] self.ovT = r2[2] else self.cOv = false self.ovWAbv = false self.ovT = 0 end end def loop() self.age += 1 if self.ticks <= 0 self.ticks = self.cfg_min("interval", 5, 1) var host = store.get("shellyHost") if host != nil && host != "" http.get("http://" + host + "/status", / b, st -> self.on_body(b, st), {'find': "\"total_power\":", 'keep': 300}) end end self.ticks -= 1 if self.eT <= 0 self.eT = 60 var host = store.get("shellyHost") if host != nil && host != "" http.get("http://" + host + "/emeter/0", / b, st -> self.on_phase_total(0, b, st), {'find': "\"total\":", 'keep': 100}) http.get("http://" + host + "/emeter/1", / b, st -> self.on_phase_total(1, b, st), {'find': "\"total\":", 'keep': 100}) http.get("http://" + host + "/emeter/2", / b, st -> self.on_phase_total(2, b, st), {'find': "\"total\":", 'keep': 100}) end end self.eT -= 1 end def draw_bolt(col) pixel(3, 0, col) pixel(2, 1, col) pixel(3, 1, col) pixel(1, 2, col) pixel(2, 2, col) pixel(3, 2, col) pixel(0, 3, col) pixel(1, 3, col) pixel(2, 3, col) pixel(3, 3, col) pixel(4, 3, col) pixel(1, 4, col) pixel(2, 4, col) pixel(3, 4, col) pixel(4, 4, col) pixel(1, 5, col) pixel(2, 5, col) pixel(3, 5, col) pixel(1, 6, col) pixel(2, 6, col) pixel(1, 7, col) end def dim_channel(v) if v <= 0 return 0 end var d = int(v / 2.8) if d < 25 d = 25 end if d > 255 d = 255 end return d end def draw_power_bar(col) var maxP = self.cfg("maxPower", 4000) if maxP <= 0 maxP = 4000 end var ap = self.power < 0 ? -self.power : self.power var barStart = 6 var barW = width() - barStart var filled = int(ap * barW / maxP + 0.5) if filled > barW filled = barW end if filled < 0 filled = 0 end var dim = (self.dim_channel((col >> 16) & 0xFF) << 16) | (self.dim_channel((col >> 8) & 0xFF) << 8) | self.dim_channel(col & 0xFF) var i = 0 while i < barW pixel(barStart + i, 7, i < filled ? col : dim) i += 1 end end def fmt_value(v, unit) if v < 1000 return str(int(v)) + unit end var k = v / 1000.0 if k >= 10 return str(int(k)) + "k" + unit end var frac = int((k - int(k)) * 10 + 0.0001) return str(int(k)) + "." + str(frac) + "k" + unit end def draw_icon(col) var ic = store.get("iconId") var shown = ic != nil && ic != "" && icon(ic, 0, 0) if !shown self.draw_bolt(col) end end def draw_label(label, col) var x = 6 + int((25 - text_ink_width(label)) / 2) if x < 6 x = 6 end text(x, 6, label, col) end def should_show() if self.power == nil return true end return (self.power < 0 ? -self.power : self.power) >= self.cfg("minP", 0) end def draw() clear() if self.power == nil if self.failC >= 3 text(4, 6, "IP?", 0xFF0000) else text(6, 6, "...", 0x666666) end return end if self.cOv self.blink += 1 if self.blink % 10 >= 5 return end end if self.age > self.cfg_min("interval", 5, 1) * 3 pixel(width() - 1, 0, 0xFF0000) end var fixed = store.get("fixedColor") var col if self.viewMode == 1 var label = self.fmt_value(self.dWh + self.cfg("dayOffset", 0), "Wh") col = (fixed != nil && fixed != 0) ? fixed : 0x00FFFF self.draw_icon(col) self.draw_label(label, col) return end if self.viewMode == 2 var label = self.fmt_value(self.yWh, "Wh") col = (fixed != nil && fixed != 0) ? fixed : 0xFF8800 self.draw_icon(col) self.draw_label(label, col) return end var p = self.power var ap = p < 0 ? -p : p var label = self.fmt_value(ap, "W") var levelColors = [0x00A2FF, 0x00FF00, 0xFFCC00, 0xFFA500, 0xFF0000] var lvl = self.cLvl if lvl == nil lvl = 0 end col = levelColors[lvl + 1] if fixed != nil && fixed != 0 col = fixed end if self.cOv col = 0xFF0000 end self.draw_icon(col) self.draw_label(label, col) self.draw_power_bar(col) end def on_button(btn) if btn == "select" self.viewMode = (self.viewMode + 1) % 3 self.ticks = 0 end end end return ShellyPower()