@enerlence/suntropy-cli 0.1.0
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/dist/bin/suntropy.js +2926 -0
- package/dist/bin/suntropy.js.map +1 -0
- package/package.json +35 -0
- package/skills/inventory-create-kit.md +245 -0
- package/skills/inventory-create.md +210 -0
- package/skills/solar-study.md +337 -0
|
@@ -0,0 +1,2926 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// src/index.ts
|
|
10
|
+
import { Command as Command2 } from "commander";
|
|
11
|
+
|
|
12
|
+
// src/config.ts
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
import { join } from "path";
|
|
16
|
+
var CONFIG_DIR = join(homedir(), ".suntropy");
|
|
17
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
18
|
+
var DEFAULT_CONFIG = {
|
|
19
|
+
activeProfile: "default",
|
|
20
|
+
profiles: {
|
|
21
|
+
default: {
|
|
22
|
+
server: "https://api.enerlence.com"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
function ensureConfigDir() {
|
|
27
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
28
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function loadConfig() {
|
|
32
|
+
ensureConfigDir();
|
|
33
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
34
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(DEFAULT_CONFIG, null, 2));
|
|
35
|
+
return { ...DEFAULT_CONFIG };
|
|
36
|
+
}
|
|
37
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
38
|
+
}
|
|
39
|
+
function saveConfig(config) {
|
|
40
|
+
ensureConfigDir();
|
|
41
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
42
|
+
}
|
|
43
|
+
function getActiveProfile(config, profileOverride) {
|
|
44
|
+
const name = profileOverride || config.activeProfile;
|
|
45
|
+
const profile = config.profiles[name];
|
|
46
|
+
if (!profile) {
|
|
47
|
+
throw new Error(`Profile "${name}" not found. Available: ${Object.keys(config.profiles).join(", ")}`);
|
|
48
|
+
}
|
|
49
|
+
return profile;
|
|
50
|
+
}
|
|
51
|
+
function setConfigValue(key, value, profileName) {
|
|
52
|
+
const config = loadConfig();
|
|
53
|
+
const name = profileName || config.activeProfile;
|
|
54
|
+
if (!config.profiles[name]) {
|
|
55
|
+
config.profiles[name] = { server: "https://api.enerlence.com" };
|
|
56
|
+
}
|
|
57
|
+
const profile = config.profiles[name];
|
|
58
|
+
profile[key] = value;
|
|
59
|
+
saveConfig(config);
|
|
60
|
+
}
|
|
61
|
+
function getConfigValue(key, profileName) {
|
|
62
|
+
const config = loadConfig();
|
|
63
|
+
const profile = getActiveProfile(config, profileName);
|
|
64
|
+
return profile[key];
|
|
65
|
+
}
|
|
66
|
+
var SERVICE_PATHS = {
|
|
67
|
+
security: "/security",
|
|
68
|
+
solar: "/solar",
|
|
69
|
+
templates: "/templates",
|
|
70
|
+
profiles: "/profiles",
|
|
71
|
+
periods: "/periods"
|
|
72
|
+
};
|
|
73
|
+
var LOCAL_PORTS = {
|
|
74
|
+
security: 8080,
|
|
75
|
+
solar: 8086,
|
|
76
|
+
templates: 8090,
|
|
77
|
+
profiles: 8085,
|
|
78
|
+
periods: 8084
|
|
79
|
+
};
|
|
80
|
+
function getServiceUrl(baseServer, service) {
|
|
81
|
+
if (baseServer.includes("localhost") || baseServer.match(/:\d+$/)) {
|
|
82
|
+
const port = LOCAL_PORTS[service];
|
|
83
|
+
const host = baseServer.replace(/:\d+$/, "").replace(/\/$/, "");
|
|
84
|
+
return `${host}:${port}`;
|
|
85
|
+
}
|
|
86
|
+
return `${baseServer.replace(/\/$/, "")}${SERVICE_PATHS[service]}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/client.ts
|
|
90
|
+
import axios from "axios";
|
|
91
|
+
function resolveAuth(opts) {
|
|
92
|
+
const config = loadConfig();
|
|
93
|
+
const profile = getActiveProfile(config, opts.profile);
|
|
94
|
+
const server = opts.server || profile.server;
|
|
95
|
+
const token = opts.token || profile.token;
|
|
96
|
+
if (!token) {
|
|
97
|
+
throw new Error("Not authenticated. Run: suntropy auth set-key --key <jwt> or suntropy auth login");
|
|
98
|
+
}
|
|
99
|
+
return { server, token };
|
|
100
|
+
}
|
|
101
|
+
function createServiceClient(service, opts) {
|
|
102
|
+
const { server, token } = resolveAuth(opts);
|
|
103
|
+
const baseURL = getServiceUrl(server, service);
|
|
104
|
+
const client = axios.create({
|
|
105
|
+
baseURL,
|
|
106
|
+
timeout: 3e4,
|
|
107
|
+
headers: {
|
|
108
|
+
"Content-Type": "application/json",
|
|
109
|
+
Authorization: `Bearer ${token}`
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
if (opts.verbose) {
|
|
113
|
+
client.interceptors.request.use((req) => {
|
|
114
|
+
process.stderr.write(`\u2192 ${req.method?.toUpperCase()} ${req.baseURL}${req.url}
|
|
115
|
+
`);
|
|
116
|
+
if (req.data) process.stderr.write(` Body: ${JSON.stringify(req.data).slice(0, 200)}
|
|
117
|
+
`);
|
|
118
|
+
return req;
|
|
119
|
+
});
|
|
120
|
+
client.interceptors.response.use(
|
|
121
|
+
(res) => {
|
|
122
|
+
process.stderr.write(`\u2190 ${res.status} (${JSON.stringify(res.data).length} bytes)
|
|
123
|
+
`);
|
|
124
|
+
return res;
|
|
125
|
+
},
|
|
126
|
+
(err) => {
|
|
127
|
+
process.stderr.write(`\u2190 ERROR ${err.response?.status || "NETWORK"}: ${err.message}
|
|
128
|
+
`);
|
|
129
|
+
return Promise.reject(err);
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return client;
|
|
134
|
+
}
|
|
135
|
+
function createUnauthClient(baseURL) {
|
|
136
|
+
return axios.create({
|
|
137
|
+
baseURL,
|
|
138
|
+
timeout: 15e3,
|
|
139
|
+
headers: { "Content-Type": "application/json" }
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function handleApiError(err) {
|
|
143
|
+
if (axios.isAxiosError(err)) {
|
|
144
|
+
const ae = err;
|
|
145
|
+
return {
|
|
146
|
+
error: true,
|
|
147
|
+
status: ae.response?.status || 0,
|
|
148
|
+
message: ae.response?.statusText || ae.message,
|
|
149
|
+
details: ae.response?.data
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
error: true,
|
|
154
|
+
status: 0,
|
|
155
|
+
message: err instanceof Error ? err.message : String(err)
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/output.ts
|
|
160
|
+
import chalk from "chalk";
|
|
161
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
162
|
+
function pickFields(obj, fields) {
|
|
163
|
+
const result = {};
|
|
164
|
+
for (const f of fields) {
|
|
165
|
+
if (f.includes(".")) {
|
|
166
|
+
const parts = f.split(".");
|
|
167
|
+
let val = obj;
|
|
168
|
+
for (const p of parts) {
|
|
169
|
+
val = val && typeof val === "object" ? val[p] : void 0;
|
|
170
|
+
}
|
|
171
|
+
result[f] = val;
|
|
172
|
+
} else {
|
|
173
|
+
result[f] = obj[f];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
function applyFieldSelection(data, fields) {
|
|
179
|
+
if (!fields) return data;
|
|
180
|
+
const fieldList = fields.split(",").map((f) => f.trim());
|
|
181
|
+
if (Array.isArray(data)) {
|
|
182
|
+
return data.map((item) => pickFields(item, fieldList));
|
|
183
|
+
}
|
|
184
|
+
if (typeof data === "object" && data !== null) {
|
|
185
|
+
return pickFields(data, fieldList);
|
|
186
|
+
}
|
|
187
|
+
return data;
|
|
188
|
+
}
|
|
189
|
+
function formatHumanValue(val) {
|
|
190
|
+
if (val === null || val === void 0) return "-";
|
|
191
|
+
if (typeof val === "object") return JSON.stringify(val);
|
|
192
|
+
return String(val);
|
|
193
|
+
}
|
|
194
|
+
function formatHuman(data) {
|
|
195
|
+
if (Array.isArray(data)) {
|
|
196
|
+
if (data.length === 0) return chalk.dim("(empty)");
|
|
197
|
+
const keys = Object.keys(data[0]);
|
|
198
|
+
const widths = keys.map((k) => Math.max(k.length, ...data.map((r) => formatHumanValue(r[k]).length)));
|
|
199
|
+
const header = keys.map((k, i) => chalk.bold(k.padEnd(widths[i]))).join(" ");
|
|
200
|
+
const rows = data.map(
|
|
201
|
+
(row) => keys.map((k, i) => formatHumanValue(row[k]).padEnd(widths[i])).join(" ")
|
|
202
|
+
);
|
|
203
|
+
return [header, keys.map((_, i) => "\u2500".repeat(widths[i])).join(" "), ...rows].join("\n");
|
|
204
|
+
}
|
|
205
|
+
if (typeof data === "object" && data !== null) {
|
|
206
|
+
const entries = Object.entries(data);
|
|
207
|
+
const maxKey = Math.max(...entries.map(([k]) => k.length));
|
|
208
|
+
return entries.map(([k, v]) => `${chalk.bold(k.padEnd(maxKey))} ${formatHumanValue(v)}`).join("\n");
|
|
209
|
+
}
|
|
210
|
+
return String(data);
|
|
211
|
+
}
|
|
212
|
+
function formatCsv(data) {
|
|
213
|
+
if (!Array.isArray(data)) {
|
|
214
|
+
if (typeof data === "object" && data !== null) data = [data];
|
|
215
|
+
else return String(data);
|
|
216
|
+
}
|
|
217
|
+
const arr = data;
|
|
218
|
+
if (arr.length === 0) return "";
|
|
219
|
+
const keys = Object.keys(arr[0]);
|
|
220
|
+
const escape = (v) => {
|
|
221
|
+
const s = v === null || v === void 0 ? "" : String(v);
|
|
222
|
+
return s.includes(",") || s.includes('"') || s.includes("\n") ? `"${s.replace(/"/g, '""')}"` : s;
|
|
223
|
+
};
|
|
224
|
+
return [keys.join(","), ...arr.map((r) => keys.map((k) => escape(r[k])).join(","))].join("\n");
|
|
225
|
+
}
|
|
226
|
+
function output(data, opts = {}) {
|
|
227
|
+
const filtered = applyFieldSelection(data, opts.fields);
|
|
228
|
+
let text;
|
|
229
|
+
switch (opts.format) {
|
|
230
|
+
case "human":
|
|
231
|
+
text = formatHuman(filtered);
|
|
232
|
+
break;
|
|
233
|
+
case "csv":
|
|
234
|
+
text = formatCsv(filtered);
|
|
235
|
+
break;
|
|
236
|
+
default:
|
|
237
|
+
text = JSON.stringify(filtered, null, 2);
|
|
238
|
+
}
|
|
239
|
+
process.stdout.write(text + "\n");
|
|
240
|
+
if (opts.save) {
|
|
241
|
+
writeFileSync2(opts.save, JSON.stringify(filtered, null, 2));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function outputError(err) {
|
|
245
|
+
if (err && typeof err === "object" && "error" in err) {
|
|
246
|
+
const apiErr = err;
|
|
247
|
+
const out = { error: true, message: apiErr.message || "Unknown error" };
|
|
248
|
+
if (apiErr.status) out.status = apiErr.status;
|
|
249
|
+
if (apiErr.details) out.details = apiErr.details;
|
|
250
|
+
process.stderr.write(JSON.stringify(out) + "\n");
|
|
251
|
+
} else {
|
|
252
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
253
|
+
process.stderr.write(JSON.stringify({ error: true, message: msg }) + "\n");
|
|
254
|
+
}
|
|
255
|
+
process.exitCode = 1;
|
|
256
|
+
}
|
|
257
|
+
function outputPaginated(data, total, limit, offset, opts = {}) {
|
|
258
|
+
if (opts.format === "human") {
|
|
259
|
+
const filtered = applyFieldSelection(data, opts.fields);
|
|
260
|
+
const end = Math.min(offset + limit, total);
|
|
261
|
+
process.stdout.write(chalk.dim(`Showing ${offset + 1}-${end} of ${total}
|
|
262
|
+
|
|
263
|
+
`));
|
|
264
|
+
process.stdout.write(formatHuman(filtered) + "\n");
|
|
265
|
+
} else {
|
|
266
|
+
output({ data: applyFieldSelection(data, opts.fields), total, limit, offset, hasMore: offset + limit < total }, { ...opts, fields: void 0 });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/commands/auth.ts
|
|
271
|
+
function getGlobalOpts(cmd) {
|
|
272
|
+
let root = cmd;
|
|
273
|
+
while (root.parent) root = root.parent;
|
|
274
|
+
return root.opts();
|
|
275
|
+
}
|
|
276
|
+
function registerAuthCommands(program2) {
|
|
277
|
+
const auth = program2.command("auth").description("Authentication management");
|
|
278
|
+
auth.command("set-key").description("Set an API key (JWT) for authentication. Preferred method for agents.").requiredOption("--key <jwt>", "JWT API key").option("--server <url>", "API server URL").option("--profile <name>", "Profile name").action(async (opts) => {
|
|
279
|
+
try {
|
|
280
|
+
const config = loadConfig();
|
|
281
|
+
const profileName = opts.profile || config.activeProfile;
|
|
282
|
+
if (!config.profiles[profileName]) {
|
|
283
|
+
config.profiles[profileName] = { server: "https://api.enerlence.com" };
|
|
284
|
+
}
|
|
285
|
+
const profile = config.profiles[profileName];
|
|
286
|
+
profile.token = opts.key;
|
|
287
|
+
profile.authMethod = "api-key";
|
|
288
|
+
if (opts.server) profile.server = opts.server;
|
|
289
|
+
try {
|
|
290
|
+
const payload = JSON.parse(Buffer.from(opts.key.split(".")[1], "base64").toString());
|
|
291
|
+
profile.clientUID = payload.clientUID;
|
|
292
|
+
profile.userUID = payload.userUID;
|
|
293
|
+
} catch {
|
|
294
|
+
}
|
|
295
|
+
saveConfig(config);
|
|
296
|
+
output({ success: true, method: "api-key", profile: profileName, clientUID: profile.clientUID, server: profile.server }, getGlobalOpts(auth));
|
|
297
|
+
} catch (err) {
|
|
298
|
+
outputError(err);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
auth.command("login").description("Login with email and password").requiredOption("--email <email>", "User email").requiredOption("--password <password>", "User password").option("--server <url>", "API server URL").option("--profile <name>", "Profile name").action(async (opts) => {
|
|
302
|
+
try {
|
|
303
|
+
const config = loadConfig();
|
|
304
|
+
const profileName = opts.profile || config.activeProfile;
|
|
305
|
+
if (!config.profiles[profileName]) {
|
|
306
|
+
config.profiles[profileName] = { server: "https://api.enerlence.com" };
|
|
307
|
+
}
|
|
308
|
+
const profile = config.profiles[profileName];
|
|
309
|
+
if (opts.server) profile.server = opts.server;
|
|
310
|
+
const securityUrl = getServiceUrl(profile.server, "security");
|
|
311
|
+
const client = createUnauthClient(securityUrl);
|
|
312
|
+
const res = await client.post("/auth/login", { email: opts.email, password: opts.password });
|
|
313
|
+
if (res.data?.token?.access_token) {
|
|
314
|
+
profile.token = res.data.token.access_token;
|
|
315
|
+
profile.authMethod = "login";
|
|
316
|
+
profile.email = opts.email;
|
|
317
|
+
profile.clientUID = res.data.user?.clientUID;
|
|
318
|
+
profile.userUID = res.data.user?.userUID;
|
|
319
|
+
saveConfig(config);
|
|
320
|
+
output({
|
|
321
|
+
success: true,
|
|
322
|
+
method: "login",
|
|
323
|
+
profile: profileName,
|
|
324
|
+
user: {
|
|
325
|
+
email: res.data.user?.email,
|
|
326
|
+
clientUID: res.data.user?.clientUID,
|
|
327
|
+
userUID: res.data.user?.userUID
|
|
328
|
+
}
|
|
329
|
+
}, getGlobalOpts(auth));
|
|
330
|
+
} else {
|
|
331
|
+
outputError(new Error(res.data?.errors?.[0]?.message || "Login failed"));
|
|
332
|
+
}
|
|
333
|
+
} catch (err) {
|
|
334
|
+
outputError(handleApiError(err));
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
auth.command("status").description("Show current authentication status").option("--profile <name>", "Profile name").action(async (opts) => {
|
|
338
|
+
try {
|
|
339
|
+
const config = loadConfig();
|
|
340
|
+
const profile = getActiveProfile(config, opts.profile);
|
|
341
|
+
if (!profile.token) {
|
|
342
|
+
output({ authenticated: false, message: "No token configured" }, getGlobalOpts(auth));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
let expiresAt;
|
|
346
|
+
let expired = false;
|
|
347
|
+
try {
|
|
348
|
+
const payload = JSON.parse(Buffer.from(profile.token.split(".")[1], "base64").toString());
|
|
349
|
+
if (payload.exp) {
|
|
350
|
+
const expDate = new Date(payload.exp * 1e3);
|
|
351
|
+
expiresAt = expDate.toISOString();
|
|
352
|
+
expired = expDate < /* @__PURE__ */ new Date();
|
|
353
|
+
}
|
|
354
|
+
} catch {
|
|
355
|
+
}
|
|
356
|
+
output({
|
|
357
|
+
authenticated: !expired,
|
|
358
|
+
method: profile.authMethod || "unknown",
|
|
359
|
+
server: profile.server,
|
|
360
|
+
email: profile.email,
|
|
361
|
+
clientUID: profile.clientUID,
|
|
362
|
+
userUID: profile.userUID,
|
|
363
|
+
expiresAt,
|
|
364
|
+
expired
|
|
365
|
+
}, getGlobalOpts(auth));
|
|
366
|
+
} catch (err) {
|
|
367
|
+
outputError(err);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
auth.command("refresh").description("Refresh the current JWT token").option("--profile <name>", "Profile name").action(async (opts) => {
|
|
371
|
+
try {
|
|
372
|
+
const config = loadConfig();
|
|
373
|
+
const profileName = opts.profile || config.activeProfile;
|
|
374
|
+
const globalOpts = program2.opts();
|
|
375
|
+
const client = createServiceClient("security", { ...globalOpts, profile: profileName });
|
|
376
|
+
const res = await client.get("/auth/jwt/refreshToken");
|
|
377
|
+
if (res.data?.access_token || res.data?.token?.access_token) {
|
|
378
|
+
const newToken = res.data.access_token || res.data.token.access_token;
|
|
379
|
+
config.profiles[profileName].token = newToken;
|
|
380
|
+
saveConfig(config);
|
|
381
|
+
output({ success: true, message: "Token refreshed" }, getGlobalOpts(auth));
|
|
382
|
+
} else {
|
|
383
|
+
outputError(new Error("Refresh failed"));
|
|
384
|
+
}
|
|
385
|
+
} catch (err) {
|
|
386
|
+
outputError(handleApiError(err));
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/commands/config.ts
|
|
392
|
+
function registerConfigCommands(program2) {
|
|
393
|
+
const cfg = program2.command("config").description("CLI configuration management");
|
|
394
|
+
cfg.command("set <key> <value>").description("Set a configuration value. Keys: server, token, activeProfile").option("--profile <name>", "Profile to modify").action((key, value, opts) => {
|
|
395
|
+
try {
|
|
396
|
+
if (key === "activeProfile") {
|
|
397
|
+
const config = loadConfig();
|
|
398
|
+
config.activeProfile = value;
|
|
399
|
+
saveConfig(config);
|
|
400
|
+
} else {
|
|
401
|
+
setConfigValue(key, value, opts.profile);
|
|
402
|
+
}
|
|
403
|
+
output({ success: true, key, value });
|
|
404
|
+
} catch (err) {
|
|
405
|
+
outputError(err);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
cfg.command("get <key>").description("Get a configuration value").option("--profile <name>", "Profile to read from").action((key, opts) => {
|
|
409
|
+
try {
|
|
410
|
+
if (key === "activeProfile") {
|
|
411
|
+
const config = loadConfig();
|
|
412
|
+
output({ activeProfile: config.activeProfile });
|
|
413
|
+
} else {
|
|
414
|
+
output({ [key]: getConfigValue(key, opts.profile) });
|
|
415
|
+
}
|
|
416
|
+
} catch (err) {
|
|
417
|
+
outputError(err);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
cfg.command("list").description("Show all configuration").action(() => {
|
|
421
|
+
try {
|
|
422
|
+
const config = loadConfig();
|
|
423
|
+
const safe = JSON.parse(JSON.stringify(config));
|
|
424
|
+
for (const [name, profile] of Object.entries(safe.profiles)) {
|
|
425
|
+
const p = profile;
|
|
426
|
+
if (p.token) p.token = p.token.slice(0, 20) + "...";
|
|
427
|
+
}
|
|
428
|
+
output(safe);
|
|
429
|
+
} catch (err) {
|
|
430
|
+
outputError(err);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
cfg.command("create-profile <name>").description("Create a new profile").option("--server <url>", "API server URL", "https://api.enerlence.com").action((name, opts) => {
|
|
434
|
+
try {
|
|
435
|
+
const config = loadConfig();
|
|
436
|
+
if (config.profiles[name]) {
|
|
437
|
+
outputError(new Error(`Profile "${name}" already exists`));
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
config.profiles[name] = { server: opts.server };
|
|
441
|
+
saveConfig(config);
|
|
442
|
+
output({ success: true, profile: name, server: opts.server });
|
|
443
|
+
} catch (err) {
|
|
444
|
+
outputError(err);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
cfg.command("use <name>").description("Switch to a profile").action((name) => {
|
|
448
|
+
try {
|
|
449
|
+
const config = loadConfig();
|
|
450
|
+
if (!config.profiles[name]) {
|
|
451
|
+
outputError(new Error(`Profile "${name}" not found. Available: ${Object.keys(config.profiles).join(", ")}`));
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
config.activeProfile = name;
|
|
455
|
+
saveConfig(config);
|
|
456
|
+
output({ success: true, activeProfile: name });
|
|
457
|
+
} catch (err) {
|
|
458
|
+
outputError(err);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/commands/inventory/factory.ts
|
|
464
|
+
import { Command } from "commander";
|
|
465
|
+
function getGlobalOpts2(cmd) {
|
|
466
|
+
let root = cmd;
|
|
467
|
+
while (root.parent) root = root.parent;
|
|
468
|
+
return root.opts();
|
|
469
|
+
}
|
|
470
|
+
function parseData(data) {
|
|
471
|
+
if (!data) return void 0;
|
|
472
|
+
if (data === "-") {
|
|
473
|
+
const { readFileSync: readFileSync5 } = __require("fs");
|
|
474
|
+
const input = readFileSync5(0, "utf-8");
|
|
475
|
+
return JSON.parse(input);
|
|
476
|
+
}
|
|
477
|
+
return JSON.parse(data);
|
|
478
|
+
}
|
|
479
|
+
function createResourceCommands(cfg) {
|
|
480
|
+
const cmd = new Command(cfg.name).description(`Manage ${cfg.singular}s`);
|
|
481
|
+
const service = cfg.service || "solar";
|
|
482
|
+
cmd.command("list").description(`List ${cfg.singular}s with pagination. Fields: ${cfg.listFields.join(", ")}`).option("--limit <n>", "Max results", "20").option("--offset <n>", "Skip results", "0").option("--active-only", "Only active items (exclude inactive)").action(async (opts) => {
|
|
483
|
+
try {
|
|
484
|
+
const global = getGlobalOpts2(cmd);
|
|
485
|
+
const client = createServiceClient(service, global);
|
|
486
|
+
const params = {
|
|
487
|
+
limit: parseInt(opts.limit),
|
|
488
|
+
offset: parseInt(opts.offset)
|
|
489
|
+
};
|
|
490
|
+
if (!opts.activeOnly) params.unactive = true;
|
|
491
|
+
const res = await client.get(cfg.basePath, { params });
|
|
492
|
+
let data;
|
|
493
|
+
let total;
|
|
494
|
+
if (Array.isArray(res.data) && res.data.length >= 2 && Array.isArray(res.data[0]) && typeof res.data[1] === "number") {
|
|
495
|
+
data = res.data[0];
|
|
496
|
+
total = res.data[1];
|
|
497
|
+
} else if (Array.isArray(res.data)) {
|
|
498
|
+
data = res.data;
|
|
499
|
+
total = res.data.length;
|
|
500
|
+
} else {
|
|
501
|
+
data = res.data?.data || [];
|
|
502
|
+
total = res.data?.total ?? data.length;
|
|
503
|
+
}
|
|
504
|
+
const outOpts = { ...global };
|
|
505
|
+
if (!global.fields && cfg.listFields.length > 0) {
|
|
506
|
+
outOpts.fields = [cfg.idField, ...cfg.listFields].join(",");
|
|
507
|
+
}
|
|
508
|
+
if (Array.isArray(data)) {
|
|
509
|
+
outputPaginated(data, total, parseInt(opts.limit), parseInt(opts.offset), outOpts);
|
|
510
|
+
} else {
|
|
511
|
+
output(data, outOpts);
|
|
512
|
+
}
|
|
513
|
+
} catch (err) {
|
|
514
|
+
outputError(handleApiError(err));
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
cmd.command("get <id>").description(`Get a ${cfg.singular} by ID. All fields returned by default.`).action(async (id) => {
|
|
518
|
+
try {
|
|
519
|
+
const global = getGlobalOpts2(cmd);
|
|
520
|
+
const client = createServiceClient(service, global);
|
|
521
|
+
if (cfg.getViaFilter) {
|
|
522
|
+
const res = await client.get(cfg.basePath, { params: { unactive: true } });
|
|
523
|
+
const all = Array.isArray(res.data) ? res.data : Array.isArray(res.data?.[0]) ? res.data[0] : res.data?.data || [];
|
|
524
|
+
const idNum = parseInt(id);
|
|
525
|
+
const item = all.find((r) => r[cfg.idField] === idNum || r[cfg.idField] === id);
|
|
526
|
+
if (!item) {
|
|
527
|
+
outputError(new Error(`${cfg.singular} with ${cfg.idField}=${id} not found`));
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
output(item, global);
|
|
531
|
+
} else {
|
|
532
|
+
const url = cfg.getPath ? cfg.getPath.replace(":id", id) : `${cfg.basePath}/${id}`;
|
|
533
|
+
const res = await client.get(url);
|
|
534
|
+
output(res.data, global);
|
|
535
|
+
}
|
|
536
|
+
} catch (err) {
|
|
537
|
+
outputError(handleApiError(err));
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
cmd.command("create").description(`Create a new ${cfg.singular}. Pass JSON via --data or stdin (--data -)`).requiredOption("--data <json>", "JSON data (or - for stdin)").action(async (opts) => {
|
|
541
|
+
try {
|
|
542
|
+
const global = getGlobalOpts2(cmd);
|
|
543
|
+
const client = createServiceClient(service, global);
|
|
544
|
+
const body = parseData(opts.data);
|
|
545
|
+
const res = await client.post(cfg.basePath, body);
|
|
546
|
+
output(res.data, global);
|
|
547
|
+
} catch (err) {
|
|
548
|
+
outputError(handleApiError(err));
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
cmd.command("update <id>").description(`Update a ${cfg.singular}`).requiredOption("--data <json>", "JSON data with fields to update (or - for stdin)").action(async (id, opts) => {
|
|
552
|
+
try {
|
|
553
|
+
const global = getGlobalOpts2(cmd);
|
|
554
|
+
const client = createServiceClient(service, global);
|
|
555
|
+
const body = parseData(opts.data);
|
|
556
|
+
if (cfg.putBodyOnly) {
|
|
557
|
+
const res = await client.put(cfg.basePath, { ...body, [cfg.idField]: id });
|
|
558
|
+
output(res.data, global);
|
|
559
|
+
} else {
|
|
560
|
+
const res = await client.put(`${cfg.basePath}/${id}`, body);
|
|
561
|
+
output(res.data, global);
|
|
562
|
+
}
|
|
563
|
+
} catch (err) {
|
|
564
|
+
outputError(handleApiError(err));
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
cmd.command("delete <id>").description(`Delete a ${cfg.singular}`).action(async (id) => {
|
|
568
|
+
try {
|
|
569
|
+
const global = getGlobalOpts2(cmd);
|
|
570
|
+
const client = createServiceClient(service, global);
|
|
571
|
+
const res = await client.delete(`${cfg.basePath}/${id}`);
|
|
572
|
+
output(res.data ?? { success: true, deleted: id }, global);
|
|
573
|
+
} catch (err) {
|
|
574
|
+
outputError(handleApiError(err));
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
if (cfg.batchDeletePath) {
|
|
578
|
+
cmd.command("delete-batch").description(`Batch delete ${cfg.singular}s`).requiredOption("--ids <ids>", "Comma-separated IDs").action(async (opts) => {
|
|
579
|
+
try {
|
|
580
|
+
const global = getGlobalOpts2(cmd);
|
|
581
|
+
const client = createServiceClient(service, global);
|
|
582
|
+
const ids = opts.ids.split(",").map((s) => s.trim());
|
|
583
|
+
const res = await client.post(`${cfg.basePath}/${cfg.batchDeletePath}`, ids);
|
|
584
|
+
output(res.data ?? { success: true, deleted: ids.length }, global);
|
|
585
|
+
} catch (err) {
|
|
586
|
+
outputError(handleApiError(err));
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
cmd.command("filter").description(`Advanced filter for ${cfg.singular}s. Pass filter query as JSON.`).requiredOption("--query <json>", "Filter query JSON (or - for stdin)").option("--limit <n>", "Max results", "20").option("--offset <n>", "Skip results", "0").action(async (opts) => {
|
|
591
|
+
try {
|
|
592
|
+
const global = getGlobalOpts2(cmd);
|
|
593
|
+
const client = createServiceClient(service, global);
|
|
594
|
+
const query = parseData(opts.query);
|
|
595
|
+
const filterUrl = cfg.filterPath || `${cfg.basePath}/filter`;
|
|
596
|
+
const res = await client.post(filterUrl, {
|
|
597
|
+
...query,
|
|
598
|
+
limit: parseInt(opts.limit),
|
|
599
|
+
offset: parseInt(opts.offset)
|
|
600
|
+
});
|
|
601
|
+
const data = Array.isArray(res.data) ? res.data : res.data?.data || res.data;
|
|
602
|
+
const total = res.data?.total ?? (Array.isArray(data) ? data.length : 0);
|
|
603
|
+
outputPaginated(data, total, parseInt(opts.limit), parseInt(opts.offset), global);
|
|
604
|
+
} catch (err) {
|
|
605
|
+
outputError(handleApiError(err));
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
return cmd;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// src/commands/inventory/kits.ts
|
|
612
|
+
function getGlobalOpts3(cmd) {
|
|
613
|
+
let root = cmd;
|
|
614
|
+
while (root.parent) root = root.parent;
|
|
615
|
+
return root.opts();
|
|
616
|
+
}
|
|
617
|
+
function parseData2(data) {
|
|
618
|
+
if (data === "-") {
|
|
619
|
+
const { readFileSync: readFileSync5 } = __require("fs");
|
|
620
|
+
return JSON.parse(readFileSync5(0, "utf-8"));
|
|
621
|
+
}
|
|
622
|
+
return JSON.parse(data);
|
|
623
|
+
}
|
|
624
|
+
function registerKitsCommands(inventory) {
|
|
625
|
+
const kits = createResourceCommands({
|
|
626
|
+
name: "kits",
|
|
627
|
+
singular: "solar kit",
|
|
628
|
+
basePath: "/solar-kits",
|
|
629
|
+
idField: "idSolarKit",
|
|
630
|
+
listFields: ["identifier", "peakPower", "panelNumber", "inverterNumber", "price", "totalPrice", "phaseNumber", "active"],
|
|
631
|
+
filterPath: "/solar-kits/filters",
|
|
632
|
+
batchDeletePath: "delete-batch-solar-kits",
|
|
633
|
+
getViaFilter: true
|
|
634
|
+
});
|
|
635
|
+
kits.command("archive <kitId>").description("Archive a solar kit (soft-disable)").action(async (kitId) => {
|
|
636
|
+
try {
|
|
637
|
+
const global = getGlobalOpts3(kits);
|
|
638
|
+
const client = createServiceClient("solar", global);
|
|
639
|
+
const res = await client.put(`/solar-kits/archive/${kitId}`);
|
|
640
|
+
output(res.data ?? { success: true, archived: kitId }, global);
|
|
641
|
+
} catch (err) {
|
|
642
|
+
outputError(handleApiError(err));
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
const kitPanels = kits.command("panels").description("Manage kit solar panels");
|
|
646
|
+
kitPanels.command("list").description("List all kit solar panels. Fields: idKitSolarPanel, name, manufacturer, peakPower, efficiency, costPerUnit").option("--limit <n>", "Max results", "50").option("--offset <n>", "Skip results", "0").action(async (opts) => {
|
|
647
|
+
try {
|
|
648
|
+
const global = getGlobalOpts3(kits);
|
|
649
|
+
const client = createServiceClient("solar", global);
|
|
650
|
+
const res = await client.get("/solar-kits/solar-panels", { params: { limit: opts.limit, offset: opts.offset } });
|
|
651
|
+
output(res.data, global);
|
|
652
|
+
} catch (err) {
|
|
653
|
+
outputError(handleApiError(err));
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
kitPanels.command("get <id>").description("Get a kit solar panel by ID").action(async (id) => {
|
|
657
|
+
try {
|
|
658
|
+
const global = getGlobalOpts3(kits);
|
|
659
|
+
const client = createServiceClient("solar", global);
|
|
660
|
+
const res = await client.post("/solar-kits/solar-panels/filter", { idKitSolarPanel: id });
|
|
661
|
+
const data = Array.isArray(res.data) ? res.data[0] : res.data?.data?.[0] || res.data;
|
|
662
|
+
output(data, global);
|
|
663
|
+
} catch (err) {
|
|
664
|
+
outputError(handleApiError(err));
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
kitPanels.command("create").description("Create a kit solar panel").requiredOption("--data <json>", "JSON data").action(async (opts) => {
|
|
668
|
+
try {
|
|
669
|
+
const global = getGlobalOpts3(kits);
|
|
670
|
+
const client = createServiceClient("solar", global);
|
|
671
|
+
const res = await client.post("/solar-kits/solar-panels", parseData2(opts.data));
|
|
672
|
+
output(res.data, global);
|
|
673
|
+
} catch (err) {
|
|
674
|
+
outputError(handleApiError(err));
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
kitPanels.command("update <id>").description("Update a kit solar panel").requiredOption("--data <json>", "JSON data").action(async (id, opts) => {
|
|
678
|
+
try {
|
|
679
|
+
const global = getGlobalOpts3(kits);
|
|
680
|
+
const client = createServiceClient("solar", global);
|
|
681
|
+
const res = await client.put(`/solar-kits/solar-panels/${id}`, parseData2(opts.data));
|
|
682
|
+
output(res.data, global);
|
|
683
|
+
} catch (err) {
|
|
684
|
+
outputError(handleApiError(err));
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
kitPanels.command("delete <id>").description("Delete a kit solar panel").action(async (id) => {
|
|
688
|
+
try {
|
|
689
|
+
const global = getGlobalOpts3(kits);
|
|
690
|
+
const client = createServiceClient("solar", global);
|
|
691
|
+
const res = await client.delete(`/solar-kits/solar-panels/${id}`);
|
|
692
|
+
output(res.data ?? { success: true, deleted: id }, global);
|
|
693
|
+
} catch (err) {
|
|
694
|
+
outputError(handleApiError(err));
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
kitPanels.command("featured <kitPanelId>").description("List solar kits that feature this kit panel").action(async (kitPanelId) => {
|
|
698
|
+
try {
|
|
699
|
+
const global = getGlobalOpts3(kits);
|
|
700
|
+
const client = createServiceClient("solar", global);
|
|
701
|
+
const res = await client.get(`/solar-kits/solar-panels/findFeaturedSolarKits/${kitPanelId}`);
|
|
702
|
+
output(res.data, global);
|
|
703
|
+
} catch (err) {
|
|
704
|
+
outputError(handleApiError(err));
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
const kitInverters = kits.command("inverters").description("Manage kit inverters");
|
|
708
|
+
kitInverters.command("list").description("List all kit inverters. Fields: idKitInverter, name, manufacturer, nominalPower, efficiency, costPerUnit").option("--limit <n>", "Max results", "50").option("--offset <n>", "Skip results", "0").action(async (opts) => {
|
|
709
|
+
try {
|
|
710
|
+
const global = getGlobalOpts3(kits);
|
|
711
|
+
const client = createServiceClient("solar", global);
|
|
712
|
+
const res = await client.get("/solar-kits/inverters", { params: { limit: opts.limit, offset: opts.offset } });
|
|
713
|
+
output(res.data, global);
|
|
714
|
+
} catch (err) {
|
|
715
|
+
outputError(handleApiError(err));
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
kitInverters.command("create").description("Create a kit inverter").requiredOption("--data <json>", "JSON data").action(async (opts) => {
|
|
719
|
+
try {
|
|
720
|
+
const global = getGlobalOpts3(kits);
|
|
721
|
+
const client = createServiceClient("solar", global);
|
|
722
|
+
const res = await client.post("/solar-kits/inverters", parseData2(opts.data));
|
|
723
|
+
output(res.data, global);
|
|
724
|
+
} catch (err) {
|
|
725
|
+
outputError(handleApiError(err));
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
kitInverters.command("update <id>").description("Update a kit inverter").requiredOption("--data <json>", "JSON data").action(async (id, opts) => {
|
|
729
|
+
try {
|
|
730
|
+
const global = getGlobalOpts3(kits);
|
|
731
|
+
const client = createServiceClient("solar", global);
|
|
732
|
+
const res = await client.put(`/solar-kits/inverters/${id}`, parseData2(opts.data));
|
|
733
|
+
output(res.data, global);
|
|
734
|
+
} catch (err) {
|
|
735
|
+
outputError(handleApiError(err));
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
kitInverters.command("delete <id>").description("Delete a kit inverter").action(async (id) => {
|
|
739
|
+
try {
|
|
740
|
+
const global = getGlobalOpts3(kits);
|
|
741
|
+
const client = createServiceClient("solar", global);
|
|
742
|
+
const res = await client.delete(`/solar-kits/inverters/${id}`);
|
|
743
|
+
output(res.data ?? { success: true, deleted: id }, global);
|
|
744
|
+
} catch (err) {
|
|
745
|
+
outputError(handleApiError(err));
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
kitInverters.command("featured <kitInverterId>").description("List solar kits that feature this kit inverter").action(async (kitInverterId) => {
|
|
749
|
+
try {
|
|
750
|
+
const global = getGlobalOpts3(kits);
|
|
751
|
+
const client = createServiceClient("solar", global);
|
|
752
|
+
const res = await client.get(`/solar-kits/inverters/findFeaturedSolarKits/${kitInverterId}`);
|
|
753
|
+
output(res.data, global);
|
|
754
|
+
} catch (err) {
|
|
755
|
+
outputError(handleApiError(err));
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
const kitBatteries = kits.command("batteries").description("Manage kit batteries");
|
|
759
|
+
kitBatteries.command("list").description("List all kit batteries. Fields: idKitBattery, name, manufacturer, capacity, costPerUnit").option("--limit <n>", "Max results", "50").option("--offset <n>", "Skip results", "0").action(async (opts) => {
|
|
760
|
+
try {
|
|
761
|
+
const global = getGlobalOpts3(kits);
|
|
762
|
+
const client = createServiceClient("solar", global);
|
|
763
|
+
const res = await client.get("/solar-kits/batteries", { params: { limit: opts.limit, offset: opts.offset } });
|
|
764
|
+
output(res.data, global);
|
|
765
|
+
} catch (err) {
|
|
766
|
+
outputError(handleApiError(err));
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
kitBatteries.command("create").description("Create a kit battery").requiredOption("--data <json>", "JSON data").action(async (opts) => {
|
|
770
|
+
try {
|
|
771
|
+
const global = getGlobalOpts3(kits);
|
|
772
|
+
const client = createServiceClient("solar", global);
|
|
773
|
+
const res = await client.post("/solar-kits/batteries", parseData2(opts.data));
|
|
774
|
+
output(res.data, global);
|
|
775
|
+
} catch (err) {
|
|
776
|
+
outputError(handleApiError(err));
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
kitBatteries.command("delete <id>").description("Delete a kit battery").action(async (id) => {
|
|
780
|
+
try {
|
|
781
|
+
const global = getGlobalOpts3(kits);
|
|
782
|
+
const client = createServiceClient("solar", global);
|
|
783
|
+
const res = await client.delete(`/solar-kits/batteries/${id}`);
|
|
784
|
+
output(res.data ?? { success: true, deleted: id }, global);
|
|
785
|
+
} catch (err) {
|
|
786
|
+
outputError(handleApiError(err));
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
kits.command("assemble").description(
|
|
790
|
+
'Assemble a solar kit from existing components by ID.\nReferences kit panels, inverters, batteries, and custom assets by their IDs.\n\nCustom assets format: --custom-asset <assetId>:<units> (repeatable)\n\nExamples:\n suntropy inventory kits assemble --name "Kit 5kW" --panel 123 --inverter 456 --panels-count 12 --price 6500\n suntropy inventory kits assemble --name "Kit Premium" --panel 123 --inverter 456 --battery 789 \\\n --panels-count 12 --inverters-count 1 --batteries-count 1 --peak-power 5.4 --price 8500 \\\n --custom-asset 100:12 --custom-asset 200:1 --phase single_phase'
|
|
791
|
+
).requiredOption("--name <identifier>", "Kit name/identifier").option("--panel <kitPanelId>", "Kit panel ID (idKitSolarPanel)").option("--inverter <kitInverterId>", "Kit inverter ID (idKitInverter)").option("--battery <batteryId>", "Battery ID from inventory (batteryId)").option("--panels-count <n>", "Number of panels", "12").option("--inverters-count <n>", "Number of inverters", "1").option("--batteries-count <n>", "Number of batteries", "0").option("--peak-power <kW>", "Total peak power in kW").option("--price <eur>", "Kit price in EUR").option("--phase <type>", "Phase: single_phase or three_phase", "single_phase").option("--coplanar", "Coplanar mounting").option("--taxes <pct>", "Default tax percentage", "21").option("--custom-asset <id:units>", "Custom asset as id:units (repeatable)", collectCustomAssets, []).action(async (opts) => {
|
|
792
|
+
try {
|
|
793
|
+
const global = getGlobalOpts3(kits);
|
|
794
|
+
const client = createServiceClient("solar", global);
|
|
795
|
+
const body = {
|
|
796
|
+
identifier: opts.name,
|
|
797
|
+
panelNumber: parseInt(opts.panelsCount),
|
|
798
|
+
inverterNumber: parseInt(opts.invertersCount),
|
|
799
|
+
batteriesNumber: parseInt(opts.batteriesCount),
|
|
800
|
+
phaseNumber: opts.phase,
|
|
801
|
+
defaultTaxesPercentage: parseFloat(opts.taxes),
|
|
802
|
+
active: true
|
|
803
|
+
};
|
|
804
|
+
if (opts.panel) body.kitSolarPanel = { idKitSolarPanel: parseInt(opts.panel) };
|
|
805
|
+
if (opts.inverter) body.kitInverter = { idKitInverter: parseInt(opts.inverter) };
|
|
806
|
+
if (opts.battery) body.battery = { batteryId: parseInt(opts.battery) };
|
|
807
|
+
if (opts.peakPower) body.peakPower = parseFloat(opts.peakPower);
|
|
808
|
+
if (opts.price) body.price = parseFloat(opts.price);
|
|
809
|
+
if (opts.coplanar) body.coplanar = true;
|
|
810
|
+
if (opts.customAsset && opts.customAsset.length > 0) {
|
|
811
|
+
body.solarKitCustomAssets = opts.customAsset.map((ca) => ({
|
|
812
|
+
customAsset: { idCustomAsset: ca.id },
|
|
813
|
+
units: ca.units
|
|
814
|
+
}));
|
|
815
|
+
}
|
|
816
|
+
const res = await client.post("/solar-kits", body);
|
|
817
|
+
output(res.data, global);
|
|
818
|
+
} catch (err) {
|
|
819
|
+
outputError(handleApiError(err));
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
inventory.addCommand(kits);
|
|
823
|
+
}
|
|
824
|
+
function collectCustomAssets(value, previous) {
|
|
825
|
+
const [idStr, unitsStr] = value.split(":");
|
|
826
|
+
const id = parseInt(idStr);
|
|
827
|
+
const units = unitsStr ? parseInt(unitsStr) : 1;
|
|
828
|
+
if (isNaN(id)) throw new Error(`Invalid custom asset format: "${value}". Use <id>:<units> (e.g. 100:12)`);
|
|
829
|
+
return [...previous, { id, units }];
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// src/commands/inventory/manufacturers.ts
|
|
833
|
+
function getGlobalOpts4(cmd) {
|
|
834
|
+
let root = cmd;
|
|
835
|
+
while (root.parent) root = root.parent;
|
|
836
|
+
return root.opts();
|
|
837
|
+
}
|
|
838
|
+
function registerManufacturersCommands(inventory) {
|
|
839
|
+
const mfr = inventory.command("manufacturers").description("Manage manufacturers (referenced by all inventory devices)");
|
|
840
|
+
mfr.command("list").description("List all manufacturers. Fields: idManufacturer, name, imageUrl").action(async () => {
|
|
841
|
+
try {
|
|
842
|
+
const global = getGlobalOpts4(mfr);
|
|
843
|
+
const client = createServiceClient("solar", global);
|
|
844
|
+
const res = await client.get("/manufacturers");
|
|
845
|
+
output(res.data, global);
|
|
846
|
+
} catch (err) {
|
|
847
|
+
outputError(handleApiError(err));
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
mfr.command("create").description("Create a new manufacturer").requiredOption("--data <json>", 'JSON: { "name": "Manufacturer Name", "imageUrl": "..." }').action(async (opts) => {
|
|
851
|
+
try {
|
|
852
|
+
const global = getGlobalOpts4(mfr);
|
|
853
|
+
const client = createServiceClient("solar", global);
|
|
854
|
+
const body = JSON.parse(opts.data);
|
|
855
|
+
const res = await client.post("/manufacturers", body);
|
|
856
|
+
output(res.data, global);
|
|
857
|
+
} catch (err) {
|
|
858
|
+
outputError(handleApiError(err));
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// src/commands/inventory/custom-fields.ts
|
|
864
|
+
function getGlobalOpts5(cmd) {
|
|
865
|
+
let root = cmd;
|
|
866
|
+
while (root.parent) root = root.parent;
|
|
867
|
+
return root.opts();
|
|
868
|
+
}
|
|
869
|
+
function parseData3(data) {
|
|
870
|
+
if (!data) return void 0;
|
|
871
|
+
return JSON.parse(data);
|
|
872
|
+
}
|
|
873
|
+
function registerCustomFieldsCommands(inventory) {
|
|
874
|
+
const fields = inventory.command("custom-fields").description(
|
|
875
|
+
"Manage custom fields for custom asset types.\nFields define the schema of a custom asset type (e.g. text, number, options).\nTypes: text, number, date, datetime, time, email, phonenumber, website, options, labels, currency, large_text, user"
|
|
876
|
+
);
|
|
877
|
+
fields.command("list").description("List all custom fields").action(async () => {
|
|
878
|
+
try {
|
|
879
|
+
const global = getGlobalOpts5(fields);
|
|
880
|
+
const client = createServiceClient("solar", global);
|
|
881
|
+
const res = await client.get("/custom-asset/custom-field/all");
|
|
882
|
+
output(res.data, global);
|
|
883
|
+
} catch (err) {
|
|
884
|
+
outputError(handleApiError(err));
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
fields.command("get <id>").description("Get a custom field by ID").action(async (id) => {
|
|
888
|
+
try {
|
|
889
|
+
const global = getGlobalOpts5(fields);
|
|
890
|
+
const client = createServiceClient("solar", global);
|
|
891
|
+
const res = await client.get(`/custom-asset/custom-field/id/${id}`);
|
|
892
|
+
output(res.data, global);
|
|
893
|
+
} catch (err) {
|
|
894
|
+
outputError(handleApiError(err));
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
fields.command("create").description(
|
|
898
|
+
`Create a custom field for a custom asset type.
|
|
899
|
+
Required: label, type, customAssetTypeId
|
|
900
|
+
For options/labels type, include customFieldOptions array.
|
|
901
|
+
|
|
902
|
+
Example:
|
|
903
|
+
suntropy inventory custom-fields create --data '{"label":"Power","type":"number","customAssetTypeId":1}'
|
|
904
|
+
suntropy inventory custom-fields create --data '{"label":"Size","type":"options","customAssetTypeId":1,"customFieldOptions":[{"label":"S","value":"s"},{"label":"M","value":"m"},{"label":"L","value":"l"}]}'`
|
|
905
|
+
).requiredOption("--data <json>", "Field definition as JSON").action(async (opts) => {
|
|
906
|
+
try {
|
|
907
|
+
const global = getGlobalOpts5(fields);
|
|
908
|
+
const client = createServiceClient("solar", global);
|
|
909
|
+
const body = parseData3(opts.data);
|
|
910
|
+
const res = await client.post("/custom-asset/custom-field", body);
|
|
911
|
+
output(res.data, global);
|
|
912
|
+
} catch (err) {
|
|
913
|
+
outputError(handleApiError(err));
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
fields.command("update <id>").description("Update a custom field").requiredOption("--data <json>", "Updated field data as JSON").action(async (id, opts) => {
|
|
917
|
+
try {
|
|
918
|
+
const global = getGlobalOpts5(fields);
|
|
919
|
+
const client = createServiceClient("solar", global);
|
|
920
|
+
const body = parseData3(opts.data);
|
|
921
|
+
const res = await client.put(`/custom-asset/custom-field/${id}`, body);
|
|
922
|
+
output(res.data, global);
|
|
923
|
+
} catch (err) {
|
|
924
|
+
outputError(handleApiError(err));
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
fields.command("delete <id>").description("Delete a custom field").action(async (id) => {
|
|
928
|
+
try {
|
|
929
|
+
const global = getGlobalOpts5(fields);
|
|
930
|
+
const client = createServiceClient("solar", global);
|
|
931
|
+
const res = await client.delete(`/custom-asset/custom-field/${id}`);
|
|
932
|
+
output(res.data ?? { success: true, deleted: id }, global);
|
|
933
|
+
} catch (err) {
|
|
934
|
+
outputError(handleApiError(err));
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// src/commands/inventory/index.ts
|
|
940
|
+
var RESOURCES = [
|
|
941
|
+
{
|
|
942
|
+
name: "panels",
|
|
943
|
+
singular: "solar panel",
|
|
944
|
+
basePath: "/solar-panels",
|
|
945
|
+
idField: "solarPanelId",
|
|
946
|
+
listFields: ["name", "manufacturer", "peakPower", "efficiency", "costPerUnit", "active"],
|
|
947
|
+
batchDeletePath: "delete-batch-solar-panels"
|
|
948
|
+
},
|
|
949
|
+
{
|
|
950
|
+
name: "inverters",
|
|
951
|
+
singular: "solar inverter",
|
|
952
|
+
basePath: "/solar-inverter",
|
|
953
|
+
idField: "idInverter",
|
|
954
|
+
listFields: ["name", "manufacturer", "nominalPower", "efficiency", "phaseNumber", "isMicroinverter", "isHybrid", "costPerUnit", "active"],
|
|
955
|
+
batchDeletePath: "delete-batch-solar-inverters",
|
|
956
|
+
putBodyOnly: true
|
|
957
|
+
},
|
|
958
|
+
{
|
|
959
|
+
name: "batteries",
|
|
960
|
+
singular: "battery",
|
|
961
|
+
basePath: "/solar-battery",
|
|
962
|
+
idField: "batteryId",
|
|
963
|
+
listFields: ["name", "manufacturer", "capacity", "isModular", "costPerUnit", "active"],
|
|
964
|
+
batchDeletePath: "delete-batch-solar-batteries"
|
|
965
|
+
},
|
|
966
|
+
{
|
|
967
|
+
name: "chargers",
|
|
968
|
+
singular: "VE charger",
|
|
969
|
+
basePath: "/charger",
|
|
970
|
+
idField: "idCharger",
|
|
971
|
+
listFields: ["name", "manufacturer", "maxPower", "connectorType", "phaseNumber", "costPerUnit", "active"],
|
|
972
|
+
batchDeletePath: "delete-batch-chargers",
|
|
973
|
+
putBodyOnly: true
|
|
974
|
+
},
|
|
975
|
+
{
|
|
976
|
+
name: "heatpumps",
|
|
977
|
+
singular: "heat pump",
|
|
978
|
+
basePath: "/heat-pump",
|
|
979
|
+
idField: "idHeatpump",
|
|
980
|
+
listFields: ["name", "manufacturer", "lowerPower", "upperPower", "scop", "costPerUnit", "active"],
|
|
981
|
+
batchDeletePath: "delete-batch-heatpumps",
|
|
982
|
+
putBodyOnly: true
|
|
983
|
+
},
|
|
984
|
+
{
|
|
985
|
+
name: "custom-assets",
|
|
986
|
+
singular: "custom asset",
|
|
987
|
+
basePath: "/custom-asset",
|
|
988
|
+
idField: "idCustomAsset",
|
|
989
|
+
listFields: ["label", "customAssetType", "identifier", "isMaterial", "costPerUnit", "active"],
|
|
990
|
+
getPath: "/custom-asset/id/:id"
|
|
991
|
+
},
|
|
992
|
+
{
|
|
993
|
+
name: "custom-asset-types",
|
|
994
|
+
singular: "custom asset type",
|
|
995
|
+
basePath: "/custom-asset/type",
|
|
996
|
+
idField: "idCustomAssetType",
|
|
997
|
+
listFields: ["label", "isMaterialConcept", "panelsQuantity"],
|
|
998
|
+
getPath: "/custom-asset/type/id/:id"
|
|
999
|
+
},
|
|
1000
|
+
{
|
|
1001
|
+
name: "charger-kits",
|
|
1002
|
+
singular: "VE charger kit",
|
|
1003
|
+
basePath: "/charger/ve-charger-kits",
|
|
1004
|
+
idField: "idVEChargerKit",
|
|
1005
|
+
listFields: ["identifier", "charger", "price", "phaseNumber", "active"],
|
|
1006
|
+
filterPath: "/charger/ve-charger-kits/filter",
|
|
1007
|
+
batchDeletePath: "delete-batch-ve-charger-kits",
|
|
1008
|
+
putBodyOnly: true
|
|
1009
|
+
},
|
|
1010
|
+
{
|
|
1011
|
+
name: "heatpump-kits",
|
|
1012
|
+
singular: "heat pump kit",
|
|
1013
|
+
basePath: "/heat-pump/heatpump-kits",
|
|
1014
|
+
idField: "idHeatpumpKit",
|
|
1015
|
+
listFields: ["identifier", "heatpump", "price", "phaseNumber", "active"],
|
|
1016
|
+
filterPath: "/heat-pump/heatpump-kits/filter",
|
|
1017
|
+
batchDeletePath: "delete-batch-heatpump-kits",
|
|
1018
|
+
putBodyOnly: true
|
|
1019
|
+
}
|
|
1020
|
+
];
|
|
1021
|
+
function registerInventoryCommands(program2) {
|
|
1022
|
+
const inventory = program2.command("inventory").description("Manage inventory: panels, inverters, batteries, chargers, heatpumps, custom-assets, kits");
|
|
1023
|
+
registerManufacturersCommands(inventory);
|
|
1024
|
+
for (const resource of RESOURCES) {
|
|
1025
|
+
inventory.addCommand(createResourceCommands(resource));
|
|
1026
|
+
}
|
|
1027
|
+
registerCustomFieldsCommands(inventory);
|
|
1028
|
+
registerKitsCommands(inventory);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// src/commands/studies/builder.ts
|
|
1032
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, existsSync as existsSync2 } from "fs";
|
|
1033
|
+
|
|
1034
|
+
// node_modules/uuid/dist/esm-node/rng.js
|
|
1035
|
+
import crypto from "crypto";
|
|
1036
|
+
var rnds8Pool = new Uint8Array(256);
|
|
1037
|
+
var poolPtr = rnds8Pool.length;
|
|
1038
|
+
function rng() {
|
|
1039
|
+
if (poolPtr > rnds8Pool.length - 16) {
|
|
1040
|
+
crypto.randomFillSync(rnds8Pool);
|
|
1041
|
+
poolPtr = 0;
|
|
1042
|
+
}
|
|
1043
|
+
return rnds8Pool.slice(poolPtr, poolPtr += 16);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// node_modules/uuid/dist/esm-node/regex.js
|
|
1047
|
+
var regex_default = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;
|
|
1048
|
+
|
|
1049
|
+
// node_modules/uuid/dist/esm-node/validate.js
|
|
1050
|
+
function validate(uuid) {
|
|
1051
|
+
return typeof uuid === "string" && regex_default.test(uuid);
|
|
1052
|
+
}
|
|
1053
|
+
var validate_default = validate;
|
|
1054
|
+
|
|
1055
|
+
// node_modules/uuid/dist/esm-node/stringify.js
|
|
1056
|
+
var byteToHex = [];
|
|
1057
|
+
for (let i = 0; i < 256; ++i) {
|
|
1058
|
+
byteToHex.push((i + 256).toString(16).substr(1));
|
|
1059
|
+
}
|
|
1060
|
+
function stringify(arr, offset = 0) {
|
|
1061
|
+
const uuid = (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
|
|
1062
|
+
if (!validate_default(uuid)) {
|
|
1063
|
+
throw TypeError("Stringified UUID is invalid");
|
|
1064
|
+
}
|
|
1065
|
+
return uuid;
|
|
1066
|
+
}
|
|
1067
|
+
var stringify_default = stringify;
|
|
1068
|
+
|
|
1069
|
+
// node_modules/uuid/dist/esm-node/v4.js
|
|
1070
|
+
function v4(options, buf, offset) {
|
|
1071
|
+
options = options || {};
|
|
1072
|
+
const rnds = options.random || (options.rng || rng)();
|
|
1073
|
+
rnds[6] = rnds[6] & 15 | 64;
|
|
1074
|
+
rnds[8] = rnds[8] & 63 | 128;
|
|
1075
|
+
if (buf) {
|
|
1076
|
+
offset = offset || 0;
|
|
1077
|
+
for (let i = 0; i < 16; ++i) {
|
|
1078
|
+
buf[offset + i] = rnds[i];
|
|
1079
|
+
}
|
|
1080
|
+
return buf;
|
|
1081
|
+
}
|
|
1082
|
+
return stringify_default(rnds);
|
|
1083
|
+
}
|
|
1084
|
+
var v4_default = v4;
|
|
1085
|
+
|
|
1086
|
+
// src/commands/studies/builder.ts
|
|
1087
|
+
function createDefaultStudy(name) {
|
|
1088
|
+
const now = /* @__PURE__ */ new Date();
|
|
1089
|
+
const dateStr = now.toISOString().split("T")[0];
|
|
1090
|
+
return {
|
|
1091
|
+
id: v4_default(),
|
|
1092
|
+
surfaces: void 0,
|
|
1093
|
+
clientDetails: {
|
|
1094
|
+
identifier: void 0,
|
|
1095
|
+
sector: void 0,
|
|
1096
|
+
email: void 0,
|
|
1097
|
+
cups: void 0,
|
|
1098
|
+
phoneNumber: void 0,
|
|
1099
|
+
address: void 0,
|
|
1100
|
+
instalationLocation: void 0,
|
|
1101
|
+
region: void 0,
|
|
1102
|
+
subregion: void 0,
|
|
1103
|
+
addressCode: void 0,
|
|
1104
|
+
dniOrCif: void 0,
|
|
1105
|
+
includeTaxes: false,
|
|
1106
|
+
taxesPercentage: 0
|
|
1107
|
+
},
|
|
1108
|
+
location: void 0,
|
|
1109
|
+
atrTariff: void 0,
|
|
1110
|
+
geographicalZone: void 0,
|
|
1111
|
+
energyPrices: { units: "\u20AC/kWh" },
|
|
1112
|
+
contractedPower: {},
|
|
1113
|
+
powerPrices: { units: "\u20AC/kW \xB7 dia" },
|
|
1114
|
+
results: void 0,
|
|
1115
|
+
consumptionIntroductionMode: "periods",
|
|
1116
|
+
peakPowerIntroductionMode: "solarKit",
|
|
1117
|
+
instalationPhaseNumber: "single_phase",
|
|
1118
|
+
peakPowerOptimizationMethod: {
|
|
1119
|
+
maxNumberOfOverproductionMonths: 3
|
|
1120
|
+
},
|
|
1121
|
+
useAlternativePrices: false,
|
|
1122
|
+
economicResults: {
|
|
1123
|
+
guaranteeProductionPercentage: 85,
|
|
1124
|
+
inflation: 3,
|
|
1125
|
+
instalationLifeTime: 25,
|
|
1126
|
+
commercialFeePercentage: 85,
|
|
1127
|
+
instalationFinancingPercentage: 0,
|
|
1128
|
+
taxesPercentage: 21,
|
|
1129
|
+
includeTaxes: false,
|
|
1130
|
+
margen: 0
|
|
1131
|
+
},
|
|
1132
|
+
creationTimestamp: dateStr,
|
|
1133
|
+
solarStudyProgress: {
|
|
1134
|
+
stepsProgress: {
|
|
1135
|
+
clientDetails: void 0,
|
|
1136
|
+
consumption: void 0,
|
|
1137
|
+
economicBalance: void 0,
|
|
1138
|
+
production: void 0,
|
|
1139
|
+
results: void 0,
|
|
1140
|
+
surfacesSelector: void 0
|
|
1141
|
+
},
|
|
1142
|
+
stepsUpdates: {
|
|
1143
|
+
clientDetails: false,
|
|
1144
|
+
consumption: false,
|
|
1145
|
+
economicBalance: false,
|
|
1146
|
+
production: false,
|
|
1147
|
+
results: false,
|
|
1148
|
+
surfacesSelector: false
|
|
1149
|
+
},
|
|
1150
|
+
editingSolarStudy: false,
|
|
1151
|
+
lastStateUpdateMomentMS: now.getTime()
|
|
1152
|
+
},
|
|
1153
|
+
activeBatteries: false,
|
|
1154
|
+
batteriesConfiguration: {
|
|
1155
|
+
initialCapacityPercentage: 100,
|
|
1156
|
+
maxCapacityPercentage: 100,
|
|
1157
|
+
minCapacityPercentage: 0
|
|
1158
|
+
},
|
|
1159
|
+
layout: "v2",
|
|
1160
|
+
lastStateUpdateMomentMS: now.getTime(),
|
|
1161
|
+
name: name || `Estudio Solar ${dateStr}`,
|
|
1162
|
+
notes: void 0,
|
|
1163
|
+
market: "es"
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
function checkByPeriodCompletion(periodNumber, element) {
|
|
1167
|
+
if (!element) return false;
|
|
1168
|
+
let assigned = 0;
|
|
1169
|
+
for (let i = 1; i <= 6; i++) {
|
|
1170
|
+
const val = element["p" + i];
|
|
1171
|
+
if (val !== void 0 && val !== null && !isNaN(val)) {
|
|
1172
|
+
assigned++;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
return assigned >= periodNumber;
|
|
1176
|
+
}
|
|
1177
|
+
function evaluateSteps(study) {
|
|
1178
|
+
const missing = {};
|
|
1179
|
+
const atrTariff = study.atrTariff;
|
|
1180
|
+
const energyPrices = study.energyPrices;
|
|
1181
|
+
const geographicalZone = study.geographicalZone;
|
|
1182
|
+
const market = study.market;
|
|
1183
|
+
const extraCostPrice = study.extraCostPrice;
|
|
1184
|
+
let clientDetailsComplete = true;
|
|
1185
|
+
if (!atrTariff) {
|
|
1186
|
+
clientDetailsComplete = false;
|
|
1187
|
+
missing.clientDetails = "Needs atrTariff (use: studies set tariff)";
|
|
1188
|
+
} else if (!checkByPeriodCompletion(atrTariff.periods || 3, energyPrices)) {
|
|
1189
|
+
clientDetailsComplete = false;
|
|
1190
|
+
missing.clientDetails = `Needs energyPrices with ${atrTariff.periods || 3} periods filled (use: studies set prices)`;
|
|
1191
|
+
} else if (!geographicalZone) {
|
|
1192
|
+
clientDetailsComplete = false;
|
|
1193
|
+
missing.clientDetails = "Needs geographicalZone (use: studies set tariff --zone-id N)";
|
|
1194
|
+
} else if (market === "pt" && !checkByPeriodCompletion(1, extraCostPrice)) {
|
|
1195
|
+
clientDetailsComplete = false;
|
|
1196
|
+
missing.clientDetails = "PT market needs extraCostPrice with 1 period (use: studies set prices --extra-cost-p1 N)";
|
|
1197
|
+
}
|
|
1198
|
+
const consumption = study.consumption;
|
|
1199
|
+
const consumptionComplete = !!consumption?.days?.length;
|
|
1200
|
+
if (!consumptionComplete) {
|
|
1201
|
+
missing.consumption = "Needs consumption curve (use: studies set consumption)";
|
|
1202
|
+
}
|
|
1203
|
+
const surfaces = study.surfaces;
|
|
1204
|
+
const surfacesComplete = !!surfaces?.length;
|
|
1205
|
+
if (!surfacesComplete) {
|
|
1206
|
+
missing.surfacesSelector = "Needs at least one surface (use: studies add surface)";
|
|
1207
|
+
}
|
|
1208
|
+
const solarPanel = study.solarPanel;
|
|
1209
|
+
const solarKit = study.solarKit;
|
|
1210
|
+
const hasEquipment = !!solarPanel || !!solarKit;
|
|
1211
|
+
let hasProductionCurve = false;
|
|
1212
|
+
if (surfaces && surfaces.length > 0) {
|
|
1213
|
+
for (const s of surfaces) {
|
|
1214
|
+
const surf = s;
|
|
1215
|
+
if (surf.production?.days?.length) {
|
|
1216
|
+
hasProductionCurve = true;
|
|
1217
|
+
break;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
const productionComplete = hasProductionCurve && hasEquipment;
|
|
1222
|
+
if (!productionComplete) {
|
|
1223
|
+
const reasons = [];
|
|
1224
|
+
if (!hasEquipment) reasons.push("no panel or kit selected (use: studies set panel or studies set kit)");
|
|
1225
|
+
if (!hasProductionCurve) reasons.push("no surface has a production curve (use: studies calculate production)");
|
|
1226
|
+
missing.production = reasons.join("; ");
|
|
1227
|
+
}
|
|
1228
|
+
const results = study.results;
|
|
1229
|
+
const resultsComplete = results !== void 0;
|
|
1230
|
+
if (!resultsComplete) {
|
|
1231
|
+
missing.results = "Results not calculated (use: studies calculate results)";
|
|
1232
|
+
}
|
|
1233
|
+
const economicResults = study.economicResults;
|
|
1234
|
+
const economicBalanceComplete = economicResults?.margen !== void 0 && economicResults?.totalCost !== void 0 && results !== void 0;
|
|
1235
|
+
if (!economicBalanceComplete) {
|
|
1236
|
+
const reasons = [];
|
|
1237
|
+
if (economicResults?.margen === void 0) reasons.push("margen");
|
|
1238
|
+
if (economicResults?.totalCost === void 0) reasons.push("totalCost");
|
|
1239
|
+
if (!results) reasons.push("results");
|
|
1240
|
+
missing.economicBalance = `Needs: ${reasons.join(", ")} (use: studies set economics)`;
|
|
1241
|
+
}
|
|
1242
|
+
return {
|
|
1243
|
+
stepsProgress: {
|
|
1244
|
+
clientDetails: clientDetailsComplete,
|
|
1245
|
+
consumption: consumptionComplete,
|
|
1246
|
+
surfacesSelector: surfacesComplete,
|
|
1247
|
+
production: productionComplete,
|
|
1248
|
+
results: resultsComplete,
|
|
1249
|
+
economicBalance: economicBalanceComplete
|
|
1250
|
+
},
|
|
1251
|
+
missing
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
function calculateCompletionPercentage(stepsProgress) {
|
|
1255
|
+
const completed = Object.values(stepsProgress).filter(Boolean).length;
|
|
1256
|
+
return Math.round(completed / 6 * 100);
|
|
1257
|
+
}
|
|
1258
|
+
var CASCADE_RESETS = {
|
|
1259
|
+
consumption: ["production", "results", "economicBalance"],
|
|
1260
|
+
surfaces: ["production", "results", "economicBalance"]
|
|
1261
|
+
};
|
|
1262
|
+
function applyCascadeResets(study, changedField) {
|
|
1263
|
+
const resetSteps = CASCADE_RESETS[changedField];
|
|
1264
|
+
if (!resetSteps) return [];
|
|
1265
|
+
const progress = study.solarStudyProgress;
|
|
1266
|
+
const resets = [];
|
|
1267
|
+
for (const step of resetSteps) {
|
|
1268
|
+
if (progress.stepsProgress[step] !== void 0) {
|
|
1269
|
+
progress.stepsProgress[step] = void 0;
|
|
1270
|
+
resets.push(step);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
if (changedField === "consumption") {
|
|
1274
|
+
study.results = void 0;
|
|
1275
|
+
}
|
|
1276
|
+
if (changedField === "surfaces") {
|
|
1277
|
+
study.results = void 0;
|
|
1278
|
+
}
|
|
1279
|
+
return resets;
|
|
1280
|
+
}
|
|
1281
|
+
function readStudy(filePath) {
|
|
1282
|
+
if (!existsSync2(filePath)) {
|
|
1283
|
+
throw new Error(`Study file not found: ${filePath}. Use 'studies init --file ${filePath}' to create one.`);
|
|
1284
|
+
}
|
|
1285
|
+
return JSON.parse(readFileSync2(filePath, "utf-8"));
|
|
1286
|
+
}
|
|
1287
|
+
function writeStudy(filePath, study) {
|
|
1288
|
+
writeFileSync3(filePath, JSON.stringify(study, null, 2), "utf-8");
|
|
1289
|
+
}
|
|
1290
|
+
function updateStudy(filePath, updater) {
|
|
1291
|
+
const study = readStudy(filePath);
|
|
1292
|
+
const cascadeField = updater(study);
|
|
1293
|
+
let cascadeResets = [];
|
|
1294
|
+
if (cascadeField) {
|
|
1295
|
+
cascadeResets = applyCascadeResets(study, cascadeField);
|
|
1296
|
+
}
|
|
1297
|
+
const { stepsProgress, missing } = evaluateSteps(study);
|
|
1298
|
+
const progress = study.solarStudyProgress;
|
|
1299
|
+
progress.stepsProgress = stepsProgress;
|
|
1300
|
+
progress.lastStateUpdateMomentMS = Date.now();
|
|
1301
|
+
study.lastStateUpdateMomentMS = Date.now();
|
|
1302
|
+
writeStudy(filePath, study);
|
|
1303
|
+
return {
|
|
1304
|
+
stepsProgress,
|
|
1305
|
+
completionPercentage: calculateCompletionPercentage(stepsProgress),
|
|
1306
|
+
missing,
|
|
1307
|
+
cascadeResets
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
function deepMerge(target, source) {
|
|
1311
|
+
for (const key of Object.keys(source)) {
|
|
1312
|
+
const sv = source[key];
|
|
1313
|
+
const tv = target[key];
|
|
1314
|
+
if (sv && typeof sv === "object" && !Array.isArray(sv) && tv && typeof tv === "object" && !Array.isArray(tv)) {
|
|
1315
|
+
deepMerge(tv, sv);
|
|
1316
|
+
} else {
|
|
1317
|
+
target[key] = sv;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
function getGlobalOpts6(cmd) {
|
|
1322
|
+
let root = cmd;
|
|
1323
|
+
while (root.parent) root = root.parent;
|
|
1324
|
+
return root.opts();
|
|
1325
|
+
}
|
|
1326
|
+
function resolveFile(opts) {
|
|
1327
|
+
return opts.file || process.env.SUNTROPY_STUDY || "./study.json";
|
|
1328
|
+
}
|
|
1329
|
+
function registerStudyBuilderCommands(studies) {
|
|
1330
|
+
studies.command("init").description(
|
|
1331
|
+
'Create a new study workspace (local JSON file with defaults).\nExample: suntropy studies init --file /tmp/study.json --name "Residencial 5kW"'
|
|
1332
|
+
).option("--file <path>", "Output file path (default: ./study.json)").option("--name <studyName>", "Study name").option("--market <code>", "Market country: es, pt, it, fr, de, etc.", "es").action(async (opts) => {
|
|
1333
|
+
try {
|
|
1334
|
+
const filePath = resolveFile(opts);
|
|
1335
|
+
const study = createDefaultStudy(opts.name);
|
|
1336
|
+
if (opts.market) study.market = opts.market;
|
|
1337
|
+
writeStudy(filePath, study);
|
|
1338
|
+
const { stepsProgress, missing } = evaluateSteps(study);
|
|
1339
|
+
output({
|
|
1340
|
+
file: filePath,
|
|
1341
|
+
id: study.id,
|
|
1342
|
+
name: study.name,
|
|
1343
|
+
stepsProgress,
|
|
1344
|
+
completionPercentage: 0,
|
|
1345
|
+
missing
|
|
1346
|
+
}, getGlobalOpts6(studies));
|
|
1347
|
+
} catch (err) {
|
|
1348
|
+
outputError(err instanceof Error ? err : new Error(String(err)));
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1351
|
+
studies.command("validate").description(
|
|
1352
|
+
"Check completion status of all study steps.\nReturns stepsProgress, completionPercentage, and what is missing for each incomplete step."
|
|
1353
|
+
).option("--file <path>", "Study file path").action(async (opts) => {
|
|
1354
|
+
try {
|
|
1355
|
+
const study = readStudy(resolveFile(opts));
|
|
1356
|
+
const { stepsProgress, missing } = evaluateSteps(study);
|
|
1357
|
+
output({
|
|
1358
|
+
name: study.name,
|
|
1359
|
+
stepsProgress,
|
|
1360
|
+
completionPercentage: calculateCompletionPercentage(stepsProgress),
|
|
1361
|
+
missing
|
|
1362
|
+
}, getGlobalOpts6(studies));
|
|
1363
|
+
} catch (err) {
|
|
1364
|
+
outputError(err instanceof Error ? err : new Error(String(err)));
|
|
1365
|
+
}
|
|
1366
|
+
});
|
|
1367
|
+
studies.command("save").description(
|
|
1368
|
+
"Save study to the backend (POST /solar-study).\nIf the study has _id, it updates the existing study. Otherwise creates new.\nExample: suntropy studies save --file /tmp/study.json --state-id 1"
|
|
1369
|
+
).option("--file <path>", "Study file path").option("--state-id <n>", "Solar study metadata state ID").option("--credit-amount <n>", "Credit amount to consume").option("--save-as-new", "Force save as new study (even if _id exists)").action(async (opts) => {
|
|
1370
|
+
try {
|
|
1371
|
+
const filePath = resolveFile(opts);
|
|
1372
|
+
const global = getGlobalOpts6(studies);
|
|
1373
|
+
const study = readStudy(filePath);
|
|
1374
|
+
const { stepsProgress } = evaluateSteps(study);
|
|
1375
|
+
const progress = study.solarStudyProgress;
|
|
1376
|
+
progress.stepsProgress = stepsProgress;
|
|
1377
|
+
progress.lastStateUpdateMomentMS = Date.now();
|
|
1378
|
+
const isEditing = progress.editingSolarStudy || !!study._id;
|
|
1379
|
+
const comments = study.comments || [];
|
|
1380
|
+
const commentType = opts.saveAsNew ? "duplicated" : isEditing ? "modified" : "created";
|
|
1381
|
+
comments.push(createComment(commentType));
|
|
1382
|
+
study.comments = comments;
|
|
1383
|
+
if (opts.saveAsNew) {
|
|
1384
|
+
delete study._id;
|
|
1385
|
+
}
|
|
1386
|
+
const client = createServiceClient("solar", global);
|
|
1387
|
+
const params = {};
|
|
1388
|
+
if (opts.stateId) params.idSolarStudyState = opts.stateId;
|
|
1389
|
+
if (opts.creditAmount) params.creditAmount = opts.creditAmount;
|
|
1390
|
+
if (opts.saveAsNew) params.saveAsNew = "true";
|
|
1391
|
+
const queryString = Object.entries(params).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&");
|
|
1392
|
+
const url = `/solar-study${queryString ? "?" + queryString : ""}`;
|
|
1393
|
+
const res = await client.post(url, study);
|
|
1394
|
+
const responseData = res.data;
|
|
1395
|
+
if (responseData?.solarStudyId) {
|
|
1396
|
+
study._id = responseData.solarStudyId;
|
|
1397
|
+
progress.editingSolarStudy = true;
|
|
1398
|
+
writeStudy(filePath, study);
|
|
1399
|
+
}
|
|
1400
|
+
output({
|
|
1401
|
+
saved: true,
|
|
1402
|
+
metadata: responseData,
|
|
1403
|
+
file: filePath,
|
|
1404
|
+
completionPercentage: calculateCompletionPercentage(stepsProgress)
|
|
1405
|
+
}, global);
|
|
1406
|
+
} catch (err) {
|
|
1407
|
+
outputError(handleApiError(err));
|
|
1408
|
+
}
|
|
1409
|
+
});
|
|
1410
|
+
studies.command("pull <studyId>").description(
|
|
1411
|
+
"Download an existing study from the backend into a local file.\nExample: suntropy studies pull abc123 --file /tmp/study.json"
|
|
1412
|
+
).option("--file <path>", "Output file path").action(async (studyId, opts) => {
|
|
1413
|
+
try {
|
|
1414
|
+
const filePath = resolveFile(opts);
|
|
1415
|
+
const global = getGlobalOpts6(studies);
|
|
1416
|
+
const client = createServiceClient("solar", global);
|
|
1417
|
+
const res = await client.get(`/solar-study/findById/${studyId}`);
|
|
1418
|
+
const study = res.data;
|
|
1419
|
+
const { stepsProgress, missing } = evaluateSteps(study);
|
|
1420
|
+
const progress = study.solarStudyProgress || {};
|
|
1421
|
+
progress.stepsProgress = stepsProgress;
|
|
1422
|
+
progress.editingSolarStudy = true;
|
|
1423
|
+
study.solarStudyProgress = progress;
|
|
1424
|
+
writeStudy(filePath, study);
|
|
1425
|
+
output({
|
|
1426
|
+
file: filePath,
|
|
1427
|
+
_id: study._id,
|
|
1428
|
+
name: study.name,
|
|
1429
|
+
stepsProgress,
|
|
1430
|
+
completionPercentage: calculateCompletionPercentage(stepsProgress),
|
|
1431
|
+
missing
|
|
1432
|
+
}, global);
|
|
1433
|
+
} catch (err) {
|
|
1434
|
+
outputError(handleApiError(err));
|
|
1435
|
+
}
|
|
1436
|
+
});
|
|
1437
|
+
const set = studies.command("set").description("Set study properties. Each sub-command auto-validates after changes.");
|
|
1438
|
+
set.command("name").description("Set the study name").option("--file <path>", "Study file path").requiredOption("--name <studyName>", "Study name").action(async (opts) => {
|
|
1439
|
+
try {
|
|
1440
|
+
const result = updateStudy(resolveFile(opts), (study) => {
|
|
1441
|
+
study.name = opts.name;
|
|
1442
|
+
return void 0;
|
|
1443
|
+
});
|
|
1444
|
+
output(result, getGlobalOpts6(studies));
|
|
1445
|
+
} catch (err) {
|
|
1446
|
+
outputError(err instanceof Error ? err : new Error(String(err)));
|
|
1447
|
+
}
|
|
1448
|
+
});
|
|
1449
|
+
set.command("market").description("Set the market country code (es, pt, it, fr, de, etc.)").option("--file <path>", "Study file path").requiredOption("--market <code>", "Market country code").action(async (opts) => {
|
|
1450
|
+
try {
|
|
1451
|
+
const result = updateStudy(resolveFile(opts), (study) => {
|
|
1452
|
+
study.market = opts.market;
|
|
1453
|
+
return void 0;
|
|
1454
|
+
});
|
|
1455
|
+
output(result, getGlobalOpts6(studies));
|
|
1456
|
+
} catch (err) {
|
|
1457
|
+
outputError(err instanceof Error ? err : new Error(String(err)));
|
|
1458
|
+
}
|
|
1459
|
+
});
|
|
1460
|
+
set.command("client").description(
|
|
1461
|
+
'Set client details (name, email, phone, cups, address, etc.)\nExample: suntropy studies set client --file study.json --name "Juan Garc\xEDa" --email j@co.com --cups ES001234'
|
|
1462
|
+
).option("--file <path>", "Study file path").option("--name <clientName>", "Client name (identifier/raz\xF3n social)").option("--email <email>", "Client email").option("--phone <phone>", "Phone number").option("--cups <cups>", "CUPS code").option("--address <addr>", "Address").option("--city <city>", "City").option("--region <region>", "Region (comunidad aut\xF3noma)").option("--subregion <subregion>", "Subregion (provincia)").option("--dni <dni>", "DNI or CIF").option("--sector <sector>", "Client sector").option("--installation-location <loc>", "Installation location description").action(async (opts) => {
|
|
1463
|
+
try {
|
|
1464
|
+
const result = updateStudy(resolveFile(opts), (study) => {
|
|
1465
|
+
const cd = study.clientDetails || {};
|
|
1466
|
+
if (opts.name) cd.identifier = opts.name;
|
|
1467
|
+
if (opts.email) cd.email = opts.email;
|
|
1468
|
+
if (opts.phone) cd.phoneNumber = opts.phone;
|
|
1469
|
+
if (opts.cups) cd.cups = opts.cups;
|
|
1470
|
+
if (opts.address) cd.address = opts.address;
|
|
1471
|
+
if (opts.city) cd.city = opts.city;
|
|
1472
|
+
if (opts.region) cd.region = opts.region;
|
|
1473
|
+
if (opts.subregion) cd.subregion = opts.subregion;
|
|
1474
|
+
if (opts.dni) cd.dniOrCif = opts.dni;
|
|
1475
|
+
if (opts.sector) cd.sector = opts.sector;
|
|
1476
|
+
if (opts.installationLocation) cd.instalationLocation = opts.installationLocation;
|
|
1477
|
+
study.clientDetails = cd;
|
|
1478
|
+
return void 0;
|
|
1479
|
+
});
|
|
1480
|
+
output(result, getGlobalOpts6(studies));
|
|
1481
|
+
} catch (err) {
|
|
1482
|
+
outputError(err instanceof Error ? err : new Error(String(err)));
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
set.command("tariff").description(
|
|
1486
|
+
"Set ATR tariff and geographical zone. Auto-sets phase (>3 periods \u2192 three_phase).\nCommon tariff IDs (Spain): 13=2.0TD (3 periods), 14=3.0TD (6 periods)\nZone IDs (Spain): 1=Peninsula, 2=Canarias, 3=Baleares\nExample: suntropy studies set tariff --file study.json --tariff-id 13 --zone-id 1"
|
|
1487
|
+
).option("--file <path>", "Study file path").requiredOption("--tariff-id <n>", "ATR tariff ID").option("--zone-id <n>", "Geographical zone ID", "1").option("--market <code>", "Market code for tariff lookup", "es").action(async (opts) => {
|
|
1488
|
+
try {
|
|
1489
|
+
const global = getGlobalOpts6(studies);
|
|
1490
|
+
const client = createServiceClient("periods", global);
|
|
1491
|
+
const tariffRes = await client.get("/tarifas-atr", {
|
|
1492
|
+
params: { market: opts.market }
|
|
1493
|
+
});
|
|
1494
|
+
const tariffs = Array.isArray(tariffRes.data) ? tariffRes.data : tariffRes.data?.data || [];
|
|
1495
|
+
const tariffId = parseInt(opts.tariffId);
|
|
1496
|
+
const tariff = tariffs.find((t) => t.idTarifaATR === tariffId);
|
|
1497
|
+
if (!tariff) {
|
|
1498
|
+
outputError(new Error(`Tariff ID ${tariffId} not found. Available: ${tariffs.map((t) => `${t.idTarifaATR}=${t.nombre}`).join(", ")}`));
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
const zoneRes = await client.get("/zonas", {
|
|
1502
|
+
params: { market: opts.market }
|
|
1503
|
+
});
|
|
1504
|
+
const zones = Array.isArray(zoneRes.data) ? zoneRes.data : zoneRes.data?.data || [];
|
|
1505
|
+
const zoneId = parseInt(opts.zoneId);
|
|
1506
|
+
const zone = zones.find((z) => z.idZona === zoneId);
|
|
1507
|
+
const result = updateStudy(resolveFile(opts), (study) => {
|
|
1508
|
+
study.atrTariff = tariff;
|
|
1509
|
+
if (zone) study.geographicalZone = zone;
|
|
1510
|
+
const periods = tariff.periods || 3;
|
|
1511
|
+
study.instalationPhaseNumber = periods > 3 ? "three_phase" : "single_phase";
|
|
1512
|
+
return void 0;
|
|
1513
|
+
});
|
|
1514
|
+
output(result, global);
|
|
1515
|
+
} catch (err) {
|
|
1516
|
+
outputError(handleApiError(err));
|
|
1517
|
+
}
|
|
1518
|
+
});
|
|
1519
|
+
set.command("prices").description(
|
|
1520
|
+
`Set energy prices by period.
|
|
1521
|
+
Examples:
|
|
1522
|
+
suntropy studies set prices --file study.json --energy '{"p1":0.15,"p2":0.08,"p3":0.06}'
|
|
1523
|
+
suntropy studies set prices --file study.json --energy-p1 0.15 --energy-p2 0.08 --energy-p3 0.06
|
|
1524
|
+
suntropy studies set prices --file study.json --energy '{"p1":0.15,...}' --power '{"p1":40,...}' --contracted '{"p1":5.5,...}'`
|
|
1525
|
+
).option("--file <path>", "Study file path").option("--energy <json>", 'Energy prices JSON: {"p1":N,"p2":N,...}').option("--energy-p1 <n>", "Energy price period 1 (\u20AC/kWh)").option("--energy-p2 <n>", "Energy price period 2 (\u20AC/kWh)").option("--energy-p3 <n>", "Energy price period 3 (\u20AC/kWh)").option("--energy-p4 <n>", "Energy price period 4 (\u20AC/kWh)").option("--energy-p5 <n>", "Energy price period 5 (\u20AC/kWh)").option("--energy-p6 <n>", "Energy price period 6 (\u20AC/kWh)").option("--power <json>", 'Power prices JSON: {"p1":N,"p2":N,...} (\u20AC/kW\xB7day)').option("--contracted <json>", 'Contracted power JSON: {"p1":N,"p2":N,...} (kW)').option("--extra-cost-p1 <n>", "Extra cost price P1 (Portugal market)").action(async (opts) => {
|
|
1526
|
+
try {
|
|
1527
|
+
const result = updateStudy(resolveFile(opts), (study) => {
|
|
1528
|
+
const ep = study.energyPrices || { units: "\u20AC/kWh" };
|
|
1529
|
+
if (opts.energy) {
|
|
1530
|
+
const parsed = JSON.parse(opts.energy);
|
|
1531
|
+
Object.assign(ep, parsed);
|
|
1532
|
+
}
|
|
1533
|
+
for (let i = 1; i <= 6; i++) {
|
|
1534
|
+
const val = opts[`energyP${i}`];
|
|
1535
|
+
if (val !== void 0) ep[`p${i}`] = parseFloat(val);
|
|
1536
|
+
}
|
|
1537
|
+
study.energyPrices = ep;
|
|
1538
|
+
if (opts.power) {
|
|
1539
|
+
const pp = study.powerPrices || { units: "\u20AC/kW \xB7 dia" };
|
|
1540
|
+
Object.assign(pp, JSON.parse(opts.power));
|
|
1541
|
+
study.powerPrices = pp;
|
|
1542
|
+
}
|
|
1543
|
+
if (opts.contracted) {
|
|
1544
|
+
const cp = study.contractedPower || {};
|
|
1545
|
+
Object.assign(cp, JSON.parse(opts.contracted));
|
|
1546
|
+
study.contractedPower = cp;
|
|
1547
|
+
}
|
|
1548
|
+
if (opts.extraCostP1 !== void 0) {
|
|
1549
|
+
study.extraCostPrice = { p1: parseFloat(opts.extraCostP1), units: "\u20AC/kWh" };
|
|
1550
|
+
}
|
|
1551
|
+
return void 0;
|
|
1552
|
+
});
|
|
1553
|
+
output(result, getGlobalOpts6(studies));
|
|
1554
|
+
} catch (err) {
|
|
1555
|
+
outputError(err instanceof Error ? err : new Error(String(err)));
|
|
1556
|
+
}
|
|
1557
|
+
});
|
|
1558
|
+
set.command("consumption").description(
|
|
1559
|
+
`Set the consumption curve.
|
|
1560
|
+
Modes:
|
|
1561
|
+
--from-file <path> Load PowerCurve from JSON file
|
|
1562
|
+
--annual + --pattern Generate via consumption-estimation API
|
|
1563
|
+
--by-period <json> Period consumption: {"p1":N,"p2":N,"p3":N} + REE profile
|
|
1564
|
+
--monthly <json> Monthly consumption: {"1":N,"2":N,...,"12":N} + REE profile
|
|
1565
|
+
--monthly-by-period Monthly by period: {"1":{"p1":N,"p2":N},...} + REE profile
|
|
1566
|
+
|
|
1567
|
+
Examples:
|
|
1568
|
+
suntropy studies set consumption --file study.json --from-file /tmp/cons.json
|
|
1569
|
+
suntropy studies set consumption --file study.json --annual 4000 --pattern Balance
|
|
1570
|
+
suntropy studies set consumption --file study.json --by-period '{"p1":2500,"p2":1000,"p3":500}'
|
|
1571
|
+
suntropy studies set consumption --file study.json --monthly '{"1":350,"2":320,...}'`
|
|
1572
|
+
).option("--file <path>", "Study file path").option("--from-file <curvePath>", "Load PowerCurve from JSON file").option("--annual <kWh>", "Annual consumption in kWh").option("--pattern <name>", "Pattern: Balance, Nightly, Morning, Afternoon, Domestic, Commercial").option("--by-period <json>", 'Period consumption JSON: {"p1":N,"p2":N,...}').option("--monthly <json>", 'Monthly consumption JSON: {"1":N,...,"12":N}').option("--monthly-by-period <json>", 'Monthly by period: {"1":{"p1":N,...},...}').option("--tariff <code>", "Tariff code for profile lookup (default: from study)").option("--market <code>", "Market code (default: from study)").action(async (opts) => {
|
|
1573
|
+
try {
|
|
1574
|
+
const global = getGlobalOpts6(studies);
|
|
1575
|
+
const filePath = resolveFile(opts);
|
|
1576
|
+
const study = readStudy(filePath);
|
|
1577
|
+
const studyTariff = study.atrTariff?.nombre || "3.0TD";
|
|
1578
|
+
const studyMarket = study.market || "es";
|
|
1579
|
+
const tariff = opts.tariff || studyTariff;
|
|
1580
|
+
const market = opts.market || studyMarket;
|
|
1581
|
+
const year = (/* @__PURE__ */ new Date()).getFullYear();
|
|
1582
|
+
let curveData;
|
|
1583
|
+
let introductionMode;
|
|
1584
|
+
if (opts.fromFile) {
|
|
1585
|
+
curveData = JSON.parse(readFileSync2(opts.fromFile, "utf-8"));
|
|
1586
|
+
introductionMode = "upload";
|
|
1587
|
+
} else if (opts.annual && opts.pattern) {
|
|
1588
|
+
const profilesClient = createServiceClient("profiles", global);
|
|
1589
|
+
const params = {
|
|
1590
|
+
startDate: `${year}-01-01`,
|
|
1591
|
+
endDate: `${year}-12-31`,
|
|
1592
|
+
tariff,
|
|
1593
|
+
type: "Final",
|
|
1594
|
+
anualConsumption: opts.annual,
|
|
1595
|
+
market,
|
|
1596
|
+
consumptionType: opts.pattern
|
|
1597
|
+
};
|
|
1598
|
+
const queryString = Object.entries(params).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&");
|
|
1599
|
+
const res = await profilesClient.post(`/consumption-estimation?${queryString}`, {});
|
|
1600
|
+
curveData = res.data;
|
|
1601
|
+
introductionMode = "consumption_patterns";
|
|
1602
|
+
} else if (opts.byPeriod) {
|
|
1603
|
+
const periodValues = JSON.parse(opts.byPeriod);
|
|
1604
|
+
curveData = await generateFromProfile(global, tariff, market, year, "CONSUMPTION_BY_PERIOD", periodValues);
|
|
1605
|
+
introductionMode = "periods";
|
|
1606
|
+
} else if (opts.monthly) {
|
|
1607
|
+
const monthValues = JSON.parse(opts.monthly);
|
|
1608
|
+
curveData = await generateFromProfile(global, tariff, market, year, "MONTH_CONSUMPTION", monthValues);
|
|
1609
|
+
introductionMode = "monthly_consumption";
|
|
1610
|
+
} else if (opts.monthlyByPeriod) {
|
|
1611
|
+
const monthPeriodValues = JSON.parse(opts.monthlyByPeriod);
|
|
1612
|
+
curveData = await generateFromProfile(global, tariff, market, year, "MONTHLY_BY_PERIOD", monthPeriodValues);
|
|
1613
|
+
introductionMode = "monthly_periods";
|
|
1614
|
+
} else {
|
|
1615
|
+
outputError(new Error("Specify one of: --from-file, --annual + --pattern, --by-period, --monthly, --monthly-by-period"));
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
const result = updateStudy(filePath, (s) => {
|
|
1619
|
+
s.consumption = curveData;
|
|
1620
|
+
s.consumptionIntroductionMode = introductionMode;
|
|
1621
|
+
if (opts.annual) s.monthlyConsumption = parseFloat(opts.annual) / 12;
|
|
1622
|
+
if (opts.pattern) s.selectedConsumptionPattern = opts.pattern;
|
|
1623
|
+
if (opts.byPeriod) s.periodConsumptionDictionary = JSON.parse(opts.byPeriod);
|
|
1624
|
+
if (opts.monthly) s.monthConsumptionDictionary = JSON.parse(opts.monthly);
|
|
1625
|
+
if (opts.monthlyByPeriod) s.monthPeriodConsumptionDictionary = JSON.parse(opts.monthlyByPeriod);
|
|
1626
|
+
return "consumption";
|
|
1627
|
+
});
|
|
1628
|
+
output(result, global);
|
|
1629
|
+
} catch (err) {
|
|
1630
|
+
outputError(handleApiError(err));
|
|
1631
|
+
}
|
|
1632
|
+
});
|
|
1633
|
+
studies.command("add").description("Add elements to the study").command("surface").description(
|
|
1634
|
+
"Add a solar surface to the study.\nExample: suntropy studies add surface --file study.json --lat 37.39 --lon -5.99 --angle 30 --azimuth 180 --power 5000"
|
|
1635
|
+
).option("--file <path>", "Study file path").requiredOption("--lat <n>", "Latitude").requiredOption("--lon <n>", "Longitude").option("--angle <n>", "Panel inclination degrees", "30").option("--azimuth <n>", "Panel azimuth degrees (0=N, 180=S)", "180").option("--power <w>", "Installed power in Watts").option("--panels-count <n>", "Number of panels").option("--production <file>", "Production PowerCurve JSON file to attach").option("--identifier <name>", "Surface name/identifier").action(async (opts) => {
|
|
1636
|
+
try {
|
|
1637
|
+
const result = updateStudy(resolveFile(opts), (study) => {
|
|
1638
|
+
const lat = parseFloat(opts.lat);
|
|
1639
|
+
const lng = parseFloat(opts.lon);
|
|
1640
|
+
const surface = {
|
|
1641
|
+
surfaceId: v4_default(),
|
|
1642
|
+
name: opts.identifier || `Surface ${(study.surfaces?.length || 0) + 1}`,
|
|
1643
|
+
inclination: parseFloat(opts.angle),
|
|
1644
|
+
orientation: parseFloat(opts.azimuth),
|
|
1645
|
+
panelInclination: parseFloat(opts.angle),
|
|
1646
|
+
panelOrientation: parseFloat(opts.azimuth),
|
|
1647
|
+
lossesPercentage: 14,
|
|
1648
|
+
// polygonPath required by backend metadata constructor
|
|
1649
|
+
polygonPath: [{ lat, lng }]
|
|
1650
|
+
};
|
|
1651
|
+
if (opts.power) surface.installedPower = parseFloat(opts.power);
|
|
1652
|
+
if (opts.panelsCount) surface.panelNumber = parseInt(opts.panelsCount);
|
|
1653
|
+
if (opts.production) {
|
|
1654
|
+
surface.production = JSON.parse(readFileSync2(opts.production, "utf-8"));
|
|
1655
|
+
}
|
|
1656
|
+
study.location = { lat: parseFloat(opts.lat), lng: parseFloat(opts.lon) };
|
|
1657
|
+
study.mapCenter = { lat: parseFloat(opts.lat), lng: parseFloat(opts.lon) };
|
|
1658
|
+
const surfaces = study.surfaces || [];
|
|
1659
|
+
surfaces.push(surface);
|
|
1660
|
+
study.surfaces = surfaces;
|
|
1661
|
+
return "surfaces";
|
|
1662
|
+
});
|
|
1663
|
+
output(result, getGlobalOpts6(studies));
|
|
1664
|
+
} catch (err) {
|
|
1665
|
+
outputError(err instanceof Error ? err : new Error(String(err)));
|
|
1666
|
+
}
|
|
1667
|
+
});
|
|
1668
|
+
studies.command("remove").description("Remove elements from the study").command("surface").description("Remove a surface by index").option("--file <path>", "Study file path").requiredOption("--index <n>", "Surface index (0-based)").action(async (opts) => {
|
|
1669
|
+
try {
|
|
1670
|
+
const result = updateStudy(resolveFile(opts), (study) => {
|
|
1671
|
+
const surfaces = study.surfaces || [];
|
|
1672
|
+
const idx = parseInt(opts.index);
|
|
1673
|
+
if (idx < 0 || idx >= surfaces.length) {
|
|
1674
|
+
throw new Error(`Surface index ${idx} out of range (0-${surfaces.length - 1})`);
|
|
1675
|
+
}
|
|
1676
|
+
surfaces.splice(idx, 1);
|
|
1677
|
+
study.surfaces = surfaces.length > 0 ? surfaces : void 0;
|
|
1678
|
+
return "surfaces";
|
|
1679
|
+
});
|
|
1680
|
+
output(result, getGlobalOpts6(studies));
|
|
1681
|
+
} catch (err) {
|
|
1682
|
+
outputError(err instanceof Error ? err : new Error(String(err)));
|
|
1683
|
+
}
|
|
1684
|
+
});
|
|
1685
|
+
set.command("panel").description(
|
|
1686
|
+
'Set solar panel for the study. Auto-sets peakPowerIntroductionMode to "solarPanel".\nFetches full panel data from inventory.\nExample: suntropy studies set panel --file study.json --panel-id 456 --panels-count 12'
|
|
1687
|
+
).option("--file <path>", "Study file path").requiredOption("--panel-id <n>", "Solar panel ID from inventory").option("--panels-count <n>", "Number of panels").action(async (opts) => {
|
|
1688
|
+
try {
|
|
1689
|
+
const global = getGlobalOpts6(studies);
|
|
1690
|
+
const solarClient = createServiceClient("solar", global);
|
|
1691
|
+
const panelRes = await solarClient.get(`/solar-panels/${opts.panelId}`);
|
|
1692
|
+
const panel = panelRes.data;
|
|
1693
|
+
const result = updateStudy(resolveFile(opts), (study) => {
|
|
1694
|
+
study.solarPanel = panel;
|
|
1695
|
+
study.peakPowerIntroductionMode = "solarPanel";
|
|
1696
|
+
study.solarKit = void 0;
|
|
1697
|
+
if (opts.panelsCount) {
|
|
1698
|
+
const surfaces = study.surfaces;
|
|
1699
|
+
if (surfaces?.length) {
|
|
1700
|
+
surfaces[0].panelNumber = parseInt(opts.panelsCount);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
return void 0;
|
|
1704
|
+
});
|
|
1705
|
+
output(result, global);
|
|
1706
|
+
} catch (err) {
|
|
1707
|
+
outputError(handleApiError(err));
|
|
1708
|
+
}
|
|
1709
|
+
});
|
|
1710
|
+
set.command("kit").description(
|
|
1711
|
+
'Set solar kit for the study. Auto-sets peakPowerIntroductionMode to "solarKit".\nFetches full kit data from inventory.\nExample: suntropy studies set kit --file study.json --kit-id 123'
|
|
1712
|
+
).option("--file <path>", "Study file path").requiredOption("--kit-id <n>", "Solar kit ID from inventory").action(async (opts) => {
|
|
1713
|
+
try {
|
|
1714
|
+
const global = getGlobalOpts6(studies);
|
|
1715
|
+
const solarClient = createServiceClient("solar", global);
|
|
1716
|
+
const kitsRes = await solarClient.get("/solar-kits", { params: { unactive: true } });
|
|
1717
|
+
const allKits = Array.isArray(kitsRes.data) && Array.isArray(kitsRes.data[0]) ? kitsRes.data[0] : Array.isArray(kitsRes.data) ? kitsRes.data : kitsRes.data?.data || [];
|
|
1718
|
+
const kitId = parseInt(opts.kitId);
|
|
1719
|
+
const kit = allKits.find((k) => k.idSolarKit === kitId);
|
|
1720
|
+
if (!kit) {
|
|
1721
|
+
outputError(new Error(`Kit ${kitId} not found`));
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
const result = updateStudy(resolveFile(opts), (study) => {
|
|
1725
|
+
study.solarKit = kit;
|
|
1726
|
+
study.peakPowerIntroductionMode = "solarKit";
|
|
1727
|
+
study.solarPanel = void 0;
|
|
1728
|
+
study.solarInverters = void 0;
|
|
1729
|
+
return void 0;
|
|
1730
|
+
});
|
|
1731
|
+
output(result, global);
|
|
1732
|
+
} catch (err) {
|
|
1733
|
+
outputError(handleApiError(err));
|
|
1734
|
+
}
|
|
1735
|
+
});
|
|
1736
|
+
set.command("inverter").description(
|
|
1737
|
+
"Set inverter(s) for the study (when using solarPanel mode).\nExample: suntropy studies set inverter --file study.json --inverter-id 789"
|
|
1738
|
+
).option("--file <path>", "Study file path").requiredOption("--inverter-id <ids>", "Inverter ID(s), comma-separated for multiple").action(async (opts) => {
|
|
1739
|
+
try {
|
|
1740
|
+
const global = getGlobalOpts6(studies);
|
|
1741
|
+
const solarClient = createServiceClient("solar", global);
|
|
1742
|
+
const ids = opts.inverterId.split(",").map((s) => parseInt(s.trim()));
|
|
1743
|
+
const inverters = [];
|
|
1744
|
+
for (const id of ids) {
|
|
1745
|
+
const res = await solarClient.get(`/solar-inverter/${id}`);
|
|
1746
|
+
inverters.push(res.data);
|
|
1747
|
+
}
|
|
1748
|
+
const result = updateStudy(resolveFile(opts), (study) => {
|
|
1749
|
+
study.solarInverters = inverters;
|
|
1750
|
+
return void 0;
|
|
1751
|
+
});
|
|
1752
|
+
output(result, global);
|
|
1753
|
+
} catch (err) {
|
|
1754
|
+
outputError(handleApiError(err));
|
|
1755
|
+
}
|
|
1756
|
+
});
|
|
1757
|
+
set.command("phase").description("Manually set installation phase (overrides auto-set from tariff)").option("--file <path>", "Study file path").requiredOption("--phase <type>", "Phase: single_phase or three_phase").action(async (opts) => {
|
|
1758
|
+
try {
|
|
1759
|
+
if (!["single_phase", "three_phase"].includes(opts.phase)) {
|
|
1760
|
+
outputError(new Error("Phase must be single_phase or three_phase"));
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
const result = updateStudy(resolveFile(opts), (study) => {
|
|
1764
|
+
study.instalationPhaseNumber = opts.phase;
|
|
1765
|
+
return void 0;
|
|
1766
|
+
});
|
|
1767
|
+
output(result, getGlobalOpts6(studies));
|
|
1768
|
+
} catch (err) {
|
|
1769
|
+
outputError(err instanceof Error ? err : new Error(String(err)));
|
|
1770
|
+
}
|
|
1771
|
+
});
|
|
1772
|
+
set.command("economics").description(
|
|
1773
|
+
"Set economic parameters for the study.\nExample: suntropy studies set economics --file study.json --margin 15 --total-cost 6500 --lifetime 25"
|
|
1774
|
+
).option("--file <path>", "Study file path").option("--margin <n>", "Margin percentage").option("--total-cost <n>", "Total installation cost (\u20AC)").option("--lifetime <n>", "Installation lifetime in years").option("--inflation <n>", "Inflation rate percentage").option("--taxes-pct <n>", "Tax percentage").option("--include-taxes", "Include taxes in pricing").option("--commercial-fee <n>", "Commercial fee percentage").option("--excesses-mode <mode>", "Excesses compensation: gridSelling, PPA, noInjection, virtualBattery").option("--excesses-buy-price <n>", "Excesses buy price (\u20AC/MWh)").option("--excesses-selling-price <n>", "Excesses selling price (\u20AC/MWh)").option("--peak-power-cost <n>", "Cost per kWp (\u20AC/kWp)").option("--guarantee-production <n>", "Guarantee production percentage").action(async (opts) => {
|
|
1775
|
+
try {
|
|
1776
|
+
const result = updateStudy(resolveFile(opts), (study) => {
|
|
1777
|
+
const er = study.economicResults || {};
|
|
1778
|
+
if (opts.margin !== void 0) er.margen = parseFloat(opts.margin);
|
|
1779
|
+
if (opts.totalCost !== void 0) er.totalCost = parseFloat(opts.totalCost);
|
|
1780
|
+
if (opts.lifetime !== void 0) er.instalationLifeTime = parseInt(opts.lifetime);
|
|
1781
|
+
if (opts.inflation !== void 0) er.inflation = parseFloat(opts.inflation);
|
|
1782
|
+
if (opts.taxesPct !== void 0) er.taxesPercentage = parseFloat(opts.taxesPct);
|
|
1783
|
+
if (opts.includeTaxes) er.includeTaxes = true;
|
|
1784
|
+
if (opts.commercialFee !== void 0) er.commercialFeePercentage = parseFloat(opts.commercialFee);
|
|
1785
|
+
if (opts.excessesMode) er.excessesCompensationMode = opts.excessesMode;
|
|
1786
|
+
if (opts.excessesBuyPrice !== void 0) er.excessesBuyPrice = parseFloat(opts.excessesBuyPrice);
|
|
1787
|
+
if (opts.excessesSellingPrice !== void 0) er.excessesSellingPrice = parseFloat(opts.excessesSellingPrice);
|
|
1788
|
+
if (opts.peakPowerCost !== void 0) er.peakPowerCost = parseFloat(opts.peakPowerCost);
|
|
1789
|
+
if (opts.guaranteeProduction !== void 0) er.guaranteeProductionPercentage = parseFloat(opts.guaranteeProduction);
|
|
1790
|
+
study.economicResults = er;
|
|
1791
|
+
return void 0;
|
|
1792
|
+
});
|
|
1793
|
+
output(result, getGlobalOpts6(studies));
|
|
1794
|
+
} catch (err) {
|
|
1795
|
+
outputError(err instanceof Error ? err : new Error(String(err)));
|
|
1796
|
+
}
|
|
1797
|
+
});
|
|
1798
|
+
set.command("custom-assets").description(
|
|
1799
|
+
"Set selected custom assets for the study.\nExample: suntropy studies set custom-assets --file study.json --asset 100:12 --asset 200:1"
|
|
1800
|
+
).option("--file <path>", "Study file path").requiredOption("--asset <id:qty>", "Custom asset as id:quantity (repeatable)", collectAssets, []).action(async (opts) => {
|
|
1801
|
+
try {
|
|
1802
|
+
const global = getGlobalOpts6(studies);
|
|
1803
|
+
const solarClient = createServiceClient("solar", global);
|
|
1804
|
+
const assets = [];
|
|
1805
|
+
for (const { id, quantity } of opts.asset) {
|
|
1806
|
+
const res = await solarClient.get(`/custom-asset/id/${id}`);
|
|
1807
|
+
assets.push({ customAsset: res.data, quantity });
|
|
1808
|
+
}
|
|
1809
|
+
const result = updateStudy(resolveFile(opts), (study) => {
|
|
1810
|
+
study.selectedCustomAssets = assets;
|
|
1811
|
+
return void 0;
|
|
1812
|
+
});
|
|
1813
|
+
output(result, global);
|
|
1814
|
+
} catch (err) {
|
|
1815
|
+
outputError(handleApiError(err));
|
|
1816
|
+
}
|
|
1817
|
+
});
|
|
1818
|
+
set.command("batteries").description(
|
|
1819
|
+
"Enable/disable batteries and set configuration.\nExample: suntropy studies set batteries --file study.json --enable --battery-id 789 --count 1"
|
|
1820
|
+
).option("--file <path>", "Study file path").option("--enable", "Enable batteries").option("--disable", "Disable batteries").option("--battery-id <n>", "Battery ID from inventory").option("--count <n>", "Number of batteries").action(async (opts) => {
|
|
1821
|
+
try {
|
|
1822
|
+
const result = updateStudy(resolveFile(opts), (study) => {
|
|
1823
|
+
if (opts.enable) study.activeBatteries = true;
|
|
1824
|
+
if (opts.disable) study.activeBatteries = false;
|
|
1825
|
+
if (opts.batteryId) {
|
|
1826
|
+
const config = study.batteriesConfiguration || {};
|
|
1827
|
+
config.batteryId = parseInt(opts.batteryId);
|
|
1828
|
+
if (opts.count) config.batteriesNumber = parseInt(opts.count);
|
|
1829
|
+
study.batteriesConfiguration = config;
|
|
1830
|
+
}
|
|
1831
|
+
return void 0;
|
|
1832
|
+
});
|
|
1833
|
+
output(result, getGlobalOpts6(studies));
|
|
1834
|
+
} catch (err) {
|
|
1835
|
+
outputError(err instanceof Error ? err : new Error(String(err)));
|
|
1836
|
+
}
|
|
1837
|
+
});
|
|
1838
|
+
set.command("data").description(
|
|
1839
|
+
`Generic JSON merge into the study (fallback for any field).
|
|
1840
|
+
Example: suntropy studies set data --file study.json --data '{"referenceId":"REF-001"}'`
|
|
1841
|
+
).option("--file <path>", "Study file path").requiredOption("--data <json>", "JSON to merge into the study").action(async (opts) => {
|
|
1842
|
+
try {
|
|
1843
|
+
const parsed = JSON.parse(opts.data);
|
|
1844
|
+
const cascadeFields = Object.keys(parsed);
|
|
1845
|
+
let cascadeField;
|
|
1846
|
+
if (cascadeFields.includes("consumption")) cascadeField = "consumption";
|
|
1847
|
+
if (cascadeFields.includes("surfaces")) cascadeField = "surfaces";
|
|
1848
|
+
const result = updateStudy(resolveFile(opts), (study) => {
|
|
1849
|
+
deepMerge(study, parsed);
|
|
1850
|
+
return cascadeField;
|
|
1851
|
+
});
|
|
1852
|
+
output(result, getGlobalOpts6(studies));
|
|
1853
|
+
} catch (err) {
|
|
1854
|
+
outputError(err instanceof Error ? err : new Error(String(err)));
|
|
1855
|
+
}
|
|
1856
|
+
});
|
|
1857
|
+
studies.command("calculate").description("Calculate derived data for the study").command("production").description(
|
|
1858
|
+
"Calculate production for study surfaces using backend API.\nExample: suntropy studies calculate production --file study.json [--surface-index 0 | --all-surfaces]"
|
|
1859
|
+
).option("--file <path>", "Study file path").option("--surface-index <n>", "Calculate for specific surface index").option("--all-surfaces", "Calculate for all surfaces").option("--losses <n>", "Losses percentage", "14").action(async (opts) => {
|
|
1860
|
+
try {
|
|
1861
|
+
const global = getGlobalOpts6(studies);
|
|
1862
|
+
const filePath = resolveFile(opts);
|
|
1863
|
+
const study = readStudy(filePath);
|
|
1864
|
+
const surfaces = study.surfaces;
|
|
1865
|
+
if (!surfaces?.length) {
|
|
1866
|
+
outputError(new Error("No surfaces in study. Use: studies add surface"));
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
const solarClient = createServiceClient("solar", global);
|
|
1870
|
+
const indicesToCalc = opts.surfaceIndex !== void 0 ? [parseInt(opts.surfaceIndex)] : Array.from({ length: surfaces.length }, (_, i) => i);
|
|
1871
|
+
for (const idx of indicesToCalc) {
|
|
1872
|
+
const surface = surfaces[idx];
|
|
1873
|
+
if (!surface) continue;
|
|
1874
|
+
const center = surface.center;
|
|
1875
|
+
const studyLocation = study.location;
|
|
1876
|
+
const lat = center?.lat || studyLocation?.lat;
|
|
1877
|
+
const lng = center?.lng || studyLocation?.lng;
|
|
1878
|
+
if (!lat || !lng) {
|
|
1879
|
+
outputError(new Error(`Surface ${idx} has no coordinates. Use: studies add surface --lat N --lon N`));
|
|
1880
|
+
continue;
|
|
1881
|
+
}
|
|
1882
|
+
const body = {
|
|
1883
|
+
lat,
|
|
1884
|
+
long: lng,
|
|
1885
|
+
instaledPower: surface.installedPower || 5e3,
|
|
1886
|
+
angle: surface.inclination || surface.angle || 30,
|
|
1887
|
+
azimuth: surface.orientation || surface.azimuth || 180,
|
|
1888
|
+
lossesPercentage: parseFloat(opts.losses),
|
|
1889
|
+
year: (/* @__PURE__ */ new Date()).getFullYear(),
|
|
1890
|
+
isOptimized: false
|
|
1891
|
+
};
|
|
1892
|
+
const res = await solarClient.post("/solar-study/calculateProduction", body);
|
|
1893
|
+
surface.production = res.data;
|
|
1894
|
+
}
|
|
1895
|
+
const result = updateStudy(filePath, (s) => {
|
|
1896
|
+
s.surfaces = surfaces;
|
|
1897
|
+
return void 0;
|
|
1898
|
+
});
|
|
1899
|
+
output(result, global);
|
|
1900
|
+
} catch (err) {
|
|
1901
|
+
outputError(handleApiError(err));
|
|
1902
|
+
}
|
|
1903
|
+
});
|
|
1904
|
+
studies.command("calculate-results").description(
|
|
1905
|
+
"Calculate energy results replicating frontend SolarResultCalculator.\nComputes: net consumption, excesses, spending/savings by period, coverage.\nRequires: consumption, production, energyPrices, atrTariff.\nExample: suntropy studies calculate-results --file study.json"
|
|
1906
|
+
).option("--file <path>", "Study file path").action(async (opts) => {
|
|
1907
|
+
try {
|
|
1908
|
+
const global = getGlobalOpts6(studies);
|
|
1909
|
+
const filePath = resolveFile(opts);
|
|
1910
|
+
const study = readStudy(filePath);
|
|
1911
|
+
const consumption = study.consumption;
|
|
1912
|
+
if (!consumption?.days?.length) {
|
|
1913
|
+
outputError(new Error("No consumption curve. Use: studies set consumption"));
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
const surfaces = study.surfaces;
|
|
1917
|
+
if (!surfaces?.length) {
|
|
1918
|
+
outputError(new Error("No surfaces. Use: studies add surface"));
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
const energyPrices = study.energyPrices;
|
|
1922
|
+
if (!energyPrices) {
|
|
1923
|
+
outputError(new Error("No energy prices. Use: studies set prices"));
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
const { PowerCurve } = await import("energy-types/lib/energy/classes/powerCurve.class");
|
|
1927
|
+
const consCurve = new PowerCurve(consumption.days, false, consumption.identifier || "consumption", false);
|
|
1928
|
+
let totalProdCurve = null;
|
|
1929
|
+
for (const surf of surfaces) {
|
|
1930
|
+
const prod = surf.production;
|
|
1931
|
+
if (prod?.days?.length) {
|
|
1932
|
+
const surfProd = new PowerCurve(prod.days, false, prod.identifier || "production", false);
|
|
1933
|
+
totalProdCurve = totalProdCurve ? totalProdCurve.aggregatePowerCurve(surfProd) : surfProd;
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
if (!totalProdCurve) {
|
|
1937
|
+
outputError(new Error("No production curves found. Use: studies calculate production"));
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
const results = {};
|
|
1941
|
+
const netConsumptionCurve = consCurve.aggregatePowerCurve(totalProdCurve.applyMultiplier(-1));
|
|
1942
|
+
results.netConsumption = netConsumptionCurve;
|
|
1943
|
+
results.totalProduction = totalProdCurve.getTotalAcumulate();
|
|
1944
|
+
results.totalConsumptionCoverage = results.totalProduction / consCurve.getTotalAcumulate() * 100;
|
|
1945
|
+
const periodDistribution = await fetchPeriodDistribution(study, global);
|
|
1946
|
+
if (periodDistribution) {
|
|
1947
|
+
const clientDetails = study.clientDetails;
|
|
1948
|
+
const market = study.market;
|
|
1949
|
+
const oneBaseEnergyDiscountFactor = 1 - (study.energyPricesDiscount || 0) / 100;
|
|
1950
|
+
const oneBasePowerDiscountFactor = 1 - (study.powerPricesDiscount || 0) / 100;
|
|
1951
|
+
const positiveNetConsumptionCurve = netConsumptionCurve.filterNegativeValues();
|
|
1952
|
+
const positiveNetByPeriod = positiveNetConsumptionCurve.aggregateByPeriod(periodDistribution);
|
|
1953
|
+
const rawConsumptionByPeriod = consCurve.aggregateByPeriod(periodDistribution);
|
|
1954
|
+
const contractedPower = study.contractedPower || {};
|
|
1955
|
+
const powerPrices = study.powerPrices || {};
|
|
1956
|
+
const contractedPowerRawCostByPeriod = {};
|
|
1957
|
+
if (study.useAlternativePrices) {
|
|
1958
|
+
for (let i = 1; i <= 6; i++) {
|
|
1959
|
+
const p = `p${i}`;
|
|
1960
|
+
if (contractedPower[p] && powerPrices[p]) {
|
|
1961
|
+
contractedPowerRawCostByPeriod[p] = contractedPower[p] * 365 * powerPrices[p] * oneBasePowerDiscountFactor;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
if (market === "pt") {
|
|
1966
|
+
const extraCostPrice = study.extraCostPrice || {};
|
|
1967
|
+
let numberOfP1hours = 0;
|
|
1968
|
+
periodDistribution.forEach((day) => {
|
|
1969
|
+
Object.values(day.valuesList).forEach((hour) => {
|
|
1970
|
+
if (hour === 1) numberOfP1hours++;
|
|
1971
|
+
});
|
|
1972
|
+
});
|
|
1973
|
+
const coefficient = 1 / numberOfP1hours * (extraCostPrice.p1 || 0) * netConsumptionCurve.days.length;
|
|
1974
|
+
results.rawPeakConsumptionCostPt = (rawConsumptionByPeriod?.p1 || 0) * coefficient;
|
|
1975
|
+
results.netPeakConsumptionCostPt = (positiveNetByPeriod?.p1 || 0) * coefficient;
|
|
1976
|
+
}
|
|
1977
|
+
const totalRawSpendingByPeriod = {};
|
|
1978
|
+
for (let i = 1; i <= 6; i++) {
|
|
1979
|
+
const p = `p${i}`;
|
|
1980
|
+
const rawConsP = rawConsumptionByPeriod?.[p] || 0;
|
|
1981
|
+
const priceP = energyPrices[p] || 0;
|
|
1982
|
+
const includeTaxes = clientDetails?.includeTaxes;
|
|
1983
|
+
const taxPct = clientDetails?.taxesPercentage || 0;
|
|
1984
|
+
const priceWithTax = includeTaxes && taxPct ? priceP * (1 + taxPct / 100) : priceP;
|
|
1985
|
+
totalRawSpendingByPeriod[p] = rawConsP * priceWithTax * oneBaseEnergyDiscountFactor + (contractedPowerRawCostByPeriod[p] || 0);
|
|
1986
|
+
}
|
|
1987
|
+
results.totalRawSpendingByPeriod = totalRawSpendingByPeriod;
|
|
1988
|
+
results.totalRawSpending = Object.values(totalRawSpendingByPeriod).filter((v) => typeof v === "number").reduce((a, b) => a + b, 0) + (results.rawPeakConsumptionCostPt || 0);
|
|
1989
|
+
const alternativeEnergyPrices = study.alternativeEnergyPrices || {};
|
|
1990
|
+
const alternativePowerPrices = study.alternativePowerPrices || {};
|
|
1991
|
+
const contractedPowerFinalCostByPeriod = {};
|
|
1992
|
+
const contractedPowerSavingsByPeriod = {};
|
|
1993
|
+
if (study.useAlternativePrices) {
|
|
1994
|
+
for (let i = 1; i <= 6; i++) {
|
|
1995
|
+
const p = `p${i}`;
|
|
1996
|
+
if (contractedPower[p] && alternativePowerPrices[p]) {
|
|
1997
|
+
contractedPowerFinalCostByPeriod[p] = contractedPower[p] * 365 * alternativePowerPrices[p];
|
|
1998
|
+
}
|
|
1999
|
+
if (contractedPower[p] && powerPrices[p] && alternativePowerPrices[p]) {
|
|
2000
|
+
contractedPowerSavingsByPeriod[p] = contractedPower[p] * 365 * powerPrices[p] * oneBasePowerDiscountFactor - contractedPower[p] * 365 * alternativePowerPrices[p] * oneBasePowerDiscountFactor;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
results.contractedPowerSavingsByPeriod = contractedPowerSavingsByPeriod;
|
|
2004
|
+
}
|
|
2005
|
+
const totalNetSpendingByPeriod = {};
|
|
2006
|
+
const alternativeTotalNetSpendingByPeriod = {};
|
|
2007
|
+
for (let i = 1; i <= 6; i++) {
|
|
2008
|
+
const p = `p${i}`;
|
|
2009
|
+
const posNetP = positiveNetByPeriod?.[p] || 0;
|
|
2010
|
+
const priceP = energyPrices[p] || 0;
|
|
2011
|
+
const altPriceP = alternativeEnergyPrices[p] || 0;
|
|
2012
|
+
const includeTaxes = clientDetails?.includeTaxes;
|
|
2013
|
+
const taxPct = clientDetails?.taxesPercentage || 0;
|
|
2014
|
+
totalNetSpendingByPeriod[p] = posNetP * (includeTaxes ? priceP * (1 + taxPct / 100) : priceP) * oneBaseEnergyDiscountFactor;
|
|
2015
|
+
if (study.alternativeEnergyPrices) {
|
|
2016
|
+
alternativeTotalNetSpendingByPeriod[p] = posNetP * altPriceP + (contractedPowerFinalCostByPeriod[p] || 0);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
results.totalNetSpendingByPeriod = totalNetSpendingByPeriod;
|
|
2020
|
+
if (study.alternativeEnergyPrices) {
|
|
2021
|
+
results.alternativeTotalNetSpendingByPeriod = alternativeTotalNetSpendingByPeriod;
|
|
2022
|
+
}
|
|
2023
|
+
results.totalNetSpending = Object.values(totalNetSpendingByPeriod).filter((v) => typeof v === "number").reduce((a, b) => a + b, 0) + (results.netPeakConsumptionCostPt || 0);
|
|
2024
|
+
const totalSavingsByPeriod = {};
|
|
2025
|
+
for (let i = 1; i <= 6; i++) {
|
|
2026
|
+
const p = `p${i}`;
|
|
2027
|
+
totalSavingsByPeriod[p] = (totalRawSpendingByPeriod[p] || 0) - (totalNetSpendingByPeriod[p] || 0);
|
|
2028
|
+
}
|
|
2029
|
+
results.totalSavingsByPeriod = totalSavingsByPeriod;
|
|
2030
|
+
if (study.alternativeEnergyPrices) {
|
|
2031
|
+
const altSavings = {};
|
|
2032
|
+
for (let i = 1; i <= 6; i++) {
|
|
2033
|
+
const p = `p${i}`;
|
|
2034
|
+
altSavings[p] = (totalRawSpendingByPeriod[p] || 0) - (alternativeTotalNetSpendingByPeriod[p] || 0);
|
|
2035
|
+
}
|
|
2036
|
+
results.totalSavingsByPeriodAlternativeSavings = altSavings;
|
|
2037
|
+
}
|
|
2038
|
+
results.totalSavings = Object.values(totalSavingsByPeriod).filter((v) => typeof v === "number").reduce((a, b) => a + b, 0) + ((results.rawPeakConsumptionCostPt || 0) - (results.netPeakConsumptionCostPt || 0));
|
|
2039
|
+
if (study.alternativeEnergyPrices) {
|
|
2040
|
+
results.totalSavingsAlternativePrices = Object.values(alternativeTotalNetSpendingByPeriod).filter((v) => typeof v === "number").reduce((a, b) => a + b, 0) + ((results.rawPeakConsumptionCostPt || 0) - (results.netPeakConsumptionCostPt || 0));
|
|
2041
|
+
}
|
|
2042
|
+
const excessesCurve = totalProdCurve.aggregatePowerCurve(consCurve.applyMultiplier(-1)).filterNegativeValues();
|
|
2043
|
+
results.excessesCurve = excessesCurve;
|
|
2044
|
+
results.totalExcessesByPeriod = excessesCurve.aggregateByPeriod(periodDistribution);
|
|
2045
|
+
results.totalExcesses = Object.values(results.totalExcessesByPeriod).filter((v) => typeof v === "number").reduce((a, b) => a + b, 0);
|
|
2046
|
+
}
|
|
2047
|
+
const result = updateStudy(filePath, (s) => {
|
|
2048
|
+
s.results = results;
|
|
2049
|
+
return void 0;
|
|
2050
|
+
});
|
|
2051
|
+
output(result, global);
|
|
2052
|
+
} catch (err) {
|
|
2053
|
+
outputError(handleApiError(err));
|
|
2054
|
+
}
|
|
2055
|
+
});
|
|
2056
|
+
studies.command("add-comment").description(
|
|
2057
|
+
'Add a comment to the local study file.\nExample: suntropy studies add-comment --file study.json --content "Panel layout reviewed"'
|
|
2058
|
+
).option("--file <path>", "Study file path").requiredOption("--content <text>", "Comment text").action(async (opts) => {
|
|
2059
|
+
try {
|
|
2060
|
+
const result = updateStudy(resolveFile(opts), (study) => {
|
|
2061
|
+
const comments = study.comments || [];
|
|
2062
|
+
comments.push(createComment("commented", opts.content));
|
|
2063
|
+
study.comments = comments;
|
|
2064
|
+
return void 0;
|
|
2065
|
+
});
|
|
2066
|
+
output(result, getGlobalOpts6(studies));
|
|
2067
|
+
} catch (err) {
|
|
2068
|
+
outputError(err instanceof Error ? err : new Error(String(err)));
|
|
2069
|
+
}
|
|
2070
|
+
});
|
|
2071
|
+
studies.command("comment <studyId>").description(
|
|
2072
|
+
'Add a comment to an existing study via API.\nExample: suntropy studies comment abc123 --content "Revisado por agente"'
|
|
2073
|
+
).requiredOption("--content <text>", "Comment text").action(async (studyId, opts) => {
|
|
2074
|
+
try {
|
|
2075
|
+
const global = getGlobalOpts6(studies);
|
|
2076
|
+
const client = createServiceClient("solar", global);
|
|
2077
|
+
const comment = createComment("commented", opts.content);
|
|
2078
|
+
const res = await client.post(`/solar-study/addSolarStudyComment/${studyId}`, comment);
|
|
2079
|
+
output(res.data, global);
|
|
2080
|
+
} catch (err) {
|
|
2081
|
+
outputError(handleApiError(err));
|
|
2082
|
+
}
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
async function generateFromProfile(global, tariff, market, year, mode, data) {
|
|
2086
|
+
const profilesClient = createServiceClient("profiles", global);
|
|
2087
|
+
const periodsClient = createServiceClient("periods", global);
|
|
2088
|
+
const profileRes = await profilesClient.get("/ree-profiles", {
|
|
2089
|
+
params: {
|
|
2090
|
+
startDate: `${year}-01-01`,
|
|
2091
|
+
endDate: `${year}-12-31`,
|
|
2092
|
+
tariff,
|
|
2093
|
+
type: "Final",
|
|
2094
|
+
market
|
|
2095
|
+
}
|
|
2096
|
+
});
|
|
2097
|
+
const profile = profileRes.data?.profile || profileRes.data;
|
|
2098
|
+
if (!profile || !Array.isArray(profile) || profile.length === 0) {
|
|
2099
|
+
throw new Error("Failed to fetch REE profiles");
|
|
2100
|
+
}
|
|
2101
|
+
const { applyProfileToConsumption, ConsumptionIntroductionModes } = await import("energy-types/lib/energy/calculations/applyProfileToConsumption");
|
|
2102
|
+
if (mode === "CONSUMPTION_BY_PERIOD") {
|
|
2103
|
+
const tariffIdMap = { "2.0TD": 13, "3.0TD": 14, "6.1TD": 15 };
|
|
2104
|
+
const tariffId = tariffIdMap[tariff] || 14;
|
|
2105
|
+
const periodsRes = await periodsClient.get("/periodos", {
|
|
2106
|
+
params: {
|
|
2107
|
+
idTarifa: tariffId,
|
|
2108
|
+
idZona: 1,
|
|
2109
|
+
fechaInicio: `${year}-01-01`,
|
|
2110
|
+
fechaFin: `${year}-12-31`,
|
|
2111
|
+
market
|
|
2112
|
+
}
|
|
2113
|
+
});
|
|
2114
|
+
const periodDistribution = periodsRes.data;
|
|
2115
|
+
const result = applyProfileToConsumption(
|
|
2116
|
+
ConsumptionIntroductionModes.CONSUMPTION_BY_PERIOD,
|
|
2117
|
+
profile,
|
|
2118
|
+
data,
|
|
2119
|
+
periodDistribution
|
|
2120
|
+
);
|
|
2121
|
+
return result;
|
|
2122
|
+
} else if (mode === "MONTH_CONSUMPTION") {
|
|
2123
|
+
const flatData = data;
|
|
2124
|
+
const yearMonthData = { [year]: {} };
|
|
2125
|
+
for (const [month, value] of Object.entries(flatData)) {
|
|
2126
|
+
yearMonthData[year][parseInt(month)] = value;
|
|
2127
|
+
}
|
|
2128
|
+
const result = applyProfileToConsumption(
|
|
2129
|
+
ConsumptionIntroductionModes.MONTH_CONSUMPTION,
|
|
2130
|
+
profile,
|
|
2131
|
+
yearMonthData
|
|
2132
|
+
);
|
|
2133
|
+
return result;
|
|
2134
|
+
} else if (mode === "MONTHLY_BY_PERIOD") {
|
|
2135
|
+
const monthlyData = data;
|
|
2136
|
+
const monthTotals = {};
|
|
2137
|
+
for (const [month, periods] of Object.entries(monthlyData)) {
|
|
2138
|
+
monthTotals[month] = Object.values(periods).reduce((a, b) => a + b, 0);
|
|
2139
|
+
}
|
|
2140
|
+
const result = applyProfileToConsumption(
|
|
2141
|
+
ConsumptionIntroductionModes.MONTH_CONSUMPTION,
|
|
2142
|
+
profile,
|
|
2143
|
+
monthTotals
|
|
2144
|
+
);
|
|
2145
|
+
return result;
|
|
2146
|
+
}
|
|
2147
|
+
throw new Error(`Unknown consumption mode: ${mode}`);
|
|
2148
|
+
}
|
|
2149
|
+
function collectAssets(value, previous) {
|
|
2150
|
+
const [idStr, qtyStr] = value.split(":");
|
|
2151
|
+
const id = parseInt(idStr);
|
|
2152
|
+
const quantity = qtyStr ? parseInt(qtyStr) : 1;
|
|
2153
|
+
if (isNaN(id)) throw new Error(`Invalid asset format: "${value}". Use <id>:<quantity>`);
|
|
2154
|
+
return [...previous, { id, quantity }];
|
|
2155
|
+
}
|
|
2156
|
+
async function fetchPeriodDistribution(study, global) {
|
|
2157
|
+
const atrTariff = study.atrTariff;
|
|
2158
|
+
const geoZone = study.geographicalZone;
|
|
2159
|
+
if (!atrTariff) return null;
|
|
2160
|
+
const tariffId = atrTariff.idTarifaATR;
|
|
2161
|
+
const zoneId = geoZone?.idZona || 1;
|
|
2162
|
+
const market = study.market || "es";
|
|
2163
|
+
const year = (/* @__PURE__ */ new Date()).getFullYear();
|
|
2164
|
+
try {
|
|
2165
|
+
const periodsClient = createServiceClient("periods", global);
|
|
2166
|
+
const res = await periodsClient.get("/periodos", {
|
|
2167
|
+
params: {
|
|
2168
|
+
idTarifa: tariffId,
|
|
2169
|
+
idZona: zoneId,
|
|
2170
|
+
fechaInicio: `${year}-01-01`,
|
|
2171
|
+
fechaFin: `${year}-12-31`,
|
|
2172
|
+
market
|
|
2173
|
+
}
|
|
2174
|
+
});
|
|
2175
|
+
const data = res.data;
|
|
2176
|
+
if (Array.isArray(data) && data.length > 0) return data;
|
|
2177
|
+
return null;
|
|
2178
|
+
} catch {
|
|
2179
|
+
return null;
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
function createComment(type, content) {
|
|
2183
|
+
const config = loadConfig();
|
|
2184
|
+
const profile = getActiveProfile(config);
|
|
2185
|
+
const autoContent = {
|
|
2186
|
+
created: "Estudio creado via CLI",
|
|
2187
|
+
modified: "Estudio actualizado via CLI"
|
|
2188
|
+
};
|
|
2189
|
+
return {
|
|
2190
|
+
content: content || autoContent[type] || "",
|
|
2191
|
+
type,
|
|
2192
|
+
creationTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2193
|
+
creationUserUID: profile.userUID || "cli-agent"
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
// src/commands/studies/index.ts
|
|
2198
|
+
function getGlobalOpts7(cmd) {
|
|
2199
|
+
let root = cmd;
|
|
2200
|
+
while (root.parent) root = root.parent;
|
|
2201
|
+
return root.opts();
|
|
2202
|
+
}
|
|
2203
|
+
function compactCurves(obj) {
|
|
2204
|
+
if (obj === null || obj === void 0 || typeof obj !== "object") return obj;
|
|
2205
|
+
if (Array.isArray(obj)) return obj.map(compactCurves);
|
|
2206
|
+
const record = obj;
|
|
2207
|
+
if (Array.isArray(record.days) && record.days.length > 0 && record.identifier !== void 0) {
|
|
2208
|
+
return { _type: "PowerCurve", days: record.days.length, identifier: record.identifier };
|
|
2209
|
+
}
|
|
2210
|
+
const result = {};
|
|
2211
|
+
for (const [key, val] of Object.entries(record)) {
|
|
2212
|
+
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
2213
|
+
const v = val;
|
|
2214
|
+
if (Array.isArray(v.days) && v.days.length > 0 && v.identifier !== void 0) {
|
|
2215
|
+
result[key] = { _type: "PowerCurve", days: v.days.length, identifier: v.identifier };
|
|
2216
|
+
continue;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
result[key] = compactCurves(val);
|
|
2220
|
+
}
|
|
2221
|
+
return result;
|
|
2222
|
+
}
|
|
2223
|
+
var EXPAND_SECTIONS = {
|
|
2224
|
+
surfaces: ["surfaces"],
|
|
2225
|
+
results: ["results"],
|
|
2226
|
+
economics: ["economicResults"],
|
|
2227
|
+
batteries: ["activeBatteries", "batteriesConfiguration", "batteriesEnergyBalance", "batteriesResults", "bateries"],
|
|
2228
|
+
consumption: ["consumption", "consumptionIntroductionMode", "monthConsumptionDictionary", "monthPeriodConsumptionDictionary", "periodConsumptionDictionary"],
|
|
2229
|
+
equipment: ["solarPanel", "solarKit", "solarInverters", "selectedChargers", "selectedCustomAssets"],
|
|
2230
|
+
client: ["clientDetails", "clientsDetails"],
|
|
2231
|
+
location: ["location", "mapCenter", "geographicalZone", "atrTariff"]
|
|
2232
|
+
};
|
|
2233
|
+
var CORE_FIELDS = [
|
|
2234
|
+
"_id",
|
|
2235
|
+
"id",
|
|
2236
|
+
"name",
|
|
2237
|
+
"identifier",
|
|
2238
|
+
"clientUID",
|
|
2239
|
+
"creationTimestamp",
|
|
2240
|
+
"lastEditTimestamp",
|
|
2241
|
+
"assignedUserUID",
|
|
2242
|
+
"creationUserUID",
|
|
2243
|
+
"selectedSolarStudyMode",
|
|
2244
|
+
"solarStudyProgress",
|
|
2245
|
+
"market",
|
|
2246
|
+
"layout",
|
|
2247
|
+
"peakPowerIntroductionMode",
|
|
2248
|
+
"instalationPhaseNumber"
|
|
2249
|
+
];
|
|
2250
|
+
function filterStudy(study, expand) {
|
|
2251
|
+
if (expand === "all") return study;
|
|
2252
|
+
const sections = expand ? expand.split(",").map((s) => s.trim()) : [];
|
|
2253
|
+
const allowedFields = new Set(CORE_FIELDS);
|
|
2254
|
+
for (const section of sections) {
|
|
2255
|
+
const fields = EXPAND_SECTIONS[section];
|
|
2256
|
+
if (fields) fields.forEach((f) => allowedFields.add(f));
|
|
2257
|
+
}
|
|
2258
|
+
if (sections.length === 0) {
|
|
2259
|
+
["peakPowerIntroductionMode", "instalationPhaseNumber", "location", "atrTariff"].forEach((f) => allowedFields.add(f));
|
|
2260
|
+
}
|
|
2261
|
+
const result = {};
|
|
2262
|
+
for (const key of allowedFields) {
|
|
2263
|
+
if (key in study) result[key] = study[key];
|
|
2264
|
+
}
|
|
2265
|
+
return compactCurves(result);
|
|
2266
|
+
}
|
|
2267
|
+
function registerStudiesCommands(program2) {
|
|
2268
|
+
const studies = program2.command("studies").description(
|
|
2269
|
+
"Explore and manage solar studies. Progressive exploration: list \u2192 metadata \u2192 get \u2192 get --expand \u2192 curves"
|
|
2270
|
+
);
|
|
2271
|
+
studies.command("list").description("List solar studies metadata. Fields: idSolarStudyMetadata, solarStudyId, clientName, peakPower, currentState, creationTimestamp").option("--limit <n>", "Max results", "20").option("--offset <n>", "Skip results", "0").option("--state <state>", "Filter by state name").option("--client-name <name>", "Filter by client name").option("--from <date>", "Filter from date (YYYY-MM-DD)").option("--to <date>", "Filter to date (YYYY-MM-DD)").action(async (opts) => {
|
|
2272
|
+
try {
|
|
2273
|
+
const global = getGlobalOpts7(studies);
|
|
2274
|
+
const client = createServiceClient("solar", global);
|
|
2275
|
+
const body = {
|
|
2276
|
+
limit: parseInt(opts.limit),
|
|
2277
|
+
offset: parseInt(opts.offset)
|
|
2278
|
+
};
|
|
2279
|
+
if (opts.state) body.state = opts.state;
|
|
2280
|
+
if (opts.clientName) body.clientName = opts.clientName;
|
|
2281
|
+
if (opts.from) body.fromDate = opts.from;
|
|
2282
|
+
if (opts.to) body.toDate = opts.to;
|
|
2283
|
+
const res = await client.post("/solar-study/findWithPaginationAndFilters", body);
|
|
2284
|
+
let data;
|
|
2285
|
+
let total;
|
|
2286
|
+
if (Array.isArray(res.data) && res.data.length >= 2 && Array.isArray(res.data[0]) && typeof res.data[1] === "number") {
|
|
2287
|
+
data = res.data[0];
|
|
2288
|
+
total = res.data[1];
|
|
2289
|
+
} else if (Array.isArray(res.data)) {
|
|
2290
|
+
data = res.data;
|
|
2291
|
+
total = res.data.length;
|
|
2292
|
+
} else {
|
|
2293
|
+
data = res.data?.data || res.data?.solarStudiesMetadata || [];
|
|
2294
|
+
total = res.data?.total ?? res.data?.count ?? (Array.isArray(data) ? data.length : 0);
|
|
2295
|
+
}
|
|
2296
|
+
const outOpts = { ...global };
|
|
2297
|
+
if (!global.fields) {
|
|
2298
|
+
outOpts.fields = "idSolarStudyMetadata,solarStudyId,clientName,peakPower,anualProduction,anualConsumption,totalCost,sellingPrice,currentState,creationTimestamp";
|
|
2299
|
+
}
|
|
2300
|
+
outputPaginated(data, total, parseInt(opts.limit), parseInt(opts.offset), outOpts);
|
|
2301
|
+
} catch (err) {
|
|
2302
|
+
outputError(handleApiError(err));
|
|
2303
|
+
}
|
|
2304
|
+
});
|
|
2305
|
+
studies.command("metadata <id>").description("Get solar study metadata by metadata ID (relational). Full MySQL record with state, costs, versions.").option("--by-study-id", "Interpret <id> as MongoDB solarStudyId instead of metadata ID").action(async (id, opts) => {
|
|
2306
|
+
try {
|
|
2307
|
+
const global = getGlobalOpts7(studies);
|
|
2308
|
+
const client = createServiceClient("solar", global);
|
|
2309
|
+
const path = opts.byStudyId ? `/solar-study/metadata/solar-study-id/${id}` : `/solar-study/findSolarStudyMetadataById/${id}`;
|
|
2310
|
+
const res = await client.get(path);
|
|
2311
|
+
output(res.data, global);
|
|
2312
|
+
} catch (err) {
|
|
2313
|
+
outputError(handleApiError(err));
|
|
2314
|
+
}
|
|
2315
|
+
});
|
|
2316
|
+
studies.command("get <studyId>").description(
|
|
2317
|
+
"Get solar study by MongoDB ID. By default returns summary (no heavy curves).\nExpand sections: surfaces, results, economics, batteries, consumption, equipment, client, location\nExamples:\n suntropy studies get abc123\n suntropy studies get abc123 --expand surfaces,results\n suntropy studies get abc123 --expand all"
|
|
2318
|
+
).option("--expand <sections>", 'Comma-separated sections to expand (or "all")').action(async (studyId, opts) => {
|
|
2319
|
+
try {
|
|
2320
|
+
const global = getGlobalOpts7(studies);
|
|
2321
|
+
const client = createServiceClient("solar", global);
|
|
2322
|
+
const res = await client.get(`/solar-study/findById/${studyId}`);
|
|
2323
|
+
const study = res.data;
|
|
2324
|
+
const filtered = filterStudy(study, opts.expand);
|
|
2325
|
+
output(filtered, global);
|
|
2326
|
+
} catch (err) {
|
|
2327
|
+
outputError(handleApiError(err));
|
|
2328
|
+
}
|
|
2329
|
+
});
|
|
2330
|
+
studies.command("curves <studyId> <curveName>").description(
|
|
2331
|
+
"Extract and analyze a PowerCurve from a study.\nCurve names: consumption, production, net-consumption, excesses\nDefault: --stats. Use --raw for full hourly data (8760 values).\nUse --monthly for monthly aggregates, --daily for daily totals."
|
|
2332
|
+
).option("--stats", "Show statistics (default if no other flag)").option("--monthly", "Monthly accumulated values").option("--daily", "Daily accumulated values").option("--raw", "Full hourly DayCurve[] data").option("--total", "Just the total accumulated value").option("--surface-index <n>", "Surface index for production curve", "0").option("--save <file>", "Save curve data to file").action(async (studyId, curveName, opts) => {
|
|
2333
|
+
try {
|
|
2334
|
+
const global = getGlobalOpts7(studies);
|
|
2335
|
+
const client = createServiceClient("solar", global);
|
|
2336
|
+
const res = await client.get(`/solar-study/findById/${studyId}`);
|
|
2337
|
+
const study = res.data;
|
|
2338
|
+
let curveData;
|
|
2339
|
+
switch (curveName) {
|
|
2340
|
+
case "consumption":
|
|
2341
|
+
curveData = study.consumption;
|
|
2342
|
+
break;
|
|
2343
|
+
case "production": {
|
|
2344
|
+
const surfaces = study.surfaces;
|
|
2345
|
+
const idx = parseInt(opts.surfaceIndex);
|
|
2346
|
+
if (surfaces && surfaces[idx]?.production) {
|
|
2347
|
+
curveData = surfaces[idx].production;
|
|
2348
|
+
} else {
|
|
2349
|
+
outputError(new Error(`No production curve at surface index ${idx}. Study has ${surfaces?.length || 0} surfaces.`));
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
break;
|
|
2353
|
+
}
|
|
2354
|
+
case "net-consumption":
|
|
2355
|
+
curveData = study.results?.netConsumption;
|
|
2356
|
+
break;
|
|
2357
|
+
case "excesses":
|
|
2358
|
+
curveData = study.results?.excessesCurve;
|
|
2359
|
+
break;
|
|
2360
|
+
default:
|
|
2361
|
+
outputError(new Error(`Unknown curve: ${curveName}. Available: consumption, production, net-consumption, excesses`));
|
|
2362
|
+
return;
|
|
2363
|
+
}
|
|
2364
|
+
if (!curveData) {
|
|
2365
|
+
outputError(new Error(`Curve "${curveName}" not found or empty in this study.`));
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2368
|
+
const { PowerCurve } = await import("energy-types/lib/energy/classes/powerCurve.class");
|
|
2369
|
+
const cd = curveData;
|
|
2370
|
+
const pc = new PowerCurve(cd.days || cd, cd.ignore0 ?? false, cd.identifier || curveName, cd.parseDate ?? false);
|
|
2371
|
+
const showRaw = opts.raw;
|
|
2372
|
+
const showMonthly = opts.monthly;
|
|
2373
|
+
const showDaily = opts.daily;
|
|
2374
|
+
const showTotal = opts.total;
|
|
2375
|
+
const showStats = opts.stats || !showRaw && !showMonthly && !showDaily && !showTotal;
|
|
2376
|
+
let result;
|
|
2377
|
+
if (showRaw) {
|
|
2378
|
+
result = pc.days;
|
|
2379
|
+
} else if (showTotal) {
|
|
2380
|
+
result = { total: pc.getTotalAcumulate(), identifier: curveName, days: pc.days.length };
|
|
2381
|
+
} else if (showStats) {
|
|
2382
|
+
const raw = pc.calculateStatistics();
|
|
2383
|
+
result = raw.statistics || raw;
|
|
2384
|
+
} else if (showMonthly) {
|
|
2385
|
+
const raw = pc.calculateStatistics();
|
|
2386
|
+
const s = raw.statistics || raw;
|
|
2387
|
+
result = { anualMonthAccumulate: s.anualMonthAccumulate, anualMonthCount: s.anualMonthCount, anualMonthlyAverage: s.anualMonthlyAverage };
|
|
2388
|
+
} else if (showDaily) {
|
|
2389
|
+
const raw = pc.calculateStatistics();
|
|
2390
|
+
const s = raw.statistics || raw;
|
|
2391
|
+
result = { dailyAccumulate: s.dailyAccumulate };
|
|
2392
|
+
}
|
|
2393
|
+
const outOpts = { ...global, save: opts.save || global.save };
|
|
2394
|
+
output(result, outOpts);
|
|
2395
|
+
} catch (err) {
|
|
2396
|
+
outputError(handleApiError(err));
|
|
2397
|
+
}
|
|
2398
|
+
});
|
|
2399
|
+
studies.command("calculate-production").description("Calculate solar production curve for given coordinates and configuration").requiredOption("--lat <n>", "Latitude").requiredOption("--lon <n>", "Longitude").requiredOption("--power <w>", "Installed power in Watts").option("--angle <n>", "Panel inclination degrees", "30").option("--azimuth <n>", "Panel orientation degrees (0=north, 180=south)", "180").option("--losses <n>", "Losses percentage", "14").option("--year <n>", "Year for calculation", String((/* @__PURE__ */ new Date()).getFullYear())).option("--save <file>", "Save result to file").action(async (opts) => {
|
|
2400
|
+
try {
|
|
2401
|
+
const global = getGlobalOpts7(studies);
|
|
2402
|
+
const client = createServiceClient("solar", global);
|
|
2403
|
+
const body = {
|
|
2404
|
+
lat: parseFloat(opts.lat),
|
|
2405
|
+
long: parseFloat(opts.lon),
|
|
2406
|
+
instaledPower: parseFloat(opts.power),
|
|
2407
|
+
angle: parseFloat(opts.angle),
|
|
2408
|
+
azimuth: parseFloat(opts.azimuth),
|
|
2409
|
+
lossesPercentage: parseFloat(opts.losses),
|
|
2410
|
+
year: parseInt(opts.year),
|
|
2411
|
+
isOptimized: false
|
|
2412
|
+
};
|
|
2413
|
+
const res = await client.post("/solar-study/calculateProduction", body);
|
|
2414
|
+
output(res.data, { ...global, save: opts.save || global.save });
|
|
2415
|
+
} catch (err) {
|
|
2416
|
+
outputError(handleApiError(err));
|
|
2417
|
+
}
|
|
2418
|
+
});
|
|
2419
|
+
studies.command("optimize-surfaces").description("Calculate optimal panel angle and azimuth for coordinates").requiredOption("--lat <n>", "Latitude").requiredOption("--lon <n>", "Longitude").action(async (opts) => {
|
|
2420
|
+
try {
|
|
2421
|
+
const global = getGlobalOpts7(studies);
|
|
2422
|
+
const client = createServiceClient("solar", global);
|
|
2423
|
+
const res = await client.get("/solar-study/optimizeSurfaces", {
|
|
2424
|
+
params: { lat: parseFloat(opts.lat), lon: parseFloat(opts.lon) }
|
|
2425
|
+
});
|
|
2426
|
+
output(res.data, global);
|
|
2427
|
+
} catch (err) {
|
|
2428
|
+
outputError(handleApiError(err));
|
|
2429
|
+
}
|
|
2430
|
+
});
|
|
2431
|
+
registerStudyBuilderCommands(studies);
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
// src/commands/curves/index.ts
|
|
2435
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2436
|
+
function getGlobalOpts8(cmd) {
|
|
2437
|
+
let root = cmd;
|
|
2438
|
+
while (root.parent) root = root.parent;
|
|
2439
|
+
return root.opts();
|
|
2440
|
+
}
|
|
2441
|
+
function readStdin() {
|
|
2442
|
+
return new Promise((resolve, reject) => {
|
|
2443
|
+
let data = "";
|
|
2444
|
+
process.stdin.setEncoding("utf-8");
|
|
2445
|
+
process.stdin.on("data", (chunk) => {
|
|
2446
|
+
data += chunk;
|
|
2447
|
+
});
|
|
2448
|
+
process.stdin.on("end", () => resolve(data));
|
|
2449
|
+
process.stdin.on("error", reject);
|
|
2450
|
+
});
|
|
2451
|
+
}
|
|
2452
|
+
async function readCurveInput(inputPath) {
|
|
2453
|
+
let raw;
|
|
2454
|
+
if (inputPath && inputPath !== "-") {
|
|
2455
|
+
raw = readFileSync3(inputPath, "utf-8");
|
|
2456
|
+
} else {
|
|
2457
|
+
raw = await readStdin();
|
|
2458
|
+
}
|
|
2459
|
+
return JSON.parse(raw);
|
|
2460
|
+
}
|
|
2461
|
+
async function buildCurve(data, identifier = "curve") {
|
|
2462
|
+
const { PowerCurve } = await import("energy-types/lib/energy/classes/powerCurve.class");
|
|
2463
|
+
if (Array.isArray(data)) {
|
|
2464
|
+
return new PowerCurve(data, false, identifier, false);
|
|
2465
|
+
}
|
|
2466
|
+
return new PowerCurve(data.days || data, data.ignore0 ?? false, data.identifier || identifier, data.parseDate ?? false);
|
|
2467
|
+
}
|
|
2468
|
+
function serializeCurve(pc) {
|
|
2469
|
+
return { days: pc.days, identifier: pc.identifier, ignore0: pc.ignore0 };
|
|
2470
|
+
}
|
|
2471
|
+
function registerCurvesCommands(program2) {
|
|
2472
|
+
const curves = program2.command("curves").description(
|
|
2473
|
+
"PowerCurve operations (pipe-friendly). Accept input via --input <file> or stdin.\nCurve-returning commands output serialized PowerCurve JSON for chaining.\nUse --save <file> to persist and still output to stdout."
|
|
2474
|
+
);
|
|
2475
|
+
curves.command("stats").description("Calculate comprehensive statistics: monthly/daily/hourly averages, max, min, period aggregation").option("--input <file>", "Input file (or - for stdin)").action(async (opts) => {
|
|
2476
|
+
try {
|
|
2477
|
+
const data = await readCurveInput(opts.input);
|
|
2478
|
+
const pc = await buildCurve(data, "stats");
|
|
2479
|
+
const raw = pc.calculateStatistics();
|
|
2480
|
+
output(raw.statistics || raw, getGlobalOpts8(curves));
|
|
2481
|
+
} catch (err) {
|
|
2482
|
+
outputError(err);
|
|
2483
|
+
}
|
|
2484
|
+
});
|
|
2485
|
+
curves.command("total").description("Calculate total accumulated value across all hours").option("--input <file>", "Input file (or - for stdin)").action(async (opts) => {
|
|
2486
|
+
try {
|
|
2487
|
+
const data = await readCurveInput(opts.input);
|
|
2488
|
+
const pc = await buildCurve(data, "total");
|
|
2489
|
+
output({ total: pc.getTotalAcumulate(), days: pc.days.length }, getGlobalOpts8(curves));
|
|
2490
|
+
} catch (err) {
|
|
2491
|
+
outputError(err);
|
|
2492
|
+
}
|
|
2493
|
+
});
|
|
2494
|
+
curves.command("multiply <factor>").description("Multiply all hourly values by a factor. Returns a new PowerCurve.").option("--input <file>", "Input file (or - for stdin)").action(async (factor, opts) => {
|
|
2495
|
+
try {
|
|
2496
|
+
const data = await readCurveInput(opts.input);
|
|
2497
|
+
const pc = await buildCurve(data, "multiplied");
|
|
2498
|
+
const result = pc.applyMultiplier(parseFloat(factor));
|
|
2499
|
+
output(serializeCurve(result), getGlobalOpts8(curves));
|
|
2500
|
+
} catch (err) {
|
|
2501
|
+
outputError(err);
|
|
2502
|
+
}
|
|
2503
|
+
});
|
|
2504
|
+
curves.command("aggregate").description("Sum two PowerCurves (A + B). Returns a new PowerCurve.").requiredOption("--a <file>", "First curve file").requiredOption("--b <file>", "Second curve file").action(async (opts) => {
|
|
2505
|
+
try {
|
|
2506
|
+
const dataA = JSON.parse(readFileSync3(opts.a, "utf-8"));
|
|
2507
|
+
const dataB = JSON.parse(readFileSync3(opts.b, "utf-8"));
|
|
2508
|
+
const pcA = await buildCurve(dataA, "a");
|
|
2509
|
+
const pcB = await buildCurve(dataB, "b");
|
|
2510
|
+
const result = pcA.aggregatePowerCurve(pcB);
|
|
2511
|
+
output(serializeCurve(result), getGlobalOpts8(curves));
|
|
2512
|
+
} catch (err) {
|
|
2513
|
+
outputError(err);
|
|
2514
|
+
}
|
|
2515
|
+
});
|
|
2516
|
+
curves.command("subtract").description("Subtract two PowerCurves (A - B). Returns a new PowerCurve.").requiredOption("--a <file>", "First curve file (minuend)").requiredOption("--b <file>", "Second curve file (subtrahend)").action(async (opts) => {
|
|
2517
|
+
try {
|
|
2518
|
+
const dataA = JSON.parse(readFileSync3(opts.a, "utf-8"));
|
|
2519
|
+
const dataB = JSON.parse(readFileSync3(opts.b, "utf-8"));
|
|
2520
|
+
const pcA = await buildCurve(dataA, "a");
|
|
2521
|
+
const pcB = await buildCurve(dataB, "b");
|
|
2522
|
+
const negB = pcB.applyMultiplier(-1);
|
|
2523
|
+
const result = pcA.aggregatePowerCurve(negB);
|
|
2524
|
+
output(serializeCurve(result), getGlobalOpts8(curves));
|
|
2525
|
+
} catch (err) {
|
|
2526
|
+
outputError(err);
|
|
2527
|
+
}
|
|
2528
|
+
});
|
|
2529
|
+
curves.command("filter-positive").description("Keep only non-negative hourly values (zero out negatives). Returns a new PowerCurve.").option("--input <file>", "Input file (or - for stdin)").action(async (opts) => {
|
|
2530
|
+
try {
|
|
2531
|
+
const data = await readCurveInput(opts.input);
|
|
2532
|
+
const pc = await buildCurve(data, "positive");
|
|
2533
|
+
const result = pc.filterNegativeValues();
|
|
2534
|
+
output(serializeCurve(result), getGlobalOpts8(curves));
|
|
2535
|
+
} catch (err) {
|
|
2536
|
+
outputError(err);
|
|
2537
|
+
}
|
|
2538
|
+
});
|
|
2539
|
+
curves.command("filter-negative").description("Keep only non-positive hourly values (zero out positives). Returns a new PowerCurve.").option("--input <file>", "Input file (or - for stdin)").action(async (opts) => {
|
|
2540
|
+
try {
|
|
2541
|
+
const data = await readCurveInput(opts.input);
|
|
2542
|
+
const pc = await buildCurve(data, "negative");
|
|
2543
|
+
const result = pc.filterPositiveValues();
|
|
2544
|
+
output(serializeCurve(result), getGlobalOpts8(curves));
|
|
2545
|
+
} catch (err) {
|
|
2546
|
+
outputError(err);
|
|
2547
|
+
}
|
|
2548
|
+
});
|
|
2549
|
+
curves.command("sort").description("Sort curve days by date ascending. Returns a new PowerCurve.").option("--input <file>", "Input file (or - for stdin)").action(async (opts) => {
|
|
2550
|
+
try {
|
|
2551
|
+
const data = await readCurveInput(opts.input);
|
|
2552
|
+
const pc = await buildCurve(data, "sorted");
|
|
2553
|
+
const sortedDays = pc.sortByDate();
|
|
2554
|
+
output({ days: sortedDays, identifier: pc.identifier, ignore0: pc.ignore0 }, getGlobalOpts8(curves));
|
|
2555
|
+
} catch (err) {
|
|
2556
|
+
outputError(err);
|
|
2557
|
+
}
|
|
2558
|
+
});
|
|
2559
|
+
curves.command("filter-dates").description("Filter curve to a date range. Returns a new PowerCurve.").option("--input <file>", "Input file (or - for stdin)").option("--start <date>", "Start date (YYYY-MM-DD)").option("--end <date>", "End date (YYYY-MM-DD)").action(async (opts) => {
|
|
2560
|
+
try {
|
|
2561
|
+
const data = await readCurveInput(opts.input);
|
|
2562
|
+
const pc = await buildCurve(data, "filtered");
|
|
2563
|
+
const start = opts.start ? new Date(opts.start) : void 0;
|
|
2564
|
+
const end = opts.end ? new Date(opts.end) : void 0;
|
|
2565
|
+
const filteredDays = pc.filterByDates(start, end);
|
|
2566
|
+
output({ days: filteredDays, identifier: pc.identifier, ignore0: pc.ignore0 }, getGlobalOpts8(curves));
|
|
2567
|
+
} catch (err) {
|
|
2568
|
+
outputError(err);
|
|
2569
|
+
}
|
|
2570
|
+
});
|
|
2571
|
+
curves.command("to-serie").description("Convert curve to chart-ready series format (x/y arrays)").option("--input <file>", "Input file (or - for stdin)").action(async (opts) => {
|
|
2572
|
+
try {
|
|
2573
|
+
const data = await readCurveInput(opts.input);
|
|
2574
|
+
const pc = await buildCurve(data, "serie");
|
|
2575
|
+
const serie = pc.convertoToSerie();
|
|
2576
|
+
output(serie, getGlobalOpts8(curves));
|
|
2577
|
+
} catch (err) {
|
|
2578
|
+
outputError(err);
|
|
2579
|
+
}
|
|
2580
|
+
});
|
|
2581
|
+
curves.command("by-period").description(
|
|
2582
|
+
"Aggregate a PowerCurve by tariff periods (P1-P6) using a period distribution.\nReturns { p1, p2, p3, p4, p5, p6 } with accumulated kWh per period.\n\nThe period distribution maps each hour of each day to a tariff period.\nGet it with: suntropy consumption periods --save /tmp/periods.json\n\nExamples:\n suntropy curves by-period --input /tmp/production.json --periods /tmp/periods.json\n suntropy curves by-period --input /tmp/consumption.json --periods /tmp/periods.json"
|
|
2583
|
+
).requiredOption("--periods <file>", "Period distribution file (DayCurve[] from consumption periods)").option("--input <file>", "Input PowerCurve file (or stdin)").action(async (opts) => {
|
|
2584
|
+
try {
|
|
2585
|
+
const data = await readCurveInput(opts.input);
|
|
2586
|
+
const pc = await buildCurve(data, "by-period");
|
|
2587
|
+
const periodsData = JSON.parse(readFileSync3(opts.periods, "utf-8"));
|
|
2588
|
+
const result = pc.aggregateByPeriod(periodsData);
|
|
2589
|
+
output(result, getGlobalOpts8(curves));
|
|
2590
|
+
} catch (err) {
|
|
2591
|
+
outputError(err);
|
|
2592
|
+
}
|
|
2593
|
+
});
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
// src/commands/consumption/index.ts
|
|
2597
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
2598
|
+
function getGlobalOpts9(cmd) {
|
|
2599
|
+
let root = cmd;
|
|
2600
|
+
while (root.parent) root = root.parent;
|
|
2601
|
+
return root.opts();
|
|
2602
|
+
}
|
|
2603
|
+
function registerConsumptionCommands(program2) {
|
|
2604
|
+
const consumption = program2.command("consumption").description(
|
|
2605
|
+
"Generate consumption curves using the profiles service.\nSupports multiple estimation methods: standard patterns, custom profiles, monthly data.\nReturns PowerCurve JSON compatible with `suntropy curves` for further processing."
|
|
2606
|
+
);
|
|
2607
|
+
consumption.command("estimate").description(
|
|
2608
|
+
`Generate a consumption PowerCurve from patterns and annual consumption.
|
|
2609
|
+
Patterns: Balance, Nightly, Morning, Afternoon, Domestic, Commercial
|
|
2610
|
+
Examples:
|
|
2611
|
+
suntropy consumption estimate --annual 5000 --pattern Balance
|
|
2612
|
+
suntropy consumption estimate --annual 8000 --pattern Domestic --tariff 3.0TD --market es
|
|
2613
|
+
suntropy consumption estimate --annual 3500 --monthly-data '{"1":300,"2":280,...}'
|
|
2614
|
+
suntropy consumption estimate --annual 5000 --custom-profile-id abc123`
|
|
2615
|
+
).requiredOption("--annual <kWh>", "Annual consumption in kWh").option("--pattern <name>", "Consumption pattern: Balance, Nightly, Morning, Afternoon, Domestic, Commercial").option("--start-date <YYYY-MM-DD>", "Start date (default: Jan 1 current year)").option("--end-date <YYYY-MM-DD>", "End date (default: Dec 31 current year)").option("--tariff <code>", "Electricity tariff code (e.g. 3.0TD)", "3.0TD").option("--type <type>", "Profile type: Initial or Final", "Final").option("--market <code>", "Market: es, pt, it", "es").option("--custom-profile-id <id>", "Use a custom consumption profile by ID").option("--monthly-data <json>", 'Monthly consumption JSON: {"1":val,"2":val,...,"12":val}').option("--daily-curve <json>", "Custom daily curve JSON (DayCurve format)").option("--save <file>", "Save result to file").action(async (opts) => {
|
|
2616
|
+
try {
|
|
2617
|
+
const global = getGlobalOpts9(consumption);
|
|
2618
|
+
const client = createServiceClient("profiles", global);
|
|
2619
|
+
const year = (/* @__PURE__ */ new Date()).getFullYear();
|
|
2620
|
+
const startDate = opts.startDate || `${year}-01-01`;
|
|
2621
|
+
const endDate = opts.endDate || `${year}-12-31`;
|
|
2622
|
+
const params = {
|
|
2623
|
+
startDate,
|
|
2624
|
+
endDate,
|
|
2625
|
+
tariff: opts.tariff,
|
|
2626
|
+
type: opts.type,
|
|
2627
|
+
anualConsumption: opts.annual,
|
|
2628
|
+
market: opts.market
|
|
2629
|
+
};
|
|
2630
|
+
if (opts.pattern) params.consumptionType = opts.pattern;
|
|
2631
|
+
if (opts.customProfileId) params.customProfileId = opts.customProfileId;
|
|
2632
|
+
let body;
|
|
2633
|
+
if (opts.monthlyData || opts.dailyCurve) {
|
|
2634
|
+
body = {};
|
|
2635
|
+
if (opts.monthlyData) body.consumptionByMonth = JSON.parse(opts.monthlyData);
|
|
2636
|
+
if (opts.dailyCurve) body.dailyCurve = JSON.parse(opts.dailyCurve);
|
|
2637
|
+
}
|
|
2638
|
+
const queryString = Object.entries(params).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&");
|
|
2639
|
+
const res = await client.post(`/consumption-estimation?${queryString}`, body || {});
|
|
2640
|
+
output(res.data, { ...global, save: opts.save || global.save });
|
|
2641
|
+
} catch (err) {
|
|
2642
|
+
outputError(handleApiError(err));
|
|
2643
|
+
}
|
|
2644
|
+
});
|
|
2645
|
+
consumption.command("ree-profiles").description(
|
|
2646
|
+
"Fetch REE (Red El\xE9ctrica) hourly profiles for a date range and tariff.\nReturns the raw profile data used as basis for consumption estimation.\nExample: suntropy consumption ree-profiles --start 2024-01-01 --end 2024-12-31 --tariff 3.0TD"
|
|
2647
|
+
).requiredOption("--start <YYYY-MM-DD>", "Start date").requiredOption("--end <YYYY-MM-DD>", "End date").option("--tariff <code>", "Tariff code", "3.0TD").option("--type <type>", "Profile type: Initial or Final", "Final").option("--market <code>", "Market code: es, pt, it", "es").option("--save <file>", "Save result to file").action(async (opts) => {
|
|
2648
|
+
try {
|
|
2649
|
+
const global = getGlobalOpts9(consumption);
|
|
2650
|
+
const client = createServiceClient("profiles", global);
|
|
2651
|
+
const res = await client.get("/ree-profiles", {
|
|
2652
|
+
params: {
|
|
2653
|
+
startDate: opts.start,
|
|
2654
|
+
endDate: opts.end,
|
|
2655
|
+
tariff: opts.tariff,
|
|
2656
|
+
type: opts.type,
|
|
2657
|
+
market: opts.market
|
|
2658
|
+
}
|
|
2659
|
+
});
|
|
2660
|
+
output(res.data, { ...global, save: opts.save || global.save });
|
|
2661
|
+
} catch (err) {
|
|
2662
|
+
outputError(handleApiError(err));
|
|
2663
|
+
}
|
|
2664
|
+
});
|
|
2665
|
+
consumption.command("custom-tags").description("List available custom consumption profile tags for the authenticated client").action(async () => {
|
|
2666
|
+
try {
|
|
2667
|
+
const global = getGlobalOpts9(consumption);
|
|
2668
|
+
const client = createServiceClient("profiles", global);
|
|
2669
|
+
const res = await client.get("/custom-profiles/getTags");
|
|
2670
|
+
output(res.data, global);
|
|
2671
|
+
} catch (err) {
|
|
2672
|
+
outputError(handleApiError(err));
|
|
2673
|
+
}
|
|
2674
|
+
});
|
|
2675
|
+
consumption.command("custom-profile-info").description("Get details of a custom consumption profile by ID").requiredOption("--id <profileId>", "Custom profile ID").action(async (opts) => {
|
|
2676
|
+
try {
|
|
2677
|
+
const global = getGlobalOpts9(consumption);
|
|
2678
|
+
const client = createServiceClient("profiles", global);
|
|
2679
|
+
const res = await client.get("/custom-profiles/getInfo", {
|
|
2680
|
+
params: { id: opts.id }
|
|
2681
|
+
});
|
|
2682
|
+
output(res.data, global);
|
|
2683
|
+
} catch (err) {
|
|
2684
|
+
outputError(handleApiError(err));
|
|
2685
|
+
}
|
|
2686
|
+
});
|
|
2687
|
+
consumption.command("periods").description(
|
|
2688
|
+
"Fetch period distribution (hour\u2192P1-P6 mapping) from the periods service.\nReturns DayCurve[] where each hour's value is the period number (1-6).\nUse with `suntropy curves by-period` to aggregate any curve by tariff period.\n\nCommon tariff IDs (Spain): 13=2.0TD, 14=3.0TD\nZone IDs (Spain): 1=Peninsula, 2=Canarias, 3=Baleares\n\nExamples:\n suntropy consumption periods --tariff-id 14 --zone-id 1 --save /tmp/periods.json\n suntropy consumption periods --tariff-id 13 --start 2025-01-01 --end 2025-12-31"
|
|
2689
|
+
).option("--tariff-id <n>", "ATR tariff ID (13=2.0TD, 14=3.0TD)", "14").option("--zone-id <n>", "Geographical zone ID (1=Peninsula)", "1").option("--start <YYYY-MM-DD>", "Start date (default: Jan 1 current year)").option("--end <YYYY-MM-DD>", "End date (default: Dec 31 current year)").option("--market <code>", "Market: es, pt, it, fr", "es").option("--save <file>", "Save result to file").action(async (opts) => {
|
|
2690
|
+
try {
|
|
2691
|
+
const global = getGlobalOpts9(consumption);
|
|
2692
|
+
const client = createServiceClient("periods", global);
|
|
2693
|
+
const year = (/* @__PURE__ */ new Date()).getFullYear();
|
|
2694
|
+
const res = await client.get("/periodos", {
|
|
2695
|
+
params: {
|
|
2696
|
+
idTarifa: parseInt(opts.tariffId),
|
|
2697
|
+
idZona: parseInt(opts.zoneId),
|
|
2698
|
+
fechaInicio: opts.start || `${year}-01-01`,
|
|
2699
|
+
fechaFin: opts.end || `${year}-12-31`,
|
|
2700
|
+
market: opts.market
|
|
2701
|
+
}
|
|
2702
|
+
});
|
|
2703
|
+
output(res.data, { ...global, save: opts.save || global.save });
|
|
2704
|
+
} catch (err) {
|
|
2705
|
+
outputError(handleApiError(err));
|
|
2706
|
+
}
|
|
2707
|
+
});
|
|
2708
|
+
consumption.command("from-file").description(
|
|
2709
|
+
"Generate consumption curve from an uploaded file.\nSupported formats: Portuguese EREDES ZIP files.\nExample: suntropy consumption from-file --eredes-zip /path/to/file.zip"
|
|
2710
|
+
).option("--eredes-zip <path>", "Path to Portuguese EREDES ZIP file").option("--save <file>", "Save result to file").action(async (opts) => {
|
|
2711
|
+
try {
|
|
2712
|
+
const global = getGlobalOpts9(consumption);
|
|
2713
|
+
const client = createServiceClient("profiles", global);
|
|
2714
|
+
if (opts.eredesZip) {
|
|
2715
|
+
const FormData = (await import("form-data")).default;
|
|
2716
|
+
const form = new FormData();
|
|
2717
|
+
form.append("file", readFileSync4(opts.eredesZip), {
|
|
2718
|
+
filename: opts.eredesZip.split("/").pop(),
|
|
2719
|
+
contentType: "application/zip"
|
|
2720
|
+
});
|
|
2721
|
+
const res = await client.post("/consumption-files-processor/portugal/eredes-zip", form, {
|
|
2722
|
+
headers: form.getHeaders()
|
|
2723
|
+
});
|
|
2724
|
+
output(res.data, { ...global, save: opts.save || global.save });
|
|
2725
|
+
} else {
|
|
2726
|
+
outputError(new Error("Specify a file format: --eredes-zip <path>"));
|
|
2727
|
+
}
|
|
2728
|
+
} catch (err) {
|
|
2729
|
+
outputError(handleApiError(err));
|
|
2730
|
+
}
|
|
2731
|
+
});
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
// src/commands/solarform/index.ts
|
|
2735
|
+
function getGlobalOpts10(cmd) {
|
|
2736
|
+
let root = cmd;
|
|
2737
|
+
while (root.parent) root = root.parent;
|
|
2738
|
+
return root.opts();
|
|
2739
|
+
}
|
|
2740
|
+
function readStdin2() {
|
|
2741
|
+
return new Promise((resolve, reject) => {
|
|
2742
|
+
let data = "";
|
|
2743
|
+
process.stdin.setEncoding("utf-8");
|
|
2744
|
+
process.stdin.on("data", (chunk) => {
|
|
2745
|
+
data += chunk;
|
|
2746
|
+
});
|
|
2747
|
+
process.stdin.on("end", () => resolve(data));
|
|
2748
|
+
process.stdin.on("error", reject);
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2751
|
+
async function parseData4(data) {
|
|
2752
|
+
if (!data) return void 0;
|
|
2753
|
+
if (data === "-") {
|
|
2754
|
+
const input = await readStdin2();
|
|
2755
|
+
return JSON.parse(input);
|
|
2756
|
+
}
|
|
2757
|
+
return JSON.parse(data);
|
|
2758
|
+
}
|
|
2759
|
+
function unwrapPublicApiResponse(resData) {
|
|
2760
|
+
if (resData && typeof resData === "object" && !Array.isArray(resData)) {
|
|
2761
|
+
const r = resData;
|
|
2762
|
+
if ("code" in r && typeof r.code === "number") {
|
|
2763
|
+
if (r.data !== void 0) return r.data;
|
|
2764
|
+
if (r.error) {
|
|
2765
|
+
const err = r.error;
|
|
2766
|
+
throw new Error(`API error (${err.code || r.code}): ${err.message || JSON.stringify(err)}`);
|
|
2767
|
+
}
|
|
2768
|
+
return resData;
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
return resData;
|
|
2772
|
+
}
|
|
2773
|
+
function compactStudyOutput(study) {
|
|
2774
|
+
if (study === null || study === void 0 || typeof study !== "object") return study;
|
|
2775
|
+
if (Array.isArray(study)) return study.map(compactStudyOutput);
|
|
2776
|
+
const record = study;
|
|
2777
|
+
if (Array.isArray(record.days) && record.days.length > 0 && record.identifier !== void 0) {
|
|
2778
|
+
return { _type: "PowerCurve", days: record.days.length, identifier: record.identifier };
|
|
2779
|
+
}
|
|
2780
|
+
const result = {};
|
|
2781
|
+
for (const [key, val] of Object.entries(record)) {
|
|
2782
|
+
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
2783
|
+
const v = val;
|
|
2784
|
+
if (Array.isArray(v.days) && v.days.length > 0 && v.identifier !== void 0) {
|
|
2785
|
+
result[key] = { _type: "PowerCurve", days: v.days.length, identifier: v.identifier };
|
|
2786
|
+
continue;
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
result[key] = compactStudyOutput(val);
|
|
2790
|
+
}
|
|
2791
|
+
return result;
|
|
2792
|
+
}
|
|
2793
|
+
function registerSolarformCommands(program2) {
|
|
2794
|
+
const solarform = program2.command("solarform").description(
|
|
2795
|
+
'Create solar studies via the Solar Form API.\nTwo modes: "simple" (minimal params, auto-optimized) and "calculate" (full control).\nUse --save flag to persist the study to the database.'
|
|
2796
|
+
);
|
|
2797
|
+
solarform.command("simple").description(
|
|
2798
|
+
'Create a solar study with minimal parameters (simplified mode).\nAutomatically resolves location from region/subregion, applies consumption patterns,\nand optimizes kit selection.\n\nConsumption patterns: Balance, Nightly, Morning, Afternoon, Domestic, Commercial\nConsumption mode: monthlyConsumption (kWh) or monthlySpending (EUR)\nExcesses modes: PPA, gridSelling, noInjection, virtualBattery\n\nExamples:\n suntropy solarform simple --region "Andaluc\xEDa" --sub-region "Sevilla" --consumption 5000\n suntropy solarform simple --region "Catalu\xF1a" --sub-region "Barcelona" --consumption 300 --consumption-mode monthlySpending --save\n suntropy solarform simple --region "Madrid" --sub-region "Madrid" --consumption 8000 --pattern Domestic --kit-id abc123 --save'
|
|
2799
|
+
).requiredOption("--region <name>", "Region name (must exist in database)").requiredOption("--sub-region <name>", "Sub-region name").requiredOption("--consumption <value>", "Consumption value (kWh or EUR depending on --consumption-mode)").option("--pattern <name>", "Consumption pattern: Balance, Nightly, Morning, Afternoon, Domestic, Commercial", "Balance").option("--consumption-mode <mode>", "monthlyConsumption (kWh) or monthlySpending (EUR)", "monthlyConsumption").option("--kit-id <id>", "Use a specific solar kit instead of auto-optimization").option("--excesses-mode <mode>", "Excesses compensation: PPA, gridSelling, noInjection, virtualBattery").option("--assigned-user <uid>", "Assign study to a user UID").option("--email <email>", "Send results to this email").option("--save", "Save study to database").option("--raw", "Return full study with PowerCurve data (no compaction)").option("--save-file <file>", "Save result to local file").action(async (opts) => {
|
|
2800
|
+
try {
|
|
2801
|
+
const global = getGlobalOpts10(solarform);
|
|
2802
|
+
const client = createServiceClient("solar", global);
|
|
2803
|
+
const body = {
|
|
2804
|
+
region: opts.region,
|
|
2805
|
+
subRegion: opts.subRegion,
|
|
2806
|
+
selectedConsumptionPattern: opts.pattern,
|
|
2807
|
+
consumptionQuantity: parseFloat(opts.consumption),
|
|
2808
|
+
consumptionQuantityIntroductionMode: opts.consumptionMode
|
|
2809
|
+
};
|
|
2810
|
+
if (opts.assignedUser) body.assignedUserUID = opts.assignedUser;
|
|
2811
|
+
const params = {};
|
|
2812
|
+
if (opts.save) params.save = "true";
|
|
2813
|
+
if (opts.email) params.email = opts.email;
|
|
2814
|
+
if (opts.kitId) params.solarKitId = opts.kitId;
|
|
2815
|
+
if (opts.excessesMode) params.excessesCompensationMode = opts.excessesMode;
|
|
2816
|
+
const queryString = Object.entries(params).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&");
|
|
2817
|
+
const url = `/api/solar-form/simple${queryString ? "?" + queryString : ""}`;
|
|
2818
|
+
const res = await client.post(url, body);
|
|
2819
|
+
const study = unwrapPublicApiResponse(res.data);
|
|
2820
|
+
const result = opts.raw ? study : compactStudyOutput(study);
|
|
2821
|
+
output(result, { ...global, save: opts.saveFile || global.save });
|
|
2822
|
+
} catch (err) {
|
|
2823
|
+
outputError(handleApiError(err));
|
|
2824
|
+
}
|
|
2825
|
+
});
|
|
2826
|
+
solarform.command("calculate").description(
|
|
2827
|
+
`Create a solar study with full control over all parameters.
|
|
2828
|
+
Pass the complete SimplifiedSolarStudy body as JSON.
|
|
2829
|
+
|
|
2830
|
+
Required body fields:
|
|
2831
|
+
center: { lat, lng } \u2014 Installation coordinates
|
|
2832
|
+
surfaces: [{ path, inclination, orientation }] \u2014 Panel surfaces
|
|
2833
|
+
consumptionMode: "cups" | "consumptionPatterns"
|
|
2834
|
+
clientDetails: { name, email, phone }
|
|
2835
|
+
|
|
2836
|
+
Optional: atrTariff, geographicalZone, location, contractedPower,
|
|
2837
|
+
monthPeriodConsumptionDictionary, selectedConsumptionPattern,
|
|
2838
|
+
consumptionQuantity, consumptionQuantityIntroductionMode
|
|
2839
|
+
|
|
2840
|
+
Examples:
|
|
2841
|
+
suntropy solarform calculate --data '{"center":{"lat":37.39,"lng":-5.99},...}' --save
|
|
2842
|
+
cat study-input.json | suntropy solarform calculate --data - --save --email client@co.com`
|
|
2843
|
+
).requiredOption("--data <json>", "Full SimplifiedSolarStudy JSON body (or - for stdin)").option("--save", "Save study to database").option("--email <email>", "Send results to this email").option("--raw", "Return full study with PowerCurve data (no compaction)").option("--save-file <file>", "Save result to local file").action(async (opts) => {
|
|
2844
|
+
try {
|
|
2845
|
+
const global = getGlobalOpts10(solarform);
|
|
2846
|
+
const client = createServiceClient("solar", global);
|
|
2847
|
+
const body = await parseData4(opts.data);
|
|
2848
|
+
if (!body) {
|
|
2849
|
+
outputError(new Error("--data is required. Pass JSON or - for stdin."));
|
|
2850
|
+
return;
|
|
2851
|
+
}
|
|
2852
|
+
const params = {};
|
|
2853
|
+
if (opts.save) params.save = "true";
|
|
2854
|
+
if (opts.email) params.email = opts.email;
|
|
2855
|
+
const queryString = Object.entries(params).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&");
|
|
2856
|
+
const url = `/api/solar-form${queryString ? "?" + queryString : ""}`;
|
|
2857
|
+
const res = await client.post(url, body);
|
|
2858
|
+
const study = unwrapPublicApiResponse(res.data);
|
|
2859
|
+
if (!study || typeof study === "object" && !Array.isArray(study) && !("selectedSolarStudyMode" in study)) {
|
|
2860
|
+
outputError(new Error('API returned no study data. The /api/solar-form endpoint requires a complete body with: center, surfaces, consumptionMode, and typically locationMode. Use "solarform simple" for minimal-parameter study creation.'));
|
|
2861
|
+
return;
|
|
2862
|
+
}
|
|
2863
|
+
const result = opts.raw ? study : compactStudyOutput(study);
|
|
2864
|
+
output(result, { ...global, save: opts.saveFile || global.save });
|
|
2865
|
+
} catch (err) {
|
|
2866
|
+
outputError(handleApiError(err));
|
|
2867
|
+
}
|
|
2868
|
+
});
|
|
2869
|
+
solarform.command("config").description(
|
|
2870
|
+
"Get the solar form configuration for the authenticated client.\nReturns form settings, appearance, enabled steps, custom fields, etc."
|
|
2871
|
+
).action(async () => {
|
|
2872
|
+
try {
|
|
2873
|
+
const global = getGlobalOpts10(solarform);
|
|
2874
|
+
const client = createServiceClient("solar", global);
|
|
2875
|
+
const res = await client.get("/solar-form/solar-form-config");
|
|
2876
|
+
output(res.data, global);
|
|
2877
|
+
} catch (err) {
|
|
2878
|
+
outputError(handleApiError(err));
|
|
2879
|
+
}
|
|
2880
|
+
});
|
|
2881
|
+
solarform.command("statistics").description(
|
|
2882
|
+
"List or create solar form submission statistics.\nUsed for tracking form analytics and conversion data."
|
|
2883
|
+
).option("--create", "Create a new statistics entry").option("--update", "Update an existing statistics entry").option("--data <json>", "Statistics data as JSON").option("--stats-id <id>", "Statistics ID (for update or linking)").action(async (opts) => {
|
|
2884
|
+
try {
|
|
2885
|
+
const global = getGlobalOpts10(solarform);
|
|
2886
|
+
const client = createServiceClient("solar", global);
|
|
2887
|
+
if (opts.update) {
|
|
2888
|
+
const body = await parseData4(opts.data);
|
|
2889
|
+
const res = await client.put("/solar-form/solar-form-statistics", body);
|
|
2890
|
+
output(res.data, global);
|
|
2891
|
+
} else if (opts.create) {
|
|
2892
|
+
const body = await parseData4(opts.data) || {};
|
|
2893
|
+
const params = {};
|
|
2894
|
+
if (opts.statsId) params.statsId = opts.statsId;
|
|
2895
|
+
const res = await client.post("/solar-form/solar-form-statistics", body, { params });
|
|
2896
|
+
output(res.data, global);
|
|
2897
|
+
} else {
|
|
2898
|
+
outputError(new Error("Specify --create or --update"));
|
|
2899
|
+
}
|
|
2900
|
+
} catch (err) {
|
|
2901
|
+
outputError(handleApiError(err));
|
|
2902
|
+
}
|
|
2903
|
+
});
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
// src/index.ts
|
|
2907
|
+
function createProgram() {
|
|
2908
|
+
const program2 = new Command2();
|
|
2909
|
+
program2.name("suntropy").description("Agent-first CLI for Suntropy solar platform. Optimized for programmatic data manipulation and progressive exploration.").version("0.1.0").option("--format <format>", "Output format: json (default), human, csv", "json").option("--fields <fields>", "Comma-separated fields to include in output").option("--server <url>", "Override API server URL").option("--token <jwt>", "Override authentication token").option("--profile <name>", "Use a specific config profile").option("--verbose", "Show HTTP request/response details on stderr").option("--quiet", "Suppress non-data output").option("--save <file>", "Save output to file (also writes to stdout)");
|
|
2910
|
+
registerAuthCommands(program2);
|
|
2911
|
+
registerConfigCommands(program2);
|
|
2912
|
+
registerInventoryCommands(program2);
|
|
2913
|
+
registerStudiesCommands(program2);
|
|
2914
|
+
registerCurvesCommands(program2);
|
|
2915
|
+
registerConsumptionCommands(program2);
|
|
2916
|
+
registerSolarformCommands(program2);
|
|
2917
|
+
return program2;
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
// bin/suntropy.ts
|
|
2921
|
+
var program = createProgram();
|
|
2922
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
2923
|
+
process.stderr.write(JSON.stringify({ error: true, message: err.message }) + "\n");
|
|
2924
|
+
process.exit(1);
|
|
2925
|
+
});
|
|
2926
|
+
//# sourceMappingURL=suntropy.js.map
|