feat: add `deleteChargingStations` SRPC command to UI Services
authorJérôme Benoit <jerome.benoit@sap.com>
Sun, 11 Feb 2024 11:15:14 +0000 (12:15 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Sun, 11 Feb 2024 11:15:14 +0000 (12:15 +0100)
Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
18 files changed:
README.md
pnpm-lock.yaml
src/assets/ui-protocol/Insomnia-CSSimulatorUIHTTPProtocol.json
src/assets/ui-protocol/Insomnia-CSSimulatorUIWSProtocol.json
src/charging-station/AutomaticTransactionGenerator.ts
src/charging-station/Bootstrap.ts
src/charging-station/ChargingStation.ts
src/charging-station/broadcast-channel/ChargingStationWorkerBroadcastChannel.ts
src/charging-station/ui-server/AbstractUIServer.ts
src/charging-station/ui-server/ui-services/AbstractUIService.ts
src/performance/PerformanceStatistics.ts
src/types/ChargingStationEvents.ts
src/types/UIProtocol.ts
src/types/WorkerBroadcastChannel.ts
src/utils/Constants.ts
src/utils/MessageChannelUtils.ts
src/utils/index.ts
ui/web/pnpm-lock.yaml

index 2234043f48ec2078d2eb24b654a1271a3964c22f..84347a67d8fd428fffc6ea0dbf83271a255f1da6 100644 (file)
--- a/README.md
+++ b/README.md
@@ -587,6 +587,23 @@ Set the Websocket header _Sec-Websocket-Protocol_ to `ui0.0.1`.
    `status`: 'success' | 'failure'  
   }
 
+###### Delete Charging Stations
+
+- Request:  
+  `ProcedureName`: 'deleteChargingStations'  
+  `PDU`: {  
+   `hashIds`: charging station unique identifier strings array (optional, default: all charging stations),  
+   `deleteConfiguration?`: boolean  
+  }
+
+- Response:  
+  `PDU`: {  
+   `status`: 'success' | 'failure',  
+   `hashIdsSucceeded`: charging station unique identifier strings array,  
+   `hashIdsFailed`: charging station unique identifier strings array (optional),  
+   `responsesFailed`: failed responses payload array (optional)  
+  }
+
 ###### Set Charging Station Supervision Url
 
 - Request:  
index e476785f168084c60faadde71d025f06abf75c3c..be83ed4b0d13a3b5ecd36f9fd024a06609f815db 100644 (file)
@@ -3834,7 +3834,7 @@ packages:
       has-property-descriptors: 1.0.1
       has-proto: 1.0.1
       has-symbols: 1.0.3
-      hasown: 2.0.0
+      hasown: 2.0.1
       internal-slot: 1.0.7
       is-array-buffer: 3.0.4
       is-callable: 1.2.7
@@ -3890,13 +3890,13 @@ packages:
     dependencies:
       get-intrinsic: 1.2.4
       has-tostringtag: 1.0.2
-      hasown: 2.0.0
+      hasown: 2.0.1
     dev: true
 
   /es-shim-unscopables@1.0.2:
     resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==}
     dependencies:
-      hasown: 2.0.0
+      hasown: 2.0.1
     dev: true
 
   /es-to-primitive@1.2.1:
@@ -4245,7 +4245,7 @@ packages:
       eslint: 8.56.0
       eslint-import-resolver-node: 0.3.9
       eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0)
-      hasown: 2.0.0
+      hasown: 2.0.1
       is-core-module: 2.13.1
       is-glob: 4.0.3
       minimatch: 3.1.2
@@ -4858,7 +4858,7 @@ packages:
       function-bind: 1.1.2
       has-proto: 1.0.1
       has-symbols: 1.0.3
-      hasown: 2.0.0
+      hasown: 2.0.1
     dev: true
 
   /get-package-type@0.1.0:
