sonance-py 0.1.0__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.
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.3
2
+ Name: sonance-py
3
+ Version: 0.1.0
4
+ Summary: An async Python library and CLI for interacting with Sonance DSP amplifiers.
5
+ Requires-Dist: aiohttp>=3.12.0
6
+ Requires-Dist: pydantic>=2.12.0
7
+ Requires-Dist: typer>=0.12.5
8
+ Requires-Python: >=3.14
9
+ Description-Content-Type: text/markdown
10
+
11
+ # Sonance Py
12
+
13
+ Sonance Py is an async Python library for controlling Sonance DSP amplifiers,
14
+ including the DSP8-130 and related models that expose the same unauthenticated
15
+ HTTP interface.
16
+
17
+ The project is intended to support a future Home Assistant integration and a
18
+ Typer-based CLI for local development, testing, and troubleshooting.
19
+
20
+ ## Current Status
21
+
22
+ This project is in early development. The initial client implements the basic
23
+ HTTP API shape discovered from the amplifier web UI:
24
+
25
+ - General settings read/write operations
26
+ - Input/output settings read/write operations
27
+ - EQ preset read/write/action operations
28
+ - Shared async HTTP session support
29
+
30
+ The CLI is not implemented yet.
31
+
32
+ ## HTTP API
33
+
34
+ The amplifier web UI calls an unauthenticated endpoint at:
35
+
36
+ ```text
37
+ /Web/Handler.php
38
+ ```
39
+
40
+ The API uses query parameters for reads and writes, and returns JSON state
41
+ objects. The documented API shape is available in:
42
+
43
+ ```text
44
+ Docs/http-api.md
45
+ ```
46
+
47
+ ## Basic Usage
48
+
49
+ ```python
50
+ import asyncio
51
+
52
+ from sonance_py import SonanceDSP
53
+
54
+
55
+ async def main() -> None:
56
+ async with SonanceDSP("192.168.1.50") as amp:
57
+ general = await amp.read_general()
58
+ print(general.amplifier_model)
59
+
60
+
61
+ asyncio.run(main())
62
+ ```
63
+
64
+ ## Development
65
+
66
+ This project uses UV for dependency management and packaging.
67
+
68
+ Install dependencies:
69
+
70
+ ```shell
71
+ uv sync
72
+ ```
73
+
74
+ Run Ruff:
75
+
76
+ ```shell
77
+ uv run ruff check .
78
+ uv run ruff format . --check
79
+ ```
80
+
81
+ Build the package:
82
+
83
+ ```shell
84
+ uv build
85
+ ```
86
+
87
+ ## Notes
88
+
89
+ - The device endpoint is unauthenticated HTTP.
90
+ - API indexes are zero-based because that is how the web UI addresses arrays.
91
+ - The project targets Python 3.14 and newer.
92
+ - Behavior still needs validation against real amplifier hardware and firmware
93
+ versions.
@@ -0,0 +1,83 @@
1
+ # Sonance Py
2
+
3
+ Sonance Py is an async Python library for controlling Sonance DSP amplifiers,
4
+ including the DSP8-130 and related models that expose the same unauthenticated
5
+ HTTP interface.
6
+
7
+ The project is intended to support a future Home Assistant integration and a
8
+ Typer-based CLI for local development, testing, and troubleshooting.
9
+
10
+ ## Current Status
11
+
12
+ This project is in early development. The initial client implements the basic
13
+ HTTP API shape discovered from the amplifier web UI:
14
+
15
+ - General settings read/write operations
16
+ - Input/output settings read/write operations
17
+ - EQ preset read/write/action operations
18
+ - Shared async HTTP session support
19
+
20
+ The CLI is not implemented yet.
21
+
22
+ ## HTTP API
23
+
24
+ The amplifier web UI calls an unauthenticated endpoint at:
25
+
26
+ ```text
27
+ /Web/Handler.php
28
+ ```
29
+
30
+ The API uses query parameters for reads and writes, and returns JSON state
31
+ objects. The documented API shape is available in:
32
+
33
+ ```text
34
+ Docs/http-api.md
35
+ ```
36
+
37
+ ## Basic Usage
38
+
39
+ ```python
40
+ import asyncio
41
+
42
+ from sonance_py import SonanceDSP
43
+
44
+
45
+ async def main() -> None:
46
+ async with SonanceDSP("192.168.1.50") as amp:
47
+ general = await amp.read_general()
48
+ print(general.amplifier_model)
49
+
50
+
51
+ asyncio.run(main())
52
+ ```
53
+
54
+ ## Development
55
+
56
+ This project uses UV for dependency management and packaging.
57
+
58
+ Install dependencies:
59
+
60
+ ```shell
61
+ uv sync
62
+ ```
63
+
64
+ Run Ruff:
65
+
66
+ ```shell
67
+ uv run ruff check .
68
+ uv run ruff format . --check
69
+ ```
70
+
71
+ Build the package:
72
+
73
+ ```shell
74
+ uv build
75
+ ```
76
+
77
+ ## Notes
78
+
79
+ - The device endpoint is unauthenticated HTTP.
80
+ - API indexes are zero-based because that is how the web UI addresses arrays.
81
+ - The project targets Python 3.14 and newer.
82
+ - Behavior still needs validation against real amplifier hardware and firmware
83
+ versions.
@@ -0,0 +1,43 @@
1
+ [project]
2
+ name = "sonance-py"
3
+ version = "0.1.0"
4
+ description = "An async Python library and CLI for interacting with Sonance DSP amplifiers."
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ dependencies = [
8
+ "aiohttp>=3.12.0",
9
+ "pydantic>=2.12.0",
10
+ "typer>=0.12.5",
11
+ ]
12
+
13
+ [project.scripts]
14
+ sonance = "sonance_py.cli:app"
15
+
16
+ [dependency-groups]
17
+ dev = [
18
+ "pyright>=1.1.400",
19
+ "ruff>=0.14.0",
20
+ ]
21
+
22
+ [build-system]
23
+ requires = ["uv_build>=0.11.14,<0.12"]
24
+ build-backend = "uv_build"
25
+
26
+ [tool.ruff]
27
+ target-version = "py314"
28
+ line-length = 88
29
+ src = ["src"]
30
+
31
+ [tool.ruff.lint]
32
+ select = [
33
+ "E",
34
+ "F",
35
+ "I",
36
+ "UP",
37
+ ]
38
+
39
+
40
+ [tool.pyright]
41
+ include = ["src", "main.py"]
42
+ pythonVersion = "3.14"
43
+ typeCheckingMode = "standard"
@@ -0,0 +1,52 @@
1
+ """Async client for Sonance DSP amplifiers."""
2
+
3
+ from .amplifier import SonanceDSP, SonanceOutput
4
+ from .models import (
5
+ AutoOnMethod,
6
+ BasicStatus,
7
+ BridgeModeItem,
8
+ CrossoverBand,
9
+ CrossoverFilterType,
10
+ CrossoverSettings,
11
+ DelaySettings,
12
+ EqSettings,
13
+ GeneralSettings,
14
+ InOutSettings,
15
+ Limiter,
16
+ OnOff,
17
+ Output,
18
+ OutputGroup,
19
+ OutputGroupItem,
20
+ ParametricEqBand,
21
+ PresetItem,
22
+ SourceMode,
23
+ StereoMode,
24
+ TiltBand,
25
+ TiltSettings,
26
+ )
27
+
28
+ __all__ = [
29
+ "AutoOnMethod",
30
+ "BasicStatus",
31
+ "BridgeModeItem",
32
+ "CrossoverBand",
33
+ "CrossoverFilterType",
34
+ "CrossoverSettings",
35
+ "DelaySettings",
36
+ "EqSettings",
37
+ "GeneralSettings",
38
+ "InOutSettings",
39
+ "Limiter",
40
+ "OnOff",
41
+ "Output",
42
+ "OutputGroupItem",
43
+ "OutputGroup",
44
+ "ParametricEqBand",
45
+ "PresetItem",
46
+ "SonanceDSP",
47
+ "SonanceOutput",
48
+ "SourceMode",
49
+ "StereoMode",
50
+ "TiltBand",
51
+ "TiltSettings",
52
+ ]
@@ -0,0 +1,302 @@
1
+ """Pydantic models for deserializing Sonance DSP HTTP API payloads."""
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+
5
+ from .models import (
6
+ AutoOnMethod,
7
+ BasicStatus,
8
+ BridgeModeItem,
9
+ CrossoverBand,
10
+ CrossoverFilterType,
11
+ CrossoverSettings,
12
+ DelaySettings,
13
+ EqSettings,
14
+ GeneralSettings,
15
+ InOutSettings,
16
+ Limiter,
17
+ OnOff,
18
+ OutputGroup,
19
+ OutputGroupItem,
20
+ ParametricEqBand,
21
+ PresetItem,
22
+ SourceMode,
23
+ StereoMode,
24
+ TiltBand,
25
+ TiltSettings,
26
+ )
27
+
28
+
29
+ class SonanceWireModel(BaseModel):
30
+ """Base model for wire-format API payloads."""
31
+
32
+ model_config = ConfigDict(extra="allow", populate_by_name=True)
33
+
34
+
35
+ class WirePresetItem(SonanceWireModel):
36
+ """Named preset option returned by the amplifier."""
37
+
38
+ name: str
39
+ value: str
40
+
41
+ def to_model(self) -> PresetItem:
42
+ """Convert the wire payload to a public dataclass."""
43
+
44
+ return PresetItem(name=self.name, value=self.value)
45
+
46
+
47
+ class WireGeneralSettings(SonanceWireModel):
48
+ """Payload returned by the general settings read endpoint."""
49
+
50
+ ip_address: str = Field(alias="ip-address")
51
+ ip_subnet_mask: str = Field(alias="ip-subnet-mask")
52
+ dhcp_switch: OnOff = Field(alias="dhcp-switch")
53
+ flash_power_switch: OnOff = Field(alias="flash-power-switch")
54
+ power: OnOff
55
+ standby_mode: OnOff = Field(alias="standby-mode")
56
+ auto_on_method: AutoOnMethod = Field(alias="auto-on-method")
57
+ auto_on_delay: str = Field(alias="auto-on-delay")
58
+ amplifier_name: str = Field(alias="amplifier-name")
59
+ dealer_name: str = Field(alias="dealer-name")
60
+ amplifier_model: str = Field(alias="amplifier-model")
61
+ installer_name: str = Field(alias="installer-name")
62
+ customer_name: str = Field(alias="customer-name")
63
+ installition_date: str = Field(alias="installition-date")
64
+ firmware_version: str = Field(alias="firmware-version")
65
+ serial_number: str = Field(alias="serial-number")
66
+
67
+ def to_basic_status(self) -> BasicStatus:
68
+ """Convert the wire payload to a compact public status dataclass."""
69
+
70
+ return BasicStatus(
71
+ power=self.power,
72
+ firmware_version=self.firmware_version,
73
+ amplifier_name=self.amplifier_name,
74
+ serial_number=self.serial_number,
75
+ )
76
+
77
+ def to_model(self) -> GeneralSettings:
78
+ """Convert the wire payload to a public dataclass."""
79
+
80
+ return GeneralSettings(
81
+ ip_address=self.ip_address,
82
+ ip_subnet_mask=self.ip_subnet_mask,
83
+ dhcp_switch=self.dhcp_switch,
84
+ flash_power_switch=self.flash_power_switch,
85
+ power=self.power,
86
+ standby_mode=self.standby_mode,
87
+ auto_on_method=self.auto_on_method,
88
+ auto_on_delay=self.auto_on_delay,
89
+ amplifier_name=self.amplifier_name,
90
+ dealer_name=self.dealer_name,
91
+ amplifier_model=self.amplifier_model,
92
+ installer_name=self.installer_name,
93
+ customer_name=self.customer_name,
94
+ installition_date=self.installition_date,
95
+ firmware_version=self.firmware_version,
96
+ serial_number=self.serial_number,
97
+ )
98
+
99
+
100
+ class WireOutputGroupItem(SonanceWireModel):
101
+ """Output group option returned by the input/output settings endpoint."""
102
+
103
+ name: str
104
+ value: OutputGroup
105
+
106
+ def to_model(self) -> OutputGroupItem:
107
+ """Convert the wire payload to a public dataclass."""
108
+
109
+ return OutputGroupItem(name=self.name, value=self.value)
110
+
111
+
112
+ class WireBridgeModeItem(SonanceWireModel):
113
+ """Bridge mode option returned by the input/output settings endpoint."""
114
+
115
+ name: str
116
+ value: OnOff
117
+
118
+ def to_model(self) -> BridgeModeItem:
119
+ """Convert the wire payload to a public dataclass."""
120
+
121
+ return BridgeModeItem(name=self.name, value=self.value)
122
+
123
+
124
+ class WireInOutSettings(SonanceWireModel):
125
+ """Payload returned by the input/output settings read endpoint."""
126
+
127
+ dsp_preset_items: list[WirePresetItem] = Field(alias="dsp-preset-items")
128
+ input_names: list[str] = Field(alias="input-names")
129
+ input_titles: list[str] = Field(alias="input-titles")
130
+ output_titles: list[str] = Field(alias="output-titles")
131
+ level_trim_dbs: list[str] = Field(alias="level-trim-dBs")
132
+ output_names: list[str] = Field(alias="output-names")
133
+ stereo_or_mono: list[StereoMode] = Field(alias="stereo-or-mono")
134
+ dsp_presets: list[int] = Field(alias="dsp-presets")
135
+ output_group_items: list[WireOutputGroupItem] = Field(alias="output-group-items")
136
+ output_groups: list[OutputGroup] = Field(alias="output-groups")
137
+ bridge_mode_items: list[WireBridgeModeItem] = Field(alias="bridge-mode-items")
138
+ bridge_modes: list[OnOff] = Field(alias="bridge-modes")
139
+ sources_1: list[int] = Field(alias="sources-1")
140
+ sources_2: list[int] = Field(alias="sources-2")
141
+ mode_sources: list[SourceMode] = Field(alias="mode-sources")
142
+ output_volumes: list[str] = Field(alias="output-volumes")
143
+ turn_on_volumes: list[str] = Field(alias="turn-on-volumes")
144
+ maximum_volumes: list[str] = Field(alias="maximum-volumes")
145
+ gain_offset: list[str] = Field(alias="gain-offset")
146
+ mute_volumes: list[OnOff] = Field(alias="mute-volumes")
147
+
148
+ def to_model(self) -> InOutSettings:
149
+ """Convert the wire payload to a public dataclass."""
150
+
151
+ return InOutSettings(
152
+ dsp_preset_items=[item.to_model() for item in self.dsp_preset_items],
153
+ input_names=self.input_names,
154
+ input_titles=self.input_titles,
155
+ output_titles=self.output_titles,
156
+ level_trim_dbs=self.level_trim_dbs,
157
+ output_names=self.output_names,
158
+ stereo_or_mono=self.stereo_or_mono,
159
+ dsp_presets=self.dsp_presets,
160
+ output_group_items=[item.to_model() for item in self.output_group_items],
161
+ output_groups=self.output_groups,
162
+ bridge_mode_items=[item.to_model() for item in self.bridge_mode_items],
163
+ bridge_modes=self.bridge_modes,
164
+ sources_1=self.sources_1,
165
+ sources_2=self.sources_2,
166
+ mode_sources=self.mode_sources,
167
+ output_volumes=self.output_volumes,
168
+ turn_on_volumes=self.turn_on_volumes,
169
+ maximum_volumes=self.maximum_volumes,
170
+ gain_offset=self.gain_offset,
171
+ mute_volumes=self.mute_volumes,
172
+ )
173
+
174
+
175
+ class WireParametricEqBand(SonanceWireModel):
176
+ """Single parametric EQ band."""
177
+
178
+ enable_status: OnOff = Field(alias="enable-status")
179
+ freq: int
180
+ q: float
181
+ gain: float
182
+
183
+ def to_model(self) -> ParametricEqBand:
184
+ """Convert the wire payload to a public dataclass."""
185
+
186
+ return ParametricEqBand(
187
+ enable_status=self.enable_status,
188
+ freq=self.freq,
189
+ q=self.q,
190
+ gain=self.gain,
191
+ )
192
+
193
+
194
+ class WireTiltBand(SonanceWireModel):
195
+ """Low or high tilt EQ band."""
196
+
197
+ on_or_off: OnOff = Field(alias="on-or-off")
198
+ freq: int
199
+ gain: float
200
+
201
+ def to_model(self) -> TiltBand:
202
+ """Convert the wire payload to a public dataclass."""
203
+
204
+ return TiltBand(on_or_off=self.on_or_off, freq=self.freq, gain=self.gain)
205
+
206
+
207
+ class WireTiltSettings(SonanceWireModel):
208
+ """Tilt control settings."""
209
+
210
+ low: WireTiltBand
211
+ high: WireTiltBand
212
+
213
+ def to_model(self) -> TiltSettings:
214
+ """Convert the wire payload to a public dataclass."""
215
+
216
+ return TiltSettings(low=self.low.to_model(), high=self.high.to_model())
217
+
218
+
219
+ class WireCrossoverBand(SonanceWireModel):
220
+ """Low-pass or high-pass crossover settings."""
221
+
222
+ on_or_off: OnOff = Field(alias="on-or-off")
223
+ freq: int
224
+ filter_type: CrossoverFilterType = Field(alias="filter-type")
225
+
226
+ def to_model(self) -> CrossoverBand:
227
+ """Convert the wire payload to a public dataclass."""
228
+
229
+ return CrossoverBand(
230
+ on_or_off=self.on_or_off,
231
+ freq=self.freq,
232
+ filter_type=self.filter_type,
233
+ )
234
+
235
+
236
+ class WireCrossoverSettings(SonanceWireModel):
237
+ """Crossover settings."""
238
+
239
+ low_pass: WireCrossoverBand = Field(alias="low-pass")
240
+ high_pass: WireCrossoverBand = Field(alias="high-pass")
241
+
242
+ def to_model(self) -> CrossoverSettings:
243
+ """Convert the wire payload to a public dataclass."""
244
+
245
+ return CrossoverSettings(
246
+ low_pass=self.low_pass.to_model(),
247
+ high_pass=self.high_pass.to_model(),
248
+ )
249
+
250
+
251
+ class WireDelaySettings(SonanceWireModel):
252
+ """Delay settings in the units returned by the amplifier."""
253
+
254
+ seconds: float
255
+ feet: float
256
+ meters: float
257
+
258
+ def to_model(self) -> DelaySettings:
259
+ """Convert the wire payload to a public dataclass."""
260
+
261
+ return DelaySettings(seconds=self.seconds, feet=self.feet, meters=self.meters)
262
+
263
+
264
+ class WireEqSettings(SonanceWireModel):
265
+ """Payload returned by the EQ settings read endpoint."""
266
+
267
+ output_names: list[str] = Field(alias="output-names")
268
+ dsp_presets: list[int] = Field(alias="dsp-presets")
269
+ output_titles: list[str] = Field(alias="output-titles")
270
+ amplifier_model: str = Field(alias="amplifier-model")
271
+ input_names: list[str] = Field(alias="input-names")
272
+ source_select: list[int] = Field(alias="source-select")
273
+ output_volumes: list[str] = Field(alias="output-volumes")
274
+ mute_volumes: list[OnOff] = Field(alias="mute-volumes")
275
+ eq_presets: list[WirePresetItem] = Field(alias="eq-presets")
276
+ current_eq_preset: str = Field(alias="current-eq-preset")
277
+ parametric_eq: list[WireParametricEqBand] = Field(alias="parametric-eq")
278
+ tilt: WireTiltSettings
279
+ crossover: WireCrossoverSettings
280
+ limiter_limiters: Limiter = Field(alias="limiter-limiters")
281
+ delay: WireDelaySettings
282
+
283
+ def to_model(self) -> EqSettings:
284
+ """Convert the wire payload to a public dataclass."""
285
+
286
+ return EqSettings(
287
+ output_names=self.output_names,
288
+ dsp_presets=self.dsp_presets,
289
+ output_titles=self.output_titles,
290
+ amplifier_model=self.amplifier_model,
291
+ input_names=self.input_names,
292
+ source_select=self.source_select,
293
+ output_volumes=self.output_volumes,
294
+ mute_volumes=self.mute_volumes,
295
+ eq_presets=[item.to_model() for item in self.eq_presets],
296
+ current_eq_preset=self.current_eq_preset,
297
+ parametric_eq=[band.to_model() for band in self.parametric_eq],
298
+ tilt=self.tilt.to_model(),
299
+ crossover=self.crossover.to_model(),
300
+ limiter_limiters=self.limiter_limiters,
301
+ delay=self.delay.to_model(),
302
+ )