@enyo-energy/energy-app-sdk 0.0.188 → 0.0.190
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/dist/cjs/energy-app-permission.type.cjs +1 -0
- package/dist/cjs/energy-app-permission.type.d.cts +1 -0
- package/dist/cjs/energy-app.cjs +16 -0
- package/dist/cjs/energy-app.d.cts +15 -0
- package/dist/cjs/enyo-energy-app-sdk.d.cts +3 -0
- package/dist/cjs/implementations/modbus/define-modbus-server-register.cjs +85 -0
- package/dist/cjs/implementations/modbus/define-modbus-server-register.d.cts +71 -0
- package/dist/cjs/implementations/modbus/modbus-server-validators.cjs +161 -0
- package/dist/cjs/implementations/modbus/modbus-server-validators.d.cts +73 -0
- package/dist/cjs/index.cjs +4 -0
- package/dist/cjs/index.d.cts +4 -0
- package/dist/cjs/packages/energy-app-modbus-server.cjs +14 -0
- package/dist/cjs/packages/energy-app-modbus-server.d.cts +122 -0
- package/dist/cjs/types/enyo-inverter-appliance.d.cts +13 -0
- package/dist/cjs/types/enyo-modbus-server.cjs +149 -0
- package/dist/cjs/types/enyo-modbus-server.d.cts +346 -0
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/energy-app-permission.type.d.ts +1 -0
- package/dist/energy-app-permission.type.js +1 -0
- package/dist/energy-app.d.ts +15 -0
- package/dist/energy-app.js +16 -0
- package/dist/enyo-energy-app-sdk.d.ts +3 -0
- package/dist/implementations/modbus/define-modbus-server-register.d.ts +71 -0
- package/dist/implementations/modbus/define-modbus-server-register.js +82 -0
- package/dist/implementations/modbus/modbus-server-validators.d.ts +73 -0
- package/dist/implementations/modbus/modbus-server-validators.js +154 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/packages/energy-app-modbus-server.d.ts +122 -0
- package/dist/packages/energy-app-modbus-server.js +13 -0
- package/dist/types/enyo-inverter-appliance.d.ts +13 -0
- package/dist/types/enyo-modbus-server.d.ts +346 -0
- package/dist/types/enyo-modbus-server.js +144 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -27,6 +27,7 @@ var EnergyAppPermissionTypeEnum;
|
|
|
27
27
|
EnergyAppPermissionTypeEnum["EnergyManager"] = "EnergyManager";
|
|
28
28
|
EnergyAppPermissionTypeEnum["ElectricityTariff"] = "ElectricityTariff";
|
|
29
29
|
EnergyAppPermissionTypeEnum["ModbusRtu"] = "ModbusRtu";
|
|
30
|
+
EnergyAppPermissionTypeEnum["ModbusServer"] = "ModbusServer";
|
|
30
31
|
EnergyAppPermissionTypeEnum["EnergyPrices"] = "EnergyPrices";
|
|
31
32
|
EnergyAppPermissionTypeEnum["WeatherForecastRegister"] = "WeatherForecastRegister";
|
|
32
33
|
EnergyAppPermissionTypeEnum["WeatherForecastUse"] = "WeatherForecastUse";
|
|
@@ -24,6 +24,7 @@ export declare enum EnergyAppPermissionTypeEnum {
|
|
|
24
24
|
EnergyManager = "EnergyManager",
|
|
25
25
|
ElectricityTariff = "ElectricityTariff",
|
|
26
26
|
ModbusRtu = "ModbusRtu",
|
|
27
|
+
ModbusServer = "ModbusServer",
|
|
27
28
|
EnergyPrices = "EnergyPrices",
|
|
28
29
|
WeatherForecastRegister = "WeatherForecastRegister",
|
|
29
30
|
WeatherForecastUse = "WeatherForecastUse",
|
package/dist/cjs/energy-app.cjs
CHANGED
|
@@ -212,6 +212,22 @@ class EnergyApp {
|
|
|
212
212
|
useModbusRtu() {
|
|
213
213
|
return this.energyAppSdk.useModbusRtu();
|
|
214
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Gets the Modbus server API for serving your app's data to Modbus clients.
|
|
217
|
+
*
|
|
218
|
+
* The inverse of {@link EnergyApp.useModbus}: the hub itself answers Modbus
|
|
219
|
+
* requests, and this app supplies the values behind individual registers by
|
|
220
|
+
* registering them with read/write handlers. The listener is host-owned and
|
|
221
|
+
* shared with every other installed app, so address ranges are
|
|
222
|
+
* first-come-first-served across apps.
|
|
223
|
+
*
|
|
224
|
+
* Requires the `ModbusServer` permission.
|
|
225
|
+
*
|
|
226
|
+
* @returns The Modbus server API instance
|
|
227
|
+
*/
|
|
228
|
+
useModbusServer() {
|
|
229
|
+
return this.energyAppSdk.useModbusServer();
|
|
230
|
+
}
|
|
215
231
|
/**
|
|
216
232
|
* Gets the EEbus API for SHIP/SPINE device communication.
|
|
217
233
|
*
|
|
@@ -26,6 +26,7 @@ import { EnergyAppDynamicPriceForecast } from "./packages/energy-app-dynamic-pri
|
|
|
26
26
|
import { EnergyAppPvSystem } from "./packages/energy-app-pv-system.cjs";
|
|
27
27
|
import { EnergyAppSequenceGenerator } from "./packages/energy-app-sequence-generator.cjs";
|
|
28
28
|
import { EnergyAppModbusRtu } from "./packages/energy-app-modbus-rtu.cjs";
|
|
29
|
+
import { EnergyAppModbusServer } from "./packages/energy-app-modbus-server.cjs";
|
|
29
30
|
import { EnergyAppEebus } from "./packages/energy-app-eebus.cjs";
|
|
30
31
|
import { EnergyAppMqtt } from "./packages/energy-app-mqtt.cjs";
|
|
31
32
|
import { EnergyAppBluetooth } from "./packages/energy-app-bluetooth.cjs";
|
|
@@ -171,6 +172,20 @@ export declare class EnergyApp implements EnyoEnergyAppSdk {
|
|
|
171
172
|
* @returns The Modbus RTU API instance
|
|
172
173
|
*/
|
|
173
174
|
useModbusRtu(): EnergyAppModbusRtu;
|
|
175
|
+
/**
|
|
176
|
+
* Gets the Modbus server API for serving your app's data to Modbus clients.
|
|
177
|
+
*
|
|
178
|
+
* The inverse of {@link EnergyApp.useModbus}: the hub itself answers Modbus
|
|
179
|
+
* requests, and this app supplies the values behind individual registers by
|
|
180
|
+
* registering them with read/write handlers. The listener is host-owned and
|
|
181
|
+
* shared with every other installed app, so address ranges are
|
|
182
|
+
* first-come-first-served across apps.
|
|
183
|
+
*
|
|
184
|
+
* Requires the `ModbusServer` permission.
|
|
185
|
+
*
|
|
186
|
+
* @returns The Modbus server API instance
|
|
187
|
+
*/
|
|
188
|
+
useModbusServer(): EnergyAppModbusServer;
|
|
174
189
|
/**
|
|
175
190
|
* Gets the EEbus API for SHIP/SPINE device communication.
|
|
176
191
|
*
|
|
@@ -25,6 +25,7 @@ import { EnergyAppDynamicPriceForecast } from "./packages/energy-app-dynamic-pri
|
|
|
25
25
|
import { EnergyAppPvSystem } from "./packages/energy-app-pv-system.cjs";
|
|
26
26
|
import { EnergyAppSequenceGenerator } from "./packages/energy-app-sequence-generator.cjs";
|
|
27
27
|
import { EnergyAppModbusRtu } from "./packages/energy-app-modbus-rtu.cjs";
|
|
28
|
+
import { EnergyAppModbusServer } from "./packages/energy-app-modbus-server.cjs";
|
|
28
29
|
import { EnergyAppEebus } from "./packages/energy-app-eebus.cjs";
|
|
29
30
|
import { EnergyAppMqtt } from "./packages/energy-app-mqtt.cjs";
|
|
30
31
|
import { EnergyAppBluetooth } from "./packages/energy-app-bluetooth.cjs";
|
|
@@ -123,6 +124,8 @@ export interface EnyoEnergyAppSdk {
|
|
|
123
124
|
useSequenceGenerator: () => EnergyAppSequenceGenerator;
|
|
124
125
|
/** Get the Modbus RTU serial communication API */
|
|
125
126
|
useModbusRtu: () => EnergyAppModbusRtu;
|
|
127
|
+
/** Get the Modbus server API for serving registers to Modbus clients */
|
|
128
|
+
useModbusServer: () => EnergyAppModbusServer;
|
|
126
129
|
/** Get the EEbus SHIP/SPINE communication API for device pairing, data access, and power management */
|
|
127
130
|
useEebus: () => EnergyAppEebus;
|
|
128
131
|
/** Get the MQTT communication API for connecting to internal or external MQTT brokers */
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Ergonomic authoring helpers for {@link EnyoModbusServerRegistration}s.
|
|
4
|
+
*
|
|
5
|
+
* These exist for one concrete reason: type inference. A registration's handler
|
|
6
|
+
* signatures depend on its `dataType` — a `float32` register reads a `number`, a
|
|
7
|
+
* `string` register reads a `string` — but that link only holds when TypeScript
|
|
8
|
+
* can *infer* the data type from the literal. Annotating an object literal as
|
|
9
|
+
* {@link EnyoModbusServerRegistration} defeats that: the generic falls back to
|
|
10
|
+
* the full data-type union and every handler widens to `string | number`.
|
|
11
|
+
*
|
|
12
|
+
* Building registers through these factories keeps the link intact, so a handler
|
|
13
|
+
* returning the wrong shape is a compile error rather than a runtime decode
|
|
14
|
+
* failure. They also fill in `space`, which is the one field that is pure
|
|
15
|
+
* boilerplate.
|
|
16
|
+
*
|
|
17
|
+
* Mirrors the SDK's `onboardingV2Block` pattern.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.modbusServerRegister = void 0;
|
|
21
|
+
const enyo_modbus_server_js_1 = require("../../types/enyo-modbus-server.cjs");
|
|
22
|
+
/**
|
|
23
|
+
* Typed factories for each Modbus address space. Each fills in `space` and
|
|
24
|
+
* returns the corresponding registration, inferring the word data type from the
|
|
25
|
+
* supplied `dataType` so handler values stay exact.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* const registers = [
|
|
30
|
+
* modbusServerRegister.holding({
|
|
31
|
+
* address: 40071,
|
|
32
|
+
* key: 'active-power-w',
|
|
33
|
+
* name: [{language: 'en', value: 'Active power'}],
|
|
34
|
+
* unit: 'W',
|
|
35
|
+
* dataType: 'float32',
|
|
36
|
+
* onRead: () => currentPowerW, // must be a number
|
|
37
|
+
* onWrite: (value) => applyLimit(value),
|
|
38
|
+
* }),
|
|
39
|
+
* modbusServerRegister.input({
|
|
40
|
+
* address: 40080,
|
|
41
|
+
* key: 'serial',
|
|
42
|
+
* name: [{language: 'en', value: 'Serial number'}],
|
|
43
|
+
* dataType: 'string',
|
|
44
|
+
* quantity: 8,
|
|
45
|
+
* onRead: () => deviceSerial, // must be a string
|
|
46
|
+
* }),
|
|
47
|
+
* ];
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
exports.modbusServerRegister = {
|
|
51
|
+
/**
|
|
52
|
+
* A read/write single-bit coil.
|
|
53
|
+
* @param registration - Everything but `space`; omit `onWrite` for a read-only coil.
|
|
54
|
+
*/
|
|
55
|
+
coil: (registration) => ({
|
|
56
|
+
...registration,
|
|
57
|
+
space: enyo_modbus_server_js_1.EnyoModbusServerRegisterSpace.Coil,
|
|
58
|
+
}),
|
|
59
|
+
/**
|
|
60
|
+
* A read-only single-bit discrete input.
|
|
61
|
+
* @param registration - Everything but `space`.
|
|
62
|
+
*/
|
|
63
|
+
discreteInput: (registration) => ({
|
|
64
|
+
...registration,
|
|
65
|
+
space: enyo_modbus_server_js_1.EnyoModbusServerRegisterSpace.DiscreteInput,
|
|
66
|
+
}),
|
|
67
|
+
/**
|
|
68
|
+
* A read/write 16-bit holding register.
|
|
69
|
+
* @param registration - Everything but `space`. The `dataType` fixes the
|
|
70
|
+
* handler value types; omit `onWrite` for a read-only holding register.
|
|
71
|
+
*/
|
|
72
|
+
holding: (registration) => ({
|
|
73
|
+
...registration,
|
|
74
|
+
space: enyo_modbus_server_js_1.EnyoModbusServerRegisterSpace.Holding,
|
|
75
|
+
}),
|
|
76
|
+
/**
|
|
77
|
+
* A read-only 16-bit input register.
|
|
78
|
+
* @param registration - Everything but `space`. The `dataType` fixes the
|
|
79
|
+
* handler value type.
|
|
80
|
+
*/
|
|
81
|
+
input: (registration) => ({
|
|
82
|
+
...registration,
|
|
83
|
+
space: enyo_modbus_server_js_1.EnyoModbusServerRegisterSpace.Input,
|
|
84
|
+
}),
|
|
85
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ergonomic authoring helpers for {@link EnyoModbusServerRegistration}s.
|
|
3
|
+
*
|
|
4
|
+
* These exist for one concrete reason: type inference. A registration's handler
|
|
5
|
+
* signatures depend on its `dataType` — a `float32` register reads a `number`, a
|
|
6
|
+
* `string` register reads a `string` — but that link only holds when TypeScript
|
|
7
|
+
* can *infer* the data type from the literal. Annotating an object literal as
|
|
8
|
+
* {@link EnyoModbusServerRegistration} defeats that: the generic falls back to
|
|
9
|
+
* the full data-type union and every handler widens to `string | number`.
|
|
10
|
+
*
|
|
11
|
+
* Building registers through these factories keeps the link intact, so a handler
|
|
12
|
+
* returning the wrong shape is a compile error rather than a runtime decode
|
|
13
|
+
* failure. They also fill in `space`, which is the one field that is pure
|
|
14
|
+
* boilerplate.
|
|
15
|
+
*
|
|
16
|
+
* Mirrors the SDK's `onboardingV2Block` pattern.
|
|
17
|
+
*/
|
|
18
|
+
import { type EnyoModbusServerCoilRegistration, type EnyoModbusServerDiscreteInputRegistration, type EnyoModbusServerHoldingRegistration, type EnyoModbusServerInputRegistration } from '../../types/enyo-modbus-server.cjs';
|
|
19
|
+
import type { EnergyAppModbusDataType } from './interfaces.cjs';
|
|
20
|
+
/**
|
|
21
|
+
* Typed factories for each Modbus address space. Each fills in `space` and
|
|
22
|
+
* returns the corresponding registration, inferring the word data type from the
|
|
23
|
+
* supplied `dataType` so handler values stay exact.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const registers = [
|
|
28
|
+
* modbusServerRegister.holding({
|
|
29
|
+
* address: 40071,
|
|
30
|
+
* key: 'active-power-w',
|
|
31
|
+
* name: [{language: 'en', value: 'Active power'}],
|
|
32
|
+
* unit: 'W',
|
|
33
|
+
* dataType: 'float32',
|
|
34
|
+
* onRead: () => currentPowerW, // must be a number
|
|
35
|
+
* onWrite: (value) => applyLimit(value),
|
|
36
|
+
* }),
|
|
37
|
+
* modbusServerRegister.input({
|
|
38
|
+
* address: 40080,
|
|
39
|
+
* key: 'serial',
|
|
40
|
+
* name: [{language: 'en', value: 'Serial number'}],
|
|
41
|
+
* dataType: 'string',
|
|
42
|
+
* quantity: 8,
|
|
43
|
+
* onRead: () => deviceSerial, // must be a string
|
|
44
|
+
* }),
|
|
45
|
+
* ];
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare const modbusServerRegister: {
|
|
49
|
+
/**
|
|
50
|
+
* A read/write single-bit coil.
|
|
51
|
+
* @param registration - Everything but `space`; omit `onWrite` for a read-only coil.
|
|
52
|
+
*/
|
|
53
|
+
coil: (registration: Omit<EnyoModbusServerCoilRegistration, "space">) => EnyoModbusServerCoilRegistration;
|
|
54
|
+
/**
|
|
55
|
+
* A read-only single-bit discrete input.
|
|
56
|
+
* @param registration - Everything but `space`.
|
|
57
|
+
*/
|
|
58
|
+
discreteInput: (registration: Omit<EnyoModbusServerDiscreteInputRegistration, "space">) => EnyoModbusServerDiscreteInputRegistration;
|
|
59
|
+
/**
|
|
60
|
+
* A read/write 16-bit holding register.
|
|
61
|
+
* @param registration - Everything but `space`. The `dataType` fixes the
|
|
62
|
+
* handler value types; omit `onWrite` for a read-only holding register.
|
|
63
|
+
*/
|
|
64
|
+
holding: <D extends EnergyAppModbusDataType>(registration: Omit<EnyoModbusServerHoldingRegistration<D>, "space">) => EnyoModbusServerHoldingRegistration<D>;
|
|
65
|
+
/**
|
|
66
|
+
* A read-only 16-bit input register.
|
|
67
|
+
* @param registration - Everything but `space`. The `dataType` fixes the
|
|
68
|
+
* handler value type.
|
|
69
|
+
*/
|
|
70
|
+
input: <D extends EnergyAppModbusDataType>(registration: Omit<EnyoModbusServerInputRegistration<D>, "space">) => EnyoModbusServerInputRegistration<D>;
|
|
71
|
+
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Client-side validator for {@link EnyoModbusServerRegistration} lists.
|
|
4
|
+
*
|
|
5
|
+
* The host rejects a conflicting registration at call time by throwing
|
|
6
|
+
* {@link EnyoModbusServerRegistrationConflictError}, which is the authoritative
|
|
7
|
+
* check but a poor way to find out your own register map overlaps itself. This
|
|
8
|
+
* validator catches everything that is knowable without the host — self-overlap,
|
|
9
|
+
* duplicate keys, missing metadata — so an app can fail fast at startup with all
|
|
10
|
+
* the problems at once instead of one throw at a time.
|
|
11
|
+
*
|
|
12
|
+
* It cannot see other apps' registrations, so a clean result here does **not**
|
|
13
|
+
* guarantee registration will succeed.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.ModbusServerValidationError = void 0;
|
|
17
|
+
exports.modbusServerRegisterSpan = modbusServerRegisterSpan;
|
|
18
|
+
exports.validateModbusServerRegistrations = validateModbusServerRegistrations;
|
|
19
|
+
exports.assertValidModbusServerRegistrations = assertValidModbusServerRegistrations;
|
|
20
|
+
const enyo_modbus_server_js_1 = require("../../types/enyo-modbus-server.cjs");
|
|
21
|
+
/**
|
|
22
|
+
* Thrown by {@link assertValidModbusServerRegistrations} when a register map
|
|
23
|
+
* fails validation. The message lists every blocking error.
|
|
24
|
+
*/
|
|
25
|
+
class ModbusServerValidationError extends Error {
|
|
26
|
+
/** The individual blocking errors that caused the failure. */
|
|
27
|
+
errors;
|
|
28
|
+
/**
|
|
29
|
+
* @param errors - The blocking validation errors.
|
|
30
|
+
*/
|
|
31
|
+
constructor(errors) {
|
|
32
|
+
super(`Invalid Modbus server register map:\n- ${errors.join('\n- ')}`);
|
|
33
|
+
this.name = 'ModbusServerValidationError';
|
|
34
|
+
this.errors = errors;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.ModbusServerValidationError = ModbusServerValidationError;
|
|
38
|
+
/** Word counts for the data types whose span is fixed. */
|
|
39
|
+
const WORDS_BY_DATA_TYPE = {
|
|
40
|
+
uint16: 1,
|
|
41
|
+
int16: 1,
|
|
42
|
+
acc16: 1,
|
|
43
|
+
uint32: 2,
|
|
44
|
+
int32: 2,
|
|
45
|
+
float32: 2,
|
|
46
|
+
acc32: 2,
|
|
47
|
+
};
|
|
48
|
+
/** The two spaces addressed in bits rather than 16-bit words. */
|
|
49
|
+
const BIT_SPACES = new Set([
|
|
50
|
+
enyo_modbus_server_js_1.EnyoModbusServerRegisterSpace.Coil,
|
|
51
|
+
enyo_modbus_server_js_1.EnyoModbusServerRegisterSpace.DiscreteInput,
|
|
52
|
+
]);
|
|
53
|
+
/**
|
|
54
|
+
* How many words (or bits) a registration occupies, starting at its `address`.
|
|
55
|
+
*
|
|
56
|
+
* Bit spaces always occupy one. Word spaces derive their span from `dataType`,
|
|
57
|
+
* except `'string'`, which needs an explicit `quantity` — when that is missing
|
|
58
|
+
* the span is unknowable and this returns `undefined` so the caller can report
|
|
59
|
+
* it rather than guess.
|
|
60
|
+
*
|
|
61
|
+
* @param registration - The registration to measure.
|
|
62
|
+
* @returns The span, or `undefined` when it cannot be derived.
|
|
63
|
+
*/
|
|
64
|
+
function modbusServerRegisterSpan(registration) {
|
|
65
|
+
if (BIT_SPACES.has(registration.space))
|
|
66
|
+
return 1;
|
|
67
|
+
const { dataType, quantity } = registration;
|
|
68
|
+
if (dataType === 'string') {
|
|
69
|
+
return quantity && quantity > 0 ? quantity : undefined;
|
|
70
|
+
}
|
|
71
|
+
return WORDS_BY_DATA_TYPE[dataType];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Checks a register map for problems that are knowable without the host.
|
|
75
|
+
*
|
|
76
|
+
* Reports as errors: overlapping ranges within the same space, duplicate `key`s,
|
|
77
|
+
* a missing or empty `name`, a `'string'` register without `quantity`, an
|
|
78
|
+
* unknown `dataType`, and a negative address. Reports as warnings: `scale` on a
|
|
79
|
+
* string register (which the host ignores) and a `unit` on a coil or discrete
|
|
80
|
+
* input (a bit has no unit).
|
|
81
|
+
*
|
|
82
|
+
* Two registrations only conflict within the same space — holding 40071 and
|
|
83
|
+
* input 40071 are different addresses and both are fine.
|
|
84
|
+
*
|
|
85
|
+
* @param registrations - The register map to check.
|
|
86
|
+
* @returns The {@link ModbusServerValidationResult}.
|
|
87
|
+
*/
|
|
88
|
+
function validateModbusServerRegistrations(registrations) {
|
|
89
|
+
const errors = [];
|
|
90
|
+
const warnings = [];
|
|
91
|
+
if (!registrations?.length) {
|
|
92
|
+
return { ok: true, errors, warnings };
|
|
93
|
+
}
|
|
94
|
+
const keys = new Set();
|
|
95
|
+
/** Claimed ranges per space, so the overlap check never crosses spaces. */
|
|
96
|
+
const claimed = new Map();
|
|
97
|
+
for (const [i, registration] of registrations.entries()) {
|
|
98
|
+
const at = `registrations[${i}] (${registration.key || '?'})`;
|
|
99
|
+
if (!registration.key?.trim()) {
|
|
100
|
+
errors.push(`${at}: key is required.`);
|
|
101
|
+
}
|
|
102
|
+
else if (keys.has(registration.key)) {
|
|
103
|
+
errors.push(`${at}: duplicate key "${registration.key}".`);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
keys.add(registration.key);
|
|
107
|
+
}
|
|
108
|
+
if (!registration.name?.length) {
|
|
109
|
+
errors.push(`${at}: name is required — it is what an installer sees in the register map.`);
|
|
110
|
+
}
|
|
111
|
+
if (!Number.isInteger(registration.address) || registration.address < 0) {
|
|
112
|
+
errors.push(`${at}: address must be a non-negative integer.`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (BIT_SPACES.has(registration.space) && registration.unit) {
|
|
116
|
+
warnings.push(`${at}: unit "${registration.unit}" on a ${registration.space} is meaningless — a bit has no unit.`);
|
|
117
|
+
}
|
|
118
|
+
const span = modbusServerRegisterSpan(registration);
|
|
119
|
+
if (span === undefined) {
|
|
120
|
+
const { dataType } = registration;
|
|
121
|
+
if (dataType === 'string') {
|
|
122
|
+
errors.push(`${at}: dataType "string" needs a positive quantity — its length cannot be derived.`);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
errors.push(`${at}: unknown dataType "${dataType}".`);
|
|
126
|
+
}
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (!BIT_SPACES.has(registration.space)) {
|
|
130
|
+
const { dataType, scale } = registration;
|
|
131
|
+
if (dataType === 'string' && scale !== undefined) {
|
|
132
|
+
warnings.push(`${at}: scale is ignored on a string register.`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const from = registration.address;
|
|
136
|
+
const to = from + span - 1;
|
|
137
|
+
const inSpace = claimed.get(registration.space) ?? [];
|
|
138
|
+
const clash = inSpace.find((r) => from <= r.to && to >= r.from);
|
|
139
|
+
if (clash) {
|
|
140
|
+
errors.push(`${at}: ${registration.space} range ${from}..${to} overlaps ${clash.at} (${clash.from}..${clash.to}).`);
|
|
141
|
+
}
|
|
142
|
+
inSpace.push({ from, to, at });
|
|
143
|
+
claimed.set(registration.space, inSpace);
|
|
144
|
+
}
|
|
145
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Like {@link validateModbusServerRegistrations}, but throws
|
|
149
|
+
* {@link ModbusServerValidationError} when there are blocking errors. Warnings
|
|
150
|
+
* never throw; the validated list is returned on success for chaining.
|
|
151
|
+
*
|
|
152
|
+
* @param registrations - The register map to check.
|
|
153
|
+
* @returns The same list when it has no blocking errors.
|
|
154
|
+
* @throws {ModbusServerValidationError} When validation produces any error.
|
|
155
|
+
*/
|
|
156
|
+
function assertValidModbusServerRegistrations(registrations) {
|
|
157
|
+
const { ok, errors } = validateModbusServerRegistrations(registrations);
|
|
158
|
+
if (!ok)
|
|
159
|
+
throw new ModbusServerValidationError(errors);
|
|
160
|
+
return registrations;
|
|
161
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side validator for {@link EnyoModbusServerRegistration} lists.
|
|
3
|
+
*
|
|
4
|
+
* The host rejects a conflicting registration at call time by throwing
|
|
5
|
+
* {@link EnyoModbusServerRegistrationConflictError}, which is the authoritative
|
|
6
|
+
* check but a poor way to find out your own register map overlaps itself. This
|
|
7
|
+
* validator catches everything that is knowable without the host — self-overlap,
|
|
8
|
+
* duplicate keys, missing metadata — so an app can fail fast at startup with all
|
|
9
|
+
* the problems at once instead of one throw at a time.
|
|
10
|
+
*
|
|
11
|
+
* It cannot see other apps' registrations, so a clean result here does **not**
|
|
12
|
+
* guarantee registration will succeed.
|
|
13
|
+
*/
|
|
14
|
+
import { type EnyoModbusServerRegistration } from '../../types/enyo-modbus-server.cjs';
|
|
15
|
+
/** The outcome of validating a list of registrations. */
|
|
16
|
+
export interface ModbusServerValidationResult {
|
|
17
|
+
/** True when there are no blocking `errors` (warnings are still allowed). */
|
|
18
|
+
ok: boolean;
|
|
19
|
+
/** Blocking problems — registration will fail or serve wrong data. */
|
|
20
|
+
errors: string[];
|
|
21
|
+
/** Advisory problems — allowed, but usually worth fixing. */
|
|
22
|
+
warnings: string[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Thrown by {@link assertValidModbusServerRegistrations} when a register map
|
|
26
|
+
* fails validation. The message lists every blocking error.
|
|
27
|
+
*/
|
|
28
|
+
export declare class ModbusServerValidationError extends Error {
|
|
29
|
+
/** The individual blocking errors that caused the failure. */
|
|
30
|
+
readonly errors: string[];
|
|
31
|
+
/**
|
|
32
|
+
* @param errors - The blocking validation errors.
|
|
33
|
+
*/
|
|
34
|
+
constructor(errors: string[]);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* How many words (or bits) a registration occupies, starting at its `address`.
|
|
38
|
+
*
|
|
39
|
+
* Bit spaces always occupy one. Word spaces derive their span from `dataType`,
|
|
40
|
+
* except `'string'`, which needs an explicit `quantity` — when that is missing
|
|
41
|
+
* the span is unknowable and this returns `undefined` so the caller can report
|
|
42
|
+
* it rather than guess.
|
|
43
|
+
*
|
|
44
|
+
* @param registration - The registration to measure.
|
|
45
|
+
* @returns The span, or `undefined` when it cannot be derived.
|
|
46
|
+
*/
|
|
47
|
+
export declare function modbusServerRegisterSpan(registration: EnyoModbusServerRegistration): number | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Checks a register map for problems that are knowable without the host.
|
|
50
|
+
*
|
|
51
|
+
* Reports as errors: overlapping ranges within the same space, duplicate `key`s,
|
|
52
|
+
* a missing or empty `name`, a `'string'` register without `quantity`, an
|
|
53
|
+
* unknown `dataType`, and a negative address. Reports as warnings: `scale` on a
|
|
54
|
+
* string register (which the host ignores) and a `unit` on a coil or discrete
|
|
55
|
+
* input (a bit has no unit).
|
|
56
|
+
*
|
|
57
|
+
* Two registrations only conflict within the same space — holding 40071 and
|
|
58
|
+
* input 40071 are different addresses and both are fine.
|
|
59
|
+
*
|
|
60
|
+
* @param registrations - The register map to check.
|
|
61
|
+
* @returns The {@link ModbusServerValidationResult}.
|
|
62
|
+
*/
|
|
63
|
+
export declare function validateModbusServerRegistrations(registrations: EnyoModbusServerRegistration[]): ModbusServerValidationResult;
|
|
64
|
+
/**
|
|
65
|
+
* Like {@link validateModbusServerRegistrations}, but throws
|
|
66
|
+
* {@link ModbusServerValidationError} when there are blocking errors. Warnings
|
|
67
|
+
* never throw; the validated list is returned on success for chaining.
|
|
68
|
+
*
|
|
69
|
+
* @param registrations - The register map to check.
|
|
70
|
+
* @returns The same list when it has no blocking errors.
|
|
71
|
+
* @throws {ModbusServerValidationError} When validation produces any error.
|
|
72
|
+
*/
|
|
73
|
+
export declare function assertValidModbusServerRegistrations(registrations: EnyoModbusServerRegistration[]): EnyoModbusServerRegistration[];
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -54,6 +54,10 @@ __exportStar(require("./types/enyo-currency.cjs"), exports);
|
|
|
54
54
|
__exportStar(require("./packages/energy-app-sequence-generator.cjs"), exports);
|
|
55
55
|
__exportStar(require("./packages/energy-app-energy-prices.cjs"), exports);
|
|
56
56
|
__exportStar(require("./packages/energy-app-modbus-rtu.cjs"), exports);
|
|
57
|
+
__exportStar(require("./types/enyo-modbus-server.cjs"), exports);
|
|
58
|
+
__exportStar(require("./packages/energy-app-modbus-server.cjs"), exports);
|
|
59
|
+
__exportStar(require("./implementations/modbus/define-modbus-server-register.cjs"), exports);
|
|
60
|
+
__exportStar(require("./implementations/modbus/modbus-server-validators.cjs"), exports);
|
|
57
61
|
__exportStar(require("./types/enyo-spine.cjs"), exports);
|
|
58
62
|
__exportStar(require("./types/enyo-eebus.cjs"), exports);
|
|
59
63
|
__exportStar(require("./types/enyo-eebus-use-cases.cjs"), exports);
|
package/dist/cjs/index.d.cts
CHANGED
|
@@ -38,6 +38,10 @@ export * from './types/enyo-currency.cjs';
|
|
|
38
38
|
export * from './packages/energy-app-sequence-generator.cjs';
|
|
39
39
|
export * from './packages/energy-app-energy-prices.cjs';
|
|
40
40
|
export * from './packages/energy-app-modbus-rtu.cjs';
|
|
41
|
+
export * from './types/enyo-modbus-server.cjs';
|
|
42
|
+
export * from './packages/energy-app-modbus-server.cjs';
|
|
43
|
+
export * from './implementations/modbus/define-modbus-server-register.cjs';
|
|
44
|
+
export * from './implementations/modbus/modbus-server-validators.cjs';
|
|
41
45
|
export * from './types/enyo-spine.cjs';
|
|
42
46
|
export * from './types/enyo-eebus.cjs';
|
|
43
47
|
export * from './types/enyo-eebus-use-cases.cjs';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The **Modbus server** package: serve your app's data to Modbus clients on the
|
|
4
|
+
* hub's own listener.
|
|
5
|
+
*
|
|
6
|
+
* This is the inverse of {@link EnergyAppModbus}. There, the app is a client
|
|
7
|
+
* polling somebody else's device. Here, the hub *is* the device: a third-party
|
|
8
|
+
* energy manager, a building-automation controller or an installer's diagnostic
|
|
9
|
+
* tool connects to the hub, and the values it reads come from handlers this app
|
|
10
|
+
* registers.
|
|
11
|
+
*
|
|
12
|
+
* Requires the `ModbusServer` permission.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The **Modbus server** package: serve your app's data to Modbus clients on the
|
|
3
|
+
* hub's own listener.
|
|
4
|
+
*
|
|
5
|
+
* This is the inverse of {@link EnergyAppModbus}. There, the app is a client
|
|
6
|
+
* polling somebody else's device. Here, the hub *is* the device: a third-party
|
|
7
|
+
* energy manager, a building-automation controller or an installer's diagnostic
|
|
8
|
+
* tool connects to the hub, and the values it reads come from handlers this app
|
|
9
|
+
* registers.
|
|
10
|
+
*
|
|
11
|
+
* Requires the `ModbusServer` permission.
|
|
12
|
+
*/
|
|
13
|
+
import type { EnergyAppModbusDataType } from '../implementations/modbus/interfaces.cjs';
|
|
14
|
+
import type { EnyoModbusServerConfig, EnyoModbusServerRegistration, EnyoModbusServerRegistrationHandle, EnyoModbusServerRegistrationOf } from '../types/enyo-modbus-server.cjs';
|
|
15
|
+
/**
|
|
16
|
+
* Interface for serving Modbus registers from an enyo package.
|
|
17
|
+
*
|
|
18
|
+
* **The server is shared.** One host-owned TCP listener, one unit id, and every
|
|
19
|
+
* installed app carving up the same address space. Two consequences an app
|
|
20
|
+
* author has to design around:
|
|
21
|
+
*
|
|
22
|
+
* 1. Address ranges are first-come, first-served across *all* apps, so
|
|
23
|
+
* registration can fail with
|
|
24
|
+
* {@link EnyoModbusServerRegistrationConflictError} through no fault of your
|
|
25
|
+
* own. Handle it at runtime; do not assume startup registration succeeds.
|
|
26
|
+
* 2. The unit id from {@link getModbusServerConfig} does not identify your app.
|
|
27
|
+
* A client addressing it reaches the whole hub, not your package.
|
|
28
|
+
*
|
|
29
|
+
* Register metadata (translated name, description, unit) never travels over
|
|
30
|
+
* Modbus — the protocol carries bare words. It exists so the host can show the
|
|
31
|
+
* hub's register map and generate installer documentation.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* const server = energyApp.useModbusServer();
|
|
36
|
+
* const {addresses, port, unitId} = await server.getModbusServerConfig();
|
|
37
|
+
* console.log(`Reachable at ${addresses.join(', ')}:${port}, unit ${unitId}`);
|
|
38
|
+
*
|
|
39
|
+
* const handle = await server.registerRegister(modbusServerRegister.holding({
|
|
40
|
+
* address: 40071,
|
|
41
|
+
* key: 'active-power-w',
|
|
42
|
+
* name: [
|
|
43
|
+
* {language: 'de', value: 'Momentanleistung'},
|
|
44
|
+
* {language: 'en', value: 'Active power'},
|
|
45
|
+
* ],
|
|
46
|
+
* description: [
|
|
47
|
+
* {language: 'de', value: 'Aktuelle Wirkleistung der Anlage.'},
|
|
48
|
+
* {language: 'en', value: 'Current active power of the plant.'},
|
|
49
|
+
* ],
|
|
50
|
+
* unit: 'W',
|
|
51
|
+
* dataType: 'float32', // claims 40071..40072
|
|
52
|
+
* onRead: async () => currentPowerW,
|
|
53
|
+
* onWrite: async (value) => applyPowerLimit(value),
|
|
54
|
+
* }));
|
|
55
|
+
*
|
|
56
|
+
* // later, on shutdown
|
|
57
|
+
* await handle.unregister();
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export interface EnergyAppModbusServer {
|
|
61
|
+
/**
|
|
62
|
+
* Returns where the hub's Modbus server can be reached — addresses, port and
|
|
63
|
+
* the shared unit id.
|
|
64
|
+
*
|
|
65
|
+
* Everything returned is host-owned and read-only. Use it to tell an
|
|
66
|
+
* installer where to point their client; prefer showing
|
|
67
|
+
* {@link EnyoModbusServerConfig.addresses} in full over guessing which
|
|
68
|
+
* interface they are on.
|
|
69
|
+
*
|
|
70
|
+
* @returns The current server configuration.
|
|
71
|
+
*/
|
|
72
|
+
getModbusServerConfig(): Promise<EnyoModbusServerConfig>;
|
|
73
|
+
/**
|
|
74
|
+
* Claims one address range and serves it from the supplied handlers.
|
|
75
|
+
*
|
|
76
|
+
* The range claimed runs from `address` for as many words as the
|
|
77
|
+
* registration's `dataType` implies (1 for `uint16`, 2 for `float32`,
|
|
78
|
+
* `quantity` for `string`, 1 bit for a coil or discrete input). Any overlap
|
|
79
|
+
* with an existing registration — yours or another app's — fails the whole
|
|
80
|
+
* call; nothing partial is claimed.
|
|
81
|
+
*
|
|
82
|
+
* @param registration - The register to serve, with its metadata and handlers.
|
|
83
|
+
* @returns A handle for releasing the range again.
|
|
84
|
+
* @throws {EnyoModbusServerRegistrationConflictError} If any word of the
|
|
85
|
+
* range is already registered. Check `heldByCaller` to tell your own
|
|
86
|
+
* double-registration from another app holding the address.
|
|
87
|
+
*/
|
|
88
|
+
registerRegister<D extends EnergyAppModbusDataType>(registration: EnyoModbusServerRegistrationOf<D>): Promise<EnyoModbusServerRegistrationHandle>;
|
|
89
|
+
/**
|
|
90
|
+
* Claims several address ranges at once, atomically.
|
|
91
|
+
*
|
|
92
|
+
* Either every registration succeeds or none does — a conflict partway
|
|
93
|
+
* through the list leaves no ranges claimed. Prefer this over a loop of
|
|
94
|
+
* {@link registerRegister} when registering a device's whole register map,
|
|
95
|
+
* so a mid-list conflict cannot strand you with half a map published.
|
|
96
|
+
*
|
|
97
|
+
* @param registrations - The registers to serve.
|
|
98
|
+
* @returns One handle per registration, in the order supplied.
|
|
99
|
+
* @throws {EnyoModbusServerRegistrationConflictError} If any range in the
|
|
100
|
+
* list conflicts, with the details of the first conflict found. Overlaps
|
|
101
|
+
* *within* the supplied list are reported the same way, with
|
|
102
|
+
* `heldByCaller` true.
|
|
103
|
+
*/
|
|
104
|
+
registerRegisters(registrations: EnyoModbusServerRegistration[]): Promise<EnyoModbusServerRegistrationHandle[]>;
|
|
105
|
+
/**
|
|
106
|
+
* Lists the registrations this app currently holds.
|
|
107
|
+
*
|
|
108
|
+
* Scoped to the calling app — registrations belonging to other apps are
|
|
109
|
+
* never visible, even though they share the address space.
|
|
110
|
+
*
|
|
111
|
+
* @returns The live handles, in no guaranteed order.
|
|
112
|
+
*/
|
|
113
|
+
getRegisteredRegisters(): Promise<EnyoModbusServerRegistrationHandle[]>;
|
|
114
|
+
/**
|
|
115
|
+
* Releases every range this app holds.
|
|
116
|
+
*
|
|
117
|
+
* Idempotent, and safe to call when nothing is registered. Worth calling on
|
|
118
|
+
* shutdown: ranges stay claimed while the app is installed, so a stale
|
|
119
|
+
* registration blocks a later re-registration of the same address.
|
|
120
|
+
*/
|
|
121
|
+
unregisterAll(): Promise<void>;
|
|
122
|
+
}
|
|
@@ -42,4 +42,17 @@ export interface EnyoInverterApplianceMetadata {
|
|
|
42
42
|
* `undefined` means "not configured" and should be treated as `false`.
|
|
43
43
|
*/
|
|
44
44
|
blockFeedInOnNegativePrices?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Whether the energy manager may actively steer this inverter (e.g. curtail
|
|
47
|
+
* production). When omitted, consumers fall back to their configured
|
|
48
|
+
* default behaviour — same contract as the SDK's own `controlAllowed`.
|
|
49
|
+
*/
|
|
50
|
+
controlAllowed?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Peak power of the PV modules attached to this inverter, in Watt-peak, as
|
|
53
|
+
* stated on the Anlagenpass. Distinct from `maxPvProductionW`, which is
|
|
54
|
+
* what the inverter itself can put out on the AC side; together with
|
|
55
|
+
* `moduleGroups` it also gives the power of a single module.
|
|
56
|
+
*/
|
|
57
|
+
installedPeakPowerWp?: number;
|
|
45
58
|
}
|