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.
- package/LICENSE +21 -0
- package/README.md +230 -0
- package/dist/adapter/adapter.js +1 -0
- package/dist/application-context.js +31 -0
- package/dist/configuration/configuration-loader.js +48 -0
- package/dist/configuration/configuration-validator.js +24 -0
- package/dist/device/shelly-pro-em/em1-status-provider.js +49 -0
- package/dist/device/shelly-pro-em/energy-information-em1-status-adapter.js +24 -0
- package/dist/device/shelly-pro-em/index.js +8 -0
- package/dist/factory/em1-status-provider-factory.js +48 -0
- package/dist/factory/executable-service-factory.js +1 -0
- package/dist/factory/mqtt-feed-executable-service-factory.js +68 -0
- package/dist/factory/mqtt-pull-executable-service-factory.js +73 -0
- package/dist/factory/mqtt-push-executable-service-factory.js +36 -0
- package/dist/factory/mqtt-service-factory.js +61 -0
- package/dist/factory/rest-service-factory.js +35 -0
- package/dist/index.js +53 -0
- package/dist/provider/adapter-provider.js +54 -0
- package/dist/provider/factory.js +1 -0
- package/dist/provider/fixed-value-provider.js +54 -0
- package/dist/provider/provider-factory.js +1 -0
- package/dist/provider/provider.js +1 -0
- package/dist/registry/registry.js +27 -0
- package/dist/schema/configuration.js +7 -0
- package/dist/schema/expectation-body.js +15 -0
- package/dist/schema/mqtt-configuration.js +62 -0
- package/dist/schema/rest-configuration.js +73 -0
- package/dist/schema/status-query.js +7 -0
- package/dist/service/executable-service.js +1 -0
- package/dist/service/mqtt-bridge-service.js +55 -0
- package/dist/service/mqtt-service.js +79 -0
- package/dist/service/no-op-executable-service.js +25 -0
- package/dist/service/pull-push-service.js +81 -0
- package/dist/service/rest-service.js +107 -0
- package/dist/template/response.js +8 -0
- package/dist/utils/callback-properties.js +1 -0
- package/dist/utils/logger.js +15 -0
- package/dist/utils/mappers.js +18 -0
- package/dist/utils/mqtt-callbacks.js +1 -0
- package/dist/utils/mqtt.js +21 -0
- 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
|
+

|
|
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
|
+
}
|