Showing preview only (297K chars total). Download the full file or copy to clipboard to get everything.
Repository: grzegorz914/homebridge-xbox-tv
Branch: main
Commit: 8532599aea6d
Files: 37
Total size: 283.1 KB
Directory structure:
gitextract_t1gbyqj_/
├── .github/
│ └── workflows/
│ └── stale.yml
├── .gitignore
├── CHANGELOG.md
├── CLAUDE.md
├── LICENSE
├── README.md
├── config.schema.json
├── homebridge-ui/
│ ├── public/
│ │ └── index.html
│ └── server.js
├── index.js
├── package.json
├── sample-config.json
└── src/
├── constants.js
├── functions.js
├── impulsegenerator.js
├── localApi/
│ ├── message.js
│ ├── packets.js
│ ├── sgcrypto.js
│ ├── simple.js
│ ├── structure.js
│ └── xboxlocalapi.js
├── mqtt.js
├── restful.js
├── webApi/
│ ├── authentication.js
│ ├── providers/
│ │ ├── achievements.js
│ │ ├── catalog.js
│ │ ├── gameclips.js
│ │ ├── messages.js
│ │ ├── people.js
│ │ ├── pins.js
│ │ ├── screenshots.js
│ │ ├── social.js
│ │ ├── titlehub.js
│ │ ├── userpresence.js
│ │ └── userstats.js
│ └── xboxwebapi.js
└── xboxdevice.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/stale.yml
================================================
name: Mark stale issues and pull requests
on:
schedule:
- cron: '0 0 * * *' # Runs daily at midnight (UTC)
workflow_dispatch: # Allows you to manually trigger the workflow
jobs:
stale:
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: write
steps:
- uses: actions/stale@v8
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.'
days-before-stale: 30
days-before-close: 7
stale-pr-message: 'This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.'
stale-issue-label: 'stale'
exempt-issue-labels: 'pinned,security'
close-issue-message: 'Closing this issue due to inactivity.'
close-pr-message: 'Closing this pull request due to inactivity.'
================================================
FILE: .gitignore
================================================
node_modules
npm-debug.log
.DS_Store
.vscode
================================================
FILE: CHANGELOG.md
================================================
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### NOTE
- After update to 2.x.x the plugin settings (xboxLiveId) need to be updated
- After update to v3.0.0 RESTFull and MQTT config settings need to be updated
- After update to v3.4.0 all buttons in config need to be updated
- After update to v3.9.0 plugin need to be reconfigured and console reauthorized
- After update to v4.0.0 sensors need to be reconfigured!!!
## Warning
- For plugin < v4.1.0 use Homebridge UI <= v5.5.0
- For plugin >= v4.1.0 use Homebridge UI >= v5.13.0
## [4.1.13] - (06.04.2026)
## Changes
- refactor inputs switch
- bump dependencies
- cleanup
## [4.1.4] - (27.01.2026)
## Changes
- bump dependencies
- cleanup
## [4.1.1] - (03.01.2026)
## Changes
- bump dependencies
- warn if console not exist on server
- cleanup
## [4.1.0] - (01.01.2026)
## Changes
- added support for Homebridge UI >= v5.13.0
- config schema updated
- readme updated
## [4.0.0] - (12.12.2025)
## Changes
- after update to v4.0.0 sensors need to be reconfigured!!!
- full refactor the sensor section code, now is possible to create multiple sensors of differrent types
- config schema updated
- redme updated
- cleanup
## [3.9.16] - (09.12.2025)
## Changes
- moved to MQTT v5
## [3.9.11] - (08.11.2025)
## Changes
- stability and performance improvements
- bump deependencies
- config schema updated
- redme updated
- cleanup
## [3.9.1] - (30.09.2025)
## Changes
- added possibility to change accessory display type in Home app
- stability improvements
- redme updated
- cleanup
## [3.9.0] - (24.09.2025)
## Changes
- config schema and json refactor
- plugin need to be reconfigured
- console need to be reauthorized
- fix [#234](https://github.com/grzegorz914/homebridge-xbox-tv/issues/234)
- stability improvements
- bump deependencies
- redme updated
- cleanup
## [3.8.1] - (13.09.2025)
## Changes
- fix [#232](https://github.com/grzegorz914/homebridge-xbox-tv/issues/232)
- bump dependencies
- cleanup
## [3.8.0] - (10.09.2025)
## Changes
- stability and performance improvement
- added dynamic firmware info update if changed
- bump dependencies
- cleanup
## [3.7.0] - (17.08.2025)
## Changes
- added dynamic add/update/remove input/channel if load from device
- added dynamic firmware info update if changed
- cleanup
## [3.6.0] - (30.05.2025)
## Changes
- added speaker option to volume control (for feature use)
- now if volume control option is set to disable/nonethe also TV Speakers (hardware control) is disabled
- stability improvements
- cleanup
## [3.5.1] - (15.03.2025)
## Changes
- fix [#224](https://github.com/grzegorz914/homebridge-xbox-tv/issues/224)
- cleanup
## [3.5.0] - (13.03.2025)
## Changes
- added possibility to disable indyvidual accessory
- bump dependencies
- config schema updated
- redme updated
- cleanup
## [3.4.0] - (05.03.2025)
## Changes
- after update to this version all buttons in config need to be updated
- refactor code of buttons
- bump dependencies
- config schema updated
- cleanup
## [3.3.11] - (20.02.2025)
## Changes
- stability and improvements
- fix deprected method
- cleanup
## [3.3.9] - (07.02.2025)
## Changes
- stability and improvements
## [3.3.8] - (07.02.2025)
## Changes
- fix inputs display order
## [3.3.7] - (06.02.2025)
## Changes
- cleanup and optimizations of web api control
## [3.3.6] - (06.02.2025)
## Changes
- fix HAP-NodeJS WARNING: The accessory has an invalid 'Name' characteristic 'configuredName'
- Please use only alphanumeric, space, and apostrophe characters
- Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis
## [3.3.4] - (05.02.2025)
## Changes
- Web Api improvements of authoriztion check and remote management
- cleanup
## [3.3.3] - (04.02.2025)
## Changes
- fix [#221](https://github.com/grzegorz914/homebridge-xbox-tv/issues/221)
## [3.3.2] - (04.02.2025)
## Changes
- update RESTFul
- bump dependencies
## [3.3.0] - (19.01.2025)
## Changes
- added possibility to disable/enable log success, info, warn, error
- refactor cnnect code
- bump dependencies
- config schema updated
- redme updated
- cleanup
## [3.2.0] - (30.11.2024)
## Changes
- move from commonJS to esm module
- moved constants.json to constants.js
- cleanup
## [3.1.12] - (27.09.2024)
## Changes
- fix restFul start [#212](https://github.com/grzegorz914/homebridge-xbox-tv/issues/212)
- cleanup
## [3.1.6] - (06.09.2024)
## Changes
- Authorization manager layout improvements
- cleanup
## [3.1.5] - (06.09.2024)
## Changes
- cleanup
## [3.1.4] - (06.09.2024)
## Changes
- fix display duplicated dev info
- cleanup
## [3.1.3] - (06.09.2024)
## Changes
- refactor web and local api connect code
- cleanup
## [3.1.0] - (23.08.2024)
## Changes
- add control over RESTFul POST JSON Object
- cleanup
## [3.0.2] - (18.08.2024)
## Changes
- fixed authorization manager [#204](https://github.com/grzegorz914/homebridge-xbox-tv/issues/204)
- cleanup
## [3.0.0] - (14.08.2024)
## Changes
### After update to v3.0.0 RESTFull and MQTT config settings need to be updated
- hide passwords by typing and display in Config UI
- remove return duplicate promises from whole code
- bump dependencies
- cleanup
## [2.14.0] - (04.08.2024)
## Changes
- added possiblity to set own volume control name and enable/disable prefix
- config schema updated
- bump dependencies
- cleanup
## [2.13.0] - (05.03.2024)
## Changes
- added support to subscribe MQTT and control device
- config schema updated
- cleanup
## [2.12.0] - (02.01.2024)
## Changes
- added possibility to disable prefix name for buttons and sensors
- config schema updated
- cleanup
## [2.11.0] - (29.12.2023)
## Changes
- added possibility to select display inputs order, possible by `None`, `Alphabetically Name`, `Alphabetically Reference`
- config schema updated
- cleanup
## [2.10.0] - (26.12.2023)
## After update to this version the plugin properties are changed and console must be authorized and settings need to be corrected
## Changes
- full code refactor
- added possibility toggle Power control between local/web api
- fixed disconnect problem on first run
- performance and stability improvements
- config.schema updated
- readme updated
- cleanup
## [2.9.0] - (29.07.2023)
## Changes
- added RESTFul server
- use JWT token for lokal api if console authorizen
- code refactor and cleanup
- config.schema updated
- fixed some minor issues
- prepare for next release and features
## [2.8.0] - (20.02.2023)
## Changes
- fix load plugin gui on first start after install
- authorization manager updated
- added possibility to set IP Address and Xbox Live ID from Authorization Manager.
- added possibility to enable Web Api Control from Authorization Manager after authorization successfull done.
- cleanup
## [2.7.0] - (13.02.2023)
## Changes
- standarize function of display type and volume control, now volume control -1 None/Disabled, 0 Slider, 1 Fan, please see in readme
- config.schema updated
- fix expose extra input tile in homekit app
- other small fixes and improvements
- cleanup
## [2.6.0] - (12.02.2023)
## Changes
- integrate web api library in to the plugin
- simplify the authorization manager process(reduced 1 step, correct some words)
- bump dependencies
- stability improvements
- config.schema updated
- cleanup
## [2.5.0] - (29.01.2023)
## Changes
- update logging
- added new mqtt topics *Consoles List*, *Profile*, *Apps*, *Storages*, *Status*
- bump dependencies
- stability improwements
- config.schema updated
- cleanup
## [2.4.0] - (24.01.2023)
## Changes
- added Power Sensor for use with automations (active if power is ON)
- added Input Sensor for use with automations (activ on every Input change)
- added Screen Saver Sensor for use with automations (active on change to Screen Saver)
- added custom Inputs Sensor based on reference for use with automations (active on change to Input)
- config.schema updated
- cleanup
## [2.3.16] - (04.01.2023)
## Changes
- fix wrong state after power Off
- fix display current app
- fix save target visibility
- fix save custom names
## [2.3.15] - (04.01.2023)
## Changes
- fix #147 #148
## [2.3.14] - (03.01.2023)
## Changes
- code refactor
- stability improwements
## [2.3.13] - (31.12.2022)
## Changes
- dynamic update accessory information
## [2.3.12] - (24.12.2022)
## Changed
- fix #145
## [2.3.11] - (18.12.2022)
## Changed
- fix buttons and switch services
## [2.3.10] - (02.12.2022)
## Changed
- fix [#143](https://github.com/grzegorz914/homebridge-xbox-tv/issues/143)
## [2.3.9] - (28.11.2022)
## Changed
- fix [#143](https://github.com/grzegorz914/homebridge-xbox-tv/issues/143)
- update dependencies
## [2.3.8] - (02.11.2022)
## Changed
- fix error with 2.3.7
## [2.3.7] - (02.11.2022)
## Changed
- code refactor
## [2.3.6] - (10.09.2022)
## Changed
- cleanup
- added content type properties to inputs
- bump dependencies
## [2.3.3] - (29.08.2022)
## Changed
- cleanup
- rebuild mqtt topics
## [2.3.2] - (28.08.2022)
## Changed
- fix publish MQTT message
## [2.3.0] - (24.08.2022)
## Changed
- fix MQTT device info
- refactor debug and info log
- refactor send mqtt message
- bump dependencies
- code cleanup
- added Xbox Guide as default input
- fix [#137](https://github.com/grzegorz914/homebridge-xbox-tv/issues/137)
## [2.2.2] - (09.03.2022)
## Changed
- MQTT Client connection process
## Fixed
- webApiControl switch state
## [2.2.0] - (27.02.2022)
## Added
- MQTT Client, publish all device data
- possibility to set custom command for Info button in RC
## Changes
- update dependencies
- code refactor
## [2.1.3] - (28.01.2022)
### Fixed
- offset out of range
- code refactor
## [2.1.2] - (21.01.2022)
### Fixed
- [#136](https://github.com/grzegorz914/homebridge-xbox-tv/issues/136)
## [2.1.1] - (21.01.2022)
### Changed
- refactor debug message logging
- update readme
### Fixed
- wrong variables
- removed unnecessary async
- report unknown message if power on fail
## [2.1.0] - (21.01.2022)
### Added
- check authorization state of console every 10 min. if powered ON and web api control enabled
- check cosole data and installed apps every 10 min. if powered ON and web api control enabled
### Changed
- send status message data only if changed
- debug message logging
- code refactor
- code cleanup
- stability and performance improvements
### Fixed
- unexpected set authorization to true however the console is not authorized
- data offset out of range [#133](https://github.com/grzegorz914/homebridge-xbox-tv/issues/133)
- incorrect client authorization on console
## [2.0.13] - (15.01.2022)
### Added
- Network Troubleshooter as defaul input
### Changed
- removed manual authorization method
- code cleanup
- redme update
### Fixed
- services calculation count
## [2.0.12] - (09.01.2022)
### Changed
- code cleanup
## [2.0.10/11] - (08.01.2022)
### Changed
- rebuild device info read and write
## [2.0.9] - (03.01.2022)
### Added
- ability to disable log device info by every connections device to the network (Advanced Section)
### Fixed
- unexpected power on after power off
## [2.0.8] - 2021-12-29
### Added
- prevent load plugin if host or xboxLiveId not set
- prepare directory and files synchronously
## [2.0.6] - 2021-12-28
### Added
- better handle clientId if not defined in config
## [2.0.3] - 2021-12-28
### Added
- Selectable display type of buttons in HomeKit app
## [2.0.2] - 2021-12-28
### Changed
- Changed switches to buttons appear in HomeKit accessory
## [2.0.1] - 2021-12-26
### Fixed
- RC Control
## [2.0.0] - 2021-12-25
### Added
- Screensaver and Settings TV input as default
- Smartglass library (based on @unknownskl code) as standalone packet, completelly rebuilded
- Debug mode
- TV Remote control (buttons)
- Media control (buttons)
- Game Pad control (buttons)
- Clear web api token from plugin config menu
### Changes
- full code rebuild
- config.schema updated
- dependencies updated
- authorizatin manager updated
- removed bramnding
### Fixed
- memmory leak on some scenerious
- protocol disconnect if send multiple command at once
- authorization manager
## [1.8.13] - 2021-12-01
### Fixed
- fix authorization UI Manager open URI
## [1.8.12] - 2021-12-01
### Fixed
- fix authorization UI Manager
## [1.8.8] - 2021-11-04
### Fixed
- fix some connect/disconnect case
- fix some remote command not send
## [1.8.7] - 2021-11-01
### Changes
- performance improvement
## [1.8.6] - 2021-10-30
### Fixed
- fix powerOn
## [1.8.3] - 2021-10-30
### Fixed
- fix graphic in settings
## [1.8.2] - 2021-10-30
### Changes
- code optimize
- config.schema update
- redme update
## [1.8.1] - 2021-10-26
### Fixed
- fixed callback issue ([#105](https://github.com/grzegorz914/homebridge-xbox-tv/issues/105))
## [1.8.0] - 2021-10-26
### Changes
- added possibility Record Game DVR
- rebuild connection proces to console
- fixed Authorization Manager error on first run
- removed 'Undefined Input', not nedded any more
- code cleanup
## [1.7.9] - 2021-09-26
### Changes
- config.schema update
## [1.7.8] - 2021-09-24
### Changes
- update authorization manager
- code cleanup
## [1.7.5] - 2021-09-05
### Changes
- update config schema
- extend fiter possibility
- code cleanup
## [1.7.3] - 2021-09-03
### Changes
- update config schema
## [1.7.2] - 2021-09-03
### Changes
- added filter for Games, Apps, Dlc
## [1.7.1] - 2021-08-31
### Changes
- code refactorin
- added default inputs TV, Settings, Dashboard, Accessory, no need to create it in config
- many small changes and stability improvements
## [1.6.3] - 2021-08-05
### Changes
- added alternative check current running app if reference app is missing
- removed unnecessary reference property from buttons in config.json
## [1.6.2] - 2021-08-05
### Changes
- added possibility reboot console
- added possibility switch to Television input
- code and config reconfigured
- update config schema
## [1.6.0] - 2021-08-04
### Changes
- fixes
## [1.6.0] - 2021-08-04
### Changes
- added possibility load inputs list direct from device
- chenged config properties, please adapted config to latest one
- changed stored files names, may be need authenticate console again or just copy authentication Token to the new created file(authToken_xxxx)
- update dependencies
- code rebuild
## [1.5.0] - 2021-04-11
### Changes
- added control over Web Api
- code rebuild
## [1.4.0] - 2021-02-19
### Changes
- code rebuild, use Characteristic.onSet
- require Homebridge 1.3.x or above
## [1.3.10] - 2021-02-15
### Added and Fixed
- Add possibility disable log info, options available in config
- Fix memory leak
## [1.3.2] - 2021-01-18
### Fixed
- Fix log info regarding Input references ([#63](https://github.com/grzegorz914/homebridge-xbox-tv/issues/63))
## [1.3.1] - 2021-01-06
### Fixed
- Fix `getAppChannelLineups` data error.
## [1.3.0] - 2020-11-20
### Fixed
- Dependency bump ([#55](https://github.com/grzegorz914/homebridge-xbox-tv/issues/55))
## [1.2.41] - 2020-11-20
### Fixed
- Fix slow response on RC control.
## [1.2.1] - 2020-09-18
### Changes
- Updated device category to `TV_SET_TOP_BOX` ([#47](https://github.com/grzegorz914/homebridge-xbox-tv/pull/47))
## [1.2.0] - 2020-09-17
### Changes
- Fix send power on until successful ([#38](https://github.com/grzegorz914/homebridge-xbox-tv/issues/38))
- Fix remote control function ([#28](https://github.com/grzegorz914/homebridge-xbox-tv/issues/28))
- Add `refreshInterval` with a default of five seconds.
- Updated config layout.
## [1.1.0] - 2020-09-06
### Changes
- Completely reconfigured layout of the configuration schema.
## [1.0.0] - 2020-06-28
### Added
- Release version.
## [0.9.0] - 2020-05-23
### Added
- Add possibility to select what a type of extra volume control you want to use. None, Slider, Fan.
## [0.8.21] - 2020-05-23
### Changes
- Output app reference to log when opening app ([#22](https://github.com/grzegorz914/homebridge-xbox-tv/issues/22), [#26](https://github.com/grzegorz914/homebridge-xbox-tv/issues/26))
- Used for discovering the value to use for `reference` when adding new inputs.
## [0.8.0] - 2020-05-20
### Added
- Add mute ON/OFF to the slider volume.
## [0.7.60] - 2020-05-18
### Fixed
- Fix bug in RC control.
## [0.7.35] - 2020-05-17
### Added
- Add read console configuration after Homebridge restart and save to `/homebridge_folder/xboxTv/` file.
## [0.7.2] - 2020-05-14
### Added
- Add descriptions in `config.schema.json`.
## [0.7.0] - 2020-05-14
### Added
- Revert back with defaults inputs.
- Add input type to inputs.
- Add other fixes in code to prevent app crash without configured inputs.
## [0.6.0] - 2020-05-14
### Breaking Changes
- Update your config.json: Add types to the inputs.
### Default Inputs
```json
"inputs": [
{
"name": "TV",
"reference": "Microsoft.Xbox.LiveTV_8wekyb3d8bbwe!Microsoft.Xbox.LiveTV.Application",
"type": "HDMI"
},
{
"name": "Dashboard",
"reference": "Xbox.Dashboard_8wekyb3d8bbwe!Xbox.Dashboard.Application",
"type": "HOME_SCREEN"
},
{
"name": "Settings",
"reference": "Microsoft.Xbox.Settings_8wekyb3d8bbwe!Xbox.Settings.Application",
"type": "OTHER"
},
{
"name": "Accessory",
"reference": "Microsoft.XboxDevices_8wekyb3d8bbwe!App",
"type": "OTHER"
}
]
```
## [0.5.0] - 2020-05-10
### Changes
- Code cleanup.
- Miscellaneous fixes and performance improvements.
## [0.4.0] - 2020-05-06
### Changes
- Adapted to HAP-Node JS lib.
## [0.3.12] - 2020-05-05
### Changes
- Cleanup code.
### Breaking Changes
- Update your config.json: replace `apps` with `inputs`.
## [0.3.12] - 2020-05-05
### Changes
- Fix and performance improvements.
- Corrected logging state.
## [0.3.9] - 2020-05-05
### Added
- Add real time read and write data for lightbulb slider volume value.
## [0.2.3] - 2020-04-27
### Added
- Add switch ON/OFF volume control.
## [0.2.1] - 2020-04-27
### Added
- Add Siri volume control.
- Add slider or Brightness volume control.
## [0.1.39] - 2020-04-21
- Different fixes.
## [0.1.12] - 2020-04-13
- Fix memory leak.
## [0.1.9] - 2020-04-07
- Fix store of position in HomeKit favorites.
## [0.1.6] - 2020-04-06
- Test 2.
## [0.1.5] - 2020-04-05
- Test 1.
## [0.1.2] - 2020-04-05
- Some improvements.
## [0.1.1] - 2020-04-05
- Update `README.md`.
- Update `sample-config.json`.
## [0.1.0] - 2020-03-29
- Fix crash if no device name defined.
- Fix `config.schema.json`.
- Fix store file inside the Homebridge directory.
## [0.0.118] - 2020-03-29
- Small fixes.
## [0.0.115] - 2020-03-21
- Corrections for Homebridge git.
- Performance improvements.
## [0.0.1] - 2020-02-05
- Initial release.
================================================
FILE: CLAUDE.md
================================================
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
```bash
# Install dependencies
npm install
# Run the plugin locally (requires a working Homebridge setup)
homebridge
# No test suite exists — the test script exits with an error by design
npm test
```
There is no lint or build step. The project uses native ES modules (`"type": "module"` in package.json) and runs directly in Node.js without transpilation.
## Architecture
This is a Homebridge platform plugin that exposes Xbox consoles as HomeKit TV accessories. The entry point is [index.js](index.js), which registers `XboxPlatform` under the `XboxTv` platform name.
### Startup flow
`XboxPlatform` (index.js) reads `config.devices[]` and, for each device, runs a startup `ImpulseGenerator` that retries every 120 s until `XboxDevice.start()` succeeds. Once a device is up, it is published via `api.publishExternalAccessories()` and the startup generator stops, handing off to `XboxDevice`'s own connect generator (6 s interval).
### Core classes
- **[src/xboxdevice.js](src/xboxdevice.js)** — Central device class (`EventEmitter`). Orchestrates both APIs, builds all HAP services/characteristics, handles HomeKit gets/sets, and drives external integrations. All log events bubble up via `emit('info'|'warn'|'error'|'debug'|'success')`.
- **[src/impulsegenerator.js](src/impulsegenerator.js)** — Lightweight interval scheduler. `state(true, [{name, sampling}])` starts named intervals that fire events; `state(false)` clears them. Used as a polling/heartbeat mechanism throughout the codebase.
- **[src/functions.js](src/functions.js)** — Shared utilities: file I/O (`readData`/`saveData`), network ping, diacritics normalization.
- **[src/constants.js](src/constants.js)** — All protocol constants: HAP `InputSourceTypes`, Web API URLs/scopes, Local API channel UUIDs, SmartGlass message type/flag maps, default built-in inputs (Dashboard, Settings, etc.), `DiacriticsMap`.
### Two control paths
**Local API** (`src/localApi/`) — UDP-based SmartGlass protocol over the LAN:
- `xboxlocalapi.js` — Manages a UDP socket, discovery/connect handshake, heartbeat ping, and inbound message dispatch.
- `sgcrypto.js` — ECDH key exchange (P-256) + AES-128 + HMAC-SHA256 for the SmartGlass session.
- `simple.js` / `message.js` / `packets.js` / `structure.js` — Packet construction and parsing for the two SmartGlass packet families (simple pre-auth, encrypted post-auth).
**Web API** (`src/webApi/`) — Microsoft Xbox cloud REST API:
- `xboxwebapi.js` — Polls authorization every 15 min, sends commands via `axios` to `xccs.xboxlive.com`, retrieves console status and installed app list.
- `authentication.js` — OAuth 2.0 flow (OAuth → User token → XSTS token) with token persistence to `authToken_<host>` file.
- `providers/` — Individual REST endpoint wrappers (achievements, catalog, people, etc.). These are used by the web API to fetch app/game metadata.
### External integrations
- **[src/restful.js](src/restful.js)** — Express HTTP server. GET routes expose device state; POST `/` accepts `{key: value}` JSON to send commands back to the device.
- **[src/mqtt.js](src/mqtt.js)** — MQTT v5 client. Publishes state updates to `microsoft/<prefix>/<name>/<key>` topics; subscribes to `…/Set` for inbound commands.
### Persistent storage
All per-device state files live under `<homebridge-storage>/xboxTv/` with a host-IP-derived suffix (dots stripped):
- `authToken_<host>` — OAuth + XSTS tokens (JSON)
- `devInfo_<host>` — Console hardware info
- `inputs_<host>` — Installed app/game list from Web API
- `inputsNames_<host>` — User-customized input display names (from HomeKit)
- `inputsTargetVisibility_<host>` — Per-input visibility state (from HomeKit)
### Homebridge UI
`homebridge-ui/server.js` runs as a `HomebridgePluginUiServer` and exposes two custom endpoints used by the Config UI:
- `/clearToken` — Wipes the auth token file so the OAuth flow can restart.
- `/startAuthorization` — Drives the OAuth token exchange when the user pastes a code.
### HomeKit accessory model
Each Xbox is published as an external accessory (`Categories.TELEVISION`). Services created in `XboxDevice.start()`:
- `Television` — primary, with `ActiveIdentifier` mapped to inputs
- `TelevisionSpeaker` — volume/mute
- Up to 85 `InputSource` services (built-in defaults + Web API app list + user-configured static inputs)
- Optional `Switch`/`Outlet` services for each configured button
- Optional `MotionSensor`/`OccupancySensor`/`ContactSensor` services for each configured sensor
- Optional `Lightbulb` service for volume control
Input identifiers are 1-based integers assigned at build time; display order can be sorted by name or reference via `inputs.displayOrder` config.
================================================
FILE: LICENSE
================================================
# MIT License
Copyright (c) 2022 Grzegorz Kaczor
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
================================================
FILE: README.md
================================================
<p align="center">
<a href="https://github.com/grzegorz914/homebridge-xbox-tv"><img alt="Xbox and controller" src="https://raw.githubusercontent.com/grzegorz914/homebridge-xbox-tv/master/graphics/homebridge-xbox-tv.png" width="640"></a>
</p>
<span align="center">
# Homebridge Xbox TV
[](https://github.com/homebridge/homebridge/wiki/Verified-Plugins)
[](https://www.npmjs.com/package/homebridge-xbox-tv)
[](https://www.npmjs.com/package/homebridge-xbox-tv)
[](https://www.npmjs.com/package/homebridge-xbox-tv)
[](https://github.com/grzegorz914/homebridge-xbox-tv/pulls)
[](https://github.com/grzegorz914/homebridge-xbox-tv/issues)
<a href="https://buycoffee.to/grzegorz914" target="_blank"><img src="https://raw.githubusercontent.com/grzegorz914/homebridge-xbox-tv/main/graphics/buycoffee-button.png" style="width: 234px; height: 61px" alt="Supports My Work"></a> <a href="https://github.com/grzegorz914/homebridge-xbox-tv"><img src="https://raw.githubusercontent.com/grzegorz914/homebridge-xbox-tv/main/graphics/QR_buycoffee.png" width="61"></a>
</span>
## Package Requirements
| Package | Installation | Role | Required |
| --- | --- | --- | --- |
| [Homebridge](https://github.com/homebridge/homebridge) | [Homebridge Wiki](https://github.com/homebridge/homebridge/wiki) | HomeKit Bridge | Required |
| [Homebridge UI](https://github.com/homebridge/homebridge-config-ui-x) | [Homebridge UI Wiki](https://github.com/homebridge/homebridge-config-ui-x/wiki) | Homebridge Web User Interface | Required |
| [Xbox TV](https://www.npmjs.com/package/homebridge-xbox-tv) | [Plug-In Wiki](https://github.com/grzegorz914/homebridge-xbox-tv/wiki) | Homebridge Plug-In | Required |
## Warning
* For plugin < v4.1.0 use Homebridge UI <= v5.5.0.
* For plugin >= v4.1.0 use Homebridge UI >= v5.13.0.
## About The Plugin
* Power ON/OFF short press tile in HomeKit app.
* Reboot Console with button, rquired `webApiControl` enabled.
* Record Game DVR with button, rquired `webApiControl` enabled.
* RC/Media/Volume control from RC app on iPhone/iPad and possible over web/local api.
* Speaker control from RC app on iPhone/iPad `Speaker Service`.
* Legacy Volume/Mute control is possible throught extra `lightbulb`/`fan` (slider).
* Apps, Inputs, Games can be switched if `webApiControl` is enabled and console is authorized.
* Siri can be used for all functions, some times need to be created legacy buttons/switches/sensors.
* Automations can be used for all functions, some times need to be created legacy buttons/switches/sensors.
* Support external integrations, [RESTFul](https://github.com/grzegorz914/homebridge-xbox-tv?tab=readme-ov-file#restful-integration), [MQTT](https://github.com/grzegorz914/homebridge-xbox-tv?tab=readme-ov-file#mqtt-integration).
<p align="center">
<a href="https://github.com/grzegorz914/homebridge-xbox-tv"><img alt="Accessory tile in the HomeKit app" src="https://raw.githubusercontent.com/grzegorz914/homebridge-xbox-tv/master/graphics/homekit.png" width="382" /></a>
<a href="https://github.com/grzegorz914/homebridge-xbox-tv"><img alt="Changing the accessory input" src="https://raw.githubusercontent.com/grzegorz914/homebridge-xbox-tv/master/graphics/inputs.png" width="135" /></a>
<a href="https://github.com/grzegorz914/homebridge-xbox-tv"><img alt="Arrow pointing to the remote control icon in the control center" src="https://raw.githubusercontent.com/grzegorz914/homebridge-xbox-tv/master/graphics/rc1.png" width="135" /></a>
<a href="https://github.com/grzegorz914/homebridge-xbox-tv"><img alt="Remote control interface" src="https://raw.githubusercontent.com/grzegorz914/homebridge-xbox-tv/master/graphics/RC.png" width="135" /></a>
</p>
## Authorization Manager
* First of all please use built in Authorization Manager.
* Start new authorization need remove old token first, to clear token use Authorization Manager GUI.
* Make sure Your web browser do not block pop-up window, if Yes allow pop-up window for this app.
<p align="center">
<a href="https://github.com/grzegorz914/homebridge-xbox-tv"><img alt="Authentication Manager" src="https://raw.githubusercontent.com/grzegorz914/homebridge-xbox-tv/master/graphics/config manager.png" width="540"></a>
</p>
## Configuration
* [Device must have Instant-on power mode enabled](https://support.xbox.com/help/hardware-network/power/learn-about-power-modes)
* Profile & System > Settings > General > Power mode & startup
* Console need to allow connect from any 3rd app. *Allow Connections from any device* should be enabled.
* Profile & System > Settings > Devices & Connections > Remote features > Xbox app preferences.
* Run this plugin as a [Child Bridge](https://github.com/homebridge/homebridge/wiki/Child-Bridges) (Highly Recommended), this prevent crash Homebridge if plugin crashes.
* Install and use [Homebridge UI](https://github.com/homebridge/homebridge-config-ui-x/wiki) to configure this plugin.
* The `sample-config.json` can be edited and used as an alternative (advanced users).
<p align="center">
<a href="https://github.com/grzegorz914/homebridge-xbox-tv"><img src="https://raw.githubusercontent.com/grzegorz914/homebridge-xbox-tv/master/graphics/ustawienia.png" width="840"></a>
</p>
| Key | Description |
| --- | --- |
| `name` | Here set the accessory `Name` to be displayed in `Homebridge/HomeKit`. |
| `host` | Here set the `Hsostname or Address IP` of Console.|
| `xboxLiveId` | On your console select Profile > Settings > System > Console info, listed as ``Xbox network device ID``. `You can only find the Xbox network device ID in Settings on your console, this is different from your console serial number`. |
| `displayType` | Accessory type to be displayed in Home app: `0 - None/Disabled`, `1 - Television` , `2 - TV Set Top Box`, `3 - TV Streaming Stick`, `4 - Audio Receiver`. |
| `webApi{}` | Web Api object. |
| `webApi.enable` | This enable console control over Web Api. Additional functions are available in `Advanced Settings` section. |
| `webApi.token` | Required if `webApiControl` enabled, use Authorization Manager to get it. |
| `webApi.clientId` | Here set your own Client Id from Azure AD or leave empty if you do not have own account. |
| `webApi.clientSecret` | Here set your Client Secret from Azure AD or leave empty if you do not have own account. |
| `inputs{}` | Inputs object. |
| `inputs.getFromDevice`| If enabled, apps will be loaded from device, only available if `webApiControl` enabled. |
| `inputs.filterGames` | If enabled, Games will be hidden and not displayed in the inputs list, only available if `webApiControl` enabled. |
| `inputs.filterApps` | If enabled, Apps will be hidden and not displayed in the inputs list, only available if `webApiControl` enabled. |
| `inputs.filterSystemApps` | If enabled, System Apps (Accessory, Microsoft Store, Television) will be hidden and not displayed in the inputs list, only available if `webApiControl` enabled. |
| `inputs.filterDlc` | If enabled, Dlc will be hidden and not displayed in the inputs list, only available if `webApiControl` enabled. |
| `inputs.displayOrder` | Here select display order of the inputs list, `0 -None`, `1 -Ascending by Name`, `2 - Descending by Name`, `3 - Ascending by Reference`, `4 - Ascending by Reference`. |
| `inputs.data[]` | Inputs data array. |
| `inputs.data[].name` | Here set `Input Name` which You want expose to the `Homebridge/HomeKit`, `Screensaver`, `Television`, `TV Settings`, `Dashboard`, `Accessory`, `Settings`, `Network Troubleshooter`, `Microsoft Store`, `Xbox Guide` are created by default. |
| `inputs.data[].reference` | Required to identify current running app. |
| `inputs.data[].oneStoreProductId` | Required to switch apps. |
| `inputs.data[].contentType` | Here select from available content types. |
| `buttons[]` | Buttons array. |
| `buttons[].displayType` | Here select display type in HomeKit app, possible `0 - None/Disabled`, `1 - Outlet`, `2 - Switch`. |
| `buttons[].name` | Here set `Button Name` which You want expose to the `Homebridge/HomeKit`. |
| `buttons[].mode` | Here select button mode, `0 - Media Control`, `1 - Game Pad Control`, `2 - TV Remote Control`, `3 - Console Control`, `4 - Game/App Control`. |
| `buttons[].mediaCommand` | Here select media control command. |
| `buttons[].gamePadCommand` | Here select game pad control command. |
| `buttons[].tvRemoteCommand` | Here select tv remote control command. |
| `buttons[].consoleControlCommand` | Here select console control command. |
| `buttons[].gameAppControlCommand` | Here set `oneStoreProductId`, only possible if `webApiControl` enabled. |
| `buttons[].namePrefix` | Here enable/disable the accessory name as a prefix for button name.|
| `sensors[]` | Sensors array. |
| `sensors[].displayType` | Here choose the sensor type to be exposed in HomeKit app, possible `0 - None/Disabled`, `1 - Motion Sensor`, `2 - Occupancy Sensor`, `3 - Contact Sensor`. |
| `sensors[].mode` | Here choose the sensor mode, possible `0 - App / Games`, `1 - Power`, `2 - Volume`, `3 - Mute`, `4 - Screen Saver`, `5 - Play State`. |
| `sensors[].name` | Here set own sensor `Name` which You want expose to the `Homebridge/HomeKit`. |
| `sensors[].reference` | Here set mode `Reference` like `Xbox.Dashboard_8wekyb3d8bbwe!Xbox.Dashboard.Application`, sensor fired on switch to this reference. |
| `sensors[].pulse` | Here enable sensor pulse, sensor send pulse and fired on every value change of selected mode. |
| `sensors[].namePrefix` | Here enable the accessory name as a prefix for sensor name. |
| `sensors[].level` | Here set `Level` between `0-100`, sensor fired on this level. |
| `volume{}` | Volume object. |
| `volume.displayType` | Here choice what a additional volume control mode You want to use `0 - None/Disabled`, `1 - Lightbulb`, `2 - Fan`, `3 - TV Speaker (only hardware buttons on R.C. app)`, `4 - TV Speaker / Lightbulb`, `5 - TV Speaker / Fan`. |
| `volume.name` | Here set Your own volume control name or leave empty. |
| `volume.namePrefix` | Here enable/disable the accessory name as a prefix for volume control name. |
| `infoButtonCommand` | Here select the function of `I` button in RC app. |
| `log{}` | Log object. |
| `log.deviceInfo` | If enabled, log device info will be displayed by every connections device to the network. |
| `log.sSuccess` | If enabled, success log will be displayed in console. |
| `log.info` | If enabled, info log will be displayed in console. |
| `log.warn` | If enabled, warn log will be displayed in console. |
| `log.error` | If enabled, error log will be displayed in console. |
| `log.debug` | If enabled, debug log will be displayed in console. |
| `restFul{}` | RESTFul object. |
| `restFul.enable` | If enabled, RESTful server will start automatically and respond to any path request. |
| `restFul.port` | Here set the listening `Port` for RESTful server. |
| `mqtt{}` | MQTT object. |
| `mqtt.enable` | If enabled, MQTT Broker will start automatically and publish all awailable PV data. |
| `mqtt.host` | Here set the `IP Address` or `Hostname` for MQTT Broker. |
| `mqtt.port` | Here set the `Port` for MQTT Broker, default 1883. |
| `mqtt.clientId` | Here optional set the `Client Id` of MQTT Broker. |
| `mqtt.prefix` | Here set the `Prefix` for `Topic` or leave empty. |
| `mqtt.auth{}` | MQTT authorization object. |
| `mqtt.auth.enable` | Here enable authorization for MQTT Broker. |
| `mqtt.auth.user` | Here set the MQTT Broker user. |
| `mqtt.auth.passwd` | Here set the MQTT Broker password. |
| `reference`, `oneStoreProductId` | If web Api enabled then all available in `./homebridge/xboxTv/inputs_xxxxxx` file. |
## Create App on Azure AD
* Go to [Azure Portal](https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade)
* Register new *App* + *New registration*
* Enter a name for your app
* Set *Supported account types* to *Personal Microsoft accounts only*
* Click register
* Choose *Redirect URIs* -> *Add a Redirect URI*
* Click *Add a platform* -> *Mobile and desktop applications*
* Enter custom redirect URI *<http://localhost:8888/auth/callback>*
* From the overview of your app page, copy *Application (client) ID* to `webApiClientId`
* Save restart plugin and authorize console again and have fun.
### RESTFul Integration
* POST data as a JSON Object `{Power: true}`, content type must be `application/json`
* Path `status` response all available paths.
| Method | URL | Path | Response | Type |
| --- | --- | --- | --- | --- |
| GET | `http//ip:port` | `info`, `state`, `consoleslist`, `profile`, `apps`, `storages`, `status`. | `{"power": true, "app": Xbox.Dashboard_8wekyb3d8bbwe!Xbox.Dashboard.Application}` | JSON object. |
| Method | URL | Key | Value | Type | Description |
| --- | --- | --- | --- | --- | --- |
| POST | `http//ip:port` | `Power` | `true`, `false` | boolean | Power state. |
| | `http//ip:port` | `App` | `Xbox.Dashboard_8wekyb3d8bbwe!Xbox.Dashboard.Application` | string | Set app. |
| | `http//ip:port` | `RcControl` | `fastForward` | string | Send RC command. |
| | `http//ip:port` | `Volume` | `up`, `down` | string | Set volume. |
| | `http//ip:port` | `Mute` | `true`, `false` | boolean | Set mute. |
### MQTT Integration
* Subscribe data as a JSON Object `{App: "Xbox.Dashboard_8wekyb3d8bbwe!Xbox.Dashboard.Application"}`
| Method | Topic | Message | Type |
| --- | --- | --- | --- |
| Publish | `Info`, `State`, `Consoles List`, `Profile`, `Apps`, `Storages`, `Status` | `{"power": true, "app": Xbox.Dashboard_8wekyb3d8bbwe!Xbox.Dashboard.Application}` | JSON object. |
| Method | Topic | Key | Value | Type | Description |
| --- | --- | --- | --- | --- | --- |
| Subscribe | `Set` | `Power` | `true`, `false` | boolean | Power state. |
| | `Set` | `App` | `Xbox.Dashboard_8wekyb3d8bbwe!Xbox.Dashboard.Application` | string | Set app. |
| | `Set` | `RcControl` | `fastForward` | string | Send RC command. |
| | `Set` | `Volume` | `up`, `down` | string | Set volume. |
| | `Set` | `Mute` | `true`, `false` | boolean | Set mute. |
================================================
FILE: config.schema.json
================================================
{
"pluginAlias": "XboxTv",
"pluginType": "platform",
"singular": true,
"fixArrays": true,
"strictValidation": true,
"customUi": true,
"headerDisplay": "This plugin works with Xbox Game Consoles. Devices are exposed to HomeKit as separate accessories and each needs to be manually paired.",
"footerDisplay": "For documentation please see [GitHub repository](https://github.com/grzegorz914/homebridge-xbox-tv).",
"schema": {
"type": "object",
"properties": {
"name": {
"title": "Platform",
"type": "string",
"default": "Xbox TV"
},
"devices": {
"type": "array",
"items": {
"title": "Device",
"type": "object",
"properties": {
"name": {
"title": "Name",
"type": "string",
"placeholder": "Game console"
},
"host": {
"title": "IP/Hostname",
"type": "string",
"placeholder": "192.168.1.10 or xbox.local",
"pattern": "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}|([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)(\\.([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?))*)$"
},
"xboxLiveId": {
"title": "Live ID",
"type": "string",
"placeholder": "FD00000000000000",
"description": "Xbox Live ID"
},
"displayType": {
"title": "Accessory type",
"type": "integer",
"minimum": 0,
"maximum": 4,
"default": 2,
"anyOf": [
{
"title": "None / Disabled",
"enum": [
0
]
},
{
"title": "Television",
"enum": [
1
]
},
{
"title": "TV Set Top Box",
"enum": [
2
]
},
{
"title": "TV Streaming Stick",
"enum": [
3
]
},
{
"title": "Audio Receiver",
"enum": [
4
]
}
],
"description": "Accessory type for Home app"
},
"webApi": {
"title": "Web Api",
"type": "object",
"properties": {
"enable": {
"title": "Enable",
"type": "boolean",
"default": false,
"description": "This enable console control over Web Api."
},
"token": {
"title": "Token",
"type": "string",
"placeholder": "Web Api Token",
"description": "Here put the reponse Token (value after ?code=) from the authorization URL.",
"format": "password",
"condition": {
"functionBody": "return model.devices[arrayIndices].webApi.enable === true;"
}
},
"clientId": {
"title": "Client Id",
"type": "string",
"placeholder": "a34ac209-edab-4b08-91e7-a4558d8da1bd",
"description": "Here set your own Client Id from Azure AD or leave empty if you do not have own account.",
"condition": {
"functionBody": "return model.devices[arrayIndices].webApi.enable === true;"
}
},
"clientSecret": {
"title": "Client Secret",
"type": "string",
"placeholder": "Client Secret",
"description": "Here set your Client Secret from Azure AD or leave empty if you do not have own account.",
"format": "password",
"condition": {
"functionBody": "return model.devices[arrayIndices].webApi.enable === true;"
}
}
},
"allOf": [
{
"if": {
"required": [
"enable"
],
"properties": {
"enable": {
"const": true
}
}
},
"then": {
"required": [
"token"
]
}
}
]
},
"inputs": {
"title": "Inputs",
"type": "object",
"properties": {
"getFromDevice": {
"title": "Load Inputs From Device",
"type": "boolean",
"description": "This function get all available inputs direct from device, manually configured inputs will be skipped.",
"condition": {
"functionBody": "return model.devices[arrayIndices].webApi.enable === true;"
}
},
"filterGames": {
"title": "Hide Games",
"type": "boolean",
"default": false,
"description": "If enabled, Games will not be displayed on the list of inputs."
},
"filterApps": {
"title": "Hide Apps",
"type": "boolean",
"default": false,
"description": "If enabled, Apps will not be displayed on the list of inputs."
},
"filterSystemApps": {
"title": "Hide System Apps",
"type": "boolean",
"default": false,
"description": "If enabled, System Apps (Accessory, TV, Network Troubleshooter, Xbox Guide) will not be displayed on the list of inputs."
},
"filterDlc": {
"title": "Hide DLC",
"type": "boolean",
"default": false,
"description": "If enabled, DLC will not be displayed on the list of inputs."
},
"displayOrder": {
"title": "Display Order",
"type": "integer",
"minimum": 0,
"maximum": 4,
"description": "Here select display order of the inputs list.",
"anyOf": [
{
"title": "Disabled",
"enum": [
0
]
},
{
"title": "Ascending by Name",
"enum": [
1
]
},
{
"title": "Descending by Name",
"enum": [
2
]
},
{
"title": "Ascending by Reference",
"enum": [
3
]
},
{
"title": "Descending by Reference",
"enum": [
4
]
}
]
},
"data": {
"title": "Inputs",
"type": "array",
"items": {
"title": "Input",
"type": "object",
"properties": {
"name": {
"title": "Name",
"type": "string",
"placeholder": "Input name",
"description": "Here set Your own name."
},
"titleId": {
"title": "Title Id",
"type": "string",
"placeholder": "Input Title Id",
"description": "Here set title Id. If web api is enable all available in */var/lib/homebridge/xboxTv/inputs_xxxxxx* file."
},
"reference": {
"title": "Reference",
"type": "string",
"placeholder": "Input reference",
"description": "Here set reference. If web api is enable all available in */var/lib/homebridge/xboxTv/inputs_xxxxxx* file."
},
"oneStoreProductId": {
"title": "Product Id",
"type": "string",
"placeholder": "oneStoreProductId",
"description": "Here set the *oneStoreProductId*. If web api enable, switch app/games will be possible, all available in */var/lib/homebridge/xboxTv/inputs_xxxxxx* file.",
"condition": {
"functionBody": "return model.devices[arrayIndices].webApi.enable === true;"
}
},
"contentType": {
"title": "Content Type",
"type": "string",
"description": "Here select source input type.",
"anyOf": [
{
"title": "Game",
"enum": [
"Game"
]
},
{
"title": "Application",
"enum": [
"App"
]
},
{
"title": "System Appliction",
"enum": [
"System App"
]
},
{
"title": "Dlc",
"enum": [
"Dlc"
]
}
]
}
}
},
"condition": {
"functionBody": "return model.devices[arrayIndices].inputs.getFromDevice === false || model.devices[arrayIndices].webApi.enable === false;"
}
}
}
},
"buttons": {
"title": "Button",
"type": "array",
"items": {
"type": "object",
"properties": {
"displayType": {
"title": "Display Type",
"type": "integer",
"minimum": 0,
"maximum": 2,
"description": "Here select display type in HomeKit app.",
"anyOf": [
{
"title": "Disabled",
"enum": [
0
]
},
{
"title": "Outlet",
"enum": [
1
]
},
{
"title": "Switch",
"enum": [
2
]
}
]
},
"name": {
"title": "Name",
"type": "string",
"placeholder": "Button name",
"description": "Here set Your own name."
},
"mode": {
"title": "Mode",
"type": "integer",
"minimum": 0,
"maximum": 4,
"description": "Here select the function mode.",
"anyOf": [
{
"title": "Media Control",
"enum": [
0
]
},
{
"title": "Game Pad Control",
"enum": [
1
]
},
{
"title": "TV Remote Control",
"enum": [
2
]
},
{
"title": "Console Control",
"enum": [
3
]
},
{
"title": "Game/App Control",
"enum": [
4
]
}
],
"condition": {
"functionBody": "return model.devices[arrayIndices[0]].buttons[arrayIndices[1]].displayType > 0;"
}
},
"mediaCommand": {
"title": "Media Command",
"type": "string",
"description": "Here select the media command.",
"anyOf": [
{
"title": "Play",
"enum": [
"play"
]
},
{
"title": "Pause",
"enum": [
"pause"
]
},
{
"title": "Play/Pause",
"enum": [
"playPause"
]
},
{
"title": "Stop",
"enum": [
"stop"
]
},
{
"title": "Record",
"enum": [
"record"
]
},
{
"title": "Next Track",
"enum": [
"nextTrack"
]
},
{
"title": "Previous Track",
"enum": [
"prevTrack"
]
},
{
"title": "Fast Forward",
"enum": [
"fastForward"
]
},
{
"title": "Rewind",
"enum": [
"rewind"
]
},
{
"title": "Channel Up",
"enum": [
"channelUp"
]
},
{
"title": "Channel Down",
"enum": [
"channelDown"
]
},
{
"title": "Back",
"enum": [
"back"
]
},
{
"title": "View",
"enum": [
"view"
]
},
{
"title": "Menu",
"enum": [
"menu"
]
},
{
"title": "Seek",
"enum": [
"seek"
]
}
],
"condition": {
"functionBody": "return model.devices[arrayIndices[0]].buttons[arrayIndices[1]].displayType > 0 && model.devices[arrayIndices[0]].buttons[arrayIndices[1]].mode === 0;"
}
},
"gamePadCommand": {
"title": "Game Pad Command",
"type": "string",
"description": "Here select the controler command.",
"anyOf": [
{
"title": "Nexus",
"enum": [
"nexus"
]
},
{
"title": "View",
"enum": [
"view"
]
},
{
"title": "Menu",
"enum": [
"menu"
]
},
{
"title": "A",
"enum": [
"a"
]
},
{
"title": "B",
"enum": [
"b"
]
},
{
"title": "X",
"enum": [
"x"
]
},
{
"title": "Y",
"enum": [
"y"
]
},
{
"title": "Up",
"enum": [
"up"
]
},
{
"title": "Down",
"enum": [
"down"
]
},
{
"title": "Left",
"enum": [
"left"
]
},
{
"title": "Right",
"enum": [
"right"
]
}
],
"condition": {
"functionBody": "return model.devices[arrayIndices[0]].buttons[arrayIndices[1]].displayType > 0 && model.devices[arrayIndices[0]].buttons[arrayIndices[1]].mode === 1;"
}
},
"tvRemoteCommand": {
"title": "TV Remote Command",
"type": "string",
"description": "Here select the tv remote command.",
"anyOf": [
{
"title": "Volume Up",
"enum": [
"volUp"
]
},
{
"title": "Volume Down",
"enum": [
"volDown"
]
},
{
"title": "Mute",
"enum": [
"volMute"
]
}
],
"condition": {
"functionBody": "return model.devices[arrayIndices[0]].buttons[arrayIndices[1]].displayType > 0 && model.devices[arrayIndices[0]].buttons[arrayIndices[1]].mode === 2;"
}
},
"consoleControlCommand": {
"title": "Console Control Command",
"type": "string",
"description": "Here select the console control command.",
"anyOf": [
{
"title": "Reboot",
"enum": [
"reboot"
]
},
{
"title": "Record Game DVR",
"enum": [
"recordGameDvr"
]
}
],
"condition": {
"functionBody": "return model.devices[arrayIndices[0]].buttons[arrayIndices[1]].displayType > 0 && model.devices[arrayIndices[0]].buttons[arrayIndices[1]].mode === 3;"
}
},
"gameAppControlCommand": {
"title": "Product Id",
"type": "string",
"placeholder": "oneStoreProductId",
"description": "Here set the *oneStoreProductId*. If web api enabled, switch app/games will be possible, all available in */var/lib/homebridge/xboxTv/inputs_xxxxxx* file.",
"condition": {
"functionBody": "return model.devices[arrayIndices[0]].buttons[arrayIndices[1]].displayType > 0 && model.devices[arrayIndices[0]].buttons[arrayIndices[1]].mode === 4;"
}
},
"namePrefix": {
"title": "Prefix",
"type": "boolean",
"description": "Here enable/disable the accessory name as a prefix for button name.",
"condition": {
"functionBody": "return model.devices[arrayIndices[0]].buttons[arrayIndices[1]].displayType > 0;"
}
}
},
"allOf": [
{
"if": {
"required": [
"displayType"
],
"properties": {
"displayType": {
"enum": [
1,
2
]
}
}
},
"then": {
"required": [
"mode"
]
}
},
{
"if": {
"required": [
"displayType",
"mode"
],
"properties": {
"displayType": {
"enum": [
1,
2
]
},
"mode": {
"enum": [
0
]
}
}
},
"then": {
"required": [
"mediaCommand"
]
}
},
{
"if": {
"required": [
"displayType",
"mode"
],
"properties": {
"displayType": {
"enum": [
1,
2
]
},
"mode": {
"enum": [
1
]
}
}
},
"then": {
"required": [
"gamePadCommand"
]
}
},
{
"if": {
"required": [
"displayType",
"mode"
],
"properties": {
"displayType": {
"enum": [
1,
2
]
},
"mode": {
"enum": [
2
]
}
}
},
"then": {
"required": [
"tvRemoteCommand"
]
}
},
{
"if": {
"required": [
"displayType",
"mode"
],
"properties": {
"displayType": {
"enum": [
1,
2
]
},
"mode": {
"enum": [
3
]
}
}
},
"then": {
"required": [
"consoleControlCommand"
]
}
},
{
"if": {
"required": [
"displayType",
"mode"
],
"properties": {
"displayType": {
"enum": [
1,
2
]
},
"mode": {
"enum": [
4
]
}
}
},
"then": {
"required": [
"gameAppControlCommand"
]
}
}
]
}
},
"sensors": {
"type": "array",
"items": {
"title": "Sensors",
"type": "object",
"properties": {
"displayType": {
"title": "Display Type",
"type": "integer",
"minimum": 0,
"maximum": 3,
"default": 0,
"anyOf": [
{
"title": "Disabled",
"enum": [
0
]
},
{
"title": "Motion Sensor",
"enum": [
1
]
},
{
"title": "Occupancy Sensor",
"enum": [
2
]
},
{
"title": "Contact Sensor",
"enum": [
3
]
}
],
"description": "Here select sensor type to be exposed in HomeKit app."
},
"mode": {
"title": "Mode",
"type": "integer",
"minimum": 0,
"maximum": 5,
"anyOf": [
{
"title": "App / Games",
"enum": [
0
]
},
{
"title": "Power",
"enum": [
1
]
},
{
"title": "Volume",
"enum": [
2
]
},
{
"title": "Mute",
"enum": [
3
]
},
{
"title": "Screen Saver",
"enum": [
4
]
},
{
"title": "Play State",
"enum": [
5
]
}
],
"description": "Here select the sensor mode.",
"condition": {
"functionBody": "return model.devices[arrayIndices[0]].sensors[arrayIndices[1]].displayType > 0;"
}
},
"name": {
"title": "Name",
"type": "string",
"placeholder": "Name",
"description": "Here set Your own name."
},
"namePrefix": {
"title": "Prefix",
"type": "boolean",
"description": "Here enable the accessory name as a prefix for sensor name.",
"condition": {
"functionBody": "return model.devices[arrayIndices[0]].sensors[arrayIndices[1]].displayType > 0;"
}
},
"pulse": {
"title": "Pulse",
"type": "boolean",
"description": "Here enable sensor pulse, sensor send pulse and fired on every value change of selected mode.",
"condition": {
"functionBody": "return model.devices[arrayIndices[0]].sensors[arrayIndices[1]].displayType > 0;"
}
},
"reference": {
"title": "Reference",
"type": "string",
"placeholder": "Reference",
"description": "Here set the channel reference, sensor fired if switch to this reference.",
"condition": {
"functionBody": "return model.devices[arrayIndices[0]].sensors[arrayIndices[1]].displayType > 0 && model.devices[arrayIndices[0]].sensors[arrayIndices[1]].pulse === false && model.devices[arrayIndices[0]].sensors[arrayIndices[1]].mode === 0;"
}
},
"level": {
"title": "Level",
"type": "number",
"minimum": 0,
"maximum": 100,
"multipleOf": 1,
"description": "Here set the level between 0-100% at which the sensor fired.",
"condition": {
"functionBody": "return model.devices[arrayIndices[0]].sensors[arrayIndices[1]].displayType > 0 && model.devices[arrayIndices[0]].sensors[arrayIndices[1]].pulse === false && model.devices[arrayIndices[0]].sensors[arrayIndices[1]].mode === 2;"
}
}
},
"allOf": [
{
"if": {
"required": [
"displayType"
],
"properties": {
"displayType": {
"enum": [
1,
2,
3
]
}
}
},
"then": {
"required": [
"mode"
]
}
},
{
"if": {
"required": [
"displayType",
"mode",
"pulse"
],
"properties": {
"displayType": {
"enum": [
1,
2,
3
]
},
"pulse": {
"const": false
},
"mode": {
"enum": [
0
]
}
}
},
"then": {
"required": [
"reference"
]
}
},
{
"if": {
"required": [
"displayType",
"mode",
"pulse"
],
"properties": {
"displayType": {
"enum": [
1,
2,
3
]
},
"pulse": {
"const": false
},
"mode": {
"enum": [
2
]
}
}
},
"then": {
"required": [
"level"
]
}
}
]
}
},
"volume": {
"title": "Volume",
"type": "object",
"properties": {
"displayType": {
"title": "Display Type",
"type": "integer",
"description": "Here select what a extra volume/mute control type You want to use.",
"anyOf": [
{
"title": "Disabled",
"enum": [
0
]
},
{
"title": "Lightbulb",
"enum": [
1
]
},
{
"title": "Fan",
"enum": [
2
]
},
{
"title": "TV Speaker (HW buttons)",
"enum": [
3
]
},
{
"title": "TV Speaker / Lightbulb",
"enum": [
4
]
},
{
"title": "TV Speaker / Fan",
"enum": [
5
]
}
]
},
"name": {
"title": "Name",
"type": "string",
"placeholder": "Volume Control Name",
"description": "Here set Your own volume control name or leave empty.",
"condition": {
"functionBody": "return model.devices[arrayIndices[0]].volume.displayType > 0;"
}
},
"namePrefix": {
"title": "Prefix",
"type": "boolean",
"description": "Here enable/disable the accessory name as a prefix for volume control name.",
"condition": {
"functionBody": "return model.devices[arrayIndices[0]].volume.displayType > 0;"
}
}
}
},
"infoButtonCommand": {
"title": "Info Button",
"type": "string",
"default": "nexus",
"description": "Here select the function of info button in RC.",
"anyOf": [
{
"title": "Game Pad View",
"enum": [
"view"
]
},
{
"title": "Game Pad Nexus",
"enum": [
"nexus"
]
},
{
"title": "Game Pad Menu",
"enum": [
"menu"
]
},
{
"title": "Game Pad A",
"enum": [
"a"
]
},
{
"title": "Game Pad B",
"enum": [
"b"
]
},
{
"title": "Game Pad X",
"enum": [
"x"
]
},
{
"title": "Game Pad Y",
"enum": [
"y"
]
}
]
},
"log": {
"title": "Log",
"type": "object",
"properties": {
"deviceInfo": {
"title": "Device Info",
"type": "boolean",
"default": true,
"description": "This enable log level device info, will display overall device info by every plugin restart"
},
"success": {
"title": "Success",
"type": "boolean",
"default": true,
"description": "This enable log level success"
},
"info": {
"title": "Info",
"type": "boolean",
"default": false,
"description": "This enable log level info"
},
"warn": {
"title": "Warn",
"type": "boolean",
"default": true,
"description": "This enable log level warn"
},
"error": {
"title": "Error",
"type": "boolean",
"default": true,
"description": "This enable log level error"
},
"debug": {
"title": "Debug",
"type": "boolean",
"default": false,
"description": "This enable log level debug"
}
}
},
"restFul": {
"title": "RESTFul",
"type": "object",
"properties": {
"enable": {
"title": "Enable",
"type": "boolean",
"default": false,
"description": "This enable RESTful server."
},
"port": {
"title": "Port",
"type": "integer",
"placeholder": 3000,
"description": "Here set the listening Port for RESTful server.",
"condition": {
"functionBody": "return model.devices[arrayIndices].restFul.enable === true;"
}
}
}
},
"mqtt": {
"title": "MQTT",
"type": "object",
"properties": {
"enable": {
"title": "Enable",
"type": "boolean",
"default": false,
"description": "This enable MQTT client."
},
"host": {
"title": "IP/Hostname",
"type": "string",
"placeholder": "192.168.1.20 or mqtt.local",
"pattern": "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}|([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)(\\.([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?))*)$",
"description": "Here set the IP/Hostname of MQTT Broker.",
"condition": {
"functionBody": "return model.devices[arrayIndices].mqtt.enable === true;"
}
},
"port": {
"title": "Port",
"type": "integer",
"placeholder": 1883,
"description": "Here set the port of MQTT Broker.",
"condition": {
"functionBody": "return model.devices[arrayIndices].mqtt.enable === true;"
}
},
"clientId": {
"title": "Client ID",
"type": "string",
"placeholder": "client id",
"description": "Here optional set the Client ID of MQTT Broker.",
"condition": {
"functionBody": "return model.devices[arrayIndices].mqtt.enable === true"
}
},
"prefix": {
"title": "Prefix",
"type": "string",
"placeholder": "home",
"description": "Here set the prefix.",
"condition": {
"functionBody": "return model.devices[arrayIndices].mqtt.enable === true;"
}
},
"auth": {
"title": "Authorization",
"type": "object",
"properties": {
"enable": {
"title": "Enable",
"type": "boolean",
"description": "This enable authorization for MQTT Broker."
},
"user": {
"title": "User",
"type": "string",
"placeholder": "user",
"description": "Here set the user of MQTT Broker.",
"condition": {
"functionBody": "return model.devices[arrayIndices].mqtt.auth.enable === true;"
}
},
"passwd": {
"title": "Password",
"type": "string",
"placeholder": "password",
"description": "Here set the password of MQTT Broker.",
"format": "password",
"condition": {
"functionBody": "return model.devices[arrayIndices].mqtt.auth.enable === true;"
}
}
},
"condition": {
"functionBody": "return model.devices[arrayIndices].mqtt.enable === true;"
},
"allOf": [
{
"if": {
"required": [
"enable"
],
"properties": {
"enable": {
"const": true
}
}
},
"then": {
"required": [
"user",
"passwd"
]
}
}
]
}
},
"allOf": [
{
"if": {
"required": [
"enable"
],
"properties": {
"enable": {
"const": true
}
}
},
"then": {
"required": [
"host"
]
}
}
]
}
},
"required": [
"name",
"host",
"xboxLiveId",
"displayType"
]
}
}
}
},
"layout": [
{
"key": "devices",
"type": "tabarray",
"title": "{{ value.name || 'Device' }}",
"items": [
"devices[].name",
"devices[].host",
"devices[].xboxLiveId",
"devices[].displayType",
{
"key": "devices[].inputs",
"type": "section",
"title": "Inputs",
"expandable": true,
"expanded": false,
"items": [
"devices[].inputs.getFromDevice",
"devices[].inputs.filterGames",
"devices[].inputs.filterApps",
"devices[].inputs.filterSystemApps",
"devices[].inputs.filterDlc",
"devices[].inputs.displayOrder",
{
"key": "devices[].inputs.data",
"type": "tabarray",
"title": "{{ value.name || 'Input' }}",
"items": [
"devices[].inputs.data[].name",
"devices[].inputs.data[].titleId",
"devices[].inputs.data[].reference",
"devices[].inputs.data[].oneStoreProductId",
"devices[].inputs.data[].contentType"
]
}
]
},
{
"key": "devices[]",
"type": "section",
"title": "Buttons",
"expandable": true,
"expanded": false,
"items": [
{
"key": "devices[].buttons",
"type": "tabarray",
"title": "{{ value.name || 'Button' }}",
"items": [
"devices[].buttons[].displayType",
"devices[].buttons[].name",
"devices[].buttons[].mode",
"devices[].buttons[].mediaCommand",
"devices[].buttons[].gamePadCommand",
"devices[].buttons[].tvRemoteCommand",
"devices[].buttons[].consoleControlCommand",
"devices[].buttons[].gameAppControlCommand",
"devices[].buttons[].namePrefix"
]
}
]
},
{
"key": "devices[]",
"type": "section",
"title": "Advanced Settings",
"expandable": true,
"expanded": false,
"items": [
{
"key": "devices[]",
"type": "tabarray",
"title": "{{ value.title }}",
"items": [
{
"key": "devices[].volume",
"title": "Volume",
"items": [
"devices[].volume.displayType",
"devices[].volume.name",
"devices[].volume.namePrefix"
]
},
{
"key": "devices[]",
"title": "Sensors",
"items": [
{
"key": "devices[].sensors",
"type": "tabarray",
"title": "{{ value.name || 'Sensor' }}",
"items": [
"devices[].sensors[].displayType",
"devices[].sensors[].mode",
"devices[].sensors[].name",
"devices[].sensors[].namePrefix",
"devices[].sensors[].pulse",
"devices[].sensors[].reference",
"devices[].sensors[].level"
]
}
]
},
{
"key": "devices[]",
"title": "Device",
"items": [
"devices[].infoButtonCommand"
]
},
{
"key": "devices[].webApi",
"title": "Web Api",
"items": [
"devices[].webApi.enable",
{
"key": "devices[].webApi.token",
"type": "password"
},
{
"key": "devices[].webApi.clientId",
"type": "password"
},
{
"key": "devices[].webApi.clientSecret",
"type": "password"
}
]
},
{
"key": "devices[].log",
"title": "Log",
"items": [
"devices[].log.deviceInfo",
"devices[].log.success",
"devices[].log.info",
"devices[].log.warn",
"devices[].log.error",
"devices[].log.debug"
]
},
{
"key": "devices[]",
"title": "External Integrations",
"items": [
{
"key": "devices[]",
"type": "tabarray",
"title": "{{ value.title }}",
"items": [
{
"key": "devices[].restFul",
"title": "RESTFul",
"items": [
"devices[].restFul.enable",
"devices[].restFul.port"
]
},
{
"key": "devices[].mqtt",
"title": "MQTT",
"items": [
"devices[].mqtt.enable",
"devices[].mqtt.host",
"devices[].mqtt.port",
"devices[].mqtt.clientId",
"devices[].mqtt.prefix",
{
"key": "devices[].mqtt.auth",
"title": "Authorization",
"items": [
"devices[].mqtt.auth.enable",
"devices[].mqtt.auth.user",
{
"key": "devices[].mqtt.auth.passwd",
"type": "password"
}
]
}
]
}
]
}
]
}
]
}
]
}
]
}
]
}
================================================
FILE: homebridge-ui/public/index.html
================================================
<div class="container mt-3">
<div class="text-center">
<img src="homebridge-xbox-tv.png" alt="Image" height="120" />
</div>
<div id="authorizationManager" class="card card-body mt-2">
<form id="configForm">
<div class="text-center">
<label id="deviceName" class="fw-bold" style="font-size: 23px;">Xbox</label><br>
<label id="info" class="d-block" style="font-size: 17px;"></label>
<label id="info1" class="d-block" style="font-size: 15px;"></label>
</div>
<div class="mb-2">
<label for="deviceHost" class="form-label">Host</label>
<input id="deviceHost" type="text" class="form-control" required>
</div>
<div class="mb-2 position-relative">
<label for="deviceLiveId" class="form-label">Xbox Live ID</label>
<div class="input-group">
<input id="deviceLiveId" type="password" class="form-control" autocomplete="new-password" required>
<button type="button" id="toggleLiveId" class="btn btn-outline-secondary">
<i class="fas fa-eye"></i>
</button>
</div>
</div>
<div class="mb-2 position-relative">
<label for="deviceToken" class="form-label">Web API Token</label>
<div class="input-group">
<input id="deviceToken" type="password" class="form-control" autocomplete="new-password" required>
<button type="button" id="toggleToken" class="btn btn-outline-secondary">
<i class="fas fa-eye"></i>
</button>
</div>
</div>
<div class="form-check mb-2">
<input id="deviceWebApiControl" type="checkbox" class="form-check-input">
<label for="deviceWebApiControl" class="form-check-label">Web API Control</label>
</div>
<div class="text-center">
<button id="startAuthorizationButton" type="button" class="btn btn-primary">Start Authorization</button>
<button id="clearTokenButton" type="button" class="btn btn-secondary">Clear Web API Token</button>
<button id="configButton" type="button" class="btn btn-secondary"><i class="fas fa-gear"></i></button>
</div>
</form>
<div id="consoleButton" class="d-flex flex-wrap justify-content-center gap-1 mt-3"></div>
</div>
</div>
<script>
(async () => {
const pluginConfig = await homebridge.getPluginConfig();
if (!pluginConfig.length) {
pluginConfig.push({});
await homebridge.updatePluginConfig(pluginConfig);
homebridge.showSchemaForm();
return;
}
const devices = pluginConfig[0].devices || [];
const devicesCount = devices.length;
// Helper to get DOM elements
const $ = id => document.getElementById(id);
// Helper to set button classes
const setButtonClass = (activeIndex) => {
for (let j = 0; j < devicesCount; j++) {
$(`button${j}`).className = j === activeIndex ? "btn btn-primary" : "btn btn-secondary";
}
};
// Helper to update the device form
const updateDeviceForm = (device) => {
$('deviceName').innerHTML = device.name || '';
$('deviceHost').value = device.host || '';
$('deviceLiveId').value = device.xboxLiveId || '';
$('deviceToken').value = device.webApi.token || '';
$('deviceWebApiControl').checked = device.webApi.enable || false;
const tokenLength = device.webApi.token?.length || 0;
$('startAuthorizationButton').innerText = tokenLength <= 10 ? "Start Authorization" : "Check State";
$('deviceWebApiControl').disabled = tokenLength <= 10;
if (tokenLength <= 10) {
$('deviceWebApiControl').checked = false;
device.webApi.enable = false;
}
};
// Create buttons for each device
const container = document.getElementById("consoleButton");
container.style.display = 'flex';
container.style.flexWrap = 'wrap';
container.style.justifyContent = 'center';
container.style.gap = '0.25rem';
devices.forEach((device, i) => {
this.device = device;
const button = document.createElement("button");
button.type = "button";
button.id = `button${i}`;
button.className = "btn btn-primary";
button.style.textTransform = 'none';
button.innerText = device.name || `Device ${i + 1}`;
container.appendChild(button);
button.addEventListener("click", async () => {
setButtonClass(i);
updateDeviceForm(devices[i]);
this.device = device;
});
// Auto-select the first device
if (i === devicesCount - 1) {
$("button0").click();
}
});
// Show the authorization form
$("authorizationManager").style.display = "block";
// Config button toggle
let configButtonState = false;
$("configButton").addEventListener("click", () => {
configButtonState = !configButtonState;
homebridge[configButtonState ? 'showSchemaForm' : 'hideSchemaForm']();
configButton.className = configButtonState ? 'btn btn-primary' : 'btn btn-secondary';
});
// Token toggle
$("toggleLiveId").addEventListener("click", () => {
const liveIdInput = $("deviceLiveId");
const icon = document.querySelector('#toggleLiveId i');
if (liveIdInput.type === 'password') {
liveIdInput.type = 'text';
icon.classList.replace('fa-eye', 'fa-eye-slash');
} else {
liveIdInput.type = 'password';
icon.classList.replace('fa-eye-slash', 'fa-eye');
}
});
// Token toggle
$("toggleToken").addEventListener("click", () => {
const tokenInput = $("deviceToken");
const icon = document.querySelector('#toggleToken i');
if (tokenInput.type === 'password') {
tokenInput.type = 'text';
icon.classList.replace('fa-eye', 'fa-eye-slash');
} else {
tokenInput.type = 'password';
icon.classList.replace('fa-eye-slash', 'fa-eye');
}
});
// Update config on form input
$("configForm").addEventListener("input", async () => {
const currentDevice = this.device;
currentDevice.host = $('deviceHost').value;
currentDevice.xboxLiveId = $('deviceLiveId').value;
currentDevice.webApi.token = $('deviceToken').value;
currentDevice.webApi.enable = $('deviceWebApiControl').checked;
const tokenLength = currentDevice.webApi.token?.length || 0;
$('startAuthorizationButton').innerText = tokenLength <= 10 ? "Start Authorization" : "Check State";
if (tokenLength <= 10) {
$('startAuthorizationButton').removeAttribute('disabled');
}
await homebridge.updatePluginConfig(pluginConfig);
await homebridge.savePluginConfig(pluginConfig);
});
function updateInfo(id, text, color) {
const el = document.getElementById(id);
if (el) {
el.innerText = text;
el.style.color = color;
}
}
// Clear token button logic
$("clearTokenButton").addEventListener("click", async () => {
updateInfo('info', 'Start clearing token...', 'yellow');
homebridge.showSpinner();
try {
const host = this.device.host;
await homebridge.request('/clearToken', { host });
Object.assign(this.device, {
webApi: {
token: '',
enable: false
}
});
updateDeviceForm(this.device);
updateInfo('info', "Web API token cleared, now you can start a new authorization process.", "green");
$("startAuthorizationButton").removeAttribute("disabled");
await homebridge.updatePluginConfig(pluginConfig);
await homebridge.savePluginConfig(pluginConfig);
} catch (error) {
updateInfo('info', "Clear Web API token error.", "yellow");
updateInfo('info1', `Error: ${error}`, "red");
} finally {
homebridge.hideSpinner();
}
});
// Start authorization logic
$("startAuthorizationButton").addEventListener("click", async () => {
$("startAuthorizationButton").setAttribute("disabled", "true"); // blokada wielokliku
updateInfo('info', "Starting authorization...", "yellow");
homebridge.showSpinner();
try {
const { host, webApi } = this.device;
const { token, clientId, clientSecret } = webApi;
const response = await homebridge.request('/startAuthorization', {
host, token, clientId, clientSecret
});
const { info, status } = response;
switch (status) {
case 0: // Authorized
updateInfo('info', info, "green");
$("startAuthorizationButton").innerText = "Check State";
$("deviceWebApiControl").disabled = false;
break;
case 1: // Needs user interaction
$("startAuthorizationButton").innerText = "Activate Console";
$("deviceWebApiControl").checked = false;
$("deviceWebApiControl").disabled = true;
webApi.enable = false;
let timeLeft = 10;
const timerId = setInterval(() => {
if (timeLeft <= 0) {
open(info);
clearInterval(timerId);
updateInfo('info', "Now paste the *code* into *Web API Token* and press *Activate Console*.", "yellow");
} else {
updateInfo('info', `After ${timeLeft} sec, sign in to Xbox Live and authorize. Then copy the code after ?code= and return here.`, "yellow");
timeLeft--;
}
}, 1000);
break;
case 2: // Successfully authorized
updateInfo('info', info, "green");
$("startAuthorizationButton").innerText = "Check State";
$("deviceWebApiControl").disabled = false;
$("deviceWebApiControl").checked = true;
webApi.enable = true;
await homebridge.updatePluginConfig(pluginConfig);
await homebridge.savePluginConfig(pluginConfig);
break;
}
} catch (error) {
updateInfo('info', "Authorization error.", "yellow");
updateInfo('info1', `Error: ${JSON.stringify(error)}`, "red");
} finally {
$("startAuthorizationButton").removeAttribute("disabled");
homebridge.hideSpinner();
}
});
})();
</script>
================================================
FILE: homebridge-ui/server.js
================================================
import { HomebridgePluginUiServer } from '@homebridge/plugin-ui-utils';
import Authentication from '../src/webApi/authentication.js';
import Functions from '../src/functions.js';
class PluginUiServer extends HomebridgePluginUiServer {
constructor() {
super();
this.functions = new Functions();
//clear web api token
this.onRequest('/clearToken', this.clearToken.bind(this));
//start console authorization
this.onRequest('/startAuthorization', this.startAuthorization.bind(this));
//this MUST be called when you are ready to accept requests
this.ready();
};
async clearToken(payload) {
const hostKey = payload.host.replace(/\./g, '');
const tokensFile = `${this.homebridgeStoragePath}/xboxTv/authToken_${hostKey}`;
try {
const emptyTokens = { oauth: {}, user: {}, xsts: {} };
await this.functions.saveData(tokensFile, emptyTokens);
return true;
} catch (error) {
throw new Error(`Clear token error: ${error?.message ?? error}`);
}
}
async startAuthorization(payload) {
const hostKey = payload.host.replace(/\./g, '');
const tokensFile = `${this.homebridgeStoragePath}/xboxTv/authToken_${hostKey}`;
const authConfig = {
clientId: payload.clientId,
clientSecret: payload.clientSecret,
tokensFile
};
const authentication = new Authentication(authConfig);
const webApiToken = payload.token;
try {
// Case: Console already authorized
await authentication.checkAuthorization();
return {
info: 'Console authorized and activated. To start a new process, please clear the Web API Token first.',
status: 0 // Authorized
};
} catch {
if (webApiToken) {
try {
await authentication.accessToken(webApiToken);
return {
info: 'Activation successful! Now restart the plugin and have fun!',
status: 2 // Token activated
};
} catch (error) {
throw new Error(`Activation console error: ${error?.message ?? error}`);
}
}
// No token, generate auth URL
try {
const oauth2URI = await authentication.generateAuthorizationUrl();
return {
info: oauth2URI,
status: 1 // Needs user authorization
};
} catch (error) {
throw new Error(error);
}
}
}
}
(() => {
return new PluginUiServer();
})();
================================================
FILE: index.js
================================================
import { join } from 'path';
import { mkdirSync, existsSync, writeFileSync } from 'fs';
import XboxDevice from './src/xboxdevice.js';
import ImpulseGenerator from './src/impulsegenerator.js';
import { PluginName, PlatformName } from './src/constants.js';
class XboxPlatform {
constructor(log, config, api) {
// only load if configured
if (!config || !Array.isArray(config.devices)) {
log.warn(`No configuration found for ${PluginName}`);
return;
}
this.accessories = [];
const prefDir = join(api.user.storagePath(), 'xboxTv');
try {
mkdirSync(prefDir, { recursive: true });
} catch (error) {
log.error(`Prepare directory error: ${error.message ?? error}`);
return;
}
api.on('didFinishLaunching', () => {
// Each device is set up independently — a failure in one does not
// block the others. Promise.allSettled runs all in parallel.
Promise.allSettled(
config.devices.map(device =>
this.setupDevice(device, prefDir, log, api)
)
).then(results => {
results.forEach((result, i) => {
if (result.status === 'rejected') {
log.error(`Device[${i}] setup error: ${result.reason?.message ?? result.reason}`);
}
});
});
});
}
// ── Per-device setup ──────────────────────────────────────────────────────
async setupDevice(device, prefDir, log, api) {
const { name, host, xboxLiveId, displayType } = device;
if (!name || !host || !xboxLiveId || !displayType) {
log.warn(`Device: ${host || 'host missing'}, ${name || 'name missing'}, ${xboxLiveId || 'xbox live id missing'}${!displayType ? ', display type disabled' : ''} in config, will not be published in the Home app`);
return;
}
const logLevel = {
devInfo: device.log?.deviceInfo,
success: device.log?.success,
info: device.log?.info,
warn: device.log?.warn,
error: device.log?.error,
debug: device.log?.debug,
};
if (logLevel.debug) {
log.info(`Device: ${host} ${name}, did finish launching.`);
const safeConfig = {
...device,
xboxLiveId: 'removed',
webApi: {
token: 'removed',
clientSecret: 'removed',
clientId: 'removed',
},
mqtt: {
auth: {
...device.mqtt?.auth,
passwd: 'removed',
},
},
};
log.info(`Device: ${host} ${name}, Config: ${JSON.stringify(safeConfig, null, 2)}.`);
}
// Resolve all file paths up front — before the impulse generator starts,
// so a file-creation failure aborts early rather than inside the retry loop.
const postFix = host.split('.').join('');
const authTokenFile = `${prefDir}/authToken_${postFix}`;
const devInfoFile = `${prefDir}/devInfo_${postFix}`;
const inputsFile = `${prefDir}/inputs_${postFix}`;
const inputsNamesFile = `${prefDir}/inputsNames_${postFix}`;
const inputsTargetVisibilityFile = `${prefDir}/inputsTargetVisibility_${postFix}`;
try {
const files = [
authTokenFile,
devInfoFile,
inputsFile,
inputsNamesFile,
inputsTargetVisibilityFile,
];
files.forEach(file => {
if (!existsSync(file)) {
writeFileSync(file, '');
}
});
} catch (error) {
if (logLevel.error) log.error(`Device: ${host} ${name}, Prepare files error: ${error.message ?? error}`);
return;
}
// The startup impulse generator retries the full connect cycle
// every 120 s until it succeeds, then hands off to the xboxDevice
// impulse generator and stops itself.
const impulseGenerator = new ImpulseGenerator()
.on('start', async () => {
try {
await this.startDevice({
device, name, host,
authTokenFile, devInfoFile, inputsFile, inputsNamesFile, inputsTargetVisibilityFile,
logLevel, log, api, impulseGenerator,
});
} catch (error) {
if (logLevel.error) log.error(`Device: ${host} ${name}, Start impulse generator error: ${error.message ?? error}, trying again.`);
}
})
.on('state', (state) => {
if (logLevel.debug) log.info(`Device: ${host} ${name}, Start impulse generator ${state ? 'started' : 'stopped'}.`);
});
await impulseGenerator.state(true, [{ name: 'start', sampling: 120_000 }]);
}
// ── Connect and register a single Xbox device as a Homebridge accessory ───
async startDevice({ device, name, host, authTokenFile, devInfoFile, inputsFile, inputsNamesFile, inputsTargetVisibilityFile, logLevel, log, api, impulseGenerator }) {
const xboxDevice = new XboxDevice(api, device, authTokenFile, devInfoFile, inputsFile, inputsNamesFile, inputsTargetVisibilityFile)
.on('devInfo', (info) => logLevel.devInfo && log.info(info))
.on('success', (msg) => logLevel.success && log.success(`Device: ${host} ${name}, ${msg}`))
.on('info', (msg) => logLevel.info && log.info(`Device: ${host} ${name}, ${msg}`))
.on('debug', (msg) => logLevel.debug && log.info(`Device: ${host} ${name}, debug: ${msg}`))
.on('warn', (msg) => logLevel.warn && log.warn(`Device: ${host} ${name}, ${msg}`))
.on('error', (msg) => logLevel.error && log.error(`Device: ${host} ${name}, ${msg}`));
const accessory = await xboxDevice.start();
if (!accessory) return;
api.publishExternalAccessories(PluginName, [accessory]);
if (logLevel.success) log.success(`Device: ${host} ${name}, Published as external accessory.`);
// Hand off to the xboxDevice impulse generator and stop the startup one.
await xboxDevice.startStopImpulseGenerator(true, [{ name: 'connect', sampling: 6000 }]);
await impulseGenerator.state(false);
}
// ── Homebridge accessory cache ────────────────────────────────────────────
configureAccessory(accessory) {
this.accessories.push(accessory);
}
}
export default (api) => {
api.registerPlatform(PluginName, PlatformName, XboxPlatform);
};
================================================
FILE: package.json
================================================
{
"displayName": "Xbox TV",
"name": "homebridge-xbox-tv",
"version": "4.1.17",
"description": "Homebridge plugin to control Xbox game consoles.",
"license": "MIT",
"author": "grzegorz914",
"maintainers": [
"grzegorz914"
],
"homepage": "https://github.com/grzegorz914/homebridge-xbox-tv#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/grzegorz914/homebridge-xbox-tv.git"
},
"bugs": {
"url": "https://github.com/grzegorz914/homebridge-xbox-tv/issues"
},
"type": "module",
"exports": {
".": "./index.js"
},
"files": [
"src",
"homebridge-ui",
"index.js",
"config.schema.json",
"package.json",
"CHANGELOG.md",
"README.md",
"LICENSE"
],
"engines": {
"homebridge": "^1.8.0 || ^2.0.0 || ^2.0.0-beta.79 || ^2.0.0-alpha.81",
"node": "^20 || ^22 || ^24 || ^25"
},
"dependencies": {
"@homebridge/plugin-ui-utils": "^2.2.3",
"mqtt": "^5.15.1",
"elliptic": "^6.6.1",
"hex-to-binary": "^1.0.1",
"jsrsasign": "^11.1.3",
"uuid": "^14.0.0",
"express": "^5.2.1",
"axios": "^1.15.2"
},
"keywords": [
"homebridge",
"homebridge-plugin",
"homekit",
"xbox",
"microsoft",
"smartglass",
"mqtt",
"restful"
],
"funding": {
"type": "Buy Coffee To",
"url": "https://buycoffee.to/grzegorz914"
},
"contributors": [],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
}
}
================================================
FILE: sample-config.json
================================================
{
"bridge": {
"name": "Homebridge",
"username": "AA:BB:CC:DD:EE:FF",
"manufacturer": "homebridge.io",
"model": "homebridge",
"port": 9100,
"pin": "123-45-678"
},
"description": "HomeKit Bridge",
"ports": {
"start": 9101,
"end": 9150,
"comment": "In this section set the port for Homebridge accessories."
},
"accessories": [],
"platforms": [
{
"platform": "XboxTv",
"devices": [
{
"name": "Xbox One",
"host": "192.168.1.6",
"xboxLiveId": "FD0000000000",
"displayType": 1,
"webApi": {
"enable": false,
"token": "",
"clientId": "",
"clientSecret": ""
},
"inputs": {
"getFromDevice": false,
"filterGames": false,
"filterApps": false,
"filterSystemApps": false,
"filterDlc": false,
"displayOrder": 0,
"data": [
{
"name": "A Way Out",
"reference": "AWayOut_zwks512sysnyr!AppAWayOut",
"oneStoreProductId": "",
"contentType": "Game"
}
]
},
"buttons": [
{
"displayType": 0,
"name": "Media Play",
"mode": 0,
"mediaCommand": "play",
"namePrefix": false
}
],
"sensors": [
{
"displayType": 0,
"mode": 0,
"name": "",
"reference": "",
"pulse": false,
"namePrefix": false,
"level": 0
}
],
"volume": {
"displayType": 0,
"name": "Volume",
"namePrefix": false
},
"log": {
"deviceInfo": true,
"success": true,
"info": false,
"warn": true,
"error": true,
"debug": false
},
"infoButtonCommand": "nexus",
"restFul": {
"enable": false,
"port": 3000
},
"mqtt": {
"enable": false,
"host": "",
"port": 1883,
"clientId": "",
"prefix": "",
"auth": {
"enable": false,
"user": "",
"passwd": ""
}
}
}
]
}
]
}
================================================
FILE: src/constants.js
================================================
export const PlatformName = "XboxTv";
export const PluginName = "homebridge-xbox-tv";
export const DefaultInputs = [
{
"oneStoreProductId": "Screensaver",
"titleId": "851275400",
"reference": "Xbox.IdleScreen_8wekyb3d8bbwe!Xbox.IdleScreen.Application",
"isGame": false,
"name": "Screensaver",
"contentType": "Dashboard"
},
{
"oneStoreProductId": "Dashboard",
"titleId": "750323071",
"reference": "Xbox.Dashboard_8wekyb3d8bbwe!Xbox.Dashboard.Application",
"isGame": false,
"name": "Dashboard",
"contentType": "Dashboard"
},
{
"oneStoreProductId": "Settings",
"titleId": "1837352387",
"reference": "Microsoft.Xbox.Settings_8wekyb3d8bbwe!Xbox.Settings.Application",
"isGame": false,
"name": "Settings",
"contentType": "Dashboard"
},
{
"oneStoreProductId": "Television",
"titleId": "371594669",
"reference": "Microsoft.Xbox.LiveTV_8wekyb3d8bbwe!Microsoft.Xbox.LiveTV.Application",
"isGame": false,
"name": "Television",
"contentType": "systemApp"
},
{
"oneStoreProductId": "SettingsTv",
"titleId": "2019308066",
"reference": "Microsoft.Xbox.TvSettings_8wekyb3d8bbwe!Microsoft.Xbox.TvSettings.Application",
"isGame": false,
"name": "Settings TV",
"contentType": "Dashboard"
},
{
"oneStoreProductId": "Accessory",
"titleId": "758407307",
"reference": "Microsoft.XboxDevices_8wekyb3d8bbwe!App",
"isGame": false,
"name": "Accessory",
"contentType": "systemApp"
},
{
"oneStoreProductId": "NetworkTroubleshooter",
"titleId": "1614319806",
"reference": "Xbox.NetworkTroubleshooter_8wekyb3d8bbwe!Xbox.NetworkTroubleshooter.Application",
"isGame": false,
"name": "Network Troubleshooter",
"contentType": "systemApp"
},
{
"oneStoreProductId": "MicrosoftStore",
"titleId": "1864271209",
"reference": "Microsoft.storify_8wekyb3d8bbwe!App",
"isGame": false,
"name": "Microsoft Store",
"contentType": "Dashboard"
},
{
"oneStoreProductId": "XboxGuide",
"titleId": "1052052983",
"reference": "Xbox.Guide_8wekyb3d8bbwe!Xbox.Guide.Application",
"isGame": false,
"name": "Xbox Guide",
"contentType": "systemApp"
}
];
export const InputSourceTypes = [
"OTHER",
"HOME_SCREEN",
"TUNER",
"HDMI",
"COMPOSITE_VIDEO",
"S_VIDEO",
"COMPONENT_VIDEO",
"DVI",
"AIRPLAY",
"USB",
"APPLICATION"
];
export const WebApi = {
"Url": {
"oauth2": "https://login.live.com/oauth20_authorize.srf",
"AccessToken": "https://login.live.com/oauth20_token.srf",
"RefreshToken": "https://login.live.com/oauth20_token.srf",
"UserToken": "https://user.auth.xboxlive.com/user/authenticate",
"XstsToken": "https://xsts.auth.xboxlive.com/xsts/authorize",
"Redirect": "http://localhost:8888/auth/callback",
"Xccs": "https://xccs.xboxlive.com"
},
"Scopes": "XboxLive.signin XboxLive.offline_access",
"ClientId": "a34ac209-edab-4b08-91e7-a4558d8da1bd",
"Console": {
"Name": {
"XboxSeriesX": "Xbox Series X",
"XboxSeriesS": "Xbox Series S",
"XboxOne": "Xbox One",
"XboxOneS": "Xbox One S",
"XboxOneX": "Xbox One X"
},
"PowerState": {
"Off": 0,
"On": 1,
"ConnectedStandby": 2,
"SystemUpdate": 3,
"Unknown": 4
},
"PlaybackState": {//0 - STOP, 1 - PLAY, 2 - PAUSE
"Stopped": 0,
"Playing": 1,
"Paused": 2,
"Unknown": 3
},
"PlaybackStateHomeKit": { //0 - PLAY, 1 - PAUSE, 2 - STOP, 3 - LOADING, 4 - INTERRUPTED
"Stopped": 2,
"Playing": 0,
"Paused": 1,
"Unknown": 4
}
}
};
export const LocalApi = {
"ParticipantId": {
"Target": 0
},
"ClientId": "e8ff5828-5cce-4f90-89a4-117d127e3838",
"Console": {
"Name": {
"1": "Xbox One",
"2": "Xbox 360",
"3": "Windows Desktop",
"4": "Windows Store",
"5": "Windows Phone",
"6": "iPhone",
"7": "iPad",
"8": "Android"
},
"PairingState": {
"0": "Not Paired",
"1": "Paired"
}
},
"Channels": {
"System": {
"Input": {
"Id": 0,
"Uuid": "fa20b8ca66fb46e0adb60b978a59d35f",
"Commands": {
"unpress": 0,
"enroll": 1,
"nexus": 2,
"view": 4,
"menu": 8,
"a": 16,
"b": 32,
"x": 64,
"y": 128,
"up": 256,
"down": 512,
"left": 1024,
"right": 2048,
"leftShoulder": 4096,
"rightShoulder": 8192,
"leftThumbstick": 16384,
"rightThumbstick": 32768
}
},
"TvRemote": {
"Id": 1,
"Uuid": "d451e3b360bb4c71b3dbf994b1aca3a7",
"Commands": {
"volUp": "btn.vol_up",
"volDown": "btn.vol_down",
"volMute": "btn.vol_mute"
},
"MessageType": {
"Error": "Error",
"GetConfiguration": "GetConfiguration",
"GetHeadendInfo": "GetHeadendInfo",
"GetLiveTVInfo": "GetLiveTVInfo",
"GetProgramInfo": "GetProgramInfo",
"GetRecentChannels": "GetRecentChannels",
"GetTunerLineups": "GetTunerLineups",
"GetAppChannelData": "GetAppChannelData",
"GetAppChannelLineups": "GetAppChannelLineups",
"GetAppChannelProgramData": "GetAppChannelProgramData",
"SendKey": "SendKey",
"SetChannel": "SetChannel"
}
},
"Media": {
"Id": 2,
"Uuid": "48a9ca24eb6d4e128c43d57469edd3cd",
"Commands": {
"play": 2,
"pause": 4,
"playPause": 8,
"stop": 16,
"record": 32,
"nextTrack": 64,
"previousTrack": 128,
"fastForward": 256,
"rewind": 512,
"channelUp": 1024,
"channelDown": 2048,
"back": 4096,
"view": 8192,
"menu": 16384,
"seek": 32786
}
},
"Text": {
"Id": 3,
"Uuid": "7af3e6a2488b40cba93179c04b7da3a0"
},
"Broadcast": {
"Id": 4,
"Uuid": "b6a117d8f5e245d7862e8fd8e3156476"
},
"Title": {
"Id": 5,
"Uuid": "00000000000000000000000000000000"
}
}
},
"Media": {
"Types": {
"0": "No Media",
"1": "Music",
"2": "Video",
"3": "Image",
"4": "Conversation",
"5": "Game"
},
"PlaybackState": {
"0": "Closed",
"1": "Changing",
"2": "Stopped",
"3": "Playing",
"4": "Paused"
},
"SoundLevel": {
"0": "Muted",
"1": "Low",
"2": "Full"
}
},
"Messages": {
"Category": {
"d00d": "message",
"dd00": "simple",
"dd01": "simple",
"dd02": "simple",
"cc00": "simple",
"cc01": "simple"
},
"CategoryTypes": {
"d00d": "message",
"dd00": "discoveryRequest",
"dd01": "discoveryResponse",
"dd02": "powerOn",
"cc00": "connectRequest",
"cc01": "connectResponse",
},
"Types": {
0x1: "acknowledge",
0x2: "group",
0x3: "localJoin",
0x5: "stopActivity",
0x19: "auxilaryStream",
0x1A: "activeSurfaceChange",
0x1B: "navigate",
0x1C: "json",
0x1D: "tunnel",
0x1E: "consoleStatus",
0x1F: "titleTextConfiguration",
0x20: "titleTextInput",
0x21: "titleTextSelection",
0x22: "mirroringRequest",
0x23: "titleLaunch",
0x26: "channelStartRequest",
0x27: "channelStartResponse",
0x28: "channelStop",
0x29: "system",
0x2A: "disconnect",
0x2E: "titleTouch",
0x2F: "accelerometer",
0x30: "gyrometer",
0x31: "inclinometer",
0x32: "compass",
0x33: "orientation",
0x36: "pairedIdentityStateChanged",
0x37: "unsnap",
0x38: "recordGameDvr",
0x39: "powerOff",
0xF00: "mediaControllerRemoved",
0xF01: "mediaCommand",
0xF02: "mediaCommandResult",
0xF03: "mediaState",
0xF0A: "gamepad",
0xF2B: "systemTextConfiguration",
0xF2C: "systemTextInput",
0xF2E: "systemTouch",
0xF34: "systemTextAck",
0xF35: "systemTextDone"
},
"Flags": {
acknowledge: Buffer.from('8001', 'hex'),
0x2: "group",
localJoin: Buffer.from('2003', 'hex'),
0x5: "stopActivity",
0x19: "auxilaryStream",
0x1A: "activeSurfaceChange",
0x1B: "navigate",
json: Buffer.from('a01c', 'hex'),
0x1D: "tunnel",
consoleStatus: Buffer.from('a01e', 'hex'),
0x1F: "titleTextConfiguration",
0x20: "titleTextInput",
0x21: "titleTextSelection",
0x22: "mirroringRequest",
0x23: "titleLaunch",
channelStartRequest: Buffer.from('a026', 'hex'),
channelStartResponse: Buffer.from('a027', 'hex'),
0x28: "channelStop",
0x29: "system",
disconnect: Buffer.from('802a', 'hex'),
0x2E: "titleTouch",
0x2F: "accelerometer",
0x30: "gyrometer",
0x31: "inclinometer",
0x32: "compass",
0x33: "orientation",
0x36: "pairedIdentityStateChanged",
0x37: "unsnap",
recordGameDvr: Buffer.from('a038', 'hex'),
powerOff: Buffer.from('a039', 'hex'),
0xF00: "mediaControllerRemoved",
mediaCommand: Buffer.from('af01', 'hex'),
mediaCommandResult: Buffer.from('af02', 'hex'),
mediaState: Buffer.from('af03', 'hex'),
gamepad: Buffer.from('8f0a', 'hex'),
0xF2B: "systemTextConfiguration",
0xF2C: "systemTextInput",
0xF2E: "systemTouch",
0xF34: "systemTextAck",
0xF35: "systemTextDone",
powerOn: Buffer.from('dd02', 'hex'),
discoveryRequest: Buffer.from('dd00', 'hex'),
discoveryResponse: Buffer.from('dd01', 'hex'),
connectRequest: Buffer.from('cc00', 'hex'),
connectResponse: Buffer.from('cc01', 'hex'),
}
}
};
export const DiacriticsMap = {
// Polish
'ą': 'a', 'ć': 'c', 'ę': 'e', 'ł': 'l', 'ń': 'n',
'ó': 'o', 'ś': 's', 'ź': 'z', 'ż': 'z',
'Ą': 'A', 'Ć': 'C', 'Ę': 'E', 'Ł': 'L', 'Ń': 'N',
'Ó': 'O', 'Ś': 'S', 'Ź': 'Z', 'Ż': 'Z',
// German
'ä': 'a', 'ö': 'o', 'ü': 'u', 'ß': 'ss',
'Ä': 'A', 'Ö': 'O', 'Ü': 'U',
// French
'à': 'a', 'â': 'a', 'ç': 'c', 'é': 'e', 'è': 'e',
'ê': 'e', 'ë': 'e', 'î': 'i', 'ï': 'i', 'ô': 'o',
'û': 'u', 'ù': 'u', 'ü': 'u', 'ÿ': 'y',
'À': 'A', 'Â': 'A', 'Ç': 'C', 'É': 'E', 'È': 'E',
'Ê': 'E', 'Ë': 'E', 'Î': 'I', 'Ï': 'I', 'Ô': 'O',
'Û': 'U', 'Ù': 'U', 'Ü': 'U', 'Ÿ': 'Y',
// Spanish & Portuguese
'á': 'a', 'í': 'i', 'ó': 'o', 'ú': 'u', 'ñ': 'n',
'Á': 'A', 'Í': 'I', 'Ó': 'O', 'Ú': 'U', 'Ñ': 'N',
// Scandinavian
'å': 'a', 'Å': 'A', 'ø': 'o', 'Ø': 'O', 'æ': 'ae', 'Æ': 'AE',
// Other common
'Š': 'S', 'š': 's', 'Ž': 'Z', 'ž': 'z'
};
================================================
FILE: src/functions.js
================================================
import { promises as fsPromises } from 'fs';
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
import { DiacriticsMap } from './constants.js';
const execAsync = promisify(exec);
class Functions {
constructor() {
}
async saveData(path, data, stringify = true) {
try {
data = stringify ? JSON.stringify(data, null, 2) : data;
await fsPromises.writeFile(path, data);
return true;
} catch (error) {
throw new Error(`Save data error: ${error}`);
}
}
async readData(path, parseJson = false) {
try {
const data = await fsPromises.readFile(path, 'utf8');
if (parseJson) {
if (!data.trim()) {
// Empty file when expecting JSON
return null;
}
try {
return JSON.parse(data);
} catch (jsonError) {
throw new Error(`JSON parse error in file "${path}": ${jsonError.message}`);
}
}
// For non-JSON, just return file content (can be empty string)
return data;
} catch (error) {
if (error.code === 'ENOENT') {
// File does not exist
return null;
}
// Preserve original error details
const wrappedError = new Error(`Read data error for "${path}": ${error.message}`);
wrappedError.original = error;
throw wrappedError;
}
}
async sanitizeString(str) {
if (!str) return '';
// Replace diacritics using map
str = str.replace(/[^\u0000-\u007E]/g, ch => DiacriticsMap[ch] || ch);
// Replace separators between words with space
str = str.replace(/(\w)[.:;+\-\/]+(\w)/g, '$1 $2');
// Replace remaining standalone separators with space
str = str.replace(/[.:;+\-\/]/g, ' ');
// Remove remaining invalid characters (keep letters, digits, space, apostrophe)
str = str.replace(/[^A-Za-z0-9 ']/g, ' ');
// Collapse multiple spaces
str = str.replace(/\s+/g, ' ');
// Trim
return str.trim();
}
async scaleValue(value, inMin, inMax, outMin, outMax) {
const scaledValue = parseFloat((((Math.max(inMin, Math.min(inMax, value)) - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin).toFixed(0));
return scaledValue;
}
async ping(ipOrHost) {
const isWindows = process.platform === 'win32';
const cmd = isWindows ? `ping -n 1 ${ipOrHost}` : `ping -c 1 ${ipOrHost}`;
try {
const { stdout } = await execAsync(cmd);
const alive = /ttl=/i.test(stdout);
const timeMatch = stdout.match(/time[=<]([\d.]+)\s*ms/i);
const time = timeMatch ? parseFloat(timeMatch[1]) : null;
return { online: alive, time };
} catch {
return { online: false };
}
}
}
export default Functions
================================================
FILE: src/impulsegenerator.js
================================================
import EventEmitter from 'events';
class ImpulseGenerator extends EventEmitter {
constructor() {
super();
this.timersState = false;
this.timers = [];
}
async state(state, timers = [], runOnStart = true) {
// Stop current timers before new start
if (this.timersState && state) {
await this.state(false);
}
if (state) {
if (!Array.isArray(timers)) throw new Error('Timers must be an array');
for (const { name, sampling } of timers) {
if (!name || !sampling) continue;
if (runOnStart) this.emit(name);
const interval = setInterval(() => {
this.emit(name);
}, sampling);
this.timers.push(interval);
}
} else {
this.timers.forEach(clearInterval);
this.timers = [];
}
this.timersState = state;
this.emit('state', state);
return true;
}
}
export default ImpulseGenerator;
================================================
FILE: src/localApi/message.js
================================================
import HexToBin from 'hex-to-binary';
import Packets from './packets.js';
import Structure from './structure.js';
import { LocalApi } from '../constants.js';
class Message {
constructor(type) {
this.type = type;
this.packets = new Packets();
this.packet = this.packets[type];
this.channelId = '\x00\x00\x00\x00\x00\x00\x00\x00';
}
readFlags(flags) {
const binaryFlag = HexToBin(flags.toString('hex'));
const version = parseInt(binaryFlag.slice(0, 2), 2);
const needAcknowlegde = binaryFlag.slice(2, 3) === '1';
const isFragment = binaryFlag.slice(3, 4) === '1';
const type = LocalApi.Messages.Types[parseInt(binaryFlag.slice(4, 16), 2)];
return { version, needAcknowlegde, isFragment, type };
}
set(key, value, subkey = false) {
if (subkey === false) {
this.packet[key].value = value;
} else {
this.packet[subkey][key].value = value;
}
}
pack(crypto, sequenceNumber, targetParticipantId, sourceParticipantId, channelId = false) {
const structure = new Structure();
for (const name in this.packet) {
this.packet[name].pack(structure);
}
// Padding PKCS7
if (structure.toBuffer().length % 16 > 0) {
const padStart = structure.toBuffer().length % 16;
const padTotal = 16 - padStart;
for (let i = padStart + 1; i <= 16; i++) {
structure.writeUInt8(padTotal);
}
}
const header = new Structure();
header.writeBytes(Buffer.from('d00d', 'hex'));
header.writeUInt16(structure.toBuffer().length);
header.writeUInt32(sequenceNumber);
header.writeUInt32(targetParticipantId);
header.writeUInt32(sourceParticipantId);
header.writeBytes(LocalApi.Messages.Flags[this.type]);
header.writeBytes(Buffer.from(channelId || this.channelId));
// Original sgcrypto.encrypt(data, key, iv) and decrypt(data, iv, key) — kept as-is
const payloadEncrypted = crypto.encrypt(structure.toBuffer(), crypto.getKey(), crypto.encrypt(header.toBuffer().subarray(0, 16), crypto.getIv()));
let packet = Buffer.concat([header.toBuffer(), payloadEncrypted]);
const payloadProtected = crypto.sign(packet);
return Buffer.concat([packet, Buffer.from(payloadProtected)]);
}
unpack(crypto = undefined, data = false) {
const structure = new Structure(data);
const typeHex = structure.readBytes(2).toString('hex');
let packet = {
typeHex,
payloadLength: structure.readUInt16(),
sequenceNumber: structure.readUInt32(),
targetParticipantId: structure.readUInt32(),
sourceParticipantId: structure.readUInt32(),
flags: this.readFlags(structure.readBytes(2)),
channelId: structure.readBytes(8),
payloadProtected: structure.readBytes()
};
packet.type = packet.flags.type;
// FIX: extract signature BEFORE truncating payloadProtected.
// Original order was reversed: truncate first, then subarray(-32) of truncated buffer
// → read bytes [-64..-32] instead of the actual last 32 bytes [-32..].
packet.signature = packet.payloadProtected.subarray(-32);
packet.payloadProtected = Buffer.from(packet.payloadProtected.subarray(0, -32));
this.type = packet.type;
this.channelId = packet.channelId;
if (packet.payloadProtected.length > 0 && crypto) {
// Original: decrypt(data, iv) — matches sgcrypto.decrypt(data, iv, key)
const payloadDecrypted = crypto.decrypt(packet.payloadProtected, crypto.encrypt(data.subarray(0, 16), crypto.getIv()));
const structurePayloadProtected = new Structure(payloadDecrypted);
packet.payloadDecrypted = structurePayloadProtected.toBuffer();
packet.payloadProtected = {};
const packetDef = this.packets[packet.type];
if (packetDef) {
for (const name in packetDef) {
packet.payloadProtected[name] = packetDef[name].unpack(structurePayloadProtected);
}
}
}
return packet;
}
}
export default Message;
================================================
FILE: src/localApi/packets.js
================================================
import { LocalApi } from '../constants.js';
class Packets {
constructor(type) {
this.type = type;
const types = {
flags(length, value) {
const packet = {
value,
length,
pack(packetStructure) {
return packetStructure.writeBytes(setFlags(this.value));
},
unpack(packetStructure) {
return readFlags(packetStructure.readBytes(this.length));
}
};
return packet;
},
bytes(length = 0, value = Buffer.alloc(length)) {
const packet = {
value,
length,
pack(packetStructure) {
return packetStructure.writeBytes(this.value);
},
unpack(packetStructure) {
const readLength = this.length > 0 ? this.length : packetStructure.packet.length - packetStructure.offset;
return packetStructure.readBytes(readLength);
}
};
return packet;
},
uInt16(value) {
const packet = {
value,
pack(packetStructure) {
return packetStructure.writeUInt16(this.value);
},
unpack(packetStructure) {
return packetStructure.readUInt16();
}
};
return packet;
},
uInt32(value) {
const packet = {
value,
pack(packetStructure) {
return packetStructure.writeUInt32(this.value);
},
unpack(packetStructure) {
return packetStructure.readUInt32();
}
};
return packet;
},
sInt32(value) {
const packet = {
value,
pack(packetStructure) {
return packetStructure.writeInt32(this.value);
},
unpack(packetStructure) {
return packetStructure.readInt32();
}
};
return packet;
},
// FIX: uInt64 value must be a Buffer — '' produces a 0-byte buffer,
// corrupting the 8-byte field on the wire. Default is Buffer.alloc(length).
uInt64(length, value = Buffer.alloc(length)) {
const packet = {
value: Buffer.isBuffer(value) ? value : Buffer.alloc(length),
length,
pack(packetStructure) {
return packetStructure.writeBytes(this.value);
},
unpack(packetStructure) {
return packetStructure.readBytes(length);
}
};
return packet;
},
sgString(value) {
const packet = {
value,
pack(packetStructure) {
return packetStructure.writeSGString(this.value);
},
unpack(packetStructure) {
return packetStructure.readSGString().toString();
}
};
return packet;
},
sgArray(type, value = []) {
const packet = {
value,
type,
pack(packetStructure) {
packetStructure.writeUInt16(this.value.length);
const arrayStructure = packets[this.type];
for (const item of this.value) {
Object.keys(arrayStructure).forEach(name => {
arrayStructure[name].value = item[name];
packetStructure = arrayStructure[name].pack(packetStructure);
});
}
return packetStructure;
},
unpack(packetStructure) {
const arrayCount = packetStructure.readUInt16();
const array = [];
for (let i = 0; i < arrayCount; i++) {
const arrayStructure = packets[this.type];
const item = {};
Object.keys(arrayStructure).forEach(name => {
item[name] = arrayStructure[name].unpack(packetStructure);
});
array.push(item);
}
return array;
}
};
return packet;
},
sgList(type, value = []) {
const packet = {
value,
type,
pack(packetStructure) {
packetStructure.writeUInt32(this.value.length);
const arrayStructure = packets[this.type];
for (const item of this.value) {
Object.keys(arrayStructure).forEach(name => {
arrayStructure[name].value = item[name];
packetStructure = arrayStructure[name].pack(packetStructure);
});
}
return packetStructure;
},
unpack(packetStructure) {
const arrayCount = packetStructure.readUInt32();
const array = [];
for (let i = 0; i < arrayCount; i++) {
const arrayStructure = packets[this.type];
const item = {};
Object.keys(arrayStructure).forEach(name => {
item[name] = arrayStructure[name].unpack(packetStructure);
});
array.push(item);
}
return array;
}
};
return packet;
},
mapper(map, item) {
const packet = {
item,
value: false,
pack(packetStructure) {
return item.pack(packetStructure);
},
unpack(packetStructure) {
const key = item.unpack(packetStructure);
return map[key] ?? key;
}
};
return packet;
}
};
// FIX: uInt64 fields that were '' now default to Buffer.alloc(8) via fixed uInt64() factory.
// acknowledge keeps sgList (UInt32 count) — console accepts it and original worked with it.
const packets = {
powerOn: { liveId: types.sgString() },
json: { json: types.sgString('{}') },
discoveryRequest: { flags: types.uInt32(0), clientType: types.uInt16(3), minVersion: types.uInt16(0), maxVersion: types.uInt16(2) },
discoveryResponse: { flags: types.uInt32(0), clientType: types.uInt16(0), consoleName: types.sgString(), uuid: types.sgString(), lastError: types.uInt32(0), certificateLength: types.uInt16(0), certificate: types.bytes() },
connectRequest: { uuid: types.bytes(16, ''), publicKeyType: types.uInt16(0), publicKey: types.bytes(64, ''), iv: types.bytes(16, ''), payloadProtected: types.bytes() },
connectResponse: { iv: types.bytes(16, ''), payloadProtected: types.bytes() },
connectRequestProtected: { userHash: types.sgString(''), token: types.sgString(''), connectRequestNum: types.uInt32(0), connectRequestGroupStart: types.uInt32(0), connectRequestGroupEnd: types.uInt32(1) },
connectResponseProtected: { connectResult: types.uInt16(1), pairingState: types.uInt16(2), participantId: types.uInt32(0) },
localJoin: { clientType: types.uInt16(3), nativeWidth: types.uInt16(1080), nativeHeight: types.uInt16(1920), dpiX: types.uInt16(96), dpiY: types.uInt16(96), deviceCapabilities: types.uInt64(8, Buffer.from('ffffffffffffffff', 'hex')), clientVersion: types.uInt32(15), osMajorVersion: types.uInt32(6), osMinorVersion: types.uInt32(2), displayName: types.sgString('Xbox-TV') },
channelStartRequest: { channelRequestId: types.uInt32(0), titleId: types.uInt32(0), service: types.bytes(16, ''), activityId: types.uInt32(0) },
channelStartResponse: { channelRequestId: types.uInt32(0), channelTargetId: types.uInt64(8), result: types.uInt32(0) },
acknowledge: { lowWatermark: types.uInt32(0), processedList: types.sgList('processedList', []), rejectedList: types.sgList('rejectedList', []) },
processedList: { id: types.uInt32(0) },
rejectedList: { id: types.uInt32(0) },
consoleStatus: { liveTvProvider: types.uInt32(0), majorVersion: types.uInt32(0), minorVersion: types.uInt32(0), buildNumber: types.uInt32(0), locale: types.sgString('en-US'), activeTitles: types.sgArray('activeTitle') },
activeTitle: { flags: types.bytes(2), titleId: types.uInt32(0), productId: types.bytes(16, ''), sandboxId: types.bytes(16, ''), aumId: types.sgString('') },
recordGameDvr: { startTimeDelta: types.sInt32(0), endTimeDelta: types.sInt32(0) },
gamepad: { timestamp: types.uInt64(8), buttons: types.uInt16(0), leftTrigger: types.uInt32(0), rightTrigger: types.uInt32(0), leftThumbstickX: types.uInt32(0), leftThumbstickY: types.uInt32(0), rightThumbstickX: types.uInt32(0), rightThumbstickY: types.uInt32(0) },
mediaState: { titleId: types.uInt32(0), aumId: types.sgString(), assetId: types.sgString(), mediaType: types.mapper(LocalApi.Media.Types, types.uInt16(0)), soundLevel: types.mapper(LocalApi.Media.SoundLevel, types.uInt16(0)), enabledCommands: types.uInt32(0), playbackStatus: types.mapper(LocalApi.Media.PlaybackState, types.uInt16(0)), rate: types.uInt32(0), position: types.uInt64(8), mediaStart: types.uInt64(8), mediaEnd: types.uInt64(8), minSeek: types.uInt64(8), maxSeek: types.uInt64(8), metadata: types.sgArray('mediaStateList', []) },
mediaStateList: { name: types.sgString(), value: types.sgString() },
mediaCommand: { requestId: types.uInt64(8), titleId: types.uInt32(0), command: types.uInt32(0) },
powerOff: { liveId: types.sgString('') },
disconnect: { reason: types.uInt32(1), errorCode: types.uInt32(0) }
};
return packets;
}
}
export default Packets;
================================================
FILE: src/localApi/sgcrypto.js
================================================
import JsRsaSign from 'jsrsasign';
import Crypto from 'crypto';
import { EOL } from 'os';
import Elliptic from 'elliptic';
const EC = Elliptic.ec;
const IV = Buffer.from('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00');
class SgCrypto {
constructor() {
this.key = false;
this.iv = false;
this.hashKey = false;
this.ec = new EC('p256'); // P-256
}
async getPublicKey(certificate) {
try {
certificate = certificate.toString('base64').match(/.{0,64}/g).join('\n');
const pem = `-----BEGIN CERTIFICATE-----${EOL}${certificate}-----END CERTIFICATE-----`;
const ecKey = JsRsaSign.X509.getPublicKeyFromCertPEM(pem);
const sha512 = Crypto.createHash('sha512');
const key1 = this.ec.genKeyPair();
const key2 = this.ec.keyFromPublic(ecKey.pubKeyHex, 'hex');
const shared1 = key1.derive(key2.getPublic());
const derivedSecret = Buffer.from(shared1.toString(16), 'hex');
const publicKeyClient = key1.getPublic('hex');
const preSalt = Buffer.from('d637f1aae2f0418c', 'hex');
const postSalt = Buffer.from('a8f81a574e228ab7', 'hex');
const prePostSalt = Buffer.concat([preSalt, derivedSecret, postSalt]);
const shaSecret = sha512.update(prePostSalt).digest();
const publicKey = Buffer.from(publicKeyClient.substring(2), 'hex');
const secret = Buffer.from(shaSecret.toString('hex'), 'hex');
this.key = secret.subarray(0, 16);
this.iv = secret.subarray(16, 32);
this.hashKey = secret.subarray(32, 64);
return { publicKey, iv: this.iv };
} catch (error) {
throw new Error(`sign public key error: ${error}`);
}
}
getKey() {
return this.key;
}
getIv() {
return this.iv;
}
encrypt(data, key, iv) {
data = Buffer.from(data);
key = key || this.key;
iv = iv || IV;
const cipher = Crypto.createCipheriv('aes-128-cbc', key, iv);
cipher.setAutoPadding(false);
let encryptedPayload = cipher.update(data, 'binary', 'binary');
encryptedPayload += cipher.final('binary');
return Buffer.from(encryptedPayload, 'binary');
}
// Note: argument order (data, iv, key) differs from encrypt(data, key, iv) — kept as original.
// All call sites pass (data, iv) relying on key defaulting to this.key.
decrypt(data, iv, key) {
key = key || this.key;
iv = iv || IV;
const decipher = Crypto.createDecipheriv('aes-128-cbc', key, iv);
decipher.setAutoPadding(false);
let decryptedPayload = decipher.update(data, 'binary', 'binary');
decryptedPayload += decipher.final('binary');
return this.removePadding(Buffer.from(decryptedPayload, 'binary'));
}
sign(data) {
const hashHmac = Crypto.createHmac('sha256', this.hashKey);
hashHmac.update(data, 'binary', 'binary');
return Buffer.from(hashHmac.digest('binary'), 'binary');
}
removePadding(payload) {
const payloadLength = payload.subarray(-1).readUInt8(0);
if (payloadLength > 0 && payloadLength < 16) {
return payload.subarray(0, payload.length - payloadLength);
}
return payload;
}
}
export default SgCrypto;
================================================
FILE: src/localApi/simple.js
================================================
import Packets from './packets.js';
import Structure from './structure.js';
import { LocalApi } from '../constants.js';
class Simple {
constructor(type) {
this.type = type;
this.packets = new Packets();
this.packet = this.packets[type];
this.packetProtected = this.packet.payloadProtected ? this.packets[`${type}Protected`] : false;
}
// === Helpers ===
static applyPKCS7Padding(structure) {
const blockSize = 16;
const length = structure.toBuffer().length;
const padTotal = blockSize - (length % blockSize || blockSize);
for (let i = 0; i < padTotal; i++) {
structure.writeUInt8(padTotal);
}
}
set(key, value, isProtected = false) {
const targetPacket = isProtected ? this.packetProtected : this.packet;
if (!targetPacket || !targetPacket[key]) return;
const currentLength = targetPacket[key].length || 0;
targetPacket[key].value = value;
targetPacket[key].length = currentLength > 0 ? value.length : currentLength;
}
pack(crypto = false) {
const structure = new Structure();
let packet;
let payloadProtectedLength = 0;
let payloadProtectedLengthReal = 0;
for (const name in this.packet) {
if (name === 'payloadProtected' && this.packetProtected) {
const structureProtected = new Structure();
for (const fieldName in this.packetProtected) {
if (this.packet.payloadProtected?.value?.[fieldName] !== undefined) {
this.packetProtected[fieldName].value = this.packet.payloadProtected.value[fieldName];
}
this.packetProtected[fieldName].pack(structureProtected);
}
payloadProtectedLength = structureProtected.toBuffer().length;
Simple.applyPKCS7Padding(structureProtected);
payloadProtectedLengthReal = structureProtected.toBuffer().length;
const payloadEncrypted = crypto.encrypt(
structureProtected.toBuffer(),
crypto.getKey(),
this.packet.iv?.value
);
structure.writeBytes(payloadEncrypted);
} else {
this.packet[name].pack(structure);
}
}
const payload = structure.toBuffer();
switch (this.type) {
case 'powerOn':
// FIX: powerOn (dd02) wire format: [type 2B][unprotectedLen 2B][protectedLen 2B][version=2 2B][payload]
// Original pack1('', ...) produced [type][len][00 00][payload] — missing protectedLen field.
{
const hdr = new Structure();
hdr.writeUInt16(payload.length); // unprotected_payload_length
hdr.writeUInt16(0); // protected_payload_length = 0
hdr.writeUInt16(2); // version = 2
packet = Buffer.concat([LocalApi.Messages.Flags.powerOn, hdr.toBuffer(), payload]);
}
break;
case 'discoveryRequest':
packet = this.pack1(LocalApi.Messages.Flags.discoveryRequest, payload, Buffer.from('0000', 'hex'));
break;
case 'discoveryResponse':
packet = this.pack1(LocalApi.Messages.Flags.discoveryResponse, payload, Buffer.from([0, 2]));
break;
case 'connectRequest':
packet = this.pack1(LocalApi.Messages.Flags.connectRequest, payload, Buffer.from('0002', 'hex'), payloadProtectedLength, payloadProtectedLengthReal);
const payloadProtected = crypto.sign(packet);
packet = Buffer.concat([packet, Buffer.from(payloadProtected)]);
break;
case 'connectRequestProtected':
Simple.applyPKCS7Padding(structure);
// FIX: original passed crypto.getIv() as key — encrypt(data, key, iv) requires key first
let payloadEncrypted = crypto.encrypt(structure.toBuffer(), crypto.getKey(), crypto.getIv());
payloadEncrypted = new Structure(payloadEncrypted);
packet = payloadEncrypted.toBuffer();
break;
case 'connectResponse':
packet = this.pack1(LocalApi.Messages.Flags.connectResponse, payload, Buffer.from([0, 2]));
break;
default:
packet = payload;
}
return packet;
}
pack1(type, payload, version, payloadProtectedLength = 0, payloadProtectedLengthReal = 0) {
const structure = new Structure();
const structureProtected = new Structure();
if (payloadProtectedLength > 0) {
structure.writeUInt16(payload.length - payloadProtectedLengthReal);
const payloadLength = structure.toBuffer();
structureProtected.writeUInt16(payloadProtectedLength);
payloadProtectedLength = structureProtected.toBuffer();
return Buffer.concat([type, payloadLength, payloadProtectedLength, version, payload]);
}
structure.writeUInt16(payload.length);
const payloadLength = structure.toBuffer();
return Buffer.concat([type, payloadLength, Buffer.from([0, version[1] || 0]), payload]);
}
unpack(crypto = undefined, data = false) {
const structure = new Structure(data);
const typeHex = structure.readBytes(2).toString('hex');
const type = typeHex === 'dd02' ? 'powerOn' : this.type;
let packet = {
typeHex,
type,
payloadLength: structure.readUInt16(),
version: structure.readUInt16(),
};
if (packet.version !== 0 && packet.version !== 2) {
packet.payloadProtectedLength = packet.version;
packet.version = structure.readUInt16();
}
for (const name in this.packet) {
packet[name] = this.packet[name].unpack(structure);
this.set(name, packet[name]);
}
if (packet.payloadProtected !== undefined) {
// FIX: extract signature BEFORE truncating the buffer
const signature = packet.payloadProtected.subarray(-32);
const encryptedData = packet.payloadProtected.subarray(0, -32);
// Original: decrypt(data, iv) — matches sgcrypto.decrypt(data, iv, key) signature
const decrypted = crypto.decrypt(encryptedData, packet.iv).subarray(0, packet.payloadProtectedLength);
packet.payloadProtected = {};
const structurePayloadDecrypted = new Structure(decrypted);
const packetProtected = this.packets[`${packet.type}Protected`];
for (const name in packetProtected) {
packet.payloadProtected[name] = packetProtected[name].unpack(structurePayloadDecrypted);
this.set('payloadProtected', packet.payloadProtected);
}
// Note: crypto.verify not implemented in original sgcrypto — skipped intentionally
packet.signature = signature;
}
return packet;
}
}
export default Simple;
================================================
FILE: src/localApi/structure.js
================================================
class Structure {
constructor(packet) {
this.packet = packet ? packet : Buffer.alloc(0);
this.totalLength = this.packet.length;
this.offset = 0;
}
writeSGString(data) {
if (typeof data !== 'string') {
throw new Error('data must be a string');
}
const dataLength = Buffer.byteLength(data, 'utf8');
if (dataLength > 65535) {
throw new Error('data exceeds the maximum allowed length');
}
const stringLengthBuffer = Buffer.allocUnsafe(2);
stringLengthBuffer.writeUInt16BE(dataLength, 0);
const dataBuffer = Buffer.from(data, 'utf8');
const nullTerminator = Buffer.from([0]);
this.add(Buffer.concat([stringLengthBuffer, dataBuffer, nullTerminator]));
return this;
}
readSGString() {
const stringLength = this.readUInt16();
const stringBuffer = this.packet.subarray(this.offset, this.offset + stringLength);
this.offset += stringLength + 1; // skip null terminator
return stringBuffer.toString('utf8');
}
writeBytes(data, type) {
const dataBuffer = Buffer.from(data, type);
this.add(dataBuffer);
return this;
}
readBytes(length = false) {
let rawData;
if (length === false) {
rawData = this.packet.subarray(this.offset);
this.offset = this.totalLength;
} else {
rawData = this.packet.subarray(this.offset, this.offset + length);
this.offset += length;
}
return rawData;
}
writeUInt8(data) {
if (data < 0 || data > 255) {
throw new Error('data must be a valid unsigned 8-bit integer');
}
const buf = Buffer.allocUnsafe(1);
buf.writeUInt8(data, 0);
this.add(buf);
return this;
}
readUInt8() {
const value = this.packet.readUInt8(this.offset);
this.offset += 1;
return value;
}
writeUInt16(data) {
if (data < 0 || data > 65535) {
throw new Error('data must be a valid unsigned 16-bit integer');
}
const buf = Buffer.allocUnsafe(2);
buf.writeUInt16BE(data, 0);
this.add(buf);
return this;
}
readUInt16() {
const value = this.packet.readUInt16BE(this.offset);
this.offset += 2;
return value;
}
writeUInt32(data) {
if (data < 0 || data > 0xFFFFFFFF) {
throw new Error('data must be a valid unsigned 32-bit integer');
}
const buf = Buffer.allocUnsafe(4);
buf.writeUInt32BE(data, 0);
this.add(buf);
return this;
}
readUInt32() {
const value = this.packet.readUInt32BE(this.offset);
this.offset += 4;
return value;
}
writeInt32(data) {
if (data < -2147483648 || data > 2147483647) {
throw new Error('data must be a valid signed 32-bit integer');
}
const buf = Buffer.allocUnsafe(4);
buf.writeInt32BE(data, 0);
this.add(buf);
return this;
}
readInt32() {
const value = this.packet.readInt32BE(this.offset);
this.offset += 4;
return value;
}
writeUInt64(value) {
if (typeof value !== 'bigint') {
throw new Error('value must be a BigInt');
}
const buf = Buffer.allocUnsafe(8);
buf.writeUInt32BE(Number(value >> 32n), 0);
buf.writeUInt32BE(Number(value & 0xFFFFFFFFn), 4);
this.add(buf);
return this;
}
readUInt64() {
const high = this.packet.readUInt32BE(this.offset);
const low = this.packet.readUInt32BE(this.offset + 4);
this.offset += 8;
return (BigInt(high) << 32n) | BigInt(low);
}
toBuffer() {
return this.packet;
}
add(data) {
if (!Buffer.isBuffer(data)) {
throw new Error('Data must be a Buffer object');
}
this.packet = Buffer.concat([this.packet, data]);
this.totalLength = this.packet.length;
}
}
export default Structure;
================================================
FILE: src/localApi/xboxlocalapi.js
================================================
import dgram from 'dgram';
import { parse as UuIdParse, v4 as UuIdv4 } from 'uuid';
import EventEmitter from 'events';
import SimplePacket from './simple.js';
import MessagePacket from './message.js';
import SGCrypto from './sgcrypto.js';
import { LocalApi } from '../constants.js';
import ImpulseGenerator from '../impulsegenerator.js';
import Functions from '../functions.js';
class XboxLocalApi extends EventEmitter {
constructor(config, tokensFile, devInfoFile, restFulEnabled, mqttEnabled) {
super();
this.crypto = new SGCrypto();
this.host = config.host;
this.liveId = config.xboxLiveId;
this.logSuccess = config.log?.success;
this.logWarn = config.log?.warn;
this.logError = config.log?.error;
this.logDebug = config.log?.debug;
this.tokensFile = tokensFile;
this.devInfoFile = devInfoFile;
// FIX: guard with || false so undefined becomes false
this.restFulEnabled = restFulEnabled || false;
this.mqttEnabled = mqttEnabled || false;
this.connected = false;
this.power = false;
this.volume = 0;
this.mute = false;
this.titleId = '';
this.reference = '';
this.playState = false;
this.firstRun = false;
this.fragments = {};
this.socket = null;
this.acknowledgeInterval = null;
this.sequenceNumber = 0;
this.sourceParticipantId = 0;
this.functions = new Functions();
//create impulse generator
this.impulseGenerator = new ImpulseGenerator()
.on('connect', async () => {
try {
if (this.connected || this.connecting) return;
if (this.logDebug) this.emit('debug', `Plugin send heartbeat to console`);
const state = await this.functions.ping(this.host);
if (!state.online) {
return;
}
if (this.logDebug) this.emit('debug', `Plugin received heartbeat from console`);
this.connecting = true;
try {
await this.connect();
// FIX: discoveryRequest in try (not finally) — only sent when socket is ready
const discoveryRequest = new SimplePacket('discoveryRequest');
const message = discoveryRequest.pack(this.crypto);
await this.sendSocketMessage(message, 'discoveryRequest');
} catch (error) {
if (this.logError) this.emit('error', `Connection error: ${error}`);
} finally {
// FIX: always release the connecting lock so next heartbeat can retry
this.connecting = false;
}
} catch (error) {
if (this.logError) this.emit('error', `Local API heartbeat error: ${error}, will retry`);
}
})
.on('state', (state) => {
this.emit(state ? 'success' : 'warn', `Local Api monitoring ${state ? 'started' : 'stopped'}`);
});
};
async updateState() {
// FIX: clearInterval before nulling — without this the old timer survives
// reconnect and fires a spurious disconnect after 14 s of the new session.
if (this.acknowledgeInterval) {
clearInterval(this.acknowledgeInterval);
}
this.socket = null;
this.connected = false;
this.firstRun = false;
this.acknowledgeInterval = null;
this.sequenceNumber = 0;
this.targetParticipantId = 0;
this.sourceParticipantId = 0;
this.power = false;
this.emit('stateChanged', this.power, this.titleId, this.reference, this.volume, this.mute, this.playState);
return true;
};
async getSequenceNumber() {
const seq = this.sequenceNumber;
this.sequenceNumber = (this.sequenceNumber + 1) >>> 0;
// FIX: typo 'Sqquence' → 'Sequence'
if (this.logDebug) this.emit('debug', `Sequence number set to: ${this.sequenceNumber}`);
return seq;
};
async sendSocketMessage(message, type, host = this.host) {
return new Promise((resolve, reject) => {
if (!this.socket) {
return reject(new Error(`Socket not initialized, cannot send message: ${type}`));
}
const offset = 0;
const length = message.byteLength;
this.socket.send(message, offset, length, 5050, host, (error, bytes) => {
if (error) {
return reject(new Error(`Socket send error: ${error}`));
}
if (this.logDebug) this.emit('debug', `Socket send: ${type} → ${host}, ${bytes}B`);
resolve(true);
});
});
};
async connect() {
return new Promise((resolve, reject) => {
try {
this.socket = dgram.createSocket('udp4')
.on('error', (error) => {
if (this.logError) this.emit('error', `Socket error: ${error}`);
this.socket?.close();
reject(`Socket error: ${error}`);
})
.on('close', async () => {
if (this.logDebug) this.emit('debug', 'Socket closed.');
await this.updateState();
})
.on('listening', () => {
this.socket.setBroadcast(true);
const address = this.socket.address();
if (this.logDebug) this.emit('debug', `Socket start listening: ${address.address}:${address.port}`);
resolve(true);
})
.on('message', async (data) => {
try {
// get message type in hex
const messageTypeHex = data.subarray(0, 2).toString('hex');
if (this.logDebug) this.emit('debug', `Received message type: ${messageTypeHex}`);
// check message type exists
if (!Object.keys(LocalApi.Messages.Category).includes(messageTypeHex)) {
if (this.logWarn) this.emit('warn', `Received unknown message type: ${messageTypeHex}, message: ${data}`);
return;
}
// get message type and request
const messageType = LocalApi.Messages.Category[messageTypeHex];
const messageRequest = LocalApi.Messages.CategoryTypes[messageTypeHex];
// create packet structure
let packetStructure;
switch (messageRequest) {
case 'discoveryRequest':
case 'discoveryResponse':
case 'connectRequest':
case 'connectResponse':
packetStructure = new SimplePacket(messageRequest);
break;
case 'message':
packetStructure = new MessagePacket(messageRequest);
break;
default:
if (this.logDebug) this.emit('debug', `No handler for type: ${messageTypeHex}`);
return;
}
// unpack packet
let packet;
try {
packet = packetStructure.unpack(this.crypto, data);
if (this.logDebug) this.emit('debug', `Received packet type: ${packet.type}, packet: ${JSON.stringify(packet, null, 2)}`);
} catch (error) {
if (this.logError) this.emit('error', `Failed to unpack packet type: ${messageType}, error: ${error.message}`);
return;
}
if (messageType === 'message') {
const targetId = packet.targetParticipantId;
// FIX: targetId=0 is a broadcast (console keepalive acknowledge) —
// must pass through regardless of our sourceParticipantId.
if (targetId !== 0 && targetId !== this.sourceParticipantId) {
if (this.logDebug) this.emit('debug', `ParticipantId mismatch: ${targetId} !== ${this.sourceParticipantId}. Ignoring packet`);
return;
}
if (packet.flags.needAcknowlegde) {
try {
const acknowledge = new MessagePacket('acknowledge');
acknowledge.set('lowWatermark', packet.sequenceNumber);
acknowledge.packet.processedList.value.push({ id: packet.sequenceNumber });
const sequenceNumber1 = await this.getSequenceNumber();
const message = acknowledge.pack(this.crypto, sequenceNumber1, this.targetParticipantId, this.sourceParticipantId);
await this.sendSocketMessage(message, 'acknowledge');
} catch (error) {
if (this.logError) this.emit('error', `Heartbeat error: ${error}`);
}
}
}
// handle packet types
switch (packet.type) {
case 'json':
const fragments = this.fragments;
let jsonMessage;
try {
jsonMessage = JSON.parse(packet.payloadProtected.json);
} catch (error) {
if (this.logDebug) this.emit('debug', `Failed to parse JSON payload: ${error.message}`);
return;
}
const datagramId = jsonMessage.datagramId;
if (datagramId) {
if (!fragments[datagramId]) {
fragments[datagramId] = {
partials: {},
getValue() {
const buffers = Object.keys(this.partials)
.sort((a, b) => Number(a) - Number(b))
.map(offset => Buffer.from(this.partials[offset], 'base64'));
return Buffer.concat(buffers);
},
isValid() {
try {
JSON.parse(this.getValue().toString());
return true;
} catch {
return false;
}
}
};
}
fragments[datagramId].partials[jsonMessage.fragmentOffset] = jsonMessage.fragmentData;
if (fragments[datagramId].isValid()) {
const fullJson = fragments[datagramId].getValue().toString();
packet.payloadProtected = JSON.parse(fullJson);
if (this.logDebug) this.emit('debug', `Reassembled JSON packet: ${fullJson}`);
delete fragments[datagramId];
}
}
break;
case 'discoveryResponse':
if (this.connected) return;
const deviceType = packet.clientType;
const deviceName = packet.consoleName;
const certificate = packet.certificate;
// FIX: typo 'athorized' → 'authorized'
let authorized = false;
if (this.logDebug) this.emit('debug', `Discovered device: ${LocalApi.Console.Name[deviceType] || 'Unknown'}, name: ${deviceName}`);
if (!certificate) {
if (this.logError) this.emit('error', 'Certificate missing from device packet');
return;
}
let token = null;
let userHash = null;
try {
const response = await this.functions.readData(this.tokensFile, true);
token = response?.xsts?.Token || null;
userHash = response.xsts.DisplayClaims?.xui?.[0]?.uhs;
if (token && userHash) {
authorized = true;
}
} catch (error) {
this.emit('debug', 'No valid token data found, connecting anonymously');
}
try {
const data = await this.crypto.getPublicKey(certificate);
if (this.logDebug) this.emit('debug', `Signed public key: ${data.publicKey.toString('hex')}, iv: ${data.iv.toString('hex')}`);
const connectRequest = new SimplePacket('connectRequest');
const uuidBuffer = Buffer.from(UuIdParse(UuIdv4()));
if (uuidBuffer.length !== 16) {
if (this.logError) this.emit('error', 'Invalid UUID length');
return;
}
connectRequest.set('uuid', uuidBuffer);
connectRequest.set('publicKey', data.publicKey);
connectRequest.set('iv', data.iv);
if (authorized) {
const sequenceNumber = await this.getSequenceNumber();
connectRequest.set('userHash', userHash, true);
connectRequest.set('token', token, true);
connectRequest.set('connectRequestNum', sequenceNumber);
connectRequest.set('connectRequestGroupStart', 0);
connectRequest.set('connectRequestGroupEnd', 1);
}
// Track auth state so powerOff() can detect anonymous sessions
this.authorized = authorized;
if (this.logDebug) this.emit('debug', `Client connecting using: ${authorized ? 'XSTS token' : 'Anonymous'}`);
const message = connectRequest.pack(this.crypto);
await this.sendSocketMessage(message, 'connectRequest');
} catch (error) {
if (this.logError) this.emit('error', `Sign certificate error: ${error}`);
}
break;
case 'connectResponse':
const { connectResult, pairingState, participantId } = packet.payloadProtected;
const errorTable = {
0: 'Success.',
1: 'Pending login. Reconnect to complete.',
2: 'Unknown.',
3: 'Anonymous connections disabled.',
4: 'Device limit exceeded.',
5: 'Remote connect is disabled on the console.',
6: 'User authentication failed.',
7: 'User Sign-In failed.',
8: 'User Sign-In timeout.',
9: 'User Sign-In required.'
};
if (connectResult !== 0) {
if (this.logError) this.emit('error', `Connect error: ${errorTable[connectResult] || connectResult}`);
return;
}
if (this.logDebug) this.emit('debug', `Client connected, pairing state: ${LocalApi.Console.PairingState[pairingState]}`);
this.connected = true;
try {
this.sourceParticipantId = participantId;
this.targetParticipantId = packet.sourceParticipantId || this.targetParticipantId || 0;
const sequenceNumber = await this.getSequenceNumber();
const localJoin = new MessagePacket('localJoin');
const message = localJoin.pack(this.crypto, sequenceNumber, this.targetParticipantId, this.sourceParticipantId);
await this.sendSocketMessage(message, 'localJoin');
this.firstRun = true;
} catch (error) {
if (this.logError) this.emit('error', `Send local join error: ${error}`);
}
break;
case 'consoleStatus':
if (!packet.payloadProtected) return;
if (this.firstRun) {
if (this.logSuccess) this.emit('success', `Connect Success`);
const { majorVersion, minorVersion, buildNumber, locale } = packet.payloadProtected;
const firmwareRevision = `${majorVersion}.${minorVersion}.${buildNumber}`;
const info = { locale, firmwareRevision };
this.emit('deviceInfo', info);
this.firstRun = false;
}
const activeTitles = Array.isArray(packet.payloadProtected.activeTitles) ? packet.payloadProtected.activeTitles : [];
// FIX: power derived from activeTitles presence
const power = activeTitles.length > 0;
if (power) {
// FIX: use last element — activeTitles ordered oldest→newest,
// last entry is the foreground title
const title = activeTitles[0];
this.titleId = title.titleId;
this.reference = title.aumId;
}
this.power = power;
this.playState = false;
// FIX: emit stateChanged always — when activeTitles is empty
// (console turning off), power=false must reach HomeKit immediately.
this.emit('stateChanged', this.power, this.titleId, this.reference, this.volume, this.mute, this.playState);
if (this.logDebug) this.emit('debug', `Status changed, power: ${this.power}, app Id: ${this.titleId}, reference: ${this.reference}`);
const statusState = { power: this.power, titleId: this.titleId, reference: this.reference, volume: this.volume, mute: this.mute };
if (this.restFulEnabled) this.emit('restFul', 'state', statusState);
if (this.mqttEnabled) this.emit('mqtt', 'State', statusState);
// Inactivity watchdog — consoleStatus is the primary sign-of-life.
// We ping the host every 5 s to reset the timer when the console is
// idle between status packets. After 14 s silence the socket is
// closed so the impulse generator can reconnect.
this.heartBeatStartTime = Date.now();
if (!this.acknowledgeInterval) {
this.acknowledgeInterval = setInterval(async () => {
const elapsed = (Date.now() - this.heartBeatStartTime) / 1000;
if (this.logDebug) this.emit('debug', `Console last seen: ${elapsed.toFixed(1)}s ago`);
if (elapsed >= 14) {
clearInterval(this.acknowledgeInterval);
this.acknowledgeInterval = null;
if (this.logDebug) this.emit('debug', `Console inactivity timeout — disconnecting`);
// Close socket first so on('close') triggers updateState().
// Calling updateState() directly would null this.socket before
// close(), causing the old socket to leak in the OS.
const socketToClose = this.socket;
this.socket = null;
this.connected = false;
if (socketToClose) socketToClose.close();
return;
}
// Network ping every 5 s — resets the watchdog when console
// is reachable but has no state change to report.
if (Math.round(elapsed) % 5 === 0 && Math.round(elapsed) > 0) {
try {
const pingResult = await this.functions.ping(this.host);
if (pingResult.online) {
this.heartBeatStartTime = Date.now();
if (this.logDebug) this.emit('debug', `Ping OK — console reachable`);
} else {
if (this.logDebug) this.emit('debug', `Ping failed — console unreachable`);
}
} catch (error) {
if (this.logError) this.emit('error', `Ping error: ${error}`);
}
}
}, 1000);
}
break;
case 'acknowledge':
this.heartBeatStartTime = Date.now();
if (!this.acknowledgeInterval) {
this.acknowledgeInterval = setInterval(async () => {
const elapsed = (Date.now() - this.heartBeatStartTime) / 1000;
if (this.logDebug) this.emit('debug', `Socket received heart beat: ${elapsed.toFixed(1)} sec ago`);
if (elapsed >= 14) {
clearInterval(this.acknowledgeInterval);
const sequenceNumber = await this.getSequenceNumber();
const disconnect = new MessagePacket('disconnect');
disconnect.set('reason', 2);
disconnect.set('errorCode', 0);
const message = disconnect.pack(this.crypto, sequenceNumber, this.targetParticipantId, this.sourceParticipantId);
await this.sendSocketMessage(message, 'disconnect');
await this.updateState();
}
}, 1000);
}
break;
case 'pairedIdentityStateChanged':
const pairingState1 = packet.payloadProtected.pairingState || 0;
if (this.logDebug) this.emit('debug', `Client pairing state: ${LocalApi.Console.PairingState[pairingState1]}`);
break;
default:
if (this.logWarn) this.emit('warn', `Received unknown packet type: ${packet.type}`);
break;
}
} catch (error) {
if (this.logError) this.emit('error', `Handle message error: ${error.message || error}`);
}
})
.bind();
} catch (error) {
reject(`Connect error: ${error.message || error}`);
};
});
};
};
export default XboxLocalApi;
================================================
FILE: src/mqtt.js
================================================
import { connect } from 'mqtt';
import EventEmitter from 'events';
class Mqtt extends EventEmitter {
constructor(config) {
super();
this.config = config;
const url = `mqtt://${config.host}:${config.port}`;
const subscribeTopic = `${config.prefix}/Set`;
const options = {
clientId: config.clientId,
username: config.user,
password: config.passwd,
protocolVersion: 5,
clean: false,
properties: {
sessionExpiryInterval: 60 * 60,
userProperties: {
source: 'node-client'
}
}
};
this.mqttClient = connect(url, options)
.on('connect', async () => {
this.emit('connected', 'MQTT v5 connected.');
try {
await new Promise((resolve, reject) => {
this.mqttClient.subscribe(subscribeTopic,
{
qos: 1,
properties: {
userProperties: {
type: 'subscription'
}
}
},
(error) => {
if (error) return reject(error);
resolve();
}
);
});
this.emit('connected', `MQTT Subscribe topic: ${subscribeTopic}`);
} catch (error) {
if (config.logWarn) this.emit('warn', `MQTT Subscribe error: ${error.message}`);
}
})
.on('message', (topic, payload, packet) => {
try {
const parsedMessage = JSON.parse(payload.toString());
if (config.logDebug) this.emit('debug', `MQTT Received Topic: ${topic}, Payload: ${JSON.stringify(parsedMessage, null, 2)}`);
for (const [key, value] of Object.entries(parsedMessage)) {
this.emit('set', key, value);
}
} catch (error) {
if (config.logWarn) this.emit('warn', `MQTT Parse error: ${error.message}`);
}
})
.on('error', (error) => {
this.emit('warn', `MQTT Error: ${error.message}`);
})
.on('reconnect', () => {
if (config.logDebug) this.emit('debug', 'MQTT Reconnecting...');
})
.on('close', () => {
if (config.logDebug) this.emit('debug', 'MQTT Connection closed.');
});
}
publish(topic, message) {
return new Promise((resolve, reject) => {
const fullTopic = `${this.config.prefix}/${topic}`;
const publishMessage = JSON.stringify(message);
this.mqttClient.publish(fullTopic, publishMessage,
{
qos: 1,
properties: {
contentType: 'application/json',
userProperties: {
source: 'node',
action: 'set'
}
}
},
(error) => {
if (error) {
if (this.config.logWarn) this.emit('warn', `MQTT Publish error: ${error.message}`);
return reject(error);
}
if (this.config.logDebug) this.emit('debug', `MQTT Publish Topic: ${fullTopic}, Payload: ${publishMessage}`);
resolve();
}
);
});
}
}
export default Mqtt;
================================================
FILE: src/restful.js
================================================
import express, { json } from 'express';
import EventEmitter from 'events';
const DEFAULT_MESSAGE = 'This data is not available at this time.';
class RestFul extends EventEmitter {
constructor(config) {
super();
this.port = config.port;
this.logWarn = config.logWarn;
this.logDebug = config.logDebug;
this.restFulData = {
info: DEFAULT_MESSAGE,
state: DEFAULT_MESSAGE,
operation: DEFAULT_MESSAGE,
consoleslist: DEFAULT_MESSAGE,
profile: DEFAULT_MESSAGE,
apps: DEFAULT_MESSAGE,
storages: DEFAULT_MESSAGE,
status: DEFAULT_MESSAGE
}
this.connect();
}
connect() {
try {
const app = express();
app.set('json spaces', 2);
app.use(json());
// Register GET routes for all keys
for (const key of Object.keys(this.restFulData)) {
app.get(`/${key}`, (req, res) => {
res.json(this.restFulData[key]);
});
}
// Health check route
app.get('/status', (req, res) => {
res.json({
status: 'online',
uptime: process.uptime(),
available_paths: Object.keys(this.restFulData).map(k => `/${k}`)
});
});
// POST route to update values
app.post('/', (req, res) => {
try {
const obj = req.body;
if (!obj || typeof obj !== 'object' || Object.keys(obj).length === 0) {
if (this.logWarn) this.emit('warn', 'RESTFul Invalid JSON payload');
return res.status(400).json({ error: 'RESTFul Invalid JSON payload' });
}
const key = Object.keys(obj)[0];
const value = obj[key];
this.emit('set', key, value);
this.update(key, value);
if (this.logDebug) this.emit('debug', `RESTFul post data: ${JSON.stringify(obj, null, 2)}`);
res.json({ success: true, received: obj });
} catch (error) {
if (this.logWarn) this.emit('warn', `RESTFul Parse error: ${error}`);
res.status(500).json({ error: 'RESTFul Internal Server Error' });
}
});
// Start the server
app.listen(this.port, () => {
this.emit('connected', `RESTful started on port: ${this.port}`);
});
} catch (error) {
if (this.logWarn) this.emit('warn', `RESTful Connect error: ${error}`);
}
}
update(path, data) {
if (this.restFulData.hasOwnProperty(path)) {
this.restFulData[path] = data;
} else {
if (this.logWarn) this.emit('warn', `Unknown RESTFul update path: ${path}, data: ${JSON.stringify(data)}`);
return;
}
if (this.logDebug) this.emit('debug', `RESTFul update path: ${path}, data: ${JSON.stringify(data)}`);
}
}
export default RestFul;
================================================
FILE: src/webApi/authentication.js
================================================
import QueryString from 'querystring';
import axios from 'axios';
import { WebApi } from '../constants.js';
import Functions from '../functions.js';
class Authentication {
constructor(config) {
this.webApiClientId = config.clientId || WebApi.ClientId;
this.webApiClientSecret = config.clientSecret;
this.tokensFile = config.tokensFile;
this.tokens = {
oauth: {},
user: {},
xsts: {}
};
this.functions = new Functions();
}
async refreshToken(token) {
try {
const payload = {
'client_id': this.webApiClientId,
'grant_type': 'refresh_token',
'scope': WebApi.Scopes,
'refresh_token': token,
};
if (this.webApiClientSecret) {
payload.client_secret = this.webApiClientSecret;
}
const postData = QueryString.stringify(payload);
// BŁ12 FIX: axios.post expects config object as 3rd arg, not bare headers object
const response = await axios.post(WebApi.Url.RefreshToken, postData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
const refreshToken = response.data;
refreshToken.issued = new Date().toISOString();
this.tokens.oauth = refreshToken;
return true;
} catch (error) {
throw new Error(`Refresh token error: ${error}`);
}
}
async getUserToken(accessToken) {
try {
const payload = {
'RelyingParty': 'http://auth.xboxlive.com',
'TokenType': 'JWT',
'Properties': {
'AuthMethod': 'RPS',
'SiteName': 'user.auth.xboxlive.com',
'RpsTicket': `d=${accessToken}`
}
};
const postData = JSON.stringify(payload);
const response = await axios.post(WebApi.Url.UserToken, postData, { headers: { 'Content-Type': 'application/json' } });
const userToken = response.data;
this.tokens.user = userToken;
this.tokens.xsts = {};
return true;
} catch (error) {
throw new Error(`User token error: ${error}`);
}
}
async getXstsToken(userToken) {
try {
const payload = {
'RelyingParty': 'http://xboxlive.com',
'TokenType': 'JWT',
'Properties': {
'UserTokens': [userToken],
'SandboxId': 'RETAIL',
}
};
const postData = JSON.stringify(payload);
// BŁ12 FIX: was passing bare headers object without { headers } wrapper
const response = await axios.post(WebApi.Url.XstsToken, postData, { headers: { 'Content-Type': 'application/json', 'x-xbl-contract-version': '1' } });
const xstsToken = response.data;
this.tokens.xsts = xstsToken;
return true;
} catch (error) {
throw new Error(`Xsts token error: ${error}`);
}
}
async accessToken(webApiToken) {
try {
const payload = {
'client_id': this.webApiClientId,
'grant_type': 'authorization_code',
'scope': WebApi.Scopes,
'code': webApiToken,
'redirect_uri': WebApi.Url.Redirect
};
if (this.webApiClientSecret) {
payload.client_secret = this.webApiClientSecret;
}
const postData = QueryString.stringify(payload);
// BŁ12 FIX: consistent { headers } wrapper
const response = await axios.post(WebApi.Url.AccessToken, postData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
const accessToken = response.data;
accessToken.issued = new Date().toISOString();
this.tokens.oauth = accessToken;
await this.functions.saveData(this.tokensFile, this.tokens);
return true;
} catch (error) {
throw new Error(`Access token error: ${error}`);
}
}
async refreshTokens(type) {
switch (type) {
case 'user':
if (this.tokens.user.Token) {
const tokenExpired = new Date() > new Date(this.tokens.user.NotAfter).getTime();
if (tokenExpired) {
try {
await this.refreshToken(this.tokens.oauth.refresh_token);
await this.getUserToken(this.tokens.oauth.access_token);
await this.refreshTokens('xsts');
return true;
} catch (error) {
throw new Error(error);
}
} else {
try {
await this.refreshTokens('xsts');
return true;
} catch (error) {
throw new Error(error);
}
}
} else {
try {
await this.getUserToken(this.tokens.oauth.access_token);
await this.refreshTokens('xsts');
return true;
} catch (error) {
throw new Error(error);
}
}
case 'xsts':
if (this.tokens.xsts.Token) {
const tokenExpired = new Date() > new Date(this.tokens.xsts.NotAfter).getTime();
if (tokenExpired) {
try {
// BŁ13 FIX: was calling refreshTokens('xsts') after getXstsToken → infinite recursion
// when token expired. Now just refresh once and return.
await this.getXstsToken(this.tokens.user.Token);
return true;
} catch (error) {
throw new Error(error);
}
} else {
return true;
}
} else {
try {
await this.getXstsToken(this.tokens.user.Token);
return true;
} catch (error) {
throw new Error(error);
}
}
default:
throw new Error(`Unknown refresh token type: ${type}`);
}
}
async checkAuthorization() {
if (this.webApiClientId) {
try {
const tokens = await this.functions.readData(this.tokensFile, true);
this.tokens = !tokens ? this.tokens : tokens;
const refreshToken = this.tokens.oauth.refresh_token ?? false;
if (refreshToken) {
await this.refreshTokens('user');
await this.functions.saveData(this.tokensFile, this.tokens);
return { headers: `XBL3.0 x=${this.tokens.xsts.DisplayClaims.xui[0].uhs};${this.tokens.xsts.Token}`, tokens: this.tokens };
} else {
throw new Error('No oauth token found. Use authorization manager first.');
}
} catch (error) {
throw new Error(error);
}
} else {
throw new Error(`Authorization not possible, check plugin settings - Client Id: ${this.webApiClientId}`);
}
}
async generateAuthorizationUrl() {
try {
const payload = {
'client_id': this.webApiClientId,
'response_type': 'code',
'approval_prompt': 'auto',
'scope': WebApi.Scopes,
'redirect_uri': WebApi.Url.Redirect
};
const params = QueryString.stringify(payload);
const oauth2URI = `${WebApi.Url.oauth2}?${params}`;
return oauth2URI;
} catch (error) {
throw new Error(`Authorization URL error: ${error}`);
}
}
}
export default Authentication;
================================================
FILE: src/webApi/providers/achievements.js
================================================
import axios from 'axios';
class Archivements {
constructor(tokens, authorizationHeaders) {
this.tokens = tokens;
const headers = authorizationHeaders;
headers['x-xbl-contract-version'] = '2';
//create axios instance
this.axiosInstance = axios.create({
method: 'GET',
headers: headers
});
}
getTitleAchievements(continuationToken = 0) {
return new Promise(async (resolve, reject) => {
try {
const url = `https://achievements.xboxlive.com/users/xuid(${this.tokens.xsts.DisplayClaims.xui[0].xid})/history/titles?continuationToken=${continuationToken}`;
const response = await this.axiosInstance(url);
resolve(response.data);
} catch (error) {
reject(error);
};
});
}
getTitleAchievements360(continuationToken = 0) {
return new Promise(async (resolve, reject) => {
try {
this.headers['x-xbl-contract-version'] = 1
const url = `https://achievements.xboxlive.com/users/xuid(${this.tokens.xsts.DisplayClaims.xui[0].xid})/history/titles?continuationToken=${continuationToken}`;
const response = await this.axiosInstance(url);
resolve(response.data);
} catch (error) {
reject(error);
};
});
}
getTitleId(titleId, continuationToken = 0) {
return new Promise(async (resolve, reject) => {
try {
const url = `https://achievements.xboxlive.com/users/xuid(${this.tokens.xsts.DisplayClaims.xui[0].xid})/achievements?titleId=${titleId}&continuationToken=${continuationToken}`;
const response = await this.axiosInstance(url);
resolve(response.data);
} catch (error) {
reject(error);
};
});
}
getTitleId360(titleId, continuationToken = 0) {
return new Promise(async (resolve, reject) => {
try {
this.headers['x-xbl-contract-version'] = 1
const url = `https://achievements.xboxlive.com/users/xuid(${this.tokens.xsts.DisplayClaims.xui[0].xid})/achievements?titleId=${titleId}&continuationToken=${continuationToken}`;
const response = await this.axiosInstance(url);
resolve(response.data);
} catch (error) {
reject(error);
};
});
}
}
export default Archivements;
================================================
FILE: src/webApi/providers/catalog.js
================================================
import QueryString from 'querystring';
import axios from 'axios';
class Catalog {
constructor(authorizationHeaders) {
const headers = authorizationHeaders;
headers = { 'MS-CV': '0' };
//create axios instance
this.axiosInstance = axios.create({
method: 'GET',
headers: headers
});
}
searchTitle(query, marketLocale = 'US', languagesLocale = 'en-US') {
return new Promise(async (resolve, reject) => {
try {
const searchParams = {
"languages": languagesLocale,
"market": marketLocale,
"platformdependencyname": 'windows.xbox',
"productFamilyNames": "Games,Apps",
"query": query,
"topProducts": 25,
}
const queryParams = QueryString.stringify(searchParams);
const url = `https://displaycatalog.mp.microsoft.com/v7.0/productFamilies/autosuggest?${queryParams}`;
const response = await this.axiosInstance(url);
resolve(response.data);
} catch (error) {
reject(error);
};
});
}
getProductId(query, marketLocale = 'US', languagesLocale = 'en-US') {
return new Promise(async (resolve, reject) => {
try {
const searchParams = {
"actionFilter": 'Browse',
"bigIds": [query],
"fieldsTemplate": 'details',
"languages": languagesLocale,
"market": marketLocale,
}
const queryParams = QueryString.stringify(searchParams);
const url = `https://displaycatalog.mp.microsoft.com/v7.0/products?${queryParams}`;
const response = await this.axiosInstance(url);
resolve(response.data);
} catch (error) {
reject(error);
};
});
}
getProductFromAlternateId(titleId, titleType, marketLocale = 'US', languagesLocale = 'en-US') {
return new Promise(async (resolve, reject) => {
try {
const searchParams = {
"top": 25,
"alternateId": titleType,
"fieldsTemplate": 'details',
"languages": languagesLocale,
"market": marketLocale,
"value": titleId,
}
const queryParams = QueryString.stringify(searchParams);
const url = `https://displaycatalog.mp.microsoft.com/v7.0/products/lookup${queryParams}`;
const response = await this.axiosInstance(url);
resolve(response.data);
} catch (error) {
reject(error);
};
});
}
}
export default Catalog;
================================================
FILE: src/webApi/providers/gameclips.js
===
gitextract_t1gbyqj_/
├── .github/
│ └── workflows/
│ └── stale.yml
├── .gitignore
├── CHANGELOG.md
├── CLAUDE.md
├── LICENSE
├── README.md
├── config.schema.json
├── homebridge-ui/
│ ├── public/
│ │ └── index.html
│ └── server.js
├── index.js
├── package.json
├── sample-config.json
└── src/
├── constants.js
├── functions.js
├── impulsegenerator.js
├── localApi/
│ ├── message.js
│ ├── packets.js
│ ├── sgcrypto.js
│ ├── simple.js
│ ├── structure.js
│ └── xboxlocalapi.js
├── mqtt.js
├── restful.js
├── webApi/
│ ├── authentication.js
│ ├── providers/
│ │ ├── achievements.js
│ │ ├── catalog.js
│ │ ├── gameclips.js
│ │ ├── messages.js
│ │ ├── people.js
│ │ ├── pins.js
│ │ ├── screenshots.js
│ │ ├── social.js
│ │ ├── titlehub.js
│ │ ├── userpresence.js
│ │ └── userstats.js
│ └── xboxwebapi.js
└── xboxdevice.js
SYMBOL INDEX (154 symbols across 26 files)
FILE: homebridge-ui/server.js
class PluginUiServer (line 5) | class PluginUiServer extends HomebridgePluginUiServer {
method constructor (line 6) | constructor() {
method clearToken (line 20) | async clearToken(payload) {
method startAuthorization (line 33) | async startAuthorization(payload) {
FILE: index.js
class XboxPlatform (line 7) | class XboxPlatform {
method constructor (line 8) | constructor(log, config, api) {
method setupDevice (line 43) | async setupDevice(device, prefDir, log, api) {
method startDevice (line 132) | async startDevice({ device, name, host, authTokenFile, devInfoFile, in...
method configureAccessory (line 154) | configureAccessory(accessory) {
FILE: src/functions.js
class Functions (line 7) | class Functions {
method constructor (line 8) | constructor() {
method saveData (line 11) | async saveData(path, data, stringify = true) {
method readData (line 21) | async readData(path, parseJson = false) {
method sanitizeString (line 51) | async sanitizeString(str) {
method scaleValue (line 73) | async scaleValue(value, inMin, inMax, outMin, outMax) {
method ping (line 78) | async ping(ipOrHost) {
FILE: src/impulsegenerator.js
class ImpulseGenerator (line 3) | class ImpulseGenerator extends EventEmitter {
method constructor (line 4) | constructor() {
method state (line 10) | async state(state, timers = [], runOnStart = true) {
FILE: src/localApi/message.js
class Message (line 6) | class Message {
method constructor (line 7) | constructor(type) {
method readFlags (line 14) | readFlags(flags) {
method set (line 23) | set(key, value, subkey = false) {
method pack (line 31) | pack(crypto, sequenceNumber, targetParticipantId, sourceParticipantId,...
method unpack (line 62) | unpack(crypto = undefined, data = false) {
FILE: src/localApi/packets.js
class Packets (line 3) | class Packets {
method constructor (line 4) | constructor(type) {
FILE: src/localApi/sgcrypto.js
class SgCrypto (line 8) | class SgCrypto {
method constructor (line 9) | constructor() {
method getPublicKey (line 16) | async getPublicKey(certificate) {
method getKey (line 50) | getKey() {
method getIv (line 54) | getIv() {
method encrypt (line 58) | encrypt(data, key, iv) {
method decrypt (line 73) | decrypt(data, iv, key) {
method sign (line 85) | sign(data) {
method removePadding (line 91) | removePadding(payload) {
FILE: src/localApi/simple.js
class Simple (line 5) | class Simple {
method constructor (line 6) | constructor(type) {
method applyPKCS7Padding (line 14) | static applyPKCS7Padding(structure) {
method set (line 23) | set(key, value, isProtected = false) {
method pack (line 31) | pack(crypto = false) {
method pack1 (line 102) | pack1(type, payload, version, payloadProtectedLength = 0, payloadProte...
method unpack (line 119) | unpack(crypto = undefined, data = false) {
FILE: src/localApi/structure.js
class Structure (line 1) | class Structure {
method constructor (line 2) | constructor(packet) {
method writeSGString (line 8) | writeSGString(data) {
method readSGString (line 28) | readSGString() {
method writeBytes (line 35) | writeBytes(data, type) {
method readBytes (line 41) | readBytes(length = false) {
method writeUInt8 (line 53) | writeUInt8(data) {
method readUInt8 (line 63) | readUInt8() {
method writeUInt16 (line 69) | writeUInt16(data) {
method readUInt16 (line 79) | readUInt16() {
method writeUInt32 (line 85) | writeUInt32(data) {
method readUInt32 (line 95) | readUInt32() {
method writeInt32 (line 101) | writeInt32(data) {
method readInt32 (line 111) | readInt32() {
method writeUInt64 (line 117) | writeUInt64(value) {
method readUInt64 (line 128) | readUInt64() {
method toBuffer (line 135) | toBuffer() {
method add (line 139) | add(data) {
FILE: src/localApi/xboxlocalapi.js
class XboxLocalApi (line 11) | class XboxLocalApi extends EventEmitter {
method constructor (line 12) | constructor(config, tokensFile, devInfoFile, restFulEnabled, mqttEnabl...
method updateState (line 81) | async updateState() {
method getSequenceNumber (line 100) | async getSequenceNumber() {
method sendSocketMessage (line 108) | async sendSocketMessage(message, type, host = this.host) {
method connect (line 127) | async connect() {
FILE: src/mqtt.js
class Mqtt (line 4) | class Mqtt extends EventEmitter {
method constructor (line 5) | constructor(config) {
method publish (line 76) | publish(topic, message) {
FILE: src/restful.js
constant DEFAULT_MESSAGE (line 4) | const DEFAULT_MESSAGE = 'This data is not available at this time.';
class RestFul (line 6) | class RestFul extends EventEmitter {
method constructor (line 7) | constructor(config) {
method connect (line 26) | connect() {
method update (line 80) | update(path, data) {
FILE: src/webApi/authentication.js
class Authentication (line 6) | class Authentication {
method constructor (line 7) | constructor(config) {
method refreshToken (line 20) | async refreshToken(token) {
method getUserToken (line 45) | async getUserToken(accessToken) {
method getXstsToken (line 68) | async getXstsToken(userToken) {
method accessToken (line 90) | async accessToken(webApiToken) {
method refreshTokens (line 117) | async refreshTokens(type) {
method checkAuthorization (line 176) | async checkAuthorization() {
method generateAuthorizationUrl (line 198) | async generateAuthorizationUrl() {
FILE: src/webApi/providers/achievements.js
class Archivements (line 3) | class Archivements {
method constructor (line 4) | constructor(tokens, authorizationHeaders) {
method getTitleAchievements (line 16) | getTitleAchievements(continuationToken = 0) {
method getTitleAchievements360 (line 28) | getTitleAchievements360(continuationToken = 0) {
method getTitleId (line 41) | getTitleId(titleId, continuationToken = 0) {
method getTitleId360 (line 53) | getTitleId360(titleId, continuationToken = 0) {
FILE: src/webApi/providers/catalog.js
class Catalog (line 4) | class Catalog {
method constructor (line 5) | constructor(authorizationHeaders) {
method searchTitle (line 16) | searchTitle(query, marketLocale = 'US', languagesLocale = 'en-US') {
method getProductId (line 37) | getProductId(query, marketLocale = 'US', languagesLocale = 'en-US') {
method getProductFromAlternateId (line 58) | getProductFromAlternateId(titleId, titleType, marketLocale = 'US', lan...
FILE: src/webApi/providers/gameclips.js
class GameClip (line 4) | class GameClip {
method constructor (line 5) | constructor(tokens, authorizationHeaders) {
method getUserGameclips (line 17) | getUserGameclips() {
method getCommunityGameclipsByTitleId (line 29) | getCommunityGameclipsByTitleId(titleId) {
method getGameclipsByXuid (line 41) | getGameclipsByXuid(titleId, skipItems, maxItems) {
FILE: src/webApi/providers/messages.js
class Messages (line 3) | class Messages {
method constructor (line 4) | constructor(tokens, authorizationHeaders) {
method getInbox (line 15) | getInbox() {
method getConversation (line 28) | getConversation() {
FILE: src/webApi/providers/people.js
class People (line 3) | class People {
method constructor (line 4) | constructor(authorizationHeaders) {
method getFriends (line 15) | getFriends() {
method recentPlayers (line 34) | recentPlayers() {
FILE: src/webApi/providers/pins.js
class Pins (line 3) | class Pins {
method constructor (line 4) | constructor(tokens, authorizationHeaders) {
method getPins (line 16) | getPins(list = 'XBLPins') {
method getSaveForLater (line 28) | getSaveForLater() {
FILE: src/webApi/providers/screenshots.js
class Catalog (line 4) | class Catalog {
method constructor (line 5) | constructor(tokens, authorizationHeaders) {
method getUserScreenshots (line 17) | getUserScreenshots() {
method getCommunityScreenshotsByTitleId (line 30) | getCommunityScreenshotsByTitleId(titleId) {
method getScreenshotsByXuid (line 42) | getScreenshotsByXuid(titleId, skipItems, maxItems) {
FILE: src/webApi/providers/social.js
class Social (line 3) | class Social {
method constructor (line 4) | constructor(authorizationHeaders) {
method getFriends (line 14) | getFriends() {
FILE: src/webApi/providers/titlehub.js
class TitleHub (line 3) | class TitleHub {
method constructor (line 4) | constructor(tokens, authorizationHeaders) {
method getTitleHistory (line 15) | getTitleHistory() {
method getTitleId (line 33) | getTitleId(titleId) {
FILE: src/webApi/providers/userpresence.js
class UserPresence (line 3) | class UserPresence {
method constructor (line 4) | constructor(authorizationHeaders) {
method getCurrentUser (line 15) | getCurrentUser() {
FILE: src/webApi/providers/userstats.js
class UserStats (line 3) | class UserStats {
method constructor (line 4) | constructor(tokens, authorizationHeaders) {
method getUserTitleStats (line 16) | getUserTitleStats(titleId) {
FILE: src/webApi/xboxwebapi.js
class XboxWebApi (line 9) | class XboxWebApi extends EventEmitter {
method constructor (line 10) | constructor(config, authTokenFile, inputsFile, restFulEnabled, mqttEna...
method checkAuthorization (line 54) | async checkAuthorization() {
method consolesList (line 107) | async consolesList() {
method consoleStatus (line 160) | async consoleStatus() {
method installedApps (line 201) | async installedApps() {
method mediaState (line 240) | async mediaState(tokens) {
method send (line 277) | async send(commandType, command, payload) {
method next (line 306) | async next() { return this.send('Media', 'Next'); }
method previous (line 307) | async previous() { return this.send('Media', 'Previous'); }
method pause (line 308) | async pause() { return this.send('Media', 'Pause'); }
method play (line 309) | async play() { return this.send('Media', 'Play'); }
method goBack (line 310) | async goBack() { return this.send('Shell', 'GoBack'); }
FILE: src/xboxdevice.js
class XboxDevice (line 11) | class XboxDevice extends EventEmitter {
method constructor (line 12) | constructor(api, device, authTokenFile, devInfoFile, inputsFile, input...
method setOverExternalIntegration (line 84) | async setOverExternalIntegration(integration, key, value) {
method externalIntegrations (line 140) | async externalIntegrations() {
method prepareDataForAccessory (line 208) | async prepareDataForAccessory() {
method startStopImpulseGenerator (line 232) | async startStopImpulseGenerator(state, timers = []) {
method displayOrder (line 245) | async displayOrder() {
method addRemoveOrUpdateInput (line 282) | async addRemoveOrUpdateInput(inputs, remove = false) {
method setInput (line 394) | async setInput(input) {
method prepareAccessory (line 431) | async prepareAccessory() {
method start (line 1063) | async start() {
Condensed preview — 37 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (320K chars).
[
{
"path": ".github/workflows/stale.yml",
"chars": 1060,
"preview": "name: Mark stale issues and pull requests\n\non:\n schedule:\n - cron: '0 0 * * *' # Runs daily at midnight (UTC)\n wor"
},
{
"path": ".gitignore",
"chars": 45,
"preview": "node_modules\nnpm-debug.log\n.DS_Store\n.vscode\n"
},
{
"path": "CHANGELOG.md",
"chars": 19038,
"preview": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Change"
},
{
"path": "CLAUDE.md",
"chars": 4815,
"preview": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## "
},
{
"path": "LICENSE",
"chars": 1064,
"preview": "# MIT License\n\nCopyright (c) 2022 Grzegorz Kaczor\n\nPermission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "README.md",
"chars": 14599,
"preview": "<p align=\"center\">\n <a href=\"https://github.com/grzegorz914/homebridge-xbox-tv\"><img alt=\"Xbox and controller\" src=\"htt"
},
{
"path": "config.schema.json",
"chars": 37621,
"preview": "{\n\t\"pluginAlias\": \"XboxTv\",\n\t\"pluginType\": \"platform\",\n\t\"singular\": true,\n\t\"fixArrays\": true,\n\t\"strictValidation\": true,"
},
{
"path": "homebridge-ui/public/index.html",
"chars": 10311,
"preview": "<div class=\"container mt-3\">\n <div class=\"text-center\">\n <img src=\"homebridge-xbox-tv.png\" alt=\"Image\" height=\"120\" "
},
{
"path": "homebridge-ui/server.js",
"chars": 2439,
"preview": "import { HomebridgePluginUiServer } from '@homebridge/plugin-ui-utils';\nimport Authentication from '../src/webApi/authen"
},
{
"path": "index.js",
"chars": 5696,
"preview": "import { join } from 'path';\nimport { mkdirSync, existsSync, writeFileSync } from 'fs';\nimport XboxDevice from './src/xb"
},
{
"path": "package.json",
"chars": 1473,
"preview": "{\n \"displayName\": \"Xbox TV\",\n \"name\": \"homebridge-xbox-tv\",\n \"version\": \"4.1.17\",\n \"description\": \"Homebridge plugin"
},
{
"path": "sample-config.json",
"chars": 1983,
"preview": "{\n\t\"bridge\": {\n\t\t\"name\": \"Homebridge\",\n\t\t\"username\": \"AA:BB:CC:DD:EE:FF\",\n\t\t\"manufacturer\": \"homebridge.io\",\n\t\t\"model\": "
},
{
"path": "src/constants.js",
"chars": 12890,
"preview": "export const PlatformName = \"XboxTv\";\nexport const PluginName = \"homebridge-xbox-tv\";\n\nexport const DefaultInputs = [\n "
},
{
"path": "src/functions.js",
"chars": 3075,
"preview": "import { promises as fsPromises } from 'fs';\nimport { exec } from 'node:child_process';\nimport { promisify } from 'node:"
},
{
"path": "src/impulsegenerator.js",
"chars": 1060,
"preview": "import EventEmitter from 'events';\n\nclass ImpulseGenerator extends EventEmitter {\n constructor() {\n super();\n "
},
{
"path": "src/localApi/message.js",
"chars": 4344,
"preview": "import HexToBin from 'hex-to-binary';\nimport Packets from './packets.js';\nimport Structure from './structure.js';\nimport"
},
{
"path": "src/localApi/packets.js",
"chars": 11107,
"preview": "import { LocalApi } from '../constants.js';\n\nclass Packets {\n constructor(type) {\n this.type = type;\n\n "
},
{
"path": "src/localApi/sgcrypto.js",
"chars": 3425,
"preview": "import JsRsaSign from 'jsrsasign';\nimport Crypto from 'crypto';\nimport { EOL } from 'os';\nimport Elliptic from 'elliptic"
},
{
"path": "src/localApi/simple.js",
"chars": 7275,
"preview": "import Packets from './packets.js';\nimport Structure from './structure.js';\nimport { LocalApi } from '../constants.js';\n"
},
{
"path": "src/localApi/structure.js",
"chars": 4142,
"preview": "class Structure {\n constructor(packet) {\n this.packet = packet ? packet : Buffer.alloc(0);\n this.totalL"
},
{
"path": "src/localApi/xboxlocalapi.js",
"chars": 28192,
"preview": "import dgram from 'dgram';\nimport { parse as UuIdParse, v4 as UuIdv4 } from 'uuid';\nimport EventEmitter from 'events';\ni"
},
{
"path": "src/mqtt.js",
"chars": 3889,
"preview": "import { connect } from 'mqtt';\nimport EventEmitter from 'events';\n\nclass Mqtt extends EventEmitter {\n constructor(co"
},
{
"path": "src/restful.js",
"chars": 3207,
"preview": "import express, { json } from 'express';\nimport EventEmitter from 'events';\n\nconst DEFAULT_MESSAGE = 'This data is not a"
},
{
"path": "src/webApi/authentication.js",
"chars": 8377,
"preview": "import QueryString from 'querystring';\nimport axios from 'axios';\nimport { WebApi } from '../constants.js';\nimport Funct"
},
{
"path": "src/webApi/providers/achievements.js",
"chars": 2556,
"preview": "import axios from 'axios';\n\nclass Archivements {\n constructor(tokens, authorizationHeaders) {\n this.tokens = t"
},
{
"path": "src/webApi/providers/catalog.js",
"chars": 2939,
"preview": "import QueryString from 'querystring';\nimport axios from 'axios';\n\nclass Catalog {\n constructor(authorizationHeaders)"
},
{
"path": "src/webApi/providers/gameclips.js",
"chars": 2058,
"preview": "import QueryString from 'querystring';\nimport axios from 'axios';\n\nclass GameClip {\n constructor(tokens, authorizatio"
},
{
"path": "src/webApi/providers/messages.js",
"chars": 1209,
"preview": "import axios from 'axios';\n\nclass Messages {\n constructor(tokens, authorizationHeaders) {\n this.tokens = token"
},
{
"path": "src/webApi/providers/people.js",
"chars": 1357,
"preview": "import axios from 'axios';\n\nclass People {\n constructor(authorizationHeaders) {\n const headers = authorization"
},
{
"path": "src/webApi/providers/pins.js",
"chars": 1281,
"preview": "import axios from 'axios';\n\nclass Pins {\n constructor(tokens, authorizationHeaders) {\n this.tokens = tokens;\n "
},
{
"path": "src/webApi/providers/screenshots.js",
"chars": 2093,
"preview": "import QueryString from 'querystring';\nimport axios from 'axios';\n\nclass Catalog {\n constructor(tokens, authorization"
},
{
"path": "src/webApi/providers/social.js",
"chars": 673,
"preview": "import axios from 'axios';\n\nclass Social {\n constructor(authorizationHeaders) {\n const headers = authorization"
},
{
"path": "src/webApi/providers/titlehub.js",
"chars": 1645,
"preview": "import axios from 'axios';\n\nclass TitleHub {\n constructor(tokens, authorizationHeaders) {\n this.tokens = token"
},
{
"path": "src/webApi/providers/userpresence.js",
"chars": 746,
"preview": "import axios from 'axios';\n\nclass UserPresence {\n constructor(authorizationHeaders) {\n const headers = authori"
},
{
"path": "src/webApi/providers/userstats.js",
"chars": 1033,
"preview": "import axios from 'axios';\n\nclass UserStats {\n constructor(tokens, authorizationHeaders) {\n this.tokens = toke"
},
{
"path": "src/webApi/xboxwebapi.js",
"chars": 12717,
"preview": "import EventEmitter from 'events';\nimport { v4 as UuIdv4 } from 'uuid';\nimport axios from 'axios';\nimport Authentication"
},
{
"path": "src/xboxdevice.js",
"chars": 68470,
"preview": "import EventEmitter from 'events';\nimport RestFul from './restful.js';\nimport Mqtt from './mqtt.js';\nimport XboxWebApi f"
}
]
About this extraction
This page contains the full source code of the grzegorz914/homebridge-xbox-tv GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 37 files (283.1 KB), approximately 63.4k tokens, and a symbol index with 154 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.