sofapython 0.0.1rc1__py3-none-any.whl

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/version.py ADDED
@@ -0,0 +1,4 @@
1
+ # version.py — single source of truth for the standalone library
2
+ # ("sofapython" on PyPI). Versioned independently of the Home Assistant
3
+ # integration's manifest.json.
4
+ __version__ = "0.0.1-rc1"
@@ -0,0 +1,164 @@
1
+ """Per-variant wire-format schema for the three hub firmware lines.
2
+
3
+ The hub speaks the same opcode set across the X1, X1S and X2 product
4
+ lines, but a handful of record-level shape choices differ between
5
+ "narrow" hubs (X1: ASCII labels, 30-byte name slots) and "wide" hubs
6
+ (X1S/X2: UTF-16BE labels, 60-byte name slots, larger input-entry
7
+ stride). This module is the *single* place where those per-variant
8
+ numeric choices are declared.
9
+
10
+ Every builder and parser in the integration that needs a variant
11
+ decision reads it from :func:`schema_for`. Callers never sniff payload
12
+ bytes to *decide* which variant they are talking to; the hub version
13
+ is established once during connect (mDNS classification, re-confirmed
14
+ by the connect banner) and stored on the proxy. Shape sniffing is
15
+ allowed only to *validate* an incoming payload against the expected
16
+ variant -- mismatches log a warning and proceed.
17
+
18
+ If a previously-unknown firmware lineage appears, :func:`schema_for`
19
+ raises a ``ValueError`` rather than silently picking a default. The
20
+ intent is to fail loudly at the boundary so a future variant cannot
21
+ quietly inherit the X1 layout and corrupt writes for the user.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from dataclasses import dataclass
27
+ from enum import Enum
28
+ from typing import Final, Mapping
29
+
30
+ from .hub_versions import HUB_VERSION_X1, HUB_VERSION_X1S, HUB_VERSION_X2
31
+
32
+
33
+ class InputEntryLayout(Enum):
34
+ """Per-entry layout used inside a family-0x46 inputs page.
35
+
36
+ ``NARROW_ASCII`` is the 27-byte stride seen on X1 hubs: a 1-byte
37
+ key id followed by a 6-byte fid and a 20-byte ASCII label slot.
38
+
39
+ ``WIDE_UTF16BE`` is the 48-byte stride seen on X1S/X2 hubs: a
40
+ 1-byte key id, a 6-byte fid, a 1-byte ordinal and a 40-byte
41
+ UTF-16BE label slot.
42
+ """
43
+
44
+ NARROW_ASCII = "narrow_ascii"
45
+ WIDE_UTF16BE = "wide_utf16be"
46
+
47
+
48
+ class InputsTrailingLayout(Enum):
49
+ """Shape of the trailing region following the entry list in a
50
+ family-0x46 inputs page. Phase 3 fleshes out the canonical layout;
51
+ Phase 1 simply needs a stable enum tag per variant so call sites
52
+ stop branching on raw ``hub_version`` strings.
53
+ """
54
+
55
+ CONTROL_KEYS_PLUS_FAVORITES = "control_keys_plus_favorites"
56
+
57
+
58
+ @dataclass(slots=True, frozen=True)
59
+ class WireSchema:
60
+ """All per-variant wire choices for one hub firmware line."""
61
+
62
+ #: Width in bytes of the name / brand / tail slots inside a family-0x07
63
+ #: device record body. 30 on X1, 60 on X1S/X2.
64
+ device_slot_width: int
65
+
66
+ #: Total length of a device-record body, including the trailing checksum.
67
+ device_body_len: int
68
+
69
+ #: Codec used for the name and brand slots. ``"ascii"`` on X1,
70
+ #: ``"utf-16-be"`` on X1S/X2.
71
+ device_label_encoding: str
72
+
73
+ #: Per-record stride in an assembled family-0x0E command-record body.
74
+ command_stride: int
75
+
76
+ #: Length of the trailing label slot inside one command record.
77
+ command_label_slot_len: int
78
+
79
+ #: Codec used for command labels.
80
+ command_label_encoding: str
81
+
82
+ #: Length of the trailing label slot inside a family-0x12 macro
83
+ #: region (X1 ASCII / X1S/X2 UTF-16BE).
84
+ macro_label_slot_len: int
85
+
86
+ #: Codec used for macro labels.
87
+ macro_label_encoding: str
88
+
89
+ #: Per-entry stride inside the family-0x46 inputs entry region.
90
+ input_entry_stride: int
91
+
92
+ #: Tag describing per-entry field layout. See :class:`InputEntryLayout`.
93
+ input_entry_layout: InputEntryLayout
94
+
95
+ #: Tag describing the shape of the trailing region (control keys,
96
+ #: favorite slots, state byte) following the entries.
97
+ inputs_trailing_layout: InputsTrailingLayout
98
+
99
+
100
+ _X1_SCHEMA: Final[WireSchema] = WireSchema(
101
+ device_slot_width=30,
102
+ device_body_len=120,
103
+ device_label_encoding="ascii",
104
+ command_stride=40,
105
+ command_label_slot_len=30,
106
+ command_label_encoding="ascii",
107
+ macro_label_slot_len=30,
108
+ macro_label_encoding="ascii",
109
+ input_entry_stride=27,
110
+ input_entry_layout=InputEntryLayout.NARROW_ASCII,
111
+ inputs_trailing_layout=InputsTrailingLayout.CONTROL_KEYS_PLUS_FAVORITES,
112
+ )
113
+
114
+
115
+ _X1S_X2_SCHEMA: Final[WireSchema] = WireSchema(
116
+ device_slot_width=60,
117
+ device_body_len=210,
118
+ device_label_encoding="utf-16-be",
119
+ command_stride=70,
120
+ command_label_slot_len=60,
121
+ command_label_encoding="utf-16-be",
122
+ macro_label_slot_len=60,
123
+ macro_label_encoding="utf-16-be",
124
+ input_entry_stride=48,
125
+ input_entry_layout=InputEntryLayout.WIDE_UTF16BE,
126
+ inputs_trailing_layout=InputsTrailingLayout.CONTROL_KEYS_PLUS_FAVORITES,
127
+ )
128
+
129
+
130
+ SCHEMAS: Final[Mapping[str, WireSchema]] = {
131
+ HUB_VERSION_X1: _X1_SCHEMA,
132
+ HUB_VERSION_X1S: _X1S_X2_SCHEMA,
133
+ HUB_VERSION_X2: _X1S_X2_SCHEMA,
134
+ }
135
+
136
+
137
+ def schema_for(hub_version: str) -> WireSchema:
138
+ """Return the :class:`WireSchema` for ``hub_version``.
139
+
140
+ Raises :class:`ValueError` if ``hub_version`` is anything other
141
+ than one of the known ``HUB_VERSION_*`` constants. Callers must
142
+ never paper over an unknown variant with a default -- if the
143
+ classification surface produces an unfamiliar value, that is a
144
+ signal that a new firmware lineage exists and the wire layouts
145
+ must be re-derived before writes can be trusted.
146
+ """
147
+
148
+ try:
149
+ return SCHEMAS[hub_version]
150
+ except KeyError as exc:
151
+ known = ", ".join(sorted(SCHEMAS))
152
+ raise ValueError(
153
+ f"schema_for: unknown hub_version={hub_version!r}; "
154
+ f"expected one of {known}."
155
+ ) from exc
156
+
157
+
158
+ __all__ = [
159
+ "InputEntryLayout",
160
+ "InputsTrailingLayout",
161
+ "SCHEMAS",
162
+ "WireSchema",
163
+ "schema_for",
164
+ ]