Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter

- CitrineOS core extracted (CSMS OCPP 2.0.1)
- OpenOCPP extracted (firmware OCPP 1.6J/2.0.1)
- ShapeShifter library installed (pip install -e)
- ShapeShifter specification extracted
- EVerest extracted

TODO updated with progress
This commit is contained in:
Eric F
2026-06-08 00:38:27 -04:00
parent 468cfeaa50
commit d398a6ced2
7326 changed files with 1177561 additions and 7 deletions

View File

@@ -0,0 +1,88 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { Transaction as SequelizeTransaction } from 'sequelize';
export { SequelizeTransaction };
export * as sequelize from './layers/sequelize/index.js';
export * from './interfaces/index.js';
export * from 'sequelize-typescript';
export type { PaginatedParams } from './layers/sequelize/index.js';
export {
Authorization,
Boot,
ChangeConfiguration,
ChargingNeeds,
ChargingProfile,
ChargingSchedule,
ChargingStation,
ChargingStationSequence,
Component,
Connector,
DefaultSequelizeInstance,
Evse,
Location,
MeterValue,
OCPPMessage,
Tariff,
StartTransaction,
StopTransaction,
Transaction,
Reservation,
Subscription,
EvseType,
Variable,
VariableAttribute,
VariableCharacteristics,
VariableStatus,
Certificate,
InstalledCertificate,
InstallCertificateAttempt,
DeleteCertificateAttempt,
CountryNameEnumType,
TransactionEvent,
LocalListAuthorization,
LocalListVersion,
SendLocalList,
ServerNetworkProfile,
SetNetworkProfile,
StatusNotification,
ChargingStationSecurityInfo,
ChargingStationNetworkProfile,
Tenant,
TenantPartner,
AsyncJobStatus,
AsyncJobStatusDTO,
AsyncJobRequest,
SignatureAlgorithmEnumType,
SequelizeAuthorizationRepository,
SequelizeBootRepository,
SequelizeOCPPMessageRepository,
SequelizeCertificateRepository,
SequelizeInstalledCertificateRepository,
SequelizeInstallCertificateAttemptRepository,
SequelizeDeleteCertificateAttemptRepository,
SequelizeChangeConfigurationRepository,
SequelizeChargingProfileRepository,
SequelizeChargingStationSecurityInfoRepository,
SequelizeDeviceModelRepository,
SequelizeLocationRepository,
SequelizeMessageInfoRepository,
SequelizeRepository,
SequelizeReservationRepository,
SequelizeSecurityEventRepository,
SequelizeSubscriptionRepository,
SequelizeTariffRepository,
SequelizeTransactionEventRepository,
SequelizeVariableMonitoringRepository,
SequelizeChargingStationSequenceRepository,
SequelizeTenantRepository,
SequelizeAsyncJobStatusRepository,
SequelizeServerNetworkProfileRepository,
OCPP2_0_1_Mapper,
OCPP1_6_Mapper,
} from './layers/sequelize/index.js'; // TODO ensure all needed modules are properly exported
export { RepositoryStore } from './layers/sequelize/repository/RepositoryStore.js';
export { DefaultDrizzleInstance } from './layers/drizzle/index.js';
export { CryptoUtils } from './util/CryptoUtils.js';

View File

@@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import {
CountryNameEnumType,
SignatureAlgorithmEnumType,
} from '../../layers/sequelize/model/Certificate/index.js';
export class GenerateCertificateChainRequest {
// Fields for generating a certificate
// Refer to 1.4.1. Certificate Properties in OCPP 2.0.1 Part 2
selfSigned: boolean;
organizationName: string;
commonName: string;
keyLength?: number;
validBefore?: string;
countryName?: CountryNameEnumType;
signatureAlgorithm?: SignatureAlgorithmEnumType;
pathLen?: number;
// The file path to store the generated certificate.
filePath?: string;
constructor(
selfSigned: boolean,
organizationName: string,
commonName: string,
keyLength?: number,
validBefore?: string,
countryName?: CountryNameEnumType,
signatureAlgorithm?: SignatureAlgorithmEnumType,
pathLen?: number,
filePath?: string,
) {
this.selfSigned = selfSigned;
this.organizationName = organizationName;
this.commonName = commonName;
this.keyLength = keyLength;
this.validBefore = validBefore;
this.countryName = countryName;
this.signatureAlgorithm = signatureAlgorithm;
this.pathLen = pathLen;
this.filePath = filePath;
}
}

View File

@@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { OCPP2_0_1 } from '@citrineos/base';
export class InstallRootCertificateRequest {
// Fields for InstallCertificate message request
ocppConnectionName: string;
certificateType: OCPP2_0_1.InstallCertificateUseEnumType;
tenantId: number;
callbackUrl?: string;
// The file id of the root CA certificate. If not provided, it uses one from the external CA Server
// according to the certificate type, e.g., lets encrypt, hubject.
fileId?: string;
constructor(
ocppConnectionName: string,
tenantId: number,
certificateType: OCPP2_0_1.InstallCertificateUseEnumType,
callbackUrl?: string,
fileId?: string,
) {
this.ocppConnectionName = ocppConnectionName;
this.tenantId = tenantId;
this.certificateType = certificateType;
this.callbackUrl = callbackUrl;
this.fileId = fileId;
}
}

View File

@@ -0,0 +1,7 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
export class RegenerateExistingCertificate {
installedCertificateId!: number;
validBefore?: string;
}

View File

@@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { OCPP2_0_1 } from '@citrineos/base';
export class UploadExistingCertificate {
certificate!: string;
certificateType!: OCPP2_0_1.GetCertificateIdUseEnumType;
filePath?: string;
}

View File

@@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
export * from './repositories.js';
// Data endpoints query models
export { AuthorizationQuerySchema } from './queries/Authorization.js';
export type { AuthorizationQuerystring } from './queries/Authorization.js';
export { ChargingStationKeyQuerySchema } from './queries/ChargingStation.js';
export type { ChargingStationKeyQuerystring } from './queries/ChargingStation.js';
export { ConnectionDeleteQuerySchema } from './queries/Connection.js';
export type { ConnectionDeleteQuerystring } from './queries/Connection.js';
export { ModelKeyQuerystringSchema } from './queries/Model.js';
export type { ModelKeyQuerystring } from './queries/Model.js';
export {
NetworkProfileDeleteQuerySchema,
NetworkProfileQuerySchema,
} from './queries/NetworkProfile.js';
export type {
NetworkProfileDeleteQuerystring,
NetworkProfileQuerystring,
} from './queries/NetworkProfile.js';
export {
GenerateCertificateChainSchema,
InstallRootCertificateSchema,
RegenerateInstalledCertificateSchema,
UploadExistingCertificateSchema,
} from './queries/RootCertificate.js';
export { CreateSubscriptionSchema } from './queries/Subscription.js';
export { TariffQuerySchema } from './queries/Tariff.js';
export type { TariffQueryString } from './queries/Tariff.js';
export { CreateTenantQuerySchema, TenantQuerySchema } from './queries/Tenant.js';
export type { TenantQueryString } from './queries/Tenant.js';
export { TlsReloadQuerySchema } from './queries/TlsReload.js';
export type { TlsReloadQueryString } from './queries/TlsReload.js';
export { TransactionEventQuerySchema } from './queries/TransactionEvent.js';
export type { TransactionEventQuerystring } from './queries/TransactionEvent.js';
export { UpdateChargingStationPasswordQuerySchema } from './queries/UpdateChargingStationPasswordQuery.js';
export type { UpdateChargingStationPasswordQueryString } from './queries/UpdateChargingStationPasswordQuery.js';
export {
CreateOrUpdateVariableAttributeQuerySchema,
VariableAttributeQuerySchema,
} from './queries/VariableAttribute.js';
export type {
CreateOrUpdateVariableAttributeQuerystring,
VariableAttributeQuerystring,
} from './queries/VariableAttribute.js';
export {
WebsocketDeleteQuerySchema,
WebsocketGetQuerySchema,
WebsocketMappingQuerySchema,
WebsocketRequestSchema,
} from './queries/Websocket.js';
export type {
WebsocketDeleteQuerystring,
WebsocketGetQuerystring,
WebsocketMappingQuerystring,
} from './queries/Websocket.js';
// Data projection models
export type { AuthorizationRestrictions } from './projections/AuthorizationRestrictions.js';
export { default as AuthorizationRestrictionsSchema } from './projections/schemas/AuthorizationRestrictionsSchema.json' with { type: 'json' };
export { default as TariffSchema } from './projections/schemas/TariffSchema.json' with { type: 'json' };
// Date endpoints DTOs
export { GenerateCertificateChainRequest } from './dtos/GenerateCertificateChainRequest.js';
export { InstallRootCertificateRequest } from './dtos/InstallRootCertificateRequest.js';
export { RegenerateExistingCertificate } from './dtos/RegenerateExistingCertificate.js';
export { UploadExistingCertificate } from './dtos/UploadExistingCertificate.js';

View File

@@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
export interface AuthorizationRestrictions {
/**
* If present, connector types this authorization profile is permitted to charge at.
* SHALL use options in {@link ConnectorEnumType} if applicable, plus "cGBT, cChaoJi,
* OppCharge" as mentioned in information model, or a custom option if nothing else
* fits.
*/
allowedConnectorTypes?: string[];
/**
* If present, this list will be used to prevent charging at evses which match one of
* its strings. EvseId is as defined in Part 2 - Appendices of OCPP 2.0.1, which
* references the ISO 15118/IEC 63119-2 format. Strings in this list are treated as
* prefixes for matching purposes to allow hierarchical id semantics to exclude entire
* stations with one entry, i.e. "US\*A23\*E00235" will match "US\*A23\*E00235\*1" and
* "US\*A23\*E00235\*2", which could represent Evse 1 and 2 at the same station.
*/
disallowedEvseIdPrefixes?: string[];
}

View File

