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 CHANGED
@@ -1,4 +1,6 @@
1
+ <!-- markdownlint-disable MD041 -->
1
2
  ![v2a - V2C any](docs/assets/images/v2ca.png)
3
+ <!-- markdownlint-enable MD041 -->
2
4
 
3
5
  ![GitHub License](https://img.shields.io/github/license/tvcsantos/v2c-any)
4
6
  ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/tvcsantos/v2c-any/ci-build.yml)
@@ -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 *V2C*.
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 *any* power data source
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
- ip: 192.168.1.100
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
- **Configuration example:**
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
- ip: 192.168.1.100
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 endpoints matching Shelly Pro EM API format
169
- 3. V2C wallbox polls these endpoints (e.g., `/rpc/EM1.GetStatus?id=0`)
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
- **Configuration example:**
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 # v2ca polls your device
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 # Every 2 seconds
197
- ip: 192.168.1.100
195
+ interval: 2000
196
+ host: 192.168.1.100
198
197
  solar:
199
- mode: push # v2ca subscribes to MQTT topic
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 *not*
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
- export class EM1StatusProvider {
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(host, energyType) {
17
- this.host = host;
18
- this.energyType = energyType;
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(`http://${this.host}`).compose(responseError());
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({ host: this.host, energyType: this.energyType }, 'Fetching EM1Status');
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({ ip: options.properties.ip, energyType: options.energyType }, 'Creating EM1StatusProvider');
49
- const provider = new EM1StatusProvider(options.properties.ip, options.energyType);
50
- return createResiliantProvider(provider, em1StatusInterpolator, em1StatusZeroValue, em1StatusComparator, options.properties.breaker, options.properties.retry, options.properties.ema);
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 adapterRegistry - Registry of device adapters for transforming provider output
14
+ * @param adapterFactoryRegistry - Registry of device adapters for transforming provider output
15
15
  */
16
- constructor(providerFactoryRegistry, adapterRegistry) {
16
+ constructor(providerFactoryRegistry, adapterFactoryRegistry) {
17
17
  this.providerFactoryRegistry = providerFactoryRegistry;
18
- this.adapterRegistry = adapterRegistry;
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.adapterRegistry.get(device);
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 devicesAdapter - Registry of device adapters to transform incoming messages
11
+ * @param devicesAdapterFactoryRegistry - Registry of device adapters to transform incoming messages
12
12
  */
13
- constructor(devicesAdapter) {
14
- this.devicesAdapter = devicesAdapter;
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 adapter = this.devicesAdapter.get(device);
28
- if (!adapter) {
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, adapter.create({ energyType: options.energyType }));
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 InnerAsymmetricEMAProvider.
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
- * @returns A promise that resolves to the updated EMA value
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
- * Resilient Asymmetric EMA Provider with circuit breaker pattern.
4
- * Wraps an InnerAsymmetricEMAProvider with resilience features, providing
5
- * fallback to the last known EMA value when the underlying provider fails.
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 AsymmetricEMAProvider with circuit breaker protection.
12
- * @param provider - The underlying provider to fetch raw values from
13
- * @param interpolator - The interpolator to use for blending values
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 the value from the wrapped provider with resilience features.
23
- * If the underlying provider fails or times out, falls back to the last known EMA value.
24
- * @returns A promise that resolves to the EMA value
25
- * @throws {Error} If no EMA value is available for fallback when the provider fails
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
- ip: z.string(),
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
  /**
@@ -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
  */
@@ -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
- export function createResiliantProvider(provider, interpolator, zeroValue, comparator, breakerOptions, retryOptions, emaOptions) {
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?.alphaRise,
40
- alphaFall: emaOptions?.alphaFall,
41
- alphaMissing: emaOptions?.alphaMissing,
42
- freshnessThreshold: emaOptions?.freshnessThreshold,
50
+ alphaRise: emaOptions.alphaRise,
51
+ alphaFall: emaOptions.alphaFall,
52
+ alphaMissing: emaOptions.alphaMissing,
53
+ freshnessThreshold: emaOptions.freshnessThreshold,
43
54
  zeroValue,
44
55
  comparator,
45
56
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "v2c-any",
3
- "version": "0.5.3",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "description": "Turn any device into V2C Dynamic Power Control",
6
6
  "main": "dist/index.js",