ecoflow2mqtt 0.0.1 → 0.2.0

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/lib/items.js ADDED
@@ -0,0 +1,211 @@
1
+ /**
2
+ * The item table and the small amount of state behind it.
3
+ *
4
+ * One row per published item (ROADMAP.md E-7: a new reading is a row, not code). `field` is the
5
+ * protobuf field of `DisplayPropertyUpload`, `precision` the decimals the device's float32 values
6
+ * are worth, `measurement` whether the value is a reading (Home Assistant `state_class`) rather
7
+ * than a setting, `category` marks the ones Home Assistant should file under diagnostics.
8
+ *
9
+ * `pv_watts` is the only computed item: the STREAM Microinverter firmware never sends
10
+ * `pow_get_pv_sum`, so the adapter adds the inputs up (E-4).
11
+ */
12
+
13
+ const SOLAR = 'mdi:solar-power-variant';
14
+
15
+ /** `grid_connection_sta` (R §4.5). */
16
+ export const GRID_STATUS = {
17
+ 0: 'invalid',
18
+ 1: 'grid_in',
19
+ 2: 'offline',
20
+ 3: 'feed_grid',
21
+ };
22
+
23
+ export const ITEMS = [
24
+ {item: 'pv1_watts', field: 'powGetPv', label: 'PV 1', unit: 'W', deviceClass: 'power', icon: SOLAR},
25
+ {item: 'pv2_watts', field: 'powGetPv2', label: 'PV 2', unit: 'W', deviceClass: 'power', icon: SOLAR},
26
+ {
27
+ item: 'pv_watts',
28
+ field: 'powGetPvSum',
29
+ label: 'PV total',
30
+ unit: 'W',
31
+ deviceClass: 'power',
32
+ icon: SOLAR,
33
+ computed: true,
34
+ },
35
+ {
36
+ item: 'grid_watts',
37
+ field: 'gridConnectionPower',
38
+ label: 'Grid',
39
+ unit: 'W',
40
+ deviceClass: 'power',
41
+ icon: 'mdi:transmission-tower',
42
+ },
43
+ {
44
+ item: 'grid_status',
45
+ field: 'gridConnectionSta',
46
+ label: 'Grid status',
47
+ map: GRID_STATUS,
48
+ icon: 'mdi:transmission-tower-export',
49
+ },
50
+ {
51
+ item: 'pv1_volts',
52
+ field: 'plugInInfoPvVol',
53
+ label: 'PV 1 voltage',
54
+ unit: 'V',
55
+ deviceClass: 'voltage',
56
+ precision: 2,
57
+ category: 'diagnostic',
58
+ },
59
+ {
60
+ item: 'pv1_amps',
61
+ field: 'plugInInfoPvAmp',
62
+ label: 'PV 1 current',
63
+ unit: 'A',
64
+ deviceClass: 'current',
65
+ precision: 2,
66
+ category: 'diagnostic',
67
+ },
68
+ {
69
+ item: 'pv2_volts',
70
+ field: 'plugInInfoPv2Vol',
71
+ label: 'PV 2 voltage',
72
+ unit: 'V',
73
+ deviceClass: 'voltage',
74
+ precision: 2,
75
+ category: 'diagnostic',
76
+ },
77
+ {
78
+ item: 'pv2_amps',
79
+ field: 'plugInInfoPv2Amp',
80
+ label: 'PV 2 current',
81
+ unit: 'A',
82
+ deviceClass: 'current',
83
+ precision: 2,
84
+ category: 'diagnostic',
85
+ },
86
+ {
87
+ item: 'grid_volts',
88
+ field: 'gridConnectionVol',
89
+ label: 'Grid voltage',
90
+ unit: 'V',
91
+ deviceClass: 'voltage',
92
+ precision: 1,
93
+ category: 'diagnostic',
94
+ },
95
+ {
96
+ item: 'grid_amps',
97
+ field: 'gridConnectionAmp',
98
+ label: 'Grid current',
99
+ unit: 'A',
100
+ deviceClass: 'current',
101
+ precision: 2,
102
+ category: 'diagnostic',
103
+ },
104
+ {
105
+ item: 'grid_hz',
106
+ field: 'gridConnectionFreq',
107
+ label: 'Grid frequency',
108
+ unit: 'Hz',
109
+ deviceClass: 'frequency',
110
+ precision: 2,
111
+ category: 'diagnostic',
112
+ },
113
+ {
114
+ item: 'feed_limit_watts',
115
+ field: 'feedGridModePowLimit',
116
+ label: 'Feed-in limit',
117
+ unit: 'W',
118
+ deviceClass: 'power',
119
+ precision: 0,
120
+ measurement: false,
121
+ category: 'diagnostic',
122
+ icon: 'mdi:speedometer-slow',
123
+ },
124
+ {
125
+ item: 'feed_limit_max_watts',
126
+ field: 'feedGridModePowMax',
127
+ label: 'Feed-in limit maximum',
128
+ unit: 'W',
129
+ deviceClass: 'power',
130
+ precision: 0,
131
+ measurement: false,
132
+ category: 'diagnostic',
133
+ icon: 'mdi:speedometer',
134
+ },
135
+ {
136
+ item: 'wifi_rssi',
137
+ field: 'moduleWifiRssi',
138
+ label: 'Wi-Fi signal',
139
+ unit: 'dBm',
140
+ deviceClass: 'signal_strength',
141
+ precision: 0,
142
+ category: 'diagnostic',
143
+ icon: 'mdi:wifi',
144
+ },
145
+ ];
146
+
147
+ /** Round to the decimals a row is worth — the device sends float32, the rest is float64 noise. */
148
+ export function round(value, precision = 1) {
149
+ const factor = 10 ** precision;
150
+ return Math.round(value * factor) / factor;
151
+ }
152
+
153
+ /**
154
+ * Turns decoded `DisplayPropertyUpload` payloads into items to publish.
155
+ *
156
+ * Frames are incremental: one carries only what changed, so only the items present in it come
157
+ * back. `pv_watts` follows whenever an input was in the frame and both inputs are known — or
158
+ * directly, should a future firmware ever send `pow_get_pv_sum`.
159
+ *
160
+ * @returns {{update: (data: object) => Array<{item: string, value: number|string}>,
161
+ * get: (item: string) => number|string|undefined, known: () => object}}
162
+ */
163
+ export function createItems() {
164
+ const state = new Map();
165
+
166
+ function valueOf(row, raw) {
167
+ if (row.map) {
168
+ return row.map[raw] ?? `unknown_${raw}`;
169
+ }
170
+ return round(raw, row.precision ?? 1);
171
+ }
172
+
173
+ /** The PV total for this frame, or undefined when there is nothing new to say. */
174
+ function totalFor(data, published) {
175
+ if (Object.hasOwn(data, 'powGetPvSum')) {
176
+ return round(data.powGetPvSum);
177
+ }
178
+ const inputChanged = published.some(({item}) => item === 'pv1_watts' || item === 'pv2_watts');
179
+ if (inputChanged && state.has('pv1_watts') && state.has('pv2_watts')) {
180
+ return round(state.get('pv1_watts') + state.get('pv2_watts'));
181
+ }
182
+ return undefined;
183
+ }
184
+
185
+ function update(data = {}) {
186
+ const out = [];
187
+
188
+ // ITEMS order is publish order, and the computed total sits right behind its inputs
189
+ for (const row of ITEMS) {
190
+ const value = row.computed ? totalFor(data, out) : undefined;
191
+ if (row.computed) {
192
+ if (value === undefined) {
193
+ continue;
194
+ }
195
+ } else if (!Object.hasOwn(data, row.field)) {
196
+ continue;
197
+ }
198
+ const published = row.computed ? value : valueOf(row, data[row.field]);
199
+ state.set(row.item, published);
200
+ out.push({item: row.item, value: published});
201
+ }
202
+
203
+ return out;
204
+ }
205
+
206
+ return {
207
+ update,
208
+ get: (item) => state.get(item),
209
+ known: () => Object.fromEntries(state),
210
+ };
211
+ }
package/lib/mask.js ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Serial numbers identify a device and the account it is bound to, so they are masked everywhere
3
+ * they could be read by someone else — logs at info level, `<name>/info`, issue reports
4
+ * (ROADMAP.md E-2). The full SN appears at debug level only, and in captures it is replaced
5
+ * altogether (lib/capture.js).
6
+ */
7
+
8
+ /** Prefix (device family + region) and the last two characters: `BK01Z…9K`. */
9
+ export function maskSn(sn) {
10
+ if (typeof sn !== 'string' || sn.length <= 8) {
11
+ return sn ? '…' : '';
12
+ }
13
+ return `${sn.slice(0, 5)}…${sn.slice(-2)}`;
14
+ }
15
+
16
+ /**
17
+ * The placeholder captures use: the family prefix, then X for every remaining character, so the
18
+ * string keeps its length and frames stay byte-compatible (`BK01ZXXXXXXXXXXX`).
19
+ */
20
+ export function placeholderSn(sn) {
21
+ if (typeof sn !== 'string' || sn.length <= 5) {
22
+ return 'X'.repeat(typeof sn === 'string' ? sn.length : 0);
23
+ }
24
+ return sn.slice(0, 5) + 'X'.repeat(sn.length - 5);
25
+ }
26
+
27
+ /** Model name from the SN prefix (R §1); undefined for an unknown prefix. */
28
+ export function modelOf(sn) {
29
+ if (typeof sn !== 'string') {
30
+ return undefined;
31
+ }
32
+ const prefix = sn.slice(0, 4).toUpperCase();
33
+ if (prefix === 'BK01' || prefix === 'BK02' || prefix === 'N011') {
34
+ return 'STREAM Microinverter';
35
+ }
36
+ if (prefix === 'HW51') {
37
+ return 'PowerStream';
38
+ }
39
+ return undefined;
40
+ }
@@ -0,0 +1,50 @@
1
+ // EcoFlow STREAM family (BK series: STREAM Microinverter BK01/BK02/N011, STREAM AC/Ultra/Pro).
2
+ //
3
+ // Field numbers reverse-engineered by the community; this file follows
4
+ // tolwi/hassio-ecoflow-cloud (Apache License 2.0) `ef_bk_series.proto` and
5
+ // rabits/ha-ef-ble (Apache License 2.0) `bk_series.proto`. Only the subset ecoflow2mqtt decodes
6
+ // today is declared — an unknown field is skipped by protobuf, so adding a row later is enough.
7
+ //
8
+ // All values are floats in their natural unit (W, V, A, Hz) — no scaling. Every field is
9
+ // `optional`: DisplayPropertyUpload is incremental (only changed fields, every ~2 s), the full
10
+ // frame (~31 fields) arrives every ~120 s and as the reply to a property get.
11
+ //
12
+ // Observed on a STREAM Microinverter (product_id 17409) on 2026-08-28: pow_get_pv (361) and
13
+ // pow_get_pv2 (70) are sent, pow_get_pv_sum (517) is never sent — the adapter computes the total
14
+ // (ROADMAP.md E-4). A full frame carries 31 fields; the ones still unnamed are logged at debug
15
+ // (133 = 200, 135, 371 = 31..32, 617, 618 = 1, 638, 729-734 with 731 = 10001, and a few empty
16
+ // byte fields). 371 and 730 look like temperatures in °C but nothing confirms that yet, so they
17
+ // stay out of the item table rather than being guessed at (ROADMAP.md OQ-E7).
18
+
19
+ syntax = "proto3";
20
+
21
+ message DisplayPropertyUpload {
22
+ optional float pow_get_pv2 = 70; // PV input 2 power, W
23
+ optional float plug_in_info_pv2_amp = 71; // PV input 2 current, A
24
+ optional float pow_get_pv = 361; // PV input 1 power, W
25
+ optional float plug_in_info_pv_vol = 380; // PV input 1 voltage, V
26
+ optional float plug_in_info_pv_amp = 381; // PV input 1 current, A
27
+ optional float plug_in_info_pv2_vol = 442; // PV input 2 voltage, V
28
+ optional float pow_get_pv_sum = 517; // total PV power, W (not sent by the firmware seen so far)
29
+ optional uint32 feed_grid_mode_pow_limit = 521; // feed-in limit, W
30
+ optional float module_wifi_rssi = 602; // wifi signal, dBm
31
+ optional float grid_connection_vol = 613; // grid voltage, V
32
+ optional float grid_connection_amp = 614; // grid current, A
33
+ optional float grid_connection_freq = 615; // grid frequency, Hz
34
+ optional float grid_connection_power = 616; // power fed into the grid, W
35
+ optional uint32 grid_connection_sta = 619; // 0 invalid, 1 grid in, 2 offline, 3 feed grid
36
+ optional uint32 feed_grid_mode_pow_max = 727; // highest feed-in limit the device accepts, W
37
+ }
38
+
39
+ // cmd_func 254 / cmd_id 22: the upload periods the device runs with (ms). No temperatures, so it
40
+ // contributes no items — it is decoded to make the cadence visible at debug level.
41
+ message RuntimePropertyUpload {
42
+ optional uint32 display_property_full_upload_period = 293; // 120000 ms observed
43
+ optional uint32 display_property_incremental_upload_period = 294; // 2000 ms observed
44
+ // 295 (300000) and 296 (60000) are the same pair for the runtime properties [unverified]
45
+ }
46
+
47
+ // app -> device "activate stream": cmd_func 96, cmd_id 97. Off by default (ROADMAP.md E-9).
48
+ message EnergyStreamSwitch {
49
+ optional int32 sw = 1;
50
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Protobuf layer: MQTT payload -> frames.
3
+ *
4
+ * A payload is a `HeaderMessage` with one or more `Header`s; each header carries a `pdata` blob
5
+ * that belongs to the message identified by `(cmd_func, cmd_id)`. STREAM devices obfuscate pdata
6
+ * by XORing every byte with `seq & 0xFF` (`enc_type == 1`, device -> app direction, `src != 32`).
7
+ *
8
+ * The `.proto` files are vendored text and parsed at runtime (ROADMAP.md E-7): a new firmware
9
+ * field is a line in `bk_series.proto`, not a code change. Unknown `(cmd_func, cmd_id)` pairs are
10
+ * returned as frames without `data` so the caller can log them once and carry on.
11
+ */
12
+
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import {fileURLToPath} from 'node:url';
16
+ import protobuf from 'protobufjs';
17
+
18
+ const dir = path.dirname(fileURLToPath(import.meta.url));
19
+
20
+ const root = new protobuf.Root();
21
+ for (const file of ['header.proto', 'bk_series.proto']) {
22
+ protobuf.parse(fs.readFileSync(path.join(dir, file), 'utf8'), root);
23
+ }
24
+
25
+ export const HeaderMessage = root.lookupType('HeaderMessage');
26
+ export const DisplayPropertyUpload = root.lookupType('DisplayPropertyUpload');
27
+ export const RuntimePropertyUpload = root.lookupType('RuntimePropertyUpload');
28
+ export const EnergyStreamSwitch = root.lookupType('EnergyStreamSwitch');
29
+
30
+ /** `${cmd_func}/${cmd_id}` -> message. Everything else is returned undecoded. */
31
+ export const MESSAGES = {
32
+ '254/21': {name: 'DisplayPropertyUpload', type: DisplayPropertyUpload, items: true},
33
+ '254/22': {name: 'RuntimePropertyUpload', type: RuntimePropertyUpload},
34
+ };
35
+
36
+ /** Names of the frames we know of but do not decode — for readable debug logs (R §4.5). */
37
+ export const FRAME_NAMES = {
38
+ '254/17': 'ConfigWrite',
39
+ '254/18': 'ConfigWriteAck',
40
+ '254/19': 'ConfigRead',
41
+ '254/20': 'ConfigReadAck',
42
+ '254/21': 'DisplayPropertyUpload',
43
+ '254/22': 'RuntimePropertyUpload',
44
+ '254/32': 'BatchEnergyTotalReport',
45
+ '32/2': 'CMSHeartBeatReport',
46
+ '32/50': 'BMSHeartBeatReport',
47
+ '96/97': 'EnergyStreamSwitch',
48
+ };
49
+
50
+ /**
51
+ * Undo the XOR obfuscation. STREAM devices XOR every pdata byte with the low byte of `seq`;
52
+ * frames the app sends (`src == 32`) and replies with `enc_type == 0` are plain.
53
+ *
54
+ * @param {Buffer|Uint8Array} pdata
55
+ * @param {{encType?: number, src?: number, seq?: number}} header
56
+ * @returns {Buffer} a new buffer, never the input
57
+ */
58
+ export function deobfuscate(pdata, {encType = 0, src = 0, seq = 0} = {}) {
59
+ const buffer = Buffer.from(pdata ?? []);
60
+ if (encType !== 1 || src === 32) {
61
+ return buffer;
62
+ }
63
+ const key = seq & 0xff;
64
+ for (const [index, byte] of buffer.entries()) {
65
+ buffer[index] = byte ^ key;
66
+ }
67
+ return buffer;
68
+ }
69
+
70
+ /**
71
+ * Decode one MQTT payload.
72
+ *
73
+ * Throws only when the outer envelope is not a `HeaderMessage` (a JSON ping, a truncated frame) —
74
+ * a payload whose inner pdata is garbled yields a frame with `error` set, so one bad message
75
+ * cannot take the stream down.
76
+ *
77
+ * @param {Buffer} payload
78
+ * @returns {Array<{cmdFunc: number, cmdId: number, key: string, name: string, seq: number,
79
+ * src: number, dest: number, encType: number, productId: number, version: number,
80
+ * deviceSn: string, moduleSn: string, code: string, pdata: Buffer, data: object|null,
81
+ * items: boolean, error: string|null}>}
82
+ */
83
+ export function decodeFrames(payload) {
84
+ const message = HeaderMessage.decode(payload);
85
+ return (message.header ?? []).map((header) => {
86
+ const cmdFunc = header.cmdFunc ?? 0;
87
+ const cmdId = header.cmdId ?? 0;
88
+ const key = `${cmdFunc}/${cmdId}`;
89
+ const pdata = deobfuscate(header.pdata, header);
90
+ const frame = {
91
+ cmdFunc,
92
+ cmdId,
93
+ key,
94
+ name: FRAME_NAMES[key] ?? `unknown ${key}`,
95
+ seq: header.seq ?? 0,
96
+ src: header.src ?? 0,
97
+ dest: header.dest ?? 0,
98
+ encType: header.encType ?? 0,
99
+ productId: header.productId ?? 0,
100
+ version: header.version ?? 0,
101
+ deviceSn: header.deviceSn ?? '',
102
+ moduleSn: header.moduleSn ?? '',
103
+ code: header.code ?? '',
104
+ pdata,
105
+ data: null,
106
+ items: false,
107
+ error: null,
108
+ };
109
+ const known = MESSAGES[key];
110
+ if (!known || pdata.length === 0) {
111
+ return frame;
112
+ }
113
+ try {
114
+ frame.data = known.type.toObject(known.type.decode(pdata));
115
+ frame.items = Boolean(known.items);
116
+ } catch (error) {
117
+ frame.error = error.message;
118
+ }
119
+ return frame;
120
+ });
121
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The two frames ecoflow2mqtt sends. Both are what the EcoFlow app sends, replayed byte for byte
3
+ * (R §4.4, §4.5) — 0.1.0 has no `set` items, so nothing here changes the device's configuration.
4
+ */
5
+
6
+ import {HeaderMessage, EnergyStreamSwitch} from './decode.js';
7
+
8
+ /** protobuf seq is a uint32; the app uses a millisecond timestamp. */
9
+ function nextSeq(now = Date.now()) {
10
+ return now % 0xffffffff;
11
+ }
12
+
13
+ /**
14
+ * "Give me everything": an empty header `src 32 -> dest 32`. The device answers with a full
15
+ * DisplayPropertyUpload on `.../thing/property/get_reply`. Not a keep-alive (ROADMAP.md E-9), a
16
+ * full-frame refresh.
17
+ *
18
+ * @returns {Buffer}
19
+ */
20
+ export function encodeGet({seq = nextSeq()} = {}) {
21
+ return Buffer.from(HeaderMessage.encode({header: [{src: 32, dest: 32, seq, from: 'ios'}]}).finish());
22
+ }
23
+
24
+ /**
25
+ * `EnergyStreamSwitch {1: 1}` (cmd_func 96 / cmd_id 97) — the app's "activate stream" frame for
26
+ * the STREAM family. Off by default (`--stream-interval 0`); kept for the case that the cloud
27
+ * starts throttling a passive subscriber (OQ-E1).
28
+ *
29
+ * @param {{sn: string, seq?: number}} options
30
+ * @returns {Buffer}
31
+ */
32
+ export function encodeEnergyStreamSwitch({sn, seq = nextSeq()}) {
33
+ const pdata = EnergyStreamSwitch.encode({sw: 1}).finish();
34
+ return Buffer.from(
35
+ HeaderMessage.encode({
36
+ header: [
37
+ {
38
+ pdata,
39
+ src: 32,
40
+ dest: 53,
41
+ dSrc: 1,
42
+ dDest: 1,
43
+ checkType: 3,
44
+ cmdFunc: 96,
45
+ cmdId: 97,
46
+ dataLen: pdata.length,
47
+ needAck: 1,
48
+ seq,
49
+ version: 19,
50
+ payloadVer: 1,
51
+ from: 'ios',
52
+ deviceSn: sn,
53
+ },
54
+ ],
55
+ }).finish(),
56
+ );
57
+ }
@@ -0,0 +1,43 @@
1
+ // EcoFlow protobuf envelope, used by every protobuf device (PowerStream, Smart Plug, STREAM
2
+ // family). One MQTT message may stack several Header entries.
3
+ //
4
+ // Field numbers reverse-engineered by the community; this file follows
5
+ // tolwi/hassio-ecoflow-cloud (Apache License 2.0)
6
+ // custom_components/ecoflow_cloud/devices/internal/proto/*.proto
7
+ // and rabits/ha-ef-ble (Apache License 2.0) eflib/pb/*.proto.
8
+ //
9
+ // pdata is the payload of the (cmd_func, cmd_id) message; when enc_type == 1 and src != 32 every
10
+ // byte is XORed with (seq & 0xFF) — see lib/proto/decode.js.
11
+
12
+ syntax = "proto3";
13
+
14
+ message Header {
15
+ bytes pdata = 1;
16
+ int32 src = 2;
17
+ int32 dest = 3;
18
+ int32 d_src = 4;
19
+ int32 d_dest = 5;
20
+ int32 enc_type = 6;
21
+ int32 check_type = 7;
22
+ int32 cmd_func = 8;
23
+ int32 cmd_id = 9;
24
+ int32 data_len = 10;
25
+ int32 need_ack = 11;
26
+ int32 is_ack = 12;
27
+ uint32 seq = 14;
28
+ int32 product_id = 15;
29
+ int32 version = 16;
30
+ int32 payload_ver = 17;
31
+ int32 time_snap = 18;
32
+ int32 is_rw_cmd = 19;
33
+ int32 is_queue = 20;
34
+ int32 ack_type = 21;
35
+ string code = 22;
36
+ string from = 23;
37
+ string module_sn = 24;
38
+ string device_sn = 25;
39
+ }
40
+
41
+ message HeaderMessage {
42
+ repeated Header header = 1;
43
+ }
package/package.json CHANGED
@@ -1,20 +1,28 @@
1
1
  {
2
2
  "name": "ecoflow2mqtt",
3
- "version": "0.0.1",
4
- "description": "Interface between EcoFlow PowerStream / STREAM micro-inverters and MQTT, with Home Assistant discovery (placeholder release, work in progress)",
3
+ "version": "0.2.0",
4
+ "description": "Interface between EcoFlow STREAM / PowerStream micro-inverters and MQTT",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
+ "preferGlobal": true,
7
8
  "bin": {
8
9
  "ecoflow2mqtt": "index.js"
9
10
  },
10
11
  "files": [
11
- "index.js"
12
+ "index.js",
13
+ "config.js",
14
+ "lib/"
12
15
  ],
13
16
  "engines": {
14
17
  "node": "^20.19 || ^22.12 || >=24"
15
18
  },
16
19
  "scripts": {
17
- "start": "node index.js"
20
+ "start": "node index.js",
21
+ "lint": "eslint . && prettier --check .",
22
+ "format": "prettier --write . && eslint --fix .",
23
+ "test": "node --test",
24
+ "test:watch": "node --test --watch",
25
+ "deploy": "bash deploy.sh"
18
26
  },
19
27
  "repository": {
20
28
  "type": "git",
@@ -24,6 +32,7 @@
24
32
  "mqtt",
25
33
  "smarthome",
26
34
  "mqtt-smarthome",
35
+ "mqtt-interfaces",
27
36
  "home-automation",
28
37
  "home-assistant",
29
38
  "ecoflow",
@@ -38,5 +47,26 @@
38
47
  "bugs": {
39
48
  "url": "https://github.com/hobbyquaker/ecoflow2mqtt/issues"
40
49
  },
41
- "homepage": "https://github.com/hobbyquaker/ecoflow2mqtt"
50
+ "homepage": "https://github.com/hobbyquaker/ecoflow2mqtt",
51
+ "mqttInterfaces": {
52
+ "spec": "2.0",
53
+ "envPrefix": "ECOFLOW2MQTT",
54
+ "needs": [
55
+ "network",
56
+ "cloud"
57
+ ],
58
+ "serviceExtra": []
59
+ },
60
+ "dependencies": {
61
+ "mqtt": "^5.10.0",
62
+ "mqtt-interfaces-core": "^0.8.0",
63
+ "protobufjs": "^7.4.0"
64
+ },
65
+ "devDependencies": {
66
+ "@eslint/js": "^9.0.0",
67
+ "eslint": "^9.0.0",
68
+ "eslint-config-prettier": "^10.0.0",
69
+ "globals": "^16.0.0",
70
+ "prettier": "^3.0.0"
71
+ }
42
72
  }