Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 22 additions & 21 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
{
"name": "@ensol-test/backend",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "nodemon --exec ts-node -r tsconfig-paths/register src/index.ts"
},
"dependencies": {
"axios": "^1.6.7",
"cors": "^2.8.5",
"express": "^4.18.2"
},
"devDependencies": {
"@swc/core": "^1.4.8",
"@types/cors": "^2",
"@types/express": "^4.17.21",
"@types/node": "^20.12.2",
"nodemon": "^3.1.0",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.4.4"
}
"name": "@ensol-test/backend",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "nodemon --exec ts-node -r tsconfig-paths/register src/index.ts"
},
"dependencies": {
"axios": "^1.6.7",
"cors": "^2.8.5",
"express": "^4.18.2",
"express-validator": "^7.2.1"
},
"devDependencies": {
"@swc/core": "^1.4.8",
"@types/cors": "^2",
"@types/express": "^4.17.21",
"@types/node": "^20.12.2",
"nodemon": "^3.1.0",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.4.4"
}
}
69 changes: 69 additions & 0 deletions packages/backend/src/libs/PVGIS/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
RoofInclination,
RoofOrientation,
} from "@ensol-test/types/simulations";
import axios from "axios";
import { PVGISApiPVResponse } from "./types";

const MAP_ORIENTATIONS_TO_JRC_API_ASPECT = {
S: 0,
W: 90,
SW: 45,
SE: -45,
};

export class PVGISApi {
static async getYearlyIrradienceByCoordinates(
latitude: number,
longitude: number,
roofOrientation: RoofOrientation,
roofInclination: RoofInclination,
) {
try {
const baseUrl = "https://re.jrc.ec.europa.eu/api/v5_3/PVcalc";

const params = new URLSearchParams({
lat: latitude.toString(),
lon: longitude.toString(),
loss: "14",
outputformat: "json",
peakpower: "1",
angle: roofInclination.toString(),
mountingplace: "building",
aspect: MAP_ORIENTATIONS_TO_JRC_API_ASPECT[
roofOrientation
].toString(),
});

const response = await axios.get<PVGISApiPVResponse>(
`${baseUrl}?${params}`,
);

const yearlyIrradiance =
response.data.outputs.totals.fixed["H(i)_y"];

return yearlyIrradiance;
} catch (error) {
if (axios.isAxiosError(error)) {
console.error(
"MRcalc API Error:",
error.response?.data || error.message,
);

if (error.response?.status === 400) {
throw new Error("Invalid coordinates or parameters");
} else if (error.response?.status === 404) {
throw new Error(
"No radiation data available for these coordinates",
);
} else if (error.response?.status === 429) {
throw new Error(
"Too many requests. Please try again later",
);
}
}
console.log(error);
throw new Error("Failed to fetch solar irradiance data");
}
}
}
9 changes: 9 additions & 0 deletions packages/backend/src/libs/PVGIS/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type PVGISApiPVResponse = {
outputs: {
totals: {
fixed: {
"H(i)_y": number;
};
};
};
};
36 changes: 36 additions & 0 deletions packages/backend/src/routes/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { RoofOrientation } from "@ensol-test/types/simulations";
import { query, ValidationChain } from "express-validator";

export const validateSimulationParams: ValidationChain[] = [
query("latitude")
.exists()
.withMessage("Latitude is required")
.isFloat({ min: -90, max: 90 })
.withMessage("Latitude must be a number between -90 and 90"),

query("longitude")
.exists()
.withMessage("Longitude is required")
.isFloat({ min: -180, max: 180 })
.withMessage("Longitude must be a number between -180 and 180"),

query("monthlyBill")
.exists()
.withMessage("Monthly bill is required")
.isInt({ min: 1 })
.withMessage("Monthly bill must be a positive integer")
.toInt(),

query("roofInclination")
.exists()
.withMessage("Roof inclination is required")
.isFloat({ min: 0, max: 90 })
.withMessage("Roof inclination must be between 0° and 90°")
.toFloat(),

query("roofOrientation")
.exists()
.withMessage("Roof orientation is required")
.isIn(Object.values(RoofOrientation))
.withMessage("Roof orientation must be one of: W, SW, S, SE"),
];
46 changes: 38 additions & 8 deletions packages/backend/src/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,47 @@
import { asyncErrorMiddleware } from '@ensol-test/backend/middlewares/asyncErrorMiddleware';
import { SimulationParameters, SimulationResponse } from '@ensol-test/types/simulations';
import { asyncErrorMiddleware } from "@ensol-test/backend/middlewares/asyncErrorMiddleware";
import {
RoofOrientation,
SimulationParameters,
SimulationResponse,
} from "@ensol-test/types/simulations";

import express from 'express';
import express from "express";

import { ValidationError, validationResult } from "express-validator";
import { validateSimulationParams } from "./helpers";
import { SimulationParametersQuery } from "../simulation/simulation.types";
import { SimulationBusiness } from "../simulation/simulation.business";

const router = express.Router();

