sofapython 0.0.1rc1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. sofapython-0.0.1rc1/.gitignore +28 -0
  2. sofapython-0.0.1rc1/LICENSE +21 -0
  3. sofapython-0.0.1rc1/PKG-INFO +162 -0
  4. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/__init__.py +136 -0
  5. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/ack.py +79 -0
  6. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/aio.py +552 -0
  7. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/backup_export.py +507 -0
  8. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/blob_decoders.py +806 -0
  9. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/cli.py +447 -0
  10. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/commands.py +1273 -0
  11. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/deframer.py +73 -0
  12. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/device_create.py +1174 -0
  13. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/devices.py +534 -0
  14. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/discovery.py +315 -0
  15. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/frame_handlers.py +131 -0
  16. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/hub_listener.py +242 -0
  17. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/hub_logging.py +152 -0
  18. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/hub_versions.py +112 -0
  19. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/inputs.py +501 -0
  20. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/macros.py +669 -0
  21. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/notify_demuxer.py +434 -0
  22. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/opcode_handlers.py +1655 -0
  23. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/protocol_const.py +633 -0
  24. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/proxy_ack_waiters.py +660 -0
  25. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/proxy_activity_ops.py +943 -0
  26. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/proxy_backup.py +504 -0
  27. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/proxy_backup_export.py +486 -0
  28. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/proxy_catalog.py +915 -0
  29. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/proxy_frame_decode.py +227 -0
  30. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/proxy_ir_blob.py +676 -0
  31. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/proxy_restore.py +2004 -0
  32. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/proxy_wifi_device.py +1101 -0
  33. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/state_helpers.py +713 -0
  34. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/transport_bridge.py +876 -0
  35. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/version.py +4 -0
  36. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/wire_schema.py +164 -0
  37. sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/x1_proxy.py +1833 -0
  38. sofapython-0.0.1rc1/pyproject.toml +60 -0
  39. sofapython-0.0.1rc1/sofapython/README.md +137 -0
  40. sofapython-0.0.1rc1/sofapython/examples/backup.py +55 -0
  41. sofapython-0.0.1rc1/sofapython/examples/catalog_details.py +73 -0
  42. sofapython-0.0.1rc1/sofapython/examples/discover.py +37 -0
  43. sofapython-0.0.1rc1/sofapython/examples/minimal_proxy.py +62 -0
  44. sofapython-0.0.1rc1/sofapython/examples/watch.py +60 -0
  45. sofapython-0.0.1rc1/sofapython/examples/wifi_http_listener.py +95 -0
