homebridge-melcloud-control 4.0.0-beta.43 → 4.0.0-beta.430
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -11
- package/config.schema.json +272 -314
- package/homebridge-ui/public/index.html +104 -64
- package/homebridge-ui/server.js +5 -9
- package/index.js +38 -20
- package/package.json +6 -3
- package/src/constants.js +15 -22
- package/src/deviceata.js +199 -188
- package/src/deviceatw.js +7 -5
- package/src/deviceerv.js +28 -26
- package/src/functions.js +124 -3
- package/src/melcloud.js +312 -131
- package/src/melcloudata.js +139 -308
- package/src/melcloudatw.js +3 -228
- package/src/melclouderv.js +5 -162
- package/src/melcloudhometoken.js +231 -0
- package/src/restful.js +1 -1
package/src/deviceatw.js
CHANGED
|
@@ -6,7 +6,7 @@ import { TemperatureDisplayUnits, HeatPump } from './constants.js';
|
|
|
6
6
|
let Accessory, Characteristic, Service, Categories, AccessoryUUID;
|
|
7
7
|
|
|
8
8
|
class DeviceAtw extends EventEmitter {
|
|
9
|
-
constructor(api, account, device, contextKey, devicesFile, useFahrenheit, restFul, mqtt) {
|
|
9
|
+
constructor(api, account, device, contextKey, devicesFile, defaultTempsFile, useFahrenheit, restFul, mqtt) {
|
|
10
10
|
super();
|
|
11
11
|
|
|
12
12
|
Accessory = api.platformAccessory;
|
|
@@ -16,8 +16,9 @@ class DeviceAtw extends EventEmitter {
|
|
|
16
16
|
AccessoryUUID = api.hap.uuid;
|
|
17
17
|
|
|
18
18
|
//account config
|
|
19
|
+
this.accountMelcloud = account.displayType === 'melcloud' ? true : false;
|
|
19
20
|
this.device = device;
|
|
20
|
-
this.
|
|
21
|
+
this.displayType = device.displayType;
|
|
21
22
|
this.hideZone = device.hideZone;
|
|
22
23
|
this.temperatureSensor = device.temperatureSensor || false;
|
|
23
24
|
this.temperatureSensorFlow = device.temperatureSensorFlow || false;
|
|
@@ -40,6 +41,7 @@ class DeviceAtw extends EventEmitter {
|
|
|
40
41
|
this.deviceName = device.name;
|
|
41
42
|
this.deviceTypeText = device.typeString;
|
|
42
43
|
this.devicesFile = devicesFile;
|
|
44
|
+
this.defaultTempsFile = defaultTempsFile;
|
|
43
45
|
this.displayDeviceInfo = true;
|
|
44
46
|
|
|
45
47
|
//external integrations
|
|
@@ -305,7 +307,7 @@ class DeviceAtw extends EventEmitter {
|
|
|
305
307
|
this.accessory.zones.forEach((zone, i) => {
|
|
306
308
|
const zoneName = zone.name
|
|
307
309
|
const serviceName = `${deviceTypeText} ${accessoryName}: ${zoneName}`;
|
|
308
|
-
switch (this.
|
|
310
|
+
switch (this.displayType) {
|
|
309
311
|
case 1: //Heater Cooler
|
|
310
312
|
if (this.logDebug) this.emit('debug', `Prepare heather/cooler ${zoneName} service`);
|
|
311
313
|
const melCloudService = new Service.HeaterCooler(serviceName, `HeaterCooler ${deviceId} ${i}`);
|
|
@@ -1284,7 +1286,7 @@ class DeviceAtw extends EventEmitter {
|
|
|
1284
1286
|
async start() {
|
|
1285
1287
|
try {
|
|
1286
1288
|
//melcloud device
|
|
1287
|
-
this.melCloudAtw = new MelCloudAtw(this.device, this.contextKey, this.devicesFile)
|
|
1289
|
+
this.melCloudAtw = new MelCloudAtw(this.device, this.contextKey, this.devicesFile, this.defaultTempsFile)
|
|
1288
1290
|
.on('deviceInfo', (manufacturer, modelIndoor, modelOutdoor, serialNumber, firmwareAppVersion, hasHotWaterTank, hasZone2) => {
|
|
1289
1291
|
if (this.logDeviceInfo && this.displayDeviceInfo) {
|
|
1290
1292
|
this.emit('devInfo', `---- ${this.deviceTypeText}: ${this.deviceName} ----`);
|
|
@@ -1435,7 +1437,7 @@ class DeviceAtw extends EventEmitter {
|
|
|
1435
1437
|
let temperatureSetPropsMaxValue = 100;
|
|
1436
1438
|
|
|
1437
1439
|
for (let i = 0; i < zonesCount; i++) {
|
|
1438
|
-
switch (this.
|
|
1440
|
+
switch (this.displayType) {
|
|
1439
1441
|
case 1: //Heater Cooler
|
|
1440
1442
|
switch (i) {
|
|
1441
1443
|
case caseHeatPump: //Heat Pump Operation Mode - IDLE, HOT WATER, HEATING ZONES, COOLING, HOT WATER STORAGE, FREEZE STAT, LEGIONELLA, HEATING ECO, MODE 1, MODE 2, MODE 3, HEATING UP /// Unit Status - HEAT, COOL
|
package/src/deviceerv.js
CHANGED
|
@@ -6,7 +6,7 @@ import { TemperatureDisplayUnits, Ventilation } from './constants.js';
|
|
|
6
6
|
let Accessory, Characteristic, Service, Categories, AccessoryUUID;
|
|
7
7
|
|
|
8
8
|
class DeviceErv extends EventEmitter {
|
|
9
|
-
constructor(api, account, device, contextKey, devicesFile, useFahrenheit, restFul, mqtt) {
|
|
9
|
+
constructor(api, account, device, contextKey, devicesFile, defaultTempsFile, useFahrenheit, restFul, mqtt) {
|
|
10
10
|
super();
|
|
11
11
|
|
|
12
12
|
Accessory = api.platformAccessory;
|
|
@@ -16,8 +16,9 @@ class DeviceErv extends EventEmitter {
|
|
|
16
16
|
AccessoryUUID = api.hap.uuid;
|
|
17
17
|
|
|
18
18
|
//account config
|
|
19
|
+
this.accountMelcloud = account.displayType === 'melcloud' ? true : false;
|
|
19
20
|
this.device = device;
|
|
20
|
-
this.
|
|
21
|
+
this.displayType = device.displayType;
|
|
21
22
|
this.temperatureSensor = device.temperatureSensor || false;
|
|
22
23
|
this.temperatureSensorOutdoor = device.temperatureSensorOutdoor || false;
|
|
23
24
|
this.temperatureSensorSupply = device.temperatureSensorSupply || false;
|
|
@@ -33,6 +34,7 @@ class DeviceErv extends EventEmitter {
|
|
|
33
34
|
this.deviceName = device.name;
|
|
34
35
|
this.deviceTypeText = device.typeString;
|
|
35
36
|
this.devicesFile = devicesFile;
|
|
37
|
+
this.defaultTempsFile = defaultTempsFile;
|
|
36
38
|
this.displayDeviceInfo = true;
|
|
37
39
|
|
|
38
40
|
//external integrations
|
|
@@ -152,57 +154,57 @@ class DeviceErv extends EventEmitter {
|
|
|
152
154
|
case 'Power':
|
|
153
155
|
deviceData.Device[key] = value;
|
|
154
156
|
deviceData.Device.EffectiveFlags = Ventilation.EffectiveFlags.Power;
|
|
155
|
-
set = await this.melCloudErv.send(deviceData, this.
|
|
157
|
+
set = await this.melCloudErv.send(deviceData, this.displayType);
|
|
156
158
|
break;
|
|
157
159
|
case 'OperationMode':
|
|
158
160
|
deviceData.Device[key] = value;
|
|
159
161
|
deviceData.Device.EffectiveFlags = Ventilation.EffectiveFlags.OperationMode;
|
|
160
|
-
set = await this.melCloudErv.send(deviceData, this.
|
|
162
|
+
set = await this.melCloudErv.send(deviceData, this.displayType);
|
|
161
163
|
break;
|
|
162
164
|
case 'VentilationMode':
|
|
163
165
|
deviceData.Device[key] = value;
|
|
164
166
|
deviceData.Device.EffectiveFlags = Ventilation.EffectiveFlags.VentilationMode;
|
|
165
|
-
set = await this.melCloudErv.send(deviceData, this.
|
|
167
|
+
set = await this.melCloudErv.send(deviceData, this.displayType);
|
|
166
168
|
break;
|
|
167
169
|
case 'SetTemperature':
|
|
168
170
|
deviceData.Device[key] = value;
|
|
169
171
|
deviceData.Device.EffectiveFlags = Ventilation.EffectiveFlags.SetTemperature;
|
|
170
|
-
set = await this.melCloudErv.send(deviceData, this.
|
|
172
|
+
set = await this.melCloudErv.send(deviceData, this.displayType);
|
|
171
173
|
break;
|
|
172
174
|
case 'DefaultCoolingSetTemperature':
|
|
173
175
|
deviceData.Device[key] = value;
|
|
174
176
|
deviceData.Device.EffectiveFlags = Ventilation.EffectiveFlags.SetTemperature;
|
|
175
|
-
set = await this.melCloudErv.send(deviceData, this.
|
|
177
|
+
set = await this.melCloudErv.send(deviceData, this.displayType);
|
|
176
178
|
break;
|
|
177
179
|
case 'DefaultHeatingSetTemperature':
|
|
178
180
|
deviceData.Device[key] = value;
|
|
179
181
|
deviceData.Device.EffectiveFlags = Ventilation.EffectiveFlags.SetTemperature;
|
|
180
|
-
set = await this.melCloudErv.send(deviceData, this.
|
|
182
|
+
set = await this.melCloudErv.send(deviceData, this.displayType);
|
|
181
183
|
break;
|
|
182
184
|
case 'NightPurgeMode':
|
|
183
185
|
deviceData.Device[key] = value;
|
|
184
186
|
deviceData.Device.EffectiveFlags = Ventilation.EffectiveFlags.NightPurgeMode;
|
|
185
|
-
set = await this.melCloudErv.send(deviceData, this.
|
|
187
|
+
set = await this.melCloudErv.send(deviceData, this.displayType);
|
|
186
188
|
break;
|
|
187
189
|
case 'SetFanSpeed':
|
|
188
190
|
deviceData.Device[key] = value;
|
|
189
191
|
deviceData.Device.EffectiveFlags = Ventilation.EffectiveFlags.SetFanSpeed;
|
|
190
|
-
set = await this.melCloudErv.send(deviceData, this.
|
|
192
|
+
set = await this.melCloudErv.send(deviceData, this.displayType);
|
|
191
193
|
break;
|
|
192
194
|
case 'HideRoomTemperature':
|
|
193
195
|
deviceData[key] = value;
|
|
194
196
|
deviceData.Device.EffectiveFlags = Ventilation.EffectiveFlags.Prohibit;
|
|
195
|
-
set = await this.melCloudErv.send(deviceData, this.
|
|
197
|
+
set = await this.melCloudErv.send(deviceData, this.displayType);
|
|
196
198
|
break;
|
|
197
199
|
case 'HideSupplyTemperature':
|
|
198
200
|
deviceData[key] = value;
|
|
199
201
|
deviceData.Device.EffectiveFlags = Ventilation.EffectiveFlags.Prohibit;
|
|
200
|
-
set = await this.melCloudErv.send(deviceData, this.
|
|
202
|
+
set = await this.melCloudErv.send(deviceData, this.displayType);
|
|
201
203
|
break;
|
|
202
204
|
case 'HideOutdoorTemperature':
|
|
203
205
|
deviceData[key] = value;
|
|
204
206
|
deviceData.Device.EffectiveFlags = Ventilation.EffectiveFlags.Prohibit;
|
|
205
|
-
set = await this.melCloudErv.send(deviceData, this.
|
|
207
|
+
set = await this.melCloudErv.send(deviceData, this.displayType);
|
|
206
208
|
break;
|
|
207
209
|
default:
|
|
208
210
|
this.emit('warn', `${integration}, received key: ${key}, value: ${value}`);
|
|
@@ -263,7 +265,7 @@ class DeviceErv extends EventEmitter {
|
|
|
263
265
|
|
|
264
266
|
//services
|
|
265
267
|
const serviceName = `${deviceTypeText} ${accessoryName}`;
|
|
266
|
-
switch (this.
|
|
268
|
+
switch (this.displayType) {
|
|
267
269
|
case 1: //Heater Cooler
|
|
268
270
|
if (this.logDebug) this.emit('debug', `Prepare heather/cooler service`);
|
|
269
271
|
this.melCloudService = new Service.HeaterCooler(serviceName, `HeaterCooler ${deviceId}`);
|
|
@@ -277,7 +279,7 @@ class DeviceErv extends EventEmitter {
|
|
|
277
279
|
try {
|
|
278
280
|
deviceData.Device.Power = [false, true][state];
|
|
279
281
|
deviceData.Device.EffectiveFlags = Ventilation.EffectiveFlags.Power;
|
|
280
|
-
await this.melCloudErv.send(deviceData, this.
|
|
282
|
+
await this.melCloudErv.send(deviceData, this.displayType);
|
|
281
283
|
if (this.logInfo) this.emit('info', `Set power: ${state ? 'ON' : 'OFF'}`);
|
|
282
284
|
} catch (error) {
|
|
283
285
|
if (this.logWarn) this.emit('warn', `Set power error: ${error}`);
|
|
@@ -313,7 +315,7 @@ class DeviceErv extends EventEmitter {
|
|
|
313
315
|
};
|
|
314
316
|
|
|
315
317
|
deviceData.Device.EffectiveFlags = Ventilation.EffectiveFlags.VentilationMode;
|
|
316
|
-
await this.melCloudErv.send(deviceData, this.
|
|
318
|
+
await this.melCloudErv.send(deviceData, this.displayType);
|
|
317
319
|
const operationModeText = Ventilation.VentilationMode[deviceData.Device.VentilationMode];
|
|
318
320
|
if (this.logInfo) this.emit('info', `Set operation mode: ${operationModeText}`);
|
|
319
321
|
} catch (error) {
|
|
@@ -357,7 +359,7 @@ class DeviceErv extends EventEmitter {
|
|
|
357
359
|
break;;
|
|
358
360
|
};
|
|
359
361
|
deviceData.Device.EffectiveFlags = Ventilation.EffectiveFlags.SetFanSpeed;
|
|
360
|
-
await this.melCloudErv.send(deviceData, this.
|
|
362
|
+
await this.melCloudErv.send(deviceData, this.displayType);
|
|
361
363
|
if (this.logInfo) this.emit('info', `Set fan speed mode: ${Ventilation.FanSpeed[fanSpeedModeText]}`);
|
|
362
364
|
} catch (error) {
|
|
363
365
|
if (this.logWarn) this.emit('warn', `Set fan speed mode error: ${error}`);
|
|
@@ -379,7 +381,7 @@ class DeviceErv extends EventEmitter {
|
|
|
379
381
|
try {
|
|
380
382
|
deviceData.Device.DefaultCoolingSetTemperature = value;
|
|
381
383
|
deviceData.Device.EffectiveFlags = Ventilation.EffectiveFlags.SetTemperature;
|
|
382
|
-
await this.melCloudErv.send(deviceData, this.
|
|
384
|
+
await this.melCloudErv.send(deviceData, this.displayType);
|
|
383
385
|
if (this.logInfo) this.emit('info', `Set cooling threshold temperature: ${value}${this.accessory.temperatureUnit}`);
|
|
384
386
|
} catch (error) {
|
|
385
387
|
if (this.logWarn) this.emit('warn', `Set cooling threshold temperature error: ${error}`);
|
|
@@ -402,7 +404,7 @@ class DeviceErv extends EventEmitter {
|
|
|
402
404
|
try {
|
|
403
405
|
deviceData.Device.DefaultHeatingSetTemperature = value;
|
|
404
406
|
deviceData.Device.EffectiveFlags = Ventilation.EffectiveFlags.SetTemperature;
|
|
405
|
-
await this.melCloudErv.send(deviceData, this.
|
|
407
|
+
await this.melCloudErv.send(deviceData, this.displayType);
|
|
406
408
|
if (this.logInfo) this.emit('info', `Set heating threshold temperature: ${value}${this.accessory.temperatureUnit}`);
|
|
407
409
|
} catch (error) {
|
|
408
410
|
if (this.logWarn) this.emit('warn', `Set heating threshold temperature error: ${error}`);
|
|
@@ -420,7 +422,7 @@ class DeviceErv extends EventEmitter {
|
|
|
420
422
|
// value = value ? true : false;
|
|
421
423
|
// deviceData.Device = deviceData.Device;
|
|
422
424
|
// deviceData.Device.EffectiveFlags = CONSTANTS.Ventilation.EffectiveFlags.Prohibit;
|
|
423
|
-
// await this.melCloudErv.send(deviceData, this.
|
|
425
|
+
// await this.melCloudErv.send(deviceData, this.displayType);
|
|
424
426
|
// if (this.logInfo) this.emit('info', `Set local physical controls: ${value ? 'LOCK' : 'UNLOCK'}`);
|
|
425
427
|
// } catch (error) {
|
|
426
428
|
// if (this.logWarn) this.emit('warn', `Set lock physical controls error: ${error}`);
|
|
@@ -486,7 +488,7 @@ class DeviceErv extends EventEmitter {
|
|
|
486
488
|
break;
|
|
487
489
|
};
|
|
488
490
|
|
|
489
|
-
await this.melCloudErv.send(deviceData, this.
|
|
491
|
+
await this.melCloudErv.send(deviceData, this.displayType);
|
|
490
492
|
const operationModeText = Ventilation.VentilationMode[deviceData.Device.VentilationMode];
|
|
491
493
|
if (this.logInfo) this.emit('info', `Set operation mode: ${operationModeText}`);
|
|
492
494
|
} catch (error) {
|
|
@@ -512,7 +514,7 @@ class DeviceErv extends EventEmitter {
|
|
|
512
514
|
try {
|
|
513
515
|
deviceData.Device.SetTemperature = value;
|
|
514
516
|
deviceData.Device.EffectiveFlags = Ventilation.EffectiveFlags.SetTemperature;
|
|
515
|
-
await this.melCloudErv.send(deviceData, this.
|
|
517
|
+
await this.melCloudErv.send(deviceData, this.displayType);
|
|
516
518
|
if (this.logInfo) this.emit('info', `Set temperature: ${value}${this.accessory.temperatureUnit}`);
|
|
517
519
|
} catch (error) {
|
|
518
520
|
if (this.logWarn) this.emit('warn', `Set temperature error: ${error}`);
|
|
@@ -704,7 +706,7 @@ class DeviceErv extends EventEmitter {
|
|
|
704
706
|
break;
|
|
705
707
|
};
|
|
706
708
|
|
|
707
|
-
await this.melCloudErv.send(deviceData, this.
|
|
709
|
+
await this.melCloudErv.send(deviceData, this.displayType);
|
|
708
710
|
if (this.logInfo) this.emit('info', `${state ? 'Set:' : 'Unset:'} ${name}`);
|
|
709
711
|
} catch (error) {
|
|
710
712
|
if (this.logWarn) this.emit('warn', `Set preset error: ${error}`);
|
|
@@ -818,7 +820,7 @@ class DeviceErv extends EventEmitter {
|
|
|
818
820
|
break;
|
|
819
821
|
};
|
|
820
822
|
|
|
821
|
-
await this.melCloudErv.send(deviceData, this.
|
|
823
|
+
await this.melCloudErv.send(deviceData, this.displayType);
|
|
822
824
|
if (this.logInfo) this.emit('info', `${state ? `Set: ${buttonName}` : `Unset: ${buttonName}, Set: ${button.previousValue}`}`);
|
|
823
825
|
} catch (error) {
|
|
824
826
|
if (this.logWarn) this.emit('warn', `Set button error: ${error}`);
|
|
@@ -839,7 +841,7 @@ class DeviceErv extends EventEmitter {
|
|
|
839
841
|
async start() {
|
|
840
842
|
try {
|
|
841
843
|
//melcloud device
|
|
842
|
-
this.melCloudErv = new MelCloudErv(this.device, this.contextKey, this.devicesFile)
|
|
844
|
+
this.melCloudErv = new MelCloudErv(this.device, this.contextKey, this.devicesFile, this.defaultTempsFile)
|
|
843
845
|
.on('deviceInfo', (manufacturer, modelIndoor, modelOutdoor, serialNumber, firmwareAppVersion) => {
|
|
844
846
|
if (this.logDeviceInfo && this.displayDeviceInfo) {
|
|
845
847
|
this.emit('devInfo', `---- ${this.deviceTypeText}: ${this.deviceName} ----`);
|
|
@@ -956,7 +958,7 @@ class DeviceErv extends EventEmitter {
|
|
|
956
958
|
};
|
|
957
959
|
|
|
958
960
|
//ventilation mode - 0, HEAT, 2, COOL, 4, 5, 6, FAN, AUTO
|
|
959
|
-
switch (this.
|
|
961
|
+
switch (this.displayType) {
|
|
960
962
|
case 1: //Heater Cooler
|
|
961
963
|
switch (ventilationMode) {
|
|
962
964
|
case 0: //LOSSNAY
|
package/src/functions.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { exec } from 'child_process';
|
|
1
3
|
import { promises as fsPromises } from 'fs';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
import puppeteer from 'puppeteer-core';
|
|
6
|
+
const execAsync = promisify(exec);
|
|
2
7
|
|
|
3
8
|
class Functions {
|
|
4
9
|
constructor() {
|
|
@@ -14,12 +19,128 @@ class Functions {
|
|
|
14
19
|
}
|
|
15
20
|
}
|
|
16
21
|
|
|
17
|
-
async readData(path) {
|
|
22
|
+
async readData(path, parseJson = false) {
|
|
18
23
|
try {
|
|
19
|
-
const data = await fsPromises.readFile(path);
|
|
24
|
+
const data = await fsPromises.readFile(path, 'utf8');
|
|
25
|
+
|
|
26
|
+
if (parseJson) {
|
|
27
|
+
if (!data.trim()) {
|
|
28
|
+
// Empty file when expecting JSON
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(data);
|
|
33
|
+
} catch (jsonError) {
|
|
34
|
+
throw new Error(`JSON parse error in file "${path}": ${jsonError.message}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// For non-JSON, just return file content (can be empty string)
|
|
20
39
|
return data;
|
|
21
40
|
} catch (error) {
|
|
22
|
-
|
|
41
|
+
if (error.code === 'ENOENT') {
|
|
42
|
+
// File does not exist
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
// Preserve original error details
|
|
46
|
+
const wrappedError = new Error(`Read data error for "${path}": ${error.message}`);
|
|
47
|
+
wrappedError.original = error;
|
|
48
|
+
throw wrappedError;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async ensureChromiumInstalled(logInfo, logWarn, logError) {
|
|
54
|
+
let chromiumPath = '/usr/bin/chromium-browser'; // default fallback Linux
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// --- Detect platform ---
|
|
58
|
+
const { stdout: archStdout } = await execAsync('uname -m');
|
|
59
|
+
const arch = archStdout.trim();
|
|
60
|
+
logInfo(`Detected architecture: ${arch}`);
|
|
61
|
+
|
|
62
|
+
const { stdout: osNameStdout } = await execAsync('uname -s');
|
|
63
|
+
const osName = osNameStdout.trim();
|
|
64
|
+
logInfo(`Detected OS: ${osName}`);
|
|
65
|
+
|
|
66
|
+
// --- macOS handling ---
|
|
67
|
+
if (osName === 'Darwin') {
|
|
68
|
+
logInfo('Running on macOS — using Puppeteer bundled Chromium');
|
|
69
|
+
chromiumPath = puppeteer.executablePath();
|
|
70
|
+
logInfo(`Chromium path: ${chromiumPath}`);
|
|
71
|
+
return chromiumPath;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- Linux handling ---
|
|
75
|
+
// Detect OS release if available
|
|
76
|
+
let linuxDistro = 'unknown';
|
|
77
|
+
try {
|
|
78
|
+
const { stdout: osRelease } = await execAsync('cat /etc/os-release');
|
|
79
|
+
linuxDistro = osRelease.split('\n')[0] || 'unknown';
|
|
80
|
+
} catch {
|
|
81
|
+
logWarn('/etc/os-release not found, skipping OS detection.');
|
|
82
|
+
}
|
|
83
|
+
logInfo(`Linux distro: ${linuxDistro}`);
|
|
84
|
+
|
|
85
|
+
// --- Check existing Chromium ---
|
|
86
|
+
const { stdout: chromiumCheck } = await execAsync('which chromium || which chromium-browser || true');
|
|
87
|
+
if (chromiumCheck.trim()) {
|
|
88
|
+
chromiumPath = chromiumCheck.trim();
|
|
89
|
+
logInfo(`Found system Chromium: ${chromiumPath}`);
|
|
90
|
+
return chromiumPath;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
logWarn('Chromium not found. Attempting installation...');
|
|
94
|
+
|
|
95
|
+
// --- Try apt-get first ---
|
|
96
|
+
try {
|
|
97
|
+
await execAsync('sudo apt-get update -y && sudo apt-get install -y chromium-browser chromium-codecs-ffmpeg');
|
|
98
|
+
chromiumPath = '/usr/bin/chromium-browser';
|
|
99
|
+
logInfo('Chromium installed successfully via apt-get.');
|
|
100
|
+
return chromiumPath;
|
|
101
|
+
} catch (aptErr) {
|
|
102
|
+
logWarn('apt-get install failed. Trying apk (Alpine) or yum (RHEL/CentOS)...');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// --- Try Alpine APK ---
|
|
106
|
+
try {
|
|
107
|
+
await execAsync('sudo apk add --no-cache chromium ffmpeg');
|
|
108
|
+
chromiumPath = '/usr/bin/chromium-browser';
|
|
109
|
+
logInfo('Chromium installed successfully via apk.');
|
|
110
|
+
return chromiumPath;
|
|
111
|
+
} catch (apkErr) {
|
|
112
|
+
logWarn('apk install failed. Trying yum (RHEL/CentOS)...');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// --- Try YUM ---
|
|
116
|
+
try {
|
|
117
|
+
await execAsync('sudo yum install -y chromium chromium-codecs-ffmpeg');
|
|
118
|
+
chromiumPath = '/usr/bin/chromium-browser';
|
|
119
|
+
logInfo('Chromium installed successfully via yum.');
|
|
120
|
+
return chromiumPath;
|
|
121
|
+
} catch (yumErr) {
|
|
122
|
+
logWarn('yum install failed. Falling back to Puppeteer bundled Chromium.');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --- Fallback: Puppeteer Chromium ---
|
|
126
|
+
chromiumPath = puppeteer.executablePath();
|
|
127
|
+
logWarn(`Using bundled Puppeteer Chromium at ${chromiumPath}`);
|
|
128
|
+
return chromiumPath;
|
|
129
|
+
|
|
130
|
+
} catch (err) {
|
|
131
|
+
logError(`Chromium detection/install error: ${err.message}`);
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async isRunningInDocker() {
|
|
138
|
+
try {
|
|
139
|
+
if (fs.existsSync('/.dockerenv')) return true;
|
|
140
|
+
const cgroup = fs.readFileSync('/proc/1/cgroup', 'utf8');
|
|
141
|
+
return cgroup.includes('docker') || cgroup.includes('kubepods');
|
|
142
|
+
} catch {
|
|
143
|
+
return false;
|
|
23
144
|
}
|
|
24
145
|
}
|
|
25
146
|
}
|