@@ -0,0 +1,20 @@
{
"$id": "AuthorizationRestrictionsSchema",
"type": "object",
"properties": {
"allowedConnectorTypes": {
"type": "array",
"items": {
"type": "string"
}
},
"disallowedEvseIdPrefixes": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [],
"additionalProperties": true
}

View File

@@ -0,0 +1,76 @@
{
"$id": "TariffSchema",
"type": "object",
"properties": {
"id": {
"type": "number"
},
"currency": {
"type": "string",
"minLength": 3,
"maxLength": 3
},
"pricePerKwh": {
"type": "number"
},
"pricePerMin": {
"type": "number"
},
"pricePerSession": {
"type": "number"
},
"paymentFee": {
"type": "number"
},
"authorizationAmount": {
"type": "number"
},
"taxRate": {
"type": "number"
},
"tariffAltText": {
"type": "array",
"items": {
"type": "object"
}
},
"tariffId": {
"type": "string"
},
"validFrom": {
"type": "string",
"format": "date-time"
},
"description": {
"type": "array",
"items": {
"type": "object"
}
},
"energy": {
"type": "object"
},
"chargingTime": {
"type": "object"
},
"idleTime": {
"type": "object"
},
"fixedFee": {
"type": "object"
},
"reservationTime": {
"type": "object"
},
"reservationFixed": {
"type": "object"
},
"minCost": {
"type": "object"
},
"maxCost": {
"type": "object"
}
},
"required": ["currency", "pricePerKwh"]
}

View File

@@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { QuerySchema } from '@citrineos/base';
export interface AuthorizationQuerystring {
idToken: string;
type?: string | null | undefined;
}
export const AuthorizationQuerySchema = QuerySchema('AuthorizationQuerySchema', [
{
key: 'idToken',
type: 'string',
required: true,
},
{
key: 'type',
type: 'string',
required: false,
},
]);

View File

@@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { DEFAULT_TENANT_ID, QuerySchema } from '@citrineos/base';
export interface ChargingStationKeyQuerystring {
ocppConnectionName: string;
tenantId: number;
}
export const ChargingStationKeyQuerySchema = QuerySchema('ChargingStationKeyQuerySchema', [
{
key: 'ocppConnectionName',
type: 'string',
required: true,
},
{
key: 'tenantId',
type: 'number',
required: true,
defaultValue: String(DEFAULT_TENANT_ID),
},
]);

View File

@@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { QuerySchema } from '@citrineos/base';
export interface ConnectionDeleteQuerystring {
ocppConnectionName: string;
tenantId: number;
}
export const ConnectionDeleteQuerySchema = QuerySchema('ConnectionDeleteQuerySchema', [
{
key: 'ocppConnectionName',
type: 'string',
required: true,
},
{
key: 'tenantId',
type: 'number',
required: true,
},
]);

View File

@@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { QuerySchema, DEFAULT_TENANT_ID } from '@citrineos/base';
export interface ModelKeyQuerystring {
id: number;
tenantId: number;
}
export const ModelKeyQuerystringSchema = QuerySchema('ModelKeyQuerystringSchema', [
{
key: 'id',
type: 'number',
required: true,
},
{
key: 'tenantId',
type: 'number',
required: true,
defaultValue: String(DEFAULT_TENANT_ID),
},
]);

View File

@@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { DEFAULT_TENANT_ID, QuerySchema } from '@citrineos/base';
export interface NetworkProfileQuerystring {
ocppConnectionName: string;
tenantId: number;
}
export const NetworkProfileQuerySchema = QuerySchema('NetworkProfileQuerySchema', [
{
key: 'ocppConnectionName',
type: 'string',
required: true,
},
{
key: 'tenantId',
type: 'number',
required: true,
defaultValue: String(DEFAULT_TENANT_ID),
},
]);
export interface NetworkProfileDeleteQuerystring {
ocppConnectionName: string;
configurationSlot: number[];
tenantId: number;
}
export const NetworkProfileDeleteQuerySchema = QuerySchema('NetworkProfileDeleteQuerySchema', [
{
key: 'ocppConnectionName',
type: 'string',
required: true,
},
{
key: 'configurationSlot',
type: 'number[]',
required: true,
},
{
key: 'tenantId',
type: 'number',
required: true,
defaultValue: String(DEFAULT_TENANT_ID),
},
]);

View File

@@ -0,0 +1,107 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { DEFAULT_TENANT_ID, QuerySchema } from '@citrineos/base';
export const GenerateCertificateChainSchema = QuerySchema('GenerateCertificateChainSchema', [
{
key: 'commonName',
type: 'string',
required: true,
},
{
key: 'organizationName',
type: 'string',
required: true,
},
{
key: 'selfSigned',
type: 'boolean',
required: true,
},
{
key: 'countryName',
type: 'string',
},
{
key: 'filePath',
type: 'string',
},
{
key: 'keyLength',
type: 'number',
},
{
key: 'pathLen',
type: 'number',
},
{
key: 'signatureAlgorithm',
type: 'string',
},
{
key: 'validBefore',
type: 'string',
},
]);
export const InstallRootCertificateSchema = QuerySchema('InstallRootCertificateSchema', [
{
key: 'certificateType',
type: 'string',
required: true,
},
{
key: 'ocppConnectionName',
type: 'string',
required: true,
},
{
key: 'tenantId',
type: 'number',
required: true,
defaultValue: String(DEFAULT_TENANT_ID),
},
{
key: 'callbackUrl',
type: 'string',
},
{
key: 'fileId',
type: 'string',
},
]);
export const UploadExistingCertificateSchema = QuerySchema('UploadExistingCertificateSchema', [
{
key: 'certificate',
type: 'string',
required: true,
},
{
key: 'certificateType',
type: 'string',
required: true,
},
{
key: 'filePath',
type: 'string',
},
]);
export const RegenerateInstalledCertificateSchema = QuerySchema(
'RegenerateInstalledCertificateSchema',
[
{
key: 'installedCertificateId',
type: 'number',
required: true,
},
{
key: 'validBefore',
type: 'string',
required: false,
},
],
);

View File

@@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { QuerySchema } from '@citrineos/base';
export const CreateSubscriptionSchema = QuerySchema('CreateSubscriptionSchema', [
{
key: 'ocppConnectionName',
type: 'string',
required: true,
},
{
key: 'url',
type: 'string',
required: true,
},
{
key: 'messageRegexFilter',
type: 'string',
},
{
key: 'onClose',
type: 'boolean',
},
{
key: 'onConnect',
type: 'boolean',
},
{
key: 'onMessage',
type: 'boolean',
},
{
key: 'sentMessage',
type: 'boolean',
},
]);

View File

@@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { DEFAULT_TENANT_ID, QuerySchema } from '@citrineos/base';
export const TariffQuerySchema = QuerySchema('TariffQuerySchema', [
{
key: 'tenantId',
type: 'number',
required: true,
defaultValue: String(DEFAULT_TENANT_ID),
},
{
key: 'id',
type: 'string',
},
]);
export interface TariffQueryString {
tenantId: number;
id?: string;
}

View File

@@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { DEFAULT_TENANT_ID, QuerySchema } from '@citrineos/base';
export const TenantQuerySchema = QuerySchema('TenantQuerySchema', [
{
key: 'tenantId',
type: 'number',
required: true,
defaultValue: String(DEFAULT_TENANT_ID),
},
]);
export interface TenantQueryString {
tenantId: number;
}
export const CreateTenantQuerySchema = QuerySchema('CreateTenantQuerySchema', [
{
key: 'name',
type: 'string',
required: true,
},
{
key: 'isUserTenant',
type: 'boolean',
},
{
key: 'maxChargingStations',
type: 'number',
},
{
key: 'url',
type: 'string',
},
{
key: 'websocketServerConfig',
type: 'object',
},
{
key: 'websocketServerId',
type: 'string',
},
{
key: 'tenantPath',
type: 'string',
pattern: '^[a-zA-Z0-9_-]+$',
},
]);

View File

@@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { QuerySchema } from '@citrineos/base';
export const TlsReloadQuerySchema = QuerySchema('TlsReloadQuerySchema', [
{
key: 'serverId',
type: 'string',
required: true,
},
]);
export interface TlsReloadQueryString {
serverId: string;
}

View File

@@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { DEFAULT_TENANT_ID, QuerySchema } from '@citrineos/base';
export interface TransactionEventQuerystring {
ocppConnectionName: string;
transactionId: string;
tenantId: number;
}
export const TransactionEventQuerySchema = QuerySchema('TransactionEventQuerySchema', [
{
key: 'ocppConnectionName',
type: 'string',
required: true,
},
{
key: 'transactionId',
type: 'string',
required: true,
},
{
key: 'tenantId',
type: 'number',
required: true,
defaultValue: String(DEFAULT_TENANT_ID),
},
]);

View File

@@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { DEFAULT_TENANT_ID, QuerySchema } from '@citrineos/base';
export const UpdateChargingStationPasswordQuerySchema = QuerySchema(
'UpdateChargingStationPasswordQuerySchema',
[
{
key: 'tenantId',
type: 'number',
required: true,
defaultValue: String(DEFAULT_TENANT_ID),
},
{
key: 'callbackUrl',
type: 'string',
},
],
);
export interface UpdateChargingStationPasswordQueryString {
tenantId: number;
callbackUrl?: string;
}

View File

@@ -0,0 +1,101 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import {
DEFAULT_TENANT_ID,
QuerySchema,
type AttributeEnumType,
type SetVariableStatusEnumType,
} from '@citrineos/base';
export interface VariableAttributeQuerystring {
ocppConnectionName: string;
tenantId: number;
type?: AttributeEnumType;
value?: string;
status?: SetVariableStatusEnumType;
component_evse_id?: number;
component_evse_connectorId?: number | null;
component_name?: string;
component_instance?: string | null;
variable_name?: string;
variable_instance?: string | null;
}
export const VariableAttributeQuerySchema = QuerySchema('VariableAttributeQuerySchema', [
{
key: 'ocppConnectionName',
type: 'string',
required: true,
},
{
key: 'tenantId',
type: 'number',
required: true,
defaultValue: String(DEFAULT_TENANT_ID),
},
{
key: 'type',
type: 'string',
},
{
key: 'value',
type: 'string',
},
{
key: 'status',
type: 'string',
},
{
key: 'component_evse_id',
type: 'number',
},
{
key: 'component_evse_connectorId',
type: 'number',
},
{
key: 'component_name',
type: 'string',
},
{
key: 'component_instance',
type: 'string',
},
{
key: 'variable_name',
type: 'string',
},
{
key: 'variable_instance',
type: 'string',
},
]);
export interface CreateOrUpdateVariableAttributeQuerystring {
tenantId: number;
ocppConnectionName: string;
setOnCharger?: boolean; // Used to indicate value has already been accepted by the station via means other than ocpp
}
export const CreateOrUpdateVariableAttributeQuerySchema = QuerySchema(
'CreateOrUpdateVariableAttributeQuerySchema',
[
{
key: 'tenantId',
type: 'number',
required: true,
defaultValue: String(DEFAULT_TENANT_ID),
},
{
key: 'ocppConnectionName',
type: 'string',
required: true,
},
{
key: 'setOnCharger',
type: 'boolean',
},
],
);

View File

@@ -0,0 +1,124 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { QuerySchema } from '@citrineos/base';
export interface WebsocketGetQuerystring {
id?: string;
tenantId?: number;
}
export const WebsocketGetQuerySchema = QuerySchema('WebsocketGetQuerySchema', [
{
key: 'id',
type: 'string',
},
{
key: 'tenantId',
type: 'string',
},
]);
export interface WebsocketDeleteQuerystring {
id: string;
}
export const WebsocketDeleteQuerySchema = QuerySchema('WebsocketDeleteQuerySchema', [
{
key: 'id',
type: 'string',
required: true,
},
]);
export const WebsocketRequestSchema = QuerySchema('WebsocketRequestSchema', [
{
key: 'id',
type: 'string',
required: true,
},
{
key: 'host',
type: 'string',
required: true,
},
{
key: 'port',
type: 'number',
required: true,
},
{
key: 'pingInterval',
type: 'number',
required: true,
},
{
key: 'protocol',
type: 'string',
required: true,
},
{
key: 'securityProfile',
type: 'number',
required: true,
},
{
key: 'allowUnknownChargingStations',
type: 'boolean',
required: true,
},
{
key: 'tlsKeyFilePath',
type: 'string',
},
{
key: 'tlsCertificateChainFilePath',
type: 'string',
},
{
key: 'mtlsCertificateAuthorityKeyFilePath',
type: 'string',
},
{
key: 'rootCACertificateFilePath',
type: 'string',
},
{
key: 'tenantId',
type: 'number',
required: true,
},
{
key: 'tenantPathMapping',
type: 'object',
},
{
key: 'dynamicTenantResolution',
type: 'boolean',
},
]);
export interface WebsocketMappingQuerystring {
id: string;
path: string;
tenantId: number;
}
export const WebsocketMappingQuerySchema = QuerySchema('WebsocketMappingQuerySchema', [
{
key: 'id',
type: 'string',
required: true,
},
{
key: 'path',
type: 'string',
required: true,
},
{
key: 'tenantId',
type: 'number',
required: true,
},
]);

View File

@@ -0,0 +1,546 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type {
BootConfig,
CallAction,
ChargingLimitSourceEnumType,
ChargingProfilePurposeEnumType,
ChargingStateEnumType,
ChargingStationSequenceTypeEnumType,
CrudRepository,
MeterValueDto,
OCPP1_6,
OCPP2_common_types,
OCPP2_request_types,
OCPPMessageDto,
OCPPVersion,
RegistrationStatusEnumType,
SecurityEventDto,
UpdateEnumType,
} from '@citrineos/base';
import type {
ChargingProfileInput,
CompositeScheduleInput,
} from '../layers/sequelize/mapper/2.0.1/ChargingProfileMapper.js';
import type { Authorization } from '../layers/sequelize/model/Authorization/Authorization.js';
import type { LocalListVersion } from '../layers/sequelize/model/Authorization/LocalListVersion.js';
import type { SendLocalList } from '../layers/sequelize/model/Authorization/SendLocalList.js';
import type { Boot } from '../layers/sequelize/model/Boot.js';
import type { Certificate } from '../layers/sequelize/model/Certificate/Certificate.js';
import type {
DeleteCertificateAttempt,
InstallCertificateAttempt,
InstalledCertificate,
} from '../layers/sequelize/model/Certificate/index.js';
import type { ChangeConfiguration } from '../layers/sequelize/model/ChangeConfiguration.js';
import type {
ChargingNeeds,
ChargingProfile,
CompositeSchedule,
} from '../layers/sequelize/model/ChargingProfile/index.js';
import type { ChargingStationSecurityInfo } from '../layers/sequelize/model/ChargingStationSecurityInfo.js';
import type { ChargingStationSequence } from '../layers/sequelize/model/ChargingStationSequence/ChargingStationSequence.js';
import type { Component } from '../layers/sequelize/model/DeviceModel/Component.js';
import type { EvseType } from '../layers/sequelize/model/DeviceModel/EvseType.js';
import type { Variable } from '../layers/sequelize/model/DeviceModel/Variable.js';
import type { VariableAttribute } from '../layers/sequelize/model/DeviceModel/VariableAttribute.js';
import type { VariableCharacteristics } from '../layers/sequelize/model/DeviceModel/VariableCharacteristics.js';
import type { ChargingStation } from '../layers/sequelize/model/Location/ChargingStation.js';
import type { Connector } from '../layers/sequelize/model/Location/Connector.js';
import type { Evse } from '../layers/sequelize/model/Location/Evse.js';
import type { Location } from '../layers/sequelize/model/Location/Location.js';
import type { ServerNetworkProfile } from '../layers/sequelize/model/Location/ServerNetworkProfile.js';
import type { StatusNotification } from '../layers/sequelize/model/Location/StatusNotification.js';
import type { MessageInfo } from '../layers/sequelize/model/MessageInfo/MessageInfo.js';
import type { OCPPMessage } from '../layers/sequelize/model/OCPPMessage.js';
import type { Reservation } from '../layers/sequelize/model/Reservation.js';
import type { Subscription } from '../layers/sequelize/model/Subscription/Subscription.js';
import type { Tariff } from '../layers/sequelize/model/Tariff/Tariffs.js';
import type { Tenant } from '../layers/sequelize/model/Tenant.js';
import type {
MeterValue,
StopTransaction,
Transaction,
} from '../layers/sequelize/model/TransactionEvent/index.js';
import type { TransactionEvent } from '../layers/sequelize/model/TransactionEvent/TransactionEvent.js';
import type {
EventData,
VariableMonitoring,
} from '../layers/sequelize/model/VariableMonitoring/index.js';
import type { AuthorizationQuerystring } from './queries/Authorization.js';
import type { TariffQueryString } from './queries/Tariff.js';
import type { VariableAttributeQuerystring } from './queries/VariableAttribute.js';
export interface IAuthorizationRepository extends CrudRepository<Authorization> {
readAllByQuerystring: (
tenantId: number,
query: AuthorizationQuerystring,
) => Promise<Authorization[]>;
readOnlyOneByQuerystring: (
tenantId: number,
query: AuthorizationQuerystring,
) => Promise<Authorization | undefined>;
}
/**
* Key is StationId
*/
export interface IBootRepository extends CrudRepository<BootConfig> {
createOrUpdateByKey: (
tenantId: number,
value: BootConfig,
key: string,
) => Promise<Boot | undefined>;
updateStatusByKey: (
tenantId: number,
status: RegistrationStatusEnumType,
statusInfo: OCPP2_common_types.StatusInfoType | undefined,
key: string,
) => Promise<Boot | undefined>;
updateLastBootTimeByKey: (
tenantId: number,
lastBootTime: string,
key: string,
) => Promise<Boot | undefined>;
readByKey: (tenantId: number, key: string) => Promise<Boot | undefined>;
existsByKey: (tenantId: number, key: string) => Promise<boolean>;
deleteByKey: (tenantId: number, key: string) => Promise<Boot | undefined>;
}
export interface IDeviceModelRepository
extends CrudRepository<OCPP2_common_types.VariableAttributeType> {
createOrUpdateDeviceModelByStationId(
tenantId: number,
value: OCPP2_common_types.ReportDataType,
ocppConnectionName: string,
isoTimestamp: string,
): Promise<VariableAttribute[]>;
createOrUpdateByGetVariablesResultAndStationId(
tenantId: number,
getVariablesResult: OCPP2_common_types.GetVariableResultType[],
ocppConnectionName: string,
isoTimestamp: string,
): Promise<VariableAttribute[]>;
createOrUpdateBySetVariablesDataAndStationId(
tenantId: number,
setVariablesData: OCPP2_common_types.SetVariableDataType[],
ocppConnectionName: string,
isoTimestamp: string,
): Promise<VariableAttribute[]>;
updateResultByStationId(
tenantId: number,
result: OCPP2_common_types.SetVariableResultType,
ocppConnectionName: string,
isoTimestamp: string,
existingVariableAttribute?: VariableAttribute,
): Promise<VariableAttribute | undefined>;
readAllSetVariableByStationId(
tenantId: number,
ocppConnectionName: string,
): Promise<OCPP2_common_types.SetVariableDataType[]>;
readAllByQuerystring(
tenantId: number,
query: VariableAttributeQuerystring,
): Promise<VariableAttribute[]>;
existByQuerystring(tenantId: number, query: VariableAttributeQuerystring): Promise<number>;
deleteAllByQuerystring(
tenantId: number,
query: VariableAttributeQuerystring,
): Promise<VariableAttribute[]>;
findComponentAndVariable(
tenantId: number,
componentType: OCPP2_common_types.ComponentType,
variableType: OCPP2_common_types.VariableType,
): Promise<[Component | undefined, Variable | undefined]>;
findOrCreateEvseAndComponentAndVariable(
tenantId: number,
componentType: OCPP2_common_types.ComponentType,
variableType: OCPP2_common_types.VariableType,
): Promise<[Component, Variable]>;
findOrCreateEvseAndComponent(
tenantId: number,
componentType: OCPP2_common_types.ComponentType,
ocppConnectionName: string,
): Promise<Component>;
findEvseByIdAndConnectorId(
tenantId: number,
id: number,
connectorId: number | null,
): Promise<EvseType | undefined>;
findVariableCharacteristicsByVariableNameAndVariableInstance(
tenantId: number,
variableName: string,
variableInstance: string | null,
): Promise<VariableCharacteristics | undefined>;
}
export interface ILocalAuthListRepository extends CrudRepository<LocalListVersion> {
/**
* Creates a SendLocalList.
* @param {number} tenantId - The tenant ID.
* @param ocppConnectionName - The connection name of the charging 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.
* @return {SendLocalList} The database object. Contains the correlationId to be used for the sendLocalListRequest.
*/
createSendLocalListFromRequestData(
tenantId: number,
ocppConnectionName: string,
correlationId: string,
updateType: UpdateEnumType,
versionNumber: number,
localAuthorizationList?: OCPP2_common_types.AuthorizationData[],
): Promise<SendLocalList>;
/**
* Used to process GetLocalListVersionResponse, if version is unknown it will create or update LocalListVersion with the new version and an empty localAuthorizationList.
* @param tenantId
* @param versionNumber
* @param ocppConnectionName - The connection name of the charging station
*/
validateOrReplaceLocalListVersionForStation(
tenantId: number,
versionNumber: number,
ocppConnectionName: string,
): Promise<void>;
getSendLocalListRequestByStationIdAndCorrelationId(
tenantId: number,
ocppConnectionName: string,
correlationId: string,
): Promise<SendLocalList | undefined>;
/**
* Used to process SendLocalListResponse.
* @param tenantId
* @param ocppConnectionName - The connection name of the charging station
* @param {SendLocalList} sendLocalList - The SendLocalList object created from the associated SendLocalListRequest.
* @returns {LocalListVersion} LocalListVersion - The updated LocalListVersion.
*/
createOrUpdateLocalListVersionFromStationIdAndSendLocalList(
tenantId: number,
ocppConnectionName: string,
sendLocalList: SendLocalList,
): Promise<LocalListVersion>;
}
export interface ILocationRepository extends CrudRepository<Location> {
readLocationById: (tenantId: number, id: number) => Promise<Location | undefined>;
readChargingStationByStationId: (
tenantId: number,
ocppConnectionName: string,
) => Promise<ChargingStation | undefined>;
readConnectorByStationIdAndOcpp16ConnectorId: (
tenantId: number,
ocppConnectionName: string,
ocpp16ConnectorId: number,
) => Promise<Connector | undefined>;
readEvseByStationIdAndOcpp201EvseId: (
tenantId: number,
ocppConnectionName: string,
ocpp201EvseId: number,
) => Promise<Evse | undefined>;
readConnectorByStationIdAndOcpp201EvseType: (
tenantId: number,
ocppConnectionName: string,
ocpp201EvseType: OCPP2_common_types.EVSEType,
) => Promise<Connector | undefined>;
setChargingStationIsOnlineAndOCPPVersion: (
tenantId: number,
ocppConnectionName: string,
isOnline: boolean,
ocppVersion: OCPPVersion | null,
) => Promise<ChargingStation | undefined>;
doesChargingStationExistByStationId: (
tenantId: number,
ocppConnectionName: string,
) => Promise<boolean>;
addStatusNotificationToChargingStation(
tenantId: number,
ocppConnectionName: string,
statusNotification: StatusNotification,
): Promise<void>;
createOrUpdateChargingStation(
tenantId: number,
chargingStation: ChargingStation,
): Promise<ChargingStation>;
createOrUpdateConnector(tenantId: number, connector: Connector): Promise<Connector | undefined>;
/**
* Commissions a default evse + evseTypeConnector record for an OCPP 1.6 connector.
* Used in ad-hoc/`allowUnknownChargingStations` flows where the charge point arrives
* uncommissioned (OCPP 1.6 has no native EVSE concept). Conservative default:
* one connector → one evse. Returns the FK ids the caller should stamp on the
* Connector record being upserted.
*/
commissionEvseForOcpp16Connector(
tenantId: number,
ocppConnectionName: string,
connectorId: number,
): Promise<{ evseId: number; evseTypeConnectorId: number }>;
updateAllConnectorsByQuery(
tenantId: number,
value: Partial<Connector>,
query: object,
): Promise<Connector[]>;
updateChargingStationTimestamp(
tenantId: number,
ocppConnectionName: string,
timestamp: string,
): Promise<void>;
}
export interface ISecurityEventRepository {
createByStationId: (
tenantId: number,
value: OCPP2_request_types.SecurityEventNotificationRequest,
ocppConnectionName: string,
) => Promise<SecurityEventDto>;
readByStationIdAndTimestamps: (
tenantId: number,
ocppConnectionName: string,
from?: Date,
to?: Date,
) => Promise<SecurityEventDto[]>;
deleteByKey: (tenantId: number, key: string) => Promise<SecurityEventDto | undefined>;
}
export interface ISubscriptionRepository extends CrudRepository<Subscription> {
create(tenantId: number, value: Subscription): Promise<Subscription>;
readAllByStationId(tenantId: number, ocppConnectionName: string): Promise<Subscription[]>;
deleteByKey(tenantId: number, key: string): Promise<Subscription | undefined>;
}
export interface ITransactionEventRepository extends CrudRepository<TransactionEvent> {
createOrUpdateTransactionByTransactionEventAndStationId(
tenantId: number,
value: OCPP2_request_types.TransactionEventRequest,
ocppConnectionName: string,
): Promise<Transaction>;
createMeterValue(
tenantId: number,
value: OCPP2_common_types.MeterValueType,
transactionDatabaseId?: number | null,
transactionId?: string | null,
tariffId?: number | null,
): Promise<MeterValue>;
createTransactionByStartTransaction(
tenantId: number,
request: OCPP1_6.StartTransactionRequest,
ocppConnectionName: string,
): Promise<Transaction>;
updateTransactionByMeterValues(
tenantId: number,
meterValues: MeterValueDto[],
ocppConnectionName: string,
transactionId: number,
): Promise<void>;
readAllByStationIdAndTransactionId(
tenantId: number,
ocppConnectionName: string,
transactionId: string,
): Promise<TransactionEvent[]>;
readTransactionByStationIdAndTransactionId(
tenantId: number,
ocppConnectionName: string,
transactionId: string,
): Promise<Transaction | undefined>;
readAllTransactionsByStationIdAndEvseAndChargingStates(
tenantId: number,
ocppConnectionName: string,
evse: OCPP2_common_types.EVSEType,
chargingStates?: ChargingStateEnumType[],
): Promise<Transaction[]>;
readAllActiveTransactionsByAuthorizationId(
tenantId: number,
authorizationId: number,
): Promise<Transaction[]>;
readAllMeterValuesByTransactionDataBaseId(
tenantId: number,
transactionDataBaseId: number,
): Promise<MeterValue[]>;
getActiveTransactionByStationIdAndEvseId(
tenantId: number,
ocppConnectionName: string,
evseId: number,
): Promise<Transaction | undefined>;
updateTransactionTotalCostById(tenantId: number, totalCost: number, id: number): Promise<void>;
createStopTransaction(
tenantId: number,
transactionDatabaseId: number,
ocppConnectionName: string,
meterStop: number,
timestamp: Date,
meterValues: MeterValueDto[],
reason?: string,
idTokenDatabaseId?: number,
): Promise<StopTransaction>;
updateTransactionByStationIdAndTransactionId(
tenantId: number,
transaction: Partial<Transaction>,
transactionId: string,
ocppConnectionName: string,
): Promise<Transaction | undefined>;
deactivateActiveTransactionsByStationIdAndEvseId(
tenantId: number,
ocppConnectionName: string,
evseId: number,
excludeTransactionId: string,
): Promise<Transaction[]>;
}
export interface IVariableMonitoringRepository extends CrudRepository<VariableMonitoring> {
createOrUpdateByMonitoringDataTypeAndStationId(
tenantId: number,
value: OCPP2_common_types.MonitoringDataType,
componentId: string,
variableId: string,
ocppConnectionName: string,
): Promise<VariableMonitoring[]>;
createOrUpdateBySetMonitoringDataTypeAndStationId(
tenantId: number,
value: OCPP2_common_types.SetMonitoringDataType,
componentId: string,
variableId: string,
ocppConnectionName: string,
): Promise<VariableMonitoring>;
rejectAllVariableMonitoringsByStationId(
tenantId: number,
action: CallAction,
ocppConnectionName: string,
): Promise<void>;
rejectVariableMonitoringByIdAndStationId(
tenantId: number,
action: CallAction,
id: number,
ocppConnectionName: string,
): Promise<void>;
updateResultByStationId(
tenantId: number,
result: OCPP2_common_types.SetMonitoringResultType,
ocppConnectionName: string,
): Promise<VariableMonitoring>;
createEventDatumByComponentIdAndVariableIdAndStationId(
tenantId: number,
event: OCPP2_common_types.EventDataType,
componentId: string,
variableId: string,
ocppConnectionName: string,
): Promise<EventData>;
}
export interface IMessageInfoRepository extends CrudRepository<MessageInfo> {
deactivateAllByStationId(tenantId: number, ocppConnectionName: string): Promise<void>;
createOrUpdateByMessageInfoTypeAndStationId(
tenantId: number,
value: OCPP2_common_types.MessageInfoType,
ocppConnectionName: string,
componentId?: number,
): Promise<MessageInfo>;
}
export interface ITariffRepository extends CrudRepository<Tariff> {
findByConnectorId(tenantId: number, connectorId: number): Promise<Tariff | undefined>;
readAllByQuerystring(tenantId: number, query: TariffQueryString): Promise<Tariff[]>;
deleteAllByQuerystring(tenantId: number, query: TariffQueryString): Promise<Tariff[]>;
upsertTariff(tenantId: number, tariff: Tariff): Promise<Tariff>;
upsertTariffByTariffId(tenantId: number, tariff: Tariff): Promise<Tariff>;
}
export interface ICertificateRepository extends CrudRepository<Certificate> {
createOrUpdateCertificate(tenantId: number, certificate: Certificate): Promise<Certificate>;
}
export interface IInstalledCertificateRepository extends CrudRepository<InstalledCertificate> {}
export interface IInstallCertificateAttemptRepository
extends CrudRepository<InstallCertificateAttempt> {}
export interface IDeleteCertificateAttemptRepository
extends CrudRepository<DeleteCertificateAttempt> {}
export interface IChargingProfileRepository extends CrudRepository<ChargingProfile> {
createOrUpdateChargingProfile(
tenantId: number,
chargingProfile: ChargingProfileInput,
ocppConnectionName: string,
evseId?: number | null,
chargingLimitSource?: ChargingLimitSourceEnumType,
isActive?: boolean,
): Promise<ChargingProfile>;
createChargingNeeds(
tenantId: number,
chargingNeeds: OCPP2_request_types.NotifyEVChargingNeedsRequest,
ocppConnectionName: string,
): Promise<ChargingNeeds>;
findChargingNeedsByEvseDBIdAndTransactionDBId(
tenantId: number,
evseDBId: number,
transactionDataBaseId: number,
): Promise<ChargingNeeds | undefined>;
createCompositeSchedule(
tenantId: number,
compositeSchedule: CompositeScheduleInput,
ocppConnectionName: string,
): Promise<CompositeSchedule>;
getNextChargingProfileId(tenantId: number, ocppConnectionName: string): Promise<number>;
getNextChargingScheduleId(tenantId: number, ocppConnectionName: string): Promise<number>;
getNextStackLevel(
tenantId: number,
ocppConnectionName: string,
transactionDatabaseId: number | null,
profilePurpose: ChargingProfilePurposeEnumType,
): Promise<number>;
}
export interface IReservationRepository extends CrudRepository<Reservation> {
createOrUpdateReservation(
tenantId: number,
reserveNowRequest: OCPP2_request_types.ReserveNowRequest,
ocppConnectionName: string,
isActive?: boolean,
): Promise<Reservation | undefined>;
}
export interface IOCPPMessageRepository extends CrudRepository<OCPPMessage> {
createOCPPMessage(tenantId: number, message: OCPPMessageDto): Promise<OCPPMessage>;
getRequestByCorrelationId(
tenantId: number,
correlationId: string,
): Promise<OCPPMessage | undefined>;
}
export interface IChargingStationSecurityInfoRepository
extends CrudRepository<ChargingStationSecurityInfo> {
readChargingStationPublicKeyFileId(tenantId: number, ocppConnectionName: string): Promise<string>;
readOrCreateChargingStationInfo(
tenantId: number,
ocppConnectionName: string,
publicKeyFileId: string,
): Promise<void>;
}
export interface IChargingStationSequenceRepository
extends CrudRepository<ChargingStationSequence> {
getNextSequenceValue(
tenantId: number,
ocppConnectionName: string,
type: ChargingStationSequenceTypeEnumType,
): Promise<number>;
}
export interface IServerNetworkProfileRepository extends CrudRepository<ServerNetworkProfile> {
upsertServerNetworkProfile(
websocketServerConfig: any,
maxCallLengthSeconds: number,
): Promise<ServerNetworkProfile>;
}
export interface IChangeConfigurationRepository extends CrudRepository<ChangeConfiguration> {
createOrUpdateChangeConfiguration(
tenantId: number,
configuration: ChangeConfiguration,
): Promise<ChangeConfiguration | undefined>;
}
export interface ITenantRepository extends CrudRepository<Tenant> {
createTenant(tenant: Tenant): Promise<Tenant>;
}

View File

@@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
export { DefaultDrizzleInstance } from './util.js';
export { DrizzleRepository } from './repository/Base.js';
export { DrizzleSecurityEventRepository, toSecurityEventDto } from './repository/SecurityEvent.js';
export {
securityEventTable,
tenantSecurityEventTable,
SecurityEventEntitySchema,
SecurityEventEntityInsertSchema,
type SecurityEventEntity,
type SecurityEventEntityInsert,
// Legacy TypeScript-only types
type SecurityEventSelect,
type SecurityEventInsert,
} from './schema/SecurityEvent.js';

View File

@@ -0,0 +1,167 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type { BootstrapConfig } from '@citrineos/base';
import { and, count, eq, type Column, type InferSelectModel } from 'drizzle-orm';
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
import type { PgTable } from 'drizzle-orm/pg-core';
import EventEmitter from 'events';
import { type ILogObj, Logger } from 'tslog';
import { DefaultDrizzleInstance } from '../util.js';
// Every CitrineOS table shares these two columns — used to implement common
// query patterns (findById, deleteById, etc.) in the base class without casting.
export type CitrineTable = PgTable & {
id: Column;
tenantId: Column;
};
export abstract class DrizzleRepository<TTable extends CitrineTable, TDto> extends EventEmitter {
protected readonly db: NodePgDatabase;
protected readonly logger: Logger<ILogObj>;
// When true, queries target a per-tenant Postgres schema ("tenant_X"."Table")
// and the tenantId column filter is omitted — the schema is the isolation boundary.
protected readonly useTenantSchema: boolean;
constructor(
config: BootstrapConfig,
logger?: Logger<ILogObj>,
db?: NodePgDatabase,
useTenantSchema = false,
) {
super();
this.db = db ?? DefaultDrizzleInstance.getInstance(config, logger);
this.logger = logger
? logger.getSubLogger({ name: this.constructor.name })
: new Logger<ILogObj>({ name: this.constructor.name });
this.useTenantSchema = useTenantSchema;
}
// Subclasses return either the public-schema table (row-level tenancy) or a
// schema-qualified table (schema-per-tenant). Every shared method calls this,
// so tenancy mode is transparent to callers.
protected abstract getTable(tenantId: number): TTable;
// Subclasses map raw DB rows to clean DTO objects — no ORM leakage.
protected abstract toDto(row: InferSelectModel<TTable>): TDto;
// Returns the tenant isolation predicate for WHERE clauses.
// Undefined in schema-per-tenant mode because isolation lives at the schema level.
private tenantFilter(table: TTable, tenantId: number) {
return this.useTenantSchema ? undefined : eq(table.tenantId, tenantId);
}
// ─── Shared read methods ──────────────────────────────────────────────────
async findById(tenantId: number, id: number): Promise<TDto | undefined> {
const table = this.getTable(tenantId);
const filter = this.tenantFilter(table, tenantId);
const where = filter ? and(eq(table.id, id), filter) : eq(table.id, id);
// `as any` on table: Drizzle's from() has internal generic constraints
// (TableLikeHasEmptySelection) that don't resolve for bounded generic PgTables.
// The public return type is fully typed via TDto.
const rows = (await this.db
.select()
.from(table as any)
.where(where)
.limit(1)) as InferSelectModel<TTable>[];
return rows[0] ? this.toDto(rows[0]) : undefined;
}
async findAll(tenantId: number): Promise<TDto[]> {
const table = this.getTable(tenantId);
const filter = this.tenantFilter(table, tenantId);
const rows = (
filter
? await this.db
.select()
.from(table as any)
.where(filter)
: await this.db.select().from(table as any)
) as InferSelectModel<TTable>[];
return rows.map((row) => this.toDto(row));
}
async exists(tenantId: number, id: number): Promise<boolean> {
const table = this.getTable(tenantId);
const filter = this.tenantFilter(table, tenantId);
const where = filter ? and(eq(table.id, id), filter) : eq(table.id, id);
const rows = await this.db
.select({ id: table.id as any })
.from(table as any)
.where(where)
.limit(1);
return rows.length > 0;
}
async countAll(tenantId: number): Promise<number> {
const table = this.getTable(tenantId);
const filter = this.tenantFilter(table, tenantId);
const result = filter
? await this.db
.select({ count: count() })
.from(table as any)
.where(filter)
: await this.db.select({ count: count() }).from(table as any);
return result[0]?.count ?? 0;
}
// ─── Shared write methods (all emit events) ───────────────────────────────
// values is typed as object here because InferInsertModel<TTable> with a generic
// TTable hits TypeScript inference limits. Subclasses expose typed create methods
// (e.g. createByStationId) that call this internally with the correct shape.
protected async insert(tenantId: number, values: object): Promise<TDto> {
const table = this.getTable(tenantId);
const rows = (await (this.db.insert(table as any) as any)
.values({ ...values, tenantId })
.returning()) as InferSelectModel<TTable>[];
const dto = this.toDto(rows[0]);
this.emit('created', [dto]);
return dto;
}
// values is typed as object for the same reason as insert above.
async updateById(tenantId: number, id: number, values: object): Promise<TDto | undefined> {
const table = this.getTable(tenantId);
const filter = this.tenantFilter(table, tenantId);
const where = filter ? and(eq(table.id, id), filter) : eq(table.id, id);
const rows = (await (this.db.update(table as any) as any)
.set(values)
.where(where)
.returning()) as InferSelectModel<TTable>[];
if (!rows[0]) return undefined;
const dto = this.toDto(rows[0]);
this.emit('updated', [dto]);
return dto;
}
async deleteById(tenantId: number, id: number): Promise<TDto | undefined> {
const table = this.getTable(tenantId);
const filter = this.tenantFilter(table, tenantId);
const where = filter ? and(eq(table.id, id), filter) : eq(table.id, id);
const rows = (await (this.db.delete(table as any) as any)
.where(where)
.returning()) as InferSelectModel<TTable>[];
if (!rows[0]) return undefined;
const dto = this.toDto(rows[0]);
this.emit('deleted', [dto]);
return dto;
}
}

View File

@@ -0,0 +1,114 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type { ISecurityEventRepository } from '@/dal/index.js';
import type { BootstrapConfig, SecurityEventDto } from '@citrineos/base';
import { OCPP2_0_1 } from '@citrineos/base';
import { and, between, eq, gte, lte } from 'drizzle-orm';
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
import type { ILogObj } from 'tslog';
import { Logger } from 'tslog';
import {
type SecurityEventEntity,
securityEventTable,
tenantSecurityEventTable,
} from '../schema/SecurityEvent.js';
import { type Explicit } from '../types.js';
import { DrizzleRepository } from './Base.js';
// ─── Mapper ──────────────────────────────────────────────────────────────────
// Maps a Drizzle entity (DB row, validated by SecurityEventEntitySchema) to the
// external SecurityEventDto contract. This keeps the ORM type contained to the
// DAL layer while letting the rest of the system work against stable DTOs.
//
// Explicit<SecurityEventDto> is used so TypeScript errors if any field — including
// optional ones — is forgotten. The value may still be undefined, but it must be
// consciously declared. See ../types.ts for the full rationale.
export function toSecurityEventDto(entity: SecurityEventEntity): SecurityEventDto {
const dto: Explicit<SecurityEventDto> = {
id: entity.id,
ocppConnectionName: entity.ocppConnectionName,
type: entity.type ?? '',
// Drizzle returns timestamp as JS Date (mode: 'date'); DTO contract is ISO string.
timestamp: entity.timestamp.toISOString(),
techInfo: entity.techInfo ?? null,
tenantId: entity.tenantId,
tenant: undefined,
createdAt: entity.createdAt,
updatedAt: entity.updatedAt,
};
return dto;
}
export class DrizzleSecurityEventRepository
extends DrizzleRepository<typeof securityEventTable, SecurityEventDto>
implements ISecurityEventRepository
{
constructor(
config: BootstrapConfig,
logger?: Logger<ILogObj>,
db?: NodePgDatabase,
useTenantSchema = false,
) {
super(config, logger, db, useTenantSchema);
}
protected getTable(tenantId: number): typeof securityEventTable {
return this.useTenantSchema ? tenantSecurityEventTable(tenantId) : securityEventTable;
}
protected toDto(row: SecurityEventEntity): SecurityEventDto {
return toSecurityEventDto(row);
}
// ─── ISecurityEventRepository methods ────────────────────────────────────
async createByStationId(
tenantId: number,
value: OCPP2_0_1.SecurityEventNotificationRequest,
ocppConnectionName: string,
): Promise<SecurityEventDto> {
// Delegates to base.insert() which handles tenantId injection and event emission.
// OCPP delivers timestamp as ISO string; Postgres expects a Date for timestamptz.
return this.insert(tenantId, {
ocppConnectionName,
type: value.type,
timestamp: new Date(value.timestamp),
techInfo: value.techInfo ?? null,
});
}
async readByStationIdAndTimestamps(
tenantId: number,
ocppConnectionName: string,
from?: Date,
to?: Date,
): Promise<SecurityEventDto[]> {
const table = this.getTable(tenantId);
const conditions = [eq(table.ocppConnectionName, ocppConnectionName)];
if (!this.useTenantSchema) {
conditions.push(eq(table.tenantId, tenantId));
}
if (from && to) {
conditions.push(between(table.timestamp, from, to));
} else if (from) {
conditions.push(gte(table.timestamp, from));
} else if (to) {
conditions.push(lte(table.timestamp, to));
}
const rows = await this.db
.select()
.from(table)
.where(and(...conditions));
return rows.map((row) => this.toDto(row as SecurityEventEntity));
}
async deleteByKey(tenantId: number, key: string): Promise<SecurityEventDto | undefined> {
return this.deleteById(tenantId, Number(key));
}
}

View File

@@ -0,0 +1,66 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { TableName } from '@dal/layers/sequelize/model/TableName.js';
import { index, integer, pgSchema, pgTable, serial, timestamp, varchar } from 'drizzle-orm/pg-core';
import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
import { type z } from 'zod';
// Column definitions are a function to ensure fresh objects per table instance,
// which is required when the same schema is used across multiple pgSchema() calls.
function securityEventColumns() {
return {
id: serial('id').primaryKey(),
ocppConnectionName: varchar('ocppConnectionName', { length: 255 }).notNull(),
type: varchar('type', { length: 255 }),
// mode: 'date' returns a JS Date — mapped to ISO string in the repository layer
timestamp: timestamp('timestamp', { withTimezone: true, mode: 'date' }).notNull(),
techInfo: varchar('techInfo', { length: 255 }),
tenantId: integer('tenantId').notNull(),
createdAt: timestamp('createdAt', { withTimezone: true, mode: 'date' })
.notNull()
.$defaultFn(() => new Date()),
updatedAt: timestamp('updatedAt', { withTimezone: true, mode: 'date' })
.notNull()
.$defaultFn(() => new Date()),
};
}
// Row-level tenancy (current approach): single public schema, tenantId column filter on every query
export const securityEventTable = pgTable(TableName.SecurityEvents, securityEventColumns(), (t) => [
index('security_events_ocpp_connection_name').on(t.ocppConnectionName),
]);
// Schema-per-tenant (future approach): one Postgres schema per tenant, no tenantId filter needed
const tenantTableCache = new Map<number, typeof securityEventTable>();
// Returns a schema-qualified table reference for schema-per-tenant queries.
// Cast to typeof securityEventTable so Drizzle's query builder can infer correct
// return types — the column structure is identical, only the schema name differs at runtime.
export function tenantSecurityEventTable(tenantId: number): typeof securityEventTable {
if (!tenantTableCache.has(tenantId)) {
const t = pgSchema(`tenant_${tenantId}`).table(
TableName.SecurityEvents,
securityEventColumns(),
) as unknown as typeof securityEventTable;
tenantTableCache.set(tenantId, t);
}
return tenantTableCache.get(tenantId)!;
}
// ─── Zod schemas (runtime validation + type inference) ───────────────────────
// Entity schema: represents a fully-hydrated row read from the database.
export const SecurityEventEntitySchema = createSelectSchema(securityEventTable);
// Insert schema: represents the subset of fields required/accepted on write.
// drizzle-zod automatically makes columns with $defaultFn optional here.
export const SecurityEventEntityInsertSchema = createInsertSchema(securityEventTable);
export type SecurityEventEntity = z.infer<typeof SecurityEventEntitySchema>;
export type SecurityEventEntityInsert = z.infer<typeof SecurityEventEntityInsertSchema>;
// Legacy TypeScript types kept for backward compatibility — prefer the Zod types above.
export type SecurityEventSelect = typeof securityEventTable.$inferSelect;
export type SecurityEventInsert = typeof securityEventTable.$inferInsert;

View File

@@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
/**
* Forces every key of T to be explicitly present in the object literal,
* even optional ones — the value can still be undefined, but it must be declared.
*
* Why: TypeScript silently allows optional keys to be omitted from object
* literals that satisfy a type with `?` properties. This means a mapper that
* adds a new optional field to a DTO will not break existing mapper
* implementations — the field will simply be absent at runtime, which can cause
* subtle bugs. Typing the intermediate result as `Explicit<T>` turns those
* silent omissions into compile errors, forcing every developer to make a
* conscious decision about each field (even if that decision is `field: undefined`).
*/
export type Explicit<T> = { [K in keyof Required<T>]: T[K] };

View File

@@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type { BootstrapConfig } from '@citrineos/base';
import { drizzle, type NodePgDatabase } from 'drizzle-orm/node-postgres';
import pg from 'pg';
import type { Pool } from 'pg';
import { type ILogObj, Logger } from 'tslog';
export class DefaultDrizzleInstance {
private static readonly DEFAULT_RETRIES = 5;
private static readonly DEFAULT_RETRY_DELAY = 5000;
private static instance: NodePgDatabase | null = null;
private static pool: Pool | null = null;
private static logger: Logger<ILogObj>;
private static config: BootstrapConfig;
private constructor() {}
public static getInstance(config: BootstrapConfig, logger?: Logger<ILogObj>): NodePgDatabase {
if (!DefaultDrizzleInstance.instance) {
DefaultDrizzleInstance.config = config;
DefaultDrizzleInstance.logger = logger
? logger.getSubLogger({ name: this.name })
: new Logger<ILogObj>({ name: this.name });
DefaultDrizzleInstance.pool = new pg.Pool({
host: config.database.host,
port: config.database.port,
database: config.database.database,
user: config.database.username,
password: config.database.password,
max: config.database.pool?.max,
min: config.database.pool?.min,
idleTimeoutMillis: config.database.pool?.idle,
connectionTimeoutMillis: config.database.pool?.acquire,
...(config.database.ssl && { ssl: config.database.ssl }),
});
DefaultDrizzleInstance.instance = drizzle(DefaultDrizzleInstance.pool);
}
return DefaultDrizzleInstance.instance;
}
public static async initialize(): Promise<void> {
const maxRetries = this.config.database.maxRetries ?? this.DEFAULT_RETRIES;
const retryDelay = this.config.database.retryDelay ?? this.DEFAULT_RETRY_DELAY;
let retryCount = 0;
while (retryCount < maxRetries) {
try {
const client = await this.pool!.connect();
client.release();
this.logger.info('Drizzle database connection established successfully');
break;
} catch (error) {
retryCount++;
this.logger.error(
`Failed to connect to database via Drizzle (attempt ${retryCount}/${maxRetries}):`,
error,
);
if (retryCount < maxRetries) {
this.logger.info(`Retrying in ${retryDelay / 1000} seconds...`);
await new Promise((resolve) => setTimeout(resolve, retryDelay));
} else {
this.logger.error(
'Max retries reached. Unable to establish Drizzle database connection.',
);
}
}
}
}
}

View File

@@ -0,0 +1,107 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
// Sequelize Persistence Models
export { Boot } from './model/Boot.js';
export {
VariableAttribute,
VariableCharacteristics,
Component,
EvseType,
Variable,
VariableStatus,
} from './model/DeviceModel/index.js';
export {
Authorization,
LocalListAuthorization,
LocalListVersion,
SendLocalList,
LocalListVersionAuthorization,
SendLocalListAuthorization,
} from './model/Authorization/index.js';
export {
StartTransaction,
StopTransaction,
Transaction,
TransactionEvent,
MeterValue,
} from './model/TransactionEvent/index.js';
export { SecurityEvent } from './model/SecurityEvent.js';
export {
VariableMonitoring,
EventData,
VariableMonitoringStatus,
} from './model/VariableMonitoring/index.js';
export {
ChargingStation,
Evse,
ChargingStationNetworkProfile,
LatestStatusNotification,
Location,
ServerNetworkProfile,
SetNetworkProfile,
StatusNotification,
Connector,
} from './model/Location/index.js';
export { ChargingStationSequence } from './model/ChargingStationSequence/index.js';
export { MessageInfo } from './model/MessageInfo/index.js';
export { Tariff } from './model/Tariff/index.js';
export { Subscription } from './model/Subscription/index.js';
export {
Certificate,
SignatureAlgorithmEnumType,
CountryNameEnumType,
InstalledCertificate,
} from './model/Certificate/index.js';
export {
ChargingProfile,
ChargingNeeds,
ChargingSchedule,
CompositeSchedule,
SalesTariff,
} from './model/ChargingProfile/index.js';
export { OCPPMessage } from './model/OCPPMessage.js';
export { Reservation } from './model/Reservation.js';
export { ChargingStationSecurityInfo } from './model/ChargingStationSecurityInfo.js';
export { ChangeConfiguration } from './model/ChangeConfiguration.js';
export { Tenant } from './model/Tenant.js';
export { TenantPartner } from './model/TenantPartner.js';
export type { PaginatedParams } from './model/AsyncJob/index.js';
export { AsyncJobStatus, AsyncJobStatusDTO, AsyncJobRequest } from './model/AsyncJob/index.js';
export { DeleteCertificateAttempt, InstallCertificateAttempt } from './model/Certificate/index.js';
// Sequelize Repositories
export { SequelizeRepository } from './repository/Base.js';
export { SequelizeAuthorizationRepository } from './repository/Authorization.js';
export { SequelizeBootRepository } from './repository/Boot.js';
export { SequelizeDeviceModelRepository } from './repository/DeviceModel.js';
export { SequelizeLocalAuthListRepository } from './repository/LocalAuthList.js';
export { SequelizeLocationRepository } from './repository/Location.js';
export { SequelizeTransactionEventRepository } from './repository/TransactionEvent.js';
export { SequelizeSecurityEventRepository } from './repository/SecurityEvent.js';
export { SequelizeVariableMonitoringRepository } from './repository/VariableMonitoring.js';
export { SequelizeMessageInfoRepository } from './repository/MessageInfo.js';
export { SequelizeTariffRepository } from './repository/Tariff.js';
export { SequelizeSubscriptionRepository } from './repository/Subscription.js';
export { SequelizeCertificateRepository } from './repository/Certificate.js';
export { SequelizeInstalledCertificateRepository } from './repository/InstalledCertificate.js';
export { SequelizeChargingProfileRepository } from './repository/ChargingProfile.js';
export { SequelizeOCPPMessageRepository } from './repository/OCPPMessage.js';
export { SequelizeReservationRepository } from './repository/Reservation.js';
export { SequelizeChargingStationSecurityInfoRepository } from './repository/ChargingStationSecurityInfo.js';
export { SequelizeChargingStationSequenceRepository } from './repository/ChargingStationSequence.js';
export { SequelizeChangeConfigurationRepository } from './repository/ChangeConfiguration.js';
export { SequelizeTenantRepository } from './repository/Tenant.js';
export { SequelizeAsyncJobStatusRepository } from './repository/AsyncJobStatus.js';
export { SequelizeServerNetworkProfileRepository } from './repository/ServerNetworkProfile.js';
export { SequelizeInstallCertificateAttemptRepository } from './repository/InstallCertificateAttempt.js';
export { SequelizeDeleteCertificateAttemptRepository } from './repository/DeleteCertificateAttempt.js';
// Sequelize Utilities
export { DefaultSequelizeInstance } from './util.js';
// Sequelize Mappers
export * as OCPP2_0_1_Mapper from './mapper/2.0.1/index.js';
export * as OCPP1_6_Mapper from './mapper/1.6/index.js';
export * as OCPP2_1_Mapper from './mapper/2.1/index.js';

View File

@@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type { AuthorizationStatusEnumType } from '@citrineos/base';
import { AuthorizationStatusEnum, OCPP1_6 } from '@citrineos/base';
export class AuthorizationMapper {
static toIdTagInfoStatus(status: AuthorizationStatusEnumType): OCPP1_6.AuthorizeResponseStatus {
switch (status) {
case AuthorizationStatusEnum.Accepted:
return OCPP1_6.AuthorizeResponseStatus.Accepted;
case AuthorizationStatusEnum.Blocked:
return OCPP1_6.AuthorizeResponseStatus.Blocked;
case AuthorizationStatusEnum.Expired:
return OCPP1_6.AuthorizeResponseStatus.Expired;
case AuthorizationStatusEnum.Invalid:
return OCPP1_6.AuthorizeResponseStatus.Invalid;
default:
throw new Error('Unknown IdTagInfoStatus status');
}
}
static toStartTransactionResponseStatus(
status: AuthorizationStatusEnumType,
): OCPP1_6.StartTransactionResponseStatus {
switch (status) {
case AuthorizationStatusEnum.Accepted:
return OCPP1_6.StartTransactionResponseStatus.Accepted;
case AuthorizationStatusEnum.Blocked:
return OCPP1_6.StartTransactionResponseStatus.Blocked;
case AuthorizationStatusEnum.ConcurrentTx:
return OCPP1_6.StartTransactionResponseStatus.ConcurrentTx;
case AuthorizationStatusEnum.Expired:
return OCPP1_6.StartTransactionResponseStatus.Expired;
case AuthorizationStatusEnum.Invalid:
return OCPP1_6.StartTransactionResponseStatus.Invalid;
default:
throw new Error('Unknown StartTransactionResponse status');
}
}
}

View File

@@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { OCPP1_6 } from '@citrineos/base';
export class BootMapper {
static toRegistrationStatusEnumType(status: string): OCPP1_6.BootNotificationResponseStatus {
switch (status) {
case 'Accepted':
return OCPP1_6.BootNotificationResponseStatus.Accepted;
case 'Pending':
return OCPP1_6.BootNotificationResponseStatus.Pending;
case 'Rejected':
return OCPP1_6.BootNotificationResponseStatus.Rejected;
default:
throw new Error(`Invalid status: ${status}`);
}
}
}

View File

@@ -0,0 +1,163 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import {
ChargingProfileKindEnum,
ChargingProfilePurposeEnum,
ChargingRateUnitEnum,
OCPP1_6,
RecurrencyKindEnum,
} from '@citrineos/base';
/**
* Input type for creating/updating a ChargingProfile via the repository.
* Uses native enum types.
*/
export interface ChargingProfileInput {
id: number;
stackLevel: number;
chargingProfilePurpose: keyof typeof ChargingProfilePurposeEnum;
chargingProfileKind: keyof typeof ChargingProfileKindEnum;
recurrencyKind?: keyof typeof RecurrencyKindEnum | null;
validFrom?: string | null;
validTo?: string | null;
chargingSchedule:
| [ChargingScheduleInput]
| [ChargingScheduleInput, ChargingScheduleInput]
| [ChargingScheduleInput, ChargingScheduleInput, ChargingScheduleInput];
transactionId?: string | null;
}
/**
* Input type for creating a ChargingSchedule via the repository.
* Uses native enum types.
*/
export interface ChargingScheduleInput {
id: number;
startSchedule?: string | null;
duration?: number | null;
chargingRateUnit: keyof typeof ChargingRateUnitEnum;
chargingSchedulePeriod: [ChargingSchedulePeriodInput, ...ChargingSchedulePeriodInput[]];
minChargingRate?: number | null;
}
export interface ChargingSchedulePeriodInput {
startPeriod: number;
limit: number;
numberPhases?: number | null;
}
export class ChargingProfileMapper {
/**
* OCPP 1.6 'ChargePointMaxProfile' maps to native 'ChargingStationMaxProfile'.
* All other enum values are identical and are type-safe casts.
*/
static fromChargingProfilePurpose(purpose: string): keyof typeof ChargingProfilePurposeEnum {
if (purpose === 'ChargePointMaxProfile') {
return 'ChargingStationMaxProfile';
}
return purpose as keyof typeof ChargingProfilePurposeEnum;
}
static fromChargingProfileKind(kind: string): keyof typeof ChargingProfileKindEnum {
return kind as unknown as keyof typeof ChargingProfileKindEnum;
}
static fromRecurrencyKind(kind?: string | null): keyof typeof RecurrencyKindEnum | undefined {
if (!kind) return undefined;
return kind as unknown as keyof typeof RecurrencyKindEnum;
}
static fromChargingRateUnit(unit: string): keyof typeof ChargingRateUnitEnum {
return unit as unknown as keyof typeof ChargingRateUnitEnum;
}
/**
* Converts an OCPP 1.6 SetChargingProfile csChargingProfiles to a native ChargingProfileInput.
*/
static fromSetChargingProfileRequest(
profile: OCPP1_6.SetChargingProfileRequest['csChargingProfiles'],
): ChargingProfileInput {
return {
id: profile.chargingProfileId,
stackLevel: profile.stackLevel,
chargingProfilePurpose: ChargingProfileMapper.fromChargingProfilePurpose(
profile.chargingProfilePurpose,
),
chargingProfileKind: ChargingProfileMapper.fromChargingProfileKind(
profile.chargingProfileKind,
),
recurrencyKind: ChargingProfileMapper.fromRecurrencyKind(profile.recurrencyKind),
validFrom: profile.validFrom,
validTo: profile.validTo,
transactionId: profile.transactionId?.toString(),
chargingSchedule: [
ChargingProfileMapper.fromChargingSchedule(
profile.chargingProfileId,
profile.chargingSchedule,
),
],
};
}
/**
* Converts an OCPP 1.6 RemoteStartTransaction chargingProfile to a native ChargingProfileInput.
*/
static fromRemoteStartChargingProfile(
profile: NonNullable<OCPP1_6.RemoteStartTransactionRequest['chargingProfile']>,
): ChargingProfileInput {
return {
id: profile.chargingProfileId ?? 0,
stackLevel: profile.stackLevel,
chargingProfilePurpose: ChargingProfileMapper.fromChargingProfilePurpose(
profile.chargingProfilePurpose,
),
chargingProfileKind: ChargingProfileMapper.fromChargingProfileKind(
profile.chargingProfileKind,
),
recurrencyKind: ChargingProfileMapper.fromRecurrencyKind(profile.recurrencyKind),
validFrom: profile.validFrom,
validTo: profile.validTo,
transactionId: profile.transactionId?.toString(),
chargingSchedule: [
ChargingProfileMapper.fromChargingSchedule(
profile.chargingProfileId ?? 0,
profile.chargingSchedule,
),
],
};
}
/**
* Converts an OCPP 1.6 ChargingSchedule to a native ChargingScheduleInput.
* Accepts a scheduleId since OCPP 1.6 schedules don't have their own id.
*/
static fromChargingSchedule(
scheduleId: number,
schedule: {
chargingRateUnit: string;
chargingSchedulePeriod: {
startPeriod: number;
limit: number;
numberPhases?: number | null;
}[];
duration?: number | null;
startSchedule?: string | null;
minChargingRate?: number | null;
},
): ChargingScheduleInput {
return {
id: scheduleId,
chargingRateUnit: ChargingProfileMapper.fromChargingRateUnit(schedule.chargingRateUnit),
chargingSchedulePeriod: schedule.chargingSchedulePeriod.map((period) => ({
startPeriod: period.startPeriod,
limit: period.limit,
numberPhases: period.numberPhases,
})) as [ChargingSchedulePeriodInput, ...ChargingSchedulePeriodInput[]],
duration: schedule.duration,
startSchedule: schedule.startSchedule,
minChargingRate: schedule.minChargingRate,
};
}
}

View File

@@ -0,0 +1,76 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type { ConnectorErrorCodeEnumType, ConnectorStatusEnumType } from '@citrineos/base';
import { ConnectorErrorCodeEnum, ConnectorStatusEnum, OCPP1_6 } from '@citrineos/base';
export class LocationMapper {
static mapStatusNotificationRequestStatusToConnectorStatus(
status: OCPP1_6.StatusNotificationRequestStatus,
): ConnectorStatusEnumType {
switch (status) {
case OCPP1_6.StatusNotificationRequestStatus.Available:
return ConnectorStatusEnum.Available;
case OCPP1_6.StatusNotificationRequestStatus.Preparing:
return ConnectorStatusEnum.Preparing;
case OCPP1_6.StatusNotificationRequestStatus.Charging:
return ConnectorStatusEnum.Charging;
case OCPP1_6.StatusNotificationRequestStatus.SuspendedEVSE:
return ConnectorStatusEnum.SuspendedEVSE;
case OCPP1_6.StatusNotificationRequestStatus.SuspendedEV:
return ConnectorStatusEnum.SuspendedEV;
case OCPP1_6.StatusNotificationRequestStatus.Finishing:
return ConnectorStatusEnum.Finishing;
case OCPP1_6.StatusNotificationRequestStatus.Reserved:
return ConnectorStatusEnum.Reserved;
case OCPP1_6.StatusNotificationRequestStatus.Unavailable:
return ConnectorStatusEnum.Unavailable;
case OCPP1_6.StatusNotificationRequestStatus.Faulted:
return ConnectorStatusEnum.Faulted;
default:
return ConnectorStatusEnum.Unknown;
}
}
static mapStatusNotificationRequestErrorCodeToConnectorErrorCode(
errorCode: OCPP1_6.StatusNotificationRequestErrorCode,
): ConnectorErrorCodeEnumType {
switch (errorCode) {
case OCPP1_6.StatusNotificationRequestErrorCode.ConnectorLockFailure:
return ConnectorErrorCodeEnum.ConnectorLockFailure;
case OCPP1_6.StatusNotificationRequestErrorCode.EVCommunicationError:
return ConnectorErrorCodeEnum.EVCommunicationError;
case OCPP1_6.StatusNotificationRequestErrorCode.GroundFailure:
return ConnectorErrorCodeEnum.GroundFailure;
case OCPP1_6.StatusNotificationRequestErrorCode.HighTemperature:
return ConnectorErrorCodeEnum.HighTemperature;
case OCPP1_6.StatusNotificationRequestErrorCode.InternalError:
return ConnectorErrorCodeEnum.InternalError;
case OCPP1_6.StatusNotificationRequestErrorCode.LocalListConflict:
return ConnectorErrorCodeEnum.LocalListConflict;
case OCPP1_6.StatusNotificationRequestErrorCode.NoError:
return ConnectorErrorCodeEnum.NoError;
case OCPP1_6.StatusNotificationRequestErrorCode.OtherError:
return ConnectorErrorCodeEnum.OtherError;
case OCPP1_6.StatusNotificationRequestErrorCode.OverCurrentFailure:
return ConnectorErrorCodeEnum.OverCurrentFailure;
case OCPP1_6.StatusNotificationRequestErrorCode.PowerMeterFailure:
return ConnectorErrorCodeEnum.PowerMeterFailure;
case OCPP1_6.StatusNotificationRequestErrorCode.PowerSwitchFailure:
return ConnectorErrorCodeEnum.PowerSwitchFailure;
case OCPP1_6.StatusNotificationRequestErrorCode.ReaderFailure:
return ConnectorErrorCodeEnum.ReaderFailure;
case OCPP1_6.StatusNotificationRequestErrorCode.ResetFailure:
return ConnectorErrorCodeEnum.ResetFailure;
case OCPP1_6.StatusNotificationRequestErrorCode.UnderVoltage:
return ConnectorErrorCodeEnum.UnderVoltage;
case OCPP1_6.StatusNotificationRequestErrorCode.OverVoltage:
return ConnectorErrorCodeEnum.OverVoltage;
case OCPP1_6.StatusNotificationRequestErrorCode.WeakSignal:
return ConnectorErrorCodeEnum.WeakSignal;
default:
throw new Error(`Unknown StatusNotificationRequestErrorCode: ${errorCode}`);
}
}
}

View File

@@ -0,0 +1,488 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import {
LocationEnum,
MeasurandEnum,
OCPP1_6,
PhaseEnum,
ReadingContextEnum,
type MeterValueDto,
type SampledValue,
} from '@citrineos/base';
export class MeterValueMapper {
/**
* Converts native ReadingContextEnum to OCPP 1.6 MeterValuesRequestContext
*/
static toReadingContextEnumType(
context?: keyof typeof ReadingContextEnum | null,
): OCPP1_6.MeterValuesRequestContext | undefined {
if (!context) return undefined;
switch (context) {
case 'Interruption.Begin':
return OCPP1_6.MeterValuesRequestContext.Interruption_Begin;
case 'Interruption.End':
return OCPP1_6.MeterValuesRequestContext.Interruption_End;
case 'Other':
return OCPP1_6.MeterValuesRequestContext.Other;
case 'Sample.Clock':
return OCPP1_6.MeterValuesRequestContext.Sample_Clock;
case 'Sample.Periodic':
return OCPP1_6.MeterValuesRequestContext.Sample_Periodic;
case 'Transaction.Begin':
return OCPP1_6.MeterValuesRequestContext.Transaction_Begin;
case 'Transaction.End':
return OCPP1_6.MeterValuesRequestContext.Transaction_End;
case 'Trigger':
return OCPP1_6.MeterValuesRequestContext.Trigger;
default:
return undefined;
}
}
/**
* Converts OCPP 1.6 MeterValuesRequestContext to native ReadingContextEnum
*/
static fromReadingContextEnumType(
context?: OCPP1_6.MeterValuesRequestContext | null,
): keyof typeof ReadingContextEnum | undefined {
if (!context) return undefined;
switch (context) {
case OCPP1_6.MeterValuesRequestContext.Interruption_Begin:
return 'Interruption.Begin';
case OCPP1_6.MeterValuesRequestContext.Interruption_End:
return 'Interruption.End';
case OCPP1_6.MeterValuesRequestContext.Other:
return 'Other';
case OCPP1_6.MeterValuesRequestContext.Sample_Clock:
return 'Sample.Clock';
case OCPP1_6.MeterValuesRequestContext.Sample_Periodic:
return 'Sample.Periodic';
case OCPP1_6.MeterValuesRequestContext.Transaction_Begin:
return 'Transaction.Begin';
case OCPP1_6.MeterValuesRequestContext.Transaction_End:
return 'Transaction.End';
case OCPP1_6.MeterValuesRequestContext.Trigger:
return 'Trigger';
default:
return 'Sample.Periodic';
}
}
/**
* Converts native MeasurandEnum to OCPP 1.6 MeterValuesRequestMeasurand
*/
static toMeasurandEnumType(
measurand?: keyof typeof MeasurandEnum | null,
): OCPP1_6.MeterValuesRequestMeasurand | undefined {
if (!measurand) return undefined;
switch (measurand) {
case 'Current.Export':
return OCPP1_6.MeterValuesRequestMeasurand.Current_Export;
case 'Current.Import':
return OCPP1_6.MeterValuesRequestMeasurand.Current_Import;
case 'Current.Offered':
return OCPP1_6.MeterValuesRequestMeasurand.Current_Offered;
case 'Energy.Active.Export.Register':
return OCPP1_6.MeterValuesRequestMeasurand.Energy_Active_Export_Register;
case 'Energy.Active.Import.Register':
return OCPP1_6.MeterValuesRequestMeasurand.Energy_Active_Import_Register;
case 'Energy.Reactive.Export.Register':
return OCPP1_6.MeterValuesRequestMeasurand.Energy_Reactive_Export_Register;
case 'Energy.Reactive.Import.Register':
return OCPP1_6.MeterValuesRequestMeasurand.Energy_Reactive_Import_Register;
case 'Energy.Active.Export.Interval':
return OCPP1_6.MeterValuesRequestMeasurand.Energy_Active_Export_Interval;
case 'Energy.Active.Import.Interval':
return OCPP1_6.MeterValuesRequestMeasurand.Energy_Active_Import_Interval;
case 'Energy.Reactive.Export.Interval':
return OCPP1_6.MeterValuesRequestMeasurand.Energy_Reactive_Export_Interval;
case 'Energy.Reactive.Import.Interval':
return OCPP1_6.MeterValuesRequestMeasurand.Energy_Reactive_Import_Interval;
case 'Frequency':
return OCPP1_6.MeterValuesRequestMeasurand.Frequency;
case 'Power.Active.Export':
return OCPP1_6.MeterValuesRequestMeasurand.Power_Active_Export;
case 'Power.Active.Import':
return OCPP1_6.MeterValuesRequestMeasurand.Power_Active_Import;
case 'Power.Factor':
return OCPP1_6.MeterValuesRequestMeasurand.Power_Factor;
case 'Power.Offered':
return OCPP1_6.MeterValuesRequestMeasurand.Power_Offered;
case 'Power.Reactive.Export':
return OCPP1_6.MeterValuesRequestMeasurand.Power_Reactive_Export;
case 'Power.Reactive.Import':
return OCPP1_6.MeterValuesRequestMeasurand.Power_Reactive_Import;
case 'SoC':
return OCPP1_6.MeterValuesRequestMeasurand.SoC;
case 'Voltage':
return OCPP1_6.MeterValuesRequestMeasurand.Voltage;
default:
// Note: OCPP 2.0.1 measurands not supported in 1.6:
// Energy.Active.Net, Energy.Reactive.Net, Energy.Apparent.Net,
// Energy.Apparent.Import, Energy.Apparent.Export
return undefined;
}
}
/**
* Converts OCPP 1.6 MeterValuesRequestMeasurand to native MeasurandEnum
*/
static fromMeasurandEnumType(
measurand?: OCPP1_6.MeterValuesRequestMeasurand | null,
): keyof typeof MeasurandEnum | undefined {
if (!measurand) return undefined;
switch (measurand) {
case OCPP1_6.MeterValuesRequestMeasurand.Current_Export:
return 'Current.Export';
case OCPP1_6.MeterValuesRequestMeasurand.Current_Import:
return 'Current.Import';
case OCPP1_6.MeterValuesRequestMeasurand.Current_Offered:
return 'Current.Offered';
case OCPP1_6.MeterValuesRequestMeasurand.Energy_Active_Export_Register:
return 'Energy.Active.Export.Register';
case OCPP1_6.MeterValuesRequestMeasurand.Energy_Active_Import_Register:
return 'Energy.Active.Import.Register';
case OCPP1_6.MeterValuesRequestMeasurand.Energy_Reactive_Export_Register:
return 'Energy.Reactive.Export.Register';
case OCPP1_6.MeterValuesRequestMeasurand.Energy_Reactive_Import_Register:
return 'Energy.Reactive.Import.Register';
case OCPP1_6.MeterValuesRequestMeasurand.Energy_Active_Export_Interval:
return 'Energy.Active.Export.Interval';
case OCPP1_6.MeterValuesRequestMeasurand.Energy_Active_Import_Interval:
return 'Energy.Active.Import.Interval';
case OCPP1_6.MeterValuesRequestMeasurand.Energy_Reactive_Export_Interval:
return 'Energy.Reactive.Export.Interval';
case OCPP1_6.MeterValuesRequestMeasurand.Energy_Reactive_Import_Interval:
return 'Energy.Reactive.Import.Interval';
case OCPP1_6.MeterValuesRequestMeasurand.Frequency:
return 'Frequency';
case OCPP1_6.MeterValuesRequestMeasurand.Power_Active_Export:
return 'Power.Active.Export';
case OCPP1_6.MeterValuesRequestMeasurand.Power_Active_Import:
return 'Power.Active.Import';
case OCPP1_6.MeterValuesRequestMeasurand.Power_Factor:
return 'Power.Factor';
case OCPP1_6.MeterValuesRequestMeasurand.Power_Offered:
return 'Power.Offered';
case OCPP1_6.MeterValuesRequestMeasurand.Power_Reactive_Export:
return 'Power.Reactive.Export';
case OCPP1_6.MeterValuesRequestMeasurand.Power_Reactive_Import:
return 'Power.Reactive.Import';
case OCPP1_6.MeterValuesRequestMeasurand.RPM:
return 'RPM';
case OCPP1_6.MeterValuesRequestMeasurand.SoC:
return 'SoC';
case OCPP1_6.MeterValuesRequestMeasurand.Temperature:
return 'Temperature';
case OCPP1_6.MeterValuesRequestMeasurand.Voltage:
return 'Voltage';
default:
return 'Energy.Active.Import.Register';
}
}
/**
* Converts native LocationEnum to OCPP 1.6 MeterValuesRequestLocation
*/
static toLocationEnumType(
location?: keyof typeof LocationEnum | null,
): OCPP1_6.MeterValuesRequestLocation | undefined {
if (!location) return undefined;
switch (location) {
case 'Body':
return OCPP1_6.MeterValuesRequestLocation.Body;
case 'Cable':
return OCPP1_6.MeterValuesRequestLocation.Cable;
case 'EV':
return OCPP1_6.MeterValuesRequestLocation.EV;
case 'Inlet':
return OCPP1_6.MeterValuesRequestLocation.Inlet;
case 'Outlet':
return OCPP1_6.MeterValuesRequestLocation.Outlet;
default:
return undefined;
}
}
/**
* Converts OCPP 1.6 MeterValuesRequestLocation to native LocationEnum
*/
static fromLocationEnumType(
location?: OCPP1_6.MeterValuesRequestLocation | null,
): keyof typeof LocationEnum | undefined {
if (!location) return undefined;
switch (location) {
case OCPP1_6.MeterValuesRequestLocation.Body:
return 'Body';
case OCPP1_6.MeterValuesRequestLocation.Cable:
return 'Cable';
case OCPP1_6.MeterValuesRequestLocation.EV:
return 'EV';
case OCPP1_6.MeterValuesRequestLocation.Inlet:
return 'Inlet';
case OCPP1_6.MeterValuesRequestLocation.Outlet:
return 'Outlet';
default:
return 'Outlet';
}
}
/**
* Converts native PhaseEnum to OCPP 1.6 MeterValuesRequestPhase
*/
static toPhaseEnumType(
phase?: keyof typeof PhaseEnum | null,
): OCPP1_6.MeterValuesRequestPhase | undefined {
if (!phase) return undefined;
switch (phase) {
case 'L1':
return OCPP1_6.MeterValuesRequestPhase.L1;
case 'L2':
return OCPP1_6.MeterValuesRequestPhase.L2;
case 'L3':
return OCPP1_6.MeterValuesRequestPhase.L3;
case 'N':
return OCPP1_6.MeterValuesRequestPhase.N;
case 'L1-N':
return OCPP1_6.MeterValuesRequestPhase.L1_N;
case 'L2-N':
return OCPP1_6.MeterValuesRequestPhase.L2_N;
case 'L3-N':
return OCPP1_6.MeterValuesRequestPhase.L3_N;
case 'L1-L2':
return OCPP1_6.MeterValuesRequestPhase.L1_L2;
case 'L2-L3':
return OCPP1_6.MeterValuesRequestPhase.L2_L3;
case 'L3-L1':
return OCPP1_6.MeterValuesRequestPhase.L3_L1;
default:
return undefined;
}
}
/**
* Converts OCPP 1.6 MeterValuesRequestPhase to native PhaseEnum
*/
static fromPhaseEnumType(
phase?: OCPP1_6.MeterValuesRequestPhase | null,
): keyof typeof PhaseEnum | undefined {
if (!phase) return undefined;
switch (phase) {
case OCPP1_6.MeterValuesRequestPhase.L1:
return 'L1';
case OCPP1_6.MeterValuesRequestPhase.L2:
return 'L2';
case OCPP1_6.MeterValuesRequestPhase.L3:
return 'L3';
case OCPP1_6.MeterValuesRequestPhase.N:
return 'N';
case OCPP1_6.MeterValuesRequestPhase.L1_N:
return 'L1-N';
case OCPP1_6.MeterValuesRequestPhase.L2_N:
return 'L2-N';
case OCPP1_6.MeterValuesRequestPhase.L3_N:
return 'L3-N';
case OCPP1_6.MeterValuesRequestPhase.L1_L2:
return 'L1-L2';
case OCPP1_6.MeterValuesRequestPhase.L2_L3:
return 'L2-L3';
case OCPP1_6.MeterValuesRequestPhase.L3_L1:
return 'L3-L1';
default:
return undefined;
}
}
/**
* Converts native UnitOfMeasure to OCPP 1.6 MeterValuesRequestUnit
*/
static toUnitEnumType(unit?: string | null): OCPP1_6.MeterValuesRequestUnit | undefined {
if (!unit) return undefined;
switch (unit) {
case 'Wh':
return OCPP1_6.MeterValuesRequestUnit.Wh;
case 'kWh':
return OCPP1_6.MeterValuesRequestUnit.kWh;
case 'varh':
return OCPP1_6.MeterValuesRequestUnit.varh;
case 'kvarh':
return OCPP1_6.MeterValuesRequestUnit.kvarh;
case 'W':
return OCPP1_6.MeterValuesRequestUnit.W;
case 'kW':
return OCPP1_6.MeterValuesRequestUnit.kW;
case 'VA':
return OCPP1_6.MeterValuesRequestUnit.VA;
case 'kVA':
return OCPP1_6.MeterValuesRequestUnit.kVA;
case 'var':
return OCPP1_6.MeterValuesRequestUnit.var;
case 'kvar':
return OCPP1_6.MeterValuesRequestUnit.kvar;
case 'A':
return OCPP1_6.MeterValuesRequestUnit.A;
case 'V':
return OCPP1_6.MeterValuesRequestUnit.V;
case 'K':
return OCPP1_6.MeterValuesRequestUnit.K;
case 'Celsius':
return OCPP1_6.MeterValuesRequestUnit.Celsius;
case 'Fahrenheit':
return OCPP1_6.MeterValuesRequestUnit.Fahrenheit;
case 'Percent':
return OCPP1_6.MeterValuesRequestUnit.Percent;
default:
return undefined;
}
}
/**
* Converts OCPP 1.6 MeterValuesRequestUnit to native unit string
*/
static fromUnitEnumType(unit?: OCPP1_6.MeterValuesRequestUnit | null): string | undefined {
if (!unit) return undefined;
switch (unit) {
case OCPP1_6.MeterValuesRequestUnit.Wh:
return 'Wh';
case OCPP1_6.MeterValuesRequestUnit.kWh:
return 'kWh';
case OCPP1_6.MeterValuesRequestUnit.varh:
return 'varh';
case OCPP1_6.MeterValuesRequestUnit.kvarh:
return 'kvarh';
case OCPP1_6.MeterValuesRequestUnit.W:
return 'W';
case OCPP1_6.MeterValuesRequestUnit.kW:
return 'kW';
case OCPP1_6.MeterValuesRequestUnit.VA:
return 'VA';
case OCPP1_6.MeterValuesRequestUnit.kVA:
return 'kVA';
case OCPP1_6.MeterValuesRequestUnit.var:
return 'var';
case OCPP1_6.MeterValuesRequestUnit.kvar:
return 'kvar';
case OCPP1_6.MeterValuesRequestUnit.A:
return 'A';
case OCPP1_6.MeterValuesRequestUnit.V:
return 'V';
case OCPP1_6.MeterValuesRequestUnit.K:
return 'K';
case OCPP1_6.MeterValuesRequestUnit.Celcius:
case OCPP1_6.MeterValuesRequestUnit.Celsius:
return 'Celsius';
case OCPP1_6.MeterValuesRequestUnit.Fahrenheit:
return 'Fahrenheit';
case OCPP1_6.MeterValuesRequestUnit.Percent:
return 'Percent';
default:
return undefined;
}
}
/**
* OCPP 1.6 SampledValue type (inline from MeterValuesRequest)
*/
static toSampledValueType(
sampledValue: SampledValue,
): OCPP1_6.MeterValuesRequest['meterValue'][0]['sampledValue'][0] {
return {
value: String(sampledValue.value * 10 ** (sampledValue.unitOfMeasure?.multiplier ?? 0)),
context: MeterValueMapper.toReadingContextEnumType(sampledValue.context),
measurand: MeterValueMapper.toMeasurandEnumType(sampledValue.measurand),
phase: MeterValueMapper.toPhaseEnumType(sampledValue.phase),
location: MeterValueMapper.toLocationEnumType(sampledValue.location),
unit: MeterValueMapper.toUnitEnumType(sampledValue.unitOfMeasure?.unit),
// Note: no support for OCPP 1.6 signedMeterValues
};
}
static toMeterValueType(meterValue: MeterValueDto): OCPP1_6.MeterValuesRequest['meterValue'][0] {
return {
timestamp: meterValue.timestamp,
sampledValue: MeterValueMapper.toSampledValueTypes(meterValue.sampledValue),
};
}
static toSampledValueTypes(
sampledValues: SampledValue[],
): OCPP1_6.MeterValuesRequest['meterValue'][0]['sampledValue'] {
return sampledValues.map((sv) => MeterValueMapper.toSampledValueType(sv));
}
/**
* Validates the format field for OCPP 1.6 sampledValue.
*/
static validateFormat(format?: OCPP1_6.MeterValuesRequestFormat | null): boolean {
return format === undefined || format === OCPP1_6.MeterValuesRequestFormat.Raw;
}
/**
* Converts OCPP 1.6 sampledValue to native SampledValue
*/
static fromSampledValueType(
sampledValueType: OCPP1_6.MeterValuesRequest['meterValue'][0]['sampledValue'][0],
): SampledValue | undefined {
// Validate format - return undefined if not Raw or undefined
if (!MeterValueMapper.validateFormat(sampledValueType.format)) {
console.warn(`Unsupported OCPP 1.6 sampledValue format: ${sampledValueType.format}`);
return undefined;
}
const sampledValue: SampledValue = {
value: parseFloat(sampledValueType.value),
context: MeterValueMapper.fromReadingContextEnumType(sampledValueType.context),
measurand: MeterValueMapper.fromMeasurandEnumType(sampledValueType.measurand),
phase: MeterValueMapper.fromPhaseEnumType(sampledValueType.phase),
location: MeterValueMapper.fromLocationEnumType(sampledValueType.location),
};
const unit = MeterValueMapper.fromUnitEnumType(sampledValueType.unit);
if (unit) {
sampledValue.unitOfMeasure = {
unit,
multiplier: 0, // OCPP 1.6 does not have multiplier
};
}
return sampledValue;
}
/**
* Converts OCPP 1.6 SampledValueType[] back to SampledValue[]
*/
static fromSampledValueTypes(
sampledValueTypes: OCPP1_6.MeterValuesRequest['meterValue'][0]['sampledValue'],
): [SampledValue, ...SampledValue[]] {
const sampledValues = sampledValueTypes.map((svt) =>
MeterValueMapper.fromSampledValueType(svt),
);
return sampledValues as [SampledValue, ...SampledValue[]];
}
/**
* Converts OCPP 1.6 MeterValueType back to a partial MeterValue structure
*/
static fromMeterValueType(
meterValueType: OCPP1_6.MeterValuesRequest['meterValue'][0],
): MeterValueDto {
return {
timestamp: meterValueType.timestamp,
sampledValue: MeterValueMapper.fromSampledValueTypes(meterValueType.sampledValue),
};
}
}

View File

@@ -0,0 +1,9 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
export { BootMapper } from './BootMapper.js';
export { AuthorizationMapper } from './AuthorizationMapper.js';
export { LocationMapper } from './LocationMapper.js';
export { MeterValueMapper } from './MeterValueMapper.js';
export { ChargingProfileMapper } from './ChargingProfileMapper.js';

View File

@@ -0,0 +1,168 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type { AuthorizationStatusEnumType, IdTokenEnumType } from '@citrineos/base';
import { AuthorizationStatusEnum, IdTokenEnum, OCPP2_0_1 } from '@citrineos/base';
import { Authorization } from '../../model/Authorization/Authorization.js';
export class AuthorizationMapper {
static toAuthorizationData(authorization: Authorization): OCPP2_0_1.AuthorizationData {
return {
customData: authorization.customData,
idToken: AuthorizationMapper.toIdToken(authorization),
idTokenInfo: AuthorizationMapper.toIdTokenInfo(authorization),
};
}
static toIdToken(authorization: Authorization): OCPP2_0_1.IdTokenType {
if (!authorization.idTokenType) {
throw new Error('IdToken type is missing.');
}
return {
customData: authorization.customData,
additionalInfo: authorization.additionalInfo ?? null,
idToken: authorization.idToken,
type: AuthorizationMapper.toIdTokenEnumType(authorization.idTokenType),
};
}
static toIdTokenInfo(authorization: Authorization): OCPP2_0_1.IdTokenInfoType {
return {
status: AuthorizationMapper.fromAuthorizationStatusEnumType(authorization.status),
cacheExpiryDateTime: authorization.cacheExpiryDateTime,
chargingPriority: authorization.chargingPriority,
language1: authorization.language1,
language2: authorization.language2,
personalMessage: authorization.personalMessage,
customData: authorization.customData,
};
}
static toMessageContentType(messageContent: any): OCPP2_0_1.MessageContentType {
return {
customData: messageContent.customData,
format: AuthorizationMapper.toMessageFormatEnum(messageContent.format),
language: messageContent.language,
content: messageContent.content,
};
}
static toMessageFormatEnum(messageFormat: string): OCPP2_0_1.MessageFormatEnumType {
switch (messageFormat) {
case 'ASCII':
return OCPP2_0_1.MessageFormatEnumType.ASCII;
case 'HTML':
return OCPP2_0_1.MessageFormatEnumType.HTML;
case 'URI':
return OCPP2_0_1.MessageFormatEnumType.URI;
case 'UTF8':
return OCPP2_0_1.MessageFormatEnumType.UTF8;
default:
throw new Error('Unknown message format');
}
}
static fromAuthorizationStatusEnumType(
status: AuthorizationStatusEnumType,
): OCPP2_0_1.AuthorizationStatusEnumType {
switch (status) {
case AuthorizationStatusEnum.Accepted:
return OCPP2_0_1.AuthorizationStatusEnumType.Accepted;
case AuthorizationStatusEnum.Blocked:
return OCPP2_0_1.AuthorizationStatusEnumType.Blocked;
case AuthorizationStatusEnum.ConcurrentTx:
return OCPP2_0_1.AuthorizationStatusEnumType.ConcurrentTx;
case AuthorizationStatusEnum.Expired:
return OCPP2_0_1.AuthorizationStatusEnumType.Expired;
case AuthorizationStatusEnum.Invalid:
return OCPP2_0_1.AuthorizationStatusEnumType.Invalid;
case AuthorizationStatusEnum.NoCredit:
return OCPP2_0_1.AuthorizationStatusEnumType.NoCredit;
case AuthorizationStatusEnum.NotAllowedTypeEVSE:
return OCPP2_0_1.AuthorizationStatusEnumType.NotAllowedTypeEVSE;
case AuthorizationStatusEnum.NotAtThisLocation:
return OCPP2_0_1.AuthorizationStatusEnumType.NotAtThisLocation;
case AuthorizationStatusEnum.NotAtThisTime:
return OCPP2_0_1.AuthorizationStatusEnumType.NotAtThisTime;
case AuthorizationStatusEnum.Unknown:
return OCPP2_0_1.AuthorizationStatusEnumType.Unknown;
default:
throw new Error('Unknown authorization status: ' + status);
}
}
static toAuthorizationStatusEnumType(
status: OCPP2_0_1.AuthorizationStatusEnumType,
): AuthorizationStatusEnumType {
switch (status) {
case OCPP2_0_1.AuthorizationStatusEnumType.Accepted:
return AuthorizationStatusEnum.Accepted;
case OCPP2_0_1.AuthorizationStatusEnumType.Blocked:
return AuthorizationStatusEnum.Blocked;
case OCPP2_0_1.AuthorizationStatusEnumType.ConcurrentTx:
return AuthorizationStatusEnum.ConcurrentTx;
case OCPP2_0_1.AuthorizationStatusEnumType.Expired:
return AuthorizationStatusEnum.Expired;
case OCPP2_0_1.AuthorizationStatusEnumType.Invalid:
return AuthorizationStatusEnum.Invalid;
case OCPP2_0_1.AuthorizationStatusEnumType.NoCredit:
return AuthorizationStatusEnum.NoCredit;
case OCPP2_0_1.AuthorizationStatusEnumType.NotAllowedTypeEVSE:
return AuthorizationStatusEnum.NotAllowedTypeEVSE;
case OCPP2_0_1.AuthorizationStatusEnumType.NotAtThisLocation:
return AuthorizationStatusEnum.NotAtThisLocation;
case OCPP2_0_1.AuthorizationStatusEnumType.NotAtThisTime:
return AuthorizationStatusEnum.NotAtThisTime;
case OCPP2_0_1.AuthorizationStatusEnumType.Unknown:
return AuthorizationStatusEnum.Unknown;
default:
throw new Error('Unknown authorization status');
}
}
static toIdTokenEnumType(type: IdTokenEnumType): OCPP2_0_1.IdTokenEnumType {
switch (type) {
case IdTokenEnum.Central:
case IdTokenEnum.Other:
return OCPP2_0_1.IdTokenEnumType.Central;
case IdTokenEnum.eMAID:
return OCPP2_0_1.IdTokenEnumType.eMAID;
case IdTokenEnum.ISO14443:
return OCPP2_0_1.IdTokenEnumType.ISO14443;
case IdTokenEnum.ISO15693:
return OCPP2_0_1.IdTokenEnumType.ISO15693;
case IdTokenEnum.KeyCode:
return OCPP2_0_1.IdTokenEnumType.KeyCode;
case IdTokenEnum.Local:
return OCPP2_0_1.IdTokenEnumType.Local;
case IdTokenEnum.MacAddress:
return OCPP2_0_1.IdTokenEnumType.MacAddress;
case IdTokenEnum.NoAuthorization:
return OCPP2_0_1.IdTokenEnumType.NoAuthorization;
default:
throw new Error(`Unknown idToken type: ${type}`);
}
}
static fromIdTokenEnumType(type: OCPP2_0_1.IdTokenEnumType): IdTokenEnumType {
switch (type) {
case OCPP2_0_1.IdTokenEnumType.Central:
return IdTokenEnum.Central;
case OCPP2_0_1.IdTokenEnumType.eMAID:
return IdTokenEnum.eMAID;
case OCPP2_0_1.IdTokenEnumType.ISO14443:
return IdTokenEnum.ISO14443;
case OCPP2_0_1.IdTokenEnumType.ISO15693:
return IdTokenEnum.ISO15693;
case OCPP2_0_1.IdTokenEnumType.KeyCode:
return IdTokenEnum.KeyCode;
case OCPP2_0_1.IdTokenEnumType.Local:
return IdTokenEnum.Local;
case OCPP2_0_1.IdTokenEnumType.MacAddress:
return IdTokenEnum.MacAddress;
case OCPP2_0_1.IdTokenEnumType.NoAuthorization:
return IdTokenEnum.NoAuthorization;
default:
throw new Error(`Unknown OCPP 2.0.1 idToken type: ${type}`);
}
}
}

View File

@@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { OCPP2_0_1 } from '@citrineos/base';
export class BootMapper {
static toRegistrationStatusEnumType(status: string): OCPP2_0_1.RegistrationStatusEnumType {
switch (status) {
case 'Accepted':
return OCPP2_0_1.RegistrationStatusEnumType.Accepted;
case 'Pending':
return OCPP2_0_1.RegistrationStatusEnumType.Pending;
case 'Rejected':
return OCPP2_0_1.RegistrationStatusEnumType.Rejected;
default:
throw new Error(`Invalid status: ${status}`);
}
}
static toStatusInfo(statusInfo?: any): any {
if (!statusInfo) {
return statusInfo;
}
return {
customData: statusInfo.customData,
reasonCode: statusInfo.reasonCode,
additionalInfo: statusInfo.additionalInfo,
};
}
}

View File

@@ -0,0 +1,287 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import {
ChargingLimitSourceEnum,
ChargingProfileKindEnum,
ChargingProfilePurposeEnum,
ChargingRateUnitEnum,
OCPP2_0_1,
OCPP2_1,
OCPP2_common_types,
RecurrencyKindEnum,
type ChargingProfileDto,
type ChargingScheduleDto,
} from '@citrineos/base';
/**
* Input type for creating/updating a ChargingProfile via the repository.
* Mirrors OCPP2_0_1.ChargingProfileType but uses native enum types.
*/
export interface ChargingProfileInput {
id: number;
stackLevel: number;
chargingProfilePurpose: keyof typeof ChargingProfilePurposeEnum;
chargingProfileKind: keyof typeof ChargingProfileKindEnum;
recurrencyKind?: keyof typeof RecurrencyKindEnum | null;
validFrom?: string | null;
validTo?: string | null;
chargingSchedule:
| [ChargingScheduleInput]
| [ChargingScheduleInput, ChargingScheduleInput]
| [ChargingScheduleInput, ChargingScheduleInput, ChargingScheduleInput];
transactionId?: string | null;
}
/**
* Input type for creating a ChargingSchedule via the repository.
* Mirrors OCPP2_0_1.ChargingScheduleType but uses native enum types.
*/
export interface ChargingScheduleInput {
id: number;
startSchedule?: string | null;
duration?: number | null;
chargingRateUnit: keyof typeof ChargingRateUnitEnum;
chargingSchedulePeriod: [ChargingSchedulePeriodInput, ...ChargingSchedulePeriodInput[]];
minChargingRate?: number | null;
salesTariff?: SalesTariffInput | null;
}
export interface ChargingSchedulePeriodInput {
startPeriod: number;
limit: number;
numberPhases?: number | null;
phaseToUse?: number | null;
}
export interface SalesTariffInput {
id: number;
salesTariffDescription?: string | null;
numEPriceLevels?: number | null;
salesTariffEntry: [OCPP2_0_1.SalesTariffEntryType, ...OCPP2_0_1.SalesTariffEntryType[]];
}
/**
* Input type for creating a CompositeSchedule via the repository.
* Mirrors OCPP2_0_1.CompositeScheduleType but uses native enum types.
*/
export interface CompositeScheduleInput {
chargingSchedulePeriod: [ChargingSchedulePeriodInput, ...ChargingSchedulePeriodInput[]];
evseId: number;
duration: number;
scheduleStart: string;
chargingRateUnit: keyof typeof ChargingRateUnitEnum;
}
export class ChargingProfileMapper {
// =========================================================================
// Enum converters: Native → OCPP 2.0.1
// Note: Native enum values are identical to OCPP 2.0.1 enum values,
// so these are type-safe casts rather than value transformations.
// =========================================================================
static toChargingProfileKindEnumType(
kind: keyof typeof ChargingProfileKindEnum,
): OCPP2_0_1.ChargingProfileKindEnumType {
return kind as unknown as OCPP2_0_1.ChargingProfileKindEnumType;
}
static fromChargingProfileKindEnumType(
kind: OCPP2_1.ChargingProfileKindEnumType,
): keyof typeof ChargingProfileKindEnum {
return kind as unknown as keyof typeof ChargingProfileKindEnum;
}
static toChargingProfilePurposeEnumType(
purpose: keyof typeof ChargingProfilePurposeEnum,
): OCPP2_0_1.ChargingProfilePurposeEnumType {
return purpose as unknown as OCPP2_0_1.ChargingProfilePurposeEnumType;
}
static fromChargingProfilePurposeEnumType(
purpose: OCPP2_1.ChargingProfilePurposeEnumType,
): keyof typeof ChargingProfilePurposeEnum {
return purpose as unknown as keyof typeof ChargingProfilePurposeEnum;
}
static toRecurrencyKindEnumType(
kind?: keyof typeof RecurrencyKindEnum | null,
): OCPP2_0_1.RecurrencyKindEnumType | undefined {
if (!kind) return undefined;
return kind as unknown as OCPP2_0_1.RecurrencyKindEnumType;
}
static fromRecurrencyKindEnumType(
kind?: OCPP2_0_1.RecurrencyKindEnumType | null,
): keyof typeof RecurrencyKindEnum | undefined {
if (!kind) return undefined;
return kind as unknown as keyof typeof RecurrencyKindEnum;
}
static toChargingRateUnitEnumType(
unit: keyof typeof ChargingRateUnitEnum,
): OCPP2_0_1.ChargingRateUnitEnumType {
return unit as unknown as OCPP2_0_1.ChargingRateUnitEnumType;
}
static fromChargingRateUnitEnumType(
unit: OCPP2_0_1.ChargingRateUnitEnumType,
): keyof typeof ChargingRateUnitEnum {
return unit as unknown as keyof typeof ChargingRateUnitEnum;
}
static toChargingLimitSourceEnumType(
source?: keyof typeof ChargingLimitSourceEnum | null,
): OCPP2_0_1.ChargingLimitSourceEnumType | undefined {
if (!source) return undefined;
return source as unknown as OCPP2_0_1.ChargingLimitSourceEnumType;
}
static fromChargingLimitSourceEnumType(
source?: OCPP2_1.ChargingLimitSourceEnumType | null,
): keyof typeof ChargingLimitSourceEnum | undefined {
if (!source) return undefined;
return source as unknown as keyof typeof ChargingLimitSourceEnum;
}
// =========================================================================
// Object converters: OCPP 2.0.1 → Native
// =========================================================================
/**
* Converts OCPP2_0_1.ChargingProfileType to a native ChargingProfileInput.
*/
static fromChargingProfileType(
chargingProfile: OCPP2_0_1.ChargingProfileType | OCPP2_1.ChargingProfileType,
): ChargingProfileInput {
return {
id: chargingProfile.id,
stackLevel: chargingProfile.stackLevel,
chargingProfilePurpose: ChargingProfileMapper.fromChargingProfilePurposeEnumType(
chargingProfile.chargingProfilePurpose,
),
chargingProfileKind: ChargingProfileMapper.fromChargingProfileKindEnumType(
chargingProfile.chargingProfileKind,
),
recurrencyKind: ChargingProfileMapper.fromRecurrencyKindEnumType(
chargingProfile.recurrencyKind,
),
validFrom: chargingProfile.validFrom,
validTo: chargingProfile.validTo,
chargingSchedule: chargingProfile.chargingSchedule.map((schedule) =>
ChargingProfileMapper.fromChargingScheduleType(schedule),
) as ChargingProfileInput['chargingSchedule'],
transactionId: chargingProfile.transactionId,
};
}
/**
* Converts OCPP2_0_1.ChargingScheduleType to a native ChargingScheduleInput.
*/
static fromChargingScheduleType(schedule: OCPP2_1.ChargingScheduleType): ChargingScheduleInput {
return {
id: schedule.id,
startSchedule: schedule.startSchedule,
duration: schedule.duration,
chargingRateUnit: ChargingProfileMapper.fromChargingRateUnitEnumType(
schedule.chargingRateUnit,
),
chargingSchedulePeriod: schedule.chargingSchedulePeriod.map((period) => ({
startPeriod: period.startPeriod,
limit: period.limit,
numberPhases: period.numberPhases,
phaseToUse: period.phaseToUse,
})) as [ChargingSchedulePeriodInput, ...ChargingSchedulePeriodInput[]],
minChargingRate: schedule.minChargingRate,
salesTariff: schedule.salesTariff
? {
id: schedule.salesTariff.id,
salesTariffDescription: schedule.salesTariff.salesTariffDescription,
numEPriceLevels: schedule.salesTariff.numEPriceLevels,
salesTariffEntry: schedule.salesTariff.salesTariffEntry,
}
: undefined,
};
}
/**
* Converts OCPP2_0_1.CompositeScheduleType to a native CompositeScheduleInput.
*/
static fromCompositeScheduleType(
compositeSchedule: OCPP2_common_types.CompositeScheduleType,
): CompositeScheduleInput {
return {
chargingSchedulePeriod: compositeSchedule.chargingSchedulePeriod.map((period) => ({
startPeriod: period.startPeriod,
limit: period.limit,
numberPhases: period.numberPhases,
phaseToUse: period.phaseToUse,
})) as [ChargingSchedulePeriodInput, ...ChargingSchedulePeriodInput[]],
evseId: compositeSchedule.evseId,
duration: compositeSchedule.duration,
scheduleStart: compositeSchedule.scheduleStart,
chargingRateUnit: ChargingProfileMapper.fromChargingRateUnitEnumType(
compositeSchedule.chargingRateUnit,
),
};
}
// =========================================================================
// Object converters: Native → OCPP 2.0.1
// =========================================================================
/**
* Converts a native ChargingProfile (Sequelize model) to OCPP2_0_1.ChargingProfileType.
*/
static toChargingProfileType(
chargingProfile: ChargingProfileDto,
transactionId?: string | null,
): OCPP2_0_1.ChargingProfileType {
return {
id: chargingProfile.id!,
stackLevel: chargingProfile.stackLevel,
chargingProfilePurpose: ChargingProfileMapper.toChargingProfilePurposeEnumType(
chargingProfile.chargingProfilePurpose,
),
chargingProfileKind: ChargingProfileMapper.toChargingProfileKindEnumType(
chargingProfile.chargingProfileKind,
),
recurrencyKind: ChargingProfileMapper.toRecurrencyKindEnumType(
chargingProfile.recurrencyKind,
),
validFrom: chargingProfile.validFrom,
validTo: chargingProfile.validTo,
chargingSchedule: chargingProfile.chargingSchedule.map((schedule) =>
ChargingProfileMapper.toChargingScheduleType(schedule),
) as OCPP2_0_1.ChargingProfileType['chargingSchedule'],
transactionId: transactionId,
};
}
/**
* Converts a native ChargingScheduleDto to OCPP2_0_1.ChargingScheduleType.
*/
static toChargingScheduleType(schedule: ChargingScheduleDto): OCPP2_0_1.ChargingScheduleType {
return {
id: schedule.id!,
startSchedule: schedule.startSchedule,
duration: schedule.duration,
chargingRateUnit: ChargingProfileMapper.toChargingRateUnitEnumType(schedule.chargingRateUnit),
chargingSchedulePeriod:
schedule.chargingSchedulePeriod as OCPP2_0_1.ChargingScheduleType['chargingSchedulePeriod'],
minChargingRate: schedule.minChargingRate,
salesTariff: schedule.salesTariff
? {
id: schedule.salesTariff.id!,
salesTariffDescription: schedule.salesTariff.salesTariffDescription,
numEPriceLevels: schedule.salesTariff.numEPriceLevels,
salesTariffEntry: schedule.salesTariff.salesTariffEntry as [
OCPP2_0_1.SalesTariffEntryType,
...OCPP2_0_1.SalesTariffEntryType[],
],
}
: undefined,
};
}
}

View File

@@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type { ConnectorStatusEnumType } from '@citrineos/base';
import { ConnectorStatusEnum, OCPP2_0_1 } from '@citrineos/base';
export class LocationMapper {
static mapConnectorStatus(status: OCPP2_0_1.ConnectorStatusEnumType): ConnectorStatusEnumType {
switch (status) {
case OCPP2_0_1.ConnectorStatusEnumType.Available:
return ConnectorStatusEnum.Available;
case OCPP2_0_1.ConnectorStatusEnumType.Occupied:
return ConnectorStatusEnum.Occupied;
case OCPP2_0_1.ConnectorStatusEnumType.Reserved:
return ConnectorStatusEnum.Reserved;
case OCPP2_0_1.ConnectorStatusEnumType.Unavailable:
return ConnectorStatusEnum.Unavailable;
case OCPP2_0_1.ConnectorStatusEnumType.Faulted:
return ConnectorStatusEnum.Faulted;
default:
return ConnectorStatusEnum.Unknown;
}
}
}

View File

@@ -0,0 +1,421 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import {
LocationEnum,
MeasurandEnum,
OCPP2_0_1,
PhaseEnum,
ReadingContextEnum,
type MeterValueDto,
type SampledValue,
} from '@citrineos/base';
export class MeterValueMapper {
/**
* Converts native ReadingContextEnum to OCPP 2.0.1 ReadingContextEnumType
*/
static toReadingContextEnumType(
context?: keyof typeof ReadingContextEnum | null,
): OCPP2_0_1.ReadingContextEnumType | undefined {
if (!context) return undefined;
switch (context) {
case 'Interruption.Begin':
return OCPP2_0_1.ReadingContextEnumType.Interruption_Begin;
case 'Interruption.End':
return OCPP2_0_1.ReadingContextEnumType.Interruption_End;
case 'Other':
return OCPP2_0_1.ReadingContextEnumType.Other;
case 'Sample.Clock':
return OCPP2_0_1.ReadingContextEnumType.Sample_Clock;
case 'Sample.Periodic':
return OCPP2_0_1.ReadingContextEnumType.Sample_Periodic;
case 'Transaction.Begin':
return OCPP2_0_1.ReadingContextEnumType.Transaction_Begin;
case 'Transaction.End':
return OCPP2_0_1.ReadingContextEnumType.Transaction_End;
case 'Trigger':
return OCPP2_0_1.ReadingContextEnumType.Trigger;
default:
return undefined;
}
}
/**
* Converts OCPP 2.0.1 ReadingContextEnumType to native ReadingContextEnum
*/
static fromReadingContextEnumType(
context?: OCPP2_0_1.ReadingContextEnumType | null,
): keyof typeof ReadingContextEnum | undefined {
if (!context) return undefined;
switch (context) {
case OCPP2_0_1.ReadingContextEnumType.Interruption_Begin:
return 'Interruption.Begin';
case OCPP2_0_1.ReadingContextEnumType.Interruption_End:
return 'Interruption.End';
case OCPP2_0_1.ReadingContextEnumType.Other:
return 'Other';
case OCPP2_0_1.ReadingContextEnumType.Sample_Clock:
return 'Sample.Clock';
case OCPP2_0_1.ReadingContextEnumType.Sample_Periodic:
return 'Sample.Periodic';
case OCPP2_0_1.ReadingContextEnumType.Transaction_Begin:
return 'Transaction.Begin';
case OCPP2_0_1.ReadingContextEnumType.Transaction_End:
return 'Transaction.End';
case OCPP2_0_1.ReadingContextEnumType.Trigger:
return 'Trigger';
default:
return 'Sample.Periodic';
}
}
/**
* Converts native MeasurandEnum to OCPP 2.0.1 MeasurandEnumType
*/
static toMeasurandEnumType(
measurand?: keyof typeof MeasurandEnum | null,
): OCPP2_0_1.MeasurandEnumType | undefined {
if (!measurand) return undefined;
switch (measurand) {
case 'Current.Export':
return OCPP2_0_1.MeasurandEnumType.Current_Export;
case 'Current.Import':
return OCPP2_0_1.MeasurandEnumType.Current_Import;
case 'Current.Offered':
return OCPP2_0_1.MeasurandEnumType.Current_Offered;
case 'Energy.Active.Export.Register':
return OCPP2_0_1.MeasurandEnumType.Energy_Active_Export_Register;
case 'Energy.Active.Import.Register':
return OCPP2_0_1.MeasurandEnumType.Energy_Active_Import_Register;
case 'Energy.Reactive.Export.Register':
return OCPP2_0_1.MeasurandEnumType.Energy_Reactive_Export_Register;
case 'Energy.Reactive.Import.Register':
return OCPP2_0_1.MeasurandEnumType.Energy_Reactive_Import_Register;
case 'Energy.Active.Export.Interval':
return OCPP2_0_1.MeasurandEnumType.Energy_Active_Export_Interval;
case 'Energy.Active.Import.Interval':
return OCPP2_0_1.MeasurandEnumType.Energy_Active_Import_Interval;
case 'Energy.Active.Net':
return OCPP2_0_1.MeasurandEnumType.Energy_Active_Net;
case 'Energy.Reactive.Export.Interval':
return OCPP2_0_1.MeasurandEnumType.Energy_Reactive_Export_Interval;
case 'Energy.Reactive.Import.Interval':
return OCPP2_0_1.MeasurandEnumType.Energy_Reactive_Import_Interval;
case 'Energy.Reactive.Net':
return OCPP2_0_1.MeasurandEnumType.Energy_Reactive_Net;
case 'Energy.Apparent.Net':
return OCPP2_0_1.MeasurandEnumType.Energy_Apparent_Net;
case 'Energy.Apparent.Import':
return OCPP2_0_1.MeasurandEnumType.Energy_Apparent_Import;
case 'Energy.Apparent.Export':
return OCPP2_0_1.MeasurandEnumType.Energy_Apparent_Export;
case 'Frequency':
return OCPP2_0_1.MeasurandEnumType.Frequency;
case 'Power.Active.Export':
return OCPP2_0_1.MeasurandEnumType.Power_Active_Export;
case 'Power.Active.Import':
return OCPP2_0_1.MeasurandEnumType.Power_Active_Import;
case 'Power.Factor':
return OCPP2_0_1.MeasurandEnumType.Power_Factor;
case 'Power.Offered':
return OCPP2_0_1.MeasurandEnumType.Power_Offered;
case 'Power.Reactive.Export':
return OCPP2_0_1.MeasurandEnumType.Power_Reactive_Export;
case 'Power.Reactive.Import':
return OCPP2_0_1.MeasurandEnumType.Power_Reactive_Import;
case 'SoC':
return OCPP2_0_1.MeasurandEnumType.SoC;
case 'Voltage':
return OCPP2_0_1.MeasurandEnumType.Voltage;
default:
// Note: Native enum measurands not supported in OCPP 2.0.1:
// Temperature, RPM - from OCPP 1.6
return undefined;
}
}
/**
* Converts OCPP 2.0.1 MeasurandEnumType to native MeasurandEnum
*/
static fromMeasurandEnumType(
measurand?: OCPP2_0_1.MeasurandEnumType | null,
): keyof typeof MeasurandEnum | undefined {
if (!measurand) return undefined;
switch (measurand) {
case OCPP2_0_1.MeasurandEnumType.Current_Export:
return 'Current.Export';
case OCPP2_0_1.MeasurandEnumType.Current_Import:
return 'Current.Import';
case OCPP2_0_1.MeasurandEnumType.Current_Offered:
return 'Current.Offered';
case OCPP2_0_1.MeasurandEnumType.Energy_Active_Export_Register:
return 'Energy.Active.Export.Register';
case OCPP2_0_1.MeasurandEnumType.Energy_Active_Import_Register:
return 'Energy.Active.Import.Register';
case OCPP2_0_1.MeasurandEnumType.Energy_Reactive_Export_Register:
return 'Energy.Reactive.Export.Register';
case OCPP2_0_1.MeasurandEnumType.Energy_Reactive_Import_Register:
return 'Energy.Reactive.Import.Register';
case OCPP2_0_1.MeasurandEnumType.Energy_Active_Export_Interval:
return 'Energy.Active.Export.Interval';
case OCPP2_0_1.MeasurandEnumType.Energy_Active_Import_Interval:
return 'Energy.Active.Import.Interval';
case OCPP2_0_1.MeasurandEnumType.Energy_Active_Net:
return 'Energy.Active.Net';
case OCPP2_0_1.MeasurandEnumType.Energy_Reactive_Export_Interval:
return 'Energy.Reactive.Export.Interval';
case OCPP2_0_1.MeasurandEnumType.Energy_Reactive_Import_Interval:
return 'Energy.Reactive.Import.Interval';
case OCPP2_0_1.MeasurandEnumType.Energy_Reactive_Net:
return 'Energy.Reactive.Net';
case OCPP2_0_1.MeasurandEnumType.Energy_Apparent_Net:
return 'Energy.Apparent.Net';
case OCPP2_0_1.MeasurandEnumType.Energy_Apparent_Import:
return 'Energy.Apparent.Import';
case OCPP2_0_1.MeasurandEnumType.Energy_Apparent_Export:
return 'Energy.Apparent.Export';
case OCPP2_0_1.MeasurandEnumType.Frequency:
return 'Frequency';
case OCPP2_0_1.MeasurandEnumType.Power_Active_Export:
return 'Power.Active.Export';
case OCPP2_0_1.MeasurandEnumType.Power_Active_Import:
return 'Power.Active.Import';
case OCPP2_0_1.MeasurandEnumType.Power_Factor:
return 'Power.Factor';
case OCPP2_0_1.MeasurandEnumType.Power_Offered:
return 'Power.Offered';
case OCPP2_0_1.MeasurandEnumType.Power_Reactive_Export:
return 'Power.Reactive.Export';
case OCPP2_0_1.MeasurandEnumType.Power_Reactive_Import:
return 'Power.Reactive.Import';
case OCPP2_0_1.MeasurandEnumType.SoC:
return 'SoC';
case OCPP2_0_1.MeasurandEnumType.Voltage:
return 'Voltage';
default:
return 'Energy.Active.Import.Register';
}
}
/**
* Converts native LocationEnum to OCPP 2.0.1 LocationEnumType
*/
static toLocationEnumType(
location?: keyof typeof LocationEnum | null,
): OCPP2_0_1.LocationEnumType | undefined {
if (!location) return undefined;
switch (location) {
case 'Body':
return OCPP2_0_1.LocationEnumType.Body;
case 'Cable':
return OCPP2_0_1.LocationEnumType.Cable;
case 'EV':
return OCPP2_0_1.LocationEnumType.EV;
case 'Inlet':
return OCPP2_0_1.LocationEnumType.Inlet;
case 'Outlet':
return OCPP2_0_1.LocationEnumType.Outlet;
default:
return undefined;
}
}
/**
* Converts OCPP 2.0.1 LocationEnumType to native LocationEnum
*/
static fromLocationEnumType(
location?: OCPP2_0_1.LocationEnumType | null,
): keyof typeof LocationEnum | undefined {
if (!location) return undefined;
switch (location) {
case OCPP2_0_1.LocationEnumType.Body:
return 'Body';
case OCPP2_0_1.LocationEnumType.Cable:
return 'Cable';
case OCPP2_0_1.LocationEnumType.EV:
return 'EV';
case OCPP2_0_1.LocationEnumType.Inlet:
return 'Inlet';
case OCPP2_0_1.LocationEnumType.Outlet:
return 'Outlet';
default:
return 'Outlet';
}
}
/**
* Converts native PhaseEnum to OCPP 2.0.1 PhaseEnumType
*/
static toPhaseEnumType(
phase?: keyof typeof PhaseEnum | null,
): OCPP2_0_1.PhaseEnumType | undefined {
if (!phase) return undefined;
switch (phase) {
case 'L1':
return OCPP2_0_1.PhaseEnumType.L1;
case 'L2':
return OCPP2_0_1.PhaseEnumType.L2;
case 'L3':
return OCPP2_0_1.PhaseEnumType.L3;
case 'N':
return OCPP2_0_1.PhaseEnumType.N;
case 'L1-N':
return OCPP2_0_1.PhaseEnumType.L1_N;
case 'L2-N':
return OCPP2_0_1.PhaseEnumType.L2_N;
case 'L3-N':
return OCPP2_0_1.PhaseEnumType.L3_N;
case 'L1-L2':
return OCPP2_0_1.PhaseEnumType.L1_L2;
case 'L2-L3':
return OCPP2_0_1.PhaseEnumType.L2_L3;
case 'L3-L1':
return OCPP2_0_1.PhaseEnumType.L3_L1;
default:
return undefined;
}
}
/**
* Converts OCPP 2.0.1 PhaseEnumType to native PhaseEnum
*/
static fromPhaseEnumType(
phase?: OCPP2_0_1.PhaseEnumType | null,
): keyof typeof PhaseEnum | undefined {
if (!phase) return undefined;
switch (phase) {
case OCPP2_0_1.PhaseEnumType.L1:
return 'L1';
case OCPP2_0_1.PhaseEnumType.L2:
return 'L2';
case OCPP2_0_1.PhaseEnumType.L3:
return 'L3';
case OCPP2_0_1.PhaseEnumType.N:
return 'N';
case OCPP2_0_1.PhaseEnumType.L1_N:
return 'L1-N';
case OCPP2_0_1.PhaseEnumType.L2_N:
return 'L2-N';
case OCPP2_0_1.PhaseEnumType.L3_N:
return 'L3-N';
case OCPP2_0_1.PhaseEnumType.L1_L2:
return 'L1-L2';
case OCPP2_0_1.PhaseEnumType.L2_L3:
return 'L2-L3';
case OCPP2_0_1.PhaseEnumType.L3_L1:
return 'L3-L1';
default:
return undefined;
}
}
static toMeterValueType(meterValue: MeterValueDto): OCPP2_0_1.MeterValueType {
return {
timestamp: meterValue.timestamp,
sampledValue: MeterValueMapper.toSampledValueTypes(meterValue.sampledValue),
};
}
static toSampledValueTypes(
sampledValues: SampledValue[],
): [OCPP2_0_1.SampledValueType, ...OCPP2_0_1.SampledValueType[]] {
if (!(sampledValues instanceof Array) || sampledValues.length === 0) {
throw new Error(`Invalid sampledValues: ${JSON.stringify(sampledValues)}`);
}
const sampledValueTypes: OCPP2_0_1.SampledValueType[] = [];
for (const sampledValue of sampledValues) {
const measurand = MeterValueMapper.toMeasurandEnumType(sampledValue.measurand);
if (measurand !== undefined) {
sampledValueTypes.push({
value: sampledValue.value,
context: MeterValueMapper.toReadingContextEnumType(sampledValue.context),
measurand: measurand,
phase: MeterValueMapper.toPhaseEnumType(sampledValue.phase),
location: MeterValueMapper.toLocationEnumType(sampledValue.location),
signedMeterValue: sampledValue.signedMeterValue
? {
signedMeterData: sampledValue.signedMeterValue.signedMeterData,
signingMethod: sampledValue.signedMeterValue.signingMethod,
encodingMethod: sampledValue.signedMeterValue.encodingMethod,
publicKey: sampledValue.signedMeterValue.publicKey,
}
: undefined,
unitOfMeasure: sampledValue.unitOfMeasure
? {
unit: sampledValue.unitOfMeasure.unit,
multiplier: sampledValue.unitOfMeasure.multiplier,
}
: undefined,
});
} else {
console.warn(`Unsupported measurand for OCPP 2.0.1: ${sampledValue.measurand}`);
}
}
return sampledValueTypes as [OCPP2_0_1.SampledValueType, ...OCPP2_0_1.SampledValueType[]];
}
/**
* Converts OCPP2_0_1.SampledValueType[] back to SampledValue[]
*/
static fromSampledValueTypes(
sampledValueTypes: OCPP2_0_1.SampledValueType[],
): [SampledValue, ...SampledValue[]] {
if (!Array.isArray(sampledValueTypes) || sampledValueTypes.length === 0) {
throw new Error(`Invalid sampledValueTypes: ${JSON.stringify(sampledValueTypes)}`);
}
const sampledValues: SampledValue[] = [];
for (const sampledValueType of sampledValueTypes) {
const sampledValue: SampledValue = {
value: sampledValueType.value,
context: MeterValueMapper.fromReadingContextEnumType(sampledValueType.context),
measurand: MeterValueMapper.fromMeasurandEnumType(sampledValueType.measurand),
phase: MeterValueMapper.fromPhaseEnumType(sampledValueType.phase),
location: MeterValueMapper.fromLocationEnumType(sampledValueType.location),
};
if (sampledValueType.signedMeterValue) {
sampledValue.signedMeterValue = {
signedMeterData: sampledValueType.signedMeterValue.signedMeterData,
signingMethod: sampledValueType.signedMeterValue.signingMethod,
encodingMethod: sampledValueType.signedMeterValue.encodingMethod,
publicKey: sampledValueType.signedMeterValue.publicKey,
};
}
if (sampledValueType.unitOfMeasure) {
sampledValue.unitOfMeasure = {
unit:
sampledValueType.unitOfMeasure.unit ||
(sampledValue.measurand?.startsWith('Energy') ? 'Wh' : undefined),
multiplier: sampledValueType.unitOfMeasure.multiplier || 0,
};
}
sampledValues.push(sampledValue);
}
return sampledValues as [SampledValue, ...SampledValue[]];
}
/**
* Converts OCPP2_0_1.MeterValueType back to a partial MeterValue structure
*/
static fromMeterValueType(meterValueType: OCPP2_0_1.MeterValueType): MeterValueDto {
return {
timestamp: meterValueType.timestamp,
sampledValue: MeterValueMapper.fromSampledValueTypes(meterValueType.sampledValue),
};
}
}

View File

@@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { OCPP2_0_1 } from '@citrineos/base';
import { Transaction } from '../../model/TransactionEvent/Transaction.js';
export class TransactionMapper {
static toTransactionType(transaction: Transaction): OCPP2_0_1.TransactionType {
return {
transactionId: transaction.transactionId,
chargingState: transaction.chargingState as OCPP2_0_1.ChargingStateEnumType,
timeSpentCharging: transaction.timeSpentCharging,
stoppedReason: transaction.stoppedReason as OCPP2_0_1.ReasonEnumType,
remoteStartId: transaction.remoteStartId,
customData: transaction.customData,
};
}
}

View File

@@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
export { AuthorizationMapper } from './AuthorizationMapper.js';
export { BootMapper } from './BootMapper.js';
export { ChargingProfileMapper } from './ChargingProfileMapper.js';
export { MeterValueMapper } from './MeterValueMapper.js';
export { TransactionMapper } from './TransactionMapper.js';
export { LocationMapper } from './LocationMapper.js';

View File

@@ -0,0 +1,182 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type { AuthorizationStatusEnumType, IdTokenEnumType } from '@citrineos/base';
import { AuthorizationStatusEnum, IdTokenEnum, OCPP2_1 } from '@citrineos/base';
import { Authorization } from '../../model/Authorization/Authorization.js';
export class AuthorizationMapper {
static toAuthorizationData(authorization: Authorization): OCPP2_1.AuthorizationData {
return {
customData: authorization.customData,
idToken: AuthorizationMapper.toIdToken(authorization),
idTokenInfo: AuthorizationMapper.toIdTokenInfo(authorization),
};
}
static toIdToken(authorization: Authorization): OCPP2_1.IdTokenType {
if (!authorization.idTokenType) {
throw new Error('IdToken type is missing.');
}
return {
customData: authorization.customData,
additionalInfo: authorization.additionalInfo ?? null,
idToken: authorization.idToken,
type: AuthorizationMapper.toIdTokenEnumType(authorization.idTokenType),
};
}
static toIdTokenInfo(authorization: Authorization): OCPP2_1.IdTokenInfoType {
return {
status: AuthorizationMapper.fromAuthorizationStatusEnumType(authorization.status),
cacheExpiryDateTime: authorization.cacheExpiryDateTime,
chargingPriority: authorization.chargingPriority,
language1: authorization.language1,
language2: authorization.language2,
personalMessage: authorization.personalMessage,
customData: authorization.customData,
};
}
static toMessageContentType(messageContent: any): OCPP2_1.MessageContentType {
return {
customData: messageContent.customData,
format: AuthorizationMapper.toMessageFormatEnum(messageContent.format),
language: messageContent.language,
content: messageContent.content,
};
}
static toMessageFormatEnum(messageFormat: string): OCPP2_1.MessageFormatEnumType {
switch (messageFormat) {
case 'ASCII':
return OCPP2_1.MessageFormatEnumType.ASCII;
case 'HTML':
return OCPP2_1.MessageFormatEnumType.HTML;
case 'URI':
return OCPP2_1.MessageFormatEnumType.URI;
case 'UTF8':
return OCPP2_1.MessageFormatEnumType.UTF8;
case 'QRCODE':
return OCPP2_1.MessageFormatEnumType.QRCODE;
default:
throw new Error('Unknown message format');
}
}
static fromAuthorizationStatusEnumType(
status: AuthorizationStatusEnumType,
): OCPP2_1.AuthorizationStatusEnumType {
switch (status) {
case AuthorizationStatusEnum.Accepted:
return OCPP2_1.AuthorizationStatusEnumType.Accepted;
case AuthorizationStatusEnum.Blocked:
return OCPP2_1.AuthorizationStatusEnumType.Blocked;
case AuthorizationStatusEnum.ConcurrentTx:
return OCPP2_1.AuthorizationStatusEnumType.ConcurrentTx;
case AuthorizationStatusEnum.Expired:
return OCPP2_1.AuthorizationStatusEnumType.Expired;
case AuthorizationStatusEnum.Invalid:
return OCPP2_1.AuthorizationStatusEnumType.Invalid;
case AuthorizationStatusEnum.NoCredit:
return OCPP2_1.AuthorizationStatusEnumType.NoCredit;
case AuthorizationStatusEnum.NotAllowedTypeEVSE:
return OCPP2_1.AuthorizationStatusEnumType.NotAllowedTypeEVSE;
case AuthorizationStatusEnum.NotAtThisLocation:
return OCPP2_1.AuthorizationStatusEnumType.NotAtThisLocation;
case AuthorizationStatusEnum.NotAtThisTime:
return OCPP2_1.AuthorizationStatusEnumType.NotAtThisTime;
case AuthorizationStatusEnum.Unknown:
return OCPP2_1.AuthorizationStatusEnumType.Unknown;
default:
throw new Error('Unknown authorization status: ' + status);
}
}
static toAuthorizationStatusEnumType(
status: OCPP2_1.AuthorizationStatusEnumType,
): AuthorizationStatusEnumType {
switch (status) {
case OCPP2_1.AuthorizationStatusEnumType.Accepted:
return AuthorizationStatusEnum.Accepted;
case OCPP2_1.AuthorizationStatusEnumType.Blocked:
return AuthorizationStatusEnum.Blocked;
case OCPP2_1.AuthorizationStatusEnumType.ConcurrentTx:
return AuthorizationStatusEnum.ConcurrentTx;
case OCPP2_1.AuthorizationStatusEnumType.Expired:
return AuthorizationStatusEnum.Expired;
case OCPP2_1.AuthorizationStatusEnumType.Invalid:
return AuthorizationStatusEnum.Invalid;
case OCPP2_1.AuthorizationStatusEnumType.NoCredit:
return AuthorizationStatusEnum.NoCredit;
case OCPP2_1.AuthorizationStatusEnumType.NotAllowedTypeEVSE:
return AuthorizationStatusEnum.NotAllowedTypeEVSE;
case OCPP2_1.AuthorizationStatusEnumType.NotAtThisLocation:
return AuthorizationStatusEnum.NotAtThisLocation;
case OCPP2_1.AuthorizationStatusEnumType.NotAtThisTime:
return AuthorizationStatusEnum.NotAtThisTime;
case OCPP2_1.AuthorizationStatusEnumType.Unknown:
return AuthorizationStatusEnum.Unknown;
default:
throw new Error('Unknown authorization status');
}
}
static toIdTokenEnumType(type: IdTokenEnumType): OCPP2_1.IdTokenEnumType {
switch (type) {
case IdTokenEnum.Central:
return OCPP2_1.IdTokenEnumType.Central;
case IdTokenEnum.DirectPayment:
return OCPP2_1.IdTokenEnumType.DirectPayment;
case IdTokenEnum.eMAID:
return OCPP2_1.IdTokenEnumType.eMAID;
case IdTokenEnum.EVCCID:
return OCPP2_1.IdTokenEnumType.EVCCID;
case IdTokenEnum.ISO14443:
return OCPP2_1.IdTokenEnumType.ISO14443;
case IdTokenEnum.ISO15693:
return OCPP2_1.IdTokenEnumType.ISO15693;
case IdTokenEnum.KeyCode:
return OCPP2_1.IdTokenEnumType.KeyCode;
case IdTokenEnum.Local:
return OCPP2_1.IdTokenEnumType.Local;
case IdTokenEnum.MacAddress:
return OCPP2_1.IdTokenEnumType.MacAddress;
case IdTokenEnum.NoAuthorization:
return OCPP2_1.IdTokenEnumType.NoAuthorization;
case IdTokenEnum.VIN:
return OCPP2_1.IdTokenEnumType.VIN;
default:
throw new Error(`Unknown idToken type: ${type}`);
}
}
static fromIdTokenEnumType(type: OCPP2_1.IdTokenEnumType): IdTokenEnumType {
switch (type) {
case OCPP2_1.IdTokenEnumType.Central:
return IdTokenEnum.Central;
case OCPP2_1.IdTokenEnumType.DirectPayment:
return IdTokenEnum.DirectPayment;
case OCPP2_1.IdTokenEnumType.eMAID:
return IdTokenEnum.eMAID;
case OCPP2_1.IdTokenEnumType.EVCCID:
return IdTokenEnum.EVCCID;
case OCPP2_1.IdTokenEnumType.ISO14443:
return IdTokenEnum.ISO14443;
case OCPP2_1.IdTokenEnumType.ISO15693:
return IdTokenEnum.ISO15693;
case OCPP2_1.IdTokenEnumType.KeyCode:
return IdTokenEnum.KeyCode;
case OCPP2_1.IdTokenEnumType.Local:
return IdTokenEnum.Local;
case OCPP2_1.IdTokenEnumType.MacAddress:
return IdTokenEnum.MacAddress;
case OCPP2_1.IdTokenEnumType.NoAuthorization:
return IdTokenEnum.NoAuthorization;
case OCPP2_1.IdTokenEnumType.VIN:
return IdTokenEnum.VIN;
default:
throw new Error(`Unknown OCPP 2.1 idToken type: ${type}`);
}
}
}

View File

@@ -0,0 +1,69 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type {
TariffEnergyType,
TariffFixedType,
TariffTimeType,
PriceType,
MessageContentType,
} from '@citrineos/base';
import { OCPP2_1 } from '@citrineos/base';
import { Tariff } from '../../model/Tariff/Tariffs.js';
export class TariffMapper {
/**
* Maps a {@link Tariff} DB model to an OCPP 2.1 {@link OCPP2_1.TariffType}.
*
* - `tariffId` falls back to the DB primary key string when not explicitly set.
* - All complex fields (`energy`, `chargingTime`, etc.) are stored as JSONB and
* passed through directly; their structure is validated at the OCPP message boundary.
*
* @throws {Error} if `currency` is missing (required by spec).
*/
static toTariffType(tariff: Tariff): OCPP2_1.TariffType {
if (!tariff.currency) {
throw new Error(`Tariff id=${tariff.id} is missing required field: currency`);
}
return {
tariffId: tariff.tariffId ?? String(tariff.id),
currency: tariff.currency,
validFrom: tariff.validFrom ?? undefined,
description: TariffMapper.toDescription(tariff.description),
energy: TariffMapper.toEnergyType(tariff.energy),
chargingTime: TariffMapper.toTimeType(tariff.chargingTime),
idleTime: TariffMapper.toTimeType(tariff.idleTime),
fixedFee: TariffMapper.toFixedType(tariff.fixedFee),
reservationTime: TariffMapper.toTimeType(tariff.reservationTime),
reservationFixed: TariffMapper.toFixedType(tariff.reservationFixed),
minCost: TariffMapper.toPriceType(tariff.minCost),
maxCost: TariffMapper.toPriceType(tariff.maxCost),
};
}
static toDescription(
description: MessageContentType[] | null | undefined,
): OCPP2_1.TariffType['description'] | undefined {
return (description as OCPP2_1.TariffType['description']) ?? undefined;
}
static toEnergyType(
energy: TariffEnergyType | null | undefined,
): OCPP2_1.TariffEnergyType | undefined {
return (energy as OCPP2_1.TariffEnergyType) ?? undefined;
}
static toTimeType(time: TariffTimeType | null | undefined): OCPP2_1.TariffTimeType | undefined {
return (time as OCPP2_1.TariffTimeType) ?? undefined;
}
static toFixedType(
fixed: TariffFixedType | null | undefined,
): OCPP2_1.TariffFixedType | undefined {
return (fixed as OCPP2_1.TariffFixedType) ?? undefined;
}
static toPriceType(price: PriceType | null | undefined): OCPP2_1.PriceType | undefined {
return (price as OCPP2_1.PriceType) ?? undefined;
}
}

View File

@@ -0,0 +1,6 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
export { AuthorizationMapper } from './AuthorizationMapper.js';
export { TariffMapper } from './TariffMapper.js';

View File

@@ -0,0 +1,258 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import {
LocationEnum,
MeasurandEnum,
OCPP2_0_1,
OCPP2_1,
OCPP2_common_types,
PhaseEnum,
ReadingContextEnum,
type MeterValueDto,
type SampledValue,
} from '@citrineos/base';
export class MeterValueMapper {
static fromMeterValueType(meterValueType: OCPP2_common_types.MeterValueType): MeterValueDto {
return {
timestamp: meterValueType.timestamp,
sampledValue: MeterValueMapper.fromSampledValueTypes(
meterValueType.sampledValue as [
OCPP2_common_types.SampledValueType,
...OCPP2_common_types.SampledValueType[],
],
),
};
}
static fromSampledValueTypes(
sampledValueTypes: [
OCPP2_common_types.SampledValueType,
...OCPP2_common_types.SampledValueType[],
],
): [SampledValue, ...SampledValue[]] {
if (!Array.isArray(sampledValueTypes) || sampledValueTypes.length === 0) {
throw new Error(`Invalid sampledValueTypes: ${JSON.stringify(sampledValueTypes)}`);
}
const sampledValues: SampledValue[] = [];
for (const sv of sampledValueTypes) {
const sampledValue: SampledValue = {
value: sv.value,
context: MeterValueMapper.fromReadingContextEnumType(sv.context),
measurand: MeterValueMapper.fromMeasurandEnumType(sv.measurand),
phase: MeterValueMapper.fromPhaseEnumType(sv.phase),
location: MeterValueMapper.fromLocationEnumType(sv.location),
};
if (sv.signedMeterValue) {
sampledValue.signedMeterValue = {
signedMeterData: sv.signedMeterValue.signedMeterData,
// 2.1 makes signingMethod/publicKey optional; fall back to empty string to satisfy internal schema
signingMethod: sv.signedMeterValue.signingMethod ?? '',
encodingMethod: sv.signedMeterValue.encodingMethod,
publicKey: sv.signedMeterValue.publicKey ?? '',
};
}
if (sv.unitOfMeasure) {
sampledValue.unitOfMeasure = {
unit:
sv.unitOfMeasure.unit ||
(sampledValue.measurand?.startsWith('Energy') ? 'Wh' : undefined),
multiplier: sv.unitOfMeasure.multiplier || 0,
};
}
sampledValues.push(sampledValue);
}
return sampledValues as [SampledValue, ...SampledValue[]];
}
static fromReadingContextEnumType(
context?: OCPP2_0_1.ReadingContextEnumType | OCPP2_1.ReadingContextEnumType | null,
): keyof typeof ReadingContextEnum | undefined {
if (!context) return undefined;
switch (context) {
case OCPP2_0_1.ReadingContextEnumType.Interruption_Begin:
case OCPP2_1.ReadingContextEnumType.Interruption_Begin:
return 'Interruption.Begin';
case OCPP2_0_1.ReadingContextEnumType.Interruption_End:
case OCPP2_1.ReadingContextEnumType.Interruption_End:
return 'Interruption.End';
case OCPP2_0_1.ReadingContextEnumType.Other:
case OCPP2_1.ReadingContextEnumType.Other:
return 'Other';
case OCPP2_0_1.ReadingContextEnumType.Sample_Clock:
case OCPP2_1.ReadingContextEnumType.Sample_Clock:
return 'Sample.Clock';
case OCPP2_0_1.ReadingContextEnumType.Sample_Periodic:
case OCPP2_1.ReadingContextEnumType.Sample_Periodic:
return 'Sample.Periodic';
case OCPP2_0_1.ReadingContextEnumType.Transaction_Begin:
case OCPP2_1.ReadingContextEnumType.Transaction_Begin:
return 'Transaction.Begin';
case OCPP2_0_1.ReadingContextEnumType.Transaction_End:
case OCPP2_1.ReadingContextEnumType.Transaction_End:
return 'Transaction.End';
case OCPP2_0_1.ReadingContextEnumType.Trigger:
case OCPP2_1.ReadingContextEnumType.Trigger:
return 'Trigger';
default:
return 'Sample.Periodic';
}
}
static fromMeasurandEnumType(
measurand?: OCPP2_0_1.MeasurandEnumType | OCPP2_1.MeasurandEnumType | null,
): keyof typeof MeasurandEnum | undefined {
if (!measurand) return undefined;
switch (measurand) {
case OCPP2_0_1.MeasurandEnumType.Current_Export:
case OCPP2_1.MeasurandEnumType.Current_Export:
return 'Current.Export';
case OCPP2_0_1.MeasurandEnumType.Current_Import:
case OCPP2_1.MeasurandEnumType.Current_Import:
return 'Current.Import';
case OCPP2_0_1.MeasurandEnumType.Current_Offered:
case OCPP2_1.MeasurandEnumType.Current_Offered:
return 'Current.Offered';
case OCPP2_0_1.MeasurandEnumType.Energy_Active_Export_Register:
case OCPP2_1.MeasurandEnumType.Energy_Active_Export_Register:
return 'Energy.Active.Export.Register';
case OCPP2_0_1.MeasurandEnumType.Energy_Active_Import_Register:
case OCPP2_1.MeasurandEnumType.Energy_Active_Import_Register:
return 'Energy.Active.Import.Register';
case OCPP2_0_1.MeasurandEnumType.Energy_Reactive_Export_Register:
case OCPP2_1.MeasurandEnumType.Energy_Reactive_Export_Register:
return 'Energy.Reactive.Export.Register';
case OCPP2_0_1.MeasurandEnumType.Energy_Reactive_Import_Register:
case OCPP2_1.MeasurandEnumType.Energy_Reactive_Import_Register:
return 'Energy.Reactive.Import.Register';
case OCPP2_0_1.MeasurandEnumType.Energy_Active_Export_Interval:
case OCPP2_1.MeasurandEnumType.Energy_Active_Export_Interval:
return 'Energy.Active.Export.Interval';
case OCPP2_0_1.MeasurandEnumType.Energy_Active_Import_Interval:
case OCPP2_1.MeasurandEnumType.Energy_Active_Import_Interval:
return 'Energy.Active.Import.Interval';
case OCPP2_0_1.MeasurandEnumType.Energy_Active_Net:
case OCPP2_1.MeasurandEnumType.Energy_Active_Net:
return 'Energy.Active.Net';
case OCPP2_0_1.MeasurandEnumType.Energy_Reactive_Export_Interval:
case OCPP2_1.MeasurandEnumType.Energy_Reactive_Export_Interval:
return 'Energy.Reactive.Export.Interval';
case OCPP2_0_1.MeasurandEnumType.Energy_Reactive_Import_Interval:
case OCPP2_1.MeasurandEnumType.Energy_Reactive_Import_Interval:
return 'Energy.Reactive.Import.Interval';
case OCPP2_0_1.MeasurandEnumType.Energy_Reactive_Net:
case OCPP2_1.MeasurandEnumType.Energy_Reactive_Net:
return 'Energy.Reactive.Net';
case OCPP2_0_1.MeasurandEnumType.Energy_Apparent_Net:
case OCPP2_1.MeasurandEnumType.Energy_Apparent_Net:
return 'Energy.Apparent.Net';
case OCPP2_0_1.MeasurandEnumType.Energy_Apparent_Import:
case OCPP2_1.MeasurandEnumType.Energy_Apparent_Import:
return 'Energy.Apparent.Import';
case OCPP2_0_1.MeasurandEnumType.Energy_Apparent_Export:
case OCPP2_1.MeasurandEnumType.Energy_Apparent_Export:
return 'Energy.Apparent.Export';
case OCPP2_0_1.MeasurandEnumType.Frequency:
case OCPP2_1.MeasurandEnumType.Frequency:
return 'Frequency';
case OCPP2_0_1.MeasurandEnumType.Power_Active_Export:
case OCPP2_1.MeasurandEnumType.Power_Active_Export:
return 'Power.Active.Export';
case OCPP2_0_1.MeasurandEnumType.Power_Active_Import:
case OCPP2_1.MeasurandEnumType.Power_Active_Import:
return 'Power.Active.Import';
case OCPP2_0_1.MeasurandEnumType.Power_Factor:
case OCPP2_1.MeasurandEnumType.Power_Factor:
return 'Power.Factor';
case OCPP2_0_1.MeasurandEnumType.Power_Offered:
case OCPP2_1.MeasurandEnumType.Power_Offered:
return 'Power.Offered';
case OCPP2_0_1.MeasurandEnumType.Power_Reactive_Export:
case OCPP2_1.MeasurandEnumType.Power_Reactive_Export:
return 'Power.Reactive.Export';
case OCPP2_0_1.MeasurandEnumType.Power_Reactive_Import:
case OCPP2_1.MeasurandEnumType.Power_Reactive_Import:
return 'Power.Reactive.Import';
case OCPP2_0_1.MeasurandEnumType.SoC:
case OCPP2_1.MeasurandEnumType.SoC:
return 'SoC';
case OCPP2_0_1.MeasurandEnumType.Voltage:
case OCPP2_1.MeasurandEnumType.Voltage:
return 'Voltage';
default:
return 'Energy.Active.Import.Register';
}
}
static fromPhaseEnumType(
phase?: OCPP2_0_1.PhaseEnumType | OCPP2_1.PhaseEnumType | null,
): keyof typeof PhaseEnum | undefined {
if (!phase) return undefined;
switch (phase) {
case OCPP2_0_1.PhaseEnumType.L1:
case OCPP2_1.PhaseEnumType.L1:
return 'L1';
case OCPP2_0_1.PhaseEnumType.L2:
case OCPP2_1.PhaseEnumType.L2:
return 'L2';
case OCPP2_0_1.PhaseEnumType.L3:
case OCPP2_1.PhaseEnumType.L3:
return 'L3';
case OCPP2_0_1.PhaseEnumType.N:
case OCPP2_1.PhaseEnumType.N:
return 'N';
case OCPP2_0_1.PhaseEnumType.L1_N:
case OCPP2_1.PhaseEnumType.L1_N:
return 'L1-N';
case OCPP2_0_1.PhaseEnumType.L2_N:
case OCPP2_1.PhaseEnumType.L2_N:
return 'L2-N';
case OCPP2_0_1.PhaseEnumType.L3_N:
case OCPP2_1.PhaseEnumType.L3_N:
return 'L3-N';
case OCPP2_0_1.PhaseEnumType.L1_L2:
case OCPP2_1.PhaseEnumType.L1_L2:
return 'L1-L2';
case OCPP2_0_1.PhaseEnumType.L2_L3:
case OCPP2_1.PhaseEnumType.L2_L3:
return 'L2-L3';
case OCPP2_0_1.PhaseEnumType.L3_L1:
case OCPP2_1.PhaseEnumType.L3_L1:
return 'L3-L1';
default:
return undefined;
}
}
static fromLocationEnumType(
location?: OCPP2_0_1.LocationEnumType | OCPP2_1.LocationEnumType | null,
): keyof typeof LocationEnum | undefined {
if (!location) return undefined;
switch (location) {
case OCPP2_0_1.LocationEnumType.Body:
case OCPP2_1.LocationEnumType.Body:
return 'Body';
case OCPP2_0_1.LocationEnumType.Cable:
case OCPP2_1.LocationEnumType.Cable:
return 'Cable';
case OCPP2_0_1.LocationEnumType.EV:
case OCPP2_1.LocationEnumType.EV:
return 'EV';
case OCPP2_0_1.LocationEnumType.Inlet:
case OCPP2_1.LocationEnumType.Inlet:
return 'Inlet';
case OCPP2_0_1.LocationEnumType.Outlet:
case OCPP2_1.LocationEnumType.Outlet:
return 'Outlet';
default:
return 'Outlet';
}
}
}

View File

@@ -0,0 +1,131 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type { TenantDto, TenantPartnerDto } from '@citrineos/base';
import { type AsyncJobNameEnumType, DEFAULT_TENANT_ID } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
Default,
ForeignKey,
Model,
PrimaryKey,
Table,
} from 'sequelize-typescript';
import { v4 as uuidv4 } from 'uuid';
import { Tenant } from '../Tenant.js';
import { TenantPartner } from '../TenantPartner.js';
export interface PaginatedParams {
offset?: number;
limit?: number;
dateFrom?: Date;
dateTo?: Date;
}
@Table
export class AsyncJobStatus extends Model {
static readonly MODEL_NAME: string = 'AsyncJobStatus';
@PrimaryKey
@Default(() => uuidv4()) // Automatically generate jobId
@Column(DataType.STRING)
declare jobId: string;
@Column(DataType.STRING)
declare jobName: AsyncJobNameEnumType;
@ForeignKey(() => TenantPartner)
@Column(DataType.INTEGER)
declare tenantPartnerId: number;
@BelongsTo(() => TenantPartner, { foreignKey: 'tenantPartnerId', as: 'asyncJobTenantPartner' })
declare tenantPartner: TenantPartnerDto;
@Column(DataType.DATE)
declare finishedAt?: Date;
@Column(DataType.DATE)
declare stoppedAt?: Date | null;
@Default(false)
@Column(DataType.BOOLEAN)
declare stopScheduled: boolean;
@Default(false)
@Column(DataType.BOOLEAN)
declare isFailed: boolean;
@Column(DataType.JSON)
declare paginationParams: PaginatedParams;
@Column(DataType.INTEGER) // Total number of objects in the client's system
declare totalObjects?: number;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: AsyncJobStatus) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
toDTO(): AsyncJobStatusDTO {
return {
jobId: this.jobId,
jobName: this.jobName,
tenantPartnerId: this.tenantPartnerId,
tenantPartner: this.tenantPartner,
createdAt: this.createdAt,
finishedAt: this.finishedAt,
stoppedAt: this.stoppedAt,
stopScheduled: this.stopScheduled,
isFailed: this.isFailed,
paginatedParams: this.paginationParams,
totalObjects: this.totalObjects,
};
}
}
export class AsyncJobStatusDTO {
jobId!: string;
jobName!: AsyncJobNameEnumType;
tenantPartnerId!: number;
tenantPartner?: TenantPartnerDto;
createdAt!: Date;
finishedAt?: Date;
stoppedAt?: Date | null;
stopScheduled!: boolean;
isFailed?: boolean;
paginatedParams!: PaginatedParams;
totalObjects?: number;
}
export class AsyncJobRequest {
tenantPartnerId!: number;
paginatedParams!: PaginatedParams;
}

