homebridge-xbox-tv 4.2.0 → 4.2.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.
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/hadiscovery.js +25 -0
- package/src/mqtt.js +1 -1
- package/src/restful.js +43 -0
- package/src/xboxdevice.js +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
18
18
|
- For plugin < v4.1.0 use Homebridge UI <= v5.5.0
|
|
19
19
|
- For plugin >= v4.1.0 use Homebridge UI >= v5.13.0
|
|
20
20
|
|
|
21
|
+
## [4.2.2] - (24.09.2026)
|
|
22
|
+
|
|
23
|
+
### Changes
|
|
24
|
+
|
|
25
|
+
- fix: with 85 inputs (the HomeKit limit) the input list from the console 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
|
|
26
|
+
|
|
27
|
+
## [4.2.1] - (24.09.2026)
|
|
28
|
+
|
|
29
|
+
### Changes
|
|
30
|
+
|
|
31
|
+
- 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.
|
|
32
|
+
|
|
21
33
|
## [4.2.0] - (23.09.2026)
|
|
22
34
|
|
|
23
35
|
### Changes
|
package/package.json
CHANGED
package/src/hadiscovery.js
CHANGED
|
@@ -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/mqtt.js
CHANGED
|
@@ -116,7 +116,7 @@ class Mqtt extends EventEmitter {
|
|
|
116
116
|
if (this.config.logWarn) this.emit('warn', `MQTT Publish error: ${error.message}`);
|
|
117
117
|
return reject(error);
|
|
118
118
|
}
|
|
119
|
-
if (this.config.logDebug) this.emit('debug', `MQTT Publish retained Topic: ${fullTopic}, Payload: ${payload}`);
|
|
119
|
+
if (this.config.logDebug) this.emit('debug', `MQTT Publish retained Topic: ${fullTopic}, Payload: ${typeof payload === 'string' ? payload : `${payload.length} bytes`}`);
|
|
120
120
|
resolve();
|
|
121
121
|
});
|
|
122
122
|
});
|
package/src/restful.js
CHANGED
|
@@ -28,6 +28,49 @@ class RestFul extends EventEmitter {
|
|
|
28
28
|
try {
|
|
29
29
|
const app = express();
|
|
30
30
|
app.set('json spaces', 2);
|
|
31
|
+
|
|
32
|
+
// Request rate limit per client, protects Homebridge from request floods (CWE-770)
|
|
33
|
+
const rateLimitWindow = 60 * 1000;
|
|
34
|
+
const rateLimitMax = 600;
|
|
35
|
+
const rateLimitClients = 1000;
|
|
36
|
+
const clients = new Map();
|
|
37
|
+
let clientsLimitWarned = 0;
|
|
38
|
+
app.use((req, res, next) => {
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
|
|
41
|
+
// Drop expired windows so the map cannot grow without limit
|
|
42
|
+
if (clients.size > 100) {
|
|
43
|
+
for (const [ip, client] of clients) {
|
|
44
|
+
if (now - client.start >= rateLimitWindow) clients.delete(ip);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let client = clients.get(req.ip);
|
|
49
|
+
if (!client || now - client.start >= rateLimitWindow) {
|
|
50
|
+
// Hard limit of tracked addresses, new addresses wait until older windows expire
|
|
51
|
+
if (!client && clients.size >= rateLimitClients) {
|
|
52
|
+
if (now - clientsLimitWarned >= rateLimitWindow) {
|
|
53
|
+
clientsLimitWarned = now;
|
|
54
|
+
if (this.logWarn) this.emit('warn', `RESTFul too many clients (${rateLimitClients}) in one minute, new clients are rejected until the window ends`);
|
|
55
|
+
}
|
|
56
|
+
res.set('Retry-After', String(Math.ceil(rateLimitWindow / 1000)));
|
|
57
|
+
return res.status(429).json({ error: 'RESTFul Too Many Requests' });
|
|
58
|
+
}
|
|
59
|
+
client = { count: 0, start: now, warned: false };
|
|
60
|
+
clients.set(req.ip, client);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (++client.count > rateLimitMax) {
|
|
64
|
+
// Warn once per client and window, a flood must not flood the log too
|
|
65
|
+
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`);
|
|
66
|
+
client.warned = true;
|
|
67
|
+
res.set('Retry-After', String(Math.ceil((client.start + rateLimitWindow - now) / 1000)));
|
|
68
|
+
return res.status(429).json({ error: 'RESTFul Too Many Requests' });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
next();
|
|
72
|
+
});
|
|
73
|
+
|
|
31
74
|
app.use(json());
|
|
32
75
|
|
|
33
76
|
// Optional token auth, when a token is configured every route requires "Authorization: Bearer <token>"
|
package/src/xboxdevice.js
CHANGED
|
@@ -227,8 +227,6 @@ class XboxDevice 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
|
// Filter
|
|
233
231
|
const contentType = input.contentType;
|
|
234
232
|
const filterGames = this.filterGames && contentType === 'Game';
|
|
@@ -268,6 +266,9 @@ class XboxDevice extends EventEmitter {
|
|
|
268
266
|
updated = true;
|
|
269
267
|
}
|
|
270
268
|
} else {
|
|
269
|
+
// HomeKit allows at most 85 inputs, the limit applies to new inputs only, existing ones are still updated
|
|
270
|
+
if (this.inputsServices.length >= 85) continue;
|
|
271
|
+
|
|
271
272
|
const identifier = this.inputsServices.length + 1;
|
|
272
273
|
inputService = this.accessory.addService(Service.InputSource, sanitizedName, `Input ${inputReference}`);
|
|
273
274
|
inputService.identifier = identifier;
|