homebridge-enphase-envoy 10.2.7-beta.3 → 10.2.7-beta.5
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/index.js +0 -1
- package/package.json +1 -1
- package/src/envoydevice.js +5 -6
- package/src/energymeter.js +0 -1208
package/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { join } from 'path';
|
|
2
2
|
import { mkdirSync, existsSync, writeFileSync } from 'fs';
|
|
3
3
|
import EnvoyDevice from './src/envoydevice.js';
|
|
4
|
-
import EnergyMeter from './src/energymeter.js';
|
|
5
4
|
import ImpulseGenerator from './src/impulsegenerator.js';
|
|
6
5
|
import { PluginName, PlatformName } from './src/constants.js';
|
|
7
6
|
import CustomCharacteristics from './src/customcharacteristics.js';
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"private": false,
|
|
3
3
|
"displayName": "Enphase Envoy",
|
|
4
4
|
"name": "homebridge-enphase-envoy",
|
|
5
|
-
"version": "10.2.7-beta.
|
|
5
|
+
"version": "10.2.7-beta.5",
|
|
6
6
|
"description": "Homebridge p7ugin for Photovoltaic Energy System manufactured by Enphase.",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"author": "grzegorz914",
|
package/src/envoydevice.js
CHANGED
|
@@ -3007,15 +3007,12 @@ class EnvoyDevice extends EventEmitter {
|
|
|
3007
3007
|
path: this.prefDir,
|
|
3008
3008
|
filename: this.energyMeterHistoryFileName
|
|
3009
3009
|
})
|
|
3010
|
-
this.fakegatoHistoryService.addEntry({
|
|
3011
|
-
time: Math.floor(Date.now() / 1000),
|
|
3012
|
-
power: this.pv.powerAndEnergyData.data[0].power ?? 0
|
|
3013
|
-
});
|
|
3014
3010
|
|
|
3015
3011
|
// Energy Meter Service
|
|
3016
3012
|
this.energyMeterServices = [];
|
|
3017
3013
|
for (const source of this.pv.powerAndEnergyData.data) {
|
|
3018
3014
|
const measurementType = source.measurementType;
|
|
3015
|
+
const power = source.power > 0 ? source.power : 0;
|
|
3019
3016
|
|
|
3020
3017
|
if (this.logDebug) this.emit('debug', `Prepare Energy Meter ${measurementType} Service`);
|
|
3021
3018
|
const energyMeterService = accessory.addService(Service.EvePowerMeter, `Energy Meter ${measurementType}`, `energyMeterService${measurementType}`);
|
|
@@ -3023,7 +3020,7 @@ class EnvoyDevice extends EventEmitter {
|
|
|
3023
3020
|
|
|
3024
3021
|
// Create characteristics
|
|
3025
3022
|
const characteristics = [
|
|
3026
|
-
{ type: Characteristic.EvePower, label: 'power', value:
|
|
3023
|
+
{ type: Characteristic.EvePower, label: 'power', value: power, unit: 'W' },
|
|
3027
3024
|
{ type: Characteristic.EveEnergyLifetime, label: 'energy lifetime', value: source.energyLifetimeWithOffset, unit: 'kWh' },
|
|
3028
3025
|
];
|
|
3029
3026
|
|
|
@@ -3094,6 +3091,7 @@ class EnvoyDevice extends EventEmitter {
|
|
|
3094
3091
|
this.emit('devInfo', `Firmware: ${info.software}`);
|
|
3095
3092
|
this.emit('devInfo', `SerialNr: ${info.serialNumber}`);
|
|
3096
3093
|
this.emit('devInfo', `Time: ${this.functions.formatTimestamp(info.time, timeZone)}`);
|
|
3094
|
+
this.emit('devInfo', `Energy Meter: ${this.energyMeter ? 'Enabled' : 'Disabled'}`);
|
|
3097
3095
|
this.emit('devInfo', `------------------------------`);
|
|
3098
3096
|
|
|
3099
3097
|
// Inventory
|
|
@@ -4106,6 +4104,7 @@ class EnvoyDevice extends EventEmitter {
|
|
|
4106
4104
|
|
|
4107
4105
|
// Energy meter
|
|
4108
4106
|
if (key === 'production' && this.energyMeter) {
|
|
4107
|
+
const power = obj.power > 0 ? obj.power : 0;
|
|
4109
4108
|
// Add to fakegato history
|
|
4110
4109
|
this.fakegatoHistoryService?.addEntry({
|
|
4111
4110
|
time: Math.floor(Date.now() / 1000),
|
|
@@ -4114,7 +4113,7 @@ class EnvoyDevice extends EventEmitter {
|
|
|
4114
4113
|
|
|
4115
4114
|
// Create characteristics energy meter
|
|
4116
4115
|
const characteristics2 = [
|
|
4117
|
-
{ type: Characteristic.EvePower, value:
|
|
4116
|
+
{ type: Characteristic.EvePower, value: power },
|
|
4118
4117
|
{ type: Characteristic.EveEnergyLifetime, value: obj.energyLifetimeKw },
|
|
4119
4118
|
];
|
|
4120
4119
|
|
package/src/energymeter.js
DELETED
|
@@ -1,1208 +0,0 @@
|
|
|
1
|
-
import { XMLParser, XMLBuilder, XMLValidator } from 'fast-xml-parser';
|
|
2
|
-
import EventEmitter from 'events';
|
|
3
|
-
import EnvoyToken from './envoytoken.js';
|
|
4
|
-
import ImpulseGenerator from './impulsegenerator.js';
|
|
5
|
-
import Functions from './functions.js';
|
|
6
|
-
import { ApiUrls, PartNumbers, ApiCodes, MetersKeyMap } from './constants.js';
|
|
7
|
-
import fakegato from 'fakegato-history';
|
|
8
|
-
let Accessory, Characteristic, Service, Categories, AccessoryUUID;
|
|
9
|
-
|
|
10
|
-
class EnergyMeter extends EventEmitter {
|
|
11
|
-
constructor(api, log, url, deviceName, device, envoyIdFile, envoyTokenFile, prefDir, energyMeterHistoryFileName) {
|
|
12
|
-
super();
|
|
13
|
-
|
|
14
|
-
Accessory = api.platformAccessory;
|
|
15
|
-
Characteristic = api.hap.Characteristic;
|
|
16
|
-
Service = api.hap.Service;
|
|
17
|
-
Categories = api.hap.Categories;
|
|
18
|
-
AccessoryUUID = api.hap.uuid;
|
|
19
|
-
|
|
20
|
-
//device configuration
|
|
21
|
-
this.log = log;
|
|
22
|
-
this.url = url;
|
|
23
|
-
this.name = deviceName;
|
|
24
|
-
this.envoyFirmware7xxTokenGenerationMode = device.envoyFirmware7xxTokenGenerationMode;
|
|
25
|
-
this.enlightenUser = device.enlightenUser;
|
|
26
|
-
this.enlightenPasswd = device.enlightenPasswd;
|
|
27
|
-
this.energyProductionLifetimeOffset = device.energyProductionLifetimeOffset || 0;
|
|
28
|
-
this.energyConsumptionTotalLifetimeOffset = device.energyConsumptionTotalLifetimeOffset || 0;
|
|
29
|
-
this.energyConsumptionNetLifetimeOffset = device.energyConsumptionNetLifetimeOffset || 0;
|
|
30
|
-
|
|
31
|
-
//log
|
|
32
|
-
this.logDeviceInfo = device.log?.deviceInfo || true;
|
|
33
|
-
this.logInfo = device.log?.info || false;
|
|
34
|
-
this.logWarn = device.log?.warn || true;
|
|
35
|
-
this.logError = device.log?.error || true;
|
|
36
|
-
this.logDebug = device.log?.debug || false;
|
|
37
|
-
|
|
38
|
-
//setup variables
|
|
39
|
-
this.functions = new Functions();
|
|
40
|
-
this.envoyTokenFile = envoyTokenFile;
|
|
41
|
-
this.checkTokenRunning = false;
|
|
42
|
-
|
|
43
|
-
//fakegato
|
|
44
|
-
this.fakegatoHistory = fakegato(api);
|
|
45
|
-
this.prefDir = prefDir;
|
|
46
|
-
this.energyMeterHistoryFileName = energyMeterHistoryFileName;
|
|
47
|
-
this.lastReset = 0;
|
|
48
|
-
|
|
49
|
-
//supported functions
|
|
50
|
-
this.feature = {
|
|
51
|
-
info: {
|
|
52
|
-
devId: '',
|
|
53
|
-
envoyPasswd: '',
|
|
54
|
-
installerPasswd: '',
|
|
55
|
-
firmware: 500,
|
|
56
|
-
tokenRequired: false,
|
|
57
|
-
tokenValid: false,
|
|
58
|
-
cookie: '',
|
|
59
|
-
jwtToken: {
|
|
60
|
-
generation_time: 0,
|
|
61
|
-
token: device.envoyToken,
|
|
62
|
-
expires_at: 0,
|
|
63
|
-
installer: this.envoyFirmware7xxTokenGenerationMode === 2 ? device.envoyTokenInstaller : false
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
|
-
meters: {
|
|
67
|
-
supported: false,
|
|
68
|
-
installed: false,
|
|
69
|
-
count: 0,
|
|
70
|
-
production: {
|
|
71
|
-
supported: false,
|
|
72
|
-
enabled: false
|
|
73
|
-
},
|
|
74
|
-
consumptionNet: {
|
|
75
|
-
supported: false,
|
|
76
|
-
enabled: false
|
|
77
|
-
},
|
|
78
|
-
consumptionTotal: {
|
|
79
|
-
supported: false,
|
|
80
|
-
enabled: false
|
|
81
|
-
},
|
|
82
|
-
storage: {
|
|
83
|
-
supported: false,
|
|
84
|
-
enabled: false
|
|
85
|
-
},
|
|
86
|
-
backfeed: {
|
|
87
|
-
supported: false,
|
|
88
|
-
enabled: false
|
|
89
|
-
},
|
|
90
|
-
load: {
|
|
91
|
-
supported: false,
|
|
92
|
-
enabled: false
|
|
93
|
-
},
|
|
94
|
-
evse: {
|
|
95
|
-
supported: false,
|
|
96
|
-
enabled: false
|
|
97
|
-
},
|
|
98
|
-
pv3p: {
|
|
99
|
-
supported: false,
|
|
100
|
-
enabled: false
|
|
101
|
-
},
|
|
102
|
-
generator: {
|
|
103
|
-
supported: false,
|
|
104
|
-
enabled: false
|
|
105
|
-
},
|
|
106
|
-
detailedData: {
|
|
107
|
-
supported: false
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
metersReading: {
|
|
111
|
-
supported: false,
|
|
112
|
-
installed: false
|
|
113
|
-
},
|
|
114
|
-
metersReports: {
|
|
115
|
-
supported: false,
|
|
116
|
-
installed: false
|
|
117
|
-
},
|
|
118
|
-
production: {
|
|
119
|
-
supported: false
|
|
120
|
-
},
|
|
121
|
-
productionPdm: {
|
|
122
|
-
supported: false,
|
|
123
|
-
pcu: {
|
|
124
|
-
supported: false
|
|
125
|
-
},
|
|
126
|
-
eim: {
|
|
127
|
-
supported: false
|
|
128
|
-
},
|
|
129
|
-
rgm: {
|
|
130
|
-
supported: false
|
|
131
|
-
},
|
|
132
|
-
pmu: {
|
|
133
|
-
supported: false
|
|
134
|
-
}
|
|
135
|
-
},
|
|
136
|
-
energyPdm: {
|
|
137
|
-
supported: false,
|
|
138
|
-
production: {
|
|
139
|
-
supported: false,
|
|
140
|
-
pcu: {
|
|
141
|
-
supported: false
|
|
142
|
-
},
|
|
143
|
-
eim: {
|
|
144
|
-
supported: false
|
|
145
|
-
},
|
|
146
|
-
rgm: {
|
|
147
|
-
supported: false
|
|
148
|
-
},
|
|
149
|
-
pmu: {
|
|
150
|
-
supported: false
|
|
151
|
-
}
|
|
152
|
-
},
|
|
153
|
-
comsumptionNet: {
|
|
154
|
-
supported: false
|
|
155
|
-
},
|
|
156
|
-
consumptionTotal: {
|
|
157
|
-
supported: false
|
|
158
|
-
}
|
|
159
|
-
},
|
|
160
|
-
productionCt: {
|
|
161
|
-
supported: false,
|
|
162
|
-
production: {
|
|
163
|
-
supported: false,
|
|
164
|
-
pcu: {
|
|
165
|
-
supported: false
|
|
166
|
-
},
|
|
167
|
-
eim: {
|
|
168
|
-
supported: false
|
|
169
|
-
}
|
|
170
|
-
},
|
|
171
|
-
consumptionNet: {
|
|
172
|
-
supported: false
|
|
173
|
-
},
|
|
174
|
-
consumptionTotal: {
|
|
175
|
-
supported: false
|
|
176
|
-
},
|
|
177
|
-
storage: {
|
|
178
|
-
supported: false
|
|
179
|
-
}
|
|
180
|
-
},
|
|
181
|
-
powerAndEnergyData: {
|
|
182
|
-
supported: false
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
//pv object
|
|
187
|
-
this.pv = {
|
|
188
|
-
info: {},
|
|
189
|
-
meters: [],
|
|
190
|
-
powerAndEnergy: {
|
|
191
|
-
data: [],
|
|
192
|
-
production: {
|
|
193
|
-
pcu: {},
|
|
194
|
-
eim: {},
|
|
195
|
-
rgm: {},
|
|
196
|
-
pmu: {},
|
|
197
|
-
},
|
|
198
|
-
consumptionNet: {},
|
|
199
|
-
consumptionTotal: {},
|
|
200
|
-
},
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
//lock flags
|
|
204
|
-
this.locks = {
|
|
205
|
-
updatePowerAndEnergy: false
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
//impulse generator
|
|
209
|
-
this.impulseGenerator = new ImpulseGenerator()
|
|
210
|
-
.on('updatePowerAndEnergy', () => this.handleWithLock('updatePowerAndEnergy', async () => {
|
|
211
|
-
if (this.feature.meters.supported) {
|
|
212
|
-
await this.updateMeters();
|
|
213
|
-
if (this.feature.metersReading.installed && !this.feature.metersReports.installed) await this.updateMetersReading(false);
|
|
214
|
-
if (this.feature.metersReports.installed) await this.updateMetersReports(false);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (this.feature.production.supported) await this.updateProduction();
|
|
218
|
-
if (this.feature.productionPdm.supported && !this.feature.energyPdm.supported) await this.updateProductionPdm();
|
|
219
|
-
if (this.feature.energyPdm.supported) await this.updateEnergyPdm();
|
|
220
|
-
if (this.feature.productionCt.supported) await this.updateProductionCt();
|
|
221
|
-
await this.updatePowerAndEnergyData();
|
|
222
|
-
}))
|
|
223
|
-
.on('state', (state) => {
|
|
224
|
-
this.emit(state ? 'success' : 'warn', `Impulse generator ${state ? 'started' : 'stopped'}`);
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
handleError(error) {
|
|
229
|
-
const errorString = error.toString();
|
|
230
|
-
const tokenNotValid = errorString.includes('status code 401');
|
|
231
|
-
if (tokenNotValid) {
|
|
232
|
-
if (this.checkTokenRunning) return;
|
|
233
|
-
|
|
234
|
-
this.feature.info.jwtToken.token = '';
|
|
235
|
-
this.feature.tokenValid = false;
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
if (this.logError) this.emit('error', `Impulse generator: ${error}`);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async startStopImpulseGenerator(state) {
|
|
242
|
-
try {
|
|
243
|
-
//start impulse generator
|
|
244
|
-
const timers = state ? this.timers : [];
|
|
245
|
-
await this.impulseGenerator.state(state, timers)
|
|
246
|
-
return true;
|
|
247
|
-
} catch (error) {
|
|
248
|
-
throw new Error(`Impulse generator start error: ${error}`);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
async handleWithLock(lockKey, fn) {
|
|
253
|
-
if (this.locks[lockKey]) return;
|
|
254
|
-
|
|
255
|
-
const tokenValid = await this.checkToken();
|
|
256
|
-
if (!tokenValid) return;
|
|
257
|
-
|
|
258
|
-
this.locks[lockKey] = true;
|
|
259
|
-
try {
|
|
260
|
-
await fn();
|
|
261
|
-
} catch (error) {
|
|
262
|
-
this.handleError(error);
|
|
263
|
-
} finally {
|
|
264
|
-
this.locks[lockKey] = false;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
async getInfo() {
|
|
269
|
-
if (this.logDebug) this.emit('debug', 'Requesting info');
|
|
270
|
-
|
|
271
|
-
try {
|
|
272
|
-
const response = await this.axiosInstance.get(ApiUrls.GetInfo);
|
|
273
|
-
const xmlString = response.data;
|
|
274
|
-
|
|
275
|
-
// XML Parsing options
|
|
276
|
-
const options = {
|
|
277
|
-
ignoreAttributes: false,
|
|
278
|
-
ignorePiTags: true,
|
|
279
|
-
allowBooleanAttributes: true
|
|
280
|
-
};
|
|
281
|
-
const parser = new XMLParser(options);
|
|
282
|
-
const parsed = parser.parse(xmlString);
|
|
283
|
-
|
|
284
|
-
// Defensive structure checks
|
|
285
|
-
const envoyInfo = parsed.envoy_info ?? {};
|
|
286
|
-
const device = envoyInfo.device ?? {};
|
|
287
|
-
const buildInfo = envoyInfo.build_info ?? {};
|
|
288
|
-
|
|
289
|
-
// Masked debug version
|
|
290
|
-
const debugParsed = {
|
|
291
|
-
...parsed,
|
|
292
|
-
envoy_info: {
|
|
293
|
-
...envoyInfo,
|
|
294
|
-
device: {
|
|
295
|
-
...device,
|
|
296
|
-
sn: 'removed'
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
if (this.logDebug) this.emit('debug', 'Parsed info:', debugParsed);
|
|
302
|
-
|
|
303
|
-
const serialNumber = device.sn?.toString() ?? null;
|
|
304
|
-
if (!serialNumber) {
|
|
305
|
-
if (this.logWarn) this.emit('warn', 'Envoy serial number missing!');
|
|
306
|
-
return null;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Construct info object
|
|
310
|
-
const obj = {
|
|
311
|
-
time: envoyInfo.time,
|
|
312
|
-
serialNumber,
|
|
313
|
-
partNumber: device.pn,
|
|
314
|
-
modelName: PartNumbers[device.pn] ?? device.pn,
|
|
315
|
-
software: device.software,
|
|
316
|
-
euaid: device.euaid,
|
|
317
|
-
seqNum: device.seqnum,
|
|
318
|
-
apiVer: device.apiver,
|
|
319
|
-
imeter: !!device.imeter,
|
|
320
|
-
webTokens: !!envoyInfo['web-tokens'],
|
|
321
|
-
packages: envoyInfo.package ?? [],
|
|
322
|
-
buildInfo: {
|
|
323
|
-
buildId: buildInfo.build_id,
|
|
324
|
-
buildTimeQmt: buildInfo.build_time_gmt,
|
|
325
|
-
releaseVer: buildInfo.release_ver,
|
|
326
|
-
releaseStage: buildInfo.release_stage
|
|
327
|
-
}
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
this.pv.info = obj;
|
|
331
|
-
|
|
332
|
-
// Feature: meters
|
|
333
|
-
this.feature.meters.supported = obj.imeter;
|
|
334
|
-
|
|
335
|
-
// Feature: firmware version
|
|
336
|
-
const cleanedVersion = obj.software?.replace(/\D/g, '') ?? '';
|
|
337
|
-
const parsedFirmware = cleanedVersion ? parseInt(cleanedVersion.slice(0, 3)) : 500;
|
|
338
|
-
this.feature.info.firmware = parsedFirmware;
|
|
339
|
-
this.feature.info.tokenRequired = obj.webTokens;
|
|
340
|
-
|
|
341
|
-
return true;
|
|
342
|
-
} catch (error) {
|
|
343
|
-
throw new Error(`Update info error: ${error}`);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
async checkToken(start) {
|
|
348
|
-
if (this.logDebug) this.emit('debug', 'Requesting check token');
|
|
349
|
-
|
|
350
|
-
if (this.checkTokenRunning) {
|
|
351
|
-
if (this.logDebug) this.emit('debug', 'Token check already running');
|
|
352
|
-
return null;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
if (!this.feature.info.tokenRequired) {
|
|
356
|
-
if (this.logDebug) this.emit('debug', 'Token not required, skipping token check');
|
|
357
|
-
return true;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
this.checkTokenRunning = true;
|
|
361
|
-
try {
|
|
362
|
-
const now = Math.floor(Date.now() / 1000);
|
|
363
|
-
|
|
364
|
-
// Load token from file on startup, only if mode is 1
|
|
365
|
-
if (this.envoyFirmware7xxTokenGenerationMode === 1 && start) {
|
|
366
|
-
try {
|
|
367
|
-
const data = await this.functions.readData(this.envoyTokenFile);
|
|
368
|
-
try {
|
|
369
|
-
const parsedData = JSON.parse(data);
|
|
370
|
-
const fileTokenExist = parsedData.token ? 'Exist' : 'Missing';
|
|
371
|
-
if (this.logDebug) this.emit('debug', `Token from file: ${fileTokenExist}`);
|
|
372
|
-
if (parsedData.token) {
|
|
373
|
-
this.feature.info.jwtToken = parsedData;
|
|
374
|
-
}
|
|
375
|
-
} catch (error) {
|
|
376
|
-
if (this.logWarn) this.emit('warn', `Token parse error: ${error}`);
|
|
377
|
-
}
|
|
378
|
-
} catch (error) {
|
|
379
|
-
if (this.logWarn) this.emit('warn', `Read Token from file error: ${error}`);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const jwt = this.feature.info.jwtToken || {};
|
|
384
|
-
const tokenExist = jwt.token && (this.envoyFirmware7xxTokenGenerationMode === 2 || jwt.expires_at >= now + 60);
|
|
385
|
-
|
|
386
|
-
if (this.logDebug) {
|
|
387
|
-
const remaining = jwt.expires_at ? jwt.expires_at - now : 'N/A';
|
|
388
|
-
this.emit('debug', `Token: ${tokenExist ? 'Exist' : 'Missing'}, expires in ${remaining} seconds`);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const tokenValid = this.feature.info.tokenValid;
|
|
392
|
-
if (this.logDebug) this.emit('debug', `Token: ${tokenValid ? 'Valid' : 'Not valid'}`);
|
|
393
|
-
|
|
394
|
-
if (tokenExist && tokenValid) {
|
|
395
|
-
if (this.logDebug) this.emit('debug', 'Token check complete, state: Valid');
|
|
396
|
-
return true;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
if (!tokenExist) {
|
|
400
|
-
if (this.logWarn) this.emit('warn', 'Token not exist, requesting new');
|
|
401
|
-
await this.delayBeforeRetry?.() ?? new Promise(resolve => setTimeout(resolve, 30000));
|
|
402
|
-
const gotToken = await this.getToken();
|
|
403
|
-
if (!gotToken) return null;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
if (!this.feature.info.jwtToken.token) {
|
|
407
|
-
if (this.logWarn) this.emit('warn', 'Token became invalid before validation');
|
|
408
|
-
return null;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
if (this.logWarn) this.emit('warn', 'Token exist but not valid, validating');
|
|
412
|
-
const validated = await this.validateToken();
|
|
413
|
-
if (!validated) return null;
|
|
414
|
-
|
|
415
|
-
if (this.logDebug) this.emit('debug', 'Token check complete: Valid=true');
|
|
416
|
-
return true;
|
|
417
|
-
} catch (error) {
|
|
418
|
-
throw new Error(`Check token error: ${error}`);
|
|
419
|
-
} finally {
|
|
420
|
-
this.checkTokenRunning = false;
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
async getToken() {
|
|
425
|
-
if (this.logDebug) this.emit('debug', 'Requesting token');
|
|
426
|
-
|
|
427
|
-
try {
|
|
428
|
-
// Create EnvoyToken instance and attach log event handlers
|
|
429
|
-
const envoyToken = new EnvoyToken({
|
|
430
|
-
user: this.enlightenUser,
|
|
431
|
-
passwd: this.enlightenPasswd,
|
|
432
|
-
serialNumber: this.pv.info.serialNumber,
|
|
433
|
-
logWarn: this.logWarn,
|
|
434
|
-
logError: this.logError,
|
|
435
|
-
})
|
|
436
|
-
.on('success', message => this.emit('success', message))
|
|
437
|
-
.on('warn', warn => this.emit('warn', warn))
|
|
438
|
-
.on('error', error => this.emit('error', error));
|
|
439
|
-
|
|
440
|
-
// Attempt to refresh token
|
|
441
|
-
const tokenData = await envoyToken.refreshToken();
|
|
442
|
-
|
|
443
|
-
if (!tokenData || !tokenData.token) {
|
|
444
|
-
if (this.logWarn) this.emit('warn', 'Token request returned empty or invalid');
|
|
445
|
-
return null;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// Mask token in debug output
|
|
449
|
-
const maskedTokenData = {
|
|
450
|
-
...tokenData,
|
|
451
|
-
token: `${tokenData.token.slice(0, 5)}...<redacted>`
|
|
452
|
-
};
|
|
453
|
-
if (this.logDebug) this.emit('debug', 'Token:', maskedTokenData);
|
|
454
|
-
|
|
455
|
-
// Save token in memory
|
|
456
|
-
this.feature.info.jwtToken = tokenData;
|
|
457
|
-
|
|
458
|
-
// Persist token to disk
|
|
459
|
-
try {
|
|
460
|
-
await this.functions.saveData(this.envoyTokenFile, tokenData);
|
|
461
|
-
} catch (error) {
|
|
462
|
-
if (this.logError) this.emit('error', `Save token error: ${error}`);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
return true;
|
|
466
|
-
} catch (error) {
|
|
467
|
-
throw new Error(`Get token error: ${error}`);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
async validateToken() {
|
|
472
|
-
if (this.logDebug) this.emit('debug', 'Requesting validate token');
|
|
473
|
-
|
|
474
|
-
this.feature.info.tokenValid = false;
|
|
475
|
-
|
|
476
|
-
try {
|
|
477
|
-
const jwt = this.feature.info.jwtToken;
|
|
478
|
-
|
|
479
|
-
// Create a token-authenticated Axios instance
|
|
480
|
-
const axiosInstance = this.functions.createAxiosInstance(this.url, `Bearer ${jwt.token}`, null);
|
|
481
|
-
|
|
482
|
-
// Send validation request
|
|
483
|
-
const response = await axiosInstance.get(ApiUrls.CheckJwt);
|
|
484
|
-
const responseBody = response.data;
|
|
485
|
-
|
|
486
|
-
// Check for expected response string
|
|
487
|
-
const tokenValid = typeof responseBody === 'string' && responseBody.includes('Valid token');
|
|
488
|
-
if (!tokenValid) {
|
|
489
|
-
if (this.logWarn) this.emit('warn', `Token not valid. Response: ${responseBody}`);
|
|
490
|
-
return null;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// Extract and validate cookie
|
|
494
|
-
const cookie = response.headers['set-cookie'];
|
|
495
|
-
if (!cookie) {
|
|
496
|
-
if (this.logWarn) this.emit('warn', 'No cookie received during token validation');
|
|
497
|
-
return null;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Replace axios instance with cookie-authenticated one
|
|
501
|
-
this.axiosInstance = this.functions.createAxiosInstance(this.url, null, cookie);
|
|
502
|
-
|
|
503
|
-
// Update internal state
|
|
504
|
-
this.feature.info.tokenValid = true;
|
|
505
|
-
this.feature.info.cookie = cookie;
|
|
506
|
-
|
|
507
|
-
this.emit('success', 'Token validate success');
|
|
508
|
-
return true;
|
|
509
|
-
} catch (error) {
|
|
510
|
-
this.feature.info.tokenValid = false;
|
|
511
|
-
throw new Error(`Validate token error: ${error}`);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
async updateMeters() {
|
|
516
|
-
if (this.logDebug) this.emit('debug', `Requesting meters info`);
|
|
517
|
-
|
|
518
|
-
try {
|
|
519
|
-
const response = await this.axiosInstance.get(ApiUrls.InternalMeterInfo);
|
|
520
|
-
const responseData = response.data;
|
|
521
|
-
if (this.logDebug) this.emit('debug', `Meters:`, responseData);
|
|
522
|
-
|
|
523
|
-
// Check if any meters are installed
|
|
524
|
-
const metersInstalled = responseData.length > 0;
|
|
525
|
-
if (metersInstalled) {
|
|
526
|
-
const arr = [];
|
|
527
|
-
for (const meter of responseData) {
|
|
528
|
-
const key = MetersKeyMap[meter.measurementType];
|
|
529
|
-
if (!key) {
|
|
530
|
-
if (this.logDebug) this.emit('debug', `Unknown meter measurement type: ${meter.measurementType}`);
|
|
531
|
-
continue;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
const phaseModeCode = ApiCodes[meter.phaseMode];
|
|
535
|
-
const meteringStatusCode = ApiCodes[meter.meteringStatus];
|
|
536
|
-
const voltageDivide = meter.phaseMode === 'split' ? 2 : meter.phaseMode === 'three' ? 3 : 1;
|
|
537
|
-
const powerFactorDivide = meter.phaseMode === 'split' ? 2 : 1;
|
|
538
|
-
|
|
539
|
-
const obj = {
|
|
540
|
-
eid: meter.eid,
|
|
541
|
-
type: 'eim',
|
|
542
|
-
activeCount: 1,
|
|
543
|
-
measurementType: meter.measurementType,
|
|
544
|
-
state: meter.state === 'enabled',
|
|
545
|
-
phaseMode: phaseModeCode,
|
|
546
|
-
phaseCount: meter.phaseCount ?? 1,
|
|
547
|
-
meteringStatus: meteringStatusCode,
|
|
548
|
-
statusFlags: meter.statusFlags,
|
|
549
|
-
voltageDivide: voltageDivide,
|
|
550
|
-
powerFactorDivide: powerFactorDivide,
|
|
551
|
-
};
|
|
552
|
-
arr.push(obj);
|
|
553
|
-
|
|
554
|
-
this.feature.meters[key].supported = true;
|
|
555
|
-
this.feature.meters[key].enabled = obj.state;
|
|
556
|
-
}
|
|
557
|
-
this.pv.meters = arr;
|
|
558
|
-
this.feature.meters.installed = arr.some(m => m.state);
|
|
559
|
-
this.feature.meters.count = arr.length;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
//meters supported
|
|
563
|
-
this.feature.meters.supported = true;
|
|
564
|
-
|
|
565
|
-
return true;
|
|
566
|
-
} catch (error) {
|
|
567
|
-
throw new Error(`Update meters error: ${error}`);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
async updateMetersReading(start) {
|
|
572
|
-
if (this.logDebug) this.emit('debug', `Requesting meters reading`);
|
|
573
|
-
|
|
574
|
-
try {
|
|
575
|
-
const response = await this.axiosInstance.get(ApiUrls.InternalMeterReadings);
|
|
576
|
-
const responseData = response.data;
|
|
577
|
-
if (this.logDebug) this.emit('debug', `Meters reading:`, responseData);
|
|
578
|
-
|
|
579
|
-
// Check if readings exist and are valid
|
|
580
|
-
const metersReadingInstalled = Array.isArray(responseData) && responseData.length > 0;
|
|
581
|
-
if (metersReadingInstalled) {
|
|
582
|
-
for (const meter of responseData) {
|
|
583
|
-
const meterConfig = this.pv.meters.find(m => m.eid === meter.eid && m.state === true);
|
|
584
|
-
if (!meterConfig) continue;
|
|
585
|
-
|
|
586
|
-
const obj = {
|
|
587
|
-
readingTime: meter.timestamp,
|
|
588
|
-
power: meter.activePower,
|
|
589
|
-
apparentPower: meter.apparentPower,
|
|
590
|
-
reactivePower: meter.reactivePower,
|
|
591
|
-
energyLifetime: meter.actEnergyDlvd,
|
|
592
|
-
energyLifetimeUpload: meter.actEnergyRcvd,
|
|
593
|
-
apparentEnergy: meter.apparentEnergy,
|
|
594
|
-
current: meter.current,
|
|
595
|
-
voltage: meter.voltage / meterConfig.voltageDivide,
|
|
596
|
-
pwrFactor: meter.pwrFactor / meterConfig.powerFactorDivide,
|
|
597
|
-
frequency: meter.freq,
|
|
598
|
-
channels: meter.channels ?? [],
|
|
599
|
-
};
|
|
600
|
-
Object.assign(meterConfig, obj);
|
|
601
|
-
}
|
|
602
|
-
this.feature.metersReading.installed = true;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
//meters readings supported
|
|
606
|
-
this.feature.metersReading.supported = true;
|
|
607
|
-
|
|
608
|
-
return true;
|
|
609
|
-
} catch (error) {
|
|
610
|
-
if (start) {
|
|
611
|
-
if (this.logWarn) this.emit('warn', `Meters readings not supported, dont worry all working correct, only some additional data will not be present, error: ${error}`);
|
|
612
|
-
return null;
|
|
613
|
-
}
|
|
614
|
-
throw new Error(`Update meters reading error: ${error}`);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
async updateMetersReports(start) {
|
|
619
|
-
if (this.logDebug) this.emit('debug', `Requesting meters reports`);
|
|
620
|
-
|
|
621
|
-
try {
|
|
622
|
-
const response = await this.axiosInstance.get(ApiUrls.InternalMetersReports);
|
|
623
|
-
const responseData = response.data;
|
|
624
|
-
if (this.logDebug) this.emit('debug', `Meters reports:`, responseData);
|
|
625
|
-
|
|
626
|
-
// Check if reports exist
|
|
627
|
-
const metersReportsInstalled = Array.isArray(responseData) && responseData.length > 0;
|
|
628
|
-
if (metersReportsInstalled) {
|
|
629
|
-
for (const meter of responseData) {
|
|
630
|
-
const key = MetersKeyMap[meter.reportType];
|
|
631
|
-
if (!key) {
|
|
632
|
-
if (!this.logDebug) this.emit('debug', `Unknown meters reports type: ${meter.reportType}`);
|
|
633
|
-
continue;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
const meterConfig = key === 'consumptionTotal' ? this.pv.meters.find(m => m.measurementType === 'net-consumption' && m.state === true) : this.pv.meters.find(m => m.measurementType === meter.reportType && m.state === true);
|
|
637
|
-
if (!meterConfig) continue;
|
|
638
|
-
|
|
639
|
-
const cumulative = meter.cumulative;
|
|
640
|
-
const obj = {
|
|
641
|
-
readingTime: meter.createdAt,
|
|
642
|
-
power: cumulative.actPower,
|
|
643
|
-
apparentPower: cumulative.apprntPwr,
|
|
644
|
-
reactivePower: cumulative.reactPwr,
|
|
645
|
-
energyLifetime: cumulative.whDlvdCum,
|
|
646
|
-
energyLifetimeUpload: cumulative.whRcvdCum,
|
|
647
|
-
apparentEnergy: cumulative.vahCum,
|
|
648
|
-
current: cumulative.rmsCurrent,
|
|
649
|
-
voltage: cumulative.rmsVoltage / meterConfig.voltageDivide,
|
|
650
|
-
pwrFactor: cumulative.pwrFactor / meterConfig.powerFactorDivide,
|
|
651
|
-
frequency: cumulative.freqHz,
|
|
652
|
-
channels: meter.lines ?? [],
|
|
653
|
-
};
|
|
654
|
-
|
|
655
|
-
// Handle each meter type
|
|
656
|
-
switch (key) {
|
|
657
|
-
case 'consumptionTotal':
|
|
658
|
-
const obj1 = {
|
|
659
|
-
eid: meterConfig.eid,
|
|
660
|
-
type: meterConfig.type,
|
|
661
|
-
activeCount: meterConfig.activeCount,
|
|
662
|
-
measurementType: 'total-consumption',
|
|
663
|
-
readingTime: meterConfig.readingTime,
|
|
664
|
-
state: meterConfig.state,
|
|
665
|
-
phaseMode: meterConfig.phaseMode,
|
|
666
|
-
phaseCount: meterConfig.phaseCount,
|
|
667
|
-
meteringStatus: meterConfig.meteringStatus,
|
|
668
|
-
statusFlags: meterConfig.statusFlags,
|
|
669
|
-
voltageDivide: meterConfig.voltageDivide,
|
|
670
|
-
powerFactorDivide: meterConfig.powerFactorDivide,
|
|
671
|
-
};
|
|
672
|
-
this.pv.meters = [...this.pv.meters, { ...obj1, ...obj }];
|
|
673
|
-
this.feature.meters[key].supported = true;
|
|
674
|
-
this.feature.meters[key].enabled = true;
|
|
675
|
-
break;
|
|
676
|
-
default:
|
|
677
|
-
Object.assign(meterConfig, obj);
|
|
678
|
-
break;
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
this.feature.metersReports.installed = true;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
//meters reports supported
|
|
685
|
-
this.feature.metersReports.supported = true;
|
|
686
|
-
|
|
687
|
-
return true;
|
|
688
|
-
} catch (error) {
|
|
689
|
-
if (start) {
|
|
690
|
-
if (this.logWarn) this.emit('warn', `Meters reports not supported, dont worry all working correct, only some additional data will not be present, error: ${error}`);
|
|
691
|
-
return null;
|
|
692
|
-
}
|
|
693
|
-
throw new Error(`Update meters reports error: ${error}`);
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
async updateProduction() {
|
|
698
|
-
if (this.logDebug) this.emit('debug', `Requesting production`);
|
|
699
|
-
|
|
700
|
-
try {
|
|
701
|
-
const response = await this.axiosInstance.get(ApiUrls.Production);
|
|
702
|
-
const production = response.data;
|
|
703
|
-
if (this.logDebug) this.emit('debug', `Production:`, production);
|
|
704
|
-
|
|
705
|
-
const productionSupported = Object.keys(production).length > 0;
|
|
706
|
-
if (productionSupported) {
|
|
707
|
-
const readingTime = this.functions.formatTimestamp();
|
|
708
|
-
const obj = {
|
|
709
|
-
type: 'pcu',
|
|
710
|
-
activeCount: this.feature.inventory.pcus.count,
|
|
711
|
-
measurementType: 'production',
|
|
712
|
-
readingTime,
|
|
713
|
-
power: production.wattsNow,
|
|
714
|
-
energyToday: production.wattHoursToday,
|
|
715
|
-
energyLastSevenDays: production.wattHoursSevenDays,
|
|
716
|
-
energyLifetime: production.wattHoursLifetime
|
|
717
|
-
};
|
|
718
|
-
|
|
719
|
-
this.pv.powerAndEnergy.production.pcu = obj;
|
|
720
|
-
this.feature.production.supported = true;
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
return true;
|
|
724
|
-
} catch (error) {
|
|
725
|
-
throw new Error(`Update production error: ${error}`);
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
async updateProductionPdm() {
|
|
730
|
-
if (this.logDebug) this.emit('debug', `Requesting production pdm`);
|
|
731
|
-
|
|
732
|
-
try {
|
|
733
|
-
const response = await this.axiosInstance.get(ApiUrls.ProductionPdm);
|
|
734
|
-
const data = response.data;
|
|
735
|
-
if (this.logDebug) this.emit('debug', `Production pdm:`, data);
|
|
736
|
-
|
|
737
|
-
const readingTime = this.functions.formatTimestamp();
|
|
738
|
-
|
|
739
|
-
// PCU
|
|
740
|
-
const pcu = {
|
|
741
|
-
type: 'pcu',
|
|
742
|
-
measurementType: 'production',
|
|
743
|
-
activeCount: this.feature.inventory?.pcus?.count,
|
|
744
|
-
readingTime,
|
|
745
|
-
power: data.watts_now_pcu,
|
|
746
|
-
energyToday: data.joules_today_pcu / 3600,
|
|
747
|
-
energyLastSevenDays: data.pcu_joules_seven_days / 3600,
|
|
748
|
-
energyLifetime: data.joules_lifetime_pcu / 3600
|
|
749
|
-
};
|
|
750
|
-
this.pv.powerAndEnergy.production.pcu = pcu;
|
|
751
|
-
this.feature.productionPdm.pcu.supported = pcu.power > 0;
|
|
752
|
-
|
|
753
|
-
// EIM
|
|
754
|
-
const eimActive = !!data.there_is_an_active_eim;
|
|
755
|
-
const eim = {
|
|
756
|
-
type: 'eim',
|
|
757
|
-
measurementType: 'production',
|
|
758
|
-
activeCount: 1,
|
|
759
|
-
readingTime,
|
|
760
|
-
active: eimActive,
|
|
761
|
-
power: data.watts_now_eim,
|
|
762
|
-
energyToday: data.watt_hours_today_eim?.aggregate,
|
|
763
|
-
energyLastSevenDays: data.eim_watt_hours_seven_days?.aggregate,
|
|
764
|
-
energyLifetime: data.watt_hours_lifetime_eim?.aggregate
|
|
765
|
-
};
|
|
766
|
-
this.pv.powerAndEnergy.production.eim = eim;
|
|
767
|
-
this.feature.productionPdm.eim.supported = eimActive;
|
|
768
|
-
|
|
769
|
-
// RGM
|
|
770
|
-
const rgmActive = !!data.there_is_an_active_rgm;
|
|
771
|
-
const rgm = {
|
|
772
|
-
type: 'rgm',
|
|
773
|
-
measurementType: 'production',
|
|
774
|
-
activeCount: 1,
|
|
775
|
-
readingTime,
|
|
776
|
-
active: rgmActive,
|
|
777
|
-
power: data.watts_now_rgm,
|
|
778
|
-
energyToday: data.watt_hours_today_rgm,
|
|
779
|
-
energyLastSevenDays: data.rgm_watt_hours_seven_days,
|
|
780
|
-
energyLifetime: data.watt_hours_lifetime_rgm
|
|
781
|
-
};
|
|
782
|
-
this.pv.powerAndEnergy.production.rgm = rgm;
|
|
783
|
-
this.feature.productionPdm.rgm.supported = rgmActive;
|
|
784
|
-
|
|
785
|
-
// PMU
|
|
786
|
-
const pmuActive = !!data.there_is_an_active_pmu;
|
|
787
|
-
const pmu = {
|
|
788
|
-
type: 'pmu',
|
|
789
|
-
measurementType: 'production',
|
|
790
|
-
activeCount: 1,
|
|
791
|
-
readingTime,
|
|
792
|
-
active: pmuActive,
|
|
793
|
-
power: data.watts_now_pmu,
|
|
794
|
-
energyToday: data.watt_hours_today_pmu,
|
|
795
|
-
energyLastSevenDays: data.pmu_watt_hours_seven_days,
|
|
796
|
-
energyLifetime: data.watt_hours_lifetime_pmu
|
|
797
|
-
};
|
|
798
|
-
this.pv.powerAndEnergy.production.pmu = pmu;
|
|
799
|
-
this.feature.productionPdm.pmu.supported = pmuActive;
|
|
800
|
-
|
|
801
|
-
// Mark as supported
|
|
802
|
-
this.feature.productionPdm.supported = true;
|
|
803
|
-
|
|
804
|
-
return true;
|
|
805
|
-
} catch (error) {
|
|
806
|
-
throw new Error(`Update production pdm error: ${error}`);
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
async updateEnergyPdm() {
|
|
811
|
-
if (this.logDebug) this.emit('debug', `Requesting energy pdm`);
|
|
812
|
-
|
|
813
|
-
try {
|
|
814
|
-
const response = await this.axiosInstance.get(ApiUrls.EnergyPdm);
|
|
815
|
-
const energyPdm = response.data;
|
|
816
|
-
if (this.logDebug) this.emit('debug', `Energy pdm: `, energyPdm);
|
|
817
|
-
|
|
818
|
-
const readingTime = this.functions.formatTimestamp();
|
|
819
|
-
|
|
820
|
-
// Process production data
|
|
821
|
-
if ('production' in energyPdm && energyPdm.production) {
|
|
822
|
-
for (const [type, data] of Object.entries(energyPdm.production)) {
|
|
823
|
-
if (data) {
|
|
824
|
-
const obj = {
|
|
825
|
-
type,
|
|
826
|
-
activeCount: 1,
|
|
827
|
-
measurementType: 'production',
|
|
828
|
-
readingTime,
|
|
829
|
-
power: data.wattsNow,
|
|
830
|
-
energyToday: data.wattHoursToday,
|
|
831
|
-
energyLastSevenDays: data.wattHoursSevenDays,
|
|
832
|
-
energyLifetime: data.wattHoursLifetime
|
|
833
|
-
};
|
|
834
|
-
this.pv.powerAndEnergy.production[type] = obj;
|
|
835
|
-
this.feature.energyPdm.production[type].supported = true;
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
this.feature.energyPdm.production.supported = true;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
// Process consumption data
|
|
842
|
-
if ('consumption' in energyPdm && energyPdm.consumption?.eim) {
|
|
843
|
-
const data = energyPdm.consumption.eim;
|
|
844
|
-
const obj = {
|
|
845
|
-
type: 'eim',
|
|
846
|
-
activeCount: 1,
|
|
847
|
-
measurementType: 'net-consumption',
|
|
848
|
-
readingTime,
|
|
849
|
-
power: data.wattsNow,
|
|
850
|
-
energyToday: data.wattHoursToday,
|
|
851
|
-
energyLastSevenDays: data.wattHoursSevenDays,
|
|
852
|
-
energyLifetime: data.wattHoursLifetime
|
|
853
|
-
};
|
|
854
|
-
Object.assign(this.pv.powerAndEnergy.consumptionNet, obj);
|
|
855
|
-
this.feature.energyPdm.comsumptionNet.supported = true;
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
this.feature.energyPdm.supported = true;
|
|
859
|
-
|
|
860
|
-
return true;
|
|
861
|
-
} catch (error) {
|
|
862
|
-
throw new Error(`Update energy pdm error: ${error}`);
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
async updateProductionCt() {
|
|
867
|
-
if (this.logDebug) this.emit('debug', `Requesting production ct`);
|
|
868
|
-
|
|
869
|
-
try {
|
|
870
|
-
const response = await this.axiosInstance.get(ApiUrls.SystemReadingStats);
|
|
871
|
-
const data = response.data;
|
|
872
|
-
if (this.logDebug) this.emit('debug', `Production ct:`, data);
|
|
873
|
-
|
|
874
|
-
const keys = Object.keys(data);
|
|
875
|
-
|
|
876
|
-
// --- Production: PCU ---
|
|
877
|
-
if (keys.includes('production') && Array.isArray(data.production)) {
|
|
878
|
-
const productionPcu = data.production[0];
|
|
879
|
-
if (productionPcu) {
|
|
880
|
-
this.feature.productionCt.production.pcu.supported = true;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
const productionEim = data.production[1];
|
|
884
|
-
if (productionEim) {
|
|
885
|
-
const energyToday = (productionEim.lines[0]?.whToday || 0) + (productionEim.lines[1]?.whToday || 0) + (productionEim.lines[2]?.whToday || 0);
|
|
886
|
-
const energyLastSevenDays = (productionEim.lines[0]?.whLastSevenDays || 0) + (productionEim.lines[1]?.whLastSevenDays || 0) + (productionEim.lines[2]?.whLastSevenDays || 0);
|
|
887
|
-
const energyLifetime = (productionEim.lines[0]?.whLifetime || 0) + (productionEim.lines[1]?.whLifetime || 0) + (productionEim.lines[2]?.whLifetime || 0);
|
|
888
|
-
const obj = {
|
|
889
|
-
type: 'eim',
|
|
890
|
-
activeCount: 1,
|
|
891
|
-
measurementType: productionEim.measurementType,
|
|
892
|
-
readingTime: productionEim.readingTime,
|
|
893
|
-
power: productionEim.wNow,
|
|
894
|
-
energyToday,
|
|
895
|
-
energyLastSevenDays,
|
|
896
|
-
energyLifetime,
|
|
897
|
-
reactivePower: productionEim.reactPwr,
|
|
898
|
-
apparentPower: productionEim.apprntPwr,
|
|
899
|
-
current: productionEim.rmsCurrent,
|
|
900
|
-
voltage: productionEim.rmsVoltage,
|
|
901
|
-
pwrFactor: productionEim.pwrFactor
|
|
902
|
-
};
|
|
903
|
-
this.pv.powerAndEnergy.production.eim = obj;
|
|
904
|
-
this.feature.productionCt.production.eim.supported = true;
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
// --- Consumption: EIM ---
|
|
909
|
-
if (keys.includes('consumption') && Array.isArray(data.consumption) && this.feature.meters.consumptionNet.enabled) {
|
|
910
|
-
for (const item of data.consumption) {
|
|
911
|
-
const key = MetersKeyMap[item.measurementType];
|
|
912
|
-
const energyToday = (item.lines[0]?.whToday || 0) + (item.lines[1]?.whToday || 0) + (item.lines[2]?.whToday || 0);
|
|
913
|
-
const energyLastSevenDays = (item.lines[0]?.whLastSevenDays || 0) + (item.lines[1]?.whLastSevenDays || 0) + (item.lines[2]?.whLastSevenDays || 0);
|
|
914
|
-
const energyLifetime = (item.lines[0]?.whLifetime || 0) + (item.lines[1]?.whLifetime || 0) + (item.lines[2]?.whLifetime || 0);
|
|
915
|
-
const obj = {
|
|
916
|
-
type: 'eim',
|
|
917
|
-
measurementType: item.measurementType,
|
|
918
|
-
activeCount: 1,
|
|
919
|
-
readingTime: item.readingTime,
|
|
920
|
-
power: item.wNow,
|
|
921
|
-
energyToday,
|
|
922
|
-
energyLastSevenDays,
|
|
923
|
-
energyLifetime,
|
|
924
|
-
reactivePower: item.reactPwr,
|
|
925
|
-
apparentPower: item.apprntPwr,
|
|
926
|
-
current: item.rmsCurrent,
|
|
927
|
-
voltage: item.rmsVoltage,
|
|
928
|
-
pwrFactor: item.pwrFactor
|
|
929
|
-
};
|
|
930
|
-
Object.assign(this.pv.powerAndEnergy[key], obj);
|
|
931
|
-
this.feature.productionCt[key].supported = true;
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
// --- Finalize ---
|
|
936
|
-
this.feature.productionCt.supported = true;
|
|
937
|
-
|
|
938
|
-
return true;
|
|
939
|
-
} catch (error) {
|
|
940
|
-
throw new Error(`Update production ct error: ${error}`);
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
async updatePowerAndEnergyData() {
|
|
945
|
-
if (this.logDebug) this.emit('debug', `Requesting power and energy data`);
|
|
946
|
-
|
|
947
|
-
try {
|
|
948
|
-
const powerAndEnergy = [];
|
|
949
|
-
const powerAndEnergyTypeArr = [
|
|
950
|
-
{ type: 'production', state: this.feature.meters.production.enabled },
|
|
951
|
-
{ type: 'net-consumption', state: this.feature.meters.consumptionNet.enabled },
|
|
952
|
-
{ type: 'total-consumption', state: this.feature.meters.consumptionTotal.enabled }
|
|
953
|
-
];
|
|
954
|
-
|
|
955
|
-
for (const [index, data] of powerAndEnergyTypeArr.entries()) {
|
|
956
|
-
const { type: meterType, state: meterEnabled } = data;
|
|
957
|
-
if (meterType !== 'production' && !meterEnabled) continue;
|
|
958
|
-
|
|
959
|
-
const key = MetersKeyMap[meterType];
|
|
960
|
-
const measurementType = ApiCodes[meterType];
|
|
961
|
-
|
|
962
|
-
let sourceMeter, sourceEnergy;
|
|
963
|
-
let power, energyLifetime, energyLifetimeWithOffset;
|
|
964
|
-
switch (key) {
|
|
965
|
-
case 'production': {
|
|
966
|
-
const sourcePcu = this.pv.powerAndEnergy[key].pcu;
|
|
967
|
-
const sourceEim = this.pv.powerAndEnergy[key].eim;
|
|
968
|
-
sourceMeter = meterEnabled ? this.pv.meters.find(m => m.measurementType === 'production') : sourcePcu;
|
|
969
|
-
sourceEnergy = meterEnabled ? sourceEim : sourcePcu;
|
|
970
|
-
power = this.functions.isValidValue(sourceMeter.power) && sourceMeter.power >= 0 ? sourceMeter.power : 0;
|
|
971
|
-
energyLifetime = this.functions.isValidValue(sourceMeter.energyLifetime) ? sourceMeter.energyLifetime / 1000 : null;
|
|
972
|
-
energyLifetimeWithOffset = this.functions.isValidValue(energyLifetime) ? energyLifetime + this.energyProductionLifetimeOffset : null;
|
|
973
|
-
break;
|
|
974
|
-
}
|
|
975
|
-
case 'consumptionNet': {
|
|
976
|
-
sourceMeter = this.pv.meters.find(m => m.measurementType === 'net-consumption');
|
|
977
|
-
sourceEnergy = this.pv.powerAndEnergy.consumptionNet;
|
|
978
|
-
power = this.functions.isValidValue(sourceMeter.power) && sourceMeter.power >= 0 ? sourceMeter.power : 0;
|
|
979
|
-
energyLifetime = this.functions.isValidValue(sourceMeter.energyLifetime) ? sourceMeter.energyLifetime / 1000 : null;
|
|
980
|
-
energyLifetimeWithOffset = this.functions.isValidValue(energyLifetime) ? energyLifetime + this.energyConsumptionNetLifetimeOffset : null;
|
|
981
|
-
break;
|
|
982
|
-
}
|
|
983
|
-
case 'consumptionTotal': {
|
|
984
|
-
sourceMeter = this.pv.meters.find(m => m.measurementType === 'total-consumption');
|
|
985
|
-
sourceEnergy = this.pv.powerAndEnergy.consumptionTotal;
|
|
986
|
-
power = this.functions.isValidValue(sourceMeter.power) && sourceMeter.power >= 0 ? sourceMeter.power : 0;
|
|
987
|
-
energyLifetime = this.functions.isValidValue(sourceMeter.energyLifetime) ? sourceMeter.energyLifetime / 1000 : null;
|
|
988
|
-
energyLifetimeWithOffset = this.functions.isValidValue(energyLifetime) ? energyLifetime + this.energyConsumptionTotalLifetimeOffset : null;
|
|
989
|
-
break;
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
if (!sourceMeter) continue;
|
|
994
|
-
if (this.logDebug) {
|
|
995
|
-
this.emit('debug', `${measurementType} data source meter:`, sourceMeter);
|
|
996
|
-
this.emit('debug', `${measurementType} data source energy:`, sourceEnergy);
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
if (key === 'production') {
|
|
1000
|
-
const type = ApiCodes[sourceMeter.type] ?? sourceMeter.type;
|
|
1001
|
-
const obj = {
|
|
1002
|
-
type,
|
|
1003
|
-
measurementType,
|
|
1004
|
-
power,
|
|
1005
|
-
energyLifetimeWithOffset,
|
|
1006
|
-
gridQualityState: meterEnabled,
|
|
1007
|
-
};
|
|
1008
|
-
|
|
1009
|
-
// Add to fakegato history
|
|
1010
|
-
this.fakegatoHistoryService?.addEntry({
|
|
1011
|
-
time: Math.floor(Date.now() / 1000),
|
|
1012
|
-
power: power ?? 0
|
|
1013
|
-
});
|
|
1014
|
-
|
|
1015
|
-
// Create characteristics energy meter
|
|
1016
|
-
const characteristics = [
|
|
1017
|
-
{ type: Characteristic.EvePower, value: power },
|
|
1018
|
-
{ type: Characteristic.EveEnergyLifetime, value: energyLifetimeWithOffset },
|
|
1019
|
-
];
|
|
1020
|
-
|
|
1021
|
-
// Create characteristics energy meter
|
|
1022
|
-
if (meterEnabled) {
|
|
1023
|
-
Object.assign(obj, {
|
|
1024
|
-
current: sourceMeter.current,
|
|
1025
|
-
voltage: sourceMeter.voltage
|
|
1026
|
-
});
|
|
1027
|
-
|
|
1028
|
-
characteristics.push([
|
|
1029
|
-
{ type: Characteristic.EveCurrent, value: sourceMeter.current },
|
|
1030
|
-
{ type: Characteristic.EveVoltage, value: sourceMeter.voltage },
|
|
1031
|
-
]);
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
// Update characteristics
|
|
1035
|
-
for (const { type, value } of characteristics) {
|
|
1036
|
-
if (!this.functions.isValidValue(value)) continue;
|
|
1037
|
-
this.energyMeterServices?.[index]?.updateCharacteristic(type, value);
|
|
1038
|
-
};
|
|
1039
|
-
|
|
1040
|
-
powerAndEnergy.push(obj);
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
this.pv.powerAndEnergy.data = powerAndEnergy.filter(Boolean);
|
|
1045
|
-
this.feature.powerAndEnergyData.supported = true;
|
|
1046
|
-
|
|
1047
|
-
return true;
|
|
1048
|
-
} catch (error) {
|
|
1049
|
-
throw new Error(`Update power and energy data error: ${error.message || error}`);
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
async getDeviceInfo() {
|
|
1054
|
-
if (this.logDebug) {
|
|
1055
|
-
this.emit('debug', `Requesting device info`);
|
|
1056
|
-
this.emit('debug', `Pv object:`, this.pv);
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
// Device basic info
|
|
1060
|
-
this.emit('devInfo', `-------- ${this.name} --------`);
|
|
1061
|
-
this.emit('devInfo', `Manufacturer: Enphase`);
|
|
1062
|
-
this.emit('devInfo', `Model: ${this.pv.info.modelName}`);
|
|
1063
|
-
this.emit('devInfo', `------------------------------`);
|
|
1064
|
-
return true;
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
//prepare accessory
|
|
1068
|
-
async prepareAccessory() {
|
|
1069
|
-
try {
|
|
1070
|
-
if (this.logDebug) this.emit('debug', `Prepare accessory`);
|
|
1071
|
-
|
|
1072
|
-
const envoySerialNumber = this.pv.info.serialNumber;
|
|
1073
|
-
const accessoryName = `${this.name} ${this.pv.powerAndEnergy.data[0].measurementType}`;
|
|
1074
|
-
const accessoryUUID = AccessoryUUID.generate(envoySerialNumber + 'Energy Meter');
|
|
1075
|
-
const accessoryCategory = Categories.SENSOR;
|
|
1076
|
-
const accessory = new Accessory(accessoryName, accessoryUUID, accessoryCategory);
|
|
1077
|
-
accessory.log = this.log;
|
|
1078
|
-
|
|
1079
|
-
// Accessory Info Service
|
|
1080
|
-
if (this.logDebug) this.emit('debug', `Prepare Information Service`);
|
|
1081
|
-
accessory.getService(Service.AccessoryInformation)
|
|
1082
|
-
.setCharacteristic(Characteristic.Manufacturer, 'Enphase')
|
|
1083
|
-
.setCharacteristic(Characteristic.Model, this.pv.info.modelName ?? 'Model Name')
|
|
1084
|
-
.setCharacteristic(Characteristic.SerialNumber, envoySerialNumber ?? 'Serial Number')
|
|
1085
|
-
.setCharacteristic(Characteristic.FirmwareRevision, this.pv.info.software?.replace(/[a-zA-Z]/g, '') ?? '0');
|
|
1086
|
-
|
|
1087
|
-
// Create FakeGatoHistory
|
|
1088
|
-
if (this.logDebug) this.emit('debug', `Prepare Fakegato ${this.pv.powerAndEnergy.data[0].measurementType} Service`);
|
|
1089
|
-
this.fakegatoHistoryService = new this.fakegatoHistory(`energy`, accessory, {
|
|
1090
|
-
storage: 'fs',
|
|
1091
|
-
disableRepeatLastData: true,
|
|
1092
|
-
disableTimer: false,
|
|
1093
|
-
path: this.prefDir,
|
|
1094
|
-
filename: this.energyMeterHistoryFileName
|
|
1095
|
-
})
|
|
1096
|
-
this.fakegatoHistoryService.addEntry({
|
|
1097
|
-
time: Math.floor(Date.now() / 1000),
|
|
1098
|
-
power: this.pv.powerAndEnergy.data[0].power ?? 0
|
|
1099
|
-
});
|
|
1100
|
-
|
|
1101
|
-
// Energy Meter Service
|
|
1102
|
-
this.energyMeterServices = [];
|
|
1103
|
-
for (const source of this.pv.powerAndEnergy.data) {
|
|
1104
|
-
const measurementType = source.measurementType;
|
|
1105
|
-
|
|
1106
|
-
if (this.logDebug) this.emit('debug', `Prepare Energy Meter ${measurementType} Service`);
|
|
1107
|
-
const energyMeterService = accessory.addService(Service.EvePowerMeter, `Energy Meter ${measurementType}`, `energyMeterService${measurementType}`);
|
|
1108
|
-
energyMeterService.setCharacteristic(Characteristic.ConfiguredName, `Energy Meter ${measurementType}`);
|
|
1109
|
-
|
|
1110
|
-
// Create characteristics
|
|
1111
|
-
const characteristics = [
|
|
1112
|
-
{ type: Characteristic.EvePower, label: 'power', value: source.power, unit: 'W' },
|
|
1113
|
-
{ type: Characteristic.EveEnergyLifetime, label: 'energy lifetime', value: source.energyLifetimeWithOffset, unit: 'kWh' },
|
|
1114
|
-
];
|
|
1115
|
-
|
|
1116
|
-
if (source.gridQualityState) {
|
|
1117
|
-
characteristics.push(
|
|
1118
|
-
{ type: Characteristic.EveCurrent, label: 'current', value: source.current, unit: 'A' },
|
|
1119
|
-
{ type: Characteristic.EveVoltage, label: 'voltage', value: source.voltage, unit: 'V' }
|
|
1120
|
-
);
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
for (const { type, label, value, unit = '' } of characteristics) {
|
|
1124
|
-
if (!this.functions.isValidValue(value)) continue;
|
|
1125
|
-
|
|
1126
|
-
energyMeterService.getCharacteristic(type)
|
|
1127
|
-
.onGet(async () => {
|
|
1128
|
-
const currentValue = value;
|
|
1129
|
-
if (this.logInfo) this.emit('info', `Energy Meter: ${measurementType}, ${label}: ${currentValue} ${unit}`);
|
|
1130
|
-
return currentValue;
|
|
1131
|
-
});
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
energyMeterService.getCharacteristic(Characteristic.EveResetTime)
|
|
1135
|
-
.onGet(async () => {
|
|
1136
|
-
const resetTime = this.lastReset;
|
|
1137
|
-
if (this.logInfo) this.emit('info', `${measurementType}, reset time: ${resetTime}`);
|
|
1138
|
-
return resetTime;
|
|
1139
|
-
})
|
|
1140
|
-
.onSet(async (value) => {
|
|
1141
|
-
try {
|
|
1142
|
-
this.lastReset = value;
|
|
1143
|
-
} catch (error) {
|
|
1144
|
-
if (this.logWarn) this.emit('warn', `${measurementType}, Reset time error: ${error}`);
|
|
1145
|
-
}
|
|
1146
|
-
});
|
|
1147
|
-
|
|
1148
|
-
this.energyMeterServices.push(energyMeterService);
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
return accessory;
|
|
1152
|
-
} catch (error) {
|
|
1153
|
-
throw new Error(`Prepare accessory error: ${error}`);
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
//start
|
|
1158
|
-
async start() {
|
|
1159
|
-
if (this.logDebug) this.emit('debug', `Start`);
|
|
1160
|
-
|
|
1161
|
-
try {
|
|
1162
|
-
// Create axios instance
|
|
1163
|
-
this.axiosInstance = this.functions.createAxiosInstance(this.url);
|
|
1164
|
-
|
|
1165
|
-
// Get basic PV info
|
|
1166
|
-
const getInfo = await this.getInfo();
|
|
1167
|
-
if (!getInfo) return null;
|
|
1168
|
-
|
|
1169
|
-
const tokenRequired = this.feature.info.tokenRequired;
|
|
1170
|
-
|
|
1171
|
-
// Authenticate
|
|
1172
|
-
const tokenValid = tokenRequired ? await this.checkToken(true) : true;
|
|
1173
|
-
if (tokenRequired && !tokenValid) return null;
|
|
1174
|
-
|
|
1175
|
-
// Meters
|
|
1176
|
-
const getMeters = this.feature.meters.supported ? await this.updateMeters() : false;
|
|
1177
|
-
if (getMeters && this.feature.meters.installed) await this.updateMetersReading(true);
|
|
1178
|
-
if (getMeters && this.feature.meters.installed) await this.updateMetersReports(true);
|
|
1179
|
-
|
|
1180
|
-
// Production
|
|
1181
|
-
if (this.feature.info.firmware < 824) await this.updateProduction();
|
|
1182
|
-
if (this.feature.info.firmware >= 824) await this.updateProductionPdm();
|
|
1183
|
-
if (this.feature.info.firmware >= 824) await this.updateEnergyPdm();
|
|
1184
|
-
if (this.feature.meters.installed) await this.updateProductionCt();
|
|
1185
|
-
|
|
1186
|
-
// Data
|
|
1187
|
-
const getPowerAndEnergyData = await this.updatePowerAndEnergyData();
|
|
1188
|
-
|
|
1189
|
-
// Success message
|
|
1190
|
-
this.emit('success', `Connect Success`);
|
|
1191
|
-
|
|
1192
|
-
// Optional logging
|
|
1193
|
-
if (this.logDeviceInfo) await this.getDeviceInfo();
|
|
1194
|
-
|
|
1195
|
-
// Setup timers
|
|
1196
|
-
this.timers = [];
|
|
1197
|
-
if (getPowerAndEnergyData) this.timers.push({ name: 'updatePowerAndEnergy', sampling: 10000 });
|
|
1198
|
-
|
|
1199
|
-
// Prepare HomeKit accessory
|
|
1200
|
-
const accessory = await this.prepareAccessory();
|
|
1201
|
-
return accessory;
|
|
1202
|
-
} catch (error) {
|
|
1203
|
-
throw new Error(`Start error: ${error}`);
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
}
|
|
1208
|
-
export default EnergyMeter;
|