homebridge-eosstb 2.2.15 → 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
@@ -9,6 +9,23 @@ Please restart Homebridge after every plugin update.
9
9
  * Add ability to log and read current program name
10
10
 
11
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
+
25
+ ## 2.2.16 (2024-01-16)
26
+ * Removed AZ, CZ, DE, HU, RO from config.json and Readme. These countries no longer offer UPC TV.
27
+
28
+
12
29
  ## 2.2.15 (2024-01-14)
13
30
  * Fixed issue with MQTT connection failure in CH due to change of MQTT endpoint
14
31
  * Bumped dependency "axios-cookiejar-support": "^5.0.0"
package/README.md CHANGED
@@ -47,11 +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
- | AT | [Magenta](https://www.magenta.at/) | [Magenta TV](https://www.magentatv.at/de.html) | [Entertain Box 4K](https://www.magenta.at/entertain-box) | _Testers Wanted_ |
52
- | DE | [Vodafone DE](https://zuhauseplus.vodafone.de/digital-fernsehen/) | [Horizon Go](https://www.horizon.tv/de_de.html) | [GigaTV Cable Box](https://zuhauseplus.vodafone.de/digital-fernsehen/tv-endgeraete/) | _Testers Wanted_ |
53
- | PL | [UPC PL](https://www.upc.pl/) | [UPC TV GO](https://www.upctv.pl/pl/home) | Horizon decoder | _Testers Wanted_ |
54
- | SK | [UPC Broadband Slovakia](https://www.upc.sk/) | [Horizon Go](https://www.horizon.tv/sk_sk.html) | Horizon 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_ |
55
53
 
56
54
 
57
55
  If you subscribe to a TV service from one of these countries, you are lucky, this plugin will work for you.
@@ -72,7 +70,7 @@ In January 2023, an ARRIS VIP5002W appeared, which identifies itself as an APLST
72
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.
73
71
 
74
72
  ## Requirements
75
- * 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.
76
74
  * [Homebridge](https://homebridge.io/) v1.1.116 (or later). Developed on Homebridge 1.1.116....1.7.0, earlier versions not tested.
77
75
  * A TV subscription from one of the supported countries and TV providers.
78
76
  * An online account for viewing TV in the web app (often part of your TV package), see the table above.
@@ -115,7 +113,7 @@ This plugin is not provided by Magenta or Telenet or Sunrise or Virgin Media or
115
113
 
116
114
  * **Fully Configurable**: A large amount of configuration items exist to allow you to configure your plugin the way you want.
117
115
 
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 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.
119
117
 
120
118
 
121
119
 
@@ -173,7 +171,7 @@ The following keys are supported by in the **Apple TV Remote** in the Control Ce
173
171
  | Volume Up | volUpCommand | - |
174
172
  | Volume Down | volDownCommand | 3 clicks = mute |
175
173
 
176
- 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.
177
175
 
178
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).
179
177
 
@@ -208,13 +206,13 @@ Services used in this set-top box accessory are:
208
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.
209
207
 
210
208
  ### Media State (Play/Pause) Limitations
211
- 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.
212
210
 
213
211
  ### Recording State Limitations
214
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.
215
213
 
216
214
  ### Closed Captions Limitations
217
- 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.
218
216
 
219
217
  ## Configuration
220
218
  Add a new platform to the platforms section of your homebridge `config.json`.
@@ -23,19 +23,14 @@
23
23
  "default": "ch",
24
24
  "required": true,
25
25
  "oneOf": [
26
- { "title": "AT: Magenta TV", "enum": ["at"] },
27
26
  { "title": "BE-FR: Telenet TV", "enum": ["be-fr"] },
28
27
  { "title": "BE-NL: Telenet TV", "enum": ["be-nl"] },
29
28
  { "title": "CH: Sunrise TV", "enum": ["ch"] },
30
- { "title": "CZ", "enum": ["cz"] },
31
- { "title": "DE", "enum": ["de"] },
32
29
  { "title": "GB: Virgin Media TV 360", "enum": ["gb"] },
33
- { "title": "HU", "enum": ["hu"] },
34
30
  { "title": "IE. Virgin Media TV 360", "enum": ["ie"] },
35
31
  { "title": "NL: Ziggo TV", "enum": ["nl"] },
36
- { "title": "PL", "enum": ["pl"] },
37
- { "title": "SK", "enum": ["sk"] },
38
- { "title": "RO", "enum": ["ro"] }
32
+ { "title": "PL: UPC TV GO", "enum": ["pl"] },
33
+ { "title": "SK: UPC TV", "enum": ["sk"] }
39
34
  ]
40
35
  },
41
36
  "username": {
@@ -52,7 +47,27 @@
52
47
  "placeholder": "yourTvProviderPassword",
53
48
  "required": true
54
49
  },
55
-
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
+
56
71
  "doublePressTime": {
57
72
  "title": "Double-Press Time",
58
73
  "type": "integer",
@@ -846,8 +861,10 @@
846
861
  "type": "flex",
847
862
  "flex-flow": "column",
848
863
  "items": [
849
- "country"
850
- ]
864
+ "country",
865
+ "authmethod",
866
+ "watchdogDisabled"
867
+ ]
851
868
  },
852
869
  {
853
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,59 +56,34 @@ 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
- 'at': 'https://prod.spark.magentatv.at',
55
- 'be-fr': 'https://prod.spark.telenet.tv',
56
- 'be-nl': 'https://prod.spark.telenet.tv',
57
- 'ch': 'https://prod.spark.sunrisetv.ch',
58
- 'de': 'https://prod.spark.upctv.de',
59
- 'gb': 'https://prod.spark.virginmedia.com',
60
- 'ie': 'https://prod.spark.virginmediatv.ie',
61
- 'nl': 'https://prod.spark.ziggogo.tv',
62
- 'pl': 'https://prod.spark.upctv.pl',
63
- 'sk': 'https://prod.spark.upctv.sk',
64
- // old endpoints:
65
- //'at': 'https://prod.oesp.magentatv.at/oesp/v4/AT/deu/web', // v3 and v4 works old
66
- //'ch': 'https://web-api-prod-obo.horizon.tv/oesp/v4/CH/eng/web', // v2, v3 and v4 work old
67
- //'de': 'https://web-api-pepper.horizon.tv/oesp/v4/DE/deu/web', // v2, v3 and v4 work
68
- //'gb': 'https://web-api-prod-obo.horizon.tv/oesp/v4/GB/eng/web',
69
- //'nl': 'https://web-api-prod-obo.horizon.tv/oesp/v4/NL/nld/web', // old
70
- //'pl': 'https://web-api-pepper.horizon.tv/oesp/v4/PL/pol/web', // v2, v3 and v4 work
71
- //'ie': 'https://prod.oesp.virginmediatv.ie/oesp/v4/IE/eng/web/', // old
72
- //'sk': 'https://web-api-pepper.horizon.tv/oesp/v4/SK/slk/web', // v2, v3 and v4 work
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
73
73
  };
74
74
 
75
75
  // mqtt endpoints varies by country, unchanged after backend change on 13.10.2022
76
+ /*
76
77
  const mqttUrlArray = {
77
- 'at': 'wss://obomsg.prod.at.horizon.tv/mqtt',
78
78
  'be-fr': 'wss://obomsg.prod.be.horizon.tv/mqtt',
79
79
  'be-nl': 'wss://obomsg.prod.be.horizon.tv/mqtt',
80
- //'ch': 'wss://obomsg.prod.ch.horizon.tv/mqtt',
81
80
  'ch': 'wss://messagebroker-prod-ch.gnp.cloud.dmdsdp.com/mqtt', // from 11.02.2024
82
- 'de': 'wss://obomsg.prod.de.horizon.tv/mqtt',
83
81
  'gb': 'wss://obomsg.prod.gb.horizon.tv/mqtt',
84
82
  'ie': 'wss://obomsg.prod.ie.horizon.tv/mqtt',
85
83
  'nl': 'wss://obomsg.prod.nl.horizon.tv/mqtt',
86
84
  'pl': 'wss://obomsg.prod.pl.horizon.tv/mqtt',
87
85
  'sk': 'wss://obomsg.prod.sk.horizon.tv/mqtt'
88
- };
89
-
90
- // profile url endpoints varies by country
91
- // https://prod.spark.sunrisetv.ch/deu/web/personalization-service/v1/customer/{household_id}/devices
92
- // without terminating /
93
- // no longer needed in v2
94
- /*
95
- const personalizationServiceUrlArray = {
96
- 'at': 'https://prod.spark.magentatv.at/deu/web/personalization-service/v1/customer/{householdId}',
97
- 'be-fr': 'https://prod.spark.telenettv.be/fr/web/personalization-service/v1/customer/{householdId}',
98
- 'be-nl': 'https://prod.spark.telenettv.be/nld/web/personalization-service/v1/customer/{householdId}',
99
- 'ch': 'https://prod.spark.sunrisetv.ch/eng/web/personalization-service/v1/customer/{householdId}',
100
- 'de': '',
101
- 'gb': 'https://prod.spark.virginmedia.com/eng/web/personalization-service/v1/customer/{householdId}',
102
- 'ie': 'https://prod.spark.virginmediatv.ie/eng/web/personalization-service/v1/customer/{householdId}',
103
- 'nl': 'https://prod.spark.ziggogo.tv/nld/web/personalization-service/v1/customer/{householdId}',
104
- 'pl': 'https://prod.spark.unknown.pl/pol/web/personalization-service/v1/customer/{householdId}'
105
- };
106
- */
86
+ };*/
107
87
 
108
88
 
109
89
  // openid logon url used in Telenet.be Belgium for be-nl and be-fr sessions
@@ -275,6 +255,17 @@ async function waitprom(ms) {
275
255
  }
276
256
 
277
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
+
278
269
 
279
270
  // ++++++++++++++++++++++++++++++++++++++++++++
280
271
  // config end
@@ -346,7 +337,11 @@ class stbPlatform {
346
337
  setTimeout(this.sessionWatchdog.bind(this),500); // wait 500ms then call this.sessionWatchdog
347
338
 
348
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
349
- 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
+ }
350
345
 
351
346
  // check for a channel list update every MASTER_CHANNEL_LIST_REFRESH_CHECK_INTERVAL_S seconds
352
347
  this.checkChannelListInterval = setInterval(() => {
@@ -386,7 +381,7 @@ class stbPlatform {
386
381
  debug('stbPlatform:apievent :: shutdown')
387
382
  if (this.config.debugLevel > 2) { this.log.warn('API event: shutdown'); }
388
383
  isShuttingDown = true;
389
- this.endMqttClient()
384
+ this.endMqttSession()
390
385
  .then(() => {
391
386
  this.log('Goodbye');
392
387
  }
@@ -533,72 +528,80 @@ class stbPlatform {
533
528
  if (this.config.debugLevel > 2) { this.log.warn('%s: Attempting to create session', watchdogInstance); }
534
529
 
535
530
  // asnyc startup sequence with chain of promises
536
- this.log.debug('%s: ++++ step 1: calling createSession', watchdogInstance)
537
- errorTitle = 'Failed to create session';
538
- debug(debugPrefix + 'calling createSession')
539
- 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
+ })
540
543
  .then((sessionHouseholdId) => {
541
- this.log.debug('%s: ++++++ step 2: session was created, connected to sessionHouseholdId %s', watchdogInstance, sessionHouseholdId)
542
- 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)
543
546
  this.log('Discovering platform...');
544
547
  errorTitle = 'Failed to discover platform';
545
548
  debug(debugPrefix + 'calling getPersonalizationData')
546
549
  return this.getPersonalizationData(this.session.householdId) // returns customer object, with devices and profiles, stores object in this.customer
547
550
  })
548
551
  .then((objCustomer) => {
549
- this.log.debug('%s: ++++++ step 3: personalization data was retrieved, customerId %s customerStatus %s', watchdogInstance, objCustomer.customerId, objCustomer.customerStatus)
550
- 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)
551
554
  debug(debugPrefix + 'calling getEntitlements')
552
555
  return this.getEntitlements(this.customer.customerId) // returns customer object
553
556
  })
554
557
  .then((objEntitlements) => {
555
- this.log.debug('%s: ++++++ step 4: entitlements data was retrieved, objEntitlements.token %s', watchdogInstance, objEntitlements.token)
556
- 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)
557
560
  debug(debugPrefix + 'calling refreshMasterChannelList')
558
561
  return this.refreshMasterChannelList() // returns entitlements object
559
562
  })
560
563
  .then((objChannels) => {
561
- 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)
562
565
  // Recording needs entitlements of PVR or LOCALDVR
563
566
  const pvrFeatureFound = this.entitlements.features.find(feature => (feature === 'PVR' || feature === 'LOCALDVR'));
564
- this.log.debug('%s: ++++++ step 5: foundPvrEntitlement %s', watchdogInstance, pvrFeatureFound);
567
+ this.log.debug('%s: ++++++ step 6: foundPvrEntitlement %s', watchdogInstance, pvrFeatureFound);
565
568
  if (pvrFeatureFound) {
566
- 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)
567
570
  this.getRecordingState(this.session.householdId) // returns true when successful
568
571
  }
569
572
  return true
570
573
  })
