26 Commits

Author SHA1 Message Date
3a70a4c578 upd docs 2024-02-15 18:35:34 +00:00
8b1b2c7f79 log value given as well as value expected 2024-02-15 16:58:18 +00:00
1e5e458105 log OOB as warnings
patch bump

closes #8
2024-02-15 15:19:05 +00:00
e05460e998 implement module level loggers
class loggers are now child loggers

minor version bump

closes #7
2024-02-15 15:15:30 +00:00
d27824d1cf should an incorrect kind be passed to entry point, raise XAirRemoteError
remove the print statement

patch bump
2024-02-15 13:05:54 +00:00
764195a452 remove unused opts dict in geq_prop 2024-02-14 22:47:12 +00:00
b295fee6e1 lint fixes
fix {DCA}.name setter

removed unused imports

patch bump
2024-02-14 22:06:28 +00:00
06be2f2831 fix date 2024-02-14 21:39:58 +00:00
2d0c0f91f0 upd CHANGELOG
bump to 2.2.4
2024-02-14 21:38:56 +00:00
6e017b4afc add poetry script sends
bump to 2.2.4a0
2024-02-08 21:38:30 +00:00
85664c8465 swap i, remote order 2024-02-08 18:15:18 +00:00
a3473d5922 swap i, remote order for consistency. 2024-02-08 17:54:27 +00:00
e9ef113b5c upd __init__ methods 2024-02-08 17:41:56 +00:00
56ec9a17c0 print back level send values in sends example 2024-02-08 17:41:37 +00:00
9a7d98d58b fix docstrings
remove pass
2024-02-08 17:40:49 +00:00
f3cf215a76 upd decorator func names 2024-02-08 15:27:15 +00:00
a62a46d61a upd docs 2024-02-08 13:46:49 +00:00
5eeaff2371 now using sys.executable, fixes issue with pyenv-win 2024-02-08 13:46:26 +00:00
c2cf2fe523 sends example added.
obs example updated to reflect updates to obsws
2024-02-08 13:45:55 +00:00
265c26eb67 import util as namespace 2024-02-08 13:44:55 +00:00
467b769ea4 from_db, to_db decorator functions added to util.
_get_{fader,level}_val, _set_{fader,level}_val removed
2024-02-08 13:43:49 +00:00
27d0811091 Send class added to shared module
Send mixed into Strip, AuxRtn, FxRtn classes.

addresses #4
2024-02-08 13:43:15 +00:00
df2d158618 patch bump
closes #6
2024-02-03 13:04:25 +00:00
035c8d6507 move delay into {XAirRemote}.query()
update getters
2024-02-03 13:03:49 +00:00
cab3888946 upd lock file 2023-08-27 19:14:32 +01:00
eddfb89fa9 add group dev (dev-dependencies deprecated) 2023-08-27 19:13:20 +01:00
20 changed files with 568 additions and 418 deletions

View File

@@ -11,6 +11,33 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
- [ ] - [ ]
## [2.3.1] - 2024-02-15
### Changed
- Module level loggers implemented
- class loggers are now child loggers
- Passing an incorrect kind_id to the entry point now raises an XAirRemoteError.
- Passing a value out of bounds to a setter now logs a warning instead of raising an exception.
- Send class added to README.
## [2.2.4] - 2024-02-14
### Added
- Send class mixed into Strip, AuxRtn, FxRtn. May now be accessed with {Class}.send
- Sends example added
### Changed
- delay kwarg now applies to getters. See [Issue #6](https://github.com/onyx-and-iris/xair-api-python/issues/6).
## [2.2.0] - 2022-11-08
### Added
- mute prop to Bus, FX, LR, RTN, Strip classes.
## [2.1.0] - 2022-11-08 ## [2.1.0] - 2022-11-08
### Added ### Added

View File

@@ -70,7 +70,7 @@ The following keyword arguments may be passed:
- `ip`: ip address of the mixer - `ip`: ip address of the mixer
- `port`: mixer port, defaults to 10023 for x32 and 10024 for xair - `port`: mixer port, defaults to 10023 for x32 and 10024 for xair
- `delay`: a delay between each command, defaults to 20ms. - `delay`: a delay between each command (applies to the getters). Defaults to 20ms.
- a note about delay, stability may rely on network connection. For wired connections the delay can be safely reduced. - a note about delay, stability may rely on network connection. For wired connections the delay can be safely reduced.
## API ## API
@@ -121,7 +121,7 @@ Contains the subclasses:
### `Strip` ### `Strip`
Contains the subclasses: Contains the subclasses:
(`Config`, `Preamp`, `Gate`, `Dyn`, `Insert`, `GEQ`, `EQ`, `Mix`, `Group`, `Automix`) (`Config`, `Preamp`, `Gate`, `Dyn`, `Insert`, `GEQ`, `EQ`, `Mix`, `Group`, `Automix`, `Send`)
### `Bus` ### `Bus`
@@ -136,12 +136,12 @@ Contains the subclasses:
### `FXRtn` ### `FXRtn`
Contains the subclasses: Contains the subclasses:
(`Config`, `Preamp`, `EQ`, `Mix`, `Group`) (`Config`, `Preamp`, `EQ`, `Mix`, `Group`, `Send`)
### `AuxRtn` ### `AuxRtn`
Contains the subclasses: Contains the subclasses:
(`Config`, `Preamp`, `EQ`, `Mix`, `Group`) (`Config`, `Preamp`, `EQ`, `Mix`, `Group`, `Send`)
### `Subclasses` ### `Subclasses`
@@ -288,6 +288,12 @@ tuple containing a class for each mute group
for example: `config.mute_group[0].on = True` for example: `config.mute_group[0].on = True`
### `Send`
- `level`: float, -inf to 10.0
for example: `mixer.strip[10].send[3].level = -16.5`
### XAirRemote class (lower level) ### XAirRemote class (lower level)
Send an OSC command directly to the mixer Send an OSC command directly to the mixer

View File

@@ -0,0 +1,24 @@
import logging
import xair_api
logging.basicConfig(level=logging.DEBUG)
def main():
with xair_api.connect("XR18", ip="mixer.local") as mixer:
for send in mixer.strip[0].send:
send.level = -22.8
mixer.strip[15].send[0].level = -16.5
print(mixer.strip[15].send[0].level)
mixer.auxreturn.send[0].level = -15.5
print(mixer.auxreturn.send[0].level)
mixer.fxreturn[0].send[0].level = -14.5
print(mixer.fxreturn[0].send[0].level)
if __name__ == "__main__":
main()

View File

@@ -1,12 +1,19 @@
import obsws_python as obs import obsws_python as obs
import xair_api import xair_api
class Observer: class Observer:
def __init__(self, mixer): def __init__(self, mixer):
self._mixer = mixer self._mixer = mixer
self._cl = obs.EventClient() self._client = obs.EventClient()
self._cl.callback.register(self.on_current_program_scene_changed) self._client.callback.register(self.on_current_program_scene_changed)
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
self._client.disconnect()
def on_current_program_scene_changed(self, data): def on_current_program_scene_changed(self, data):
scene = data.scene_name scene = data.scene_name
@@ -28,11 +35,9 @@ class Observer:
def main(): def main():
with xair_api.connect("MR18", ip="mixer.local") as mixer: with xair_api.connect("MR18", ip="mixer.local") as mixer:
Observer(mixer) with Observer(mixer):
while _ := input("Press <Enter> to exit\n"):
while cmd := input("<Enter> to exit\n"): pass
if not cmd:
break
if __name__ == "__main__": if __name__ == "__main__":

185
poetry.lock generated
View File

@@ -1,24 +1,53 @@
# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
[[package]] [[package]]
name = "attrs" name = "attrs"
version = "22.1.0" version = "22.1.0"
description = "Classes Without Boilerplate" description = "Classes Without Boilerplate"
category = "dev"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
files = [
{file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
{file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
]
[package.extras] [package.extras]
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
[[package]] [[package]]
name = "black" name = "black"
version = "22.8.0" version = "22.8.0"
description = "The uncompromising code formatter." description = "The uncompromising code formatter."
category = "dev"
optional = false optional = false
python-versions = ">=3.6.2" python-versions = ">=3.6.2"
files = [
{file = "black-22.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce957f1d6b78a8a231b18e0dd2d94a33d2ba738cd88a7fe64f53f659eea49fdd"},
{file = "black-22.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5107ea36b2b61917956d018bd25129baf9ad1125e39324a9b18248d362156a27"},
{file = "black-22.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8166b7bfe5dcb56d325385bd1d1e0f635f24aae14b3ae437102dedc0c186747"},
{file = "black-22.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd82842bb272297503cbec1a2600b6bfb338dae017186f8f215c8958f8acf869"},
{file = "black-22.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d839150f61d09e7217f52917259831fe2b689f5c8e5e32611736351b89bb2a90"},
{file = "black-22.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a05da0430bd5ced89176db098567973be52ce175a55677436a271102d7eaa3fe"},
{file = "black-22.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a098a69a02596e1f2a58a2a1c8d5a05d5a74461af552b371e82f9fa4ada8342"},
{file = "black-22.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5594efbdc35426e35a7defa1ea1a1cb97c7dbd34c0e49af7fb593a36bd45edab"},
{file = "black-22.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a983526af1bea1e4cf6768e649990f28ee4f4137266921c2c3cee8116ae42ec3"},
{file = "black-22.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b2c25f8dea5e8444bdc6788a2f543e1fb01494e144480bc17f806178378005e"},
{file = "black-22.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:78dd85caaab7c3153054756b9fe8c611efa63d9e7aecfa33e533060cb14b6d16"},
{file = "black-22.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cea1b2542d4e2c02c332e83150e41e3ca80dc0fb8de20df3c5e98e242156222c"},
{file = "black-22.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b879eb439094751185d1cfdca43023bc6786bd3c60372462b6f051efa6281a5"},
{file = "black-22.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a12e4e1353819af41df998b02c6742643cfef58282915f781d0e4dd7a200411"},
{file = "black-22.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3a73f66b6d5ba7288cd5d6dad9b4c9b43f4e8a4b789a94bf5abfb878c663eb3"},
{file = "black-22.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:e981e20ec152dfb3e77418fb616077937378b322d7b26aa1ff87717fb18b4875"},
{file = "black-22.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ce13ffed7e66dda0da3e0b2eb1bdfc83f5812f66e09aca2b0978593ed636b6c"},
{file = "black-22.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:32a4b17f644fc288c6ee2bafdf5e3b045f4eff84693ac069d87b1a347d861497"},
{file = "black-22.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ad827325a3a634bae88ae7747db1a395d5ee02cf05d9aa7a9bd77dfb10e940c"},
{file = "black-22.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53198e28a1fb865e9fe97f88220da2e44df6da82b18833b588b1883b16bb5d41"},
{file = "black-22.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:bc4d4123830a2d190e9cc42a2e43570f82ace35c3aeb26a512a2102bce5af7ec"},
{file = "black-22.8.0-py3-none-any.whl", hash = "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4"},
{file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"},
]
[package.dependencies] [package.dependencies]
click = ">=8.0.0" click = ">=8.0.0"
@@ -37,9 +66,12 @@ uvloop = ["uvloop (>=0.15.2)"]
name = "click" name = "click"
version = "8.1.3" version = "8.1.3"
description = "Composable command line interface toolkit" description = "Composable command line interface toolkit"
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
]
[package.dependencies] [package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""} colorama = {version = "*", markers = "platform_system == \"Windows\""}
@@ -48,47 +80,62 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
name = "colorama" name = "colorama"
version = "0.4.5" version = "0.4.5"
description = "Cross-platform colored terminal text." description = "Cross-platform colored terminal text."
category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
files = [
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
]
[[package]] [[package]]
name = "iniconfig" name = "iniconfig"
version = "1.1.1" version = "1.1.1"
description = "iniconfig: brain-dead simple config-ini parsing" description = "iniconfig: brain-dead simple config-ini parsing"
category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
[[package]] [[package]]
name = "isort" name = "isort"
version = "5.10.1" version = "5.10.1"
description = "A Python utility / library to sort Python imports." description = "A Python utility / library to sort Python imports."
category = "dev"
optional = false optional = false
python-versions = ">=3.6.1,<4.0" python-versions = ">=3.6.1,<4.0"
files = [
{file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
{file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
]
[package.extras] [package.extras]
pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
requirements_deprecated_finder = ["pipreqs", "pip-api"]
colors = ["colorama (>=0.4.3,<0.5.0)"] colors = ["colorama (>=0.4.3,<0.5.0)"]
pipfile-deprecated-finder = ["pipreqs", "requirementslib"]
plugins = ["setuptools"] plugins = ["setuptools"]
requirements-deprecated-finder = ["pip-api", "pipreqs"]
[[package]] [[package]]
name = "mypy-extensions" name = "mypy-extensions"
version = "0.4.3" version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker." description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "21.3" version = "21.3"
description = "Core utilities for Python packages" description = "Core utilities for Python packages"
category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
files = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
]
[package.dependencies] [package.dependencies]
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
@@ -97,29 +144,38 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
name = "pathspec" name = "pathspec"
version = "0.10.1" version = "0.10.1"
description = "Utility library for gitignore style pattern matching of file paths." description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [
{file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"},
{file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"},
]
[[package]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "2.5.2" version = "2.5.2"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
]
[package.extras] [package.extras]
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"]
test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
[[package]] [[package]]
name = "pluggy" name = "pluggy"
version = "1.0.0" version = "1.0.0"
description = "plugin and hook calling mechanisms for python" description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
files = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
[package.extras] [package.extras]
dev = ["pre-commit", "tox"] dev = ["pre-commit", "tox"]
@@ -129,28 +185,37 @@ testing = ["pytest", "pytest-benchmark"]
name = "py" name = "py"
version = "1.11.0" version = "1.11.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities" description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
files = [
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
]
[[package]] [[package]]
name = "pyparsing" name = "pyparsing"
version = "3.0.9" version = "3.0.9"
description = "pyparsing module - Classes and methods to define and execute parsing grammars" description = "pyparsing module - Classes and methods to define and execute parsing grammars"
category = "dev"
optional = false optional = false
python-versions = ">=3.6.8" python-versions = ">=3.6.8"
files = [
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
]
[package.extras] [package.extras]
diagrams = ["railroad-diagrams", "jinja2"] diagrams = ["jinja2", "railroad-diagrams"]
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "7.1.3" version = "7.1.3"
description = "pytest: simple powerful testing with Python" description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [
{file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"},
{file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"},
]
[package.dependencies] [package.dependencies]
attrs = ">=19.2.0" attrs = ">=19.2.0"
@@ -168,9 +233,12 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.
name = "pytest-randomly" name = "pytest-randomly"
version = "3.12.0" version = "3.12.0"
description = "Pytest plugin to randomly order tests and control random.seed." description = "Pytest plugin to randomly order tests and control random.seed."
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [
{file = "pytest-randomly-3.12.0.tar.gz", hash = "sha256:d60c2db71ac319aee0fc6c4110a7597d611a8b94a5590918bfa8583f00caccb2"},
{file = "pytest_randomly-3.12.0-py3-none-any.whl", hash = "sha256:f4f2e803daf5d1ba036cc22bf4fe9dbbf99389ec56b00e5cba732fb5c1d07fdd"},
]
[package.dependencies] [package.dependencies]
pytest = "*" pytest = "*"
@@ -179,74 +247,25 @@ pytest = "*"
name = "python-osc" name = "python-osc"
version = "1.8.0" version = "1.8.0"
description = "Open Sound Control server and client implementations in pure Python" description = "Open Sound Control server and client implementations in pure Python"
category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [
{file = "python-osc-1.8.0.tar.gz", hash = "sha256:2f8c187c68d239960fb2eddcb5346a62a9b35e64f2de045b3e5e509f475ca73d"},
{file = "python_osc-1.8.0-py3-none-any.whl", hash = "sha256:9e2abb2fc9ba2c356f8e951609a03c9c7017bf0bad82cca8490e9b8af9e92a0b"},
]
[[package]] [[package]]
name = "tomli" name = "tomli"
version = "2.0.1" version = "2.0.1"
description = "A lil' TOML parser" description = "A lil' TOML parser"
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [
[metadata]
lock-version = "1.1"
python-versions = "^3.10"
content-hash = "80440f75f4191b46dc73824fbfc4fd2fc1ea4dfbdba08591cabb600a86ae2400"
[metadata.files]
attrs = []
black = []
click = [
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
]
colorama = [
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
isort = [
{file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
{file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
packaging = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
]
pathspec = []
platformdirs = [
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
]
pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
py = [
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
]
pyparsing = [
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
]
pytest = []
pytest-randomly = [
{file = "pytest-randomly-3.12.0.tar.gz", hash = "sha256:d60c2db71ac319aee0fc6c4110a7597d611a8b94a5590918bfa8583f00caccb2"},
{file = "pytest_randomly-3.12.0-py3-none-any.whl", hash = "sha256:f4f2e803daf5d1ba036cc22bf4fe9dbbf99389ec56b00e5cba732fb5c1d07fdd"},
]
python-osc = []
tomli = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
] ]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "def96d1658f870a9820fef363ee6a04455f1d895e15a189ea4f39801f168552f"

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "xair-api" name = "xair-api"
version = "2.2.2" version = "2.3.1"
description = "Remote control Behringer X-Air | Midas MR mixers through OSC" description = "Remote control Behringer X-Air | Midas MR mixers through OSC"
authors = ["onyx-and-iris <code@onyxandiris.online>"] authors = ["onyx-and-iris <code@onyxandiris.online>"]
license = "MIT" license = "MIT"
@@ -12,7 +12,7 @@ python = "^3.10"
python-osc = "^1.8.0" python-osc = "^1.8.0"
tomli = { version = "^2.0.1", python = "<3.11" } tomli = { version = "^2.0.1", python = "<3.11" }
[tool.poetry.dev-dependencies] [tool.poetry.group.dev.dependencies]
pytest = "^7.1.2" pytest = "^7.1.2"
pytest-randomly = "^3.12.0" pytest-randomly = "^3.12.0"
black = "^22.6.0" black = "^22.6.0"
@@ -24,5 +24,6 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts] [tool.poetry.scripts]
obs = "scripts:ex_obs" obs = "scripts:ex_obs"
sends = "scripts:ex_sends"
xair = "scripts:test_xair" xair = "scripts:test_xair"
x32 = "scripts:test_x32" x32 = "scripts:test_x32"

View File

@@ -1,10 +1,16 @@
import subprocess import subprocess
import sys
from pathlib import Path from pathlib import Path
def ex_obs(): def ex_obs():
path = Path.cwd() / "examples" / "xair_obs" / "." path = Path.cwd() / "examples" / "xair_obs" / "."
subprocess.run(["py", str(path)]) subprocess.run([sys.executable, str(path)])
def ex_sends():
path = Path.cwd() / "examples" / "sends" / "."
subprocess.run([sys.executable, str(path)])
def test_xair(): def test_xair():

View File

@@ -25,13 +25,13 @@ class FxRtn(IFxRtn):
class MainStereo(ILR): class MainStereo(ILR):
@property @property
def address(self) -> str: def address(self) -> str:
return f"/main/st" return "/main/st"
class MainMono(ILR): class MainMono(ILR):
@property @property
def address(self) -> str: def address(self) -> str:
return f"/main/m" return "/main/m"
class Matrix(ILR): class Matrix(ILR):

View File

@@ -1,8 +1,10 @@
import abc import abc
import logging
from .errors import XAirRemoteError
from .meta import mute_prop from .meta import mute_prop
from .shared import EQ, GEQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp from .shared import EQ, GEQ, Config, Dyn, Group, Insert, Mix
logger = logging.getLogger(__name__)
class IBus(abc.ABC): class IBus(abc.ABC):
@@ -11,10 +13,10 @@ class IBus(abc.ABC):
def __init__(self, remote, index: int): def __init__(self, remote, index: int):
self._remote = remote self._remote = remote
self.index = index + 1 self.index = index + 1
self.logger = logger.getChild(self.__class__.__name__)
def getter(self, param: str): def getter(self, param: str):
self._remote.send(f"{self.address}/{param}") return self._remote.query(f"{self.address}/{param}")
return self._remote.info_response
def setter(self, param: str, val: int): def setter(self, param: str, val: int):
self._remote.send(f"{self.address}/{param}", val) self._remote.send(f"{self.address}/{param}", val)

View File

@@ -1,9 +1,10 @@
import abc import abc
import logging
from . import kinds from . import kinds, util
from .errors import XAirRemoteError
from .meta import bool_prop from .meta import bool_prop
from .util import _get_level_val, _set_level_val, lin_get, lin_set
logger = logging.getLogger(__name__)
class IConfig(abc.ABC): class IConfig(abc.ABC):
@@ -11,10 +12,10 @@ class IConfig(abc.ABC):
def __init__(self, remote): def __init__(self, remote):
self._remote = remote self._remote = remote
self.logger = logger.getChild(self.__class__.__name__)
def getter(self, param: str): def getter(self, param: str):
self._remote.send(f"{self.address}/{param}") return self._remote.query(f"{self.address}/{param}")
return self._remote.info_response
def setter(self, param: str, val: int): def setter(self, param: str, val: int):
self._remote.send(f"{self.address}/{param}", val) self._remote.send(f"{self.address}/{param}", val)
@@ -35,8 +36,8 @@ class Config(IConfig):
Returns a Config class of a kind. Returns a Config class of a kind.
""" """
LINKS_cls = _make_links_mixins[remote.kind.id_] LINKS_cls = _make_links_mixins[remote.kind.id_]
MUTEGROUP_cls = type(f"MuteGroup", (Config.MuteGroup, cls), {}) MUTEGROUP_cls = type("MuteGroup", (Config.MuteGroup, cls), {})
MONITOR_cls = type(f"ConfigMonitor", (Config.Monitor, cls), {}) MONITOR_cls = type("ConfigMonitor", (Config.Monitor, cls), {})
CONFIG_cls = type( CONFIG_cls = type(
f"Config{remote.kind}", f"Config{remote.kind}",
(cls, LINKS_cls), (cls, LINKS_cls),
@@ -49,7 +50,7 @@ class Config(IConfig):
@property @property
def address(self) -> str: def address(self) -> str:
return f"/config" return "/config"
@property @property
def amixenable(self) -> bool: def amixenable(self) -> bool:
@@ -92,13 +93,14 @@ class Config(IConfig):
return f"{root}/solo" return f"{root}/solo"
@property @property
@util.db_from
def level(self) -> float: def level(self) -> float:
retval = self.getter("level")[0] return self.getter("level")[0]
return _get_level_val(retval)
@level.setter @level.setter
@util.db_to
def level(self, val: float): def level(self, val: float):
_set_level_val(self, val) self.setter("level", val)
@property @property
def source(self) -> int: def source(self) -> int:
@@ -106,17 +108,19 @@ class Config(IConfig):
@source.setter @source.setter
def source(self, val: int): def source(self, val: int):
self.setter(f"source", val) self.setter("source", val)
@property @property
def sourcetrim(self) -> float: def sourcetrim(self) -> float:
return round(lin_get(-18, 18, self.getter("sourcetrim")[0]), 1) return round(util.lin_get(-18, 18, self.getter("sourcetrim")[0]), 1)
@sourcetrim.setter @sourcetrim.setter
def sourcetrim(self, val: float): def sourcetrim(self, val: float):
if not -18 <= val <= 18: if not -18 <= val <= 18:
raise XAirRemoteError("expected value in range -18.0 to 18.0") self.logger.warning(
self.setter("sourcetrim", lin_set(-18, 18, val)) f"sourcetrim got {val}, expected value in range -18.0 to 18.0"
)
self.setter("sourcetrim", util.lin_set(-18, 18, val))
@property @property
def chmode(self) -> bool: def chmode(self) -> bool:
@@ -136,13 +140,15 @@ class Config(IConfig):
@property @property
def dimgain(self) -> int: def dimgain(self) -> int:
return int(lin_get(-40, 0, self.getter("dimatt")[0])) return int(util.lin_get(-40, 0, self.getter("dimatt")[0]))
@dimgain.setter @dimgain.setter
def dimgain(self, val: int): def dimgain(self, val: int):
if not -40 <= val <= 0: if not -40 <= val <= 0:
raise XAirRemoteError("expected value in range -40 to 0") self.logger.warning(
self.setter("dimatt", lin_set(-40, 0, val)) f"dimgain got {val}, expected value in range -40 to 0"
)
self.setter("dimatt", util.lin_set(-40, 0, val))
@property @property
def dim(self) -> bool: def dim(self) -> bool:

View File

@@ -1,6 +1,7 @@
import abc import abc
import logging
from .errors import XAirRemoteError logger = logging.getLogger(__name__)
class IDCA(abc.ABC): class IDCA(abc.ABC):
@@ -9,10 +10,10 @@ class IDCA(abc.ABC):
def __init__(self, remote, index: int): def __init__(self, remote, index: int):
self._remote = remote self._remote = remote
self.index = index + 1 self.index = index + 1
self.logger = logger.getChild(self.__class__.__name__)
def getter(self, param: str) -> tuple: def getter(self, param: str) -> tuple:
self._remote.send(f"{self.address}/{param}") return self._remote.query(f"{self.address}/{param}")
return self._remote.info_response
def setter(self, param: str, val: int): def setter(self, param: str, val: int):
self._remote.send(f"{self.address}/{param}", val) self._remote.send(f"{self.address}/{param}", val)
@@ -51,7 +52,7 @@ class DCA(IDCA):
@name.setter @name.setter
def name(self, val: str): def name(self, val: str):
self.setter("config/name")[0] self.setter("config/name", val)
@property @property
def color(self) -> int: def color(self) -> int:

View File

@@ -1,4 +1,2 @@
class XAirRemoteError(Exception): class XAirRemoteError(Exception):
"""Base error class for XAIR Remote.""" """Base error class for XAIR Remote."""
pass

View File

@@ -1,8 +1,10 @@
import abc import abc
import logging
from .errors import XAirRemoteError
from .meta import mute_prop from .meta import mute_prop
from .shared import EQ, GEQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp from .shared import Config, Group, Mix
logger = logging.getLogger(__name__)
class IFX(abc.ABC): class IFX(abc.ABC):
@@ -11,10 +13,10 @@ class IFX(abc.ABC):
def __init__(self, remote, index: int): def __init__(self, remote, index: int):
self._remote = remote self._remote = remote
self.index = index + 1 self.index = index + 1
self.logger = logger.getChild(self.__class__.__name__)
def getter(self, param: str): def getter(self, param: str):
self._remote.send(f"{self.address}/{param}") return self._remote.query(f"{self.address}/{param}")
return self._remote.info_response
def setter(self, param: str, val: int): def setter(self, param: str, val: int):
self._remote.send(f"{self.address}/{param}", val) self._remote.send(f"{self.address}/{param}", val)

View File

@@ -1,22 +1,24 @@
import abc import abc
import logging
from typing import Optional from typing import Optional
from .errors import XAirRemoteError
from .meta import mute_prop from .meta import mute_prop
from .shared import EQ, GEQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp from .shared import EQ, GEQ, Config, Dyn, Insert, Mix
logger = logging.getLogger(__name__)
class ILR(abc.ABC): class ILR(abc.ABC):
"""Abstract Base Class for buses""" """Abstract Base Class for lr"""
def __init__(self, remote, index: Optional[int] = None): def __init__(self, remote, index: Optional[int] = None):
self._remote = remote self._remote = remote
if index is not None: if index is not None:
self.index = index + 1 self.index = index + 1
self.logger = logger.getChild(self.__class__.__name__)
def getter(self, param: str): def getter(self, param: str):
self._remote.send(f"{self.address}/{param}") return self._remote.query(f"{self.address}/{param}")
return self._remote.info_response
def setter(self, param: str, val: int): def setter(self, param: str, val: int):
self._remote.send(f"{self.address}/{param}", val) self._remote.send(f"{self.address}/{param}", val)
@@ -27,7 +29,7 @@ class ILR(abc.ABC):
class LR(ILR): class LR(ILR):
"""Concrete class for buses""" """Concrete class for lr"""
@classmethod @classmethod
def make(cls, remote, index=None): def make(cls, remote, index=None):
@@ -62,4 +64,4 @@ class LR(ILR):
@property @property
def address(self) -> str: def address(self) -> str:
return f"/lr" return "/lr"

View File

@@ -1,4 +1,3 @@
from .errors import XAirRemoteError
from .util import lin_get, lin_set from .util import lin_get, lin_set
@@ -51,13 +50,6 @@ def float_prop(param):
def geq_prop(param): def geq_prop(param):
# fmt: off
opts = {
"1k": 1000, "1k25": 1250, "1k6": 1600, "2k": 2000, "3k15": 3150, "4k": 4000,
"5k": 5000, "6k3": 6300, "8k": 8000, "10k": 10000, "12k5": 12500, "16k": 16000,
"20k": 20000,
}
# fmt: on
param = param.replace("_", ".") param = param.replace("_", ".")
def fget(self) -> float: def fget(self) -> float:
@@ -65,7 +57,9 @@ def geq_prop(param):
def fset(self, val): def fset(self, val):
if not -15 <= val <= 15: if not -15 <= val <= 15:
raise XAirRemoteError("expected value in range -15.0 to 15.0") self.logger.warning(
f"slider_{param} got {val}, expected value in range -15.0 to 15.0"
)
self.setter(param, lin_set(-15, 15, val)) self.setter(param, lin_set(-15, 15, val))
return property(fget, fset) return property(fget, fset)

View File

@@ -1,22 +1,24 @@
import abc import abc
import logging
from typing import Optional from typing import Optional
from .errors import XAirRemoteError
from .meta import mute_prop from .meta import mute_prop
from .shared import EQ, GEQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp from .shared import EQ, Config, Group, Mix, Preamp, Send
logger = logging.getLogger(__name__)
class IRtn(abc.ABC): class IRtn(abc.ABC):
"""Abstract Base Class for aux""" """Abstract Base Class for rtn"""
def __init__(self, remote, index: Optional[int] = None): def __init__(self, remote, index: Optional[int] = None):
self._remote = remote self._remote = remote
if index is not None: if index is not None:
self.index = index + 1 self.index = index + 1
self.logger = logger.getChild(self.__class__.__name__)
def getter(self, param: str): def getter(self, param: str):
self._remote.send(f"{self.address}/{param}") return self._remote.query(f"{self.address}/{param}")
return self._remote.info_response
def setter(self, param: str, val: int): def setter(self, param: str, val: int):
self._remote.send(f"{self.address}/{param}", val) self._remote.send(f"{self.address}/{param}", val)
@@ -27,13 +29,15 @@ class IRtn(abc.ABC):
class AuxRtn(IRtn): class AuxRtn(IRtn):
"""Concrete class for aux""" """Concrete class for auxrtn"""
@classmethod @classmethod
def make(cls, remote, index=None): def make(cls, remote, index=None):
""" """
Factory function for auxrtn Factory function for auxrtn
Creates a mixin of shared subclasses, sets them as class attributes. Creates a mixin of shared subclasses, sets them as class attributes.
Returns an AuxRtn class of a kind. Returns an AuxRtn class of a kind.
""" """
AUXRTN_cls = type( AUXRTN_cls = type(
@@ -52,6 +56,10 @@ class AuxRtn(IRtn):
Group, Group,
) )
}, },
"send": tuple(
Send.make(cls, i, remote)
for i in range(remote.kind.num_bus + remote.kind.num_fx)
),
"mute": mute_prop(), "mute": mute_prop(),
}, },
) )
@@ -63,13 +71,15 @@ class AuxRtn(IRtn):
class FxRtn(IRtn): class FxRtn(IRtn):
"""Concrete class for rtn""" """Concrete class for fxrtn"""
@classmethod @classmethod
def make(cls, remote, index): def make(cls, remote, index):
""" """
Factory function for fxrtn Factory function for fxrtn
Creates a mixin of shared subclasses, sets them as class attributes. Creates a mixin of shared subclasses, sets them as class attributes.
Returns an FxRtn class of a kind. Returns an FxRtn class of a kind.
""" """
FXRTN_cls = type( FXRTN_cls = type(
@@ -88,6 +98,10 @@ class FxRtn(IRtn):
Group, Group,
) )
}, },
"send": tuple(
Send.make(cls, i, remote, index)
for i in range(remote.kind.num_bus + remote.kind.num_fx)
),
"mute": mute_prop(), "mute": mute_prop(),
}, },
) )