View File

@@ -0,0 +1,5 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
export * from './AsyncJobStatus.js';

View File

@@ -0,0 +1,164 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type {
AdditionalInfo,
AuthorizationDto,
AuthorizationStatusEnumType,
AuthorizationWhitelistEnumType,
IdTokenEnumType,
RealTimeAuthLastAttempt,
TariffDto,
TenantDto,
TenantPartnerDto,
TransactionDto,
} from '@citrineos/base';
import { DEFAULT_TENANT_ID, Namespace } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
Default,
ForeignKey,
HasMany,
Model,
Table,
} from 'sequelize-typescript';
import { Tenant } from '../Tenant.js';
import { TenantPartner } from '../TenantPartner.js';
import { Transaction } from '../TransactionEvent/Transaction.js';
import { Tariff } from '../Tariff/Tariffs.js';
@Table
export class Authorization extends Model implements AuthorizationDto {
static readonly MODEL_NAME: string = Namespace.AuthorizationData;
@Column(DataType.ARRAY(DataType.STRING))
declare allowedConnectorTypes?: string[];
@Column(DataType.ARRAY(DataType.STRING))
declare disallowedEvseIdPrefixes?: string[];
@Column({
type: DataType.CITEXT,
unique: 'idToken_type',
})
declare idToken: string;
@Column({
type: DataType.STRING,
unique: 'idToken_type',
})
declare idTokenType?: IdTokenEnumType | null;
@Column(DataType.JSONB)
declare additionalInfo?: [AdditionalInfo, ...AdditionalInfo[]] | null; // JSONB for AdditionalInfo
@Column(DataType.STRING)
declare status: AuthorizationStatusEnumType;
@Column({
type: DataType.DATE,
get() {
return this.getDataValue('cacheExpiryDateTime')?.toISOString();
},
})
declare cacheExpiryDateTime?: string | null;
@Column(DataType.INTEGER)
declare chargingPriority?: number | null;
@Column(DataType.STRING)
declare language1?: string | null;
@Column(DataType.STRING)
declare language2?: string | null;
@Column(DataType.JSON)
declare personalMessage?: any | null;
@Column(DataType.STRING)
declare realTimeAuth?: AuthorizationWhitelistEnumType | null;
@Column(DataType.JSONB)
declare realTimeAuthLastAttempt?: RealTimeAuthLastAttempt | null;
@Column(DataType.INTEGER)
declare realTimeAuthTimeout?: number | null;
@Column(DataType.STRING)
declare realTimeAuthUrl?: string;
// Reference to another Authorization for groupAuthorization
@ForeignKey(() => Authorization)
@Column(DataType.INTEGER)
declare groupAuthorizationId?: number | null;
@ForeignKey(() => Tariff)
@Column(DataType.INTEGER)
declare tariffId?: number | null;
@BelongsTo(() => Authorization, { foreignKey: 'groupAuthorizationId', as: 'groupAuthorization' })
declare groupAuthorization?: Authorization;
@BelongsTo(() => Tariff, { foreignKey: 'tariffId', as: 'tariff' })
declare tariff?: TariffDto | null;
@Default(false)
@Column(DataType.BOOLEAN)
declare concurrentTransaction?: boolean;
@Default(false)
@Column(DataType.BOOLEAN)
declare isPrepaid?: boolean;
@Column(DataType.DECIMAL)
declare prepaidBalance?: number | null;
declare customData?: any | null;
// For cases where Authorization is owned by an upstream partner, i.e. an eMSP
@ForeignKey(() => TenantPartner)
@Column(DataType.INTEGER)
declare tenantPartnerId?: number | null;
@BelongsTo(() => TenantPartner, {
foreignKey: 'tenantPartnerId',
as: 'authTenantPartnerAuthorization',
})
declare tenantPartner?: TenantPartnerDto | null;
@HasMany(() => Transaction, 'authorizationId')
declare transactions?: TransactionDto[];
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
unique: 'idToken_type',
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: Authorization) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,126 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { DEFAULT_TENANT_ID } from '@citrineos/base';
import type {
TenantDto,
AuthorizationDto,
LocalListVersionDto,
SendLocalListDto,
} from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
BelongsToMany,
Column,
DataType,
ForeignKey,
Model,
Table,
} from 'sequelize-typescript';
import { type AuthorizationRestrictions } from '@dal/interfaces/projections/AuthorizationRestrictions.js';
import { Tenant } from '../Tenant.js';
import { Authorization } from './Authorization.js';
import { SendLocalList } from './SendLocalList.js';
import { SendLocalListAuthorization } from './SendLocalListAuthorization.js';
import { LocalListVersion } from './LocalListVersion.js';
import { LocalListVersionAuthorization } from './LocalListVersionAuthorization.js';
/**
*
* This class represents static information about an authorization used in a local auth list.
* When a local auth list is put onto the charging station, the state of those authorizations is no longer tied to the actual authorization.
* Example: A charger receives a local auth list with Authorization id = 1 in it, but then Authorization id = 1 is deleted.
* Authorization id = 1 is still on the auth list and must be returned when upstream systems check the state of the auth list for that station, until a SendLocalListRequest removing it is successfully processed.
* To facilitate that, this collection exists to reflect the state of Authorizations as they exist on charging stations' local auth lists.
* In turn, the 'authorization' relation on this table links back to the "actual" authorization.
*
**/
@Table // implements the same as Authorization, not OCPP2_0_1.AuthorizationData
export class LocalListAuthorization extends Model implements AuthorizationRestrictions {
static readonly MODEL_NAME: string = 'LocalListAuthorization';
@Column(DataType.ARRAY(DataType.STRING))
declare allowedConnectorTypes?: string[];
@Column(DataType.ARRAY(DataType.STRING))
declare disallowedEvseIdPrefixes?: string[];
@Column(DataType.STRING)
declare idToken: string;
@Column(DataType.STRING)
declare idTokenType?: string | null;
@Column(DataType.JSONB)
declare additionalInfo?: any | null;
@Column(DataType.STRING)
declare status: string;
@Column(DataType.DATE)
declare cacheExpiryDateTime?: string | null;
@Column(DataType.INTEGER)
declare chargingPriority?: number | null;
@Column(DataType.STRING)
declare language1?: string | null;
@Column(DataType.STRING)
declare language2?: string | null;
@Column(DataType.JSON)
declare personalMessage?: any | null;
@ForeignKey(() => Authorization)
@Column(DataType.INTEGER)
declare groupAuthorizationId?: number | null;
@BelongsTo(() => Authorization, { foreignKey: 'groupAuthorizationId', as: 'groupAuth' })
declare groupAuthorization?: AuthorizationDto;
@ForeignKey(() => Authorization)
@Column(DataType.INTEGER)
declare authorizationId?: string;
@BelongsTo(() => Authorization, { foreignKey: 'authorizationId', as: 'authorization' })
declare authorization?: AuthorizationDto;
@BelongsToMany(() => SendLocalList, () => SendLocalListAuthorization)
declare sendLocalLists?: SendLocalListDto[];
@BelongsToMany(() => LocalListVersion, () => LocalListVersionAuthorization)
declare localListVersions?: LocalListVersionDto[];
declare customData?: any | null;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: LocalListAuthorization) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,69 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { DEFAULT_TENANT_ID, OCPP2_0_1, OCPP2_Namespace } from '@citrineos/base';
import type { TenantDto, LocalListAuthorizationDto } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
BelongsToMany,
Column,
DataType,
ForeignKey,
Model,
Table,
} from 'sequelize-typescript';
import { Tenant } from '../Tenant.js';
import { LocalListAuthorization } from './LocalListAuthorization.js';
import { LocalListVersionAuthorization } from './LocalListVersionAuthorization.js';
@Table
export class LocalListVersion extends Model {
static readonly MODEL_NAME: string = OCPP2_Namespace.LocalListVersion;
@Column({
type: DataType.STRING,
unique: 'stationName_tenantId',
})
declare ocppConnectionName: string;
@Column(DataType.INTEGER)
declare versionNumber: number;
@BelongsToMany(() => LocalListAuthorization, () => LocalListVersionAuthorization)
declare localAuthorizationList?:
| [LocalListAuthorizationDto, ...LocalListAuthorizationDto[]]
| undefined;
customData?: OCPP2_0_1.CustomDataType | null | undefined;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
unique: 'stationName_tenantId',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: LocalListVersion) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type { TenantDto } from '@citrineos/base';
import { DEFAULT_TENANT_ID } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
Model,
Table,
} from 'sequelize-typescript';
import { Tenant } from '../Tenant.js';
import { LocalListAuthorization, LocalListVersion } from './index.js';
@Table
export class LocalListVersionAuthorization extends Model {
// Namespace enum not used as this is not a model required by CitrineOS
static readonly MODEL_NAME: string = 'LocalListVersionAuthorization';
@ForeignKey(() => LocalListVersion)
@Column(DataType.INTEGER)
declare localListVersionId: number;
@ForeignKey(() => LocalListAuthorization)
@Column(DataType.INTEGER)
declare authorizationId: number;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: LocalListVersionAuthorization) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,101 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { DEFAULT_TENANT_ID, OCPP2_0_1, OCPP2_Namespace } from '@citrineos/base';
import type { TenantDto, LocalListAuthorizationDto } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
BelongsToMany,
Column,
DataType,
ForeignKey,
Model,
Table,
} from 'sequelize-typescript';
import { Tenant } from '../Tenant.js';
import { LocalListAuthorization } from './LocalListAuthorization.js';
import { SendLocalListAuthorization } from './SendLocalListAuthorization.js';
@Table
export class SendLocalList extends Model implements OCPP2_0_1.SendLocalListRequest {
static readonly MODEL_NAME: string = OCPP2_Namespace.SendLocalListRequest;
@Column(DataType.STRING)
declare ocppConnectionName: string;
@Column(DataType.STRING)
declare correlationId: string;
@Column(DataType.INTEGER)
declare versionNumber: number;
@Column(DataType.STRING)
declare updateType: OCPP2_0_1.UpdateEnumType;
// ORM relation: LocalListAuthorization[]; API contract: AuthorizationData[]
@BelongsToMany(() => LocalListAuthorization, () => SendLocalListAuthorization)
declare localAuthorizationList?: any;
customData?: OCPP2_0_1.CustomDataType | null | undefined;
toSendLocalListRequest(): OCPP2_0_1.SendLocalListRequest {
const localAuthList = (this.localAuthorizationList || [])
.map((localListAuth: LocalListAuthorizationDto) => {
return {
idToken: {
idToken: String(localListAuth.idToken), // ensure string
type: localListAuth.idTokenType,
additionalInfo: localListAuth.additionalInfo,
},
idTokenInfo: {
status: localListAuth.status,
cacheExpiryDateTime: localListAuth.cacheExpiryDateTime,
chargingPriority: localListAuth.chargingPriority,
language1: localListAuth.language1,
groupIdToken: localListAuth.groupAuthorizationId,
language2: localListAuth.language2,
personalMessage: localListAuth.personalMessage,
},
} as OCPP2_0_1.AuthorizationData;
})
.filter(Boolean);
return {
versionNumber: this.versionNumber,
updateType: this.updateType,
localAuthorizationList:
localAuthList.length > 0
? (localAuthList as [OCPP2_0_1.AuthorizationData, ...OCPP2_0_1.AuthorizationData[]])
: null,
};
}
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: SendLocalList) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type { TenantDto } from '@citrineos/base';
import { DEFAULT_TENANT_ID } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
Model,
Table,
} from 'sequelize-typescript';
import { Tenant } from '../Tenant.js';
import { LocalListAuthorization, SendLocalList } from './index.js';
@Table
export class SendLocalListAuthorization extends Model {
// Namespace enum not used as this is not a model required by CitrineOS
static readonly MODEL_NAME: string = 'SendLocalListAuthorization';
@ForeignKey(() => SendLocalList)
@Column(DataType.INTEGER)
declare sendLocalListId: number;
@ForeignKey(() => LocalListAuthorization)
@Column(DataType.INTEGER)
declare authorizationId: number;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: SendLocalListAuthorization) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
export { Authorization } from './Authorization.js';
export { LocalListVersion } from './LocalListVersion.js';
export { SendLocalList } from './SendLocalList.js';
export { LocalListAuthorization } from './LocalListAuthorization.js';
export { LocalListVersionAuthorization } from './LocalListVersionAuthorization.js';
export { SendLocalListAuthorization } from './SendLocalListAuthorization.js';

