changeset 18:499996b8224a

more electricity summary info
author drewp@bigasterisk.com
date Wed, 19 Jun 2024 18:35:12 -0700
parents 472003015880
children 6960c172f1a9
files src/FdElectricity.ts src/main.css
diffstat 2 files changed, 147 insertions(+), 57 deletions(-) [+]
line wrap: on
line diff
--- a/src/FdElectricity.ts	Fri Jun 07 21:22:37 2024 -0700
+++ b/src/FdElectricity.ts	Wed Jun 19 18:35:12 2024 -0700
@@ -1,7 +1,11 @@
-import { LitElement, PropertyValues, css, html } from "lit";
-import { customElement, property } from "lit/decorators.js";
+import { LitElement, PropertyValues, TemplateResult, css, html } from "lit";
+import { customElement, state } from "lit/decorators.js";
 import { shared } from "./shared";
 
+const HR = 3600;
+const off_peak_wh_cost = 0.27 / 1000;
+const on_peak_wh_cost = 0.312 / 1000;
+
 @customElement("fd-electricity")
 export class FdElectricity extends LitElement {
   static styles = [
@@ -10,7 +14,10 @@
       :host {
         font-size: 14px;
       }
-
+      .unit {
+        color: #6c7d6c;
+        font-family: monospace; /* for dollar signs */
+      }
       :host > table {
         margin-top: 10px;
       }
@@ -26,89 +33,172 @@
       }
       tr.total td,
       tr.total th {
-        border-top: 1px solid white;
+        border-top: 1px solid #6c7d6c;
       }
       .bar {
         background: yellow;
         display: inline-block;
         height: 10px;
       }
+      .bar > div {
+        transition: width 3s ease-out;
+      }
       .total .bar {
         background: pink;
       }
+      table {
+        border-collapse: collapse;
+      }
+      .summary td {
+        padding: 2px 6px;
+      }
+      .summary td,
+      .summary th {
+        border: 1px solid #2f2f2f;
+      }
+      .summary td.odometer {
+        width: 4.7em;
+        text-align: left;
+      }
     `,
   ];
-  @property() seriesData: Map<string, number> = new Map();
+  @state() seriesData: Map<string, number> = new Map();
+  @state() isPeak = false;
+  @state() yesterday_charge = 0; // $$
+  @state() today_charge = 0; // $$
+  @state() today_peak_penalty = 0; // $$
+  @state() yesterday_peak_penalty = 0; // $$
+  @state() lastLoad = 0; // unix secs
+
   constructor() {
     super();
     this.load();
-    setInterval(this.load.bind(this), 10 * 1000);
+    setInterval(this.load.bind(this), 8 * 1000);
+    setInterval(this.computeLiveState.bind(this), 60);
   }
+
   async load() {
-    this.seriesData = new Map();
+    const seriesData = new Map();
     const base = "https://bigasterisk.com/m/vmselect/select/0/prometheus/api/v1/query";
-    const now = Date.now() / 1000 - 10;
+    const { t, day0, day1, hoursSinceMidnight, hours } = this.timeMath();
+
     const seriesUrls = [
-      { url: base + `?query=powermeter_w&time=${now}&step=725ms`, label: "powermeter" },
-      { url: base + `?query=(sum%20by%20%20(s)%20(house_power_w))%20-%20(sum%20by(s)%20(powermeter_w))&time=${now}&step=725ms`, label: "unmetered" },
-      { url: base + `?query=house_power_w&time=${now}&step=725ms`, label: "total" },
+      { url: base + `?query=powermeter_w&time=${t}`, label: "powermeter" },
+      { url: base + `?query=sum (powermeter_w{sensor=~".*_fridge"})&time=${t}`, label: "fridges" },
+      { url: base + `?query=sum (powermeter_w{sensor=~"ws_.*"})&time=${t}`, label: "ws" },
+      { url: base + `?query=(sum%20by%20%20(s)%20(house_power_w))%20-%20(sum%20by(s)%20(powermeter_w))&time=${t}`, label: "unmetered" },
+      { url: base + `?query=house_power_w&time=${t}`, label: "total" },
+
+      { url: base + `?query=avg_over_time(house_power_w[17h]) @ ${day0 + 17 * HR}`, label: "day0_off_0" },
+      { url: base + `?query=avg_over_time(house_power_w[3h]) @ ${day0 + 20 * HR}`, label: "day0_on_0" },
+      { url: base + `?query=avg_over_time(house_power_w[4h]) @ ${day1}`, label: "day0_off_1" },
+      { url: base + `?query=avg_over_time(house_power_w[24h]) @ ${day1}`, label: "day0_avg_w" },
+
+      { url: base + `?query=avg_over_time(house_power_w[17h]) @ ${day1 + 17 * HR}`, label: "day1_off_0" },
+      { url: base + `?query=avg_over_time(house_power_w[3h]) @ ${day1 + 20 * HR}`, label: "day1_on_0" },
+      { url: base + `?query=avg_over_time(house_power_w[4h]) @ ${day1 + 24 * HR}`, label: "day1_off_1" },
+      { url: base + `?query=avg_over_time(house_power_w[${hoursSinceMidnight}h]) @ ${t}`, label: "day1_avg_w" },
     ];
 
     for (const series of seriesUrls) {
       const response = await fetch(series.url);
       const data = await response.json();
       for (let row of data.data.result) {
-        this.seriesData.set(series.label + (row.metric.sensor ? "-" + row.metric.sensor : ""), parseFloat(row.value[1]));
+        const key = series.label + (row.metric.sensor ? "-" + row.metric.sensor : "");
+        const value = parseFloat(row.value[1]);
+        seriesData.set(key, value);
       }
     }
+
+    this.lastLoad = t;
+    this.isPeak = hours >= 17 && hours < 20;
+    this.seriesData = seriesData;
     this.update(new Map() as PropertyValues);
   }
+
+  private timeMath() {
+    const now = new Date();
+
+    const t = now.getTime() / 1000;
+
+    const hours = now.getHours();
+    const minutes = now.getMinutes();
+    const seconds = now.getSeconds();
+    const hoursSinceMidnight = (hours * 60 * 60 + minutes * 60 + seconds) / HR;
+
+    const lastMidnight = new Date(now);
+    lastMidnight.setHours(0, 0, 0, 0);
+    const day1 = lastMidnight.getTime() / 1000;
+    const day0 = day1 - 86400;
+
+    return { t, day0, day1, hoursSinceMidnight, hours };
+  }
+
+  private computeLiveState() {
+    const data = (label: string): number => this.seriesData.get(label) || 0;
+
+    const day0_off_peak_wh = data("day0_off_0") * 17 + data("day0_off_1") * 4;
+    const day0_on_peak_wh = data("day0_on_0") * 3;
+    this.yesterday_charge = off_peak_wh_cost * day0_off_peak_wh + on_peak_wh_cost * day0_on_peak_wh;
+
+    const day1_off_peak_wh = data("day1_off_0") * 17 + data("day1_off_1") * 4;
+    const day1_on_peak_wh = data("day1_on_0") * 3;
+    this.today_charge = off_peak_wh_cost * day1_off_peak_wh + on_peak_wh_cost * day1_on_peak_wh + this.costSinceLastLoad();
+
+    this.yesterday_peak_penalty = this.yesterday_charge - off_peak_wh_cost * (day0_off_peak_wh + day0_on_peak_wh);
+    this.today_peak_penalty = this.today_charge - off_peak_wh_cost * (day1_off_peak_wh + day1_on_peak_wh);
+  }
+
+  private costSinceLastLoad() {
+    const t = Date.now() / 1000;
+    const hr_since_last_load = (t - this.lastLoad) / HR;
+    const wh_since_last_load = (this.seriesData.get("total") || 0) * hr_since_last_load;
+    const cost_since_load = (this.isPeak ? on_peak_wh_cost : off_peak_wh_cost) * wh_since_last_load;
+    return cost_since_load;
+  }
+
   render() {
-    const disp = (n: number | undefined) => (n ? `${n.toFixed(1)}` : "");
-    const pxPerWatt = 150 / this.seriesData.get("total")!;
-    const bar = (n: number | undefined) => (n ? html`<div style="width: ${(n * pxPerWatt).toFixed(1)}px"></div>` : "");
-    return html`<h1 data-text="Electricity">Electricity</h1>
+    const disp0 = (n: number | undefined) => (n ? `${n.toFixed(0)}` : "0");
+    const disp1 = (n: number | undefined) => (n ? `${n.toFixed(1)}` : "0.0");
+    const dispDollar = (n: number | undefined, places = 2) => (n ? html`<span class="unit">$</span>${n.toFixed(places)}` : "");
+    const pxPerWatt = 150 / 1500;
+    const bar = (n: number | undefined) => (n ? html`<div style="width: ${Math.ceil(n * pxPerWatt).toFixed(1)}px"></div>` : "");
+
+    const seriesRow = (label: string, value: number | undefined, unitColumn: TemplateResult | string) => html`
+      <tr>
+        <th>${label}</th>
+        <td>${disp1(value)}</td>
+        <td>${unitColumn}</td>
+        <td class="bar">${bar(value)}</td>
+      </tr>
+    `;
+
+    return html`<h1 data-text="Electricity">Electricity (now is ${this.isPeak ? "Peak" : "Off-Peak"})</h1>
       <table>
+        ${seriesRow("washer", this.seriesData.get("powermeter-ga_washer"), html`<span class="unit">W</span>`)}
+        ${seriesRow("theater console", this.seriesData.get("powermeter-tt_console"), "")} ${seriesRow("workshop", this.seriesData.get("ws"), "")}
+        ${seriesRow("dash", this.seriesData.get("powermeter-do_r"), "")} ${seriesRow("fridges", this.seriesData.get("fridges"), "")}
+        ${seriesRow("server closet", this.seriesData.get("powermeter-st_wall"), "")} ${seriesRow("(unmetered)", this.seriesData.get("unmetered"), "")}
+        <tr class="total">
+          <th>Total</th>
+          <td>${(this.seriesData.get("total") || 0).toFixed(1)}</td>
+          <td></td>
+          <td class="bar">${bar(this.seriesData.get("total"))}</td>
+        </tr>
+      </table>
+      <table class="summary">
         <tr>
-          <th>(unmetered)</th>
-          <td>${disp(this.seriesData.get("unmetered"))}</td>
-          <td class="bar">${bar(this.seriesData.get("unmetered"))}</td>
-        </tr>
-        <tr>
-          <th>dash</th>
-          <td>${disp(this.seriesData.get("powermeter-do_r"))}</td>
-          <td class="bar">${bar(this.seriesData.get("powermeter-do_r"))}</td>
-        </tr>
-        <tr>
-          <th>washer</th>
-          <td>${disp(this.seriesData.get("powermeter-ga_washer"))}</td>
-          <td class="bar">${bar(this.seriesData.get("powermeter-ga_washer"))}</td>
+          <th>yesterday</th>
+          <td>${disp0(this.seriesData.get("day0_avg_w"))} <span class="unit">W avg</span></td>
+          <td class="odometer">${dispDollar(this.yesterday_charge)}</td>
+          <td>${dispDollar(this.yesterday_peak_penalty)} peak penalty</td>
         </tr>
         <tr>
-          <th>fridge</th>
-          <td>${disp(this.seriesData.get("powermeter-ki_fridge"))}</td>
-          <td class="bar">${bar(this.seriesData.get("powermeter-ki_fridge"))}</td>
-        </tr>
-        <tr>
-          <th>server closet</th>
-          <td>${disp(this.seriesData.get("powermeter-st_wall"))}</td>
-          <td class="bar">${bar(this.seriesData.get("powermeter-st_wall"))}</td>
+          <th>today</th>
+          <td>${disp0(this.seriesData.get("day1_avg_w"))} <span class="unit">W avg</span></td>
+          <td class="odometer">${dispDollar(this.today_charge, 6)}</td>
+          <td>${dispDollar(this.today_peak_penalty)} peak penalty</td>
         </tr>
-        <tr>
-          <th>theater console</th>
-          <td>${disp(this.seriesData.get("powermeter-tt_console"))}</td>
-          <td class="bar">${bar(this.seriesData.get("powermeter-tt_console"))}</td>
-        </tr>
-        <tr>
-          <th>workshop desk</th>
-          <td>${disp(this.seriesData.get("powermeter-ws_desk"))}</td>
-          <td class="bar">${bar(this.seriesData.get("powermeter-ws_desk"))}</td>
-        </tr>
-        <tr class="total">
-          <th>Total</th>
-          <td>${this.seriesData.get("total")} W</td>
-          <td class="bar">${bar(this.seriesData.get("total"))}</td>
-        </tr>
-      </table> `;
+      </table>`;
   }
 }
--- a/src/main.css	Fri Jun 07 21:22:37 2024 -0700
+++ b/src/main.css	Wed Jun 19 18:35:12 2024 -0700
@@ -21,11 +21,11 @@
 
 #grid {
   display: grid;
-  grid-template-rows: 54px 206px 1fr;
+  grid-template-rows: 54px 165px 1fr;
   grid-template-columns: 1.7fr 1fr 80px;
 }
 
-.debug #grid>* {
+.debug #grid > * {
   outline: 1px rgb(68, 68, 68) solid;
 }
 
@@ -42,9 +42,9 @@
 }
 
 fd-countdown {
-  grid-area: 2/2/2/4
+  grid-area: 2/2/2/4;
 }
 
 fd-electricity {
   grid-area: 3/2/3/4;
-}
\ No newline at end of file
+}