@@ -5175,8 +5175,8 @@ packages:
       minimalistic-assert: 1.0.1
     dev: true
 
-  /hasown@2.0.0:
-    resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==}
+  /hasown@2.0.1:
+    resolution: {integrity: sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==}
     engines: {node: '>= 0.4'}
     dependencies:
       function-bind: 1.1.2
@@ -5522,7 +5522,7 @@ packages:
     engines: {node: '>= 0.4'}
     dependencies:
       es-errors: 1.3.0
-      hasown: 2.0.0
+      hasown: 2.0.1
       side-channel: 1.0.5
     dev: true
 
@@ -5642,7 +5642,7 @@ packages:
   /is-core-module@2.13.1:
     resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==}
     dependencies:
-      hasown: 2.0.0
+      hasown: 2.0.1
 
   /is-date-object@1.0.5:
     resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==}
index 759a22681cf9a1c65da9c47283748e3accb93124..afb407789d2f4e25996a7bba42ed7085038251e5 100644 (file)
@@ -1,7 +1,7 @@
 {
   "_type": "export",
   "__export_format": 4,
-  "__export_date": "2024-02-09T13:02:45.966Z",
+  "__export_date": "2024-02-11T11:11:14.532Z",
   "__export_source": "insomnia.desktop.app:v8.6.1",
   "resources": [
     {
       "settingFollowRedirects": "global",
       "_type": "request"
     },
+    {
+      "_id": "req_5db3c8b9a7094ec5806bcc804752f968",
+      "parentId": "wrk_509d4a5094fa485ba93e53bc735e8ac3",
+      "modified": 1707649833755,
+      "created": 1707649782517,
+      "url": "{{baseUrl}}/{{protocol}}/{{version}}/deleteChargingStations",
+      "name": "deleteChargingStations",
+      "description": "",
+      "method": "POST",
+      "body": {
+        "mimeType": "application/json",
+        "text": "{\n\t\"hashIds\": [\n\t\t\"0058d8b50e422cce5bbd0c0a4ad13d5d657e8a88670dcf04c1b2b563fea3db5b96a3686278b374ed050e21baef89060e\"\n\t]\n}"
+      },
+      "parameters": [],
+      "headers": [
+        {
+          "name": "Content-Type",
+          "value": "application/json",
+          "id": "pair_af9f914ca52f407488bc6df6c7db3a08"
+        }
+      ],
+      "authentication": {
+        "type": "basic",
+        "useISO88591": false,
+        "disabled": false,
+        "username": "{{username}}",
+        "password": "{{password}}"
+      },
+      "metaSortKey": -999999971.875,
+      "isPrivate": false,
+      "pathParameters": [],
+      "settingStoreCookies": true,
+      "settingSendCookies": true,
+      "settingDisableRenderRequestBody": false,
+      "settingEncodeUrl": true,
+      "settingRebuildPath": true,
+      "settingFollowRedirects": "global",
+      "_type": "request"
+    },
     {
       "_id": "req_79422f6238204d9b8f832f2cb3ebfde3",
       "parentId": "wrk_509d4a5094fa485ba93e53bc735e8ac3",
index cd50ed5e09c47516f2afd7757b5022858ea7c9c8..888346bb49344a489777823a9875cd7a61d245c3 100644 (file)
@@ -1,7 +1,7 @@
 {
   "_type": "export",
   "__export_format": 4,
-  "__export_date": "2024-02-09T13:03:00.609Z",
+  "__export_date": "2024-02-11T11:07:54.308Z",
   "__export_source": "insomnia.desktop.app:v8.6.1",
   "resources": [
     {
       "description": "",
       "_type": "websocket_request"
     },
+    {
+      "_id": "ws-req_c36eca7e4cdd453dac690a46cba1afdf",
+      "parentId": "wrk_64c9d5670f014930baf668326b95e601",
+      "modified": 1707645101146,
+      "created": 1707645101146,
+      "name": "deleteChargingStations",
+      "url": "{{ _.baseUrl }}",
+      "metaSortKey": -1671191988792.4688,
+      "headers": [
+        {
+          "id": "pair_9a64d3b0bc654ab68710ef138f00d3f5",
+          "name": "Sec-WebSocket-Protocol",
+          "value": "{{ _.protocol }}{{ _.version }}",
+          "description": ""
+        }
+      ],
+      "authentication": {
+        "type": "basic",
+        "useISO88591": false,
+        "disabled": false,
+        "username": "{{ _.username }}",
+        "password": "{{ _.password }}"
+      },
+      "parameters": [],
+      "pathParameters": [],
+      "settingEncodeUrl": true,
+      "settingStoreCookies": true,
+      "settingSendCookies": true,
+      "settingFollowRedirects": "global",
+      "description": "",
+      "_type": "websocket_request"
+    },
     {
       "_id": "ws-req_afbfa6e6824b427e99e735c0b1eabe3b",
       "parentId": "wrk_64c9d5670f014930baf668326b95e601",
     {
       "_id": "ws-payload_2872e2656c164769acf98cc7ba7ea028",
       "parentId": "ws-req_e5902850ac1d40369bd6e942a2755a9d",
-      "modified": 1707477801921,
+      "modified": 1707649312368,
       "created": 1671297544207,
       "name": "New Payload",
       "value": "[\n\t\"{% uuid 'v4' %}\",\n\t\"listChargingStations\",\n\t{}\n]",
     {
       "_id": "ws-payload_edebda0226aa43f88712d7feb60ac645",
       "parentId": "ws-req_ebe5a555a6344dfba7e29f857af11d08",
-      "modified": 1707475947625,
+      "modified": 1707649325381,
       "created": 1671297697172,
       "name": "New Payload",
       "value": "[\n\t\"{% uuid 'v4' %}\",\n\t\"startChargingStation\",\n\t{\n\t\t\"hashIds\": [\n\t\t\t\"5b82a559d2b453f6277e272e134ae824ae358cfb6ee2415af9f7c2f325ef8b3e930aeeadcd866df4b8aec58786e60ae7\"\n\t\t]\n\t}\n]",
     {
       "_id": "ws-payload_d8e66e0f933e4d74bb5fbff4d15a44bf",
       "parentId": "ws-req_23025e078480491daf01406b2b5e9cc2",
-      "modified": 1707157205853,
+      "modified": 1707589443241,
       "created": 1671298432039,
       "name": "New Payload",
       "value": "[\n\t\"{% uuid 'v4' %}\",\n\t\"startAutomaticTransactionGenerator\",\n\t{}\n]",
     {
       "_id": "ws-payload_95c28d71c8d940bb83ac514f8916a66d",
       "parentId": "ws-req_afbfa6e6824b427e99e735c0b1eabe3b",
-      "modified": 1707157216642,
+      "modified": 1707649322410,
       "created": 1678991663554,
       "name": "New Payload",
       "value": "[\n\t\"{% uuid 'v4' %}\",\n\t\"setSupervisionUrl\",\n\t{\n\t\t\"hashIds\": [\n\t\t\t\"5b82a559d2b453f6277e272e134ae824ae358cfb6ee2415af9f7c2f325ef8b3e930aeeadcd866df4b8aec58786e60ae7\"\n\t\t],\n\t\t\"url\": \"wss://domain.tld\"\n\t}\n]",
     {
       "_id": "ws-payload_3e1dffbcefcc481286b44c694b9e6496",
       "parentId": "ws-req_3a0ff14878b449f4be3dfbb7432b5f87",
-      "modified": 1707481292801,
+      "modified": 1707645536699,
       "created": 1706726300041,
       "name": "New Payload",
       "value": "[\n\t\"{% uuid 'v4' %}\",\n\t\"listTemplates\",\n\t{}\n]",
     {
       "_id": "ws-payload_5e2dfed34a104c28b887c885ada1b4af",
       "parentId": "ws-req_8777c5635dd64fccbc2b0f450be656c0",
-      "modified": 1707482458081,
+      "modified": 1707649639026,
       "created": 1706778795544,
       "name": "New Payload",
       "value": "[\n\t\"{% uuid 'v4' %}\",\n\t\"addChargingStations\",\n\t{\n\t\t\"template\": \"evlink.station-template\",\n\t\t\"numberOfStations\": 1,\n\t\t\"options\": {\n\t\t\t\"autoStart\": false\n\t\t}\n\t}\n]",
     {
       "_id": "ws-payload_494fba679fa644ccb318f092e780834f",
       "parentId": "ws-req_0bab7a97ceda4944976a463f616dec5c",
-      "modified": 1707475888681,
+      "modified": 1707649324584,
       "created": 1707143784130,
       "name": "New Payload",
       "value": "[\n\t\"{% uuid 'v4' %}\",\n\t\"performanceStatistics\",\n\t{}\n]",
       "mode": "application/json",
       "_type": "websocket_payload"
+    },
+    {
+      "_id": "ws-payload_c4a0dbcd684e430b9b27298fe2bd088a",
+      "parentId": "ws-req_c36eca7e4cdd453dac690a46cba1afdf",
+      "modified": 1707649358965,
+      "created": 1707645101151,
+      "name": "New Payload",
+      "value": "[\n\t\"{% uuid 'v4' %}\",\n\t\"deleteChargingStations\",\n\t{\n\t\t\"hashIds\": [\n\t\t\t\"5b82a559d2b453f6277e272e134ae824ae358cfb6ee2415af9f7c2f325ef8b3e930aeeadcd866df4b8aec58786e60ae7\"\n\t\t]\n\t}\n]",
+      "mode": "application/json",
+      "_type": "websocket_payload"
     }
   ]
 }
index 37c9b63113f3cc03e916ded80b8b470807d0dfb4..95483b8ec54dd80072f9fe48171e45fe08761de0 100644 (file)
@@ -66,6 +66,11 @@ export class AutomaticTransactionGenerator {
     return AutomaticTransactionGenerator.instances.get(chargingStation.stationInfo!.hashId)
   }
 
+  public static deleteInstance (chargingStation: ChargingStation): boolean {
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    return AutomaticTransactionGenerator.instances.delete(chargingStation.stationInfo!.hashId)
+  }
+
   public start (stopAbsoluteDuration?: boolean): void {
     if (!checkChargingStation(this.chargingStation, this.logPrefix())) {
       return
index ddf8ee7016c7ab5bf82f09d9f80ca1bec32ee68b..5356a15569fbce4db81a738dc7e24274d93f2118 100644 (file)
@@ -144,6 +144,7 @@ export class Bootstrap extends EventEmitter {
       if (!this.starting) {
         this.starting = true
         this.on(ChargingStationWorkerMessageEvents.added, this.workerEventAdded)
+        this.on(ChargingStationWorkerMessageEvents.deleted, this.workerEventDeleted)
         this.on(ChargingStationWorkerMessageEvents.started, this.workerEventStarted)
         this.on(ChargingStationWorkerMessageEvents.stopped, this.workerEventStopped)
         this.on(ChargingStationWorkerMessageEvents.updated, this.workerEventUpdated)
@@ -351,6 +352,9 @@ export class Bootstrap extends EventEmitter {
         case ChargingStationWorkerMessageEvents.added:
           this.emit(ChargingStationWorkerMessageEvents.added, msg.data)
           break
+        case ChargingStationWorkerMessageEvents.deleted:
+          this.emit(ChargingStationWorkerMessageEvents.deleted, msg.data)
+          break
         case ChargingStationWorkerMessageEvents.started:
           this.emit(ChargingStationWorkerMessageEvents.started, msg.data)
           break
@@ -399,6 +403,19 @@ export class Bootstrap extends EventEmitter {
     )
   }
 
+  private readonly workerEventDeleted = (data: ChargingStationData): void => {
+    this.uiServer?.chargingStations.delete(data.stationInfo.hashId)
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    --this.chargingStationsByTemplate.get(data.stationInfo.templateName)!.added
+    logger.info(
+      `${this.logPrefix()} ${moduleName}.workerEventDeleted: Charging station ${
+        data.stationInfo.chargingStationId
+      } (hashId: ${data.stationInfo.hashId}) deleted (${
+        this.numberOfAddedChargingStations
+      } added from ${this.numberOfConfiguredChargingStations} configured charging station(s))`
+    )
+  }
+
   private readonly workerEventStarted = (data: ChargingStationData): void => {
     this.uiServer?.chargingStations.set(data.stationInfo.hashId, data)
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
index 4ef9ab5d92e5d1f80307f44314d3e5bbb3cbc894..2b5b5f8376fca34c915965ab1b0d46df590ef6ad 100644 (file)
@@ -2,7 +2,7 @@
 
 import { createHash } from 'node:crypto'
 import { EventEmitter } from 'node:events'
-import { type FSWatcher, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
+import { type FSWatcher, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
 import { dirname, join, parse } from 'node:path'
 import { URL } from 'node:url'
 import { parentPort } from 'node:worker_threads'
@@ -127,6 +127,7 @@ import {
   buildAddedMessage,
   buildChargingStationAutomaticTransactionGeneratorConfiguration,
   buildConnectorsStatus,
+  buildDeletedMessage,
   buildEvsesStatus,
   buildStartedMessage,
   buildStoppedMessage,
@@ -166,7 +167,7 @@ export class ChargingStation extends EventEmitter {
   public readonly connectors: Map<number, ConnectorStatus>
   public readonly evses: Map<number, EvseStatus>
   public readonly requests: Map<string, CachedRequest>
-  public performanceStatistics!: PerformanceStatistics | undefined
+  public performanceStatistics: PerformanceStatistics | undefined
   public heartbeatSetInterval?: NodeJS.Timeout
   public ocppRequestService!: OCPPRequestService
   public bootNotificationRequest?: BootNotificationRequest
@@ -183,7 +184,7 @@ export class ChargingStation extends EventEmitter {
   private configuredSupervisionUrl!: URL
   private wsConnectionRetried: boolean
   private wsConnectionRetryCount: number
-  private templateFileWatcher!: FSWatcher | undefined
+  private templateFileWatcher: FSWatcher | undefined
   private templateFileHash!: string
   private readonly sharedLRUCache: SharedLRUCache
   private wsPingSetInterval?: NodeJS.Timeout
@@ -211,6 +212,9 @@ export class ChargingStation extends EventEmitter {
     this.on(ChargingStationEvents.added, () => {
       parentPort?.postMessage(buildAddedMessage(this))
     })
+    this.on(ChargingStationEvents.deleted, () => {
+      parentPort?.postMessage(buildDeletedMessage(this))
+    })
     this.on(ChargingStationEvents.started, () => {
       parentPort?.postMessage(buildStartedMessage(this))
     })
@@ -666,6 +670,23 @@ export class ChargingStation extends EventEmitter {
     this.emit(ChargingStationEvents.added)
   }
 
+  public async delete (deleteConfiguration = true): Promise<void> {
+    if (this.started) {
+      await this.stop()
+    }
+    AutomaticTransactionGenerator.deleteInstance(this)
+    PerformanceStatistics.deleteInstance(this.stationInfo?.hashId)
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo!)!)
+    this.requests.clear()
+    this.connectors.clear()
+    this.evses.clear()
+    this.templateFileWatcher?.unref()
+    deleteConfiguration && rmSync(this.configurationFile, { force: true })
+    this.chargingStationWorkerBroadcastChannel.unref()
+    this.emit(ChargingStationEvents.deleted)
+  }
+
   public start (): void {
     if (!this.started) {
       if (!this.starting) {
@@ -689,10 +710,10 @@ export class ChargingStation extends EventEmitter {
                   } file have changed, reload`
                 )
                 this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash)
-                // Initialize
-                this.initialize()
                 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                 this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo!)!)
+                // Initialize
+                this.initialize()
                 // Restart the ATG
                 const ATGStarted = this.automaticTransactionGenerator?.started
                 if (ATGStarted === true) {
@@ -743,12 +764,11 @@ export class ChargingStation extends EventEmitter {
         if (this.stationInfo?.enableStatistics === true) {
           this.performanceStatistics?.stop()
         }
-        this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash)
         this.templateFileWatcher?.close()
-        this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash)
         delete this.bootNotificationResponse
         this.started = false
         this.saveConfiguration()
+        this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash)
         this.emit(ChargingStationEvents.stopped)
         this.stopping = false
       } else {
index 10d0c75c3aff0ad88776f0d76a2fb80cca665c5e..26aa61d38e7c97149877fcb13584715afc430d34 100644 (file)
@@ -86,6 +86,12 @@ export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChanne
           await this.chargingStation.stop()
         }
       ],
+      [
+        BroadcastChannelProcedureName.DELETE_CHARGING_STATIONS,
+        async (requestPayload?: BroadcastChannelRequestPayload) => {
+          await this.chargingStation.delete(requestPayload?.deleteConfiguration as boolean)
+        }
+      ],
       [
         BroadcastChannelProcedureName.OPEN_CONNECTION,
         () => {
index 99dbc657e2708cbf92b9b4aa80244c2de4d7a769..a1375cea3786d131013685e39786032e7f1e4d6f 100644 (file)
@@ -59,6 +59,9 @@ export abstract class AbstractUIServer {
 
   public stop (): void {
     this.stopHttpServer()
+    for (const uiService of this.uiServices.values()) {
+      uiService.stop()
+    }
     this.chargingStations.clear()
     this.chargingStationTemplates.clear()
   }
index 4055837fe18d89700e4f22f31bc01f0a1391fa1a..f8a147895414112bd9f774179092d8d7a89a0a73 100644 (file)
@@ -29,6 +29,10 @@ export abstract class AbstractUIService {
   >([
     [ProcedureName.START_CHARGING_STATION, BroadcastChannelProcedureName.START_CHARGING_STATION],
     [ProcedureName.STOP_CHARGING_STATION, BroadcastChannelProcedureName.STOP_CHARGING_STATION],
+    [
+      ProcedureName.DELETE_CHARGING_STATIONS,
+      BroadcastChannelProcedureName.DELETE_CHARGING_STATIONS
+    ],
     [ProcedureName.CLOSE_CONNECTION, BroadcastChannelProcedureName.CLOSE_CONNECTION],
     [ProcedureName.OPEN_CONNECTION, BroadcastChannelProcedureName.OPEN_CONNECTION],
     [
@@ -79,6 +83,11 @@ export abstract class AbstractUIService {
     this.broadcastChannelRequests = new Map<string, number>()
   }
 
+  public stop (): void {
+    this.broadcastChannelRequests.clear()
+    this.uiServiceWorkerBroadcastChannel.close()
+  }
+
   public async requestHandler (request: ProtocolRequest): Promise<ProtocolResponse | undefined> {
     let messageId: string | undefined
     let command: ProcedureName | undefined
index 3bb72ffa5f711b2b6c3c2af6d3e2501454503f45..e90595225278d3a926059a2ca3ab7821b800d644 100644 (file)
@@ -67,20 +67,19 @@ export class PerformanceStatistics {
     objName: string | undefined,
     uri: URL | undefined
   ): PerformanceStatistics | undefined {
-    const logPfx = logPrefix(' Performance statistics')
     if (objId == null) {
       const errMsg = 'Cannot get performance statistics instance without specifying object id'
-      logger.error(`${logPfx} ${errMsg}`)
+      logger.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`)
       throw new BaseError(errMsg)
     }
     if (objName == null) {
       const errMsg = 'Cannot get performance statistics instance without specifying object name'
-      logger.error(`${logPfx} ${errMsg}`)
+      logger.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`)
       throw new BaseError(errMsg)
     }
     if (uri == null) {
       const errMsg = 'Cannot get performance statistics instance without specifying object uri'
-      logger.error(`${logPfx} ${errMsg}`)
+      logger.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`)
       throw new BaseError(errMsg)
     }
     if (!PerformanceStatistics.instances.has(objId)) {
@@ -89,6 +88,15 @@ export class PerformanceStatistics {
     return PerformanceStatistics.instances.get(objId)
   }
 
+  public static deleteInstance (objId: string | undefined): boolean {
+    if (objId == null) {
+      const errMsg = 'Cannot delete performance statistics instance without specifying object id'
+      logger.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`)
+      throw new BaseError(errMsg)
+    }
+    return PerformanceStatistics.instances.delete(objId)
+  }
+
   public static beginMeasure (id: string): string {
     const markId = `${id.charAt(0).toUpperCase()}${id.slice(1)}~${generateUUID()}`
     performance.mark(markId)
@@ -308,6 +316,10 @@ export class PerformanceStatistics {
     }
   }
 
+  private static readonly logPrefix = (): string => {
+    return logPrefix(' Performance statistics')
+  }
+
   private readonly logPrefix = (): string => {
     return logPrefix(` ${this.objName} | Performance statistics`)
   }
index bfdf7a00b4a3a557da6bd09ac0d6c2d142de7afa..b690bc916f2dca73d604349dc508f11177ff0643 100644 (file)
@@ -1,5 +1,6 @@
 export enum ChargingStationEvents {
   added = 'added',
+  deleted = 'deleted',
   started = 'started',
   stopped = 'stopped',
   updated = 'updated',
index 30fd26c370cc80f43591c908285ad84cf94737bc..e2d65804b78be82cc0dc74af1282ca25c5db960a 100644 (file)
@@ -33,6 +33,7 @@ export enum ProcedureName {
   LIST_TEMPLATES = 'listTemplates',
   LIST_CHARGING_STATIONS = 'listChargingStations',
   ADD_CHARGING_STATIONS = 'addChargingStations',
+  DELETE_CHARGING_STATIONS = 'deleteChargingStations',
   PERFORMANCE_STATISTICS = 'performanceStatistics',
   START_CHARGING_STATION = 'startChargingStation',
   STOP_CHARGING_STATION = 'stopChargingStation',
index 902ba0af604b3a603e5b436e5601607e5835e780..caa56e9ff9ae5e674c069bd98c828ced97495cd5 100644 (file)
@@ -10,6 +10,7 @@ export type BroadcastChannelResponse = [string, BroadcastChannelResponsePayload]
 export enum BroadcastChannelProcedureName {
   START_CHARGING_STATION = 'startChargingStation',
   STOP_CHARGING_STATION = 'stopChargingStation',
+  DELETE_CHARGING_STATIONS = 'deleteChargingStations',
   OPEN_CONNECTION = 'openConnection',
   CLOSE_CONNECTION = 'closeConnection',
   START_AUTOMATIC_TRANSACTION_GENERATOR = 'startAutomaticTransactionGenerator',
index c467df73a853574ec274c69e93d791b17a0e97a5..ea0ff8fca7dece1e9f88092664e698006724ae7a 100644 (file)
@@ -14,7 +14,7 @@ export class Constants {
   private static readonly SEMVER_PATTERN =
     '^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$'
 
-  private static readonly DEFAULT_CHARGING_STATION_RESET_TIME = 60000 // Ms
+  private static readonly DEFAULT_CHARGING_STATION_RESET_TIME = 30000 // Ms
 
   static readonly DEFAULT_STATION_INFO: Partial<ChargingStationInfo> = Object.freeze({
     enableStatistics: false,
index a3b267eee84aa82ccd30f106f2a5119b323d9d0b..36f797f5eb88feb3a9f5e0a4b6ea978b660a9f9b 100644 (file)
@@ -21,6 +21,15 @@ export const buildAddedMessage = (
   }
 }
 
+export const buildDeletedMessage = (
+  chargingStation: ChargingStation
+): ChargingStationWorkerMessage<ChargingStationData> => {
+  return {
+    event: ChargingStationWorkerMessageEvents.deleted,
+    data: buildChargingStationDataPayload(chargingStation)
+  }
+}
+
 export const buildStartedMessage = (
   chargingStation: ChargingStation
 ): ChargingStationWorkerMessage<ChargingStationData> => {
index f16b793d55297b31d2c6661a01670167442c93c7..21a650ef42d92d1d69a6b67bc1b85cfe083ce811 100644 (file)
@@ -20,6 +20,7 @@ export { watchJsonFile } from './FileUtils.js'
 export {
   buildAddedMessage,
   buildChargingStationDataPayload,
+  buildDeletedMessage,
   buildPerformanceStatisticsMessage,
   buildStartedMessage,
   buildStoppedMessage,
index 07bfbb1a133cee856e4c01022df96e8b7d7052f7..0fc52763ca1f6568b90f21944be6e668bf9ea784 100644 (file)
@@ -1836,7 +1836,7 @@ packages:
       has-property-descriptors: 1.0.1
       has-proto: 1.0.1
       has-symbols: 1.0.3
-      hasown: 2.0.0
+      hasown: 2.0.1
       internal-slot: 1.0.7
       is-array-buffer: 3.0.4
       is-callable: 1.2.7
@@ -1878,13 +1878,13 @@ packages:
     dependencies:
       get-intrinsic: 1.2.4
       has-tostringtag: 1.0.2
-      hasown: 2.0.0
+      hasown: 2.0.1
     dev: true
 
   /es-shim-unscopables@1.0.2:
     resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==}
     dependencies:
-      hasown: 2.0.0
+      hasown: 2.0.1
     dev: true
 
   /es-to-primitive@1.2.1:
@@ -2043,7 +2043,7 @@ packages:
       eslint: 8.56.0
       eslint-import-resolver-node: 0.3.9
       eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0)
-      hasown: 2.0.0
+      hasown: 2.0.1
       is-core-module: 2.13.1
       is-glob: 4.0.3
       minimatch: 3.1.2
@@ -2377,7 +2377,7 @@ packages:
       function-bind: 1.1.2
       has-proto: 1.0.1
       has-symbols: 1.0.3
-      hasown: 2.0.0
+      hasown: 2.0.1
     dev: true
 
   /get-stream@8.0.1:
@@ -2519,8 +2519,8 @@ packages:
       has-symbols: 1.0.3
     dev: true
 
-  /hasown@2.0.0:
-    resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==}
+  /hasown@2.0.1:
+    resolution: {integrity: sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==}
     engines: {node: '>= 0.4'}
     dependencies:
       function-bind: 1.1.2
@@ -2622,7 +2622,7 @@ packages:
     engines: {node: '>= 0.4'}
     dependencies:
       es-errors: 1.3.0
-      hasown: 2.0.0
+      hasown: 2.0.1
       side-channel: 1.0.5
     dev: true
 
@@ -2656,7 +2656,7 @@ packages:
   /is-core-module@2.13.1:
     resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==}
     dependencies:
-      hasown: 2.0.0
+      hasown: 2.0.1
     dev: true
 
   /is-date-object@1.0.5: