homebridge-denon-tv 6.2.1 → 6.2.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 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.3] - (24.09.2026)
25
+
26
+ ### Changes
27
+
28
+ - changed: HA Discovery, sound modes are shown with readable names (e.g. `Pure Direct`, `DTS Surround` instead of `PURE DIRECT`, `DTS SURROUND`). Commands are unchanged
29
+
30
+ ## [6.2.2] - (24.09.2026)
31
+
32
+ ### Changes
33
+
34
+ - added: RESTFul request rate limit, 600 requests per minute per client and at most 1000 clients per minute, answered with `429` and `Retry-After`, one log warning per client and minute.
35
+
24
36
  ## [6.2.1] - (23.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.1",
4
+ "version": "6.2.3",
5
5
  "description": "Homebridge plugin to control Denon/Marantz AV Receivers.",
6
6
  "license": "MIT",
7
7
  "author": "grzegorz914",
package/src/constants.js CHANGED
@@ -661,3 +661,32 @@ export const ZonePrefixMap = {
661
661
  3: 'MS',
662
662
  4: 'SI'
663
663
  };
664
+
665
+ // Display names for the normalized sound modes (Home Assistant)
666
+ export const SoundModeDisplayName = {
667
+ "AUTO": "Auto",
668
+ "MOVIE": "Movie",
669
+ "MUSIC": "Music",
670
+ "GAME": "Game",
671
+ "DIRECT": "Direct",
672
+ "PURE DIRECT": "Pure Direct",
673
+ "STEREO": "Stereo",
674
+ "MCH STEREO": "Multi Ch Stereo",
675
+ "DOLBY DIGITAL": "Dolby Digital",
676
+ "DTS SURROUND": "DTS Surround",
677
+ "AURO3D": "Auro-3D",
678
+ "AURO2DSURR": "Auro-2D Surround",
679
+ "WIDE SCREEN": "Wide Screen",
680
+ "SUPER STADIUM": "Super Stadium",
681
+ "ROCK ARENA": "Rock Arena",
682
+ "JAZZ CLUB": "Jazz Club",
683
+ "CLASSIC CONCERT": "Classic Concert",
684
+ "MONO MOVIE": "Mono Movie",
685
+ "MATRIX": "Matrix",
686
+ "VIDEO GAME": "Video Game",
687
+ "VIRTUAL": "Virtual",
688
+ "NEURAL": "Neural",
689
+ "STANDARD": "Standard",
690
+ "LEFT": "Left",
691
+ "RIGHT": "Right"
692
+ };
package/src/mainzone.js CHANGED
@@ -2,7 +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 { PictureModesConversionToHomeKit, PictureModesDenonNumber, PictureModesDenonString, DirectSoundMode, SoundModeConversion } from './constants.js';
5
+ import { PictureModesConversionToHomeKit, PictureModesDenonNumber, PictureModesDenonString, DirectSoundMode, SoundModeConversion, SoundModeDisplayName } from './constants.js';
6
6
  let Accessory, Characteristic, Service, Categories, Encode, AccessoryUUID;
7
7
 
