v2c-any 0.1.2

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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +230 -0
  3. package/dist/adapter/adapter.js +1 -0
  4. package/dist/application-context.js +31 -0
  5. package/dist/configuration/configuration-loader.js +48 -0
  6. package/dist/configuration/configuration-validator.js +24 -0
  7. package/dist/device/shelly-pro-em/em1-status-provider.js +49 -0
  8. package/dist/device/shelly-pro-em/energy-information-em1-status-adapter.js +24 -0
  9. package/dist/device/shelly-pro-em/index.js +8 -0
  10. package/dist/factory/em1-status-provider-factory.js +48 -0
  11. package/dist/factory/executable-service-factory.js +1 -0
  12. package/dist/factory/mqtt-feed-executable-service-factory.js +68 -0
  13. package/dist/factory/mqtt-pull-executable-service-factory.js +73 -0
  14. package/dist/factory/mqtt-push-executable-service-factory.js +36 -0
  15. package/dist/factory/mqtt-service-factory.js +61 -0
  16. package/dist/factory/rest-service-factory.js +35 -0
  17. package/dist/index.js +53 -0
  18. package/dist/provider/adapter-provider.js +54 -0
  19. package/dist/provider/factory.js +1 -0
  20. package/dist/provider/fixed-value-provider.js +54 -0
  21. package/dist/provider/provider-factory.js +1 -0
  22. package/dist/provider/provider.js +1 -0
  23. package/dist/registry/registry.js +27 -0
  24. package/dist/schema/configuration.js +7 -0
  25. package/dist/schema/expectation-body.js +15 -0
  26. package/dist/schema/mqtt-configuration.js +62 -0
  27. package/dist/schema/rest-configuration.js +73 -0
  28. package/dist/schema/status-query.js +7 -0
  29. package/dist/service/executable-service.js +1 -0
  30. package/dist/service/mqtt-bridge-service.js +55 -0
  31. package/dist/service/mqtt-service.js +79 -0
  32. package/dist/service/no-op-executable-service.js +25 -0
  33. package/dist/service/pull-push-service.js +81 -0
  34. package/dist/service/rest-service.js +107 -0
  35. package/dist/template/response.js +8 -0
  36. package/dist/utils/callback-properties.js +1 -0
  37. package/dist/utils/logger.js +15 -0
  38. package/dist/utils/mappers.js +18 -0
  39. package/dist/utils/mqtt-callbacks.js +1 -0
  40. package/dist/utils/mqtt.js +21 -0
  41. package/package.json +50 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Tiago Santos
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,230 @@
1
+ ![v2a - V2C any](docs/assets/images/v2ca.png)
2
+
3
+ # v2c-any (v2ca)
4
+
5
+ > **Turn `{ device: any }` into V2C Dynamic Power Control**
6
+
7
+ **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**.
8
+
9
+ If it can expose power data, **v2c-any** can make it speak *V2C*.
10
+
11
+ ## Why v2c-any?
12
+
13
+ V2C wallboxes support Dynamic Power Control via specific meters or MQTT inputs.
14
+ In real installations, however, power data often comes from **heterogeneous sources**:
15
+
16
+ - Different brands of energy meters
17
+ - Existing MQTT infrastructures
18
+ - Home Assistant sensors
19
+ - Custom hardware or software systems
20
+ - Simulated or virtual meters for testing
21
+
22
+ **v2c-any** bridges that gap.
23
+
24
+ It adapts **any input** into the protocol and format expected by a V2C wallbox — without changing your existing setup.
25
+
26
+ ## The idea
27
+
28
+ ```ts
29
+ { device: any } → V2C
30
+ ```
31
+
32
+ Or in practical terms:
33
+
34
+ ```
35
+ [Any Meter | MQTT | API | Simulator]
36
+
37
+
38
+ v2c-any
39
+
40
+
41
+ V2C Wallbox
42
+ ```
43
+
44
+ ## Key features
45
+
46
+ - 🔌 **Universal adapter** – works with *any* power data source
47
+ - 📡 **MQTT support** – publish once, charge dynamically
48
+ - ⚡ **Dynamic Power Control** – grid, solar, or hybrid scenarios
49
+ - 🧪 **Simulation mode** – emulate supported meters for testing
50
+ - 🔁 **Proxy mode** – forward and transform existing devices
51
+ - 🧩 **Extensible architecture** – add new adapters easily
52
+ - 🟦 **TypeScript-first** – predictable, typed, maintainable
53
+
54
+ ## Quick Start
55
+
56
+ ### Installation
57
+
58
+ ```bash
59
+ # Install globally via npm
60
+ npm install -g v2c-any
61
+
62
+ # Or run directly with npx
63
+ npx v2c-any
64
+ ```
65
+
66
+ ### Configuration
67
+
68
+ `v2ca` uses [cosmiconfig](https://github.com/cosmiconfig/cosmiconfig) to load configuration. Create a configuration file in one of these formats:
69
+
70
+ - `.v2carc` (JSON or YAML)
71
+ - `.v2carc.json`
72
+ - `.v2carc.yaml` or `.v2carc.yml`
73
+ - `v2ca.config.js` (CommonJS or ESM)
74
+ - `package.json` with a `"v2ca"` key
75
+
76
+ **Example configuration** (`.v2carc.yaml`):
77
+
78
+ ```yaml
79
+ provider: mqtt
80
+ properties:
81
+ url: mqtt://localhost:1883
82
+ device: shelly-pro-em
83
+ meters:
84
+ grid:
85
+ mode: pull
86
+ feed:
87
+ type: adapter
88
+ properties:
89
+ interval: 5000
90
+ ip: 192.168.1.100
91
+ solar:
92
+ mode: pull
93
+ feed:
94
+ type: mock
95
+ properties:
96
+ interval: 5000
97
+ value:
98
+ power: 2500
99
+ ```
100
+
101
+ ### Running
102
+
103
+ ```bash
104
+ # Run with auto-detected configuration
105
+ v2ca
106
+
107
+ # Run from source (development)
108
+ npm run dev
109
+
110
+ # Run with Docker
111
+ docker run -v $(pwd)/.v2carc.yaml:/app/.v2carc.yaml v2c-any
112
+ ```
113
+
114
+ ## Operating Modes
115
+
116
+ `v2c-any` supports two primary operating modes depending on how your V2C wallbox is configured:
117
+
118
+ ### REST Mode (Shelly EM1 Emulator)
119
+
120
+ Emulates a **Shelly Pro EM** energy meter by exposing a REST API that V2C wallboxes can poll.
121
+
122
+ **Use this when:**
123
+ - Your V2C wallbox is configured to poll a Shelly meter
124
+ - You want to act as a drop-in replacement for physical hardware
125
+ - You prefer a pull-based (polling) approach
126
+
127
+ **Configuration example:**
128
+
129
+ ```yaml
130
+ provider: rest
131
+ properties:
132
+ port: 3000
133
+ device: shelly-pro-em
134
+ meters:
135
+ grid:
136
+ feed:
137
+ type: adapter
138
+ properties:
139
+ ip: 192.168.1.100
140
+ solar:
141
+ feed:
142
+ type: mock
143
+ properties:
144
+ value:
145
+ id: 1
146
+ voltage: 230.2
147
+ current: 3.785
148
+ act_power: 852.7
149
+ aprt_power: 873.1
150
+ pf: 0.98
151
+ freq: 50
152
+ calibration: factory
153
+ ```
154
+
155
+ **How it works:**
156
+ 1. `v2ca` starts a Fastify HTTP server
157
+ 2. Exposes endpoints matching Shelly Pro EM API format
158
+ 3. V2C wallbox polls these endpoints (e.g., `/rpc/EM1.GetStatus?id=0`)
159
+ 4. Returns real-time power data from your configured sources
160
+
161
+ ### MQTT Mode (Direct Publisher)
162
+
163
+ Publishes power data directly to MQTT topics that V2C wallboxes subscribe to.
164
+
165
+ **Use this when:**
166
+ - Your V2C wallbox is configured for MQTT integration
167
+ - You have an existing MQTT broker
168
+ - You want push-based (event-driven) updates
169
+ - You need lower latency or more frequent updates
170
+
171
+ **Configuration example:**
172
+
173
+ ```yaml
174
+ provider: mqtt
175
+ properties:
176
+ url: mqtt://broker.local:1883
177
+ device: shelly-pro-em
178
+ meters:
179
+ grid:
180
+ mode: pull # v2ca polls your device
181
+ feed:
182
+ type: adapter
183
+ properties:
184
+ interval: 2000 # Every 2 seconds
185
+ ip: 192.168.1.100
186
+ solar:
187
+ mode: push # v2ca subscribes to MQTT topic
188
+ feed:
189
+ type: bridge
190
+ properties:
191
+ url: mqtt://solar-meter.local:1883
192
+ topic: solar/power
193
+ ```
194
+
195
+ **How it works:**
196
+ 1. `v2ca` connects to your MQTT broker
197
+ 2. Publishes to V2C-expected topics (e.g., `trydan_v2c_sun_power`)
198
+ 3. Supports both **pull** (polling devices) and **push** (subscribing to topics)
199
+ 4. V2C wallbox subscribes and receives real-time updates
200
+
201
+ ### Mode Comparison
202
+
203
+ | Feature | REST Mode | MQTT Mode |
204
+ |-----------------|----------------------------|-----------------------------|
205
+ | **Protocol** | HTTP/REST | MQTT |
206
+ | **Direction** | Pull (V2C polls v2ca) | Push (v2ca publishes) |
207
+ | **Latency** | Higher (polling interval) | Lower (event-driven) |
208
+ | **Setup** | Simpler (no broker needed) | Requires MQTT broker |
209
+ | **Use Case** | Shelly meter replacement | MQTT-native setups |
210
+ | **Scalability** | Limited by polling | Better for multiple devices |
211
+
212
+ ## What v2c-any is *not*
213
+
214
+ - ❌ Not a replacement for your existing meters
215
+ - ❌ Not tied to a single vendor or ecosystem
216
+ - ❌ Not limited to one communication protocol
217
+
218
+ It’s an **adapter**, not a lock-in.
219
+
220
+ ## Name origin
221
+
222
+ `v2c-any` comes from the TypeScript `any` type:
223
+
224
+ > “I don’t care what you are — I can work with you.”
225
+
226
+ Exactly the philosophy behind this project.
227
+
228
+ ## License
229
+
230
+ MIT License - see [LICENSE](LICENSE) for details.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ import { Registry } from './registry/registry.js';
2
+ import { fileURLToPath } from 'url';
3
+ import { dirname } from 'path';
4
+ import { globSync } from 'glob';
5
+ import { logger } from './utils/logger.js';
6
+ /**
7
+ * Registry of device provider factories keyed by device identifier.
8
+ * Each factory produces an `EM1Status` provider for a specific device.
9
+ */
10
+ export const devicesProviderRegistry = new Registry();
11
+ /**
12
+ * Registry of device adapters keyed by device identifier.
13
+ * Each adapter transforms raw device messages into `EnergyInformation`.
14
+ */
15
+ export const devicesAdapterRegistry = new Registry();
16
+ /**
17
+ * Dynamically loads all device modules discovered under devices.
18
+ * Imports each module to allow self-registration into application registries.
19
+ *
20
+ * @returns A promise that resolves when all device modules are loaded
21
+ */
22
+ export async function loadDeviceModules() {
23
+ logger.info('Loading device modules...');
24
+ const __dirname = dirname(fileURLToPath(import.meta.url));
25
+ // Support both .ts (dev/tsx) and .js (built) files
26
+ const devicePaths = globSync('device/**/index.{ts,js}', { cwd: __dirname });
27
+ logger.info({ count: devicePaths.length }, 'Found device modules.');
28
+ for (const path of devicePaths) {
29
+ await import(`./${path}`);
30
+ }
31
+ }
@@ -0,0 +1,48 @@
1
+ import { cosmiconfig } from 'cosmiconfig';
2
+ import { logger } from '../utils/logger.js';
3
+ /**
4
+ * Loads and merges v2ca configuration from various sources (.v2carc, v2ca.config.js, package.json).
5
+ * Uses cosmiconfig for auto-discovery and merges user config with defaults.
6
+ */
7
+ export class ConfigurationLoader {
8
+ /**
9
+ * Creates a new configuration loader.
10
+ * @param configurationValidator - Validator to ensure configuration integrity
11
+ */
12
+ constructor(configurationValidator) {
13
+ this.configurationValidator = configurationValidator;
14
+ // Initialize cosmiconfig explorer once for reuse across multiple loads
15
+ this.explorer = cosmiconfig('v2ca');
16
+ }
17
+ /**
18
+ * Loads and validates the v2ca configuration.
19
+ * @returns A promise that resolves to the fully merged and validated configuration
20
+ * @throws {Error} If configuration loading or validation fails
21
+ */
22
+ async load() {
23
+ try {
24
+ logger.info('Searching for configuration...');
25
+ // Search for config in standard locations
26
+ const result = await this.explorer.search();
27
+ let config;
28
+ if (result?.config) {
29
+ // Configuration found - merge, validate, and use it
30
+ const configSource = result.filepath ? result.filepath : 'package.json';
31
+ logger.info({ source: configSource }, 'Configuration loaded');
32
+ config = this.configurationValidator.validate(result.config);
33
+ }
34
+ else {
35
+ // No configuration found - use defaults
36
+ logger.info('No configuration found');
37
+ throw new Error('No configuration found');
38
+ }
39
+ logger.info('Configuration loaded successfully');
40
+ return config;
41
+ }
42
+ catch (error) {
43
+ // Wrap any errors with context for better debugging
44
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
45
+ throw new Error(`Failed to load configuration: ${error}`);
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,24 @@
1
+ import { configurationSchema, } from '../schema/configuration.js';
2
+ /**
3
+ * Validates v2ca configuration against Zod schema for type safety and correctness.
4
+ * Ensures all configuration values conform to expected types and constraints,
5
+ * providing detailed error messages for any validation failures.
6
+ */
7
+ export class ConfigurationValidator {
8
+ /**
9
+ * Validates configuration against the schema and returns the validated result.
10
+ * @param config - Configuration object to validate
11
+ * @returns Validated and typed configuration object
12
+ * @throws {Error} If validation fails, with detailed error information
13
+ */
14
+ validate(config) {
15
+ const result = configurationSchema.safeParse(config);
16
+ if (!result.success) {
17
+ const errors = result.error.issues
18
+ .map((e) => `${e.path.join('.')}:${e.message}`)
19
+ .join(', ');
20
+ throw new Error(`Configuration validation failed: ${errors}`);
21
+ }
22
+ return result.data;
23
+ }
24
+ }
@@ -0,0 +1,49 @@
1
+ import { request } from 'undici';
2
+ import { logger } from '../../utils/logger.js';
3
+ import { energyTypeToId } from '../../utils/mappers.js';
4
+ /**
5
+ * Provider that fetches EM1 status data from a Shelly Pro EM device via HTTP.
6
+ * Retrieves real-time energy monitoring data by querying the device's RPC API endpoint.
7
+ */
8
+ export class EM1StatusProvider {
9
+ /**
10
+ * Creates a new EM1 status provider.
11
+ * @param host - The IP address or hostname of the Shelly Pro EM device
12
+ * @param energyType - The type of energy data to retrieve (e.g., active, reactive)
13
+ */
14
+ constructor(host, energyType) {
15
+ this.host = host;
16
+ this.energyType = energyType;
17
+ }
18
+ /**
19
+ * Fetches the current EM1 status from the device.
20
+ * @returns A promise that resolves to the EM1 status object containing energy metrics
21
+ * @throws {Error} If the HTTP request fails or returns invalid data
22
+ */
23
+ async get() {
24
+ 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' });
27
+ return (await res.body.json());
28
+ }
29
+ }
30
+ /**
31
+ * Factory for creating EM1StatusProvider instances.
32
+ * Implements the factory pattern to instantiate providers with the appropriate configuration.
33
+ */
34
+ class EM1StatusProviderFactory {
35
+ /**
36
+ * Creates a new EM1StatusProvider instance with the specified configuration.
37
+ * @param options - Configuration options including target IP and energy type
38
+ * @returns A configured EM1StatusProvider instance
39
+ */
40
+ create(options) {
41
+ logger.debug({ ip: options.properties.ip, energyType: options.energyType }, 'Creating EM1StatusProvider');
42
+ return new EM1StatusProvider(options.properties.ip, options.energyType);
43
+ }
44
+ }
45
+ /**
46
+ * Singleton factory instance for creating `EM1StatusProvider` objects.
47
+ * Provides a ready-to-use factory to build providers with supplied options.
48
+ */
49
+ export const em1StatusProviderFactory = new EM1StatusProviderFactory();
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Adapter that transforms EM1Status data into EnergyInformation format.
3
+ * Extracts active power metrics from Shelly Pro EM device status and converts them
4
+ * to a standardized energy information structure.
5
+ */
6
+ class EnergyInformationEM1StatusAdapter {
7
+ /**
8
+ * Adapts EM1 status data to energy information.
9
+ * Extracts the active power value if available, otherwise returns undefined.
10
+ * @param input - The EM1Status object containing device status data
11
+ * @returns A promise that resolves to EnergyInformation with power data, or undefined if power data is unavailable
12
+ */
13
+ adapt(input) {
14
+ if (input.act_power !== undefined) {
15
+ return Promise.resolve({ power: input.act_power });
16
+ }
17
+ return Promise.resolve(undefined);
18
+ }
19
+ }
20
+ /**
21
+ * Singleton adapter instance for converting `EM1Status` to `EnergyInformation`.
22
+ * Use this ready-to-use instance where an adapter object is required.
23
+ */
24
+ export const energyInformationEM1StatusAdapter = new EnergyInformationEM1StatusAdapter();
@@ -0,0 +1,8 @@
1
+ import { devicesAdapterRegistry, devicesProviderRegistry, } from '../../application-context.js';
2
+ import { logger } from '../../utils/logger.js';
3
+ import { energyInformationEM1StatusAdapter } from './energy-information-em1-status-adapter.js';
4
+ import { em1StatusProviderFactory } from './em1-status-provider.js';
5
+ const DEVICE_NAME = 'shelly-pro-em';
6
+ devicesProviderRegistry.register(DEVICE_NAME, em1StatusProviderFactory);
7
+ devicesAdapterRegistry.register(DEVICE_NAME, energyInformationEM1StatusAdapter);
8
+ logger.info('Shelly Pro EM registered');
@@ -0,0 +1,48 @@
1
+ import { FixedValueProviderFactory } from '../provider/fixed-value-provider.js';
2
+ /**
3
+ * Factory for creating EM1Status providers with flexible data source strategies.
4
+ * Supports multiple feed types: adapter-based providers, mock values, or disabled providers.
5
+ */
6
+ export class EM1StatusProviderFactory {
7
+ /**
8
+ * Creates a new EM1Status provider factory.
9
+ * @param devicesProvider - Registry containing provider factories for different devices
10
+ */
11
+ constructor(devicesProvider) {
12
+ this.devicesProvider = devicesProvider;
13
+ }
14
+ /**
15
+ * Creates the appropriate provider factory based on the feed configuration type.
16
+ * @param options - Configuration options specifying the feed type and properties
17
+ * @returns A provider factory matching the requested feed type
18
+ * @throws {Error} If an adapter feed is requested but the device is not registered
19
+ */
20
+ createProviderFactory(options) {
21
+ switch (options.configuration.feed.type) {
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}`);
26
+ }
27
+ return provider;
28
+ }
29
+ case 'mock':
30
+ return new FixedValueProviderFactory({
31
+ value: options.configuration.feed.properties?.value,
32
+ });
33
+ case 'off':
34
+ return new FixedValueProviderFactory({ value: undefined });
35
+ }
36
+ }
37
+ /**
38
+ * Creates an EM1Status provider with the specified configuration.
39
+ * @param options - Configuration options including energy type, device, and feed type
40
+ * @returns A provider that supplies EM1Status or undefined
41
+ */
42
+ create(options) {
43
+ return this.createProviderFactory(options).create({
44
+ energyType: options.energyType,
45
+ ...options.configuration.feed,
46
+ });
47
+ }
48
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Converts numeric callback properties to accept EnergyInformation objects.
3
+ * Extracts the power value from EnergyInformation and passes it to the underlying callback.
4
+ *
5
+ * @param callbackProperties - The callback properties expecting numeric power values
6
+ * @returns Converted callback properties that accept EnergyInformation
7
+ */
8
+ function convertCallbackProperties(callbackProperties) {
9
+ return {
10
+ callback: async (data) => {
11
+ if (data?.power !== undefined) {
12
+ await callbackProperties.callback(data.power);
13
+ }
14
+ },
15
+ };
16
+ }
17
+ /**
18
+ * Factory for creating MQTT feed executable services with flexible communication modes.
19
+ * Supports both pull (periodic polling) and push (event-driven) MQTT feed strategies.
20
+ * Automatically routes to the appropriate service factory based on configuration.
21
+ */
22
+ export class MqttFeedExecutableServiceFactory {
23
+ /**
24
+ * Creates a new MQTT feed executable service factory.
25
+ * @param pullExecutableServiceFactory - Factory for pull-mode services
26
+ * @param pushExecutableServiceFactory - Factory for push-mode services
27
+ */
28
+ constructor(pullExecutableServiceFactory, pushExecutableServiceFactory) {
29
+ this.pullExecutableServiceFactory = pullExecutableServiceFactory;
30
+ this.pushExecutableServiceFactory = pushExecutableServiceFactory;
31
+ }
32
+ /**
33
+ * Creates an executable service using the appropriate mode-specific factory.
34
+ * Routes to pull or push factory based on the configuration mode.
35
+ *
36
+ * @param options - Configuration options including energy type, device, mode, and callbacks
37
+ * @returns An ExecutableService configured for the specified MQTT feed mode
38
+ */
39
+ create(options) {
40
+ let callbackProperties;
41
+ switch (options.energyType) {
42
+ case 'solar': {
43
+ callbackProperties = convertCallbackProperties(options.callbacks.sun);
44
+ break;
45
+ }
46
+ case 'grid': {
47
+ callbackProperties = convertCallbackProperties(options.callbacks.grid);
48
+ break;
49
+ }
50
+ }
51
+ switch (options.configuration.mode) {
52
+ case 'pull':
53
+ return this.pullExecutableServiceFactory.create({
54
+ energyType: options.energyType,
55
+ device: options.device,
56
+ configuration: options.configuration.feed,
57
+ callbackProperties,
58
+ });
59
+ case 'push':
60
+ return this.pushExecutableServiceFactory.create({
61
+ energyType: options.energyType,
62
+ device: options.device,
63
+ configuration: options.configuration.feed,
64
+ callbackProperties,
65
+ });
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,73 @@
1
+ import { AdapterProviderFactory } from '../provider/adapter-provider.js';
2
+ import { FixedValueProviderFactory } from '../provider/fixed-value-provider.js';
3
+ import { PullPushService } from '../service/pull-push-service.js';
4
+ import { noOpExecutableService } from '../service/no-op-executable-service.js';
5
+ /**
6
+ * Factory for creating MQTT pull-mode (polling) executable services.
7
+ * Supports multiple data source strategies: device adapters, mock values, or disabled sources.
8
+ * Periodically polls energy data and invokes a callback with the results.
9
+ */
10
+ export class MqttPullExecutableServiceFactory {
11
+ /**
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
15
+ */
16
+ constructor(devicesProviderRegistry, devicesAdapterRegistry) {
17
+ this.devicesProviderRegistry = devicesProviderRegistry;
18
+ this.devicesAdapterRegistry = devicesAdapterRegistry;
19
+ }
20
+ /**
21
+ * Creates the appropriate provider factory based on the feed configuration type.
22
+ * Combines provider and adapter for adapter-based sources, or returns mock/off providers.
23
+ *
24
+ * @param options - Configuration options specifying the feed type and device
25
+ * @returns A provider factory matching the requested feed type
26
+ * @throws {Error} If an adapter feed is requested but the device provider or adapter is not registered
27
+ */
28
+ createProviderFactory(options) {
29
+ switch (options.configuration.type) {
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}`);
34
+ }
35
+ const adapter = this.devicesAdapterRegistry.get(options.device);
36
+ if (!adapter) {
37
+ throw new Error(`No adapter registered for device: ${options.device}`);
38
+ }
39
+ return {
40
+ providerFactory: new AdapterProviderFactory(provider, adapter),
41
+ interval: options.configuration.properties.interval,
42
+ };
43
+ }
44
+ case 'mock':
45
+ return {
46
+ providerFactory: new FixedValueProviderFactory({
47
+ value: options.configuration.properties.value,
48
+ }),
49
+ interval: options.configuration.properties.interval,
50
+ };
51
+ case 'off':
52
+ return null;
53
+ }
54
+ }
55
+ /**
56
+ * Creates an executable service that periodically polls energy data.
57
+ * @param options - Configuration options including device, interval, and callback
58
+ * @returns A PullPushService configured to poll at the specified interval
59
+ */
60
+ create(options) {
61
+ const energyProviderFactory = this.createProviderFactory(options);
62
+ if (!energyProviderFactory) {
63
+ return noOpExecutableService;
64
+ }
65
+ const { providerFactory, interval } = energyProviderFactory;
66
+ const energyProvider = providerFactory.create({
67
+ energyType: options.energyType,
68
+ ...options.configuration,
69
+ });
70
+ const service = new PullPushService(energyProvider, interval, options.callbackProperties);
71
+ return service;
72
+ }
73
+ }
@@ -0,0 +1,36 @@
1
+ import { noOpExecutableService } from '../service/no-op-executable-service.js';
2
+ import { MqttBridgeService } from '../service/mqtt-bridge-service.js';
3
+ /**
4
+ * Factory for creating MQTT push-mode (event-driven) executable services.
5
+ * Supports MQTT bridge (subscribing to device topics) or disabled sources.
6
+ * Returns a ready-to-run `ExecutableService` instance.
7
+ */
8
+ export class MqttPushExecutableServiceFactory {
9
+ /**
10
+ * Creates a new MQTT push executable service factory.
11
+ * @param devicesAdapter - Registry of device adapters to transform incoming messages
12
+ */
13
+ constructor(devicesAdapter) {
14
+ this.devicesAdapter = devicesAdapter;
15
+ }
16
+ /**
17
+ * Creates an executable service using the appropriate push strategy.
18
+ * Returns a bridge service when configured, or a no-op service if disabled.
19
+ * @param options - Configuration including device, energy type, push config, and callback
20
+ * @returns An `ExecutableService` configured for the specified MQTT push mode
21
+ * @throws {Error} If `bridge` is selected but no adapter is registered for the device
22
+ */
23
+ create(options) {
24
+ switch (options.configuration.type) {
25
+ case 'bridge': {
26
+ const adapter = this.devicesAdapter.get(options.device);
27
+ if (!adapter) {
28
+ throw new Error(`No adapter registered for device: ${options.device}`);
29
+ }
30
+ return new MqttBridgeService(options.configuration.properties, options.callbackProperties, adapter);
31
+ }
32
+ case 'off':
33
+ return noOpExecutableService;
34
+ }
35
+ }
36
+ }