kirby-ocpp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/THIRD_PARTY_NOTICES.md +15 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +72 -0
  6. package/dist/config.d.ts +2 -0
  7. package/dist/config.js +53 -0
  8. package/dist/ocpp/client.d.ts +59 -0
  9. package/dist/ocpp/client.js +294 -0
  10. package/dist/ocpp/schema-validator.d.ts +15 -0
  11. package/dist/ocpp/schema-validator.js +75 -0
  12. package/dist/ocpp/schemas/json/Authorize.json +16 -0
  13. package/dist/ocpp/schemas/json/AuthorizeResponse.json +40 -0
  14. package/dist/ocpp/schemas/json/BootNotification.json +49 -0
  15. package/dist/ocpp/schemas/json/BootNotificationResponse.json +30 -0
  16. package/dist/ocpp/schemas/json/CancelReservation.json +15 -0
  17. package/dist/ocpp/schemas/json/CancelReservationResponse.json +20 -0
  18. package/dist/ocpp/schemas/json/ChangeAvailability.json +24 -0
  19. package/dist/ocpp/schemas/json/ChangeAvailabilityResponse.json +21 -0
  20. package/dist/ocpp/schemas/json/ChangeConfiguration.json +21 -0
  21. package/dist/ocpp/schemas/json/ChangeConfigurationResponse.json +22 -0
  22. package/dist/ocpp/schemas/json/ClearCache.json +8 -0
  23. package/dist/ocpp/schemas/json/ClearCacheResponse.json +20 -0
  24. package/dist/ocpp/schemas/json/ClearChargingProfile.json +27 -0
  25. package/dist/ocpp/schemas/json/ClearChargingProfileResponse.json +20 -0
  26. package/dist/ocpp/schemas/json/DataTransfer.json +23 -0
  27. package/dist/ocpp/schemas/json/DataTransferResponse.json +25 -0
  28. package/dist/ocpp/schemas/json/DiagnosticsStatusNotification.json +22 -0
  29. package/dist/ocpp/schemas/json/DiagnosticsStatusNotificationResponse.json +8 -0
  30. package/dist/ocpp/schemas/json/FirmwareStatusNotification.json +25 -0
  31. package/dist/ocpp/schemas/json/FirmwareStatusNotificationResponse.json +8 -0
  32. package/dist/ocpp/schemas/json/GetCompositeSchedule.json +27 -0
  33. package/dist/ocpp/schemas/json/GetCompositeScheduleResponse.json +79 -0
  34. package/dist/ocpp/schemas/json/GetConfiguration.json +16 -0
  35. package/dist/ocpp/schemas/json/GetConfigurationResponse.json +40 -0
  36. package/dist/ocpp/schemas/json/GetDiagnostics.json +30 -0
  37. package/dist/ocpp/schemas/json/GetDiagnosticsResponse.json +13 -0
  38. package/dist/ocpp/schemas/json/GetLocalListVersion.json +8 -0
  39. package/dist/ocpp/schemas/json/GetLocalListVersionResponse.json +15 -0
  40. package/dist/ocpp/schemas/json/Heartbeat.json +8 -0
  41. package/dist/ocpp/schemas/json/HeartbeatResponse.json +16 -0
  42. package/dist/ocpp/schemas/json/MeterValues.json +151 -0
  43. package/dist/ocpp/schemas/json/MeterValuesResponse.json +8 -0
  44. package/dist/ocpp/schemas/json/RemoteStartTransaction.json +127 -0
  45. package/dist/ocpp/schemas/json/RemoteStartTransactionResponse.json +20 -0
  46. package/dist/ocpp/schemas/json/RemoteStopTransaction.json +15 -0
  47. package/dist/ocpp/schemas/json/RemoteStopTransactionResponse.json +20 -0
  48. package/dist/ocpp/schemas/json/ReserveNow.json +33 -0
  49. package/dist/ocpp/schemas/json/ReserveNowResponse.json +23 -0
  50. package/dist/ocpp/schemas/json/Reset.json +20 -0
  51. package/dist/ocpp/schemas/json/ResetResponse.json +20 -0
  52. package/dist/ocpp/schemas/json/SendLocalList.json +68 -0
  53. package/dist/ocpp/schemas/json/SendLocalListResponse.json +22 -0
  54. package/dist/ocpp/schemas/json/SetChargingProfile.json +124 -0
  55. package/dist/ocpp/schemas/json/SetChargingProfileResponse.json +21 -0
  56. package/dist/ocpp/schemas/json/StartTransaction.json +32 -0
  57. package/dist/ocpp/schemas/json/StartTransactionResponse.json +44 -0
  58. package/dist/ocpp/schemas/json/StatusNotification.json +70 -0
  59. package/dist/ocpp/schemas/json/StatusNotificationResponse.json +8 -0
  60. package/dist/ocpp/schemas/json/StopTransaction.json +177 -0
  61. package/dist/ocpp/schemas/json/StopTransactionResponse.json +37 -0
  62. package/dist/ocpp/schemas/json/TriggerMessage.json +27 -0
  63. package/dist/ocpp/schemas/json/TriggerMessageResponse.json +21 -0
  64. package/dist/ocpp/schemas/json/UnlockConnector.json +15 -0
  65. package/dist/ocpp/schemas/json/UnlockConnectorResponse.json +21 -0
  66. package/dist/ocpp/schemas/json/UpdateFirmware.json +27 -0
  67. package/dist/ocpp/schemas/json/UpdateFirmwareResponse.json +8 -0
  68. package/dist/ocpp/types.d.ts +106 -0
  69. package/dist/ocpp/types.js +1 -0
  70. package/dist/station/authorization.d.ts +20 -0
  71. package/dist/station/authorization.js +78 -0
  72. package/dist/station/charging-profile-registry.d.ts +18 -0
  73. package/dist/station/charging-profile-registry.js +125 -0
  74. package/dist/station/configuration-commands.d.ts +11 -0
  75. package/dist/station/configuration-commands.js +29 -0
  76. package/dist/station/configuration-registry.d.ts +13 -0
  77. package/dist/station/configuration-registry.js +39 -0
  78. package/dist/station/configuration.d.ts +7 -0
  79. package/dist/station/configuration.js +88 -0
  80. package/dist/station/connector-registry.d.ts +25 -0
  81. package/dist/station/connector-registry.js +89 -0
  82. package/dist/station/connector-status.d.ts +7 -0
  83. package/dist/station/connector-status.js +66 -0
  84. package/dist/station/local-auth-list.d.ts +8 -0
  85. package/dist/station/local-auth-list.js +31 -0
  86. package/dist/station/maintenance-lifecycle.d.ts +25 -0
  87. package/dist/station/maintenance-lifecycle.js +110 -0
  88. package/dist/station/meter-value-scheduler.d.ts +21 -0
  89. package/dist/station/meter-value-scheduler.js +78 -0
  90. package/dist/station/metering.d.ts +15 -0
  91. package/dist/station/metering.js +138 -0
  92. package/dist/station/ocpp-message.d.ts +2 -0
  93. package/dist/station/ocpp-message.js +9 -0
  94. package/dist/station/offline-transaction-sync.d.ts +29 -0
  95. package/dist/station/offline-transaction-sync.js +99 -0
  96. package/dist/station/payload.d.ts +4 -0
  97. package/dist/station/payload.js +12 -0
  98. package/dist/station/persistence.d.ts +28 -0
  99. package/dist/station/persistence.js +206 -0
  100. package/dist/station/reservation-rules.d.ts +3 -0
  101. package/dist/station/reservation-rules.js +18 -0
  102. package/dist/station/reservation-scheduler.d.ts +11 -0
  103. package/dist/station/reservation-scheduler.js +33 -0
  104. package/dist/station/smart-charging.d.ts +45 -0
  105. package/dist/station/smart-charging.js +251 -0
  106. package/dist/station/status-scheduler.d.ts +16 -0
  107. package/dist/station/status-scheduler.js +58 -0
  108. package/dist/station/transaction-message-retry.d.ts +3 -0
  109. package/dist/station/transaction-message-retry.js +7 -0
  110. package/dist/station/transaction-rules.d.ts +7 -0
  111. package/dist/station/transaction-rules.js +34 -0
  112. package/dist/station/trigger-message.d.ts +2 -0
  113. package/dist/station/trigger-message.js +11 -0
  114. package/dist/station-store.d.ts +41 -0
  115. package/dist/station-store.js +104 -0
  116. package/dist/station.d.ts +112 -0
  117. package/dist/station.js +1201 -0
  118. package/dist/tui.d.ts +95 -0
  119. package/dist/tui.js +1601 -0
  120. package/docs/OCPP_1.6J_CONFORMANCE.md +61 -0
  121. package/package.json +62 -0
