Optimized Site Area
MODIFY
src/server/rest/CentralRestServerService.ts
src/server/rest/CentralRestServerService.ts
@@ -144,7 +144,6 @@ class RequestMapper {
[ServerAction.TENANT_LOGO]: TenantService.handleGetTenantLogo.bind(this),
[ServerAction.SITE_AREAS]: SiteAreaService.handleGetSiteAreas.bind(this),
[ServerAction.SITE_AREA]: SiteAreaService.handleGetSiteArea.bind(this),
- [ServerAction.SITE_AREA_IMAGE]: SiteAreaService.handleGetSiteAreaImage.bind(this),
[ServerAction.SITE_AREA_CONSUMPTION]: SiteAreaService.handleGetSiteAreaConsumption.bind(this),
[ServerAction.USERS]: UserService.handleGetUsers.bind(this),
[ServerAction.USER_SITES]: UserService.handleGetSites.bind(this),
@@ -304,6 +303,9 @@ export default class CentralRestServerService {
case ServerAction.SITE_IMAGE:
await SiteService.handleGetSiteImage(action, req, res, next);
break;
+ case ServerAction.SITE_AREA_IMAGE:
+ await SiteAreaService.handleGetSiteAreaImage(action, req, res, next);
+ break;
// Firmware Download
case ServerAction.FIRMWARE_DOWNLOAD:
await ChargingStationService.handleGetFirmware(action, req, res, next);

MODIFY
src/server/rest/v1/service/SiteAreaService.ts
src/server/rest/v1/service/SiteAreaService.ts
@@ -271,7 +271,15 @@ export default class SiteAreaService {
UtilsService.assertIdIsProvided(action, filteredRequest.ID, MODULE_NAME, 'handleGetSiteArea', req.user);
// Get it
const siteArea = await SiteAreaStorage.getSiteArea(req.user.tenantID, filteredRequest.ID,
- { withSite: filteredRequest.WithSite, withChargingStations: filteredRequest.WithChargingStations });
+ {
+ withSite: filteredRequest.WithSite,
+ withChargingStations: filteredRequest.WithChargingStations
+ },
+ [
+ 'id', 'name', 'issuer', 'image', 'address', 'maximumPower', 'numberOfPhases',
+ 'voltage', 'smartCharging', 'accessControl', 'connectorStats', 'siteID', 'site.name'
+ ]
+ );
UtilsService.assertObjectExists(action, siteArea, `Site Area with ID '${filteredRequest.ID}' does not exist`,
MODULE_NAME, 'handleGetSiteArea', req.user);
// Check auth
@@ -285,38 +293,31 @@ export default class SiteAreaService {
});
}
// Return
- res.json(
- // Filter
- SiteAreaSecurity.filterSiteAreaResponse(siteArea, req.user)
- );
+ res.json(siteArea);
next();
}
public static async handleGetSiteAreaImage(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
- // Check if component is active
- UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.ORGANIZATION,
- Action.READ, Entity.SITE_AREA, MODULE_NAME, 'handleGetSiteAreaImage');
// Filter
- const siteAreaID = SiteAreaSecurity.filterSiteAreaRequestByID(req.query);
- UtilsService.assertIdIsProvided(action, siteAreaID, MODULE_NAME, 'handleGetSiteAreaImage', req.user);
- // Get it
- const siteArea = await SiteAreaStorage.getSiteArea(req.user.tenantID, siteAreaID);
- UtilsService.assertObjectExists(action, siteArea, `Site Area with ID '${siteAreaID}' does not exist`,
- MODULE_NAME, 'handleGetSiteAreaImage', req.user);
- // Check auth
- if (!Authorizations.canReadSiteArea(req.user, siteArea.siteID)) {
- throw new AppAuthError({
- errorCode: HTTPAuthError.ERROR,
- user: req.user,
- action: Action.READ, entity: Entity.SITE_AREA,
- module: MODULE_NAME, method: 'handleGetSiteAreaImage',
- value: siteAreaID
- });
- }
+ const filteredRequest = SiteAreaSecurity.filterSiteImageRequest(req.query);
+ UtilsService.assertIdIsProvided(action, filteredRequest.ID, MODULE_NAME, 'handleGetSiteAreaImage', req.user);
// Get it
- const siteAreaImage = await SiteAreaStorage.getSiteAreaImage(req.user.tenantID, siteAreaID);
+ const siteAreaImage = await SiteAreaStorage.getSiteAreaImage(filteredRequest.TenantID, filteredRequest.ID);
// Return
- res.json(siteAreaImage);
+ if (siteAreaImage?.image) {
+ let header = 'image';
+ let encoding: BufferEncoding = 'base64';
+ // Remove encoding header
+ if (siteAreaImage.image.startsWith('data:image/')) {
+ header = siteAreaImage.image.substring(5, siteAreaImage.image.indexOf(';'));
+ encoding = siteAreaImage.image.substring(siteAreaImage.image.indexOf(';') + 1, siteAreaImage.image.indexOf(',')) as BufferEncoding;
+ siteAreaImage.image = siteAreaImage.image.substring(siteAreaImage.image.indexOf(',') + 1);
+ }
+ res.setHeader('content-type', header);
+ res.send(siteAreaImage.image ? Buffer.from(siteAreaImage.image, encoding) : null);
+ } else {
+ res.send(null);
+ }
next();
}
@@ -348,11 +349,11 @@ export default class SiteAreaService {
locMaxDistanceMeters: filteredRequest.LocMaxDistanceMeters,
},
{ limit: filteredRequest.Limit, skip: filteredRequest.Skip, sort: filteredRequest.Sort, onlyRecordCount: filteredRequest.OnlyRecordCount },
- ['id', 'name', 'siteID', 'maximumPower', 'voltage', 'numberOfPhases', 'accessControl', 'smartCharging', 'address',
- 'site.id', 'site.name', 'issuer', 'distanceMeters', 'createdOn', 'createdBy', 'lastChangedOn', 'lastChangedBy']
+ [
+ 'id', 'name', 'siteID', 'maximumPower', 'voltage', 'numberOfPhases', 'accessControl', 'smartCharging', 'address',
+ 'site.id', 'site.name', 'issuer', 'distanceMeters', 'createdOn', 'createdBy', 'lastChangedOn', 'lastChangedBy'
+ ]
);
- // Filter
- SiteAreaSecurity.filterSiteAreasResponse(siteAreas, req.user);
// Return
res.json(siteAreas);
next();
@@ -367,7 +368,8 @@ export default class SiteAreaService {
UtilsService.assertIdIsProvided(action, filteredRequest.SiteAreaID, MODULE_NAME,
'handleGetSiteAreaConsumption', req.user);
// Get it
- const siteArea = await SiteAreaStorage.getSiteArea(req.user.tenantID, filteredRequest.SiteAreaID);
+ const siteArea = await SiteAreaStorage.getSiteArea(req.user.tenantID, filteredRequest.SiteAreaID, {},
+ [ 'id', 'name', 'siteID' ]);
UtilsService.assertObjectExists(action, siteArea, `Site Area with ID '${filteredRequest.SiteAreaID}' does not exist`,
MODULE_NAME, 'handleGetSiteArea', req.user);
// Check auth
@@ -408,9 +410,11 @@ export default class SiteAreaService {
siteAreaID: filteredRequest.SiteAreaID,
startDate: filteredRequest.StartDate,
endDate: filteredRequest.EndDate
- });
+ }, [ 'startedAt', 'instantAmps', 'instantWatts', 'limitAmps', 'limitWatts' ]);
+ // Assign
+ siteArea.values = consumptions;
// Return
- res.json(SiteAreaSecurity.filterSiteAreaConsumptionResponse(siteArea, consumptions, req.user));
+ res.json(siteArea);
next();
}
@@ -518,9 +522,11 @@ export default class SiteAreaService {
// Update
siteArea.name = filteredRequest.name;
siteArea.address = filteredRequest.address;
- siteArea.image = filteredRequest.image;
siteArea.maximumPower = filteredRequest.maximumPower;
siteArea.voltage = filteredRequest.voltage;
+ if (Utils.objectHasProperty(filteredRequest, 'image')) {
+ siteArea.image = filteredRequest.image;
+ }
if (filteredRequest.smartCharging && filteredRequest.numberOfPhases === 1) {
for (const chargingStation of siteArea.chargingStations) {
for (const connector of chargingStation.connectors) {
@@ -551,7 +557,7 @@ export default class SiteAreaService {
siteArea.lastChangedBy = { 'id': req.user.id };
siteArea.lastChangedOn = new Date();
// Update Site Area
- await SiteAreaStorage.saveSiteArea(req.user.tenantID, siteArea, true);
+ await SiteAreaStorage.saveSiteArea(req.user.tenantID, siteArea, Utils.objectHasProperty(filteredRequest, 'image'));
// Retrigger Smart Charging
if (filteredRequest.smartCharging) {
// eslint-disable-next-line @typescript-eslint/no-misused-promises, no-undef

MODIFY
src/server/rest/v1/service/security/ChargingStationSecurity.ts
src/server/rest/v1/service/security/ChargingStationSecurity.ts
@@ -131,7 +131,7 @@ export default class ChargingStationSecurity {
}
}
if (chargingStation.siteArea) {
- filteredChargingStation.siteArea = SiteAreaSecurity.filterSiteAreaResponse(chargingStation.siteArea, loggedUser);
+ filteredChargingStation.siteArea = chargingStation.siteArea;
}
// Sort Connector
filteredChargingStation.connectors.sort(
@@ -160,7 +160,7 @@ export default class ChargingStationSecurity {
filteredChargingStation.id = chargingStation.id;
}
if (chargingStation.siteArea) {
- filteredChargingStation.siteArea = SiteAreaSecurity.filterMinimalSiteAreaResponse(chargingStation.siteArea, loggedUser);
+ filteredChargingStation.siteArea = chargingStation.siteArea;
}
return filteredChargingStation;
}

MODIFY
src/server/rest/v1/service/security/SiteAreaSecurity.ts
src/server/rest/v1/service/security/SiteAreaSecurity.ts
-import { HttpAssignAssetsToSiteAreaRequest, HttpAssignChargingStationToSiteAreaRequest, HttpSiteAreaConsumptionsRequest, HttpSiteAreaRequest, HttpSiteAreasRequest } from '../../../../../types/requests/HttpSiteAreaRequest';
+import { HttpAssignAssetsToSiteAreaRequest, HttpAssignChargingStationToSiteAreaRequest, HttpSiteAreaConsumptionsRequest, HttpSiteAreaImageRequest, HttpSiteAreaRequest, HttpSiteAreasRequest } from '../../../../../types/requests/HttpSiteAreaRequest';
-import Authorizations from '../../../../../authorization/Authorizations';
-import ChargingStationSecurity from './ChargingStationSecurity';
-import Consumption from '../../../../../types/Consumption';
-import { DataResult } from '../../../../../types/DataResult';
import SiteArea from '../../../../../types/SiteArea';
-import SiteSecurity from './SiteSecurity';
-import UserToken from '../../../../../types/UserToken';
import Utils from '../../../../../utils/Utils';
import UtilsSecurity from './UtilsSecurity';
import sanitize from 'mongo-sanitize';
@@ -17,6 +11,13 @@ export default class SiteAreaSecurity {
return sanitize(request.ID);
}
+ public static filterSiteImageRequest(request: any): HttpSiteAreaImageRequest {
+ return {
+ ID: sanitize(request.ID),
+ TenantID: sanitize(request.TenantID),
+ };
+ }
+
public static filterAssignAssetsToSiteAreaRequest(request: any): HttpAssignAssetsToSiteAreaRequest {
return {
siteAreaID: sanitize(request.siteAreaID),
@@ -87,117 +88,20 @@ export default class SiteAreaSecurity {
return SiteAreaSecurity._filterSiteAreaRequest(request);
}
- static filterSiteAreaResponse(siteArea: SiteArea, loggedUser: UserToken, forList = false): SiteArea {
- let filteredSiteArea: SiteArea;
- if (!siteArea) {
- return null;
- }
- // Check auth
- if (Authorizations.canReadSiteArea(loggedUser, siteArea.siteID)) {
- filteredSiteArea = {} as SiteArea;
- filteredSiteArea.id = siteArea.id;
- filteredSiteArea.name = siteArea.name;
- filteredSiteArea.issuer = siteArea.issuer;
- filteredSiteArea.siteID = siteArea.siteID;
- filteredSiteArea.maximumPower = siteArea.maximumPower;
- filteredSiteArea.numberOfPhases = siteArea.numberOfPhases;
- filteredSiteArea.voltage = siteArea.voltage;
- filteredSiteArea.numberOfPhases = siteArea.numberOfPhases;
- filteredSiteArea.smartCharging = siteArea.smartCharging;
- filteredSiteArea.accessControl = siteArea.accessControl;
- if (Utils.objectHasProperty(siteArea, 'address')) {
- filteredSiteArea.address = UtilsSecurity.filterAddressRequest(siteArea.address);
- }
- if (siteArea.connectorStats) {
- filteredSiteArea.connectorStats = siteArea.connectorStats;
- }
- if (siteArea.site) {
- filteredSiteArea.site = siteArea.site;
- }
- if (siteArea.chargingStations) {
- filteredSiteArea.chargingStations = siteArea.chargingStations.map((chargingStation) =>
- ChargingStationSecurity.filterChargingStationResponse(chargingStation, loggedUser)
- );
- }
- if (Utils.objectHasProperty(siteArea, 'distanceMeters')) {
- filteredSiteArea.distanceMeters = siteArea.distanceMeters;
- }
- // Created By / Last Changed By
- if (Authorizations.canUpdateSiteArea(loggedUser, siteArea.siteID)) {
- UtilsSecurity.filterCreatedAndLastChanged(filteredSiteArea, siteArea, loggedUser);
- }
- }
- return filteredSiteArea;
- }
-
- static filterMinimalSiteAreaResponse(siteArea: SiteArea, loggedUser: UserToken): SiteArea {
- let filteredSiteArea: SiteArea;
- if (!siteArea) {
- return null;
- }
- // Check auth
- if (Authorizations.canReadSiteArea(loggedUser, siteArea.siteID)) {
- // Set only necessary info
- filteredSiteArea = {} as SiteArea;
- filteredSiteArea.id = siteArea.id;
- filteredSiteArea.name = siteArea.name;
- filteredSiteArea.siteID = siteArea.siteID;
- filteredSiteArea.maximumPower = siteArea.maximumPower;
- filteredSiteArea.voltage = siteArea.voltage;
- filteredSiteArea.numberOfPhases = siteArea.numberOfPhases;
- }
- return filteredSiteArea;
- }
-
- static filterSiteAreasResponse(siteAreas: DataResult<SiteArea>, loggedUser) {
- const filteredSiteAreas = [];
- if (!siteAreas.result) {
- return null;
- }
- if (!Authorizations.canListSiteAreas(loggedUser)) {
- return null;
- }
- for (const siteArea of siteAreas.result) {
- const filteredSiteArea = SiteAreaSecurity.filterSiteAreaResponse(siteArea, loggedUser);
- if (filteredSiteArea) {
- filteredSiteAreas.push(filteredSiteArea);
- }
- }
- siteAreas.result = filteredSiteAreas;
- }
-
- static filterSiteAreaConsumptionResponse(siteArea: SiteArea, consumptions: Consumption[], loggedUser: UserToken): SiteArea {
- siteArea.values = [];
- if (!consumptions) {
- consumptions = [];
- }
- const filteredSiteArea = SiteAreaSecurity.filterSiteAreaResponse(siteArea, loggedUser);
- if (consumptions.length === 0) {
- filteredSiteArea.values = [];
- return filteredSiteArea;
- }
- // Clean
- filteredSiteArea.values = consumptions.map((consumption) => ({
- date: consumption.endedAt,
- instantWatts: consumption.instantWatts,
- instantAmps: consumption.instantAmps,
- limitWatts: consumption.limitWatts,
- limitAmps: consumption.limitAmps,
- }));
- return filteredSiteArea;
- }
-
private static _filterSiteAreaRequest(request: any): Partial<SiteArea> {
- return {
+ const filteredRequest = {
name: sanitize(request.name),
address: UtilsSecurity.filterAddressRequest(request.address),
- image: sanitize(request.image),
maximumPower: sanitize(request.maximumPower),
numberOfPhases: sanitize(request.numberOfPhases),
voltage: sanitize(request.voltage),
smartCharging: UtilsSecurity.filterBoolean(request.smartCharging),
accessControl: UtilsSecurity.filterBoolean(request.accessControl),
siteID: sanitize(request.siteID)
- };
+ } as Partial<SiteArea>;
+ if (Utils.objectHasProperty(request, 'image')) {
+ filteredRequest.image = sanitize(request.image);
+ }
+ return filteredRequest;
}
}

MODIFY
src/server/rest/v1/service/security/SiteSecurity.ts
src/server/rest/v1/service/security/SiteSecurity.ts
@@ -103,13 +103,13 @@ export default class SiteSecurity {
}
public static _filterSiteRequest(request: any): Partial<Site> {
- const filteredRequest: any = {};
- filteredRequest.name = sanitize(request.name);
- filteredRequest.address = UtilsSecurity.filterAddressRequest(request.address);
- filteredRequest.public = UtilsSecurity.filterBoolean(request.public);
- filteredRequest.autoUserSiteAssignment =
- UtilsSecurity.filterBoolean(request.autoUserSiteAssignment);
- filteredRequest.companyID = sanitize(request.companyID);
+ const filteredRequest = {
+ name: sanitize(request.name),
+ address: UtilsSecurity.filterAddressRequest(request.address),
+ public: UtilsSecurity.filterBoolean(request.public),
+ autoUserSiteAssignment: UtilsSecurity.filterBoolean(request.autoUserSiteAssignment),
+ companyID: sanitize(request.companyID),
+ } as Partial<Site>;
if (Utils.objectHasProperty(request, 'image')) {
filteredRequest.image = sanitize(request.image);
}

MODIFY
src/storage/mongodb/ConsumptionStorage.ts
src/storage/mongodb/ConsumptionStorage.ts
@@ -177,7 +177,9 @@ export default class ConsumptionStorage {
return consumptionsMDB;
}
- static async getSiteAreaConsumptions(tenantID: string, params: { siteAreaID: string; startDate: Date; endDate: Date }, projectFields?: string[]): Promise<Consumption[]> {
+ static async getSiteAreaConsumptions(tenantID: string,
+ params: { siteAreaID: string; startDate: Date; endDate: Date },
+ projectFields?: string[]): Promise<Consumption[]> {
// Debug
const uniqueTimerID = Logging.traceStart(tenantID, MODULE_NAME, 'getSiteAreaConsumptions');
// Check
@@ -248,7 +250,7 @@ export default class ConsumptionStorage {
}
});
// Project
- DatabaseUtils.projectFields(aggregation, projectFields);
+ DatabaseUtils.projectFields(aggregation, projectFields, ['_id']);
// Read DB
const consumptionsMDB = await global.database.getCollection<Consumption>(tenantID, 'consumptions')
.aggregate(...aggregation, { allowDiskUse: true })

MODIFY
src/storage/mongodb/DatabaseUtils.ts
src/storage/mongodb/DatabaseUtils.ts
@@ -248,7 +248,7 @@ export default class DatabaseUtils {
aggregation.push(DatabaseUtils.buildChargingStationInactiveFlagQuery());
}
- public static projectFields(aggregation: any[], projectedFields: string[]): void {
+ public static projectFields(aggregation: any[], projectedFields: string[], removedFields: string[] = []): void {
if (projectedFields) {
const project = {
$project: {}
@@ -256,6 +256,9 @@ export default class DatabaseUtils {
for (const projectedField of projectedFields) {
project.$project[projectedField] = 1;
}
+ for (const removedField of removedFields) {
+ project.$project[removedField] = 0;
+ }
aggregation.push(project);
}
}

MODIFY
src/storage/mongodb/SiteAreaStorage.ts
src/storage/mongodb/SiteAreaStorage.ts
@@ -70,13 +70,14 @@ export default class SiteAreaStorage {
}
public static async getSiteArea(tenantID: string, id: string = Constants.UNKNOWN_OBJECT_ID,
- params: { withSite?: boolean; withChargingStations?: boolean } = {}): Promise<SiteArea> {
+ params: { withSite?: boolean; withChargingStations?: boolean } = {}, projectFields?: string[]): Promise<SiteArea> {
const siteAreasMDB = await SiteAreaStorage.getSiteAreas(tenantID, {
siteAreaIDs: [id],
withSite: params.withSite,
withChargingStations: params.withChargingStations,
- withAvailableChargingStations: true
- }, Constants.DB_PARAMS_SINGLE_RECORD);
+ withAvailableChargingStations: true,
+ withImage: true,
+ }, Constants.DB_PARAMS_SINGLE_RECORD, projectFields);
return siteAreasMDB.count === 1 ? siteAreasMDB.result[0] : null;
}
@@ -130,7 +131,7 @@ export default class SiteAreaStorage {
params: {
siteAreaIDs?: string[]; search?: string; siteIDs?: string[]; withSite?: boolean; issuer?: boolean;
withChargingStations?: boolean; withOnlyChargingStations?: boolean; withAvailableChargingStations?: boolean;
- locCoordinates?: number[]; locMaxDistanceMeters?: number; smartCharging?: boolean;
+ locCoordinates?: number[]; locMaxDistanceMeters?: number; smartCharging?: boolean; withImage?: boolean;
} = {},
dbParams: DbParams, projectFields?: string[]): Promise<DataResult<SiteArea>> {
// Debug
@@ -244,6 +245,21 @@ export default class SiteAreaStorage {
asField: 'chargingStations'
});
}
+ // Site Area Image
+ if (params.withImage) {
+ aggregation.push({
+ $addFields: {
+ image: {
+ $concat: [
+ `${Utils.buildRestServerURL()}/client/util/SiteAreaImage?ID=`,
+ { $toString: '$_id' },
+ `&TenantID=${tenantID}&LastChangedOn=`,
+ { $toString: '$lastChangedOn' }
+ ]
+ }
+ }
+ });
+ }
// Convert Object ID to string
DatabaseUtils.pushConvertObjectIDToString(aggregation, 'siteID');
// Add Last Changed / Created

MODIFY
src/types/SiteArea.ts
src/types/SiteArea.ts
@@ -2,6 +2,7 @@ import ChargingStation, { Voltage } from '../types/ChargingStation';
import Address from './Address';
import ConnectorStats from './ConnectorStats';
+import Consumption from './Consumption';
import CreatedUpdatedProps from './CreatedUpdatedProps';
import Site from '../types/Site';
@@ -20,14 +21,6 @@ export default interface SiteArea extends CreatedUpdatedProps {
accessControl: boolean;
chargingStations: ChargingStation[];
connectorStats: ConnectorStats;
- values: SiteAreaConsumption[];
+ values: Consumption[];
distanceMeters?: number;
}
-
-export interface SiteAreaConsumption {
- date: Date;
- instantWatts: number;
- instantAmps: number;
- limitWatts: number;
- limitAmps: number;
-}

MODIFY
src/types/requests/HttpSiteAreaRequest.ts
src/types/requests/HttpSiteAreaRequest.ts
@@ -28,6 +28,9 @@ export interface HttpAssignChargingStationToSiteAreaRequest {
chargingStationIDs: string[];
}
+export interface HttpSiteAreaImageRequest extends HttpByIDRequest {
+ TenantID: string;
+}
export interface HttpAssignAssetsToSiteAreaRequest {
siteAreaID: string;
assetIDs: string[];

Optimized Site Area