@@ -0,0 +1,28 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.bak
5
+ *.log
6
+ *.zip
7
+ *.patch
8
+ .vscode/
9
+ .idea/
10
+ .mypy_cache/
11
+ .pytest_cache/
12
+ .pytest_tmp/
13
+ .pytest_tmp*/
14
+ pytest-cache-files-*/
15
+ .codex-tmp/
16
+ .venv/
17
+ .venv-py313/
18
+ .venv-py313-smoke/
19
+ pytest.ini
20
+ .python-version
21
+ .claude/
22
+ node_modules/
23
+ playwright-report/
24
+ test-results/
25
+ artifacts/
26
+ docs/internal/
27
+ pytest.cmd
28
+ dist/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 m3tac0de
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,162 @@
1
+ Metadata-Version: 2.4
2
+ Name: sofapython
3
+ Version: 0.0.1rc1
4
+ Summary: Unofficial protocol library and man-in-the-middle proxy for Sofabaton X1 / X1S / X2 universal remote hubs
5
+ Project-URL: Homepage, https://github.com/m3tac0de/home-assistant-sofabaton-x1s
6
+ Project-URL: Issues, https://github.com/m3tac0de/home-assistant-sofabaton-x1s/issues
7
+ Project-URL: Documentation, https://github.com/m3tac0de/home-assistant-sofabaton-x1s/tree/main/docs
8
+ Author: m3tac0de
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: home-automation,mdns,proxy,sofabaton,universal-remote,x1,x1s,x2
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Home Automation
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: zeroconf>=0.131
24
+ Description-Content-Type: text/markdown
25
+
26
+ # sofapython
27
+
28
+ Unofficial Python library for **Sofabaton X1 / X1S / X2** universal remote
29
+ hubs: a reverse-engineered protocol implementation and a man-in-the-middle
30
+ **proxy** that sits between the hub and the official mobile app.
31
+
32
+ This is the protocol engine extracted from the
33
+ [Home Assistant Sofabaton X1S integration](https://github.com/m3tac0de/home-assistant-sofabaton-x1s);
34
+ the integration is its reference consumer.
35
+
36
+ > **Disclaimer:** this project is not affiliated with or endorsed by
37
+ > Sofabaton. The protocol was reverse-engineered from network captures;
38
+ > behavior may break with future hub firmware.
39
+
40
+ ## What it does
41
+
42
+ - **Proxy** a physical hub: the library advertises itself via mDNS exactly
43
+ like a real hub, the official app connects to it, and every frame is
44
+ relayed, decoded and observable. The hub keeps working with the app while
45
+ your application gets full visibility and control.
46
+ - **Catalogs**: read activities, devices, buttons, commands, macros and
47
+ favorites from the hub's wire protocol.
48
+ - **Control**: send button/command presses, switch activities, trigger
49
+ find-my-remote.
50
+ - **Provisioning** (protocol side): create/update/delete devices and
51
+ activities, including virtual WiFi/IP devices.
52
+ - **Backup / restore**: export and restore hub configuration.
53
+ - **Events**: subscribe to hub/app connection state, activity changes,
54
+ OTA progress and catalog updates.
55
+
56
+ Deliberately **out of scope**: executing the HTTP callbacks that virtual
57
+ WiFi/IP devices define (e.g. a Roku-style ECP listener). The library carries
58
+ the protocol artifacts for those features so applications can build them on
59
+ top — the Home Assistant integration does exactly that.
60
+
61
+ ## Install
62
+
63
+ ```
64
+ pip install sofapython
65
+ ```
66
+
67
+ Python 3.11+. The only dependency is
68
+ [python-zeroconf](https://pypi.org/project/zeroconf/) (mDNS advertising and
69
+ hub discovery).
70
+
71
+ ## Quickstart
72
+
73
+ Find a hub, then proxy it. Blocking work runs in the event loop's
74
+ executor and callbacks (plain functions or coroutines) are delivered on
75
+ the loop, so application code never touches the engine threads:
76
+
77
+ ```python
78
+ import asyncio
79
+ from sofapython import AsyncX1Proxy, async_discover_hubs
80
+
81
+ async def main():
82
+ hubs = await async_discover_hubs(timeout=5.0) # physical hubs; proxies filtered
83
+ hub = hubs[0]
84
+
85
+ proxy = AsyncX1Proxy(
86
+ real_hub_ip=hub.host,
87
+ mdns_instance=hub.name,
88
+ mdns_txt=hub.txt, # carries HVER -> X1/X1S/X2 classification
89
+ hub_version=hub.hub_version,
90
+ )
91
+ proxy.on_activity_change(lambda new, old, name: print(f"activity -> {name}"))
92
+
93
+ async with proxy:
94
+ await proxy.wait_until_controllable() # own the hub (see below)
95
+ activities = await proxy.activities() # {id: {name, active, ...}}
96
+ for dev_id in await proxy.devices():
97
+ print(dev_id, await proxy.commands(dev_id)) # {code: label}
98
+
99
+ await proxy.start_activity(next(iter(activities)))
100
+
101
+ asyncio.run(main())
102
+ ```
103
+
104
+ The reads return data directly — a cached result comes back immediately,
105
+ otherwise the call fetches from the hub and awaits completion. The whole
106
+ app-builder surface is a handful of coroutines: `activities()`,
107
+ `devices()`, `commands(dev)`, `buttons(ent)`, `macros(act)`,
108
+ `favorites(act)` to read; `press(ent, button)`, `start_activity(act)`,
109
+ `stop_activity(act)`, `find_remote()` to control.
110
+
111
+ ### Two modes
112
+
113
+ The proxy sits transparently between the hub and the official app, which
114
+ gives it two distinct modes:
115
+
116
+ - **Observe** — the app is connected through the proxy. You watch
117
+ activity changes, connects and OTA events in real time, but the app
118
+ owns the hub, so you can't issue commands. Gate on
119
+ `await proxy.wait_connected()`.
120
+ - **Control** — no app attached; the proxy owns the hub, so reads fetch
121
+ fresh and commands/backup work. Gate on
122
+ `await proxy.wait_until_controllable()`.
123
+
124
+ `start()` only spawns the transport; the connect handshake happens
125
+ afterwards, so await the matching readiness primitive before reading or
126
+ acting (otherwise a read raises with the reason — hub not connected, or
127
+ an app holds it).
128
+
129
+ A synchronous core (`X1Proxy`, `discover_hubs`) is also available for
130
+ scripts and REPL use; the async class is a facade over it (its raw
131
+ `get_*` snapshot getters are reachable via `proxy.sync`).
132
+
133
+ A CLI ships as a console script:
134
+
135
+ ```
136
+ sofapython discover # scan the LAN for hubs
137
+ sofapython run --hub 192.168.1.50 # proxy + interactive shell
138
+ x1> status
139
+ x1> activities
140
+ x1> send 101 POWER_ON
141
+ ```
142
+
143
+ Runnable examples — discovery, watching a live session (observe mode),
144
+ taking control of a hub, reading per-entity detail
145
+ (commands/macros/favorites), schema-versioned backup/restore, and
146
+ building an HTTP callback listener on top of the library — live in
147
+ [`sofapython/examples/`](https://github.com/m3tac0de/home-assistant-sofabaton-x1s/tree/main/sofapython/examples).
148
+
149
+ ## Stability
150
+
151
+ Names importable from the package root — `from sofapython import ...`,
152
+ the set listed in `sofapython.__all__` — are the supported API and follow
153
+ semver. Everything else (`sofapython.opcode_handlers`, frame parsing,
154
+ wire schemas, the `proxy_*` mixin modules) is internal and may change
155
+ between minor releases. The library raises stdlib exceptions
156
+ (`ValueError` for malformed/unclassifiable input, `RuntimeError` /
157
+ `TimeoutError` for transport and ack failures); there are no custom
158
+ exception types. Until 1.0, pin a minor version.
159
+
160
+ ## License
161
+
162
+ MIT — see [LICENSE](https://github.com/m3tac0de/home-assistant-sofabaton-x1s/blob/main/LICENSE).
@@ -0,0 +1,136 @@
1
+ """sofapython — unofficial Sofabaton X1/X1S/X2 protocol library and proxy.
2
+
3
+ This module is the curated public API: every name exported here (see
4
+ ``__all__``) is semver-stable for the ``sofapython`` distribution.
5
+ Submodule internals (``opcode_handlers``, frame parsing, wire schemas,
6
+ the ``proxy_*`` mixins, ...) remain importable but are NOT a stable
7
+ surface and may change between minor releases.
8
+
9
+ The library raises stdlib exceptions (``ValueError`` for unclassifiable
10
+ or malformed input, ``RuntimeError``/``TimeoutError`` for transport and
11
+ ack failures) rather than custom exception types.
12
+
13
+ In-tree, this package doubles as ``custom_components.sofabaton_x1s.lib``
14
+ for the Home Assistant integration; the wheel build remaps it to the
15
+ top-level ``sofapython`` package (see pyproject.toml).
16
+ """
17
+
18
+ from .version import __version__ # noqa: F401
19
+
20
+ # Protocol constants (ButtonName, BUTTONNAME_BY_CODE, DEVICE_CLASS_*,
21
+ # opcode helpers, ...). Star re-export predates the curated API and is
22
+ # kept for backward compatibility; protocol_const defines __all__.
23
+ from . import protocol_const as _protocol_const
24
+ from .protocol_const import * # noqa: F401,F403
25
+
26
+ # Hub-variant classification and shared defaults.
27
+ from .hub_versions import ( # noqa: F401
28
+ ACTIVITY_BACKUP_SCHEMA_VERSION,
29
+ DEFAULT_HUB_LISTEN_BASE,
30
+ DEFAULT_PROXY_UDP_PORT,
31
+ DEVICE_BACKUP_SCHEMA_VERSION,
32
+ HUB_BUNDLE_SCHEMA_VERSION,
33
+ HUB_VERSION_BY_HVER,
34
+ HUB_VERSION_X1,
35
+ HUB_VERSION_X1S,
36
+ HUB_VERSION_X2,
37
+ HVER_BY_HUB_VERSION,
38
+ HVER_X1,
39
+ HVER_X1S,
40
+ HVER_X2,
41
+ MDNS_SERVICE_TYPE_BY_VERSION,
42
+ MDNS_SERVICE_TYPE_X1,
43
+ MDNS_SERVICE_TYPE_X2,
44
+ MDNS_SERVICE_TYPES,
45
+ PROXY_TXT_KEY,
46
+ PROXY_TXT_VALUE,
47
+ classify_hub_version,
48
+ is_proxy_advertisement,
49
+ mdns_service_type_for_props,
50
+ )
51
+
52
+ # Per-hub logging helper. LogTag/HubLogger remain importable from
53
+ # sofapython.hub_logging but are internal plumbing, not public API.
54
+ from .hub_logging import get_hub_logger # noqa: F401
55
+
56
+ # The proxy engine.
57
+ from .x1_proxy import X1Proxy # noqa: F401
58
+
59
+ # LAN discovery of physical hubs.
60
+ from .discovery import ( # noqa: F401
61
+ DEFAULT_DISCOVERY_TIMEOUT,
62
+ DiscoveredHub,
63
+ HubBrowser,
64
+ decode_txt_properties,
65
+ discover_hubs,
66
+ normalize_advertisement,
67
+ )
68
+
69
+ # Device catalog records and provisioning flows.
70
+ from .devices import DeviceConfig, device_config_from_backup, parse_device_record # noqa: F401
71
+ from .device_create import ( # noqa: F401
72
+ DeviceCreateRequest,
73
+ DeviceCreateResult,
74
+ run_device_create,
75
+ )
76
+
77
+ # Step/ack result types surfaced by proxy operations.
78
+ from .ack import AckOutcome, InputsBurstResult, SendStepResult # noqa: F401
79
+
80
+ # Asyncio facade over the threaded core.
81
+ from .aio import AsyncHubBrowser, AsyncX1Proxy, async_discover_hubs # noqa: F401
82
+
83
+ _CURATED = [
84
+ "__version__",
85
+ # hub_versions
86
+ "ACTIVITY_BACKUP_SCHEMA_VERSION",
87
+ "DEFAULT_HUB_LISTEN_BASE",
88
+ "DEFAULT_PROXY_UDP_PORT",
89
+ "DEVICE_BACKUP_SCHEMA_VERSION",
90
+ "HUB_BUNDLE_SCHEMA_VERSION",
91
+ "HUB_VERSION_BY_HVER",
92
+ "HUB_VERSION_X1",
93
+ "HUB_VERSION_X1S",
94
+ "HUB_VERSION_X2",
95
+ "HVER_BY_HUB_VERSION",
96
+ "HVER_X1",
97
+ "HVER_X1S",
98
+ "HVER_X2",
99
+ "MDNS_SERVICE_TYPE_BY_VERSION",
100
+ "MDNS_SERVICE_TYPE_X1",
101
+ "MDNS_SERVICE_TYPE_X2",
102
+ "MDNS_SERVICE_TYPES",
103
+ "PROXY_TXT_KEY",
104
+ "PROXY_TXT_VALUE",
105
+ "classify_hub_version",
106
+ "is_proxy_advertisement",
107
+ "mdns_service_type_for_props",
108
+ # hub_logging
109
+ "get_hub_logger",
110
+ # proxy
111
+ "X1Proxy",
112
+ # discovery
113
+ "DEFAULT_DISCOVERY_TIMEOUT",
114
+ "DiscoveredHub",
115
+ "HubBrowser",
116
+ "decode_txt_properties",
117
+ "discover_hubs",
118
+ "normalize_advertisement",
119
+ # devices / provisioning
120
+ "DeviceConfig",
121
+ "device_config_from_backup",
122
+ "parse_device_record",
123
+ "DeviceCreateRequest",
124
+ "DeviceCreateResult",
125
+ "run_device_create",
126
+ # ack results
127
+ "AckOutcome",
128
+ "InputsBurstResult",
129
+ "SendStepResult",
130
+ # asyncio facade
131
+ "AsyncHubBrowser",
132
+ "AsyncX1Proxy",
133
+ "async_discover_hubs",
134
+ ]
135
+
136
+ __all__ = list(getattr(_protocol_const, "__all__", [])) + _CURATED
@@ -0,0 +1,79 @@
1
+ """Typed outcomes for hub ack waits.
2
+
3
+ Every ``wait_for_*`` site distinguishes three states:
4
+
5
+ * ``acked`` -- the hub answered, and the answer was a success ack.
6
+ * ``rejected`` -- the hub answered explicitly, but the answer was a
7
+ rejection (e.g. ``STATUS_ACK`` carrying a non-zero status byte).
8
+ * ``timeout`` -- the hub did not answer within the wait window.
9
+
10
+ Conflating the latter two leads to fail-slow behaviour during multi-step
11
+ sequences: the orchestration spins out the full per-step timeout rather
12
+ than aborting at the first hub-side refusal. The dataclasses below give
13
+ callers a uniform way to branch.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from dataclasses import dataclass
19
+ from enum import Enum
20
+
21
+
22
+ class AckOutcome(Enum):
23
+ """Three-way classification of an ack wait."""
24
+
25
+ acked = "acked"
26
+ rejected = "rejected"
27
+ timeout = "timeout"
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class SendStepResult:
32
+ """Outcome of a single ``_send_step`` exchange."""
33
+
34
+ outcome: AckOutcome
35
+ ack_opcode: int | None = None
36
+ ack_payload: bytes | None = None
37
+
38
+ @property
39
+ def ok(self) -> bool:
40
+ return self.outcome is AckOutcome.acked
41
+
42
+ @property
43
+ def rejected(self) -> bool:
44
+ return self.outcome is AckOutcome.rejected
45
+
46
+ @property
47
+ def timed_out(self) -> bool:
48
+ return self.outcome is AckOutcome.timeout
49
+
50
+
51
+ @dataclass(frozen=True, slots=True)
52
+ class InputsBurstResult:
53
+ """Outcome of :meth:`X1Proxy.wait_for_activity_inputs_burst`.
54
+
55
+ ``payloads`` is populated on :attr:`AckOutcome.acked` and is empty
56
+ on rejection or timeout.
57
+ """
58
+
59
+ outcome: AckOutcome
60
+ payloads: tuple[bytes, ...] = ()
61
+
62
+ @property
63
+ def ok(self) -> bool:
64
+ return self.outcome is AckOutcome.acked
65
+
66
+ @property
67
+ def rejected(self) -> bool:
68
+ return self.outcome is AckOutcome.rejected
69
+
70
+ @property
71
+ def timed_out(self) -> bool:
72
+ return self.outcome is AckOutcome.timeout
73
+
74
+
75
+ __all__ = [
76
+ "AckOutcome",
77
+ "InputsBurstResult",
78
+ "SendStepResult",
79
+ ]