homebridge-eosstb 2.2.16 → 2.3.0

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
@@ -8,12 +8,27 @@ Please restart Homebridge after every plugin update.
8
8
  ## Current To-Do and In-Work List (For Future Releases, in rough order of priority):
9
9
  * Add ability to log and read current program name
10
10
 
11
+
12
+ ## 2.3.0-beta.8 (2024-01-25)
13
+ * Added auto endpoint detection for all services, this fixes connection issues in many countries
14
+ * Added ability to set authentication method. You must select the method in the plugin config. If none set, logon method falls back to using country code
15
+ * Added Disable Session Watchdog to config.schema to make it easier to debug by turning off the session watchdog
16
+ * Fixed issue connecting to mqtt broker (issue started ca. 23 Jan 2024) by adding extra subprotocol headers
17
+ * Fixed bug in getMostWatchedChannels where the endpoint was incorrect
18
+ * Updated Readme plugin status for various countries
19
+ * Updated iOS version references in Readme
20
+ * Bumped dependency "axios": "^1.6.6"
21
+ * Bumped dependency "mqtt": "^5.3.5"
22
+ * IN WORK: Reworking GB authentication methods. NOT YET WORKING, PLEASE BE PATIENT
23
+
24
+
11
25
  ## 2.2.16 (2024-01-16)
12
26
  * Removed AZ, CZ, DE, HU, RO from config.json and Readme. These countries no longer offer UPC TV.
13
27
 
28
+
14
29
  ## 2.2.15 (2024-01-14)
15
30
  * Fixed issue with MQTT connection failure in CH due to change of MQTT endpoint
16
- * Bumped dependency "axios-cookiejar-support": "^5.0.0",
31
+ * Bumped dependency "axios-cookiejar-support": "^5.0.0"
17
32
 
18
33
 
19
34
  ## 2.2.14 (2024-01-06)