View File

@@ -1,8 +1,7 @@
from typing import Union from typing import Optional, Union
from .errors import XAirRemoteError from . import util
from .meta import geq_prop from .meta import geq_prop
from .util import _get_fader_val, _set_fader_val, lin_get, lin_set, log_get, log_set
""" """
Classes shared by /ch, /rtn, /rtn/aux, /bus, /fxsend, /lr Classes shared by /ch, /rtn, /rtn/aux, /bus, /fxsend, /lr
@@ -56,13 +55,15 @@ class Preamp:
@property @property
def usbtrim(self) -> float: def usbtrim(self) -> float:
return round(lin_get(-18, 18, self.getter("rtntrim")[0]), 1) return round(util.lin_get(-18, 18, self.getter("rtntrim")[0]), 1)
@usbtrim.setter @usbtrim.setter
def usbtrim(self, val: float): def usbtrim(self, val: float):
if not -18 <= val <= 18: if not -18 <= val <= 18:
raise XAirRemoteError("expected value in range -18.0 to 18.0") self.logger.warning(
self.setter("rtntrim", lin_set(-18, 18, val)) f"usbtrim got {val}, expected value in range -18.0 to 18.0"
)
self.setter("rtntrim", util.lin_set(-18, 18, val))
@property @property
def usbinput(self) -> bool: def usbinput(self) -> bool:
@@ -90,13 +91,15 @@ class Preamp:
@property @property
def highpassfilter(self) -> int: def highpassfilter(self) -> int:
return int(log_get(20, 400, self.getter("hpf")[0])) return int(util.log_get(20, 400, self.getter("hpf")[0]))
@highpassfilter.setter @highpassfilter.setter
def highpassfilter(self, val: int): def highpassfilter(self, val: int):
if not 20 <= val <= 400: if not 20 <= val <= 400:
raise XAirRemoteError("expected value in range 20 to 400") self.logger.warning(
self.setter("hpf", log_set(20, 400, val)) f"highpassfilter got {val}, expected value in range 20 to 400"
)
self.setter("hpf", util.log_set(20, 400, val))
class Gate: class Gate:
@@ -122,59 +125,63 @@ class Gate:
def mode(self, val: str): def mode(self, val: str):
opts = ("gate", "exp2", "exp3", "exp4", "duck") opts = ("gate", "exp2", "exp3", "exp4", "duck")
if val not in opts: if val not in opts:
raise XAirRemoteError(f"expected one of {opts}") self.logger.warning(f"mode got {val}, expected one of {opts}")
self.setter("mode", opts.index(val)) self.setter("mode", opts.index(val))
@property @property
def threshold(self) -> float: def threshold(self) -> float:
return round(lin_get(-80, 0, self.getter("thr")[0]), 1) return round(util.lin_get(-80, 0, self.getter("thr")[0]), 1)
@threshold.setter @threshold.setter
def threshold(self, val: float): def threshold(self, val: float):
if not -80 <= val <= 0: if not -80 <= val <= 0:
raise XAirRemoteError("expected value in range -80.0 to 0.0") self.logger.warning(
self.setter("thr", lin_set(-80, 0, val)) f"threshold got {val}, expected value in range -80.0 to 0.0"
)
self.setter("thr", util.lin_set(-80, 0, val))
@property @property
def range(self) -> int: def range(self) -> int:
return int(lin_get(3, 60, self.getter("range")[0])) return int(util.lin_get(3, 60, self.getter("range")[0]))
@range.setter @range.setter
def range(self, val: int): def range(self, val: int):
if not 3 <= val <= 60: if not 3 <= val <= 60:
raise XAirRemoteError("expected value in range 3 to 60") self.logger.warning(f"range got {val}, expected value in range 3 to 60")
self.setter("range", lin_set(3, 60, val)) self.setter("range", util.lin_set(3, 60, val))
@property @property
def attack(self) -> int: def attack(self) -> int:
return int(lin_get(0, 120, self.getter("attack")[0])) return int(util.lin_get(0, 120, self.getter("attack")[0]))
@attack.setter @attack.setter
def attack(self, val: int): def attack(self, val: int):
if not 0 <= val <= 120: if not 0 <= val <= 120:
raise XAirRemoteError("expected value in range 0 to 120") self.logger.warning(f"attack got {val}, expected value in range 0 to 120")
self.setter("attack", lin_set(0, 120, val)) self.setter("attack", util.lin_set(0, 120, val))
@property @property
def hold(self) -> Union[float, int]: def hold(self) -> Union[float, int]:
val = log_get(0.02, 2000, self.getter("hold")[0]) val = util.log_get(0.02, 2000, self.getter("hold")[0])
return round(val, 1) if val < 100 else int(val) return round(val, 1) if val < 100 else int(val)
@hold.setter @hold.setter
def hold(self, val: float): def hold(self, val: float):
if not 0.02 <= val <= 2000: if not 0.02 <= val <= 2000:
raise XAirRemoteError("expected value in range 0.02 to 2000.0") self.logger.warning(
self.setter("hold", log_set(0.02, 2000, val)) f"hold got {val}, expected value in range 0.02 to 2000.0"
)
self.setter("hold", util.log_set(0.02, 2000, val))
@property @property
def release(self) -> int: def release(self) -> int:
return int(log_get(5, 4000, self.getter("release")[0])) return int(util.log_get(5, 4000, self.getter("release")[0]))
@release.setter @release.setter
def release(self, val: int): def release(self, val: int):
if not 5 <= val <= 4000: if not 5 <= val <= 4000:
raise XAirRemoteError("expected value in range 5 to 4000") self.logger.warning(f"release got {val}, expected value in range 5 to 4000")
self.setter("release", log_set(5, 4000, val)) self.setter("release", util.log_set(5, 4000, val))
@property @property
def keysource(self): def keysource(self):
@@ -202,14 +209,16 @@ class Gate:
@property @property
def filterfreq(self) -> Union[float, int]: def filterfreq(self) -> Union[float, int]:
retval = log_get(20, 20000, self.getter("filter/f")[0]) retval = util.log_get(20, 20000, self.getter("filter/f")[0])
return int(retval) if retval > 1000 else round(retval, 1) return int(retval) if retval > 1000 else round(retval, 1)
@filterfreq.setter @filterfreq.setter
def filterfreq(self, val: Union[float, int]): def filterfreq(self, val: Union[float, int]):
if not 20 <= val <= 20000: if not 20 <= val <= 20000:
raise XAirRemoteError("expected value in range 20 to 20000") self.logger.warning(
self.setter("filter/f", log_set(20, 20000, val)) f"filterfreq got {val}, expected value in range 20 to 20000"
)
self.setter("filter/f", util.log_set(20, 20000, val))
class Dyn: class Dyn:
@@ -235,7 +244,7 @@ class Dyn:
def mode(self, val: str): def mode(self, val: str):
opts = ("comp", "exp") opts = ("comp", "exp")
if val not in opts: if val not in opts:
raise XAirRemoteError(f"expected one of {opts}") self.logger.warning(f"mode got {val}, expected one of {opts}")
self.setter("mode", opts.index(val)) self.setter("mode", opts.index(val))
@property @property
@@ -247,7 +256,7 @@ class Dyn:
def det(self, val: str): def det(self, val: str):
opts = ("peak", "rms") opts = ("peak", "rms")
if val not in opts: if val not in opts:
raise XAirRemoteError(f"expected one of {opts}") self.logger.warning(f"det got {val}, expected one of {opts}")
self.setter("det", opts.index(val)) self.setter("det", opts.index(val))
@property @property
@@ -259,18 +268,20 @@ class Dyn:
def env(self, val: str): def env(self, val: str):
opts = ("lin", "log") opts = ("lin", "log")
if val not in opts: if val not in opts:
raise XAirRemoteError(f"expected one of {opts}") self.logger.warning(f"env got {val}, expected one of {opts}")
self.setter("env", opts.index(val)) self.setter("env", opts.index(val))
@property @property
def threshold(self) -> float: def threshold(self) -> float:
return round(lin_get(-60, 0, self.getter("thr")[0]), 1) return round(util.lin_get(-60, 0, self.getter("thr")[0]), 1)
@threshold.setter @threshold.setter
def threshold(self, val: float): def threshold(self, val: float):
if not -60 <= val <= 0: if not -60 <= val <= 0:
raise XAirRemoteError("expected value in range -60.0 to 0") self.logger.warning(
self.setter("thr", lin_set(-60, 0, val)) f"threshold got {val}, expected value in range -60.0 to 0"
)
self.setter("thr", util.lin_set(-60, 0, val))
@property @property
def ratio(self) -> Union[float, int]: def ratio(self) -> Union[float, int]:
@@ -283,64 +294,66 @@ class Dyn:
@property @property
def knee(self) -> int: def knee(self) -> int:
return int(lin_get(0, 5, self.getter("knee")[0])) return int(util.lin_get(0, 5, self.getter("knee")[0]))
@knee.setter @knee.setter
def knee(self, val: int): def knee(self, val: int):
if not 0 <= val <= 5: if not 0 <= val <= 5:
raise XAirRemoteError("expected value in range 0 to 5") self.logger.warning(f"knee got {val}, expected value in range 0 to 5")
self.setter("knee", lin_set(0, 5, val)) self.setter("knee", util.lin_set(0, 5, val))
@property @property
def mgain(self) -> float: def mgain(self) -> float:
return round(lin_get(0, 24, self.getter("mgain")[0]), 1) return round(util.lin_get(0, 24, self.getter("mgain")[0]), 1)
@mgain.setter @mgain.setter
def mgain(self, val: float): def mgain(self, val: float):
if not 0 <= val <= 24: if not 0 <= val <= 24:
raise XAirRemoteError("expected value in range 0.0 to 24.0") self.logger.warning(f"mgain got {val}, expected value in range 0.0 to 24.0")
self.setter("mgain", lin_set(0, 24, val)) self.setter("mgain", util.lin_set(0, 24, val))
@property @property
def attack(self) -> int: def attack(self) -> int:
return int(lin_get(0, 120, self.getter("attack")[0])) return int(util.lin_get(0, 120, self.getter("attack")[0]))
@attack.setter @attack.setter
def attack(self, val: int): def attack(self, val: int):
if not 0 <= val <= 120: if not 0 <= val <= 120:
raise XAirRemoteError("expected value in range 0 to 120") self.logger.warning(f"attack got {val}, expected value in range 0 to 120")
self.setter("attack", lin_set(0, 120, val)) self.setter("attack", util.lin_set(0, 120, val))
@property @property
def hold(self) -> Union[float, int]: def hold(self) -> Union[float, int]:
val = log_get(0.02, 2000, self.getter("hold")[0]) val = util.log_get(0.02, 2000, self.getter("hold")[0])
return round(val, 1) if val < 100 else int(val) return round(val, 1) if val < 100 else int(val)
@hold.setter @hold.setter
def hold(self, val: float): def hold(self, val: float):
if not 0.02 <= val <= 2000: if not 0.02 <= val <= 2000:
raise XAirRemoteError("expected value in range 0.02 to 2000.0") self.logger.warning(
self.setter("hold", log_set(0.02, 2000, val)) f"hold got {val}, expected value in range 0.02 to 2000.0"
)
self.setter("hold", util.log_set(0.02, 2000, val))
@property @property
def release(self) -> int: def release(self) -> int:
return int(log_get(5, 4000, self.getter("release")[0])) return int(util.log_get(5, 4000, self.getter("release")[0]))
@release.setter @release.setter
def release(self, val: int): def release(self, val: int):
if not 5 <= val <= 4000: if not 5 <= val <= 4000:
raise XAirRemoteError("expected value in range 5 to 4000") self.logger.warning(f"release got {val}, expected value in range 5 to 4000")
self.setter("release", log_set(5, 4000, val)) self.setter("release", util.log_set(5, 4000, val))
@property @property
def mix(self) -> int: def mix(self) -> int:
return int(lin_get(0, 100, self.getter("mix")[0])) return int(util.lin_get(0, 100, self.getter("mix")[0]))
@mix.setter @mix.setter
def mix(self, val: int): def mix(self, val: int):
if not 0 <= val <= 100: if not 0 <= val <= 100:
raise XAirRemoteError("expected value in range 0 to 100") self.logger.warning(f"mix got {val}, expected value in range 0 to 100")
self.setter("mix", lin_set(0, 100, val)) self.setter("mix", util.lin_set(0, 100, val))
@property @property
def keysource(self): def keysource(self):
@@ -376,14 +389,16 @@ class Dyn:
@property @property
def filterfreq(self) -> Union[float, int]: def filterfreq(self) -> Union[float, int]:
retval = log_get(20, 20000, self.getter("filter/f")[0]) retval = util.log_get(20, 20000, self.getter("filter/f")[0])
return int(retval) if retval > 1000 else round(retval, 1) return int(retval) if retval > 1000 else round(retval, 1)
@filterfreq.setter @filterfreq.setter
def filterfreq(self, val: Union[float, int]): def filterfreq(self, val: Union[float, int]):
if not 20 <= val <= 20000: if not 20 <= val <= 20000:
raise XAirRemoteError("expected value in range 20 to 20000") self.logger.warning(
self.setter("filter/f", log_set(20, 20000, val)) f"filterfreq got {val}, expected value in range 20 to 20000"
)
self.setter("filter/f", util.log_set(20, 20000, val))
class Insert: class Insert:
@@ -462,14 +477,11 @@ class EQ:
def mode(self, val: str): def mode(self, val: str):
opts = ("peq", "geq", "teq") opts = ("peq", "geq", "teq")
if val not in opts: if val not in opts:
raise XAirRemoteError(f"expected one of {opts}") self.logger.warning(f"mode got {val}, expected one of {opts}")
self.setter("mode", opts.index(val)) self.setter("mode", opts.index(val))
class EQBand: class EQBand:
def __init__(self, i, remote, index): def __init__(self, i, remote, index):
if index is None:
super(EQ.EQBand, self).__init__(remote)
else:
super(EQ.EQBand, self).__init__(remote, index) super(EQ.EQBand, self).__init__(remote, index)
self.i = i self.i = i
@@ -484,39 +496,45 @@ class EQ:
@type.setter @type.setter
def type(self, val: int): def type(self, val: int):
self.setter(f"type", val) self.setter("type", val)
@property @property
def frequency(self) -> float: def frequency(self) -> float:
retval = log_get(20, 20000, self.getter("f")[0]) retval = util.log_get(20, 20000, self.getter("f")[0])
return round(retval, 1) return round(retval, 1)
@frequency.setter @frequency.setter
def frequency(self, val: float): def frequency(self, val: float):
if not 20 <= val <= 20000: if not 20 <= val <= 20000:
raise XAirRemoteError("expected value in range 20.0 to 20000.0") self.logger.warning(
self.setter("f", log_set(20, 20000, val)) f"frequency got {val}, expected value in range 20.0 to 20000.0"
)
self.setter("f", util.log_set(20, 20000, val))
@property @property
def gain(self) -> float: def gain(self) -> float:
return round(lin_get(-15, 15, self.getter("g")[0]), 1) return round(util.lin_get(-15, 15, self.getter("g")[0]), 1)
@gain.setter @gain.setter
def gain(self, val: float): def gain(self, val: float):
if not -15 <= val <= 15: if not -15 <= val <= 15:
raise XAirRemoteError("expected value in range -15.0 to 15.0") self.logger.warning(
self.setter("g", lin_set(-15, 15, val)) f"gain got {val}, expected value in range -15.0 to 15.0"
)
self.setter("g", util.lin_set(-15, 15, val))
@property @property
def quality(self) -> float: def quality(self) -> float:
retval = log_get(0.3, 10, self.getter("q")[0]) retval = util.log_get(0.3, 10, self.getter("q")[0])
return round(retval, 1) return round(retval, 1)
@quality.setter @quality.setter
def quality(self, val: float): def quality(self, val: float):
if not 0.3 <= val <= 10: if not 0.3 <= val <= 10:
raise XAirRemoteError("expected value in range 0.3 to 10.0") self.logger.warning(
self.setter("q", log_set(0.3, 10, val)) f"quality got {val}, expected value in range 0.3 to 10.0"
)
self.setter("q", util.log_set(0.3, 10, val))
class GEQ: class GEQ:
@@ -531,7 +549,7 @@ class GEQ:
f"slider_{param}": geq_prop(param) f"slider_{param}": geq_prop(param)
for param in [ for param in [
"20", "25", "31_5", "40", "50", "63", "80", "100", "125", "20", "25", "31_5", "40", "50", "63", "80", "100", "125",
"160", "200", "250", "315" "400", "500", "630", "800", "1k", "160", "200", "250", "315", "400", "500", "630", "800", "1k",
"1k25", "1k6", "2k", "2k5", "3k15", "4k", "5k", "6k3", "8k", "1k25", "1k6", "2k", "2k5", "3k15", "4k", "5k", "6k3", "8k",
"10k", "12k5", "16k", "20k", "10k", "12k5", "16k", "20k",
] ]
@@ -561,13 +579,14 @@ class Mix:
self.setter("on", 1 if val else 0) self.setter("on", 1 if val else 0)
@property @property
@util.db_from
def fader(self) -> float: def fader(self) -> float:
retval = self.getter("fader")[0] return self.getter("fader")[0]
return _get_fader_val(retval)
@fader.setter @fader.setter
@util.db_to
def fader(self, val: float): def fader(self, val: float):
_set_fader_val(self, val) self.setter("fader", val)
@property @property
def lr(self) -> bool: def lr(self) -> bool:
@@ -617,10 +636,38 @@ class Automix:
@property @property
def weight(self) -> float: def weight(self) -> float:
return round(lin_get(-12, 12, self.getter("weight")[0]), 1) return round(util.lin_get(-12, 12, self.getter("weight")[0]), 1)
@weight.setter @weight.setter
def weight(self, val: float): def weight(self, val: float):
if not -12 <= val <= 12: if not -12 <= val <= 12:
raise XAirRemoteError("expected value in range -12.0 to 12.0") self.logger.warning(
self.setter("weight", lin_set(-12, 12, val)) f"weight got {val}, expected value in range -12.0 to 12.0"
)
self.setter("weight", util.lin_set(-12, 12, val))
class Send:
def __init__(self, i, remote, index: Optional[int] = None):
super(Send, self).__init__(remote, index)
self.i = i + 1
@classmethod
def make(cls, _cls, i, remote, index=None):
SEND_cls = type("Send", (cls, _cls), {})
return SEND_cls(i, remote, index)
@property
def address(self) -> str:
root = super(Send, self).address
return f"{root}/mix/{str(self.i).zfill(2)}"
@property
@util.db_from
def level(self) -> float:
return self.getter("level")[0]
@level.setter
@util.db_to
def level(self, val: float):
self.setter("level", val)

