homebridge-eosstb 2.4.0-beta.2 → 2.4.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +1 -2
  3. package/index.js +227 -178
  4. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ See the [Readme file](https://github.com/jsiegenthaler/homebridge-eosstb/blob/ma
5
5
  Please restart Homebridge after every plugin update.
6
6
 
7
7
 
8
+ ## 2.4.0-beta.3 (2026-05-10)
9
+
10
+ This release focusses improved error handling and has extra debugging added to catch a refresh error
11
+
12
+ - Improved handling of errors for web requests
13
+ - Added extra debugging to assist in catching a refresh error in refreshChannelList
14
+
8
15
  ## 2.4.0-beta.2 (2026-05-09)
9
16
 
10
17
  This release focusses on ensuring mqtt long-term stability, and fixes an issue where the channel name was not shown on startup.
@@ -44,6 +51,20 @@ This release represents a major rewrite of the plugin, significantly improving r
44
51
  - Bumped dependency "tough-cookie": "^6.0.1",
45
52
  - Bumped dependency "ws": "^8.20.0"
46
53
 
54
+ ## 2.3.9 (2026-05-09)
55
+ - Fixed Error on Homebridge v2: Cannot read properties of undefined (reading 'STRING')
56
+ - Adapted hidden channel name to reduce warning messages with Homebridge v2
57
+ - Updated iOS and Homebridge version references in Readme
58
+ - Bumped engine "^1.11.4||^2.0.0",
59
+ - Bumped engine "node": "^24.15.0"
60
+ - Bumped dependency "axios": "^1.16.0",
61
+ - Bumped dependency "axios-cookiejar-support": "^7.0.0",
62
+ - Bumped dependency "mqtt": "^5.15.1",
63
+ - Bumped dependency "qs": "^6.15.1",
64
+ - Bumped dependency "semver": "^7.8.0",
65
+ - Bumped dependency "tough-cookie": "^6.0.1",
66
+ - Bumped dependency "ws": "^8.20.0"
67
+
47
68
  ## 2.3.8 (2026-02-27)
48
69
 
49
70
  This is a maintenance release to bring dependencies up to date.
package/README.md CHANGED
@@ -20,7 +20,6 @@
20
20
 
21
21
  The logon method to the backend systems changed in January / February 2024. I managed to get CH working again from April 2026 for CH. NL was confirmed working OK in May 2026. BE was confirmed working OK in June 2024.
22
22
 
23
- If you know anything about session authentication and are able to help, please get in touch.
24
23
  </b><hr>
25
24
 
26
25
  [![npm](https://badgen.net/npm/dt/homebridge-eosstb)](https://www.npmjs.com/package/homebridge-eosstb)
@@ -110,7 +109,7 @@ This plugin is not provided by Telenet or Sunrise or Virgin Media or Ziggo or an
110
109
  ## Requirements
111
110
 
112
111
  - An Apple iPhone or iPad with iOS/iPadOS 14.0 (or later). Developed on iOS 14.1-26.4, earlier versions not tested.
113
- - [Homebridge](https://homebridge.io/) v1.1.116 (or later). Developed on Homebridge 1.1.116-2.0.1, earlier versions not tested.
112
+ - [Homebridge](https://homebridge.io/) v1.1.116 (or later). Developed on Homebridge 1.1.116-2.0.2, earlier versions not tested.
114
113
  - A TV subscription from one of the supported countries and TV providers.
115
114
  - An online account for viewing TV in the web app (often part of your TV package), see the table above.
116
115
  - An ARRIS DCX960 or HUMAX EOS1008R / 2008C / VIP5002W set-top box, provided by your TV provider as part of your TV subscription, called by the system an "EOSSTB", "EOS2STB" or "APLSTB" and marketed under different names in different countries.
package/index.js CHANGED
@@ -355,7 +355,8 @@ class StbPlatform {
355
355
  this.accessories = [];
356
356
  this.stbDevices = []; // store stbDevice in this.stbDevices
357
357
  this.masterChannelList = [];
358
- this.masterChannelListExpiryDate = 0; // epoch = always expired on first run
358
+ this.masterChannelListRefreshedAt = null; // null = never fetched since startup
359
+ this.masterChannelListExpiryDate = 0; // epoch = always expired on first run, forces immediate fetch
359
360
  this.checkChannelListTimeout = null; // nightly scheduler handler
360
361
  this.mqttReconnecting = false; // nightly reconnect indicator
361
362
  this.isDev = config.devMode === true;
@@ -856,9 +857,10 @@ class StbPlatform {
856
857
  // refreshing to avoid a race condition during session startup
857
858
  if (this.mqttReconnecting) {
858
859
  const THREE_MIN_MS = 3 * 60 * 1000;
859
- const retryDelayMs = THREE_MIN_MS + Math.floor(Math.random() * THREE_MIN_MS);
860
+ const retryDelayMs =
861
+ THREE_MIN_MS + Math.floor(Math.random() * THREE_MIN_MS);
860
862
  this.log.info(
861
- 'StbPlatform: channel list refresh deferred - MQTT reconnect in progress, retrying in a few minutes',
863
+ "StbPlatform: channel list refresh deferred - MQTT reconnect in progress, retrying in a few minutes",
862
864
  );
863
865
  this.checkChannelListTimeout = setTimeout(async () => {
864
866
  if (this.isShuttingDown) return;
@@ -903,7 +905,6 @@ class StbPlatform {
903
905
  }, msUntilReconnect);
904
906
  } // end of _scheduleNightlyMqttReconnect
905
907
 
906
-
907
908
  /**
908
909
  * Attempt the nightly MQTT reconnect.
909
910
  * If any STB is currently online (user may be watching), defers by 1 hour
@@ -926,7 +927,7 @@ class StbPlatform {
926
927
  // give up if max retries reached
927
928
  if (retryCount >= MAX_RETRIES) {
928
929
  this.log.info(
929
- 'StbPlatform: nightly MQTT reconnect skipped - STB still active after max retries, rescheduling for next night',
930
+ "StbPlatform: nightly MQTT reconnect skipped - STB still active after max retries, rescheduling for next night",
930
931
  );
931
932
  this._scheduleNightlyMqttReconnect();
932
933
  return;
@@ -935,7 +936,8 @@ class StbPlatform {
935
936
  // retry in 1 hour plus a random 0–30 min buffer
936
937
  const ONE_HOUR_MS = 60 * 60 * 1000;
937
938
  const THIRTY_MIN_MS = 30 * 60 * 1000;
938
- const retryDelayMs = ONE_HOUR_MS + Math.floor(Math.random() * THIRTY_MIN_MS);
939
+ const retryDelayMs =
940
+ ONE_HOUR_MS + Math.floor(Math.random() * THIRTY_MIN_MS);
939
941
  const retryAt = new Date(Date.now() + retryDelayMs);
940
942
 
941
943
  this.log.info(
@@ -953,12 +955,15 @@ class StbPlatform {
953
955
  // no STB is active - safe to reconnect
954
956
  try {
955
957
  this.mqttReconnecting = true; // signal to channel list refresh to pause
956
- this.log.info('StbPlatform: nightly MQTT reconnect starting...');
958
+ this.log.info("StbPlatform: nightly MQTT reconnect starting...");
957
959
  await this.endMqttSession();
958
960
  await this.startMqttClient();
959
- this.log.info('StbPlatform: nightly MQTT reconnect completed');
961
+ this.log.info("StbPlatform: nightly MQTT reconnect completed");
960
962
  } catch (err) {
961
- this.log.error('StbPlatform: nightly MQTT reconnect failed:', err.message);
963
+ this.log.error(
964
+ "StbPlatform: nightly MQTT reconnect failed:",
965
+ err.message,
966
+ );
962
967
  } finally {
963
968
  this.mqttReconnecting = false; // always clear the flag, even on failure
964
969
  }
@@ -983,7 +988,7 @@ class StbPlatform {
983
988
  * 10. startMqttClient — connect to the mqtt broker
984
989
  *
985
990
  * Each step is individually awaited, so failures are caught at the right
986
- * step and errorTitle is accurate when logged.
991
+ * step and stepName is accurate when logged.
987
992
  *
988
993
  * @param {string} watchdogInstance - log prefix for this watchdog invocation
989
994
  * @param {string} debugPrefix - debug() colour prefix
@@ -994,12 +999,13 @@ class StbPlatform {
994
999
  Object.keys(sessionState)[this.currentSessionState],
995
1000
  );
996
1001
 
997
- // errorTitle tracks which step we're on so the catch block can log a
1002
+ // stepName tracks which step we're on so the catch block can log a
998
1003
  // meaningful message rather than a generic "something failed".
999
- let errorTitle = "Failed to get config";
1004
+ let stepName = "";
1000
1005
 
1001
1006
  try {
1002
1007
  // ── Step 1: Get backend config (endpoint URLs) for the country ──────────
1008
+ stepName = "get config";
1003
1009
  this.log.debug("%s: ++++ step 1: calling getConfig", watchdogInstance);
1004
1010
  debug(debugPrefix + "calling getConfig");
1005
1011
 
@@ -1013,7 +1019,7 @@ class StbPlatform {
1013
1019
  );
1014
1020
 
1015
1021
  // ── Step 2: Authenticate and create a session ──────────────────────────
1016
- errorTitle = "Failed to create session";
1022
+ stepName = "create session";
1017
1023
  this.log.debug(
1018
1024
  "%s: ++++ step 2: calling createSession for country %s",
1019
1025
  watchdogInstance,
@@ -1024,6 +1030,12 @@ class StbPlatform {
1024
1030
 
1025
1031
  const sessionHouseholdId = await this.createSession();
1026
1032
  // Result stored in this.session by createSession()
1033
+ // debugging help to get session keys
1034
+ // session object keys: ["accessToken","householdId","refreshToken","refreshTokenExpiry","username","issuedAt"]
1035
+ this.log.debug(
1036
+ "session object keys: %s",
1037
+ JSON.stringify(Object.keys(this.session ?? {})),
1038
+ );
1027
1039
 
1028
1040
  this.log.debug(
1029
1041
  "%s: ++++++ step 2 done: session created, householdId %s",
@@ -1032,7 +1044,7 @@ class StbPlatform {
1032
1044
  );
1033
1045
 
1034
1046
  // ── Step 3: Fetch customer profile and assigned devices ────────────────
1035
- errorTitle = "Failed to discover platform";
1047
+ stepName = "discover platform";
1036
1048
  this.log.debug(
1037
1049
  "%s: ++++ step 3: calling getPersonalizationData for householdId %s",
1038
1050
  watchdogInstance,
@@ -1128,7 +1140,7 @@ class StbPlatform {
1128
1140
  }
1129
1141
 
1130
1142
  // ── Step 8: Discover and configure HomeKit accessories ─────────────────
1131
- errorTitle = "Failed to discover devices";
1143
+ stepName = "discover devices";
1132
1144
  this.log.debug(
1133
1145
  "%s: ++++ step 8: calling discoverDevices",
1134
1146
  watchdogInstance,
@@ -1146,7 +1158,7 @@ class StbPlatform {
1146
1158
  );
1147
1159
 
1148
1160
  // ── Step 9: Get the mqtt broker token ─────────────────────────────────
1149
- errorTitle = "Failed to start mqtt session";
1161
+ stepName = "start mqtt session";
1150
1162
  this.log.debug("%s: ++++ step 9: calling getMqttToken", watchdogInstance);
1151
1163
  debug(debugPrefix + "calling getMqttToken");
1152
1164
 
@@ -1183,15 +1195,66 @@ class StbPlatform {
1183
1195
  watchdogInstance,
1184
1196
  );
1185
1197
  } catch (errorReason) {
1186
- // One of the steps above threw or rejected. Log the failed step name
1187
- // (errorTitle) alongside the reason for easy diagnosis.
1188
- this.log.warn("%s: %s — %s", watchdogInstance, errorTitle, errorReason);
1198
+ // One of the steps above threw or rejected. Log the failed stepName
1199
+ // alongside the error message for easy diagnosis.
1200
+ const errMsg =
1201
+ errorReason instanceof Error
1202
+ ? errorReason.message
1203
+ : String(errorReason);
1204
+ this.log.warn("%s: Failed to %s: %s", watchdogInstance, stepName, errMsg);
1189
1205
  this.currentSessionState = sessionState.DISCONNECTED;
1190
1206
  this.currentStatusFault = Characteristic.StatusFault.GENERAL_FAULT;
1191
1207
  // sessionWatchdogRunning is reset in the finally block of the caller.
1192
1208
  }
1193
1209
  } // end of _runFullStartupSequence
1194
1210
 
1211
+ /**
1212
+ * _handleWebError
1213
+ *
1214
+ * Standardised catch-block handler for all outbound HTTP/Axios calls.
1215
+ * Builds a consistent human-readable error message, sets session state
1216
+ * to DISCONNECTED on ENOTFOUND, logs at debug level, then re-throws.
1217
+ *
1218
+ * @param {Error} error - the caught error
1219
+ * @param {string} action - what the caller was trying to do, e.g.
1220
+ * "get config data for countryCode ch"
1221
+ * @param {string|URL} url - the URL that was called (for debug context)
1222
+ */
1223
+ _handleWebError(error, action, url) {
1224
+ const urlStr = String(url ?? error.config?.url ?? "");
1225
+ let errReason = `Could not ${action}:`;
1226
+
1227
+ if (error.isAxiosError) {
1228
+ errReason += ` ${error.code}`;
1229
+
1230
+ if (error.response) {
1231
+ errReason += ` (HTTP ${error.response.status})`;
1232
+ }
1233
+
1234
+ if (error.code === "ENOTFOUND") {
1235
+ errReason += " - no internet connection";
1236
+ this.currentSessionState = sessionState.DISCONNECTED;
1237
+ }
1238
+ } else {
1239
+ errReason += ` — ${error.message ?? String(error)}`;
1240
+ }
1241
+
1242
+ if (urlStr) errReason += ` — ${urlStr}`;
1243
+
1244
+ // Summary always visible:
1245
+ this.log.error("_handleWebError: %s", errReason);
1246
+
1247
+ this.log.debug("_handleWebError: %s — full error:", errReason, error);
1248
+ if (error.response?.data) {
1249
+ this.log.debug(
1250
+ "_handleWebError: response body: %s",
1251
+ JSON.stringify(error.response.data).substring(0, 400),
1252
+ );
1253
+ }
1254
+
1255
+ throw new Error(errReason, { cause: error });
1256
+ }
1257
+
1195
1258
  /**
1196
1259
  * Discovers all physical devices from the backend and maps them to HomeKit accessories.
1197
1260
  * Creates new accessories for uncached devices, and restores existing ones from cache.
@@ -2720,8 +2783,8 @@ class StbPlatform {
2720
2783
  );
2721
2784
  }
2722
2785
  throw new Error(
2723
- `Step 4 of 5: login did not redirect to login_success.html ` +
2724
- `check your username and password. Final URL: ${finalUrl}`,
2786
+ // use a simple clean message to tell the user his credentials are likely wrong
2787
+ `Step 4 of 5: login failed — check your username and password`,
2725
2788
  );
2726
2789
  }
2727
2790
 
@@ -3247,6 +3310,18 @@ class StbPlatform {
3247
3310
  return;
3248
3311
  }
3249
3312
 
3313
+ // Log session state before the request - DIAGNOSES DEBUG ONLY
3314
+ const tokenAge = this.session?.issuedAt
3315
+ ? Math.round((Date.now() - this.session.issuedAt) / 1000 / 60)
3316
+ : null;
3317
+ const sessionId = this.session?.householdId ?? "(none)";
3318
+
3319
+ this.log.debug(
3320
+ "refreshMasterChannelList: starting | householdId=%s | tokenAgeMinutes=%s",
3321
+ sessionId,
3322
+ tokenAge ?? "unknown",
3323
+ );
3324
+
3250
3325
  // exit immediately if channel list has not expired
3251
3326
  if (Date.now() < this.masterChannelListExpiryDate) {
3252
3327
  if (this.debugLevel > 1) {
@@ -3265,16 +3340,6 @@ class StbPlatform {
3265
3340
  // syntax:
3266
3341
  // https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/channels?byLocationId=41043&includeInvisible=true&includeNotEntitled=true&personalised=true&sort=channelNumber
3267
3342
  // https://prod.spark.sunrisetv.ch/eng/web/linear-service/v2/channels?cityId=401&language=en&productClass=Orion-DASH
3268
- /*
3269
- let url = COUNTRY_BASE_URLS[this.config.country.toLowerCase()] + '/channels';
3270
- url = url + '?byLocationId=' + this.session.locationId // locationId needed to get user-specific list
3271
- url = url + '&includeInvisible=true' // includeInvisible
3272
- url = url + '&includeNotEntitled=true' // includeNotEntitled
3273
- url = url + '&personalised=true' // personalised
3274
- url = url + '&sort=channelNumber' // sort
3275
- */
3276
- //url = 'https://prod.spark.sunrisetv.ch/eng/web/linear-service/v2/channels?cityId=401&language=en&productClass=Orion-DASH'
3277
- //let url = COUNTRY_BASE_URLS[this.config.country.toLowerCase()] + '/eng/web/linear-service/v2/channels';
3278
3343
  const url = new URL(`${this.configsvc.linearService.URL}/v2/channels`);
3279
3344
  url.searchParams.set("cityId", this.customer.cityId);
3280
3345
  url.searchParams.set("language", "en");
@@ -3283,6 +3348,7 @@ class StbPlatform {
3283
3348
  if (this.debugLevel > 1) {
3284
3349
  this.log.warn("refreshMasterChannelList: GET %s", url);
3285
3350
  }
3351
+
3286
3352
  try {
3287
3353
  // call the webservice to get all available channels
3288
3354
  const config = {
@@ -3297,6 +3363,29 @@ class StbPlatform {
3297
3363
  "https://www.horizon.tv/",
3298
3364
  },
3299
3365
  };
3366
+ // extra debugging to help catch refresh issues
3367
+ this.log.debug(
3368
+ "refreshMasterChannelList: request headers | x-oesp-token=%s...%s | x-oesp-username=%s | Referer=%s",
3369
+ this.session.accessToken?.substring(0, 8) ?? "(null)", // first 8 chars only — don't log the full token
3370
+ this.session.accessToken?.slice(-4) ?? "", // last 4 chars
3371
+ this.session.username ?? "(null)",
3372
+ config.headers.Referer,
3373
+ );
3374
+ // extra debugging to help catch refresh issues
3375
+ this.log.debug(
3376
+ "refreshMasterChannelList: preflight | cityId=%s | tokenPresent=%s | tokenLength=%s | username=%s | prevExpiryWas=%s | lastRefreshedAt=%s",
3377
+ this.customer.cityId,
3378
+ !!this.session?.accessToken,
3379
+ this.session?.accessToken?.length ?? 0,
3380
+ this.session?.username ?? "(none)",
3381
+ this.masterChannelListExpiryDate
3382
+ ? new Date(this.masterChannelListExpiryDate).toLocaleString()
3383
+ : "(never set)",
3384
+ this.masterChannelListRefreshedAt
3385
+ ? new Date(this.masterChannelListRefreshedAt).toLocaleString()
3386
+ : "(never)",
3387
+ );
3388
+
3300
3389
  const response = await axiosWS(config);
3301
3390
  if (this.debugLevel > 1) {
3302
3391
  this.log.warn(
@@ -3309,7 +3398,6 @@ class StbPlatform {
3309
3398
  response.data.length,
3310
3399
  );
3311
3400
  }
3312
- //this.log(response.data);
3313
3401
 
3314
3402
  // the header contains the following:
3315
3403
  // Cache-Control: max-age=600, public, stale-if-error=43200
@@ -3350,6 +3438,9 @@ class StbPlatform {
3350
3438
  this.masterChannelList.map((ch) => [ch.id, ch]),
3351
3439
  );
3352
3440
 
3441
+ // record when we refreshed it
3442
+ this.masterChannelListRefreshedAt = Date.now()
3443
+
3353
3444
  this.log(
3354
3445
  "MasterChannelList contains %s channels, valid until %s",
3355
3446
  this.masterChannelList.length,
@@ -3365,15 +3456,11 @@ class StbPlatform {
3365
3456
  }
3366
3457
  return this.masterChannelList;
3367
3458
  } catch (error) {
3368
- const errReason = error.isAxiosError
3369
- ? `${error.code}: ${error.hostname ?? error.config?.url ?? ""}`
3370
- : (error.message ?? String(error));
3371
-
3372
- if (error.isAxiosError && error.code === "ENOTFOUND") {
3373
- this.currentSessionState = sessionState.DISCONNECTED;
3374
- }
3375
- this.log.warn("refreshMasterChannelList error:", errReason);
3376
- throw new Error(errReason, { cause: error }); // { cause } preserves the original for debugging
3459
+ this._handleWebError(
3460
+ error,
3461
+ `refresh master channel list`,
3462
+ url,
3463
+ );
3377
3464
  }
3378
3465
  } // end of refreshMasterChannelList
3379
3466
 
@@ -3438,16 +3525,11 @@ class StbPlatform {
3438
3525
  this.configsvc = response.data; // store the entire config data for future use in this.configsvc
3439
3526
  return this.configsvc;
3440
3527
  } catch (error) {
3441
- let errReason = `Could not get config data for ${countryCode} - check your internet connection`;
3442
- if (error.isAxiosError) {
3443
- errReason = error.code + ": " + (error.hostname || "");
3444
- // if no connection then set session to disconnected to force a session reconnect
3445
- if (error.code === "ENOTFOUND") {
3446
- this.currentSessionState = sessionState.DISCONNECTED;
3447
- }
3448
- }
3449
- this.log.debug(`getConfig error:`, error);
3450
- throw new Error(errReason);
3528
+ this._handleWebError(
3529
+ error,
3530
+ `get config data for countryCode ${countryCode}`,
3531
+ url,
3532
+ );
3451
3533
  }
3452
3534
  } // end of getConfig
3453
3535
 
@@ -3598,20 +3680,11 @@ class StbPlatform {
3598
3680
  //this.log.warn('getPersonalizationData: all done, returnng customerStatus: %s', this.customer.customerStatus);
3599
3681
  return this.customer;
3600
3682
  } catch (error) {
3601
- let errReason =
3602
- "Could not refresh personalization data for " +
3603
- householdId +
3604
- " - check your internet connection";
3605
- if (error.isAxiosError) {
3606
- errReason = error.code + ": " + (error.hostname || "");
3607
- // if no connection then set session to disconnected to force a session reconnect
3608
- if (error.code === "ENOTFOUND") {
3609
- this.currentSessionState = sessionState.DISCONNECTED;
3610
- }
3611
- }
3612
- //this.log('%s %s', errText, (errReason || ''));
3613
- this.log.debug(`getPersonalizationData error:`, error);
3614
- throw error;
3683
+ this._handleWebError(
3684
+ error,
3685
+ `get personalization data for household ${householdId}`,
3686
+ url,
3687
+ );
3615
3688
  }
3616
3689
  } // end of getPersonalizationData
3617
3690
 
@@ -3661,12 +3734,11 @@ class StbPlatform {
3661
3734
  );
3662
3735
  }
3663
3736
  } catch (error) {
3664
- this.log.warn(
3665
- "setPersonalizationDataForDevice failed: %s %s",
3666
- error.response?.status,
3667
- error.response?.statusText,
3737
+ this._handleWebError(
3738
+ error,
3739
+ `set personalization data for device ${deviceId}`,
3740
+ url,
3668
3741
  );
3669
- this.log.debug("setPersonalizationDataForDevice: error:", error);
3670
3742
  }
3671
3743
  } // end of setPersonalizationDataForDevice
3672
3744
 
@@ -3725,15 +3797,11 @@ class StbPlatform {
3725
3797
  }
3726
3798
  return this.entitlements;
3727
3799
  } catch (error) {
3728
- let errReason = `Could not refresh entitlements data for ${householdId} - check your internet connection`;
3729
- if (error.isAxiosError) {
3730
- errReason = `${error.code}: ${error.hostname || ""}`;
3731
- }
3732
- if (error.code === "ENOTFOUND") {
3733
- this.currentSessionState = sessionState.DISCONNECTED;
3734
- }
3735
- this.log.debug(`getEntitlements error:`, error);
3736
- throw new Error(errReason);
3800
+ this._handleWebError(
3801
+ error,
3802
+ `get entitlements data for household ${householdId}`,
3803
+ url,
3804
+ );
3737
3805
  }
3738
3806
  } // end of getEntitlements
3739
3807
 
@@ -3910,29 +3978,11 @@ class StbPlatform {
3910
3978
 
3911
3979
  return this.currentRecordingState;
3912
3980
  } catch (error) {
3913
- if (error.isAxiosError) {
3914
- const errReason =
3915
- "getRecordingState" +
3916
- ": " +
3917
- error.code +
3918
- " " +
3919
- (error.hostname || "") +
3920
- ": " +
3921
- (error.response?.status ?? "") +
3922
- " " +
3923
- (error.response?.statusText ?? "") +
3924
- ": " +
3925
- (error.config?.url ?? "");
3926
- if (error.code === "ENOTFOUND") {
3927
- this.currentSessionState = sessionState.DISCONNECTED;
3928
- }
3929
- this.log.debug("getRecordingState error:", error);
3930
- throw new Error(errReason);
3931
- } else {
3932
- this.log.warn("getRecordingState error:");
3933
- this.log.warn(error);
3934
- throw error;
3935
- }
3981
+ this._handleWebError(
3982
+ error,
3983
+ `get recording status for household ${householdId}`,
3984
+ url,
3985
+ );
3936
3986
  }
3937
3987
  } // end of getRecordingState
3938
3988
 
@@ -4122,29 +4172,11 @@ class StbPlatform {
4122
4172
 
4123
4173
  return this.currentRecordingState;
4124
4174
  } catch (error) {
4125
- if (error.isAxiosError) {
4126
- const errReason =
4127
- "getRecordingBookings" +
4128
- ": " +
4129
- error.code +
4130
- " " +
4131
- (error.hostname || "") +
4132
- ": " +
4133
- (error.response?.status ?? "") +
4134
- " " +
4135
- (error.response?.statusText ?? "") +
4136
- ": " +
4137
- (error.config?.url ?? "");
4138
- if (error.code === "ENOTFOUND") {
4139
- this.currentSessionState = sessionState.DISCONNECTED;
4140
- }
4141
- this.log.debug("getRecordingBookings error:", error);
4142
- throw new Error(errReason);
4143
- } else {
4144
- this.log.warn("getRecordingBookings error:");
4145
- this.log.warn(error);
4146
- throw error;
4147
- }
4175
+ this._handleWebError(
4176
+ error,
4177
+ `get recording bookings for household ${householdId}`,
4178
+ url,
4179
+ );
4148
4180
  }
4149
4181
  } // end of getRecordingBookings
4150
4182
 
@@ -4181,14 +4213,11 @@ class StbPlatform {
4181
4213
  this.log.warn(response.data);
4182
4214
  return true;
4183
4215
  } catch (error) {
4184
- let errReason = `Could not get experimental data for ${householdId} - check your internet connection`;
4185
- if (error.isAxiosError) {
4186
- errReason = `${error.code}: ${error.hostname || ""}`;
4187
- if (error.code === "ENOTFOUND") {
4188
- this.currentSessionState = sessionState.DISCONNECTED;
4189
- }
4190
- }
4191
- this.log.warn(`getExperimentalEndpoint error:`, error);
4216
+ this._handleWebError(
4217
+ error,
4218
+ `get experimental endpoint for household ${householdId}`,
4219
+ url,
4220
+ );
4192
4221
  }
4193
4222
  } // end of getExperimentalEndpoint
4194
4223
 
@@ -4429,7 +4458,11 @@ class StbPlatform {
4429
4458
 
4430
4459
  // announce ourselves as an active HGO client before requesting UI status
4431
4460
  // the STB uses this retained presence message to decide which clients to respond to
4432
- this.setHgoState(householdId, this.mqttClient.options.clientId, 'ONLINE_RUNNING');
4461
+ this.setHgoState(
4462
+ householdId,
4463
+ this.mqttClient.options.clientId,
4464
+ "ONLINE_RUNNING",
4465
+ );
4433
4466
 
4434
4467
  // request initial UI status for each device, with a short delay to allow
4435
4468
  // the STB to process the HGO presence announcement first
@@ -4441,7 +4474,10 @@ class StbPlatform {
4441
4474
  setTimeout(() => {
4442
4475
  this.devices.forEach((device) => {
4443
4476
  // request the initial UI status for each device
4444
- this.getUiStatus(device.deviceId, this.mqttClient.options.clientId);
4477
+ this.getUiStatus(
4478
+ device.deviceId,
4479
+ this.mqttClient.options.clientId,
4480
+ );
4445
4481
 
4446
4482
  // retry after 10 seconds if no CPE.uiStatus response has arrived yet
4447
4483
  setTimeout(() => {
@@ -4452,7 +4488,10 @@ class StbPlatform {
4452
4488
  device.deviceId,
4453
4489
  );
4454
4490
  }
4455
- this.getUiStatus(device.deviceId, this.mqttClient.options.clientId);
4491
+ this.getUiStatus(
4492
+ device.deviceId,
4493
+ this.mqttClient.options.clientId,
4494
+ );
4456
4495
  }
4457
4496
  }, 10 * 1000); // 10 second retry delay
4458
4497
  });
@@ -4556,7 +4595,8 @@ class StbPlatform {
4556
4595
  const deviceIndex = this.devices.findIndex(
4557
4596
  (device) => device.deviceId === deviceId,
4558
4597
  );
4559
- const stbDevice = deviceIndex > -1 ? this.stbDevices[deviceIndex] : null;
4598
+ const stbDevice =
4599
+ deviceIndex > -1 ? this.stbDevices[deviceIndex] : null;
4560
4600
 
4561
4601
  // Box setting: StandbyPowerConsumption = FastStart / ActiveStart / EcoSlowstart
4562
4602
  // "Fast start": when turned off, goes to ONLINE_STANDBY and stays there. Box can be turned on via mqtt
@@ -4569,7 +4609,10 @@ class StbPlatform {
4569
4609
  // Detect power-off → power-on transition per device.
4570
4610
  // Set PLAY immediately; CPE.uiStatus will overwrite with the
4571
4611
  // accurate speed-derived state shortly after.
4572
- if (stbDevice?.previousPowerState === Characteristic.Active.INACTIVE) {
4612
+ if (
4613
+ stbDevice?.previousPowerState ===
4614
+ Characteristic.Active.INACTIVE
4615
+ ) {
4573
4616
  currMediaState = Characteristic.CurrentMediaState.PLAY;
4574
4617
  if (this.debugLevel > 0) {
4575
4618
  this.log.warn(
@@ -4577,7 +4620,7 @@ class StbPlatform {
4577
4620
  deviceId,
4578
4621
  );
4579
4622
  }
4580
- }
4623
+ }
4581
4624
  break;
4582
4625
  case "ONLINE_STANDBY": // ONLINE_STANDBY: power is off, device is on standby, still reachable over the network, can be turned on via mqtt.
4583
4626
  currStatusActive = Characteristic.Active.ACTIVE; // bool, 0 = not active, 1 = active
@@ -4610,9 +4653,9 @@ class StbPlatform {
4610
4653
 
4611
4654
  // After the switch, if box is running, request current UI state
4612
4655
  //if (stbState === 'ONLINE_RUNNING') {
4613
- // Small delay gives the STB a moment to settle before responding
4614
- //setTimeout(() => this.mqttRequestUiStatus(deviceId), 500);
4615
- //}
4656
+ // Small delay gives the STB a moment to settle before responding
4657
+ //setTimeout(() => this.mqttRequestUiStatus(deviceId), 500);
4658
+ //}
4616
4659
  }
4617
4660
 
4618
4661
  // handle CPE UI status messages for the STB
@@ -5002,7 +5045,7 @@ class StbPlatform {
5002
5045
  this.setHgoState(
5003
5046
  this.session.householdId,
5004
5047
  this.mqttClient.options.clientId,
5005
- 'OFFLINE',
5048
+ "OFFLINE",
5006
5049
  );
5007
5050
 
5008
5051
  // unsubscribe from all subscribedTopics before tearing down the session
@@ -5010,7 +5053,7 @@ class StbPlatform {
5010
5053
  this.log.info(
5011
5054
  "mqttClient: No topics to unsubscribe from, skipping unsubscribe.",
5012
5055
  );
5013
-
5056
+
5014
5057
  this.mqttClient.end(false, {}, (endErr) => {
5015
5058
  if (endErr) {
5016
5059
  this.log.error("MQTT end error:", endErr);
@@ -5216,17 +5259,16 @@ class StbPlatform {
5216
5259
  const message = JSON.stringify({
5217
5260
  source: mqttClientId,
5218
5261
  state: state,
5219
- deviceType: 'HGO',
5220
- mac: '',
5221
- ipAddress: '',
5262
+ deviceType: "HGO",
5263
+ mac: "",
5264
+ ipAddress: "",
5222
5265
  });
5223
5266
  if (this.debugLevel > 0) {
5224
- this.log.warn('setHgoState: publishing %s to topic: %s', state, topic);
5267
+ this.log.warn("setHgoState: publishing %s to topic: %s", state, topic);
5225
5268
  }
5226
5269
  this.mqttPublishMessage(topic, message, { qos: 2, retain: true });
5227
5270
  }
5228
5271
 
5229
-
5230
5272
  // send a channel change request to the settopbox via mqtt
5231
5273
  // using the CPE.pushToTV message
5232
5274
  // the friendlyDeviceName appears on the TV in a popup window
@@ -5285,7 +5327,10 @@ class StbPlatform {
5285
5327
  // @param {string} deviceId - The STB device ID (e.g. "000378-EOS2STB-00852052xxxx")
5286
5328
  mqttRequestUiStatus(deviceId) {
5287
5329
  if (!this.mqttClient?.connected) {
5288
- this.log.warn('%s: mqttRequestUiStatus: MQTT not connected, skipping', deviceId);
5330
+ this.log.warn(
5331
+ "%s: mqttRequestUiStatus: MQTT not connected, skipping",
5332
+ deviceId,
5333
+ );
5289
5334
  return;
5290
5335
  }
5291
5336
  if (this.debugLevel > 0) {
@@ -5296,9 +5341,9 @@ class StbPlatform {
5296
5341
  }
5297
5342
 
5298
5343
  const payload = JSON.stringify({
5299
- version: '1.3.18',
5300
- type: 'CPE.pullFromTV',
5301
- source: this.mqttClient.options.clientId, // your mqttClientId
5344
+ version: "1.3.18",
5345
+ type: "CPE.pullFromTV",
5346
+ source: this.mqttClient.options.clientId, // your mqttClientId
5302
5347
  messageTimeStamp: Date.now(),
5303
5348
  });
5304
5349
 
@@ -5308,8 +5353,7 @@ class StbPlatform {
5308
5353
  qos: 1,
5309
5354
  retain: false,
5310
5355
  });
5311
-
5312
- }
5356
+ }
5313
5357
 
5314
5358
  // set the media state of the settopbox via mqtt
5315
5359
  // media state is controlled by speedRate
@@ -6566,10 +6610,7 @@ class StbDevice {
6566
6610
  inputSourceService
6567
6611
  .getCharacteristic(Characteristic.ConfiguredName)
6568
6612
  .setProps({
6569
- perms: [
6570
- this.api.hap.Perms.PAIRED_READ,
6571
- this.api.hap.Perms.NOTIFY,
6572
- ],
6613
+ perms: [this.api.hap.Perms.PAIRED_READ, this.api.hap.Perms.NOTIFY],
6573
6614
  })
6574
6615
  .onGet(() => this.getInputName(i));
6575
6616
  //.onSet((value) => this.setInputName(i, value));
@@ -7960,7 +8001,10 @@ class StbDevice {
7960
8001
  // triple rapid VolDown presses triggers setMute
7961
8002
  if (volumeSelectorValue === Characteristic.VolumeSelector.DECREMENT) {
7962
8003
  // Guard: ensure array is properly initialised
7963
- if (!Array.isArray(this.lastVolDownKeyPress) || this.lastVolDownKeyPress.length < 3) {
8004
+ if (
8005
+ !Array.isArray(this.lastVolDownKeyPress) ||
8006
+ this.lastVolDownKeyPress.length < 3
8007
+ ) {
7964
8008
  this.lastVolDownKeyPress = [0, 0, 0];
7965
8009
  }
7966
8010
 
@@ -7969,14 +8013,14 @@ class StbDevice {
7969
8013
  this.lastVolDownKeyPress = this.lastVolDownKeyPress.slice(0, 3); // keep only last 3
7970
8014
 
7971
8015
  // Now assign the calculated value to the outer variable
7972
- tripleVolDownPress = this.lastVolDownKeyPress[0] - this.lastVolDownKeyPress[2];
8016
+ tripleVolDownPress =
8017
+ this.lastVolDownKeyPress[0] - this.lastVolDownKeyPress[2];
7973
8018
 
7974
8019
  this.log.debug(
7975
8020
  "%s: setVolume: Timediff between volDownKeyPress[0] and volDownKeyPress[2]: %s ms",
7976
8021
  this.name,
7977
8022
  tripleVolDownPress,
7978
- );
7979
-
8023
+ );
7980
8024
  }
7981
8025
 
7982
8026
  // check for triple press of volDown, send setMute if tripleVolDownPress less than triplePressTime of 800ms
@@ -8014,7 +8058,11 @@ class StbDevice {
8014
8058
 
8015
8059
  try {
8016
8060
  if (this.debugLevel > 0) {
8017
- this.log.warn("%s: setVolume: Sending command %s", this.name, command);
8061
+ this.log.warn(
8062
+ "%s: setVolume: Sending command %s",
8063
+ this.name,
8064
+ command,
8065
+ );
8018
8066
  }
8019
8067
  await new Promise((resolve, reject) => {
8020
8068
  exec(command, (error, _stdout, stderr) => {
@@ -8380,13 +8428,13 @@ class StbDevice {
8380
8428
  // logChangeOnly = TRUE: only the changes are logged, no media state change occurs. Needed when sending remote keypresses to prevent double commands
8381
8429
  // CHAR_NAMES: TargetMediaState: [ 'UUID', 'PLAY', 'PAUSE', 'STOP' ]
8382
8430
  //if (this.debugLevel > 1) {
8383
- this.log.info(
8384
- "%s: setTargetMediaState to %s [%s]",
8385
- this.name,
8386
- targetMediaState,
8387
- CHAR_NAMES.TargetMediaState[targetMediaState + 1],
8388
- );
8389
- // }
8431
+ this.log.info(
8432
+ "%s: setTargetMediaState to %s [%s]",
8433
+ this.name,
8434
+ targetMediaState,
8435
+ CHAR_NAMES.TargetMediaState[targetMediaState + 1],
8436
+ );
8437
+ // }
8390
8438
 
8391
8439
  if (!logChangeOnly) {
8392
8440
  // send the setMediaState command if we are not just logging the change
@@ -8399,16 +8447,17 @@ class StbDevice {
8399
8447
  // PLAY 0 - 1 Play
8400
8448
  // PAUSE 1 - 0 Paused
8401
8449
  // STOP 2 - 0 Paused
8402
- const newBoxMediaState = targetMediaState === Characteristic.TargetMediaState.PLAY ? 1 : 0;
8450
+ const newBoxMediaState =
8451
+ targetMediaState === Characteristic.TargetMediaState.PLAY ? 1 : 0;
8403
8452
  const newBoxMediaStateName = newBoxMediaState === 1 ? "Play" : "Paused";
8404
8453
 
8405
8454
  //if (this.debugLevel >= 0) {
8406
- this.log(
8407
- "%s: setTargetMediaState: Calling setMediaState with newBoxMediaState %s [%s]",
8408
- this.name,
8409
- newBoxMediaState,
8410
- newBoxMediaStateName,
8411
- );
8455
+ this.log(
8456
+ "%s: setTargetMediaState: Calling setMediaState with newBoxMediaState %s [%s]",
8457
+ this.name,
8458
+ newBoxMediaState,
8459
+ newBoxMediaStateName,
8460
+ );
8412
8461
  //}
8413
8462
  /*
8414
8463
  switch (targetMediaState) {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "displayName": "Homebridge EOSSTB",
4
4
  "description": "Add your set-top box to Homekit (for Telenet BE, Sunrise CH, UPC SK, Virgin Media GB & IE, Ziggo NL)",
5
5
  "author": "Jochen Siegenthaler (https://github.com/jsiegenthaler/)",
6
- "version": "2.4.0-beta.2",
6
+ "version": "2.4.0-beta.3",
7
7
  "platformname": "eosstb",
8
8
  "dependencies": {
9
9
  "axios": "^1.16.0",