@enyo-energy/energy-app-sdk 0.0.134 → 0.0.136

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.
@@ -52,6 +52,7 @@ __exportStar(require("./packages/energy-app-energy-prices.cjs"), exports);
52
52
  __exportStar(require("./packages/energy-app-modbus-rtu.cjs"), exports);
53
53
  __exportStar(require("./types/enyo-eebus.cjs"), exports);
54
54
  __exportStar(require("./types/enyo-eebus-use-cases.cjs"), exports);
55
+ __exportStar(require("./types/enyo-eebus-features.cjs"), exports);
55
56
  __exportStar(require("./packages/energy-app-eebus.cjs"), exports);
56
57
  __exportStar(require("./types/enyo-mqtt.cjs"), exports);
57
58
  __exportStar(require("./packages/energy-app-mqtt.cjs"), exports);
@@ -36,6 +36,7 @@ export * from './packages/energy-app-energy-prices.cjs';
36
36
  export * from './packages/energy-app-modbus-rtu.cjs';
37
37
  export * from './types/enyo-eebus.cjs';
38
38
  export * from './types/enyo-eebus-use-cases.cjs';
39
+ export * from './types/enyo-eebus-features.cjs';
39
40
  export * from './packages/energy-app-eebus.cjs';
40
41
  export * from './types/enyo-mqtt.cjs';
41
42
  export * from './packages/energy-app-mqtt.cjs';
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,90 @@
1
+ import { EebusRemoteFeatureCatalog } from '../../types/enyo-eebus-features.cjs';
2
+ /**
3
+ * Per-peer SPINE entity/feature catalog for paired EEbus remotes.
4
+ *
5
+ * Exposes the lib's `RemoteDevice` view — the set of entities and
6
+ * features the peer actually advertises via
7
+ * `NodeManagement.DetailedDiscoveryData`, kept in sync by the SDK as the
8
+ * remote emits `NodeManagement.NotifyChange` events.
9
+ *
10
+ * Use this service in preference to {@link EebusIdentityService.getSupportedUseCases}
11
+ * when gating package behaviour on remote capability. Non-certified peers
12
+ * and simulators routinely under-populate `NodeManagement.UseCaseData`
13
+ * while still exposing the matching SPINE features (e.g. a `LoadControl`
14
+ * server with `loadControlLimitDescriptionData` of
15
+ * `limitDirection: 'consume'` but no corresponding
16
+ * `limitationOfPowerConsumption` use case). Feature-level gates work on
17
+ * these peers; use-case gates do not.
18
+ *
19
+ * Identity, like the use-case list, is observable rather than one-shot:
20
+ * remotes add or remove entities and features after a firmware update or
21
+ * a runtime mode change. Always pair {@link get} with
22
+ * {@link onFeaturesChanged} for any package that reacts to peer
23
+ * capabilities, otherwise the package will keep operating against a
24
+ * stale snapshot.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * // Feature-based LPC gate that works on peers with incomplete UseCaseData
29
+ * const catalog = await eebus.features.get(ski);
30
+ * const loadControl = catalog.entities
31
+ * .flatMap(e => e.features)
32
+ * .find(f =>
33
+ * f.type === 'LoadControl'
34
+ * && f.role === 'server'
35
+ * && f.supportedFunctions.some(s => s.function === 'loadControlLimitListData'),
36
+ * );
37
+ * if (loadControl) {
38
+ * await eebus.useCases.lpc(ski).setConsumptionLimit({ value: 11000, isActive: true });
39
+ * }
40
+ *
41
+ * // React to the peer adding or removing features at runtime
42
+ * const listenerId = eebus.features.onFeaturesChanged(ski, next => {
43
+ * refreshCapabilityGates(next);
44
+ * });
45
+ * ```
46
+ */
47
+ export interface EebusFeatureCatalog {
48
+ /**
49
+ * Get the current SPINE entity/feature catalog snapshot for a remote node.
50
+ *
51
+ * The snapshot reflects the most recent `NodeManagement.DetailedDiscoveryData`
52
+ * state the SDK has observed; it does not trigger a re-fetch from the
53
+ * remote. To observe live additions, removals, and updates use
54
+ * {@link onFeaturesChanged}.
55
+ *
56
+ * If the peer identified by `ski` is not paired or not currently
57
+ * connected, the returned snapshot resolves with `found: false` and an
58
+ * empty `entities` list rather than throwing — so packages can use the
59
+ * call as a capability gate without wrapping it in `try`/`catch`.
60
+ *
61
+ * @param ski Subject Key Identifier of the remote node
62
+ * @returns The current entity/feature catalog snapshot
63
+ */
64
+ get: (ski: string) => Promise<EebusRemoteFeatureCatalog>;
65
+ /**
66
+ * Subscribe to feature/entity catalog changes for a remote node.
67
+ *
68
+ * The listener is invoked with the full updated snapshot whenever the
69
+ * lib emits `featureAdded`, `featureUpdated`, `featureRemoved`,
70
+ * `entityAdded`, or `entityRemoved` for the peer. The payload shape
71
+ * matches {@link get} so subscribers can replace their cached catalog
72
+ * on every event without diffing.
73
+ *
74
+ * Subscriptions are scoped to the peer identified by `ski`. If the
75
+ * peer disconnects, the listener remains registered and resumes
76
+ * delivering events when the peer reconnects — packages do not need
77
+ * to re-subscribe after a transient disconnect.
78
+ *
79
+ * @param ski Subject Key Identifier of the remote node
80
+ * @param listener Callback invoked with the full updated catalog snapshot
81
+ * @returns Listener ID that can be passed to {@link removeListener} to cancel
82
+ */
83
+ onFeaturesChanged: (ski: string, listener: (catalog: EebusRemoteFeatureCatalog) => void) => string;
84
+ /**
85
+ * Remove a feature-catalog listener previously registered via
86
+ * {@link onFeaturesChanged}.
87
+ * @param listenerId The ID returned by the registration method
88
+ */
89
+ removeListener: (listenerId: string) => void;
90
+ }
@@ -1,8 +1,10 @@
1
1
  import { EebusDeviceManagement } from './eebus-device-management.cjs';
