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.
- sofapython-0.0.1rc1/.gitignore +28 -0
- sofapython-0.0.1rc1/LICENSE +21 -0
- sofapython-0.0.1rc1/PKG-INFO +162 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/__init__.py +136 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/ack.py +79 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/aio.py +552 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/backup_export.py +507 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/blob_decoders.py +806 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/cli.py +447 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/commands.py +1273 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/deframer.py +73 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/device_create.py +1174 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/devices.py +534 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/discovery.py +315 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/frame_handlers.py +131 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/hub_listener.py +242 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/hub_logging.py +152 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/hub_versions.py +112 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/inputs.py +501 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/macros.py +669 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/notify_demuxer.py +434 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/opcode_handlers.py +1655 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/protocol_const.py +633 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/proxy_ack_waiters.py +660 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/proxy_activity_ops.py +943 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/proxy_backup.py +504 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/proxy_backup_export.py +486 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/proxy_catalog.py +915 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/proxy_frame_decode.py +227 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/proxy_ir_blob.py +676 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/proxy_restore.py +2004 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/proxy_wifi_device.py +1101 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/state_helpers.py +713 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/transport_bridge.py +876 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/version.py +4 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/wire_schema.py +164 -0
- sofapython-0.0.1rc1/custom_components/sofabaton_x1s/lib/x1_proxy.py +1833 -0
- sofapython-0.0.1rc1/pyproject.toml +60 -0
- sofapython-0.0.1rc1/sofapython/README.md +137 -0
- sofapython-0.0.1rc1/sofapython/examples/backup.py +55 -0
- sofapython-0.0.1rc1/sofapython/examples/catalog_details.py +73 -0
- sofapython-0.0.1rc1/sofapython/examples/discover.py +37 -0
- sofapython-0.0.1rc1/sofapython/examples/minimal_proxy.py +62 -0
- sofapython-0.0.1rc1/sofapython/examples/watch.py +60 -0
- 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
|
+
]
|