View File

@@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
// Internal types for Authorization models to break circular dependencies
export interface ITenant {
id: number;
name: string;
}
export interface ITenantPartner {
id: number;
name: string;
}

View File

@@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { DEFAULT_TENANT_ID } from '@citrineos/base';
import type { TenantDto } from '@citrineos/base';
import { BeforeCreate, BeforeUpdate, Column, DataType, Model } from 'sequelize-typescript';
export abstract class BaseModelWithTenant<
TModelAttributes extends {} = any,
TCreationAttributes extends {} = TModelAttributes,
> extends Model<TModelAttributes, TCreationAttributes> {
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE', // update tenantId if the tenant primary key is updated (should never happen)
onDelete: 'RESTRICT', // ensure tenant row cannot be deleted if there are existing records using it
})
declare tenantId: number;
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: BaseModelWithTenant) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,103 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type { BootDto, TenantDto, VariableAttributeDto } from '@citrineos/base';
import { DEFAULT_TENANT_ID, Namespace } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
HasMany,
Model,
PrimaryKey,
Table,
} from 'sequelize-typescript';
import { VariableAttribute } from './DeviceModel/VariableAttribute.js';
import { Tenant } from './Tenant.js';
@Table
export class Boot extends Model implements BootDto {
static readonly MODEL_NAME: string = Namespace.BootConfig;
/**
* StationId
*/
@PrimaryKey
@Column(DataType.STRING)
declare id: string;
@Column({
type: DataType.DATE,
get() {
const lastBootTimeValue = this.getDataValue('lastBootTime');
return lastBootTimeValue ? lastBootTimeValue.toISOString() : null;
},
})
declare lastBootTime?: string | null;
@Column(DataType.INTEGER)
declare heartbeatInterval?: number | null;
@Column(DataType.INTEGER)
declare bootRetryInterval?: number | null;
@Column(DataType.STRING)
declare status: string;
@Column(DataType.JSON)
declare statusInfo?: object | null;
@Column(DataType.BOOLEAN)
declare getBaseReportOnPending?: boolean | null;
/**
* Variable attributes to be sent in SetVariablesRequest on pending boot
*/
@HasMany(() => VariableAttribute, 'bootConfigId')
declare pendingBootSetVariables?: VariableAttributeDto[];
@Column(DataType.JSON)
declare variablesRejectedOnLastBoot?: object[] | null;
@Column(DataType.BOOLEAN)
declare bootWithRejectedVariables?: boolean | null;
@Column(DataType.BOOLEAN)
declare changeConfigurationsOnPending?: boolean | null;
@Column(DataType.BOOLEAN)
declare getConfigurationsOnPending?: boolean | null;
declare customData?: object | null;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: Boot) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,126 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type { CertificateDto, CountryName, SignatureAlgorithm, TenantDto } from '@citrineos/base';
import { DEFAULT_TENANT_ID, OCPP2_Namespace } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
HasMany,
Model,
Table,
} from 'sequelize-typescript';
import { Tenant } from '../Tenant.js';
@Table({
indexes: [
{
unique: true,
fields: ['tenantId', 'serialNumber', 'issuerName'],
name: 'tenantId_serialNumber_issuerName',
},
{
unique: true,
fields: ['tenantId', 'certificateFileHash'],
name: 'tenantId_certificateFileHash',
},
],
})
export class Certificate extends Model implements CertificateDto {
static readonly MODEL_NAME: string = OCPP2_Namespace.Certificate;
/**
* Fields
*/
// use serialNumber and issuerName as unique constraint based on 4.1.2.2 in https://www.rfc-editor.org/rfc/rfc5280
@Column(DataType.BIGINT)
declare serialNumber: number;
@Column(DataType.STRING)
declare issuerName: string;
@Column(DataType.STRING)
declare organizationName: string;
@Column(DataType.STRING)
declare commonName: string;
@Column(DataType.INTEGER)
declare keyLength?: number | null;
@Column({
type: DataType.DATE,
get() {
const validBefore: Date = this.getDataValue('validBefore');
return validBefore ? validBefore.toISOString() : null;
},
})
declare validBefore?: string | null;
@Column(DataType.STRING)
declare signatureAlgorithm?: SignatureAlgorithm | null;
@Column(DataType.STRING)
declare countryName?: CountryName | null;
@Column(DataType.BOOLEAN)
declare isCA?: boolean;
// A pathLenConstraint of zero indicates that no intermediate CA certificates may
// follow in a valid certification path. Where it appears, the pathLenConstraint field MUST be greater than or
// equal to zero. Where pathLenConstraint does not appear, no limit is imposed.
// Reference: https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.9
@Column(DataType.INTEGER)
declare pathLen?: number | null;
@Column(DataType.STRING)
declare certificateFileId?: string | null;
@Column(DataType.STRING)
declare certificateFileHash?: string | null;
@Column(DataType.STRING)
declare privateKeyFileId?: string | null;
@ForeignKey(() => Certificate)
@Column(DataType.INTEGER)
declare signedBy?: number | null; // certificate id
@BelongsTo(() => Certificate, { foreignKey: 'signedBy', as: 'signingCertificate' })
declare signingCertificate?: Certificate;
@HasMany(() => Certificate, { foreignKey: 'signedBy', as: 'signedCertificates' })
declare signedCertificates?: Certificate[];
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: Certificate) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
export const enum SignatureAlgorithmEnumType {
RSA = 'SHA256withRSA',
ECDSA = 'SHA256withECDSA',
}
export const enum CountryNameEnumType {
US = 'US',
}