2
+ import { EebusFeatureCatalog } from './eebus-feature-catalog.cjs';
2
3
  import { EebusIdentityService } from './eebus-identity-service.cjs';
3
4
  import { EebusSpineLowLevel } from './eebus-spine-low-level.cjs';
4
5
  import { EebusUseCaseRegistry } from './eebus-use-case-registry.cjs';
5
6
  export { EebusDeviceManagement } from './eebus-device-management.cjs';
7
+ export { EebusFeatureCatalog } from './eebus-feature-catalog.cjs';
6
8
  export { EebusIdentityService } from './eebus-identity-service.cjs';
7
9
  export { EebusSpineLowLevel } from './eebus-spine-low-level.cjs';
8
10
  export { EebusUseCaseRegistry } from './eebus-use-case-registry.cjs';
@@ -16,10 +18,11 @@ export { EebusSetpointClient } from './eebus-setpoint-client.cjs';
16
18
  /**
17
19
  * Interface for EEbus (SHIP/SPINE) device communication in enyo packages.
18
20
  *
19
- * The API is split into four orthogonal concerns, each its own sub-interface:
21
+ * The API is split into five orthogonal concerns, each its own sub-interface:
20
22
  *
21
23
  * - {@link devices} — SHIP-level device lifecycle: discovery, pairing, connection
22
24
  * - {@link identity} — NID: observable per-node identity, diagnosis state, use-case discovery
25
+ * - {@link features} — observable per-peer SPINE entity/feature catalog
23
26
  * - {@link useCases} — typed use-case clients: LPC, LPP, MGCP, MPC, OHPCF, Setpoint, Hvac
24
27
  * - {@link spine} — low-level SPINE escape hatch for features not yet wrapped
25
28
  *
@@ -41,16 +44,21 @@ export { EebusSetpointClient } from './eebus-setpoint-client.cjs';
41
44
  * console.log(`${identity.brandName} ${identity.deviceName} v${identity.softwareRevision}`);
42
45
  * eebus.identity.onIdentityChanged(device.ski, next => updateStatusBadge(next));
43
46
  *
44
- * // 3. Use casestyped, role-aware
45
- * const supported = await eebus.identity.getSupportedUseCases(device.ski);
46
- * if (supported.some(u => u.name === 'limitationOfPowerConsumption')) {
47
+ * // 3. Feature cataloggate behaviour on what the peer actually advertises
48
+ * const catalog = await eebus.features.get(device.ski);
49
+ * const hasLpcServer = catalog.entities
50
+ * .flatMap(e => e.features)
51
+ * .some(f => f.type === 'LoadControl' && f.role === 'server');
52
+ *
53
+ * // 4. Use cases — typed, role-aware
54
+ * if (hasLpcServer) {
47
55
  * await eebus.useCases.lpc(device.ski).setConsumptionLimit({
48
56
  * value: 11000,
49
57
  * isActive: true,
50
58
  * });
51
59
  * }
52
60
  *
53
- * // 4. Escape hatch — raw SPINE for unmodelled features
61
+ * // 5. Escape hatch — raw SPINE for unmodelled features
54
62
  * const dp = await eebus.spine.readData(device.ski, 'DeviceConfiguration', 'keyValueListData');
55
63
  * ```
56
64
  */
