UNPKG

@citrineos/configuration

Version:

The configuration module for OCPP v2.0.1. This module is not intended to be used directly, but rather as a dependency for other modules.

248 lines 12.9 kB
import { Boot, OCPP1_6_Mapper, OCPP2_0_1_Mapper } from '@citrineos/data'; import { BOOT_STATUS, OCPP1_6, OCPP1_6_CALL_SCHEMA_MAP, OCPP1_6_CallAction, OCPP2_0_1, OCPP2_0_1_CALL_SCHEMA_MAP, OCPP2_0_1_CallAction, } from '@citrineos/base'; import { Logger } from 'tslog'; export class BootNotificationService { _bootRepository; _cache; _logger; _config; constructor(bootRepository, cache, config, logger) { this._bootRepository = bootRepository; this._cache = cache; this._config = config; this._logger = logger ? logger.getSubLogger({ name: this.constructor.name }) : new Logger({ name: this.constructor.name }); } determineBootStatus(bootConfig) { let bootStatus = bootConfig ? OCPP2_0_1_Mapper.BootMapper.toRegistrationStatusEnumType(bootConfig.status) : this._config.ocpp2_0_1.unknownChargerStatus; if (bootStatus === OCPP2_0_1.RegistrationStatusEnumType.Pending) { let needToGetBaseReport = this._config.ocpp2_0_1.getBaseReportOnPending; let needToSetVariables = false; if (bootConfig) { if (bootConfig.getBaseReportOnPending !== undefined && bootConfig.getBaseReportOnPending !== null) { needToGetBaseReport = bootConfig.getBaseReportOnPending; } if (bootConfig.pendingBootSetVariables && bootConfig.pendingBootSetVariables.length > 0) { needToSetVariables = true; } } if (!needToGetBaseReport && !needToSetVariables && this._config.ocpp2_0_1.autoAccept) { bootStatus = OCPP2_0_1.RegistrationStatusEnumType.Accepted; } } return bootStatus; } async createBootNotificationResponse(tenantId, stationId) { // Unknown chargers, chargers without a BootConfig, will use SystemConfig.unknownChargerStatus for status. const bootConfig = await this._bootRepository.readByKey(tenantId, stationId); const bootStatus = this.determineBootStatus(bootConfig); // When any BootConfig field is not set, the corresponding field on the SystemConfig will be used. return { currentTime: new Date().toISOString(), status: bootStatus, statusInfo: OCPP2_0_1_Mapper.BootMapper.toStatusInfo(bootConfig?.statusInfo), interval: bootStatus === OCPP2_0_1.RegistrationStatusEnumType.Accepted ? bootConfig?.heartbeatInterval || this._config.heartbeatInterval : bootConfig?.bootRetryInterval || this._config.bootRetryInterval, }; } async updateBootConfig(bootNotificationResponse, tenantId, stationId) { let bootConfigDbEntity = await this._bootRepository.readByKey(tenantId, stationId); if (!bootConfigDbEntity) { const unknownChargerBootConfig = { status: bootNotificationResponse.status, statusInfo: bootNotificationResponse.statusInfo, }; bootConfigDbEntity = await this._bootRepository.createOrUpdateByKey(tenantId, unknownChargerBootConfig, stationId); } if (!bootConfigDbEntity) { throw new Error('Unable to create/update BootConfig...'); } else { bootConfigDbEntity.lastBootTime = bootNotificationResponse.currentTime; await bootConfigDbEntity.save(); } return bootConfigDbEntity; } /** * Determines whether to blacklist or whitelist charger actions based on its boot status. * * If the new boot is accepted and the charger actions were previously blacklisted, then whitelist the charger actions. * If the new boot is not accepted and charger actions were previously whitelisted, then blacklist the charger actions. * * @param stationId * @param cachedBootStatus * @param bootNotificationResponseStatus */ async cacheChargerActionsPermissions(stationId, cachedBootStatus, bootNotificationResponseStatus) { // New boot status is Accepted and cachedBootStatus exists (meaning there was a previous Rejected or Pending boot) if (bootNotificationResponseStatus === OCPP2_0_1.RegistrationStatusEnumType.Accepted) { if (cachedBootStatus) { // Undo blacklisting of charger-originated actions const promises = Array.from(OCPP2_0_1_CALL_SCHEMA_MAP).map(async ([action]) => { if (action !== OCPP2_0_1_CallAction.BootNotification) { return this._cache.remove(action, stationId); } }); await Promise.all(promises); // Remove cached boot status await this._cache.remove(BOOT_STATUS, stationId); this._logger.debug('Cached boot status removed: ', cachedBootStatus); } } else if (!cachedBootStatus) { // Status is not Accepted; i.e. Status is Rejected or Pending. // Cached boot status for charger did not exist; i.e. this is the first BootNotificationResponse to be Rejected or Pending. // Blacklist all charger-originated actions except BootNotification // GetReport messages will need to un-blacklist NotifyReport // TriggerMessage will need to un-blacklist the message it triggers const promises = Array.from(OCPP2_0_1_CALL_SCHEMA_MAP).map(async ([action]) => { if (action !== OCPP2_0_1_CallAction.BootNotification) { return this._cache.set(action, 'blacklisted', stationId); } }); await Promise.all(promises); } } async createGetBaseReportRequest(stationId, maxCachingSeconds) { // OCTT tool does not meet B07.FR.04; instead always sends requestId === 0 // Commenting out this line, using requestId === 0 until fixed (10/26/2023) // const requestId = Math.floor(Math.random() * ConfigurationModule.GET_BASE_REPORT_REQUEST_ID_MAX); const requestId = 0; await this._cache.set(requestId.toString(), 'ongoing', stationId, maxCachingSeconds); return { requestId: requestId, reportBase: OCPP2_0_1.ReportBaseEnumType.FullInventory, }; } /** * Based on the GetBaseReportMessageConfirmation, checks the cache to ensure GetBaseReport truly succeeded. * If GetBaseReport did not succeed, this method will throw. Otherwise, it will finish without throwing. * * @param stationId * @param requestId * @param getBaseReportMessageConfirmation * @param maxCachingSeconds */ async confirmGetBaseReportSuccess(stationId, requestId, getBaseReportMessageConfirmation, maxCachingSeconds) { if (getBaseReportMessageConfirmation.success) { this._logger.debug(`GetBaseReport successfully sent to charger: ${getBaseReportMessageConfirmation}`); // Wait for GetBaseReport to complete let getBaseReportCacheValue = await this._cache.onChange(requestId, maxCachingSeconds, stationId); while (getBaseReportCacheValue === 'ongoing') { getBaseReportCacheValue = await this._cache.onChange(requestId, maxCachingSeconds, stationId); } if (getBaseReportCacheValue === 'complete') { this._logger.debug('GetBaseReport process successful.'); // All NotifyReports have been processed } else { throw new Error('GetBaseReport process failed--message timed out without a response.'); } } else { throw new Error(`GetBaseReport failed: ${JSON.stringify(getBaseReportMessageConfirmation)}`); } } /** * Methods for OCPP 1.6 */ determineOcpp16BootStatus(bootConfig) { let bootStatus = bootConfig ? OCPP1_6_Mapper.BootMapper.toRegistrationStatusEnumType(bootConfig.status) : this._config.ocpp1_6.unknownChargerStatus; if (bootStatus === OCPP1_6.BootNotificationResponseStatus.Pending) { let needToGetConfigurations = true; let needToChangeConfigurations = true; if (bootConfig) { if (bootConfig.getConfigurationsOnPending !== undefined && bootConfig.getConfigurationsOnPending !== null) { needToGetConfigurations = bootConfig.getConfigurationsOnPending; } if (bootConfig.changeConfigurationsOnPending !== undefined && bootConfig.changeConfigurationsOnPending !== null) { needToChangeConfigurations = bootConfig.changeConfigurationsOnPending; } } if (!needToGetConfigurations && !needToChangeConfigurations) { bootStatus = OCPP1_6.BootNotificationResponseStatus.Accepted; } } return bootStatus; } async createOcpp16BootNotificationResponse(tenantId, stationId) { const boot = await this._bootRepository.readByKey(tenantId, stationId); const status = this.determineOcpp16BootStatus(boot); return { currentTime: new Date().toISOString(), status, interval: status === OCPP1_6.BootNotificationResponseStatus.Accepted ? boot?.heartbeatInterval || this._config.heartbeatInterval : boot?.bootRetryInterval || this._config.bootRetryInterval, }; } /** * Determines whether to blacklist or whitelist charger actions based on its boot status. * * If the new boot is accepted and the charger actions were previously blacklisted, then whitelist the charger actions. * If the new boot is not accepted and charger actions were previously whitelisted, then blacklist the charger actions. * * @param stationId * @param cachedBootStatus * @param bootNotificationResponseStatus */ async cacheOcpp16ChargerActionsPermissions(stationId, cachedBootStatus, bootNotificationResponseStatus) { // New boot status is Accepted and cachedBootStatus exists (meaning there was a previous Rejected or Pending boot) if (bootNotificationResponseStatus === OCPP1_6.BootNotificationResponseStatus.Accepted) { if (cachedBootStatus) { // Undo blacklisting of charger-originated actions const promises = Array.from(OCPP1_6_CALL_SCHEMA_MAP).map(async ([action]) => { if (action !== OCPP1_6_CallAction.BootNotification) { return this._cache.remove(action, stationId); } }); await Promise.all(promises); // Remove cached boot status await this._cache.remove(BOOT_STATUS, stationId); this._logger.debug(`Cached boot status ${cachedBootStatus} removed for station ${stationId}.`); } } else if (!cachedBootStatus) { // Status is not Accepted; i.e. Status is Rejected or Pending. // Cached boot status for charger did not exist; i.e. this is the first BootNotificationResponse to be Rejected or Pending. // Blacklist all charger-originated actions except BootNotification // ChangeConfiguration, GetConfiguration and TriggerMessage will need to un-blacklist the message it triggers const promises = Array.from(OCPP1_6_CALL_SCHEMA_MAP).map(async ([action]) => { if (action !== OCPP1_6_CallAction.BootNotification) { return this._cache.set(action, 'blacklisted', stationId); } }); await Promise.all(promises); } } async updateOcpp16BootConfig(response, tenantId, stationId) { const heartbeatInterval = response.status === OCPP1_6.BootNotificationResponseStatus.Accepted ? response.interval : undefined; const bootRetryInterval = response.status !== OCPP1_6.BootNotificationResponseStatus.Accepted ? response.interval : undefined; const unknownChargerBootConfig = { status: response.status, heartbeatInterval, bootRetryInterval, }; let bootConfigDbEntity = await this._bootRepository.createOrUpdateByKey(tenantId, unknownChargerBootConfig, stationId); if (bootConfigDbEntity) { bootConfigDbEntity = await this._bootRepository.updateLastBootTimeByKey(tenantId, response.currentTime, stationId); } if (!bootConfigDbEntity) { throw new Error('Unable to create/update BootConfig...'); } return bootConfigDbEntity; } } //# sourceMappingURL=BootNotificationService.js.map