@totemsdk/act-ocpp 0.0.0-stage → 0.1.1
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.
- package/LICENSE +202 -0
- package/NOTICE +60 -0
- package/README.md +116 -2
- package/dist/adapter.d.ts +32 -0
- package/dist/adapter.js +533 -0
- package/dist/capabilities.d.ts +32 -0
- package/dist/capabilities.js +48 -0
- package/dist/cjs/adapter.d.ts +32 -0
- package/dist/cjs/adapter.js +537 -0
- package/dist/cjs/capabilities.d.ts +32 -0
- package/dist/cjs/capabilities.js +53 -0
- package/dist/cjs/effects.d.ts +22 -0
- package/dist/cjs/effects.js +27 -0
- package/dist/cjs/index.d.ts +19 -0
- package/dist/cjs/index.js +34 -0
- package/dist/cjs/meter.d.ts +39 -0
- package/dist/cjs/meter.js +81 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/profile.d.ts +40 -0
- package/dist/cjs/profile.js +175 -0
- package/dist/cjs/transport.d.ts +104 -0
- package/dist/cjs/transport.js +33 -0
- package/dist/effects.d.ts +22 -0
- package/dist/effects.js +24 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +13 -0
- package/dist/meter.d.ts +39 -0
- package/dist/meter.js +77 -0
- package/dist/profile.d.ts +40 -0
- package/dist/profile.js +167 -0
- package/dist/transport.d.ts +104 -0
- package/dist/transport.js +28 -0
- package/package.json +71 -3
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC-024 — governed OCPP / EV charging profile.
|
|
3
|
+
*
|
|
4
|
+
* `@totemsdk/act-ocpp` authorizes and bounds a charging operation; the connector
|
|
5
|
+
* (`@totemsdk/edge-ocpp`) carries the wire frames and the charger/CSMS owns
|
|
6
|
+
* electrical protection and the control loop.
|
|
7
|
+
*/
|
|
8
|
+
export { OCPP_ACCEPTED, OcppTransportError, assertAccepted, } from './transport.js';
|
|
9
|
+
export type { OcppChargingProfileRequest, OcppClearProfileRequest, OcppMeterQuery, OcppMeterValue, OcppReserveRequest, OcppResponse, OcppStartRequest, OcppStopRequest, OcppTransactionEvent, OcppTransactionEventType, OcppTransactionState, OcppTransportPort, } from './transport.js';
|
|
10
|
+
export { OCPP_CORE_PROFILES, OCPP_VERSION_PREFERENCE, negotiateOcppCapabilities, supportsOcppProfile, } from './capabilities.js';
|
|
11
|
+
export type { NegotiateOptions, OcppCapabilitySet, OcppProfileName, OcppVersion } from './capabilities.js';
|
|
12
|
+
export { deriveChargingEffects } from './effects.js';
|
|
13
|
+
export type { ChargingLimits } from './effects.js';
|
|
14
|
+
export { meterEvidenceDigest, reconcileChargingSession } from './meter.js';
|
|
15
|
+
export type { ChargingMeterEvidence, ChargingMeterQuery, ChargingReconcileResult, ChargingReconcileStatus, } from './meter.js';
|
|
16
|
+
export { OCPP_PROTOCOL, chargingInterlocks, chargingResource, chargingResourceId, createOcppProfile, ocppErrorTaxonomy, } from './profile.js';
|
|
17
|
+
export type { ChargingConnector, OcppProfileOptions } from './profile.js';
|
|
18
|
+
export { createOcppDefinitions, errorCodeOf } from './adapter.js';
|
|
19
|
+
export type { ChargingCommand, OcppAdapterOptions } from './adapter.js';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* RFC-024 — governed OCPP / EV charging profile.
|
|
4
|
+
*
|
|
5
|
+
* `@totemsdk/act-ocpp` authorizes and bounds a charging operation; the connector
|
|
6
|
+
* (`@totemsdk/edge-ocpp`) carries the wire frames and the charger/CSMS owns
|
|
7
|
+
* electrical protection and the control loop.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.errorCodeOf = exports.createOcppDefinitions = exports.ocppErrorTaxonomy = exports.createOcppProfile = exports.chargingResourceId = exports.chargingResource = exports.chargingInterlocks = exports.OCPP_PROTOCOL = exports.reconcileChargingSession = exports.meterEvidenceDigest = exports.deriveChargingEffects = exports.supportsOcppProfile = exports.negotiateOcppCapabilities = exports.OCPP_VERSION_PREFERENCE = exports.OCPP_CORE_PROFILES = exports.assertAccepted = exports.OcppTransportError = exports.OCPP_ACCEPTED = void 0;
|
|
11
|
+
var transport_js_1 = require("./transport.js");
|
|
12
|
+
Object.defineProperty(exports, "OCPP_ACCEPTED", { enumerable: true, get: function () { return transport_js_1.OCPP_ACCEPTED; } });
|
|
13
|
+
Object.defineProperty(exports, "OcppTransportError", { enumerable: true, get: function () { return transport_js_1.OcppTransportError; } });
|
|
14
|
+
Object.defineProperty(exports, "assertAccepted", { enumerable: true, get: function () { return transport_js_1.assertAccepted; } });
|
|
15
|
+
var capabilities_js_1 = require("./capabilities.js");
|
|
16
|
+
Object.defineProperty(exports, "OCPP_CORE_PROFILES", { enumerable: true, get: function () { return capabilities_js_1.OCPP_CORE_PROFILES; } });
|
|
17
|
+
Object.defineProperty(exports, "OCPP_VERSION_PREFERENCE", { enumerable: true, get: function () { return capabilities_js_1.OCPP_VERSION_PREFERENCE; } });
|
|
18
|
+
Object.defineProperty(exports, "negotiateOcppCapabilities", { enumerable: true, get: function () { return capabilities_js_1.negotiateOcppCapabilities; } });
|
|
19
|
+
Object.defineProperty(exports, "supportsOcppProfile", { enumerable: true, get: function () { return capabilities_js_1.supportsOcppProfile; } });
|
|
20
|
+
var effects_js_1 = require("./effects.js");
|
|
21
|
+
Object.defineProperty(exports, "deriveChargingEffects", { enumerable: true, get: function () { return effects_js_1.deriveChargingEffects; } });
|
|
22
|
+
var meter_js_1 = require("./meter.js");
|
|
23
|
+
Object.defineProperty(exports, "meterEvidenceDigest", { enumerable: true, get: function () { return meter_js_1.meterEvidenceDigest; } });
|
|
24
|
+
Object.defineProperty(exports, "reconcileChargingSession", { enumerable: true, get: function () { return meter_js_1.reconcileChargingSession; } });
|
|
25
|
+
var profile_js_1 = require("./profile.js");
|
|
26
|
+
Object.defineProperty(exports, "OCPP_PROTOCOL", { enumerable: true, get: function () { return profile_js_1.OCPP_PROTOCOL; } });
|
|
27
|
+
Object.defineProperty(exports, "chargingInterlocks", { enumerable: true, get: function () { return profile_js_1.chargingInterlocks; } });
|
|
28
|
+
Object.defineProperty(exports, "chargingResource", { enumerable: true, get: function () { return profile_js_1.chargingResource; } });
|
|
29
|
+
Object.defineProperty(exports, "chargingResourceId", { enumerable: true, get: function () { return profile_js_1.chargingResourceId; } });
|
|
30
|
+
Object.defineProperty(exports, "createOcppProfile", { enumerable: true, get: function () { return profile_js_1.createOcppProfile; } });
|
|
31
|
+
Object.defineProperty(exports, "ocppErrorTaxonomy", { enumerable: true, get: function () { return profile_js_1.ocppErrorTaxonomy; } });
|
|
32
|
+
var adapter_js_1 = require("./adapter.js");
|
|
33
|
+
Object.defineProperty(exports, "createOcppDefinitions", { enumerable: true, get: function () { return adapter_js_1.createOcppDefinitions; } });
|
|
34
|
+
Object.defineProperty(exports, "errorCodeOf", { enumerable: true, get: function () { return adapter_js_1.errorCodeOf; } });
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC-024 §5.7 — metering and settlement evidence.
|
|
3
|
+
*
|
|
4
|
+
* Meter values must come from the charger through the transport, never from the
|
|
5
|
+
* caller. The evidence digest is canonical so a settlement action can cite the
|
|
6
|
+
* charging operation id and prove which meter readings backed it.
|
|
7
|
+
*/
|
|
8
|
+
import type { OcppMeterValue, OcppTransportPort } from './transport.js';
|
|
9
|
+
export interface ChargingMeterEvidence {
|
|
10
|
+
operationId: string;
|
|
11
|
+
connectorId: number;
|
|
12
|
+
transactionId: number;
|
|
13
|
+
/** Latest cumulative energy register, in Wh. */
|
|
14
|
+
energyWattHours: string;
|
|
15
|
+
readings: readonly OcppMeterValue[];
|
|
16
|
+
digest: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ChargingMeterQuery {
|
|
19
|
+
operationId: string;
|
|
20
|
+
connectorId: number;
|
|
21
|
+
transactionId: number;
|
|
22
|
+
}
|
|
23
|
+
export type ChargingReconcileStatus = 'running' | 'confirmed' | 'unknown';
|
|
24
|
+
export interface ChargingReconcileResult {
|
|
25
|
+
status: ChargingReconcileStatus;
|
|
26
|
+
energyWattHours?: string;
|
|
27
|
+
evidence?: ChargingMeterEvidence;
|
|
28
|
+
detail?: string;
|
|
29
|
+
}
|
|
30
|
+
export declare function meterEvidenceDigest(evidence: Omit<ChargingMeterEvidence, 'digest'>): string;
|
|
31
|
+
/**
|
|
32
|
+
* Reconcile an accepted charging operation against the charger.
|
|
33
|
+
*
|
|
34
|
+
* - `confirmed` only when the charger reports the transaction ended (an energy
|
|
35
|
+
* register alone is progress, not completion);
|
|
36
|
+
* - `running` while the transaction is active or meter values advance;
|
|
37
|
+
* - `unknown` when the charger cannot be read — the reservation must stay held.
|
|
38
|
+
*/
|
|
39
|
+
export declare function reconcileChargingSession(transport: OcppTransportPort, query: ChargingMeterQuery): Promise<ChargingReconcileResult>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* RFC-024 §5.7 — metering and settlement evidence.
|
|
4
|
+
*
|
|
5
|
+
* Meter values must come from the charger through the transport, never from the
|
|
6
|
+
* caller. The evidence digest is canonical so a settlement action can cite the
|
|
7
|
+
* charging operation id and prove which meter readings backed it.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.meterEvidenceDigest = meterEvidenceDigest;
|
|
11
|
+
exports.reconcileChargingSession = reconcileChargingSession;
|
|
12
|
+
const act_core_1 = require("@totemsdk/act-core");
|
|
13
|
+
function meterEvidenceDigest(evidence) {
|
|
14
|
+
return (0, act_core_1.hashCanonical)('TOTEM_ACT_OCPP_METER_EVIDENCE_V1', {
|
|
15
|
+
operationId: evidence.operationId,
|
|
16
|
+
connectorId: evidence.connectorId,
|
|
17
|
+
transactionId: evidence.transactionId,
|
|
18
|
+
energyWattHours: evidence.energyWattHours,
|
|
19
|
+
readings: evidence.readings
|
|
20
|
+
.map((reading) => ({
|
|
21
|
+
transactionId: reading.transactionId,
|
|
22
|
+
connectorId: reading.connectorId,
|
|
23
|
+
timestamp: reading.timestamp,
|
|
24
|
+
energyWattHours: reading.energyWattHours,
|
|
25
|
+
measurand: reading.measurand ?? null,
|
|
26
|
+
}))
|
|
27
|
+
.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp)),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function latestReading(readings) {
|
|
31
|
+
return readings
|
|
32
|
+
.slice()
|
|
33
|
+
.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp))
|
|
34
|
+
.at(-1);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Reconcile an accepted charging operation against the charger.
|
|
38
|
+
*
|
|
39
|
+
* - `confirmed` only when the charger reports the transaction ended (an energy
|
|
40
|
+
* register alone is progress, not completion);
|
|
41
|
+
* - `running` while the transaction is active or meter values advance;
|
|
42
|
+
* - `unknown` when the charger cannot be read — the reservation must stay held.
|
|
43
|
+
*/
|
|
44
|
+
async function reconcileChargingSession(transport, query) {
|
|
45
|
+
const readings = await transport.readMeterValues({
|
|
46
|
+
connectorId: query.connectorId,
|
|
47
|
+
transactionId: query.transactionId,
|
|
48
|
+
});
|
|
49
|
+
const scoped = readings.filter((reading) => reading.transactionId === query.transactionId);
|
|
50
|
+
const latest = latestReading(scoped);
|
|
51
|
+
let state = { status: 'unknown' };
|
|
52
|
+
if (transport.readTransactionState) {
|
|
53
|
+
state = await transport.readTransactionState({
|
|
54
|
+
connectorId: query.connectorId,
|
|
55
|
+
transactionId: query.transactionId,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const energyWattHours = state.energyWattHours ?? latest?.energyWattHours;
|
|
59
|
+
if (state.status === 'ended' && energyWattHours !== undefined) {
|
|
60
|
+
const base = {
|
|
61
|
+
operationId: query.operationId,
|
|
62
|
+
connectorId: query.connectorId,
|
|
63
|
+
transactionId: query.transactionId,
|
|
64
|
+
energyWattHours,
|
|
65
|
+
readings: scoped,
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
status: 'confirmed',
|
|
69
|
+
energyWattHours,
|
|
70
|
+
evidence: { ...base, digest: meterEvidenceDigest(base) },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (state.status === 'active' || scoped.length > 0) {
|
|
74
|
+
return {
|
|
75
|
+
status: 'running',
|
|
76
|
+
...(energyWattHours !== undefined ? { energyWattHours } : {}),
|
|
77
|
+
detail: state.status === 'active' ? 'transaction active' : 'meter values observed',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return { status: 'unknown', detail: 'no transaction state or meter readback' };
|
|
81
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC-024 §5.4 / §5.9 — OCPP charging profile.
|
|
3
|
+
*
|
|
4
|
+
* Connectors are resources; the safe state is `stop session` (never leave an
|
|
5
|
+
* active transaction). Interlocks enforce the connector's declared physical
|
|
6
|
+
* ceiling before authorization, and the error taxonomy classifies OCPP status
|
|
7
|
+
* codes so a safety error is never retried.
|
|
8
|
+
*/
|
|
9
|
+
import { type DeviceErrorTaxonomy, type IndustrialProfile, type Interlock, type Resource, type ResourceId } from '@totemsdk/act-core';
|
|
10
|
+
import type { OcppCapabilitySet } from './capabilities.js';
|
|
11
|
+
import { OCPP_CORE_PROFILES } from './capabilities.js';
|
|
12
|
+
export declare const OCPP_PROTOCOL = "ocpp";
|
|
13
|
+
export interface ChargingConnector {
|
|
14
|
+
connectorId: number;
|
|
15
|
+
/** Physical ceiling at this connector, in W (decimal string). */
|
|
16
|
+
maxPowerWatts: string;
|
|
17
|
+
/** Contract/session energy ceiling, in Wh. */
|
|
18
|
+
maxEnergyWattHours?: string;
|
|
19
|
+
/** Maximum session duration, in seconds. */
|
|
20
|
+
maxSessionSeconds?: string;
|
|
21
|
+
phases?: 1 | 3;
|
|
22
|
+
connectorType?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function chargingResourceId(site: string, connectorId: number, asset?: string): ResourceId;
|
|
25
|
+
export declare function chargingResource(site: string, connector: ChargingConnector): Resource;
|
|
26
|
+
/** Interlocks that enforce a connector's declared ceiling before authorization. */
|
|
27
|
+
export declare function chargingInterlocks(site: string, connector: ChargingConnector): Interlock[];
|
|
28
|
+
/**
|
|
29
|
+
* OCPP status → device error classification (RFC-011 §4.7).
|
|
30
|
+
* Safety codes require a safe state and are never retried.
|
|
31
|
+
*/
|
|
32
|
+
export declare function ocppErrorTaxonomy(): DeviceErrorTaxonomy;
|
|
33
|
+
export interface OcppProfileOptions {
|
|
34
|
+
site: string;
|
|
35
|
+
connectors: readonly ChargingConnector[];
|
|
36
|
+
capabilities?: OcppCapabilitySet;
|
|
37
|
+
}
|
|
38
|
+
/** Data-only charger profile (definitions are supplied by the adapter). */
|
|
39
|
+
export declare function createOcppProfile(options: OcppProfileOptions): IndustrialProfile;
|
|
40
|
+
export { OCPP_CORE_PROFILES };
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* RFC-024 §5.4 / §5.9 — OCPP charging profile.
|
|
4
|
+
*
|
|
5
|
+
* Connectors are resources; the safe state is `stop session` (never leave an
|
|
6
|
+
* active transaction). Interlocks enforce the connector's declared physical
|
|
7
|
+
* ceiling before authorization, and the error taxonomy classifies OCPP status
|
|
8
|
+
* codes so a safety error is never retried.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.OCPP_CORE_PROFILES = exports.OCPP_PROTOCOL = void 0;
|
|
12
|
+
exports.chargingResourceId = chargingResourceId;
|
|
13
|
+
exports.chargingResource = chargingResource;
|
|
14
|
+
exports.chargingInterlocks = chargingInterlocks;
|
|
15
|
+
exports.ocppErrorTaxonomy = ocppErrorTaxonomy;
|
|
16
|
+
exports.createOcppProfile = createOcppProfile;
|
|
17
|
+
const act_core_1 = require("@totemsdk/act-core");
|
|
18
|
+
const capabilities_js_1 = require("./capabilities.js");
|
|
19
|
+
Object.defineProperty(exports, "OCPP_CORE_PROFILES", { enumerable: true, get: function () { return capabilities_js_1.OCPP_CORE_PROFILES; } });
|
|
20
|
+
exports.OCPP_PROTOCOL = 'ocpp';
|
|
21
|
+
function chargingResourceId(site, connectorId, asset = `EVSE-${connectorId}`) {
|
|
22
|
+
return { site, area: 'charging', asset, point: 'connector' };
|
|
23
|
+
}
|
|
24
|
+
function chargingResource(site, connector) {
|
|
25
|
+
const id = chargingResourceId(site, connector.connectorId);
|
|
26
|
+
return {
|
|
27
|
+
id,
|
|
28
|
+
kind: 'actuator',
|
|
29
|
+
addresses: [{ protocol: exports.OCPP_PROTOCOL, endpoint: `connector:${connector.connectorId}` }],
|
|
30
|
+
// Never leave an active transaction: the safe state stops the session.
|
|
31
|
+
safeState: {
|
|
32
|
+
resourceId: id,
|
|
33
|
+
command: { action: 'charging:stopSession', connectorId: connector.connectorId },
|
|
34
|
+
},
|
|
35
|
+
metadata: {
|
|
36
|
+
connectorId: connector.connectorId,
|
|
37
|
+
maxPowerWatts: connector.maxPowerWatts,
|
|
38
|
+
...(connector.maxEnergyWattHours !== undefined
|
|
39
|
+
? { maxEnergyWattHours: connector.maxEnergyWattHours }
|
|
40
|
+
: {}),
|
|
41
|
+
...(connector.maxSessionSeconds !== undefined
|
|
42
|
+
? { maxSessionSeconds: connector.maxSessionSeconds }
|
|
43
|
+
: {}),
|
|
44
|
+
...(connector.phases !== undefined ? { phases: connector.phases } : {}),
|
|
45
|
+
...(connector.connectorType !== undefined ? { connectorType: connector.connectorType } : {}),
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function numeric(value) {
|
|
50
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
51
|
+
return value;
|
|
52
|
+
if (typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value))) {
|
|
53
|
+
return Number(value);
|
|
54
|
+
}
|
|
55
|
+
if (value && typeof value === 'object' && 'value' in value) {
|
|
56
|
+
return numeric(value.value);
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
/** Interlocks that enforce a connector's declared ceiling before authorization. */
|
|
61
|
+
function chargingInterlocks(site, connector) {
|
|
62
|
+
const id = chargingResourceId(site, connector.connectorId);
|
|
63
|
+
const safeState = {
|
|
64
|
+
resourceId: id,
|
|
65
|
+
command: { action: 'charging:stopSession', connectorId: connector.connectorId },
|
|
66
|
+
};
|
|
67
|
+
const interlocks = [
|
|
68
|
+
{
|
|
69
|
+
id: `ocpp:power-ceiling:${connector.connectorId}`,
|
|
70
|
+
kind: 'precondition',
|
|
71
|
+
resources: [id],
|
|
72
|
+
safeState,
|
|
73
|
+
evaluate({ parameters }) {
|
|
74
|
+
const requested = numeric(parameters.maxPowerWatts);
|
|
75
|
+
if (requested === undefined)
|
|
76
|
+
return { satisfied: true };
|
|
77
|
+
const ceiling = Number(connector.maxPowerWatts);
|
|
78
|
+
return requested <= ceiling
|
|
79
|
+
? { satisfied: true }
|
|
80
|
+
: { satisfied: false, reason: `requested ${requested} W exceeds connector ceiling ${ceiling} W` };
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
if (connector.maxEnergyWattHours !== undefined) {
|
|
85
|
+
interlocks.push({
|
|
86
|
+
id: `ocpp:energy-ceiling:${connector.connectorId}`,
|
|
87
|
+
kind: 'precondition',
|
|
88
|
+
resources: [id],
|
|
89
|
+
safeState,
|
|
90
|
+
evaluate({ parameters }) {
|
|
91
|
+
const requested = numeric(parameters.maxEnergyWattHours);
|
|
92
|
+
if (requested === undefined)
|
|
93
|
+
return { satisfied: true };
|
|
94
|
+
const ceiling = Number(connector.maxEnergyWattHours);
|
|
95
|
+
return requested <= ceiling
|
|
96
|
+
? { satisfied: true }
|
|
97
|
+
: { satisfied: false, reason: `requested ${requested} Wh exceeds connector ceiling ${ceiling} Wh` };
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (connector.maxSessionSeconds !== undefined) {
|
|
102
|
+
interlocks.push({
|
|
103
|
+
id: `ocpp:duration-ceiling:${connector.connectorId}`,
|
|
104
|
+
kind: 'precondition',
|
|
105
|
+
resources: [id],
|
|
106
|
+
safeState,
|
|
107
|
+
evaluate({ parameters }) {
|
|
108
|
+
const requested = numeric(parameters.maxSessionSeconds);
|
|
109
|
+
if (requested === undefined)
|
|
110
|
+
return { satisfied: true };
|
|
111
|
+
const ceiling = Number(connector.maxSessionSeconds);
|
|
112
|
+
return requested <= ceiling
|
|
113
|
+
? { satisfied: true }
|
|
114
|
+
: { satisfied: false, reason: `requested ${requested} s exceeds connector ceiling ${ceiling} s` };
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return interlocks;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* OCPP status → device error classification (RFC-011 §4.7).
|
|
122
|
+
* Safety codes require a safe state and are never retried.
|
|
123
|
+
*/
|
|
124
|
+
function ocppErrorTaxonomy() {
|
|
125
|
+
const taxonomy = (0, act_core_1.createDeviceErrorTaxonomy)();
|
|
126
|
+
const classes = {
|
|
127
|
+
// Charger/connector protection — never retry.
|
|
128
|
+
ConnectorLockFailure: 'safety',
|
|
129
|
+
EVCommunicationError: 'safety',
|
|
130
|
+
GroundFailure: 'safety',
|
|
131
|
+
HighTemperature: 'safety',
|
|
132
|
+
InternalError: 'safety',
|
|
133
|
+
OverCurrentFailure: 'safety',
|
|
134
|
+
OverVoltage: 'safety',
|
|
135
|
+
PowerMeterFailure: 'safety',
|
|
136
|
+
PowerSwitchFailure: 'safety',
|
|
137
|
+
ResetFailure: 'safety',
|
|
138
|
+
UnderVoltage: 'safety',
|
|
139
|
+
Faulted: 'safety',
|
|
140
|
+
// Occupancy/state — retrying the same command cannot succeed.
|
|
141
|
+
Occupied: 'permanent',
|
|
142
|
+
Reserved: 'permanent',
|
|
143
|
+
Unavailable: 'permanent',
|
|
144
|
+
NotSupported: 'permanent',
|
|
145
|
+
Rejected: 'permanent',
|
|
146
|
+
Invalid: 'permanent',
|
|
147
|
+
// Link/availability — a retry with the same operation id is safe.
|
|
148
|
+
Offline: 'transient',
|
|
149
|
+
Timeout: 'transient',
|
|
150
|
+
WeakSignal: 'transient',
|
|
151
|
+
// Identity — never retry without a new authorization.
|
|
152
|
+
InvalidIdTag: 'auth',
|
|
153
|
+
UnknownIdTag: 'auth',
|
|
154
|
+
AuthorizationFailed: 'auth',
|
|
155
|
+
};
|
|
156
|
+
for (const [code, classification] of Object.entries(classes)) {
|
|
157
|
+
taxonomy.register(exports.OCPP_PROTOCOL, code, classification);
|
|
158
|
+
}
|
|
159
|
+
return taxonomy;
|
|
160
|
+
}
|
|
161
|
+
/** Data-only charger profile (definitions are supplied by the adapter). */
|
|
162
|
+
function createOcppProfile(options) {
|
|
163
|
+
const resources = options.connectors.map((connector) => chargingResource(options.site, connector));
|
|
164
|
+
const interlocks = options.connectors.flatMap((connector) => chargingInterlocks(options.site, connector));
|
|
165
|
+
return {
|
|
166
|
+
id: 'ocpp',
|
|
167
|
+
version: 1,
|
|
168
|
+
resources,
|
|
169
|
+
interlocks,
|
|
170
|
+
errorTaxonomy: ocppErrorTaxonomy(),
|
|
171
|
+
capabilities: ['industrial:action', 'industrial:read', 'transport:ocpp'],
|
|
172
|
+
recipes: [],
|
|
173
|
+
definitions: [],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC-024 §5.2 — OCPP transport port.
|
|
3
|
+
*
|
|
4
|
+
* The caller injects a transport that talks to a CSMS or a local charger stack.
|
|
5
|
+
* This package owns the governed operation semantics only: it never opens a
|
|
6
|
+
* socket and never implements an OCPP stack. A deployment can back the port
|
|
7
|
+
* with a CSMS client, a charger stack, or a simulator.
|
|
8
|
+
*
|
|
9
|
+
* Every operation is idempotent at the port boundary: the prepared command and
|
|
10
|
+
* the operation id are stable across retries (RFC-022 §5.1), so a transport may
|
|
11
|
+
* safely deduplicate a replayed request.
|
|
12
|
+
*/
|
|
13
|
+
export interface OcppResponse {
|
|
14
|
+
/** OCPP status (`Accepted`, `Rejected`, `Occupied`, `Faulted`, …). */
|
|
15
|
+
status?: string;
|
|
16
|
+
payload?: Record<string, unknown>;
|
|
17
|
+
errorCode?: string;
|
|
18
|
+
errorDescription?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface OcppStartRequest {
|
|
21
|
+
connectorId: number;
|
|
22
|
+
idTag: string;
|
|
23
|
+
maxPowerWatts?: string;
|
|
24
|
+
maxEnergyWattHours?: string;
|
|
25
|
+
maxSessionSeconds?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface OcppStopRequest {
|
|
28
|
+
connectorId: number;
|
|
29
|
+
transactionId: number;
|
|
30
|
+
}
|
|
31
|
+
export interface OcppChargingProfileRequest {
|
|
32
|
+
connectorId: number;
|
|
33
|
+
profileId: number;
|
|
34
|
+
maxPowerWatts: string;
|
|
35
|
+
maxEnergyWattHours?: string;
|
|
36
|
+
maxSessionSeconds?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface OcppClearProfileRequest {
|
|
39
|
+
connectorId: number;
|
|
40
|
+
profileId?: number;
|
|
41
|
+
}
|
|
42
|
+
export interface OcppReserveRequest {
|
|
43
|
+
connectorId: number;
|
|
44
|
+
idTag: string;
|
|
45
|
+
reservationId: number;
|
|
46
|
+
expiresInSeconds: number;
|
|
47
|
+
}
|
|
48
|
+
export interface OcppMeterValue {
|
|
49
|
+
transactionId: number;
|
|
50
|
+
connectorId: number;
|
|
51
|
+
/** ISO-8601 timestamp from the charger. */
|
|
52
|
+
timestamp: string;
|
|
53
|
+
/** Cumulative energy register for the transaction, in Wh (decimal string). */
|
|
54
|
+
energyWattHours: string;
|
|
55
|
+
measurand?: string;
|
|
56
|
+
}
|
|
57
|
+
export type OcppTransactionEventType = 'Started' | 'Updated' | 'Ended';
|
|
58
|
+
export interface OcppTransactionEvent {
|
|
59
|
+
transactionId: number;
|
|
60
|
+
connectorId: number;
|
|
61
|
+
eventType: OcppTransactionEventType;
|
|
62
|
+
timestamp: string;
|
|
63
|
+
energyWattHours?: string;
|
|
64
|
+
}
|
|
65
|
+
export interface OcppMeterQuery {
|
|
66
|
+
connectorId: number;
|
|
67
|
+
transactionId?: number;
|
|
68
|
+
}
|
|
69
|
+
export interface OcppTransactionState {
|
|
70
|
+
status: 'active' | 'ended' | 'unknown';
|
|
71
|
+
/** Final cumulative energy register, in Wh. */
|
|
72
|
+
energyWattHours?: string;
|
|
73
|
+
endedAt?: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Injected OCPP port. Implementations map these calls onto their CSMS/charger
|
|
77
|
+
* stack (RemoteStartTransaction, SetChargingProfile, ReserveNow, …).
|
|
78
|
+
*/
|
|
79
|
+
export interface OcppTransportPort {
|
|
80
|
+
remoteStartTransaction(request: OcppStartRequest): Promise<OcppResponse>;
|
|
81
|
+
remoteStopTransaction(request: OcppStopRequest): Promise<OcppResponse>;
|
|
82
|
+
/**
|
|
83
|
+
* Stop whatever session is active on a connector. Used as the connector's
|
|
84
|
+
* safe state, where the transaction id may not be known.
|
|
85
|
+
*/
|
|
86
|
+
stopConnector?(connectorId: number): Promise<OcppResponse>;
|
|
87
|
+
setChargingProfile(request: OcppChargingProfileRequest): Promise<OcppResponse>;
|
|
88
|
+
clearChargingProfile(request: OcppClearProfileRequest): Promise<OcppResponse>;
|
|
89
|
+
reserveNow(request: OcppReserveRequest): Promise<OcppResponse>;
|
|
90
|
+
/** Readback path used for reconciliation; must originate from the charger. */
|
|
91
|
+
readMeterValues(query: OcppMeterQuery): Promise<OcppMeterValue[]>;
|
|
92
|
+
/** Charger/CSMS view of the transaction; required for definitive completion. */
|
|
93
|
+
readTransactionState?(query: OcppMeterQuery): Promise<OcppTransactionState>;
|
|
94
|
+
/** Optional push path for transaction events (Started/Updated/Ended). */
|
|
95
|
+
onTransactionEvent?(handler: (event: OcppTransactionEvent) => void): () => void;
|
|
96
|
+
}
|
|
97
|
+
/** Transport/OCPP rejection carrying the protocol code. */
|
|
98
|
+
export declare class OcppTransportError extends Error {
|
|
99
|
+
readonly code: string;
|
|
100
|
+
constructor(code: string, message: string);
|
|
101
|
+
}
|
|
102
|
+
export declare const OCPP_ACCEPTED = "Accepted";
|
|
103
|
+
/** Throw unless the response status is `Accepted`. */
|
|
104
|
+
export declare function assertAccepted(response: OcppResponse, action: string): void;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* RFC-024 §5.2 — OCPP transport port.
|
|
4
|
+
*
|
|
5
|
+
* The caller injects a transport that talks to a CSMS or a local charger stack.
|
|
6
|
+
* This package owns the governed operation semantics only: it never opens a
|
|
7
|
+
* socket and never implements an OCPP stack. A deployment can back the port
|
|
8
|
+
* with a CSMS client, a charger stack, or a simulator.
|
|
9
|
+
*
|
|
10
|
+
* Every operation is idempotent at the port boundary: the prepared command and
|
|
11
|
+
* the operation id are stable across retries (RFC-022 §5.1), so a transport may
|
|
12
|
+
* safely deduplicate a replayed request.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.OCPP_ACCEPTED = exports.OcppTransportError = void 0;
|
|
16
|
+
exports.assertAccepted = assertAccepted;
|
|
17
|
+
/** Transport/OCPP rejection carrying the protocol code. */
|
|
18
|
+
class OcppTransportError extends Error {
|
|
19
|
+
constructor(code, message) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = 'OcppTransportError';
|
|
22
|
+
this.code = code;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.OcppTransportError = OcppTransportError;
|
|
26
|
+
exports.OCPP_ACCEPTED = 'Accepted';
|
|
27
|
+
/** Throw unless the response status is `Accepted`. */
|
|
28
|
+
function assertAccepted(response, action) {
|
|
29
|
+
const status = response.status ?? exports.OCPP_ACCEPTED;
|
|
30
|
+
if (status !== exports.OCPP_ACCEPTED) {
|
|
31
|
+
throw new OcppTransportError(status, `${action} rejected with status '${status}'`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -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;
|
package/dist/effects.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
export function deriveChargingEffects(limits) {
|
|
9
|
+
const physical = {
|
|
10
|
+
power: [{ resource: limits.resource, watts: limits.maxPowerWatts }],
|
|
11
|
+
};
|
|
12
|
+
if (limits.maxEnergyWattHours !== undefined) {
|
|
13
|
+
physical.energy = [{ resource: limits.resource, wattHours: limits.maxEnergyWattHours }];
|
|
14
|
+
}
|
|
15
|
+
if (limits.maxSessionSeconds !== undefined) {
|
|
16
|
+
physical.duration = [{ resource: limits.resource, seconds: limits.maxSessionSeconds }];
|
|
17
|
+
}
|
|
18
|
+
if (limits.profileId !== undefined && limits.profileDigest !== undefined) {
|
|
19
|
+
physical.envelopes = [
|
|
20
|
+
{ envelopeId: limits.profileId, envelopeDigest: limits.profileDigest },
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
return { spends: [], physical };
|
|
24
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC-024 — governed OCPP / EV charging profile.
|
|
3
|
+
*
|
|
4
|
+
* `@totemsdk/act-ocpp` authorizes and bounds a charging operation; the connector
|
|
5
|
+
* (`@totemsdk/edge-ocpp`) carries the wire frames and the charger/CSMS owns
|
|
6
|
+
* electrical protection and the control loop.
|
|
7
|
+
*/
|
|
8
|
+
export { OCPP_ACCEPTED, OcppTransportError, assertAccepted, } from './transport.js';
|
|
9
|
+
export type { OcppChargingProfileRequest, OcppClearProfileRequest, OcppMeterQuery, OcppMeterValue, OcppReserveRequest, OcppResponse, OcppStartRequest, OcppStopRequest, OcppTransactionEvent, OcppTransactionEventType, OcppTransactionState, OcppTransportPort, } from './transport.js';
|
|
10
|
+
export { OCPP_CORE_PROFILES, OCPP_VERSION_PREFERENCE, negotiateOcppCapabilities, supportsOcppProfile, } from './capabilities.js';
|
|
11
|
+
export type { NegotiateOptions, OcppCapabilitySet, OcppProfileName, OcppVersion } from './capabilities.js';
|
|
12
|
+
export { deriveChargingEffects } from './effects.js';
|
|
13
|
+
export type { ChargingLimits } from './effects.js';
|
|
14
|
+
export { meterEvidenceDigest, reconcileChargingSession } from './meter.js';
|
|
15
|
+
export type { ChargingMeterEvidence, ChargingMeterQuery, ChargingReconcileResult, ChargingReconcileStatus, } from './meter.js';
|
|
16
|
+
export { OCPP_PROTOCOL, chargingInterlocks, chargingResource, chargingResourceId, createOcppProfile, ocppErrorTaxonomy, } from './profile.js';
|
|
17
|
+
export type { ChargingConnector, OcppProfileOptions } from './profile.js';
|
|
18
|
+
export { createOcppDefinitions, errorCodeOf } from './adapter.js';
|
|
19
|
+
export type { ChargingCommand, OcppAdapterOptions } from './adapter.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC-024 — governed OCPP / EV charging profile.
|
|
3
|
+
*
|
|
4
|
+
* `@totemsdk/act-ocpp` authorizes and bounds a charging operation; the connector
|
|
5
|
+
* (`@totemsdk/edge-ocpp`) carries the wire frames and the charger/CSMS owns
|
|
6
|
+
* electrical protection and the control loop.
|
|
7
|
+
*/
|
|
8
|
+
export { OCPP_ACCEPTED, OcppTransportError, assertAccepted, } from './transport.js';
|
|
9
|
+
export { OCPP_CORE_PROFILES, OCPP_VERSION_PREFERENCE, negotiateOcppCapabilities, supportsOcppProfile, } from './capabilities.js';
|
|
10
|
+
export { deriveChargingEffects } from './effects.js';
|
|
11
|
+
export { meterEvidenceDigest, reconcileChargingSession } from './meter.js';
|
|
12
|
+
export { OCPP_PROTOCOL, chargingInterlocks, chargingResource, chargingResourceId, createOcppProfile, ocppErrorTaxonomy, } from './profile.js';
|
|
13
|
+
export { createOcppDefinitions, errorCodeOf } from './adapter.js';
|
package/dist/meter.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC-024 §5.7 — metering and settlement evidence.
|
|
3
|
+
*
|
|
4
|
+
* Meter values must come from the charger through the transport, never from the
|
|
5
|
+
* caller. The evidence digest is canonical so a settlement action can cite the
|
|
6
|
+
* charging operation id and prove which meter readings backed it.
|
|
7
|
+
*/
|
|
8
|
+
import type { OcppMeterValue, OcppTransportPort } from './transport.js';
|
|
9
|
+
export interface ChargingMeterEvidence {
|
|
10
|
+
operationId: string;
|
|
11
|
+
connectorId: number;
|
|
12
|
+
transactionId: number;
|
|
13
|
+
/** Latest cumulative energy register, in Wh. */
|
|
14
|
+
energyWattHours: string;
|
|
15
|
+
readings: readonly OcppMeterValue[];
|
|
16
|
+
digest: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ChargingMeterQuery {
|
|
19
|
+
operationId: string;
|
|
20
|
+
connectorId: number;
|
|
21
|
+
transactionId: number;
|
|
22
|
+
}
|
|
23
|
+
export type ChargingReconcileStatus = 'running' | 'confirmed' | 'unknown';
|
|
24
|
+
export interface ChargingReconcileResult {
|
|
25
|
+
status: ChargingReconcileStatus;
|
|
26
|
+
energyWattHours?: string;
|
|
27
|
+
evidence?: ChargingMeterEvidence;
|
|
28
|
+
detail?: string;
|
|
29
|
+
}
|
|
30
|
+
export declare function meterEvidenceDigest(evidence: Omit<ChargingMeterEvidence, 'digest'>): string;
|
|
31
|
+
/**
|
|
32
|
+
* Reconcile an accepted charging operation against the charger.
|
|
33
|
+
*
|
|
34
|
+
* - `confirmed` only when the charger reports the transaction ended (an energy
|
|
35
|
+
* register alone is progress, not completion);
|
|
36
|
+
* - `running` while the transaction is active or meter values advance;
|
|
37
|
+
* - `unknown` when the charger cannot be read — the reservation must stay held.
|
|
38
|
+
*/
|
|
39
|
+
export declare function reconcileChargingSession(transport: OcppTransportPort, query: ChargingMeterQuery): Promise<ChargingReconcileResult>;
|