View File

@@ -0,0 +1,101 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
Model,
Table,
} from 'sequelize-typescript';
import {
DEFAULT_TENANT_ID,
OCPP2_Namespace,
type DeleteCertificateStatusEnumType,
type HashAlgorithmEnumType,
type TenantDto,
type ChargingStationDto,
} from '@citrineos/base';
import { ChargingStation } from '../Location/index.js';
import { Tenant } from '../Tenant.js';
@Table
export class DeleteCertificateAttempt extends Model {
static readonly MODEL_NAME: string = OCPP2_Namespace.DeleteCertificateAttempt;
@ForeignKey(() => ChargingStation)
@Column(DataType.INTEGER)
declare stationId?: number;
@Column({
type: DataType.STRING(36),
allowNull: false,
})
declare ocppConnectionName: string;
@BelongsTo(() => ChargingStation, 'stationId')
declare station?: ChargingStationDto;
@Column({
type: DataType.STRING,
allowNull: false,
})
declare hashAlgorithm: HashAlgorithmEnumType;
@Column(DataType.STRING)
declare issuerNameHash: string;
@Column(DataType.STRING)
declare issuerKeyHash: string;
@Column(DataType.STRING)
declare serialNumber: string;
@Column({
type: DataType.STRING,
})
declare status?: DeleteCertificateStatusEnumType | null;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeCreate
static async resolveStationId(instance: DeleteCertificateAttempt): Promise<void> {
if (instance.stationId == null && instance.ocppConnectionName && instance.tenantId != null) {
const station = await ChargingStation.findOne({
where: { ocppConnectionName: instance.ocppConnectionName, tenantId: instance.tenantId },
attributes: ['id'],
});
if (station) {
instance.stationId = station.id;
}
}
}
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: DeleteCertificateAttempt) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,111 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import {
DEFAULT_TENANT_ID,
OCPP2_Namespace,
type CertificateDto,
type CertificateUseEnumType,
type ChargingStationDto,
type InstallCertificateStatusEnumType,
type TenantDto,
} from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
Model,
Table,
} from 'sequelize-typescript';
import { ChargingStation } from '../Location/index.js';
import { Tenant } from '../Tenant.js';
import { Certificate } from './Certificate.js';
@Table
export class InstallCertificateAttempt extends Model {
static readonly MODEL_NAME: string = OCPP2_Namespace.InstallCertificateAttempt;
@ForeignKey(() => ChargingStation)
@Column(DataType.INTEGER)
declare stationId?: number;
@Column({
type: DataType.STRING(36),
allowNull: false,
})
declare ocppConnectionName: string;
@BelongsTo(() => ChargingStation, 'stationId')
declare station?: ChargingStationDto;
@Column({
type: DataType.STRING,
allowNull: false,
})
declare certificateType: CertificateUseEnumType;
@ForeignKey(() => Certificate)
@Column({
type: DataType.INTEGER,
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
})
declare certificateId: number;
@BelongsTo(() => Certificate, 'certificateId')
declare certificate?: CertificateDto;
@Column({
type: DataType.INTEGER,
allowNull: true,
})
declare requestId?: number | null;
@Column({
type: DataType.STRING,
})
declare status?: InstallCertificateStatusEnumType | null;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeCreate
static async resolveStationId(instance: InstallCertificateAttempt): Promise<void> {
if (instance.stationId == null && instance.ocppConnectionName && instance.tenantId != null) {
const station = await ChargingStation.findOne({
where: { ocppConnectionName: instance.ocppConnectionName, tenantId: instance.tenantId },
attributes: ['id'],
});
if (station) {
instance.stationId = station.id;
}
}
}
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: InstallCertificateAttempt) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,123 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type {
CertificateDto,
CertificateUseEnumType,
HashAlgorithmEnumType,
InstalledCertificateDto,
TenantDto,
} from '@citrineos/base';
import { DEFAULT_TENANT_ID, OCPP2_Namespace, type ChargingStationDto } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
Model,
Table,
} from 'sequelize-typescript';
import { ChargingStation } from '../Location/index.js';
import { Tenant } from '../Tenant.js';
import { Certificate } from './Certificate.js';
@Table
export class InstalledCertificate extends Model implements InstalledCertificateDto {
static readonly MODEL_NAME: string = OCPP2_Namespace.InstalledCertificate;
@ForeignKey(() => ChargingStation)
@Column({
type: DataType.INTEGER,
allowNull: true,
})
declare stationId?: number;
@Column({
type: DataType.STRING(36),
allowNull: false,
})
declare ocppConnectionName: string;
@Column({
type: DataType.STRING,
allowNull: true,
})
declare hashAlgorithm: HashAlgorithmEnumType;
@Column({
type: DataType.STRING,
allowNull: true,
})
declare issuerNameHash?: string | null;
@Column({
type: DataType.STRING,
allowNull: true,
})
declare issuerKeyHash?: string | null;
@Column({
type: DataType.STRING,
allowNull: true,
})
declare serialNumber?: string | null;
@Column({
type: DataType.STRING,
allowNull: false,
})
declare certificateType: CertificateUseEnumType;
@ForeignKey(() => Certificate)
@Column(DataType.INTEGER)
declare certificateId?: number | null;
@BelongsTo(() => Certificate, 'certificateId')
declare certificate?: CertificateDto;
@BelongsTo(() => ChargingStation, 'stationId')
declare station?: ChargingStationDto;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeCreate
static async resolveStationId(instance: InstalledCertificate): Promise<void> {
if (instance.stationId == null && instance.ocppConnectionName && instance.tenantId != null) {
const station = await ChargingStation.findOne({
where: { ocppConnectionName: instance.ocppConnectionName, tenantId: instance.tenantId },
attributes: ['id'],
});
if (station) {
instance.stationId = station.id;
}
}
}
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: InstalledCertificate) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,9 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
export { Certificate } from './Certificate.js';
export { InstalledCertificate } from './InstalledCertificate.js';
export { InstallCertificateAttempt } from './InstallCertificateAttempt.js';
export { DeleteCertificateAttempt } from './DeleteCertificateAttempt.js';
export { SignatureAlgorithmEnumType, CountryNameEnumType } from './CertificateTypes.js';

