v2c-any 0.1.9 → 0.2.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
@@ -40,7 +40,7 @@ It adapts **any input** into the protocol and format expected by a V2C wallbox
40
40
 
41
41
  Or in practical terms:
42
42
 
43
- ```
43
+ ```text
44
44
  [Any Meter | MQTT | API | Simulator]
45
45
 
46
46
 
@@ -129,6 +129,7 @@ docker run -v $(pwd)/.v2carc.yaml:/app/.v2carc.yaml v2c-any
129
129
  Emulates a **Shelly Pro EM** energy meter by exposing a REST API that V2C wallboxes can poll.
130
130
 
131
131
  **Use this when:**
132
+
132
133
  - Your V2C wallbox is configured to poll a Shelly meter
133
134
  - You want to act as a drop-in replacement for physical hardware
134
135
  - You prefer a pull-based (polling) approach
@@ -139,12 +140,12 @@ Emulates a **Shelly Pro EM** energy meter by exposing a REST API that V2C wallbo
139
140
  provider: rest
140
141
  properties:
141
142
  port: 3000
142
- device: shelly-pro-em
143
143
  meters:
144
144
  grid:
145
145
  feed:
146
146
  type: adapter
147
147
  properties:
148
+ device: shelly-pro-em
148
149
  ip: 192.168.1.100
149
150
  solar:
150
151
  feed:
@@ -162,6 +163,7 @@ properties:
162
163
  ```
163
164
 
164
165
  **How it works:**
166
+
165
167
  1. `v2ca` starts a Fastify HTTP server
166
168
  2. Exposes endpoints matching Shelly Pro EM API format
167
169
  3. V2C wallbox polls these endpoints (e.g., `/rpc/EM1.GetStatus?id=0`)
@@ -172,6 +174,7 @@ properties:
172
174
  Publishes power data directly to MQTT topics that V2C wallboxes subscribe to.
173
175
 
174
176
  **Use this when:**
177
+
175
178
  - Your V2C wallbox is configured for MQTT integration
176
179
  - You have an existing MQTT broker
177
180
  - You want push-based (event-driven) updates
@@ -183,13 +186,13 @@ Publishes power data directly to MQTT topics that V2C wallboxes subscribe to.
183
186
  provider: mqtt
184
187
  properties:
185
188
  url: mqtt://broker.local:1883
186
- device: shelly-pro-em
187
189
  meters:
188
190
  grid:
189
191
  mode: pull # v2ca polls your device
190
192
  feed:
191
193
  type: adapter
192
194
  properties:
195
+ device: shelly-pro-em
193
196
  interval: 2000 # Every 2 seconds
194
197
  ip: 192.168.1.100
195
198
  solar:
@@ -202,6 +205,7 @@ properties:
202
205
  ```
203
206
 
204
207
  **How it works:**
208
+
205
209
  1. `v2ca` connects to your MQTT broker
206
210
  2. Publishes to V2C-expected topics (e.g., `trydan_v2c_sun_power`)
207
211
  3. Supports both **pull** (polling devices) and **push** (subscribing to topics)
@@ -1,6 +1,10 @@
1
- import { request } from 'undici';
1
+ import { Client, interceptors } from 'undici';
2
2
  import { logger } from '../../utils/logger.js';
3
3
  import { energyTypeToId } from '../../utils/mappers.js';
4
+ import { AsymmetricEMAProvider } from '../../provider/asymmetric-emea-provider.js';
5
+ import { RetryableProvider } from '../../provider/retryable-provider.js';
6
+ import { em1StatusInterpolator } from '../../utils/interpolator.js';
7
+ import { DecoratorProviderFactory } from '../../factory/decorator-provider-factory.js';
4
8
  /**
5
9
  * Provider that fetches EM1 status data from a Shelly Pro EM device via HTTP.
6
10
  * Retrieves real-time energy monitoring data by querying the device's RPC API endpoint.
@@ -14,6 +18,9 @@ export class EM1StatusProvider {
14
18
  constructor(host, energyType) {
15
19
  this.host = host;
16
20
  this.energyType = energyType;
21
+ const { responseError } = interceptors;
22
+ this.client = new Client(`http://${this.host}`).compose(responseError());
23
+ this.id = energyTypeToId(energyType);
17
24
  }
18
25
  /**
19
26
  * Fetches the current EM1 status from the device.
@@ -22,8 +29,10 @@ export class EM1StatusProvider {
22
29
  */
