xair-api 2.4.0__tar.gz → 2.4.1__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.
@@ -1,21 +1,19 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: xair-api
3
- Version: 2.4.0
3
+ Version: 2.4.1
4
4
  Summary: Remote control Behringer X-Air | Midas MR mixers through OSC
5
- Home-page: https://github.com/onyx-and-iris/xair-api-python
6
5
  License: MIT
7
- Author: onyx-and-iris
6
+ Author: Onyx and Iris
8
7
  Author-email: code@onyxandiris.online
9
- Requires-Python: >=3.10,<4.0
8
+ Requires-Python: <4.0,>=3.10
10
9
  Classifier: License :: OSI Approved :: MIT License
11
10
  Classifier: Programming Language :: Python :: 3
12
11
  Classifier: Programming Language :: Python :: 3.10
13
12
  Classifier: Programming Language :: Python :: 3.11
14
13
  Classifier: Programming Language :: Python :: 3.12
15
14
  Classifier: Programming Language :: Python :: 3.13
16
- Requires-Dist: python-osc (>=1.8.0,<2.0.0)
17
- Requires-Dist: tomli (>=2.0.1,<3.0.0) ; python_version < "3.11"
18
- Project-URL: Repository, https://github.com/onyx-and-iris/xair-api-python
15
+ Requires-Dist: python-osc (>=1.9.3,<2.0.0)
16
+ Requires-Dist: tomli (>=2.0.1,<3.0) ; python_version < "3.11"
19
17
  Description-Content-Type: text/markdown
20
18
 
