homebridge-denon-tv 6.2.4 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -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.3.0] - (25.09.2026)
25
+
26
+ ### Changes
27
+
28
+ - added: Home Assistant discovery shows an icon of the current input (TV, Blu-ray, game, tuner, Bluetooth, Spotify, AUX, quick select and more) for Main Zone, Zone 2 and Zone 3. The receiver has no input icons, the plugin bundles icons from Material Design Icons (Apache 2.0), published on the `HA Image` topic
29
+
30
+ ## [6.2.5] - (24.09.2026)
31
+
32
+ ### Changes
33
+
34
+ - 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
35
+
24
36
  ## [6.2.4] - (24.09.2026)
25
37
 
26
38
  ### Changes
package/README.md CHANGED
@@ -172,6 +172,7 @@
172
172
  * `homeassistant/media_player/<id>/config` - discovery message.
173
173
  * `HA State` - `{"power": true, "volume": 49, "muted": false, "source": "SITV", "sound_mode": "MOVIE"}`.
174
174
  * `Availability` - `online`, `offline` (last will).
175
+ * `HA Image` - PNG icon of the current input (TV, Blu-ray, game, tuner, Bluetooth and more), shown by Home Assistant as the source image. The receiver has no input icons, the plugin bundles icons from [Material Design Icons](https://pictogrammers.com/library/mdi/) (Apache 2.0).
175
176
 
176
177
  | Method | Topic | Message | Type |
177
178
  | --- | --- | --- | --- |
package/icons/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Input icons rendered to PNG from Material Design Icons 7.4.47 by Pictogrammers (https://pictogrammers.com/library/mdi/).
2
+
3
+ Pictogrammers Free License
4
+ --------------------------
5
+
6
+ This icon collection is released as free, open source, and GPL friendly by
7
+ the [Pictogrammers](http://pictogrammers.com/) icon group. You may use it
8
+ for commercial projects, open source projects, or anything really.
9
+
10
+ # Icons: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
11
+ Some of the icons are redistributed under the Apache 2.0 license. All other
12
+ icons are either redistributed under their respective licenses or are
13
+ distributed under the Apache 2.0 license.
14
+
15
+ # Fonts: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
16
+ All web and desktop fonts are distributed under the Apache 2.0 license. Web
17
+ and desktop fonts contain some icons that are redistributed under the Apache
18
+ 2.0 license. All other icons are either redistributed under their respective
19
+ licenses or are distributed under the Apache 2.0 license.
20
+
21
+ # Code: MIT (https://opensource.org/licenses/MIT)
22
+ The MIT license applies to all non-font and non-icon files.
Binary file
Binary file
Binary file
package/icons/disc.png ADDED
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/icons/lan.png ADDED
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/icons/star.png ADDED
Binary file
Binary file
package/icons/usb.png ADDED
Binary file
Binary file
Binary file
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.4",
4
+ "version": "6.3.0",
5
5
  "description": "Homebridge plugin to control Denon/Marantz AV Receivers.",
6
6
  "license": "MIT",
7
7
  "author": "grzegorz914",
@@ -22,6 +22,7 @@
22
22
  },
23
23
  "files": [
24
24
  "src",
25
+ "icons",
25
26
  "index.js",
26
27
  "config.schema.json",
27
28
  "package.json",
@@ -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)) {
@@ -0,0 +1,77 @@
1
+ import { readFile } from 'fs/promises';
2
+
3
+ // Icons of the receiver inputs for Home Assistant, the receiver does not provide any.
4
+ // Material Design Icons (Apache 2.0, https://pictogrammers.com) rendered to PNG in /icons
5
+ const IconsDir = new URL('../icons/', import.meta.url);
6
+
7
+ const InputIcons = {
8
+ 'PHONO': 'record-player',
9
+ 'CD': 'disc',
10
+ 'CDR': 'disc',
11
+ 'DVD': 'disc-player',
12
+ 'BD': 'disc-player',
13
+ 'HDP': 'disc-player',
14
+ 'TV': 'television',
15
+ 'SAT/CBL': 'satellite-variant',
16
+ 'SAT': 'satellite-variant',
17
+ 'CBL': 'satellite-variant',
18
+ 'MPLAY': 'play-circle',
19
+ 'OTP': 'play-circle',
20
+ 'GAME': 'gamepad-variant',
21
+ 'GAME1': 'gamepad-variant',
22
+ 'GAME2': 'gamepad-variant',
23
+ 'TUNER': 'radio',
24
+ 'HDRADIO': 'radio',
25
+ 'DVR': 'harddisk',
26
+ 'VCR': 'video-vintage',
27
+ 'DOCK': 'dock-window',
28
+ 'NET/USB': 'usb',
29
+ 'USB/IPOD': 'usb',
30
+ 'USB': 'usb',
31
+ 'IPD': 'usb',
32
+ 'NET': 'lan',
33
+ 'BT': 'bluetooth',
34
+ '8K': 'video-input-hdmi',
35
+ 'DIGITALIN1': 'audio-input-rca',
36
+ 'IRADIO': 'radio-tower',
37
+ 'IRP': 'radio-tower',
38
+ 'SERVER': 'server-network',
39
+ 'SPOTIFY': 'spotify',
40
+ 'SPOTIFYCONNECT': 'spotify',
41
+ 'FLICKR': 'image-multiple',
42
+ 'SIRIUS': 'music-circle',
43
+ 'SIRIUSXM': 'music-circle',
44
+ 'RHAPSODY': 'music-circle',
45
+ 'PANDORA': 'music-circle',
46
+ 'NAPSTER': 'music-circle',
47
+ 'LASTFM': 'music-circle',
48
+ 'FAVORITES': 'star',
49
+ 'FVP': 'star',
50
+ 'SOURCE': 'speaker'
51
+ };
52
+
53
+ class InputIconsStore {
54
+ constructor() {
55
+ this.cache = new Map();
56
+ }
57
+
58
+ // Icon file name of an input reference, AUX and quick / smart / favorite selects by prefix, others the generic input icon
59
+ iconName(reference) {
60
+ const ref = String(reference ?? '').toUpperCase().replace(/ MEMORY$/, '');
61
+ if (InputIcons[ref]) return InputIcons[ref];
62
+ if (/^(AUX|V\.AUX|M-XPORT)/.test(ref)) return 'audio-input-stereo-minijack';
63
+ if (/^FAVORITE\d/.test(ref)) return 'star';
64
+ const select = ref.match(/^(QUICK|SMART)([1-5])$/);
65
+ if (select) return `numeric-${select[2]}-circle`;
66
+ return 'import';
67
+ }
68
+
69
+ // PNG bytes of the input icon, read once and cached
70
+ async get(reference) {
71
+ const name = this.iconName(reference);
72
+ if (!this.cache.has(name)) this.cache.set(name, await readFile(new URL(`${name}.png`, IconsDir)).catch(() => null));
73
+ return this.cache.get(name);
74
+ }
75
+ }
76
+
77
+ export default new InputIconsStore();
package/src/mainzone.js CHANGED
@@ -2,6 +2,7 @@ import EventEmitter from 'events';
2
2
  import Zone from './zone.js';
3
3
  import Functions from './functions.js';
4
4
  import HaDiscovery from './hadiscovery.js';
5
+ import InputIcons from './inputicons.js';
5
6
  import { PictureModesConversionToHomeKit, PictureModesDenonNumber, PictureModesDenonString, DirectSoundMode, SoundModeConversion, SoundModeDisplayName } from './constants.js';
6
7
  let Accessory, Characteristic, Service, Categories, Encode, AccessoryUUID;
7
8
 
@@ -227,8 +228,6 @@ class MainZone extends EventEmitter {
227
228
  let updated = false;
228
229
 
229
230
  for (const input of inputs) {
230
- if (this.inputsServices.length >= 85 && !remove) continue;
231
-
232
231
  const inputReference = input.reference;
233
232
  const savedName = this.savedInputsNames[inputReference] ?? input.name;
234
233
  const sanitizedName = await this.functions.sanitizeString(savedName);
@@ -259,6 +258,9 @@ class MainZone extends EventEmitter {
259
258
  updated = true;
260
259
  }
261
260
  } else {
261
+ // HomeKit allows at most 85 inputs, the limit applies to new inputs only, existing ones are still updated
262
+ if (this.inputsServices.length >= 85) continue;
263
+
262
264
  const identifier = this.inputsServices.length + 1;
263
265
  inputService = this.accessory.addService(Service.InputSource, sanitizedName, `Input ${inputReference}`);
264
266
  inputService.identifier = identifier;
@@ -979,6 +981,7 @@ class MainZone extends EventEmitter {
979
981
  objectId: `denon_${this.savedInfo.serialNumber}_zone${this.zoneControl}`,
980
982
  name: this.name,
981
983
  deviceClass: 'receiver',
984
+ image: true,
982
985
  device: {
983
986
  manufacturer: this.savedInfo.manufacturer,
984
987
  model: this.savedInfo.modelName,
@@ -1024,6 +1027,10 @@ class MainZone extends EventEmitter {
1024
1027
  source: input ? `${input.zonePrefix}${input.reference}` : this.reference,
1025
1028
  sound_mode: this.ha.commands.sound_mode ? this.zone.soundMode || undefined : undefined
1026
1029
  });
1030
+
1031
+ // Icon of the current input, bundled with the plugin
1032
+ const reference = this.power ? this.reference || null : null;
1033
+ this.ha.updateImage(reference, () => InputIcons.get(reference)).catch(() => { });
1027
1034
  } catch (error) {
1028
1035
  if (this.logWarn) this.emit('warn', `HA Discovery state error: ${error}`);
1029
1036
  }
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/zone2.js CHANGED
@@ -2,6 +2,7 @@ import EventEmitter from 'events';
2
2
  import Zone from './zone.js';
3
3
  import Functions from './functions.js';
4
4
  import HaDiscovery from './hadiscovery.js';
5
+ import InputIcons from './inputicons.js';
5
6
  import { PictureModesConversionToHomeKit, PictureModesDenonNumber, SoundModeConversion, SoundModeDisplayName } from './constants.js';
6
7
  let Accessory, Characteristic, Service, Categories, Encode, AccessoryUUID;
7
8
 
@@ -225,8 +226,6 @@ class Zone2 extends EventEmitter {
225
226
  let updated = false;
226
227
 
227
228
  for (const input of inputs) {
228
- if (this.inputsServices.length >= 85 && !remove) continue;
229
-
230
229
  const inputReference = input.reference;
231
230
  const savedName = this.savedInputsNames[inputReference] ?? input.name;
232
231
  const sanitizedName = await this.functions.sanitizeString(savedName);
@@ -257,6 +256,9 @@ class Zone2 extends EventEmitter {
257
256
  updated = true;
258
257
  }
259
258
  } else {
259
+ // HomeKit allows at most 85 inputs, the limit applies to new inputs only, existing ones are still updated
260
+ if (this.inputsServices.length >= 85) continue;
261
+
260
262
  const identifier = this.inputsServices.length + 1;
261
263
  inputService = this.accessory.addService(Service.InputSource, sanitizedName, `Input ${inputReference}`);
262
264
  inputService.identifier = identifier;
@@ -893,6 +895,7 @@ class Zone2 extends EventEmitter {
893
895
  objectId: `denon_${this.savedInfo.serialNumber}_zone${this.zoneControl}`,
894
896
  name: this.name,
895
897
  deviceClass: 'receiver',
898
+ image: true,
896
899
  device: {
897
900
  manufacturer: this.savedInfo.manufacturer,
898
901
  model: this.savedInfo.modelName,
@@ -938,6 +941,10 @@ class Zone2 extends EventEmitter {
938
941
  source: input ? `${input.zonePrefix}${input.reference}` : this.reference,
939
942
  sound_mode: this.ha.commands.sound_mode ? this.zone.soundMode || undefined : undefined
940
943
  });
944
+
945
+ // Icon of the current input, bundled with the plugin
946
+ const reference = this.power ? this.reference || null : null;
947
+ this.ha.updateImage(reference, () => InputIcons.get(reference)).catch(() => { });
941
948
  } catch (error) {
942
949
  if (this.logWarn) this.emit('warn', `HA Discovery state error: ${error}`);
943
950
  }
package/src/zone3.js CHANGED
@@ -2,6 +2,7 @@ import EventEmitter from 'events';
2
2
  import Zone from './zone.js';
3
3
  import Functions from './functions.js';
4
4
  import HaDiscovery from './hadiscovery.js';
5
+ import InputIcons from './inputicons.js';
5
6
  import { PictureModesConversionToHomeKit, PictureModesDenonNumber, SoundModeConversion, SoundModeDisplayName } from './constants.js';
6
7
  let Accessory, Characteristic, Service, Categories, Encode, AccessoryUUID;
7
8
 
@@ -225,8 +226,6 @@ class Zone3 extends EventEmitter {
225
226
  let updated = false;
226
227
 
227
228
  for (const input of inputs) {
228
- if (this.inputsServices.length >= 85 && !remove) continue;
229
-
230
229
  const inputReference = input.reference;
231
230
  const savedName = this.savedInputsNames[inputReference] ?? input.name;
232
231
  const sanitizedName = await this.functions.sanitizeString(savedName);
@@ -257,6 +256,9 @@ class Zone3 extends EventEmitter {
257
256
  updated = true;
258
257
  }
259
258
  } else {
259
+ // HomeKit allows at most 85 inputs, the limit applies to new inputs only, existing ones are still updated
260
+ if (this.inputsServices.length >= 85) continue;
261
+
260
262
  const identifier = this.inputsServices.length + 1;
261
263
  inputService = this.accessory.addService(Service.InputSource, sanitizedName, `Input ${inputReference}`);
262
264
  inputService.identifier = identifier;
@@ -893,6 +895,7 @@ class Zone3 extends EventEmitter {
893
895
  objectId: `denon_${this.savedInfo.serialNumber}_zone${this.zoneControl}`,
894
896
  name: this.name,
895
897
  deviceClass: 'receiver',
898
+ image: true,
896
899
  device: {
897
900
  manufacturer: this.savedInfo.manufacturer,
898
901
  model: this.savedInfo.modelName,
@@ -938,6 +941,10 @@ class Zone3 extends EventEmitter {
938
941
  source: input ? `${input.zonePrefix}${input.reference}` : this.reference,
939
942
  sound_mode: this.ha.commands.sound_mode ? this.zone.soundMode || undefined : undefined
940
943
  });
944
+
945
+ // Icon of the current input, bundled with the plugin
946
+ const reference = this.power ? this.reference || null : null;
947
+ this.ha.updateImage(reference, () => InputIcons.get(reference)).catch(() => { });
941
948
  } catch (error) {
942
949
  if (this.logWarn) this.emit('warn', `HA Discovery state error: ${error}`);
943
950
  }