router.get(
'/simulations',
asyncErrorMiddleware<SimulationParameters, SimulationResponse>(async (req, res) => {
//...
"/simulations",
validateSimulationParams,
asyncErrorMiddleware<
{},
SimulationResponse | { errors: ValidationError[] },
{},
SimulationParametersQuery
>(async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const params: SimulationParameters = {
latitude: parseFloat(req.query.latitude),
longitude: parseFloat(req.query.longitude),
monthlyBill: parseInt(req.query.monthlyBill, 10),
roofInclination: parseFloat(req.query.roofInclination),
roofOrientation: req.query.roofOrientation as RoofOrientation,
};

const simulation =
await SimulationBusiness.getSolarSystemSimulation(params);

res.json({});
}),
res.json({
...simulation,
});
}),
);

export { router };
58 changes: 58 additions & 0 deletions packages/backend/src/simulation/simulation.business.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {
SimulationParameters,
SimulationResponse,
} from "@ensol-test/types/simulations";
import { SimulationService } from "./simulation.service";
import { PVGISApi } from "../libs/PVGIS";

export class SimulationBusiness {
static async getSolarSystemSimulation(
simulation: SimulationParameters,
): Promise<SimulationResponse> {
const {
latitude,
longitude,
monthlyBill,
roofInclination,
roofOrientation,
} = simulation;

const yearlyElectricityConsumption =
SimulationService.calculateYearlyElectricityConsumption(
monthlyBill,
);
const yearlyIrradience =
await PVGISApi.getYearlyIrradienceByCoordinates(
latitude,
longitude,
roofOrientation,
roofInclination,
);

const correctedYearlyIrradience =
SimulationService.calculateYearlyIrradienceWithAzimuthAndInclination(
yearlyIrradience,
roofOrientation,
roofInclination,
);
const yearlyProductionPerCapacity =
SimulationService.calculateYearlyProductionPerCapacity(
correctedYearlyIrradience,
);

const yearlyEnergyProduction =
SimulationService.calculateYearlyProduction(
yearlyProductionPerCapacity,
);

const estimatedPanels = SimulationService.calculateEstimatedPanels(
yearlyElectricityConsumption,
yearlyProductionPerCapacity,
);

return {
estimatedPanels,
yearlyEnergyProduction,
};
}
}
71 changes: 71 additions & 0 deletions packages/backend/src/simulation/simulation.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {
RoofInclination,
RoofOrientation,
} from "@ensol-test/types/simulations";

export const ELECTRICITY_COST_PER_KWH_FRANCE_2024 = 0.22;
export const GLOBAL_SYSTEM_EFFICIENCY = 0.8;

export const PANELS_SOLARS = {
"DualSun Flash 425 TopCon": {
powerAreaRatio: 0.22,
efficiency: 0.21,
power: 425,
productionCapacity: 2.98,
},
};

export const EFFICIENCY_LOSS_PER_ORIENTATION: {
[key in RoofOrientation]: {
[key in RoofInclination]: number;
};
} = {
[RoofOrientation.W]: {
[RoofInclination.None]: 0.2,
[RoofInclination.Ten]: 0.18,
[RoofInclination.Twenty]: 0.16,
[RoofInclination.Thirty]: 0.15,
[RoofInclination.Forty]: 0.16,
[RoofInclination.Fifty]: 0.18,
[RoofInclination.Sixty]: 0.2,
[RoofInclination.Seventy]: 0.25,
[RoofInclination.Eighty]: 0.3,
[RoofInclination.Ninety]: 0.35,
},
[RoofOrientation.SW]: {
[RoofInclination.None]: 0.15,
[RoofInclination.Ten]: 0.12,
[RoofInclination.Twenty]: 0.1,
[RoofInclination.Thirty]: 0.08,
[RoofInclination.Forty]: 0.1,
[RoofInclination.Fifty]: 0.12,
[RoofInclination.Sixty]: 0.15,
[RoofInclination.Seventy]: 0.2,
[RoofInclination.Eighty]: 0.25,
[RoofInclination.Ninety]: 0.3,
},
[RoofOrientation.S]: {
[RoofInclination.None]: 0.12,
[RoofInclination.Ten]: 0.08,
[RoofInclination.Twenty]: 0.05,
[RoofInclination.Thirty]: 0.0, // Optimal
[RoofInclination.Forty]: 0.03,
[RoofInclination.Fifty]: 0.06,
[RoofInclination.Sixty]: 0.1,
[RoofInclination.Seventy]: 0.15,
[RoofInclination.Eighty]: 0.2,
[RoofInclination.Ninety]: 0.25,
},
[RoofOrientation.SE]: {
[RoofInclination.None]: 0.15,
[RoofInclination.Ten]: 0.12,
[RoofInclination.Twenty]: 0.1,
[RoofInclination.Thirty]: 0.08,
[RoofInclination.Forty]: 0.1,
[RoofInclination.Fifty]: 0.12,
[RoofInclination.Sixty]: 0.15,
[RoofInclination.Seventy]: 0.2,
[RoofInclination.Eighty]: 0.25,
[RoofInclination.Ninety]: 0.3,
},
};
Loading