View File

@@ -0,0 +1,69 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { DEFAULT_TENANT_ID, OCPP1_6_Namespace } from '@citrineos/base';
import type { ChangeConfigurationDto, TenantDto } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
Model,
Table,
} from 'sequelize-typescript';
import { Tenant } from './Tenant.js';
@Table
export class ChangeConfiguration extends Model implements ChangeConfigurationDto {
static readonly MODEL_NAME: string = OCPP1_6_Namespace.ChangeConfiguration;
@Column({
unique: 'stationName_tenantId_key',
allowNull: false,
type: DataType.STRING,
})
declare ocppConnectionName: string;
@Column({
unique: 'stationName_tenantId_key',
allowNull: false,
type: DataType.STRING(50),
})
declare key: string;
@Column(DataType.STRING(500))
declare value?: string | null;
@Column(DataType.BOOLEAN)
declare readonly?: boolean | null;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
unique: 'stationName_tenantId_key',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: ChangeConfiguration) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,101 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type {
ACChargingParametersType,
ChargingNeedsDto,
DCChargingParametersType,
EnergyTransferModeEnumType,
EvseDto,
TenantDto,
TransactionDto,
} from '@citrineos/base';
import { DEFAULT_TENANT_ID, OCPP2_0_1, OCPP2_Namespace } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
Model,
Table,
} from 'sequelize-typescript';
import { Evse } from '../Location/index.js';
import { Tenant } from '../Tenant.js';
import { Transaction } from '../TransactionEvent/Transaction.js';
@Table
export class ChargingNeeds extends Model implements ChargingNeedsDto {
static readonly MODEL_NAME: string = OCPP2_Namespace.ChargingNeeds;
/**
* Fields
*/
@Column(DataType.JSONB)
declare acChargingParameters?: ACChargingParametersType | null;
@Column(DataType.JSONB)
declare dcChargingParameters?: DCChargingParametersType | null;
@Column({
type: DataType.DATE,
get() {
const departureTime: Date = this.getDataValue('departureTime');
return departureTime ? departureTime.toISOString() : null;
},
})
declare departureTime?: string | null;
@Column(DataType.STRING)
declare requestedEnergyTransfer: EnergyTransferModeEnumType;
@Column(DataType.INTEGER)
declare maxScheduleTuples?: number | null;
/**
* Relations
*/
@ForeignKey(() => Evse)
@Column(DataType.INTEGER)
declare evseId: number;
@BelongsTo(() => Evse, 'evseId')
declare evse: EvseDto;
@ForeignKey(() => Transaction)
@Column(DataType.INTEGER)
declare transactionDatabaseId: number;
@BelongsTo(() => Transaction, 'transactionDatabaseId')
declare transaction: TransactionDto;
declare customData?: OCPP2_0_1.CustomDataType | null;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: ChargingNeeds) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,146 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type {
ChargingLimitSourceEnumType,
ChargingProfileDto,
ChargingProfileKindEnumType,
ChargingProfilePurposeEnumType,
ChargingScheduleDto,
RecurrencyKindEnumType,
TenantDto,
TransactionDto,
} from '@citrineos/base';
import { DEFAULT_TENANT_ID, Namespace } from '@citrineos/base';
import {
AutoIncrement,
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
HasMany,
Model,
PrimaryKey,
Table,
} from 'sequelize-typescript';
import { Transaction } from '../TransactionEvent/Transaction.js';
import { ChargingSchedule } from './ChargingSchedule.js';
import { Tenant } from '../Tenant.js';
@Table
export class ChargingProfile extends Model implements ChargingProfileDto {
static readonly MODEL_NAME: string = Namespace.ChargingProfile;
/**
* Fields
*/
@PrimaryKey
@AutoIncrement
@Column(DataType.INTEGER)
declare databaseId: number;
@Column({
type: DataType.STRING,
unique: 'stationName_tenantId_id',
})
declare ocppConnectionName: string;
@Column({
type: DataType.INTEGER,
unique: 'stationName_tenantId_id',
})
declare id: number;
@Column(DataType.STRING)
declare chargingProfileKind: ChargingProfileKindEnumType;
@Column(DataType.STRING)
declare chargingProfilePurpose: ChargingProfilePurposeEnumType;
@Column(DataType.STRING)
declare recurrencyKind?: RecurrencyKindEnumType | null;
@Column(DataType.INTEGER)
declare stackLevel: number;
@Column({
type: DataType.DATE,
get() {
const validFrom: Date = this.getDataValue('validFrom');
return validFrom ? validFrom.toISOString() : null;
},
})
declare validFrom?: string | null;
@Column({
type: DataType.DATE,
get() {
const validTo: Date = this.getDataValue('validTo');
return validTo ? validTo.toISOString() : null;
},
})
declare validTo?: string | null;
@Column(DataType.INTEGER)
declare evseId?: number | null;
// this value indicates whether the ChargingProfile is set on charger
@Column({
type: DataType.BOOLEAN,
defaultValue: false,
})
declare isActive: boolean;
@Column({
type: DataType.STRING,
defaultValue: 'CSO',
})
declare chargingLimitSource?: ChargingLimitSourceEnumType | null;
/**
* Relations
*/
@HasMany(() => ChargingSchedule, 'chargingProfileDatabaseId')
declare chargingSchedule:
| [ChargingScheduleDto]
| [ChargingScheduleDto, ChargingScheduleDto]
| [ChargingScheduleDto, ChargingScheduleDto, ChargingScheduleDto];
@ForeignKey(() => Transaction)
declare transactionDatabaseId?: number | null;
@BelongsTo(() => Transaction, 'transactionDatabaseId')
declare transaction?: TransactionDto;
declare customData?: object | null;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
unique: 'stationName_tenantId_id',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: ChargingProfile) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,127 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type {
ChargingProfileDto,
ChargingRateUnitEnumType,
ChargingScheduleDto,
SalesTariffDto,
TenantDto,
} from '@citrineos/base';
import { DEFAULT_TENANT_ID, Namespace } from '@citrineos/base';
import {
AutoIncrement,
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
HasMany,
Model,
PrimaryKey,
Table,
} from 'sequelize-typescript';
import { Tenant } from '../Tenant.js';
import { ChargingProfile } from './ChargingProfile.js';
import { SalesTariff } from './SalesTariff.js';
@Table
export class ChargingSchedule extends Model implements ChargingScheduleDto {
static readonly MODEL_NAME: string = Namespace.ChargingSchedule;
/**
* Fields
*/
@PrimaryKey
@AutoIncrement
@Column(DataType.INTEGER)
declare databaseId: number;
@Column({
type: DataType.INTEGER,
unique: 'stationName_tenantId_id',
})
declare id: number;
@Column({
type: DataType.STRING,
unique: 'stationName_tenantId_id',
})
declare ocppConnectionName: string;
@Column(DataType.STRING)
declare chargingRateUnit: ChargingRateUnitEnumType;
@Column(DataType.JSONB)
declare chargingSchedulePeriod: [any, ...any[]];
@Column(DataType.INTEGER)
declare duration?: number | null;
@Column(DataType.DECIMAL)
declare minChargingRate?: number | null;
@Column(DataType.STRING)
declare startSchedule?: string | null;
// Periods contained in the charging profile are relative to this point in time.
// From NotifyEVChargingScheduleRequest
@Column({
type: DataType.DATE,
get() {
const timeBase: Date = this.getDataValue('timeBase');
return timeBase ? timeBase.toISOString() : null;
},
})
declare timeBase?: string;
/**
* Relations
*/
@BelongsTo(() => ChargingProfile, 'chargingProfileDatabaseId')
declare chargingProfile: ChargingProfileDto;
@ForeignKey(() => ChargingProfile)
@Column(DataType.INTEGER)
declare chargingProfileDatabaseId?: number;
@ForeignKey(() => SalesTariff)
declare salesTariffId?: number | null;
@BelongsTo(() => SalesTariff, 'salesTariffId')
declare salesTariff?: SalesTariffDto;
@HasMany(() => SalesTariff, 'chargingScheduleDatabaseId')
declare salesTariffs?: SalesTariffDto[];
declare customData?: object | null;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
unique: 'stationName_tenantId_id',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: ChargingSchedule) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type { CompositeScheduleDto, EvseDto, TenantDto } from '@citrineos/base';
import { DEFAULT_TENANT_ID, Namespace } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
Model,
Table,
} from 'sequelize-typescript';
import { Evse } from '../Location/index.js';
import { Tenant } from '../Tenant.js';
@Table
export class CompositeSchedule extends Model implements CompositeScheduleDto {
static readonly MODEL_NAME: string = Namespace.CompositeSchedule;
@Column(DataType.STRING)
declare ocppConnectionName: string;
@ForeignKey(() => Evse)
@Column(DataType.INTEGER)
declare evseId: number;
@BelongsTo(() => Evse, 'evseId')
declare evse?: EvseDto;
@Column(DataType.INTEGER)
declare duration: number;
@Column({
type: DataType.DATE,
get() {
const scheduleStart: Date = this.getDataValue('scheduleStart');
return scheduleStart ? scheduleStart.toISOString() : null;
},
})
declare scheduleStart: string;
@Column(DataType.STRING)
declare chargingRateUnit: string;
@Column(DataType.JSONB)
declare chargingSchedulePeriod: [object, ...object[]];
declare customData?: object | null;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: CompositeSchedule) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,99 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type {
ChargingScheduleDto,
SalesTariffDto,
SalesTariffEntry,
TenantDto,
} from '@citrineos/base';
import { DEFAULT_TENANT_ID, OCPP2_0_1, OCPP2_Namespace } from '@citrineos/base';
import {
AutoIncrement,
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
HasMany,
Model,
PrimaryKey,
Table,
} from 'sequelize-typescript';
import { Tenant } from '../Tenant.js';
import { ChargingSchedule } from './ChargingSchedule.js';
@Table
export class SalesTariff extends Model implements SalesTariffDto {
static readonly MODEL_NAME: string = OCPP2_Namespace.SalesTariff;
/**
* Fields
*/
@PrimaryKey
@AutoIncrement
@Column(DataType.INTEGER)
declare databaseId: number;
@Column({
type: DataType.INTEGER,
unique: 'id_chargingScheduleDatabaseId',
})
declare id: number;
@Column(DataType.INTEGER)
declare numEPriceLevels?: number | null;
@Column(DataType.STRING)
declare salesTariffDescription?: string | null;
@Column(DataType.JSONB)
declare salesTariffEntry: [SalesTariffEntry, ...SalesTariffEntry[]];
/**
* Relations
*/
@ForeignKey(() => ChargingSchedule)
@Column({
type: DataType.INTEGER,
unique: 'id_chargingScheduleDatabaseId',
})
declare chargingScheduleDatabaseId: number;
@BelongsTo(() => ChargingSchedule, 'chargingScheduleDatabaseId')
declare chargingSchedule?: ChargingScheduleDto;
@HasMany(() => ChargingSchedule, 'salesTariffId')
declare chargingSchedulesBySalesTariff?: ChargingScheduleDto[];
declare customData?: OCPP2_0_1.CustomDataType | null;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: SalesTariff) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,9 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
export { ChargingProfile } from './ChargingProfile.js';
export { ChargingNeeds } from './ChargingNeeds.js';
export { ChargingSchedule } from './ChargingSchedule.js';
export { SalesTariff } from './SalesTariff.js';
export { CompositeSchedule } from './CompositeSchedule.js';