571
574
  .then((objRecordingStateFound) => {
572
- 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)
573
576
  // Recording needs entitlements of PVR or LOCALDVR
574
577
  const pvrFeatureFound = this.entitlements.features.find(feature => (feature === 'PVR' || feature === 'LOCALDVR'));
575
- this.log.debug('%s: ++++++ step 6: foundPvrEntitlement %s', watchdogInstance, pvrFeatureFound);
578
+ this.log.debug('%s: ++++++ step 7: foundPvrEntitlement %s', watchdogInstance, pvrFeatureFound);
576
579
  if (pvrFeatureFound) {
577
- 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)
578
581
  this.getRecordingBookings(this.session.householdId) // returns true when successful
579
582
  }
580
583
  return true
581
584
  })
582
585
  .then((objRecordingBookingsFound) => {
583
- this.log.debug('%s: ++++++ step 7: recording bookings data was retrieved, objRecordingBookingsFound: %s', watchdogInstance, objRecordingBookingsFound)
584
- 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)
585
588
  errorTitle = 'Failed to discover devices';
586
589
  debug(debugPrefix + 'calling discoverDevices')
587
590
  return this.discoverDevices() // returns stbDevices object
588
591
  })
589
592
  .then((objStbDevices) => {
590
593
  this.log('Discovery completed');
591
- this.log.debug('%s: ++++++ step 8: devices found:', watchdogInstance, this.devices.length)
592
- 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)
593
596
  errorTitle = 'Failed to start mqtt session';
594
597
  debug(debugPrefix + 'calling getMqttToken')
595
598
  return this.getMqttToken(this.session.username, this.session.accessToken, this.session.householdId);
596
599
  })
