homebridge-denon-tv 6.2.3 → 6.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -21,6 +21,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
21
21
  - For plugin < v6.1.0 use Homebridge UI <= v5.5.0
22
22
  - For plugin >= v6.1.0 use Homebridge UI >= v5.13.0
23
23
 
24
+ ## [6.2.5] - (24.09.2026)
25
+
26
+ ### Changes
27
+
28
+ - fix: with 85 inputs (the HomeKit limit) the input list from the receiver no longer updated existing inputs, the limit skipped every input instead of only new ones, so names of existing inputs were never refreshed. The limit now applies to new inputs only
29
+
30
+ ## [6.2.4] - (24.09.2026)
31
+
32
+ ### Changes
33
+
34
+ - fix: with more than one zone configured for the same receiver, every zone overwrote the shared receiver capabilities. The Main Zone lost sound mode and picture mode support (sound mode was no longer read, so Home Assistant did not show the current sound mode) and could report the name of another zone. Each zone now works on its own copy of the receiver capabilities
35
+
24
36
  ## [6.2.3] - (24.09.2026)
25
37
 
26
38
  ### Changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "displayName": "Denon TV",
3
3
  "name": "homebridge-denon-tv",
4
- "version": "6.2.3",
4
+ "version": "6.2.5",
5
5
  "description": "Homebridge plugin to control Denon/Marantz AV Receivers.",
6
6
  "license": "MIT",
7
7
  "author": "grzegorz914",
@@ -20,12 +20,14 @@ class HaDiscovery {
20
20
  this.configTopic = `${mqtt.haPrefix}/media_player/${this.objectId}/config`;
21
21
  this.stateTopic = `${mqtt.config.prefix}/HA State`;
22
22
  this.commandTopic = `${mqtt.config.prefix}/Set`;
23
+ this.imageTopic = config.image ? `${mqtt.config.prefix}/HA Image` : null;
23
24
 
24
25
  this.sources = [];
25
26
  this.soundModes = [];
26
27
  this.state = {};
27
28
  this.lastConfig = '';
28
29
  this.lastState = '';
30
+ this.lastImageKey = undefined;
29
31
  }
30
32
 
31
33
  // Publish (or republish when changed) the retained discovery message