View File

@@ -1,8 +1,10 @@
import abc import abc
import logging
from .errors import XAirRemoteError
from .meta import mute_prop from .meta import mute_prop
from .shared import EQ, GEQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp from .shared import EQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp, Send
logger = logging.getLogger(__name__)
class IStrip(abc.ABC): class IStrip(abc.ABC):
@@ -11,10 +13,10 @@ class IStrip(abc.ABC):
def __init__(self, remote, index: int): def __init__(self, remote, index: int):
self._remote = remote self._remote = remote
self.index = index + 1 self.index = index + 1
self.logger = logger.getChild(self.__class__.__name__)
def getter(self, param: str) -> tuple: def getter(self, param: str) -> tuple:
self._remote.send(f"{self.address}/{param}") return self._remote.query(f"{self.address}/{param}")
return self._remote.info_response
def setter(self, param: str, val: int): def setter(self, param: str, val: int):
self._remote.send(f"{self.address}/{param}", val) self._remote.send(f"{self.address}/{param}", val)
@@ -57,6 +59,10 @@ class Strip(IStrip):
Automix, Automix,
) )
}, },
"send": tuple(
Send.make(cls, i, remote, index)
for i in range(remote.kind.num_bus + remote.kind.num_fx)
),
"mute": mute_prop(), "mute": mute_prop(),
}, },
) )

