Skip to main content

Unofficial protocol library and man-in-the-middle proxy for Sofabaton X1 / X1S / X2 universal remote hubs

Project description

sofabaton-x

Unofficial Python library for Sofabaton X1 / X1S / X2 universal remote hubs: a reverse-engineered protocol implementation and a man-in-the-middle proxy that sits between the hub and the official mobile app.

This is the protocol engine extracted from the Home Assistant Sofabaton X1S integration; the integration is its reference consumer.

Disclaimer: this project is not affiliated with or endorsed by Sofabaton. The protocol was reverse-engineered from network captures; behavior may break with future hub firmware.

What it does

  • Proxy a physical hub: the library advertises itself via mDNS exactly like a real hub, the official app connects to it, and every frame is relayed, decoded and observable. The hub keeps working with the app while your application gets full visibility and control.
  • Catalogs: read activities, devices, buttons, commands, macros and favorites from the hub's wire protocol.
  • Control: send button/command presses, switch activities, trigger find-my-remote.
  • Provisioning (protocol side): create/update/delete devices and activities, including virtual WiFi/IP devices.
  • Backup / restore: export and restore hub configuration.
  • Events: subscribe to hub/app connection state, activity changes, OTA progress and catalog updates.

Deliberately out of scope: executing the HTTP callbacks that virtual WiFi/IP devices define (e.g. a Roku-style ECP listener). The library carries the protocol artifacts for those features so applications can build them on top — the Home Assistant integration does exactly that.

Install

pip install sofabaton-x

Python 3.11+. The only dependency is python-zeroconf (mDNS advertising and hub discovery).

Quickstart

Find a hub, then proxy it. Blocking work runs in the event loop's executor and callbacks (plain functions or coroutines) are delivered on the loop, so application code never touches the engine threads:

import asyncio
from sofabaton import AsyncX1Proxy, async_discover_hubs

async def main():
    hubs = await async_discover_hubs(timeout=5.0)   # physical hubs; proxies filtered
    hub = hubs[0]

    proxy = AsyncX1Proxy(
        hub_ip=hub.host,                # the physical hub's IP
        mdns_instance=hub.name,
        mdns_txt=hub.txt,               # carries HVER -> X1/X1S/X2 classification
        hub_version=hub.hub_version,    # optional; the connect banner confirms it
    )
    proxy.on_activity_change(lambda new, old, name: print(f"activity -> {name}"))

    async with proxy:
        await proxy.wait_until_controllable()      # own the hub (see below)
        activities = await proxy.activities()      # {id: {name, active, ...}}
        for dev_id in await proxy.devices():
            for cmd in await proxy.commands(dev_id):   # [{command_id, label}]
                await proxy.send(dev_id, cmd["command_id"])   # fire a command

        await proxy.start_activity(next(iter(activities)))

asyncio.run(main())

Ports

The proxy has two network faces. Apart from hub_ip, every port defaults to the right value — you usually only touch hub_listen_port to avoid a local collision:

Argument Default Side What it is
hub_ip hub the physical hub's IPv4 address
hub_port 8102 hub UDP port on the hub we send CALL_ME to (protocol-fixed)
hub_listen_port 8200 hub TCP port on this host the hub connects back to
app_discovery_port 8102 app UDP port on this host the app finds + calls us on (keep 8102 for iOS)

The hub model (X1/X1S/X2) is confirmed from the connect banner, so hub_version is only a pre-connect hint. See docs/networking.md for the complete port map and firewall guidance.

Everything is keyed on (entity_id, command_id) — you browse to get those ids, then send(entity_id, command_id). The reads return them directly (cached if available, else fetched):

read returns
activities() / devices() {id: {name, ...}}
commands(device_id) [{command_id, label}]
macros(activity_id) [{command_id, label}]
favorites(activity_id) [{device_id, command_id, label}]
buttons(entity_id) [{button_code, name, device_id, command_id}]

Control: send(entity_id, command_id) (alias press), start_activity(act), stop_activity(act), find_remote().

Two modes

The proxy sits transparently between the hub and the official app, which gives it two distinct modes:

  • Observe — the app is connected through the proxy. You watch activity changes, connects and OTA events in real time, but the app owns the hub, so you can't issue commands. Gate on await proxy.wait_connected().
  • Control — no app attached; the proxy owns the hub, so reads fetch fresh and commands/backup work. Gate on await proxy.wait_until_controllable().

start() only spawns the transport; the connect handshake happens afterwards, so await the matching readiness primitive before reading or acting (otherwise a read raises with the reason — hub not connected, or an app holds it).

A synchronous core (X1Proxy, discover_hubs) is also available for scripts and REPL use; the async class is a facade over it (its raw get_* snapshot getters are reachable via proxy.sync).

A CLI ships as a console script:

sofabaton discover                   # scan the LAN for hubs
sofabaton run --hub-ip 192.168.1.50  # proxy + interactive shell
x1> status
x1> activities
x1> send 101 POWER_ON

Runnable examples — discovery, watching a live session (observe mode), taking control of a hub, reading per-entity detail (commands/macros/favorites), schema-versioned backup/restore, and building an HTTP callback listener on top of the library — live in sofabaton-x/examples/.

Protocol & networking docs

This library is a reverse-engineered implementation; the wire protocol and network topology are documented in the repository:

Stability

Names importable from the package root — from sofabaton import ..., the set listed in sofabaton.__all__ — are the supported API and follow semver. Everything else (sofabaton.opcode_handlers, frame parsing, wire schemas, the proxy_* mixin modules) is internal and may change between minor releases. The library raises stdlib exceptions (ValueError for malformed/unclassifiable input, RuntimeError / TimeoutError for transport and ack failures); there are no custom exception types. Until 1.0, pin a minor version.

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sofabaton_x-0.0.1rc5.tar.gz (203.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sofabaton_x-0.0.1rc5-py3-none-any.whl (219.9 kB view details)

Uploaded Python 3

File details

Details for the file sofabaton_x-0.0.1rc5.tar.gz.

File metadata

  • Download URL: sofabaton_x-0.0.1rc5.tar.gz
  • Upload date:
  • Size: 203.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for sofabaton_x-0.0.1rc5.tar.gz
Algorithm Hash digest
SHA256 8eae21fb3e8dc33337cbe45e1e79d945881af85c3c47c7eea21a78511b53c736
MD5 8a91a88e11a40ad2392bd492fd0120c0
BLAKE2b-256 4607405d18fbd61ed2ce03fec89f4191a4b85c18d21b0497caa7ad6e88ff60f2

See more details on using hashes here.

Provenance

The following attestation bundles were made for sofabaton_x-0.0.1rc5.tar.gz:

Publisher: sofabaton-x-release.yml on m3tac0de/home-assistant-sofabaton-x1s

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file sofabaton_x-0.0.1rc5-py3-none-any.whl.

File metadata

  • Download URL: sofabaton_x-0.0.1rc5-py3-none-any.whl
  • Upload date:
  • Size: 219.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for sofabaton_x-0.0.1rc5-py3-none-any.whl
Algorithm Hash digest
SHA256 e4c2378494daf14fbf09e4a0d30ea8e4850ead491e10f03c19f85ae6227d5a4a
MD5 656ed5ff3dc7a8b9f25f9c7608a48dd2
BLAKE2b-256 d1c8834e7215ed797ad9781f175823fec5edb5a96e9102f3dbf0c3265e6a934a

See more details on using hashes here.

Provenance

The following attestation bundles were made for sofabaton_x-0.0.1rc5-py3-none-any.whl:

Publisher: sofabaton-x-release.yml on m3tac0de/home-assistant-sofabaton-x1s

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page