8
8
  class MainZone extends EventEmitter {
@@ -1004,7 +1004,7 @@ class MainZone extends EventEmitter {
1004
1004
 
1005
1005
  try {
1006
1006
  const sources = (this.inputsServices ?? []).map(input => ({ id: `${input.zonePrefix}${input.reference}`, name: input.name }));
1007
- const soundModes = this.ha.commands.sound_mode ? [...new Set(Object.values(SoundModeConversion))].map(mode => ({ id: mode, name: mode })) : [];
1007
+ const soundModes = this.ha.commands.sound_mode ? [...new Set(Object.values(SoundModeConversion))].map(mode => ({ id: mode, name: SoundModeDisplayName[mode] ?? mode })) : [];
1008
1008
  await this.ha.publishConfig({ sources, soundModes });
1009
1009
  await this.haUpdateState();
1010
1010
  } catch (error) {
package/src/restful.js CHANGED
@@ -26,6 +26,49 @@ class RestFul extends EventEmitter {
26
26
  try {
27
27
  const app = express();
28
28
  app.set('json spaces', 2);
29
+
30
+ // Request rate limit per client, protects Homebridge from request floods (CWE-770)
31
+ const rateLimitWindow = 60 * 1000;
32
+ const rateLimitMax = 600;
33
+ const rateLimitClients = 1000;
34
+ const clients = new Map();
35
+ let clientsLimitWarned = 0;
36
+ app.use((req, res, next) => {
37
+ const now = Date.now();
38
+
39
+ // Drop expired windows so the map cannot grow without limit
40
+ if (clients.size > 100) {
41
+ for (const [ip, client] of clients) {
42
+ if (now - client.start >= rateLimitWindow) clients.delete(ip);
43
+ }
44
+ }
45
+
46
+ let client = clients.get(req.ip);
47
+ if (!client || now - client.start >= rateLimitWindow) {
48
+ // Hard limit of tracked addresses, new addresses wait until older windows expire
49
+ if (!client && clients.size >= rateLimitClients) {
50
+ if (now - clientsLimitWarned >= rateLimitWindow) {
51
+ clientsLimitWarned = now;
52
+ if (this.logWarn) this.emit('warn', `RESTFul too many clients (${rateLimitClients}) in one minute, new clients are rejected until the window ends`);
53
+ }
54
+ res.set('Retry-After', String(Math.ceil(rateLimitWindow / 1000)));
55
+ return res.status(429).json({ error: 'RESTFul Too Many Requests' });
56
+ }
57
+ client = { count: 0, start: now, warned: false };
58
+ clients.set(req.ip, client);
59
+ }
60
+
61
+ if (++client.count > rateLimitMax) {
62
+ // Warn once per client and window, a flood must not flood the log too
63
+ if (!client.warned && this.logWarn) this.emit('warn', `RESTFul rate limit ${rateLimitMax} requests per minute exceeded by: ${req.ip}, further requests are rejected until the window ends`);
64
+ client.warned = true;
65
+ res.set('Retry-After', String(Math.ceil((client.start + rateLimitWindow - now) / 1000)));
66
+ return res.status(429).json({ error: 'RESTFul Too Many Requests' });
67
+ }
68
+
69
+ next();
70
+ });
71
+
29
72
  app.use(json());
30
73
 
31
74
  // Optional token auth, when a token is configured every route requires "Authorization: Bearer <token>"
package/src/zone2.js CHANGED
@@ -2,7 +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 { PictureModesConversionToHomeKit, PictureModesDenonNumber, SoundModeConversion } from './constants.js';
5
+ import { PictureModesConversionToHomeKit, PictureModesDenonNumber, SoundModeConversion, SoundModeDisplayName } from './constants.js';
6
6
  let Accessory, Characteristic, Service, Categories, Encode, AccessoryUUID;
7
7
 
8
8
  class Zone2 extends EventEmitter {
@@ -918,7 +918,7 @@ class Zone2 extends EventEmitter {
918
918
 
919
919
  try {
920
920
  const sources = (this.inputsServices ?? []).map(input => ({ id: `${input.zonePrefix}${input.reference}`, name: input.name }));
921
- const soundModes = this.ha.commands.sound_mode ? [...new Set(Object.values(SoundModeConversion))].map(mode => ({ id: mode, name: mode })) : [];
921
+ const soundModes = this.ha.commands.sound_mode ? [...new Set(Object.values(SoundModeConversion))].map(mode => ({ id: mode, name: SoundModeDisplayName[mode] ?? mode })) : [];
922
922
  await this.ha.publishConfig({ sources, soundModes });
923
923
  await this.haUpdateState();
924
924
  } catch (error) {
package/src/zone3.js CHANGED
@@ -2,7 +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 { PictureModesConversionToHomeKit, PictureModesDenonNumber, SoundModeConversion } from './constants.js';
5
+ import { PictureModesConversionToHomeKit, PictureModesDenonNumber, SoundModeConversion, SoundModeDisplayName } from './constants.js';
6
6
  let Accessory, Characteristic, Service, Categories, Encode, AccessoryUUID;
7
7
 
8
8
  class Zone3 extends EventEmitter {
@@ -918,7 +918,7 @@ class Zone3 extends EventEmitter {
918
918
 
919
919
  try {
920
920
  const sources = (this.inputsServices ?? []).map(input => ({ id: `${input.zonePrefix}${input.reference}`, name: input.name }));
921
- const soundModes = this.ha.commands.sound_mode ? [...new Set(Object.values(SoundModeConversion))].map(mode => ({ id: mode, name: mode })) : [];
921
+ const soundModes = this.ha.commands.sound_mode ? [...new Set(Object.values(SoundModeConversion))].map(mode => ({ id: mode, name: SoundModeDisplayName[mode] ?? mode })) : [];
922
922
  await this.ha.publishConfig({ sources, soundModes });
923
923
  await this.haUpdateState();
924
924
  } catch (error) {