597
600
  .then((mqttToken) => {
598
- this.log.debug('%s: ++++++ step 9: getMqttToken token was retrieved, token %s', watchdogInstance, mqttToken)
599
- this.log.debug('%s: ++++++ step 9: start mqtt client', watchdogInstance)
600
- debug(debugPrefix + 'calling startMqttClient')
601
- 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
602
605
  })
603
606
  .catch(errorReason => {
604
607
  // log any errors and set the currentSessionState
@@ -722,7 +725,8 @@ class stbPlatform {
722
725
  const axiosConfig = {
723
726
  method: 'POST',
724
727
  // https://prod.spark.sunrisetv.ch/auth-service/v1/authorization/refresh
725
- 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',
726
730
  headers: {
727
731
  "accept": "*/*", // mandatory
728
732
  "content-type": "application/json; charset=UTF-8", // mandatory
@@ -782,18 +786,24 @@ class stbPlatform {
782
786
  async createSession(country) {
783
787
  return new Promise((resolve, reject) => {
784
788
  this.currentStatusFault = Characteristic.StatusFault.NO_FAULT;
785
- switch(country) {
786
- 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':
787
797
  this.getSessionBE()
788
798
  .then((getSessionResponse) => { resolve(getSessionResponse); }) // return the getSessionResponse for the promise
789
799
  .catch(error => { reject(error); }); // on any error, reject the promise and pass back the error
790
800
  break;
791
- case 'gb':
801
+ case 'gb': case 'C':
792
802
  this.getSessionGB()
793
803
  .then((getSessionResponse) => { resolve(getSessionResponse); }) // return the getSessionResponse for the promise
794
804
  .catch(error => { reject(error); }); // on any error, reject the promise and pass back the error
795
805
  break;
796
- default: // ch, nl, ie, at
806
+ default: // ch, nl, ie, at, method A
797
807
  this.getSession()
798
808
  .then((getSessionResponse) => { resolve(getSessionResponse); }) // resolve with the getSessionResponse for the promise
799
809
  .catch(error => { reject(error); }); // on any error, reject the promise and pass back the error
@@ -801,6 +811,286 @@ class stbPlatform {
801
811
  })
802
812
  }
803
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
+
804
1094
  // get session ch, nl, ie, at
805
1095
  // using new auth method, as of 13.10.2022
806
1096
  async getSession() {
@@ -837,7 +1127,8 @@ class stbPlatform {
837
1127
 
838
1128
  const axiosConfig = {
839
1129
  method: 'POST',
840
- 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',
841
1132
  headers: {
842
1133
  "accept": "*/*", // added 07.08.2023
843
1134
  "content-type": "application/json; charset=utf-8", // added 07.08.2023
@@ -958,7 +1249,8 @@ class stbPlatform {
958
1249
 
959
1250
 
960
1251
  // Step 1: # get authentication details
961
- 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';
962
1254
  this.log('Step 1 of 6: get authentication details');
963
1255
  if (this.config.debugLevel > 1) { this.log.warn('Step 1 of 6: get authentication details from',apiAuthorizationUrl); }
964
1256
  axiosWS.get(apiAuthorizationUrl)
@@ -1469,7 +1761,8 @@ class stbPlatform {
1469
1761
  url = url + '&sort=channelNumber' // sort
1470
1762
  */
1471
1763
  //url = 'https://prod.spark.sunrisetv.ch/eng/web/linear-service/v2/channels?cityId=401&language=en&productClass=Orion-DASH'
1472
- 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';
1473
1766
  url = url + '?cityId=' + this.customer.cityId; //+ this.customer.cityId // cityId needed to get user-specific list
1474
1767
  url = url + '&language=en'; // language
1475
1768
  url = url + '&productClass=Orion-DASH'; // productClass, must be Orion-DASH
@@ -1574,6 +1867,45 @@ class stbPlatform {
1574
1867
  })
1575
1868
  }
1576
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
+
1577
1909
 
1578
1910
 
1579
1911
 
@@ -1586,7 +1918,10 @@ class stbPlatform {
1586
1918
 
1587
1919
  //const url = personalizationServiceUrlArray[this.config.country.toLowerCase()].replace("{householdId}", this.session.householdId) + '/' + requestType;
1588
1920
  //const url='https://prod.spark.sunrisetv.ch/eng/web/personalization-service/v1/customer/' + householdId + '?with=profiles%2Cdevices';
1589
- 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
+
1590
1925
  // headers are in the web client
1591
1926
  let config={}
1592
1927
  if (this.config.country.toLowerCase() == 'gb'){
@@ -1693,7 +2028,9 @@ class stbPlatform {
1693
2028
  async setPersonalizationDataForDevice(deviceId, deviceSettings, callback) {
1694
2029
  if (this.config.debugLevel > 0) { this.log.warn('setPersonalizationDataForDevice: deviceSettings:', deviceSettings); }
1695
2030
  // https://prod.spark.sunrisetv.ch/eng/web/personalization-service/v1/customer/1012345_ch/devices/3C36E4-EOSSTB-003656123456
1696
- 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
+
1697
2034
  const data = {"settings": deviceSettings};
1698
2035
  // gb needs x-cus, x-oesp-token and x-oesp-username
1699
2036
  let config={}
@@ -1734,7 +2071,8 @@ class stbPlatform {
1734
2071
 
1735
2072
  //const url = personalizationServiceUrlArray[this.config.country.toLowerCase()].replace("{householdId}", this.session.householdId) + '/' + requestType;
1736
2073
  //const url='https://prod.spark.sunrisetv.ch/eng/web/purchase-service/v2/customers/107xxxx_ch/entitlements?enableDaypass=true'
1737
- 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';
1738
2076
  //const config = {headers: {"x-cus": this.session.householdId, "x-oesp-token": this.session.accessToken, "x-oesp-username": this.session.username}};
1739
2077
  const config = {headers: {
1740
2078
  "x-cus": householdId,
@@ -1802,7 +2140,8 @@ class stbPlatform {
1802
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
1803
2141
  // const url = countryBaseUrlArray[this.config.country.toLowerCase()] + '/' + 'networkdvrrecordings?isAdult=false&plannedOnly=false&range=1-20'; // works
1804
2142
  // parameter plannedOnly=false did not work
1805
- 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
1806
2145
  if (this.config.debugLevel > 0) { this.log.warn('getRecordingState: GET %s', url); }
1807
2146
  axiosWS.get(url, config)
1808
2147
  .then(response => {
@@ -2036,7 +2375,8 @@ class stbPlatform {
2036
2375
  let url
2037
2376
  //url=countryBaseUrlArray[this.config.country.toLowerCase()] + '/eng/web/purchase-service/v2/customers/' + householdId + '/entitlements?enableDaypass=true';
2038
2377
  //url='https://web-api-prod-obo.horizon.tv/oesp/v4/CH/eng/web/eng/session'
2039
- 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;
2040
2380
  const config = {headers: {
2041
2381
  "x-cus": householdId,
2042
2382
  "x-oesp-token": this.session.accessToken,
@@ -2102,11 +2442,11 @@ class stbPlatform {
2102
2442
 
2103
2443
  const mqttAxiosConfig = {
2104
2444
  method: 'GET',
2105
- //url: countryBaseUrlArray[this.config.country.toLowerCase()] + '/tokens/jwt', prior to October 2022
2106
2445
  // examples of auth-service/v1/mqtt/token urls:
2107
2446
  // https://prod.spark.ziggogo.tv/auth-service/v1/mqtt/token
2108
2447
  // https://prod.spark.sunrisetv.ch/auth-service/v1/mqtt/token
2109
- 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',
2110
2450
  headers: {
2111
2451
  'X-OESP-Token': accessToken,
2112
2452
  'X-OESP-Username': oespUsername,
@@ -2120,8 +2460,6 @@ class stbPlatform {
2120
2460
  }
2121
2461
  mqttUsername = householdId; // used in sendKey to ensure that mqtt is connected
2122
2462
  resolve(response.data.token); // resolve with the token
2123
- //this.startMqttClient(this, householdId, response.data.token); // this starts the mqtt session
2124
-
2125
2463
  })
2126
2464
  .catch(error => {
2127
2465
  this.log.debug('getMqttToken error details:', error);
@@ -2137,7 +2475,7 @@ class stbPlatform {
2137
2475
  // a sync procedure, no promise returned
2138
2476
  // https://github.com/mqttjs/MQTT.js#readme
2139
2477
  // http://www.steves-internet-guide.com/mqtt-publish-subscribe/
2140
- startMqttClient(parent, mqttUsername, mqttPassword) {
2478
+ statMqttClient(parent, mqttUsername, mqttPassword) {
2141
2479
  return new Promise((resolve, reject) => {
2142
2480
  try {
2143
2481
  if (this.config.debugLevel > 0) {
@@ -2150,28 +2488,45 @@ class stbPlatform {
2150
2488
 
2151
2489
 
2152
2490
  // create mqtt client instance and connect to the mqttUrl
2153
- const mqttUrl = mqttUrlArray[this.config.country.toLowerCase()];
2491
+ //const mqttBroker = mqttUrlArray[this.config.country.toLowerCase()];
2492
+ const mqttBrokerUrl = this.configsvc.mqttBroker.URL;
2154
2493
  if (this.config.debugLevel > 0) {
2155
- this.log.warn('startMqttClient: mqttUrl:', mqttUrl );
2494
+ this.log.warn('statMqttClient: mqttBrokerUrl:', mqttBrokerUrl );
2156
2495
  }
2157
2496
  if (this.config.debugLevel > 0) {
2158
- 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 );
2159
2498
  }
2160
2499
 
2161
- // 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
2162
2501
  //mqttClientId = makeId(32);
2163
2502
  mqttClientId = makeFormattedId(32);
2164
- //mqttClientId = makeId(32);
2165
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
+
2166
2520
  // https://github.com/mqttjs/MQTT.js#connect
2167
- mqttClient = mqtt.connect(mqttUrl, {
2168
- connectTimeout: 10 * 1000, // 10s
2521
+ mqttClient = mqtt.connect(mqttBrokerUrl, {
2522
+ createWebsocket: createCustomWebsocket,
2169
2523
  clientId: mqttClientId,
2524
+ connectTimeout: 10 * 1000, // 10s
2170
2525
  username: mqttUsername,
2171
2526
  password: mqttPassword
2172
2527
  });
2173
2528
  if (this.config.debugLevel > 0) {
2174
- 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 );
2175
2530
  }
2176
2531
 
2177
2532
  //mqttClient.setMaxListeners(20); // default is 10 sometimes causes issues when the listeners reach 11
@@ -2578,20 +2933,21 @@ class stbPlatform {
2578
2933
 
2579
2934
 
2580
2935
  if (this.config.debugLevel > 0) {
2581
- this.log.warn("mqttClient: end of code block");
2936
+ this.log.warn("statMqttClient: end of code block");
2582
2937
  }
2583
2938
  resolve(mqttClient.connected); // return the promise with the connected state
2584
2939
 
2585
2940
  } catch (err) {
2941
+ this.log.error(err);
2586
2942
  reject('Cannot connect to mqtt broker', err); // reject the promise
2587
2943
  }
2588
2944
 
2589
2945
  })
2590
- } // end of startMqttClient
2946
+ } // end of statMqttClient
2591
2947
 
2592
2948
 
2593
- // end the mqtt client cleanly
2594
- endMqttClient() {
2949
+ // end the mqtt session cleanly
2950
+ endMqttSession() {
2595
2951
  return new Promise((resolve, reject) => {
2596
2952
  if (this.config.debugLevel > -1) {
2597
2953
  this.log('Shutting down mqttClient...');
@@ -4388,11 +4744,11 @@ class stbDevice {
4388
4744
  this.log("%s: Refreshing most watched channels for profile '%s'", this.name, (profile || {}).name);
4389
4745
 
4390
4746
  // https://prod.spark.sunrisetv.ch/eng/web/linear-service/v1/mostWatchedChannels?cityId=401&productClass=Orion-DASH"
4391
- 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';
4392
4749
  // add url standard parameters
4393
4750
  url = url + '?cityId=' + this.customer.cityId; //+ this.customer.cityId // cityId needed to get user-specific list
4394
4751
  url = url + '&productClass=Orion-DASH'; // productClass, must be Orion-DASH
4395
- if (this.config.debugLevel > 2) { this.log.warn('getMostWatchedChannels: loading from',url); }
4396
4752
 
4397
4753
  const config = {headers: {
4398
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.15",
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": {