mirror of
https://github.com/onyx-and-iris/xair-api-python.git
synced 2024-11-24 22:00:57 +00:00
adapter module added
factory method for x32 added logging module added for tracing OSC requests/reponses.
This commit is contained in:
parent
d765c5b027
commit
a69734b738
@ -22,7 +22,6 @@ class Data:
|
|||||||
strip: int = kind.num_strip - 1
|
strip: int = kind.num_strip - 1
|
||||||
bus: int = kind.num_bus - 1
|
bus: int = kind.num_bus - 1
|
||||||
fx: int = kind.num_fx - 1
|
fx: int = kind.num_fx - 1
|
||||||
rtn: int = kind.num_rtn - 1
|
|
||||||
|
|
||||||
|
|
||||||
data = Data()
|
data = Data()
|
||||||
|
@ -259,7 +259,7 @@ class TestSetAndGetStripAutomixHigher:
|
|||||||
"param,value",
|
"param,value",
|
||||||
[("group", 0), ("group", 2)],
|
[("group", 0), ("group", 2)],
|
||||||
)
|
)
|
||||||
def test_it_sets_and_gets_fxsend_int_params(self, param, value):
|
def test_it_sets_and_gets_strip_int_params(self, param, value):
|
||||||
setattr(self.target, param, value)
|
setattr(self.target, param, value)
|
||||||
assert getattr(self.target, param) == value
|
assert getattr(self.target, param) == value
|
||||||
|
|
||||||
@ -267,7 +267,7 @@ class TestSetAndGetStripAutomixHigher:
|
|||||||
"param,value",
|
"param,value",
|
||||||
[("weight", -10.5), ("weight", 3.5)],
|
[("weight", -10.5), ("weight", 3.5)],
|
||||||
)
|
)
|
||||||
def test_it_sets_and_gets_fxsend_float_params(self, param, value):
|
def test_it_sets_and_gets_strip_float_params(self, param, value):
|
||||||
setattr(self.target, param, value)
|
setattr(self.target, param, value)
|
||||||
assert getattr(self.target, param) == value
|
assert getattr(self.target, param) == value
|
||||||
|
|
||||||
|
40
xair_api/adapter.py
Normal file
40
xair_api/adapter.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from .bus import Bus as IBus
|
||||||
|
from .lr import LR as ILR
|
||||||
|
from .rtn import AuxRtn as IAuxRtn
|
||||||
|
from .rtn import FxRtn as IFxRtn
|
||||||
|
|
||||||
|
|
||||||
|
class Bus(IBus):
|
||||||
|
@property
|
||||||
|
def address(self):
|
||||||
|
return f"/bus/{str(self.index).zfill(2)}"
|
||||||
|
|
||||||
|
|
||||||
|
class AuxRtn(IAuxRtn):
|
||||||
|
@property
|
||||||
|
def address(self):
|
||||||
|
return f"/auxin/{str(self.index).zfill(2)}"
|
||||||
|
|
||||||
|
|
||||||
|
class FxRtn(IFxRtn):
|
||||||
|
@property
|
||||||
|
def address(self):
|
||||||
|
return f"/fxrtn/{str(self.index).zfill(2)}"
|
||||||
|
|
||||||
|
|
||||||
|
class MainStereo(ILR):
|
||||||
|
@property
|
||||||
|
def address(self) -> str:
|
||||||
|
return f"/main/st"
|
||||||
|
|
||||||
|
|
||||||
|
class MainMono(ILR):
|
||||||
|
@property
|
||||||
|
def address(self) -> str:
|
||||||
|
return f"/main/m"
|
||||||
|
|
||||||
|
|
||||||
|
class Matrix(ILR):
|
||||||
|
@property
|
||||||
|
def address(self) -> str:
|
||||||
|
return f"/mtx/{str(self.index).zfill(2)}"
|
@ -23,6 +23,24 @@ class IFX(abc.ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FX(IFX):
|
||||||
|
"""Concrete class for fx"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def address(self) -> str:
|
||||||
|
return f"/fx/{self.index}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type(self) -> int:
|
||||||
|
return self.getter("type")[0]
|
||||||
|
|
||||||
|
@type.setter
|
||||||
|
def type(self, val: int):
|
||||||
|
if not isinstance(val, int):
|
||||||
|
raise XAirRemoteError("type is an integer parameter")
|
||||||
|
self.setter("type", val)
|
||||||
|
|
||||||
|
|
||||||
class FXSend(IFX):
|
class FXSend(IFX):
|
||||||
"""Concrete class for fxsend"""
|
"""Concrete class for fxsend"""
|
||||||
|
|
||||||
@ -52,21 +70,3 @@ class FXSend(IFX):
|
|||||||
@property
|
@property
|
||||||
def address(self) -> str:
|
def address(self) -> str:
|
||||||
return f"/fxsend/{self.index}"
|
return f"/fxsend/{self.index}"
|
||||||
|
|
||||||
|
|
||||||
class FXReturn(IFX):
|
|
||||||
"""Concrete class for fxreturn"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def address(self) -> str:
|
|
||||||
return f"/fx/{self.index}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def type(self) -> int:
|
|
||||||
return self.getter("type")[0]
|
|
||||||
|
|
||||||
@type.setter
|
|
||||||
def type(self, val: int):
|
|
||||||
if not isinstance(val, int):
|
|
||||||
raise XAirRemoteError("type is an integer parameter")
|
|
||||||
self.setter("type", val)
|
|
||||||
|
@ -1,18 +1,5 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
"""
|
|
||||||
# osc slightly different, interface would need adjusting to support this mixer.
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class X32KindMap:
|
|
||||||
id_: str = "X32"
|
|
||||||
num_dca: int = 8
|
|
||||||
num_strip: int = 32
|
|
||||||
num_bus: int = 16
|
|
||||||
num_fx: int = 8
|
|
||||||
num_rtn: int = 6
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class KindMap:
|
class KindMap:
|
||||||
@ -20,6 +7,17 @@ class KindMap:
|
|||||||
return self.id_
|
return self.id_
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class X32KindMap(KindMap):
|
||||||
|
id_: str
|
||||||
|
num_dca: int = 8
|
||||||
|
num_strip: int = 32
|
||||||
|
num_bus: int = 16
|
||||||
|
num_fx: int = 8
|
||||||
|
num_auxrtn: int = 8
|
||||||
|
num_matrix: int = 6
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MR18KindMap(KindMap):
|
class MR18KindMap(KindMap):
|
||||||
# note ch 17-18 defined as aux rtn
|
# note ch 17-18 defined as aux rtn
|
||||||
@ -28,7 +26,6 @@ class MR18KindMap(KindMap):
|
|||||||
num_strip: int = 16
|
num_strip: int = 16
|
||||||
num_bus: int = 6
|
num_bus: int = 6
|
||||||
num_fx: int = 4
|
num_fx: int = 4
|
||||||
num_rtn: int = 4
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -38,7 +35,6 @@ class XR16KindMap(KindMap):
|
|||||||
num_strip: int = 16
|
num_strip: int = 16
|
||||||
num_bus: int = 4
|
num_bus: int = 4
|
||||||
num_fx: int = 4
|
num_fx: int = 4
|
||||||
num_rtn: int = 4
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -48,10 +44,10 @@ class XR12KindMap(KindMap):
|
|||||||
num_strip: int = 12
|
num_strip: int = 12
|
||||||
num_bus: int = 2
|
num_bus: int = 2
|
||||||
num_fx: int = 4
|
num_fx: int = 4
|
||||||
num_rtn: int = 4
|
|
||||||
|
|
||||||
|
|
||||||
_kinds = {
|
_kinds = {
|
||||||
|
"X32": X32KindMap(id_="X32"),
|
||||||
"XR18": MR18KindMap(id_="XR18"),
|
"XR18": MR18KindMap(id_="XR18"),
|
||||||
"MR18": MR18KindMap(id_="MR18"),
|
"MR18": MR18KindMap(id_="MR18"),
|
||||||
"XR16": XR16KindMap(id_="XR16"),
|
"XR16": XR16KindMap(id_="XR16"),
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import abc
|
import abc
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from .errors import XAirRemoteError
|
from .errors import XAirRemoteError
|
||||||
from .shared import EQ, GEQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp
|
from .shared import EQ, GEQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Preamp
|
||||||
@ -7,8 +8,10 @@ from .shared import EQ, GEQ, Automix, Config, Dyn, Gate, Group, Insert, Mix, Pre
|
|||||||
class ILR(abc.ABC):
|
class ILR(abc.ABC):
|
||||||
"""Abstract Base Class for buses"""
|
"""Abstract Base Class for buses"""
|
||||||
|
|
||||||
def __init__(self, remote):
|
def __init__(self, remote, index: Optional[int] = None):
|
||||||
self._remote = remote
|
self._remote = remote
|
||||||
|
if index is not None:
|
||||||
|
self.index = index + 1
|
||||||
|
|
||||||
def getter(self, param: str):
|
def getter(self, param: str):
|
||||||
self._remote.send(f"{self.address}/{param}")
|
self._remote.send(f"{self.address}/{param}")
|
||||||
@ -26,7 +29,7 @@ class LR(ILR):
|
|||||||
"""Concrete class for buses"""
|
"""Concrete class for buses"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def make(cls, remote):
|
def make(cls, remote, index=None):
|
||||||
"""
|
"""
|
||||||
Factory function for LR
|
Factory function for LR
|
||||||
|
|
||||||
@ -41,19 +44,19 @@ class LR(ILR):
|
|||||||
**{
|
**{
|
||||||
_cls.__name__.lower(): type(
|
_cls.__name__.lower(): type(
|
||||||
f"{_cls.__name__}{remote.kind}", (_cls, cls), {}
|
f"{_cls.__name__}{remote.kind}", (_cls, cls), {}
|
||||||
)(remote)
|
)(remote, index)
|
||||||
for _cls in (
|
for _cls in (
|
||||||
Config,
|
Config,
|
||||||
Dyn,
|
Dyn,
|
||||||
Insert,
|
Insert,
|
||||||
GEQ.make(),
|
GEQ.make(),
|
||||||
EQ.make_sixband(cls, remote),
|
EQ.make_sixband(cls, remote, index),
|
||||||
Mix,
|
Mix,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return LR_cls(remote)
|
return LR_cls(remote, index)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def address(self) -> str:
|
def address(self) -> str:
|
||||||
|
@ -25,26 +25,24 @@ class IRtn(abc.ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Aux(IRtn):
|
class AuxRtn(IRtn):
|
||||||
"""Concrete class for aux"""
|
"""Concrete class for aux"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def make(cls, remote):
|
def make(cls, remote, index=None):
|
||||||
"""
|
"""
|
||||||
Factory function for aux
|
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 Aux class of a kind.
|
|
||||||
"""
|
"""
|
||||||
AUX_cls = type(
|
AUXRTN_cls = type(
|
||||||
f"Aux{remote.kind}",
|
f"AuxRtn{remote.kind}",
|
||||||
(cls,),
|
(cls,),
|
||||||
{
|
{
|
||||||
**{
|
**{
|
||||||
_cls.__name__.lower(): type(
|
_cls.__name__.lower(): type(
|
||||||
f"{_cls.__name__}{remote.kind}", (_cls, cls), {}
|
f"{_cls.__name__}{remote.kind}", (_cls, cls), {}
|
||||||
)(remote)
|
)(remote, index)
|
||||||
for _cls in (
|
for _cls in (
|
||||||
Config,
|
Config,
|
||||||
Preamp,
|
Preamp,
|
||||||
@ -55,32 +53,30 @@ class Aux(IRtn):
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return AUX_cls(remote)
|
return AUXRTN_cls(remote, index)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def address(self):
|
def address(self):
|
||||||
return "/rtn/aux"
|
return "/rtn/aux"
|
||||||
|
|
||||||
|
|
||||||
class Rtn(IRtn):
|
class FxRtn(IRtn):
|
||||||
"""Concrete class for rtn"""
|
"""Concrete class for rtn"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def make(cls, remote, index):
|
def make(cls, remote, index):
|
||||||
"""
|
"""
|
||||||
Factory function for rtn
|
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 Rtn class of a kind.
|
|
||||||
"""
|
"""
|
||||||
RTN_cls = type(
|
FXRTN_cls = type(
|
||||||
f"Rtn{remote.kind.id_}",
|
f"FxRtn{remote.kind}",
|
||||||
(cls,),
|
(cls,),
|
||||||
{
|
{
|
||||||
**{
|
**{
|
||||||
_cls.__name__.lower(): type(
|
_cls.__name__.lower(): type(
|
||||||
f"{_cls.__name__}{remote.kind.id_}", (_cls, cls), {}
|
f"{_cls.__name__}{remote.kind}", (_cls, cls), {}
|
||||||
)(remote, index)
|
)(remote, index)
|
||||||
for _cls in (
|
for _cls in (
|
||||||
Config,
|
Config,
|
||||||
@ -92,7 +88,7 @@ class Rtn(IRtn):
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return RTN_cls(remote, index)
|
return FXRTN_cls(remote, index)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def address(self):
|
def address(self):
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import abc
|
import abc
|
||||||
|
import logging
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -13,15 +14,15 @@ from pythonosc.dispatcher import Dispatcher
|
|||||||
from pythonosc.osc_message_builder import OscMessageBuilder
|
from pythonosc.osc_message_builder import OscMessageBuilder
|
||||||
from pythonosc.osc_server import BlockingOSCUDPServer
|
from pythonosc.osc_server import BlockingOSCUDPServer
|
||||||
|
|
||||||
from . import kinds
|
from . import adapter, kinds
|
||||||
from .bus import Bus
|
from .bus import Bus
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .dca import DCA
|
from .dca import DCA
|
||||||
from .errors import XAirRemoteError
|
from .errors import XAirRemoteError
|
||||||
from .fx import FXReturn, FXSend
|
from .fx import FX, FXSend
|
||||||
from .kinds import KindMap
|
from .kinds import KindMap
|
||||||
from .lr import LR
|
from .lr import LR
|
||||||
from .rtn import Aux, Rtn
|
from .rtn import AuxRtn, FxRtn
|
||||||
from .strip import Strip
|
from .strip import Strip
|
||||||
|
|
||||||
|
|
||||||
@ -47,19 +48,19 @@ 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
|
||||||
_WAIT_TIME = 0.025
|
_WAIT_TIME = 0.025
|
||||||
_REFRESH_TIMEOUT = 5
|
_REFRESH_TIMEOUT = 5
|
||||||
|
|
||||||
XAIR_PORT = 10024
|
|
||||||
|
|
||||||
info_response = []
|
info_response = []
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
dispatcher = Dispatcher()
|
dispatcher = Dispatcher()
|
||||||
dispatcher.set_default_handler(self.msg_handler)
|
dispatcher.set_default_handler(self.msg_handler)
|
||||||
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"] or self.XAIR_PORT
|
self.xair_port = kwargs["port"]
|
||||||
if not (self.xair_ip and self.xair_port):
|
if not (self.xair_ip and self.xair_port):
|
||||||
raise XAirRemoteError("No valid ip or password detected")
|
raise XAirRemoteError("No valid ip or password detected")
|
||||||
self.server = OSCClientServer((self.xair_ip, self.xair_port), dispatcher)
|
self.server = OSCClientServer((self.xair_ip, self.xair_port), dispatcher)
|
||||||
@ -80,7 +81,9 @@ class XAirRemote(abc.ABC):
|
|||||||
self.send("/xinfo")
|
self.send("/xinfo")
|
||||||
time.sleep(self._CONNECT_TIMEOUT)
|
time.sleep(self._CONNECT_TIMEOUT)
|
||||||
if len(self.info_response) > 0:
|
if len(self.info_response) > 0:
|
||||||
print(f"Successfully connected to {self.info_response[2]}.")
|
print(
|
||||||
|
f"Successfully connected to {self.info_response[2]} at {self.info_response[0]}."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
print(
|
print(
|
||||||
"Error: Failed to setup OSC connection to mixer. Please check for correct ip address."
|
"Error: Failed to setup OSC connection to mixer. Please check for correct ip address."
|
||||||
@ -90,10 +93,12 @@ class XAirRemote(abc.ABC):
|
|||||||
self.server.serve_forever()
|
self.server.serve_forever()
|
||||||
|
|
||||||
def msg_handler(self, addr, *data):
|
def msg_handler(self, addr, *data):
|
||||||
|
self.logger.debug(f"received: {addr} {data if data else ''}")
|
||||||
self.info_response = data[:]
|
self.info_response = data[:]
|
||||||
|
|
||||||
def send(self, address: str, param: Optional[str] = None):
|
def send(self, addr: str, param: Optional[str] = None):
|
||||||
self.server.send_message(address, param)
|
self.logger.debug(f"sending: {addr} {param if param else ''}")
|
||||||
|
self.server.send_message(addr, param)
|
||||||
time.sleep(self._WAIT_TIME)
|
time.sleep(self._WAIT_TIME)
|
||||||
|
|
||||||
def _query(self, address):
|
def _query(self, address):
|
||||||
@ -112,8 +117,26 @@ def _make_remote(kind: KindMap) -> XAirRemote:
|
|||||||
The returned class will subclass XAirRemote.
|
The returned class will subclass XAirRemote.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def init(self, *args, **kwargs):
|
def init_x32(self, *args, **kwargs):
|
||||||
defaultkwargs = {"ip": None, "port": None}
|
defaultkwargs = {"ip": None, "port": 10023}
|
||||||
|
kwargs = defaultkwargs | kwargs
|
||||||
|
XAirRemote.__init__(self, *args, **kwargs)
|
||||||
|
self.kind = kind
|
||||||
|
self.mainst = adapter.MainStereo.make(self)
|
||||||
|
self.mainmono = adapter.MainMono.make(self)
|
||||||
|
self.matrix = tuple(
|
||||||
|
adapter.Matrix.make(self, i) for i in range(kind.num_matrix)
|
||||||
|
)
|
||||||
|
self.strip = tuple(Strip.make(self, i) for i in range(kind.num_strip))
|
||||||
|
self.bus = tuple(adapter.Bus.make(self, i) for i in range(kind.num_bus))
|
||||||
|
self.dca = tuple(DCA(self, i) for i in range(kind.num_dca))
|
||||||
|
self.fx = tuple(FX(self, i) for i in range(kind.num_fx))
|
||||||
|
self.fxreturn = tuple(adapter.FxRtn.make(self, i) for i in range(kind.num_fx))
|
||||||
|
self.config = Config.make(self)
|
||||||
|
self.auxin = tuple(adapter.AuxRtn.make(self, i) for i in range(kind.num_auxrtn))
|
||||||
|
|
||||||
|
def init_xair(self, *args, **kwargs):
|
||||||
|
defaultkwargs = {"ip": None, "port": 10024}
|
||||||
kwargs = defaultkwargs | kwargs
|
kwargs = defaultkwargs | kwargs
|
||||||
XAirRemote.__init__(self, *args, **kwargs)
|
XAirRemote.__init__(self, *args, **kwargs)
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
@ -121,17 +144,25 @@ def _make_remote(kind: KindMap) -> XAirRemote:
|
|||||||
self.strip = tuple(Strip.make(self, i) for i in range(kind.num_strip))
|
self.strip = tuple(Strip.make(self, i) for i in range(kind.num_strip))
|
||||||
self.bus = tuple(Bus.make(self, i) for i in range(kind.num_bus))
|
self.bus = tuple(Bus.make(self, i) for i in range(kind.num_bus))
|
||||||
self.dca = tuple(DCA(self, i) for i in range(kind.num_dca))
|
self.dca = tuple(DCA(self, i) for i in range(kind.num_dca))
|
||||||
|
self.fx = tuple(FX(self, i) for i in range(kind.num_fx))
|
||||||
self.fxsend = tuple(FXSend.make(self, i) for i in range(kind.num_fx))
|
self.fxsend = tuple(FXSend.make(self, i) for i in range(kind.num_fx))
|
||||||
self.fxreturn = tuple(FXReturn(self, i) for i in range(kind.num_fx))
|
self.fxreturn = tuple(FxRtn.make(self, i) for i in range(kind.num_fx))
|
||||||
self.config = Config.make(self)
|
self.config = Config.make(self)
|
||||||
self.aux = Aux.make(self)
|
self.auxreturn = AuxRtn.make(self)
|
||||||
self.rtn = tuple(Rtn.make(self, i) for i in range(kind.num_rtn))
|
|
||||||
|
|
||||||
|
if kind.id_ == "X32":
|
||||||
return type(
|
return type(
|
||||||
f"XAirRemote{kind}",
|
f"XAirRemote{kind}",
|
||||||
(XAirRemote,),
|
(XAirRemote,),
|
||||||
{
|
{
|
||||||
"__init__": init,
|
"__init__": init_x32,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return type(
|
||||||
|
f"XAirRemote{kind}",
|
||||||
|
(XAirRemote,),
|
||||||
|
{
|
||||||
|
"__init__": init_xair,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user