21
19
  [![PyPI version](https://badge.fury.io/py/xair-api.svg)](https://badge.fury.io/py/xair-api)
@@ -59,18 +57,18 @@ import xair_api
59
57
 
60
58
 
61
59
  def main():
62
- kind_id = "XR18"
63
- ip = "<ip address>"
60
+ kind_id = 'XR18'
61
+ ip = '<ip address>'
64
62
 
65
63
  with xair_api.connect(kind_id, ip=ip) as mixer:
66
- mixer.strip[8].config.name = "sm7b"
64
+ mixer.strip[8].config.name = 'sm7b'
67
65
  mixer.strip[8].mix.on = True
68
66
  print(
69
- f"strip 09 ({mixer.strip[8].config.name}) on has been set to {mixer.strip[8].mix.on}"
67
+ f'strip 09 ({mixer.strip[8].config.name}) on has been set to {mixer.strip[8].mix.on}'
70
68
  )
71
69
 
72
70
 
73
- if __name__ == "__main__":
71
+ if __name__ == '__main__':
74
72
  main()
75
73
  ```
76
74
 
@@ -334,8 +332,8 @@ Send an OSC command directly to the mixer
334
332
  for example:
335
333
 
336
334
  ```python
337
- mixer.send("/ch/01/mix/on", 1)
338
- mixer.send("/bus/2/config/name", "somename")
335
+ mixer.send('/ch/01/mix/on', 1)
336
+ mixer.send('/bus/2/config/name', 'somename')
339
337
  ```
340
338
 
341
339
  Query the value of a command:
@@ -345,7 +343,7 @@ Query the value of a command:
345
343
  for example:
346
344
 
347
345
  ```python
348
- print(mixer.query("/ch/01/mix/on"))
346
+ print(mixer.query('/ch/01/mix/on'))
349
347
  ```
350
348
 
351
349
  ### Errors
@@ -39,18 +39,18 @@ import xair_api
39
39
 
40
40
 
41
41
  def main():
42
- kind_id = "XR18"
43
- ip = "<ip address>"
42
+ kind_id = 'XR18'
43
+ ip = '<ip address>'
44
44
 
45
45
  with xair_api.connect(kind_id, ip=ip) as mixer:
46
- mixer.strip[8].config.name = "sm7b"
46
+ mixer.strip[8].config.name = 'sm7b'
47
47
  mixer.strip[8].mix.on = True
48
48
  print(
49
- f"strip 09 ({mixer.strip[8].config.name}) on has been set to {mixer.strip[8].mix.on}"
49
+ f'strip 09 ({mixer.strip[8].config.name}) on has been set to {mixer.strip[8].mix.on}'
50
50
  )
51
51
 
52
52
 
53
- if __name__ == "__main__":
53
+ if __name__ == '__main__':
54
54
  main()
55
55
  ```
56
56
 
@@ -314,8 +314,8 @@ Send an OSC command directly to the mixer
314
314
  for example:
315
315
 
316
316
  ```python
317
- mixer.send("/ch/01/mix/on", 1)
318
- mixer.send("/bus/2/config/name", "somename")
317
+ mixer.send('/ch/01/mix/on', 1)
318
+ mixer.send('/bus/2/config/name', 'somename')
319
319
  ```
320
320
 
321
321
  Query the value of a command:
@@ -325,7 +325,7 @@ Query the value of a command:
325
325
  for example:
326
326
 
327
327
  ```python
328
- print(mixer.query("/ch/01/mix/on"))
328
+ print(mixer.query('/ch/01/mix/on'))
329
329
  ```
330
330
 
331
331
  ### Errors
@@ -0,0 +1,139 @@
1
+ [project]
2
+ name = "xair-api"
3
+ version = "2.4.1"
4
+ description = "Remote control Behringer X-Air | Midas MR mixers through OSC"
5
+ authors = [
6
+ {name = "Onyx and Iris",email = "code@onyxandiris.online"}
7
+ ]
8
+ license = {text = "MIT"}
9
+ readme = "README.md"
10
+ requires-python = "<4.0,>=3.10"
11
+ dependencies = [
12
+ "python-osc (>=1.9.3,<2.0.0)",
13
+ "tomli (>=2.0.1,<3.0) ; python_version < '3.11'"
14
+ ]
15
+
16
+ [tool.poetry.requires-plugins]
17
+ poethepoet = "^0.32.1"
18
+
19
+ [tool.poetry.group.dev.dependencies]
20
+ pytest = "^8.3.4"
21
+ pytest-randomly = "^3.16.0"
22
+ ruff = "^0.8.6"
23
+ tox = "^4.23.2"
24
+ virtualenv-pyenv = "^0.5.0"
25
+
26
+ [build-system]
27
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
28
+ build-backend = "poetry.core.masonry.api"
29
+
30
+ [tool.poe.tasks]
31
+ obs.script = "scripts:ex_obs"
32
+ sends.script = "scripts:ex_sends"
33
+ headamp.script = "scripts:ex_headamp"
34
+ xair.script = "scripts:test_xair"
35
+ x32.script = "scripts:test_x32"
36
+ all.script = "scripts:test_all"
37
+
38
+ [tool.tox]
39
+ legacy_tox_ini = """
40
+ [tox]
41
+ envlist = py310,py311,py312
42
+
43
+ [testenv]
44
+ setenv = VIRTUALENV_DISCOVERY=pyenv
45
+ allowlist_externals = poetry
46
+ commands =
47
+ poetry install -v
48
+ poetry run pytest tests/
49
+
50
+ [testenv:obs]
51
+ setenv = VIRTUALENV_DISCOVERY=pyenv
52
+ allowlist_externals = poetry
53
+ deps = obsws-python
54
+ commands =
55
+ poetry install -v --without dev
56
+ poetry run python examples/xair_obs/
57
+ """
58
+
59
+ [tool.ruff]
60
+ exclude = [
61
+ ".bzr",
62
+ ".direnv",
63
+ ".eggs",
64
+ ".git",
65
+ ".git-rewrite",
66
+ ".hg",
67
+ ".mypy_cache",
68
+ ".nox",
69
+ ".pants.d",
70
+ ".pytype",
71
+ ".ruff_cache",
72
+ ".svn",
73
+ ".tox",
74
+ ".venv",
75
+ "__pypackages__",
76
+ "_build",
77
+ "buck-out",
78
+ "build",
79
+ "dist",
80
+ "node_modules",
81
+ "venv",
82
+ ]
83
+
84
+ # Same as Black.
85
+ line-length = 88
86
+ indent-width = 4
87
+
88
+ # Assume Python 3.10
89
+ target-version = "py310"
90
+
91
+ [tool.ruff.lint]
92
+ # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
93
+ # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
94
+ # McCabe complexity (`C901`) by default.
95
+ select = ["E4", "E7", "E9", "F"]
96
+ ignore = []
97
+
98
+ # Allow fix for all enabled rules (when `--fix`) is provided.
99
+ fixable = ["ALL"]
100
+ unfixable = []
101
+
102
+ # Allow unused variables when underscore-prefixed.
103
+ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
104
+
105
+ [tool.ruff.format]
106
+ # Unlike Black, use single quotes for strings.
107
+ quote-style = "single"
108
+
109
+ # Like Black, indent with spaces, rather than tabs.
110
+ indent-style = "space"
111
+
112
+ # Like Black, respect magic trailing commas.
113
+ skip-magic-trailing-comma = false
114
+
115
+ # Like Black, automatically detect the appropriate line ending.
116
+ line-ending = "auto"
117
+
118
+ # Enable auto-formatting of code examples in docstrings. Markdown,
119
+ # reStructuredText code/literal blocks and doctests are all supported.
120
+ #
121
+ # This is currently disabled by default, but it is planned for this
122
+ # to be opt-out in the future.
123
+ docstring-code-format = false
124
+
125
+ # Set the line length limit used when formatting code snippets in
126
+ # docstrings.
127
+ #
128
+ # This only has an effect when the `docstring-code-format` setting is
129
+ # enabled.
130
+ docstring-code-line-length = "dynamic"
131
+
132
+ [tool.ruff.lint.mccabe]
133
+ max-complexity = 10
134
+
135
+ [tool.ruff.lint.per-file-ignores]
136
+ "__init__.py" = [
137
+ "E402",
138
+ "F401",
139
+ ]
@@ -1,3 +1,3 @@
1
1
  from .xair import request_remote_obj as connect
2
2
 
3
- _ALL__ = ["connect"]
3
+ _ALL__ = ['connect']
@@ -8,40 +8,40 @@ from .rtn import FxRtn as IFxRtn
8
8
  class Bus(IBus):
9
9
  @property
10
10
  def address(self):
11
- return f"/bus/{str(self.index).zfill(2)}"
11
+ return f'/bus/{str(self.index).zfill(2)}'
12
12
 
13
13
 
14
14
  class AuxRtn(IAuxRtn):
15
15
  @property
16
16
  def address(self):
17
- return f"/auxin/{str(self.index).zfill(2)}"
17
+ return f'/auxin/{str(self.index).zfill(2)}'
18
18
 
19
19
 
20
20
  class FxRtn(IFxRtn):
21
21
  @property
22
22
  def address(self):
23
- return f"/fxrtn/{str(self.index).zfill(2)}"
23
+ return f'/fxrtn/{str(self.index).zfill(2)}'
24
24
 
25
25
 
26
26
  class MainStereo(ILR):
27
27
  @property
28
28
  def address(self) -> str:
29
- return "/main/st"
29
+ return '/main/st'
30
30
 
31
31
 
32
32
  class MainMono(ILR):
33
33
  @property
34
34
  def address(self) -> str:
35
- return "/main/m"
35
+ return '/main/m'
36
36
 
37
37
 
38
38
  class Matrix(ILR):
39
39
  @property
40
40
  def address(self) -> str:
41
- return f"/mtx/{str(self.index).zfill(2)}"
41
+ return f'/mtx/{str(self.index).zfill(2)}'
42
42
 
43
43
 
44
44
  class HeadAmp(IHeadAmp):
45
45
  @property
46
46
  def address(self):
47
- return f"/headamp/{str(self.index).zfill(3)}"
47
+ return f'/headamp/{str(self.index).zfill(3)}'
@@ -16,10 +16,10 @@ class IBus(abc.ABC):
16
16
  self.logger = logger.getChild(self.__class__.__name__)
17
17
 
18
18
  def getter(self, param: str):
19
- return self._remote.query(f"{self.address}/{param}")
19
+ return self._remote.query(f'{self.address}/{param}')
20
20
 
21
21
  def setter(self, param: str, val: int):
22
- self._remote.send(f"{self.address}/{param}", val)
22
+ self._remote.send(f'{self.address}/{param}', val)
23
23
 
24
24
  @abc.abstractmethod
25
25
  def address(self):
@@ -39,12 +39,12 @@ class Bus(IBus):
39
39
  Returns a Bus class of a kind.
40
40
  """
41
41
  BUS_cls = type(
42
- f"Bus{remote.kind}",
42
+ f'Bus{remote.kind}',
43
43
  (cls,),
44
44
  {
45
45
  **{
46
46
  _cls.__name__.lower(): type(
47
- f"{_cls.__name__}{remote.kind}", (_cls, cls), {}
47
+ f'{_cls.__name__}{remote.kind}', (_cls, cls), {}
48
48
  )(remote, index)
49
49
  for _cls in (
50
50
  Config,
@@ -56,11 +56,11 @@ class Bus(IBus):
56
56
  Group,
57
57
  )
58
58
  },
59
- "mute": mute_prop(),
59
+ 'mute': mute_prop(),
60
60
  },
61
61
  )
62
62
  return BUS_cls(remote, index)
63
63
 
64
64
  @property
65
65
  def address(self) -> str:
66
- return f"/bus/{self.index}"
66
+ return f'/bus/{self.index}'
@@ -15,10 +15,10 @@ class IConfig(abc.ABC):
15
15
  self.logger = logger.getChild(self.__class__.__name__)
16
16
 
17
17
  def getter(self, param: str):
18
- return self._remote.query(f"{self.address}/{param}")
18
+ return self._remote.query(f'{self.address}/{param}')
19
19
 
20
20
  def setter(self, param: str, val: int):
21
- self._remote.send(f"{self.address}/{param}", val)
21
+ self._remote.send(f'{self.address}/{param}', val)
22
22
 
23
23
  @abc.abstractmethod
24
24
  def address(self):
@@ -36,37 +36,37 @@ class Config(IConfig):
36
36
  Returns a Config class of a kind.
37
37
  """
38
38
  LINKS_cls = _make_links_mixins[remote.kind.id_]
39
- MUTEGROUP_cls = type("MuteGroup", (Config.MuteGroup, cls), {})
40
- MONITOR_cls = type("ConfigMonitor", (Config.Monitor, cls), {})
39
+ MUTEGROUP_cls = type('MuteGroup', (Config.MuteGroup, cls), {})
40
+ MONITOR_cls = type('ConfigMonitor', (Config.Monitor, cls), {})
41
41
  CONFIG_cls = type(
42
- f"Config{remote.kind}",
42
+ f'Config{remote.kind}',
43
43
  (cls, LINKS_cls),
44
44
  {
45
- "mute_group": tuple(MUTEGROUP_cls(remote, i) for i in range(4)),
46
- "monitor": MONITOR_cls(remote),
45
+ 'mute_group': tuple(MUTEGROUP_cls(remote, i) for i in range(4)),
46
+ 'monitor': MONITOR_cls(remote),
47
47
  },
48
48
  )
49
49
  return CONFIG_cls(remote)
50
50
 
51
51
  @property
52
52
  def address(self) -> str:
53
- return "/config"
53
+ return '/config'
54
54
 
55
55
  @property
56
56
  def amixenable(self) -> bool:
57
- return self.getter("mute")[0] == 1
57
+ return self.getter('mute')[0] == 1
58
58
 
59
59
  @amixenable.setter
60
60
  def amixenable(self, val: bool):
61
- self.setter("amixenable", 1 if val else 0)
61
+ self.setter('amixenable', 1 if val else 0)
62
62
 
63
63
  @property
64
64
  def amixlock(self) -> bool:
65
- return self.getter("amixlock")[0] == 1
65
+ return self.getter('amixlock')[0] == 1
66
66
 
67
67
  @amixlock.setter
68
68
  def amixlock(self, val: bool):
69
- self.setter("amixlock", 1 if val else 0)
69
+ self.setter('amixlock', 1 if val else 0)
70
70
 
71
71
  class MuteGroup:
72
72
  def __init__(self, remote, i):
@@ -76,128 +76,128 @@ class Config(IConfig):
76
76
  @property
77
77
  def address(self) -> str:
78
78
  root = super(Config.MuteGroup, self).address
79
- return f"{root}/mute"
79
+ return f'{root}/mute'
80
80
 
81
81
  @property
82
82
  def on(self) -> bool:
83
- return self.getter(f"{self.i}")[0] == 1
83
+ return self.getter(f'{self.i}')[0] == 1
84
84
 
85
85
  @on.setter
86
86
  def on(self, val: bool):
87
- self.setter(f"{self.i}", 1 if val else 0)
87
+ self.setter(f'{self.i}', 1 if val else 0)
88
88
 
89
89
  class Monitor:
90
90
  @property
91
91
  def address(self) -> str:
92
92
  root = super(Config.Monitor, self).address
93
- return f"{root}/solo"
93
+ return f'{root}/solo'
94
94
 
95
95
  @property
96
96
  @util.db_from
97
97
  def level(self) -> float:
98
- return self.getter("level")[0]
98
+ return self.getter('level')[0]
99
99
 
100
100
  @level.setter
101
101
  @util.db_to
102
102
  def level(self, val: float):
103
- self.setter("level", val)
103
+ self.setter('level', val)
104
104
 
105
105
  @property
106
106
  def source(self) -> int:
107
- return int(self.getter("source")[0])
107
+ return int(self.getter('source')[0])
108
108
 
109
109
  @source.setter
110
110
  def source(self, val: int):
111
- self.setter("source", val)
111
+ self.setter('source', val)
112
112
 
113
113
  @property
114
114
  def sourcetrim(self) -> float:
115
- return round(util.lin_get(-18, 18, self.getter("sourcetrim")[0]), 1)
115
+ return round(util.lin_get(-18, 18, self.getter('sourcetrim')[0]), 1)
116
116
 
117
117
  @sourcetrim.setter
118
118
  def sourcetrim(self, val: float):
119
119
  if not -18 <= val <= 18:
120
120
  self.logger.warning(
121
- f"sourcetrim got {val}, expected value in range -18.0 to 18.0"
121
+ f'sourcetrim got {val}, expected value in range -18.0 to 18.0'
122
122
  )
123
- self.setter("sourcetrim", util.lin_set(-18, 18, val))
123
+ self.setter('sourcetrim', util.lin_set(-18, 18, val))
124
124
 
125
125
  @property
126
126
  def chmode(self) -> bool:
127
- return self.getter("chmode")[0] == 1
127
+ return self.getter('chmode')[0] == 1
128
128
 
129
129
  @chmode.setter
130
130
  def chmode(self, val: bool):
131
- self.setter("chmode", 1 if val else 0)
131
+ self.setter('chmode', 1 if val else 0)
132
132
 
133
133
  @property
134
134
  def busmode(self) -> bool:
135
- return self.getter("busmode")[0] == 1
135
+ return self.getter('busmode')[0] == 1
136
136
 
137
137
  @busmode.setter
138
138
  def busmode(self, val: bool):
139
- self.setter("busmode", 1 if val else 0)
139
+ self.setter('busmode', 1 if val else 0)
140
140
 
141
141
  @property
142
142
  def dimgain(self) -> int:
143
- return int(util.lin_get(-40, 0, self.getter("dimatt")[0]))
143
+ return int(util.lin_get(-40, 0, self.getter('dimatt')[0]))
144
144
 
145
145
  @dimgain.setter
146
146
  def dimgain(self, val: int):
147
147
  if not -40 <= val <= 0:
148
148
  self.logger.warning(
149
- f"dimgain got {val}, expected value in range -40 to 0"
149
+ f'dimgain got {val}, expected value in range -40 to 0'
150
150
  )
151
- self.setter("dimatt", util.lin_set(-40, 0, val))
151
+ self.setter('dimatt', util.lin_set(-40, 0, val))
152
152
 
153
153
  @property
154
154
  def dim(self) -> bool:
155
- return self.getter("dim")[0] == 1
155
+ return self.getter('dim')[0] == 1
156
156
 
157
157
  @dim.setter
158
158
  def dim(self, val: bool):
159
- self.setter("dim", 1 if val else 0)
159
+ self.setter('dim', 1 if val else 0)
160
160
 
161
161
  @property
162
162
  def mono(self) -> bool:
163
- return self.getter("mono")[0] == 1
163
+ return self.getter('mono')[0] == 1
164
164
 
165
165
  @mono.setter
166
166
  def mono(self, val: bool):
167
- self.setter("mono", 1 if val else 0)
167
+ self.setter('mono', 1 if val else 0)
168
168
 
169
169
  @property
170
170
  def mute(self) -> bool:
171
- return self.getter("mute")[0] == 1
171
+ return self.getter('mute')[0] == 1
172
172
 
173
173
  @mute.setter
174
174
  def mute(self, val: bool):
175
- self.setter("mute", 1 if val else 0)
175
+ self.setter('mute', 1 if val else 0)
176
176
 
177
177
  @property
178
178
  def dimfpl(self) -> bool:
179
- return self.getter("dimfpl")[0] == 1
179
+ return self.getter('dimfpl')[0] == 1
180
180
 
181
181
  @dimfpl.setter
182
182
  def dimfpl(self, val: bool):
183
- self.setter("dimfpl", 1 if val else 0)
183
+ self.setter('dimfpl', 1 if val else 0)
184
184
 
185
185
 
186
186
  def _make_links_mixin(kind):
187
187
  """Creates a links mixin"""
188
188
  return type(
189
- f"Links{kind}",
189
+ f'Links{kind}',
190
190
  (),
191
191
  {
192
- "link_eq": bool_prop("linkcfg/eq"),
193
- "link_dyn": bool_prop("linkcfg/dyn"),
194
- "link_fader_mute": bool_prop("linkcfg/fdrmute"),
192
+ 'link_eq': bool_prop('linkcfg/eq'),
193
+ 'link_dyn': bool_prop('linkcfg/dyn'),
194
+ 'link_fader_mute': bool_prop('linkcfg/fdrmute'),
195
195
  **{
196
- f"chlink{i}_{i+1}": bool_prop(f"chlink/{i}-{i+1}")
196
+ f'chlink{i}_{i+1}': bool_prop(f'chlink/{i}-{i+1}')
197
197
  for i in range(1, kind.num_strip, 2)
198
198
  },
199
199
  **{
200
- f"buslink{i}_{i+1}": bool_prop(f"buslink/{i}-{i+1}")
200
+ f'buslink{i}_{i+1}': bool_prop(f'buslink/{i}-{i+1}')
201
201
  for i in range(1, kind.num_bus, 2)
202
202
  },
203
203
  },
@@ -13,10 +13,10 @@ class IDCA(abc.ABC):
13
13
  self.logger = logger.getChild(self.__class__.__name__)
14
14
 
15
15
  def getter(self, param: str) -> tuple:
16
- return self._remote.query(f"{self.address}/{param}")
16
+ return self._remote.query(f'{self.address}/{param}')
17
17
 
18
18
  def setter(self, param: str, val: int):
19
- self._remote.send(f"{self.address}/{param}", val)
19
+ self._remote.send(f'{self.address}/{param}', val)
20
20
 
21
21
  @abc.abstractmethod
22
22
  def address(self):
@@ -28,15 +28,15 @@ class DCA(IDCA):
28
28
 
29
29
  @property
30
30
  def address(self) -> str:
31
- return f"/dca/{self.index}"
31
+ return f'/dca/{self.index}'
32
32
 
33
33
  @property
34
34
  def on(self) -> bool:
35
- return self.getter("on")[0] == 1
35
+ return self.getter('on')[0] == 1
36
36
 
37
37
  @on.setter
38
38
  def on(self, val: bool):
39
- self.setter("on", 1 if val else 0)
39
+ self.setter('on', 1 if val else 0)
40
40
 
41
41
  @property
42
42
  def mute(self) -> bool:
@@ -48,16 +48,16 @@ class DCA(IDCA):
48
48
 
49
49
  @property
50
50
  def name(self) -> str:
51
- return self.getter("config/name")[0]
51
+ return self.getter('config/name')[0]
52
52
 
53
53
  @name.setter
54
54
  def name(self, val: str):
55
- self.setter("config/name", val)
55
+ self.setter('config/name', val)
56
56
 
57
57
  @property
58
58
  def color(self) -> int:
59
- return self.getter("config/color")[0]
59
+ return self.getter('config/color')[0]
60
60
 
61
61
  @color.setter
62
62
  def color(self, val: int):
63
- self.setter("config/color", val)
63
+ self.setter('config/color', val)
@@ -10,5 +10,5 @@ class XAirRemoteConnectionTimeoutError(XAirRemoteError):
10
10
  self.port = port
11
11
 
12
12
  super().__init__(
13
- f"Timeout attempting to connect to mixer at {self.ip}:{self.port}"
13
+ f'Timeout attempting to connect to mixer at {self.ip}:{self.port}'
14
14
  )