@@ -45,6 +47,7 @@ class HaDiscovery {
45
47
  state_topic: this.stateTopic,
46
48
  command_topic: this.commandTopic,
47
49
  availability_topic: this.mqtt.availabilityTopic,
50
+ ...(this.imageTopic ? { image_topic: this.imageTopic } : {}),
48
51
  device,
49
52
  commands: this.commands,
50
53
  sources: this.sources,
@@ -58,6 +61,28 @@ class HaDiscovery {
58
61
  return true;
59
62
  }
60
63
 
64
+ // Publish the image of the current source (app icon, channel picon) once per source.
65
+ // key identifies the image, fetchImage returns a Buffer or null, null clears the image
66
+ async updateImage(key, fetchImage) {
67
+ if (!this.imageTopic || key === this.lastImageKey) return false;
68
+ this.lastImageKey = key;
69
+
70
+ let image = null;
71
+ try {
72
+ image = key ? await fetchImage() : null;
73
+ } catch {
74
+ // Network or device error, allow a retry on the next state update
75
+ image = null;
76
+ if (key === this.lastImageKey) this.lastImageKey = undefined;
77
+ return false;
78
+ }
79
+
80
+ // The source changed again while fetching, a newer call publishes its own image
81
+ if (key !== this.lastImageKey) return false;
82
+ await this.mqtt.publishRetained(this.imageTopic, image ?? '');
83
+ return true;
84
+ }
85
+
61
86
  // Merge a partial state and publish it retained when something changed
62
87
  async updateState(partial) {
63
88
  for (const [key, value] of Object.entries(partial)) {
package/src/mainzone.js CHANGED
@@ -227,8 +227,6 @@ class MainZone extends EventEmitter {
227
227
  let updated = false;
228
228
 
229
229
  for (const input of inputs) {
230
- if (this.inputsServices.length >= 85 && !remove) continue;
231
-
232
230
  const inputReference = input.reference;
233
231
  const savedName = this.savedInputsNames[inputReference] ?? input.name;
234
232
  const sanitizedName = await this.functions.sanitizeString(savedName);
@@ -259,6 +257,9 @@ class MainZone extends EventEmitter {
259
257
  updated = true;
260
258
  }
261
259
  } else {
260
+ // HomeKit allows at most 85 inputs, the limit applies to new inputs only, existing ones are still updated
261
+ if (this.inputsServices.length >= 85) continue;
262
+
262
263
  const identifier = this.inputsServices.length + 1;
263
264
  inputService = this.accessory.addService(Service.InputSource, sanitizedName, `Input ${inputReference}`);
264
265
  inputService.identifier = identifier;
package/src/mqtt.js CHANGED
@@ -110,7 +110,7 @@ class Mqtt extends EventEmitter {
110
110
  if (this.config.logWarn) this.emit('warn', `MQTT Publish error: ${error.message}`);
111
111
  return reject(error);
112
112
  }
113
- if (this.config.logDebug) this.emit('debug', `MQTT Publish retained Topic: ${fullTopic}, Payload: ${payload}`);
113
+ if (this.config.logDebug) this.emit('debug', `MQTT Publish retained Topic: ${fullTopic}, Payload: ${typeof payload === 'string' ? payload : `${payload.length} bytes`}`);
114
114
  resolve();
115
115
  });
116
116
  });
@@ -107,8 +107,6 @@ class PassThroughInputs extends EventEmitter {
107
107
  let updated = false;
108
108
 
109
109
  for (const input of inputs) {
110
- if (this.inputsServices.length >= 85 && !remove) continue;
111
-
112
110
  const inputReference = input.reference;
113
111
  const savedName = this.savedInputsNames[inputReference] ?? input.name;
114
112
  const sanitizedName = await this.functions.sanitizeString(savedName);
@@ -139,6 +137,9 @@ class PassThroughInputs extends EventEmitter {
139
137
  updated = true;
140
138
  }
141
139
  } else {
140
+ // HomeKit allows at most 85 inputs, the limit applies to new inputs only, existing ones are still updated
141
+ if (this.inputsServices.length >= 85) continue;
142
+
142
143
  const identifier = this.inputsServices.length + 1;
143
144
  inputService = this.accessory.addService(Service.InputSource, sanitizedName, `Input ${inputReference}`);
144
145
  inputService.identifier = identifier;
package/src/surrounds.js CHANGED
@@ -118,8 +118,6 @@ class Surrounds extends EventEmitter {
118
118
  let updated = false;
119
119
 
120
120
  for (const input of inputs) {
121
- if (this.inputsServices.length >= 85 && !remove) continue;
122
-
123
121
  const inputReference = input.reference;
124
122
  const savedName = this.savedInputsNames[inputReference] ?? input.name;
125
123
  const sanitizedName = await this.functions.sanitizeString(savedName);
@@ -150,6 +148,9 @@ class Surrounds extends EventEmitter {
150
148
  updated = true;
151
149
  }
152
150
  } else {
151
+ // HomeKit allows at most 85 inputs, the limit applies to new inputs only, existing ones are still updated
152
+ if (this.inputsServices.length >= 85) continue;
153
+
153
154
  const identifier = this.inputsServices.length + 1;
154
155
  inputService = this.accessory.addService(Service.InputSource, sanitizedName, `Input ${inputReference}`);
155
156
  inputService.identifier = identifier;
package/src/zone.js CHANGED
@@ -261,10 +261,15 @@ class Zone extends EventEmitter {
261
261
 
262
262
  async checkInfo(denonInfo) {
263
263
  try {
264
+ // All zones of one receiver share the same denonInfo object. Keep the receiver capabilities
265
+ // untouched and work on a copy per zone, otherwise every zone overwrites the flags of the others
266
+ const deviceInfo = denonInfo.deviceInfo ??= { ...denonInfo.info };
267
+ const info = { ...deviceInfo };
268
+
264
269
  // Capabilities
265
- denonInfo.info.supportPictureMode = this.zoneControl === 0 && denonInfo.info.supportPictureMode;
266
- denonInfo.info.supportSoundMode = (this.zoneControl === 0 || this.zoneControl === 3) && denonInfo.info.supportSoundMode;
267
- denonInfo.info.supportFavorites = this.zoneControl < 3 && denonInfo.info.supportFavorites;
270
+ info.supportPictureMode = this.zoneControl === 0 && deviceInfo.supportPictureMode;
271
+ info.supportSoundMode = (this.zoneControl === 0 || this.zoneControl === 3) && deviceInfo.supportSoundMode;
272
+ info.supportFavorites = this.zoneControl < 3 && deviceInfo.supportFavorites;
268
273
 
269
274
  // Zone capabilities
270
275
  const keys = Object.keys(denonInfo);
@@ -274,14 +279,14 @@ class Zone extends EventEmitter {
274
279
  const zone = this.zoneControl === 4 ? 0 : this.zoneControl;
275
280
  zoneCaps = zones[zone] || {};
276
281
  }
277
- denonInfo.info.supportShortcuts = zoneCaps.ShortcutControl?.Control === 1;
278
- denonInfo.info.supportQuickSelect = zoneCaps.Operation?.QuickSelect?.Control === 1;
279
- denonInfo.info.controlZone = ZoneName[this.zoneControl];
280
- this.info = denonInfo.info;
282
+ info.supportShortcuts = zoneCaps.ShortcutControl?.Control === 1;
283
+ info.supportQuickSelect = zoneCaps.Operation?.QuickSelect?.Control === 1;
284
+ info.controlZone = ZoneName[this.zoneControl];
285
+ this.info = info;
281
286
 
282
287
  // Success event
283
288
  if (this.firstRun) {
284
- this.emit('deviceInfo', denonInfo.info);
289
+ this.emit('deviceInfo', info);
285
290
  this.firstRun = false;
286
291
  }
287
292
 
@@ -295,7 +300,7 @@ class Zone extends EventEmitter {
295
300
  2: inputsNewDevice
296
301
  };
297
302
  const inputs = this.getInputsFromDevice ? inputsMap[this.generation] : this.inputs;
298
- const allInputs = await this.prepareInputs(denonInfo, this.generation, this.zoneControl, inputs, zoneCaps, this.getInputsFromDevice, this.getFavoritesFromDevice, this.getQuickSmartSelectFromDevice, denonInfo.info.supportFavorites, denonInfo.info.supportShortcuts, denonInfo.info.supportQuickSelect);
303
+ const allInputs = await this.prepareInputs(denonInfo, this.generation, this.zoneControl, inputs, zoneCaps, this.getInputsFromDevice, this.getFavoritesFromDevice, this.getQuickSmartSelectFromDevice, info.supportFavorites, info.supportShortcuts, info.supportQuickSelect);
299
304
 
300
305
  // Emit inputs
301
306
  this.emit('addRemoveOrUpdateInput', allInputs, false);
package/src/zone2.js CHANGED
@@ -225,8 +225,6 @@ class Zone2 extends EventEmitter {
225
225
  let updated = false;
226
226
 
227
227
  for (const input of inputs) {
228
- if (this.inputsServices.length >= 85 && !remove) continue;
229
-
230
228
  const inputReference = input.reference;
231
229
  const savedName = this.savedInputsNames[inputReference] ?? input.name;
232
230
  const sanitizedName = await this.functions.sanitizeString(savedName);
@@ -257,6 +255,9 @@ class Zone2 extends EventEmitter {
257
255
  updated = true;
258
256
  }
259
257
  } else {
258
+ // HomeKit allows at most 85 inputs, the limit applies to new inputs only, existing ones are still updated
259
+ if (this.inputsServices.length >= 85) continue;
260
+
260
261
  const identifier = this.inputsServices.length + 1;
261
262
  inputService = this.accessory.addService(Service.InputSource, sanitizedName, `Input ${inputReference}`);
262
263
  inputService.identifier = identifier;
package/src/zone3.js CHANGED
@@ -225,8 +225,6 @@ class Zone3 extends EventEmitter {
225
225
  let updated = false;
226
226
 
227
227
  for (const input of inputs) {
228
- if (this.inputsServices.length >= 85 && !remove) continue;
229
-
230
228
  const inputReference = input.reference;
231
229
  const savedName = this.savedInputsNames[inputReference] ?? input.name;
232
230
  const sanitizedName = await this.functions.sanitizeString(savedName);
@@ -257,6 +255,9 @@ class Zone3 extends EventEmitter {
257
255
  updated = true;
258
256
  }
259
257
  } else {
258
+ // HomeKit allows at most 85 inputs, the limit applies to new inputs only, existing ones are still updated
259
+ if (this.inputsServices.length >= 85) continue;
260
+
260
261
  const identifier = this.inputsServices.length + 1;
261
262
  inputService = this.accessory.addService(Service.InputSource, sanitizedName, `Input ${inputReference}`);
262
263
  inputService.identifier = identifier;