View File

@@ -1,3 +1,4 @@
import functools
from math import exp, log from math import exp, log
@@ -17,7 +18,13 @@ def log_set(min, max, val):
return log(val / min) / log(max / min) return log(val / min) / log(max / min)
def _get_fader_val(retval): def db_from(func):
"""fader|level converter for getters"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
retval = func(*args, **kwargs)
if retval >= 1: if retval >= 1:
return 10 return 10
elif retval >= 0.5: elif retval >= 0.5:
@@ -31,47 +38,28 @@ def _get_fader_val(retval):
else: else:
return -90 return -90
return wrapper
def _set_fader_val(self, val):
def db_to(func):
"""fader|level converter for setters"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
param, val = args
if val >= 10: if val >= 10:
self.setter("fader", 1) val = 1
elif val >= -10: elif val >= -10:
self.setter("fader", (val + 30) / 40) val = (val + 30) / 40
elif val >= -30: elif val >= -30:
self.setter("fader", (val + 50) / 80) val = (val + 50) / 80
elif val >= -60: elif val >= -60:
self.setter("fader", (val + 70) / 160) val = (val + 70) / 160
elif val >= -90: elif val >= -90:
self.setter("fader", (val + 90) / 480) val = (val + 90) / 480
else: else:
self.setter("fader", 0) val = 0
func(param, val, **kwargs)
def _get_level_val(retval): return wrapper
if retval >= 1:
return 10
elif retval >= 0.5:
return round((40 * retval) - 30, 1)
elif retval >= 0.25:
return round((80 * retval) - 50, 1)
elif retval >= 0.0625:
return round((160 * retval) - 70, 1)
elif retval >= 0:
return round((480 * retval) - 90, 1)
else:
return -90
def _set_level_val(self, val):
if val >= 10:
self.setter("level", 1)
elif val >= -10:
self.setter("level", (val + 30) / 40)
elif val >= -30:
self.setter("level", (val + 50) / 80)
elif val >= -60:
self.setter("level", (val + 70) / 160)
elif val >= -90:
self.setter("level", (val + 90) / 480)
else:
self.setter("level", 0)