View File

@@ -0,0 +1,87 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { DEFAULT_TENANT_ID, OCPP2_Namespace } from '@citrineos/base';
import type {
ChargingStationDto,
ChargingStationSecurityInfoDto,
TenantDto,
} from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
Model,
Table,
} from 'sequelize-typescript';
import { ChargingStation } from './Location/index.js';
import { Tenant } from './Tenant.js';
/**
* Represents the security information found on a particular charging station.
*/
@Table
export class ChargingStationSecurityInfo extends Model implements ChargingStationSecurityInfoDto {
static readonly MODEL_NAME: string = OCPP2_Namespace.ChargingStationSecurityInfo;
@ForeignKey(() => ChargingStation)
@Column(DataType.INTEGER)
declare stationId?: number;
@BelongsTo(() => ChargingStation, 'stationId')
declare chargingStation?: ChargingStationDto;
@Column({
type: DataType.STRING,
unique: 'stationName_tenantId',
})
ocppConnectionName!: string;
@Column(DataType.STRING)
publicKeyFileId!: string;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
unique: 'stationName_tenantId',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeCreate
static async resolveStationId(instance: ChargingStationSecurityInfo): Promise<void> {
if (instance.stationId == null && instance.ocppConnectionName && instance.tenantId != null) {
const { ChargingStation } = await import('./Location/ChargingStation.js');
const station = await ChargingStation.findOne({
where: { ocppConnectionName: instance.ocppConnectionName, tenantId: instance.tenantId },
attributes: ['id'],
});
if (station) {
instance.stationId = station.id;
}
}
}
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: ChargingStationSecurityInfo) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,94 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type { ChargingStationSequenceTypeEnumType, TenantDto } from '@citrineos/base';
import { DEFAULT_TENANT_ID } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
Model,
Table,
} from 'sequelize-typescript';
import { ChargingStation } from '../Location/index.js';
import type { ChargingStation as ChargingStationType } from '../Location/index.js';
import { Tenant } from '../Tenant.js';
@Table
export class ChargingStationSequence extends Model {
static readonly MODEL_NAME: string = 'ChargingStationSequence';
@ForeignKey(() => ChargingStation)
@Column({
type: DataType.INTEGER,
allowNull: true,
unique: 'stationId_type',
})
declare stationId?: number;
@Column({
type: DataType.STRING(36),
allowNull: false,
})
declare ocppConnectionName: string;
@Column({
type: DataType.STRING,
allowNull: false,
unique: 'stationId_type',
})
type!: ChargingStationSequenceTypeEnumType;
@Column({
type: DataType.BIGINT,
allowNull: false,
defaultValue: 0,
})
value!: number;
@BelongsTo(() => ChargingStation, 'stationId')
declare station: ChargingStationType;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeCreate
static async resolveStationId(instance: ChargingStationSequence): Promise<void> {
if (instance.stationId == null && instance.ocppConnectionName && instance.tenantId != null) {
const station = await ChargingStation.findOne({
where: { ocppConnectionName: instance.ocppConnectionName, tenantId: instance.tenantId },
attributes: ['id'],
});
if (station) {
instance.stationId = station.id;
}
}
}
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: ChargingStationSequence) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,4 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
export { ChargingStationSequence } from './ChargingStationSequence.js';

View File

@@ -0,0 +1,122 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type {
ComponentDto,
MessageInfoDto,
TenantDto,
VariableAttributeDto,
VariableDto,
VariableMonitoringDto,
} from '@citrineos/base';
import { DEFAULT_TENANT_ID, OCPP2_0_1, OCPP2_Namespace } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
BelongsToMany,
Column,
DataType,
ForeignKey,
HasMany,
Model,
Table,
} from 'sequelize-typescript';
import { MessageInfo } from '../MessageInfo/MessageInfo.js';
import { Tenant } from '../Tenant.js';
import { VariableMonitoring } from '../VariableMonitoring/VariableMonitoring.js';
import { ComponentVariable } from './ComponentVariable.js';
import { EvseType } from './EvseType.js';
import { Variable } from './Variable.js';
import { VariableAttribute } from './VariableAttribute.js';
@Table({
indexes: [
{
unique: true,
name: 'components_tenantId_name',
fields: ['tenantId', 'name'],
where: {
instance: null,
},
},
],
})
export class Component extends Model implements OCPP2_0_1.ComponentType, ComponentDto {
static readonly MODEL_NAME: string = OCPP2_Namespace.ComponentType;
/**
* Fields
*/
@Column({
type: DataType.STRING,
unique: 'tenantId_name_instance',
})
declare name: string;
@Column({
type: DataType.STRING,
unique: 'tenantId_name_instance',
})
declare instance?: string | null;
/**
* Relations
*/
@BelongsTo(() => EvseType, 'evseDatabaseId')
declare evse?: EvseType;
@ForeignKey(() => EvseType)
@Column(DataType.INTEGER)
declare evseDatabaseId?: number | null;
@BelongsToMany(() => Variable, { through: () => ComponentVariable, foreignKey: 'componentId' })
declare variables?: VariableDto[];
declare customData?: OCPP2_0_1.CustomDataType | null;
// Declare the association methods, to be automatically generated by Sequelize at runtime
public addVariable!: (variable: VariableDto) => Promise<void>;
public getVariables!: () => Promise<VariableDto[]>;
@HasMany(() => VariableAttribute, 'componentId')
declare variableAttributes?: VariableAttributeDto[];
@HasMany(() => VariableMonitoring, 'componentId')
declare variableMonitorings?: VariableMonitoringDto[];
@HasMany(() => MessageInfo, 'displayComponentId')
declare messageInfos?: MessageInfoDto[];
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
unique: 'tenantId_name_instance',
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: Component) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type { TenantDto } from '@citrineos/base';
import { DEFAULT_TENANT_ID } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
Model,
Table,
} from 'sequelize-typescript';
import { Tenant } from '../Tenant.js';
import { Component, Variable } from './index.js';
@Table
export class ComponentVariable extends Model {
// Namespace enum not used as this is not a model required by CitrineOS
static readonly MODEL_NAME: string = 'ComponentVariable';
@ForeignKey(() => Component)
@Column(DataType.INTEGER)
declare componentId: number;
@ForeignKey(() => Variable)
@Column(DataType.INTEGER)
declare variableId: number;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: ComponentVariable) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,85 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type { EvseTypeDto, TenantDto } from '@citrineos/base';
import { DEFAULT_TENANT_ID, OCPP2_0_1, OCPP2_Namespace } from '@citrineos/base';
import {
AutoIncrement,
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
Model,
PrimaryKey,
Table,
} from 'sequelize-typescript';
import { Tenant } from '../Tenant.js';
@Table({
indexes: [
{
unique: true,
name: 'evse_types_tenantId_id',
fields: ['tenantId', 'id'],
where: {
connectorId: null,
},
},
],
})
export class EvseType extends Model implements OCPP2_0_1.EVSEType, EvseTypeDto {
static readonly MODEL_NAME: string = OCPP2_Namespace.EVSEType;
/**
* Fields
*/
@PrimaryKey
@AutoIncrement
@Column(DataType.INTEGER)
declare databaseId: number;
@Column({
type: DataType.INTEGER,
unique: 'tenantId_id_connectorId',
})
declare id: number;
@Column({
type: DataType.INTEGER,
unique: 'tenantId_id_connectorId',
})
declare connectorId?: number | null;
declare customData?: OCPP2_0_1.CustomDataType | null;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
unique: 'tenantId_id_connectorId',
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: EvseType) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,113 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type {
ComponentDto,
TenantDto,
VariableAttributeDto,
VariableCharacteristicsDto,
VariableDto,
VariableMonitoringDto,
} from '@citrineos/base';
import { DEFAULT_TENANT_ID, OCPP2_0_1, OCPP2_Namespace } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
BelongsToMany,
Column,
DataType,
ForeignKey,
HasMany,
HasOne,
Model,
Table,
} from 'sequelize-typescript';
import { Tenant } from '../Tenant.js';
import { VariableMonitoring } from '../VariableMonitoring/VariableMonitoring.js';
import { Component } from './Component.js';
import { ComponentVariable } from './ComponentVariable.js';
import { VariableAttribute } from './VariableAttribute.js';
import { VariableCharacteristics } from './VariableCharacteristics.js';
@Table({
indexes: [
{
unique: true,
name: 'variables_tenantId_name',
fields: ['tenantId', 'name'],
where: {
instance: null,
},
},
],
})
export class Variable extends Model implements OCPP2_0_1.VariableType, VariableDto {
static readonly MODEL_NAME: string = OCPP2_Namespace.VariableType;
/**
* Fields
*/
@Column({
type: DataType.STRING,
unique: 'tenantId_name_instance',
})
declare name: string;
@Column({
type: DataType.STRING,
unique: 'tenantId_name_instance',
})
declare instance?: string | null;
/**
* Relations
*/
@BelongsToMany(() => Component, { through: () => ComponentVariable, foreignKey: 'variableId' })
declare components?: ComponentDto[];
@HasMany(() => VariableAttribute, 'variableId')
declare variableAttributes?: VariableAttributeDto[];
@HasOne(() => VariableCharacteristics, 'variableId')
declare variableCharacteristics?: VariableCharacteristicsDto;
@HasMany(() => VariableMonitoring, 'variableId')
declare variableMonitorings?: VariableMonitoringDto[];
declare customData?: OCPP2_0_1.CustomDataType | null;
// Declare the association methods, to be automatically generated by Sequelize at runtime
public addComponent!: (variable: ComponentDto) => Promise<void>;
public getComponents!: () => Promise<ComponentDto[]>;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
unique: 'tenantId_name_instance',
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: Variable) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,277 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type {
BootDto,
ChargingStationDto,
ComponentDto,
EvseTypeDto,
TenantDto,
VariableAttributeDto,
VariableDto,
VariableStatusDto,
} from '@citrineos/base';
import { DEFAULT_TENANT_ID, OCPP2_0_1, OCPP2_Namespace } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
HasMany,
Index,
Model,
Table,
} from 'sequelize-typescript';
import { CryptoUtils } from '../../../../util/CryptoUtils.js';
import { ChargingStation } from '../Location/index.js';
import { Boot } from '../Boot.js';
import { Tenant } from '../Tenant.js';
import { Component } from './Component.js';
import { EvseType } from './EvseType.js';
import { Variable } from './Variable.js';
import { VariableStatus } from './VariableStatus.js';
@Table({
indexes: [
{
unique: true,
name: 'variable_attributes_stationId',
fields: ['stationId'],
where: {
type: null,
variableId: null,
componentId: null,
},
},
{
unique: true,
name: 'variable_attributes_stationId_type',
fields: ['stationId', 'type'],
where: {
variableId: null,
componentId: null,
},
},
{
unique: true,
name: 'variable_attributes_stationId_variableId',
fields: ['stationId', 'variableId'],
where: {
type: null,
componentId: null,
},
},
{
unique: true,
name: 'variable_attributes_stationId_componentId',
fields: ['stationId', 'componentId'],
where: {
type: null,
variableId: null,
},
},
{
unique: true,
name: 'variable_attributes_stationId_type_variableId',
fields: ['stationId', 'type', 'variableId'],
where: {
componentId: null,
},
},
{
unique: true,
name: 'variable_attributes_stationId_type_componentId',
fields: ['stationId', 'type', 'componentId'],
where: {
variableId: null,
},
},
{
unique: true,
name: 'variable_attributes_stationId_variableId_componentId',
fields: ['stationId', 'variableId', 'componentId'],
where: {
type: null,
},
},
],
})
export class VariableAttribute
extends Model
implements OCPP2_0_1.VariableAttributeType, VariableAttributeDto
{
static readonly MODEL_NAME: string = OCPP2_Namespace.VariableAttributeType;
/**
* Fields
*/
@ForeignKey(() => ChargingStation)
@Column({
type: DataType.INTEGER,
unique: 'stationId_type_variableId_componentId',
allowNull: true,
})
declare stationId?: number;
@Index
@Column({
type: DataType.STRING,
allowNull: false,
})
declare ocppConnectionName: string;
@BelongsTo(() => ChargingStation, 'stationId')
declare chargingStation: ChargingStationDto;
@Column({
type: DataType.STRING,
defaultValue: OCPP2_0_1.AttributeEnumType.Actual,
unique: 'stationId_type_variableId_componentId',
})
declare type?: OCPP2_0_1.AttributeEnumType | null;
// From VariableCharacteristics, which belongs to Variable associated with this VariableAttribute
@Column({
type: DataType.STRING,
defaultValue: OCPP2_0_1.DataEnumType.string,
})
declare dataType: OCPP2_0_1.DataEnumType;
@Column({
// TODO: Make this configurable? also used in VariableStatus model
type: DataType.STRING(4000),
set(valueString: string) {
if (valueString) {
const valueType = (this as VariableAttribute).dataType;
switch (valueType) {
case OCPP2_0_1.DataEnumType.passwordString:
valueString = CryptoUtils.getPasswordHash(valueString);
break;
default:
// Do nothing
break;
}
}
this.setDataValue('value', valueString);
},
})
declare value?: string | null;
@Column({
type: DataType.STRING,
defaultValue: OCPP2_0_1.MutabilityEnumType.ReadWrite,
})
declare mutability?: OCPP2_0_1.MutabilityEnumType | null;
@Column({
type: DataType.BOOLEAN,
defaultValue: false,
})
declare persistent?: boolean | null;
@Column({
type: DataType.BOOLEAN,
defaultValue: false,
})
declare constant?: boolean | null;
@Column({
type: DataType.DATE,
get() {
return this.getDataValue('generatedAt').toISOString();
},
})
declare generatedAt: string;
/**
* Relations
*/
@BelongsTo(() => Variable, 'variableId')
declare variable: VariableDto;
@ForeignKey(() => Variable)
@Column({
type: DataType.INTEGER,
unique: 'stationId_type_variableId_componentId',
})
declare variableId?: number | null;
@BelongsTo(() => Component, 'componentId')
declare component: ComponentDto;
@ForeignKey(() => Component)
@Column({
type: DataType.INTEGER,
unique: 'stationId_type_variableId_componentId',
})
declare componentId?: number | null;
@BelongsTo(() => EvseType, 'evseDatabaseId')
declare evse?: EvseTypeDto;
@ForeignKey(() => EvseType)
@Column(DataType.INTEGER)
declare evseDatabaseId?: number | null;
// History of variable status. Can be directly from GetVariablesResponse or SetVariablesResponse, or from NotifyReport handling, or from 'setOnCharger' option for data api
@HasMany(() => VariableStatus, 'variableAttributeId')
declare statuses?: VariableStatusDto[];
// Below used to associate attributes with boot process
@BelongsTo(() => Boot, 'bootConfigId')
declare bootConfig?: BootDto;
@ForeignKey(() => Boot)
@Column(DataType.STRING)
declare bootConfigId?: string | null;
declare customData?: OCPP2_0_1.CustomDataType | null;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeCreate
static async resolveStationId(instance: VariableAttribute): Promise<void> {
if (instance.stationId == null && instance.ocppConnectionName && instance.tenantId != null) {
const station = await ChargingStation.findOne({
where: { ocppConnectionName: instance.ocppConnectionName, tenantId: instance.tenantId },
attributes: ['id'],
});
if (station) {
instance.stationId = station.id;
}
}
}
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: VariableAttribute) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,90 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type { VariableCharacteristicsDto, VariableDto, TenantDto } from '@citrineos/base';
import { DEFAULT_TENANT_ID, OCPP2_0_1, OCPP2_Namespace } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
Model,
Table,
} from 'sequelize-typescript';
import { Variable } from './Variable.js';
import { Tenant } from '../Tenant.js';
@Table
export class VariableCharacteristics
extends Model
implements OCPP2_0_1.VariableCharacteristicsType, VariableCharacteristicsDto
{
static readonly MODEL_NAME: string = OCPP2_Namespace.VariableCharacteristicsType;
/**
* Fields
*/
@Column(DataType.STRING)
declare unit?: string | null;
@Column(DataType.STRING)
declare dataType: OCPP2_0_1.DataEnumType;
@Column(DataType.DECIMAL)
declare minLimit?: number | null;
@Column(DataType.DECIMAL)
declare maxLimit?: number | null;
@Column(DataType.STRING(4000))
declare valuesList?: string | null;
@Column(DataType.BOOLEAN)
declare supportsMonitoring: boolean;
/**
* Relations
*/
@BelongsTo(() => Variable, 'variableId')
declare variable: VariableDto;
@ForeignKey(() => Variable)
@Column({
type: DataType.INTEGER,
unique: true,
})
declare variableId?: number | null;
declare customData?: OCPP2_0_1.CustomDataType | null;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: VariableCharacteristics) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,76 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type {
VariableAttributeDto,
VariableStatusDto,
TenantDto,
StatusInfo,
} from '@citrineos/base';
import { DEFAULT_TENANT_ID, OCPP2_0_1, OCPP2_Namespace } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
Model,
Table,
} from 'sequelize-typescript';
import { VariableAttribute } from './VariableAttribute.js';
import { Tenant } from '../Tenant.js';
@Table
export class VariableStatus extends Model implements VariableStatusDto {
static readonly MODEL_NAME: string = OCPP2_Namespace.VariableStatus;
@Column(DataType.STRING(4000))
declare value: string;
@Column(DataType.STRING)
declare status: string;
@Column(DataType.JSON)
declare statusInfo?: StatusInfo | null;
/**
* Relations
*/
@BelongsTo(() => VariableAttribute, 'variableAttributeId')
declare variable: VariableAttributeDto;
@ForeignKey(() => VariableAttribute)
@Column(DataType.INTEGER)
declare variableAttributeId?: number | null;
declare customData?: OCPP2_0_1.CustomDataType | null;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: VariableStatus) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
export { Component } from './Component.js';
export { ComponentVariable } from './ComponentVariable.js';
export { EvseType } from './EvseType.js';
export { Variable } from './Variable.js';
export { VariableAttribute } from './VariableAttribute.js';
export { VariableCharacteristics } from './VariableCharacteristics.js';
export { VariableStatus } from './VariableStatus.js';

View File

