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

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 +25 -0
  2. package/README.md +1 -2
  3. package/index.js +250 -206
  4. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,17 @@ 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.4 (2026-05-10)
9
+
10
+ - Improved performance of refreshMasterChannelList
11
+
12
+ ## 2.4.0-beta.3 (2026-05-10)
13
+
14
+ This release focusses improved error handling and has extra debugging added to catch a refresh error
15
+
16
+ - Improved handling of errors for web requests
17
+ - Added extra debugging to assist in catching a refresh error in refreshMasterChannelList
18
+
8
19
  ## 2.4.0-beta.2 (2026-05-09)
9
20
 
10
21
  This release focusses on ensuring mqtt long-term stability, and fixes an issue where the channel name was not shown on startup.
@@ -44,6 +55,20 @@ This release represents a major rewrite of the plugin, significantly improving r
44
55
  - Bumped dependency "tough-cookie": "^6.0.1",
45
56
  - Bumped dependency "ws": "^8.20.0"
46
57
 
58
+ ## 2.3.9 (2026-05-09)
59
+ - Fixed Error on Homebridge v2: Cannot read properties of undefined (reading 'STRING')
60
+ - Adapted hidden channel name to reduce warning messages with Homebridge v2
61
+ - Updated iOS and Homebridge version references in Readme
62
+ - Bumped engine "^1.11.4||^2.0.0",
63
+ - Bumped engine "node": "^24.15.0"
64
+ - Bumped dependency "axios": "^1.16.0",
65
+ - Bumped dependency "axios-cookiejar-support": "^7.0.0",
66
+ - Bumped dependency "mqtt": "^5.15.1",
67
+ - Bumped dependency "qs": "^6.15.1",
68
+ - Bumped dependency "semver": "^7.8.0",
69
+ - Bumped dependency "tough-cookie": "^6.0.1",
70
+ - Bumped dependency "ws": "^8.20.0"
71
+
47
72
  ## 2.3.8 (2026-02-27)
48
73
 
49
74
  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,46 +3398,53 @@ 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
3316
- // this could be used to set expiry date...
3317
3404
  const cacheControl = response.headers["cache-control"];
3318
- const maxAgeMatch = cacheControl?.match(/max-age=(\d+)/);
3319
- const serverMaxAge = maxAgeMatch ? parseInt(maxAgeMatch[1], 10) : null;
3405
+ const maxAge = cacheControl
3406
+ ?.split(",")
3407
+ .find((part) => part.trim().startsWith("max-age="))
3408
+ ?.split("=")[1];
3409
+ const serverMaxAge = maxAge ? parseInt(maxAge, 10) : null; //get the max age from the server
3320
3410
  const validForSecs =
3321
3411
  serverMaxAge ||
3322
3412
  this.config.masterChannelListValidFor ||
3323
3413
  MASTER_CHANNEL_LIST_VALID_FOR_S;
3324
3414
 
3325
- // Expiry priority: server Cache-Control max-age → config override → hardcoded constant
3326
- this.masterChannelListExpiryDate = Date.now() + validForSecs * 1000; // always a number
3415
+ this.masterChannelListExpiryDate = Date.now() + validForSecs * 1000;
3327
3416
 
3328
- // load the channel list with all channels found
3329
- this.masterChannelList = [];
3330
3417
  const channels = response.data;
3331
3418
  this.log.debug("Channels to process:", channels.length);
