pyambeo 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,26 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ lint:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: astral-sh/setup-uv@v6
13
+ - run: uv run --group dev ruff check .
14
+ - run: uv run --group dev ruff format --check .
15
+
16
+ test:
17
+ runs-on: ubuntu-latest
18
+ strategy:
19
+ matrix:
20
+ python-version: ["3.12", "3.13"]
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ - uses: astral-sh/setup-uv@v6
24
+ with:
25
+ python-version: ${{ matrix.python-version }}
26
+ - run: uv run --group dev pytest --cov=pyambeo --cov-report=term-missing
@@ -0,0 +1,40 @@
1
+ name: Release
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+ inputs:
8
+ repository:
9
+ description: "Package index"
10
+ type: choice
11
+ options: [testpypi, pypi]
12
+ default: testpypi
13
+
14
+ jobs:
15
+ build:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: astral-sh/setup-uv@v6
20
+ - run: uv build
21
+ - uses: actions/upload-artifact@v4
22
+ with:
23
+ name: dist
24
+ path: dist/
25
+
26
+ publish:
27
+ needs: build
28
+ runs-on: ubuntu-latest
29
+ # Trusted publishing: configure the matching environment on (test.)pypi.org.
30
+ environment: ${{ github.event_name == 'release' && 'pypi' || inputs.repository }}
31
+ permissions:
32
+ id-token: write
33
+ steps:
34
+ - uses: actions/download-artifact@v4
35
+ with:
36
+ name: dist
37
+ path: dist/
38
+ - uses: pypa/gh-action-pypi-publish@release/v1
39
+ with:
40
+ repository-url: ${{ (github.event_name != 'release' && inputs.repository == 'testpypi') && 'https://test.pypi.org/legacy/' || 'https://upload.pypi.org/legacy/' }}
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .coverage
10
+ htmlcov/
@@ -0,0 +1,7 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.15.10
4
+ hooks:
5
+ - id: ruff
6
+ args: [--fix]
7
+ - id: ruff-format
pyambeo-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 François AIZPURU
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.
pyambeo-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.5
2
+ Name: pyambeo
3
+ Version: 0.1.0
4
+ Summary: Async client for Sennheiser AMBEO soundbars
5
+ Project-URL: Homepage, https://github.com/faizpuru/pyambeo
6
+ Project-URL: Issues, https://github.com/faizpuru/pyambeo/issues
7
+ Author: François AIZPURU
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: ambeo,asyncio,home-assistant,sennheiser,soundbar
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Framework :: AsyncIO
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Home Automation
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.12
21
+ Requires-Dist: aiohttp>=3.9
22
+ Requires-Dist: yarl>=1.9
23
+ Description-Content-Type: text/markdown
24
+
25
+ # pyambeo
26
+
27
+ Async Python client for Sennheiser AMBEO soundbars (Max, Plus and Mini), built on `aiohttp`.
28
+
29
+ It powers the [AMBEO Soundbar integration](https://github.com/faizpuru/ha-ambeo_soundbar) for Home Assistant, but does not depend on it.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install pyambeo
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ```python
40
+ import asyncio
41
+
42
+ import aiohttp
43
+
44
+ from pyambeo import AmbeoSoundbar
45
+
46
+
47
+ async def main() -> None:
48
+ async with aiohttp.ClientSession() as session:
49
+ # Detects the model and returns the matching implementation.
50
+ bar = await AmbeoSoundbar.connect("192.168.1.10", session)
51
+ print(bar.info)
52
+
53
+ state = await bar.fetch_state()
54
+ print(state)
55
+
56
+ await bar.set_volume(20)
57
+
58
+ # Push updates from the device.
59
+ async for updates in bar.listen():
60
+ print(updates)
61
+
62
+
63
+ asyncio.run(main())
64
+ ```
65
+
66
+ The caller owns the `aiohttp.ClientSession`. All errors raised by the library inherit from `AmbeoError`:
67
+
68
+ | Exception | Meaning |
69
+ |---|---|
70
+ | `AmbeoConnectionError` | The device cannot be reached |
71
+ | `AmbeoTimeoutError` | A request timed out (subclass of `AmbeoConnectionError`) |
72
+ | `AmbeoResponseError` | The device answered with an unexpected response |
73
+ | `AmbeoUnsupportedModelError` | The device model is not supported |
74
+
75
+ ## Supported devices
76
+
77
+ | Model | Class |
78
+ |---|---|
79
+ | AMBEO Soundbar Max | `AmbeoEspresso` |
80
+ | AMBEO Soundbar Plus | `AmbeoPopcorn` |
81
+ | AMBEO Soundbar Mini | `AmbeoPopcorn` |
82
+
83
+ ## Development
84
+
85
+ ```bash
86
+ uv run --group dev pytest
87
+ uv run --group dev ruff check .
88
+ ```
89
+
90
+ ## License
91
+
92
+ MIT
@@ -0,0 +1,68 @@
1
+ # pyambeo
2
+
3
+ Async Python client for Sennheiser AMBEO soundbars (Max, Plus and Mini), built on `aiohttp`.
4
+
5
+ It powers the [AMBEO Soundbar integration](https://github.com/faizpuru/ha-ambeo_soundbar) for Home Assistant, but does not depend on it.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install pyambeo
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```python
16
+ import asyncio
17
+
18
+ import aiohttp
19
+
20
+ from pyambeo import AmbeoSoundbar
21
+
22
+
23
+ async def main() -> None:
24
+ async with aiohttp.ClientSession() as session:
25
+ # Detects the model and returns the matching implementation.
26
+ bar = await AmbeoSoundbar.connect("192.168.1.10", session)
27
+ print(bar.info)
28
+
29
+ state = await bar.fetch_state()
30
+ print(state)
31
+
32
+ await bar.set_volume(20)
33
+
34
+ # Push updates from the device.
35
+ async for updates in bar.listen():
36
+ print(updates)
37
+
38
+
39
+ asyncio.run(main())
40
+ ```
41
+
42
+ The caller owns the `aiohttp.ClientSession`. All errors raised by the library inherit from `AmbeoError`:
43
+
44
+ | Exception | Meaning |
45
+ |---|---|
46
+ | `AmbeoConnectionError` | The device cannot be reached |
47
+ | `AmbeoTimeoutError` | A request timed out (subclass of `AmbeoConnectionError`) |
48
+ | `AmbeoResponseError` | The device answered with an unexpected response |
49
+ | `AmbeoUnsupportedModelError` | The device model is not supported |
50
+
51
+ ## Supported devices
52
+
53
+ | Model | Class |
54
+ |---|---|
55
+ | AMBEO Soundbar Max | `AmbeoEspresso` |
56
+ | AMBEO Soundbar Plus | `AmbeoPopcorn` |
57
+ | AMBEO Soundbar Mini | `AmbeoPopcorn` |
58
+
59
+ ## Development
60
+
61
+ ```bash
62
+ uv run --group dev pytest
63
+ uv run --group dev ruff check .
64
+ ```
65
+
66
+ ## License
67
+
68
+ MIT
@@ -0,0 +1,83 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pyambeo"
7
+ version = "0.1.0"
8
+ description = "Async client for Sennheiser AMBEO soundbars"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "François AIZPURU" }]
13
+ requires-python = ">=3.12"
14
+ dependencies = [
15
+ "aiohttp>=3.9",
16
+ "yarl>=1.9",
17
+ ]
18
+ classifiers = [
19
+ "Development Status :: 4 - Beta",
20
+ "Framework :: AsyncIO",
21
+ "Intended Audience :: Developers",
22
+ "Operating System :: OS Independent",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Topic :: Home Automation",
27
+ "Typing :: Typed",
28
+ ]
29
+ keywords = ["ambeo", "sennheiser", "soundbar", "asyncio", "home-assistant"]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/faizpuru/pyambeo"
33
+ Issues = "https://github.com/faizpuru/pyambeo/issues"
34
+
35
+ [dependency-groups]
36
+ dev = [
37
+ "aioresponses",
38
+ "pre-commit",
39
+ "pytest",
40
+ "pytest-asyncio",
41
+ "pytest-cov",
42
+ "ruff",
43
+ ]
44
+
45
+ [tool.uv]
46
+ # Dev/test only: aioresponses 0.7.9 does not support aiohttp 3.14 yet.
47
+ constraint-dependencies = ["aiohttp<3.14"]
48
+
49
+ [tool.hatch.build.targets.wheel]
50
+ packages = ["src/pyambeo"]
51
+
52
+ [tool.pytest.ini_options]
53
+ asyncio_mode = "auto"
54
+ asyncio_default_fixture_loop_scope = "function"
55
+
56
+ [tool.ruff]
57
+ target-version = "py312"
58
+ line-length = 88
59
+
60
+ [tool.ruff.lint]
61
+ select = [
62
+ "E", # pycodestyle errors
63
+ "W", # pycodestyle warnings
64
+ "F", # pyflakes
65
+ "I", # isort
66
+ "UP", # pyupgrade
67
+ "B", # flake8-bugbear
68
+ "SIM", # flake8-simplify
69
+ "D", # pydocstyle (PEP 257)
70
+ ]
71
+ ignore = [
72
+ "E501", # line too long, handled by formatter
73
+ ]
74
+
75
+ [tool.ruff.lint.pydocstyle]
76
+ convention = "pep257"
77
+
78
+ [tool.ruff.lint.isort]
79
+ known-first-party = ["pyambeo"]
80
+
81
+ [tool.ruff.format]
82
+ quote-style = "double"
83
+ indent-style = "space"
@@ -0,0 +1,31 @@
1
+ """Async client for Sennheiser Ambeo soundbars."""
2
+
3
+ from .const import Capability, StateKey
4
+ from .espresso import AmbeoEspresso
5
+ from .exceptions import (
6
+ AmbeoConnectionError,
7
+ AmbeoError,
8
+ AmbeoResponseError,
9
+ AmbeoTimeoutError,
10
+ AmbeoUnsupportedModelError,
11
+ )
12
+ from .models import DeviceInfo, PlayerStatus, Preset, Source
13
+ from .popcorn import AmbeoPopcorn
14
+ from .soundbar import AmbeoSoundbar
15
+
16
+ __all__ = [
17
+ "AmbeoConnectionError",
18
+ "AmbeoError",
19
+ "AmbeoEspresso",
20
+ "AmbeoPopcorn",
21
+ "AmbeoResponseError",
22
+ "AmbeoSoundbar",
23
+ "AmbeoTimeoutError",
24
+ "AmbeoUnsupportedModelError",
25
+ "Capability",
26
+ "DeviceInfo",
27
+ "PlayerStatus",
28
+ "Preset",
29
+ "Source",
30
+ "StateKey",
31
+ ]
@@ -0,0 +1,159 @@
1
+ """Constants for Ambeo Soundbar API."""
2
+
3
+ from enum import StrEnum
4
+ from typing import NamedTuple
5
+
6
+ DEFAULT_PORT = 80
7
+ DEFAULT_TIMEOUT = 5
8
+
9
+ # ESPRESSO: MAX
10
+ MAX_SOUNDBAR = "AMBEO Soundbar Max"
11
+ EXCLUDE_SOURCES_MAX = ["aes"]
12
+ AMBEO_MAX_VOLUME_STEP = 0.02
13
+
14
+
15
+ # POPCORN: PLUS/MINI
16
+ PLUS_SOUNDBAR = "AMBEO Soundbar Plus"
17
+ MINI_SOUNDBAR = "AMBEO Soundbar Mini"
18
+ AMBEO_POPCORN_VOLUME_STEP = 0.01
19
+
20
+
21
+ BRIGHTNESS_RANGE_DEFAULT = (0, 100)
22
+ BRIGHTNESS_RANGE_AMBEO_MAX_DISPLAY = (1, 126)
23
+ BRIGHTNESS_RANGE_AMBEO_MAX_LOGO = (1, 118)
24
+
25
+
26
+ class Capability(StrEnum):
27
+ """Device capability identifiers."""
28
+
29
+ AMBEO_LOGO = "AmbeoLogo"
30
+ DECODER_STATUS = "DecoderStatus"
31
+ AMBEO_MODE_LEVEL = "AmbeoModeLevel"
32
+ BLUETOOTH_PAIRING = "AmbeoBluetoothPairing"
33
+ CENTER_SPEAKER_LEVEL = "CenterSpeakerLevel"
34
+ CENTER_VOLUME = "CenterVolume"
35
+ CODEC_LED = "CodecLED"
36
+ ECO_MODE = "EcoMode"
37
+ LED_BAR = "LEDBar"
38
+ MAX_DISPLAY = "AmbeoMaxDisplay"
39
+ MAX_LOGO = "AmbeoMaxLogo"
40
+ RESET_EXPERT_SETTINGS = "ResetExpertSettings"
41
+ SIDE_FIRING_LEVEL = "SideFiringLevel"
42
+ STANDBY = "standby"
43
+ SUBWOOFER = "SubWoofer"
44
+ UP_FIRING_LEVEL = "UpFiringLevel"
45
+ NATIVE_VOLUME = "NativeVolume"
46
+ VOICE_ENHANCEMENT_LEVEL = "VoiceEnhancementLevel"
47
+ VOICE_ENHANCEMENT_TOGGLE = "VoiceEnhancementMode"
48
+
49
+
50
+ class StateKey(StrEnum):
51
+ """Keys of the device state returned by fetch_state() and listen()."""
52
+
53
+ VOLUME = "volume"
54
+ MUTED = "muted"
55
+ STATE = "state"
56
+ CURRENT_SOURCE = "current_source"
57
+ CURRENT_PRESET = "current_preset"
58
+ PLAYER_DATA = "player_data"
59
+ PLAY_TIME = "play_time"
60
+ LED_BAR_BRIGHTNESS = "led_bar_brightness"
61
+ CODEC_LED_BRIGHTNESS = "codec_led_brightness"
62
+ LOGO_BRIGHTNESS = "logo_brightness"
63
+ LOGO_STATE = "logo_state"
64
+ DISPLAY_BRIGHTNESS = "display_brightness"
65
+ NIGHT_MODE = "night_mode"
66
+ AMBEO_MODE = "ambeo_mode"
67
+ AMBEO_MODE_LEVEL = "ambeo_mode_level"
68
+ SOUND_FEEDBACK = "sound_feedback"
69
+ VOICE_ENHANCEMENT = "voice_enhancement"
70
+ VOICE_ENHANCEMENT_LEVEL = "voice_enhancement_level"
71
+ BLUETOOTH_PAIRING = "bluetooth_pairing"
72
+ SUBWOOFER_STATUS = "subwoofer_status"
73
+ SUBWOOFER_VOLUME = "subwoofer_volume"
74
+ CENTER_SPEAKER_LEVEL = "center_speaker_level"
75
+ SIDE_FIRING_LEVEL = "side_firing_level"
76
+ UP_FIRING_LEVEL = "up_firing_level"
77
+ CENTER_VOLUME = "center_volume"
78
+ ECO_MODE = "eco_mode"
79
+ DECODER_STATUS = "decoder_status"
80
+
81
+
82
+ class PathSub(NamedTuple):
83
+ """Describes a single event-queue subscription.
84
+
85
+ path -- Device API path to subscribe to.
86
+ data_key -- Key used in the state dict.
87
+ type_key -- JSON type key inside the event's itemValue dict.
88
+ capability -- Required device capability, or None if always subscribed.
89
+ sub_key -- If set, the final value is item_value[type_key][sub_key]
90
+ (used when the type value is itself a dict).
91
+ """
92
+
93
+ path: str
94
+ data_key: StateKey
95
+ type_key: str
96
+ capability: Capability | None = None
97
+ sub_key: str | None = None
98
+
99
+
100
+ class FeatureDef(NamedTuple):
101
+ """Optional state value fetched by fetch_state() when the capability matches."""
102
+
103
+ data_key: StateKey
104
+ api_method: str
105
+ capability: Capability | None = None
106
+
107
+
108
+ OPTIONAL_FEATURES: tuple[FeatureDef, ...] = (
109
+ FeatureDef(StateKey.PLAY_TIME, "get_play_time"),
110
+ FeatureDef(
111
+ StateKey.LED_BAR_BRIGHTNESS, "get_led_bar_brightness", Capability.LED_BAR
112
+ ),
113
+ FeatureDef(
114
+ StateKey.CODEC_LED_BRIGHTNESS, "get_codec_led_brightness", Capability.CODEC_LED
115
+ ),
116
+ FeatureDef(StateKey.LOGO_BRIGHTNESS, "get_logo_brightness", Capability.AMBEO_LOGO),
117
+ FeatureDef(StateKey.LOGO_STATE, "get_logo_state", Capability.AMBEO_LOGO),
118
+ FeatureDef(StateKey.NIGHT_MODE, "get_night_mode"),
119
+ FeatureDef(StateKey.AMBEO_MODE, "get_ambeo_mode"),
120
+ FeatureDef(StateKey.SOUND_FEEDBACK, "get_sound_feedback"),
121
+ FeatureDef(
122
+ StateKey.VOICE_ENHANCEMENT,
123
+ "get_voice_enhancement",
124
+ Capability.VOICE_ENHANCEMENT_TOGGLE,
125
+ ),
126
+ FeatureDef(
127
+ StateKey.BLUETOOTH_PAIRING,
128
+ "get_bluetooth_pairing_state",
129
+ Capability.BLUETOOTH_PAIRING,
130
+ ),
131
+ FeatureDef(StateKey.SUBWOOFER_STATUS, "get_subwoofer_status", Capability.SUBWOOFER),
132
+ FeatureDef(StateKey.SUBWOOFER_VOLUME, "get_subwoofer_volume", Capability.SUBWOOFER),
133
+ FeatureDef(
134
+ StateKey.VOICE_ENHANCEMENT_LEVEL,
135
+ "get_voice_enhancement_level",
136
+ Capability.VOICE_ENHANCEMENT_LEVEL,
137
+ ),
138
+ FeatureDef(
139
+ StateKey.CENTER_SPEAKER_LEVEL,
140
+ "get_center_speaker_level",
141
+ Capability.CENTER_SPEAKER_LEVEL,
142
+ ),
143
+ FeatureDef(
144
+ StateKey.SIDE_FIRING_LEVEL,
145
+ "get_side_firing_level",
146
+ Capability.SIDE_FIRING_LEVEL,
147
+ ),
148
+ FeatureDef(
149
+ StateKey.UP_FIRING_LEVEL, "get_up_firing_level", Capability.UP_FIRING_LEVEL
150
+ ),
151
+ FeatureDef(StateKey.CENTER_VOLUME, "get_center_volume", Capability.CENTER_VOLUME),
152
+ FeatureDef(StateKey.ECO_MODE, "get_eco_mode", Capability.ECO_MODE),
153
+ FeatureDef(
154
+ StateKey.DECODER_STATUS, "get_decoder_status", Capability.DECODER_STATUS
155
+ ),
156
+ FeatureDef(
157
+ StateKey.AMBEO_MODE_LEVEL, "get_ambeo_mode_level", Capability.AMBEO_MODE_LEVEL
158
+ ),
159
+ )