Skip to content

Commit c47e6b4

Browse files
committed
Add history node
1 parent 03d8e9f commit c47e6b4

File tree

4 files changed

+378
-1
lines changed

4 files changed

+378
-1
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@teslemetry/node-red-contrib-teslemetry": minor
3+
---
4+
5+
Add teslemetry-energy-history node for retrieving historical energy data
6+
7+
New node supports three history types:
8+
- Energy History - energy measurements (solar, battery, grid) aggregated by period
9+
- Backup History - off-grid event history aggregated by period
10+
- Telemetry (Charging) - wall connector charging history
11+
12+
Configurable date range with start/end dates and timezone support.

packages/node-red-contrib-teslemetry/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@
5050
"teslemetry-vehicle-command": "dist/nodes/teslemetry-vehicle-command.cjs",
5151
"teslemetry-event": "dist/nodes/teslemetry-event.cjs",
5252
"teslemetry-signal": "dist/nodes/teslemetry-signal.cjs",
53-
"teslemetry-energy-command": "dist/nodes/teslemetry-energy-command.cjs"
53+
"teslemetry-energy-command": "dist/nodes/teslemetry-energy-command.cjs",
54+
"teslemetry-energy-history": "dist/nodes/teslemetry-energy-history.cjs"
5455
}
5556
},
5657
"devDependencies": {
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
<script type="text/javascript">
2+
RED.nodes.registerType("teslemetry-energy-history", {
3+
category: "Teslemetry Energy",
4+
paletteLabel: "History",
5+
color: "#e6e600",
6+
defaults: {
7+
name: { value: "" },
8+
teslemetryConfig: {
9+
value: "",
10+
type: "teslemetry-config",
11+
required: true,
12+
},
13+
siteId: { value: "" },
14+
historyType: { value: "energy" },
15+
period: { value: "day" },
16+
startDate: { value: "" },
17+
endDate: { value: "" },
18+
timeZone: { value: "" },
19+
},
20+
inputs: 1,
21+
outputs: 1,
22+
icon: "font-awesome/fa-line-chart",
23+
label: function () {
24+
return this.name || "Energy: " + (this.historyType || "History");
25+
},
26+
oneditprepare: function () {
27+
const node = this;
28+
const siteSelect = $("#node-input-siteId");
29+
const configInput = $("#node-input-teslemetryConfig");
30+
const historyTypeSelect = $("#node-input-historyType");
31+
const periodRow = $("#period-row");
32+
33+
function updateSites() {
34+
const configId = configInput.val();
35+
if (configId && configId !== "_ADD_") {
36+
$.getJSON("teslemetry/energy_sites", { config: configId })
37+
.done(function (data) {
38+
const current = siteSelect.val() || node.siteId;
39+
siteSelect.empty();
40+
siteSelect.append(
41+
'<option value="">From msg.siteId</option>',
42+
);
43+
if (data && data.length > 0) {
44+
data.forEach(function ([id, name]) {
45+
siteSelect.append(
46+
'<option value="' +
47+
id +
48+
'">' +
49+
name +
50+
" (" +
51+
id +
52+
")</option>",
53+
);
54+
});
55+
}
56+
if (current) {
57+
siteSelect.val(current);
58+
}
59+
})
60+
.fail(function (jqxhr, textStatus, error) {
61+
console.error(
62+
"Failed to fetch energy sites",
63+
error,
64+
);
65+
});
66+
}
67+
}
68+
69+
function updatePeriodVisibility() {
70+
const historyType = historyTypeSelect.val();
71+
if (historyType === "telemetry") {
72+
periodRow.hide();
73+
} else {
74+
periodRow.show();
75+
}
76+
}
77+
78+
configInput.change(function () {
79+
updateSites();
80+
});
81+
82+
historyTypeSelect.change(function () {
83+
updatePeriodVisibility();
84+
});
85+
86+
if (configInput.val()) {
87+
updateSites();
88+
}
89+
90+
updatePeriodVisibility();
91+
},
92+
});
93+
</script>
94+
95+
<script type="text/html" data-template-name="teslemetry-energy-history">
96+
<div class="form-row">
97+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
98+
<input type="text" id="node-input-name" placeholder="Name" />
99+
</div>
100+
<div class="form-row">
101+
<label for="node-input-teslemetryConfig">
102+
<i class="fa fa-globe"></i> Config
103+
</label>
104+
<input type="text" id="node-input-teslemetryConfig" />
105+
</div>
106+
<div class="form-row">
107+
<label for="node-input-siteId">
108+
<i class="fa fa-home"></i> Site ID
109+
</label>
110+
<select id="node-input-siteId" style="width: 70%;">
111+
<option value="">From msg.siteId</option>
112+
</select>
113+
</div>
114+
<div class="form-row">
115+
<label for="node-input-historyType">
116+
<i class="fa fa-database"></i> History Type
117+
</label>
118+
<select id="node-input-historyType">
119+
<option value="energy">Energy History</option>
120+
<option value="backup">Backup History</option>
121+
<option value="telemetry">Telemetry (Charging)</option>
122+
</select>
123+
</div>
124+
<div class="form-row" id="period-row">
125+
<label for="node-input-period">
126+
<i class="fa fa-calendar"></i> Period
127+
</label>
128+
<select id="node-input-period">
129+
<option value="day">Day</option>
130+
<option value="week">Week</option>
131+
<option value="month">Month</option>
132+
<option value="year">Year</option>
133+
</select>
134+
</div>
135+
<div class="form-row">
136+
<label for="node-input-startDate">
137+
<i class="fa fa-calendar-o"></i> Start Date
138+
</label>
139+
<input
140+
type="text"
141+
id="node-input-startDate"
142+
placeholder="From msg.startDate (ISO 8601)"
143+
/>
144+
</div>
145+
<div class="form-row">
146+
<label for="node-input-endDate">
147+
<i class="fa fa-calendar-check-o"></i> End Date
148+
</label>
149+
<input
150+
type="text"
151+
id="node-input-endDate"
152+
placeholder="From msg.endDate (ISO 8601)"
153+
/>
154+
</div>
155+
<div class="form-row">
156+
<label for="node-input-timeZone">
157+
<i class="fa fa-clock-o"></i> Time Zone
158+
</label>
159+
<input
160+
type="text"
161+
id="node-input-timeZone"
162+
placeholder="From msg.timeZone (e.g., America/Los_Angeles)"
163+
/>
164+
</div>
165+
</script>
166+
167+
<script type="text/html" data-help-name="teslemetry-energy-history">
168+
<p>Retrieves historical data from an Energy Site via Teslemetry API.</p>
169+
<p>Requires a valid Teslemetry configuration and a Site ID.</p>
170+
171+
<h3>History Types</h3>
172+
<dl>
173+
<dt>Energy History</dt>
174+
<dd>
175+
Returns energy measurements (solar production, battery usage, grid
176+
imports/exports) aggregated by the selected period.
177+
</dd>
178+
<dt>Backup History</dt>
179+
<dd>
180+
Returns backup (off-grid) event history aggregated by the selected
181+
period.
182+
</dd>
183+
<dt>Telemetry (Charging)</dt>
184+
<dd>
185+
Returns wall connector charging history. Period is not used for this
186+
type.
187+
</dd>
188+
</dl>
189+
190+
<h3>Inputs</h3>
191+
<dl class="message-properties">
192+
<dt class="optional">
193+
siteId <span class="property-type">number</span>
194+
</dt>
195+
<dd>Energy Site ID (if not set in config)</dd>
196+
<dt class="optional">
197+
historyType <span class="property-type">string</span>
198+
</dt>
199+
<dd>Type of history: "energy", "backup", or "telemetry"</dd>
200+
<dt class="optional">
201+
period <span class="property-type">string</span>
202+
</dt>
203+
<dd>Aggregation period: "day", "week", "month", or "year"</dd>
204+
<dt class="optional">
205+
startDate <span class="property-type">string</span>
206+
</dt>
207+
<dd>Start date in ISO 8601 format (defaults to start of today)</dd>
208+
<dt class="optional">
209+
endDate <span class="property-type">string</span>
210+
</dt>
211+
<dd>End date in ISO 8601 format (defaults to end of today)</dd>
212+
<dt class="optional">
213+
timeZone <span class="property-type">string</span>
214+
</dt>
215+
<dd>IANA time zone identifier (e.g., "America/Los_Angeles")</dd>
216+
</dl>
217+
218+
<h3>Outputs</h3>
219+
<dl class="message-properties">
220+
<dt>payload <span class="property-type">object</span></dt>
221+
<dd>The historical data from the API.</dd>
222+
</dl>
223+
224+
<h3>References</h3>
225+
<ul>
226+
<li>
227+
<a href="https://teslemetry.com/docs">Teslemetry API Documentation</a>
228+
</li>
229+
</ul>
230+
</script>
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { Node, NodeAPI, NodeDef } from "node-red";
2+
import { Teslemetry } from "@teslemetry/api";
3+
import { instances } from "../shared";
4+
import { validateParameters } from "../validation";
5+
import { Msg } from "../types";
6+
7+
export interface TeslemetryEnergyHistoryNodeDef extends NodeDef {
8+
teslemetryConfig: string;
9+
siteId: string;
10+
historyType: string;
11+
period: string;
12+
startDate: string;
13+
endDate: string;
14+
timeZone: string;
15+
}
16+
17+
export interface TeslemetryEnergyHistoryNode extends Node {
18+
teslemetry?: Teslemetry;
19+
siteId: string;
20+
historyType: string;
21+
period: string;
22+
startDate: string;
23+
endDate: string;
24+
timeZone: string;
25+
}
26+
27+
export default function (RED: NodeAPI) {
28+
function TeslemetryEnergyHistoryNode(
29+
this: TeslemetryEnergyHistoryNode,
30+
config: TeslemetryEnergyHistoryNodeDef,
31+
) {
32+
RED.nodes.createNode(this, config);
33+
const node = this;
34+
35+
node.teslemetry = instances.get(config.teslemetryConfig)?.teslemetry;
36+
node.siteId = config.siteId;
37+
node.historyType = config.historyType;
38+
node.period = config.period;
39+
node.startDate = config.startDate;
40+
node.endDate = config.endDate;
41+
node.timeZone = config.timeZone;
42+
43+
if (!node.teslemetry) {
44+
node.status({ fill: "red", shape: "ring", text: "Config missing" });
45+
node.error("No Teslemetry configuration found");
46+
return;
47+
} else node.status({});
48+
49+
node.on("input", async function (msg: Msg, send, done) {
50+
const siteId: string = node.siteId || (msg.siteId as string) || "";
51+
const historyType: string =
52+
node.historyType || (msg.historyType as string) || "energy";
53+
const period: string =
54+
node.period || (msg.period as string) || "day";
55+
const startDate: string | undefined =
56+
node.startDate || (msg.startDate as string) || undefined;
57+
const endDate: string | undefined =
58+
node.endDate || (msg.endDate as string) || undefined;
59+
const timeZone: string | undefined =
60+
node.timeZone || (msg.timeZone as string) || undefined;
61+
62+
const site = node.teslemetry!.api.getEnergySite(Number(siteId));
63+
64+
try {
65+
if (!siteId) {
66+
throw new Error("No Energy Site ID provided");
67+
}
68+
69+
validateParameters(
70+
{ historyType, period },
71+
{
72+
historyType: {
73+
required: true,
74+
type: "string",
75+
allowedValues: ["energy", "backup", "telemetry"],
76+
},
77+
period: {
78+
required: historyType !== "telemetry",
79+
type: "string",
80+
allowedValues: ["day", "week", "month", "year"],
81+
},
82+
},
83+
);
84+
85+
node.status({ fill: "blue", shape: "dot", text: "fetching history" });
86+
87+
let result: { response?: any } | undefined;
88+
89+
switch (historyType) {
90+
case "energy":
91+
result = await site.getCalendarHistory(
92+
"energy",
93+
period as "day" | "week" | "month" | "year",
94+
startDate,
95+
endDate,
96+
timeZone,
97+
);
98+
break;
99+
case "backup":
100+
result = await site.getCalendarHistory(
101+
"backup",
102+
period as "day" | "week" | "month" | "year",
103+
startDate,
104+
endDate,
105+
timeZone,
106+
);
107+
break;
108+
case "telemetry":
109+
result = await site.getTelemetryHistory(
110+
startDate,
111+
endDate,
112+
timeZone,
113+
);
114+
break;
115+
default:
116+
throw new Error(`Unknown history type: ${historyType}`);
117+
}
118+
119+
msg.payload = result?.response;
120+
node.status({});
121+
send(msg);
122+
done();
123+
} catch (err: any) {
124+
node.status({ fill: "red", shape: "ring", text: err.message });
125+
node.error(err.message || "Teslemetry API Error", msg);
126+
done();
127+
}
128+
});
129+
}
130+
RED.nodes.registerType(
131+
"teslemetry-energy-history",
132+
TeslemetryEnergyHistoryNode,
133+
);
134+
}

0 commit comments

Comments
 (0)