gwatt 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +36 -0
- package/src/api.ts +233 -0
- package/src/config.ts +39 -0
- package/src/index.ts +406 -0
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gwatt",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Agent-first CLI for Growatt solar monitoring — JSON output, device filtering, hourly history",
|
|
5
|
+
"author": "",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"growatt",
|
|
9
|
+
"solar",
|
|
10
|
+
"cli",
|
|
11
|
+
"energy",
|
|
12
|
+
"inverter"
|
|
13
|
+
],
|
|
14
|
+
"module": "src/index.ts",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"bin": {
|
|
17
|
+
"gwat": "src/index.ts"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"src/**/*",
|
|
21
|
+
"package.json",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"bun": ">=1.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/bun": "latest"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"typescript": "^5"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"arg": "^5.0.2"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import { type Config } from "./config";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_SERVER = "https://mqtt.growatt.com";
|
|
5
|
+
|
|
6
|
+
const HEADERS = {
|
|
7
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
8
|
+
"Accept": "application/json, text/javascript, */*; q=0.01",
|
|
9
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
10
|
+
"Referer": "https://mqtt.growatt.com/",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function hashPassword(password: string): string {
|
|
14
|
+
let md5 = createHash("md5").update(password).digest("hex");
|
|
15
|
+
for (let i = 0; i < md5.length; i += 2) {
|
|
16
|
+
if (md5[i] === "0") {
|
|
17
|
+
md5 = md5.slice(0, i) + "c" + md5.slice(i + 1);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return md5;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeRequest(
|
|
24
|
+
url: string,
|
|
25
|
+
opts: { method?: string; body?: Record<string, string>; headers?: Record<string, string> } = {},
|
|
26
|
+
cookies?: string
|
|
27
|
+
) {
|
|
28
|
+
const headers = { ...HEADERS, ...opts.headers };
|
|
29
|
+
if (cookies) {
|
|
30
|
+
headers["Cookie"] = cookies;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const body = opts.body ? new URLSearchParams(opts.body) : undefined;
|
|
34
|
+
return fetch(url, {
|
|
35
|
+
method: opts.method || "GET",
|
|
36
|
+
headers,
|
|
37
|
+
body: body ? body.toString() : undefined,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function login(username: string, password: string, serverUrl = DEFAULT_SERVER) {
|
|
42
|
+
const url = `${serverUrl}/newTwoLoginAPI.do`;
|
|
43
|
+
const hashed = hashPassword(password);
|
|
44
|
+
|
|
45
|
+
const resp = await makeRequest(url, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
body: { userName: username, password: hashed },
|
|
48
|
+
headers: {
|
|
49
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const data = (await resp.json()) as { back?: { success: boolean; msg?: string; data?: any[]; user?: any; deviceCount?: string } };
|
|
54
|
+
|
|
55
|
+
if (!data.back || !data.back.success) {
|
|
56
|
+
throw new Error(`Login failed: ${data.back?.msg || "Unknown error"}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const cookies = (resp.headers as any).getSetCookie?.() as string[] | undefined;
|
|
60
|
+
const cookieStr = cookies ? cookies.join("; ") : resp.headers.get("set-cookie") || "";
|
|
61
|
+
const token = data.back.user?.token as string | undefined;
|
|
62
|
+
const userId = data.back.user?.id as number | undefined;
|
|
63
|
+
const plant = data.back.data?.[0];
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
cookies: cookieStr,
|
|
67
|
+
token,
|
|
68
|
+
userId,
|
|
69
|
+
plantId: plant?.plantId as string | undefined,
|
|
70
|
+
plantName: plant?.plantName as string | undefined,
|
|
71
|
+
deviceCount: data.back.deviceCount as string | undefined,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function getPlantList(config: Config) {
|
|
76
|
+
const url = `${config.serverUrl || DEFAULT_SERVER}/PlantListAPI.do?userId=${config.userId}`;
|
|
77
|
+
|
|
78
|
+
const resp = await makeRequest(url, {}, config.cookies);
|
|
79
|
+
const data = (await resp.json()) as {
|
|
80
|
+
back?: {
|
|
81
|
+
success: boolean;
|
|
82
|
+
totalData?: any;
|
|
83
|
+
data?: any[];
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (!data.back || !data.back.success) {
|
|
88
|
+
throw new Error("Failed to fetch plant list");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
plants: data.back.data || [],
|
|
93
|
+
total: data.back.totalData,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function getDevices(config: Config) {
|
|
98
|
+
const url = `${config.serverUrl || DEFAULT_SERVER}/newTwoPlantAPI.do?op=getAllDeviceList&plantId=${config.plantId}&language=1`;
|
|
99
|
+
|
|
100
|
+
const resp = await makeRequest(url, {}, config.cookies);
|
|
101
|
+
const data = (await resp.json()) as any;
|
|
102
|
+
|
|
103
|
+
const devices: Array<{
|
|
104
|
+
name: string;
|
|
105
|
+
serialNumber: string;
|
|
106
|
+
type: string;
|
|
107
|
+
status: number;
|
|
108
|
+
eToday: number;
|
|
109
|
+
eTotal: number;
|
|
110
|
+
currentPower: number;
|
|
111
|
+
powerStr: string;
|
|
112
|
+
eTodayStr: string;
|
|
113
|
+
raw: any;
|
|
114
|
+
}> = [];
|
|
115
|
+
|
|
116
|
+
const list = data?.deviceList;
|
|
117
|
+
if (Array.isArray(list)) {
|
|
118
|
+
for (const item of list) {
|
|
119
|
+
const eToday = parseFloat(item.eToday ?? item.etoday ?? "0");
|
|
120
|
+
const eTotal = parseFloat(item.energy ?? item.etotal ?? item.eTotal ?? "0");
|
|
121
|
+
const currentPower = parseFloat(item.power ?? item.currentPac ?? "0");
|
|
122
|
+
const deviceStatus = item.deviceStatus ?? item.status ?? 0;
|
|
123
|
+
|
|
124
|
+
devices.push({
|
|
125
|
+
name: item.deviceAilas || item.deviceAlias || item.alias || item.deviceName || item.deviceSn || "Unknown",
|
|
126
|
+
serialNumber: item.deviceSn || item.sn || "",
|
|
127
|
+
type: item.deviceType || item.type || "unknown",
|
|
128
|
+
status: deviceStatus,
|
|
129
|
+
eToday,
|
|
130
|
+
eTotal,
|
|
131
|
+
currentPower,
|
|
132
|
+
powerStr: item.powerStr || `${(currentPower / 1000).toFixed(2)}kW`,
|
|
133
|
+
eTodayStr: item.eTodayStr || `${eToday.toFixed(1)}kWh`,
|
|
134
|
+
raw: item,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return devices;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function getHistory(config: Config, type: number, date: string) {
|
|
143
|
+
const url = `${config.serverUrl || DEFAULT_SERVER}/PlantDetailAPI.do?plantId=${config.plantId}&type=${type}&date=${date}`;
|
|
144
|
+
|
|
145
|
+
const resp = await makeRequest(url, {}, config.cookies);
|
|
146
|
+
const data = (await resp.json()) as {
|
|
147
|
+
back?: {
|
|
148
|
+
success: boolean;
|
|
149
|
+
plantData?: any;
|
|
150
|
+
data?: Record<string, string>;
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
if (!data.back || !data.back.success) {
|
|
155
|
+
throw new Error("Failed to fetch history data");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const entries = Object.entries(data.back.data || {});
|
|
159
|
+
|
|
160
|
+
// For type=1 (day), values are Watts at each time point; convert to kWh for display
|
|
161
|
+
const isDayView = type === 1;
|
|
162
|
+
|
|
163
|
+
const points = entries.map(([key, value]) => ({
|
|
164
|
+
key,
|
|
165
|
+
value: isDayView ? Math.round((parseFloat(value) / 1000) * 1000) / 1000 : parseFloat(value),
|
|
166
|
+
unit: isDayView ? "kWh" : "kWh",
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
const total = Math.round(points.reduce((sum, p) => sum + p.value, 0) * 1000) / 1000;
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
plantData: data.back.plantData,
|
|
173
|
+
points,
|
|
174
|
+
total,
|
|
175
|
+
isDayView,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function getTodayData(config: Config) {
|
|
180
|
+
const url = `${config.serverUrl || DEFAULT_SERVER}/newTwoPlantAPI.do?op=getUserCenterEnertyDataByPlantid`;
|
|
181
|
+
|
|
182
|
+
const resp = await makeRequest(
|
|
183
|
+
url,
|
|
184
|
+
{
|
|
185
|
+
method: "POST",
|
|
186
|
+
body: {
|
|
187
|
+
language: "1",
|
|
188
|
+
plantId: config.plantId || "",
|
|
189
|
+
},
|
|
190
|
+
headers: {
|
|
191
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
config.cookies
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const data = (await resp.json()) as {
|
|
198
|
+
yearStr?: string;
|
|
199
|
+
weatherMap?: { tmp?: string; cond_txt?: string; cond_code?: string };
|
|
200
|
+
powerValueStr?: string;
|
|
201
|
+
alarmValue?: number;
|
|
202
|
+
plantBean?: {
|
|
203
|
+
plantName?: string;
|
|
204
|
+
nominalPower?: number;
|
|
205
|
+
eToday?: number;
|
|
206
|
+
eTotal?: number;
|
|
207
|
+
formulaMoney?: number;
|
|
208
|
+
formulaMoneyUnitId?: string;
|
|
209
|
+
hasDeviceOnLine?: number;
|
|
210
|
+
hasStorage?: number;
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
plantName: data.plantBean?.plantName || config.plantName,
|
|
216
|
+
today: data.plantBean?.eToday ?? 0,
|
|
217
|
+
total: data.plantBean?.eTotal ?? 0,
|
|
218
|
+
nominalPower: data.plantBean?.nominalPower ?? 0,
|
|
219
|
+
currency: data.plantBean?.formulaMoneyUnitId || "",
|
|
220
|
+
revenuePerKwh: data.plantBean?.formulaMoney ?? 0,
|
|
221
|
+
todayRevenue: data.plantBean?.eToday && data.plantBean?.formulaMoney
|
|
222
|
+
? data.plantBean.eToday * data.plantBean.formulaMoney
|
|
223
|
+
: 0,
|
|
224
|
+
totalRevenue: data.plantBean?.eTotal && data.plantBean?.formulaMoney
|
|
225
|
+
? data.plantBean.eTotal * data.plantBean.formulaMoney
|
|
226
|
+
: 0,
|
|
227
|
+
weather: data.weatherMap?.cond_txt,
|
|
228
|
+
temperature: data.weatherMap?.tmp,
|
|
229
|
+
alarms: data.alarmValue ?? 0,
|
|
230
|
+
devicesOnline: data.plantBean?.hasDeviceOnLine ?? 0,
|
|
231
|
+
hasStorage: data.plantBean?.hasStorage ?? 0,
|
|
232
|
+
};
|
|
233
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = join(homedir(), ".gwat");
|
|
6
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
7
|
+
|
|
8
|
+
export interface Config {
|
|
9
|
+
userId?: number;
|
|
10
|
+
token?: string;
|
|
11
|
+
cookies?: string;
|
|
12
|
+
serverUrl?: string;
|
|
13
|
+
plantId?: string;
|
|
14
|
+
plantName?: string;
|
|
15
|
+
username?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function readConfig(): Config {
|
|
19
|
+
try {
|
|
20
|
+
const data = readFileSync(CONFIG_FILE, "utf-8");
|
|
21
|
+
return JSON.parse(data) as Config;
|
|
22
|
+
} catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function writeConfig(config: Config) {
|
|
28
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
29
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function hasConfig(): boolean {
|
|
33
|
+
try {
|
|
34
|
+
readFileSync(CONFIG_FILE, "utf-8");
|
|
35
|
+
return true;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import arg from "arg";
|
|
3
|
+
import { readConfig, writeConfig, hasConfig } from "./config";
|
|
4
|
+
import { login, getPlantList, getTodayData, getDevices, getHistory } from "./api";
|
|
5
|
+
|
|
6
|
+
const help = `Usage: gwat <command> [options]
|
|
7
|
+
|
|
8
|
+
Commands:
|
|
9
|
+
login Authenticate with Growatt
|
|
10
|
+
today Current day energy and device breakdown
|
|
11
|
+
history Energy data for day, month, year, or lifetime
|
|
12
|
+
status List all plants and totals
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
-u, --username Growatt username
|
|
16
|
+
-p, --password Growatt password
|
|
17
|
+
-s, --server Server URL (default: https://mqtt.growatt.com)
|
|
18
|
+
-d, --day Day to view (YYYY-MM-DD)
|
|
19
|
+
-m, --month Month to view (YYYY-MM)
|
|
20
|
+
-y, --year Year to view (YYYY)
|
|
21
|
+
-t, --total Show lifetime totals
|
|
22
|
+
-n, --device Filter to a specific device name
|
|
23
|
+
-j, --json Output JSON (default: plain text)
|
|
24
|
+
-h, --help Show this help
|
|
25
|
+
-v, --version Show version
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
gwat login -u alice -p secret
|
|
29
|
+
gwat today -j
|
|
30
|
+
gwat today -n Abdulaziz
|
|
31
|
+
gwat history -m 2026-06 -j
|
|
32
|
+
gwat history -t -j
|
|
33
|
+
gwat status -j
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
const args = arg({
|
|
37
|
+
"--username": String,
|
|
38
|
+
"-u": "--username",
|
|
39
|
+
"--password": String,
|
|
40
|
+
"-p": "--password",
|
|
41
|
+
"--server": String,
|
|
42
|
+
"-s": "--server",
|
|
43
|
+
"--day": String,
|
|
44
|
+
"-d": "--day",
|
|
45
|
+
"--month": String,
|
|
46
|
+
"-m": "--month",
|
|
47
|
+
"--year": String,
|
|
48
|
+
"-y": "--year",
|
|
49
|
+
"--total": Boolean,
|
|
50
|
+
"-t": "--total",
|
|
51
|
+
"--device": String,
|
|
52
|
+
"-n": "--device",
|
|
53
|
+
"--json": Boolean,
|
|
54
|
+
"-j": "--json",
|
|
55
|
+
"--help": Boolean,
|
|
56
|
+
"-h": "--help",
|
|
57
|
+
"--version": Boolean,
|
|
58
|
+
"-v": "--version",
|
|
59
|
+
}, { permissive: true });
|
|
60
|
+
|
|
61
|
+
const [cmd] = args._;
|
|
62
|
+
const isJson = args["--json"] ?? false;
|
|
63
|
+
|
|
64
|
+
if (args["--help"] || (!cmd && !args["--version"])) {
|
|
65
|
+
console.log(help);
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (args["--version"]) {
|
|
70
|
+
console.log(isJson ? JSON.stringify({ version: "0.0.1" }) : "gwat 0.0.1");
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function out(data: unknown) {
|
|
75
|
+
if (isJson) {
|
|
76
|
+
console.log(JSON.stringify(data, null, 2));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function fail(err: { error: string; details?: string }) {
|
|
81
|
+
if (isJson) {
|
|
82
|
+
console.log(JSON.stringify(err, null, 2));
|
|
83
|
+
} else {
|
|
84
|
+
console.error(`Error: ${err.error}${err.details ? " - " + err.details : ""}`);
|
|
85
|
+
}
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function run() {
|
|
90
|
+
switch (cmd) {
|
|
91
|
+
case "login":
|
|
92
|
+
await handleLogin();
|
|
93
|
+
break;
|
|
94
|
+
case "today":
|
|
95
|
+
await handleToday();
|
|
96
|
+
break;
|
|
97
|
+
case "history":
|
|
98
|
+
await handleHistory();
|
|
99
|
+
break;
|
|
100
|
+
case "status":
|
|
101
|
+
await handleStatus();
|
|
102
|
+
break;
|
|
103
|
+
default:
|
|
104
|
+
fail({ error: "Unknown command", details: cmd });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function handleLogin() {
|
|
109
|
+
const username = args["--username"];
|
|
110
|
+
const password = args["--password"];
|
|
111
|
+
const server = args["--server"];
|
|
112
|
+
|
|
113
|
+
if (!username || !password) {
|
|
114
|
+
fail({ error: "Missing credentials", details: "--username and --password are required" });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const result = await login(username, password, server);
|
|
118
|
+
|
|
119
|
+
writeConfig({
|
|
120
|
+
username,
|
|
121
|
+
cookies: result.cookies,
|
|
122
|
+
token: result.token,
|
|
123
|
+
userId: result.userId,
|
|
124
|
+
plantId: result.plantId,
|
|
125
|
+
plantName: result.plantName,
|
|
126
|
+
serverUrl: server || "https://mqtt.growatt.com",
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
out({
|
|
130
|
+
success: true,
|
|
131
|
+
username,
|
|
132
|
+
plantId: result.plantId,
|
|
133
|
+
plantName: result.plantName,
|
|
134
|
+
deviceCount: result.deviceCount,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (!isJson) {
|
|
138
|
+
console.log(`Logged in as ${username}`);
|
|
139
|
+
console.log(`Plant: ${result.plantName || "(unknown)"}`);
|
|
140
|
+
if (result.deviceCount) {
|
|
141
|
+
console.log(`Devices: ${result.deviceCount}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function handleToday() {
|
|
147
|
+
if (!hasConfig()) {
|
|
148
|
+
fail({ error: "Not logged in", details: "Run: gwat login -u <username> -p <password>" });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const config = readConfig();
|
|
152
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
153
|
+
const deviceFilter = args["--device"];
|
|
154
|
+
|
|
155
|
+
const [data, { plants }, devices, history] = await Promise.all([
|
|
156
|
+
getTodayData(config),
|
|
157
|
+
getPlantList(config),
|
|
158
|
+
getDevices(config),
|
|
159
|
+
getHistory(config, 1, today),
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
const plant = plants.find((p: any) => p.plantId === config.plantId);
|
|
163
|
+
const todayEnergyStr = plant?.todayEnergy as string | undefined;
|
|
164
|
+
const todayEnergy = todayEnergyStr
|
|
165
|
+
? parseFloat(todayEnergyStr.replace(/[^0-9.]/g, ""))
|
|
166
|
+
: data.today;
|
|
167
|
+
|
|
168
|
+
const todayRevenue = todayEnergy && data.revenuePerKwh
|
|
169
|
+
? todayEnergy * data.revenuePerKwh
|
|
170
|
+
: 0;
|
|
171
|
+
|
|
172
|
+
const sortedHours = history.points.slice().sort((a, b) => a.key.localeCompare(b.key));
|
|
173
|
+
|
|
174
|
+
let filteredDevices = devices;
|
|
175
|
+
if (deviceFilter) {
|
|
176
|
+
const f = deviceFilter.toLowerCase();
|
|
177
|
+
filteredDevices = devices.filter((d) => d.name.toLowerCase().includes(f));
|
|
178
|
+
if (filteredDevices.length === 0) {
|
|
179
|
+
fail({ error: "Device not found", details: `No device matching "${deviceFilter}"` });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const isSingleDevice = deviceFilter && filteredDevices.length === 1;
|
|
184
|
+
|
|
185
|
+
if (isSingleDevice) {
|
|
186
|
+
const d = filteredDevices[0];
|
|
187
|
+
const deviceRevenue = d.eToday && data.revenuePerKwh ? d.eToday * data.revenuePerKwh : 0;
|
|
188
|
+
const status = d.status === 1 || d.status === 2 ? "Online" : "Offline";
|
|
189
|
+
|
|
190
|
+
const result = {
|
|
191
|
+
device: {
|
|
192
|
+
name: d.name,
|
|
193
|
+
serial: d.serialNumber,
|
|
194
|
+
type: d.type,
|
|
195
|
+
status,
|
|
196
|
+
todayEnergy: d.eToday,
|
|
197
|
+
power: d.currentPower,
|
|
198
|
+
powerStr: d.powerStr,
|
|
199
|
+
currency: data.currency,
|
|
200
|
+
revenuePerKwh: data.revenuePerKwh,
|
|
201
|
+
todayRevenue: deviceRevenue,
|
|
202
|
+
},
|
|
203
|
+
hours: sortedHours.map((p) => ({
|
|
204
|
+
time: p.key,
|
|
205
|
+
energy: p.value,
|
|
206
|
+
unit: "kWh",
|
|
207
|
+
})),
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
out(result);
|
|
211
|
+
|
|
212
|
+
if (!isJson) {
|
|
213
|
+
console.log(`Device: ${result.device.name}`);
|
|
214
|
+
console.log(`Today: ${result.device.todayEnergy.toFixed(1)} kWh`);
|
|
215
|
+
console.log(`Power: ${result.device.powerStr}`);
|
|
216
|
+
console.log(`Status: ${result.device.status}`);
|
|
217
|
+
if (result.device.currency && result.device.revenuePerKwh) {
|
|
218
|
+
console.log(`Today: ${result.device.todayRevenue.toFixed(0)} ${result.device.currency}`);
|
|
219
|
+
}
|
|
220
|
+
console.log(`
|
|
221
|
+
Hours (${result.hours.length}):`);
|
|
222
|
+
for (const h of result.hours) {
|
|
223
|
+
console.log(` ${h.time.padStart(5)} ${h.energy.toFixed(1).padStart(8)} kWh`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const result = {
|
|
230
|
+
plant: {
|
|
231
|
+
id: config.plantId,
|
|
232
|
+
name: data.plantName,
|
|
233
|
+
todayEnergy: todayEnergy,
|
|
234
|
+
nominalPower: data.nominalPower,
|
|
235
|
+
capacityKw: data.nominalPower / 1000,
|
|
236
|
+
currency: data.currency,
|
|
237
|
+
revenuePerKwh: data.revenuePerKwh,
|
|
238
|
+
todayRevenue,
|
|
239
|
+
weather: data.weather,
|
|
240
|
+
temperature: data.temperature,
|
|
241
|
+
alarms: data.alarms,
|
|
242
|
+
devicesOnline: data.devicesOnline,
|
|
243
|
+
hasStorage: data.hasStorage,
|
|
244
|
+
},
|
|
245
|
+
devices: filteredDevices.map((d) => ({
|
|
246
|
+
name: d.name,
|
|
247
|
+
serial: d.serialNumber,
|
|
248
|
+
type: d.type,
|
|
249
|
+
status: d.status,
|
|
250
|
+
eToday: d.eToday,
|
|
251
|
+
currentPower: d.currentPower,
|
|
252
|
+
powerStr: d.powerStr,
|
|
253
|
+
eTodayStr: d.eTodayStr,
|
|
254
|
+
})),
|
|
255
|
+
hours: sortedHours.map((p) => ({
|
|
256
|
+
time: p.key,
|
|
257
|
+
energy: p.value,
|
|
258
|
+
unit: "kWh",
|
|
259
|
+
})),
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
out(result);
|
|
263
|
+
|
|
264
|
+
if (!isJson) {
|
|
265
|
+
console.log(`Plant: ${result.plant.name}`);
|
|
266
|
+
console.log(`Today: ${result.plant.todayEnergy.toFixed(1)} kWh`);
|
|
267
|
+
console.log(`Capacity: ${result.plant.capacityKw.toFixed(1)} kW`);
|
|
268
|
+
if (result.plant.currency) {
|
|
269
|
+
console.log(`Today: ${result.plant.todayRevenue.toFixed(0)} ${result.plant.currency}`);
|
|
270
|
+
}
|
|
271
|
+
if (result.plant.weather) {
|
|
272
|
+
console.log(`Weather: ${result.plant.weather}, ${result.plant.temperature}C`);
|
|
273
|
+
}
|
|
274
|
+
console.log(`Devices: ${result.plant.devicesOnline === 0 ? "Offline" : "Online"}`);
|
|
275
|
+
console.log(`
|
|
276
|
+
Devices (${result.devices.length}):`);
|
|
277
|
+
for (const d of result.devices) {
|
|
278
|
+
console.log(` ${d.name}: ${d.eToday.toFixed(1)} kWh (${d.powerStr})`);
|
|
279
|
+
}
|
|
280
|
+
console.log(`
|
|
281
|
+
Hours (${result.hours.length}):`);
|
|
282
|
+
for (const h of result.hours) {
|
|
283
|
+
console.log(` ${h.time.padStart(5)} ${h.energy.toFixed(1).padStart(8)} kWh`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function handleHistory() {
|
|
289
|
+
if (!hasConfig()) {
|
|
290
|
+
fail({ error: "Not logged in", details: "Run: gwat login -u <username> -p <password>" });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const day = args["--day"];
|
|
294
|
+
const month = args["--month"];
|
|
295
|
+
const year = args["--year"];
|
|
296
|
+
const total = args["--total"];
|
|
297
|
+
|
|
298
|
+
if (!day && !month && !year && !total) {
|
|
299
|
+
fail({ error: "Missing range", details: "Specify --day, --month, --year, or --total" });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
let type: number;
|
|
303
|
+
let date: string;
|
|
304
|
+
let range: string;
|
|
305
|
+
|
|
306
|
+
if (day) {
|
|
307
|
+
type = 1;
|
|
308
|
+
date = day;
|
|
309
|
+
range = "day";
|
|
310
|
+
} else if (month) {
|
|
311
|
+
type = 2;
|
|
312
|
+
date = month;
|
|
313
|
+
range = "month";
|
|
314
|
+
} else if (total) {
|
|
315
|
+
type = 4;
|
|
316
|
+
date = "";
|
|
317
|
+
range = "total";
|
|
318
|
+
} else {
|
|
319
|
+
type = 3;
|
|
320
|
+
date = year!;
|
|
321
|
+
range = "year";
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const config = readConfig();
|
|
325
|
+
const result = await getHistory(config, type, date);
|
|
326
|
+
|
|
327
|
+
const sorted = result.points.slice().sort((a, b) => a.key.localeCompare(b.key));
|
|
328
|
+
|
|
329
|
+
const payload = {
|
|
330
|
+
range,
|
|
331
|
+
date: date || undefined,
|
|
332
|
+
plantId: config.plantId,
|
|
333
|
+
plantName: result.plantData?.plantName || config.plantName,
|
|
334
|
+
totalEnergy: result.total,
|
|
335
|
+
data: sorted.map((p) => ({
|
|
336
|
+
label: p.key,
|
|
337
|
+
value: p.value,
|
|
338
|
+
unit: "kWh",
|
|
339
|
+
})),
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
out(payload);
|
|
343
|
+
|
|
344
|
+
if (!isJson) {
|
|
345
|
+
console.log(`Plant: ${payload.plantName}`);
|
|
346
|
+
console.log(`Range: ${range}${date ? " " + date : ""}`);
|
|
347
|
+
console.log(`Total: ${payload.totalEnergy.toFixed(1)} kWh`);
|
|
348
|
+
if (sorted.length === 0) {
|
|
349
|
+
console.log("No data available.");
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
console.log();
|
|
353
|
+
for (const p of sorted) {
|
|
354
|
+
console.log(` ${p.key.padStart(8)} ${p.value.toFixed(1).padStart(8)} kWh`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function handleStatus() {
|
|
360
|
+
if (!hasConfig()) {
|
|
361
|
+
fail({ error: "Not logged in", details: "Run: gwat login -u <username> -p <password>" });
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const config = readConfig();
|
|
365
|
+
const { plants, total } = await getPlantList(config);
|
|
366
|
+
|
|
367
|
+
const payload = {
|
|
368
|
+
plants: plants.map((p: any) => ({
|
|
369
|
+
id: p.plantId,
|
|
370
|
+
name: p.plantName,
|
|
371
|
+
todayEnergy: p.todayEnergy,
|
|
372
|
+
totalEnergy: p.totalEnergy,
|
|
373
|
+
currentPower: p.currentPower,
|
|
374
|
+
isHaveStorage: p.isHaveStorage,
|
|
375
|
+
})),
|
|
376
|
+
total: total ? {
|
|
377
|
+
todayEnergySum: total.todayEnergySum,
|
|
378
|
+
totalEnergySum: total.totalEnergySum,
|
|
379
|
+
currentPowerSum: total.currentPowerSum,
|
|
380
|
+
co2Sum: total.CO2Sum,
|
|
381
|
+
} : undefined,
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
out(payload);
|
|
385
|
+
|
|
386
|
+
if (!isJson) {
|
|
387
|
+
for (const p of payload.plants) {
|
|
388
|
+
console.log(`${p.name}`);
|
|
389
|
+
console.log(` Today: ${p.todayEnergy || "N/A"}`);
|
|
390
|
+
console.log(` Total: ${p.totalEnergy || "N/A"}`);
|
|
391
|
+
console.log(` Power: ${p.currentPower || "N/A"}`);
|
|
392
|
+
console.log(` ID: ${p.id}`);
|
|
393
|
+
}
|
|
394
|
+
if (payload.total) {
|
|
395
|
+
console.log(`\nCombined`);
|
|
396
|
+
console.log(` Today: ${payload.total.todayEnergySum || "N/A"}`);
|
|
397
|
+
console.log(` Total: ${payload.total.totalEnergySum || "N/A"}`);
|
|
398
|
+
console.log(` Power: ${payload.total.currentPowerSum || "N/A"}`);
|
|
399
|
+
console.log(` CO2: ${payload.total.co2Sum || "N/A"}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
run().catch((err) => {
|
|
405
|
+
fail({ error: err.message });
|
|
406
|
+
});
|