homebridge-eosstb 2.4.0-beta.1 → 2.4.0-beta.3
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 +31 -0
- package/README.md +1 -2
- package/index.js +406 -197
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
See the [Readme file](https://github.com/jsiegenthaler/homebridge-eosstb/blob/master/README.md) for full plugin documentation.
|
|
5
5
|
Please restart Homebridge after every plugin update.
|
|
6
6
|
|
|
7
|
+
|
|
8
|
+
## 2.4.0-beta.3 (2026-05-10)
|
|
9
|
+
|
|
10
|
+
This release focusses improved error handling and has extra debugging added to catch a refresh error
|
|
11
|
+
|
|
12
|
+
- Improved handling of errors for web requests
|
|
13
|
+
- Added extra debugging to assist in catching a refresh error in refreshChannelList
|
|
14
|
+
|
|
15
|
+
## 2.4.0-beta.2 (2026-05-09)
|
|
16
|
+
|
|
17
|
+
This release focusses on ensuring mqtt long-term stability, and fixes an issue where the channel name was not shown on startup.
|
|
18
|
+
|
|
19
|
+
- Rescheduled nightly channel list refresh to 0000-0400 instead of 0000-0600
|
|
20
|
+
- Added an automatic daily mqtt reconnect at a random time between 0400-0600 to avoid long running mqtt sessions. Only restarts if settop box is turned off
|
|
21
|
+
- Fixed issue where current channel was not displayed on plugin startup
|
|
22
|
+
|
|
23
|
+
|
|
7
24
|
## 2.4.0-beta.1 (2026-05-09)
|
|
8
25
|
|
|
9
26
|
This release represents a major rewrite of the plugin, significantly improving robustness, HAP compliance, and code quality throughout, and making it work for Switzerland.
|
|
@@ -34,6 +51,20 @@ This release represents a major rewrite of the plugin, significantly improving r
|
|
|
34
51
|
- Bumped dependency "tough-cookie": "^6.0.1",
|
|
35
52
|
- Bumped dependency "ws": "^8.20.0"
|
|
36
53
|
|
|
54
|
+
## 2.3.9 (2026-05-09)
|
|
55
|
+
- Fixed Error on Homebridge v2: Cannot read properties of undefined (reading 'STRING')
|
|
56
|
+
- Adapted hidden channel name to reduce warning messages with Homebridge v2
|
|
57
|
+
- Updated iOS and Homebridge version references in Readme
|
|
58
|
+
- Bumped engine "^1.11.4||^2.0.0",
|
|
59
|
+
- Bumped engine "node": "^24.15.0"
|
|
60
|
+
- Bumped dependency "axios": "^1.16.0",
|
|
61
|
+
- Bumped dependency "axios-cookiejar-support": "^7.0.0",
|
|
62
|
+
- Bumped dependency "mqtt": "^5.15.1",
|
|
63
|
+
- Bumped dependency "qs": "^6.15.1",
|
|
64
|
+
- Bumped dependency "semver": "^7.8.0",
|
|
65
|
+
- Bumped dependency "tough-cookie": "^6.0.1",
|
|
66
|
+
- Bumped dependency "ws": "^8.20.0"
|
|
67
|
+
|
|
37
68
|
## 2.3.8 (2026-02-27)
|
|
38
69
|
|
|
39
70
|
This is a maintenance release to bring dependencies up to date.
|
package/README.md
CHANGED
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
|
|
21
21
|
The logon method to the backend systems changed in January / February 2024. I managed to get CH working again from April 2026 for CH. NL was confirmed working OK in May 2026. BE was confirmed working OK in June 2024.
|
|
22
22
|
|
|
23
|
-
If you know anything about session authentication and are able to help, please get in touch.
|
|
24
23
|
</b><hr>
|
|
25
24
|
|
|
26
25
|
[](https://www.npmjs.com/package/homebridge-eosstb)
|
|
@@ -110,7 +109,7 @@ This plugin is not provided by Telenet or Sunrise or Virgin Media or Ziggo or an
|
|
|
110
109
|
## Requirements
|
|
111
110
|
|
|
112
111
|
- An Apple iPhone or iPad with iOS/iPadOS 14.0 (or later). Developed on iOS 14.1-26.4, earlier versions not tested.
|
|
113
|
-
- [Homebridge](https://homebridge.io/) v1.1.116 (or later). Developed on Homebridge 1.1.116-2.0.
|
|
112
|
+
- [Homebridge](https://homebridge.io/) v1.1.116 (or later). Developed on Homebridge 1.1.116-2.0.2, earlier versions not tested.
|
|
114
113
|
- A TV subscription from one of the supported countries and TV providers.
|
|
115
114
|
- An online account for viewing TV in the web app (often part of your TV package), see the table above.
|
|
116
115
|
- An ARRIS DCX960 or HUMAX EOS1008R / 2008C / VIP5002W set-top box, provided by your TV provider as part of your TV subscription, called by the system an "EOSSTB", "EOS2STB" or "APLSTB" and marketed under different names in different countries.
|
package/index.js
CHANGED
|
@@ -355,8 +355,10 @@ class StbPlatform {
|
|
|
355
355
|
this.accessories = [];
|
|
356
356
|
this.stbDevices = []; // store stbDevice in this.stbDevices
|
|
357
357
|
this.masterChannelList = [];
|
|
358
|
-
this.
|
|
358
|
+
this.masterChannelListRefreshedAt = null; // null = never fetched since startup
|
|
359
|
+
this.masterChannelListExpiryDate = 0; // epoch = always expired on first run, forces immediate fetch
|
|
359
360
|
this.checkChannelListTimeout = null; // nightly scheduler handler
|
|
361
|
+
this.mqttReconnecting = false; // nightly reconnect indicator
|
|
360
362
|
this.isDev = config.devMode === true;
|
|
361
363
|
this.debugLevel = this.config.debugLevel || 0; // debugLevel defaults to 0 (minimum)
|
|
362
364
|
|
|
@@ -822,7 +824,7 @@ class StbPlatform {
|
|
|
822
824
|
|
|
823
825
|
/**
|
|
824
826
|
* Schedule the next nightly master channel list refresh.
|
|
825
|
-
* Picks a random time between 00:00 and
|
|
827
|
+
* Picks a random time between 00:00 and 04:00 the following day,
|
|
826
828
|
* then reschedules itself so the pattern repeats indefinitely.
|
|
827
829
|
*
|
|
828
830
|
* Using setTimeout (not setInterval) means each day gets a fresh
|
|
@@ -834,9 +836,9 @@ class StbPlatform {
|
|
|
834
836
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
835
837
|
tomorrow.setHours(0, 0, 0, 0);
|
|
836
838
|
|
|
837
|
-
// Add a random offset: anywhere from 0 ms up to (but not including)
|
|
838
|
-
const
|
|
839
|
-
const randomOffsetMs = Math.floor(Math.random() *
|
|
839
|
+
// Add a random offset: anywhere from 0 ms up to (but not including) 4 hours
|
|
840
|
+
const FOUR_HOURS_MS = 4 * 60 * 60 * 1000;
|
|
841
|
+
const randomOffsetMs = Math.floor(Math.random() * FOUR_HOURS_MS);
|
|
840
842
|
|
|
841
843
|
const nextRefreshAt = new Date(tomorrow.getTime() + randomOffsetMs);
|
|
842
844
|
const msUntilRefresh = nextRefreshAt.getTime() - Date.now();
|
|
@@ -850,11 +852,125 @@ class StbPlatform {
|
|
|
850
852
|
// Store the timer handle so shutdown can cancel it
|
|
851
853
|
this.checkChannelListTimeout = setTimeout(async () => {
|
|
852
854
|
if (this.isShuttingDown) return; // bail out if we're going down
|
|
855
|
+
|
|
856
|
+
// if an MQTT reconnect is in progress, wait a few minutes before
|
|
857
|
+
// refreshing to avoid a race condition during session startup
|
|
858
|
+
if (this.mqttReconnecting) {
|
|
859
|
+
const THREE_MIN_MS = 3 * 60 * 1000;
|
|
860
|
+
const retryDelayMs =
|
|
861
|
+
THREE_MIN_MS + Math.floor(Math.random() * THREE_MIN_MS);
|
|
862
|
+
this.log.info(
|
|
863
|
+
"StbPlatform: channel list refresh deferred - MQTT reconnect in progress, retrying in a few minutes",
|
|
864
|
+
);
|
|
865
|
+
this.checkChannelListTimeout = setTimeout(async () => {
|
|
866
|
+
if (this.isShuttingDown) return;
|
|
867
|
+
await this._refreshChannelList();
|
|
868
|
+
this._scheduleNightlyChannelListRefresh();
|
|
869
|
+
}, retryDelayMs);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
|
|
853
873
|
await this._refreshChannelList();
|
|
854
874
|
this._scheduleNightlyChannelListRefresh(); // reschedule for the next day
|
|
855
875
|
}, msUntilRefresh);
|
|
856
876
|
} // end of _scheduleNightlyChannelListRefresh
|
|
857
877
|
|
|
878
|
+
/**
|
|
879
|
+
* Schedule the next nightly MQTT reconnect.
|
|
880
|
+
* Picks a random time between 04:00 and 06:00 the following day
|
|
881
|
+
* to avoid overlapping with the channel list refresh (00:00–04:00).
|
|
882
|
+
* Reschedules itself so the pattern repeats indefinitely.
|
|
883
|
+
*/
|
|
884
|
+
_scheduleNightlyMqttReconnect() {
|
|
885
|
+
const tomorrow = new Date();
|
|
886
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
887
|
+
tomorrow.setHours(4, 0, 0, 0);
|
|
888
|
+
|
|
889
|
+
const TWO_HOURS_MS = 2 * 60 * 60 * 1000;
|
|
890
|
+
const randomOffsetMs = Math.floor(Math.random() * TWO_HOURS_MS);
|
|
891
|
+
|
|
892
|
+
const nextReconnectAt = new Date(tomorrow.getTime() + randomOffsetMs);
|
|
893
|
+
const msUntilReconnect = nextReconnectAt.getTime() - Date.now();
|
|
894
|
+
|
|
895
|
+
if (this.debugLevel > 0) {
|
|
896
|
+
this.log.warn(
|
|
897
|
+
`StbPlatform: next nightly MQTT reconnect scheduled for ${nextReconnectAt.toLocaleString()}`,
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
this.mqttReconnectTimeout = setTimeout(async () => {
|
|
902
|
+
if (this.isShuttingDown) return;
|
|
903
|
+
await this._attemptNightlyMqttReconnect();
|
|
904
|
+
// _attemptNightlyMqttReconnect reschedules for the next night once done
|
|
905
|
+
}, msUntilReconnect);
|
|
906
|
+
} // end of _scheduleNightlyMqttReconnect
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Attempt the nightly MQTT reconnect.
|
|
910
|
+
* If any STB is currently online (user may be watching), defers by 1 hour
|
|
911
|
+
* plus a random offset and tries again, rather than interrupting the session.
|
|
912
|
+
* Once the reconnect completes (or fails), reschedules for the next night.
|
|
913
|
+
* Retries 3 times then gives up, and the next reconnect will be the next day.
|
|
914
|
+
*/
|
|
915
|
+
async _attemptNightlyMqttReconnect(retryCount = 0) {
|
|
916
|
+
if (this.isShuttingDown) return;
|
|
917
|
+
|
|
918
|
+
const MAX_RETRIES = 3; // give up after 3 deferrals (~3-4.5 hours past 04:00)
|
|
919
|
+
|
|
920
|
+
// check if any STB is currently active - if so, defer to avoid
|
|
921
|
+
// interrupting a user who may be watching TV and using the remote
|
|
922
|
+
const anyStbOnline = this.devices.some(
|
|
923
|
+
(device) => device.currentPowerState === Characteristic.Active.ACTIVE,
|
|
924
|
+
);
|
|
925
|
+
|
|
926
|
+
if (anyStbOnline) {
|
|
927
|
+
// give up if max retries reached
|
|
928
|
+
if (retryCount >= MAX_RETRIES) {
|
|
929
|
+
this.log.info(
|
|
930
|
+
"StbPlatform: nightly MQTT reconnect skipped - STB still active after max retries, rescheduling for next night",
|
|
931
|
+
);
|
|
932
|
+
this._scheduleNightlyMqttReconnect();
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// retry in 1 hour plus a random 0–30 min buffer
|
|
937
|
+
const ONE_HOUR_MS = 60 * 60 * 1000;
|
|
938
|
+
const THIRTY_MIN_MS = 30 * 60 * 1000;
|
|
939
|
+
const retryDelayMs =
|
|
940
|
+
ONE_HOUR_MS + Math.floor(Math.random() * THIRTY_MIN_MS);
|
|
941
|
+
const retryAt = new Date(Date.now() + retryDelayMs);
|
|
942
|
+
|
|
943
|
+
this.log.info(
|
|
944
|
+
`StbPlatform: nightly MQTT reconnect deferred - STB is active (attempt ${retryCount + 1}/${MAX_RETRIES}). Retrying at ${retryAt.toLocaleString()}`,
|
|
945
|
+
);
|
|
946
|
+
|
|
947
|
+
// store handle so shutdown can cancel the deferred retry too
|
|
948
|
+
this.mqttReconnectTimeout = setTimeout(async () => {
|
|
949
|
+
if (this.isShuttingDown) return;
|
|
950
|
+
await this._attemptNightlyMqttReconnect(retryCount + 1);
|
|
951
|
+
}, retryDelayMs);
|
|
952
|
+
return; // don't reschedule for next night yet - that happens after a successful reconnect
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// no STB is active - safe to reconnect
|
|
956
|
+
try {
|
|
957
|
+
this.mqttReconnecting = true; // signal to channel list refresh to pause
|
|
958
|
+
this.log.info("StbPlatform: nightly MQTT reconnect starting...");
|
|
959
|
+
await this.endMqttSession();
|
|
960
|
+
await this.startMqttClient();
|
|
961
|
+
this.log.info("StbPlatform: nightly MQTT reconnect completed");
|
|
962
|
+
} catch (err) {
|
|
963
|
+
this.log.error(
|
|
964
|
+
"StbPlatform: nightly MQTT reconnect failed:",
|
|
965
|
+
err.message,
|
|
966
|
+
);
|
|
967
|
+
} finally {
|
|
968
|
+
this.mqttReconnecting = false; // always clear the flag, even on failure
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
this._scheduleNightlyMqttReconnect(); // reschedule for next night
|
|
972
|
+
} // end of _attemptNightlyMqttReconnect
|
|
973
|
+
|
|
858
974
|
/**
|
|
859
975
|
* _runFullStartupSequence
|
|
860
976
|
*
|
|
@@ -872,7 +988,7 @@ class StbPlatform {
|
|
|
872
988
|
* 10. startMqttClient — connect to the mqtt broker
|
|
873
989
|
*
|
|
874
990
|
* Each step is individually awaited, so failures are caught at the right
|
|
875
|
-
* step and
|
|
991
|
+
* step and stepName is accurate when logged.
|
|
876
992
|
*
|
|
877
993
|
* @param {string} watchdogInstance - log prefix for this watchdog invocation
|
|
878
994
|
* @param {string} debugPrefix - debug() colour prefix
|
|
@@ -883,12 +999,13 @@ class StbPlatform {
|
|
|
883
999
|
Object.keys(sessionState)[this.currentSessionState],
|
|
884
1000
|
);
|
|
885
1001
|
|
|
886
|
-
//
|
|
1002
|
+
// stepName tracks which step we're on so the catch block can log a
|
|
887
1003
|
// meaningful message rather than a generic "something failed".
|
|
888
|
-
let
|
|
1004
|
+
let stepName = "";
|
|
889
1005
|
|
|
890
1006
|
try {
|
|
891
1007
|
// ── Step 1: Get backend config (endpoint URLs) for the country ──────────
|
|
1008
|
+
stepName = "get config";
|
|
892
1009
|
this.log.debug("%s: ++++ step 1: calling getConfig", watchdogInstance);
|
|
893
1010
|
debug(debugPrefix + "calling getConfig");
|
|
894
1011
|
|
|
@@ -902,7 +1019,7 @@ class StbPlatform {
|
|
|
902
1019
|
);
|
|
903
1020
|
|
|
904
1021
|
// ── Step 2: Authenticate and create a session ──────────────────────────
|
|
905
|
-
|
|
1022
|
+
stepName = "create session";
|
|
906
1023
|
this.log.debug(
|
|
907
1024
|
"%s: ++++ step 2: calling createSession for country %s",
|
|
908
1025
|
watchdogInstance,
|
|
@@ -913,6 +1030,12 @@ class StbPlatform {
|
|
|
913
1030
|
|
|
914
1031
|
const sessionHouseholdId = await this.createSession();
|
|
915
1032
|
// Result stored in this.session by createSession()
|
|
1033
|
+
// debugging help to get session keys
|
|
1034
|
+
// session object keys: ["accessToken","householdId","refreshToken","refreshTokenExpiry","username","issuedAt"]
|
|
1035
|
+
this.log.debug(
|
|
1036
|
+
"session object keys: %s",
|
|
1037
|
+
JSON.stringify(Object.keys(this.session ?? {})),
|
|
1038
|
+
);
|
|
916
1039
|
|
|
917
1040
|
this.log.debug(
|
|
918
1041
|
"%s: ++++++ step 2 done: session created, householdId %s",
|
|
@@ -921,7 +1044,7 @@ class StbPlatform {
|
|
|
921
1044
|
);
|
|
922
1045
|
|
|
923
1046
|
// ── Step 3: Fetch customer profile and assigned devices ────────────────
|
|
924
|
-
|
|
1047
|
+
stepName = "discover platform";
|
|
925
1048
|
this.log.debug(
|
|
926
1049
|
"%s: ++++ step 3: calling getPersonalizationData for householdId %s",
|
|
927
1050
|
watchdogInstance,
|
|
@@ -1017,7 +1140,7 @@ class StbPlatform {
|
|
|
1017
1140
|
}
|
|
1018
1141
|
|
|
1019
1142
|
// ── Step 8: Discover and configure HomeKit accessories ─────────────────
|
|
1020
|
-
|
|
1143
|
+
stepName = "discover devices";
|
|
1021
1144
|
this.log.debug(
|
|
1022
1145
|
"%s: ++++ step 8: calling discoverDevices",
|
|
1023
1146
|
watchdogInstance,
|
|
@@ -1035,7 +1158,7 @@ class StbPlatform {
|
|
|
1035
1158
|
);
|
|
1036
1159
|
|
|
1037
1160
|
// ── Step 9: Get the mqtt broker token ─────────────────────────────────
|
|
1038
|
-
|
|
1161
|
+
stepName = "start mqtt session";
|
|
1039
1162
|
this.log.debug("%s: ++++ step 9: calling getMqttToken", watchdogInstance);
|
|
1040
1163
|
debug(debugPrefix + "calling getMqttToken");
|
|
1041
1164
|
|
|
@@ -1072,15 +1195,66 @@ class StbPlatform {
|
|
|
1072
1195
|
watchdogInstance,
|
|
1073
1196
|
);
|
|
1074
1197
|
} catch (errorReason) {
|
|
1075
|
-
// One of the steps above threw or rejected. Log the failed
|
|
1076
|
-
//
|
|
1077
|
-
|
|
1198
|
+
// One of the steps above threw or rejected. Log the failed stepName
|
|
1199
|
+
// alongside the error message for easy diagnosis.
|
|
1200
|
+
const errMsg =
|
|
1201
|
+
errorReason instanceof Error
|
|
1202
|
+
? errorReason.message
|
|
1203
|
+
: String(errorReason);
|
|
1204
|
+
this.log.warn("%s: Failed to %s: %s", watchdogInstance, stepName, errMsg);
|
|
1078
1205
|
this.currentSessionState = sessionState.DISCONNECTED;
|
|
1079
1206
|
this.currentStatusFault = Characteristic.StatusFault.GENERAL_FAULT;
|
|
1080
1207
|
// sessionWatchdogRunning is reset in the finally block of the caller.
|
|
1081
1208
|
}
|
|
1082
1209
|
} // end of _runFullStartupSequence
|
|
1083
1210
|
|
|
1211
|
+
/**
|
|
1212
|
+
* _handleWebError
|
|
1213
|
+
*
|
|
1214
|
+
* Standardised catch-block handler for all outbound HTTP/Axios calls.
|
|
1215
|
+
* Builds a consistent human-readable error message, sets session state
|
|
1216
|
+
* to DISCONNECTED on ENOTFOUND, logs at debug level, then re-throws.
|
|
1217
|
+
*
|
|
1218
|
+
* @param {Error} error - the caught error
|
|
1219
|
+
* @param {string} action - what the caller was trying to do, e.g.
|
|
1220
|
+
* "get config data for countryCode ch"
|
|
1221
|
+
* @param {string|URL} url - the URL that was called (for debug context)
|
|
1222
|
+
*/
|
|
1223
|
+
_handleWebError(error, action, url) {
|
|
1224
|
+
const urlStr = String(url ?? error.config?.url ?? "");
|
|
1225
|
+
let errReason = `Could not ${action}:`;
|
|
1226
|
+
|
|
1227
|
+
if (error.isAxiosError) {
|
|
1228
|
+
errReason += ` ${error.code}`;
|
|
1229
|
+
|
|
1230
|
+
if (error.response) {
|
|
1231
|
+
errReason += ` (HTTP ${error.response.status})`;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
if (error.code === "ENOTFOUND") {
|
|
1235
|
+
errReason += " - no internet connection";
|
|
1236
|
+
this.currentSessionState = sessionState.DISCONNECTED;
|
|
1237
|
+
}
|
|
1238
|
+
} else {
|
|
1239
|
+
errReason += ` — ${error.message ?? String(error)}`;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
if (urlStr) errReason += ` — ${urlStr}`;
|
|
1243
|
+
|
|
1244
|
+
// Summary always visible:
|
|
1245
|
+
this.log.error("_handleWebError: %s", errReason);
|
|
1246
|
+
|
|
1247
|
+
this.log.debug("_handleWebError: %s — full error:", errReason, error);
|
|
1248
|
+
if (error.response?.data) {
|
|
1249
|
+
this.log.debug(
|
|
1250
|
+
"_handleWebError: response body: %s",
|
|
1251
|
+
JSON.stringify(error.response.data).substring(0, 400),
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
throw new Error(errReason, { cause: error });
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1084
1258
|
/**
|
|
1085
1259
|
* Discovers all physical devices from the backend and maps them to HomeKit accessories.
|
|
1086
1260
|
* Creates new accessories for uncached devices, and restores existing ones from cache.
|
|
@@ -2609,8 +2783,8 @@ class StbPlatform {
|
|
|
2609
2783
|
);
|
|
2610
2784
|
}
|
|
2611
2785
|
throw new Error(
|
|
2612
|
-
|
|
2613
|
-
|
|
2786
|
+
// use a simple clean message to tell the user his credentials are likely wrong
|
|
2787
|
+
`Step 4 of 5: login failed — check your username and password`,
|
|
2614
2788
|
);
|
|
2615
2789
|
}
|
|
2616
2790
|
|
|
@@ -3136,6 +3310,18 @@ class StbPlatform {
|
|
|
3136
3310
|
return;
|
|
3137
3311
|
}
|
|
3138
3312
|
|
|
3313
|
+
// Log session state before the request - DIAGNOSES DEBUG ONLY
|
|
3314
|
+
const tokenAge = this.session?.issuedAt
|
|
3315
|
+
? Math.round((Date.now() - this.session.issuedAt) / 1000 / 60)
|
|
3316
|
+
: null;
|
|
3317
|
+
const sessionId = this.session?.householdId ?? "(none)";
|
|
3318
|
+
|
|
3319
|
+
this.log.debug(
|
|
3320
|
+
"refreshMasterChannelList: starting | householdId=%s | tokenAgeMinutes=%s",
|
|
3321
|
+
sessionId,
|
|
3322
|
+
tokenAge ?? "unknown",
|
|
3323
|
+
);
|
|
3324
|
+
|
|
3139
3325
|
// exit immediately if channel list has not expired
|
|
3140
3326
|
if (Date.now() < this.masterChannelListExpiryDate) {
|
|
3141
3327
|
if (this.debugLevel > 1) {
|
|
@@ -3154,16 +3340,6 @@ class StbPlatform {
|
|
|
3154
3340
|
// syntax:
|
|
3155
3341
|
// https://prod.oesp.virginmedia.com/oesp/v4/GB/eng/web/channels?byLocationId=41043&includeInvisible=true&includeNotEntitled=true&personalised=true&sort=channelNumber
|
|
3156
3342
|
// https://prod.spark.sunrisetv.ch/eng/web/linear-service/v2/channels?cityId=401&language=en&productClass=Orion-DASH
|
|
3157
|
-
/*
|
|
3158
|
-
let url = COUNTRY_BASE_URLS[this.config.country.toLowerCase()] + '/channels';
|
|
3159
|
-
url = url + '?byLocationId=' + this.session.locationId // locationId needed to get user-specific list
|
|
3160
|
-
url = url + '&includeInvisible=true' // includeInvisible
|
|
3161
|
-
url = url + '&includeNotEntitled=true' // includeNotEntitled
|
|
3162
|
-
url = url + '&personalised=true' // personalised
|
|
3163
|
-
url = url + '&sort=channelNumber' // sort
|
|
3164
|
-
*/
|
|
3165
|
-
//url = 'https://prod.spark.sunrisetv.ch/eng/web/linear-service/v2/channels?cityId=401&language=en&productClass=Orion-DASH'
|
|
3166
|
-
//let url = COUNTRY_BASE_URLS[this.config.country.toLowerCase()] + '/eng/web/linear-service/v2/channels';
|
|
3167
3343
|
const url = new URL(`${this.configsvc.linearService.URL}/v2/channels`);
|
|
3168
3344
|
url.searchParams.set("cityId", this.customer.cityId);
|
|
3169
3345
|
url.searchParams.set("language", "en");
|
|
@@ -3172,6 +3348,7 @@ class StbPlatform {
|
|
|
3172
3348
|
if (this.debugLevel > 1) {
|
|
3173
3349
|
this.log.warn("refreshMasterChannelList: GET %s", url);
|
|
3174
3350
|
}
|
|
3351
|
+
|
|
3175
3352
|
try {
|
|
3176
3353
|
// call the webservice to get all available channels
|
|
3177
3354
|
const config = {
|
|
@@ -3186,6 +3363,29 @@ class StbPlatform {
|
|
|
3186
3363
|
"https://www.horizon.tv/",
|
|
3187
3364
|
},
|
|
3188
3365
|
};
|
|
3366
|
+
// extra debugging to help catch refresh issues
|
|
3367
|
+
this.log.debug(
|
|
3368
|
+
"refreshMasterChannelList: request headers | x-oesp-token=%s...%s | x-oesp-username=%s | Referer=%s",
|
|
3369
|
+
this.session.accessToken?.substring(0, 8) ?? "(null)", // first 8 chars only — don't log the full token
|
|
3370
|
+
this.session.accessToken?.slice(-4) ?? "", // last 4 chars
|
|
3371
|
+
this.session.username ?? "(null)",
|
|
3372
|
+
config.headers.Referer,
|
|
3373
|
+
);
|
|
3374
|
+
// extra debugging to help catch refresh issues
|
|
3375
|
+
this.log.debug(
|
|
3376
|
+
"refreshMasterChannelList: preflight | cityId=%s | tokenPresent=%s | tokenLength=%s | username=%s | prevExpiryWas=%s | lastRefreshedAt=%s",
|
|
3377
|
+
this.customer.cityId,
|
|
3378
|
+
!!this.session?.accessToken,
|
|
3379
|
+
this.session?.accessToken?.length ?? 0,
|
|
3380
|
+
this.session?.username ?? "(none)",
|
|
3381
|
+
this.masterChannelListExpiryDate
|
|
3382
|
+
? new Date(this.masterChannelListExpiryDate).toLocaleString()
|
|
3383
|
+
: "(never set)",
|
|
3384
|
+
this.masterChannelListRefreshedAt
|
|
3385
|
+
? new Date(this.masterChannelListRefreshedAt).toLocaleString()
|
|
3386
|
+
: "(never)",
|
|
3387
|
+
);
|
|
3388
|
+
|
|
3189
3389
|
const response = await axiosWS(config);
|
|
3190
3390
|
if (this.debugLevel > 1) {
|
|
3191
3391
|
this.log.warn(
|
|
@@ -3198,7 +3398,6 @@ class StbPlatform {
|
|
|
3198
3398
|
response.data.length,
|
|
3199
3399
|
);
|
|
3200
3400
|
}
|
|
3201
|
-
//this.log(response.data);
|
|
3202
3401
|
|
|
3203
3402
|
// the header contains the following:
|
|
3204
3403
|
// Cache-Control: max-age=600, public, stale-if-error=43200
|
|
@@ -3239,6 +3438,9 @@ class StbPlatform {
|
|
|
3239
3438
|
this.masterChannelList.map((ch) => [ch.id, ch]),
|
|
3240
3439
|
);
|
|
3241
3440
|
|
|
3441
|
+
// record when we refreshed it
|
|
3442
|
+
this.masterChannelListRefreshedAt = Date.now()
|
|
3443
|
+
|
|
3242
3444
|
this.log(
|
|
3243
3445
|
"MasterChannelList contains %s channels, valid until %s",
|
|
3244
3446
|
this.masterChannelList.length,
|
|
@@ -3254,15 +3456,11 @@ class StbPlatform {
|
|
|
3254
3456
|
}
|
|
3255
3457
|
return this.masterChannelList;
|
|
3256
3458
|
} catch (error) {
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
this.currentSessionState = sessionState.DISCONNECTED;
|
|
3263
|
-
}
|
|
3264
|
-
this.log.warn("refreshMasterChannelList error:", errReason);
|
|
3265
|
-
throw new Error(errReason, { cause: error }); // { cause } preserves the original for debugging
|
|
3459
|
+
this._handleWebError(
|
|
3460
|
+
error,
|
|
3461
|
+
`refresh master channel list`,
|
|
3462
|
+
url,
|
|
3463
|
+
);
|
|
3266
3464
|
}
|
|
3267
3465
|
} // end of refreshMasterChannelList
|
|
3268
3466
|
|
|
@@ -3327,16 +3525,11 @@ class StbPlatform {
|
|
|
3327
3525
|
this.configsvc = response.data; // store the entire config data for future use in this.configsvc
|
|
3328
3526
|
return this.configsvc;
|
|
3329
3527
|
} catch (error) {
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
this.currentSessionState = sessionState.DISCONNECTED;
|
|
3336
|
-
}
|
|
3337
|
-
}
|
|
3338
|
-
this.log.debug(`getConfig error:`, error);
|
|
3339
|
-
throw new Error(errReason);
|
|
3528
|
+
this._handleWebError(
|
|
3529
|
+
error,
|
|
3530
|
+
`get config data for countryCode ${countryCode}`,
|
|
3531
|
+
url,
|
|
3532
|
+
);
|
|
3340
3533
|
}
|
|
3341
3534
|
} // end of getConfig
|
|
3342
3535
|
|
|
@@ -3487,20 +3680,11 @@ class StbPlatform {
|
|
|
3487
3680
|
//this.log.warn('getPersonalizationData: all done, returnng customerStatus: %s', this.customer.customerStatus);
|
|
3488
3681
|
return this.customer;
|
|
3489
3682
|
} catch (error) {
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
householdId
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
errReason = error.code + ": " + (error.hostname || "");
|
|
3496
|
-
// if no connection then set session to disconnected to force a session reconnect
|
|
3497
|
-
if (error.code === "ENOTFOUND") {
|
|
3498
|
-
this.currentSessionState = sessionState.DISCONNECTED;
|
|
3499
|
-
}
|
|
3500
|
-
}
|
|
3501
|
-
//this.log('%s %s', errText, (errReason || ''));
|
|
3502
|
-
this.log.debug(`getPersonalizationData error:`, error);
|
|
3503
|
-
throw error;
|
|
3683
|
+
this._handleWebError(
|
|
3684
|
+
error,
|
|
3685
|
+
`get personalization data for household ${householdId}`,
|
|
3686
|
+
url,
|
|
3687
|
+
);
|
|
3504
3688
|
}
|
|
3505
3689
|
} // end of getPersonalizationData
|
|
3506
3690
|
|
|
@@ -3550,12 +3734,11 @@ class StbPlatform {
|
|
|
3550
3734
|
);
|
|
3551
3735
|
}
|
|
3552
3736
|
} catch (error) {
|
|
3553
|
-
this.
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3737
|
+
this._handleWebError(
|
|
3738
|
+
error,
|
|
3739
|
+
`set personalization data for device ${deviceId}`,
|
|
3740
|
+
url,
|
|
3557
3741
|
);
|
|
3558
|
-
this.log.debug("setPersonalizationDataForDevice: error:", error);
|
|
3559
3742
|
}
|
|
3560
3743
|
} // end of setPersonalizationDataForDevice
|
|
3561
3744
|
|
|
@@ -3614,15 +3797,11 @@ class StbPlatform {
|
|
|
3614
3797
|
}
|
|
3615
3798
|
return this.entitlements;
|
|
3616
3799
|
} catch (error) {
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
this.currentSessionState = sessionState.DISCONNECTED;
|
|
3623
|
-
}
|
|
3624
|
-
this.log.debug(`getEntitlements error:`, error);
|
|
3625
|
-
throw new Error(errReason);
|
|
3800
|
+
this._handleWebError(
|
|
3801
|
+
error,
|
|
3802
|
+
`get entitlements data for household ${householdId}`,
|
|
3803
|
+
url,
|
|
3804
|
+
);
|
|
3626
3805
|
}
|
|
3627
3806
|
} // end of getEntitlements
|
|
3628
3807
|
|
|
@@ -3799,29 +3978,11 @@ class StbPlatform {
|
|
|
3799
3978
|
|
|
3800
3979
|
return this.currentRecordingState;
|
|
3801
3980
|
} catch (error) {
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
" " +
|
|
3808
|
-
(error.hostname || "") +
|
|
3809
|
-
": " +
|
|
3810
|
-
(error.response?.status ?? "") +
|
|
3811
|
-
" " +
|
|
3812
|
-
(error.response?.statusText ?? "") +
|
|
3813
|
-
": " +
|
|
3814
|
-
(error.config?.url ?? "");
|
|
3815
|
-
if (error.code === "ENOTFOUND") {
|
|
3816
|
-
this.currentSessionState = sessionState.DISCONNECTED;
|
|
3817
|
-
}
|
|
3818
|
-
this.log.debug("getRecordingState error:", error);
|
|
3819
|
-
throw new Error(errReason);
|
|
3820
|
-
} else {
|
|
3821
|
-
this.log.warn("getRecordingState error:");
|
|
3822
|
-
this.log.warn(error);
|
|
3823
|
-
throw error;
|
|
3824
|
-
}
|
|
3981
|
+
this._handleWebError(
|
|
3982
|
+
error,
|
|
3983
|
+
`get recording status for household ${householdId}`,
|
|
3984
|
+
url,
|
|
3985
|
+
);
|
|
3825
3986
|
}
|
|
3826
3987
|
} // end of getRecordingState
|
|
3827
3988
|
|
|
@@ -4011,29 +4172,11 @@ class StbPlatform {
|
|
|
4011
4172
|
|
|
4012
4173
|
return this.currentRecordingState;
|
|
4013
4174
|
} catch (error) {
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
" " +
|
|
4020
|
-
(error.hostname || "") +
|
|
4021
|
-
": " +
|
|
4022
|
-
(error.response?.status ?? "") +
|
|
4023
|
-
" " +
|
|
4024
|
-
(error.response?.statusText ?? "") +
|
|
4025
|
-
": " +
|
|
4026
|
-
(error.config?.url ?? "");
|
|
4027
|
-
if (error.code === "ENOTFOUND") {
|
|
4028
|
-
this.currentSessionState = sessionState.DISCONNECTED;
|
|
4029
|
-
}
|
|
4030
|
-
this.log.debug("getRecordingBookings error:", error);
|
|
4031
|
-
throw new Error(errReason);
|
|
4032
|
-
} else {
|
|
4033
|
-
this.log.warn("getRecordingBookings error:");
|
|
4034
|
-
this.log.warn(error);
|
|
4035
|
-
throw error;
|
|
4036
|
-
}
|
|
4175
|
+
this._handleWebError(
|
|
4176
|
+
error,
|
|
4177
|
+
`get recording bookings for household ${householdId}`,
|
|
4178
|
+
url,
|
|
4179
|
+
);
|
|
4037
4180
|
}
|
|
4038
4181
|
} // end of getRecordingBookings
|
|
4039
4182
|
|
|
@@ -4070,14 +4213,11 @@ class StbPlatform {
|
|
|
4070
4213
|
this.log.warn(response.data);
|
|
4071
4214
|
return true;
|
|
4072
4215
|
} catch (error) {
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
}
|
|
4079
|
-
}
|
|
4080
|
-
this.log.warn(`getExperimentalEndpoint error:`, error);
|
|
4216
|
+
this._handleWebError(
|
|
4217
|
+
error,
|
|
4218
|
+
`get experimental endpoint for household ${householdId}`,
|
|
4219
|
+
url,
|
|
4220
|
+
);
|
|
4081
4221
|
}
|
|
4082
4222
|
} // end of getExperimentalEndpoint
|
|
4083
4223
|
|
|
@@ -4278,12 +4418,8 @@ class StbPlatform {
|
|
|
4278
4418
|
// ------ device subscriptions ------
|
|
4279
4419
|
// subscribe only to what we need
|
|
4280
4420
|
|
|
4281
|
-
// turn on our clientId. This is similar to turning on a box, it tells the server we are online
|
|
4282
|
-
// our clientId must be up and running to send commands (power, channel, etc) to the physical device
|
|
4283
|
-
// this.setHgoOnlineRunning(householdId, mqttClientId);
|
|
4284
|
-
|
|
4285
4421
|
// householdId/mqttClientId: subscribe to own clientId to get data for ourselves
|
|
4286
|
-
// subscribe to all devices
|
|
4422
|
+
// subscribe to all devices before the setHgoState is sent
|
|
4287
4423
|
this.mqttSubscribeToTopic(
|
|
4288
4424
|
householdId + "/" + this.mqttClient.options.clientId,
|
|
4289
4425
|
); // subscribe to our own mqttClientId to get all data
|
|
@@ -4320,32 +4456,46 @@ class StbPlatform {
|
|
|
4320
4456
|
// reset so the 10-second retry fires correctly if the box doesn't respond
|
|
4321
4457
|
this.lastMqttUiStatusMessageReceived = null;
|
|
4322
4458
|
|
|
4459
|
+
// announce ourselves as an active HGO client before requesting UI status
|
|
4460
|
+
// the STB uses this retained presence message to decide which clients to respond to
|
|
4461
|
+
this.setHgoState(
|
|
4462
|
+
householdId,
|
|
4463
|
+
this.mqttClient.options.clientId,
|
|
4464
|
+
"ONLINE_RUNNING",
|
|
4465
|
+
);
|
|
4466
|
+
|
|
4467
|
+
// request initial UI status for each device, with a short delay to allow
|
|
4468
|
+
// the STB to process the HGO presence announcement first
|
|
4323
4469
|
// CPE.uiStatus messages are received via the householdId and mqttClientId
|
|
4324
4470
|
// topics which are already subscribed above.
|
|
4325
4471
|
// getUiStatus is called here to request the initial UI state from each device.
|
|
4326
4472
|
// retain: false is used (see getUiStatus) so a retry is scheduled in case the box
|
|
4327
4473
|
// is temporarily unreachable when the initial request is sent.
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4474
|
+
setTimeout(() => {
|
|
4475
|
+
this.devices.forEach((device) => {
|
|
4476
|
+
// request the initial UI status for each device
|
|
4477
|
+
this.getUiStatus(
|
|
4478
|
+
device.deviceId,
|
|
4479
|
+
this.mqttClient.options.clientId,
|
|
4480
|
+
);
|
|
4481
|
+
|
|
4482
|
+
// retry after 10 seconds if no CPE.uiStatus response has arrived yet
|
|
4483
|
+
setTimeout(() => {
|
|
4484
|
+
if (!this.lastMqttUiStatusMessageReceived) {
|
|
4485
|
+
if (this.debugLevel > 0) {
|
|
4486
|
+
this.log.warn(
|
|
4487
|
+
"getUiStatus: no CPE.uiStatus received yet for %s, retrying",
|
|
4488
|
+
device.deviceId,
|
|
4489
|
+
);
|
|
4490
|
+
}
|
|
4491
|
+
this.getUiStatus(
|
|
4339
4492
|
device.deviceId,
|
|
4493
|
+
this.mqttClient.options.clientId,
|
|
4340
4494
|
);
|
|
4341
4495
|
}
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
);
|
|
4346
|
-
}
|
|
4347
|
-
}, 10 * 1000); // 10 second retry delay
|
|
4348
|
-
});
|
|
4496
|
+
}, 10 * 1000); // 10 second retry delay
|
|
4497
|
+
});
|
|
4498
|
+
}, 500); // 500ms for STB to register our HGO presence before we request status
|
|
4349
4499
|
|
|
4350
4500
|
resolve(true); // all subscriptions registered — session is ready
|
|
4351
4501
|
} catch (err) {
|
|
@@ -4445,7 +4595,8 @@ class StbPlatform {
|
|
|
4445
4595
|
const deviceIndex = this.devices.findIndex(
|
|
4446
4596
|
(device) => device.deviceId === deviceId,
|
|
4447
4597
|
);
|
|
4448
|
-
const stbDevice =
|
|
4598
|
+
const stbDevice =
|
|
4599
|
+
deviceIndex > -1 ? this.stbDevices[deviceIndex] : null;
|
|
4449
4600
|
|
|
4450
4601
|
// Box setting: StandbyPowerConsumption = FastStart / ActiveStart / EcoSlowstart
|
|
4451
4602
|
// "Fast start": when turned off, goes to ONLINE_STANDBY and stays there. Box can be turned on via mqtt
|
|
@@ -4458,15 +4609,18 @@ class StbPlatform {
|
|
|
4458
4609
|
// Detect power-off → power-on transition per device.
|
|
4459
4610
|
// Set PLAY immediately; CPE.uiStatus will overwrite with the
|
|
4460
4611
|
// accurate speed-derived state shortly after.
|
|
4461
|
-
if (
|
|
4612
|
+
if (
|
|
4613
|
+
stbDevice?.previousPowerState ===
|
|
4614
|
+
Characteristic.Active.INACTIVE
|
|
4615
|
+
) {
|
|
4462
4616
|
currMediaState = Characteristic.CurrentMediaState.PLAY;
|
|
4463
4617
|
if (this.debugLevel > 0) {
|
|
4464
4618
|
this.log.warn(
|
|
4465
|
-
"mqttClient: STB status:
|
|
4619
|
+
"mqttClient: STB status: Power-on transition detected for %s, setting mediaState to PLAY",
|
|
4466
4620
|
deviceId,
|
|
4467
4621
|
);
|
|
4468
4622
|
}
|
|
4469
|
-
}
|
|
4623
|
+
}
|
|
4470
4624
|
break;
|
|
4471
4625
|
case "ONLINE_STANDBY": // ONLINE_STANDBY: power is off, device is on standby, still reachable over the network, can be turned on via mqtt.
|
|
4472
4626
|
currStatusActive = Characteristic.Active.ACTIVE; // bool, 0 = not active, 1 = active
|
|
@@ -4496,6 +4650,12 @@ class StbPlatform {
|
|
|
4496
4650
|
this.log.warn("mqttClient: %s %s", deviceId, stbState);
|
|
4497
4651
|
}
|
|
4498
4652
|
}
|
|
4653
|
+
|
|
4654
|
+
// After the switch, if box is running, request current UI state
|
|
4655
|
+
//if (stbState === 'ONLINE_RUNNING') {
|
|
4656
|
+
// Small delay gives the STB a moment to settle before responding
|
|
4657
|
+
//setTimeout(() => this.mqttRequestUiStatus(deviceId), 500);
|
|
4658
|
+
//}
|
|
4499
4659
|
}
|
|
4500
4660
|
|
|
4501
4661
|
// handle CPE UI status messages for the STB
|
|
@@ -4878,16 +5038,26 @@ class StbPlatform {
|
|
|
4878
5038
|
return resolve(true);
|
|
4879
5039
|
}
|
|
4880
5040
|
|
|
4881
|
-
//
|
|
5041
|
+
// get all subscribed topics
|
|
4882
5042
|
const topics = this.subscribedTopics ?? [];
|
|
5043
|
+
|
|
5044
|
+
// announce HGO offline while the connection is still live, before any teardown
|
|
5045
|
+
this.setHgoState(
|
|
5046
|
+
this.session.householdId,
|
|
5047
|
+
this.mqttClient.options.clientId,
|
|
5048
|
+
"OFFLINE",
|
|
5049
|
+
);
|
|
5050
|
+
|
|
5051
|
+
// unsubscribe from all subscribedTopics before tearing down the session
|
|
4883
5052
|
if (topics.length === 0) {
|
|
4884
5053
|
this.log.info(
|
|
4885
5054
|
"mqttClient: No topics to unsubscribe from, skipping unsubscribe.",
|
|
4886
5055
|
);
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
5056
|
+
|
|
5057
|
+
this.mqttClient.end(false, {}, (endErr) => {
|
|
5058
|
+
if (endErr) {
|
|
5059
|
+
this.log.error("MQTT end error:", endErr);
|
|
5060
|
+
return reject(endErr);
|
|
4891
5061
|
}
|
|
4892
5062
|
this.log.info(
|
|
4893
5063
|
"mqttClient: Disconnected cleanly. No topics found to unsubscribe from.",
|
|
@@ -4897,15 +5067,15 @@ class StbPlatform {
|
|
|
4897
5067
|
return;
|
|
4898
5068
|
}
|
|
4899
5069
|
|
|
4900
|
-
this.mqttClient.unsubscribe(topics, (
|
|
4901
|
-
if (
|
|
4902
|
-
this.log.error("MQTT unsubscribe error:",
|
|
5070
|
+
this.mqttClient.unsubscribe(topics, (unsubErr) => {
|
|
5071
|
+
if (unsubErr) {
|
|
5072
|
+
this.log.error("MQTT unsubscribe error:", unsubErr);
|
|
4903
5073
|
// still attempt to end even if unsubscribe failed
|
|
4904
5074
|
}
|
|
4905
|
-
this.mqttClient.end(false, {}, (
|
|
4906
|
-
if (
|
|
4907
|
-
this.log.error("MQTT end error:",
|
|
4908
|
-
return reject(
|
|
5075
|
+
this.mqttClient.end(false, {}, (endErr) => {
|
|
5076
|
+
if (endErr) {
|
|
5077
|
+
this.log.error("MQTT end error:", endErr);
|
|
5078
|
+
return reject(endErr);
|
|
4909
5079
|
}
|
|
4910
5080
|
this.log.info(
|
|
4911
5081
|
"mqttClient: Disconnected cleanly. All topics unsubscribed.",
|
|
@@ -4990,7 +5160,7 @@ class StbPlatform {
|
|
|
4990
5160
|
"mqttPublishMessage: Publish Message:\r\nTopic: %s\r\nMessage: %s\r\nOptions: %s",
|
|
4991
5161
|
Topic,
|
|
4992
5162
|
Message,
|
|
4993
|
-
Options,
|
|
5163
|
+
JSON.stringify(Options),
|
|
4994
5164
|
);
|
|
4995
5165
|
}
|
|
4996
5166
|
this.mqttClient.publish(Topic, Message, Options, (err) => {
|
|
@@ -5081,19 +5251,20 @@ class StbPlatform {
|
|
|
5081
5251
|
});
|
|
5082
5252
|
}
|
|
5083
5253
|
|
|
5084
|
-
//
|
|
5085
|
-
|
|
5086
|
-
|
|
5254
|
+
// set the HGO session state (online or offline)
|
|
5255
|
+
// called on mqtt connect (ONLINE_RUNNING) and on mqtt disconnect (OFFLINE)
|
|
5256
|
+
// retain: true ensures the broker overwrites any previous retained state
|
|
5257
|
+
setHgoState(householdId, mqttClientId, state) {
|
|
5087
5258
|
const topic = `${householdId}/${mqttClientId}/status`;
|
|
5088
5259
|
const message = JSON.stringify({
|
|
5089
5260
|
source: mqttClientId,
|
|
5090
|
-
state:
|
|
5261
|
+
state: state,
|
|
5091
5262
|
deviceType: "HGO",
|
|
5092
5263
|
mac: "",
|
|
5093
5264
|
ipAddress: "",
|
|
5094
5265
|
});
|
|
5095
5266
|
if (this.debugLevel > 0) {
|
|
5096
|
-
this.log.warn("
|
|
5267
|
+
this.log.warn("setHgoState: publishing %s to topic: %s", state, topic);
|
|
5097
5268
|
}
|
|
5098
5269
|
this.mqttPublishMessage(topic, message, { qos: 2, retain: true });
|
|
5099
5270
|
}
|
|
@@ -5151,6 +5322,39 @@ class StbPlatform {
|
|
|
5151
5322
|
}
|
|
5152
5323
|
}
|
|
5153
5324
|
|
|
5325
|
+
// Request the current UI status from the STB.
|
|
5326
|
+
// The STB responds with a CPE.uiStatus message on the household channel.
|
|
5327
|
+
// @param {string} deviceId - The STB device ID (e.g. "000378-EOS2STB-00852052xxxx")
|
|
5328
|
+
mqttRequestUiStatus(deviceId) {
|
|
5329
|
+
if (!this.mqttClient?.connected) {
|
|
5330
|
+
this.log.warn(
|
|
5331
|
+
"%s: mqttRequestUiStatus: MQTT not connected, skipping",
|
|
5332
|
+
deviceId,
|
|
5333
|
+
);
|
|
5334
|
+
return;
|
|
5335
|
+
}
|
|
5336
|
+
if (this.debugLevel > 0) {
|
|
5337
|
+
this.log.warn(
|
|
5338
|
+
"mqttRequestUiStatus: Requesting UI status for %s",
|
|
5339
|
+
deviceId,
|
|
5340
|
+
);
|
|
5341
|
+
}
|
|
5342
|
+
|
|
5343
|
+
const payload = JSON.stringify({
|
|
5344
|
+
version: "1.3.18",
|
|
5345
|
+
type: "CPE.pullFromTV",
|
|
5346
|
+
source: this.mqttClient.options.clientId, // your mqttClientId
|
|
5347
|
+
messageTimeStamp: Date.now(),
|
|
5348
|
+
});
|
|
5349
|
+
|
|
5350
|
+
const topic = `${this.session.householdId}/${deviceId}`;
|
|
5351
|
+
|
|
5352
|
+
this.mqttPublishMessage(topic, payload, {
|
|
5353
|
+
qos: 1,
|
|
5354
|
+
retain: false,
|
|
5355
|
+
});
|
|
5356
|
+
}
|
|
5357
|
+
|
|
5154
5358
|
// set the media state of the settopbox via mqtt
|
|
5155
5359
|
// media state is controlled by speedRate
|
|
5156
5360
|
// speedRate can be one of: -64 -30 -6 -2 0 2 6 30 64. 0=Paused, 1=Play, >1=FastForward, <0=Rewind
|
|
@@ -6406,10 +6610,7 @@ class StbDevice {
|
|
|
6406
6610
|
inputSourceService
|
|
6407
6611
|
.getCharacteristic(Characteristic.ConfiguredName)
|
|
6408
6612
|
.setProps({
|
|
6409
|
-
perms: [
|
|
6410
|
-
this.api.hap.Perms.PAIRED_READ,
|
|
6411
|
-
this.api.hap.Perms.NOTIFY,
|
|
6412
|
-
],
|
|
6613
|
+
perms: [this.api.hap.Perms.PAIRED_READ, this.api.hap.Perms.NOTIFY],
|
|
6413
6614
|
})
|
|
6414
6615
|
.onGet(() => this.getInputName(i));
|
|
6415
6616
|
//.onSet((value) => this.setInputName(i, value));
|
|
@@ -7800,7 +8001,10 @@ class StbDevice {
|
|
|
7800
8001
|
// triple rapid VolDown presses triggers setMute
|
|
7801
8002
|
if (volumeSelectorValue === Characteristic.VolumeSelector.DECREMENT) {
|
|
7802
8003
|
// Guard: ensure array is properly initialised
|
|
7803
|
-
if (
|
|
8004
|
+
if (
|
|
8005
|
+
!Array.isArray(this.lastVolDownKeyPress) ||
|
|
8006
|
+
this.lastVolDownKeyPress.length < 3
|
|
8007
|
+
) {
|
|
7804
8008
|
this.lastVolDownKeyPress = [0, 0, 0];
|
|
7805
8009
|
}
|
|
7806
8010
|
|
|
@@ -7809,14 +8013,14 @@ class StbDevice {
|
|
|
7809
8013
|
this.lastVolDownKeyPress = this.lastVolDownKeyPress.slice(0, 3); // keep only last 3
|
|
7810
8014
|
|
|
7811
8015
|
// Now assign the calculated value to the outer variable
|
|
7812
|
-
tripleVolDownPress =
|
|
8016
|
+
tripleVolDownPress =
|
|
8017
|
+
this.lastVolDownKeyPress[0] - this.lastVolDownKeyPress[2];
|
|
7813
8018
|
|
|
7814
8019
|
this.log.debug(
|
|
7815
8020
|
"%s: setVolume: Timediff between volDownKeyPress[0] and volDownKeyPress[2]: %s ms",
|
|
7816
8021
|
this.name,
|
|
7817
8022
|
tripleVolDownPress,
|
|
7818
|
-
);
|
|
7819
|
-
|
|
8023
|
+
);
|
|
7820
8024
|
}
|
|
7821
8025
|
|
|
7822
8026
|
// check for triple press of volDown, send setMute if tripleVolDownPress less than triplePressTime of 800ms
|
|
@@ -7854,7 +8058,11 @@ class StbDevice {
|
|
|
7854
8058
|
|
|
7855
8059
|
try {
|
|
7856
8060
|
if (this.debugLevel > 0) {
|
|
7857
|
-
this.log.warn(
|
|
8061
|
+
this.log.warn(
|
|
8062
|
+
"%s: setVolume: Sending command %s",
|
|
8063
|
+
this.name,
|
|
8064
|
+
command,
|
|
8065
|
+
);
|
|
7858
8066
|
}
|
|
7859
8067
|
await new Promise((resolve, reject) => {
|
|
7860
8068
|
exec(command, (error, _stdout, stderr) => {
|
|
@@ -8220,13 +8428,13 @@ class StbDevice {
|
|
|
8220
8428
|
// logChangeOnly = TRUE: only the changes are logged, no media state change occurs. Needed when sending remote keypresses to prevent double commands
|
|
8221
8429
|
// CHAR_NAMES: TargetMediaState: [ 'UUID', 'PLAY', 'PAUSE', 'STOP' ]
|
|
8222
8430
|
//if (this.debugLevel > 1) {
|
|
8223
|
-
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
8229
|
-
|
|
8431
|
+
this.log.info(
|
|
8432
|
+
"%s: setTargetMediaState to %s [%s]",
|
|
8433
|
+
this.name,
|
|
8434
|
+
targetMediaState,
|
|
8435
|
+
CHAR_NAMES.TargetMediaState[targetMediaState + 1],
|
|
8436
|
+
);
|
|
8437
|
+
// }
|
|
8230
8438
|
|
|
8231
8439
|
if (!logChangeOnly) {
|
|
8232
8440
|
// send the setMediaState command if we are not just logging the change
|
|
@@ -8239,16 +8447,17 @@ class StbDevice {
|
|
|
8239
8447
|
// PLAY 0 - 1 Play
|
|
8240
8448
|
// PAUSE 1 - 0 Paused
|
|
8241
8449
|
// STOP 2 - 0 Paused
|
|
8242
|
-
const newBoxMediaState =
|
|
8450
|
+
const newBoxMediaState =
|
|
8451
|
+
targetMediaState === Characteristic.TargetMediaState.PLAY ? 1 : 0;
|
|
8243
8452
|
const newBoxMediaStateName = newBoxMediaState === 1 ? "Play" : "Paused";
|
|
8244
8453
|
|
|
8245
8454
|
//if (this.debugLevel >= 0) {
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
|
|
8250
|
-
|
|
8251
|
-
|
|
8455
|
+
this.log(
|
|
8456
|
+
"%s: setTargetMediaState: Calling setMediaState with newBoxMediaState %s [%s]",
|
|
8457
|
+
this.name,
|
|
8458
|
+
newBoxMediaState,
|
|
8459
|
+
newBoxMediaStateName,
|
|
8460
|
+
);
|
|
8252
8461
|
//}
|
|
8253
8462
|
/*
|
|
8254
8463
|
switch (targetMediaState) {
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"displayName": "Homebridge EOSSTB",
|
|
4
4
|
"description": "Add your set-top box to Homekit (for Telenet BE, Sunrise CH, UPC SK, Virgin Media GB & IE, Ziggo NL)",
|
|
5
5
|
"author": "Jochen Siegenthaler (https://github.com/jsiegenthaler/)",
|
|
6
|
-
"version": "2.4.0-beta.
|
|
6
|
+
"version": "2.4.0-beta.3",
|
|
7
7
|
"platformname": "eosstb",
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"axios": "^1.16.0",
|