3332
- for (const channel of channels) {
3419
+ response.data = null; // release raw payload for GC
3420
+
3421
+ // Performance optimisation: Single pass — pre-allocated array + Map built together, shared object refs
3422
+ const newList = new Array(channels.length);
3423
+ const newMap = new Map();
3424
+
3425
+ for (let i = 0; i < channels.length; i++) {
3426
+ const ch = channels[i];
3333
3427
  if (this.debugLevel > 2) {
3334
3428
  this.log(
3335
3429
  "Processing channel:",
3336
- channel.logicalChannelNumber,
3337
- channel.id,
3338
- channel.name,
3430
+ ch.logicalChannelNumber,
3431
+ ch.id,
3432
+ ch.name,
3339
3433
  );
3340
3434
  }
3341
- this.masterChannelList.push({
3342
- id: channel.id,
3343
- name: cleanNameForHomeKit(channel.name),
3344
- logicalChannelNumber: channel.logicalChannelNumber,
3345
- linearProducts: channel.linearProducts,
3346
- });
3435
+ const entry = {
3436
+ id: ch.id,
3437
+ name: cleanNameForHomeKit(ch.name),
3438
+ logicalChannelNumber: ch.logicalChannelNumber,
3439
+ linearProducts: ch.linearProducts,
3440
+ };
3441
+ newList[i] = entry;
3442
+ newMap.set(ch.id, entry);
3347
3443
  }
3348
- // add a map for faster access to the master channel list
3349
- this.masterChannelMap = new Map(
3350
- this.masterChannelList.map((ch) => [ch.id, ch]),
3351
- );
3444
+
3445
+ this.masterChannelList = newList;
3446
+ this.masterChannelMap = newMap;
3447
+ this.masterChannelListRefreshedAt = Date.now();
3352
3448
 
3353
3449
  this.log(
3354
3450
  "MasterChannelList contains %s channels, valid until %s",
@@ -3356,24 +3452,10 @@ class StbPlatform {
3356
3452
  new Date(this.masterChannelListExpiryDate).toLocaleString(), // format for display only
3357
3453
  );
3358
3454
 
3359
- if (this.debugLevel > 1) {
3360
- this.log.warn(
3361
- "refreshMasterChannelList: Master channel list refreshed with %s channels, valid until %s",
3362
- this.masterChannelList.length,
3363
- new Date(this.masterChannelListExpiryDate).toLocaleString(),
3364
- );
3365
- }
3366
3455
  return this.masterChannelList;
3456
+ //++++++++++++++++++++++++++++++++++++++++++++++++
3367
3457
  } 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
3458
+ this._handleWebError(error, `refresh master channel list`, url);
3377
3459
  }
3378
3460
  } // end of refreshMasterChannelList
3379
3461
 
@@ -3438,16 +3520,11 @@ class StbPlatform {
3438
3520
  this.configsvc = response.data; // store the entire config data for future use in this.configsvc
3439
3521
  return this.configsvc;
3440
3522
  } 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);
3523
+ this._handleWebError(
3524
+ error,
3525
+ `get config data for countryCode ${countryCode}`,
3526
+ url,
3527
+ );
3451
3528
  }
3452
3529
  } // end of getConfig
3453
3530
 
@@ -3598,20 +3675,11 @@ class StbPlatform {
3598
3675
  //this.log.warn('getPersonalizationData: all done, returnng customerStatus: %s', this.customer.customerStatus);
3599
3676
  return this.customer;
3600
3677
  } 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;
3678
+ this._handleWebError(
3679
+ error,
3680
+ `get personalization data for household ${householdId}`,
3681
+ url,
3682
+ );
3615
3683
  }
3616
3684
  } // end of getPersonalizationData
3617
3685
 
@@ -3661,12 +3729,11 @@ class StbPlatform {
3661
3729
  );
3662
3730
  }
3663
3731
  } catch (error) {
3664
- this.log.warn(
3665
- "setPersonalizationDataForDevice failed: %s %s",
3666
- error.response?.status,
3667
- error.response?.statusText,
3732
+ this._handleWebError(
3733
+ error,
3734
+ `set personalization data for device ${deviceId}`,
3735
+ url,
3668
3736
  );
3669
- this.log.debug("setPersonalizationDataForDevice: error:", error);
3670
3737
  }
3671
3738
  } // end of setPersonalizationDataForDevice
3672
3739
 
@@ -3725,15 +3792,11 @@ class StbPlatform {
3725
3792
  }
3726
3793
  return this.entitlements;
3727
3794
  } 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);
3795
+ this._handleWebError(
3796
+ error,
3797
+ `get entitlements data for household ${householdId}`,
3798
+ url,
3799
+ );
3737
3800
  }
3738
3801
  } // end of getEntitlements
3739
3802
 
@@ -3910,29 +3973,11 @@ class StbPlatform {
3910
3973
 
3911
3974
  return this.currentRecordingState;
3912
3975
  } 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
- }
3976
+ this._handleWebError(
3977
+ error,
3978
+ `get recording status for household ${householdId}`,
3979
+ url,
3980
+ );
3936
3981
  }
3937
3982
  } // end of getRecordingState
3938
3983
 
@@ -4122,29 +4167,11 @@ class StbPlatform {
4122
4167
 
4123
4168
  return this.currentRecordingState;
4124
4169
  } 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
- }
4170
+ this._handleWebError(
4171
+ error,
4172
+ `get recording bookings for household ${householdId}`,
4173
+ url,
4174
+ );
4148
4175
  }