View File

@@ -25,6 +25,8 @@ from .lr import LR
from .rtn import AuxRtn, FxRtn from .rtn import AuxRtn, FxRtn
from .strip import Strip from .strip import Strip
logger = logging.getLogger(__name__)
class OSCClientServer(BlockingOSCUDPServer): class OSCClientServer(BlockingOSCUDPServer):
def __init__(self, address: str, dispatcher: Dispatcher): def __init__(self, address: str, dispatcher: Dispatcher):
@@ -45,8 +47,6 @@ class OSCClientServer(BlockingOSCUDPServer):
class XAirRemote(abc.ABC): class XAirRemote(abc.ABC):
"""Handles the communication with the mixer via the OSC protocol""" """Handles the communication with the mixer via the OSC protocol"""
logger = logging.getLogger("xair.xairremote")
_CONNECT_TIMEOUT = 0.5 _CONNECT_TIMEOUT = 0.5
_info_response = [] _info_response = []
@@ -57,6 +57,7 @@ class XAirRemote(abc.ABC):
self.xair_ip = kwargs["ip"] or self._ip_from_toml() self.xair_ip = kwargs["ip"] or self._ip_from_toml()
self.xair_port = kwargs["port"] self.xair_port = kwargs["port"]
self._delay = kwargs["delay"] self._delay = kwargs["delay"]
self.logger = logger.getChild(self.__class__.__name__)
if not self.xair_ip: if not self.xair_ip:
raise XAirRemoteError("No valid ip detected") raise XAirRemoteError("No valid ip detected")
self.server = OSCClientServer((self.xair_ip, self.xair_port), dispatcher) self.server = OSCClientServer((self.xair_ip, self.xair_port), dispatcher)
@@ -80,7 +81,7 @@ class XAirRemote(abc.ABC):
raise XAirRemoteError( raise XAirRemoteError(
"Failed to setup OSC connection to mixer. Please check for correct ip address." "Failed to setup OSC connection to mixer. Please check for correct ip address."
) )
print( self.logger.info(
f"Successfully connected to {self.info_response[2]} at {self.info_response[0]}." f"Successfully connected to {self.info_response[2]} at {self.info_response[0]}."
) )
@@ -98,10 +99,10 @@ class XAirRemote(abc.ABC):
def send(self, addr: str, param: Optional[str] = None): def send(self, addr: str, param: Optional[str] = None):
self.logger.debug(f"sending: {addr} {param if param is not None else ''}") self.logger.debug(f"sending: {addr} {param if param is not None else ''}")
self.server.send_message(addr, param) self.server.send_message(addr, param)
time.sleep(self._delay)
def query(self, address): def query(self, address):
self.send(address) self.send(address)
time.sleep(self._delay)
return self.info_response return self.info_response
def __exit__(self, exc_type, exc_value, exc_tr): def __exit__(self, exc_type, exc_value, exc_tr):
@@ -174,9 +175,10 @@ def request_remote_obj(kind_id: str, *args, **kwargs) -> XAirRemote:
Returns a reference to an XAirRemote class of a kind Returns a reference to an XAirRemote class of a kind
""" """
XAIRREMOTE_cls = None XAIRREMOTE_cls = None
try: try:
XAIRREMOTE_cls = _remotes[kind_id] XAIRREMOTE_cls = _remotes[kind_id]
except ValueError as e: except KeyError as e:
raise SystemExit(e) raise XAirRemoteError(f"Unknown mixer kind '{kind_id}'") from e
return XAIRREMOTE_cls(*args, **kwargs) return XAIRREMOTE_cls(*args, **kwargs)