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 +16 -1
- package/README.md +7 -7
- package/config.schema.json +25 -3
- package/index.js +455 -67
- package/package.json +5 -4
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
|
-
|
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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`.
|
package/config.schema.json
CHANGED
|
@@ -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
|
-
|
|
55
|
-
'be-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
'
|
|
60
|
-
|
|
61
|
-
'
|
|
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
|
-
|
|
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.
|
|
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
|
|
505
|
-
errorTitle = 'Failed to
|
|
506
|
-
debug(debugPrefix + 'calling
|
|
507
|
-
await this.
|
|
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
|
|
510
|
-
this.log.debug('%s: ++++++ step
|
|
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
|
|
518
|
-
this.log.debug('%s: ++++++ step
|
|
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
|
|
524
|
-
this.log.debug('%s: ++++++ step
|
|
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
|
|
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
|
|
567
|
+
this.log.debug('%s: ++++++ step 6: foundPvrEntitlement %s', watchdogInstance, pvrFeatureFound);
|
|
533
568
|
if (pvrFeatureFound) {
|
|
534
|
-
this.log.debug('%s: ++++++ step
|
|
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
|
|
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
|
|
578
|
+
this.log.debug('%s: ++++++ step 7: foundPvrEntitlement %s', watchdogInstance, pvrFeatureFound);
|
|
544
579
|
if (pvrFeatureFound) {
|
|
545
|
-
this.log.debug('%s: ++++++ step
|
|
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
|
|
552
|
-
this.log.debug('%s: ++++++ step
|
|
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
|
|
560
|
-
this.log.debug('%s: ++++++ step
|
|
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
|
|
567
|
-
this.log.debug('%s: ++++++ step
|
|
568
|
-
debug(debugPrefix + 'calling
|
|
569
|
-
return this.
|
|
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
|
|
754
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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('
|
|
2494
|
+
this.log.warn('statMqttClient: mqttBrokerUrl:', mqttBrokerUrl );
|
|
2124
2495
|
}
|
|
2125
2496
|
if (this.config.debugLevel > 0) {
|
|
2126
|
-
this.log.warn('
|
|
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
|
|
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(
|
|
2136
|
-
|
|
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('
|
|
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("
|
|
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
|
|
2946
|
+
} // end of statMqttClient
|
|
2559
2947
|
|
|
2560
2948
|
|
|
2561
|
-
// end the mqtt
|
|
2562
|
-
|
|
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.
|
|
6
|
+
"version": "2.3.0",
|
|
7
7
|
"platformname": "eosstb",
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"axios-cookiejar-support": "^5.0.0",
|
|
10
|
-
"axios": "^1.6.
|
|
10
|
+
"axios": "^1.6.6",
|
|
11
11
|
"debug": "^4.3.4",
|
|
12
|
-
"mqtt": "^5.3.
|
|
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": {
|