package/README.md CHANGED
@@ -47,9 +47,9 @@ As [UPC](https://en.wikipedia.org/wiki/UPC_Broadband) (the operator of the Horiz
47
47
  | GB | [Virgin Media](https://www.virginmedia.com/) | [Virgin TV Go](https://virgintvgo.virginmedia.com/en.html) | [Virgin TV 360](https://www.virginmedia.com/shop/tv/virgin-tv-360) and [Virgin TV 360 Mini](https://www.virginmedia.com/shop/tv/multiroom) | Fully Working |
48
48
  | IE | [Virgin Media](https://www.virginmedia.ie/) | [Virgin TV Anywhere](https://www.virginmediatv.ie/en.html) | [360 Box](https://www.virginmedia.ie/virgintv360support/) | Fully Working |
49
49
  | NL | [Ziggo](https://www.ziggo.nl/) | [Ziggo GO](https://www.ziggogo.tv/nl.html) | [Mediabox Next](https://www.ziggo.nl/televisie/mediaboxen/mediabox-next#ziggo-tv) | Fully Working |
50
+ | PL | [UPC PL](https://www.upc.pl/) | [UPC TV GO](https://www.upctv.pl/pl/home) | UPC TV Box | Fully Working |
50
51
  | ------- | ----------- | ------- | -------- | ------------- |
51
- | PL | [UPC PL](https://www.upc.pl/) | [UPC TV GO](https://www.upctv.pl/pl/home) | Horizon decoder | _Testers Wanted_ |
52
- | SK | [UPC Broadband Slovakia](https://www.upc.sk/) | [UPC TV](https://www.upctv.sk/sk/home) | UPC TV | _Testers Wanted_ |
52
+ | SK | [UPC Broadband Slovakia](https://www.upc.sk/) | [UPC TV](https://www.upctv.sk/sk/home) | UPC TV Box | _Testers Wanted_ |
53
53
 
54
54
 
55
55
  If you subscribe to a TV service from one of these countries, you are lucky, this plugin will work for you.
@@ -70,7 +70,7 @@ In January 2023, an ARRIS VIP5002W appeared, which identifies itself as an APLST
70
70
  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.
71
71
 
72
72
  ## Requirements
73
- * An Apple iPhone or iPad with iOS/iPadOS 14.0 (or later). Developed on iOS 14.1...17.2, earlier versions not tested.
73
+ * An Apple iPhone or iPad with iOS/iPadOS 14.0 (or later). Developed on iOS 14.1...17.3, earlier versions not tested.
74
74
  * [Homebridge](https://homebridge.io/) v1.1.116 (or later). Developed on Homebridge 1.1.116....1.7.0, earlier versions not tested.
75
75
  * A TV subscription from one of the supported countries and TV providers.
76
76
  * An online account for viewing TV in the web app (often part of your TV package), see the table above.
@@ -113,7 +113,7 @@ This plugin is not provided by Magenta or Telenet or Sunrise or Virgin Media or
113
113
 
114
114
  * **Fully Configurable**: A large amount of configuration items exist to allow you to configure your plugin the way you want.
115
115
 
116
- * **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 17.2). Hopefully, Apple will add support for these features in the future. You can however use this data in Home Automations or the Shortcuts app.
116
+ * **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 17.3). Hopefully, Apple will add support for these features in the future. You can however use this data in Home Automations or the Shortcuts app.
117
117
 
118
118
 
119
119
 
@@ -171,7 +171,7 @@ The following keys are supported by in the **Apple TV Remote** in the Control Ce
171
171
  | Volume Up | volUpCommand | - |
172
172
  | Volume Down | volDownCommand | 3 clicks = mute |
173
173
 
174
- NOTE: The Mute and Power buttons appear in the Remote Control as of iOS 17.2, however they are disabled. Currently, I do not know how to enable these buttons. If you have any information about these buttons, please get in touch with me.
174
+ NOTE: The Mute and Power buttons appear in the Remote Control as of iOS 17.3, however they are disabled. Currently, I do not know how to enable these buttons. If you have any information about these buttons, please get in touch with me.
175
175
 
176
176
  The table shows the default key mappings. You can map any Apple TV Remote button to any set-top box remote control button, see the Wiki for all of the known [KeyEvents](https://github.com/jsiegenthaler/homebridge-eosstb/wiki/KeyEvents).
177
177
 
@@ -206,13 +206,13 @@ Services used in this set-top box accessory are:
206
206
  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.
207
207
 
208
208
  ### Media State (Play/Pause) Limitations
209
- 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 17.2) apart from allow you to read it in Shortcuts or Automations. Hopefully this will improve in the future.
209
+ 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 17.3) apart from allow you to read it in Shortcuts or Automations. Hopefully this will improve in the future.
210
210
 
211
211
  ### Recording State Limitations
212
212
  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.
213
213
 
214
214
  ### Closed Captions Limitations
215
- 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 17.2) apart from allow you to read it in Shortcuts or Automations. Hopefully this will improve in the future.
215
+ 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 17.3) apart from allow you to read it in Shortcuts or Automations. Hopefully this will improve in the future.
216
216
 
217
217
  ## Configuration
218
218
  Add a new platform to the platforms section of your homebridge `config.json`.
@@ -47,7 +47,27 @@
47
47
  "placeholder": "yourTvProviderPassword",
48
48
  "required": true
49
49
  },
50
-
50
+ "authmethod": {
51
+ "title": "Authentication Method",
52
+ "type": "string",
53
+ "description": "The authentication method. Select the option for your country. If it doesn't work, try another method.",
54
+ "default": "A",
55
+ "required": true,
56
+ "oneOf": [
57
+ { "title": "Method A: CH, NL, IE", "enum": ["A"] },
58
+ { "title": "Method B: BE", "enum": ["B"] },
59
+ { "title": "Method C: GB", "enum": ["C"] },
60
+ { "title": "Method D: OAuth 2.0 PKCE EXPERIMENTAL", "enum": ["D"] }
61
+ ]
62
+ },
63
+
64
+ "watchdogDisabled": {
65
+ "title": "Disable Session Watchdog",
66
+ "type": "boolean",
67
+ "description": "Disables the session watchdog to assist with debugging. Default: false",
68
+ "default": false
69
+ },
70
+
51
71
  "doublePressTime": {
52
72
  "title": "Double-Press Time",
53
73
  "type": "integer",
@@ -841,8 +861,10 @@
841
861
  "type": "flex",
842
862
  "flex-flow": "column",
843
863
  "items": [
844
- "country"
845
- ]
864
+ "country",
865
+ "authmethod",
866
+ "watchdogDisabled"
867
+ ]
846
868
  },
847
869
  {
848
870
  "type": "flex",
package/index.js CHANGED
@@ -18,6 +18,11 @@ const semver = require('semver') // https://github.com/npm/node-semver
18
18
 
19
19
  const mqtt = require('mqtt'); // https://github.com/mqttjs
20
20
  const qs = require('qs'); // https://github.com/ljharb/qs
21
+ const WebSocket = require('ws'); // https://github.com/websockets/ws for the mqtt websocket
22
+
23
+
24
+ // needed for sso logon with pkce OAuth 2.0
25
+ const {randomBytes, createHash} = require("node:crypto");
21
26
 
22
27
 
23
28
  // axios-cookiejar-support v2.0.2 syntax
@@ -51,17 +56,24 @@ axiosCookieJarSupport(axiosWS);
51
56
  // without any trailing /
52
57
  // refer https://github.com/Sholofly/lghorizon-python/blob/features/telenet/lghorizon/const.py
53
58
  const countryBaseUrlArray = {
54
- 'be-fr': 'https://prod.spark.telenet.tv',
55
- 'be-nl': 'https://prod.spark.telenet.tv',
56
- 'ch': 'https://prod.spark.sunrisetv.ch',
57
- 'gb': 'https://prod.spark.virginmedia.com',
58
- 'ie': 'https://prod.spark.virginmediatv.ie',
59
- 'nl': 'https://prod.spark.ziggogo.tv',
60
- 'pl': 'https://prod.spark.upctv.pl',
61
- 'sk': 'https://prod.spark.upctv.sk',
59
+ //https://spark-prod-be.gnp.cloud.telenet.tv/be/en/config-service/conf/web/backoffice.json
60
+ //'be-fr': 'https://prod.spark.telenet.tv',
61
+ //'be-nl': 'https://prod.spark.telenet.tv',
62
+ 'be': 'https://spark-prod-be.gnp.cloud.telenet.tv', // verified 14.01.2024
63
+ // https://spark-prod-ch.gnp.cloud.sunrisetv.ch/ch/en/config-service/conf/web/backoffice.json
64
+ //'ch': 'https://prod.spark.sunrisetv.ch',
65
+ 'ch': 'https://spark-prod-ch.gnp.cloud.sunrisetv.ch', // verified 14.01.2024
66
+ 'gb': 'https://spark-prod-gb.gnp.cloud.virgintvgo.virginmedia.com', // verified 14.01.2024
67
+ 'ie': 'https://spark-prod-ie.gnp.cloud.virginmediatv.ie', // verified 14.01.2024
68
+ 'nl': 'https://prod.spark.ziggogo.tv', // verified 14.01.2024
69
+ //'pl': 'https://prod.spark.upctv.pl',
70
+ 'pl': 'https://spark-prod-pl.gnp.cloud.upctv.pl', // verified 14.01.2024
71
+ //'sk': 'https://prod.spark.upctv.sk',
72
+ 'sk': 'https://spark-prod-sk.gnp.cloud.upctv.sk', // verified 14.01.2024
62
73
  };
63
74
 
64
75
  // mqtt endpoints varies by country, unchanged after backend change on 13.10.2022
76
+ /*
65
77
  const mqttUrlArray = {
66
78
  'be-fr': 'wss://obomsg.prod.be.horizon.tv/mqtt',
67
79
  'be-nl': 'wss://obomsg.prod.be.horizon.tv/mqtt',
@@ -71,7 +83,7 @@ const mqttUrlArray = {
71
83
  'nl': 'wss://obomsg.prod.nl.horizon.tv/mqtt',
72
84
  'pl': 'wss://obomsg.prod.pl.horizon.tv/mqtt',
73
85
  'sk': 'wss://obomsg.prod.sk.horizon.tv/mqtt'
74
- };
86
+ };*/
75
87
 
76
88
 
77
89
  // openid logon url used in Telenet.be Belgium for be-nl and be-fr sessions
@@ -243,6 +255,17 @@ async function waitprom(ms) {
243
255
  }
244
256
 
245
257
 
258
+ // generate PKCE code verifier pair for OAuth 2.0
259
+ function generatePKCEPair() {
260
+ const NUM_OF_BYTES = 22; // Total of 44 characters (1 Byte = 2 char) (standard states that: 43 chars <= verifier <= 128 chars)
261
+ const HASH_ALG = "sha256";
262
+ const code_verifier = randomBytes(NUM_OF_BYTES).toString('hex');
263
+ const code_verifier_hash = createHash(HASH_ALG).update(code_verifier).digest('base64');
264
+ const code_challenge = code_verifier_hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); // Clean base64 to make it URL safe
265
+ return {verifier: code_verifier, code_challenge}
266
+ }
267
+
268
+
246
269
 
247
270
  // ++++++++++++++++++++++++++++++++++++++++++++
248
271
  // config end
@@ -314,7 +337,11 @@ class stbPlatform {
314
337
  setTimeout(this.sessionWatchdog.bind(this),500); // wait 500ms then call this.sessionWatchdog
315
338
 
316
339
  // the session watchdog creates a session when none exists, and recreates one if the session ever fails due to internet failure or anything else
317
- this.checkSessionInterval = setInterval(this.sessionWatchdog.bind(this),SESSION_WATCHDOG_INTERVAL_MS);
340
+ if ((this.config.watchdogDisabled || false) == true) {
341
+ this.log.warn('WARNING: Session watchdog disabled')
342
+ } else {
343
+ this.checkSessionInterval = setInterval(this.sessionWatchdog.bind(this),SESSION_WATCHDOG_INTERVAL_MS);
344
+ }
318
345
 
319
346
  // check for a channel list update every MASTER_CHANNEL_LIST_REFRESH_CHECK_INTERVAL_S seconds
320
347
  this.checkChannelListInterval = setInterval(() => {
@@ -354,7 +381,7 @@ class stbPlatform {
354
381
  debug('stbPlatform:apievent :: shutdown')
355
382
  if (this.config.debugLevel > 2) { this.log.warn('API event: shutdown'); }
356
383
  isShuttingDown = true;
357
- this.endMqttClient()
384
+ this.endMqttSession()
358
385
  .then(() => {
359
386
  this.log('Goodbye');
360
387
  }
@@ -501,72 +528,80 @@ class stbPlatform {
501
528
  if (this.config.debugLevel > 2) { this.log.warn('%s: Attempting to create session', watchdogInstance); }
502
529
 
503
530
  // asnyc startup sequence with chain of promises
504
- this.log.debug('%s: ++++ step 1: calling createSession', watchdogInstance)
505
- errorTitle = 'Failed to create session';
506
- debug(debugPrefix + 'calling createSession')
507
- await this.createSession(this.config.country.toLowerCase()) // returns householdId, stores session in this.session
531
+ this.log.debug('%s: ++++ step 1: calling config service', watchdogInstance)
532
+ errorTitle = 'Failed to get config';
533
+ debug(debugPrefix + 'calling getConfig')
534
+ await this.getConfig(this.config.country.toLowerCase()) // returns config, stores config in this.config
535
+ .then((session) => {
536
+ this.log.debug('%s: ++++++ step 2: config was retrieved', watchdogInstance)
537
+ this.log.debug('%s: ++++++ step 2: calling createSession with country code %s ', watchdogInstance, this.config.country.toLowerCase())
538
+ this.log('Creating session...');
539
+ errorTitle = 'Failed to create session';
540
+ debug(debugPrefix + 'calling createSession')
541
+ return this.createSession(this.config.country.toLowerCase()) // returns householdId, stores session in this.session
542
+ })
508
543
  .then((sessionHouseholdId) => {
509
- this.log.debug('%s: ++++++ step 2: session was created, connected to sessionHouseholdId %s', watchdogInstance, sessionHouseholdId)
510
- this.log.debug('%s: ++++++ step 2: calling getPersonalizationData with sessionHouseholdId %s ', watchdogInstance, sessionHouseholdId)
544
+ this.log.debug('%s: ++++++ step 3: session was created, connected to sessionHouseholdId %s', watchdogInstance, sessionHouseholdId)
545
+ this.log.debug('%s: ++++++ step 3: calling getPersonalizationData with sessionHouseholdId %s ', watchdogInstance, sessionHouseholdId)
511
546
  this.log('Discovering platform...');
512
547
  errorTitle = 'Failed to discover platform';
513
548
  debug(debugPrefix + 'calling getPersonalizationData')
514
549
  return this.getPersonalizationData(this.session.householdId) // returns customer object, with devices and profiles, stores object in this.customer
515
550
  })
516
551
  .then((objCustomer) => {
517
- this.log.debug('%s: ++++++ step 3: personalization data was retrieved, customerId %s customerStatus %s', watchdogInstance, objCustomer.customerId, objCustomer.customerStatus)
518
- this.log.debug('%s: ++++++ step 3: calling getEntitlements with customerId %s ', watchdogInstance, objCustomer.customerId)
552
+ this.log.debug('%s: ++++++ step 4: personalization data was retrieved, customerId %s customerStatus %s', watchdogInstance, objCustomer.customerId, objCustomer.customerStatus)
553
+ this.log.debug('%s: ++++++ step 4: calling getEntitlements with customerId %s ', watchdogInstance, objCustomer.customerId)
519
554
  debug(debugPrefix + 'calling getEntitlements')
520
555
  return this.getEntitlements(this.customer.customerId) // returns customer object
521
556
  })
522
557
  .then((objEntitlements) => {
523
- this.log.debug('%s: ++++++ step 4: entitlements data was retrieved, objEntitlements.token %s', watchdogInstance, objEntitlements.token)
524
- this.log.debug('%s: ++++++ step 4: calling refreshMasterChannelList', watchdogInstance)
558
+ this.log.debug('%s: ++++++ step 5: entitlements data was retrieved, objEntitlements.token %s', watchdogInstance, objEntitlements.token)
559
+ this.log.debug('%s: ++++++ step 5: calling refreshMasterChannelList', watchdogInstance)
525
560
  debug(debugPrefix + 'calling refreshMasterChannelList')
526
561
  return this.refreshMasterChannelList() // returns entitlements object
527
562
  })
528
563
  .then((objChannels) => {
529
- this.log.debug('%s: ++++++ step 5: masterchannelList data was retrieved, channels found: %s', watchdogInstance, objChannels.length)
564
+ this.log.debug('%s: ++++++ step 6: masterchannelList data was retrieved, channels found: %s', watchdogInstance, objChannels.length)
530
565
  // Recording needs entitlements of PVR or LOCALDVR
531
566
  const pvrFeatureFound = this.entitlements.features.find(feature => (feature === 'PVR' || feature === 'LOCALDVR'));
532
- this.log.debug('%s: ++++++ step 5: foundPvrEntitlement %s', watchdogInstance, pvrFeatureFound);
567
+ this.log.debug('%s: ++++++ step 6: foundPvrEntitlement %s', watchdogInstance, pvrFeatureFound);
533
568
  if (pvrFeatureFound) {
534
- this.log.debug('%s: ++++++ step 5: calling getRecordingState with householdId %s', watchdogInstance, this.session.householdId)
569
+ this.log.debug('%s: ++++++ step 6: calling getRecordingState with householdId %s', watchdogInstance, this.session.householdId)
535
570
  this.getRecordingState(this.session.householdId) // returns true when successful
536
571
  }
537
572
  return true
538
573
  })
539
574
  .then((objRecordingStateFound) => {
540
- this.log.debug('%s: ++++++ step 6: recording state data was retrieved, objRecordingStateFound: %s', watchdogInstance, objRecordingStateFound)
575
+ this.log.debug('%s: ++++++ step 7: recording state data was retrieved, objRecordingStateFound: %s', watchdogInstance, objRecordingStateFound)
541
576
  // Recording needs entitlements of PVR or LOCALDVR
542
577
  const pvrFeatureFound = this.entitlements.features.find(feature => (feature === 'PVR' || feature === 'LOCALDVR'));
543
- this.log.debug('%s: ++++++ step 6: foundPvrEntitlement %s', watchdogInstance, pvrFeatureFound);
578
+ this.log.debug('%s: ++++++ step 7: foundPvrEntitlement %s', watchdogInstance, pvrFeatureFound);
544
579
  if (pvrFeatureFound) {
545
- this.log.debug('%s: ++++++ step 6: calling getRecordingBookings with householdId %s', watchdogInstance, this.session.householdId)
580
+ this.log.debug('%s: ++++++ step 7: calling getRecordingBookings with householdId %s', watchdogInstance, this.session.householdId)
546
581
  this.getRecordingBookings(this.session.householdId) // returns true when successful
547
582
  }
548
583
  return true
549
584
  })
550
585
  .then((objRecordingBookingsFound) => {
551
- this.log.debug('%s: ++++++ step 7: recording bookings data was retrieved, objRecordingBookingsFound: %s', watchdogInstance, objRecordingBookingsFound)
552
- this.log.debug('%s: ++++++ step 7: calling discoverDevices', watchdogInstance)
586
+ this.log.debug('%s: ++++++ step 8: recording bookings data was retrieved, objRecordingBookingsFound: %s', watchdogInstance, objRecordingBookingsFound)
587
+ this.log.debug('%s: ++++++ step 8: calling discoverDevices', watchdogInstance)
553
588
  errorTitle = 'Failed to discover devices';
554
589
  debug(debugPrefix + 'calling discoverDevices')
555
590
  return this.discoverDevices() // returns stbDevices object
556
591
  })
557
592
  .then((objStbDevices) => {
558
593
  this.log('Discovery completed');
559
- this.log.debug('%s: ++++++ step 8: devices found:', watchdogInstance, this.devices.length)
560
- this.log.debug('%s: ++++++ step 8: calling getMqttToken', watchdogInstance)
594
+ this.log.debug('%s: ++++++ step 9: devices found:', watchdogInstance, this.devices.length)
595
+ this.log.debug('%s: ++++++ step 9: calling getMqttToken', watchdogInstance)
561
596
  errorTitle = 'Failed to start mqtt session';
562
597
  debug(debugPrefix + 'calling getMqttToken')
563
598
  return this.getMqttToken(this.session.username, this.session.accessToken, this.session.householdId);
564
599
  })
565
600
  .then((mqttToken) => {
566
- this.log.debug('%s: ++++++ step 9: getMqttToken token was retrieved, token %s', watchdogInstance, mqttToken)
567
- this.log.debug('%s: ++++++ step 9: start mqtt client', watchdogInstance)
568
- debug(debugPrefix + 'calling startMqttClient')
569
- return this.startMqttClient(this, this.session.householdId, mqttToken); // returns true
601
+ this.log.debug('%s: ++++++ step 10: getMqttToken token was retrieved, token %s', watchdogInstance, mqttToken)
602
+ this.log.debug('%s: ++++++ step 10: start mqtt client', watchdogInstance)
603
+ debug(debugPrefix + 'calling statMqttClient')
604
+ return this.statMqttClient(this, this.session.householdId, mqttToken); // returns true
570
605
  })
571
606
  .catch(errorReason => {
572
607
  // log any errors and set the currentSessionState
@@ -690,7 +725,8 @@ class stbPlatform {
690
725
  const axiosConfig = {
691
726
  method: 'POST',
692
727
  // https://prod.spark.sunrisetv.ch/auth-service/v1/authorization/refresh
693
- url: countryBaseUrlArray[this.config.country.toLowerCase()] + '/auth-service/v1/authorization/refresh',
728
+ //url: countryBaseUrlArray[this.config.country.toLowerCase()] + '/auth-service/v1/authorization/refresh',
729
+ url: this.configsvc.authorizationService.URL + '/v1/authorization/refresh',
694
730
  headers: {
695
731
  "accept": "*/*", // mandatory
696
732
  "content-type": "application/json; charset=UTF-8", // mandatory
@@ -750,18 +786,24 @@ class stbPlatform {
750
786
  async createSession(country) {
751
787
  return new Promise((resolve, reject) => {
752
788
  this.currentStatusFault = Characteristic.StatusFault.NO_FAULT;
753
- switch(country) {
754
- case 'be-nl': case 'be-fr':
789
+ //switch using authmethod with backup of country
790
+ switch(this.config.authmethod || this.config.country) {
791
+ case 'D': // OAuth 2.0 with PKCE
792
+ this.getSessionOAuth2Pkce()
793
+ .then((getSessionResponse) => { resolve(getSessionResponse); }) // return the getSessionResponse for the promise
794
+ .catch(error => { reject(error); }); // on any error, reject the promise and pass back the error
795
+ break;
796
+ case 'be-nl': case 'be-fr': case 'B':
755
797
  this.getSessionBE()
756
798
  .then((getSessionResponse) => { resolve(getSessionResponse); }) // return the getSessionResponse for the promise
757
799
  .catch(error => { reject(error); }); // on any error, reject the promise and pass back the error
758
800
  break;
759
- case 'gb':
801
+ case 'gb': case 'C':
760
802
  this.getSessionGB()
761
803
  .then((getSessionResponse) => { resolve(getSessionResponse); }) // return the getSessionResponse for the promise
762
804
  .catch(error => { reject(error); }); // on any error, reject the promise and pass back the error
763
805
  break;
764
- default: // ch, nl, ie, at
806
+ default: // ch, nl, ie, at, method A
765
807
  this.getSession()
766
808
  .then((getSessionResponse) => { resolve(getSessionResponse); }) // resolve with the getSessionResponse for the promise
767
809
  .catch(error => { reject(error); }); // on any error, reject the promise and pass back the error
@@ -769,6 +811,286 @@ class stbPlatform {
769
811
  })
770
812
  }
771
813
 
814
+
815
+ // get session for OAuth 2.0 PKCE (special logon sequence)
816
+ getSessionOAuth2Pkce() {
817
+ return new Promise((resolve, reject) => {
818
+ this.log('Creating %s OAuth 2.0 PKCE session...',PLATFORM_NAME);
819
+ this.log.warn('++++ PLEASE NOTE: This is current test code with lots of debugging. Do not expect it to work yet. ++++');
820
+ currentSessionState = sessionState.LOADING;
821
+
822
+
823
+ // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
824
+ // axios interceptors to log request and response for debugging
825
+ // works on all following requests in this sub
826
+ /*
827
+ axiosWS.interceptors.request.use(req => {
828
+ this.log.warn('+++INTERCEPTED BEFORE HTTP REQUEST COOKIEJAR:\n', cookieJar.getCookies(req.url));
829
+ this.log.warn('+++INTERCEPTOR HTTP REQUEST:',
830
+ '\nMethod:', req.method, '\nURL:', req.url,
831
+ '\nBaseURL:', req.baseURL, '\nHeaders:', req.headers,
832
+ '\nParams:', req.params, '\nData:', req.data
833
+ );
834
+ this.log.warn(req);
835
+ return req; // must return request
836
+ });
837
+ axiosWS.interceptors.response.use(res => {
838
+ this.log.warn('+++INTERCEPTED HTTP RESPONSE:', res.status, res.statusText,
839
+ '\nHeaders:', res.headers,
840
+ '\nUrl:', res.url,
841
+ //'\nData:', res.data,
842
+ '\nLast Request:', res.request
843
+ );
844
+ //this.log.warn(res);
845
+ this.log('+++INTERCEPTED AFTER HTTP RESPONSE COOKIEJAR:');
846
+ if (cookieJar) { this.log(cookieJar); }// watch out for empty cookieJar
847
+ return res; // must return response
848
+ });
849
+ */
850
+ // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
851
+
852
+ // good description of PKCE
853
+ // https://www.authlete.com/developers/pkce/
854
+ // creake a PKCE code pair and save it
855
+ this.pkcePair = generatePKCEPair();
856
+ //this.log('PKCE pair:', pkcePair);
857
+
858
+
859
+ // Step 1: # get authentication details
860
+ // Recorded sequence step 1: https://id.virginmedia.com/rest/v40/session/start?protocol=oidc&rememberMe=true
861
+ // const GB_AUTH_OESP_URL = 'https://web-api-prod-obo.horizon.tv/oesp/v4/GB/eng/web';
862
+ // https://spark-prod-gb.gnp.cloud.virgintvgo.virginmedia.com/auth-service/v1/sso/authorization?code_challenge=aHsoE2kJlwA4qGOcx1OCH7i__1bBdV1l6yLOKUvW24U&language=en
863
+ let apiAuthorizationUrl = this.configsvc.authorizationService.URL + '/v1/sso/authorization?'
864
+ + 'code_challenge=' + this.pkcePair.code_challenge
865
+ + '&language=en';
866
+
867
+ this.log('Step 1 of 7: get authentication details');
868
+ if (this.config.debugLevel > 1) { this.log.warn('Step 1 of 7: get authentication details from',apiAuthorizationUrl); }
869
+ axiosWS.get(apiAuthorizationUrl)
870
+ .then(response => {
871
+ this.log('Step 1 of 7: response:',response.status, response.statusText);
872
+ this.log('Step 1 of 7: response.data',response.data);
873
+
874
+ // get the data we need for further steps
875
+ let auth = response.data;
876
+ let authState = auth.state;
877
+ let authAuthorizationUri = auth.authorizationUri;
878
+ let authValidtyToken = auth.validityToken;
879
+ this.log('Step 1 of 7: results: authState',authState);
880
+ this.log('Step 1 of 7: results: authAuthorizationUri',authAuthorizationUri);
881
+ this.log('Step 1 of 7: results: authValidtyToken',authValidtyToken);
882
+
883
+ // Step 2: # follow authorizationUri to get AUTH cookie (ULM-JSESSIONID)
884
+ this.log('Step 2 of 7: get AUTH cookie');
885
+ this.log.debug('Step 2 of 7: get AUTH cookie ULM-JSESSIONID from',authAuthorizationUri);
886
+ axiosWS.get(authAuthorizationUri, {
887
+ jar: cookieJar,
888
+ // unsure what minimum headers will here
889
+ headers: {
890
+ Accept: 'application/json, text/plain, */*'
891
+ //Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
892
+ }, })
893
+ .then(response => {
894
+ this.log('Step 2 of 7: response:',response.status, response.statusText);
895
+ this.log.warn('Step 2 of 7 response.data',response.data); // an html logon page
896
+
897
+ // Step 3: # login
898
+ this.log('Step 3 of 7: logging in with username %s', this.config.username);
899
+ currentSessionState = sessionState.LOGGING_IN;
900
+
901
+ // we want to POST to
902
+ // 'https://id.virginmedia.com/rest/v40/session/start?protocol=oidc&rememberMe=true';
903
+ // see https://auth0.com/intro-to-iam/what-is-openid-connect-oidc
904
+ const GB_AUTH_URL = 'https://id.virginmedia.com/rest/v40/session/start?protocol=oidc&rememberMe=true';
905
+ this.log.debug('Step 3 of 7: POST request will contain this data: {"username":"' + this.config.username + '","credential":"' + this.config.password + '"}');
906
+ axiosWS(GB_AUTH_URL,{
907
+ //axiosWS('https://id.virginmedia.com/rest/v40/session/start?protocol=oidc&rememberMe=true',{
908
+ jar: cookieJar,
909
+ // However, since v2.0, axios-cookie-jar will always ignore invalid cookies. See https://github.com/3846masa/axios-cookiejar-support/blob/main/MIGRATION.md
910
+ data: '{"username":"' + this.config.username + '","credential":"' + this.config.password + '"}',
911
+ method: "POST",
912
+ // minimum headers are "accept": "*/*", "content-type": "application/json; charset=UTF-8",
913
+ headers: {
914
+ "accept": "*/*", // mandatory
915
+ "content-type": "application/json; charset=UTF-8", // mandatory
916
+ },
917
+ maxRedirects: 0, // do not follow redirects
918
+ validateStatus: function (status) {
919
+ return ((status >= 200 && status < 300) || status == 302) ; // allow 302 redirect as OK. GB returns 200
920
+ },
921
+ })
922
+ .then(response => {
923
+ this.log('Step 3 of 7: response:',response.status, response.statusText);
924
+ this.log.warn('Step 3 of 7: response.headers:',response.headers);
925
+ // responds with a userId, this will need to be used somewhere...
926
+ this.log.warn('Step 3 of 7: response.data:',response.data); // { userId: 28786528, runtimeId: 79339515 }
927
+
928
+
929
+ var url = response.headers['x-redirect-location'] // must be lowercase
930
+ if (!url) { // robustness: fail if url missing
931
+ this.log.warn('getSessionGB: Step 3: x-redirect-location url empty!');
932
+ currentSessionState = sessionState.DISCONNECTED;
933
+ this.currentStatusFault = Characteristic.StatusFault.GENERAL_FAULT;
934
+ return false;
935
+ }
936
+ //location is h??=... if success
937
+ //location is https?? if not authorised
938
+ //location is https:... error=session_expired if session has expired
939
+ if (url.indexOf('authentication_error=true') > 0 ) { // >0 if found
940
+ //this.log.warn('Step 3 of 7: Unable to login: wrong credentials');
941
+ reject('Step 3 of 7: Unable to login: wrong credentials'); // reject the promise and return the error
942
+ } else if (url.indexOf('error=session_expired') > 0 ) { // >0 if found
943
+ //this.log.warn('Step 3 of 7: Unable to login: session expired');
944
+ cookieJar.removeAllCookies(); // remove all the locally cached cookies
945
+ reject('Step 3 of 7: Unable to login: session expired'); // reject the promise and return the error
946
+ } else {
947
+ this.log.debug('Step 3 of 7: login successful');
948
+
949
+ // Step 4: # follow redirect url
950
+ this.log('Step 4 of 7: follow redirect url');
951
+ axiosWS.get(url,{
952
+ jar: cookieJar,
953
+ maxRedirects: 0, // do not follow redirects
954
+ validateStatus: function (status) {
955
+ return ((status >= 200 && status < 300) || status == 302) ; // allow 302 redirect as OK
956
+ },
957
+ })
958
+ .then(response => {
959
+ this.log('Step 4 of 7: response:',response.status, response.statusText);
960
+ this.log.warn('Step 4 of 7: response.headers.location:',response.headers.location); // is https://www.telenet.be/nl/login_success_code=... if success
961
+ this.log.warn('Step 4 of 7: response.data:',response.data);
962
+ url = response.headers.location;
963
+ if (!url) { // robustness: fail if url missing
964
+ this.log.warn('getSessionGB: Step 4 of 7 location url empty!');
965
+ currentSessionState = sessionState.DISCONNECTED;
966
+ this.currentStatusFault = Characteristic.StatusFault.GENERAL_FAULT;
967
+ return false;
968
+ }
969
+
970
+ // look for login_success?code=
971
+ if (url.indexOf('login_success?code=') < 0 ) { // <0 if not found
972
+ //this.log.warn('Step 4 of 7: Unable to login: wrong credentials');
973
+ reject('Step 4 of 7: Unable to login: wrong credentials'); // reject the promise and return the error
974
+ } else if (url.indexOf('error=session_expired') > 0 ) {
975
+ //this.log.warn('Step 4 of 7: Unable to login: session expired');
976
+ cookieJar.removeAllCookies(); // remove all the locally cached cookies
977
+ reject('Step 4 of 7: Unable to login: session expired'); // reject the promise and return the error
978
+ } else {
979
+
980
+ // Step 5: # obtain authorizationCode
981
+ this.log('Step 5 of 7: extract authorizationCode');
982
+ /*
983
+ url = response.headers.location;
984
+ if (!url) { // robustness: fail if url missing
985
+ this.log.warn('getSessionGB: Step 5: location url empty!');
986
+ currentSessionState = sessionState.DISCONNECTED;
987
+ this.currentStatusFault = Characteristic.StatusFault.GENERAL_FAULT;
988
+ return false;
989
+ }
990
+ */
991
+
992
+ var codeMatches = url.match(/code=(?:[^&]+)/g)[0].split('=');
993
+ var authorizationCode = codeMatches[1];
994
+ if (codeMatches.length !== 2 ) { // length must be 2 if code found
995
+ this.log.warn('Step 5 of 7: Unable to extract authorizationCode');
996
+ } else {
997
+ this.log('Step 5 of 7: authorizationCode OK');
998
+ this.log.debug('Step 5 of 7: authorizationCode:',authorizationCode);
999
+
1000
+ // Step 6: # authorize again
1001
+ this.log('Step 6 of 7: post auth data with valid code');
1002
+ this.log.debug('Step 6 of 7: post auth data with valid code to',apiAuthorizationUrl);
1003
+ currentSessionState = sessionState.AUTHENTICATING;
1004
+ var payload = {'authorizationGrant':{
1005
+ 'authorizationCode':authorizationCode,
1006
+ 'validityToken':authValidtyToken,
1007
+ 'state':authState
1008
+ }};
1009
+ axiosWS.post(apiAuthorizationUrl, payload, {jar: cookieJar})
1010
+ .then(response => {
1011
+ this.log('Step 6 of 7: response:',response.status, response.statusText);
1012
+ this.log.debug('Step 6 of 7: response.data:',response.data);
1013
+
1014
+ auth = response.data;
1015
+ this.log.debug('Step 6 of 7: refreshToken:',auth.refreshToken);
1016
+
1017
+ // Step 7: # get OESP code
1018
+ this.log('Step 7 of 7: post refreshToken request');
1019
+ this.log.debug('Step 7 of 7: post refreshToken request to',apiAuthorizationUrl);
1020
+ payload = {'refreshToken':auth.refreshToken,'username':auth.username};
1021
+ // must resolve to
1022
+ // 'https://web-api-prod-obo.horizon.tv/oesp/v4/GB/eng/web/session';',
1023
+ var sessionUrl = GB_AUTH_OESP_URL + '/session';
1024
+ axiosWS.post(sessionUrl + "?token=true", payload, {jar: cookieJar})
1025
+ .then(response => {
1026
+ this.log('Step 7 of 7: response:',response.status, response.statusText);
1027
+ currentSessionState = sessionState.VERIFYING;
1028
+
1029
+ this.log.debug('Step 7 of 7: response.headers:',response.headers);
1030
+ this.log.debug('Step 7 of 7: response.data:',response.data);
1031
+ this.log.debug('Cookies for the session:',cookieJar.getCookies(sessionUrl));
1032
+ if (this.config.debugLevel > 2) {
1033
+ this.log('getSessionGB: response data (saved to this.session):');
1034
+ this.log(response.data);
1035
+ }
1036
+
1037
+ // get device data from the session
1038
+ this.session = response.data;
1039
+ // New APLSTB Apollo box on NL does not return username in during session logon, so store username from settings if missing
1040
+ if (this.session.username == '') { this.session.username = this.config.username; }
1041
+
1042
+ currentSessionState = sessionState.CONNECTED;
1043
+ this.currentStatusFault = Characteristic.StatusFault.NO_FAULT;
1044
+ this.log('Session created');
1045
+ resolve(this.session.householdId) // resolve the promise with the householdId
1046
+ })
1047
+ // Step 7 http errors
1048
+ .catch(error => {
1049
+ this.log.debug("Step 7 of 7: error:",error);
1050
+ reject("Step 7 of 7: Unable to get OESP token: " + error.response.status + ' ' + error.response.statusText); // reject the promise and return the error
1051
+ });
1052
+ })
1053
+ // Step 6 http errors
1054
+ .catch(error => {
1055
+ reject("Step 6 of 7: Unable to authorize with oauth code, http error: " + error.response.status + ' ' + error.response.statusText); // reject the promise and return the error
1056
+ });
1057
+ };
1058
+ };
1059
+ })
1060
+ // Step 4 http errors
1061
+ .catch(error => {
1062
+ this.log.debug("Step 4 of 7: error:",error);
1063
+ this.log.warn("Step 4 of 7: error:",error);
1064
+ reject("Step 4 of 7: Unable to oauth authorize: " + error.response.status + ' ' + error.response.statusText); // reject the promise and return the error
1065
+ });
1066
+ };
1067
+ })
1068
+ // Step 3 http errors
1069
+ .catch(error => {
1070
+ this.log.debug("Step 3 of 7: error:",error);
1071
+ this.log.warn("Step 3 of 7: error:",error);
1072
+ reject("Step 3 of 7: Unable to login: " + error.response.status + ' ' + error.response.statusText); // reject the promise and return the error
1073
+ });
1074
+ })
1075
+ // Step 2 http errors
1076
+ .catch(error => {
1077
+ this.log.debug("Step 2 of 7: error:",error);
1078
+ reject("Step 2 of 7: Could not get authorizationUri: " + error.response.status + ' ' + error.response.statusText); // reject the promise and return the error
1079
+ });
1080
+ })
1081
+ // Step 1 http errors
1082
+ .catch(error => {
1083
+ this.log.debug("Step 1 of 7: error:",error);
1084
+ reject("Step 1 of 7: Failed to create session - check your internet connection"); // reject the promise and return the error
1085
+ });
1086
+
1087
+ currentSessionState = sessionState.DISCONNECTED;
1088
+ this.currentStatusFault = Characteristic.StatusFault.GENERAL_FAULT;
1089
+ })
1090
+ }
1091
+
1092
+
1093
+
772
1094
  // get session ch, nl, ie, at
773
1095
  // using new auth method, as of 13.10.2022
774
1096
  async getSession() {
@@ -805,7 +1127,8 @@ class stbPlatform {
805
1127
 
806
1128
  const axiosConfig = {
807
1129
  method: 'POST',
808
- url: countryBaseUrlArray[this.config.country.toLowerCase()] + '/auth-service/v1/authorization',
1130
+ //url: countryBaseUrlArray[this.config.country.toLowerCase()] + '/auth-service/v1/authorization',
1131
+ url: this.configsvc.authorizationService.URL + '/v1/authorization',
809
1132
  headers: {
810
1133
  "accept": "*/*", // added 07.08.2023
811
1134
  "content-type": "application/json; charset=utf-8", // added 07.08.2023
@@ -926,7 +1249,8 @@ class stbPlatform {
926
1249
 
927
1250
 
928
1251
  // Step 1: # get authentication details
929
- let apiAuthorizationUrl = countryBaseUrlArray[this.config.country.toLowerCase()] + '/auth-service/v1/sso/authorization';
1252
+ //let apiAuthorizationUrl = countryBaseUrlArray[this.config.country.toLowerCase()] + '/auth-service/v1/sso/authorization';
1253
+ let apiAuthorizationUrl = this.configsvc.authorizationService.URL + '/v1/sso/authorization';
930
1254
  this.log('Step 1 of 6: get authentication details');
931
1255
  if (this.config.debugLevel > 1) { this.log.warn('Step 1 of 6: get authentication details from',apiAuthorizationUrl); }
932
1256
  axiosWS.get(apiAuthorizationUrl)
@@ -1437,7 +1761,8 @@ class stbPlatform {
1437
1761
  url = url + '&sort=channelNumber' // sort
1438
1762
  */
1439
1763
  //url = 'https://prod.spark.sunrisetv.ch/eng/web/linear-service/v2/channels?cityId=401&language=en&productClass=Orion-DASH'
1440
- let url = countryBaseUrlArray[this.config.country.toLowerCase()] + '/eng/web/linear-service/v2/channels';
1764
+ //let url = countryBaseUrlArray[this.config.country.toLowerCase()] + '/eng/web/linear-service/v2/channels';
1765
+ let url = this.configsvc.linearService.URL + '/v2/channels';
1441
1766
  url = url + '?cityId=' + this.customer.cityId; //+ this.customer.cityId // cityId needed to get user-specific list
1442
1767
  url = url + '&language=en'; // language
1443
1768
  url = url + '&productClass=Orion-DASH'; // productClass, must be Orion-DASH
@@ -1542,6 +1867,45 @@ class stbPlatform {
1542
1867
  })
1543
1868
  }
1544
1869
 
1870
+
1871
+ // get the config (containing all endpoints) for the country
1872
+ // added 14.01.2024
1873
+ async getConfig(countryCode, callback) {
1874
+ return new Promise((resolve, reject) => {
1875
+ this.log("Retrieving config for countryCode %s", countryCode);
1876
+
1877
+ // https://spark-prod-ch.gnp.cloud.sunrisetv.ch/ch/en/config-service/conf/web/backoffice.json
1878
+ // https://prod.spark.upctv.ch/ch/en/config-service/conf/web/backoffice.json
1879
+ const ctryCode = countryCode.substr(0, 2);
1880
+
1881
+ //const url = 'https://spark-prod-ch.gnp.cloud.sunrisetv.ch/ch/en/config-service/conf/web/backoffice.json'
1882
+ // use countryCode.substr(1, 2) for backwards-compatibility to allow be-fr to map to be
1883
+ const url=countryBaseUrlArray[ctryCode] + '/' + ctryCode + '/en/config-service/conf/web/backoffice.json';
1884
+ if (this.config.debugLevel > 0) { this.log.warn('getConfig: GET %s', url); }
1885
+ axiosWS.get(url)
1886
+ .then(response => {
1887
+ if (this.config.debugLevel > 0) { this.log.warn('getConfig: response: %s %s', response.status, response.statusText); }
1888
+ if (this.config.debugLevel > 2) {
1889
+ this.log.warn('getConfig: response data (saved to this.configsvc):');
1890
+ this.log.warn(response.data);
1891
+ }
1892
+ this.configsvc = response.data; // store the entire config data for future use in this.configsvc
1893
+ resolve(this.configsvc); // resolve the promise with the configsvc object
1894
+ })
1895
+ .catch(error => {
1896
+ let errReason;
1897
+ errReason = 'Could not get config data for ' + countryCode + ' - check your internet connection'
1898
+ if (error.isAxiosError) {
1899
+ errReason = error.code + ': ' + (error.hostname || '');
1900
+ // if no connection then set session to disconnected to force a session reconnect
1901
+ if (error.code == 'ENOTFOUND') { currentSessionState = sessionState.DISCONNECTED; }
1902
+ }
1903
+ this.log.debug(`getConfig error:`, error);
1904
+ reject(errReason);
1905
+ });
1906
+ })
1907
+ }
1908
+
1545
1909
 
1546
1910
 
1547
1911
 
@@ -1554,7 +1918,10 @@ class stbPlatform {
1554
1918
 
1555
1919
  //const url = personalizationServiceUrlArray[this.config.country.toLowerCase()].replace("{householdId}", this.session.householdId) + '/' + requestType;
1556
1920
  //const url='https://prod.spark.sunrisetv.ch/eng/web/personalization-service/v1/customer/' + householdId + '?with=profiles%2Cdevices';
1557
- const url=countryBaseUrlArray[this.config.country.toLowerCase()] + '/eng/web/personalization-service/v1/customer/' + householdId + '?with=profiles%2Cdevices';
1921
+ // https://spark-prod-ch.gnp.cloud.sunrisetv.ch/eng/web/personalization-service
1922
+ //const url=countryBaseUrlArray[this.config.country.toLowerCase()] + '/eng/web/personalization-service/v1/customer/' + householdId + '?with=profiles%2Cdevices';
1923
+ const url = this.configsvc.personalizationService.URL + '/v1/customer/' + householdId + '?with=profiles%2Cdevices';
1924
+
1558
1925
  // headers are in the web client
1559
1926
  let config={}
1560
1927
  if (this.config.country.toLowerCase() == 'gb'){
@@ -1661,7 +2028,9 @@ class stbPlatform {
1661
2028
  async setPersonalizationDataForDevice(deviceId, deviceSettings, callback) {
1662
2029
  if (this.config.debugLevel > 0) { this.log.warn('setPersonalizationDataForDevice: deviceSettings:', deviceSettings); }
1663
2030
  // https://prod.spark.sunrisetv.ch/eng/web/personalization-service/v1/customer/1012345_ch/devices/3C36E4-EOSSTB-003656123456
1664
- const url = countryBaseUrlArray[this.config.country.toLowerCase()] + '/eng/web/personalization-service/v1/customer/' + this.session.householdId + '/devices/' + deviceId;
2031
+ //const url = countryBaseUrlArray[this.config.country.toLowerCase()] + '/eng/web/personalization-service/v1/customer/' + this.session.householdId + '/devices/' + deviceId;
2032
+ const url = this.configsvc.personalizationService.URL + '/v1/customer/' + this.session.householdId + '/devices/' + deviceId;
2033
+
1665
2034
  const data = {"settings": deviceSettings};
1666
2035
  // gb needs x-cus, x-oesp-token and x-oesp-username
1667
2036
  let config={}
@@ -1702,7 +2071,8 @@ class stbPlatform {
1702
2071
 
1703
2072
  //const url = personalizationServiceUrlArray[this.config.country.toLowerCase()].replace("{householdId}", this.session.householdId) + '/' + requestType;
1704
2073
  //const url='https://prod.spark.sunrisetv.ch/eng/web/purchase-service/v2/customers/107xxxx_ch/entitlements?enableDaypass=true'
1705
- const url=countryBaseUrlArray[this.config.country.toLowerCase()] + '/eng/web/purchase-service/v2/customers/' + householdId + '/entitlements?enableDaypass=true';
2074
+ //const url=countryBaseUrlArray[this.config.country.toLowerCase()] + '/eng/web/purchase-service/v2/customers/' + householdId + '/entitlements?enableDaypass=true';
2075
+ const url = this.configsvc.purchaseService.URL + '/v2/customers/' + householdId + '/entitlements?enableDaypass=true';
1706
2076
  //const config = {headers: {"x-cus": this.session.householdId, "x-oesp-token": this.session.accessToken, "x-oesp-username": this.session.username}};
1707
2077
  const config = {headers: {
1708
2078
  "x-cus": householdId,
@@ -1770,7 +2140,8 @@ class stbPlatform {
1770
2140
  // https://prod.spark.sunrisetv.ch/eng/web/recording-service/customers/107xxxx_ch/recordings?isAdult=false&offset=0&limit=100&sort=time&sortOrder=desc&profileId=4eb38207-d869-4367-8973-9467a42cad74&language=en
1771
2141
  // const url = countryBaseUrlArray[this.config.country.toLowerCase()] + '/' + 'networkdvrrecordings?isAdult=false&plannedOnly=false&range=1-20'; // works
1772
2142
  // parameter plannedOnly=false did not work
1773
- const url = countryBaseUrlArray[this.config.country.toLowerCase()] + '/eng/web/recording-service/customers/' + householdId + '/recordings/state'; // limit to 20 recordings for performance
2143
+ //const url = countryBaseUrlArray[this.config.country.toLowerCase()] + '/eng/web/recording-service/customers/' + householdId + '/recordings/state'; // limit to 20 recordings for performance
2144
+ const url = this.configsvc.recordingService.URL + '/customers/' + householdId + '/recordings/state'; // limit to 20 recordings for performance
1774
2145
  if (this.config.debugLevel > 0) { this.log.warn('getRecordingState: GET %s', url); }
1775
2146
  axiosWS.get(url, config)
1776
2147
  .then(response => {
@@ -2004,7 +2375,8 @@ class stbPlatform {
2004
2375
  let url
2005
2376
  //url=countryBaseUrlArray[this.config.country.toLowerCase()] + '/eng/web/purchase-service/v2/customers/' + householdId + '/entitlements?enableDaypass=true';
2006
2377
  //url='https://web-api-prod-obo.horizon.tv/oesp/v4/CH/eng/web/eng/session'
2007
- url='https://prod.spark.upctv.ch/ch/en/session-service'
2378
+ //url='https://prod.spark.upctv.ch/ch/en/session-service'
2379
+ url = this.configsvc.sessionService.URL;
2008
2380
  const config = {headers: {
2009
2381
  "x-cus": householdId,
2010
2382
  "x-oesp-token": this.session.accessToken,
@@ -2070,11 +2442,11 @@ class stbPlatform {
2070
2442
 
2071
2443
  const mqttAxiosConfig = {
2072
2444
  method: 'GET',
2073
- //url: countryBaseUrlArray[this.config.country.toLowerCase()] + '/tokens/jwt', prior to October 2022
2074
2445
  // examples of auth-service/v1/mqtt/token urls:
2075
2446
  // https://prod.spark.ziggogo.tv/auth-service/v1/mqtt/token
2076
2447
  // https://prod.spark.sunrisetv.ch/auth-service/v1/mqtt/token
2077
- url: countryBaseUrlArray[this.config.country.toLowerCase()] + '/auth-service/v1/mqtt/token', // new from October 2022
2448
+ //url: countryBaseUrlArray[this.config.country.toLowerCase()] + '/auth-service/v1/mqtt/token', // new from October 2022
2449
+ url: this.configsvc.authorizationService.URL + '/v1/mqtt/token',
2078
2450
  headers: {
2079
2451
  'X-OESP-Token': accessToken,
2080
2452
  'X-OESP-Username': oespUsername,
@@ -2088,8 +2460,6 @@ class stbPlatform {
2088
2460
  }
2089
2461
  mqttUsername = householdId; // used in sendKey to ensure that mqtt is connected
2090
2462
  resolve(response.data.token); // resolve with the token
2091
- //this.startMqttClient(this, householdId, response.data.token); // this starts the mqtt session
2092
-
2093
2463
  })
2094
2464
  .catch(error => {
2095
2465
  this.log.debug('getMqttToken error details:', error);
@@ -2105,7 +2475,7 @@ class stbPlatform {
2105
2475
  // a sync procedure, no promise returned
2106
2476
  // https://github.com/mqttjs/MQTT.js#readme
2107
2477
  // http://www.steves-internet-guide.com/mqtt-publish-subscribe/
2108
- startMqttClient(parent, mqttUsername, mqttPassword) {
2478
+ statMqttClient(parent, mqttUsername, mqttPassword) {
2109
2479
  return new Promise((resolve, reject) => {
2110
2480
  try {
2111
2481
  if (this.config.debugLevel > 0) {
@@ -2118,28 +2488,45 @@ class stbPlatform {
2118
2488
 
2119
2489
 
2120
2490
  // create mqtt client instance and connect to the mqttUrl
2121
- const mqttUrl = mqttUrlArray[this.config.country.toLowerCase()];
2491
+ //const mqttBroker = mqttUrlArray[this.config.country.toLowerCase()];
2492
+ const mqttBrokerUrl = this.configsvc.mqttBroker.URL;
2122
2493
  if (this.config.debugLevel > 0) {
2123
- this.log.warn('startMqttClient: mqttUrl:', mqttUrl );
2494
+ this.log.warn('statMqttClient: mqttBrokerUrl:', mqttBrokerUrl );
2124
2495
  }
2125
2496
  if (this.config.debugLevel > 0) {
2126
- this.log.warn('startMqttClient: Creating mqttClient object with username %s, password %s', mqttUsername ,mqttPassword );
2497
+ this.log.warn('statMqttClient: Creating mqttClient with username %s, password %s', mqttUsername ,mqttPassword );
2127
2498
  }
2128
2499
 
2129
- // make a new mqttClientId on every session start, much robuster, then connect
2500
+ // make a new mqttClientId on every session start (much robuster), then connect
2130
2501
  //mqttClientId = makeId(32);
2131
2502
  mqttClientId = makeFormattedId(32);
2132
- //mqttClientId = makeId(32);
2133
2503
 
2504
+ // from 24 Jan 2024 we need to set the sub protocols mqtt, mqttv3.1, mqttv3.11 to connect
2505
+ // the required header looks like this:
2506
+ // "sec-websocket-protocol": "mqtt, mqttv3.1, mqttv3.11",
2507
+ // make a new custom websocket so we can ensure the correct mqtt protocols are used in the headers
2508
+ // see https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketaddress-protocols-options
2509
+ const createCustomWebsocket = (url, websocketSubProtocols, options) => {
2510
+ //this.log.warn('statMqttClient: createCustomWebsocket: ', websocketSubProtocols[0] );
2511
+ const subProtocols = [
2512
+ 'mqtt',
2513
+ 'mqttv3.1',
2514
+ 'mqttv3.11'
2515
+ ];
2516
+ //this.log.warn('statMqttClient: createCustomWebsocket: about to return' );
2517
+ return new WebSocket(url, subProtocols);
2518
+ };
2519
+
2134
2520
  // https://github.com/mqttjs/MQTT.js#connect
2135
- mqttClient = mqtt.connect(mqttUrl, {
2136
- connectTimeout: 10 * 1000, // 10s
2521
+ mqttClient = mqtt.connect(mqttBrokerUrl, {
2522
+ createWebsocket: createCustomWebsocket,
2137
2523
  clientId: mqttClientId,
2524
+ connectTimeout: 10 * 1000, // 10s
2138
2525
  username: mqttUsername,
2139
2526
  password: mqttPassword
2140
2527
  });
2141
2528
  if (this.config.debugLevel > 0) {
2142
- this.log.warn('startMqttClient: mqttUrl connect request sent using mqttClientId %s',mqttClientId );
2529
+ this.log.warn('statMqttClient: mqttBroker connect request sent using mqttClientId %s',mqttClientId );
2143
2530
  }
2144
2531
 
2145
2532
  //mqttClient.setMaxListeners(20); // default is 10 sometimes causes issues when the listeners reach 11
@@ -2546,20 +2933,21 @@ class stbPlatform {
2546
2933
 
2547
2934
 
2548
2935
  if (this.config.debugLevel > 0) {
2549
- this.log.warn("mqttClient: end of code block");
2936
+ this.log.warn("statMqttClient: end of code block");
2550
2937
  }
2551
2938
  resolve(mqttClient.connected); // return the promise with the connected state
2552
2939
 
2553
2940
  } catch (err) {
2941
+ this.log.error(err);
2554
2942
  reject('Cannot connect to mqtt broker', err); // reject the promise
2555
2943
  }
2556
2944
 
2557
2945
  })
2558
- } // end of startMqttClient
2946
+ } // end of statMqttClient
2559
2947
 
2560
2948
 
2561
- // end the mqtt client cleanly
2562
- endMqttClient() {
2949
+ // end the mqtt session cleanly
2950
+ endMqttSession() {
2563
2951
  return new Promise((resolve, reject) => {
2564
2952
  if (this.config.debugLevel > -1) {
2565
2953
  this.log('Shutting down mqttClient...');
@@ -4356,11 +4744,11 @@ class stbDevice {
4356
4744
  this.log("%s: Refreshing most watched channels for profile '%s'", this.name, (profile || {}).name);
4357
4745
 
4358
4746
  // https://prod.spark.sunrisetv.ch/eng/web/linear-service/v1/mostWatchedChannels?cityId=401&productClass=Orion-DASH"
4359
- let url = countryBaseUrlArray[this.config.country.toLowerCase()] + '/eng/web/linear-service/v1/mostWatchedChannels';
4747
+ //let url = countryBaseUrlArray[this.config.country.toLowerCase()] + '/eng/web/linear-service/v1/mostWatchedChannels';
4748
+ let url = this.platform.configsvc.linearService.URL + '/v1/mostWatchedChannels';
4360
4749
  // add url standard parameters
4361
4750
  url = url + '?cityId=' + this.customer.cityId; //+ this.customer.cityId // cityId needed to get user-specific list
4362
4751
  url = url + '&productClass=Orion-DASH'; // productClass, must be Orion-DASH
4363
- if (this.config.debugLevel > 2) { this.log.warn('getMostWatchedChannels: loading from',url); }
4364
4752
 
4365
4753
  const config = {headers: {
4366
4754
  "x-oesp-username": this.platform.session.username, // not sure if needed
package/package.json CHANGED
@@ -3,16 +3,17 @@
3
3
  "displayName": "Homebridge EOSSTB",
4
4
  "description": "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.16",
6
+ "version": "2.3.0",
7
7
  "platformname": "eosstb",
8
8
  "dependencies": {
9
9
  "axios-cookiejar-support": "^5.0.0",
10
- "axios": "^1.6.5",
10
+ "axios": "^1.6.6",
11
11
  "debug": "^4.3.4",
12
- "mqtt": "^5.3.4",
12
+ "mqtt": "^5.3.5",
13
13
  "qs": "^6.11.2",
14
14
  "semver": "^7.5.4",
15
- "tough-cookie": "^4.1.3"
15
+ "tough-cookie": "^4.1.3",
16
+ "ws": "^8.16.0"
16
17
  },
17
18
  "deprecated": false,
18
19
  "engines": {