@enerlence/suntropy-cli 0.1.3 → 0.2.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 +327 -3
- package/dist/bin/suntropy.js.map +1 -1
- package/package.json +1 -1
- package/skills/solar-study.md +50 -26
package/dist/bin/suntropy.js
CHANGED
|
@@ -1986,7 +1986,7 @@ Example: suntropy studies set data --file study.json --data '{"referenceId":"REF
|
|
|
1986
1986
|
outputError(new Error("No energy prices. Use: studies set prices"));
|
|
1987
1987
|
return;
|
|
1988
1988
|
}
|
|
1989
|
-
const { PowerCurve } = await import("energy-types/lib/energy/classes/powerCurve.class");
|
|
1989
|
+
const { PowerCurve } = await import("energy-types/lib/energy/classes/powerCurve.class.js");
|
|
1990
1990
|
const consCurve = new PowerCurve(consumption.days, false, consumption.identifier || "consumption", false);
|
|
1991
1991
|
let totalProdCurve = null;
|
|
1992
1992
|
for (const surf of surfaces) {
|
|
@@ -2116,6 +2116,312 @@ Example: suntropy studies set data --file study.json --data '{"referenceId":"REF
|
|
|
2116
2116
|
outputError(handleApiError(err));
|
|
2117
2117
|
}
|
|
2118
2118
|
});
|
|
2119
|
+
studies.command("optimize-peakpower").description(
|
|
2120
|
+
"Optimize peak power based on consumption.\n\nSupports two modes:\n - Panel mode (default or when solarPanel is set): iterates peak power\n - Kit mode (when solarKit is set or --use-kits): evaluates available kits\n\nOptimization criteria (pick one):\n --energy-savings <pct> Target energy savings percentage\n --raw-consumption <pct> Production as percentage of consumption\n --max-excesses <pct> Max excesses as percentage of production\n --max-overproduction-months <n> Max months with overproduction\n\nSurface constraints:\n If surfaces have area + panel dimensions, max panels per surface is calculated.\n Without area, no surface constraint is applied (unlimited space).\n\nExamples:\n suntropy studies optimize-peakpower --file study.json --energy-savings 70\n suntropy studies optimize-peakpower --file study.json --raw-consumption 100 --use-kits\n suntropy studies optimize-peakpower --file study.json --max-excesses 15"
|
|
2121
|
+
).option("--file <path>", "Study file path").option("--energy-savings <pct>", "Target energy savings %").option("--raw-consumption <pct>", "Target production as % of consumption").option("--max-excesses <pct>", "Max excesses as % of production").option("--max-overproduction-months <n>", "Max months with overproduction").option("--use-kits", "Force kit mode (fetch all active kits and select optimal)").option("--apply", "Apply the result to the study file (set peakpower/kit, recalculate production)").action(async (opts) => {
|
|
2122
|
+
try {
|
|
2123
|
+
const global = getGlobalOpts6(studies);
|
|
2124
|
+
const filePath = resolveFile(opts);
|
|
2125
|
+
const study = readStudy(filePath);
|
|
2126
|
+
const evaluationMode = {};
|
|
2127
|
+
if (opts.energySavings !== void 0) evaluationMode.energyPercentageSavings = parseFloat(opts.energySavings);
|
|
2128
|
+
else if (opts.rawConsumption !== void 0) evaluationMode.rawConsumptionPercentage = parseFloat(opts.rawConsumption);
|
|
2129
|
+
else if (opts.maxExcesses !== void 0) evaluationMode.maxExcessesPercentage = parseFloat(opts.maxExcesses);
|
|
2130
|
+
else if (opts.maxOverproductionMonths !== void 0) evaluationMode.maxNumberOfOverproductionMonths = parseInt(opts.maxOverproductionMonths);
|
|
2131
|
+
else {
|
|
2132
|
+
evaluationMode.rawConsumptionPercentage = 100;
|
|
2133
|
+
}
|
|
2134
|
+
const surfaces = study.surfaces;
|
|
2135
|
+
if (!surfaces?.length) {
|
|
2136
|
+
outputError(new Error("No surfaces in study. Use: studies add surface"));
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
2139
|
+
const consumptionData = study.consumption;
|
|
2140
|
+
if (!consumptionData) {
|
|
2141
|
+
outputError(new Error("No consumption in study. Use: studies set consumption"));
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
2144
|
+
const surfacesWithoutProduction = surfaces.filter((s) => !s.production);
|
|
2145
|
+
if (surfacesWithoutProduction.length > 0) {
|
|
2146
|
+
outputError(new Error(`${surfacesWithoutProduction.length} surface(s) without production. Use: studies calculate production --all-surfaces`));
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
2149
|
+
const { PowerCurve } = await import("energy-types/lib/energy/classes/powerCurve.class.js");
|
|
2150
|
+
const cd = consumptionData;
|
|
2151
|
+
const consumptionCurve = new PowerCurve(cd.days || cd, cd.ignore0 ?? false, cd.identifier || "consumption", cd.parseDate ?? false);
|
|
2152
|
+
const totalConsumption = consumptionCurve.getTotalAcumulate();
|
|
2153
|
+
const baseProduction = {};
|
|
2154
|
+
for (const surface of surfaces) {
|
|
2155
|
+
const sid = surface.surfaceId;
|
|
2156
|
+
const prodData = surface.production;
|
|
2157
|
+
const prodCurve = new PowerCurve(prodData.days || prodData, false, sid, false);
|
|
2158
|
+
const installedPower = surface.installedPower || 5e3;
|
|
2159
|
+
baseProduction[sid] = prodCurve.applyMultiplier(1 / (installedPower / 1e3));
|
|
2160
|
+
}
|
|
2161
|
+
const useKits = opts.useKits || study.peakPowerIntroductionMode === "solarKit";
|
|
2162
|
+
const solarClient = createServiceClient("solar", global);
|
|
2163
|
+
if (useKits) {
|
|
2164
|
+
const phaseNumber = study.phaseNumber || "single_phase";
|
|
2165
|
+
const kitsRes = await solarClient.get("/solar-kits", { params: { limit: 500 } });
|
|
2166
|
+
let allKits = Array.isArray(kitsRes.data) && Array.isArray(kitsRes.data[0]) ? kitsRes.data[0] : Array.isArray(kitsRes.data) ? kitsRes.data : kitsRes.data?.data || [];
|
|
2167
|
+
allKits = allKits.filter((k) => k.phaseNumber === phaseNumber && k.active !== false);
|
|
2168
|
+
const isCoplanar = surfaces.every((s) => !s.panelInclination);
|
|
2169
|
+
allKits = allKits.filter(
|
|
2170
|
+
(k) => isCoplanar === void 0 ? true : isCoplanar === (k.coplanar === null ? true : k.coplanar)
|
|
2171
|
+
);
|
|
2172
|
+
allKits.sort(
|
|
2173
|
+
(a, b) => a.panelNumber * (a.kitSolarPanel?.peakPower || 0) - b.panelNumber * (b.kitSolarPanel?.peakPower || 0)
|
|
2174
|
+
);
|
|
2175
|
+
if (allKits.length === 0) {
|
|
2176
|
+
outputError(new Error("No compatible kits found for current phase/surface configuration"));
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
let selectedKit = null;
|
|
2180
|
+
let bestApproxKit = null;
|
|
2181
|
+
let bestApproxValue = -Infinity;
|
|
2182
|
+
let firstValidKit = null;
|
|
2183
|
+
const optimizationLog = {};
|
|
2184
|
+
for (const kit of allKits) {
|
|
2185
|
+
const kitPanel = kit.kitSolarPanel;
|
|
2186
|
+
if (!kitPanel) continue;
|
|
2187
|
+
let remainingPanels = kit.panelNumber || 0;
|
|
2188
|
+
const surfPanelCount = {};
|
|
2189
|
+
let fits = true;
|
|
2190
|
+
for (const surface of surfaces) {
|
|
2191
|
+
const sid = surface.surfaceId;
|
|
2192
|
+
const area = surface.area;
|
|
2193
|
+
let maxPanels;
|
|
2194
|
+
if (area && kitPanel.width && kitPanel.heigth) {
|
|
2195
|
+
maxPanels = calculateMaxPanels(
|
|
2196
|
+
area,
|
|
2197
|
+
kitPanel,
|
|
2198
|
+
surface.availableAreaPercentage || 85,
|
|
2199
|
+
surface.panelInclination || 0,
|
|
2200
|
+
surface.inclination || 0,
|
|
2201
|
+
surface.panelPosition || "vertical",
|
|
2202
|
+
surface.polygonPath?.[0]?.lat || 37
|
|
2203
|
+
);
|
|
2204
|
+
} else {
|
|
2205
|
+
maxPanels = remainingPanels;
|
|
2206
|
+
}
|
|
2207
|
+
const assigned = Math.min(remainingPanels, maxPanels);
|
|
2208
|
+
surfPanelCount[sid] = assigned;
|
|
2209
|
+
remainingPanels -= assigned;
|
|
2210
|
+
}
|
|
2211
|
+
if (remainingPanels > 0) continue;
|
|
2212
|
+
if (!firstValidKit) firstValidKit = kit;
|
|
2213
|
+
let totalProd = null;
|
|
2214
|
+
for (const surface of surfaces) {
|
|
2215
|
+
const sid = surface.surfaceId;
|
|
2216
|
+
const panelCount = surfPanelCount[sid] || 0;
|
|
2217
|
+
const surfPeakPower = panelCount * kitPanel.peakPower / 1e3;
|
|
2218
|
+
const surfProd = baseProduction[sid].applyMultiplier(surfPeakPower);
|
|
2219
|
+
totalProd = totalProd ? totalProd.aggregatePowerCurve(surfProd) : surfProd;
|
|
2220
|
+
}
|
|
2221
|
+
const netConsumption = consumptionCurve.aggregatePowerCurve(totalProd.applyMultiplier(-1));
|
|
2222
|
+
const kitPeakPower = (kit.panelNumber || 0) * kitPanel.peakPower / 1e3;
|
|
2223
|
+
const totalProdKwh = totalProd.getTotalAcumulate();
|
|
2224
|
+
let criterionValue;
|
|
2225
|
+
let criterionMet = false;
|
|
2226
|
+
if (evaluationMode.energyPercentageSavings !== void 0) {
|
|
2227
|
+
const netFiltered = netConsumption.filterNegativeValues().getTotalAcumulate();
|
|
2228
|
+
criterionValue = (totalConsumption - netFiltered) / totalConsumption * 100;
|
|
2229
|
+
criterionMet = criterionValue >= evaluationMode.energyPercentageSavings;
|
|
2230
|
+
} else if (evaluationMode.rawConsumptionPercentage !== void 0) {
|
|
2231
|
+
criterionValue = totalProdKwh / totalConsumption * 100;
|
|
2232
|
+
criterionMet = criterionValue >= evaluationMode.rawConsumptionPercentage;
|
|
2233
|
+
} else if (evaluationMode.maxExcessesPercentage !== void 0) {
|
|
2234
|
+
const excessesCurve = netConsumption.filterPositiveValues();
|
|
2235
|
+
criterionValue = excessesCurve.getTotalAcumulate() / totalProdKwh * 100 * -1;
|
|
2236
|
+
criterionMet = criterionValue <= evaluationMode.maxExcessesPercentage;
|
|
2237
|
+
} else if (evaluationMode.maxNumberOfOverproductionMonths !== void 0) {
|
|
2238
|
+
const stats = netConsumption.calculateStatistics().statistics.anualMonthAccumulate;
|
|
2239
|
+
let byMonth = stats[Object.keys(stats).find((k) => k !== "definition") || ""] || {};
|
|
2240
|
+
delete byMonth.yearly;
|
|
2241
|
+
criterionValue = Object.values(byMonth).filter((v) => v < 0).length;
|
|
2242
|
+
criterionMet = criterionValue <= evaluationMode.maxNumberOfOverproductionMonths;
|
|
2243
|
+
} else {
|
|
2244
|
+
criterionValue = 0;
|
|
2245
|
+
}
|
|
2246
|
+
optimizationLog[kit.idSolarKit] = {
|
|
2247
|
+
identifier: kit.identifier,
|
|
2248
|
+
peakPower: kitPeakPower,
|
|
2249
|
+
totalProduction: totalProdKwh,
|
|
2250
|
+
criterionValue,
|
|
2251
|
+
criterionMet,
|
|
2252
|
+
panelDistribution: surfPanelCount
|
|
2253
|
+
};
|
|
2254
|
+
if (criterionValue > bestApproxValue) {
|
|
2255
|
+
bestApproxValue = criterionValue;
|
|
2256
|
+
bestApproxKit = kit;
|
|
2257
|
+
}
|
|
2258
|
+
if (criterionMet) {
|
|
2259
|
+
selectedKit = kit;
|
|
2260
|
+
break;
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
if (!selectedKit) {
|
|
2264
|
+
selectedKit = bestApproxKit || firstValidKit;
|
|
2265
|
+
}
|
|
2266
|
+
const resultData = {
|
|
2267
|
+
mode: "kit",
|
|
2268
|
+
evaluationMode,
|
|
2269
|
+
selectedKit: selectedKit ? {
|
|
2270
|
+
idSolarKit: selectedKit.idSolarKit,
|
|
2271
|
+
identifier: selectedKit.identifier,
|
|
2272
|
+
peakPower: (selectedKit.panelNumber || 0) * (selectedKit.kitSolarPanel?.peakPower || 0) / 1e3,
|
|
2273
|
+
panelNumber: selectedKit.panelNumber,
|
|
2274
|
+
price: selectedKit.price
|
|
2275
|
+
} : null,
|
|
2276
|
+
kitsEvaluated: Object.keys(optimizationLog).length,
|
|
2277
|
+
optimizationLog
|
|
2278
|
+
};
|
|
2279
|
+
if (opts.apply && selectedKit) {
|
|
2280
|
+
updateStudy(filePath, (s) => {
|
|
2281
|
+
s.solarKit = selectedKit;
|
|
2282
|
+
s.peakPowerIntroductionMode = "solarKit";
|
|
2283
|
+
s.solarPanel = void 0;
|
|
2284
|
+
s.solarInverters = void 0;
|
|
2285
|
+
s.peakPowerOptimizationMethod = evaluationMode;
|
|
2286
|
+
return void 0;
|
|
2287
|
+
});
|
|
2288
|
+
resultData.applied = true;
|
|
2289
|
+
}
|
|
2290
|
+
output(resultData, global);
|
|
2291
|
+
} else {
|
|
2292
|
+
const panel = study.solarPanel;
|
|
2293
|
+
if (!panel) {
|
|
2294
|
+
outputError(new Error("No solar panel set. Use: studies set panel --panel-id <id>"));
|
|
2295
|
+
return;
|
|
2296
|
+
}
|
|
2297
|
+
const panelPeakPower = panel.peakPower || 450;
|
|
2298
|
+
const panelWidth = panel.width || 1134;
|
|
2299
|
+
const panelHeight = panel.heigth || panel.height || 1762;
|
|
2300
|
+
const panelObj = { peakPower: panelPeakPower, width: panelWidth, heigth: panelHeight };
|
|
2301
|
+
const surfacesMaxPeakPower = {};
|
|
2302
|
+
let totalMaxPeakPower = 0;
|
|
2303
|
+
for (const surface of surfaces) {
|
|
2304
|
+
const sid = surface.surfaceId;
|
|
2305
|
+
const area = surface.area;
|
|
2306
|
+
if (area && panelWidth && panelHeight) {
|
|
2307
|
+
const maxPanels = calculateMaxPanels(
|
|
2308
|
+
area,
|
|
2309
|
+
panelObj,
|
|
2310
|
+
surface.availableAreaPercentage || 85,
|
|
2311
|
+
surface.panelInclination || 0,
|
|
2312
|
+
surface.inclination || 0,
|
|
2313
|
+
surface.panelPosition || "vertical",
|
|
2314
|
+
surface.polygonPath?.[0]?.lat || 37
|
|
2315
|
+
);
|
|
2316
|
+
surfacesMaxPeakPower[sid] = maxPanels * panelPeakPower / 1e3;
|
|
2317
|
+
} else {
|
|
2318
|
+
surfacesMaxPeakPower[sid] = 500;
|
|
2319
|
+
}
|
|
2320
|
+
totalMaxPeakPower += surfacesMaxPeakPower[sid];
|
|
2321
|
+
}
|
|
2322
|
+
const isAscending = evaluationMode.energyPercentageSavings !== void 0 || evaluationMode.economicPercentageSavings !== void 0 || evaluationMode.rawConsumptionPercentage !== void 0;
|
|
2323
|
+
let peakPower = isAscending ? panelPeakPower / 1e3 : totalMaxPeakPower;
|
|
2324
|
+
const optimizationLog = {};
|
|
2325
|
+
let resultPeakPower = {};
|
|
2326
|
+
let lastResult;
|
|
2327
|
+
const MAX_ITERATIONS = 200;
|
|
2328
|
+
const TOLERANCE = 5e-3;
|
|
2329
|
+
let correctionFactor = 1;
|
|
2330
|
+
let itersSinceCorrection = 0;
|
|
2331
|
+
for (let iter = 0; iter < MAX_ITERATIONS && peakPower > 0 && peakPower <= totalMaxPeakPower; iter++) {
|
|
2332
|
+
peakPower = parseFloat(peakPower.toFixed(2));
|
|
2333
|
+
itersSinceCorrection++;
|
|
2334
|
+
if (itersSinceCorrection >= 20) {
|
|
2335
|
+
correctionFactor += 1.1;
|
|
2336
|
+
itersSinceCorrection = 0;
|
|
2337
|
+
}
|
|
2338
|
+
let changeRate;
|
|
2339
|
+
if (peakPower < 20) changeRate = panelPeakPower / 1e3 * correctionFactor;
|
|
2340
|
+
else if (peakPower < 500) changeRate = 5 * correctionFactor;
|
|
2341
|
+
else changeRate = 50 * correctionFactor;
|
|
2342
|
+
let remaining = peakPower;
|
|
2343
|
+
let totalProd = null;
|
|
2344
|
+
resultPeakPower = {};
|
|
2345
|
+
for (const surface of surfaces) {
|
|
2346
|
+
const sid = surface.surfaceId;
|
|
2347
|
+
const surfPP = Math.min(remaining, surfacesMaxPeakPower[sid]);
|
|
2348
|
+
remaining -= surfPP;
|
|
2349
|
+
resultPeakPower[sid] = surfPP;
|
|
2350
|
+
const surfProd = baseProduction[sid].applyMultiplier(surfPP);
|
|
2351
|
+
totalProd = totalProd ? totalProd.aggregatePowerCurve(surfProd) : surfProd;
|
|
2352
|
+
}
|
|
2353
|
+
const netConsumption = consumptionCurve.aggregatePowerCurve(totalProd.applyMultiplier(-1));
|
|
2354
|
+
const totalProdKwh = totalProd.getTotalAcumulate();
|
|
2355
|
+
let criterionValue;
|
|
2356
|
+
let criterionMet = false;
|
|
2357
|
+
if (evaluationMode.energyPercentageSavings !== void 0) {
|
|
2358
|
+
const netFiltered = netConsumption.filterNegativeValues().getTotalAcumulate();
|
|
2359
|
+
criterionValue = (totalConsumption - netFiltered) / totalConsumption * 100;
|
|
2360
|
+
criterionMet = criterionValue >= evaluationMode.energyPercentageSavings;
|
|
2361
|
+
} else if (evaluationMode.rawConsumptionPercentage !== void 0) {
|
|
2362
|
+
criterionValue = totalProdKwh / totalConsumption * 100;
|
|
2363
|
+
criterionMet = criterionValue >= evaluationMode.rawConsumptionPercentage;
|
|
2364
|
+
} else if (evaluationMode.maxExcessesPercentage !== void 0) {
|
|
2365
|
+
const excessesCurve = netConsumption.filterPositiveValues();
|
|
2366
|
+
criterionValue = excessesCurve.getTotalAcumulate() / totalProdKwh * 100 * -1;
|
|
2367
|
+
criterionMet = criterionValue <= evaluationMode.maxExcessesPercentage;
|
|
2368
|
+
} else if (evaluationMode.maxNumberOfOverproductionMonths !== void 0) {
|
|
2369
|
+
const stats = netConsumption.calculateStatistics().statistics.anualMonthAccumulate;
|
|
2370
|
+
let byMonth = stats[Object.keys(stats).find((k) => k !== "definition") || ""] || {};
|
|
2371
|
+
delete byMonth.yearly;
|
|
2372
|
+
criterionValue = Object.values(byMonth).filter((v) => v < 0).length;
|
|
2373
|
+
criterionMet = criterionValue <= evaluationMode.maxNumberOfOverproductionMonths;
|
|
2374
|
+
} else {
|
|
2375
|
+
criterionValue = 0;
|
|
2376
|
+
}
|
|
2377
|
+
optimizationLog[peakPower] = {
|
|
2378
|
+
totalProduction: totalProdKwh,
|
|
2379
|
+
criterionValue,
|
|
2380
|
+
criterionMet,
|
|
2381
|
+
surfacesPeakpower: { ...resultPeakPower }
|
|
2382
|
+
};
|
|
2383
|
+
if (criterionMet) break;
|
|
2384
|
+
if (isAscending && lastResult !== void 0 && lastResult * (1 + TOLERANCE) >= criterionValue) {
|
|
2385
|
+
break;
|
|
2386
|
+
}
|
|
2387
|
+
lastResult = criterionValue;
|
|
2388
|
+
peakPower += isAscending ? changeRate : -changeRate;
|
|
2389
|
+
}
|
|
2390
|
+
const totalOptimizedPeakPower = Object.values(resultPeakPower).reduce((a, b) => a + b, 0);
|
|
2391
|
+
const totalPanels = Math.round(totalOptimizedPeakPower / (panelPeakPower / 1e3));
|
|
2392
|
+
const resultData = {
|
|
2393
|
+
mode: "panel",
|
|
2394
|
+
evaluationMode,
|
|
2395
|
+
optimizedPeakPower: parseFloat(totalOptimizedPeakPower.toFixed(2)),
|
|
2396
|
+
estimatedPanels: totalPanels,
|
|
2397
|
+
surfacesPeakpower: resultPeakPower,
|
|
2398
|
+
iterations: Object.keys(optimizationLog).length,
|
|
2399
|
+
optimizationLog
|
|
2400
|
+
};
|
|
2401
|
+
if (opts.apply) {
|
|
2402
|
+
updateStudy(filePath, (s) => {
|
|
2403
|
+
const surfs = s.surfaces;
|
|
2404
|
+
if (surfs) {
|
|
2405
|
+
for (const surf of surfs) {
|
|
2406
|
+
const sid = surf.surfaceId;
|
|
2407
|
+
if (resultPeakPower[sid] !== void 0) {
|
|
2408
|
+
const surfPanels = Math.round(resultPeakPower[sid] / (panelPeakPower / 1e3));
|
|
2409
|
+
surf.panelNumber = surfPanels;
|
|
2410
|
+
surf.installedPower = resultPeakPower[sid] * 1e3;
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
s.peakPowerOptimizationMethod = evaluationMode;
|
|
2415
|
+
return "surfaces";
|
|
2416
|
+
});
|
|
2417
|
+
resultData.applied = true;
|
|
2418
|
+
}
|
|
2419
|
+
output(resultData, global);
|
|
2420
|
+
}
|
|
2421
|
+
} catch (err) {
|
|
2422
|
+
outputError(handleApiError(err));
|
|
2423
|
+
}
|
|
2424
|
+
});
|
|
2119
2425
|
studies.command("add-comment").description(
|
|
2120
2426
|
'Add a comment to the local study file.\nExample: suntropy studies add-comment --file study.json --content "Panel layout reviewed"'
|
|
2121
2427
|
).option("--file <path>", "Study file path").requiredOption("--content <text>", "Comment text").action(async (opts) => {
|
|
@@ -2209,6 +2515,24 @@ async function generateFromProfile(global, tariff, market, year, mode, data) {
|
|
|
2209
2515
|
}
|
|
2210
2516
|
throw new Error(`Unknown consumption mode: ${mode}`);
|
|
2211
2517
|
}
|
|
2518
|
+
function calculateMaxPanels(areaM2, panel, availablePercentage, panelInclination, surfaceInclination, panelPosition, latitude) {
|
|
2519
|
+
const { heigth, width } = panel;
|
|
2520
|
+
let area = areaM2 * 1e6 * (availablePercentage / 100);
|
|
2521
|
+
const x = 1 / Math.atan(61 - (latitude - surfaceInclination));
|
|
2522
|
+
let totalPanelNumber;
|
|
2523
|
+
if (panelPosition === "horizontal") {
|
|
2524
|
+
const h = width * Math.asin(panelInclination * (Math.PI / 180));
|
|
2525
|
+
const minimumDistance = x * h;
|
|
2526
|
+
const panelArea = (heigth + 20) * (width + minimumDistance);
|
|
2527
|
+
totalPanelNumber = area / panelArea;
|
|
2528
|
+
} else {
|
|
2529
|
+
const h = heigth * Math.asin(panelInclination * (Math.PI / 180));
|
|
2530
|
+
const minimumDistance = x * h;
|
|
2531
|
+
const panelArea = (width + 20) * (heigth + minimumDistance);
|
|
2532
|
+
totalPanelNumber = area / panelArea;
|
|
2533
|
+
}
|
|
2534
|
+
return isNaN(totalPanelNumber) ? 0 : Math.floor(totalPanelNumber);
|
|
2535
|
+
}
|
|
2212
2536
|
function collectAssets(value, previous) {
|
|
2213
2537
|
const [idStr, qtyStr] = value.split(":");
|
|
2214
2538
|
const id = parseInt(idStr);
|
|
@@ -2428,7 +2752,7 @@ function registerStudiesCommands(program2) {
|
|
|
2428
2752
|
outputError(new Error(`Curve "${curveName}" not found or empty in this study.`));
|
|
2429
2753
|
return;
|
|
2430
2754
|
}
|
|
2431
|
-
const { PowerCurve } = await import("energy-types/lib/energy/classes/powerCurve.class");
|
|
2755
|
+
const { PowerCurve } = await import("energy-types/lib/energy/classes/powerCurve.class.js");
|
|
2432
2756
|
const cd = curveData;
|
|
2433
2757
|
const pc = new PowerCurve(cd.days || cd, cd.ignore0 ?? false, cd.identifier || curveName, cd.parseDate ?? false);
|
|
2434
2758
|
const showRaw = opts.raw;
|
|
@@ -2522,7 +2846,7 @@ async function readCurveInput(inputPath) {
|
|
|
2522
2846
|
return JSON.parse(raw);
|
|
2523
2847
|
}
|
|
2524
2848
|
async function buildCurve(data, identifier = "curve") {
|
|
2525
|
-
const { PowerCurve } = await import("energy-types/lib/energy/classes/powerCurve.class");
|
|
2849
|
+
const { PowerCurve } = await import("energy-types/lib/energy/classes/powerCurve.class.js");
|
|
2526
2850
|
if (Array.isArray(data)) {
|
|
2527
2851
|
return new PowerCurve(data, false, identifier, false);
|
|
2528
2852
|
}
|