18
|
1 import { LitElement, PropertyValues, TemplateResult, css, html } from "lit";
|
|
2 import { customElement, state } from "lit/decorators.js";
|
16
|
3 import { shared } from "./shared";
|
|
4
|
18
|
5 const HR = 3600;
|
|
6 const off_peak_wh_cost = 0.27 / 1000;
|
|
7 const on_peak_wh_cost = 0.312 / 1000;
|
|
8
|
16
|
9 @customElement("fd-electricity")
|
|
10 export class FdElectricity extends LitElement {
|
17
|
11 static styles = [
|
|
12 shared,
|
|
13 css`
|
16
|
14 :host {
|
|
15 font-size: 14px;
|
|
16 }
|
18
|
17 .unit {
|
|
18 color: #6c7d6c;
|
|
19 font-family: monospace; /* for dollar signs */
|
|
20 }
|
16
|
21 :host > table {
|
|
22 margin-top: 10px;
|
|
23 }
|
17
|
24 th,
|
|
25 td {
|
|
26 padding: 0 0;
|
20
|
27 white-space: nowrap;
|
17
|
28 }
|
16
|
29 th {
|
|
30 text-align: left;
|
|
31 }
|
|
32 td {
|
|
33 text-align: right;
|
|
34 }
|
|
35 tr.total td,
|
|
36 tr.total th {
|
18
|
37 border-top: 1px solid #6c7d6c;
|
16
|
38 }
|
|
39 .bar {
|
|
40 background: yellow;
|
|
41 display: inline-block;
|
|
42 height: 10px;
|
20
|
43 margin-left: 5px;
|
16
|
44 }
|
18
|
45 .bar > div {
|
|
46 transition: width 3s ease-out;
|
|
47 }
|
16
|
48 .total .bar {
|
|
49 background: pink;
|
|
50 }
|
18
|
51 table {
|
|
52 border-collapse: collapse;
|
|
53 }
|
20
|
54 .summary tr:first-of-type {
|
|
55 opacity: .7;
|
|
56 }
|
18
|
57 .summary td {
|
|
58 padding: 2px 6px;
|
|
59 }
|
|
60 .summary td,
|
|
61 .summary th {
|
|
62 border: 1px solid #2f2f2f;
|
|
63 }
|
|
64 .summary td.odometer {
|
20
|
65 width: 3.4em;
|
18
|
66 text-align: left;
|
|
67 }
|
16
|
68 `,
|
17
|
69 ];
|
18
|
70 @state() seriesData: Map<string, number> = new Map();
|
|
71 @state() isPeak = false;
|
|
72 @state() yesterday_charge = 0; // $$
|
|
73 @state() today_charge = 0; // $$
|
|
74 @state() today_peak_penalty = 0; // $$
|
|
75 @state() yesterday_peak_penalty = 0; // $$
|
|
76 @state() lastLoad = 0; // unix secs
|
|
77
|
17
|
78 constructor() {
|
|
79 super();
|
|
80 this.load();
|
18
|
81 setInterval(this.load.bind(this), 8 * 1000);
|
20
|
82 setInterval(this.computeLiveState.bind(this), 500);
|
17
|
83 }
|
18
|
84
|
17
|
85 async load() {
|
18
|
86 const seriesData = new Map();
|
17
|
87 const base = "https://bigasterisk.com/m/vmselect/select/0/prometheus/api/v1/query";
|
18
|
88 const { t, day0, day1, hoursSinceMidnight, hours } = this.timeMath();
|
|
89
|
17
|
90 const seriesUrls = [
|
18
|
91 { url: base + `?query=powermeter_w&time=${t}`, label: "powermeter" },
|
|
92 { url: base + `?query=sum (powermeter_w{sensor=~".*_fridge"})&time=${t}`, label: "fridges" },
|
|
93 { url: base + `?query=sum (powermeter_w{sensor=~"ws_.*"})&time=${t}`, label: "ws" },
|
|
94 { url: base + `?query=(sum%20by%20%20(s)%20(house_power_w))%20-%20(sum%20by(s)%20(powermeter_w))&time=${t}`, label: "unmetered" },
|
|
95 { url: base + `?query=house_power_w&time=${t}`, label: "total" },
|
|
96
|
|
97 { url: base + `?query=avg_over_time(house_power_w[17h]) @ ${day0 + 17 * HR}`, label: "day0_off_0" },
|
|
98 { url: base + `?query=avg_over_time(house_power_w[3h]) @ ${day0 + 20 * HR}`, label: "day0_on_0" },
|
|
99 { url: base + `?query=avg_over_time(house_power_w[4h]) @ ${day1}`, label: "day0_off_1" },
|
|
100 { url: base + `?query=avg_over_time(house_power_w[24h]) @ ${day1}`, label: "day0_avg_w" },
|
|
101
|
|
102 { url: base + `?query=avg_over_time(house_power_w[17h]) @ ${day1 + 17 * HR}`, label: "day1_off_0" },
|
|
103 { url: base + `?query=avg_over_time(house_power_w[3h]) @ ${day1 + 20 * HR}`, label: "day1_on_0" },
|
|
104 { url: base + `?query=avg_over_time(house_power_w[4h]) @ ${day1 + 24 * HR}`, label: "day1_off_1" },
|
|
105 { url: base + `?query=avg_over_time(house_power_w[${hoursSinceMidnight}h]) @ ${t}`, label: "day1_avg_w" },
|
16
|
106 ];
|
17
|
107
|
|
108 for (const series of seriesUrls) {
|
|
109 const response = await fetch(series.url);
|
|
110 const data = await response.json();
|
|
111 for (let row of data.data.result) {
|
18
|
112 const key = series.label + (row.metric.sensor ? "-" + row.metric.sensor : "");
|
|
113 const value = parseFloat(row.value[1]);
|
|
114 seriesData.set(key, value);
|
17
|
115 }
|
16
|
116 }
|
18
|
117
|
|
118 this.lastLoad = t;
|
20
|
119 this.isPeak = hours >= 17 && hours < 20; // todo: no peak on weekends
|
18
|
120 this.seriesData = seriesData;
|
17
|
121 this.update(new Map() as PropertyValues);
|
|
122 }
|
18
|
123
|
|
124 private timeMath() {
|
|
125 const now = new Date();
|
|
126
|
|
127 const t = now.getTime() / 1000;
|
|
128
|
|
129 const hours = now.getHours();
|
|
130 const minutes = now.getMinutes();
|
|
131 const seconds = now.getSeconds();
|
|
132 const hoursSinceMidnight = (hours * 60 * 60 + minutes * 60 + seconds) / HR;
|
|
133
|
|
134 const lastMidnight = new Date(now);
|
|
135 lastMidnight.setHours(0, 0, 0, 0);
|
|
136 const day1 = lastMidnight.getTime() / 1000;
|
|
137 const day0 = day1 - 86400;
|
|
138
|
|
139 return { t, day0, day1, hoursSinceMidnight, hours };
|
|
140 }
|
|
141
|
|
142 private computeLiveState() {
|
|
143 const data = (label: string): number => this.seriesData.get(label) || 0;
|
|
144
|
|
145 const day0_off_peak_wh = data("day0_off_0") * 17 + data("day0_off_1") * 4;
|
|
146 const day0_on_peak_wh = data("day0_on_0") * 3;
|
|
147 this.yesterday_charge = off_peak_wh_cost * day0_off_peak_wh + on_peak_wh_cost * day0_on_peak_wh;
|
|
148
|
|
149 const day1_off_peak_wh = data("day1_off_0") * 17 + data("day1_off_1") * 4;
|
|
150 const day1_on_peak_wh = data("day1_on_0") * 3;
|
|
151 this.today_charge = off_peak_wh_cost * day1_off_peak_wh + on_peak_wh_cost * day1_on_peak_wh + this.costSinceLastLoad();
|
|
152
|
|
153 this.yesterday_peak_penalty = this.yesterday_charge - off_peak_wh_cost * (day0_off_peak_wh + day0_on_peak_wh);
|
|
154 this.today_peak_penalty = this.today_charge - off_peak_wh_cost * (day1_off_peak_wh + day1_on_peak_wh);
|
|
155 }
|
|
156
|
|
157 private costSinceLastLoad() {
|
|
158 const t = Date.now() / 1000;
|
|
159 const hr_since_last_load = (t - this.lastLoad) / HR;
|
|
160 const wh_since_last_load = (this.seriesData.get("total") || 0) * hr_since_last_load;
|
|
161 const cost_since_load = (this.isPeak ? on_peak_wh_cost : off_peak_wh_cost) * wh_since_last_load;
|
|
162 return cost_since_load;
|
|
163 }
|
|
164
|
17
|
165 render() {
|
18
|
166 const disp0 = (n: number | undefined) => (n ? `${n.toFixed(0)}` : "0");
|
|
167 const disp1 = (n: number | undefined) => (n ? `${n.toFixed(1)}` : "0.0");
|
|
168 const dispDollar = (n: number | undefined, places = 2) => (n ? html`<span class="unit">$</span>${n.toFixed(places)}` : "");
|
20
|
169 const pxPerWatt = 150 / 2500;
|
18
|
170 const bar = (n: number | undefined) => (n ? html`<div style="width: ${Math.ceil(n * pxPerWatt).toFixed(1)}px"></div>` : "");
|
|
171
|
|
172 const seriesRow = (label: string, value: number | undefined, unitColumn: TemplateResult | string) => html`
|
|
173 <tr>
|
|
174 <th>${label}</th>
|
|
175 <td>${disp1(value)}</td>
|
|
176 <td>${unitColumn}</td>
|
|
177 <td class="bar">${bar(value)}</td>
|
|
178 </tr>
|
|
179 `;
|
|
180
|
|
181 return html`<h1 data-text="Electricity">Electricity (now is ${this.isPeak ? "Peak" : "Off-Peak"})</h1>
|
16
|
182 <table>
|
18
|
183 ${seriesRow("washer", this.seriesData.get("powermeter-ga_washer"), html`<span class="unit">W</span>`)}
|
|
184 ${seriesRow("theater console", this.seriesData.get("powermeter-tt_console"), "")} ${seriesRow("workshop", this.seriesData.get("ws"), "")}
|
|
185 ${seriesRow("dash", this.seriesData.get("powermeter-do_r"), "")} ${seriesRow("fridges", this.seriesData.get("fridges"), "")}
|
|
186 ${seriesRow("server closet", this.seriesData.get("powermeter-st_wall"), "")} ${seriesRow("(unmetered)", this.seriesData.get("unmetered"), "")}
|
|
187 <tr class="total">
|
|
188 <th>Total</th>
|
|
189 <td>${(this.seriesData.get("total") || 0).toFixed(1)}</td>
|
|
190 <td></td>
|
|
191 <td class="bar">${bar(this.seriesData.get("total"))}</td>
|
|
192 </tr>
|
|
193 </table>
|
|
194 <table class="summary">
|
16
|
195 <tr>
|
18
|
196 <th>yesterday</th>
|
|
197 <td>${disp0(this.seriesData.get("day0_avg_w"))} <span class="unit">W avg</span></td>
|
|
198 <td class="odometer">${dispDollar(this.yesterday_charge)}</td>
|
|
199 <td>${dispDollar(this.yesterday_peak_penalty)} peak penalty</td>
|
16
|
200 </tr>
|
|
201 <tr>
|
18
|
202 <th>today</th>
|
|
203 <td>${disp0(this.seriesData.get("day1_avg_w"))} <span class="unit">W avg</span></td>
|
20
|
204 <td class="odometer">${dispDollar(this.today_charge, 4)}</td>
|
18
|
205 <td>${dispDollar(this.today_peak_penalty)} peak penalty</td>
|
16
|
206 </tr>
|
18
|
207 </table>`;
|
17
|
208 }
|
16
|
209 }
|