@@ -59,6 +67,12 @@ export interface EnergyAppEebus {
59
67
  devices: EebusDeviceManagement;
60
68
  /** EEBUS Node Identification (NID) — observable identity + use-case discovery */
61
69
  identity: EebusIdentityService;
70
+ /**
71
+ * Observable per-peer SPINE entity/feature catalog. Prefer feature-level
72
+ * gates from this catalog over use-case gates from {@link identity} when
73
+ * the peer is known to under-populate `NodeManagement.UseCaseData`.
74
+ */
75
+ features: EebusFeatureCatalog;
62
76
  /** Typed use-case clients for the implemented EEBUS use cases */
63
77
  useCases: EebusUseCaseRegistry;
64
78
  /** Low-level SPINE escape hatch for features not yet wrapped by a typed client */
@@ -98,6 +98,67 @@ export interface EnyoApplianceModbusMetadata {
98
98
  /** Optional base address for the appliance's Modbus registers */
99
99
  baseAddress?: number;
100
100
  }
101
+ /**
102
+ * EEBUS connection metadata for the appliance.
103
+ *
104
+ * Holds EEBUS-/SPINE-specific identifiers that identify the underlying
105
+ * remote node and entity. Vendor-neutral fields such as `vendorName`,
106
+ * `serialNumber`, `firmwareVersion` and `modelName` are already exposed on
107
+ * the top-level {@link EnyoApplianceMetadata} and should not be duplicated
108
+ * here — this interface only carries identifiers that have no meaningful
109
+ * equivalent outside of EEBUS.
110
+ */
111
+ export interface EnyoApplianceEebusMetadata {
112
+ /**
113
+ * Subject Key Identifier — the unique cryptographic identifier of the
114
+ * remote EEBUS node this appliance is bound to. Stable across firmware
115
+ * upgrades and the primary key for EEBUS pairing/trust.
116
+ */
117
+ ski: string;
118
+ /**
119
+ * SPINE entity type of the appliance within the remote node (e.g.
120
+ * `'EVSE'`, `'EV'`, `'HeatPumpAppliance'`). Comes from
121
+ * `NodeManagement.DetailedDiscoveryData` and identifies which sub-entity
122
+ * of the node represents this appliance when a node exposes multiple.
123
+ */
124
+ deviceType?: string;
125
+ /**
126
+ * SPINE entity address of the appliance within the remote node — a
127
+ * sequence of integers identifying the entity. Useful when the node
128
+ * exposes multiple entities of the same {@link deviceType}.
129
+ */
130
+ entityAddress?: number[];
131
+ /**
132
+ * Vendor company code as reported via SPINE
133
+ * `DeviceClassification.ManufacturerData.VendorCode`. EEBUS-specific
134
+ * counterpart to the human-readable `vendorName` on the parent
135
+ * metadata.
136
+ */
137
+ vendorCode?: string;
138
+ /**
139
+ * Brand name under which the device is sold, as reported via SPINE
140
+ * `DeviceClassification.ManufacturerData.BrandName`.
141
+ */
142
+ brandName?: string;
143
+ /**
144
+ * Manufacturer-assigned device code (model identifier) as reported via
145
+ * SPINE `DeviceClassification.ManufacturerData.DeviceCode`.
146
+ */
147
+ deviceCode?: string;
148
+ /**
149
+ * Manufacturer-assigned node identifier — the literal
150
+ * `ManufacturerNodeIdentification` field from SPINE
151
+ * `DeviceClassificationManufacturerDataType`. Often used by vendors as
152
+ * a stable identifier across firmware upgrades.
153
+ */
154
+ manufacturerNodeIdentification?: string;
155
+ /**
156
+ * User-assigned node identifier — the literal `UserNodeIdentification`
157
+ * field from SPINE `DeviceClassificationUserDataType`. May be changed
158
+ * by the end user via the device's UI at any time.
159
+ */
160
+ userNodeIdentification?: string;
161
+ }
101
162
  export declare enum EnyoApplianceConnectionType {
102
163
  Connector = "Connector",
103
164
  Cloud = "Cloud"
@@ -114,6 +175,8 @@ export interface EnyoApplianceMetadata {
114
175
  status?: EnyoApplianceStatusEnum;
115
176
  network?: EnyoApplianceNetworkMetadata;
116
177
  modbus?: EnyoApplianceModbusMetadata;
178
+ /** Optional EEBUS connection metadata (SKI, SPINE entity type, vendor code, …) */
179
+ eebus?: EnyoApplianceEebusMetadata;
117
180
  /** Optional MQTT configuration */
118
181
  mqtt?: EnyoApplianceMqttConfig;
119
182
  connectionType: EnyoApplianceConnectionType;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ /**
3
+ * Types describing the SPINE feature/entity catalog advertised by a remote
4
+ * EEbus peer.
5
+ *
6
+ * The catalog is the **authoritative source of truth** for what a peer can
7
+ * actually do. It is derived from the lib's `RemoteDevice` view, which is
8
+ * itself kept in sync with `NodeManagement.DetailedDiscoveryData` replies
9
+ * and `NodeManagement.NotifyChange` events from the remote.
10
+ *
11
+ * Prefer feature/role gates based on this catalog over use-case gates from
12
+ * {@link EebusUseCaseSupport}: many non-certified peers and simulators
13
+ * implement working SPINE features (e.g. a `LoadControl` server with a
14
+ * `loadControlLimitDescriptionData` of `limitDirection: 'consume'`) without
15
+ * advertising the matching use case in `NodeManagement.UseCaseData`.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Types describing the SPINE feature/entity catalog advertised by a remote
3
+ * EEbus peer.
4
+ *
5
+ * The catalog is the **authoritative source of truth** for what a peer can
6
+ * actually do. It is derived from the lib's `RemoteDevice` view, which is
7
+ * itself kept in sync with `NodeManagement.DetailedDiscoveryData` replies
8
+ * and `NodeManagement.NotifyChange` events from the remote.
9
+ *
10
+ * Prefer feature/role gates based on this catalog over use-case gates from
11
+ * {@link EebusUseCaseSupport}: many non-certified peers and simulators
12
+ * implement working SPINE features (e.g. a `LoadControl` server with a
13
+ * `loadControlLimitDescriptionData` of `limitDirection: 'consume'`) without
14
+ * advertising the matching use case in `NodeManagement.UseCaseData`.
15
+ */
16
+ /**
17
+ * SPINE role under which a feature is advertised by the remote node.
18
+ *
19
+ * The set is intentionally widened with `string` so that lib upgrades that
20
+ * introduce new SPINE role variants do not break consumer code that
21
+ * pattern-matches on the role.
22
+ */
23
+ export type EebusFeatureRole = 'client' | 'server' | 'special' | string;
24
+ /**
25
+ * SPINE address triple identifying a feature within the EEbus topology.
26
+ *
27
+ * - {@link entity} addresses the entity within the remote node (e.g. `[1]`
28
+ * for a flat node, `[1, 1]` when the node nests sub-entities such as a
29
+ * compressor under a heat-pump appliance).
30
+ * - {@link feature} addresses the feature within that entity.
31
+ * - {@link device} optionally carries the remote node's SPINE
32
+ * `NetworkAddressDeviceID`. Usually omitted because the surrounding
33
+ * {@link EebusRemoteFeatureCatalog.deviceAddress} already provides it.
34
+ */
35
+ export interface EebusFeatureAddress {
36
+ /** SPINE entity address — a sequence of integers identifying the entity within the node */
37
+ entity: number[];
38
+ /** SPINE feature address within the entity */
39
+ feature: number;
40
+ /** Optional SPINE `NetworkAddressDeviceID` of the remote node */
41
+ device?: string;
42
+ }
43
+ /**
44
+ * A SPINE function the remote advertises as supported on a feature,
45
+ * together with the operations (read / write) the remote permits on it.
46
+ *
47
+ * The operation payloads are intentionally opaque (`object`) — SPINE
48
+ * permits per-function filters and bindings whose shape varies by
49
+ * function and is irrelevant to most callers. Pass them through verbatim
50
+ * when forwarding to {@link EebusSpineLowLevel}.
51
+ */
52
+ export interface EebusSupportedFunction {
53
+ /** SPINE function/data-set name (e.g. `'loadControlLimitListData'`, `'measurementListData'`) */
54
+ function: string;
55
+ /** Operations the remote permits on this function */
56
+ possibleOperations: {
57
+ /** Present when the remote permits reads; payload mirrors the SPINE `read` operation parameters */
58
+ read?: object;
59
+ /** Present when the remote permits writes; payload mirrors the SPINE `write` operation parameters */
60
+ write?: object;
61
+ };
62
+ }
63
+ /**
64
+ * A single SPINE feature advertised by a remote entity.
65
+ *
66
+ * The {@link type} is a wire string (e.g. `'LoadControl'`,
67
+ * `'Measurement'`, `'DeviceClassification'`) rather than a closed enum
68
+ * so that lib upgrades that introduce new SPINE feature types do not
69
+ * break existing packages — see {@link EebusSpineLowLevel} for the same
70
+ * rationale applied to the low-level escape hatch.
71
+ */
72
+ export interface EebusRemoteFeature {
73
+ /** SPINE address triple for this feature */
74
+ address: EebusFeatureAddress;
75
+ /** SPINE feature type wire string (e.g. `'LoadControl'`, `'Measurement'`) */
76
+ type: string;
77
+ /** SPINE role under which the feature is advertised */
78
+ role: EebusFeatureRole;
79
+ /** Functions the remote advertises as supported on this feature */
80
+ supportedFunctions: EebusSupportedFunction[];
81
+ /** Optional manufacturer-provided label */
82
+ label?: string;
83
+ /** Optional manufacturer-provided description */
84
+ description?: string;
85
+ }
86
+ /**
87
+ * A SPINE entity advertised by a remote node.
88
+ *
89
+ * A node usually exposes a `DeviceInformation` entity (the node itself)
90
+ * plus one or more application-specific entities (e.g. `EVSE`, `EV`,
91
+ * `HeatPumpAppliance`, `Compressor`). Sub-entities are flattened into
92
+ * this list with their full {@link address} path preserved (e.g. `[1, 1]`
93
+ * for a compressor under a heat-pump appliance), so consumers can
94
+ * reconstruct the hierarchy when needed.
95
+ */
96
+ export interface EebusRemoteEntity {
97
+ /** SPINE entity address — a sequence of integers identifying the entity (e.g. `[1]` or `[1, 1]`) */
98
+ address: number[];
99
+ /** SPINE entity type wire string (e.g. `'HeatPumpAppliance'`, `'Compressor'`, `'EVSE'`) */
100
+ type: string;
101
+ /** Optional manufacturer-provided label */
102
+ label?: string;
103
+ /** Optional manufacturer-provided description */
104
+ description?: string;
105
+ /** Features advertised on this entity */
106
+ features: EebusRemoteFeature[];
107
+ }
108
+ /**
109
+ * Snapshot of the full SPINE entity/feature catalog advertised by a
110
+ * remote EEbus peer.
111
+ *
112
+ * Returned by {@link EebusFeatureCatalog.get} and delivered to
113
+ * {@link EebusFeatureCatalog.onFeaturesChanged} listeners. The shape is
114
+ * intentionally identical between the two so that change subscribers can
115
+ * drop and replace their cached catalog on every event without diffing.
116
+ *
117
+ * When the peer identified by {@link ski} is not known (never paired or
118
+ * not currently connected), {@link found} is `false` and {@link entities}
119
+ * is an empty array — the call resolves cleanly rather than throwing, so
120
+ * packages can use the snapshot as a feature gate without try/catch.
121
+ */
122
+ export interface EebusRemoteFeatureCatalog {
123
+ /** Subject Key Identifier of the remote node this catalog describes */
124
+ ski: string;
125
+ /** Whether the peer is currently known to the SDK (paired AND reachable) */
126
+ found: boolean;
127
+ /** Remote node's SPINE `NetworkAddressDeviceID`, when known */
128
+ deviceAddress?: string;
129
+ /** SPINE entities advertised by the remote node, flattened with their address path preserved */
130
+ entities: EebusRemoteEntity[];
131
+ }
@@ -9,7 +9,7 @@ exports.getSdkVersion = getSdkVersion;
9
9
  /**
10
10
  * Current version of the enyo Energy App SDK.
11
11
  */
12
- exports.SDK_VERSION = '0.0.134';
12
+ exports.SDK_VERSION = '0.0.136';
13
13
  /**
14
14
  * Gets the current SDK version.
15
15
  * @returns The semantic version string of the SDK
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Current version of the enyo Energy App SDK.
7
7
  */
8
- export declare const SDK_VERSION = "0.0.134";
8
+ export declare const SDK_VERSION = "0.0.136";
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
package/dist/index.d.ts CHANGED
@@ -36,6 +36,7 @@ export * from './packages/energy-app-energy-prices.js';
36
36
  export * from './packages/energy-app-modbus-rtu.js';
37
37
  export * from './types/enyo-eebus.js';
38
38
  export * from './types/enyo-eebus-use-cases.js';
39
+ export * from './types/enyo-eebus-features.js';
39
40
  export * from './packages/energy-app-eebus.js';
40
41
  export * from './types/enyo-mqtt.js';
41
42
  export * from './packages/energy-app-mqtt.js';
package/dist/index.js CHANGED
@@ -36,6 +36,7 @@ export * from './packages/energy-app-energy-prices.js';
36
36
  export * from './packages/energy-app-modbus-rtu.js';
37
37
  export * from './types/enyo-eebus.js';
38
38
  export * from './types/enyo-eebus-use-cases.js';
39
+ export * from './types/enyo-eebus-features.js';
39
40
  export * from './packages/energy-app-eebus.js';
40
41
  export * from './types/enyo-mqtt.js';
41
42
  export * from './packages/energy-app-mqtt.js';
@@ -0,0 +1,90 @@
1
+ import { EebusRemoteFeatureCatalog } from '../../types/enyo-eebus-features.js';
2
+ /**
3
+ * Per-peer SPINE entity/feature catalog for paired EEbus remotes.
4
+ *
5
+ * Exposes the lib's `RemoteDevice` view — the set of entities and
6
+ * features the peer actually advertises via
7
+ * `NodeManagement.DetailedDiscoveryData`, kept in sync by the SDK as the
8
+ * remote emits `NodeManagement.NotifyChange` events.
9
+ *
10
+ * Use this service in preference to {@link EebusIdentityService.getSupportedUseCases}
11
+ * when gating package behaviour on remote capability. Non-certified peers
12
+ * and simulators routinely under-populate `NodeManagement.UseCaseData`
13
+ * while still exposing the matching SPINE features (e.g. a `LoadControl`
14
+ * server with `loadControlLimitDescriptionData` of
15
+ * `limitDirection: 'consume'` but no corresponding
16
+ * `limitationOfPowerConsumption` use case). Feature-level gates work on
17
+ * these peers; use-case gates do not.
18
+ *
19
+ * Identity, like the use-case list, is observable rather than one-shot:
20
+ * remotes add or remove entities and features after a firmware update or
21
+ * a runtime mode change. Always pair {@link get} with
22
+ * {@link onFeaturesChanged} for any package that reacts to peer
23
+ * capabilities, otherwise the package will keep operating against a
24
+ * stale snapshot.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * // Feature-based LPC gate that works on peers with incomplete UseCaseData
29
+ * const catalog = await eebus.features.get(ski);
30
+ * const loadControl = catalog.entities
31
+ * .flatMap(e => e.features)
32
+ * .find(f =>
33
+ * f.type === 'LoadControl'
34
+ * && f.role === 'server'
35
+ * && f.supportedFunctions.some(s => s.function === 'loadControlLimitListData'),
36
+ * );
37
+ * if (loadControl) {
38
+ * await eebus.useCases.lpc(ski).setConsumptionLimit({ value: 11000, isActive: true });
39
+ * }
40
+ *
41
+ * // React to the peer adding or removing features at runtime
42
+ * const listenerId = eebus.features.onFeaturesChanged(ski, next => {
43
+ * refreshCapabilityGates(next);
44
+ * });
45
+ * ```
46
+ */
47
+ export interface EebusFeatureCatalog {
48
+ /**
49
+ * Get the current SPINE entity/feature catalog snapshot for a remote node.
50
+ *
51
+ * The snapshot reflects the most recent `NodeManagement.DetailedDiscoveryData`
52
+ * state the SDK has observed; it does not trigger a re-fetch from the
53
+ * remote. To observe live additions, removals, and updates use
54
+ * {@link onFeaturesChanged}.
55
+ *
56
+ * If the peer identified by `ski` is not paired or not currently
57
+ * connected, the returned snapshot resolves with `found: false` and an
58
+ * empty `entities` list rather than throwing — so packages can use the
59
+ * call as a capability gate without wrapping it in `try`/`catch`.
60
+ *
61
+ * @param ski Subject Key Identifier of the remote node
62
+ * @returns The current entity/feature catalog snapshot
63
+ */
64
+ get: (ski: string) => Promise<EebusRemoteFeatureCatalog>;
65
+ /**
66
+ * Subscribe to feature/entity catalog changes for a remote node.
67
+ *
68
+ * The listener is invoked with the full updated snapshot whenever the
69
+ * lib emits `featureAdded`, `featureUpdated`, `featureRemoved`,
70
+ * `entityAdded`, or `entityRemoved` for the peer. The payload shape
71
+ * matches {@link get} so subscribers can replace their cached catalog
72
+ * on every event without diffing.
73
+ *
74
+ * Subscriptions are scoped to the peer identified by `ski`. If the
75
+ * peer disconnects, the listener remains registered and resumes
76
+ * delivering events when the peer reconnects — packages do not need
77
+ * to re-subscribe after a transient disconnect.
78
+ *
79
+ * @param ski Subject Key Identifier of the remote node
80
+ * @param listener Callback invoked with the full updated catalog snapshot
81
+ * @returns Listener ID that can be passed to {@link removeListener} to cancel
82
+ */
83
+ onFeaturesChanged: (ski: string, listener: (catalog: EebusRemoteFeatureCatalog) => void) => string;
84
+ /**
85
+ * Remove a feature-catalog listener previously registered via
86
+ * {@link onFeaturesChanged}.
87
+ * @param listenerId The ID returned by the registration method
88
+ */
89
+ removeListener: (listenerId: string) => void;
90
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,8 +1,10 @@
1
1
  import { EebusDeviceManagement } from './eebus-device-management.js';
2
+ import { EebusFeatureCatalog } from './eebus-feature-catalog.js';
2
3
  import { EebusIdentityService } from './eebus-identity-service.js';
3
4
  import { EebusSpineLowLevel } from './eebus-spine-low-level.js';
4
5
  import { EebusUseCaseRegistry } from './eebus-use-case-registry.js';
5
6
  export { EebusDeviceManagement } from './eebus-device-management.js';
7
+ export { EebusFeatureCatalog } from './eebus-feature-catalog.js';
6
8
  export { EebusIdentityService } from './eebus-identity-service.js';
7
9
  export { EebusSpineLowLevel } from './eebus-spine-low-level.js';
8
10
  export { EebusUseCaseRegistry } from './eebus-use-case-registry.js';
@@ -16,10 +18,11 @@ export { EebusSetpointClient } from './eebus-setpoint-client.js';
16
18
  /**
17
19
  * Interface for EEbus (SHIP/SPINE) device communication in enyo packages.
18
20
  *
19
- * The API is split into four orthogonal concerns, each its own sub-interface:
21
+ * The API is split into five orthogonal concerns, each its own sub-interface:
20
22
  *
21
23
  * - {@link devices} — SHIP-level device lifecycle: discovery, pairing, connection
22
24
  * - {@link identity} — NID: observable per-node identity, diagnosis state, use-case discovery
25
+ * - {@link features} — observable per-peer SPINE entity/feature catalog
23
26
  * - {@link useCases} — typed use-case clients: LPC, LPP, MGCP, MPC, OHPCF, Setpoint, Hvac
24
27
  * - {@link spine} — low-level SPINE escape hatch for features not yet wrapped
25
28
  *
@@ -41,16 +44,21 @@ export { EebusSetpointClient } from './eebus-setpoint-client.js';
41
44
  * console.log(`${identity.brandName} ${identity.deviceName} v${identity.softwareRevision}`);
42
45
  * eebus.identity.onIdentityChanged(device.ski, next => updateStatusBadge(next));
43
46
  *
44
- * // 3. Use casestyped, role-aware
45
- * const supported = await eebus.identity.getSupportedUseCases(device.ski);
46
- * if (supported.some(u => u.name === 'limitationOfPowerConsumption')) {
47
+ * // 3. Feature cataloggate behaviour on what the peer actually advertises
48
+ * const catalog = await eebus.features.get(device.ski);
49
+ * const hasLpcServer = catalog.entities
50
+ * .flatMap(e => e.features)
51
+ * .some(f => f.type === 'LoadControl' && f.role === 'server');
52
+ *
53
+ * // 4. Use cases — typed, role-aware
54
+ * if (hasLpcServer) {
47
55
  * await eebus.useCases.lpc(device.ski).setConsumptionLimit({
48
56
  * value: 11000,
49
57
  * isActive: true,
50
58
  * });
51
59
  * }
52
60
  *
53
- * // 4. Escape hatch — raw SPINE for unmodelled features
61
+ * // 5. Escape hatch — raw SPINE for unmodelled features
54
62
  * const dp = await eebus.spine.readData(device.ski, 'DeviceConfiguration', 'keyValueListData');
55
63
  * ```
56
64
  */
@@ -59,6 +67,12 @@ export interface EnergyAppEebus {
59
67
  devices: EebusDeviceManagement;
60
68
  /** EEBUS Node Identification (NID) — observable identity + use-case discovery */
61
69
  identity: EebusIdentityService;
70
+ /**
71
+ * Observable per-peer SPINE entity/feature catalog. Prefer feature-level
72
+ * gates from this catalog over use-case gates from {@link identity} when
73
+ * the peer is known to under-populate `NodeManagement.UseCaseData`.
74
+ */
75
+ features: EebusFeatureCatalog;
62
76
  /** Typed use-case clients for the implemented EEBUS use cases */
63
77
  useCases: EebusUseCaseRegistry;
64
78
  /** Low-level SPINE escape hatch for features not yet wrapped by a typed client */
@@ -98,6 +98,67 @@ export interface EnyoApplianceModbusMetadata {
98
98
  /** Optional base address for the appliance's Modbus registers */
99
99
  baseAddress?: number;
100
100
  }
101
+ /**
102
+ * EEBUS connection metadata for the appliance.
103
+ *
104
+ * Holds EEBUS-/SPINE-specific identifiers that identify the underlying
105
+ * remote node and entity. Vendor-neutral fields such as `vendorName`,
106
+ * `serialNumber`, `firmwareVersion` and `modelName` are already exposed on
107
+ * the top-level {@link EnyoApplianceMetadata} and should not be duplicated
108
+ * here — this interface only carries identifiers that have no meaningful
109
+ * equivalent outside of EEBUS.
110
+ */
111
+ export interface EnyoApplianceEebusMetadata {
112
+ /**
113
+ * Subject Key Identifier — the unique cryptographic identifier of the
114
+ * remote EEBUS node this appliance is bound to. Stable across firmware
115
+ * upgrades and the primary key for EEBUS pairing/trust.
116
+ */
117
+ ski: string;
118
+ /**
119
+ * SPINE entity type of the appliance within the remote node (e.g.
120
+ * `'EVSE'`, `'EV'`, `'HeatPumpAppliance'`). Comes from
121
+ * `NodeManagement.DetailedDiscoveryData` and identifies which sub-entity
122
+ * of the node represents this appliance when a node exposes multiple.
123
+ */
124
+ deviceType?: string;
125
+ /**
126
+ * SPINE entity address of the appliance within the remote node — a
127
+ * sequence of integers identifying the entity. Useful when the node
128
+ * exposes multiple entities of the same {@link deviceType}.
129
+ */
130
+ entityAddress?: number[];
131
+ /**
132
+ * Vendor company code as reported via SPINE
133
+ * `DeviceClassification.ManufacturerData.VendorCode`. EEBUS-specific
134
+ * counterpart to the human-readable `vendorName` on the parent
135
+ * metadata.
136
+ */
137
+ vendorCode?: string;
138
+ /**
139
+ * Brand name under which the device is sold, as reported via SPINE
140
+ * `DeviceClassification.ManufacturerData.BrandName`.
141
+ */
142
+ brandName?: string;
143
+ /**
144
+ * Manufacturer-assigned device code (model identifier) as reported via
145
+ * SPINE `DeviceClassification.ManufacturerData.DeviceCode`.
146
+ */
147
+ deviceCode?: string;
148
+ /**
149
+ * Manufacturer-assigned node identifier — the literal
150
+ * `ManufacturerNodeIdentification` field from SPINE
151
+ * `DeviceClassificationManufacturerDataType`. Often used by vendors as
152
+ * a stable identifier across firmware upgrades.
153
+ */
154
+ manufacturerNodeIdentification?: string;
155
+ /**
156
+ * User-assigned node identifier — the literal `UserNodeIdentification`
157
+ * field from SPINE `DeviceClassificationUserDataType`. May be changed
158
+ * by the end user via the device's UI at any time.
159
+ */
160
+ userNodeIdentification?: string;
161
+ }
101
162
  export declare enum EnyoApplianceConnectionType {
102
163
  Connector = "Connector",
103
164
  Cloud = "Cloud"
@@ -114,6 +175,8 @@ export interface EnyoApplianceMetadata {
114
175
  status?: EnyoApplianceStatusEnum;
115
176
  network?: EnyoApplianceNetworkMetadata;
116
177
  modbus?: EnyoApplianceModbusMetadata;
178
+ /** Optional EEBUS connection metadata (SKI, SPINE entity type, vendor code, …) */
179
+ eebus?: EnyoApplianceEebusMetadata;
117
180
  /** Optional MQTT configuration */
118
181
  mqtt?: EnyoApplianceMqttConfig;
119
182
  connectionType: EnyoApplianceConnectionType;
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Types describing the SPINE feature/entity catalog advertised by a remote
3
+ * EEbus peer.
4
+ *
5
+ * The catalog is the **authoritative source of truth** for what a peer can
6
+ * actually do. It is derived from the lib's `RemoteDevice` view, which is
7
+ * itself kept in sync with `NodeManagement.DetailedDiscoveryData` replies
8
+ * and `NodeManagement.NotifyChange` events from the remote.
9
+ *
10
+ * Prefer feature/role gates based on this catalog over use-case gates from
11
+ * {@link EebusUseCaseSupport}: many non-certified peers and simulators
12
+ * implement working SPINE features (e.g. a `LoadControl` server with a
13
+ * `loadControlLimitDescriptionData` of `limitDirection: 'consume'`) without
14
+ * advertising the matching use case in `NodeManagement.UseCaseData`.
15
+ */
16
+ /**
17
+ * SPINE role under which a feature is advertised by the remote node.
18
+ *
19
+ * The set is intentionally widened with `string` so that lib upgrades that
20
+ * introduce new SPINE role variants do not break consumer code that
21
+ * pattern-matches on the role.
22
+ */
23
+ export type EebusFeatureRole = 'client' | 'server' | 'special' | string;
24
+ /**
25
+ * SPINE address triple identifying a feature within the EEbus topology.
26
+ *
27
+ * - {@link entity} addresses the entity within the remote node (e.g. `[1]`
28
+ * for a flat node, `[1, 1]` when the node nests sub-entities such as a
29
+ * compressor under a heat-pump appliance).
30
+ * - {@link feature} addresses the feature within that entity.
31
+ * - {@link device} optionally carries the remote node's SPINE
32
+ * `NetworkAddressDeviceID`. Usually omitted because the surrounding
33
+ * {@link EebusRemoteFeatureCatalog.deviceAddress} already provides it.
34
+ */
35
+ export interface EebusFeatureAddress {
36
+ /** SPINE entity address — a sequence of integers identifying the entity within the node */
37
+ entity: number[];
38
+ /** SPINE feature address within the entity */
39
+ feature: number;
40
+ /** Optional SPINE `NetworkAddressDeviceID` of the remote node */
41
+ device?: string;
42
+ }
43
+ /**
44
+ * A SPINE function the remote advertises as supported on a feature,
45
+ * together with the operations (read / write) the remote permits on it.
46
+ *
47
+ * The operation payloads are intentionally opaque (`object`) — SPINE
48
+ * permits per-function filters and bindings whose shape varies by
49
+ * function and is irrelevant to most callers. Pass them through verbatim
50
+ * when forwarding to {@link EebusSpineLowLevel}.
51
+ */
52
+ export interface EebusSupportedFunction {
53
+ /** SPINE function/data-set name (e.g. `'loadControlLimitListData'`, `'measurementListData'`) */
54
+ function: string;
55
+ /** Operations the remote permits on this function */
56
+ possibleOperations: {
57
+ /** Present when the remote permits reads; payload mirrors the SPINE `read` operation parameters */
58
+ read?: object;
59
+ /** Present when the remote permits writes; payload mirrors the SPINE `write` operation parameters */
60
+ write?: object;
61
+ };
62
+ }
63
+ /**
64
+ * A single SPINE feature advertised by a remote entity.
65
+ *
66
+ * The {@link type} is a wire string (e.g. `'LoadControl'`,
67
+ * `'Measurement'`, `'DeviceClassification'`) rather than a closed enum
68
+ * so that lib upgrades that introduce new SPINE feature types do not
69
+ * break existing packages — see {@link EebusSpineLowLevel} for the same
70
+ * rationale applied to the low-level escape hatch.
71
+ */
72
+ export interface EebusRemoteFeature {
73
+ /** SPINE address triple for this feature */
74
+ address: EebusFeatureAddress;
75
+ /** SPINE feature type wire string (e.g. `'LoadControl'`, `'Measurement'`) */
76
+ type: string;
77
+ /** SPINE role under which the feature is advertised */
78
+ role: EebusFeatureRole;
79
+ /** Functions the remote advertises as supported on this feature */
80
+ supportedFunctions: EebusSupportedFunction[];
81
+ /** Optional manufacturer-provided label */
82
+ label?: string;
83
+ /** Optional manufacturer-provided description */
84
+ description?: string;
85
+ }
86
+ /**
87
+ * A SPINE entity advertised by a remote node.
88
+ *
89
+ * A node usually exposes a `DeviceInformation` entity (the node itself)
90
+ * plus one or more application-specific entities (e.g. `EVSE`, `EV`,
91
+ * `HeatPumpAppliance`, `Compressor`). Sub-entities are flattened into
92
+ * this list with their full {@link address} path preserved (e.g. `[1, 1]`
93
+ * for a compressor under a heat-pump appliance), so consumers can
94
+ * reconstruct the hierarchy when needed.
95
+ */
96
+ export interface EebusRemoteEntity {
97
+ /** SPINE entity address — a sequence of integers identifying the entity (e.g. `[1]` or `[1, 1]`) */
98
+ address: number[];
99
+ /** SPINE entity type wire string (e.g. `'HeatPumpAppliance'`, `'Compressor'`, `'EVSE'`) */
100
+ type: string;
101
+ /** Optional manufacturer-provided label */
102
+ label?: string;
103
+ /** Optional manufacturer-provided description */
104
+ description?: string;
105
+ /** Features advertised on this entity */
106
+ features: EebusRemoteFeature[];
107
+ }
108
+ /**
109
+ * Snapshot of the full SPINE entity/feature catalog advertised by a
110
+ * remote EEbus peer.
111
+ *
112
+ * Returned by {@link EebusFeatureCatalog.get} and delivered to
113
+ * {@link EebusFeatureCatalog.onFeaturesChanged} listeners. The shape is
114
+ * intentionally identical between the two so that change subscribers can
115
+ * drop and replace their cached catalog on every event without diffing.
116
+ *
117
+ * When the peer identified by {@link ski} is not known (never paired or
118
+ * not currently connected), {@link found} is `false` and {@link entities}
119
+ * is an empty array — the call resolves cleanly rather than throwing, so
120
+ * packages can use the snapshot as a feature gate without try/catch.
121
+ */
122
+ export interface EebusRemoteFeatureCatalog {
123
+ /** Subject Key Identifier of the remote node this catalog describes */
124
+ ski: string;
125
+ /** Whether the peer is currently known to the SDK (paired AND reachable) */
126
+ found: boolean;
127
+ /** Remote node's SPINE `NetworkAddressDeviceID`, when known */
128
+ deviceAddress?: string;
129
+ /** SPINE entities advertised by the remote node, flattened with their address path preserved */
130
+ entities: EebusRemoteEntity[];
131
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Types describing the SPINE feature/entity catalog advertised by a remote
3
+ * EEbus peer.
4
+ *
5
+ * The catalog is the **authoritative source of truth** for what a peer can
6
+ * actually do. It is derived from the lib's `RemoteDevice` view, which is
7
+ * itself kept in sync with `NodeManagement.DetailedDiscoveryData` replies
8
+ * and `NodeManagement.NotifyChange` events from the remote.
9
+ *
10
+ * Prefer feature/role gates based on this catalog over use-case gates from
11
+ * {@link EebusUseCaseSupport}: many non-certified peers and simulators
12
+ * implement working SPINE features (e.g. a `LoadControl` server with a
13
+ * `loadControlLimitDescriptionData` of `limitDirection: 'consume'`) without
14
+ * advertising the matching use case in `NodeManagement.UseCaseData`.
15
+ */
16
+ export {};
package/dist/version.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Current version of the enyo Energy App SDK.
7
7
  */
8
- export declare const SDK_VERSION = "0.0.134";
8
+ export declare const SDK_VERSION = "0.0.136";
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
package/dist/version.js CHANGED
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Current version of the enyo Energy App SDK.
7
7
  */
8
- export const SDK_VERSION = '0.0.134';
8
+ export const SDK_VERSION = '0.0.136';
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enyo-energy/energy-app-sdk",
3
- "version": "0.0.134",
3
+ "version": "0.0.136",
4
4
  "description": "enyo Energy App SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",