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,66 @@
1
+ export const CONNECTOR_STATUS_SEQUENCE = [
2
+ "Available",
3
+ "Preparing",
4
+ "Charging",
5
+ "SuspendedEVSE",
6
+ "SuspendedEV",
7
+ "Finishing",
8
+ "Reserved",
9
+ "Unavailable",
10
+ "Faulted"
11
+ ];
12
+ export function nextConnectorStatus(status) {
13
+ const currentIndex = CONNECTOR_STATUS_SEQUENCE.indexOf(status);
14
+ return CONNECTOR_STATUS_SEQUENCE[(currentIndex + 1) % CONNECTOR_STATUS_SEQUENCE.length] ?? "Available";
15
+ }
16
+ export function faultErrorCode(connector) {
17
+ return connector.errorCode === "NoError" ? "OtherError" : connector.errorCode;
18
+ }
19
+ export function statusAfterReservationClear(connector) {
20
+ if (connector.availability === "Inoperative" || connector.status === "Unavailable") {
21
+ return "Unavailable";
22
+ }
23
+ if (connector.status === "Faulted") {
24
+ return "Faulted";
25
+ }
26
+ if (connector.transactionId) {
27
+ return "Charging";
28
+ }
29
+ if (connector.evConnected) {
30
+ return "Preparing";
31
+ }
32
+ return "Available";
33
+ }
34
+ export function statusAfterFaultClear(connector) {
35
+ if (connector.availability === "Inoperative") {
36
+ return "Unavailable";
37
+ }
38
+ if (connector.transactionId) {
39
+ return "Charging";
40
+ }
41
+ if (connector.evConnected) {
42
+ return "Preparing";
43
+ }
44
+ if (connector.reservationId) {
45
+ return "Reserved";
46
+ }
47
+ return "Available";
48
+ }
49
+ export function statusAfterAvailabilityChange(connector, availability) {
50
+ if (connector.status === "Faulted") {
51
+ return "Faulted";
52
+ }
53
+ if (availability === "Inoperative") {
54
+ return "Unavailable";
55
+ }
56
+ if (connector.status === "Finishing") {
57
+ return "Finishing";
58
+ }
59
+ if (connector.reservationId) {
60
+ return "Reserved";
61
+ }
62
+ if (connector.evConnected) {
63
+ return "Preparing";
64
+ }
65
+ return "Available";
66
+ }
@@ -0,0 +1,8 @@
1
+ export interface LocalAuthListUpdateResult {
2
+ status: "Accepted" | "Failed" | "NotSupported" | "VersionMismatch";
3
+ listVersion: number;
4
+ }
5
+ export declare function applyLocalAuthListUpdate(localAuthorizationList: Map<string, Record<string, unknown> | undefined>, currentVersion: number, payload: Record<string, unknown>, options: {
6
+ enabled: boolean;
7
+ maxLength: number;
8
+ }): LocalAuthListUpdateResult;
@@ -0,0 +1,31 @@
1
+ import { readNumber, readObject, readString } from "./payload.js";
2
+ export function applyLocalAuthListUpdate(localAuthorizationList, currentVersion, payload, options) {
3
+ const listVersion = readNumber(payload.listVersion, currentVersion);
4
+ const updateType = readString(payload.updateType, "Full");
5
+ const entries = Array.isArray(payload.localAuthorizationList) ? payload.localAuthorizationList : [];
6
+ if (!options.enabled) {
7
+ return { status: "NotSupported", listVersion: currentVersion };
8
+ }
9
+ if (entries.length > options.maxLength) {
10
+ return { status: "Failed", listVersion: currentVersion };
11
+ }
12
+ if (updateType === "Differential" && listVersion <= currentVersion) {
13
+ return { status: "VersionMismatch", listVersion: currentVersion };
14
+ }
15
+ if (updateType === "Full") {
16
+ localAuthorizationList.clear();
17
+ }
18
+ for (const item of entries) {
19
+ const entry = readObject(item);
20
+ const idTag = readString(entry.idTag, "");
21
+ if (!idTag) {
22
+ continue;
23
+ }
24
+ if (updateType === "Differential" && !("idTagInfo" in entry)) {
25
+ localAuthorizationList.delete(idTag);
26
+ continue;
27
+ }
28
+ localAuthorizationList.set(idTag, "idTagInfo" in entry ? readObject(entry.idTagInfo) : undefined);
29
+ }
30
+ return { status: "Accepted", listVersion };
31
+ }
@@ -0,0 +1,25 @@
1
+ import type { DiagnosticsStatus, FirmwareStatus } from "../ocpp/types.js";
2
+ export type DiagnosticsOutcome = "success" | "uploadFailure";
3
+ export type FirmwareOutcome = "success" | "downloadFailure" | "installationFailure";
4
+ export declare class MaintenanceLifecycle {
5
+ private readonly options;
6
+ private diagnosticsTimers;
7
+ private firmwareTimers;
8
+ private diagnosticsOutcome;
9
+ private firmwareOutcome;
10
+ constructor(options: {
11
+ run: (task: () => Promise<void>) => void;
12
+ diagnosticsStatus: (status: DiagnosticsStatus) => Promise<void>;
13
+ firmwareStatus: (status: FirmwareStatus) => Promise<void>;
14
+ log: (level: "info" | "warn", message: string) => void;
15
+ });
16
+ setDiagnosticsOutcome(outcome: DiagnosticsOutcome): void;
17
+ setFirmwareOutcome(outcome: FirmwareOutcome): void;
18
+ startDiagnostics(payload: Record<string, unknown>): string;
19
+ startFirmware(payload: Record<string, unknown>): void;
20
+ clearAll(): void;
21
+ private clearDiagnostics;
22
+ private clearFirmware;
23
+ private schedule;
24
+ private clear;
25
+ }
@@ -0,0 +1,110 @@
1
+ import { readNumber, readString } from "./payload.js";
2
+ export class MaintenanceLifecycle {
3
+ options;
4
+ diagnosticsTimers = [];
5
+ firmwareTimers = [];
6
+ diagnosticsOutcome = "success";
7
+ firmwareOutcome = "success";
8
+ constructor(options) {
9
+ this.options = options;
10
+ }
11
+ setDiagnosticsOutcome(outcome) {
12
+ this.diagnosticsOutcome = outcome;
13
+ }
14
+ setFirmwareOutcome(outcome) {
15
+ this.firmwareOutcome = outcome;
16
+ }
17
+ startDiagnostics(payload) {
18
+ this.clearDiagnostics();
19
+ const location = readString(payload.location, "");
20
+ const fileName = diagnosticsFileName(location);
21
+ const retries = boundedRetries(readNumber(payload.retries, 0));
22
+ const retryInterval = readNumber(payload.retryInterval, 0);
23
+ const startDelay = retryInterval > 0 ? Math.min(retryInterval * 1000, 1000) : 100;
24
+ const stepDelay = retryInterval > 0 ? Math.min(retryInterval * 1000, 1000) : 400;
25
+ const statuses = diagnosticsStatuses(this.diagnosticsOutcome, retries, startDelay, stepDelay);
26
+ this.options.log("info", `Diagnostics upload scheduled to ${location}`);
27
+ this.diagnosticsTimers = this.schedule(statuses, this.options.diagnosticsStatus);
28
+ return fileName;
29
+ }
30
+ startFirmware(payload) {
31
+ this.clearFirmware();
32
+ const location = readString(payload.location, "");
33
+ const retrieveDate = readString(payload.retrieveDate, "");
34
+ const retries = boundedRetries(readNumber(payload.retries, 0));
35
+ const retryInterval = readNumber(payload.retryInterval, 0);
36
+ const startDelay = Math.min(Math.max(Date.parse(retrieveDate) - Date.now(), 100), 1000);
37
+ const stepDelay = retryInterval > 0 ? Math.min(retryInterval * 1000, 1000) : 400;
38
+ const statuses = firmwareStatuses(this.firmwareOutcome, retries, startDelay, stepDelay);
39
+ this.options.log("info", `Firmware update scheduled from ${location}`);
40
+ this.firmwareTimers = this.schedule(statuses, this.options.firmwareStatus);
41
+ }
42
+ clearAll() {
43
+ this.clearDiagnostics();
44
+ this.clearFirmware();
45
+ }
46
+ clearDiagnostics() {
47
+ this.clear(this.diagnosticsTimers);
48
+ this.diagnosticsTimers = [];
49
+ }
50
+ clearFirmware() {
51
+ this.clear(this.firmwareTimers);
52
+ this.firmwareTimers = [];
53
+ }
54
+ schedule(statuses, send) {
55
+ return statuses.map(({ status, delayMs }) => setTimeout(() => {
56
+ this.options.run(() => send(status));
57
+ }, delayMs));
58
+ }
59
+ clear(timers) {
60
+ for (const timer of timers) {
61
+ clearTimeout(timer);
62
+ }
63
+ }
64
+ }
65
+ function diagnosticsFileName(location) {
66
+ try {
67
+ const url = new URL(location);
68
+ const leaf = url.pathname.split("/").filter(Boolean).at(-1);
69
+ return leaf || "kirby-ocpp-diagnostics.log";
70
+ }
71
+ catch {
72
+ return "kirby-ocpp-diagnostics.log";
73
+ }
74
+ }
75
+ function diagnosticsStatuses(outcome, retries, startDelay, stepDelay) {
76
+ if (outcome === "uploadFailure") {
77
+ return retryingStatuses("Uploading", "UploadFailed", retries, startDelay, stepDelay);
78
+ }
79
+ return [
80
+ ...Array.from({ length: retries + 1 }, (_, index) => ({ status: "Uploading", delayMs: startDelay + stepDelay * index })),
81
+ { status: "Uploaded", delayMs: startDelay + stepDelay * (retries + 1) }
82
+ ];
83
+ }
84
+ function firmwareStatuses(outcome, retries, startDelay, stepDelay) {
85
+ if (outcome === "downloadFailure") {
86
+ return retryingStatuses("Downloading", "DownloadFailed", retries, startDelay, stepDelay);
87
+ }
88
+ if (outcome === "installationFailure") {
89
+ return [
90
+ { status: "Downloading", delayMs: startDelay },
91
+ { status: "Downloaded", delayMs: startDelay + stepDelay },
92
+ ...retryingStatuses("Installing", "InstallationFailed", retries, startDelay + stepDelay * 2, stepDelay)
93
+ ];
94
+ }
95
+ return [
96
+ { status: "Downloading", delayMs: startDelay },
97
+ { status: "Downloaded", delayMs: startDelay + stepDelay },
98
+ { status: "Installing", delayMs: startDelay + stepDelay * 2 },
99
+ { status: "Installed", delayMs: startDelay + stepDelay * 3 }
100
+ ];
101
+ }
102
+ function retryingStatuses(activeStatus, failureStatus, retries, startDelay, stepDelay) {
103
+ return [
104
+ ...Array.from({ length: retries + 1 }, (_, index) => ({ status: activeStatus, delayMs: startDelay + stepDelay * index })),
105
+ { status: failureStatus, delayMs: startDelay + stepDelay * (retries + 1) }
106
+ ];
107
+ }
108
+ function boundedRetries(value) {
109
+ return Number.isInteger(value) && value > 0 ? Math.min(value, 5) : 0;
110
+ }
@@ -0,0 +1,21 @@
1
+ export declare class MeterValueScheduler {
2
+ private readonly options;
3
+ private readonly periodicTimers;
4
+ private clockAlignedTimer?;
5
+ constructor(options: {
6
+ sampleIntervalSeconds: () => number;
7
+ clockAlignedIntervalSeconds: () => number;
8
+ isConnected: () => boolean;
9
+ activeConnectorIds: () => number[];
10
+ isPeriodicConnectorActive: (connectorId: number) => boolean;
11
+ sendPeriodic: (connectorId: number, deltaWh: number) => Promise<void>;
12
+ sendClockAligned: (connectorIds: number[], deltaWh: number) => Promise<void>;
13
+ runSafely: (task: () => Promise<void>) => void;
14
+ });
15
+ startPeriodic(connectorId: number): void;
16
+ stopPeriodic(connectorId: number): void;
17
+ stopAllPeriodic(): void;
18
+ reschedulePeriodic(connectorIds: number[]): void;
19
+ startClockAligned(): void;
20
+ stopClockAligned(): void;
21
+ }
@@ -0,0 +1,78 @@
1
+ import { clockAlignedDelayMs, periodicMeterDeltaWh } from "./metering.js";
2
+ export class MeterValueScheduler {
3
+ options;
4
+ periodicTimers = new Map();
5
+ clockAlignedTimer;
6
+ constructor(options) {
7
+ this.options = options;
8
+ }
9
+ startPeriodic(connectorId) {
10
+ this.stopPeriodic(connectorId);
11
+ const intervalSeconds = this.options.sampleIntervalSeconds();
12
+ if (intervalSeconds <= 0) {
13
+ return;
14
+ }
15
+ const timer = setInterval(() => {
16
+ this.options.runSafely(async () => {
17
+ if (!this.options.isConnected()) {
18
+ return;
19
+ }
20
+ if (!this.options.isPeriodicConnectorActive(connectorId)) {
21
+ this.stopPeriodic(connectorId);
22
+ return;
23
+ }
24
+ await this.options.sendPeriodic(connectorId, periodicMeterDeltaWh(intervalSeconds));
25
+ });
26
+ }, intervalSeconds * 1000);
27
+ this.periodicTimers.set(connectorId, timer);
28
+ }
29
+ stopPeriodic(connectorId) {
30
+ const timer = this.periodicTimers.get(connectorId);
31
+ if (!timer) {
32
+ return;
33
+ }
34
+ clearInterval(timer);
35
+ this.periodicTimers.delete(connectorId);
36
+ }
37
+ stopAllPeriodic() {
38
+ for (const connectorId of [...this.periodicTimers.keys()]) {
39
+ this.stopPeriodic(connectorId);
40
+ }
41
+ }
42
+ reschedulePeriodic(connectorIds) {
43
+ for (const connectorId of connectorIds) {
44
+ if (this.options.isPeriodicConnectorActive(connectorId)) {
45
+ this.startPeriodic(connectorId);
46
+ continue;
47
+ }
48
+ this.stopPeriodic(connectorId);
49
+ }
50
+ }
51
+ startClockAligned() {
52
+ this.stopClockAligned();
53
+ const intervalSeconds = this.options.clockAlignedIntervalSeconds();
54
+ if (intervalSeconds <= 0 || !this.options.isConnected()) {
55
+ return;
56
+ }
57
+ this.clockAlignedTimer = setTimeout(() => {
58
+ this.clockAlignedTimer = undefined;
59
+ this.options.runSafely(async () => {
60
+ try {
61
+ await this.options.sendClockAligned(this.options.activeConnectorIds(), periodicMeterDeltaWh(intervalSeconds));
62
+ }
63
+ finally {
64
+ if (this.options.isConnected()) {
65
+ this.startClockAligned();
66
+ }
67
+ }
68
+ });
69
+ }, clockAlignedDelayMs(intervalSeconds));
70
+ }
71
+ stopClockAligned() {
72
+ if (!this.clockAlignedTimer) {
73
+ return;
74
+ }
75
+ clearTimeout(this.clockAlignedTimer);
76
+ this.clockAlignedTimer = undefined;
77
+ }
78
+ }
@@ -0,0 +1,15 @@
1
+ import type { ConnectorStatus } from "../ocpp/types.js";
2
+ export type MeterValueContext = "Sample.Clock" | "Sample.Periodic" | "Transaction.End";
3
+ export type Measurand = "Energy.Active.Export.Register" | "Energy.Active.Import.Register" | "Energy.Reactive.Export.Register" | "Energy.Reactive.Import.Register" | "Energy.Active.Export.Interval" | "Energy.Active.Import.Interval" | "Energy.Reactive.Export.Interval" | "Energy.Reactive.Import.Interval" | "Power.Active.Export" | "Power.Active.Import" | "Power.Offered" | "Power.Reactive.Export" | "Power.Reactive.Import" | "Power.Factor" | "Current.Import" | "Current.Export" | "Current.Offered" | "Voltage" | "Frequency" | "Temperature" | "SoC" | "RPM";
4
+ export type MeteringConfigurationKey = "MeterValuesAlignedData" | "MeterValuesSampledData" | "StopTxnAlignedData" | "StopTxnSampledData";
5
+ export interface MeteringConnector {
6
+ status: ConnectorStatus;
7
+ meterWh: number;
8
+ }
9
+ export declare const DEFAULT_SAMPLED_DATA: Measurand[];
10
+ export declare const MEASURANDS: Set<Measurand>;
11
+ export declare function buildMeterValue(connector: MeteringConnector, context: MeterValueContext, measurands: Measurand[]): Record<string, unknown>;
12
+ export declare function readConfiguredMeasurands(value: string | undefined): Measurand[];
13
+ export declare function isValidSampledDataConfiguration(value: string): boolean;
14
+ export declare function periodicMeterDeltaWh(intervalSeconds: number): number;
15
+ export declare function clockAlignedDelayMs(intervalSeconds: number): number;
@@ -0,0 +1,138 @@
1
+ export const DEFAULT_SAMPLED_DATA = [
2
+ "Energy.Active.Import.Register",
3
+ "Power.Active.Import",
4
+ "Current.Import",
5
+ "Voltage",
6
+ "Temperature",
7
+ "SoC"
8
+ ];
9
+ export const MEASURANDS = new Set([
10
+ "Energy.Active.Export.Register",
11
+ "Energy.Active.Import.Register",
12
+ "Energy.Reactive.Export.Register",
13
+ "Energy.Reactive.Import.Register",
14
+ "Energy.Active.Export.Interval",
15
+ "Energy.Active.Import.Interval",
16
+ "Energy.Reactive.Export.Interval",
17
+ "Energy.Reactive.Import.Interval",
18
+ "Power.Active.Export",
19
+ "Power.Offered",
20
+ "Power.Active.Import",
21
+ "Power.Reactive.Export",
22
+ "Power.Reactive.Import",
23
+ "Power.Factor",
24
+ "Current.Import",
25
+ "Current.Export",
26
+ "Current.Offered",
27
+ "Voltage",
28
+ "Frequency",
29
+ "Temperature",
30
+ "SoC",
31
+ "RPM"
32
+ ]);
33
+ export function buildMeterValue(connector, context, measurands) {
34
+ return {
35
+ timestamp: new Date().toISOString(),
36
+ sampledValue: measurands.map((measurand) => buildSampledValue(connector, context, measurand))
37
+ };
38
+ }
39
+ export function readConfiguredMeasurands(value) {
40
+ const measurands = (value ?? "")
41
+ .split(",")
42
+ .map((item) => item.trim())
43
+ .filter(Boolean)
44
+ .filter((item) => MEASURANDS.has(item));
45
+ return measurands.length > 0 ? measurands : DEFAULT_SAMPLED_DATA;
46
+ }
47
+ export function isValidSampledDataConfiguration(value) {
48
+ const items = value
49
+ .split(",")
50
+ .map((item) => item.trim())
51
+ .filter(Boolean);
52
+ return items.length > 0 && items.every((item) => MEASURANDS.has(item));
53
+ }
54
+ export function periodicMeterDeltaWh(intervalSeconds) {
55
+ return Math.max(0, Math.round((7200 * intervalSeconds) / 3600));
56
+ }
57
+ export function clockAlignedDelayMs(intervalSeconds) {
58
+ const intervalMs = intervalSeconds * 1000;
59
+ const remainder = Date.now() % intervalMs;
60
+ return remainder === 0 ? intervalMs : intervalMs - remainder;
61
+ }
62
+ function buildSampledValue(connector, context, measurand) {
63
+ const sample = sampleForMeasurand(connector, measurand);
64
+ const sampledValue = {
65
+ value: sample.value,
66
+ context,
67
+ format: "Raw",
68
+ measurand
69
+ };
70
+ if (sample.location) {
71
+ sampledValue.location = sample.location;
72
+ }
73
+ if (sample.phase) {
74
+ sampledValue.phase = sample.phase;
75
+ }
76
+ if (sample.unit) {
77
+ sampledValue.unit = sample.unit;
78
+ }
79
+ return sampledValue;
80
+ }
81
+ function sampleForMeasurand(connector, measurand) {
82
+ const charging = connector.status === "Charging";
83
+ const cycle = Math.abs(Math.round(connector.meterWh / 120)) % 8;
84
+ const voltageV = charging ? 228 + (cycle % 6) : 230;
85
+ const activePowerW = charging ? 6800 + cycle * 90 : 0;
86
+ const currentA = charging ? activePowerW / voltageV : 0;
87
+ const temperatureC = charging ? 29 + cycle + Math.min(8, Math.floor(connector.meterWh / 3000)) : 25;
88
+ const soc = charging ? clampNumber(18 + Math.floor(connector.meterWh / 450), 18, 96) : 0;
89
+ switch (measurand) {
90
+ case "Energy.Active.Import.Register":
91
+ return { value: String(Math.round(connector.meterWh)), unit: "Wh", location: "Outlet" };
92
+ case "Energy.Active.Export.Register":
93
+ return { value: "0", unit: "Wh", location: "Outlet" };
94
+ case "Energy.Reactive.Import.Register":
95
+ case "Energy.Reactive.Export.Register":
96
+ return { value: "0", unit: "varh", location: "Outlet" };
97
+ case "Energy.Active.Import.Interval":
98
+ return { value: charging ? "120" : "0", unit: "Wh", location: "Outlet" };
99
+ case "Energy.Active.Export.Interval":
100
+ return { value: "0", unit: "Wh", location: "Outlet" };
101
+ case "Energy.Reactive.Import.Interval":
102
+ case "Energy.Reactive.Export.Interval":
103
+ return { value: "0", unit: "varh", location: "Outlet" };
104
+ case "Power.Active.Import":
105
+ return { value: String(activePowerW), unit: "W", location: "Outlet" };
106
+ case "Power.Active.Export":
107
+ return { value: "0", unit: "W", location: "Outlet" };
108
+ case "Power.Offered":
109
+ return { value: "7400", unit: "W", location: "Outlet" };
110
+ case "Power.Reactive.Import":
111
+ case "Power.Reactive.Export":
112
+ return { value: "0", unit: "var", location: "Outlet" };
113
+ case "Power.Factor":
114
+ return { value: charging ? "1" : "0", location: "Outlet" };
115
+ case "Current.Import":
116
+ return { value: formatDecimal(currentA, 1), unit: "A", location: "Outlet", phase: "L1" };
117
+ case "Current.Export":
118
+ return { value: "0", unit: "A", location: "Outlet", phase: "L1" };
119
+ case "Current.Offered":
120
+ return { value: "32", unit: "A", location: "Outlet", phase: "L1" };
121
+ case "Voltage":
122
+ return { value: String(voltageV), unit: "V", location: "Outlet", phase: "L1-N" };
123
+ case "Temperature":
124
+ return { value: String(temperatureC), unit: "Celsius", location: "Body" };
125
+ case "SoC":
126
+ return { value: String(soc), unit: "Percent", location: "EV" };
127
+ case "Frequency":
128
+ return { value: "50", location: "Outlet" };
129
+ case "RPM":
130
+ return { value: "0", location: "Body" };
131
+ }
132
+ }
133
+ function clampNumber(value, min, max) {
134
+ return Math.min(Math.max(value, min), max);
135
+ }
136
+ function formatDecimal(value, digits) {
137
+ return value.toFixed(digits).replace(/\.0$/, "");
138
+ }
@@ -0,0 +1,2 @@
1
+ import type { OcppMessage } from "../ocpp/types.js";
2
+ export declare function describeOcppMessage(message: OcppMessage): string;
@@ -0,0 +1,9 @@
1
+ export function describeOcppMessage(message) {
2
+ if (message[0] === 2) {
3
+ return `CALL ${message[2]} (${message[1]})`;
4
+ }
5
+ if (message[0] === 3) {
6
+ return `CALLRESULT (${message[1]})`;
7
+ }
8
+ return `CALLERROR ${message[2]} (${message[1]})`;
9
+ }
@@ -0,0 +1,29 @@
1
+ import type { CallResponse, ConnectorStatus, StopReason } from "../ocpp/types.js";
2
+ import { type IdTagInfo } from "./authorization.js";
3
+ import type { ChargingProfileRegistry } from "./charging-profile-registry.js";
4
+ import type { ConnectorRegistry } from "./connector-registry.js";
5
+ export interface OfflineTransactionSyncOptions {
6
+ connectors: ConnectorRegistry;
7
+ chargingProfiles: ChargingProfileRegistry;
8
+ send: (action: string, payload: Record<string, unknown>) => Promise<CallResponse>;
9
+ readIdTagInfo: (value: unknown) => IdTagInfo;
10
+ rememberAuthorization: (idTag: string, idTagInfo: IdTagInfo) => void;
11
+ save: () => void;
12
+ log: (level: "success" | "warn", message: string) => void;
13
+ statusNotification: (connectorId: number, status: ConnectorStatus) => Promise<void>;
14
+ }
15
+ export declare class OfflineTransactionSync {
16
+ private readonly options;
17
+ constructor(options: OfflineTransactionSyncOptions);
18
+ syncPendingTransactions(): Promise<void>;
19
+ private deliverPendingStartTransaction;
20
+ private deliverPendingStopTransaction;
21
+ }
22
+ export declare function localTransactionId(connectorId: number): number;
23
+ export declare function pendingStopPayload(options: {
24
+ meterStop: number;
25
+ timestamp: string;
26
+ reason: StopReason;
27
+ idTag?: string;
28
+ transactionData: Record<string, unknown>[];
29
+ }): Record<string, unknown>;
@@ -0,0 +1,99 @@
1
+ import { isAcceptedIdTagInfo } from "./authorization.js";
2
+ import { readNumber } from "./payload.js";
3
+ import { statusAfterStopTransaction } from "./transaction-rules.js";
4
+ export class OfflineTransactionSync {
5
+ options;
6
+ constructor(options) {
7
+ this.options = options;
8
+ }
9
+ async syncPendingTransactions() {
10
+ for (const connector of this.options.connectors.all()) {
11
+ if (!connector.pendingStartTransaction) {
12
+ if (connector.pendingStopTransaction && connector.transactionId && connector.transactionId > 0) {
13
+ await this.deliverPendingStopTransaction(connector.id);
14
+ }
15
+ continue;
16
+ }
17
+ await this.deliverPendingStartTransaction(connector.id);
18
+ }
19
+ }
20
+ async deliverPendingStartTransaction(connectorId) {
21
+ const connector = this.options.connectors.get(connectorId);
22
+ const pendingStart = connector.pendingStartTransaction;
23
+ if (!pendingStart) {
24
+ return;
25
+ }
26
+ const response = await this.options.send("StartTransaction", pendingStart);
27
+ const idTagInfo = this.options.readIdTagInfo(response.payload.idTagInfo);
28
+ this.options.rememberAuthorization(pendingStart.idTag, idTagInfo);
29
+ if (!isAcceptedIdTagInfo(idTagInfo)) {
30
+ this.options.log("warn", `Offline StartTransaction rejected during sync: ${idTagInfo.status} (${pendingStart.idTag})`);
31
+ this.options.connectors.patch(connectorId, {
32
+ transactionId: undefined,
33
+ transactionStartedAt: undefined,
34
+ pendingStartTransaction: undefined,
35
+ pendingStopTransaction: undefined,
36
+ stopTransactionAtWh: undefined,
37
+ status: connector.evConnected ? "Finishing" : connector.availability === "Inoperative" ? "Unavailable" : "Available"
38
+ });
39
+ this.options.save();
40
+ return;
41
+ }
42
+ const transactionId = readNumber(response.payload.transactionId, Date.now());
43
+ const oldTransactionId = connector.transactionId;
44
+ this.options.connectors.patch(connectorId, {
45
+ transactionId,
46
+ pendingStartTransaction: undefined
47
+ });
48
+ if (oldTransactionId && oldTransactionId !== transactionId) {
49
+ this.options.chargingProfiles.clearForTransaction(oldTransactionId);
50
+ }
51
+ this.options.save();
52
+ this.options.log("success", `Offline transaction on connector ${connectorId} synced as transaction ${transactionId}`);
53
+ if (this.options.connectors.get(connectorId).pendingStopTransaction) {
54
+ await this.deliverPendingStopTransaction(connectorId);
55
+ }
56
+ }
57
+ async deliverPendingStopTransaction(connectorId) {
58
+ const connector = this.options.connectors.get(connectorId);
59
+ if (!connector.pendingStopTransaction || !connector.transactionId) {
60
+ return;
61
+ }
62
+ const payload = {
63
+ ...connector.pendingStopTransaction,
64
+ transactionId: connector.transactionId
65
+ };
66
+ await this.options.send("StopTransaction", payload);
67
+ const { evConnected, status } = statusAfterStopTransaction(connector.pendingStopTransaction.reason, connector.evConnected, connector.availability);
68
+ const transactionId = connector.transactionId;
69
+ this.options.connectors.patch(connectorId, {
70
+ transactionId: undefined,
71
+ transactionStartedAt: undefined,
72
+ stopTransactionAtWh: undefined,
73
+ pendingStartTransaction: undefined,
74
+ pendingStopTransaction: undefined,
75
+ evConnected,
76
+ status
77
+ });
78
+ if (this.options.chargingProfiles.clearForTransaction(transactionId)) {
79
+ this.options.save();
80
+ }
81
+ this.options.save();
82
+ await this.options.statusNotification(connectorId, status);
83
+ }
84
+ }
85
+ export function localTransactionId(connectorId) {
86
+ return -Math.abs(Date.now() * 100 + connectorId);
87
+ }
88
+ export function pendingStopPayload(options) {
89
+ const payload = {
90
+ meterStop: options.meterStop,
91
+ timestamp: options.timestamp,
92
+ reason: options.reason,
93
+ transactionData: options.transactionData
94
+ };
95
+ if (options.idTag) {
96
+ payload.idTag = options.idTag;
97
+ }
98
+ return payload;
99
+ }
@@ -0,0 +1,4 @@
1
+ export declare function readString(value: unknown, fallback: string): string;
2
+ export declare function readOptionalString(value: unknown): string | undefined;
3
+ export declare function readNumber(value: unknown, fallback: number): number;
4
+ export declare function readObject(value: unknown): Record<string, unknown>;
@@ -0,0 +1,12 @@
1
+ export function readString(value, fallback) {
2
+ return typeof value === "string" ? value : fallback;
3
+ }
4
+ export function readOptionalString(value) {
5
+ return typeof value === "string" ? value : undefined;
6
+ }
7
+ export function readNumber(value, fallback) {
8
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
9
+ }
10
+ export function readObject(value) {
11
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
12
+ }