homebridge-eosstb 2.2.6 → 2.2.9

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/CHANGELOG.md CHANGED
@@ -10,6 +10,17 @@ Please restart Homebridge after every plugin update.
10
10
  * Implement refreshToken capabilities
11
11
 
12
12
 
13
+ ## 2.2.9 (2023-05-29)
14
+ * Fixed bug with access token expiring causing 401 unauthorised errors when refreshing channel list
15
+ * Cleaned up some code
16
+
17
+
18
+ ## 2.2.8 (2023-05-22)
19
+ * Updated iOS version references
20
+ * Fixed bug causing Homebridge crash when master channel list is not yet loaded
21
+ * Fixed bug causing Homebridge crash when any error occurs in refresh of master channel list
22
+
23
+
13
24
  ## 2.2.6 (2023-05-19)
14
25
  * Fixed some minor logging typos
15
26
  * Added logging of version info
package/README.md CHANGED
@@ -72,7 +72,7 @@ In January 2023, an ARRIS VIP5002W appeared, which identifies itself as an APLST
72
72
  This plugin is not provided by Magenta or Telenet or Sunrise or Virgin Media or Ziggo any other affiliate of [UPC](https://en.wikipedia.org/wiki/UPC_Broadband). It is neither endorsed nor supported nor developed by [UPC](https://en.wikipedia.org/wiki/UPC_Broadband) or any affiliates. [UPC](https://en.wikipedia.org/wiki/UPC_Broadband) can change their systems at any time and that might break this plugin. But I hope not.
73
73
 
74
74
  ## Requirements
75
- * An Apple iPhone or iPad with iOS/iPadOS 14.0 (or later). Developed on iOS 14.1...16.4, earlier versions not tested.
75
+ * An Apple iPhone or iPad with iOS/iPadOS 14.0 (or later). Developed on iOS 14.1...16.5, earlier versions not tested.
76
76
  * [Homebridge](https://homebridge.io/) v1.1.116 (or later). Developed on Homebridge 1.1.116....1.6.1, earlier versions not tested.
77
77
  * A TV subscription from one of the supported countries and TV providers.
78
78
  * An online account for viewing TV in the web app (often part of your TV package), see the table above.
@@ -115,7 +115,7 @@ This plugin is not provided by Magenta or Telenet or Sunrise or Virgin Media or
115
115
 
116
116
  * **Fully Configurable**: A large amount of configuration items exist to allow you to configure your plugin the way you want.
117
117
 
118
- * **Future Feature Support**: The plugin also supports current and target media state as well as closed captions, even though the Home app accessory cannot currently display or control this data in the home app (as at iOS 16.4). Hopefully, Apple will add support for these features in the future. You can however use this data in Home Automations or the Shortcuts app.
118
+ * **Future Feature Support**: The plugin also supports current and target media state as well as closed captions, even though the Home app accessory cannot currently display or control this data in the home app (as at iOS 16.5). Hopefully, Apple will add support for these features in the future. You can however use this data in Home Automations or the Shortcuts app.
119
119
 
120
120
 
121
121
 
@@ -136,7 +136,7 @@ The set-top box accessory is exposed as a separate external accessory and each s
136
136
  4. **Add Accessory**: tap **More options...** to add the accessory manually.
137
137
  5. **Select an Accessory to Add to \<HomeName\>**: Select the accessory you want to add. You should see your set-top box here. If not, check your Homebridge config.
138
138
  6. Accept the **Uncertified Accessory** warning by tapping **Add Anyway**.
139
- 7. **Enter HomeKit Setup Code**: Enter the **HomeKit Setup Code** (displayed in Homebridge under the QR code, format XXX-XX-XXX), or use the device's camera to scan the QR code in Homebridge and tap **Continue**.
139
+ 7. **Enter HomeKit Setup Code**: Enter the **HomeKit Setup Code** (displayed in Homebridge under the QR code, format XXXX-XXXX), or use the device's camera to scan the QR code in Homebridge and tap **Continue**.
140
140
  8. **Set-Top Box Location**: Select a room for your new accessory and tap **Continue**.
141
141
  9. **Set-Top Box Name**: Give your set-top box a different name if you wish (synchronised to your real set-top box, you can change this in the Home app later) and tap **Continue**.
142
142
  10. **Name TV Input Sources**: Re-name your TV input sources if you wish (you can change these in the Home app later) and tap **Continue**.
@@ -204,13 +204,13 @@ Services used in this set-top box accessory are:
204
204
  4. Input service. The input (TV channels) utilises one service per input. The maximum possible channels (inputs) are thus 100 - 3 = 97. I have limited the inputs to maximum 95, but you can override this in the config (helpful to reduce log entries when debugging). The inputs are hard limited to 95 inputs.
205
205
 
206
206
  ### Media State (Play/Pause) Limitations
207
- The eosstb plugin can detect the target and current media state and shows STOP, PLAY, PAUSE or LOADING (loading is displayed only for current media state when fast-forwarding or rewinding) in the Homebridge logs. Unfortunately, the Apple Home app cannot do anything with the media state (as at iOS 16.4) apart from allow you to read it in Shortcuts or Automations. Hopefully this will improve in the future.
207
+ The eosstb plugin can detect the target and current media state and shows STOP, PLAY, PAUSE or LOADING (loading is displayed only for current media state when fast-forwarding or rewinding) in the Homebridge logs. Unfortunately, the Apple Home app cannot do anything with the media state (as at iOS 16.5) apart from allow you to read it in Shortcuts or Automations. Hopefully this will improve in the future.
208
208
 
209
209
  ### Recording State Limitations
210
210
  The eosstb plugin can detect the current recording state of the set-top box, both for local HDD-based recording (for boxes that have a HDD fitted) and for network recording. The plugin shows IDLE, ONGOING_NDVR or ONGOING_LOCALDVR in the Homebridge logs. DVR means digital video recorder; N for network and LOCAL for local HDD based recording. The Apple Home app cannot natively do anything with the recording state but the eosstb plugin uses it to set the inUse charateristic if the set-top box is turned on or is recording to the local HDD. This is useful in Shortcuts or Automations.
211
211
 
212
212
  ### Closed Captions Limitations
213
- The eosstb plugin can detect the closed captions state (**Subtitle options** in the set-top box menu) and shows ENABLED or DISABLED in the Homebridge logs. Unfortunately, the Apple Home app cannot do anything with the closed captions state (as at iOS 16.4) apart from allow you to read it in Shortcuts or Automations. Hopefully this will improve in the future.
213
+ The eosstb plugin can detect the closed captions state (**Subtitle options** in the set-top box menu) and shows ENABLED or DISABLED in the Homebridge logs. Unfortunately, the Apple Home app cannot do anything with the closed captions state (as at iOS 16.5) apart from allow you to read it in Shortcuts or Automations. Hopefully this will improve in the future.
214
214
 
215
215
  ## Configuration
216
216
  Add a new platform to the platforms section of your homebridge `config.json`.
package/index.js CHANGED
@@ -306,6 +306,7 @@ class stbPlatform {
306
306
  this.config = config;
307
307
  this.api = api;
308
308
  this.stbDevices = []; // store stbDevice in this.stbDevices
309
+ this.masterChannelList = [];
309
310
 
310
311
  // show some useful version info
311
312
  this.log.info('%s v%s, node %s, homebridge v%s', packagejson.name, packagejson.version, process.version, this.api.serverVersion)
@@ -347,7 +348,33 @@ class stbPlatform {
347
348
  this.checkSessionInterval = setInterval(this.sessionWatchdog.bind(this),SESSION_WATCHDOG_INTERVAL_MS);
348
349
 
349
350
  // check for a channel list update every MASTER_CHANNEL_LIST_REFRESH_CHECK_INTERVAL_S seconds
350
- this.checkChannelListInterval = setInterval(this.refreshMasterChannelList.bind(this),MASTER_CHANNEL_LIST_REFRESH_CHECK_INTERVAL_S * 1000);
351
+ //this.checkChannelListInterval = setInterval(this.refreshMasterChannelList.bind(this),MASTER_CHANNEL_LIST_REFRESH_CHECK_INTERVAL_S * 1000);
352
+
353
+
354
+ this.checkChannelListInterval = setInterval(() => {
355
+ // check if master channel list has expired. If it has, refresh auth token, then refresh channel list
356
+ if (this.config.debugLevel >= 1) { this.log.warn('stbPlatform: checkChannelListInterval Start'); }
357
+ if (this.masterChannelListExpiryDate <= Date.now()) {
358
+ // must check and refresh auth token before each call to refresh master channel list
359
+ this.refreshAccessToken()
360
+ .then(response => {
361
+ if (this.config.debugLevel >= 1) { this.log.warn('stbPlatform: refreshAccessToken completed OK'); }
362
+ return this.refreshMasterChannelList()
363
+ })
364
+ .then(response => {
365
+ if (this.config.debugLevel >= 1) { this.log.warn('stbPlatform: refreshMasterChannelList completed OK'); }
366
+ return true
367
+ })
368
+ .catch(error => {
369
+ if (error.code) {
370
+ this.log.warn('stbPlatform: checkChannelListInterval Error', (error.syscall || '') + ' ' + (error.code || '') + ' ' + (error.config.url || error.hostname || ''));
371
+ } else {
372
+ this.log.warn('stbPlatform: checkChannelListInterval Error', error);
373
+ }
374
+ })
375
+ if (this.config.debugLevel >= 1) { this.log.warn('stbPlatform: checkChannelListInterval end'); }
376
+ }
377
+ }, MASTER_CHANNEL_LIST_REFRESH_CHECK_INTERVAL_S * 1000 ) // need to pass ms
351
378
 
352
379
  debug('stbPlatform:apievent :: didFinishLaunching end of code block')
353
380
  //this.log('stbPlatform: end of code block');
@@ -564,17 +591,17 @@ class stbPlatform {
564
591
  .then((objStbDevices) => {
565
592
  this.log('Discovery completed');
566
593
  this.log.debug('%s: ++++++ step 8: devices found:', watchdogInstance, this.devices.length)
567
- this.log.debug('%s: ++++++ step 8: calling getJwtToken', watchdogInstance)
594
+ this.log.debug('%s: ++++++ step 8: calling getMqttToken', watchdogInstance)
568
595
  errorTitle = 'Failed to start mqtt session';
569
- debug(debugPrefix + 'calling getJwtToken')
570
- return this.getJwtToken(this.session.username, this.session.accessToken, this.session.householdId);
596
+ debug(debugPrefix + 'calling getMqttToken')
597
+ return this.getMqttToken(this.session.username, this.session.accessToken, this.session.householdId);
571
598
  })
572
- .then((jwToken) => {
573
- this.log.debug('%s: ++++++ step 9: getJwtToken token was retrieved, token %s', watchdogInstance, jwToken)
599
+ .then((mqttToken) => {
600
+ this.log.debug('%s: ++++++ step 9: getMqttToken token was retrieved, token %s', watchdogInstance, mqttToken)
574
601
  this.log.debug('%s: ++++++ step 9: start mqtt client', watchdogInstance)
575
602
  debug(debugPrefix + 'calling startMqttClient')
576
- return this.startMqttClient(this, this.session.householdId, jwToken); // returns true
577
- })
603
+ return this.startMqttClient(this, this.session.householdId, mqttToken); // returns true
604
+ })
578
605
  .catch(errorReason => {
579
606
  // log any errors and set the currentSessionState
580
607
  this.log.warn(errorTitle + ' - %s', errorReason);
@@ -678,6 +705,73 @@ class stbPlatform {
678
705
  }
679
706
 
680
707
 
708
+ // get a new access token
709
+ async refreshAccessToken() {
710
+ return new Promise((resolve, reject) => {
711
+
712
+ // exit immediately if access token has not expired
713
+ if (this.session.accessTokenExpiry > Date.now()) {
714
+ if (this.config.debugLevel >= 1) { this.log.warn('refreshAccessToken: Access token has not expired yet. Next refresh will occur after %s', this.session.accessTokenExpiry.toLocaleString()); }
715
+ resolve(true);
716
+ return
717
+ }
718
+
719
+ if (this.config.debugLevel >= 1) { this.log.warn('refreshAccessToken: Access token has expired at %s. Requesting refresh', this.session.accessTokenExpiry.toLocaleString()); }
720
+
721
+ const axiosConfig = {
722
+ method: 'POST',
723
+ // https://prod.spark.sunrisetv.ch/auth-service/v1/authorization/refresh
724
+ url: countryBaseUrlArray[this.config.country.toLowerCase()] + '/auth-service/v1/authorization/refresh',
725
+ headers: {
726
+ 'x-oesp-username': this.session.username
727
+ },
728
+ jar: cookieJar,
729
+ data: {
730
+ refreshToken: this.session.refreshToken,
731
+ username: this.config.username
732
+ }
733
+ };
734
+
735
+ if (this.config.debugLevel >=1) { this.log.warn('refreshAccessToken: Post auth refresh request to',axiosConfig.url); }
736
+ axiosWS(axiosConfig)
737
+ .then(response => {
738
+ if (this.config.debugLevel >= 2) {
739
+ this.log('refreshAccessToken: auth refresh response:',response.status, response.statusText);
740
+ this.log('refreshAccessToken: response data (saved to this.session):');
741
+ this.log(response.data);
742
+ //this.log(response.headers);
743
+ }
744
+ this.session = response.data;
745
+
746
+ // add an expiry date for the access token: 2 min (120000ms) after created date
747
+ this.session.accessTokenExpiry = new Date(new Date().getTime() + 2*60000);
748
+
749
+ // check if householdId exists, if so, we have authenticated ok
750
+ if (this.session.householdId) { currentSessionState = sessionState.AUTHENTICATED; }
751
+ this.log.debug('Session username:', this.session.username);
752
+ this.log.debug('Session householdId:', this.session.householdId);
753
+ this.log.debug('Session accessToken:', this.session.accessToken);
754
+ this.log.debug('Session accessTokenExpiry:', this.session.accessTokenExpiry);
755
+ this.log.debug('Session refreshToken:', this.session.refreshToken);
756
+ this.log.debug('Session refreshTokenExpiry:', this.session.refreshTokenExpiry);
757
+ // Robustness: Observed that new APLSTB Apollo box on NL did not always return username during session logon, so store username from settings if missing
758
+ if (this.session.username == '') {
759
+ this.log.debug('Session username empty, setting to %s', this.config.username);
760
+ this.session.username = this.config.username;
761
+ } else {
762
+ this.log.debug('Session username exists: %s', this.session.username);
763
+ }
764
+ currentSessionState = sessionState.CONNECTED;
765
+ this.currentStatusFault = Characteristic.StatusFault.NO_FAULT;
766
+ resolve(this.session.householdId) // resolve the promise with the householdId
767
+ })
768
+ .catch(error => {
769
+ this.log.debug('refreshAccessToken: error:', error);
770
+ reject(error); // reject the promise and return the error
771
+ });
772
+ })
773
+ }
774
+
681
775
 
682
776
  // select the right session to create
683
777
  async createSession(country) {
@@ -768,11 +862,16 @@ class stbPlatform {
768
862
  this.log(response.data);
769
863
  }
770
864
  this.session = response.data;
865
+
866
+ // add an expiry date for the access token: 2 min (120000ms) after created date
867
+ this.session.accessTokenExpiry = new Date(new Date().getTime() + 2*60000);
868
+
771
869
  // check if householdId exists, if so, we have authenticated ok
772
870
  if (this.session.householdId) { currentSessionState = sessionState.AUTHENTICATED; }
773
871
  this.log.debug('Session username:', this.session.username);
774
872
  this.log.debug('Session householdId:', this.session.householdId);
775
873
  this.log.debug('Session accessToken:', this.session.accessToken);
874
+ this.log.debug('Session accessTokenExpiry:', this.session.accessTokenExpiry);
776
875
  this.log.debug('Session refreshToken:', this.session.refreshToken);
777
876
  this.log.debug('Session refreshTokenExpiry:', this.session.refreshTokenExpiry);
778
877
  // Robustness: Observed that new APLSTB Apollo box on NL did not always return username during session logon, so store username from settings if missing
@@ -795,8 +894,10 @@ class stbPlatform {
795
894
  errReason = 'try again later: ' + error.response.status + ' ' + (error.response.statusText || '');
796
895
  } else if (error.response && error.response.status) {
797
896
  errReason = 'check your internet connection: ' + error.response.status + ' ' + (error.response.statusText || '');
798
- } else {
897
+ } else if (error.code) {
799
898
  errReason = 'check your internet connection: ' + error.code + ' ' + (error.hostname || '');
899
+ } else {
900
+ errReason = 'unexpected error: ' + error;
800
901
  }
801
902
  //this.log('%s %s', errText, (errReason || ''));
802
903
  this.log.debug('getSession: error:', error);
@@ -1336,7 +1437,7 @@ class stbPlatform {
1336
1437
 
1337
1438
  // exit immediately if channel list has not expired
1338
1439
  if (this.masterChannelListExpiryDate > Date.now()) {
1339
- if (this.config.debugLevel > 1) { this.log.warn('refreshMasterChannelList: Master channel list has not expired yet. Next refresh will occur after %s', this.masterChannelListExpiryDate.toLocaleString()); }
1440
+ if (this.config.debugLevel >= 1) { this.log.warn('refreshMasterChannelList: Master channel list has not expired yet. Next refresh will occur after %s', this.masterChannelListExpiryDate.toLocaleString()); }
1340
1441
  resolve(true);
1341
1442
  return
1342
1443
  }
@@ -1370,6 +1471,7 @@ class stbPlatform {
1370
1471
  url: url,
1371
1472
  headers: {
1372
1473
  accept: '*/*',
1474
+ "x-oesp-token": this.session.accessToken, // to try and avoid the 401 auth issues
1373
1475
  'x-oesp-username': this.session.username
1374
1476
  }
1375
1477
  };
@@ -1378,6 +1480,11 @@ class stbPlatform {
1378
1480
  if (this.config.debugLevel > 0) { this.log.warn('refreshMasterChannelList: response: %s %s', response.status, response.statusText); }
1379
1481
  //this.log(response.data);
1380
1482
 
1483
+ // the header contains the following:
1484
+ // Cache-Control: max-age=600, public, stale-if-error=43200
1485
+ // this could be used to set expiry date...
1486
+
1487
+
1381
1488
  // set the masterChannelListExpiryDate to expire at now + MASTER_CHANNEL_LIST_VALID_FOR_S
1382
1489
  this.masterChannelListExpiryDate =new Date(new Date().getTime() + (MASTER_CHANNEL_LIST_VALID_FOR_S * 1000));
1383
1490
  //this.log('MasterChannelList valid until',this.masterChannelListExpiryDate.toLocaleString())
@@ -1423,7 +1530,6 @@ class stbPlatform {
1423
1530
 
1424
1531
 
1425
1532
 
1426
-
1427
1533
  // load all recording states and bookings
1428
1534
  // called when a mqtt topic is received indicating a recording settings change
1429
1535
  async refreshRecordings(householdId, callback) {
@@ -1965,28 +2071,28 @@ class stbPlatform {
1965
2071
  // START session handler mqtt
1966
2072
  //+++++++++++++++++++++++++++++++++++++++++++++++++++++
1967
2073
 
1968
- // get a Json Web Token
1969
- async getJwtToken(oespUsername, accessToken, householdId){
2074
+ // get the mqtt token
2075
+ async getMqttToken(oespUsername, accessToken, householdId){
1970
2076
  return new Promise((resolve, reject) => {
1971
- this.log.debug("Getting jwt token for householdId %s", householdId);
2077
+ this.log.debug("Getting mqtt token for householdId %s", householdId);
1972
2078
  // get a JSON web token from the supplied accessToken and householdId
1973
- if (this.config.debugLevel > 1) { this.log.warn('getJwtToken'); }
2079
+ if (this.config.debugLevel > 1) { this.log.warn('getMqttToken'); }
1974
2080
  // robustness checks
1975
2081
  if (currentSessionState !== sessionState.CONNECTED) {
1976
- this.log.warn('Cannot get JWT token: currentSessionState incorrect:', currentSessionState);
2082
+ this.log.warn('Cannot get mqtt token: currentSessionState incorrect:', currentSessionState);
1977
2083
  return false;
1978
2084
  }
1979
2085
  if (!accessToken) {
1980
- this.log.warn('Cannot get JWT token: accessToken not set');
2086
+ this.log.warn('Cannot get mqtt token: accessToken not set');
1981
2087
  return false;
1982
2088
  }
1983
2089
 
1984
- //this.log.warn('getJwtToken disabled while I build channel list');
2090
+ //this.log.warn('getMqttToken disabled while I build channel list');
1985
2091
  //return false;
1986
2092
 
1987
- const jwtAxiosConfig = {
2093
+ const mqttAxiosConfig = {
1988
2094
  method: 'GET',
1989
- url: countryBaseUrlArray[this.config.country.toLowerCase()] + '/tokens/jwt',
2095
+ //url: countryBaseUrlArray[this.config.country.toLowerCase()] + '/tokens/jwt', prior to October 2022
1990
2096
  // examples of auth-service/v1/mqtt/token urls:
1991
2097
  // https://prod.spark.ziggogo.tv/auth-service/v1/mqtt/token
1992
2098
  // https://prod.spark.sunrisetv.ch/auth-service/v1/mqtt/token
@@ -1996,23 +2102,22 @@ class stbPlatform {
1996
2102
  'X-OESP-Username': oespUsername,
1997
2103
  }
1998
2104
  };
1999
- this.log.debug("getJwtToken: jwtAxiosConfig:", jwtAxiosConfig)
2000
- axiosWS(jwtAxiosConfig)
2105
+ this.log.debug("getMqttToken: mqttAxiosConfig:", mqttAxiosConfig)
2106
+ axiosWS(mqttAxiosConfig)
2001
2107
  .then(response => {
2002
2108
  if (this.config.debugLevel > 0) {
2003
- this.log.warn("getJwtToken: response.data:", response.data)
2109
+ this.log.warn("getMqttToken: response.data:", response.data)
2004
2110
  }
2005
- //this.jwtToken = response.data.token; // store the token
2006
2111
  mqttUsername = householdId; // used in sendKey to ensure that mqtt is connected
2007
- resolve(response.data.token); // resolve with the tokwn
2112
+ resolve(response.data.token); // resolve with the token
2008
2113
  //this.startMqttClient(this, householdId, response.data.token); // this starts the mqtt session
2009
2114
 
2010
2115
  })
2011
2116
  .catch(error => {
2012
- this.log.debug('getJwtToken error details:', error);
2117
+ this.log.debug('getMqttToken error details:', error);
2013
2118
  // set session flag to disconnected to force a session reconnect
2014
2119
  currentSessionState = sessionState.DISCONNECTED;
2015
- reject('Failed to get jwtToken: ', error.code + ' ' + (error.hostname || ''));
2120
+ reject('Failed to get mqtt token: ', error.code + ' ' + (error.hostname || ''));
2016
2121
  });
2017
2122
  })
2018
2123
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "displayName": "Homebridge EOSSTB",
4
4
  "description": "homebridge-plugin - Add your set-top box to Homekit (for Magenta AT, Telenet BE, Sunrise CH, Virgin Media GB & IE, Ziggo NL)",
5
5
  "author": "Jochen Siegenthaler (https://github.com/jsiegenthaler/)",
6
- "version": "2.2.6",
6
+ "version": "2.2.9",
7
7
  "platformname": "eosstb",
8
8
  "dependencies": {
9
9
  "axios-cookiejar-support": "^4.0.6",