4149
4176
  } // end of getRecordingBookings
4150
4177
 
@@ -4181,14 +4208,11 @@ class StbPlatform {
4181
4208
  this.log.warn(response.data);
4182
4209
  return true;
4183
4210
  } 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);
4211
+ this._handleWebError(
4212
+ error,
4213
+ `get experimental endpoint for household ${householdId}`,
4214
+ url,
4215
+ );
4192
4216
  }
4193
4217
  } // end of getExperimentalEndpoint
4194
4218
 
@@ -4429,7 +4453,11 @@ class StbPlatform {
4429
4453
 
4430
4454
  // announce ourselves as an active HGO client before requesting UI status
4431
4455
  // the STB uses this retained presence message to decide which clients to respond to
4432
- this.setHgoState(householdId, this.mqttClient.options.clientId, 'ONLINE_RUNNING');
4456
+ this.setHgoState(
4457
+ householdId,
4458
+ this.mqttClient.options.clientId,
4459
+ "ONLINE_RUNNING",
4460
+ );
4433
4461
 
4434
4462
  // request initial UI status for each device, with a short delay to allow
4435
4463
  // the STB to process the HGO presence announcement first
@@ -4441,7 +4469,10 @@ class StbPlatform {
4441
4469
  setTimeout(() => {
4442
4470
  this.devices.forEach((device) => {
4443
4471
  // request the initial UI status for each device
4444
- this.getUiStatus(device.deviceId, this.mqttClient.options.clientId);
4472
+ this.getUiStatus(
4473
+ device.deviceId,
4474
+ this.mqttClient.options.clientId,
4475
+ );
4445
4476
 
4446
4477
  // retry after 10 seconds if no CPE.uiStatus response has arrived yet
4447
4478
  setTimeout(() => {
@@ -4452,7 +4483,10 @@ class StbPlatform {
4452
4483
  device.deviceId,
4453
4484
  );
4454
4485
  }
4455
- this.getUiStatus(device.deviceId, this.mqttClient.options.clientId);
4486
+ this.getUiStatus(
4487
+ device.deviceId,
4488
+ this.mqttClient.options.clientId,
4489
+ );
4456
4490
  }
4457
4491
  }, 10 * 1000); // 10 second retry delay
4458
4492
  });
@@ -4556,7 +4590,8 @@ class StbPlatform {
4556
4590
  const deviceIndex = this.devices.findIndex(
4557
4591
  (device) => device.deviceId === deviceId,
4558
4592
  );
4559
- const stbDevice = deviceIndex > -1 ? this.stbDevices[deviceIndex] : null;
4593
+ const stbDevice =
4594
+ deviceIndex > -1 ? this.stbDevices[deviceIndex] : null;
4560
4595
 
4561
4596
  // Box setting: StandbyPowerConsumption = FastStart / ActiveStart / EcoSlowstart
4562
4597
  // "Fast start": when turned off, goes to ONLINE_STANDBY and stays there. Box can be turned on via mqtt
@@ -4569,7 +4604,10 @@ class StbPlatform {
4569
4604
  // Detect power-off → power-on transition per device.
4570
4605
  // Set PLAY immediately; CPE.uiStatus will overwrite with the
4571
4606
  // accurate speed-derived state shortly after.
4572
- if (stbDevice?.previousPowerState === Characteristic.Active.INACTIVE) {
4607
+ if (
4608
+ stbDevice?.previousPowerState ===
4609
+ Characteristic.Active.INACTIVE
4610
+ ) {
4573
4611
  currMediaState = Characteristic.CurrentMediaState.PLAY;
4574
4612
  if (this.debugLevel > 0) {
4575
4613
  this.log.warn(
@@ -4577,7 +4615,7 @@ class StbPlatform {
4577
4615
  deviceId,
4578
4616
  );
4579
4617
  }
4580
- }
4618
+ }
4581
4619
  break;
4582
4620
  case "ONLINE_STANDBY": // ONLINE_STANDBY: power is off, device is on standby, still reachable over the network, can be turned on via mqtt.
4583
4621
  currStatusActive = Characteristic.Active.ACTIVE; // bool, 0 = not active, 1 = active
@@ -4610,9 +4648,9 @@ class StbPlatform {
4610
4648
 
4611
4649
  // After the switch, if box is running, request current UI state
4612
4650
  //if (stbState === 'ONLINE_RUNNING') {
4613
- // Small delay gives the STB a moment to settle before responding
4614
- //setTimeout(() => this.mqttRequestUiStatus(deviceId), 500);
4615
- //}
4651
+ // Small delay gives the STB a moment to settle before responding
4652
+ //setTimeout(() => this.mqttRequestUiStatus(deviceId), 500);
4653
+ //}
4616
4654
  }
4617
4655
 
4618
4656
  // handle CPE UI status messages for the STB
@@ -5002,7 +5040,7 @@ class StbPlatform {
5002
5040
  this.setHgoState(
5003
5041
  this.session.householdId,
5004
5042
  this.mqttClient.options.clientId,
5005
- 'OFFLINE',
5043
+ "OFFLINE",
5006
5044
  );
5007
5045
 
5008
5046
  // unsubscribe from all subscribedTopics before tearing down the session
@@ -5010,7 +5048,7 @@ class StbPlatform {
5010
5048
  this.log.info(
5011
5049
  "mqttClient: No topics to unsubscribe from, skipping unsubscribe.",
5012
5050
  );
5013
-
5051
+
5014
5052
  this.mqttClient.end(false, {}, (endErr) => {
5015
5053
  if (endErr) {
5016
5054
  this.log.error("MQTT end error:", endErr);
@@ -5216,17 +5254,16 @@ class StbPlatform {
5216
5254
  const message = JSON.stringify({
5217
5255
  source: mqttClientId,
5218
5256
  state: state,
5219
- deviceType: 'HGO',
5220
- mac: '',
5221
- ipAddress: '',
5257
+ deviceType: "HGO",
5258
+ mac: "",
5259
+ ipAddress: "",
5222
5260
  });
5223
5261
  if (this.debugLevel > 0) {
5224
- this.log.warn('setHgoState: publishing %s to topic: %s', state, topic);
5262
+ this.log.warn("setHgoState: publishing %s to topic: %s", state, topic);
5225
5263
  }
5226
5264
  this.mqttPublishMessage(topic, message, { qos: 2, retain: true });
5227
5265
  }
5228
5266
 
5229
-
5230
5267
  // send a channel change request to the settopbox via mqtt
5231
5268
  // using the CPE.pushToTV message
5232
5269
  // the friendlyDeviceName appears on the TV in a popup window
@@ -5285,7 +5322,10 @@ class StbPlatform {
5285
5322
  // @param {string} deviceId - The STB device ID (e.g. "000378-EOS2STB-00852052xxxx")
5286
5323
  mqttRequestUiStatus(deviceId) {
5287
5324
  if (!this.mqttClient?.connected) {
5288
- this.log.warn('%s: mqttRequestUiStatus: MQTT not connected, skipping', deviceId);
5325
+ this.log.warn(
5326
+ "%s: mqttRequestUiStatus: MQTT not connected, skipping",
5327
+ deviceId,
5328
+ );
5289
5329
  return;
5290
5330
  }
5291
5331
  if (this.debugLevel > 0) {
@@ -5296,9 +5336,9 @@ class StbPlatform {
5296
5336
  }
5297
5337
 
5298
5338
  const payload = JSON.stringify({
5299
- version: '1.3.18',
5300
- type: 'CPE.pullFromTV',
5301
- source: this.mqttClient.options.clientId, // your mqttClientId
5339
+ version: "1.3.18",
5340
+ type: "CPE.pullFromTV",
5341
+ source: this.mqttClient.options.clientId, // your mqttClientId
5302
5342
  messageTimeStamp: Date.now(),
5303
5343
  });
5304
5344
 
@@ -5308,8 +5348,7 @@ class StbPlatform {
5308
5348
  qos: 1,
5309
5349
  retain: false,
5310
5350
  });
5311
-
5312
- }
5351
+ }
5313
5352
 
5314
5353
  // set the media state of the settopbox via mqtt
5315
5354
  // media state is controlled by speedRate
@@ -6566,10 +6605,7 @@ class StbDevice {
6566
6605
  inputSourceService
6567
6606
  .getCharacteristic(Characteristic.ConfiguredName)
6568
6607
  .setProps({
6569
- perms: [
6570
- this.api.hap.Perms.PAIRED_READ,
6571
- this.api.hap.Perms.NOTIFY,
6572
- ],
6608
+ perms: [this.api.hap.Perms.PAIRED_READ, this.api.hap.Perms.NOTIFY],
6573
6609
  })
6574
6610
  .onGet(() => this.getInputName(i));
6575
6611
  //.onSet((value) => this.setInputName(i, value));
@@ -7960,7 +7996,10 @@ class StbDevice {
7960
7996
  // triple rapid VolDown presses triggers setMute
7961
7997
  if (volumeSelectorValue === Characteristic.VolumeSelector.DECREMENT) {
7962
7998
  // Guard: ensure array is properly initialised
7963
- if (!Array.isArray(this.lastVolDownKeyPress) || this.lastVolDownKeyPress.length < 3) {
7999
+ if (
8000
+ !Array.isArray(this.lastVolDownKeyPress) ||
8001
+ this.lastVolDownKeyPress.length < 3
8002
+ ) {
7964
8003
  this.lastVolDownKeyPress = [0, 0, 0];
7965
8004
  }
7966
8005
 
@@ -7969,14 +8008,14 @@ class StbDevice {
7969
8008
  this.lastVolDownKeyPress = this.lastVolDownKeyPress.slice(0, 3); // keep only last 3
7970
8009
 
7971
8010
  // Now assign the calculated value to the outer variable
7972
- tripleVolDownPress = this.lastVolDownKeyPress[0] - this.lastVolDownKeyPress[2];
8011
+ tripleVolDownPress =
8012
+ this.lastVolDownKeyPress[0] - this.lastVolDownKeyPress[2];
7973
8013
 
7974
8014
  this.log.debug(
7975
8015
  "%s: setVolume: Timediff between volDownKeyPress[0] and volDownKeyPress[2]: %s ms",
7976
8016
  this.name,
7977
8017
  tripleVolDownPress,
7978
- );
7979
-
8018
+ );
7980
8019
  }
7981
8020
 
7982
8021
  // check for triple press of volDown, send setMute if tripleVolDownPress less than triplePressTime of 800ms
@@ -8014,7 +8053,11 @@ class StbDevice {
8014
8053
 
8015
8054
  try {
8016
8055
  if (this.debugLevel > 0) {
8017
- this.log.warn("%s: setVolume: Sending command %s", this.name, command);
8056
+ this.log.warn(
8057
+ "%s: setVolume: Sending command %s",
8058
+ this.name,
8059
+ command,
8060
+ );
8018
8061
  }
8019
8062
  await new Promise((resolve, reject) => {
8020
8063
  exec(command, (error, _stdout, stderr) => {
@@ -8380,13 +8423,13 @@ class StbDevice {
8380
8423
  // logChangeOnly = TRUE: only the changes are logged, no media state change occurs. Needed when sending remote keypresses to prevent double commands
8381
8424
  // CHAR_NAMES: TargetMediaState: [ 'UUID', 'PLAY', 'PAUSE', 'STOP' ]
8382
8425
  //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
- // }
8426
+ this.log.info(
8427
+ "%s: setTargetMediaState to %s [%s]",
8428
+ this.name,
8429
+ targetMediaState,
8430
+ CHAR_NAMES.TargetMediaState[targetMediaState + 1],
8431
+ );
8432
+ // }
8390
8433
 
8391
8434
  if (!logChangeOnly) {
8392
8435
  // send the setMediaState command if we are not just logging the change
@@ -8399,16 +8442,17 @@ class StbDevice {
8399
8442
  // PLAY 0 - 1 Play
8400
8443
  // PAUSE 1 - 0 Paused
8401
8444
  // STOP 2 - 0 Paused
8402
- const newBoxMediaState = targetMediaState === Characteristic.TargetMediaState.PLAY ? 1 : 0;
8445
+ const newBoxMediaState =
8446
+ targetMediaState === Characteristic.TargetMediaState.PLAY ? 1 : 0;
8403
8447
  const newBoxMediaStateName = newBoxMediaState === 1 ? "Play" : "Paused";
8404
8448
 
8405
8449
  //if (this.debugLevel >= 0) {
8406
- this.log(
8407
- "%s: setTargetMediaState: Calling setMediaState with newBoxMediaState %s [%s]",
8408
- this.name,
8409
- newBoxMediaState,
8410
- newBoxMediaStateName,
8411
- );
8450
+ this.log(
8451
+ "%s: setTargetMediaState: Calling setMediaState with newBoxMediaState %s [%s]",
8452
+ this.name,
8453
+ newBoxMediaState,
8454
+ newBoxMediaStateName,
8455
+ );
8412
8456
  //}
8413
8457
  /*
8414
8458
  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.4",
7
7
  "platformname": "eosstb",
8
8
  "dependencies": {
9
9
  "axios": "^1.16.0",