@@ -0,0 +1,106 @@
1
+ export type OcppCall = [2, string, string, Record<string, unknown>];
2
+ export type OcppCallResult = [3, string, Record<string, unknown>];
3
+ export type OcppCallError = [4, string, string, string, Record<string, unknown>];
4
+ export type OcppMessage = OcppCall | OcppCallResult | OcppCallError;
5
+ export type ConnectorStatus = "Available" | "Preparing" | "Charging" | "SuspendedEVSE" | "SuspendedEV" | "Finishing" | "Reserved" | "Unavailable" | "Faulted";
6
+ export type ChargePointErrorCode = "ConnectorLockFailure" | "EVCommunicationError" | "GroundFailure" | "HighTemperature" | "InternalError" | "LocalListConflict" | "NoError" | "OtherError" | "OverCurrentFailure" | "PowerMeterFailure" | "PowerSwitchFailure" | "ReaderFailure" | "ResetFailure" | "UnderVoltage" | "OverVoltage" | "WeakSignal";
7
+ export type AvailabilityType = "Operative" | "Inoperative";
8
+ export type RegistrationStatus = "Accepted" | "Pending" | "Rejected";
9
+ export type DiagnosticsStatus = "Idle" | "Uploaded" | "UploadFailed" | "Uploading";
10
+ export type FirmwareStatus = "Downloaded" | "DownloadFailed" | "Downloading" | "Idle" | "InstallationFailed" | "Installing" | "Installed";
11
+ export type StopReason = "EmergencyStop" | "EVDisconnected" | "HardReset" | "Local" | "Other" | "PowerLoss" | "Reboot" | "Remote" | "SoftReset" | "UnlockCommand" | "DeAuthorized";
12
+ export interface CallResponse {
13
+ messageId: string;
14
+ payload: Record<string, unknown>;
15
+ }
16
+ export interface CallErrorResponse {
17
+ messageId: string;
18
+ code: string;
19
+ description: string;
20
+ details: Record<string, unknown>;
21
+ }
22
+ export interface PendingCall {
23
+ action: string;
24
+ resolve: (response: CallResponse) => void;
25
+ reject: (error: Error) => void;
26
+ timeout: NodeJS.Timeout;
27
+ }
28
+ export interface StationConfig {
29
+ centralSystemUrl: string;
30
+ chargePointId: string;
31
+ vendor: string;
32
+ model: string;
33
+ connectorId: number;
34
+ connectorCount: number;
35
+ heartbeatIntervalSeconds: number;
36
+ persistState: boolean;
37
+ stateDirectory?: string;
38
+ webSocketSubprotocol: string;
39
+ webSocketPingIntervalSeconds: number;
40
+ webSocketReconnectEnabled: boolean;
41
+ webSocketReconnectInitialDelayMs: number;
42
+ webSocketReconnectMaxDelayMs: number;
43
+ webSocketReconnectMaxAttempts: number;
44
+ callTimeoutMs: number;
45
+ tlsRejectUnauthorized: boolean;
46
+ tlsCaFile?: string;
47
+ tlsCertFile?: string;
48
+ tlsKeyFile?: string;
49
+ tlsServerName?: string;
50
+ }
51
+ export interface ConnectorState {
52
+ id: number;
53
+ status: ConnectorStatus;
54
+ availability: AvailabilityType;
55
+ errorCode: ChargePointErrorCode;
56
+ info?: string;
57
+ vendorId?: string;
58
+ vendorErrorCode?: string;
59
+ evConnected: boolean;
60
+ transactionId?: number;
61
+ transactionStartedAt?: string;
62
+ reservationId?: number;
63
+ reservationIdTag?: string;
64
+ reservationParentIdTag?: string;
65
+ reservationExpiryDate?: string;
66
+ lastIdTag?: string;
67
+ stopTransactionAtWh?: number;
68
+ pendingStartTransaction?: PendingStartTransaction;
69
+ pendingStopTransaction?: PendingStopTransaction;
70
+ meterWh: number;
71
+ }
72
+ export interface PendingStartTransaction {
73
+ connectorId: number;
74
+ idTag: string;
75
+ meterStart: number;
76
+ timestamp: string;
77
+ reservationId?: number;
78
+ }
79
+ export interface PendingStopTransaction {
80
+ meterStop: number;
81
+ timestamp: string;
82
+ reason: StopReason;
83
+ idTag?: string;
84
+ transactionData: Record<string, unknown>[];
85
+ }
86
+ export interface StationState {
87
+ connected: boolean;
88
+ booted: boolean;
89
+ registrationStatus: RegistrationStatus | "Unknown";
90
+ connectorStatus: ConnectorStatus;
91
+ availability: AvailabilityType;
92
+ transactionId?: number;
93
+ reservationId?: number;
94
+ localListVersion: number;
95
+ meterWh: number;
96
+ connectors: ConnectorState[];
97
+ diagnosticsStatus?: DiagnosticsStatus;
98
+ firmwareStatus?: FirmwareStatus;
99
+ lastHeartbeatAt?: Date;
100
+ }
101
+ export interface LogEntry {
102
+ at: Date;
103
+ level: "info" | "success" | "warn" | "error" | "in" | "out";
104
+ message: string;
105
+ details?: string;
106
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ export type IdTagStatus = "Accepted" | "Blocked" | "Expired" | "Invalid" | "ConcurrentTx";
2
+ export interface IdTagInfo {
3
+ status: IdTagStatus;
4
+ expiryDate?: string;
5
+ parentIdTag?: string;
6
+ }
7
+ export interface AuthorizationDecision {
8
+ accepted: boolean;
9
+ idTagInfo: IdTagInfo;
10
+ source: "CentralSystem" | "LocalList" | "AuthorizationCache" | "OfflineUnknown" | "None";
11
+ }
12
+ export declare function authorizeLocally(idTag: string, requireOfflinePermission: boolean, localAuthorizationList: Map<string, Record<string, unknown> | undefined>, authorizationCache: Map<string, Record<string, unknown> | undefined>, configuration: {
13
+ localAuthListEnabled: boolean;
14
+ localAuthorizeOffline: boolean;
15
+ authorizationCacheEnabled: boolean;
16
+ allowOfflineTxForUnknownId: boolean;
17
+ }): AuthorizationDecision;
18
+ export declare function normalizeIdTagInfo(value: Record<string, unknown> | undefined): IdTagInfo;
19
+ export declare function serializeIdTagInfo(idTagInfo: IdTagInfo): Record<string, unknown>;
20
+ export declare function isAcceptedIdTagInfo(idTagInfo: IdTagInfo): boolean;
@@ -0,0 +1,78 @@
1
+ export function authorizeLocally(idTag, requireOfflinePermission, localAuthorizationList, authorizationCache, configuration) {
2
+ if (requireOfflinePermission && !configuration.localAuthorizeOffline) {
3
+ return {
4
+ accepted: false,
5
+ idTagInfo: { status: "Invalid" },
6
+ source: "None"
7
+ };
8
+ }
9
+ const localListInfo = localAuthorizationList.get(idTag);
10
+ if (configuration.localAuthListEnabled && localAuthorizationList.has(idTag)) {
11
+ const idTagInfo = normalizeIdTagInfo(localListInfo);
12
+ return {
13
+ accepted: isAcceptedIdTagInfo(idTagInfo),
14
+ idTagInfo,
15
+ source: "LocalList"
16
+ };
17
+ }
18
+ if (configuration.authorizationCacheEnabled && authorizationCache.has(idTag)) {
19
+ const idTagInfo = normalizeIdTagInfo(authorizationCache.get(idTag));
20
+ return {
21
+ accepted: isAcceptedIdTagInfo(idTagInfo),
22
+ idTagInfo,
23
+ source: "AuthorizationCache"
24
+ };
25
+ }
26
+ if (requireOfflinePermission && configuration.allowOfflineTxForUnknownId) {
27
+ return {
28
+ accepted: true,
29
+ idTagInfo: { status: "Accepted" },
30
+ source: "OfflineUnknown"
31
+ };
32
+ }
33
+ return {
34
+ accepted: false,
35
+ idTagInfo: { status: "Invalid" },
36
+ source: "None"
37
+ };
38
+ }
39
+ export function normalizeIdTagInfo(value) {
40
+ const status = readIdTagStatus(value?.status, "Accepted");
41
+ const expiryDate = readOptionalString(value?.expiryDate);
42
+ const parentIdTag = readOptionalString(value?.parentIdTag);
43
+ const idTagInfo = {
44
+ status: expiryDate && Date.parse(expiryDate) <= Date.now() ? "Expired" : status
45
+ };
46
+ if (expiryDate) {
47
+ idTagInfo.expiryDate = expiryDate;
48
+ }
49
+ if (parentIdTag) {
50
+ idTagInfo.parentIdTag = parentIdTag;
51
+ }
52
+ return idTagInfo;
53
+ }
54
+ export function serializeIdTagInfo(idTagInfo) {
55
+ const output = { status: idTagInfo.status };
56
+ if (idTagInfo.expiryDate) {
57
+ output.expiryDate = idTagInfo.expiryDate;
58
+ }
59
+ if (idTagInfo.parentIdTag) {
60
+ output.parentIdTag = idTagInfo.parentIdTag;
61
+ }
62
+ return output;
63
+ }
64
+ export function isAcceptedIdTagInfo(idTagInfo) {
65
+ return idTagInfo.status === "Accepted";
66
+ }
67
+ function readIdTagStatus(value, fallback) {
68
+ return value === "Accepted" ||
69
+ value === "Blocked" ||
70
+ value === "Expired" ||
71
+ value === "Invalid" ||
72
+ value === "ConcurrentTx"
73
+ ? value
74
+ : fallback;
75
+ }
76
+ function readOptionalString(value) {
77
+ return typeof value === "string" ? value : undefined;
78
+ }
@@ -0,0 +1,18 @@
1
+ import { type ChargingProfileEntry } from "./smart-charging.js";
2
+ export declare class ChargingProfileRegistry {
3
+ private readonly profiles;
4
+ get length(): number;
5
+ entries(): ChargingProfileEntry[];
6
+ replace(entries: ChargingProfileEntry[]): void;
7
+ set(connectorId: number, profilePayload: unknown, options: {
8
+ hasConnector: (id: number) => boolean;
9
+ transactionIdForConnector: (id: number) => number | undefined;
10
+ }): "Accepted" | "Rejected";
11
+ clear(payload: Record<string, unknown>): "Accepted" | "Unknown";
12
+ clearForTransaction(transactionId: number): boolean;
13
+ pruneExpired(at?: Date): boolean;
14
+ compositeSchedulePayload(payload: Record<string, unknown>, transactionId: number | undefined, transactionStartedAt?: string): {
15
+ payload: Record<string, unknown>;
16
+ prunedExpired: boolean;
17
+ } | undefined;
18
+ }
@@ -0,0 +1,125 @@
1
+ import { canStoreChargingProfile, effectiveChargingProfiles, isChargingProfileExpired, limitTransitionOffsets, matchesChargingProfileFilter, periodAt, readChargingProfile, readChargingRateUnit } from "./smart-charging.js";
2
+ import { readNumber } from "./payload.js";
3
+ export class ChargingProfileRegistry {
4
+ profiles = [];
5
+ get length() {
6
+ return this.profiles.length;
7
+ }
8
+ entries() {
9
+ return [...this.profiles];
10
+ }
11
+ replace(entries) {
12
+ this.profiles.splice(0, this.profiles.length, ...entries);
13
+ }
14
+ set(connectorId, profilePayload, options) {
15
+ const profile = readChargingProfile(profilePayload);
16
+ if (!profile || !canStoreChargingProfile(connectorId, profile, options)) {
17
+ return "Rejected";
18
+ }
19
+ const existingIndex = this.profiles.findIndex((entry) => entry.profile.chargingProfileId === profile.chargingProfileId);
20
+ if (existingIndex >= 0) {
21
+ this.profiles.splice(existingIndex, 1);
22
+ }
23
+ this.profiles.push({ connectorId, profile });
24
+ return "Accepted";
25
+ }
26
+ clear(payload) {
27
+ this.pruneExpired();
28
+ const before = this.profiles.length;
29
+ const remaining = this.profiles.filter((entry) => !matchesChargingProfileFilter(entry, payload));
30
+ this.profiles.splice(0, this.profiles.length, ...remaining);
31
+ return before - this.profiles.length > 0 ? "Accepted" : "Unknown";
32
+ }
33
+ clearForTransaction(transactionId) {
34
+ const before = this.profiles.length;
35
+ const remaining = this.profiles.filter((entry) => entry.profile.chargingProfilePurpose !== "TxProfile" || entry.profile.transactionId !== transactionId);
36
+ this.profiles.splice(0, this.profiles.length, ...remaining);
37
+ return this.profiles.length !== before;
38
+ }
39
+ pruneExpired(at = new Date()) {
40
+ const before = this.profiles.length;
41
+ const remaining = this.profiles.filter((entry) => !isChargingProfileExpired(entry.profile, at));
42
+ this.profiles.splice(0, this.profiles.length, ...remaining);
43
+ return this.profiles.length !== before;
44
+ }
45
+ compositeSchedulePayload(payload, transactionId, transactionStartedAt) {
46
+ const prunedExpired = this.pruneExpired();
47
+ const connectorId = readNumber(payload.connectorId, -1);
48
+ const duration = readNumber(payload.duration, 0);
49
+ const requestedUnit = readChargingRateUnit(payload.chargingRateUnit);
50
+ if (duration <= 0) {
51
+ return undefined;
52
+ }
53
+ const scheduleStart = new Date();
54
+ const defaultUnit = requestedUnit ?? effectiveChargingProfiles(this.profiles, connectorId, transactionId, scheduleStart)[0]?.profile.chargingSchedule.chargingRateUnit ?? "A";
55
+ const unit = defaultUnit;
56
+ const relativeStart = transactionStartedAt ? new Date(transactionStartedAt) : undefined;
57
+ const composite = compositeSchedule(this.profiles, connectorId, transactionId, unit, scheduleStart, duration, relativeStart);
58
+ if (composite.chargingSchedulePeriod.length === 0) {
59
+ return undefined;
60
+ }
61
+ const chargingSchedule = {
62
+ duration,
63
+ startSchedule: scheduleStart.toISOString(),
64
+ chargingRateUnit: unit,
65
+ chargingSchedulePeriod: composite.chargingSchedulePeriod
66
+ };
67
+ if (composite.minChargingRate !== undefined) {
68
+ chargingSchedule.minChargingRate = composite.minChargingRate;
69
+ }
70
+ return {
71
+ prunedExpired,
72
+ payload: {
73
+ status: "Accepted",
74
+ connectorId,
75
+ scheduleStart: scheduleStart.toISOString(),
76
+ chargingSchedule
77
+ }
78
+ };
79
+ }
80
+ }
81
+ function compositeSchedule(entries, connectorId, transactionId, unit, scheduleStart, duration, relativeStart) {
82
+ const offsets = new Set([0]);
83
+ const unitEntries = entries.filter((entry) => entry.profile.chargingSchedule.chargingRateUnit === unit);
84
+ for (const entry of unitEntries) {
85
+ for (const offset of limitTransitionOffsets(entry.profile, scheduleStart, duration, relativeStart)) {
86
+ offsets.add(offset);
87
+ }
88
+ }
89
+ let minChargingRate;
90
+ const chargingSchedulePeriod = [...offsets]
91
+ .sort((left, right) => left - right)
92
+ .map((offset) => {
93
+ const at = new Date(scheduleStart.getTime() + offset * 1000);
94
+ const activeProfiles = effectiveChargingProfiles(unitEntries, connectorId, transactionId, at, unit);
95
+ const activePeriods = activeProfiles
96
+ .map((entry) => ({ entry, period: periodAt(entry.profile, at, relativeStart, scheduleStart) }))
97
+ .filter((item) => item.period !== undefined);
98
+ for (const { entry } of activePeriods) {
99
+ const profileMin = entry.profile.chargingSchedule.minChargingRate;
100
+ if (profileMin !== undefined) {
101
+ minChargingRate = minChargingRate === undefined ? profileMin : Math.max(minChargingRate, profileMin);
102
+ }
103
+ }
104
+ if (activePeriods.length === 0) {
105
+ return undefined;
106
+ }
107
+ const limit = Math.min(...activePeriods.map(({ period }) => period.limit));
108
+ const limitingPhases = activePeriods
109
+ .filter(({ period }) => period.limit === limit)
110
+ .map(({ period }) => period.numberPhases)
111
+ .filter((numberPhases) => typeof numberPhases === "number" && Number.isFinite(numberPhases));
112
+ const period = { startPeriod: offset, limit };
113
+ if (limitingPhases.length > 0) {
114
+ period.numberPhases = Math.min(...limitingPhases);
115
+ }
116
+ return period;
117
+ })
118
+ .filter((period) => period !== undefined);
119
+ return {
120
+ chargingSchedulePeriod: chargingSchedulePeriod.filter((period, index) => index === 0 ||
121
+ period.limit !== chargingSchedulePeriod[index - 1]?.limit ||
122
+ period.numberPhases !== chargingSchedulePeriod[index - 1]?.numberPhases),
123
+ minChargingRate
124
+ };
125
+ }
@@ -0,0 +1,11 @@
1
+ import type { ConfigurationRegistry } from "./configuration-registry.js";
2
+ export interface ChangeConfigurationResult {
3
+ status: "Accepted" | "Rejected" | "NotSupported";
4
+ key: string;
5
+ value: string;
6
+ }
7
+ export declare function changeConfiguration(configuration: ConfigurationRegistry, payload: Record<string, unknown>): ChangeConfigurationResult;
8
+ export declare function getConfiguration(configuration: ConfigurationRegistry, payload: Record<string, unknown>): {
9
+ configurationKey: Record<string, unknown>[];
10
+ unknownKey: string[];
11
+ };
@@ -0,0 +1,29 @@
1
+ import { readString } from "./payload.js";
2
+ export function changeConfiguration(configuration, payload) {
3
+ const key = readString(payload.key, "");
4
+ const value = readString(payload.value, "");
5
+ const existing = configuration.get(key);
6
+ if (!existing) {
7
+ return { status: "NotSupported", key, value };
8
+ }
9
+ if (existing.readonly || (existing.validate && !existing.validate(value))) {
10
+ return { status: "Rejected", key, value };
11
+ }
12
+ configuration.setValue(key, value);
13
+ return { status: "Accepted", key, value };
14
+ }
15
+ export function getConfiguration(configuration, payload) {
16
+ const requestedKeys = Array.isArray(payload.key) ? payload.key.filter((key) => typeof key === "string") : [];
17
+ const keys = requestedKeys.length > 0 ? requestedKeys : configuration.keys();
18
+ const configurationKey = [];
19
+ const unknownKey = [];
20
+ for (const key of keys) {
21
+ const entry = configuration.get(key);
22
+ if (!entry) {
23
+ unknownKey.push(key);
24
+ continue;
25
+ }
26
+ configurationKey.push({ key, readonly: entry.readonly, value: entry.value });
27
+ }
28
+ return { configurationKey, unknownKey };
29
+ }
@@ -0,0 +1,13 @@
1
+ import type { StationConfig } from "../ocpp/types.js";
2
+ import { type ConfigurationKey } from "./configuration.js";
3
+ import { type Measurand, type MeteringConfigurationKey } from "./metering.js";
4
+ export declare class ConfigurationRegistry {
5
+ readonly values: Map<string, ConfigurationKey>;
6
+ constructor(config: StationConfig);
7
+ get(key: string): ConfigurationKey | undefined;
8
+ keys(): string[];
9
+ setValue(key: string, value: string): void;
10
+ boolean(key: string, fallback: boolean): boolean;
11
+ integer(key: string, fallback: number): number;
12
+ measurands(key: MeteringConfigurationKey): Measurand[];
13
+ }
@@ -0,0 +1,39 @@
1
+ import { createDefaultConfiguration } from "./configuration.js";
2
+ import { readConfiguredMeasurands } from "./metering.js";
3
+ export class ConfigurationRegistry {
4
+ values;
5
+ constructor(config) {
6
+ this.values = createDefaultConfiguration(config);
7
+ }
8
+ get(key) {
9
+ return this.values.get(key);
10
+ }
11
+ keys() {
12
+ return [...this.values.keys()];
13
+ }
14
+ setValue(key, value) {
15
+ const existing = this.values.get(key);
16
+ if (!existing) {
17
+ return;
18
+ }
19
+ this.values.set(key, { ...existing, value });
20
+ }
21
+ boolean(key, fallback) {
22
+ const value = this.values.get(key)?.value;
23
+ if (value === undefined) {
24
+ return fallback;
25
+ }
26
+ return value.toLowerCase() === "true";
27
+ }
28
+ integer(key, fallback) {
29
+ const value = this.values.get(key)?.value;
30
+ if (value === undefined) {
31
+ return fallback;
32
+ }
33
+ const parsed = Number.parseInt(value, 10);
34
+ return Number.isSafeInteger(parsed) ? parsed : fallback;
35
+ }
36
+ measurands(key) {
37
+ return readConfiguredMeasurands(this.values.get(key)?.value);
38
+ }
39
+ }
@@ -0,0 +1,7 @@
1
+ import type { StationConfig } from "../ocpp/types.js";
2
+ export interface ConfigurationKey {
3
+ readonly: boolean;
4
+ value: string;
5
+ validate?: (value: string) => boolean;
6
+ }
7
+ export declare function createDefaultConfiguration(config: StationConfig): Map<string, ConfigurationKey>;
@@ -0,0 +1,88 @@
1
+ import { DEFAULT_SAMPLED_DATA, isValidSampledDataConfiguration, MEASURANDS } from "./metering.js";
2
+ const SUPPORTED_FEATURE_PROFILES = [
3
+ "Core",
4
+ "FirmwareManagement",
5
+ "LocalAuthListManagement",
6
+ "Reservation",
7
+ "RemoteTrigger",
8
+ "SmartCharging"
9
+ ];
10
+ const CONNECTOR_PHASE_ROTATIONS = new Set([
11
+ "NotApplicable",
12
+ "Unknown",
13
+ "RST",
14
+ "RTS",
15
+ "SRT",
16
+ "STR",
17
+ "TRS",
18
+ "TSR"
19
+ ]);
20
+ const DEFAULT_SAMPLED_DATA_CONFIGURATION = DEFAULT_SAMPLED_DATA.join(",");
21
+ export function createDefaultConfiguration(config) {
22
+ const configuration = new Map();
23
+ const boolean = (value) => isBooleanConfigurationValue(value);
24
+ const nonNegativeInteger = (value) => isIntegerConfigurationValue(value, 0);
25
+ const positiveInteger = (value) => isIntegerConfigurationValue(value, 1);
26
+ const sampledData = (value) => isValidSampledDataConfiguration(value);
27
+ const set = (key, readonly, value, validate) => {
28
+ configuration.set(key, { readonly, value, validate });
29
+ };
30
+ set("AllowOfflineTxForUnknownId", false, "false", boolean);
31
+ set("AuthorizationCacheEnabled", false, "true", boolean);
32
+ set("AuthorizeRemoteTxRequests", false, "false", boolean);
33
+ set("BlinkRepeat", false, "0", nonNegativeInteger);
34
+ set("ChargePointModel", true, config.model);
35
+ set("ChargePointSerialNumber", true, config.chargePointId);
36
+ set("ChargePointVendor", true, config.vendor);
37
+ set("ClockAlignedDataInterval", false, "0", nonNegativeInteger);
38
+ set("ConnectionTimeOut", false, "60", positiveInteger);
39
+ set("ConnectorPhaseRotation", false, "Unknown", isConnectorPhaseRotationConfiguration);
40
+ set("ConnectorPhaseRotationMaxLength", true, String(Math.max(1, config.connectorCount)));
41
+ set("GetConfigurationMaxKeys", true, "50");
42
+ set("HeartbeatInterval", false, String(config.heartbeatIntervalSeconds), positiveInteger);
43
+ set("LightIntensity", false, "0", nonNegativeInteger);
44
+ set("LocalAuthListEnabled", false, "true", boolean);
45
+ set("LocalAuthListMaxLength", true, "1000");
46
+ set("LocalAuthorizeOffline", false, "false", boolean);
47
+ set("LocalPreAuthorize", false, "false", boolean);
48
+ set("MaxEnergyOnInvalidId", false, "0", nonNegativeInteger);
49
+ set("MeterValuesAlignedData", false, DEFAULT_SAMPLED_DATA_CONFIGURATION, sampledData);
50
+ set("MeterValuesAlignedDataMaxLength", true, String(MEASURANDS.size));
51
+ set("MeterValuesSampledData", false, DEFAULT_SAMPLED_DATA_CONFIGURATION, sampledData);
52
+ set("MeterValuesSampledDataMaxLength", true, String(MEASURANDS.size));
53
+ set("MeterValueSampleInterval", false, "60", nonNegativeInteger);
54
+ set("MinimumStatusDuration", false, "0", nonNegativeInteger);
55
+ set("NumberOfConnectors", true, String(config.connectorCount));
56
+ set("ResetRetries", false, "0", nonNegativeInteger);
57
+ set("SendLocalListMaxLength", true, "1000");
58
+ set("StopTransactionOnEVSideDisconnect", false, "true", boolean);
59
+ set("StopTransactionOnInvalidId", false, "false", boolean);
60
+ set("StopTxnAlignedData", false, DEFAULT_SAMPLED_DATA_CONFIGURATION, sampledData);
61
+ set("StopTxnAlignedDataMaxLength", true, String(MEASURANDS.size));
62
+ set("StopTxnSampledData", false, DEFAULT_SAMPLED_DATA_CONFIGURATION, sampledData);
63
+ set("StopTxnSampledDataMaxLength", true, String(MEASURANDS.size));
64
+ set("SupportedFeatureProfiles", true, SUPPORTED_FEATURE_PROFILES.join(","));
65
+ set("SupportedFeatureProfilesMaxLength", true, String(SUPPORTED_FEATURE_PROFILES.length));
66
+ set("TransactionMessageAttempts", false, "1", positiveInteger);
67
+ set("TransactionMessageRetryInterval", false, "30", positiveInteger);
68
+ set("UnlockConnectorOnEVSideDisconnect", false, "false", boolean);
69
+ set("WebSocketPingInterval", false, "0", nonNegativeInteger);
70
+ return configuration;
71
+ }
72
+ function isBooleanConfigurationValue(value) {
73
+ return value === "true" || value === "false";
74
+ }
75
+ function isIntegerConfigurationValue(value, minimum) {
76
+ if (!/^\d+$/.test(value)) {
77
+ return false;
78
+ }
79
+ const parsed = Number.parseInt(value, 10);
80
+ return Number.isSafeInteger(parsed) && parsed >= minimum;
81
+ }
82
+ function isConnectorPhaseRotationConfiguration(value) {
83
+ const items = value
84
+ .split(",")
85
+ .map((item) => item.trim())
86
+ .filter(Boolean);
87
+ return items.length > 0 && items.every((item) => CONNECTOR_PHASE_ROTATIONS.has(item));
88
+ }
@@ -0,0 +1,25 @@
1
+ import type { ConnectorState } from "../ocpp/types.js";
2
+ export declare class ConnectorRegistry {
3
+ private readonly primaryConnectorId;
4
+ private readonly connectors;
5
+ private onChange?;
6
+ constructor(primaryConnectorId: number, count: number);
7
+ setOnChange(onChange: () => void): void;
8
+ get size(): number;
9
+ has(id: number): boolean;
10
+ get(id: number): ConnectorState;
11
+ find(id: number): ConnectorState | undefined;
12
+ all(): ConnectorState[];
13
+ add(): ConnectorState;
14
+ ensureCount(count: number): void;
15
+ patch(id: number, patch: Partial<ConnectorState>): void;
16
+ snapshot(): {
17
+ connectors: ConnectorState[];
18
+ primary?: ConnectorState;
19
+ };
20
+ findAvailable(): ConnectorState | undefined;
21
+ findByTransaction(transactionId: number): ConnectorState | undefined;
22
+ findByReservation(reservationId: number): ConnectorState | undefined;
23
+ private nextId;
24
+ private notifyChange;
25
+ }
@@ -0,0 +1,89 @@
1
+ export class ConnectorRegistry {
2
+ primaryConnectorId;
3
+ connectors = new Map();
4
+ onChange;
5
+ constructor(primaryConnectorId, count) {
6
+ this.primaryConnectorId = primaryConnectorId;
7
+ const connectorCount = Math.max(validConnectorCount(count), primaryConnectorId);
8
+ for (let connectorId = 1; connectorId <= connectorCount; connectorId += 1) {
9
+ this.connectors.set(connectorId, createConnector(connectorId));
10
+ }
11
+ }
12
+ setOnChange(onChange) {
13
+ this.onChange = onChange;
14
+ }
15
+ get size() {
16
+ return this.connectors.size;
17
+ }
18
+ has(id) {
19
+ return this.connectors.has(id);
20
+ }
21
+ get(id) {
22
+ const connector = this.connectors.get(id);
23
+ if (!connector) {
24
+ throw new Error(`Connector ${id} does not exist`);
25
+ }
26
+ return connector;
27
+ }
28
+ find(id) {
29
+ return this.connectors.get(id);
30
+ }
31
+ all() {
32
+ return [...this.connectors.values()].sort((left, right) => left.id - right.id);
33
+ }
34
+ add() {
35
+ const connector = createConnector(this.nextId());
36
+ this.connectors.set(connector.id, connector);
37
+ this.notifyChange();
38
+ return connector;
39
+ }
40
+ ensureCount(count) {
41
+ const initialSize = this.connectors.size;
42
+ while (this.connectors.size < count) {
43
+ const connector = createConnector(this.nextId());
44
+ this.connectors.set(connector.id, connector);
45
+ }
46
+ if (this.connectors.size !== initialSize) {
47
+ this.notifyChange();
48
+ }
49
+ }
50
+ patch(id, patch) {
51
+ Object.assign(this.get(id), patch);
52
+ this.notifyChange();
53
+ }
54
+ snapshot() {
55
+ const connectors = this.all().map((connector) => ({ ...connector }));
56
+ const primary = this.connectors.get(this.primaryConnectorId) ?? connectors[0];
57
+ return { connectors, primary };
58
+ }
59
+ findAvailable() {
60
+ return this.all().find((connector) => !connector.transactionId &&
61
+ connector.availability === "Operative" &&
62
+ connector.status === "Available");
63
+ }
64
+ findByTransaction(transactionId) {
65
+ return this.all().find((connector) => connector.transactionId === transactionId);
66
+ }
67
+ findByReservation(reservationId) {
68
+ return this.all().find((connector) => connector.reservationId === reservationId);
69
+ }
70
+ nextId() {
71
+ return Math.max(0, ...this.connectors.keys()) + 1;
72
+ }
73
+ notifyChange() {
74
+ this.onChange?.();
75
+ }
76
+ }
77
+ function validConnectorCount(count) {
78
+ return Number.isInteger(count) && count > 0 ? count : 1;
79
+ }
80
+ function createConnector(id) {
81
+ return {
82
+ id,
83
+ status: "Available",
84
+ availability: "Operative",
85
+ errorCode: "NoError",
86
+ evConnected: false,
87
+ meterWh: 0
88
+ };
89
+ }
@@ -0,0 +1,7 @@
1
+ import type { AvailabilityType, ChargePointErrorCode, ConnectorState, ConnectorStatus } from "../ocpp/types.js";
2
+ export declare const CONNECTOR_STATUS_SEQUENCE: ConnectorStatus[];
3
+ export declare function nextConnectorStatus(status: ConnectorStatus): ConnectorStatus;
4
+ export declare function faultErrorCode(connector: ConnectorState): ChargePointErrorCode;
5
+ export declare function statusAfterReservationClear(connector: ConnectorState): ConnectorStatus;
6
+ export declare function statusAfterFaultClear(connector: ConnectorState): ConnectorStatus;
7
+ export declare function statusAfterAvailabilityChange(connector: ConnectorState, availability: AvailabilityType): ConnectorStatus;