diff --git a/package-lock.json b/package-lock.json index 8fba8f4871..95166665f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ev-server", - "version": "2.7.5", + "version": "2.7.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "ev-server", - "version": "2.7.5", + "version": "2.7.6", "license": "Apache-2.0", "dependencies": { "ajv": "^8.11.0", @@ -66,8 +66,8 @@ "role-acl": "^4.5.4", "simple-odata-server": "^1.1.2", "source-map-support": "^0.5.21", - "stripe": "^9.16.0", - "strong-soap": "^3.4.0", + "stripe": "^11.14.0", + "strong-soap": "^3.4.3", "swagger-ui-express": "^4.5.0", "tslib": "^2.4.0", "tz-lookup": "^6.1.25", @@ -5801,9 +5801,9 @@ } }, "node_modules/@xmldom/xmldom": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.6.tgz", - "integrity": "sha512-HHXP9hskkFQHy8QxxUXkS7946FFIhYVfGqsk0WLwllmexN9x/+R4UBLvurHEuyXRfVEObVR8APuQehykLviwSQ==", + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.6.tgz", + "integrity": "sha512-uRjjusqpoqfmRkTaNuLJ2VohVr67Q5YwDATW3VU7PfzTj6IRaihGrYI7zckGZjxQPBIp63nfvJbM+Yu5ICh0Bg==", "engines": { "node": ">=10.0.0" } @@ -7904,9 +7904,9 @@ "dev": true }, "node_modules/clinic": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/clinic/-/clinic-12.0.0.tgz", - "integrity": "sha512-8qCOpodVGEIFdqf4Ax3T4vPAgaGOC02zJWVoM1Yk6SeAGf9cCiBw7pkND9dldeeDHQwpzweuoPZwLKv7/fab6A==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/clinic/-/clinic-12.1.0.tgz", + "integrity": "sha512-3blFGXMfuHERn33jL7qZYoFTXLgHuz0mL2icDa7KykOvRJtGAeYwOCqeHjedB1q069B/FrCA03mKqNtN72jRlA==", "dev": true, "dependencies": { "@clinic/bubbleprof": "^9.0.0", @@ -22364,15 +22364,15 @@ } }, "node_modules/stripe": { - "version": "9.16.0", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-9.16.0.tgz", - "integrity": "sha512-Dn8K+jSoQcXjxCobRI4HXUdHjOXsiF/KszK49fJnkbeCFjZ3EZxLG2JiM/CX+Hcq27NBDtv/Sxhvy+HhTmvyaQ==", + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-11.14.0.tgz", + "integrity": "sha512-EqIMCKkfkewf3eLJo0fopDy/EN2UF6q7L3NEycwThjZRUj8HFz3BpcEDjCITfNJ04ozrAmZNqFaiW46YG4KUtw==", "dependencies": { "@types/node": ">=8.1.0", - "qs": "^6.10.3" + "qs": "^6.11.0" }, "engines": { - "node": "^8.1 || >=10.*" + "node": ">=12.*" } }, "node_modules/strnum": { @@ -22400,35 +22400,27 @@ } }, "node_modules/strong-soap": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/strong-soap/-/strong-soap-3.4.0.tgz", - "integrity": "sha512-fzMOD8nL2b4X+OTUE3z53RfjC8rlR9o6INsBWTevIF7nDNNNp2zRyKhWrWrBfY9FS9vnJ0oVEwa8aCZJ8Ukg+w==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/strong-soap/-/strong-soap-3.4.3.tgz", + "integrity": "sha512-SW1/5c56a1gQmcCGzbVV5ox+xdy0GohA4vyOoOW+lfelj+jO+lIKSQHJDHJDeFUju9YHU3ddLK7QFTR08Br5WA==", "dependencies": { "compress": "^0.99.0", - "debug": "^4.1.1", + "debug": "^4.3.4", "httpntlm-maa": "^2.0.6", - "lodash": "^4.17.20", + "lodash": "^4.17.21", "node-rsa": "^1.1.1", - "request": "^2.72.0", + "request": "^2.88.2", "sax": "^1.2", - "selectn": "^1.0.20", + "selectn": "^1.1.2", "strong-globalize": "^6.0.5", - "uuid": "^8.3.1", - "xml-crypto": "^2.1.3", + "uuid": "^9.0.0", + "xml-crypto": "^3.0.1", "xmlbuilder": "^10.1.1" }, "engines": { "node": ">=8.11.1" } }, - "node_modules/strong-soap/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/strong-soap/node_modules/xmlbuilder": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", @@ -24631,11 +24623,11 @@ } }, "node_modules/xml-crypto": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-2.1.4.tgz", - "integrity": "sha512-ModFeGOy67L/XXHcuepnYGF7DASEDw7fhvy+qIs1ORoH55G1IIr+fN0kaMtttwvmNFFMskD9AHro8wx352/mUg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-3.0.1.tgz", + "integrity": "sha512-7XrwB3ujd95KCO6+u9fidb8ajvRJvIfGNWD0XLJoTWlBKz+tFpUzEYxsN+Il/6/gHtEs1RgRh2RH+TzhcWBZUw==", "dependencies": { - "@xmldom/xmldom": "^0.7.0", + "@xmldom/xmldom": "^0.8.5", "xpath": "0.0.32" }, "engines": { @@ -29346,9 +29338,9 @@ "requires": {} }, "@xmldom/xmldom": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.6.tgz", - "integrity": "sha512-HHXP9hskkFQHy8QxxUXkS7946FFIhYVfGqsk0WLwllmexN9x/+R4UBLvurHEuyXRfVEObVR8APuQehykLviwSQ==" + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.6.tgz", + "integrity": "sha512-uRjjusqpoqfmRkTaNuLJ2VohVr67Q5YwDATW3VU7PfzTj6IRaihGrYI7zckGZjxQPBIp63nfvJbM+Yu5ICh0Bg==" }, "@xtuc/ieee754": { "version": "1.2.0", @@ -31004,9 +30996,9 @@ "dev": true }, "clinic": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/clinic/-/clinic-12.0.0.tgz", - "integrity": "sha512-8qCOpodVGEIFdqf4Ax3T4vPAgaGOC02zJWVoM1Yk6SeAGf9cCiBw7pkND9dldeeDHQwpzweuoPZwLKv7/fab6A==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/clinic/-/clinic-12.1.0.tgz", + "integrity": "sha512-3blFGXMfuHERn33jL7qZYoFTXLgHuz0mL2icDa7KykOvRJtGAeYwOCqeHjedB1q069B/FrCA03mKqNtN72jRlA==", "dev": true, "requires": { "@clinic/bubbleprof": "^9.0.0", @@ -42573,12 +42565,12 @@ "devOptional": true }, "stripe": { - "version": "9.16.0", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-9.16.0.tgz", - "integrity": "sha512-Dn8K+jSoQcXjxCobRI4HXUdHjOXsiF/KszK49fJnkbeCFjZ3EZxLG2JiM/CX+Hcq27NBDtv/Sxhvy+HhTmvyaQ==", + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-11.14.0.tgz", + "integrity": "sha512-EqIMCKkfkewf3eLJo0fopDy/EN2UF6q7L3NEycwThjZRUj8HFz3BpcEDjCITfNJ04ozrAmZNqFaiW46YG4KUtw==", "requires": { "@types/node": ">=8.1.0", - "qs": "^6.10.3" + "qs": "^6.11.0" } }, "strnum": { @@ -42603,29 +42595,24 @@ } }, "strong-soap": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/strong-soap/-/strong-soap-3.4.0.tgz", - "integrity": "sha512-fzMOD8nL2b4X+OTUE3z53RfjC8rlR9o6INsBWTevIF7nDNNNp2zRyKhWrWrBfY9FS9vnJ0oVEwa8aCZJ8Ukg+w==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/strong-soap/-/strong-soap-3.4.3.tgz", + "integrity": "sha512-SW1/5c56a1gQmcCGzbVV5ox+xdy0GohA4vyOoOW+lfelj+jO+lIKSQHJDHJDeFUju9YHU3ddLK7QFTR08Br5WA==", "requires": { "compress": "^0.99.0", - "debug": "^4.1.1", + "debug": "^4.3.4", "httpntlm-maa": "^2.0.6", - "lodash": "^4.17.20", + "lodash": "^4.17.21", "node-rsa": "^1.1.1", - "request": "^2.72.0", + "request": "^2.88.2", "sax": "^1.2", - "selectn": "^1.0.20", + "selectn": "^1.1.2", "strong-globalize": "^6.0.5", - "uuid": "^8.3.1", - "xml-crypto": "^2.1.3", + "uuid": "^9.0.0", + "xml-crypto": "^3.0.1", "xmlbuilder": "^10.1.1" }, "dependencies": { - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" - }, "xmlbuilder": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", @@ -44294,11 +44281,11 @@ } }, "xml-crypto": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-2.1.4.tgz", - "integrity": "sha512-ModFeGOy67L/XXHcuepnYGF7DASEDw7fhvy+qIs1ORoH55G1IIr+fN0kaMtttwvmNFFMskD9AHro8wx352/mUg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-3.0.1.tgz", + "integrity": "sha512-7XrwB3ujd95KCO6+u9fidb8ajvRJvIfGNWD0XLJoTWlBKz+tFpUzEYxsN+Il/6/gHtEs1RgRh2RH+TzhcWBZUw==", "requires": { - "@xmldom/xmldom": "^0.7.0", + "@xmldom/xmldom": "^0.8.5", "xpath": "0.0.32" } }, diff --git a/package.json b/package.json index afbba662cf..da5ddbcd45 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ev-server", - "version": "2.7.5", + "version": "2.7.6", "engines": { "node": "16.x.x", "npm": "8.x.x" @@ -153,8 +153,8 @@ "role-acl": "^4.5.4", "simple-odata-server": "^1.1.2", "source-map-support": "^0.5.21", - "stripe": "^9.16.0", - "strong-soap": "^3.4.0", + "stripe": "^11.14.0", + "strong-soap": "^3.4.3", "swagger-ui-express": "^4.5.0", "tslib": "^2.4.0", "tz-lookup": "^6.1.25", diff --git a/src/assets/configs-ci b/src/assets/configs-ci index 39daf9adb5..d68f9a8314 160000 --- a/src/assets/configs-ci +++ b/src/assets/configs-ci @@ -1 +1 @@ -Subproject commit 39daf9adb5058cdc3d1d16bf309d0df6661a289f +Subproject commit d68f9a8314efc19d9ff4f37ac00c5e51970466bd diff --git a/src/assets/schemas/common/common.json b/src/assets/schemas/common/common.json index 8185dbd47f..73c53d310b 100644 --- a/src/assets/schemas/common/common.json +++ b/src/assets/schemas/common/common.json @@ -125,6 +125,25 @@ } } }, + "smartChargingSessionParameters": { + "type": "object", + "properties": { + "departureTime": { + "type": "string", + "sanitize": "mongo", + "pattern": "^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$", + "nullable": true + }, + "carStateOfCharge": { + "type": "integer", + "sanitize": "mongo" + }, + "targetStateOfCharge": { + "type": "integer", + "sanitize": "mongo" + } + } + }, "acceptEula": { "type": "boolean", "sanitize": "mongo" diff --git a/src/assets/server/rest/v1/schemas/setting/setting-smart-charging-set.json b/src/assets/server/rest/v1/schemas/setting/setting-smart-charging-set.json index a4952a0ff9..f6abcc3f73 100644 --- a/src/assets/server/rest/v1/schemas/setting/setting-smart-charging-set.json +++ b/src/assets/server/rest/v1/schemas/setting/setting-smart-charging-set.json @@ -9,13 +9,15 @@ "sapSmartCharging": { "type": "object", "properties": { - "limitBufferAC" : { + "limitBufferAC": { "type": "number", - "sanitize": "mongo" + "sanitize": "mongo", + "minimum": 1 }, "limitBufferDC": { "type": "number", - "sanitize": "mongo" + "sanitize": "mongo", + "minimum": 1 }, "optimizerUrl": { "type": "string", @@ -32,13 +34,19 @@ "user": { "type": "string", "sanitize": "mongo" + }, + "prioritizationParametersActive": { + "type": "boolean", + "sanitize": "mongo" } } }, "type": { "type": "string", "sanitize": "mongo", - "enum": ["sapSmartCharging"] + "enum": [ + "sapSmartCharging" + ] } }, "links": { @@ -55,7 +63,9 @@ "identifier": { "type": "string", "sanitize": "mongo", - "enum": ["smartCharging"] + "enum": [ + "smartCharging" + ] }, "sensitiveData": { "$ref": "setting#/definitions/sensitiveData" diff --git a/src/assets/server/rest/v1/schemas/site-area/site-area-create.json b/src/assets/server/rest/v1/schemas/site-area/site-area-create.json index 24f107ed38..7b493d30ca 100644 --- a/src/assets/server/rest/v1/schemas/site-area/site-area-create.json +++ b/src/assets/server/rest/v1/schemas/site-area/site-area-create.json @@ -22,6 +22,9 @@ "$ref": "site-area#/definitions/smartCharging", "default": false }, + "smartChargingSessionParameters": { + "$ref": "common#/definitions/smartChargingSessionParameters" + }, "accessControl": { "$ref": "site-area#/definitions/accessControl" }, diff --git a/src/assets/server/rest/v1/schemas/site-area/site-area-update.json b/src/assets/server/rest/v1/schemas/site-area/site-area-update.json index 4b2b016269..c8d3096408 100644 --- a/src/assets/server/rest/v1/schemas/site-area/site-area-update.json +++ b/src/assets/server/rest/v1/schemas/site-area/site-area-update.json @@ -25,6 +25,9 @@ "$ref": "site-area#/definitions/smartCharging", "default": false }, + "smartChargingSessionParameters": { + "$ref": "common#/definitions/smartChargingSessionParameters" + }, "accessControl": { "$ref": "site-area#/definitions/accessControl" }, diff --git a/src/assets/server/rest/v1/schemas/user/user-session-context-get.json b/src/assets/server/rest/v1/schemas/user/user-session-context-get.json index ef364ffe95..25246f7c59 100644 --- a/src/assets/server/rest/v1/schemas/user/user-session-context-get.json +++ b/src/assets/server/rest/v1/schemas/user/user-session-context-get.json @@ -14,6 +14,12 @@ }, "ProjectFields": { "$ref": "common#/definitions/projectFields" + }, + "CarID": { + "$ref": "car#/definitions/id" + }, + "TagID": { + "$ref": "tag#/definitions/id" } }, "required": [ diff --git a/src/authorization/AuthorizationsDefinition.ts b/src/authorization/AuthorizationsDefinition.ts index fafdcc19fe..3454ec4fb3 100644 --- a/src/authorization/AuthorizationsDefinition.ts +++ b/src/authorization/AuthorizationsDefinition.ts @@ -413,7 +413,8 @@ export const AUTHORIZATION_DEFINITION: AuthorizationDefinition = { resource: Entity.SITE_AREA, action: [Action.READ, Action.READ_CHARGING_STATIONS_FROM_SITE_AREA], attributes: [ - 'id', 'name', 'issuer', 'image', 'maximumPower', 'numberOfPhases', 'voltage', 'smartCharging', 'accessControl', + 'id', 'name', 'issuer', 'image', 'maximumPower', 'numberOfPhases', 'voltage', 'smartCharging', 'smartChargingSessionParameters', + 'smartChargingSessionParameters.departureTime', 'smartChargingSessionParameters.carStateOfCharge', 'smartChargingSessionParameters.targetStateOfCharge', 'accessControl', 'connectorStats', 'siteID', 'site.name', 'site.public', 'parentSiteAreaID', 'parentSiteArea.name', 'tariffID', 'address.address1', 'address.address2', 'address.postalCode', 'address.city', 'address.department', 'address.region', 'address.country', 'address.coordinates' @@ -1309,6 +1310,7 @@ export const AUTHORIZATION_DEFINITION: AuthorizationDefinition = { attributes: [ 'id', 'name', 'issuer', 'image', 'maximumPower', 'numberOfPhases', 'voltage', 'smartCharging', 'accessControl', 'connectorStats', 'siteID', + 'smartChargingSessionParameters.departureTime', 'smartChargingSessionParameters.carStateOfCharge', 'smartChargingSessionParameters.targetStateOfCharge', 'parentSiteAreaID', 'site.name', 'parentSiteArea.name', 'address.address1', 'address.address2', 'address.postalCode', 'address.city', 'address.department', 'address.region', 'address.country', 'address.coordinates' @@ -1865,6 +1867,7 @@ export const AUTHORIZATION_DEFINITION: AuthorizationDefinition = { attributes: [ 'id', 'name', 'issuer', 'image', 'maximumPower', 'numberOfPhases', 'voltage', 'smartCharging', 'accessControl', 'connectorStats', + 'smartChargingSessionParameters.departureTime', 'smartChargingSessionParameters.carStateOfCharge', 'smartChargingSessionParameters.targetStateOfCharge', 'siteID', 'parentSiteAreaID', 'site.name', 'parentSiteArea.name', 'address.address1', 'address.address2', 'address.postalCode', 'address.city', 'address.department', 'address.region', 'address.country', 'address.coordinates' @@ -2336,6 +2339,7 @@ export const AUTHORIZATION_DEFINITION: AuthorizationDefinition = { attributes: [ 'id', 'name', 'issuer', 'image', 'maximumPower', 'numberOfPhases', 'voltage', 'smartCharging', 'accessControl', 'connectorStats', + 'smartChargingSessionParameters.departureTime', 'smartChargingSessionParameters.carStateOfCharge', 'smartChargingSessionParameters.targetStateOfCharge', 'siteID', 'site.name', 'site.public', 'tariffID', 'address.address1', 'address.address2', 'address.postalCode', 'address.city', 'address.department', 'address.region', 'address.country', 'address.coordinates' diff --git a/src/integration/billing/stripe/StripeHelpers.ts b/src/integration/billing/stripe/StripeHelpers.ts index 5a2e4d80d2..9a5e95aaf2 100644 --- a/src/integration/billing/stripe/StripeHelpers.ts +++ b/src/integration/billing/stripe/StripeHelpers.ts @@ -62,7 +62,7 @@ export default class StripeHelpers { }; } - public static guessStripeRootCause(error: Stripe.StripeError): { errorType: BillingErrorType, errorCode:BillingErrorCode } { + public static guessStripeRootCause(error: Stripe.errors.StripeError): { errorType: BillingErrorType, errorCode:BillingErrorCode } { let errorType: BillingErrorType, errorCode: BillingErrorCode; // eslint-disable-next-line @typescript-eslint/no-unused-vars const { statusCode, type, rawType, code, decline_code, } = error ; diff --git a/src/integration/smart-charging/SmartChargingHelper.ts b/src/integration/smart-charging/SmartChargingHelper.ts new file mode 100644 index 0000000000..1215b6169b --- /dev/null +++ b/src/integration/smart-charging/SmartChargingHelper.ts @@ -0,0 +1,73 @@ +import ChargingStation, { CurrentType } from '../../types/ChargingStation'; +import Tenant, { TenantComponents } from '../../types/Tenant'; + +import { Car } from '../../types/Car'; +import SettingStorage from '../../storage/mongodb/SettingStorage'; +import { SmartChargingRuntimeSessionParameters } from '../../types/Transaction'; +import UserToken from '../../types/UserToken'; +import Utils from '../../utils/Utils'; +import moment from 'moment'; // moment-timezone? + +export default class SmartChargingHelper { + + public static getExpectedDepartureTime(chargingStation: ChargingStation, expectedDepartureTime: string): Date { + const departureTime = moment(expectedDepartureTime, 'HH:mm'); + let departureDate: moment.Moment; + const timezone = Utils.getTimezone(chargingStation.coordinates); + if (timezone) { + // Timezone of the charging station + departureDate = moment().tz(timezone); + } + departureDate.set({ + hour: departureTime.get('hour'), + minute: departureTime.get('minute'), + }); + if (departureDate.isBefore(moment())) { + departureDate = departureDate.add(1, 'day'); + } + return departureDate.toDate(); + } + + public static async getSessionParameters(tenant: Tenant, user: UserToken, chargingStation: ChargingStation, connectorID: number, car: Car) + : Promise { + // Check prerequisites + if (chargingStation.excludeFromSmartCharging + || !chargingStation.siteArea?.smartCharging + || !chargingStation.capabilities?.supportChargingProfiles + || !Utils.isComponentActiveFromToken(user, TenantComponents.SMART_CHARGING)) { + return null; + } + const smartChargingSettings = await SettingStorage.getSmartChargingSettings(tenant); + // Build the smart charging session parameters + if (smartChargingSettings.sapSmartCharging?.prioritizationParametersActive) { + // Default values + const parameters = chargingStation.siteArea?.smartChargingSessionParameters; + const targetStateOfCharge = parameters?.targetStateOfCharge ?? 70; + const carStateOfCharge = parameters?.carStateOfCharge ?? 30; + const expectedDepartureTime: string = parameters?.departureTime || '18:00'; + const departureTime = SmartChargingHelper.getExpectedDepartureTime(chargingStation, expectedDepartureTime); + if (Utils.getChargingStationCurrentType(chargingStation, null, connectorID) === CurrentType.DC) { + // DC Charger + return { + departureTime: null, + targetStateOfCharge, + carStateOfCharge: null, + }; + } else if (car?.carConnectorData?.carConnectorID) { + // AC Charger but with a Car Connector properly set + return { + departureTime, + targetStateOfCharge, + carStateOfCharge: null, + }; + } + // AC charger with no CAR Connector + return { + departureTime, + carStateOfCharge, + targetStateOfCharge + }; + } + return null; + } +} diff --git a/src/integration/smart-charging/sap-smart-charging/SapSmartChargingIntegration.ts b/src/integration/smart-charging/sap-smart-charging/SapSmartChargingIntegration.ts index 7cea8d337c..ae114dc4e5 100644 --- a/src/integration/smart-charging/sap-smart-charging/SapSmartChargingIntegration.ts +++ b/src/integration/smart-charging/sap-smart-charging/SapSmartChargingIntegration.ts @@ -4,6 +4,7 @@ import ChargingStation, { ChargePoint, Connector, CurrentType, StaticLimitAmps, import { ConnectorAmps, ExcludedAmperage, OptimizerCar, OptimizerCarConnectorAssignment, OptimizerChargingProfilesRequest, OptimizerChargingStationConnectorFuse, OptimizerChargingStationFuse, OptimizerFuse, OptimizerFuseTree, OptimizerFuseTreeNode, OptimizerResult } from '../../../types/Optimizer'; import { ServerAction, ServerProtocol } from '../../../types/Server'; import Tenant, { TenantComponents } from '../../../types/Tenant'; +import Transaction, { SmartChargingSessionParameters } from '../../../types/Transaction'; import AssetStorage from '../../../storage/mongodb/AssetStorage'; import { AssetType } from '../../../types/Asset'; @@ -19,7 +20,6 @@ import { SapSmartChargingSetting } from '../../../types/Setting'; import SiteArea from '../../../types/SiteArea'; import SiteAreaStorage from '../../../storage/mongodb/SiteAreaStorage'; import SmartChargingIntegration from '../SmartChargingIntegration'; -import Transaction from '../../../types/Transaction'; import TransactionStorage from '../../../storage/mongodb/TransactionStorage'; import Utils from '../../../utils/Utils'; import moment from 'moment'; @@ -307,7 +307,7 @@ export default class SapSmartChargingIntegration extends SmartChargingIntegratio // Build car let car = {} as OptimizerCar; // If Car ID is provided - build custom car - car = this.buildCar(fuseID, chargingStation, transaction, currentChargingProfiles); + car = this.buildCar(fuseID, chargingStation, transaction, currentChargingProfiles, siteArea); cars.push(car); // Assign car to the connector carConnectorAssignments.push({ @@ -513,12 +513,11 @@ export default class SapSmartChargingIntegration extends SmartChargingIntegratio canLoadPhase3: 1, id: fuseID, timestampArrival: 0, // Timestamp arrival is set to 0 in order to get profiles for the next 24h. The arrival time has no real influence to the algorithm of the optimizer - // TimestampDeparture is not useful for the time being, because if hard coded it lets the request fail after 17:15, can be taken in again, when the user is able to enter a departure time (with a check if it is after the current time) - // timestampDeparture: 62100, // Mock timestamp departure (17:15) - recommendation from Oliver carType: 'BEV', maxCapacity: 100 * 1000 / voltage, // Battery capacity in Amp.h (fixed to 100kW.h) minLoadingState: (100 * 1000 / voltage) * 0.5, // Battery level at the end of the charge in Amp.h set at 50% (fixed to 50kW.h) - startCapacity: transaction.currentTotalConsumptionWh / voltage, // Total consumption in Amp.h + startCapacity: 0, + chargedCapacity: transaction.currentTotalConsumptionWh / voltage, // Total consumption in Amp.h minCurrent: StaticLimitAmps.MIN_LIMIT_PER_PHASE * 3, minCurrentPerPhase: StaticLimitAmps.MIN_LIMIT_PER_PHASE, maxCurrent: maxConnectorAmpsPerPhase * 3, // Charge capability in Amps @@ -531,19 +530,21 @@ export default class SapSmartChargingIntegration extends SmartChargingIntegratio return car; } - private buildCar(fuseID: { value: number }, chargingStation: ChargingStation, transaction: Transaction, currentChargingProfiles: ChargingProfile[]): OptimizerCar { + private buildCar(fuseID: { value: number }, chargingStation: ChargingStation, transaction: Transaction, currentChargingProfiles: ChargingProfile[], + siteArea: SiteArea): OptimizerCar { const voltage = Utils.getChargingStationVoltage(chargingStation); const customCar = this.buildSafeCar(fuseID.value, chargingStation, transaction); + const currentType = Utils.getChargingStationCurrentType(chargingStation, null, transaction.connectorId); // Handle provided Car if (!Utils.isNullOrUndefined(transaction.car)) { // Setting limit from car only for 3 phased stations (AmpPerPhase-capability variates on single phased charging) - if (Utils.getChargingStationCurrentType(chargingStation, null, transaction.connectorId) === CurrentType.AC && + if (currentType === CurrentType.AC && Utils.getNumberOfConnectedPhases(chargingStation, null, transaction.connectorId) === 3) { if (transaction.car?.converter?.amperagePerPhase > 0) { customCar.maxCurrentPerPhase = transaction.car.converter.amperagePerPhase; // Charge capability in Amps per phase customCar.maxCurrent = transaction.car.converter.amperagePerPhase * 3; // Charge capability in Amps } - } else if (Utils.getChargingStationCurrentType(chargingStation, null, transaction.connectorId) === CurrentType.DC) { + } else if (currentType === CurrentType.DC) { if (transaction?.carCatalog?.fastChargePowerMax > 0) { const maxDCCurrent = Utils.convertWattToAmp( chargingStation, null, transaction.connectorId, transaction.carCatalog.fastChargePowerMax * 1000); // Charge capability in Amps @@ -559,7 +560,7 @@ export default class SapSmartChargingIntegration extends SmartChargingIntegratio // Override this.overrideCarWithRuntimeData(chargingStation, transaction, customCar, currentChargingProfiles); // Check if CS is DC and calculate real consumption at the grid - if (Utils.getChargingStationCurrentType(chargingStation, null, transaction.connectorId) === CurrentType.DC) { + if (currentType === CurrentType.DC) { const connector = Utils.getConnectorFromID(chargingStation, transaction.connectorId); const chargePoint = Utils.getChargePointFromID(chargingStation, connector?.chargePointID); if (chargePoint?.efficiency > 0) { @@ -571,9 +572,88 @@ export default class SapSmartChargingIntegration extends SmartChargingIntegratio customCar.maxCurrent = customCar.maxCurrentPerPhase * 3; } } + // Check smart charging session parameters + if (this.setting.prioritizationParametersActive) { + // Get default session Parameters + const smartChargingSessionParameters = this.getSmartChargingSessionParameters(siteArea); + // Handle each parameter + this.handleCurrentStateOfCharge(customCar, transaction, smartChargingSessionParameters.carStateOfCharge); + this.handleTargetStateOfCharge(customCar, transaction, smartChargingSessionParameters.targetStateOfCharge); + this.handleTimestampDeparture(customCar, transaction, currentType, smartChargingSessionParameters.departureTime); + } return customCar; } + private getSmartChargingSessionParameters(siteArea: SiteArea): SmartChargingSessionParameters { + // Method will be extended with further entities in the future + return { + carStateOfCharge: siteArea.smartChargingSessionParameters?.carStateOfCharge ?? 25, + targetStateOfCharge: siteArea.smartChargingSessionParameters?.targetStateOfCharge ?? 50, + departureTime: siteArea.smartChargingSessionParameters?.departureTime ?? null, + }; + } + + private handleCurrentStateOfCharge(customCar: OptimizerCar, transaction: Transaction, defaultCurrentStateOfCharge: number): void { + // Check if technical state of charge is available + if (transaction.stateOfCharge > 0) { + customCar.startCapacity = (transaction.stateOfCharge / 100) * customCar.maxCapacity; + // Check if manual state of charge is available + } else if (transaction.carStateOfCharge > 0) { + customCar.startCapacity = (transaction.carStateOfCharge / 100) * customCar.maxCapacity; + // Handle if no state of charge is available + } else { + customCar.startCapacity = (defaultCurrentStateOfCharge / 100) * customCar.maxCapacity; + } + // Adjust battery size, when coming close to 100% state of charge (otherwise car would be suspended, also when not fully charged in real life) + if ((customCar.chargedCapacity + customCar.startCapacity) > (0.9 * customCar.maxCapacity)) { + customCar.maxCapacity *= 1.1; + } + } + + private handleTargetStateOfCharge(customCar: OptimizerCar, transaction: Transaction, defaultTargetStateOfCharge: number): void { + // Check if manual target state of charge is available + if (transaction.targetStateOfCharge > 0) { + customCar.minLoadingState = (transaction.targetStateOfCharge / 100) * customCar.maxCapacity; + // Handle if no state of charge is available + } else { + customCar.minLoadingState = (defaultTargetStateOfCharge / 100) * customCar.maxCapacity; + } + } + + private handleTimestampDeparture(optimizerCar: OptimizerCar, transaction: Transaction, currentType: CurrentType, defaultDepartureTime: string): void { + const currentTimeInSeconds = Utils.createDecimal(moment().diff(moment().startOf('hour'), 'seconds')).div(900).modulo(1).mul(900).toNumber(); + // Set departure time based on user input + if (!Utils.isNullOrUndefined(transaction.departureTime)) { + optimizerCar.timestampDeparture = moment(transaction.departureTime).diff(moment(), 'seconds') + 1 + currentTimeInSeconds; + } else if (currentType === CurrentType.DC) { + // Set static departure time for DC sessions + optimizerCar.timestampDeparture = moment(transaction.timestamp).add(1, 'hours').diff(moment(), 'seconds'); + if (optimizerCar.timestampDeparture < currentTimeInSeconds) { + optimizerCar.timestampDeparture = currentTimeInSeconds + 1; + } + } else { + // Calculate departure time + let timestampDeparture = moment(transaction.timestamp).add(8, 'hours'); + if (defaultDepartureTime) { + const defaultDepartureHour = Utils.convertToInt(defaultDepartureTime.slice(0, 2)); + const defaultDepartureMinute = Utils.convertToInt(defaultDepartureTime.slice(3, 5)); + timestampDeparture = moment().set('hour', defaultDepartureHour).set('minute', defaultDepartureMinute); + if (timestampDeparture < moment()) { + timestampDeparture.add(1, 'days'); + } + } + optimizerCar.timestampDeparture = moment(timestampDeparture).diff(moment(), 'seconds') + 1 + currentTimeInSeconds; + } + // Check if timestamp departure is in the past + if (optimizerCar.timestampDeparture <= currentTimeInSeconds) { + optimizerCar.timestampDeparture = 28800; + } + // Check if timestamp departure is too far in the future + if (optimizerCar.timestampDeparture >= 72000) { + optimizerCar.timestampDeparture = 72000; + } + } + private overrideCarWithRuntimeData(chargingStation: ChargingStation, transaction: Transaction, car: OptimizerCar, currentChargingProfiles: ChargingProfile[]) { // Check if meter value already received with phases used (only on AC stations) if (transaction.phasesUsed) { @@ -787,8 +867,7 @@ export default class SapSmartChargingIntegration extends SmartChargingIntegratio // Get OCPP Parameter for max periods const maxScheduleLength = parseInt(await ChargingStationStorage.getOcppParameterValue(this.tenant, chargingStationID, 'ChargingScheduleMaxPeriods')); // Start from now up to the third slot - for (let i = 0; i < (!isNaN(maxScheduleLength) ? maxScheduleLength : 20) && - i < car.currentPlan.length && (car.currentPlan[i] > 0 || chargingSchedule.chargingSchedulePeriod.length < 3); i++) { + for (let i = 0; i < ((!isNaN(maxScheduleLength) && maxScheduleLength < 16) ? maxScheduleLength : 16); i++) { chargingSchedule.chargingSchedulePeriod.push({ startPeriod: currentTimeSlotMins * 15 * 60, // Start period in secs (starts at 0 sec from startSchedule date/time) limit: this.calculateCarConsumption(currentChargingStation, connector, numberOfConnectedPhase, car.currentPlan[i]) diff --git a/src/server/rest/v1/service/SiteAreaService.ts b/src/server/rest/v1/service/SiteAreaService.ts index 170ded47c4..6a23761324 100644 --- a/src/server/rest/v1/service/SiteAreaService.ts +++ b/src/server/rest/v1/service/SiteAreaService.ts @@ -16,6 +16,7 @@ import Logging from '../../../../utils/Logging'; import LoggingHelper from '../../../../utils/LoggingHelper'; import OCPPUtils from '../../../ocpp/utils/OCPPUtils'; import { ServerAction } from '../../../../types/Server'; +import SettingStorage from '../../../../storage/mongodb/SettingStorage'; import { SiteAreaDataResult } from '../../../../types/DataResult'; import SiteAreaStorage from '../../../../storage/mongodb/SiteAreaStorage'; import SiteAreaValidatorRest from '../validator/SiteAreaValidatorRest'; @@ -244,6 +245,8 @@ export default class SiteAreaService { if (filteredRequest.WithAuth) { await AuthorizationService.addSiteAreasAuthorizations(req.tenant, req.user, siteAreas as SiteAreaDataResult, authorizations); } + // Handle smart charging session parameters + await SiteAreaService.addSmartChargingSessionParametersActive(req.tenant, req.user, siteAreas as SiteAreaDataResult); res.json(siteAreas); next(); } @@ -380,6 +383,7 @@ export default class SiteAreaService { } siteArea.numberOfPhases = filteredRequest.numberOfPhases; siteArea.smartCharging = filteredRequest.smartCharging; + siteArea.smartChargingSessionParameters = filteredRequest.smartChargingSessionParameters; siteArea.accessControl = filteredRequest.accessControl; if (Utils.isComponentActiveFromToken(req.user, TenantComponents.OCPI) && Utils.objectHasProperty(filteredRequest, 'tariffID')) { @@ -502,6 +506,16 @@ export default class SiteAreaService { } } + private static async addSmartChargingSessionParametersActive(tenant: Tenant, user: UserToken, siteAreas: SiteAreaDataResult) { + siteAreas.smartChargingSessionParametersActive = false; + if (Utils.isComponentActiveFromToken(user, TenantComponents.SMART_CHARGING)) { + const smartChargingSettings = await SettingStorage.getSmartChargingSettings(tenant); + if (smartChargingSettings.sapSmartCharging.prioritizationParametersActive) { + siteAreas.smartChargingSessionParametersActive = true; + } + } + } + private static async processSubSiteAreaActions(tenant: Tenant, rootSiteArea: SiteArea, siteArea: SiteArea, parentSiteArea: SiteArea, subSiteAreasActions: SubSiteAreaAction[], formerParentSiteAreaID?: string) { if (rootSiteArea && !Utils.isEmptyArray(subSiteAreasActions)) { diff --git a/src/server/rest/v1/service/UserService.ts b/src/server/rest/v1/service/UserService.ts index af3d29d1c6..b7c7ef7b7c 100644 --- a/src/server/rest/v1/service/UserService.ts +++ b/src/server/rest/v1/service/UserService.ts @@ -6,7 +6,6 @@ import { Car, CarType } from '../../../../types/Car'; import { DataResult, UserDataResult, UserSiteDataResult } from '../../../../types/DataResult'; import { HTTPAuthError, HTTPError } from '../../../../types/HTTPError'; import { NextFunction, Request, Response } from 'express'; -import { SmartChargingSessionParameters, StartTransactionErrorCode } from '../../../../types/Transaction'; import Tenant, { TenantComponents } from '../../../../types/Tenant'; import User, { ImportedUser, UserRequiredImportProperties, UserRole } from '../../../../types/User'; @@ -18,7 +17,6 @@ import BillingFactory from '../../../../integration/billing/BillingFactory'; import CSVError from 'csvtojson/v2/CSVError'; import CarStorage from '../../../../storage/mongodb/CarStorage'; import Constants from '../../../../utils/Constants'; -import { CurrentType } from '../../../../types/ChargingStation'; import EmspOCPIClient from '../../../../client/ocpi/EmspOCPIClient'; import { HttpUsersGetRequest } from '../../../../types/requests/HttpUserRequest'; import JSONStream from 'JSONStream'; @@ -32,7 +30,8 @@ import { OCPITokenWhitelist } from '../../../../types/ocpi/OCPIToken'; import OCPIUtils from '../../../ocpi/OCPIUtils'; import { Readable } from 'stream'; import { ServerAction } from '../../../../types/Server'; -import SettingStorage from '../../../../storage/mongodb/SettingStorage'; +import SmartChargingHelper from '../../../../integration/smart-charging/SmartChargingHelper'; +import { StartTransactionErrorCode } from '../../../../types/Transaction'; import { StatusCodes } from 'http-status-codes'; import Tag from '../../../../types/Tag'; import TagStorage from '../../../../storage/mongodb/TagStorage'; @@ -62,16 +61,29 @@ export default class UserService { // We retrieve Tag auth to get the projected fields here to fit with what's in auth definition const tagAuthorization = await AuthorizationService.checkAndGetTagAuthorizations(req.tenant, req.user, {}, Action.READ); let tag: Tag; - // Get the default Tag if (tagAuthorization.authorized) { - tag = await TagStorage.getDefaultUserTag(req.tenant, user.id, { - issuer: true - }, tagAuthorization.projectFields); + // Get the tag from the request TagID + if (filteredRequest.TagID) { + const tagFromID = await TagStorage.getTag(req.tenant, filteredRequest.TagID, { issuer: true }, tagAuthorization.projectFields); + if (!tagFromID || tagFromID.userID !== filteredRequest.UserID) { + throw new AppError({ + errorCode: StatusCodes.BAD_REQUEST, + message: 'This user has no tag with such TagID', + module: MODULE_NAME, + method: 'handleGetUserSessionContext', + action: action + }); + } else { + tag = tagFromID; + } + } if (!tag) { - // Get the first active Tag - tag = await TagStorage.getFirstActiveUserTag(req.tenant, user.id, { - issuer: true - }, tagAuthorization.projectFields); + // Get the default Tag + tag = await TagStorage.getDefaultUserTag(req.tenant, user.id, { issuer: true }, tagAuthorization.projectFields); + if (!tag) { + // Get the first active Tag + tag = await TagStorage.getFirstActiveUserTag(req.tenant, user.id, { issuer: true }, tagAuthorization.projectFields); + } } } // Handle Car @@ -79,11 +91,28 @@ export default class UserService { const carAuthorization = await AuthorizationService.checkAndGetCarAuthorizations(req.tenant, req.user, {}, Action.READ); let car: Car; if (Utils.isComponentActiveFromToken(req.user, TenantComponents.CAR) && carAuthorization.authorized) { - // Get the default Car - car = await CarStorage.getDefaultUserCar(req.tenant, filteredRequest.UserID, {}, carAuthorization.projectFields); + // Get the car from the request CarID + if (filteredRequest.CarID) { + const carFromID = await CarStorage.getCar(req.tenant, filteredRequest.CarID, {}, carAuthorization.projectFields); + if (!carFromID || carFromID.userID !== filteredRequest.UserID) { + throw new AppError({ + errorCode: StatusCodes.BAD_REQUEST, + message: 'This user has no car with such CarID', + module: MODULE_NAME, + method: 'handleGetUserSessionContext', + action: action + }); + } else { + car = carFromID; + } + } if (!car) { - // Get the first available car - car = await CarStorage.getFirstAvailableUserCar(req.tenant, filteredRequest.UserID, carAuthorization.projectFields); + // Get the default Car + car = await CarStorage.getDefaultUserCar(req.tenant, filteredRequest.UserID, {}, carAuthorization.projectFields); + if (!car) { + // Get the first available car + car = await CarStorage.getFirstAvailableUserCar(req.tenant, filteredRequest.UserID, carAuthorization.projectFields); + } } } let withBillingChecks = true ; @@ -99,28 +128,10 @@ export default class UserService { // Check for the billing prerequisites (such as the user's payment method) await UserService.checkBillingErrorCodes(action, req.tenant, req.user, user, errorCodes); } - let smartChargingSessionParameters: SmartChargingSessionParameters = null; - // Handle Smart Charging - if (chargingStation.siteArea?.smartCharging && !chargingStation.excludeFromSmartCharging - && chargingStation.capabilities.supportChargingProfiles && Utils.isComponentActiveFromToken(req.user, TenantComponents.SMART_CHARGING)) { - const smartChargingSettings = await SettingStorage.getSmartChargingSettings(req.tenant); - if (smartChargingSettings.sapSmartCharging.prioritizationParametersActive) { - // Default values are hard coded for now - smartChargingSessionParameters = { - departureTime: 18, - carStateOfCharge: 30, - targetStateOfCharge: 70, - }; - if (Utils.getChargingStationCurrentType(chargingStation, null, filteredRequest.ConnectorID) === CurrentType.DC) { - smartChargingSessionParameters.departureTime = null; - smartChargingSessionParameters.carStateOfCharge = null; - } else if (car.carConnectorData?.carConnectorID) { - smartChargingSessionParameters.carStateOfCharge = null; - } - } - } + // Get additional Smart Charging parameters such as the Departure Time + const parameters = await SmartChargingHelper.getSessionParameters(req.tenant, req.user, chargingStation, filteredRequest.ConnectorID, car); res.json({ - tag, car, errorCodes, smartChargingSessionParameters + tag, car, errorCodes, smartChargingSessionParameters: parameters }); next(); } diff --git a/src/storage/mongodb/SiteAreaStorage.ts b/src/storage/mongodb/SiteAreaStorage.ts index 930e156ec6..8dbfbee840 100644 --- a/src/storage/mongodb/SiteAreaStorage.ts +++ b/src/storage/mongodb/SiteAreaStorage.ts @@ -189,6 +189,15 @@ export default class SiteAreaStorage { (coordinate) => Utils.convertToFloat(coordinate)) : [], }; } + if (siteAreaToSave.smartChargingSessionParameters) { + siteAreaMDB.smartChargingSessionParameters = { + departureTime: siteAreaToSave.smartChargingSessionParameters.departureTime ?? null, + carStateOfCharge: siteAreaToSave.smartChargingSessionParameters.carStateOfCharge ? + Utils.convertToInt(siteAreaToSave.smartChargingSessionParameters.carStateOfCharge) : null, + targetStateOfCharge: siteAreaToSave.smartChargingSessionParameters.targetStateOfCharge ? + Utils.convertToInt(siteAreaToSave.smartChargingSessionParameters.targetStateOfCharge) : null, + }; + } // Add Last Changed/Created props DatabaseUtils.addLastChangedCreatedProps(siteAreaMDB, siteAreaToSave); // Modify diff --git a/src/types/DataResult.ts b/src/types/DataResult.ts index 7f3c470324..fbf56e0b39 100644 --- a/src/types/DataResult.ts +++ b/src/types/DataResult.ts @@ -67,6 +67,7 @@ export interface LogDataResult extends DataResult { export interface SiteAreaDataResult extends DataResult { canCreate: boolean; + smartChargingSessionParametersActive: boolean; } export interface CarDataResult extends DataResult { diff --git a/src/types/SiteArea.ts b/src/types/SiteArea.ts index 7318b01df5..ecba9fa194 100644 --- a/src/types/SiteArea.ts +++ b/src/types/SiteArea.ts @@ -9,6 +9,7 @@ import { OCPILocation } from './ocpi/OCPILocation'; import { OpeningTimes } from './OpeningTimes'; import Site from '../types/Site'; import { SiteAreaAuthorizationActions } from './Authorization'; +import { SmartChargingSessionParameters } from './Transaction'; export enum SiteAreaValueTypes { ASSET_CONSUMPTIONS = 'AssetConsumptions', @@ -37,6 +38,7 @@ export default interface SiteArea extends CreatedUpdatedProps, SiteAreaAuthoriza siteID: string; site: Site; smartCharging: boolean; + smartChargingSessionParameters?: SmartChargingSessionParameters; accessControl: boolean; chargingStations: ChargingStation[]; assets: Asset[]; diff --git a/src/types/Transaction.ts b/src/types/Transaction.ts index 1c578b0bca..121c956e3b 100644 --- a/src/types/Transaction.ts +++ b/src/types/Transaction.ts @@ -209,7 +209,13 @@ export interface CollectedFundReport { } export interface SmartChargingSessionParameters { - departureTime: number, + departureTime: string, carStateOfCharge: number, targetStateOfCharge: number, } + +export interface SmartChargingRuntimeSessionParameters { + departureTime?: Date, // Date of the departure time - taking into account the timezone of the charging station + carStateOfCharge?: number, + targetStateOfCharge?: number, +} diff --git a/src/types/requests/HttpUserRequest.ts b/src/types/requests/HttpUserRequest.ts index 1390939c49..db05929f2c 100644 --- a/src/types/requests/HttpUserRequest.ts +++ b/src/types/requests/HttpUserRequest.ts @@ -121,4 +121,6 @@ export interface HttpUserSessionContextGetRequest { UserID: string; ChargingStationID: string; ConnectorID: number; + CarID: string; + TagID: string; } diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts index bbdf28523b..6c3a86c352 100644 --- a/src/utils/Constants.ts +++ b/src/utils/Constants.ts @@ -472,7 +472,8 @@ export default class Constants { /* App Info for STRIPE */ public static readonly STRIPE_APP_NAME = 'Open e-Mobility'; public static readonly STRIPE_PARTNER_ID = 'TECH-000685'; - public static readonly STRIPE_API_VERSION = '2020-08-27'; + // public static readonly STRIPE_API_VERSION = '2020-08-27'; + public static readonly STRIPE_API_VERSION = '2022-11-15'; public static readonly WEB_SOCKET_OCPP_NEW_CONNECTIONS_COUNT = 'web_socket_ocpp_new_connections_count'; public static readonly WEB_SOCKET_OCPP_CLOSED_CONNECTIONS_COUNT = 'web_socket_ocpp_closed_connections_count'; diff --git a/test/api/SmartChargingTest.ts b/test/api/SmartChargingTest.ts index eb9321851f..a8c42f39db 100644 --- a/test/api/SmartChargingTest.ts +++ b/test/api/SmartChargingTest.ts @@ -22,10 +22,12 @@ import SiteContext from './context/SiteContext'; import SmartChargingFactory from '../../src/integration/smart-charging/SmartChargingFactory'; import SmartChargingIntegration from '../../src/integration/smart-charging/SmartChargingIntegration'; import TenantContext from './context/TenantContext'; +import TransactionStorage from '../../src/storage/mongodb/TransactionStorage'; import Utils from '../../src/utils/Utils'; import chaiSubset from 'chai-subset'; import config from '../config'; import global from '../../src/types/GlobalType'; +import moment from 'moment'; import responseHelper from '../helpers/responseHelper'; chai.use(chaiSubset); @@ -34,6 +36,7 @@ chai.use(responseHelper); let smartChargingIntegration: SmartChargingIntegration; let smartChargingIntegrationWithDifferentBufferValues: SmartChargingIntegration; let smartChargingIntegrationWithoutStickyLimit: SmartChargingIntegration; +let smartChargingIntegrationWithoutPriorities: SmartChargingIntegration; class TestData { public tenantContext: TenantContext; @@ -66,9 +69,13 @@ class TestData { sapSmartChargingSettings.stickyLimitation = false; await TestData.saveSmartChargingSettings(testData, sapSmartChargingSettings); smartChargingIntegrationWithoutStickyLimit = await SmartChargingFactory.getSmartChargingImpl(testData.tenantContext.getTenant()); + sapSmartChargingSettings.prioritizationParametersActive = false; + await TestData.saveSmartChargingSettings(testData, sapSmartChargingSettings); + smartChargingIntegrationWithoutPriorities = await SmartChargingFactory.getSmartChargingImpl(testData.tenantContext.getTenant()); expect(smartChargingIntegration).to.not.be.null; expect(smartChargingIntegrationWithDifferentBufferValues).to.not.be.null; expect(smartChargingIntegrationWithoutStickyLimit).to.not.be.null; + expect(smartChargingIntegrationWithoutPriorities).to.not.be.null; } public static getSmartChargingSettings(): SapSmartChargingSetting { @@ -78,7 +85,8 @@ class TestData { password: config.get('smartCharging.password'), stickyLimitation: config.get('smartCharging.stickyLimitation'), limitBufferAC: config.get('smartCharging.limitBufferAC'), - limitBufferDC: config.get('smartCharging.limitBufferDC') + limitBufferDC: config.get('smartCharging.limitBufferDC'), + prioritizationParametersActive: config.get('smartCharging.prioritizationParametersActive'), } as SapSmartChargingSetting; } @@ -393,7 +401,9 @@ describeif(testData.chargingSettingProvided)('Smart Charging Service', () => { await testData.userService.siteAreaApi.update(testData.siteAreaContext.getSiteArea()); const chargingProfiles = await smartChargingIntegration.buildChargingProfiles(testData.siteAreaContext.getSiteArea()); TestData.validateChargingProfile(chargingProfiles[0], transaction); - expect(chargingProfiles[0].profile.chargingSchedule.chargingSchedulePeriod).containSubset([ + expect(chargingProfiles[0].profile.chargingSchedule.chargingSchedulePeriod).containSubset(limit96); + TestData.validateChargingProfile(chargingProfiles[1], transaction1); + expect(chargingProfiles[1].profile.chargingSchedule.chargingSchedulePeriod).containSubset([ { 'startPeriod': 0, 'limit': 43.1 @@ -407,8 +417,6 @@ describeif(testData.chargingSettingProvided)('Smart Charging Service', () => { 'limit': 43.1 } ]); - TestData.validateChargingProfile(chargingProfiles[1], transaction1); - expect(chargingProfiles[1].profile.chargingSchedule.chargingSchedulePeriod).containSubset(limit96); }); it( @@ -421,7 +429,7 @@ describeif(testData.chargingSettingProvided)('Smart Charging Service', () => { await testData.chargingStationContext1.setConnectorStatus(chargingStationConnector1Charging); const chargingProfiles = await smartChargingIntegration.buildChargingProfiles(testData.siteAreaContext.getSiteArea()); TestData.validateChargingProfile(chargingProfiles[0], transaction); - expect(chargingProfiles[0].profile.chargingSchedule.chargingSchedulePeriod).containSubset(limit0); + expect(chargingProfiles[0].profile.chargingSchedule.chargingSchedulePeriod).containSubset(limit96); TestData.validateChargingProfile(chargingProfiles[1], transaction1); expect(chargingProfiles[1].profile.chargingSchedule.chargingSchedulePeriod).containSubset([ { @@ -438,7 +446,7 @@ describeif(testData.chargingSettingProvided)('Smart Charging Service', () => { } ]); TestData.validateChargingProfile(chargingProfiles[2], transaction2); - expect(chargingProfiles[2].profile.chargingSchedule.chargingSchedulePeriod).containSubset(limit32); + expect(chargingProfiles[2].profile.chargingSchedule.chargingSchedulePeriod).containSubset(limit0); } ); @@ -806,7 +814,9 @@ describeif(testData.chargingSettingProvided)('Smart Charging Service', () => { await testData.userService.siteAreaApi.update(testData.siteAreaContext.getSiteArea()); const chargingProfiles = await smartChargingIntegration.buildChargingProfiles(testData.siteAreaContext.getSiteArea()); TestData.validateChargingProfile(chargingProfiles[0], transaction); - expect(chargingProfiles[0].profile.chargingSchedule.chargingSchedulePeriod).containSubset([ + expect(chargingProfiles[0].profile.chargingSchedule.chargingSchedulePeriod).containSubset(limit32); + TestData.validateChargingProfile(chargingProfiles[1], transaction1); + expect(chargingProfiles[1].profile.chargingSchedule.chargingSchedulePeriod).containSubset([ { 'startPeriod': 0, 'limit': Utils.roundTo(10000 / 230 - 32, 1) @@ -820,8 +830,6 @@ describeif(testData.chargingSettingProvided)('Smart Charging Service', () => { 'limit': Utils.roundTo(10000 / 230 - 32, 1) } ]); - TestData.validateChargingProfile(chargingProfiles[1], transaction1); - expect(chargingProfiles[1].profile.chargingSchedule.chargingSchedulePeriod).containSubset(limit32); }); it( @@ -906,7 +914,9 @@ describeif(testData.chargingSettingProvided)('Smart Charging Service', () => { async () => { const chargingProfiles = await smartChargingIntegrationWithoutStickyLimit.buildChargingProfiles(testData.siteAreaContext.getSiteArea()); TestData.validateChargingProfile(chargingProfiles[0], transaction); - expect(chargingProfiles[0].profile.chargingSchedule.chargingSchedulePeriod).containSubset([ + expect(chargingProfiles[0].profile.chargingSchedule.chargingSchedulePeriod).containSubset(limit32); + TestData.validateChargingProfile(chargingProfiles[1], transaction1); + expect(chargingProfiles[1].profile.chargingSchedule.chargingSchedulePeriod).containSubset([ { 'startPeriod': 0, 'limit': Utils.roundTo(10000 / 230 - 32, 1) @@ -920,8 +930,6 @@ describeif(testData.chargingSettingProvided)('Smart Charging Service', () => { 'limit': Utils.roundTo(10000 / 230 - 32, 1) } ]); - TestData.validateChargingProfile(chargingProfiles[1], transaction1); - expect(chargingProfiles[1].profile.chargingSchedule.chargingSchedulePeriod).containSubset(limit32); } ); @@ -999,12 +1007,10 @@ describeif(testData.chargingSettingProvided)('Smart Charging Service', () => { { 'startPeriod': 900, 'limit': 654 - }, - { - 'startPeriod': 1800, - 'limit': 654 } ]); + expect(chargingProfiles[0].profile.chargingSchedule.chargingSchedulePeriod[2].limit).to.be.greaterThan(250); + expect(chargingProfiles[0].profile.chargingSchedule.chargingSchedulePeriod[2].limit).to.be.lessThanOrEqual(654); }); it( @@ -1089,31 +1095,31 @@ describeif(testData.chargingSettingProvided)('Smart Charging Service', () => { expect(chargingProfiles[0].profile.chargingSchedule.chargingSchedulePeriod).containSubset([ { 'startPeriod': 0, - 'limit': 86 + 'limit': 327 }, { 'startPeriod': 900, - 'limit': 86 + 'limit': 327 }, { 'startPeriod': 1800, - 'limit': 86 - }, + 'limit': 327 + } ]); TestData.validateChargingProfile(chargingProfiles[1], transaction1); expect(chargingProfiles[1].profile.chargingSchedule.chargingSchedulePeriod).containSubset([ { 'startPeriod': 0, - 'limit': 327 + 'limit': 86 }, { 'startPeriod': 900, - 'limit': 327 + 'limit': 86 }, { 'startPeriod': 1800, - 'limit': 327 - } + 'limit': 86 + }, ]); }); @@ -1203,31 +1209,31 @@ describeif(testData.chargingSettingProvided)('Smart Charging Service', () => { expect(chargingProfiles[0].profile.chargingSchedule.chargingSchedulePeriod).containSubset([ { 'startPeriod': 0, - 'limit': 86 + 'limit': 327 }, { 'startPeriod': 900, - 'limit': 86 + 'limit': 327 }, { 'startPeriod': 1800, - 'limit': 86 - }, + 'limit': 327 + } ]); TestData.validateChargingProfile(chargingProfiles[1], transaction1); expect(chargingProfiles[1].profile.chargingSchedule.chargingSchedulePeriod).containSubset([ { 'startPeriod': 0, - 'limit': 327 + 'limit': 86 }, { 'startPeriod': 900, - 'limit': 327 + 'limit': 86 }, { 'startPeriod': 1800, - 'limit': 327 - } + 'limit': 86 + }, ]); } ); @@ -1321,7 +1327,7 @@ describeif(testData.chargingSettingProvided)('Smart Charging Service', () => { await testData.chargingStationContext.setConnectorStatus(chargingStationConnector1Charging); await testData.chargingStationContext1.setConnectorStatus(chargingStationConnector1Charging); await testData.chargingStationContext2.setConnectorStatus(chargingStationConnector1Charging); - const chargingProfiles = await smartChargingIntegration.buildChargingProfiles(testData.siteAreaContext.getSiteArea()); + const chargingProfiles = await smartChargingIntegrationWithoutPriorities.buildChargingProfiles(testData.siteAreaContext.getSiteArea()); expect(chargingProfiles.length).to.be.eq(3); TestData.validateChargingProfile(chargingProfiles[0], transaction); expect(chargingProfiles[0].profile.chargingSchedule.chargingSchedulePeriod).containSubset([ @@ -1347,7 +1353,7 @@ describeif(testData.chargingSettingProvided)('Smart Charging Service', () => { it('Check for three cars charging on different site areas if not enough power is available on root site area', async () => { testData.siteAreaContext.getSiteArea().maximumPower = 100000; await testData.userService.siteAreaApi.update(testData.siteAreaContext.getSiteArea()); - const chargingProfiles = await smartChargingIntegration.buildChargingProfiles(testData.siteAreaContext.getSiteArea()); + const chargingProfiles = await smartChargingIntegrationWithoutPriorities.buildChargingProfiles(testData.siteAreaContext.getSiteArea()); expect(chargingProfiles.length).to.be.eq(3); TestData.validateChargingProfile(chargingProfiles[0], transaction); expect(chargingProfiles[0].profile.chargingSchedule.chargingSchedulePeriod).containSubset([ @@ -1373,7 +1379,7 @@ describeif(testData.chargingSettingProvided)('Smart Charging Service', () => { it('Check for three cars charging on different site areas if not enough power is available on sub site area', async () => { testData.siteAreaContext1.getSiteArea().maximumPower = 30000; await testData.userService.siteAreaApi.update(testData.siteAreaContext1.getSiteArea()); - const chargingProfiles = await smartChargingIntegration.buildChargingProfiles(testData.siteAreaContext.getSiteArea()); + const chargingProfiles = await smartChargingIntegrationWithoutPriorities.buildChargingProfiles(testData.siteAreaContext.getSiteArea()); expect(chargingProfiles.length).to.be.eq(3); TestData.validateChargingProfile(chargingProfiles[0], transaction); expect(chargingProfiles[0].profile.chargingSchedule.chargingSchedulePeriod).containSubset([ @@ -1665,4 +1671,161 @@ describeif(testData.chargingSettingProvided)('Smart Charging Service', () => { ]); }); }); + + describe('Test for Priorities', () => { + beforeAll(async () => { + testData.siteContext = testData.tenantContext.getSiteContext(ContextDefinition.SITE_CONTEXTS.SITE_BASIC); + testData.siteAreaContext = testData.siteContext.getSiteAreaContext(ContextDefinition.SITE_AREA_CONTEXTS.WITH_SMART_CHARGING_THREE_PHASED); + testData.siteAreaContext1 = testData.siteContext.getSiteAreaContext(ContextDefinition.SITE_AREA_CONTEXTS.WITH_SMART_CHARGING_DC); + testData.chargingStationContext = testData.siteAreaContext.getChargingStationContext(ContextDefinition.CHARGING_STATION_CONTEXTS.ASSIGNED_OCPP16); + }); + + afterAll(async () => { + await testData.chargingStationContext.stopTransaction(transaction.id, testData.userContext.tags[0].id, 180, new Date()); + await testData.chargingStationContext.stopTransaction(transaction1.id, testData.userContext.tags[0].id, 180, new Date()); + + + chargingStationConnector1Available.timestamp = new Date().toISOString(); + chargingStationConnector2Available.timestamp = new Date().toISOString(); + await testData.chargingStationContext.setConnectorStatus(chargingStationConnector1Available); + await testData.chargingStationContext.setConnectorStatus(chargingStationConnector2Available); + + // Reset modifications on siteArea + testData.siteAreaContext.getSiteArea().maximumPower = 200000; + await testData.userService.siteAreaApi.update(testData.siteAreaContext.getSiteArea()); + testData.siteAreaContext1.getSiteArea().maximumPower = 200000; + await testData.userService.siteAreaApi.update(testData.siteAreaContext1.getSiteArea()); + }); + + + it( + 'Check if session with earlier departure time is prioritized', + async () => { + testData.siteAreaContext.getSiteArea().maximumPower = 22080; + testData.siteAreaContext.getSiteArea().smartCharging = true; + await testData.userService.siteAreaApi.update(testData.siteAreaContext.getSiteArea()); + let transactionStartResponse = await testData.chargingStationContext.startTransaction(1, testData.userContext.tags[0].id, 180, new Date); + let transactionResponse = await testData.centralUserService.transactionApi.readById(transactionStartResponse.transactionId); + transaction = transactionResponse.data; + + transactionStartResponse = await testData.chargingStationContext.startTransaction(2, testData.userContext.tags[0].id, 180, new Date); + transactionResponse = await testData.centralUserService.transactionApi.readById(transactionStartResponse.transactionId); + transaction1 = transactionResponse.data; + chargingStationConnector1Charging.timestamp = new Date().toISOString(); + chargingStationConnector2Charging.timestamp = new Date().toISOString(); + + transaction.departureTime = moment(transaction.timestamp).add(8, 'hours').toDate(); + await TransactionStorage.saveTransaction(testData.tenantContext.getTenant(), transaction); + transaction1.departureTime = moment(transaction.timestamp).add(4, 'hours').toDate(); + await TransactionStorage.saveTransaction(testData.tenantContext.getTenant(), transaction1); + + await testData.chargingStationContext.setConnectorStatus(chargingStationConnector1Charging); + await testData.chargingStationContext.setConnectorStatus(chargingStationConnector2Charging); + + let chargingProfiles = await smartChargingIntegration.buildChargingProfiles(testData.siteAreaContext.getSiteArea()); + TestData.validateChargingProfile(chargingProfiles[0], transaction); + expect(chargingProfiles[0].profile.chargingSchedule.chargingSchedulePeriod).containSubset(limit0); + TestData.validateChargingProfile(chargingProfiles[1], transaction1); + expect(chargingProfiles[1].profile.chargingSchedule.chargingSchedulePeriod).containSubset(limit96); + + // Switch the priorities + transaction.departureTime = moment(transaction.timestamp).add(4, 'hours').toDate(); + await TransactionStorage.saveTransaction(testData.tenantContext.getTenant(), transaction); + transaction1.departureTime = moment(transaction.timestamp).add(8, 'hours').toDate(); + await TransactionStorage.saveTransaction(testData.tenantContext.getTenant(), transaction1); + + chargingProfiles = await smartChargingIntegration.buildChargingProfiles(testData.siteAreaContext.getSiteArea()); + TestData.validateChargingProfile(chargingProfiles[0], transaction); + expect(chargingProfiles[0].profile.chargingSchedule.chargingSchedulePeriod).containSubset(limit96); + TestData.validateChargingProfile(chargingProfiles[1], transaction1); + expect(chargingProfiles[1].profile.chargingSchedule.chargingSchedulePeriod).containSubset(limit0); + } + ); + + it( + 'Check if session with less SOC is prioritized', + async () => { + testData.siteAreaContext.getSiteArea().maximumPower = 22080; + testData.siteAreaContext.getSiteArea().smartCharging = true; + await testData.userService.siteAreaApi.update(testData.siteAreaContext.getSiteArea()); + let transactionStartResponse = await testData.chargingStationContext.startTransaction(1, testData.userContext.tags[0].id, 180, new Date); + let transactionResponse = await testData.centralUserService.transactionApi.readById(transactionStartResponse.transactionId); + transaction = transactionResponse.data; + + transactionStartResponse = await testData.chargingStationContext.startTransaction(2, testData.userContext.tags[0].id, 180, new Date); + transactionResponse = await testData.centralUserService.transactionApi.readById(transactionStartResponse.transactionId); + transaction1 = transactionResponse.data; + chargingStationConnector1Charging.timestamp = new Date().toISOString(); + chargingStationConnector2Charging.timestamp = new Date().toISOString(); + + transaction.carStateOfCharge = 20; + await TransactionStorage.saveTransaction(testData.tenantContext.getTenant(), transaction); + transaction1.stateOfCharge = 40; + await TransactionStorage.saveTransaction(testData.tenantContext.getTenant(), transaction1); + + await testData.chargingStationContext.setConnectorStatus(chargingStationConnector1Charging); + await testData.chargingStationContext.setConnectorStatus(chargingStationConnector2Charging); + + let chargingProfiles = await smartChargingIntegration.buildChargingProfiles(testData.siteAreaContext.getSiteArea()); + TestData.validateChargingProfile(chargingProfiles[0], transaction); + expect(chargingProfiles[0].profile.chargingSchedule.chargingSchedulePeriod).containSubset(limit96); + TestData.validateChargingProfile(chargingProfiles[1], transaction1); + expect(chargingProfiles[1].profile.chargingSchedule.chargingSchedulePeriod).containSubset(limit0); + + transaction.carStateOfCharge = 40; + await TransactionStorage.saveTransaction(testData.tenantContext.getTenant(), transaction); + transaction1.stateOfCharge = 20; + await TransactionStorage.saveTransaction(testData.tenantContext.getTenant(), transaction1); + + chargingProfiles = await smartChargingIntegration.buildChargingProfiles(testData.siteAreaContext.getSiteArea()); + TestData.validateChargingProfile(chargingProfiles[0], transaction); + expect(chargingProfiles[0].profile.chargingSchedule.chargingSchedulePeriod).containSubset(limit0); + TestData.validateChargingProfile(chargingProfiles[1], transaction1); + expect(chargingProfiles[1].profile.chargingSchedule.chargingSchedulePeriod).containSubset(limit96); + } + ); + + it( + 'Check if session with higher target SOC is prioritized', + async () => { + testData.siteAreaContext.getSiteArea().maximumPower = 22080; + testData.siteAreaContext.getSiteArea().smartCharging = true; + await testData.userService.siteAreaApi.update(testData.siteAreaContext.getSiteArea()); + let transactionStartResponse = await testData.chargingStationContext.startTransaction(1, testData.userContext.tags[0].id, 180, new Date); + let transactionResponse = await testData.centralUserService.transactionApi.readById(transactionStartResponse.transactionId); + transaction = transactionResponse.data; + + transactionStartResponse = await testData.chargingStationContext.startTransaction(2, testData.userContext.tags[0].id, 180, new Date); + transactionResponse = await testData.centralUserService.transactionApi.readById(transactionStartResponse.transactionId); + transaction1 = transactionResponse.data; + chargingStationConnector1Charging.timestamp = new Date().toISOString(); + chargingStationConnector2Charging.timestamp = new Date().toISOString(); + + transaction.targetStateOfCharge = 40; + await TransactionStorage.saveTransaction(testData.tenantContext.getTenant(), transaction); + transaction1.targetStateOfCharge = 20; + await TransactionStorage.saveTransaction(testData.tenantContext.getTenant(), transaction1); + + await testData.chargingStationContext.setConnectorStatus(chargingStationConnector1Charging); + await testData.chargingStationContext.setConnectorStatus(chargingStationConnector2Charging); + + let chargingProfiles = await smartChargingIntegration.buildChargingProfiles(testData.siteAreaContext.getSiteArea()); + TestData.validateChargingProfile(chargingProfiles[0], transaction); + expect(chargingProfiles[0].profile.chargingSchedule.chargingSchedulePeriod).containSubset(limit96); + TestData.validateChargingProfile(chargingProfiles[1], transaction1); + expect(chargingProfiles[1].profile.chargingSchedule.chargingSchedulePeriod).containSubset(limit0); + + transaction.targetStateOfCharge = 20; + await TransactionStorage.saveTransaction(testData.tenantContext.getTenant(), transaction); + transaction1.targetStateOfCharge = 40; + await TransactionStorage.saveTransaction(testData.tenantContext.getTenant(), transaction1); + + chargingProfiles = await smartChargingIntegration.buildChargingProfiles(testData.siteAreaContext.getSiteArea()); + TestData.validateChargingProfile(chargingProfiles[0], transaction); + expect(chargingProfiles[0].profile.chargingSchedule.chargingSchedulePeriod).containSubset(limit0); + TestData.validateChargingProfile(chargingProfiles[1], transaction1); + expect(chargingProfiles[1].profile.chargingSchedule.chargingSchedulePeriod).containSubset(limit96); + } + ); + }); }); diff --git a/test/api/UserTest.ts b/test/api/UserTest.ts index f29c575cd4..3f6b99d983 100644 --- a/test/api/UserTest.ts +++ b/test/api/UserTest.ts @@ -15,6 +15,11 @@ import TenantContext from './context/TenantContext'; import TestUtils from './TestUtils'; import chaiSubset from 'chai-subset'; import responseHelper from '../helpers/responseHelper'; +import global from '../../src/types/GlobalType'; +import MongoDBStorage from '../../src/storage/mongodb/MongoDBStorage'; +import config from '../config' +import CarStorage from '../../src/storage/mongodb/CarStorage'; +import TagStorage from '../../src/storage/mongodb/TagStorage'; chai.use(chaiSubset); chai.use(responseHelper); @@ -137,7 +142,7 @@ describe('User', () => { await testData.userService.checkEntityInListWithParams( testData.userService.siteApi, testData.siteContext.getSite(), - { 'UserID': testData.newUser.id } + {'UserID': testData.newUser.id} ); }); @@ -163,7 +168,7 @@ describe('User', () => { // Update await testData.userService.updateEntity( testData.userService.userApi, - { ...testData.newUser, password: testData.newUser.password } + {...testData.newUser, password: testData.newUser.password} ); }); @@ -173,7 +178,7 @@ describe('User', () => { // Update await testData.userService.updateEntity( testData.userService.userApi, - { id: testData.newUser.id, role: UserRole.ADMIN } + {id: testData.newUser.id, role: UserRole.ADMIN} ); testData.newUser = (await testData.userService.getEntityById( @@ -186,7 +191,7 @@ describe('User', () => { it('Should be able to export users list', async () => { const response = await testData.userService.userApi.exportUsers({}); - const users = await testData.userService.userApi.readAll({}, { limit: 1000, skip: 0 }); + const users = await testData.userService.userApi.readAll({}, {limit: 1000, skip: 0}); const responseFileArray = TestUtils.convertExportFileToObjectArray(response.data); expect(response.status).eq(StatusCodes.OK); expect(response.data).not.null; @@ -220,13 +225,16 @@ describe('User', () => { it('Should get the user default car tag', async () => { // Create a tag - testData.newTag = Factory.tag.build({ userID: testData.newUser.id }); + testData.newTag = Factory.tag.build({userID: testData.newUser.id}); let response = await testData.userService.tagApi.createTag(testData.newTag); expect(response.status).to.equal(StatusCodes.CREATED); testData.createdTags.push(testData.newTag); // Retrieve it response = await testData.userService.userApi.getUserSessionContext({ - userID: testData.newUser.id, chargingStationID: testData.chargingStationContext.getChargingStation().id, connectorID: 1 }); + userID: testData.newUser.id, + chargingStationID: testData.chargingStationContext.getChargingStation().id, + connectorID: 1 + }); expect(response.status).to.be.eq(StatusCodes.OK); expect(response.data.tag.visualID).to.be.eq(testData.newTag.visualID); expect(response.data.car).to.be.undefined; @@ -261,20 +269,29 @@ describe('User', () => { it('Should be able to set/unset the technical flag', async () => { // Check the technical flag can be set const response = await testData.userService.userApi.exportUsers({}); - let users = await testData.userService.userApi.readAll({}, { limit: Constants.DB_RECORD_COUNT_MAX_PAGE_LIMIT, skip: 0 }); + let users = await testData.userService.userApi.readAll({}, { + limit: Constants.DB_RECORD_COUNT_MAX_PAGE_LIMIT, + skip: 0 + }); expect(response.status).eq(StatusCodes.OK); expect(response.data).not.null; expect(users.data.result.length).to.be.greaterThan(1); const user1 = users.data.result[0]; user1.technical = true; await testData.userService.userApi.update(user1); - users = await testData.userService.userApi.readAll({}, { limit: Constants.DB_RECORD_COUNT_MAX_PAGE_LIMIT, skip: 0 }); + users = await testData.userService.userApi.readAll({}, { + limit: Constants.DB_RECORD_COUNT_MAX_PAGE_LIMIT, + skip: 0 + }); const user2 = users.data.result[0]; expect(user2.technical).eq(true); // Unset technical flag. user2.technical = false; await testData.userService.userApi.update(user2); - users = await testData.userService.userApi.readAll({}, { limit: Constants.DB_RECORD_COUNT_MAX_PAGE_LIMIT, skip: 0 }); + users = await testData.userService.userApi.readAll({}, { + limit: Constants.DB_RECORD_COUNT_MAX_PAGE_LIMIT, + skip: 0 + }); expect(user2.technical).eq(false); }); }); @@ -283,7 +300,7 @@ describe('User', () => { it('Should not find an active user in error', async () => { const user = await testData.userService.createEntity( testData.userService.userApi, - Factory.user.build({ status: 'A' }) + Factory.user.build({status: 'A'}) ); testData.createdUsers.push(user); const response = await testData.userService.userApi.readAllInError({}, { @@ -301,7 +318,7 @@ describe('User', () => { it('Should find a pending user', async () => { const user = await testData.userService.createEntity( testData.userService.userApi, - Factory.user.build({ status: 'P' }) + Factory.user.build({status: 'P'}) ); testData.createdUsers.push(user); const response = await testData.userService.userApi.readAllInError({}, { @@ -321,7 +338,7 @@ describe('User', () => { it('Should find a blocked user', async () => { const user = await testData.userService.createEntity( testData.userService.userApi, - Factory.user.build({ status: 'B' }) + Factory.user.build({status: 'B'}) ); testData.createdUsers.push(user); const response = await testData.userService.userApi.readAllInError({}, { @@ -341,7 +358,7 @@ describe('User', () => { it('Should find a locked user', async () => { const user = await testData.userService.createEntity( testData.userService.userApi, - Factory.user.build({ status: 'L' }) + Factory.user.build({status: 'L'}) ); testData.createdUsers.push(user); const response = await testData.userService.userApi.readAllInError({}, { @@ -361,7 +378,7 @@ describe('User', () => { it('Should find an inactive user', async () => { const user = await testData.userService.createEntity( testData.userService.userApi, - Factory.user.build({ status: 'I' }) + Factory.user.build({status: 'I'}) ); testData.createdUsers.push(user); const response = await testData.userService.userApi.readAllInError({}, { @@ -445,7 +462,7 @@ describe('User', () => { // Let's delete the user await testData.userService.deleteEntity( testData.userService.userApi, - { id: testData.createdUsers[0].id } + {id: testData.createdUsers[0].id} ); testData.createdUsers.shift(); }); @@ -454,4 +471,204 @@ describe('User', () => { }); }); + describe('Car component is active (utcar)', () => { + beforeAll(async () => { + testData.tenantContext = await ContextProvider.defaultInstance.getTenantContext(ContextDefinition.TENANT_CONTEXTS.TENANT_CAR); + testData.centralUserContext = testData.tenantContext.getUserContext(ContextDefinition.USER_CONTEXTS.DEFAULT_ADMIN); + testData.siteContext = testData.tenantContext.getSiteContext(ContextDefinition.SITE_CONTEXTS.SITE_WITH_AUTO_USER_ASSIGNMENT); + testData.centralUserService = new CentralServerService( + testData.tenantContext.getTenant().subdomain, + testData.centralUserContext + ); + testData.siteAreaContext = testData.siteContext.getSiteAreaContext(ContextDefinition.SITE_AREA_CONTEXTS.WITH_ACL); + testData.chargingStationContext = testData.siteAreaContext.getChargingStationContext(ContextDefinition.CHARGING_STATION_CONTEXTS.ASSIGNED_OCPP16); + }); + + describe('Get user session context', () => { + let carCatalogID; + beforeAll(async () => { + carCatalogID = (await testData.centralUserService.carApi.readCarCatalogs({}, Constants.DB_PARAMS_SINGLE_RECORD)).data.result[0].id; + testData.userContext = testData.tenantContext.getUserContext(ContextDefinition.USER_CONTEXTS.DEFAULT_ADMIN); + assert(testData.userContext, 'User context cannot be null'); + if (testData.userContext === testData.centralUserContext) { + // Reuse the central user service (to avoid double login) + testData.userService = testData.centralUserService; + } else { + testData.userService = new CentralServerService( + testData.tenantContext.getTenant().subdomain, + testData.userContext + ); + } + assert(!!testData.userService, 'User service cannot be null'); + }); + + describe('Given a user with no tag and no car', () => { + const user = Factory.user.build(); + + beforeAll(async () => { + const createUserResponse = await testData.userService.userApi.create(user); + user.id = createUserResponse.data.id; + }); + + test('When getting his session context for (any connector), (any charging station)', async () => { + const response = await testData.userService.userApi.getUserSessionContext({ + userID: user.id, + chargingStationID: testData.chargingStationContext.getChargingStation().id, + connectorID: 1 + }); + expect(response.status).to.be.eq(StatusCodes.OK); + assert(response.data.car == null); + assert(response.data.tag == null); + }); + + test('When getting his session context for (any connector), (any charging station) and (non null carID)', async () => { + const randomCarID = (await testData.userService.carApi.create(Factory.car.build({carCatalogID}))).data.id; + const response = await testData.userService.userApi.getUserSessionContext({ + userID: user.id, + chargingStationID: testData.chargingStationContext.getChargingStation().id, + connectorID: 1, + carID: randomCarID + }) + expect(response.status).to.be.eq(StatusCodes.BAD_REQUEST); + }); + + test('When getting his session context for (any connector), (any charging station) and (non null tagID)', async () => { + const randomTagID = (await testData.userService.tagApi.createTag(Factory.tag.build())).data.id; + const response = await testData.userService.userApi.getUserSessionContext({ + userID: user.id, + chargingStationID: testData.chargingStationContext.getChargingStation().id, + connectorID: 1, + tagID: randomTagID + }); + expect(response.status).to.be.eq(StatusCodes.BAD_REQUEST); + }); + }); + + describe('Given a user with a tag and a car but no defaults', () => { + const user = Factory.user.build(); + let car; + let tag; + + beforeAll(async () => { + global.database = new MongoDBStorage(config.get('storage')); + await global.database.start(); + + const createUserResponse = await testData.userService.userApi.create(user); + user.id = createUserResponse.data.id; + + // We use storage layer directly to enforce setting default to false + car = Factory.car.build({default: false, carCatalogID, userID: user.id}); + const createdCarID = await CarStorage.saveCar(testData.tenantContext.getTenant(), car); + car.id = createdCarID; + + // We use storage layer directly to enforce setting default to false + tag = Factory.tag.build({default: false, userID: user.id}); + await TagStorage.saveTag(testData.tenantContext.getTenant(), tag); + }); + + afterAll(async () => { + await global.database.stop(); + }); + + test('When getting his sessions context for (any connector), (any charging station)', async () => { + const response = await testData.userService.userApi.getUserSessionContext({ + userID: user.id, + chargingStationID: testData.chargingStationContext.getChargingStation().id, + connectorID: 1 + }); + + expect(response.status).to.be.eq(StatusCodes.OK); + assert(response.data.car.id === car.id); + assert(response.data.tag.id === tag.id); + assert(!response.data.car.default); + assert(!response.data.tag.default); + }); + }); + + describe('Given a user with 2 cars and 2 tags', () => { + const user = Factory.user.build(); + let defaultTag; + let defaultCar; + let car; + let tag; + + beforeAll(async () => { + global.database = new MongoDBStorage(config.get('storage')); + await global.database.start(); + + const createUserResponse = await testData.userService.userApi.create(user); + user.id = createUserResponse.data.id; + + + // Create non-default user car via storage layer + car = Factory.car.build({carCatalogID, userID: user.id, default: false}); + const createdCarID = await CarStorage.saveCar(testData.tenantContext.getTenant(), car); + car.id = createdCarID; + + // Create default user car via storage layer + defaultCar = Factory.car.build({carCatalogID, userID: user.id, default: true}); + const createDefaultCarID = await CarStorage.saveCar(testData.tenantContext.getTenant(), defaultCar); + defaultCar.id = createDefaultCarID + + // Create non-default user tag via storage layer + tag = Factory.tag.build({userID: user.id, default: false}); + await TagStorage.saveTag(testData.tenantContext.getTenant(), tag); + + // Create default user tag via storage layer + defaultTag = Factory.tag.build({userID: user.id, default: true}); + await TagStorage.saveTag(testData.tenantContext.getTenant(), defaultTag); + }); + + afterAll(async () => { + await global.database.stop(); + }); + + test('When getting his session context for (any connector), (any charging station)', async () => { + const response = await testData.userService.userApi.getUserSessionContext({ + userID: user.id, + chargingStationID: testData.chargingStationContext.getChargingStation().id, + connectorID: 1 + }); + + expect(response.status).to.be.eq(StatusCodes.OK); + assert(response.data.car.id === defaultCar.id); + assert(response.data.tag.id === defaultTag.id); + expect(response.data.car.default).to.be.true; + expect(response.data.tag.default).to.be.true; + }); + + test('When getting his session context for (any connector), (any charging station), (default tag id) and (default car id)', async () => { + const response = await testData.userService.userApi.getUserSessionContext({ + userID: user.id, + chargingStationID: testData.chargingStationContext.getChargingStation().id, + connectorID: 1, + carID: defaultCar.id, + tagID: defaultTag.id + }); + expect(response.status).to.be.eq(StatusCodes.OK); + assert(response.data.car.id === defaultCar.id); + assert(response.data.tag.id === defaultTag.id); + expect(response.data.car.default).to.be.true; + expect(response.data.tag.default).to.be.true; + }); + + test('When getting his session context for (any connector), (any charging station), (non-default tag id) and (non-default car id)', async () => { + const response = await testData.userService.userApi.getUserSessionContext({ + userID: user.id, + chargingStationID: testData.chargingStationContext.getChargingStation().id, + connectorID: 1, + carID: car.id, + tagID: tag.id + }); + + expect(response.status).to.be.eq(StatusCodes.OK); + assert(response.data.car.id === car.id); + assert(response.data.tag.id === tag.id); + assert(!response.data.car.default); + assert(!response.data.tag.default); + + }); + }); + }); + }); }); diff --git a/test/api/client/UserApi.ts b/test/api/client/UserApi.ts index 8fadce207e..cd7a5a4a5b 100644 --- a/test/api/client/UserApi.ts +++ b/test/api/client/UserApi.ts @@ -63,6 +63,6 @@ export default class UserApi extends CrudApi { public async getUserSessionContext(params) { const url = this.buildRestEndpointUrl(RESTServerRoute.REST_USER_SESSION_CONTEXT, { id: params.userID }); - return super.read({ ChargingStationID: params.chargingStationID, ConnectorID: params.connectorID }, url); + return super.read({ ChargingStationID: params.chargingStationID, ConnectorID: params.connectorID, TagID: params.tagID, CarID: params.carID }, url); } } diff --git a/test/api/context/ContextDefinition.ts b/test/api/context/ContextDefinition.ts index e244c31ff0..f9e612fa32 100644 --- a/test/api/context/ContextDefinition.ts +++ b/test/api/context/ContextDefinition.ts @@ -37,7 +37,7 @@ export default class ContextDefinition { TENANT_BILLING: 'utbilling', // Only billing and pricing component is active TENANT_BILLING_PLATFORM: 'utbillingplatform', // Only billing, pricing and billingplatform component is active TENANT_ASSET: 'utasset', // Only asset component is active - TENANT_CAR: 'utcar', // Only car component is active + TENANT_CAR: 'utcar', // Only car and organization components are active TENANT_SMART_CHARGING: 'utsmartcharging' // Organization and Smart Charging components are active }; @@ -184,7 +184,8 @@ export default class ContextDefinition { password: '', stickyLimitation: true, limitBufferDC: 20, - limitBufferAC: 10 + limitBufferAC: 10, + prioritizationParametersActive: true, } } }, @@ -342,6 +343,7 @@ export default class ContextDefinition { subdomain: ContextDefinition.TENANT_CONTEXTS.TENANT_CAR, componentSettings: { car: {}, + organization: {} } }, { @@ -362,7 +364,8 @@ export default class ContextDefinition { password: '', stickyLimitation: true, limitBufferDC: 20, - limitBufferAC: 10 + limitBufferAC: 10, + prioritizationParametersActive: true, } } }, diff --git a/test/config.js b/test/config.js index ca79af5904..01c5374a06 100644 --- a/test/config.js +++ b/test/config.js @@ -320,7 +320,12 @@ const config = convict({ doc: 'Limit buffer ac', format: Number, default: '' - } + }, + prioritizationParametersActive: { + doc: 'Use prioritization parameters', + format: Boolean, + default: '' + }, }, assetConnectors: { ioThink: {