homebridge-eosstb 2.4.0-beta.1 → 2.4.0-beta.2

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/index.js +211 -51
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@ 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.2 (2026-05-09)
9
+
10
+ This release focusses on ensuring mqtt long-term stability, and fixes an issue where the channel name was not shown on startup.
11
+
12
+ - Rescheduled nightly channel list refresh to 0000-0400 instead of 0000-0600
13
+ - 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
14
+ - Fixed issue where current channel was not displayed on plugin startup
15
+
16
+
7
17
  ## 2.4.0-beta.1 (2026-05-09)
8
18
 
9
19
  This release represents a major rewrite of the plugin, significantly improving robustness, HAP compliance, and code quality throughout, and making it work for Switzerland.
package/index.js CHANGED
@@ -357,6 +357,7 @@ class StbPlatform {
357
357
  this.masterChannelList = [];
358
358
  this.masterChannelListExpiryDate = 0; // epoch = always expired on first run
359
359
  this.checkChannelListTimeout = null; // nightly scheduler handler
360
+ this.mqttReconnecting = false; // nightly reconnect indicator
360
361
  this.isDev = config.devMode === true;
361
362
  this.debugLevel = this.config.debugLevel || 0; // debugLevel defaults to 0 (minimum)
362
363
 
@@ -822,7 +823,7 @@ class StbPlatform {
822
823
 
823
824
  /**
824
825
  * Schedule the next nightly master channel list refresh.
825
- * Picks a random time between 00:00 and 06:00 the following day,
826
+ * Picks a random time between 00:00 and 04:00 the following day,
826
827
  * then reschedules itself so the pattern repeats indefinitely.
827
828
  *
828
829
  * Using setTimeout (not setInterval) means each day gets a fresh
@@ -834,9 +835,9 @@ class StbPlatform {
834
835
  tomorrow.setDate(tomorrow.getDate() + 1);
835
836
  tomorrow.setHours(0, 0, 0, 0);
836
837
 
837
- // Add a random offset: anywhere from 0 ms up to (but not including) 6 hours
838
- const SIX_HOURS_MS = 6 * 60 * 60 * 1000;
839
- const randomOffsetMs = Math.floor(Math.random() * SIX_HOURS_MS);
838
+ // Add a random offset: anywhere from 0 ms up to (but not including) 4 hours
839
+ const FOUR_HOURS_MS = 4 * 60 * 60 * 1000;
840
+ const randomOffsetMs = Math.floor(Math.random() * FOUR_HOURS_MS);
840
841
 
841
842
  const nextRefreshAt = new Date(tomorrow.getTime() + randomOffsetMs);
842
843
  const msUntilRefresh = nextRefreshAt.getTime() - Date.now();
@@ -850,11 +851,121 @@ class StbPlatform {
850
851
  // Store the timer handle so shutdown can cancel it
851
852
  this.checkChannelListTimeout = setTimeout(async () => {
852
853
  if (this.isShuttingDown) return; // bail out if we're going down
854
+
855
+ // if an MQTT reconnect is in progress, wait a few minutes before
856
+ // refreshing to avoid a race condition during session startup
857
+ if (this.mqttReconnecting) {
858
+ const THREE_MIN_MS = 3 * 60 * 1000;
859
+ const retryDelayMs = THREE_MIN_MS + Math.floor(Math.random() * THREE_MIN_MS);
860
+ this.log.info(
861
+ 'StbPlatform: channel list refresh deferred - MQTT reconnect in progress, retrying in a few minutes',
862
+ );
863
+ this.checkChannelListTimeout = setTimeout(async () => {
864
+ if (this.isShuttingDown) return;
865
+ await this._refreshChannelList();
866
+ this._scheduleNightlyChannelListRefresh();
867
+ }, retryDelayMs);
868
+ return;
869
+ }
870
+
853
871
  await this._refreshChannelList();
854
872
  this._scheduleNightlyChannelListRefresh(); // reschedule for the next day
855
873
  }, msUntilRefresh);
856
874
  } // end of _scheduleNightlyChannelListRefresh
857
875
 
876
+ /**
877
+ * Schedule the next nightly MQTT reconnect.
878
+ * Picks a random time between 04:00 and 06:00 the following day
879
+ * to avoid overlapping with the channel list refresh (00:00–04:00).
880
+ * Reschedules itself so the pattern repeats indefinitely.
881
+ */
882
+ _scheduleNightlyMqttReconnect() {
883
+ const tomorrow = new Date();
884
+ tomorrow.setDate(tomorrow.getDate() + 1);
885
+ tomorrow.setHours(4, 0, 0, 0);
886
+
887
+ const TWO_HOURS_MS = 2 * 60 * 60 * 1000;
888
+ const randomOffsetMs = Math.floor(Math.random() * TWO_HOURS_MS);
889
+
890
+ const nextReconnectAt = new Date(tomorrow.getTime() + randomOffsetMs);
891
+ const msUntilReconnect = nextReconnectAt.getTime() - Date.now();
892
+
893
+ if (this.debugLevel > 0) {
894
+ this.log.warn(
895
+ `StbPlatform: next nightly MQTT reconnect scheduled for ${nextReconnectAt.toLocaleString()}`,
896
+ );
897
+ }
898
+
899
+ this.mqttReconnectTimeout = setTimeout(async () => {
900
+ if (this.isShuttingDown) return;
901
+ await this._attemptNightlyMqttReconnect();
902
+ // _attemptNightlyMqttReconnect reschedules for the next night once done
903
+ }, msUntilReconnect);
904
+ } // end of _scheduleNightlyMqttReconnect
905
+
906
+
907
+ /**
908
+ * Attempt the nightly MQTT reconnect.
909
+ * If any STB is currently online (user may be watching), defers by 1 hour
910
+ * plus a random offset and tries again, rather than interrupting the session.
911
+ * Once the reconnect completes (or fails), reschedules for the next night.
912
+ * Retries 3 times then gives up, and the next reconnect will be the next day.
913
+ */
914
+ async _attemptNightlyMqttReconnect(retryCount = 0) {
915
+ if (this.isShuttingDown) return;
916
+
917
+ const MAX_RETRIES = 3; // give up after 3 deferrals (~3-4.5 hours past 04:00)
918
+
919
+ // check if any STB is currently active - if so, defer to avoid
920
+ // interrupting a user who may be watching TV and using the remote
921
+ const anyStbOnline = this.devices.some(
922
+ (device) => device.currentPowerState === Characteristic.Active.ACTIVE,
923
+ );
924
+
925
+ if (anyStbOnline) {
926
+ // give up if max retries reached
927
+ if (retryCount >= MAX_RETRIES) {
928
+ this.log.info(
929
+ 'StbPlatform: nightly MQTT reconnect skipped - STB still active after max retries, rescheduling for next night',
930
+ );
931
+ this._scheduleNightlyMqttReconnect();
932
+ return;
933
+ }
934
+
935
+ // retry in 1 hour plus a random 0–30 min buffer
936
+ const ONE_HOUR_MS = 60 * 60 * 1000;
937
+ const THIRTY_MIN_MS = 30 * 60 * 1000;
938
+ const retryDelayMs = ONE_HOUR_MS + Math.floor(Math.random() * THIRTY_MIN_MS);
939
+ const retryAt = new Date(Date.now() + retryDelayMs);
940
+
941
+ this.log.info(
942
+ `StbPlatform: nightly MQTT reconnect deferred - STB is active (attempt ${retryCount + 1}/${MAX_RETRIES}). Retrying at ${retryAt.toLocaleString()}`,
943
+ );
944
+
945
+ // store handle so shutdown can cancel the deferred retry too
946
+ this.mqttReconnectTimeout = setTimeout(async () => {
947
+ if (this.isShuttingDown) return;
948
+ await this._attemptNightlyMqttReconnect(retryCount + 1);
949
+ }, retryDelayMs);
950
+ return; // don't reschedule for next night yet - that happens after a successful reconnect
951
+ }
952
+
953
+ // no STB is active - safe to reconnect
954
+ try {
955
+ this.mqttReconnecting = true; // signal to channel list refresh to pause
956
+ this.log.info('StbPlatform: nightly MQTT reconnect starting...');
957
+ await this.endMqttSession();
958
+ await this.startMqttClient();
959
+ this.log.info('StbPlatform: nightly MQTT reconnect completed');
960
+ } catch (err) {
961
+ this.log.error('StbPlatform: nightly MQTT reconnect failed:', err.message);
962
+ } finally {
963
+ this.mqttReconnecting = false; // always clear the flag, even on failure
964
+ }
965
+
966
+ this._scheduleNightlyMqttReconnect(); // reschedule for next night
967
+ } // end of _attemptNightlyMqttReconnect
968
+
858
969
  /**
859
970
  * _runFullStartupSequence
860
971
  *
@@ -4278,12 +4389,8 @@ class StbPlatform {
4278
4389
  // ------ device subscriptions ------
4279
4390
  // subscribe only to what we need
4280
4391
 
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
4392
  // householdId/mqttClientId: subscribe to own clientId to get data for ourselves
4286
- // subscribe to all devices after the setHgoOnlineRunning is sent
4393
+ // subscribe to all devices before the setHgoState is sent
4287
4394
  this.mqttSubscribeToTopic(
4288
4395
  householdId + "/" + this.mqttClient.options.clientId,
4289
4396
  ); // subscribe to our own mqttClientId to get all data
@@ -4320,32 +4427,36 @@ class StbPlatform {
4320
4427
  // reset so the 10-second retry fires correctly if the box doesn't respond
4321
4428
  this.lastMqttUiStatusMessageReceived = null;
4322
4429
 
4430
+ // announce ourselves as an active HGO client before requesting UI status
4431
+ // the STB uses this retained presence message to decide which clients to respond to
4432
+ this.setHgoState(householdId, this.mqttClient.options.clientId, 'ONLINE_RUNNING');
4433
+
4434
+ // request initial UI status for each device, with a short delay to allow
4435
+ // the STB to process the HGO presence announcement first
4323
4436
  // CPE.uiStatus messages are received via the householdId and mqttClientId
4324
4437
  // topics which are already subscribed above.
4325
4438
  // getUiStatus is called here to request the initial UI state from each device.
4326
4439
  // retain: false is used (see getUiStatus) so a retry is scheduled in case the box
4327
4440
  // is temporarily unreachable when the initial request is sent.
4328
- this.devices.forEach((device) => {
4329
- // request the initial UI status for each device
4330
- this.getUiStatus(device.deviceId, this.mqttClient.options.clientId);
4331
-
4332
- // retry getUiStatus after 10 seconds if no CPE.uiStatus response has arrived yet
4333
- // (handles case where box is briefly offline when the initial request is sent)
4334
- setTimeout(() => {
4335
- if (!this.lastMqttUiStatusMessageReceived) {
4336
- if (this.debugLevel > 0) {
4337
- this.log.warn(
4338
- "getUiStatus: no CPE.uiStatus received yet for %s, retrying",
4339
- device.deviceId,
4340
- );
4441
+ setTimeout(() => {
4442
+ this.devices.forEach((device) => {
4443
+ // request the initial UI status for each device
4444
+ this.getUiStatus(device.deviceId, this.mqttClient.options.clientId);
4445
+
4446
+ // retry after 10 seconds if no CPE.uiStatus response has arrived yet
4447
+ setTimeout(() => {
4448
+ if (!this.lastMqttUiStatusMessageReceived) {
4449
+ if (this.debugLevel > 0) {
4450
+ this.log.warn(
4451
+ "getUiStatus: no CPE.uiStatus received yet for %s, retrying",
4452
+ device.deviceId,
4453
+ );
4454
+ }
4455
+ this.getUiStatus(device.deviceId, this.mqttClient.options.clientId);
4341
4456
  }
4342
- this.getUiStatus(
4343
- device.deviceId,
4344
- this.mqttClient.options.clientId,
4345
- );
4346
- }
4347
- }, 10 * 1000); // 10 second retry delay
4348
- });
4457
+ }, 10 * 1000); // 10 second retry delay
4458
+ });
4459
+ }, 500); // 500ms for STB to register our HGO presence before we request status
4349
4460
 
4350
4461
  resolve(true); // all subscriptions registered — session is ready
4351
4462
  } catch (err) {
@@ -4462,7 +4573,7 @@ class StbPlatform {
4462
4573
  currMediaState = Characteristic.CurrentMediaState.PLAY;
4463
4574
  if (this.debugLevel > 0) {
4464
4575
  this.log.warn(
4465
- "mqttClient: STB status: power-on transition for %s, setting mediaState to PLAY",
4576
+ "mqttClient: STB status: Power-on transition detected for %s, setting mediaState to PLAY",
4466
4577
  deviceId,
4467
4578
  );
4468
4579
  }
@@ -4496,6 +4607,12 @@ class StbPlatform {
4496
4607
  this.log.warn("mqttClient: %s %s", deviceId, stbState);
4497
4608
  }
4498
4609
  }
4610
+
4611
+ // After the switch, if box is running, request current UI state
4612
+ //if (stbState === 'ONLINE_RUNNING') {
4613
+ // Small delay gives the STB a moment to settle before responding
4614
+ //setTimeout(() => this.mqttRequestUiStatus(deviceId), 500);
4615
+ //}
4499
4616
  }
4500
4617
 
4501
4618
  // handle CPE UI status messages for the STB
@@ -4878,16 +4995,26 @@ class StbPlatform {
4878
4995
  return resolve(true);
4879
4996
  }
4880
4997
 
4881
- // unsubscribe from all subscribedTopics before tearing down the session
4998
+ // get all subscribed topics
4882
4999
  const topics = this.subscribedTopics ?? [];
5000
+
5001
+ // announce HGO offline while the connection is still live, before any teardown
5002
+ this.setHgoState(
5003
+ this.session.householdId,
5004
+ this.mqttClient.options.clientId,
5005
+ 'OFFLINE',
5006
+ );
5007
+
5008
+ // unsubscribe from all subscribedTopics before tearing down the session
4883
5009
  if (topics.length === 0) {
4884
5010
  this.log.info(
4885
5011
  "mqttClient: No topics to unsubscribe from, skipping unsubscribe.",
4886
5012
  );
4887
- this.mqttClient.end(false, {}, (err) => {
4888
- if (err) {
4889
- this.log.error("MQTT end error:", err);
4890
- return reject(err);
5013
+
5014
+ this.mqttClient.end(false, {}, (endErr) => {
5015
+ if (endErr) {
5016
+ this.log.error("MQTT end error:", endErr);
5017
+ return reject(endErr);
4891
5018
  }
4892
5019
  this.log.info(
4893
5020
  "mqttClient: Disconnected cleanly. No topics found to unsubscribe from.",
@@ -4897,15 +5024,15 @@ class StbPlatform {
4897
5024
  return;
4898
5025
  }
4899
5026
 
4900
- this.mqttClient.unsubscribe(topics, (err) => {
4901
- if (err) {
4902
- this.log.error("MQTT unsubscribe error:", err);
5027
+ this.mqttClient.unsubscribe(topics, (unsubErr) => {
5028
+ if (unsubErr) {
5029
+ this.log.error("MQTT unsubscribe error:", unsubErr);
4903
5030
  // still attempt to end even if unsubscribe failed
4904
5031
  }
4905
- this.mqttClient.end(false, {}, (err) => {
4906
- if (err) {
4907
- this.log.error("MQTT end error:", err);
4908
- return reject(err);
5032
+ this.mqttClient.end(false, {}, (endErr) => {
5033
+ if (endErr) {
5034
+ this.log.error("MQTT end error:", endErr);
5035
+ return reject(endErr);
4909
5036
  }
4910
5037
  this.log.info(
4911
5038
  "mqttClient: Disconnected cleanly. All topics unsubscribed.",
@@ -4990,7 +5117,7 @@ class StbPlatform {
4990
5117
  "mqttPublishMessage: Publish Message:\r\nTopic: %s\r\nMessage: %s\r\nOptions: %s",
4991
5118
  Topic,
4992
5119
  Message,
4993
- Options,
5120
+ JSON.stringify(Options),
4994
5121
  );
4995
5122
  }
4996
5123
  this.mqttClient.publish(Topic, Message, Options, (err) => {
@@ -5081,23 +5208,25 @@ class StbPlatform {
5081
5208
  });
5082
5209
  }
5083
5210
 
5084
- // start the HGO session (switch on)
5085
- setHgoOnlineRunning(householdId, mqttClientId) {
5086
- // {"source":"fd29b575-5f2b-49a0-8efe-62a844ac2b40","state":"ONLINE_RUNNING","deviceType":"HGO","mac":"","ipAddress":""}
5211
+ // set the HGO session state (online or offline)
5212
+ // called on mqtt connect (ONLINE_RUNNING) and on mqtt disconnect (OFFLINE)
5213
+ // retain: true ensures the broker overwrites any previous retained state
5214
+ setHgoState(householdId, mqttClientId, state) {
5087
5215
  const topic = `${householdId}/${mqttClientId}/status`;
5088
5216
  const message = JSON.stringify({
5089
5217
  source: mqttClientId,
5090
- state: "ONLINE_RUNNING",
5091
- deviceType: "HGO",
5092
- mac: "",
5093
- ipAddress: "",
5218
+ state: state,
5219
+ deviceType: 'HGO',
5220
+ mac: '',
5221
+ ipAddress: '',
5094
5222
  });
5095
5223
  if (this.debugLevel > 0) {
5096
- this.log.warn("setHgoOnlineRunning: publishing to topic:", topic);
5224
+ this.log.warn('setHgoState: publishing %s to topic: %s', state, topic);
5097
5225
  }
5098
5226
  this.mqttPublishMessage(topic, message, { qos: 2, retain: true });
5099
5227
  }
5100
5228
 
5229
+
5101
5230
  // send a channel change request to the settopbox via mqtt
5102
5231
  // using the CPE.pushToTV message
5103
5232
  // the friendlyDeviceName appears on the TV in a popup window
@@ -5151,6 +5280,37 @@ class StbPlatform {
5151
5280
  }
5152
5281
  }
5153
5282
 
5283
+ // Request the current UI status from the STB.
5284
+ // The STB responds with a CPE.uiStatus message on the household channel.
5285
+ // @param {string} deviceId - The STB device ID (e.g. "000378-EOS2STB-00852052xxxx")
5286
+ mqttRequestUiStatus(deviceId) {
5287
+ if (!this.mqttClient?.connected) {
5288
+ this.log.warn('%s: mqttRequestUiStatus: MQTT not connected, skipping', deviceId);
5289
+ return;
5290
+ }
5291
+ if (this.debugLevel > 0) {
5292
+ this.log.warn(
5293
+ "mqttRequestUiStatus: Requesting UI status for %s",
5294
+ deviceId,
5295
+ );
5296
+ }
5297
+
5298
+ const payload = JSON.stringify({
5299
+ version: '1.3.18',
5300
+ type: 'CPE.pullFromTV',
5301
+ source: this.mqttClient.options.clientId, // your mqttClientId
5302
+ messageTimeStamp: Date.now(),
5303
+ });
5304
+
5305
+ const topic = `${this.session.householdId}/${deviceId}`;
5306
+
5307
+ this.mqttPublishMessage(topic, payload, {
5308
+ qos: 1,
5309
+ retain: false,
5310
+ });
5311
+
5312
+ }
5313
+
5154
5314
  // set the media state of the settopbox via mqtt
5155
5315
  // media state is controlled by speedRate
5156
5316
  // speedRate can be one of: -64 -30 -6 -2 0 2 6 30 64. 0=Paused, 1=Play, >1=FastForward, <0=Rewind
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.1",
6
+ "version": "2.4.0-beta.2",
7
7
  "platformname": "eosstb",
8
8
  "dependencies": {
9
9
  "axios": "^1.16.0",