v2c-any 0.5.3 → 0.6.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/README.md +35 -34
- package/dist/device/shelly-pro-em/em1-status-provider.js +24 -10
- package/dist/device/shelly-pro-em/energy-information-em1-notify-status-adapter.js +29 -0
- package/dist/device/shelly-pro-em/energy-information-em1-status-adapter.js +11 -0
- package/dist/factory/mqtt-pull-executable-service-factory.js +4 -4
- package/dist/factory/mqtt-push-executable-service-factory.js +6 -6
- package/dist/provider/asymmetric-ema-provider.js +21 -2
- package/dist/provider/circuit-breaker-provider.js +13 -11
- package/dist/provider/retryable-provider.js +1 -1
- package/dist/schema/mqtt-configuration.js +3 -1
- package/dist/schema/rest-configuration.js +3 -1
- package/dist/service/rest-service.js +7 -0
- package/dist/utils/mqtt.js +1 -0
- package/dist/utils/resiliance.js +16 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
<!-- markdownlint-disable MD041 -->
|
|
1
2
|

|
|
3
|
+
<!-- markdownlint-enable MD041 -->
|
|
2
4
|
|
|
3
5
|

|
|
4
6
|

|
|
@@ -15,18 +17,18 @@
|
|
|
15
17
|
|
|
16
18
|
**v2c-any** (binary: `v2ca`) is a universal adapter that allows **any device** — physical meters, MQTT topics, simulators, or proxies — to integrate with **V2C wallboxes** for **Dynamic Power Control**.
|
|
17
19
|
|
|
18
|
-
If it can expose power data, **v2c-any** can make it speak
|
|
20
|
+
If it can expose power data, **v2c-any** can make it speak _V2C_.
|
|
19
21
|
|
|
20
22
|
## Why v2c-any?
|
|
21
23
|
|
|
22
24
|
V2C wallboxes support Dynamic Power Control via specific meters or MQTT inputs.
|
|
23
25
|
In real installations, however, power data often comes from **heterogeneous sources**:
|
|
24
26
|
|
|
25
|
-
- Different brands of energy meters
|
|
26
|
-
- Existing MQTT infrastructures
|
|
27
|
-
- Home Assistant sensors
|
|
28
|
-
- Custom hardware or software systems
|
|
29
|
-
- Simulated or virtual meters for testing
|
|
27
|
+
- Different brands of energy meters
|
|
28
|
+
- Existing MQTT infrastructures
|
|
29
|
+
- Home Assistant sensors
|
|
30
|
+
- Custom hardware or software systems
|
|
31
|
+
- Simulated or virtual meters for testing
|
|
30
32
|
|
|
31
33
|
**v2c-any** bridges that gap.
|
|
32
34
|
|
|
@@ -52,13 +54,13 @@ Or in practical terms:
|
|
|
52
54
|
|
|
53
55
|
## Key features
|
|
54
56
|
|
|
55
|
-
- 🔌 **Universal adapter** – works with
|
|
56
|
-
- 📡 **MQTT support** – publish once, charge dynamically
|
|
57
|
-
- ⚡ **Dynamic Power Control** – grid, solar, or hybrid scenarios
|
|
58
|
-
- 🧪 **Simulation mode** – emulate supported meters for testing
|
|
59
|
-
- 🔁 **Proxy mode** – forward and transform existing devices
|
|
60
|
-
- 🧩 **Extensible architecture** – add new adapters easily
|
|
61
|
-
- 🟦 **TypeScript-first** – predictable, typed, maintainable
|
|
57
|
+
- 🔌 **Universal adapter** – works with _any_ power data source
|
|
58
|
+
- 📡 **MQTT support** – publish once, charge dynamically
|
|
59
|
+
- ⚡ **Dynamic Power Control** – grid, solar, or hybrid scenarios
|
|
60
|
+
- 🧪 **Simulation mode** – emulate supported meters for testing
|
|
61
|
+
- 🔁 **Proxy mode** – forward and transform existing devices
|
|
62
|
+
- 🧩 **Extensible architecture** – add new adapters easily
|
|
63
|
+
- 🟦 **TypeScript-first** – predictable, typed, maintainable
|
|
62
64
|
|
|
63
65
|
## Quick Start
|
|
64
66
|
|
|
@@ -96,7 +98,7 @@ properties:
|
|
|
96
98
|
type: adapter
|
|
97
99
|
properties:
|
|
98
100
|
interval: 5000
|
|
99
|
-
|
|
101
|
+
host: 192.168.1.100
|
|
100
102
|
solar:
|
|
101
103
|
mode: pull
|
|
102
104
|
feed:
|
|
@@ -134,7 +136,7 @@ Emulates a **Shelly Pro EM** energy meter by exposing a REST API that V2C wallbo
|
|
|
134
136
|
- You want to act as a drop-in replacement for physical hardware
|
|
135
137
|
- You prefer a pull-based (polling) approach
|
|
136
138
|
|
|
137
|
-
**
|
|
139
|
+
**Quick example:**
|
|
138
140
|
|
|
139
141
|
```yaml
|
|
140
142
|
provider: rest
|
|
@@ -146,29 +148,26 @@ properties:
|
|
|
146
148
|
type: adapter
|
|
147
149
|
properties:
|
|
148
150
|
device: shelly-pro-em
|
|
149
|
-
|
|
151
|
+
host: 192.168.1.100
|
|
150
152
|
solar:
|
|
151
153
|
feed:
|
|
152
154
|
type: mock
|
|
153
155
|
properties:
|
|
154
|
-
value:
|
|
156
|
+
value:
|
|
155
157
|
id: 1
|
|
156
|
-
voltage: 230.2
|
|
157
|
-
current: 3.785
|
|
158
158
|
act_power: 852.7
|
|
159
|
-
aprt_power: 873.1
|
|
160
|
-
pf: 0.98
|
|
161
|
-
freq: 50
|
|
162
159
|
calibration: factory
|
|
163
160
|
```
|
|
164
161
|
|
|
165
162
|
**How it works:**
|
|
166
163
|
|
|
167
164
|
1. `v2ca` starts a Fastify HTTP server
|
|
168
|
-
2. Exposes
|
|
169
|
-
3. V2C wallbox polls
|
|
165
|
+
2. Exposes endpoint matching Shelly Pro EM API format (i.e., `/rpc/EM1.GetStatus`)
|
|
166
|
+
3. V2C wallbox polls the endpoint at configured intervals
|
|
170
167
|
4. Returns real-time power data from your configured sources
|
|
171
168
|
|
|
169
|
+
📖 **[See full REST Mode documentation](docs/REST_MODE.md)** for detailed configuration options, schemas, and examples.
|
|
170
|
+
|
|
172
171
|
### MQTT Mode (Direct Publisher)
|
|
173
172
|
|
|
174
173
|
Publishes power data directly to MQTT topics that V2C wallboxes subscribe to.
|
|
@@ -180,7 +179,7 @@ Publishes power data directly to MQTT topics that V2C wallboxes subscribe to.
|
|
|
180
179
|
- You want push-based (event-driven) updates
|
|
181
180
|
- You need lower latency or more frequent updates
|
|
182
181
|
|
|
183
|
-
**
|
|
182
|
+
**Quick example:**
|
|
184
183
|
|
|
185
184
|
```yaml
|
|
186
185
|
provider: mqtt
|
|
@@ -188,15 +187,15 @@ properties:
|
|
|
188
187
|
url: mqtt://broker.local:1883
|
|
189
188
|
meters:
|
|
190
189
|
grid:
|
|
191
|
-
mode: pull
|
|
190
|
+
mode: pull # v2ca polls your device
|
|
192
191
|
feed:
|
|
193
192
|
type: adapter
|
|
194
193
|
properties:
|
|
195
194
|
device: shelly-pro-em
|
|
196
|
-
interval: 2000
|
|
197
|
-
|
|
195
|
+
interval: 2000
|
|
196
|
+
host: 192.168.1.100
|
|
198
197
|
solar:
|
|
199
|
-
mode: push
|
|
198
|
+
mode: push # v2ca subscribes to MQTT topic
|
|
200
199
|
feed:
|
|
201
200
|
type: bridge
|
|
202
201
|
properties:
|
|
@@ -211,10 +210,12 @@ properties:
|
|
|
211
210
|
3. Supports both **pull** (polling devices) and **push** (subscribing to topics)
|
|
212
211
|
4. V2C wallbox subscribes and receives real-time updates
|
|
213
212
|
|
|
213
|
+
📖 **[See full MQTT Mode documentation](docs/MQTT_MODE.md)** for detailed configuration options, schemas, and examples.
|
|
214
|
+
|
|
214
215
|
### Mode Comparison
|
|
215
216
|
|
|
216
217
|
| Feature | REST Mode | MQTT Mode |
|
|
217
|
-
|
|
218
|
+
| --------------- | -------------------------- | --------------------------- |
|
|
218
219
|
| **Protocol** | HTTP/REST | MQTT |
|
|
219
220
|
| **Direction** | Pull (V2C polls v2ca) | Push (v2ca publishes) |
|
|
220
221
|
| **Latency** | Higher (polling interval) | Lower (event-driven) |
|
|
@@ -222,11 +223,11 @@ properties:
|
|
|
222
223
|
| **Use Case** | Shelly meter replacement | MQTT-native setups |
|
|
223
224
|
| **Scalability** | Limited by polling | Better for multiple devices |
|
|
224
225
|
|
|
225
|
-
## What v2c-any is
|
|
226
|
+
## What v2c-any is _not_
|
|
226
227
|
|
|
227
|
-
- ❌ Not a replacement for your existing meters
|
|
228
|
-
- ❌ Not tied to a single vendor or ecosystem
|
|
229
|
-
- ❌ Not limited to one communication protocol
|
|
228
|
+
- ❌ Not a replacement for your existing meters
|
|
229
|
+
- ❌ Not tied to a single vendor or ecosystem
|
|
230
|
+
- ❌ Not limited to one communication protocol
|
|
230
231
|
|
|
231
232
|
It’s an **adapter**, not a lock-in.
|
|
232
233
|
|
|
@@ -7,18 +7,18 @@ import { em1StatusComparator, em1StatusInterpolator, em1StatusZeroValue, } from
|
|
|
7
7
|
* Provider that fetches EM1 status data from a Shelly Pro EM device via HTTP.
|
|
8
8
|
* Retrieves real-time energy monitoring data by querying the device's RPC API endpoint.
|
|
9
9
|
*/
|
|
10
|
-
|
|
10
|
+
class EM1StatusProvider {
|
|
11
11
|
/**
|
|
12
12
|
* Creates a new EM1 status provider.
|
|
13
13
|
* @param host - The IP address or hostname of the Shelly Pro EM device
|
|
14
14
|
* @param energyType - The type of energy data to retrieve (e.g., active, reactive)
|
|
15
15
|
*/
|
|
16
|
-
constructor(
|
|
17
|
-
this.
|
|
18
|
-
this.
|
|
16
|
+
constructor(properties) {
|
|
17
|
+
this.properties = properties;
|
|
18
|
+
this.url = `${properties.protocol}://${properties.host}:${properties.port}`;
|
|
19
19
|
const { responseError } = interceptors;
|
|
20
|
-
this.client = new Client(
|
|
21
|
-
this.id = energyTypeToId(energyType);
|
|
20
|
+
this.client = new Client(this.url).compose(responseError());
|
|
21
|
+
this.id = energyTypeToId(properties.energyType);
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
24
|
* Fetches the current EM1 status from the device.
|
|
@@ -26,7 +26,7 @@ export class EM1StatusProvider {
|
|
|
26
26
|
* @throws {Error} If the HTTP request fails or returns invalid data
|
|
27
27
|
*/
|
|
28
28
|
async get() {
|
|
29
|
-
logger.debug({
|
|
29
|
+
logger.debug({ url: this.url, energyType: this.properties.energyType }, 'Fetching EM1Status');
|
|
30
30
|
const res = await this.client.request({
|
|
31
31
|
path: `/rpc/EM1.GetStatus?id=${this.id}`,
|
|
32
32
|
method: 'GET',
|
|
@@ -45,9 +45,23 @@ class EM1StatusProviderFactory {
|
|
|
45
45
|
* @returns A configured EM1StatusProvider instance
|
|
46
46
|
*/
|
|
47
47
|
create(options) {
|
|
48
|
-
logger.debug({
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
logger.debug({
|
|
49
|
+
url: `${options.properties.protocol}://${options.properties.host}:${options.properties.port}`,
|
|
50
|
+
energyType: options.energyType,
|
|
51
|
+
}, 'Creating EM1StatusProvider');
|
|
52
|
+
const provider = new EM1StatusProvider({
|
|
53
|
+
energyType: options.energyType,
|
|
54
|
+
...options.properties,
|
|
55
|
+
});
|
|
56
|
+
return createResiliantProvider({
|
|
57
|
+
provider,
|
|
58
|
+
interpolator: em1StatusInterpolator,
|
|
59
|
+
zeroValue: em1StatusZeroValue,
|
|
60
|
+
comparator: em1StatusComparator,
|
|
61
|
+
breakerOptions: options.properties.breaker,
|
|
62
|
+
retryOptions: options.properties.retry,
|
|
63
|
+
emaOptions: options.properties.ema,
|
|
64
|
+
});
|
|
51
65
|
}
|
|
52
66
|
}
|
|
53
67
|
/**
|
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
import { energyTypeToId } from '../../utils/mappers.js';
|
|
2
|
+
/**
|
|
3
|
+
* Adapter for transforming Shelly Pro EM1 NotifyStatus notifications into energy information.
|
|
4
|
+
* Extracts active power data from the appropriate EM1 channel based on the configured energy type.
|
|
5
|
+
*/
|
|
2
6
|
class EnergyInformationEM1NotifyStatusAdapter {
|
|
7
|
+
/**
|
|
8
|
+
* Creates a new EM1 NotifyStatus adapter for a specific energy type.
|
|
9
|
+
* @param energyType - The type of energy (grid or solar) to extract from notifications
|
|
10
|
+
*/
|
|
3
11
|
constructor(energyType) {
|
|
4
12
|
this.id = energyTypeToId(energyType);
|
|
5
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Adapts an EM1 NotifyStatus notification frame into energy information.
|
|
16
|
+
* Extracts the active power from the configured EM1 channel.
|
|
17
|
+
*
|
|
18
|
+
* @param input - The notification frame containing EM1 status data
|
|
19
|
+
* @returns A promise resolving to energy information with power value, or undefined if the channel data is not present
|
|
20
|
+
*/
|
|
6
21
|
adapt(input) {
|
|
7
22
|
const key = `em1:${this.id}`;
|
|
8
23
|
const em1Status = input.params[key];
|
|
@@ -12,9 +27,23 @@ class EnergyInformationEM1NotifyStatusAdapter {
|
|
|
12
27
|
return Promise.resolve(undefined);
|
|
13
28
|
}
|
|
14
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Factory for creating EnergyInformationEM1NotifyStatusAdapter instances.
|
|
32
|
+
* Implements the AdapterFactory pattern to produce adapters configured for specific energy types.
|
|
33
|
+
*/
|
|
15
34
|
class EnergyInformationEM1NotifyStatusAdapterFactory {
|
|
35
|
+
/**
|
|
36
|
+
* Creates a new EM1 NotifyStatus adapter configured for the specified energy type.
|
|
37
|
+
*
|
|
38
|
+
* @param options - Configuration options specifying the energy type
|
|
39
|
+
* @returns An adapter that transforms EM1 NotifyStatus notifications into energy information
|
|
40
|
+
*/
|
|
16
41
|
create(options) {
|
|
17
42
|
return new EnergyInformationEM1NotifyStatusAdapter(options.energyType);
|
|
18
43
|
}
|
|
19
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Singleton instance of the EM1 NotifyStatus adapter factory.
|
|
47
|
+
* Used to create adapters for transforming Shelly Pro EM NotifyStatus notifications into energy information.
|
|
48
|
+
*/
|
|
20
49
|
export const energyInformationEM1NotifyStatusAdapterFactory = new EnergyInformationEM1NotifyStatusAdapterFactory();
|
|
@@ -22,7 +22,18 @@ class EnergyInformationEM1StatusAdapter {
|
|
|
22
22
|
* Use this ready-to-use instance where an adapter object is required.
|
|
23
23
|
*/
|
|
24
24
|
const energyInformationEM1StatusAdapter = new EnergyInformationEM1StatusAdapter();
|
|
25
|
+
/**
|
|
26
|
+
* Factory for creating EnergyInformationEM1StatusAdapter instances.
|
|
27
|
+
* Returns a singleton adapter instance that transforms EM1Status data into EnergyInformation.
|
|
28
|
+
* Implements the AdapterFactory pattern without requiring configuration options.
|
|
29
|
+
*/
|
|
25
30
|
export const energyInformationEM1StatusAdapterFactory = {
|
|
31
|
+
/**
|
|
32
|
+
* Creates and returns the singleton EM1 Status adapter instance.
|
|
33
|
+
* No configuration options are required as the adapter uses a fixed transformation logic.
|
|
34
|
+
*
|
|
35
|
+
* @returns An adapter that transforms EM1Status data into energy information
|
|
36
|
+
*/
|
|
26
37
|
create() {
|
|
27
38
|
return energyInformationEM1StatusAdapter;
|
|
28
39
|
},
|
|
@@ -11,11 +11,11 @@ export class MqttPullExecutableServiceFactory {
|
|
|
11
11
|
/**
|
|
12
12
|
* Creates a new MQTT pull executable service factory.
|
|
13
13
|
* @param providerFactoryRegistry - Registry of device providers for adapter-based sources
|
|
14
|
-
* @param
|
|
14
|
+
* @param adapterFactoryRegistry - Registry of device adapters for transforming provider output
|
|
15
15
|
*/
|
|
16
|
-
constructor(providerFactoryRegistry,
|
|
16
|
+
constructor(providerFactoryRegistry, adapterFactoryRegistry) {
|
|
17
17
|
this.providerFactoryRegistry = providerFactoryRegistry;
|
|
18
|
-
this.
|
|
18
|
+
this.adapterFactoryRegistry = adapterFactoryRegistry;
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
21
|
* Creates the appropriate provider factory based on the feed configuration type.
|
|
@@ -33,7 +33,7 @@ export class MqttPullExecutableServiceFactory {
|
|
|
33
33
|
if (!providerFactory) {
|
|
34
34
|
throw new Error(`No provider registered for device: ${device}`);
|
|
35
35
|
}
|
|
36
|
-
const adapterFactory = this.
|
|
36
|
+
const adapterFactory = this.adapterFactoryRegistry.get(device);
|
|
37
37
|
if (!adapterFactory) {
|
|
38
38
|
throw new Error(`No adapter registered for device: ${device}`);
|
|
39
39
|
}
|
|
@@ -8,10 +8,10 @@ import { MqttBridgeService } from '../service/mqtt-bridge-service.js';
|
|
|
8
8
|
export class MqttPushExecutableServiceFactory {
|
|
9
9
|
/**
|
|
10
10
|
* Creates a new MQTT push executable service factory.
|
|
11
|
-
* @param
|
|
11
|
+
* @param devicesAdapterFactoryRegistry - Registry of device adapters to transform incoming messages
|
|
12
12
|
*/
|
|
13
|
-
constructor(
|
|
14
|
-
this.
|
|
13
|
+
constructor(devicesAdapterFactoryRegistry) {
|
|
14
|
+
this.devicesAdapterFactoryRegistry = devicesAdapterFactoryRegistry;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
17
|
* Creates an executable service using the appropriate push strategy.
|
|
@@ -24,11 +24,11 @@ export class MqttPushExecutableServiceFactory {
|
|
|
24
24
|
switch (options.configuration.type) {
|
|
25
25
|
case 'bridge': {
|
|
26
26
|
const device = options.configuration.properties.device;
|
|
27
|
-
const
|
|
28
|
-
if (!
|
|
27
|
+
const adapterFactory = this.devicesAdapterFactoryRegistry.get(device);
|
|
28
|
+
if (!adapterFactory) {
|
|
29
29
|
throw new Error(`No adapter registered for device: ${device}`);
|
|
30
30
|
}
|
|
31
|
-
return new MqttBridgeService(options.configuration.properties, options.callbackProperties,
|
|
31
|
+
return new MqttBridgeService(options.configuration.properties, options.callbackProperties, adapterFactory.create({ energyType: options.energyType }));
|
|
32
32
|
}
|
|
33
33
|
case 'off':
|
|
34
34
|
return noOpExecutableService;
|
|
@@ -8,7 +8,7 @@ import { logger } from '../utils/logger.js';
|
|
|
8
8
|
*/
|
|
9
9
|
export class AsymmetricEMAProvider {
|
|
10
10
|
/**
|
|
11
|
-
* Creates a new
|
|
11
|
+
* Creates a new AsymmetricEMAProvider.
|
|
12
12
|
* @param provider - The underlying provider to fetch raw values from
|
|
13
13
|
* @param interpolator - The interpolator to use for blending values
|
|
14
14
|
* @param options - Configuration options for the asymmetric EMA calculation
|
|
@@ -18,7 +18,9 @@ export class AsymmetricEMAProvider {
|
|
|
18
18
|
this.provider = provider;
|
|
19
19
|
this.interpolator = interpolator;
|
|
20
20
|
this.options = options;
|
|
21
|
+
/** The current exponential moving average value, or null if not yet initialized */
|
|
21
22
|
this.ema = null;
|
|
23
|
+
/** Timestamp of the last successful value update in milliseconds since epoch */
|
|
22
24
|
this.lastUpdateTime = null;
|
|
23
25
|
if (options.alphaRise < 0 || options.alphaRise > 1) {
|
|
24
26
|
throw new Error('alphaRise must be between 0 and 1');
|
|
@@ -27,6 +29,13 @@ export class AsymmetricEMAProvider {
|
|
|
27
29
|
throw new Error('alphaFall must be between 0 and 1');
|
|
28
30
|
}
|
|
29
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Updates the EMA with a newly received value.
|
|
34
|
+
* Determines whether the value is rising or falling and applies the appropriate smoothing factor.
|
|
35
|
+
* Initializes the EMA on first call.
|
|
36
|
+
*
|
|
37
|
+
* @param newValue - The new value to incorporate into the EMA
|
|
38
|
+
*/
|
|
30
39
|
onNewValue(newValue) {
|
|
31
40
|
if (this.ema === null) {
|
|
32
41
|
this.ema = newValue;
|
|
@@ -36,6 +45,11 @@ export class AsymmetricEMAProvider {
|
|
|
36
45
|
const alpha = comparison >= 0 ? this.options.alphaRise : this.options.alphaFall;
|
|
37
46
|
this.ema = this.interpolator.interpolate(newValue, this.ema, alpha);
|
|
38
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Handles the case when a value fetch fails or returns no data.
|
|
50
|
+
* Decays the current EMA toward the configured zero value using the missing value smoothing factor.
|
|
51
|
+
* Only applies if the EMA has been previously initialized.
|
|
52
|
+
*/
|
|
39
53
|
onMissingValue() {
|
|
40
54
|
if (this.ema !== null) {
|
|
41
55
|
this.ema = this.interpolator.interpolate(this.options.zeroValue, this.ema, this.options.alphaMissing);
|
|
@@ -46,7 +60,10 @@ export class AsymmetricEMAProvider {
|
|
|
46
60
|
* On first call, initializes the EMA with the fetched value.
|
|
47
61
|
* On subsequent calls, interpolates between the new value and current EMA,
|
|
48
62
|
* using alphaRise if the value is increasing or alphaFall if decreasing.
|
|
49
|
-
*
|
|
63
|
+
* If the fetch fails and a freshness threshold is configured, decays toward zero value.
|
|
64
|
+
*
|
|
65
|
+
* @returns A promise that resolves to the newly fetched value, or the current EMA if fetch fails
|
|
66
|
+
* @throws {Error} If fetch fails and no EMA has been initialized yet
|
|
50
67
|
*/
|
|
51
68
|
async get() {
|
|
52
69
|
try {
|
|
@@ -79,6 +96,8 @@ export class AsymmetricEMAProvider {
|
|
|
79
96
|
/**
|
|
80
97
|
* Resets the EMA to its initial state.
|
|
81
98
|
* The next call to get() will reinitialize the EMA.
|
|
99
|
+
*
|
|
100
|
+
* @param value - Optional value to set as the new EMA. Defaults to null (uninitialized state)
|
|
82
101
|
*/
|
|
83
102
|
reset(value = null) {
|
|
84
103
|
this.ema = value;
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import CircuitBreaker from 'opossum';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Provider wrapper that implements the circuit breaker pattern for resilience.
|
|
4
|
+
* Protects against cascading failures by automatically opening the circuit when
|
|
5
|
+
* the underlying provider exceeds failure thresholds. Supports timeout, retry,
|
|
6
|
+
* and fallback mechanisms provided by the Opossum circuit breaker library.
|
|
6
7
|
*
|
|
7
8
|
* @template T - The type of value this provider supplies
|
|
8
9
|
*/
|
|
9
10
|
export class CircuitBreakerProvider {
|
|
10
11
|
/**
|
|
11
|
-
* Creates a new
|
|
12
|
-
* @param provider - The underlying provider to
|
|
13
|
-
* @param
|
|
14
|
-
* @param asymmetricEmaOptions - Configuration options for the asymmetric EMA calculation
|
|
12
|
+
* Creates a new CircuitBreakerProvider wrapping the given provider.
|
|
13
|
+
* @param provider - The underlying provider to protect with circuit breaker logic
|
|
14
|
+
* @param options - Optional Opossum circuit breaker configuration (timeout, error thresholds, etc.)
|
|
15
15
|
*/
|
|
16
16
|
constructor(provider, options) {
|
|
17
17
|
this.provider = provider;
|
|
@@ -19,10 +19,12 @@ export class CircuitBreakerProvider {
|
|
|
19
19
|
this.circuitBreaker = new CircuitBreaker(this.provider.get.bind(this.provider), this.options);
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
|
-
* Fetches
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
22
|
+
* Fetches a value from the underlying provider with circuit breaker protection.
|
|
23
|
+
* Automatically fails fast when the circuit is open due to excessive failures.
|
|
24
|
+
* Falls back to configured fallback mechanisms if the provider fails.
|
|
25
|
+
*
|
|
26
|
+
* @returns A promise that resolves to the value from the underlying provider
|
|
27
|
+
* @throws {Error} If the circuit is open or the provider fails without a configured fallback
|
|
26
28
|
*/
|
|
27
29
|
get() {
|
|
28
30
|
return this.circuitBreaker.fire();
|
|
@@ -10,7 +10,7 @@ export class RetryableProvider {
|
|
|
10
10
|
/**
|
|
11
11
|
* Creates a new RetryableProvider.
|
|
12
12
|
* @param provider - The underlying provider to wrap with retry logic
|
|
13
|
-
* @param options - Configuration options for retry behavior
|
|
13
|
+
* @param options - Configuration options for retry behavior (retries, minTimeout, maxTimeout, factor, etc.)
|
|
14
14
|
*/
|
|
15
15
|
constructor(provider, options) {
|
|
16
16
|
this.provider = provider;
|
|
@@ -29,7 +29,9 @@ export const mqttPullAdapterFeedSchema = z
|
|
|
29
29
|
.object({
|
|
30
30
|
interval: z.number().int().nonnegative(),
|
|
31
31
|
device: z.string(),
|
|
32
|
-
|
|
32
|
+
host: z.string(),
|
|
33
|
+
protocol: z.enum(['http', 'https']).default('http'),
|
|
34
|
+
port: z.number().int().min(1).max(65535).default(80),
|
|
33
35
|
breaker: breakerScehema.optional(),
|
|
34
36
|
retry: retrySchema.optional(),
|
|
35
37
|
ema: emaSchema.optional(),
|
|
@@ -45,8 +45,10 @@ export const em1StatusSchema = z.object({
|
|
|
45
45
|
});
|
|
46
46
|
export const restAdapterFeedSchema = z
|
|
47
47
|
.object({
|
|
48
|
-
ip: z.string(),
|
|
49
48
|
device: z.string(),
|
|
49
|
+
host: z.string(),
|
|
50
|
+
protocol: z.enum(['http', 'https']).default('http'),
|
|
51
|
+
port: z.number().int().min(1).max(65535).default(80),
|
|
50
52
|
breaker: breakerScehema.optional(),
|
|
51
53
|
retry: retrySchema.optional(),
|
|
52
54
|
ema: emaSchema.optional(),
|
|
@@ -11,11 +11,18 @@ import { AbstractExecutableService } from './abstract-executable-service.js';
|
|
|
11
11
|
* Implements the executable service lifecycle to start and stop the HTTP server.
|
|
12
12
|
*/
|
|
13
13
|
export class RestService extends AbstractExecutableService {
|
|
14
|
+
/**
|
|
15
|
+
* Creates a new REST service with configured energy providers.
|
|
16
|
+
* @param gridEnergyProvider - Provider for grid energy status data (EM1 channel 0)
|
|
17
|
+
* @param solarEnergyProvider - Provider for solar energy status data (EM1 channel 1)
|
|
18
|
+
* @param properties - Configuration properties including the server port
|
|
19
|
+
*/
|
|
14
20
|
constructor(gridEnergyProvider, solarEnergyProvider, properties) {
|
|
15
21
|
super();
|
|
16
22
|
this.gridEnergyProvider = gridEnergyProvider;
|
|
17
23
|
this.solarEnergyProvider = solarEnergyProvider;
|
|
18
24
|
this.properties = properties;
|
|
25
|
+
/** The Fastify application instance, or null when the service is not running */
|
|
19
26
|
this.app = null;
|
|
20
27
|
}
|
|
21
28
|
/**
|
package/dist/utils/mqtt.js
CHANGED
|
@@ -5,6 +5,7 @@ import { logger } from './logger.js';
|
|
|
5
5
|
* Subscribes to lifecycle events (connect, error, reconnect) for visibility.
|
|
6
6
|
*
|
|
7
7
|
* @param url - MQTT broker URL (e.g., mqtt://localhost:1883)
|
|
8
|
+
* @param options - Optional connection options including username and password for authentication
|
|
8
9
|
* @returns A promise that resolves to a connected MqttClient instance
|
|
9
10
|
* @throws {Error} If the underlying connection fails
|
|
10
11
|
*/
|
package/dist/utils/resiliance.js
CHANGED
|
@@ -2,7 +2,18 @@ import { AsymmetricEMAProvider } from '../provider/asymmetric-ema-provider.js';
|
|
|
2
2
|
import { CircuitBreakerProvider } from '../provider/circuit-breaker-provider.js';
|
|
3
3
|
import { RetryableProvider } from '../provider/retryable-provider.js';
|
|
4
4
|
import { logger } from './logger.js';
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Creates a resilient provider by wrapping a base provider with optional circuit breaker,
|
|
7
|
+
* retry, and exponential moving average (EMA) capabilities. The providers are composed
|
|
8
|
+
* in layers: circuit breaker (outermost) → retry → EMA → base provider (innermost).
|
|
9
|
+
* This composition provides comprehensive resilience against transient failures.
|
|
10
|
+
*
|
|
11
|
+
* @template T - The type of value the provider supplies
|
|
12
|
+
* @param options - Configuration options for creating the resilient provider
|
|
13
|
+
* @returns A composed provider with the requested resilience features
|
|
14
|
+
*/
|
|
15
|
+
export function createResiliantProvider(options) {
|
|
16
|
+
const { provider, interpolator, zeroValue, comparator, breakerOptions, retryOptions, emaOptions, } = options;
|
|
6
17
|
let result = provider;
|
|
7
18
|
if (breakerOptions) {
|
|
8
19
|
result = new CircuitBreakerProvider(result, breakerOptions);
|
|
@@ -36,10 +47,10 @@ export function createResiliantProvider(provider, interpolator, zeroValue, compa
|
|
|
36
47
|
}
|
|
37
48
|
if (emaOptions) {
|
|
38
49
|
result = new AsymmetricEMAProvider(result, interpolator, {
|
|
39
|
-
alphaRise: emaOptions
|
|
40
|
-
alphaFall: emaOptions
|
|
41
|
-
alphaMissing: emaOptions
|
|
42
|
-
freshnessThreshold: emaOptions
|
|
50
|
+
alphaRise: emaOptions.alphaRise,
|
|
51
|
+
alphaFall: emaOptions.alphaFall,
|
|
52
|
+
alphaMissing: emaOptions.alphaMissing,
|
|
53
|
+
freshnessThreshold: emaOptions.freshnessThreshold,
|
|
43
54
|
zeroValue,
|
|
44
55
|
comparator,
|
|
45
56
|
});
|