Skip to content

Commit

Permalink
Issue 91: Implement StatusNotification
Browse files Browse the repository at this point in the history
Signed-off-by: Zihe Cheng <lydiazcheng@users.noreply.github.com>
  • Loading branch information
lydiazcheng committed Feb 7, 2025
1 parent b0b6a51 commit baed9d0
Show file tree
Hide file tree
Showing 12 changed files with 204 additions and 18 deletions.
1 change: 1 addition & 0 deletions 00_Base/src/ocpp/persistence/namespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,5 @@ export enum OCPP2_0_1_Namespace {

export enum OCPP1_6_Namespace {
ChangeConfiguration = 'ChangeConfiguration',
Connector = 'Connector',
}
1 change: 1 addition & 0 deletions 01_Data/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export {
ChargingStation,
ChargingStationSequence,
Component,
Connector,
DefaultSequelizeInstance,
Location,
MeterValue,
Expand Down
7 changes: 6 additions & 1 deletion 01_Data/src/interfaces/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import {
type VariableAttribute,
VariableCharacteristics,
type VariableMonitoring,
StatusNotification,
Connector,
} from '../layers/sequelize';
import { type AuthorizationRestrictions, type VariableAttributeQuerystring } from '.';
import { TariffQueryString } from './queries/Tariff';
Expand Down Expand Up @@ -86,6 +88,7 @@ export interface ILocalAuthListRepository extends CrudRepository<LocalListVersio
/**
* Creates a SendLocalList.
* @param {string} stationId - The ID of the station.
* @param {string} correlationId - The correlation ID.
* @param {UpdateEnumType} updateType - The type of update.
* @param {number} versionNumber - The version number.
* @param {AuthorizationData[]} localAuthorizationList - The list of authorizations.
Expand Down Expand Up @@ -113,7 +116,9 @@ export interface ILocationRepository extends CrudRepository<Location> {
readChargingStationByStationId: (stationId: string) => Promise<ChargingStation | undefined>;
setChargingStationIsOnline: (stationId: string, isOnline: boolean) => Promise<boolean>;
doesChargingStationExistByStationId: (stationId: string) => Promise<boolean>;
addStatusNotificationToChargingStation(stationId: string, statusNotification: OCPP2_0_1.StatusNotificationRequest): Promise<void>;
addStatusNotificationToChargingStation(stationId: string, statusNotification: StatusNotification): Promise<void>;
createOrUpdateChargingStation(chargingStation: ChargingStation): Promise<ChargingStation>;
createOrUpdateConnector(connector: Connector): Promise<Connector | undefined>;
}

export interface ISecurityEventRepository extends CrudRepository<SecurityEvent> {
Expand Down
2 changes: 1 addition & 1 deletion 01_Data/src/layers/sequelize/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export { Authorization, IdToken, IdTokenInfo, AdditionalInfo, LocalListAuthoriza
export { Transaction, TransactionEvent, MeterValue } from './model/TransactionEvent';
export { SecurityEvent } from './model/SecurityEvent';
export { VariableMonitoring, EventData, VariableMonitoringStatus } from './model/VariableMonitoring';
export { ChargingStation, ChargingStationNetworkProfile, Location, ServerNetworkProfile, SetNetworkProfile, StatusNotification } from './model/Location';
export { ChargingStation, ChargingStationNetworkProfile, Location, ServerNetworkProfile, SetNetworkProfile, StatusNotification, Connector } from './model/Location';
export { ChargingStationSequence } from './model/ChargingStationSequence';
export { MessageInfo } from './model/MessageInfo';
export { Tariff } from './model/Tariff';
Expand Down
48 changes: 48 additions & 0 deletions 01_Data/src/layers/sequelize/model/Location/Connector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache 2.0

import { OCPP1_6_Namespace, OCPP1_6 } from '@citrineos/base';
import { Column, DataType, Model, Table } from 'sequelize-typescript';

@Table
export class Connector extends Model {
static readonly MODEL_NAME: string = OCPP1_6_Namespace.Connector;

@Column({
unique: 'stationId_connectorId',
allowNull: false,
type: DataType.STRING,
})
declare stationId: string;

@Column({
unique: 'stationId_connectorId',
allowNull: false,
type: DataType.INTEGER,
})
declare connectorId: number;

@Column(DataType.ENUM(...Object.values(OCPP1_6.StatusNotificationRequestStatus)))
declare status: OCPP1_6.StatusNotificationRequestStatus;

@Column(DataType.ENUM(...Object.values(OCPP1_6.StatusNotificationRequestErrorCode)))
declare errorCode: OCPP1_6.StatusNotificationRequestErrorCode;

@Column({
type: DataType.DATE,
get() {
return this.getDataValue('timestamp').toISOString();
},
})
declare timestamp: string;

@Column(DataType.STRING)
declare info?: string | null;

@Column(DataType.STRING)
declare vendorId?: string | null;

@Column(DataType.STRING)
declare vendorErrorCode?: string | null;
}
3 changes: 2 additions & 1 deletion 01_Data/src/layers/sequelize/model/Location/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export { ChargingStation } from './ChargingStation';
export { ChargingStationNetworkProfile } from './ChargingStationNetworkProfile';
export { StatusNotification } from './StatusNotification';
export { ServerNetworkProfile } from './ServerNetworkProfile'
export { SetNetworkProfile } from './SetNetworkProfile';
export { SetNetworkProfile } from './SetNetworkProfile';
export { Connector } from './Connector';
57 changes: 48 additions & 9 deletions 01_Data/src/layers/sequelize/repository/Location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
//
// SPDX-License-Identifier: Apache 2.0

import { CrudRepository, OCPP2_0_1, SystemConfig } from '@citrineos/base';
import { CrudRepository, SystemConfig } from '@citrineos/base';
import { Sequelize } from 'sequelize-typescript';
import { ILogObj, Logger } from 'tslog';
import { ChargingStation, Location, SequelizeRepository } from '..';
import { ChargingStation, Connector, Location, SequelizeRepository } from '..';
import { type ILocationRepository } from '../../..';
import { StatusNotification } from '../model/Location';
import { Op } from 'sequelize';
Expand All @@ -15,6 +15,7 @@ export class SequelizeLocationRepository extends SequelizeRepository<Location> i
chargingStation: CrudRepository<ChargingStation>;
statusNotification: CrudRepository<StatusNotification>;
latestStatusNotification: CrudRepository<LatestStatusNotification>;
connector: CrudRepository<Connector>;

constructor(
config: SystemConfig,
Expand All @@ -23,11 +24,13 @@ export class SequelizeLocationRepository extends SequelizeRepository<Location> i
chargingStation?: CrudRepository<ChargingStation>,
statusNotification?: CrudRepository<StatusNotification>,
latestStatusNotification?: CrudRepository<LatestStatusNotification>,
connector?: CrudRepository<Connector>,
) {
super(config, Location.MODEL_NAME, logger, sequelizeInstance);
this.chargingStation = chargingStation ? chargingStation : new SequelizeRepository<ChargingStation>(config, ChargingStation.MODEL_NAME, logger, sequelizeInstance);
this.statusNotification = statusNotification ? statusNotification : new SequelizeRepository<StatusNotification>(config, StatusNotification.MODEL_NAME, logger, sequelizeInstance);
this.latestStatusNotification = latestStatusNotification ? latestStatusNotification : new SequelizeRepository<LatestStatusNotification>(config, LatestStatusNotification.MODEL_NAME, logger, sequelizeInstance);
this.connector = connector ? connector : new SequelizeRepository<Connector>(config, Connector.MODEL_NAME, logger, sequelizeInstance);
}

async readLocationById(id: number): Promise<Location | undefined> {
Expand All @@ -49,12 +52,8 @@ export class SequelizeLocationRepository extends SequelizeRepository<Location> i
return await this.chargingStation.existsByKey(stationId);
}

async addStatusNotificationToChargingStation(stationId: string, statusNotification: OCPP2_0_1.StatusNotificationRequest): Promise<void> {
const notification = StatusNotification.build({
stationId,
...statusNotification,
});
const savedStatusNotification = await this.statusNotification.create(notification);
async addStatusNotificationToChargingStation(stationId: string, statusNotification: StatusNotification): Promise<void> {
const savedStatusNotification = await this.statusNotification.create(statusNotification);
try {
await this.updateLatestStatusNotification(stationId, savedStatusNotification);
} catch (e: any) {
Expand All @@ -66,7 +65,9 @@ export class SequelizeLocationRepository extends SequelizeRepository<Location> i
const evseId = statusNotification.evseId;
const connectorId = statusNotification.connectorId;
const statusNotificationId = statusNotification.id;
await this.latestStatusNotification.deleteAllByQuery({
// delete operation doesn't support "include" in query
// so we need to find them at first and then delete
const existingLatestStatusNotifications: LatestStatusNotification[] = await this.latestStatusNotification.readAllByQuery({
where: {
stationId,
},
Expand All @@ -77,9 +78,19 @@ export class SequelizeLocationRepository extends SequelizeRepository<Location> i
evseId,
connectorId,
},
require: true,
},
],
});
const idsToDelete = existingLatestStatusNotifications.map((l) => l.id);
await this.latestStatusNotification.deleteAllByQuery({
where: {
stationId,
id: {
[Op.in]: idsToDelete,
},
},
});
await this.latestStatusNotification.create(
LatestStatusNotification.build({
stationId,
Expand Down Expand Up @@ -172,4 +183,32 @@ export class SequelizeLocationRepository extends SequelizeRepository<Location> i
return await this.chargingStation.create(ChargingStation.build({ ...chargingStation }));
}
}

async createOrUpdateConnector(connector: Connector): Promise<Connector | undefined> {
let result;
await this.s.transaction(async (sequelizeTransaction) => {
const [savedConnector, connectorCreated] = await this.connector.readOrCreateByQuery({
where: {
stationId: connector.stationId,
connectorId: connector.connectorId,
},
defaults: {
...connector,
},
transaction: sequelizeTransaction,
});
if (!connectorCreated) {
const updatedConnectors = await this.connector.updateAllByQuery(connector, {
where: {
id: savedConnector.id,
},
transaction: sequelizeTransaction,
});
result = updatedConnectors.length > 0 ? updatedConnectors[0] : undefined;
} else {
result = savedConnector;
}
});
return result;
}
}
2 changes: 2 additions & 0 deletions 01_Data/src/layers/sequelize/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
ChargingStationSequence,
Component,
CompositeSchedule,
Connector,
EventData,
Evse,
IdToken,
Expand Down Expand Up @@ -132,6 +133,7 @@ export class DefaultSequelizeInstance {
Component,
ComponentVariable,
CompositeSchedule,
Connector,
Evse,
EventData,
IdToken,
Expand Down
46 changes: 44 additions & 2 deletions 03_Modules/Transactions/src/module/StatusNotificationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import {
IDeviceModelRepository,
ILocationRepository,
Variable,
Connector,
StatusNotification,
} from '@citrineos/data';
import { ILogObj, Logger } from 'tslog';
import {
CrudRepository,
OCPP2_0_1
OCPP2_0_1,
OCPP1_6
} from '@citrineos/base';

export class StatusNotificationService {
Expand Down Expand Up @@ -44,9 +47,13 @@ export class StatusNotificationService {
const chargingStation =
await this._locationRepository.readChargingStationByStationId(stationId);
if (chargingStation) {
const statusNotification = StatusNotification.build({
stationId,
...statusNotificationRequest,
});
await this._locationRepository.addStatusNotificationToChargingStation(
stationId,
statusNotificationRequest,
statusNotification,
);
} else {
this._logger.warn(
Expand Down Expand Up @@ -96,4 +103,39 @@ export class StatusNotificationService {
);
}
}

async processOcpp16StatusNotification(
stationId: string,
statusNotificationRequest: OCPP1_6.StatusNotificationRequest,
) {
const chargingStation =
await this._locationRepository.readChargingStationByStationId(stationId);
if (chargingStation) {
const statusNotification = StatusNotification.build({
...statusNotificationRequest,
stationId,
connectorStatus: statusNotificationRequest.status,
})
await this._locationRepository.addStatusNotificationToChargingStation(
stationId,
statusNotification,
);

const connector = {
connectorId: statusNotificationRequest.connectorId,
stationId,
status: statusNotificationRequest.status,
timestamp: statusNotificationRequest.timestamp ? statusNotificationRequest.timestamp : (new Date()).toISOString(),
errorCode: statusNotificationRequest.errorCode,
info: statusNotificationRequest.info,
vendorId: statusNotificationRequest.vendorId,
vendorErrorCode: statusNotificationRequest.vendorErrorCode,
} as Connector;
await this._locationRepository.createOrUpdateConnector(connector);
} else {
this._logger.warn(
`Charging station ${stationId} not found. Status notification cannot be associated with a charging station.`,
);
}
}
}
34 changes: 32 additions & 2 deletions 03_Modules/Transactions/src/module/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
IMessage,
IMessageHandler,
IMessageSender,
OCPP1_6,
OCPP1_6_CallAction,
OCPP2_0_1,
OCPP2_0_1_CallAction,
OcppError,
Expand Down Expand Up @@ -55,6 +57,7 @@ export class TransactionsModule extends AbstractModule {
OCPP2_0_1_CallAction.MeterValues,
OCPP2_0_1_CallAction.StatusNotification,
OCPP2_0_1_CallAction.TransactionEvent,
OCPP1_6_CallAction.StatusNotification,
];
_responses: CallAction[] = [
OCPP2_0_1_CallAction.CostUpdated,
Expand Down Expand Up @@ -247,7 +250,7 @@ export class TransactionsModule extends AbstractModule {
}

/**
* Handle requests
* Handle OCPP 2.0.1 requests
*/

@AsHandler(OCPPVersion.OCPP2_0_1, OCPP2_0_1_CallAction.TransactionEvent)
Expand Down Expand Up @@ -484,7 +487,7 @@ export class TransactionsModule extends AbstractModule {
}

/**
* Handle responses
* Handle OCPP 2.0.1 responses
*/

@AsHandler(OCPPVersion.OCPP2_0_1, OCPP2_0_1_CallAction.CostUpdated)
Expand All @@ -506,4 +509,31 @@ export class TransactionsModule extends AbstractModule {
props,
);
}

/**
* Handle OCPP 1.6 requests
*/
@AsHandler(OCPPVersion.OCPP1_6, OCPP1_6_CallAction.StatusNotification)
protected async _handleOcpp16StatusNotification(
message: IMessage<OCPP1_6.StatusNotificationRequest>,
props?: HandlerProperties,
): Promise<void> {
this._logger.debug('StatusNotification request received:', message, props);

await this._statusNotificationService.processOcpp16StatusNotification(
message.context.stationId,
message.payload,
);

// Create response
const response: OCPP1_6.StatusNotificationResponse = {};
const messageConfirmation = await this.sendCallResultWithMessage(
message,
response,
);
this._logger.debug(
'StatusNotification response sent: ',
messageConfirmation,
);
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {
Component,
IDeviceModelRepository,
ILocationRepository,
ILocationRepository, StatusNotification,
} from '@citrineos/data';
import { CrudRepository } from '@citrineos/base';
import { StatusNotificationService } from '../../src/module/StatusNotificationService';
import { aStatusNotificationRequest } from '../providers/StatusNotification';
import { aStatusNotification, aStatusNotificationRequest } from '../providers/StatusNotification';
import {
aChargingStation,
aComponent,
Expand Down Expand Up @@ -45,6 +45,9 @@ describe('StatusNotificationService', () => {
locationRepository.readChargingStationByStationId.mockResolvedValue(
aChargingStation(),
);
jest.spyOn(StatusNotification, 'build').mockImplementation(() => {
return aStatusNotification();
});

await statusNotificationService.processStatusNotification(
MOCK_STATION_ID,
Expand Down
Loading

0 comments on commit baed9d0

Please sign in to comment.