homebridge-xbox-tv 4.2.2 → 4.3.1
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 +13 -0
- package/icons/LICENSE +22 -0
- package/icons/application.png +0 -0
- package/icons/cog.png +0 -0
- package/icons/gamepad-variant.png +0 -0
- package/icons/lan-check.png +0 -0
- package/icons/microsoft-xbox-controller.png +0 -0
- package/icons/microsoft-xbox.png +0 -0
- package/icons/monitor-shimmer.png +0 -0
- package/icons/shopping.png +0 -0
- package/icons/television-classic.png +0 -0
- package/icons/tune-vertical.png +0 -0
- package/icons/view-dashboard.png +0 -0
- package/package.json +2 -1
- package/src/appicons.js +77 -0
- package/src/hadiscovery.js +48 -2
- package/src/xboxdevice.js +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,19 @@ 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.3.1] - (27.09.2026)
|
|
22
|
+
|
|
23
|
+
### Changes
|
|
24
|
+
|
|
25
|
+
- changed: the Home Assistant icons of the system menus are dark gray without a background, the same style as in the other plugins
|
|
26
|
+
|
|
27
|
+
## [4.3.0] - (27.09.2026)
|
|
28
|
+
|
|
29
|
+
### Changes
|
|
30
|
+
|
|
31
|
+
- added: Home Assistant app icons in the media browser and the media card. Store apps and games use their logo or box art from the public Microsoft Store catalog, Dashboard, Settings, Television and the other system menus use icons bundled with the plugin (MQTT Universal Media Player integration 0.5.0 or newer)
|
|
32
|
+
- added: Home Assistant media search in the apps (MQTT Universal Media Player integration 0.7.0 or newer)
|
|
33
|
+
|
|
21
34
|
## [4.2.2] - (24.09.2026)
|
|
22
35
|
|
|
23
36
|
### Changes
|
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
|
package/icons/cog.png
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"displayName": "Xbox TV",
|
|
3
3
|
"name": "homebridge-xbox-tv",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.3.1",
|
|
5
5
|
"description": "Homebridge plugin to control Xbox game consoles.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "grzegorz914",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
24
|
"src",
|
|
25
|
+
"icons",
|
|
25
26
|
"homebridge-ui",
|
|
26
27
|
"index.js",
|
|
27
28
|
"config.schema.json",
|
package/src/appicons.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
|
|
4
|
+
// Icons of the console apps for Home Assistant.
|
|
5
|
+
// Store apps and games: the logo or box art from the public Microsoft Store catalog, no sign in needed.
|
|
6
|
+
// Dashboard, Settings and the other system menus are not in the Store, they use icons bundled with the plugin,
|
|
7
|
+
// Material Design Icons (Apache 2.0, https://pictogrammers.com) rendered to PNG in /icons
|
|
8
|
+
const IconsDir = new URL('../icons/', import.meta.url);
|
|
9
|
+
const CatalogUrl = 'https://displaycatalog.mp.microsoft.com/v7.0/products';
|
|
10
|
+
const ImagePurposes = ['Logo', 'BoxArt', 'FeaturePromotionalSquareArt', 'Tile', 'Poster'];
|
|
11
|
+
const MaxCachedImages = 200;
|
|
12
|
+
|
|
13
|
+
const MenuIcons = {
|
|
14
|
+
'Screensaver': 'monitor-shimmer',
|
|
15
|
+
'Dashboard': 'view-dashboard',
|
|
16
|
+
'Settings': 'cog',
|
|
17
|
+
'Television': 'television-classic',
|
|
18
|
+
'SettingsTv': 'tune-vertical',
|
|
19
|
+
'Accessory': 'microsoft-xbox-controller',
|
|
20
|
+
'NetworkTroubleshooter': 'lan-check',
|
|
21
|
+
'MicrosoftStore': 'shopping',
|
|
22
|
+
'XboxGuide': 'microsoft-xbox'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
class AppIconsStore {
|
|
26
|
+
constructor() {
|
|
27
|
+
this.client = axios.create({ timeout: 10000 });
|
|
28
|
+
this.bundled = new Map();
|
|
29
|
+
// Product id to image url, null when the catalog has no image
|
|
30
|
+
this.urls = new Map();
|
|
31
|
+
this.images = new Map();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async bundledIcon(name) {
|
|
35
|
+
if (!this.bundled.has(name)) this.bundled.set(name, await readFile(new URL(`${name}.png`, IconsDir)).catch(() => null));
|
|
36
|
+
return this.bundled.get(name);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async imageUrl(productId) {
|
|
40
|
+
if (this.urls.has(productId)) return this.urls.get(productId);
|
|
41
|
+
|
|
42
|
+
const { data } = await this.client.get(CatalogUrl, { params: { bigIds: productId, market: 'US', languages: 'en-US' } });
|
|
43
|
+
const images = data?.Products?.[0]?.LocalizedProperties?.[0]?.Images ?? [];
|
|
44
|
+
const image = ImagePurposes.map(purpose => images.find(i => i.ImagePurpose === purpose && i.Uri)).find(Boolean);
|
|
45
|
+
const url = image ? `${image.Uri.startsWith('//') ? 'https:' : ''}${image.Uri}?w=256&h=256` : null;
|
|
46
|
+
this.urls.set(productId, url);
|
|
47
|
+
return url;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// PNG or JPEG bytes of an app (input with oneStoreProductId, isGame), the bundled icon when there is no Store image
|
|
51
|
+
async get(input) {
|
|
52
|
+
const productId = String(input?.oneStoreProductId ?? '');
|
|
53
|
+
if (MenuIcons[productId]) return this.bundledIcon(MenuIcons[productId]);
|
|
54
|
+
|
|
55
|
+
const fallback = () => this.bundledIcon(input?.isGame ? 'gamepad-variant' : 'application');
|
|
56
|
+
if (!productId) return fallback();
|
|
57
|
+
if (this.images.has(productId)) return this.images.get(productId) ?? fallback();
|
|
58
|
+
|
|
59
|
+
let image = null;
|
|
60
|
+
try {
|
|
61
|
+
const url = await this.imageUrl(productId);
|
|
62
|
+
if (url) {
|
|
63
|
+
const { data } = await this.client.get(url, { responseType: 'arraybuffer' });
|
|
64
|
+
image = Buffer.from(data);
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// Network error, not cached so the next request tries again
|
|
68
|
+
return fallback();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this.images.set(productId, image);
|
|
72
|
+
while (this.images.size > MaxCachedImages) this.images.delete(this.images.keys().next().value);
|
|
73
|
+
return image ?? fallback();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default new AppIconsStore();
|
package/src/hadiscovery.js
CHANGED
|
@@ -14,6 +14,10 @@ class HaDiscovery {
|
|
|
14
14
|
this.uniqueId = this.objectId;
|
|
15
15
|
this.name = config.name;
|
|
16
16
|
this.deviceClass = config.deviceClass;
|
|
17
|
+
// Separate play, pause and stop buttons in the Home Assistant media card
|
|
18
|
+
this.assumedState = config.assumedState === true;
|
|
19
|
+
// Players with the same group id can be grouped in Home Assistant, e.g. the zones of one receiver
|
|
20
|
+
this.group = config.group?.id ? { id: String(config.group.id), leader: config.group.leader === true } : null;
|
|
17
21
|
this.device = config.device ?? {};
|
|
18
22
|
this.commands = config.commands ?? {};
|
|
19
23
|
|
|
@@ -21,9 +25,12 @@ class HaDiscovery {
|
|
|
21
25
|
this.stateTopic = `${mqtt.config.prefix}/HA State`;
|
|
22
26
|
this.commandTopic = `${mqtt.config.prefix}/Set`;
|
|
23
27
|
this.imageTopic = config.image ? `${mqtt.config.prefix}/HA Image` : null;
|
|
28
|
+
// Media browser icons requested by Home Assistant with the BrowseImage key, answered by answerBrowseImage
|
|
29
|
+
this.browseImageTopic = config.browseImages ? `${mqtt.config.prefix}/HA Browse Image` : null;
|
|
24
30
|
|
|
25
31
|
this.sources = [];
|
|
26
32
|
this.soundModes = [];
|
|
33
|
+
this.browse = [];
|
|
27
34
|
this.state = {};
|
|
28
35
|
this.lastConfig = '';
|
|
29
36
|
this.lastState = '';
|
|
@@ -31,9 +38,11 @@ class HaDiscovery {
|
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
// Publish (or republish when changed) the retained discovery message
|
|
34
|
-
|
|
41
|
+
// browse: media browser folders [{ name, type, items: [{ id, name }] }], at most MaxBrowseItems items in total
|
|
42
|
+
async publishConfig({ sources = this.sources, soundModes = this.soundModes, browse = this.browse } = {}) {
|
|
35
43
|
this.sources = uniqueByName(sources);
|
|
36
44
|
this.soundModes = uniqueByName(soundModes);
|
|
45
|
+
this.browse = limitBrowse(browse);
|
|
37
46
|
|
|
38
47
|
const device = { identifiers: [this.uniqueId], name: this.name };
|
|
39
48
|
for (const [key, value] of Object.entries(this.device)) {
|
|
@@ -44,14 +53,18 @@ class HaDiscovery {
|
|
|
44
53
|
platform: PLATFORM,
|
|
45
54
|
unique_id: this.uniqueId,
|
|
46
55
|
device_class: this.deviceClass,
|
|
56
|
+
...(this.assumedState ? { assumed_state: true } : {}),
|
|
57
|
+
...(this.group ? { group: this.group } : {}),
|
|
47
58
|
state_topic: this.stateTopic,
|
|
48
59
|
command_topic: this.commandTopic,
|
|
49
60
|
availability_topic: this.mqtt.availabilityTopic,
|
|
50
61
|
...(this.imageTopic ? { image_topic: this.imageTopic } : {}),
|
|
62
|
+
...(this.browseImageTopic ? { browse_image_topic: this.browseImageTopic } : {}),
|
|
51
63
|
device,
|
|
52
64
|
commands: this.commands,
|
|
53
65
|
sources: this.sources,
|
|
54
|
-
sound_modes: this.soundModes
|
|
66
|
+
sound_modes: this.soundModes,
|
|
67
|
+
...(this.browse.length > 0 ? { browse: this.browse } : {})
|
|
55
68
|
};
|
|
56
69
|
|
|
57
70
|
const payload = JSON.stringify(config);
|
|
@@ -83,6 +96,21 @@ class HaDiscovery {
|
|
|
83
96
|
return true;
|
|
84
97
|
}
|
|
85
98
|
|
|
99
|
+
// Answer a media browser icon request, not retained, an empty payload when the item has no icon
|
|
100
|
+
async answerBrowseImage(key, fetchImage) {
|
|
101
|
+
// The key becomes a topic level, only the md5 hex key of the integration is accepted
|
|
102
|
+
if (!this.browseImageTopic || !/^[a-f0-9]{32}$/.test(String(key))) return false;
|
|
103
|
+
|
|
104
|
+
let image = null;
|
|
105
|
+
try {
|
|
106
|
+
image = await fetchImage();
|
|
107
|
+
} catch {
|
|
108
|
+
image = null;
|
|
109
|
+
}
|
|
110
|
+
await new Promise(resolve => this.mqtt.mqttClient.publish(`${this.browseImageTopic}/${key}`, image ?? '', { qos: 0 }, () => resolve()));
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
86
114
|
// Merge a partial state and publish it retained when something changed
|
|
87
115
|
async updateState(partial) {
|
|
88
116
|
for (const [key, value] of Object.entries(partial)) {
|
|
@@ -98,6 +126,24 @@ class HaDiscovery {
|
|
|
98
126
|
}
|
|
99
127
|
}
|
|
100
128
|
|
|
129
|
+
// The discovery message stays small enough for the broker and Home Assistant, big bouquets are cut
|
|
130
|
+
const MaxBrowseItems = 2000;
|
|
131
|
+
function limitBrowse(folders) {
|
|
132
|
+
let left = MaxBrowseItems;
|
|
133
|
+
const result = [];
|
|
134
|
+
for (const folder of folders ?? []) {
|
|
135
|
+
const name = String(folder.name ?? '').trim();
|
|
136
|
+
const items = (folder.items ?? [])
|
|
137
|
+
.filter(item => item.id !== undefined && item.id !== null && String(item.name ?? '').trim())
|
|
138
|
+
.slice(0, Math.max(left, 0))
|
|
139
|
+
.map(item => ({ id: item.id, name: String(item.name).trim() }));
|
|
140
|
+
if (!name || items.length === 0) continue;
|
|
141
|
+
left -= items.length;
|
|
142
|
+
result.push({ name, ...(folder.type ? { type: folder.type } : {}), items });
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
101
147
|
// Home Assistant needs unique names, keep the first item for a duplicated name
|
|
102
148
|
function uniqueByName(items) {
|
|
103
149
|
const seen = new Set();
|
package/src/xboxdevice.js
CHANGED
|
@@ -3,6 +3,7 @@ import XboxWebApi from './webApi/xboxwebapi.js';
|
|
|
3
3
|
import XboxLocalApi from './localApi/xboxlocalapi.js';
|
|
4
4
|
import Functions from './functions.js';
|
|
5
5
|
import HaDiscovery from './hadiscovery.js';
|
|
6
|
+
import AppIcons from './appicons.js';
|
|
6
7
|
import { DefaultInputs, WebApi } from './constants.js';
|
|
7
8
|
|
|
8
9
|
let Accessory, Characteristic, Service, Categories, Encode, AccessoryUUID;
|
|
@@ -83,6 +84,12 @@ class XboxDevice extends EventEmitter {
|
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
async setOverExternalIntegration(integration, key, value) {
|
|
87
|
+
// Media browser icon of an app, the id is the oneStoreProductId, works without the web api
|
|
88
|
+
if (key === 'BrowseImage') {
|
|
89
|
+
const input = this.inputsServices?.find(i => i.oneStoreProductId === value?.id) ?? { oneStoreProductId: value?.id };
|
|
90
|
+
return await this.ha?.answerBrowseImage(value?.key, () => AppIcons.get(input));
|
|
91
|
+
}
|
|
92
|
+
|
|
86
93
|
if (!this.consoleAuthorized && this.logWarn) {
|
|
87
94
|
this.emit('warn', `Set over external integration not possible, web api not enabled`);
|
|
88
95
|
return;
|
|
@@ -1009,6 +1016,9 @@ class XboxDevice extends EventEmitter {
|
|
|
1009
1016
|
try {
|
|
1010
1017
|
this.ha = new HaDiscovery(this.mqtt1, {
|
|
1011
1018
|
objectId: `xbox_${this.liveId}`,
|
|
1019
|
+
// App icons from the Microsoft Store, system menus with icons bundled with the plugin
|
|
1020
|
+
image: true,
|
|
1021
|
+
browseImages: true,
|
|
1012
1022
|
name: this.name,
|
|
1013
1023
|
deviceClass: 'receiver',
|
|
1014
1024
|
device: {
|
|
@@ -1055,6 +1065,10 @@ class XboxDevice extends EventEmitter {
|
|
|
1055
1065
|
source: input?.oneStoreProductId,
|
|
1056
1066
|
app_name: input?.name ?? ''
|
|
1057
1067
|
});
|
|
1068
|
+
|
|
1069
|
+
// Icon of the current app
|
|
1070
|
+
const productId = this.power ? input?.oneStoreProductId ?? null : null;
|
|
1071
|
+
this.ha.updateImage(productId, () => AppIcons.get(input)).catch(() => { });
|
|
1058
1072
|
} catch (error) {
|
|
1059
1073
|
if (this.logWarn) this.emit('warn', `HA Discovery state error: ${error}`);
|
|
1060
1074
|
}
|