@@ -0,0 +1,221 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type {
ChargingStationCapabilityEnumType,
ChargingStationDto,
ChargingStationParkingRestrictionEnumType,
ChargingStationSecurityInfoDto,
ChargingStationSequenceDto,
ConnectorDto,
EvseDto,
InstalledCertificateDto,
LocationDto,
OCPPMessageDto,
Point,
ServerNetworkProfileDto,
StatusNotificationDto,
TenantDto,
VariableAttributeDto,
VariableMonitoringDto,
} from '@citrineos/base';
import { DEFAULT_TENANT_ID, Namespace, OCPPVersion } from '@citrineos/base';
import {
AutoIncrement,
BeforeCreate,
BeforeUpdate,
BelongsTo,
BelongsToMany,
Column,
DataType,
ForeignKey,
HasMany,
Index,
Model,
PrimaryKey,
Table,
} from 'sequelize-typescript';
import { DeleteCertificateAttempt } from '../Certificate/DeleteCertificateAttempt.js';
import { InstalledCertificate } from '../Certificate/InstalledCertificate.js';
import { ChargingStationSecurityInfo } from '../ChargingStationSecurityInfo.js';
import { ChargingStationSequence } from '../ChargingStationSequence/ChargingStationSequence.js';
import { VariableAttribute } from '../DeviceModel/VariableAttribute.js';
import { OCPPMessage } from '../OCPPMessage.js';
import { Tenant } from '../Tenant.js';
import { Transaction } from '../TransactionEvent/Transaction.js';
import { EventData } from '../VariableMonitoring/EventData.js';
import { VariableMonitoring } from '../VariableMonitoring/VariableMonitoring.js';
import { ChargingStationNetworkProfile } from './ChargingStationNetworkProfile.js';
import { Connector } from './Connector.js';
import { Evse } from './Evse.js';
import { Location } from './Location.js';
import { ServerNetworkProfile } from './ServerNetworkProfile.js';
import { StatusNotification } from './StatusNotification.js';
/**
* Represents a charging station.
* Currently, this data model is internal to CitrineOS. In the future, it will be analogous to an OCPI ChargingStation.
*/
@Table
export class ChargingStation extends Model implements ChargingStationDto {
static readonly MODEL_NAME: string = Namespace.ChargingStation;
@AutoIncrement
@PrimaryKey
@Column(DataType.INTEGER)
declare id: number;
/**
* The tenant-scoped charging station identifier — used in WebSocket routing
* (the charger appends this to the end of the WebSocket URL on connect).
* Unique per tenant, but two different tenants may share the same value.
*/
@Index
@Column({
type: DataType.STRING(36),
unique: 'ChargingStations_stationName_tenantId_key',
})
declare ocppConnectionName: string;
@Column(DataType.BOOLEAN)
declare isOnline: boolean;
@Column(DataType.STRING)
declare protocol?: OCPPVersion | null;
@Column(DataType.DATE)
declare latestOcppMessageTimestamp?: string | null;
@Column(DataType.STRING(20))
declare chargePointVendor?: string | null;
@Column(DataType.STRING(20))
declare chargePointModel?: string | null;
@Column(DataType.STRING(25))
declare chargePointSerialNumber?: string | null;
@Column(DataType.STRING(25))
declare chargeBoxSerialNumber?: string | null;
@Column(DataType.STRING(50))
declare firmwareVersion?: string | null;
@Column(DataType.STRING(20))
declare iccid?: string | null;
@Column(DataType.STRING(20))
declare imsi?: string | null;
@Column(DataType.STRING(25))
declare meterType?: string | null;
@Column(DataType.STRING(25))
declare meterSerialNumber?: string | null;
/**
* [longitude, latitude]
*/
@Column(DataType.GEOMETRY('POINT'))
declare coordinates?: Point | null;
@Column(DataType.STRING)
declare floorLevel?: string | null;
@Column(DataType.JSONB)
declare parkingRestrictions?: ChargingStationParkingRestrictionEnumType[] | null;
@Column(DataType.JSONB)
declare capabilities?: ChargingStationCapabilityEnumType[] | null;
/**
* In OCPP 1.6, StatusNotifications can be sent with a connectorId of 0 to report the status of the whole charging station.
* Some charging stations instead use it in ways that cannot be applied to all connectors
* (such as sending Available when at least one connector is available, while others are charging).
* When true, this flag indicates that StatusNotifications with connectorId 0 should be used to update all connector statuses.
* When false, StatusNotifications with connectorId 0 should be ignored.
*/
@Column({
type: DataType.BOOLEAN,
defaultValue: true,
})
declare use16StatusNotification0: boolean;
@ForeignKey(() => Location)
@Column(DataType.INTEGER)
declare locationId?: number | null;
@HasMany(() => StatusNotification, 'stationId')
declare statusNotifications?: StatusNotificationDto[] | null;
@HasMany(() => InstalledCertificate, 'stationId')
declare installedCertificates?: InstalledCertificateDto[];
@HasMany(() => Transaction, 'stationId')
declare transactions?: Transaction[] | null;
/**
* The business Location of the charging station. Optional in case a charging station is not yet in the field, or retired.
*/
@BelongsTo(() => Location, 'locationId')
declare location?: LocationDto;
@BelongsToMany(() => ServerNetworkProfile, () => ChargingStationNetworkProfile)
declare networkProfiles?: ServerNetworkProfileDto[] | null;
@HasMany(() => Evse, 'stationId')
declare evses?: EvseDto[] | null;
@HasMany(() => Connector, 'stationId')
declare connectors?: ConnectorDto[] | null;
@HasMany(() => VariableAttribute, 'stationId')
declare variableAttributes?: VariableAttributeDto[];
@HasMany(() => OCPPMessage, 'stationId')
declare ocppMessages?: OCPPMessageDto[];
@HasMany(() => VariableMonitoring, 'stationId')
declare variableMonitorings?: VariableMonitoringDto[];
@HasMany(() => EventData, 'stationId')
declare stationEventData?: EventData[];
@HasMany(() => ChargingStationSecurityInfo, 'stationId')
declare securityInfo?: ChargingStationSecurityInfoDto[];
@HasMany(() => ChargingStationSequence, 'stationId')
declare sequences?: ChargingStationSequenceDto[];
@HasMany(() => DeleteCertificateAttempt, 'stationId')
declare deleteCertificateAttempts?: DeleteCertificateAttempt[];
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
unique: 'ChargingStations_stationName_tenantId_key',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeCreate
@BeforeUpdate
static setDefaultTenant(instance: ChargingStation) {
if (instance.isNewRecord && instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,120 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type {
ChargingStationDto,
ChargingStationNetworkProfileDto,
ServerNetworkProfileDto,
SetNetworkProfileDto,
TenantDto,
} from '@citrineos/base';
import { DEFAULT_TENANT_ID } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
Model,
Table,
} from 'sequelize-typescript';
import { ChargingStation } from './ChargingStation.js';
import { ServerNetworkProfile } from './ServerNetworkProfile.js';
import { SetNetworkProfile } from './SetNetworkProfile.js';
import { Tenant } from '../Tenant.js';
@Table
export class ChargingStationNetworkProfile
extends Model
implements ChargingStationNetworkProfileDto
{
// Namespace enum not used as this is not a model required by CitrineOS
static readonly MODEL_NAME: string = 'ChargingStationNetworkProfile';
@ForeignKey(() => ChargingStation)
@Column({
type: DataType.INTEGER,
unique: 'stationId_configurationSlot',
})
declare stationId?: number;
@BelongsTo(() => ChargingStation, 'stationId')
declare chargingStation?: ChargingStationDto;
@Column({
type: DataType.STRING,
})
declare ocppConnectionName: string;
/**
* Possible values for a particular station found in device model:
* OCPPCommCtrlr.NetworkConfigurationPriority.VariableCharacteristics.valuesList
*/
@Column({
type: DataType.INTEGER,
unique: 'stationId_configurationSlot',
})
declare configurationSlot: number;
@ForeignKey(() => SetNetworkProfile)
@Column(DataType.INTEGER)
declare setNetworkProfileId: number;
@BelongsTo(() => SetNetworkProfile, 'setNetworkProfileId')
declare setNetworkProfile: SetNetworkProfileDto;
/**
* If present, the websocket server that correlates to this configuration slot.
* The ws url in the network profile may not match the configured host, for example in the cloud the
* configured host will likely be behind a load balancer and a custom DNS name.
*
*/
@ForeignKey(() => ServerNetworkProfile)
@Column(DataType.STRING)
declare websocketServerConfigId?: string;
@BelongsTo(() => ServerNetworkProfile, 'websocketServerConfigId')
declare websocketServerConfig?: ServerNetworkProfileDto;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeCreate
static async resolveStationId(instance: ChargingStationNetworkProfile): Promise<void> {
if (instance.stationId == null && instance.ocppConnectionName && instance.tenantId != null) {
const station = await ChargingStation.findOne({
where: { ocppConnectionName: instance.ocppConnectionName, tenantId: instance.tenantId },
attributes: ['id'],
});
if (station) {
instance.stationId = station.id;
}
}
}
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: ChargingStationNetworkProfile) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,209 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type {
ChargingStationDto,
ConnectorDto,
ConnectorErrorCodeEnumType,
ConnectorFormatEnumType,
ConnectorPowerTypeEnumType,
ConnectorStatusEnumType,
ConnectorTypeEnumType,
EvseDto,
EvseTypeDto,
MeterValueDto,
StartTransactionDto,
StatusNotificationDto,
TariffDto,
TenantDto,
TransactionDto,
} from '@citrineos/base';
import { DEFAULT_TENANT_ID, OCPP1_6_Namespace } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
HasMany,
Model,
Table,
} from 'sequelize-typescript';
import { EvseType } from '../DeviceModel/EvseType.js';
import { Tariff } from '../Tariff/Tariffs.js';
import { Tenant } from '../Tenant.js';
import { MeterValue } from '../TransactionEvent/MeterValue.js';
import { StartTransaction } from '../TransactionEvent/StartTransaction.js';
import { Transaction } from '../TransactionEvent/Transaction.js';
import { ChargingStation } from './ChargingStation.js';
import { Evse } from './Evse.js';
import { StatusNotification } from './StatusNotification.js';
@Table
export class Connector extends Model implements ConnectorDto {
static readonly MODEL_NAME: string = OCPP1_6_Namespace.Connector;
@ForeignKey(() => ChargingStation)
@Column({
unique: 'stationId_connectorId',
allowNull: true,
type: DataType.INTEGER,
})
declare stationId?: number;
@Column({
allowNull: false,
type: DataType.STRING,
})
declare ocppConnectionName: string;
@ForeignKey(() => Evse)
@Column({
unique: 'evseId_evseTypeConnectorId',
allowNull: false,
type: DataType.INTEGER,
})
declare evseId: number;
@Column({
unique: 'stationId_connectorId',
allowNull: false,
type: DataType.INTEGER,
})
declare connectorId: number; // This is the serial int starting at 1 used in OCPP 1.6 to refer to the connector, unique per Charging Station.
@ForeignKey(() => EvseType)
@Column({
unique: 'evseId_evseTypeConnectorId',
allowNull: false,
type: DataType.INTEGER,
})
declare evseTypeConnectorId?: number; // This is the serial int starting at 1 used in OCPP 2.0.1 to refer to the connector, unique per EVSE.
@Column({
type: DataType.STRING,
defaultValue: 'Unknown',
})
declare status: ConnectorStatusEnumType;
@Column(DataType.STRING)
declare type?: ConnectorTypeEnumType | null;
@Column(DataType.STRING)
declare format?: ConnectorFormatEnumType | null;
@Column({
type: DataType.STRING,
defaultValue: 'NoError',
})
declare errorCode: ConnectorErrorCodeEnumType;
@Column(DataType.STRING)
declare powerType?: ConnectorPowerTypeEnumType | null;
@Column(DataType.INTEGER)
declare maximumAmperage?: number | null;
@Column(DataType.INTEGER)
declare maximumVoltage?: number | null;
@Column(DataType.INTEGER)
declare maximumPowerWatts?: number | null;
@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;
@Column(DataType.STRING)
declare termsAndConditionsUrl?: string | null;
@BelongsTo(() => ChargingStation, 'stationId')
declare chargingStation?: ChargingStationDto;
@BelongsTo(() => Evse, 'evseId')
declare evse?: EvseDto;
@BelongsTo(() => EvseType, 'evseTypeConnectorId')
declare evseTypeByConnector?: EvseTypeDto;
@HasMany(() => EvseType, 'connectorId')
declare evseTypes?: EvseTypeDto[];
@ForeignKey(() => Tariff)
@Column({
type: DataType.INTEGER,
allowNull: true,
onUpdate: 'CASCADE',
onDelete: 'SET NULL',
})
declare tariffId?: number | null;
@BelongsTo(() => Tariff, 'tariffId')
declare tariff?: TariffDto | null;
@HasMany(() => StatusNotification, 'connectorId')
declare statusNotifications?: StatusNotificationDto[];
@HasMany(() => MeterValue, 'connectorId')
declare meterValues?: MeterValueDto[];
@HasMany(() => Transaction, 'connectorId')
declare transactions?: TransactionDto[];
@HasMany(() => StartTransaction, 'connectorDatabaseId')
declare startTransactions?: StartTransactionDto[];
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeCreate
static async resolveStationId(instance: Connector): Promise<void> {
if (instance.stationId == null && instance.ocppConnectionName && instance.tenantId != null) {
const station = await ChargingStation.findOne({
where: { ocppConnectionName: instance.ocppConnectionName, tenantId: instance.tenantId },
attributes: ['id'],
});
if (station) {
instance.stationId = station.id;
}
}
}
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: Connector) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,117 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type {
ChargingNeedsDto,
ChargingStationDto,
CompositeScheduleDto,
ConnectorDto,
EvseDto,
TenantDto,
TransactionDto,
} from '@citrineos/base';
import { DEFAULT_TENANT_ID, Namespace } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
HasMany,
Model,
Table,
} from 'sequelize-typescript';
import { ChargingNeeds } from '../ChargingProfile/ChargingNeeds.js';
import { CompositeSchedule } from '../ChargingProfile/CompositeSchedule.js';
import { Tenant } from '../Tenant.js';
import { Transaction } from '../TransactionEvent/Transaction.js';
import { ChargingStation } from './ChargingStation.js';
import { Connector } from './Connector.js';
@Table
export class Evse extends Model implements EvseDto {
static readonly MODEL_NAME: string = Namespace.Evse;
@ForeignKey(() => ChargingStation)
@Column({
type: DataType.INTEGER,
unique: 'stationId_evseTypeId',
})
declare stationId?: number;
@Column({
type: DataType.STRING,
})
declare ocppConnectionName: string;
@Column({
type: DataType.INTEGER,
unique: 'stationId_evseTypeId',
})
declare evseTypeId?: number; // This is the serial int used in OCPP 2.0.1 to refer to the EVSE.
@Column(DataType.STRING)
declare evseId: string; // This is the eMI3 compliant EVSE ID
@Column(DataType.STRING)
declare physicalReference?: string | null; // Any identifier printed directly on the EVSE
@Column(DataType.BOOLEAN)
declare removed?: boolean;
@BelongsTo(() => ChargingStation, 'stationId')
declare chargingStation?: ChargingStationDto;
@HasMany(() => Connector, 'evseId')
declare connectors?: ConnectorDto[] | null;
@HasMany(() => ChargingNeeds, 'evseId')
declare chargingNeeds?: ChargingNeedsDto[];
@HasMany(() => CompositeSchedule, 'evseId')
declare compositeSchedules?: CompositeScheduleDto[];
@HasMany(() => Transaction, 'evseId')
declare transactions?: TransactionDto[];
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeCreate
static async resolveStationId(instance: Evse): Promise<void> {
if (instance.stationId == null && instance.ocppConnectionName && instance.tenantId != null) {
const station = await ChargingStation.findOne({
where: { ocppConnectionName: instance.ocppConnectionName, tenantId: instance.tenantId },
attributes: ['id'],
});
if (station) {
instance.stationId = station.id;
}
}
}
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: Evse) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,85 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type { StatusNotificationDto, TenantDto } from '@citrineos/base';
import {
DEFAULT_TENANT_ID,
OCPP2_Namespace,
type ChargingStationDto,
type LatestStatusNotificationDto,
} from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
Model,
Table,
} from 'sequelize-typescript';
import { Tenant } from '../Tenant.js';
import { ChargingStation } from './ChargingStation.js';
import { StatusNotification } from './StatusNotification.js';
@Table
export class LatestStatusNotification extends Model implements LatestStatusNotificationDto {
static readonly MODEL_NAME: string = OCPP2_Namespace.LatestStatusNotification;
@ForeignKey(() => ChargingStation)
@Column(DataType.INTEGER)
declare stationId?: number;
@Column(DataType.STRING)
declare ocppConnectionName: string;
@BelongsTo(() => ChargingStation, 'stationId')
declare chargingStation: ChargingStationDto;
@ForeignKey(() => StatusNotification)
declare statusNotificationId: string;
@BelongsTo(() => StatusNotification, 'statusNotificationId')
declare statusNotification: StatusNotificationDto;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeCreate
static async resolveStationId(instance: LatestStatusNotification): Promise<void> {
if (instance.stationId == null && instance.ocppConnectionName && instance.tenantId != null) {
const station = await ChargingStation.findOne({
where: { ocppConnectionName: instance.ocppConnectionName, tenantId: instance.tenantId },
attributes: ['id'],
});
if (station) {
instance.stationId = station.id;
}
}
}
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: LatestStatusNotification) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,125 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type {
ChargingStationDto,
LocationDto,
LocationFacilityEnumType,
LocationParkingEnumType,
Point,
TenantDto,
TransactionDto,
} from '@citrineos/base';
import { DEFAULT_TENANT_ID, LocationHours, OCPP2_Namespace } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
HasMany,
Model,
Table,
} from 'sequelize-typescript';
import { Tenant } from '../Tenant.js';
import { Transaction } from '../TransactionEvent/Transaction.js';
import { ChargingStation } from './ChargingStation.js';
/**
* Represents a location.
* Currently, this data model is internal to CitrineOS. In the future, it will be analogous to an OCPI Location.
*/
@Table
export class Location extends Model implements LocationDto {
static readonly MODEL_NAME: string = OCPP2_Namespace.Location;
@Column(DataType.STRING)
declare name: string;
@Column(DataType.STRING)
declare address: string;
@Column(DataType.STRING)
declare city: string;
@Column(DataType.STRING)
declare postalCode: string;
@Column(DataType.STRING)
declare state: string;
@Column(DataType.STRING)
declare country: string;
@Column({
type: DataType.BOOLEAN,
defaultValue: true,
})
declare publishUpstream: boolean;
@Column({
type: DataType.STRING,
defaultValue: 'UTC',
validate: {
isTimezone(value: string) {
try {
Intl.DateTimeFormat(undefined, { timeZone: value });
return true;
} catch (_ex) {
return false;
}
},
},
})
declare timeZone: string;
@Column(DataType.STRING)
declare parkingType?: LocationParkingEnumType | null;
@Column(DataType.JSONB)
declare facilities?: LocationFacilityEnumType[] | null;
@Column(DataType.JSONB)
declare openingHours?: LocationHours | null;
/**
* [longitude, latitude]
*/
@Column(DataType.GEOMETRY('POINT'))
declare coordinates: Point;
@HasMany(() => ChargingStation, 'locationId')
declare chargingPool: [ChargingStationDto, ...ChargingStationDto[]];
@HasMany(() => Transaction, 'locationId')
declare transactions?: TransactionDto[];
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: Location) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,107 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type {
ChargingStationDto,
OCPPVersionType,
ServerNetworkProfileDto,
TenantDto,
WebsocketServerConfig,
} from '@citrineos/base';
import { DEFAULT_TENANT_ID, OCPP2_Namespace } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
BelongsToMany,
Column,
DataType,
ForeignKey,
Model,
PrimaryKey,
Table,
} from 'sequelize-typescript';
import { Tenant } from '../Tenant.js';
import { ChargingStation } from './ChargingStation.js';
import { ChargingStationNetworkProfile } from './ChargingStationNetworkProfile.js';
@Table
export class ServerNetworkProfile
extends Model
implements WebsocketServerConfig, ServerNetworkProfileDto
{
static readonly MODEL_NAME: string = OCPP2_Namespace.ServerNetworkProfile;
@PrimaryKey
@Column(DataType.STRING)
declare id: string;
@Column(DataType.STRING)
declare host: string;
@Column(DataType.INTEGER)
declare port: number;
@Column(DataType.INTEGER)
declare pingInterval: number;
@Column(DataType.ARRAY(DataType.STRING))
declare protocols: OCPPVersionType[];
@Column(DataType.INTEGER)
declare messageTimeout: number;
@Column(DataType.INTEGER)
declare securityProfile: number;
@Column(DataType.BOOLEAN)
declare allowUnknownChargingStations: boolean;
@Column(DataType.BOOLEAN)
declare dynamicTenantResolution: boolean;
@Column(DataType.JSONB)
declare tenantPathMapping?: Record<string, number>;
@Column(DataType.STRING)
declare tlsKeyFilePath?: string;
@Column(DataType.STRING)
declare tlsCertificateChainFilePath?: string;
@Column(DataType.STRING)
declare mtlsCertificateAuthorityKeyFilePath?: string;
@Column(DataType.STRING)
declare rootCACertificateFilePath?: string;
@BelongsToMany(() => ChargingStation, () => ChargingStationNetworkProfile)
declare chargingStations?: ChargingStationDto[] | null;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: true,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId?: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: ServerNetworkProfile) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,156 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import {
DEFAULT_TENANT_ID,
type OCPPInterfaceEnumType,
type OCPPTransportEnumType,
type SetNetworkProfileDto,
} from '@citrineos/base';
import type {
ChargingStationDto,
OCPPVersionEnumType,
ServerNetworkProfileDto,
TenantDto,
} from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
Index,
Model,
Table,
} from 'sequelize-typescript';
import { ChargingStation } from './ChargingStation.js';
import { ServerNetworkProfile } from './ServerNetworkProfile.js';
import { Tenant } from '../Tenant.js';
/**
* The CallMessage model can be extended with new optional fields,
* e.g. chargingProfileId, for other correlationId related lookups.
*/
@Table
export class SetNetworkProfile extends Model implements SetNetworkProfileDto {
static readonly MODEL_NAME: string = 'SetNetworkProfile';
@ForeignKey(() => ChargingStation)
@Column({
type: DataType.INTEGER,
unique: 'stationId_correlationId',
})
declare stationId?: number;
@BelongsTo(() => ChargingStation, 'stationId')
declare chargingStation?: ChargingStationDto;
@Column(DataType.STRING)
declare ocppConnectionName: string;
@Index
@Column({
type: DataType.STRING,
unique: 'stationId_correlationId',
})
declare correlationId: string;
@ForeignKey(() => ServerNetworkProfile)
@Column(DataType.STRING)
declare websocketServerConfigId?: string;
@BelongsTo(() => ServerNetworkProfile, 'websocketServerConfigId')
declare websocketServerConfig?: ServerNetworkProfileDto;
@Column(DataType.INTEGER)
declare configurationSlot: number;
@Column(DataType.STRING)
declare ocppVersion: OCPPVersionEnumType;
@Column(DataType.STRING)
declare ocppTransport: OCPPTransportEnumType;
/**
* Communication_ Function. OCPP_ Central_ System_ URL. URI
* urn:x-oca:ocpp:uid:1:569357
* URL of the CSMS(s) that this Charging Station communicates with.
*
*/
@Column(DataType.STRING)
declare ocppCsmsUrl: string;
/**
* Duration in seconds before a message send by the Charging Station via this network connection times-out.
* The best setting depends on the underlying network and response times of the CSMS.
* If you are looking for a some guideline: use 30 seconds as a starting point.
*
*/
@Column(DataType.INTEGER)
declare messageTimeout: number;
/**
* This field specifies the security profile used when connecting to the CSMS with this NetworkConnectionProfile.
*
*/
@Column(DataType.INTEGER)
declare securityProfile: number;
@Column(DataType.STRING)
declare ocppInterface: OCPPInterfaceEnumType;
/**
* Stringified JSON of {@link OCPP2_0_1.APNType} for display purposes only
*
*/
@Column(DataType.STRING)
declare apn?: string;
/**
* Stringified JSON of {@link OCPP2_0_1.VPNType} for display purposes only
*
*/
@Column(DataType.STRING)
declare vpn?: string;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeCreate
static async resolveStationId(instance: SetNetworkProfile): Promise<void> {
if (instance.stationId == null && instance.ocppConnectionName && instance.tenantId != null) {
const station = await ChargingStation.findOne({
where: { ocppConnectionName: instance.ocppConnectionName, tenantId: instance.tenantId },
attributes: ['id'],
});
if (station) {
instance.stationId = station.id;
}
}
}
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: SetNetworkProfile) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,114 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type {
ChargingStationDto,
ConnectorStatusEnumType,
ConnectorDto,
StatusNotificationDto,
TenantDto,
} from '@citrineos/base';
import { DEFAULT_TENANT_ID, Namespace } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
Model,
Table,
} from 'sequelize-typescript';
import { ChargingStation } from './ChargingStation.js';
import { Connector } from './Connector.js';
import { Tenant } from '../Tenant.js';
@Table
export class StatusNotification extends Model implements StatusNotificationDto {
static readonly MODEL_NAME: string = Namespace.StatusNotificationRequest;
@ForeignKey(() => ChargingStation)
@Column(DataType.INTEGER)
declare stationId?: number;
@Column(DataType.STRING)
declare ocppConnectionName: string;
@BelongsTo(() => ChargingStation, 'stationId')
declare chargingStation: ChargingStationDto;
@Column({
type: DataType.DATE,
get() {
const timestamp = this.getDataValue('timestamp');
return timestamp ? timestamp.toISOString() : null;
},
})
declare timestamp?: string | null;
@Column(DataType.STRING)
declare connectorStatus: ConnectorStatusEnumType;
@Column(DataType.INTEGER)
declare evseId?: number | null;
@Column(DataType.INTEGER)
declare connectorId: number;
@Column(DataType.STRING)
declare errorCode?: string | null;
@Column(DataType.STRING)
declare info?: string | null;
@Column(DataType.STRING)
declare vendorId?: string | null;
@Column(DataType.STRING)
declare vendorErrorCode?: string | null;
declare customData?: object | null;
@BelongsTo(() => Connector, 'connectorId')
declare connector?: ConnectorDto;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeCreate
static async resolveStationId(instance: StatusNotification): Promise<void> {
if (instance.stationId == null && instance.ocppConnectionName && instance.tenantId != null) {
const station = await ChargingStation.findOne({
where: { ocppConnectionName: instance.ocppConnectionName, tenantId: instance.tenantId },
attributes: ['id'],
});
if (station) {
instance.stationId = station.id;
}
}
}
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: StatusNotification) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
export { Location } from './Location.js';
export { ChargingStation } from './ChargingStation.js';
export { Evse } from './Evse.js';
export { ChargingStationNetworkProfile } from './ChargingStationNetworkProfile.js';
export { LatestStatusNotification } from './LatestStatusNotification.js';
export { StatusNotification } from './StatusNotification.js';
export { ServerNetworkProfile } from './ServerNetworkProfile.js';
export { SetNetworkProfile } from './SetNetworkProfile.js';
export { Connector } from './Connector.js';

View File

@@ -0,0 +1,130 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type {
ComponentDto,
MessageInfoDto,
MessagePriorityEnumType,
MessageStateEnumType,
TenantDto,
MessageContent,
} from '@citrineos/base';
import { DEFAULT_TENANT_ID, OCPP2_0_1, OCPP2_Namespace } from '@citrineos/base';
import {
AutoIncrement,
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
Index,
Model,
PrimaryKey,
Table,
} from 'sequelize-typescript';
import { Component } from '../DeviceModel/Component.js';
import { Tenant } from '../Tenant.js';
@Table
export class MessageInfo extends Model implements MessageInfoDto {
static readonly MODEL_NAME: string = OCPP2_Namespace.MessageInfoType;
/**
* Fields
*/
@PrimaryKey
@AutoIncrement
@Column(DataType.INTEGER)
declare databaseId: number;
@Index
@Column({
type: DataType.STRING,
unique: 'stationName_tenantId_id',
})
declare ocppConnectionName: string;
@Column({
unique: 'stationName_tenantId_id',
type: DataType.INTEGER,
})
declare id: number;
@Column(DataType.STRING)
declare priority: MessagePriorityEnumType;
@Column(DataType.STRING)
declare state?: MessageStateEnumType | null;
@Column({
type: DataType.DATE,
get() {
const startDateTime: Date = this.getDataValue('startDateTime');
return startDateTime ? startDateTime.toISOString() : null;
},
})
declare startDateTime?: string | null;
@Column({
type: DataType.DATE,
get() {
const endDateTime: Date = this.getDataValue('endDateTime');
return endDateTime ? endDateTime.toISOString() : null;
},
})
declare endDateTime?: string | null;
@Column(DataType.STRING)
declare transactionId?: string | null;
@Column(DataType.JSON)
declare message: MessageContent;
@Column(DataType.BOOLEAN)
declare active: boolean;
/**
* Relations
*/
@BelongsTo(() => Component, 'displayComponentId')
declare display: ComponentDto;
@ForeignKey(() => Component)
@Column({
type: DataType.INTEGER,
})
declare displayComponentId?: number | null;
declare customData?: OCPP2_0_1.CustomDataType | null;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
unique: 'stationName_tenantId_id',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: MessageInfo) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,4 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
export { MessageInfo } from './MessageInfo.js';

View File

@@ -0,0 +1,114 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type { ChargingStationDto, MessageState, OCPPMessageDto, TenantDto } from '@citrineos/base';
import { DEFAULT_TENANT_ID, MessageOrigin, Namespace, OCPPVersion } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
HasMany,
Index,
Model,
Table,
} from 'sequelize-typescript';
import { ChargingStation } from './Location/index.js';
import { Tenant } from './Tenant.js';
@Table
export class OCPPMessage extends Model implements OCPPMessageDto {
static readonly MODEL_NAME: string = Namespace.OCPPMessage;
@ForeignKey(() => ChargingStation)
@Column(DataType.INTEGER)
declare stationId?: number;
@Index
@Column(DataType.STRING)
declare ocppConnectionName: string;
@Index
@Column(DataType.STRING)
declare correlationId?: string;
@Column(DataType.STRING)
declare origin: MessageOrigin;
@Column(DataType.STRING)
declare state: MessageState;
@Column(DataType.STRING)
declare protocol: OCPPVersion;
@Column(DataType.STRING)
declare action: string;
@Column(DataType.JSONB)
declare message: any;
@BelongsTo(() => ChargingStation, 'stationId')
declare chargingStation?: ChargingStationDto;
@ForeignKey(() => OCPPMessage)
@Index
@Column(DataType.INTEGER)
declare requestMessageId?: number;
@Column({
type: DataType.DATE,
get() {
return this.getDataValue('timestamp')?.toISOString();
},
})
declare timestamp: string;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BelongsTo(() => OCPPMessage, { foreignKey: 'requestMessageId', as: 'requestMessage' })
declare requestMessage?: OCPPMessage;
@HasMany(() => OCPPMessage, { foreignKey: 'requestMessageId', as: 'responseMessages' })
declare responseMessages?: OCPPMessage[];
@BeforeCreate
static async resolveStationId(instance: OCPPMessage): Promise<void> {
if (instance.stationId == null && instance.ocppConnectionName && instance.tenantId != null) {
const station = await ChargingStation.findOne({
where: { ocppConnectionName: instance.ocppConnectionName, tenantId: instance.tenantId },
attributes: ['id'],
});
if (station) {
instance.stationId = station.id;
}
}
}
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: OCPPMessage) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,110 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type { EvseTypeDto, ReservationDto, TenantDto } from '@citrineos/base';
import { DEFAULT_TENANT_ID, Namespace } from '@citrineos/base';
import {
AutoIncrement,
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
Model,
PrimaryKey,
Table,
} from 'sequelize-typescript';
import { EvseType } from './DeviceModel/index.js';
import { Tenant } from './Tenant.js';
@Table
export class Reservation extends Model implements ReservationDto {
static readonly MODEL_NAME: string = Namespace.ReserveNowRequest;
/**
* Fields
*/
@PrimaryKey
@AutoIncrement
@Column(DataType.INTEGER)
declare databaseId: number;
@Column({
type: DataType.INTEGER,
unique: 'stationName_tenantId_id',
})
declare id: number;
@Column({
type: DataType.STRING,
unique: 'stationName_tenantId_id',
})
declare ocppConnectionName: string;
@Column({
type: DataType.DATE,
get() {
const expiryDateTime: Date = this.getDataValue('expiryDateTime');
return expiryDateTime ? expiryDateTime.toISOString() : null;
},
})
declare expiryDateTime: string;
@Column(DataType.STRING)
declare connectorType?: string | null;
@Column(DataType.STRING)
declare reserveStatus?: string | null;
@Column({ type: DataType.BOOLEAN, defaultValue: false })
declare isActive: boolean;
@Column(DataType.STRING)
declare terminatedByTransaction?: string | null;
@Column(DataType.JSONB)
declare idToken: object;
@Column(DataType.JSONB)
declare groupIdToken?: object | null;
/**
* Relations
*/
@ForeignKey(() => EvseType)
declare evseId?: number | null;
@BelongsTo(() => EvseType, 'evseId')
declare evse?: EvseTypeDto | null;
declare customData?: any | null;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
unique: 'stationName_tenantId_id',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: Reservation) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,72 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { DEFAULT_TENANT_ID, OCPP2_0_1, OCPP2_Namespace } from '@citrineos/base';
import type { SecurityEventDto, TenantDto } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
Index,
Model,
Table,
} from 'sequelize-typescript';
import { Tenant } from './Tenant.js';
@Table
export class SecurityEvent extends Model implements SecurityEventDto {
static readonly MODEL_NAME: string = OCPP2_Namespace.SecurityEventNotificationRequest;
/**
* Fields
*/
@Index
@Column(DataType.STRING)
declare ocppConnectionName: string;
@Column(DataType.STRING)
declare type: string;
@Column({
type: DataType.DATE,
get() {
return this.getDataValue('timestamp').toISOString();
},
})
declare timestamp: string;
@Column(DataType.STRING)
declare techInfo?: string | null;
declare customData?: OCPP2_0_1.CustomDataType | null;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: SecurityEvent) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

View File

@@ -0,0 +1,83 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import type { SubscriptionDto, TenantDto } from '@citrineos/base';
import { DEFAULT_TENANT_ID, OCPP2_Namespace } from '@citrineos/base';
import {
BeforeCreate,
BeforeUpdate,
BelongsTo,
Column,
DataType,
ForeignKey,
Index,
Model,
Table,
} from 'sequelize-typescript';
import { Tenant } from '../Tenant.js';
@Table
export class Subscription extends Model implements SubscriptionDto {
static readonly MODEL_NAME: string = OCPP2_Namespace.Subscription;
@Index
@Column(DataType.STRING)
declare ocppConnectionName: string;
@Column({
type: DataType.BOOLEAN,
defaultValue: false,
})
declare onConnect: boolean;
@Column({
type: DataType.BOOLEAN,
defaultValue: false,
})
declare onClose: boolean;
@Column({
type: DataType.BOOLEAN,
defaultValue: false,
})
declare onMessage: boolean;
@Column({
type: DataType.BOOLEAN,
defaultValue: false,
})
declare sentMessage: boolean;
@Column(DataType.STRING)
declare messageRegexFilter?: string | null;
@Column(DataType.STRING)
declare url: string;
@ForeignKey(() => Tenant)
@Column({
type: DataType.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
onDelete: 'RESTRICT',
})
declare tenantId: number;
@BelongsTo(() => Tenant, 'tenantId')
declare tenant?: TenantDto;
@BeforeUpdate
@BeforeCreate
static setDefaultTenant(instance: Subscription) {
if (instance.tenantId == null) {
instance.tenantId = DEFAULT_TENANT_ID;
}
}
constructor(...args: any[]) {
super(...args);
if (this.tenantId == null) {
this.tenantId = DEFAULT_TENANT_ID;
}
}
}

Some files were not shown because too many files have changed in this diff Show More