@totemsdk/act-ocpp 0.0.0-stage → 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.
@@ -0,0 +1,537 @@
1
+ "use strict";
2
+ /**
3
+ * RFC-024 §5.3 — governed OCPP operations.
4
+ *
5
+ * Each definition prepares a deterministic, commitment-bound OCPP command and
6
+ * derives its physical effects from that prepared command (never from hints).
7
+ * `startSession`/`stopSession` are asynchronous: acceptance is not completion,
8
+ * and completion is established by charger meter/transaction readback.
9
+ *
10
+ * Configuration writes (`setLimit`, `clearProfile`, `reserveConnector`) use
11
+ * `confirmation: 'completed'` because the CSMS acknowledgement *is* the terminal
12
+ * state for a profile/ reservation change; only a session has a later physical
13
+ * outcome to observe.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.createOcppDefinitions = createOcppDefinitions;
17
+ exports.errorCodeOf = errorCodeOf;
18
+ const act_core_1 = require("@totemsdk/act-core");
19
+ const effects_js_1 = require("./effects.js");
20
+ const meter_js_1 = require("./meter.js");
21
+ const profile_js_1 = require("./profile.js");
22
+ const transport_js_1 = require("./transport.js");
23
+ function decimal(value) {
24
+ return Number.isInteger(value) ? String(value) : String(Number(value.toFixed(9)));
25
+ }
26
+ function quantityValue(value, unit, units) {
27
+ if (value === undefined || value === null)
28
+ return undefined;
29
+ if (typeof value === 'number' && Number.isFinite(value))
30
+ return value;
31
+ if (typeof value === 'object' && 'value' in value && 'unit' in value) {
32
+ return units.convert(value, unit).value;
33
+ }
34
+ return undefined;
35
+ }
36
+ function numberParameter(value) {
37
+ if (typeof value === 'number' && Number.isFinite(value))
38
+ return value;
39
+ throw new transport_js_1.OcppTransportError('Invalid', `expected a number, got '${String(value)}'`);
40
+ }
41
+ function stringParameter(value) {
42
+ if (typeof value === 'string' && value.length > 0)
43
+ return value;
44
+ throw new transport_js_1.OcppTransportError('Invalid', `expected a non-empty string, got '${String(value)}'`);
45
+ }
46
+ function commandOf(op) {
47
+ return op.command;
48
+ }
49
+ function findConnector(options, connectorId) {
50
+ const connector = options.connectors.find((candidate) => candidate.connectorId === connectorId);
51
+ if (!connector) {
52
+ throw new transport_js_1.OcppTransportError('Invalid', `connector ${connectorId} is not registered`);
53
+ }
54
+ return connector;
55
+ }
56
+ function responseData(response) {
57
+ return { status: response.status ?? 'Accepted', ...(response.payload ?? {}) };
58
+ }
59
+ function acceptedResult(response) {
60
+ (0, transport_js_1.assertAccepted)(response, 'OCPP operation');
61
+ return { ok: true, data: responseData(response) };
62
+ }
63
+ function transactionIdOf(record) {
64
+ const data = record.result;
65
+ return typeof data?.transactionId === 'number' ? data.transactionId : undefined;
66
+ }
67
+ function createOcppDefinitions(options) {
68
+ const units = options.units ?? (0, act_core_1.defaultUnitRegistry)();
69
+ const taxonomy = (0, profile_js_1.ocppErrorTaxonomy)();
70
+ const transport = options.transport;
71
+ function profileDigest(command) {
72
+ if (command.maxPowerWatts === undefined)
73
+ return undefined;
74
+ return (0, act_core_1.hashCanonical)('TOTEM_ACT_OCPP_CHARGING_PROFILE_V1', {
75
+ connectorId: command.connectorId,
76
+ maxPowerWatts: command.maxPowerWatts,
77
+ maxEnergyWattHours: command.maxEnergyWattHours ?? null,
78
+ maxSessionSeconds: command.maxSessionSeconds ?? null,
79
+ });
80
+ }
81
+ function resourceFor(connectorId) {
82
+ return (0, act_core_1.resourceIdKey)((0, profile_js_1.chargingResourceId)(options.site, connectorId));
83
+ }
84
+ const definitions = [
85
+ {
86
+ kind: 'charging:startSession',
87
+ description: 'Start a bounded charging session on a connector through the governed runtime.',
88
+ version: 1,
89
+ capability: 'industrial:action',
90
+ effect: 'write',
91
+ protocol: profile_js_1.OCPP_PROTOCOL,
92
+ errorTaxonomy: taxonomy,
93
+ schema: {
94
+ parameters: [
95
+ { name: 'connectorId', type: 'number', required: true, description: 'Target connector id.' },
96
+ { name: 'idTag', type: 'string', required: true, description: 'Contract/authorization tag.' },
97
+ {
98
+ name: 'maxPowerWatts', type: 'quantity', required: true,
99
+ dimension: 'power', unit: 'W', description: 'Instantaneous power ceiling.',
100
+ },
101
+ {
102
+ name: 'maxEnergyWattHours', type: 'quantity', required: false,
103
+ dimension: 'energy', unit: 'Wh', description: 'Session energy ceiling.',
104
+ },
105
+ {
106
+ name: 'maxSessionSeconds', type: 'quantity', required: false,
107
+ dimension: 'time', unit: 's', description: 'Session duration ceiling.',
108
+ },
109
+ ],
110
+ context: [],
111
+ },
112
+ policy: { failureMode: 'fail-safe', timeoutMs: 30000 },
113
+ safeState: async (op) => {
114
+ const command = commandOf(op);
115
+ if (!transport.stopConnector) {
116
+ return { ok: false, error: 'transport cannot stop an active connector session' };
117
+ }
118
+ return acceptedResult(await transport.stopConnector(command.connectorId));
119
+ },
120
+ async prepare(params) {
121
+ const connectorId = numberParameter(params.connectorId);
122
+ findConnector(options, connectorId);
123
+ const maxPowerWatts = quantityValue(params.maxPowerWatts, 'W', units);
124
+ if (maxPowerWatts === undefined) {
125
+ throw new transport_js_1.OcppTransportError('Invalid', 'maxPowerWatts is required');
126
+ }
127
+ const command = {
128
+ action: 'RemoteStartTransaction',
129
+ connectorId,
130
+ idTag: stringParameter(params.idTag),
131
+ maxPowerWatts: decimal(maxPowerWatts),
132
+ };
133
+ const maxEnergy = quantityValue(params.maxEnergyWattHours, 'Wh', units);
134
+ if (maxEnergy !== undefined)
135
+ command.maxEnergyWattHours = decimal(maxEnergy);
136
+ const maxSeconds = quantityValue(params.maxSessionSeconds, 's', units);
137
+ if (maxSeconds !== undefined)
138
+ command.maxSessionSeconds = decimal(maxSeconds);
139
+ return { resourceId: resourceFor(connectorId), command };
140
+ },
141
+ deriveEffects(op) {
142
+ const command = commandOf(op);
143
+ return (0, effects_js_1.deriveChargingEffects)({
144
+ resource: resourceFor(command.connectorId),
145
+ maxPowerWatts: String(command.maxPowerWatts),
146
+ ...(command.maxEnergyWattHours !== undefined
147
+ ? { maxEnergyWattHours: String(command.maxEnergyWattHours) }
148
+ : {}),
149
+ ...(command.maxSessionSeconds !== undefined
150
+ ? { maxSessionSeconds: String(command.maxSessionSeconds) }
151
+ : {}),
152
+ profileId: `connector:${command.connectorId}`,
153
+ profileDigest: profileDigest(command) ?? 'none',
154
+ });
155
+ },
156
+ async actuate(op) {
157
+ const command = commandOf(op);
158
+ const response = await transport.remoteStartTransaction({
159
+ connectorId: command.connectorId,
160
+ idTag: String(command.idTag),
161
+ ...(command.maxPowerWatts !== undefined ? { maxPowerWatts: String(command.maxPowerWatts) } : {}),
162
+ ...(command.maxEnergyWattHours !== undefined
163
+ ? { maxEnergyWattHours: String(command.maxEnergyWattHours) }
164
+ : {}),
165
+ ...(command.maxSessionSeconds !== undefined
166
+ ? { maxSessionSeconds: String(command.maxSessionSeconds) }
167
+ : {}),
168
+ });
169
+ return acceptedResult(response);
170
+ },
171
+ async: {
172
+ confirmation: 'accepted',
173
+ async reconcile(op, record) {
174
+ const command = commandOf(op);
175
+ const transactionId = transactionIdOf(record);
176
+ if (transactionId === undefined) {
177
+ return { status: 'unknown', error: 'no transaction id available for reconciliation' };
178
+ }
179
+ const reading = await (0, meter_js_1.reconcileChargingSession)(transport, {
180
+ operationId: record.operationId,
181
+ connectorId: command.connectorId,
182
+ transactionId,
183
+ });
184
+ if (reading.status === 'confirmed') {
185
+ return {
186
+ status: 'confirmed',
187
+ handle: { transactionId },
188
+ progress: { fraction: 1, detail: 'transaction ended', at: Date.now() },
189
+ result: reading.evidence,
190
+ };
191
+ }
192
+ if (reading.status === 'running') {
193
+ return {
194
+ status: 'running',
195
+ handle: { transactionId },
196
+ progress: { detail: reading.detail ?? 'charging', at: Date.now() },
197
+ };
198
+ }
199
+ return { status: 'unknown', error: reading.detail };
200
+ },
201
+ async cancel(op, record) {
202
+ const command = commandOf(op);
203
+ const transactionId = transactionIdOf(record);
204
+ if (transactionId !== undefined) {
205
+ await stopSession(command.connectorId, transactionId);
206
+ return;
207
+ }
208
+ if (!transport.stopConnector) {
209
+ throw new transport_js_1.OcppTransportError('Invalid', 'cannot cancel without a transaction id');
210
+ }
211
+ await transport.stopConnector(command.connectorId);
212
+ },
213
+ },
214
+ },
215
+ {
216
+ kind: 'charging:stopSession',
217
+ description: 'Stop a charging session; completion is confirmed by meter readback.',
218
+ version: 1,
219
+ capability: 'industrial:action',
220
+ effect: 'write',
221
+ protocol: profile_js_1.OCPP_PROTOCOL,
222
+ errorTaxonomy: taxonomy,
223
+ schema: {
224
+ parameters: [
225
+ { name: 'connectorId', type: 'number', required: true },
226
+ { name: 'transactionId', type: 'number', required: true },
227
+ ],
228
+ context: [],
229
+ },
230
+ policy: { failureMode: 'fail-safe', timeoutMs: 30000 },
231
+ safeState: async (op) => {
232
+ const command = commandOf(op);
233
+ return stopSession(command.connectorId, numberParameter(command.transactionId));
234
+ },
235
+ async prepare(params) {
236
+ const connectorId = numberParameter(params.connectorId);
237
+ findConnector(options, connectorId);
238
+ const command = {
239
+ action: 'RemoteStopTransaction',
240
+ connectorId,
241
+ transactionId: numberParameter(params.transactionId),
242
+ };
243
+ return { resourceId: resourceFor(connectorId), command };
244
+ },
245
+ deriveEffects(op) {
246
+ const command = commandOf(op);
247
+ return { spends: [], physical: { duration: [{ resource: resourceFor(command.connectorId), seconds: '0' }] } };
248
+ },
249
+ async actuate(op) {
250
+ const command = commandOf(op);
251
+ const response = await transport.remoteStopTransaction({
252
+ connectorId: command.connectorId,
253
+ transactionId: numberParameter(command.transactionId),
254
+ });
255
+ return acceptedResult(response);
256
+ },
257
+ async: {
258
+ confirmation: 'accepted',
259
+ async reconcile(op, record) {
260
+ const command = commandOf(op);
261
+ const transactionId = numberParameter(command.transactionId);
262
+ const reading = await (0, meter_js_1.reconcileChargingSession)(transport, {
263
+ operationId: record.operationId,
264
+ connectorId: command.connectorId,
265
+ transactionId,
266
+ });
267
+ if (reading.status === 'confirmed') {
268
+ return {
269
+ status: 'confirmed',
270
+ handle: { transactionId },
271
+ progress: { fraction: 1, detail: 'transaction ended', at: Date.now() },
272
+ result: reading.evidence,
273
+ };
274
+ }
275
+ return {
276
+ status: reading.status === 'running' ? 'running' : 'unknown',
277
+ handle: { transactionId },
278
+ ...(reading.detail !== undefined ? { error: reading.detail } : {}),
279
+ };
280
+ },
281
+ async cancel(op) {
282
+ const command = commandOf(op);
283
+ await stopSession(command.connectorId, numberParameter(command.transactionId));
284
+ },
285
+ },
286
+ },
287
+ {
288
+ kind: 'charging:setLimit',
289
+ description: 'Apply a bounded charging profile to a connector.',
290
+ version: 1,
291
+ capability: 'industrial:action',
292
+ effect: 'write',
293
+ protocol: profile_js_1.OCPP_PROTOCOL,
294
+ errorTaxonomy: taxonomy,
295
+ schema: {
296
+ parameters: [
297
+ { name: 'connectorId', type: 'number', required: true },
298
+ { name: 'profileId', type: 'number', required: true },
299
+ {
300
+ name: 'maxPowerWatts', type: 'quantity', required: true,
301
+ dimension: 'power', unit: 'W',
302
+ },
303
+ {
304
+ name: 'maxEnergyWattHours', type: 'quantity', required: false,
305
+ dimension: 'energy', unit: 'Wh',
306
+ },
307
+ {
308
+ name: 'maxSessionSeconds', type: 'quantity', required: false,
309
+ dimension: 'time', unit: 's',
310
+ },
311
+ ],
312
+ context: [],
313
+ },
314
+ policy: { failureMode: 'fail-safe', timeoutMs: 30000 },
315
+ safeState: async (op) => {
316
+ const command = commandOf(op);
317
+ return clearProfile(command.connectorId, Number(command.profileId));
318
+ },
319
+ async prepare(params) {
320
+ const connectorId = numberParameter(params.connectorId);
321
+ findConnector(options, connectorId);
322
+ const maxPowerWatts = quantityValue(params.maxPowerWatts, 'W', units);
323
+ if (maxPowerWatts === undefined) {
324
+ throw new transport_js_1.OcppTransportError('Invalid', 'maxPowerWatts is required');
325
+ }
326
+ const command = {
327
+ action: 'SetChargingProfile',
328
+ connectorId,
329
+ profileId: numberParameter(params.profileId),
330
+ maxPowerWatts: decimal(maxPowerWatts),
331
+ };
332
+ const maxEnergy = quantityValue(params.maxEnergyWattHours, 'Wh', units);
333
+ if (maxEnergy !== undefined)
334
+ command.maxEnergyWattHours = decimal(maxEnergy);
335
+ const maxSeconds = quantityValue(params.maxSessionSeconds, 's', units);
336
+ if (maxSeconds !== undefined)
337
+ command.maxSessionSeconds = decimal(maxSeconds);
338
+ return { resourceId: resourceFor(connectorId), command };
339
+ },
340
+ deriveEffects(op) {
341
+ const command = commandOf(op);
342
+ return (0, effects_js_1.deriveChargingEffects)({
343
+ resource: resourceFor(command.connectorId),
344
+ maxPowerWatts: String(command.maxPowerWatts),
345
+ ...(command.maxEnergyWattHours !== undefined
346
+ ? { maxEnergyWattHours: String(command.maxEnergyWattHours) }
347
+ : {}),
348
+ ...(command.maxSessionSeconds !== undefined
349
+ ? { maxSessionSeconds: String(command.maxSessionSeconds) }
350
+ : {}),
351
+ profileId: String(command.profileId),
352
+ profileDigest: profileDigest(command) ?? 'none',
353
+ });
354
+ },
355
+ async actuate(op) {
356
+ const command = commandOf(op);
357
+ const response = await transport.setChargingProfile({
358
+ connectorId: command.connectorId,
359
+ profileId: numberParameter(command.profileId),
360
+ maxPowerWatts: String(command.maxPowerWatts),
361
+ ...(command.maxEnergyWattHours !== undefined
362
+ ? { maxEnergyWattHours: String(command.maxEnergyWattHours) }
363
+ : {}),
364
+ ...(command.maxSessionSeconds !== undefined
365
+ ? { maxSessionSeconds: String(command.maxSessionSeconds) }
366
+ : {}),
367
+ });
368
+ return acceptedResult(response);
369
+ },
370
+ async: { confirmation: 'completed' },
371
+ },
372
+ {
373
+ kind: 'charging:reserveConnector',
374
+ description: 'Reserve a connector for a bounded window before a session starts.',
375
+ version: 1,
376
+ capability: 'industrial:action',
377
+ effect: 'write',
378
+ protocol: profile_js_1.OCPP_PROTOCOL,
379
+ errorTaxonomy: taxonomy,
380
+ schema: {
381
+ parameters: [
382
+ { name: 'connectorId', type: 'number', required: true },
383
+ { name: 'idTag', type: 'string', required: true },
384
+ { name: 'reservationId', type: 'number', required: true },
385
+ {
386
+ name: 'expiresInSeconds', type: 'quantity', required: true,
387
+ dimension: 'time', unit: 's',
388
+ },
389
+ ],
390
+ context: [],
391
+ },
392
+ // A reservation that cannot be proven is refused; the window is bounded.
393
+ policy: { failureMode: 'fail-closed', timeoutMs: 30000 },
394
+ async prepare(params) {
395
+ const connectorId = numberParameter(params.connectorId);
396
+ findConnector(options, connectorId);
397
+ const expiresInSeconds = quantityValue(params.expiresInSeconds, 's', units);
398
+ if (expiresInSeconds === undefined) {
399
+ throw new transport_js_1.OcppTransportError('Invalid', 'expiresInSeconds is required');
400
+ }
401
+ const command = {
402
+ action: 'ReserveNow',
403
+ connectorId,
404
+ idTag: stringParameter(params.idTag),
405
+ reservationId: numberParameter(params.reservationId),
406
+ expiresInSeconds: decimal(expiresInSeconds),
407
+ };
408
+ return { resourceId: resourceFor(connectorId), command };
409
+ },
410
+ deriveEffects(op) {
411
+ const command = commandOf(op);
412
+ return {
413
+ spends: [],
414
+ physical: {
415
+ duration: [{ resource: resourceFor(command.connectorId), seconds: String(command.expiresInSeconds) }],
416
+ },
417
+ };
418
+ },
419
+ async actuate(op) {
420
+ const command = commandOf(op);
421
+ const response = await transport.reserveNow({
422
+ connectorId: command.connectorId,
423
+ idTag: String(command.idTag),
424
+ reservationId: numberParameter(command.reservationId),
425
+ expiresInSeconds: Number(command.expiresInSeconds),
426
+ });
427
+ return acceptedResult(response);
428
+ },
429
+ async: { confirmation: 'completed' },
430
+ },
431
+ {
432
+ kind: 'charging:clearProfile',
433
+ description: 'Clear a charging profile from a connector.',
434
+ version: 1,
435
+ capability: 'industrial:action',
436
+ effect: 'write',
437
+ protocol: profile_js_1.OCPP_PROTOCOL,
438
+ errorTaxonomy: taxonomy,
439
+ schema: {
440
+ parameters: [
441
+ { name: 'connectorId', type: 'number', required: true },
442
+ { name: 'profileId', type: 'number', required: false },
443
+ ],
444
+ context: [],
445
+ },
446
+ policy: { failureMode: 'fail-safe', timeoutMs: 30000 },
447
+ safeState: async (op) => {
448
+ const command = commandOf(op);
449
+ return clearProfile(command.connectorId, Number(command.profileId ?? 0));
450
+ },
451
+ async prepare(params) {
452
+ const connectorId = numberParameter(params.connectorId);
453
+ findConnector(options, connectorId);
454
+ const command = { action: 'ClearChargingProfile', connectorId };
455
+ if (params.profileId !== undefined)
456
+ command.profileId = numberParameter(params.profileId);
457
+ return { resourceId: resourceFor(connectorId), command };
458
+ },
459
+ deriveEffects(op) {
460
+ const command = commandOf(op);
461
+ return { spends: [], physical: { power: [{ resource: resourceFor(command.connectorId), watts: '0' }] } };
462
+ },
463
+ async actuate(op) {
464
+ const command = commandOf(op);
465
+ const response = await transport.clearChargingProfile({
466
+ connectorId: command.connectorId,
467
+ ...(command.profileId !== undefined
468
+ ? { profileId: numberParameter(command.profileId) }
469
+ : {}),
470
+ });
471
+ return acceptedResult(response);
472
+ },
473
+ async: { confirmation: 'completed' },
474
+ },
475
+ {
476
+ kind: 'charging:readMeter',
477
+ description: 'Read the charger meter values for a connector or transaction.',
478
+ version: 1,
479
+ capability: 'industrial:read',
480
+ effect: 'read',
481
+ protocol: profile_js_1.OCPP_PROTOCOL,
482
+ errorTaxonomy: taxonomy,
483
+ schema: {
484
+ parameters: [
485
+ { name: 'connectorId', type: 'number', required: true },
486
+ { name: 'transactionId', type: 'number', required: false },
487
+ ],
488
+ context: [],
489
+ },
490
+ async prepare(params) {
491
+ const connectorId = numberParameter(params.connectorId);
492
+ findConnector(options, connectorId);
493
+ const command = { action: 'MeterValues', connectorId };
494
+ if (params.transactionId !== undefined) {
495
+ command.transactionId = numberParameter(params.transactionId);
496
+ }
497
+ return { resourceId: resourceFor(connectorId), command };
498
+ },
499
+ deriveEffects() {
500
+ return { spends: [] };
501
+ },
502
+ async actuate(op) {
503
+ const command = commandOf(op);
504
+ const readings = await transport.readMeterValues({
505
+ connectorId: command.connectorId,
506
+ ...(command.transactionId !== undefined
507
+ ? { transactionId: numberParameter(command.transactionId) }
508
+ : {}),
509
+ });
510
+ return { ok: true, data: { readings } };
511
+ },
512
+ },
513
+ ];
514
+ // Capability-gated definitions are not selectable when the negotiated set
515
+ // does not include them (RFC-024 §5.8); registration fails closed.
516
+ for (const definition of definitions) {
517
+ const capability = options.capabilities;
518
+ if (capability && !capability.profiles.includes(definition.kind)) {
519
+ throw new Error(`OCPP ${capability.version} does not provide '${definition.kind}'; refusing to register a capability-gated definition`);
520
+ }
521
+ }
522
+ async function stopSession(connectorId, transactionId) {
523
+ const response = await transport.remoteStopTransaction({ connectorId, transactionId });
524
+ return acceptedResult(response);
525
+ }
526
+ async function clearProfile(connectorId, profileId) {
527
+ const response = await transport.clearChargingProfile({
528
+ connectorId,
529
+ ...(profileId > 0 ? { profileId } : {}),
530
+ });
531
+ return acceptedResult(response);
532
+ }
533
+ return definitions;
534
+ }
535
+ function errorCodeOf(error) {
536
+ return error instanceof transport_js_1.OcppTransportError ? error.code : undefined;
537
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * RFC-024 §5.8 — explicit, versioned OCPP capability negotiation.
3
+ *
4
+ * A capability change is a new profile version, never a silent upgrade. A
5
+ * definition that needs a feature which was not negotiated is not selectable.
6
+ */
7
+ export type OcppVersion = '1.6J' | '2.0.1' | '2.1';
8
+ /** Highest first; negotiation picks the newest mutually supported version. */
9
+ export declare const OCPP_VERSION_PREFERENCE: readonly OcppVersion[];
10
+ export interface OcppCapabilitySet {
11
+ readonly version: OcppVersion;
12
+ /** Negotiated profile/feature names. */
13
+ readonly profiles: readonly string[];
14
+ }
15
+ /** Definition kinds provided by this package. */
16
+ export declare const OCPP_CORE_PROFILES: readonly ["charging:startSession", "charging:stopSession", "charging:setLimit", "charging:reserveConnector", "charging:clearProfile", "charging:readMeter"];
17
+ export type OcppProfileName = (typeof OCPP_CORE_PROFILES)[number];
18
+ export interface NegotiateOptions {
19
+ /** Versions the deployment can offer (CSMS/charger). */
20
+ offered: readonly OcppVersion[];
21
+ /** Versions this adapter supports. Defaults to 1.6J and 2.0.1. */
22
+ supported?: readonly OcppVersion[];
23
+ /** Profiles the deployment requires; absent features fail closed. */
24
+ requiredProfiles?: readonly string[];
25
+ }
26
+ /**
27
+ * Select the newest mutually supported version and assert the required profiles
28
+ * are available. Throws `ActionDefinitionError` (registration-time failure)
29
+ * rather than silently degrading.
30
+ */
31
+ export declare function negotiateOcppCapabilities(options: NegotiateOptions): OcppCapabilitySet;
32
+ export declare function supportsOcppProfile(capabilities: OcppCapabilitySet, profile: string): boolean;
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ /**
3
+ * RFC-024 §5.8 — explicit, versioned OCPP capability negotiation.
4
+ *
5
+ * A capability change is a new profile version, never a silent upgrade. A
6
+ * definition that needs a feature which was not negotiated is not selectable.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.OCPP_CORE_PROFILES = exports.OCPP_VERSION_PREFERENCE = void 0;
10
+ exports.negotiateOcppCapabilities = negotiateOcppCapabilities;
11
+ exports.supportsOcppProfile = supportsOcppProfile;
12
+ const act_core_1 = require("@totemsdk/act-core");
13
+ /** Highest first; negotiation picks the newest mutually supported version. */
14
+ exports.OCPP_VERSION_PREFERENCE = ['2.1', '2.0.1', '1.6J'];
15
+ /** Definition kinds provided by this package. */
16
+ exports.OCPP_CORE_PROFILES = [
17
+ 'charging:startSession',
18
+ 'charging:stopSession',
19
+ 'charging:setLimit',
20
+ 'charging:reserveConnector',
21
+ 'charging:clearProfile',
22
+ 'charging:readMeter',
23
+ ];
24
+ /** Version-gated features. */
25
+ const VERSION_FEATURES = {
26
+ // 1.6J and 2.0.1 expose the same governed operations under different action
27
+ // names; the adapter binds them through the transport port.
28
+ '1.6J': [...exports.OCPP_CORE_PROFILES],
29
+ '2.0.1': [...exports.OCPP_CORE_PROFILES],
30
+ '2.1': [...exports.OCPP_CORE_PROFILES, 'charging:bidirectional'],
31
+ };
32
+ /**
33
+ * Select the newest mutually supported version and assert the required profiles
34
+ * are available. Throws `ActionDefinitionError` (registration-time failure)
35
+ * rather than silently degrading.
36
+ */
37
+ function negotiateOcppCapabilities(options) {
38
+ const supported = options.supported ?? ['2.0.1', '1.6J'];
39
+ const version = exports.OCPP_VERSION_PREFERENCE.find((candidate) => options.offered.includes(candidate) && supported.includes(candidate));
40
+ if (!version) {
41
+ throw new act_core_1.ActionDefinitionError(`no mutually supported OCPP version (offered: ${options.offered.join(', ') || 'none'}; supported: ${supported.join(', ')})`);
42
+ }
43
+ const profiles = VERSION_FEATURES[version];
44
+ for (const required of options.requiredProfiles ?? []) {
45
+ if (!profiles.includes(required)) {
46
+ throw new act_core_1.ActionDefinitionError(`OCPP ${version} does not provide required profile '${required}'`);
47
+ }
48
+ }
49
+ return { version, profiles };
50
+ }
51
+ function supportsOcppProfile(capabilities, profile) {
52
+ return capabilities.profiles.includes(profile);
53
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * RFC-024 §5.5 — physical effects derived from the PREPARED charging command.
3
+ *
4
+ * These are the facts the authority layer commits to and RFC-023 enforces. They
5
+ * are computed from the prepared command only, never from caller-supplied
6
+ * hints, so a tampered or replayed command cannot widen the envelope.
7
+ */
8
+ import type { StepEffects } from '@totemsdk/agent-policy';
9
+ export interface ChargingLimits {
10
+ /** Target connector (ResourceId key). */
11
+ resource: string;
12
+ /** Instantaneous power ceiling implied by the command, in W. */
13
+ maxPowerWatts: string;
14
+ /** Total energy ceiling for the session/limit, in Wh. */
15
+ maxEnergyWattHours?: string;
16
+ /** Maximum wall-clock duration, in seconds. */
17
+ maxSessionSeconds?: string;
18
+ /** Charging profile id and schedule digest, when a profile is applied. */
19
+ profileId?: string;
20
+ profileDigest?: string;
21
+ }
22
+ export declare function deriveChargingEffects(limits: ChargingLimits): StepEffects;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ /**
3
+ * RFC-024 §5.5 — physical effects derived from the PREPARED charging command.
4
+ *
5
+ * These are the facts the authority layer commits to and RFC-023 enforces. They
6
+ * are computed from the prepared command only, never from caller-supplied
7
+ * hints, so a tampered or replayed command cannot widen the envelope.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.deriveChargingEffects = deriveChargingEffects;
11
+ function deriveChargingEffects(limits) {
12
+ const physical = {
13
+ power: [{ resource: limits.resource, watts: limits.maxPowerWatts }],
14
+ };
15
+ if (limits.maxEnergyWattHours !== undefined) {
16
+ physical.energy = [{ resource: limits.resource, wattHours: limits.maxEnergyWattHours }];
17
+ }
18
+ if (limits.maxSessionSeconds !== undefined) {
19
+ physical.duration = [{ resource: limits.resource, seconds: limits.maxSessionSeconds }];
20
+ }
21
+ if (limits.profileId !== undefined && limits.profileDigest !== undefined) {
22
+ physical.envelopes = [
23
+ { envelopeId: limits.profileId, envelopeDigest: limits.profileDigest },
24
+ ];
25
+ }
26
+ return { spends: [], physical };
27
+ }