23
30
  async get() {
24
31
  logger.debug({ host: this.host, energyType: this.energyType }, 'Fetching EM1Status');
25
- const id = energyTypeToId(this.energyType);
26
- const res = await request(`http://${this.host}/rpc/EM1.GetStatus?id=${id}`, { method: 'GET' });
32
+ const res = await this.client.request({
33
+ path: `/rpc/EM1.GetStatus?id=${this.id}`,
34
+ method: 'GET',
35
+ });
27
36
  return (await res.body.json());
28
37
  }
29
38
  }
@@ -46,4 +55,13 @@ class EM1StatusProviderFactory {
46
55
  * Singleton factory instance for creating `EM1StatusProvider` objects.
47
56
  * Provides a ready-to-use factory to build providers with supplied options.
48
57
  */
49
- export const em1StatusProviderFactory = new EM1StatusProviderFactory();
58
+ const localEm1StatusProviderFactory = new EM1StatusProviderFactory();
59
+ export const em1StatusProviderFactory = new DecoratorProviderFactory(localEm1StatusProviderFactory, (provider) => new AsymmetricEMAProvider(new RetryableProvider(provider, { retries: 3 }), em1StatusInterpolator, {
60
+ alphaRise: 1,
61
+ alphaFall: 0.2,
62
+ comparator: (a, b) => {
63
+ const powerA = a?.act_power ?? 0;
64
+ const powerB = b?.act_power ?? 0;
65
+ return powerA - powerB;
66
+ },
67
+ }));
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Generic factory that wraps another provider factory with a decorator pattern.
3
+ * Enables composition by allowing providers to be enhanced with additional behavior
4
+ * such as retry logic, caching, logging, validation, etc.
5
+ *
6
+ * This factory creates providers by first using the wrapped factory to create a base provider,
7
+ * then applying a decorator function to enhance it with additional capabilities.
8
+ *
9
+ * @example
10
+ * // Creating a provider factory with retry capability
11
+ * const retryDecorator = (provider) => new RetryableProvider(provider, { retries: 3 });
12
+ * const resilientFactory = new DecoratorProviderFactory(baseFactory, retryDecorator);
13
+ * const resilientProvider = resilientFactory.create(options);
14
+ *
15
+ * @template Options - The configuration options type required by the wrapped factory
16
+ * @template T - The type of data the created providers will supply
17
+ */
18
+ export class DecoratorProviderFactory {
19
+ /**
20
+ * Creates a new decorator provider factory.
21
+ * @param providerFactory - The underlying factory that creates base providers
22
+ * @param decorator - Function that enhances providers with additional behavior
23
+ */
24
+ constructor(providerFactory, decorator) {
25
+ this.providerFactory = providerFactory;
26
+ this.decorator = decorator;
27
+ }
28
+ /**
29
+ * Creates a decorated provider using the wrapped factory and decorator function.
30
+ * First creates a base provider using the wrapped factory, then applies the decorator
31
+ * to enhance it with additional capabilities.
32
+ *
33
+ * @param options - Configuration options for the wrapped provider factory
34
+ * @returns A decorated provider with enhanced behavior
35
+ */
36
+ create(options) {
37
+ const baseProvider = this.providerFactory.create(options);
38
+ return this.decorator(baseProvider);
39
+ }
40
+ }
@@ -6,10 +6,10 @@ import { FixedValueProviderFactory } from '../provider/fixed-value-provider.js';
6
6
  export class EM1StatusProviderFactory {
7
7
  /**
8
8
  * Creates a new EM1Status provider factory.
9
- * @param devicesProvider - Registry containing provider factories for different devices
9
+ * @param providerFactoryRegistry - Registry containing provider factories for different devices
10
10
  */
11
- constructor(devicesProvider) {
12
- this.devicesProvider = devicesProvider;
11
+ constructor(providerFactoryRegistry) {
12
+ this.providerFactoryRegistry = providerFactoryRegistry;
13
13
  }
14
14
  /**
15
15
  * Creates the appropriate provider factory based on the feed configuration type.
@@ -20,11 +20,12 @@ export class EM1StatusProviderFactory {
20
20
  createProviderFactory(options) {
21
21
  switch (options.configuration.feed.type) {
22
22
  case 'adapter': {
23
- const provider = this.devicesProvider.get(options.device);
24
- if (!provider) {
25
- throw new Error(`No provider registered for device: ${options.device}`);
23
+ const device = options.configuration.feed.properties.device;
24
+ const providerFactory = this.providerFactoryRegistry.get(device);
25
+ if (!providerFactory) {
26
+ throw new Error(`No provider registered for device: ${device}`);
26
27
  }
27
- return provider;
28
+ return providerFactory;
28
29
  }
29
30
  case 'mock':
30
31
  return new FixedValueProviderFactory({
@@ -52,14 +52,12 @@ export class MqttFeedExecutableServiceFactory {
52
52
  case 'pull':
53
53
  return this.pullExecutableServiceFactory.create({
54
54
  energyType: options.energyType,
55
- device: options.device,
56
55
  configuration: options.configuration.feed,
57
56
  callbackProperties,
58
57
  });
59
58
  case 'push':
60
59
  return this.pushExecutableServiceFactory.create({
61
60
  energyType: options.energyType,
62
- device: options.device,
63
61
  configuration: options.configuration.feed,
64
62
  callbackProperties,
65
63
  });
@@ -10,12 +10,12 @@ import { noOpExecutableService } from '../service/no-op-executable-service.js';
10
10
  export class MqttPullExecutableServiceFactory {
11
11
  /**
12
12
  * Creates a new MQTT pull executable service factory.
13
- * @param devicesProviderRegistry - Registry of device providers for adapter-based sources
14
- * @param devicesAdapterRegistry - Registry of device adapters for transforming provider output
13
+ * @param providerFactoryRegistry - Registry of device providers for adapter-based sources
14
+ * @param adapterRegistry - Registry of device adapters for transforming provider output
15
15
  */
16
- constructor(devicesProviderRegistry, devicesAdapterRegistry) {
17
- this.devicesProviderRegistry = devicesProviderRegistry;
18
- this.devicesAdapterRegistry = devicesAdapterRegistry;
16
+ constructor(providerFactoryRegistry, adapterRegistry) {
17
+ this.providerFactoryRegistry = providerFactoryRegistry;
18
+ this.adapterRegistry = adapterRegistry;
19
19
  }
20
20
  /**
21
21
  * Creates the appropriate provider factory based on the feed configuration type.
@@ -28,16 +28,17 @@ export class MqttPullExecutableServiceFactory {
28
28
  createProviderFactory(options) {
29
29
  switch (options.configuration.type) {
30
30
  case 'adapter': {
31
- const provider = this.devicesProviderRegistry.get(options.device);
32
- if (!provider) {
33
- throw new Error(`No provider registered for device: ${options.device}`);
31
+ const device = options.configuration.properties.device;
32
+ const providerFactory = this.providerFactoryRegistry.get(device);
33
+ if (!providerFactory) {
34
+ throw new Error(`No provider registered for device: ${device}`);
34
35
  }
35
- const adapter = this.devicesAdapterRegistry.get(options.device);
36
+ const adapter = this.adapterRegistry.get(device);
36
37
  if (!adapter) {
37
- throw new Error(`No adapter registered for device: ${options.device}`);
38
+ throw new Error(`No adapter registered for device: ${device}`);
38
39
  }
39
40
  return {
40
- providerFactory: new AdapterProviderFactory(provider, adapter),
41
+ providerFactory: new AdapterProviderFactory(providerFactory, adapter),
41
42
  interval: options.configuration.properties.interval,
42
43
  };
43
44
  }
@@ -23,9 +23,10 @@ export class MqttPushExecutableServiceFactory {
23
23
  create(options) {
24
24
  switch (options.configuration.type) {
25
25
  case 'bridge': {
26
- const adapter = this.devicesAdapter.get(options.device);
26
+ const device = options.configuration.properties.device;
27
+ const adapter = this.devicesAdapter.get(device);
27
28
  if (!adapter) {
28
- throw new Error(`No adapter registered for device: ${options.device}`);
29
+ throw new Error(`No adapter registered for device: ${device}`);
29
30
  }
30
31
  return new MqttBridgeService(options.configuration.properties, options.callbackProperties, adapter);
31
32
  }
@@ -36,13 +36,11 @@ export class MqttServiceFactory {
36
36
  };
37
37
  const gridEnergyPublisher = this.mqttFeedExecutableServiceFactory.create({
38
38
  configuration: configuration.properties.meters.grid,
39
- device: configuration.properties.device,
40
39
  energyType: 'grid',
41
40
  callbacks,
42
41
  });
43
42
  const sunEnergyPublisher = this.mqttFeedExecutableServiceFactory.create({
44
43
  configuration: configuration.properties.meters.solar,
45
- device: configuration.properties.device,
46
44
  energyType: 'solar',
47
45
  callbacks,
48
46
  });
@@ -20,12 +20,10 @@ export class RestServiceFactory {
20
20
  create(configuration) {
21
21
  const gridEnergyProvider = this.em1StatusProviderFactory.create({
22
22
  energyType: 'grid',
23
- device: configuration.properties.device,
24
23
  configuration: configuration.properties.meters.grid,
25
24
  });
26
25
  const solarEnergyProvider = this.em1StatusProviderFactory.create({
27
26
  energyType: 'solar',
28
- device: configuration.properties.device,
29
27
  configuration: configuration.properties.meters.solar,
30
28
  });
31
29
  return new RestService(gridEnergyProvider, solarEnergyProvider, {
@@ -0,0 +1,104 @@
1
+ import { logger } from '../utils/logger.js';
2
+ import CircuitBreaker from 'opossum';
3
+ /**
4
+ * Generic Asymmetric EMA Provider using algebraic interpolators.
5
+ * Implements exponential moving average (EMA) with different smoothing factors
6
+ * for rising and falling values, allowing asymmetric response to changes.
7
+ *
8
+ * @template T - The type of value this provider supplies
9
+ */
10
+ class InnerAsymmetricEMAProvider {
11
+ /**
12
+ * Creates a new InnerAsymmetricEMAProvider.
13
+ * @param provider - The underlying provider to fetch raw values from
14
+ * @param interpolator - The interpolator to use for blending values
15
+ * @param options - Configuration options for the asymmetric EMA calculation
16
+ * @throws {Error} If alphaRise or alphaFall is not between 0 and 1
17
+ */
18
+ constructor(provider, interpolator, options) {
19
+ this.provider = provider;
20
+ this.interpolator = interpolator;
21
+ this.options = options;
22
+ this.ema = null;
23
+ if (options.alphaRise < 0 || options.alphaRise > 1) {
24
+ throw new Error('alphaRise must be between 0 and 1');
25
+ }
26
+ if (options.alphaFall < 0 || options.alphaFall > 1) {
27
+ throw new Error('alphaFall must be between 0 and 1');
28
+ }
29
+ }
30
+ /**
31
+ * Fetches a value from the wrapped provider and updates the EMA.
32
+ * On first call, initializes the EMA with the fetched value.
33
+ * On subsequent calls, interpolates between the new value and current EMA,
34
+ * using alphaRise if the value is increasing or alphaFall if decreasing.
35
+ * @returns A promise that resolves to the updated EMA value
36
+ */
37
+ async get() {
38
+ const newValue = await this.provider.get();
39
+ if (this.ema === null) {
40
+ this.ema = newValue;
41
+ }
42
+ else {
43
+ // Determine if value is rising or falling
44
+ const comparison = this.options.comparator(newValue, this.ema);
45
+ const alpha = comparison >= 0 ? this.options.alphaRise : this.options.alphaFall;
46
+ this.ema = this.interpolator.interpolate(newValue, this.ema, alpha);
47
+ }
48
+ return this.ema;
49
+ }
50
+ /**
51
+ * Gets the current EMA value without fetching a new value.
52
+ * @returns The current EMA value, or null if not yet initialized
53
+ */
54
+ getCurrentEMA() {
55
+ return this.ema;
56
+ }
57
+ /**
58
+ * Resets the EMA to its initial state.
59
+ * The next call to get() will reinitialize the EMA.
60
+ */
61
+ reset() {
62
+ this.ema = null;
63
+ }
64
+ }
65
+ /**
66
+ * Resilient Asymmetric EMA Provider with circuit breaker pattern.
67
+ * Wraps an InnerAsymmetricEMAProvider with resilience features, providing
68
+ * fallback to the last known EMA value when the underlying provider fails.
69
+ *
70
+ * @template T - The type of value this provider supplies
71
+ */
72
+ export class AsymmetricEMAProvider {
73
+ /**
74
+ * Creates a new AsymmetricEMAProvider with circuit breaker protection.
75
+ * @param provider - The underlying provider to fetch raw values from
76
+ * @param interpolator - The interpolator to use for blending values
77
+ * @param asymmetricEmaOptions - Configuration options for the asymmetric EMA calculation
78
+ */
79
+ constructor(provider, interpolator, asymmetricEmaOptions) {
80
+ this.provider = new InnerAsymmetricEMAProvider(provider, interpolator, asymmetricEmaOptions);
81
+ this.circuitBreaker = new CircuitBreaker(this.provider.get.bind(this.provider), {
82
+ timeout: 5000, // 5 seconds
83
+ }).fallback(() => {
84
+ const fallbackValue = this.provider.getCurrentEMA();
85
+ if (fallbackValue) {
86
+ logger.warn('Using Asymmetric EMA value as fallback for ResilientProvider');
87
+ return fallbackValue;
88
+ }
89
+ else {
90
+ logger.error('No Asymmetric EMA value available for fallback in ResilientProvider');
91
+ throw new Error('No Asymmetric EMA value available for fallback');
92
+ }
93
+ });
94
+ }
95
+ /**
96
+ * Fetches the value from the wrapped provider with resilience features.
97
+ * If the underlying provider fails or times out, falls back to the last known EMA value.
98
+ * @returns A promise that resolves to the EMA value
99
+ * @throws {Error} If no EMA value is available for fallback when the provider fails
100
+ */
101
+ get() {
102
+ return this.circuitBreaker.fire();
103
+ }
104
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Collection of built-in interpolators for common data types.
3
+ * Provides factory methods for creating interpolators for numbers, objects, arrays,
4
+ * and identity transformations.
5
+ */
6
+ export const Interpolators = {
7
+ /**
8
+ * Creates a linear interpolator for numbers.
9
+ * Uses the formula: alpha * newValue + (1 - alpha) * previousValue
10
+ * @returns An interpolator for numeric values
11
+ */
12
+ number: () => ({
13
+ interpolate: (newVal, prevVal, alpha) => alpha * newVal + (1 - alpha) * prevVal,
14
+ }),
15
+ /**
16
+ * Creates a composite interpolator for objects.
17
+ * Applies property-specific interpolators to each field of the object.
18
+ * @template T - The object type with string keys
19
+ * @param interpolators - A record mapping property names to their interpolators
20
+ * @returns An interpolator that applies the appropriate interpolator to each property
21
+ */
22
+ object: (interpolators) => ({
23
+ interpolate: (newVal, prevVal, alpha) => {
24
+ const result = { ...newVal };
25
+ for (const key in interpolators) {
26
+ if (Object.prototype.hasOwnProperty.call(interpolators, key)) {
27
+ const interpolator = interpolators[key];
28
+ result[key] = interpolator.interpolate(newVal[key], prevVal[key], alpha);
29
+ }
30
+ }
31
+ return result;
32
+ },
33
+ }),
34
+ /**
35
+ * Creates an interpolator for arrays.
36
+ * Applies element-wise interpolation using the provided element interpolator.
37
+ * Assumes arrays have matching lengths.
38
+ * @template T - The type of array elements
39
+ * @param elementInterpolator - The interpolator to apply to each array element
40
+ * @returns An interpolator that processes arrays element by element
41
+ */
42
+ array: (elementInterpolator) => ({
43
+ interpolate: (newVal, prevVal, alpha) => newVal.map((val, i) => elementInterpolator.interpolate(val, prevVal[i], alpha)),
44
+ }),
45
+ /**
46
+ * Creates an identity interpolator that always returns the new value.
47
+ * Useful when no interpolation is desired.
48
+ * @template T - The type of values
49
+ * @returns An interpolator that ignores the previous value and alpha
50
+ */
51
+ identity: () => ({
52
+ interpolate: (newVal) => newVal,
53
+ }),
54
+ };
@@ -0,0 +1,37 @@
1
+ import pRetry from 'p-retry';
2
+ import { logger } from '../utils/logger.js';
3
+ /**
4
+ * Provider that wraps another provider with automatic retry logic.
5
+ * Uses exponential backoff strategy to retry failed operations,
6
+ * making the provider more resilient to transient failures.
7
+ *
8
+ * @template T - The type of value this provider supplies
9
+ */
10
+ export class RetryableProvider {
11
+ /**
12
+ * Creates a new RetryableProvider.
13
+ * @param provider - The underlying provider to wrap with retry logic
14
+ * @param options - Configuration options for retry behavior
15
+ */
16
+ constructor(provider, options) {
17
+ this.provider = provider;
18
+ this.options = options;
19
+ }
20
+ /**
21
+ * Fetches a value from the wrapped provider with automatic retry on failure.
22
+ * Retries are performed according to the configured retry options with exponential backoff.
23
+ * @returns A promise that resolves to the provided value
24
+ * @throws {Error} If all retry attempts are exhausted
25
+ */
26
+ get() {
27
+ const run = async () => {
28
+ return this.provider.get();
29
+ };
30
+ return pRetry(run, {
31
+ ...this.options,
32
+ onFailedAttempt: (error) => {
33
+ logger.warn(`Attempt ${error.attemptNumber} failed. There are ${error.retriesLeft} retries left.`);
34
+ },
35
+ });
36
+ }
37
+ }
@@ -5,6 +5,7 @@ export const energyInformationSchema = z.object({
5
5
  export const mqttPushBridgeFeedSchema = z
6
6
  .object({
7
7
  url: z.string(),
8
+ device: z.string(),
8
9
  topic: z.string(),
9
10
  })
10
11
  .loose();
@@ -24,6 +25,7 @@ export const mqttPullMockFeedSchema = z
24
25
  export const mqttPullAdapterFeedSchema = z
25
26
  .object({
26
27
  interval: z.number().int().nonnegative(),
28
+ device: z.string(),
27
29
  ip: z.string(),
28
30
  })
29
31
  .loose();
@@ -56,7 +58,6 @@ export const mqttProviderSchema = z.object({
56
58
  provider: z.literal('mqtt'),
57
59
  properties: z.object({
58
60
  url: z.string(),
59
- device: z.string(),
60
61
  meters: mqttMetersSchema,
61
62
  }),
62
63
  });
@@ -42,7 +42,12 @@ export const em1StatusSchema = z.object({
42
42
  */
43
43
  flags: z.array(z.string()).optional(),
44
44
  });
45
- export const restAdapterFeedSchema = z.object({ ip: z.string() }).loose();
45
+ export const restAdapterFeedSchema = z
46
+ .object({
47
+ ip: z.string(),
48
+ device: z.string(),
49
+ })
50
+ .loose();
46
51
  export const restMockFeedSchema = z
47
52
  .object({ value: em1StatusSchema.optional() })
48
53
  .loose();
@@ -67,7 +72,6 @@ export const restProviderSchema = z.object({
67
72
  provider: z.literal('rest'),
68
73
  properties: z.object({
69
74
  port: z.number().int().positive(),
70
- device: z.string(),
71
75
  meters: restMetersSchema,
72
76
  }),
73
77
  });
@@ -91,8 +91,6 @@ export class RestService extends AbstractExecutableService {
91
91
  return reply.send({ code: -1, message: err.message });
92
92
  }
93
93
  });
94
- // Alias for JSON-RPC style (POST)
95
- //app.post('/rpc/EM1.GetStatus', async () => app.inject({ method: 'GET', url: '/rpc/EM1.GetStatus' }).then(r => r.json()));
96
94
  await app.listen({ port: this.properties.port, host: '0.0.0.0' });
97
95
  logger.info({ port: this.properties.port }, 'Listening');
98
96
  logger.info('REST service started');
@@ -0,0 +1,13 @@
1
+ import { Interpolators } from '../provider/interpolator.js';
2
+ export const em1StatusInterpolator = Interpolators.object({
3
+ id: Interpolators.identity(),
4
+ calibration: Interpolators.identity(),
5
+ current: Interpolators.number(),
6
+ voltage: Interpolators.number(),
7
+ act_power: Interpolators.number(),
8
+ aprt_power: Interpolators.number(),
9
+ pf: Interpolators.number(),
10
+ freq: Interpolators.number(),
11
+ errors: Interpolators.identity(),
12
+ flags: Interpolators.identity(),
13
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "v2c-any",
3
- "version": "0.1.9",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "Turn any device into V2C Dynamic Power Control",
6
6
  "main": "dist/index.js",
@@ -50,6 +50,8 @@
50
50
  "fastify": "^5.0.0",
51
51
  "glob": "^13.0.0",
52
52
  "mqtt": "^5.10.3",
53
+ "opossum": "^9.0.0",
54
+ "p-retry": "^7.1.1",
53
55
  "pino-pretty": "^13.1.3",
54
56
  "undici": "^7.1.0",
55
57
  "zod": "^4.2.1"
@@ -57,6 +59,7 @@
57
59
  "devDependencies": {
58
60
  "@eslint/js": "^9.17.0",
59
61
  "@types/node": "^22.10.1",
62
+ "@types/opossum": "^8.1.9",
60
63
  "eslint": "^9.17.0",
61
64
  "eslint-config-prettier": "^9.1.0",
62
65
  "prettier": "^3.4.2",