mx-remote 2.0.0__py3-none-any.whl
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.
- mx_remote/Interface.py +1656 -0
- mx_remote/Uid.py +137 -0
- mx_remote/__init__.py +12 -0
- mx_remote/api/BayConfig.py +53 -0
- mx_remote/api/__init__.py +8 -0
- mx_remote/const.py +20 -0
- mx_remote/main.py +117 -0
- mx_remote/proto/BayConfig.py +104 -0
- mx_remote/proto/Constants.py +382 -0
- mx_remote/proto/Data.py +142 -0
- mx_remote/proto/Factory.py +168 -0
- mx_remote/proto/FrameAmpDolbySettings.py +51 -0
- mx_remote/proto/FrameAmpZoneSettings.py +123 -0
- mx_remote/proto/FrameBase.py +80 -0
- mx_remote/proto/FrameBayConfig.py +47 -0
- mx_remote/proto/FrameBayConfigSecondary.py +43 -0
- mx_remote/proto/FrameBayHide.py +53 -0
- mx_remote/proto/FrameBayStatus.py +51 -0
- mx_remote/proto/FrameConnectStatus.py +38 -0
- mx_remote/proto/FrameDiscover.py +21 -0
- mx_remote/proto/FrameEDID.py +20 -0
- mx_remote/proto/FrameEDIDProfile.py +24 -0
- mx_remote/proto/FrameFilterStatus.py +39 -0
- mx_remote/proto/FrameFirmwareVersion.py +40 -0
- mx_remote/proto/FrameHeader.py +92 -0
- mx_remote/proto/FrameHello.py +77 -0
- mx_remote/proto/FrameLinks.py +45 -0
- mx_remote/proto/FrameMeshOperation.py +66 -0
- mx_remote/proto/FrameMirrorStatus.py +49 -0
- mx_remote/proto/FrameNetworkStatus.py +163 -0
- mx_remote/proto/FramePDUState.py +71 -0
- mx_remote/proto/FramePowerChange.py +38 -0
- mx_remote/proto/FrameRCAction.py +50 -0
- mx_remote/proto/FrameRCIr.py +17 -0
- mx_remote/proto/FrameRCKey.py +40 -0
- mx_remote/proto/FrameReboot.py +26 -0
- mx_remote/proto/FrameRoutingChange.py +66 -0
- mx_remote/proto/FrameSetName.py +27 -0
- mx_remote/proto/FrameSignalStatus.py +46 -0
- mx_remote/proto/FrameSignalStatusNew.py +285 -0
- mx_remote/proto/FrameSysTemperature.py +50 -0
- mx_remote/proto/FrameTXRCAction.py +58 -0
- mx_remote/proto/FrameTopology.py +36 -0
- mx_remote/proto/FrameV2IPDeviceConfiguration.py +86 -0
- mx_remote/proto/FrameV2IPLink.py +16 -0
- mx_remote/proto/FrameV2IPSetMaster.py +16 -0
- mx_remote/proto/FrameV2IPSourceSwitch.py +84 -0
- mx_remote/proto/FrameV2IPSources.py +43 -0
- mx_remote/proto/FrameV2IPStats.py +55 -0
- mx_remote/proto/FrameV2IPStreamDetails.py +46 -0
- mx_remote/proto/FrameVolume.py +62 -0
- mx_remote/proto/FrameVolumeDown.py +28 -0
- mx_remote/proto/FrameVolumeSet.py +81 -0
- mx_remote/proto/FrameVolumeUp.py +28 -0
- mx_remote/proto/LinkConfig.py +121 -0
- mx_remote/proto/PDUState.py +95 -0
- mx_remote/proto/Svd.py +69 -0
- mx_remote/proto/V2IPConfig.py +71 -0
- mx_remote/proto/V2IPStats.py +126 -0
- mx_remote/proto/__init__.py +8 -0
- mx_remote/proto/svd.csv +157 -0
- mx_remote/remote/Bay.py +923 -0
- mx_remote/remote/ConnectionAsync.py +132 -0
- mx_remote/remote/Device.py +530 -0
- mx_remote/remote/Link.py +187 -0
- mx_remote/remote/PDU.py +152 -0
- mx_remote/remote/Remote.py +300 -0
- mx_remote/remote/State.py +28 -0
- mx_remote/remote/V2IP.py +83 -0
- mx_remote-2.0.0.dist-info/METADATA +90 -0
- mx_remote-2.0.0.dist-info/RECORD +74 -0
- mx_remote-2.0.0.dist-info/WHEEL +4 -0
- mx_remote-2.0.0.dist-info/entry_points.txt +2 -0
- mx_remote-2.0.0.dist-info/licenses/LICENSE +11 -0
mx_remote/remote/Link.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
##################################################
|
|
2
|
+
## MX Remote Python Interface ##
|
|
3
|
+
## ##
|
|
4
|
+
## author: Lars Op den Kamp (lars@opdenkamp.eu) ##
|
|
5
|
+
## copyright (c) 2024 Op den Kamp IT Solutions ##
|
|
6
|
+
##################################################
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
from ..proto import LinkConfig
|
|
10
|
+
from ..proto import Constants as proto
|
|
11
|
+
from typing import List, Tuple
|
|
12
|
+
from ..Interface import BayBase, MxrCallbacks
|
|
13
|
+
|
|
14
|
+
class Link:
|
|
15
|
+
''' Link between 2 bays on 2 devices '''
|
|
16
|
+
|
|
17
|
+
def __init__(self, bay:BayBase, link_data:LinkConfig.LinkConfig):
|
|
18
|
+
self._bay = bay
|
|
19
|
+
self._link = link_data
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def callbacks(self) -> MxrCallbacks:
|
|
23
|
+
return self._bay.callbacks
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def bays(self) -> List[BayBase]:
|
|
27
|
+
if self.configured:
|
|
28
|
+
return [ self._link.remote_bay, self._link.linked_bay ]
|
|
29
|
+
return [ self._bay ]
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def configured(self) -> bool:
|
|
33
|
+
return (self._link is not None) and self._link.is_linked
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def connected(self) -> bool:
|
|
37
|
+
return self.configured and (self._link.linked_bay is not None)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def online(self) -> bool:
|
|
41
|
+
return self.connected and (self._link.linked_bay.online)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def primary(self) -> BayBase:
|
|
45
|
+
# source type bay for linked bays. if only 1 side has been registered, that bay will be returned
|
|
46
|
+
if not self.connected:
|
|
47
|
+
return self._bay
|
|
48
|
+
if self._link.remote_bay.is_input:
|
|
49
|
+
return self._link.remote_bay
|
|
50
|
+
return self._link.linked_bay
|
|
51
|
+
|
|
52
|
+
def is_primary(self, bay:BayBase) -> bool:
|
|
53
|
+
# check whether the given bay is the primary bay of this link
|
|
54
|
+
primary = self.primary
|
|
55
|
+
return (primary is not None) and (bay == primary)
|
|
56
|
+
|
|
57
|
+
def other_bay(self, bay:BayBase) -> BayBase:
|
|
58
|
+
# return the other side of this link
|
|
59
|
+
if not self.connected:
|
|
60
|
+
return None
|
|
61
|
+
if bay == self._link.linked_bay:
|
|
62
|
+
return self._link.remote_bay
|
|
63
|
+
return self._link.linked_bay
|
|
64
|
+
|
|
65
|
+
def other_serial_bay(self, bay:BayBase) -> Tuple[str,str]:
|
|
66
|
+
# return the configuration for the other end of this link (serial + bay)
|
|
67
|
+
other_bay = self.other_bay(bay)
|
|
68
|
+
if other_bay is None:
|
|
69
|
+
return (None, None)
|
|
70
|
+
return (other_bay.device.serial, other_bay.port)
|
|
71
|
+
|
|
72
|
+
def other_serial_bay_str(self, bay:BayBase) -> str:
|
|
73
|
+
# return the link configuration for the given bay as string
|
|
74
|
+
link_serial, link_bay = self.other_serial_bay(bay)
|
|
75
|
+
if (link_serial is None) or (link_bay is None):
|
|
76
|
+
return ""
|
|
77
|
+
return f"{link_serial} {link_bay}"
|
|
78
|
+
|
|
79
|
+
def serial_bays(self) -> List[str]:
|
|
80
|
+
# return this link configuration as list of strings
|
|
81
|
+
rv = []
|
|
82
|
+
primary = self.primary
|
|
83
|
+
rv.append(f"{primary.device.serial} {primary.bay_name}")
|
|
84
|
+
if self.configured:
|
|
85
|
+
link_serial, link_bay = self.other_serial_bay(primary)
|
|
86
|
+
rv.append(f"{link_serial} {link_bay}")
|
|
87
|
+
return rv
|
|
88
|
+
|
|
89
|
+
def update(self, config:LinkConfig) -> None:
|
|
90
|
+
link = Link(config.remote_bay, config)
|
|
91
|
+
# update this link configuration with the new data from mx_remote
|
|
92
|
+
if (self.configured and not link.configured) or (self.other_bay(self._bay) != link.other_bay(self._bay)):
|
|
93
|
+
self.callbacks.on_bay_unlinked(self._bay, self)
|
|
94
|
+
self.callbacks.on_bay_unlinked(self.other_bay(self._bay), self)
|
|
95
|
+
|
|
96
|
+
if (not self.configured and link.configured) or (self.other_bay(self._bay) != link.other_bay(self._bay)):
|
|
97
|
+
self.callbacks.on_bay_linked(self._bay, link)
|
|
98
|
+
self.callbacks.on_bay_linked(link.other_bay(self._bay), link)
|
|
99
|
+
self._link = link._link
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def is_audio(self) -> bool:
|
|
103
|
+
# audio link
|
|
104
|
+
ft = self.features_mask
|
|
105
|
+
return (ft & proto.MX_LINK_FEATURE_AUDIO_OPTICAL) != 0 or \
|
|
106
|
+
(ft & proto.MX_LINK_FEATURE_AUDIO_ANALOG) != 0
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def is_video(self) -> bool:
|
|
110
|
+
# video link
|
|
111
|
+
return (self.features_mask & proto.MX_LINK_FEATURE_VIDEO_HDMI) != 0
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def features(self) -> List[str]:
|
|
115
|
+
# features supported by this link (strings)
|
|
116
|
+
ft = []
|
|
117
|
+
m = self.features_mask
|
|
118
|
+
if (m & proto.MX_LINK_FEATURE_VIDEO_HDMI):
|
|
119
|
+
ft.append("HDMI")
|
|
120
|
+
if (m & proto.MX_LINK_FEATURE_AUDIO_OPTICAL):
|
|
121
|
+
ft.append("optical audio")
|
|
122
|
+
if (m & proto.MX_LINK_FEATURE_AUDIO_ANALOG):
|
|
123
|
+
ft.append("analog audio")
|
|
124
|
+
if (m & proto.MX_LINK_FEATURE_IR):
|
|
125
|
+
ft.append("IR")
|
|
126
|
+
if (m & proto.MX_LINK_FEATURE_RC):
|
|
127
|
+
ft.append("RC")
|
|
128
|
+
return ft
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def features_mask(self) -> int:
|
|
132
|
+
# features supported by this link (bitmask)
|
|
133
|
+
bays = self.bays
|
|
134
|
+
if len(bays) < 2:
|
|
135
|
+
return 0
|
|
136
|
+
left = bays[0].features_mask
|
|
137
|
+
right = bays[1].features_mask
|
|
138
|
+
rv = 0
|
|
139
|
+
if (left & proto.MX_BAY_FEATURE_HDMI_OUT):
|
|
140
|
+
if (right & proto.MX_BAY_FEATURE_HDMI_IN):
|
|
141
|
+
rv |= proto.MX_LINK_FEATURE_VIDEO_HDMI
|
|
142
|
+
if (left & proto.MX_BAY_FEATURE_HDMI_IN):
|
|
143
|
+
if (right & proto.MX_BAY_FEATURE_HDMI_OUT):
|
|
144
|
+
rv |= proto.MX_LINK_FEATURE_VIDEO_HDMI
|
|
145
|
+
if (left & proto.MX_BAY_FEATURE_AUDIO_DIG_OUT):
|
|
146
|
+
if (right & proto.MX_BAY_FEATURE_AUDIO_DIG_IN):
|
|
147
|
+
rv |= proto.MX_LINK_FEATURE_AUDIO_OPTICAL
|
|
148
|
+
if (left & proto.MX_BAY_FEATURE_AUDIO_DIG_IN):
|
|
149
|
+
if (right & proto.MX_BAY_FEATURE_AUDIO_DIG_OUT):
|
|
150
|
+
rv |= proto.MX_LINK_FEATURE_AUDIO_OPTICAL
|
|
151
|
+
if (left & proto.MX_BAY_FEATURE_AUDIO_ANA_OUT):
|
|
152
|
+
if (right & proto.MX_BAY_FEATURE_AUDIO_ANA_IN):
|
|
153
|
+
rv |= proto.MX_LINK_FEATURE_AUDIO_ANALOG
|
|
154
|
+
if (left & proto.MX_BAY_FEATURE_AUDIO_ANA_IN):
|
|
155
|
+
if (right & proto.MX_BAY_FEATURE_AUDIO_ANA_OUT):
|
|
156
|
+
rv |= proto.MX_LINK_FEATURE_AUDIO_ANALOG
|
|
157
|
+
if (left & proto.MX_BAY_FEATURE_IR_OUT):
|
|
158
|
+
if (right & proto.MX_BAY_FEATURE_IR_IN):
|
|
159
|
+
rv |= proto.MX_LINK_FEATURE_IR
|
|
160
|
+
if (left & proto.MX_BAY_FEATURE_IR_IN):
|
|
161
|
+
if (right & proto.MX_BAY_FEATURE_IR_OUT):
|
|
162
|
+
rv |= proto.MX_LINK_FEATURE_IR
|
|
163
|
+
if (left & proto.MX_BAY_FEATURE_RC_OUT):
|
|
164
|
+
if (right & proto.MX_BAY_FEATURE_RC_IN):
|
|
165
|
+
rv |= proto.MX_LINK_FEATURE_RC
|
|
166
|
+
if (left & proto.MX_BAY_FEATURE_RC_IN):
|
|
167
|
+
if (right & proto.MX_BAY_FEATURE_RC_OUT):
|
|
168
|
+
rv |= proto.MX_LINK_FEATURE_RC
|
|
169
|
+
return rv
|
|
170
|
+
|
|
171
|
+
def __eq__(self, other:Link) -> bool:
|
|
172
|
+
return (self.configured == other.configured) and \
|
|
173
|
+
(((self._bay == other._bay) and (self.other_bay(self._bay) == other.other_bay(self._bay))) or \
|
|
174
|
+
((self.other_bay(self._bay) == other._bay) and (self._bay == other.other_bay(self._bay))))
|
|
175
|
+
|
|
176
|
+
def __str__(self) -> str:
|
|
177
|
+
primary = self.primary
|
|
178
|
+
if primary is None:
|
|
179
|
+
return "bay link incomplete"
|
|
180
|
+
if not self.configured:
|
|
181
|
+
return "{} not linked".format(str(primary))
|
|
182
|
+
other = self.other_bay(primary)
|
|
183
|
+
if other is None:
|
|
184
|
+
link_serial, link_bay = self.other_serial_bay(primary)
|
|
185
|
+
return "{} linked to ({} {}) - disconnected".format(str(primary), link_serial, link_bay)
|
|
186
|
+
return "{} linked to {} - {}".format(str(primary), str(other), str(self.features))
|
|
187
|
+
|
mx_remote/remote/PDU.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
##################################################
|
|
2
|
+
## MX Remote Python Interface ##
|
|
3
|
+
## ##
|
|
4
|
+
## author: Lars Op den Kamp (lars@opdenkamp.eu) ##
|
|
5
|
+
## copyright (c) 2024 Op den Kamp IT Solutions ##
|
|
6
|
+
##################################################
|
|
7
|
+
|
|
8
|
+
from ..proto.PDUState import PDUState
|
|
9
|
+
from typing import Any, List
|
|
10
|
+
|
|
11
|
+
class PDUOutlet:
|
|
12
|
+
''' One of the 8 PDU outlets '''
|
|
13
|
+
|
|
14
|
+
def __init__(self, pdu: Any, port: int, state: PDUState):
|
|
15
|
+
self._pdu = pdu
|
|
16
|
+
self._port = port
|
|
17
|
+
self._state = state
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def port(self) -> int:
|
|
21
|
+
# port number
|
|
22
|
+
return self._port
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def state(self) -> PDUState:
|
|
26
|
+
return self._state
|
|
27
|
+
|
|
28
|
+
@state.setter
|
|
29
|
+
def state(self, val: PDUState):
|
|
30
|
+
self._state = val
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def is_on(self) -> bool:
|
|
34
|
+
return self._state.is_on
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def is_off(self) -> bool:
|
|
38
|
+
return self._state.is_off
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def is_rebooting(self) -> bool:
|
|
42
|
+
return self._state.is_rebooting
|
|
43
|
+
|
|
44
|
+
async def turn_on(self):
|
|
45
|
+
return await self._pdu.dev.get_api('power/on/{}'.format(self.port))
|
|
46
|
+
|
|
47
|
+
async def turn_off(self):
|
|
48
|
+
return await self._pdu.dev.get_api('power/off/{}'.format(self.port))
|
|
49
|
+
|
|
50
|
+
def __str__(self) -> str:
|
|
51
|
+
return str(self._state)
|
|
52
|
+
|
|
53
|
+
def __repr__(self) -> str:
|
|
54
|
+
return "{}={}".format(str(self._port), str(self))
|
|
55
|
+
|
|
56
|
+
class PDU:
|
|
57
|
+
''' Optional PDU connected to a matrix '''
|
|
58
|
+
def __init__(self, dev, init_val:PDUState):
|
|
59
|
+
self._dev = dev
|
|
60
|
+
self._val = None
|
|
61
|
+
self._outlets = []
|
|
62
|
+
self.on_mxr_update(init_val)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def dev(self):
|
|
66
|
+
return self._dev
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def mxr(self) -> Any:
|
|
70
|
+
# mxremote instance
|
|
71
|
+
return self._dev.mxr
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def connected(self) -> bool:
|
|
75
|
+
voltage = self.voltage
|
|
76
|
+
return (voltage is not None) and (voltage > 0.0)
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def current(self) -> float:
|
|
80
|
+
if self._val is None:
|
|
81
|
+
return None
|
|
82
|
+
return self._val.current
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def voltage(self) -> float:
|
|
86
|
+
if self._val is None:
|
|
87
|
+
return None
|
|
88
|
+
return self._val.voltage
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def power(self) -> float:
|
|
92
|
+
if self._val is None:
|
|
93
|
+
return None
|
|
94
|
+
return self._val.power
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def dissipation(self) -> float:
|
|
98
|
+
if self._val is None:
|
|
99
|
+
return None
|
|
100
|
+
return self._val.dissipation
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def power_factor(self) -> float:
|
|
104
|
+
if self._val is None:
|
|
105
|
+
return None
|
|
106
|
+
return self._val.power_factor
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def frequency(self) -> float:
|
|
110
|
+
if self._val is None:
|
|
111
|
+
return None
|
|
112
|
+
return self._val.frequency
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def outlets(self) -> List[PDUOutlet]:
|
|
116
|
+
if self._val is None:
|
|
117
|
+
return None
|
|
118
|
+
return self._outlets
|
|
119
|
+
|
|
120
|
+
def outlet(self, port) -> PDUOutlet:
|
|
121
|
+
if self._val is None:
|
|
122
|
+
return None
|
|
123
|
+
return self._outlets[port]
|
|
124
|
+
|
|
125
|
+
def on_mxr_update(self, pdu_state:PDUState) -> None:
|
|
126
|
+
changed = self._val is None or (self._val != pdu_state)
|
|
127
|
+
is_new = (len(self._outlets) == 0)
|
|
128
|
+
self._val = pdu_state
|
|
129
|
+
port = 0
|
|
130
|
+
for outlet in pdu_state.outlets:
|
|
131
|
+
if is_new:
|
|
132
|
+
self._outlets.append(PDUOutlet(self, port, outlet))
|
|
133
|
+
else:
|
|
134
|
+
self._outlets[port].state = outlet
|
|
135
|
+
port = port + 1
|
|
136
|
+
if changed and not is_new:
|
|
137
|
+
# tell callbacks that this device changed
|
|
138
|
+
self.mxr.on_pdu_changed(self)
|
|
139
|
+
|
|
140
|
+
def __str__(self) -> str:
|
|
141
|
+
return "current = {}A, voltage = {}V, power = {}W, diss = {}W, freq = {}Hz, outlets = {}".format(str(self.current), str(self.voltage), str(self.power), str(self.dissipation), str(self.frequency), str(self.outlets))
|
|
142
|
+
|
|
143
|
+
def __repr__(self) -> str:
|
|
144
|
+
return str(self)
|
|
145
|
+
|
|
146
|
+
def __eq__(self, other) -> bool:
|
|
147
|
+
return isinstance(other, PDU) and \
|
|
148
|
+
(self.dev == other.dev)
|
|
149
|
+
|
|
150
|
+
def __ne__(self, other) -> bool:
|
|
151
|
+
return (not isinstance(other, PDU)) or \
|
|
152
|
+
(self.dev != other.dev)
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
##################################################
|
|
2
|
+
## MX Remote Python Interface ##
|
|
3
|
+
## ##
|
|
4
|
+
## author: Lars Op den Kamp (lars@opdenkamp.eu) ##
|
|
5
|
+
## copyright (c) 2024 Op den Kamp IT Solutions ##
|
|
6
|
+
##################################################
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import asyncio
|
|
10
|
+
import aiofiles
|
|
11
|
+
import aiohttp
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import time
|
|
15
|
+
|
|
16
|
+
from .ConnectionAsync import ConnectionAsync
|
|
17
|
+
from ..const import __version__
|
|
18
|
+
from .Device import Device
|
|
19
|
+
from ..Interface import ConnectionCallbacks, DeviceRegistry, MxrDeviceUid, BayLinks, BayBase, DeviceBase, MxrCallbacks
|
|
20
|
+
from ..proto.Constants import MXR_PROTOCOL_VERSION
|
|
21
|
+
from ..proto.FrameDiscover import FrameDiscover
|
|
22
|
+
from ..proto.Factory import process_mxr_frame
|
|
23
|
+
from ..proto.FrameBase import FrameBase
|
|
24
|
+
from ..proto.FrameHello import FrameHello
|
|
25
|
+
from ..Uid import MxrDeviceUid
|
|
26
|
+
from .State import State
|
|
27
|
+
|
|
28
|
+
import traceback
|
|
29
|
+
|
|
30
|
+
from ..const import MX_BCAST_UDP_IP, MX_BCAST_UDP_PORT, MX_MCAST_UDP_IP, MX_MCAST_UDP_PORT
|
|
31
|
+
|
|
32
|
+
_LOGGER = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
class Remote(DeviceRegistry, ConnectionCallbacks):
|
|
35
|
+
''' Main component that handles the network connections and registration of remote devices '''
|
|
36
|
+
|
|
37
|
+
def __init__(self, target_ip:str|None=None, port:int|None=None, http_session:aiohttp.ClientSession|None=None, open_connection:bool=True, callbacks:MxrCallbacks|None=None, name:str="MXR Python", local_ip:str|None=None, broadcast:bool|None=None) -> None:
|
|
38
|
+
DeviceRegistry.__init__(self)
|
|
39
|
+
ConnectionCallbacks.__init__(self)
|
|
40
|
+
self._name = name
|
|
41
|
+
self._close_session = False
|
|
42
|
+
self._http_session = None
|
|
43
|
+
self._callbacks = State(callbacks, http_session)
|
|
44
|
+
self.remotes:dict[MxrDeviceUid,Device] = {}
|
|
45
|
+
self._links = BayLinks(self)
|
|
46
|
+
self._last_hello = 0
|
|
47
|
+
self._tasks = set()
|
|
48
|
+
self._uid:bytes|None = None
|
|
49
|
+
self._local_ip = local_ip
|
|
50
|
+
self._broadcast = broadcast
|
|
51
|
+
self._target_ip = target_ip
|
|
52
|
+
self._port = port
|
|
53
|
+
self._discover_timeout = 0
|
|
54
|
+
if open_connection:
|
|
55
|
+
self.conn = ConnectionAsync(callbacks=self, target_ip=self.target_ip, port=self.port, local_ip=self._local_ip)
|
|
56
|
+
else:
|
|
57
|
+
self.conn = None
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def library_version(self) -> str:
|
|
61
|
+
''' version of the mx_remote library '''
|
|
62
|
+
return __version__
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def protocol_version(self) -> int:
|
|
66
|
+
''' protocol version used by this library '''
|
|
67
|
+
return MXR_PROTOCOL_VERSION
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def net_protocol_version_max(self) -> int:
|
|
71
|
+
''' highest protocol version used by devices on the network '''
|
|
72
|
+
proto = 0
|
|
73
|
+
for _, device in self.remotes.items():
|
|
74
|
+
if (device.protocol > proto):
|
|
75
|
+
proto = device.protocol
|
|
76
|
+
return proto
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def net_protocol_version_min(self) -> int:
|
|
80
|
+
''' lowest protocol version used by devices on the network '''
|
|
81
|
+
proto = MXR_PROTOCOL_VERSION
|
|
82
|
+
for _, device in self.remotes.items():
|
|
83
|
+
if (device.protocol < proto):
|
|
84
|
+
proto = device.protocol
|
|
85
|
+
return proto
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def target_ip(self):
|
|
89
|
+
if self._target_ip is None:
|
|
90
|
+
return MX_MCAST_UDP_IP if (self._broadcast is None or not self._broadcast) else MX_BCAST_UDP_IP
|
|
91
|
+
return self._target_ip
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def local_ip(self) -> str:
|
|
95
|
+
return self._local_ip
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def broadcast(self) -> bool:
|
|
99
|
+
return ((self._broadcast is not None) and self._broadcast)
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def port(self):
|
|
103
|
+
if self._port is None:
|
|
104
|
+
return MX_MCAST_UDP_PORT if (self._broadcast is None or not self._broadcast) else MX_BCAST_UDP_PORT
|
|
105
|
+
return self._port
|
|
106
|
+
|
|
107
|
+
async def _load_uid(self) -> None:
|
|
108
|
+
if self._uid is not None:
|
|
109
|
+
return
|
|
110
|
+
uid_path = Path.home().joinpath(".mxr-uid")
|
|
111
|
+
try:
|
|
112
|
+
async with aiofiles.open(uid_path, "rb") as f:
|
|
113
|
+
self._uid = await f.read()
|
|
114
|
+
except:
|
|
115
|
+
_LOGGER.info(f"failed to read {uid_path}. creating new file")
|
|
116
|
+
self._uid = os.urandom(16)
|
|
117
|
+
async with aiofiles.open(uid_path, "wb") as f:
|
|
118
|
+
await f.write(self._uid)
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def uid_raw(self) -> bytes:
|
|
122
|
+
return self._uid
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def uid(self) -> MxrDeviceUid:
|
|
126
|
+
return MxrDeviceUid(self.uid_raw)
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def name(self) -> str:
|
|
130
|
+
return self._name
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def callbacks(self) -> MxrCallbacks:
|
|
134
|
+
return self._callbacks.callbacks
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def http_session(self) -> aiohttp.ClientSession:
|
|
138
|
+
''' Active HTTP client session for API commands '''
|
|
139
|
+
return self._callbacks.http_session
|
|
140
|
+
|
|
141
|
+
async def update_config(self, callbacks:MxrCallbacks|None=None, name:str|None=None, target_ip:str|None=None, port:int|None=None, local_ip:str|None=None, broadcast:bool|None=None):
|
|
142
|
+
if (callbacks is not None):
|
|
143
|
+
self._callbacks = callbacks
|
|
144
|
+
if (name is not None):
|
|
145
|
+
self._name = name
|
|
146
|
+
if (target_ip is not None) or (port is not None) or (local_ip is not None) or (broadcast is not None):
|
|
147
|
+
changed = False
|
|
148
|
+
if (target_ip is not None) and (self._target_ip != target_ip):
|
|
149
|
+
_LOGGER.debug(f"updating target ip to {target_ip}")
|
|
150
|
+
self._target_ip = target_ip
|
|
151
|
+
changed = True
|
|
152
|
+
if (port is not None) and (self._port != port):
|
|
153
|
+
_LOGGER.debug(f"updating target port to {port}")
|
|
154
|
+
self._port = port
|
|
155
|
+
changed = True
|
|
156
|
+
if (local_ip is not None) and (self._local_ip != local_ip):
|
|
157
|
+
_LOGGER.debug(f"updating target ip to {local_ip}")
|
|
158
|
+
self._local_ip = local_ip
|
|
159
|
+
changed = True
|
|
160
|
+
if (broadcast is not None) and (self._broadcast != broadcast):
|
|
161
|
+
_LOGGER.debug(f"updating target ip to {broadcast}")
|
|
162
|
+
self._broadcast = broadcast
|
|
163
|
+
changed = True
|
|
164
|
+
if changed:
|
|
165
|
+
if (self.conn is not None):
|
|
166
|
+
_LOGGER.debug(f"closing connection")
|
|
167
|
+
self.conn.close()
|
|
168
|
+
_LOGGER.debug(f"opening new mx_remote listener on target={self.target_ip} listener={self._local_ip}:{self.port}")
|
|
169
|
+
self.conn = ConnectionAsync(callbacks=self, target_ip=self.target_ip, port=self.port, local_ip=self._local_ip)
|
|
170
|
+
await self.conn.start_srv()
|
|
171
|
+
|
|
172
|
+
def has_completed_devices(self) -> bool:
|
|
173
|
+
for _, device in self.remotes.items():
|
|
174
|
+
if device.configuration_complete:
|
|
175
|
+
return True
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
async def _background_probe(self) -> None:
|
|
179
|
+
while not self._close_session:
|
|
180
|
+
await asyncio.sleep(1)
|
|
181
|
+
tx_discover = False
|
|
182
|
+
if (not self.has_completed_devices()):
|
|
183
|
+
tx_discover = True
|
|
184
|
+
else:
|
|
185
|
+
for _, device in self.remotes.items():
|
|
186
|
+
device.check_online()
|
|
187
|
+
if not device.check_configuration_complete_timeout():
|
|
188
|
+
tx_discover = True
|
|
189
|
+
if tx_discover and ((time.time() - self._discover_timeout) >= 5):
|
|
190
|
+
self.tx_discover()
|
|
191
|
+
|
|
192
|
+
async def start_async(self) -> None:
|
|
193
|
+
# start the server that listens for mx_remote frames from other devices
|
|
194
|
+
await self._load_uid()
|
|
195
|
+
await self.conn.start_srv()
|
|
196
|
+
checker = asyncio.create_task(self._background_probe())
|
|
197
|
+
self._tasks.add(checker)
|
|
198
|
+
checker.add_done_callback(self._tasks.discard)
|
|
199
|
+
|
|
200
|
+
async def close(self) -> None:
|
|
201
|
+
# close all open connections
|
|
202
|
+
_LOGGER.debug("closing mx_remote listener")
|
|
203
|
+
if self.conn is not None:
|
|
204
|
+
self.conn.close()
|
|
205
|
+
if self._close_session:
|
|
206
|
+
await self.http_session.close()
|
|
207
|
+
|
|
208
|
+
def get_by_serial(self, serial:str) -> DeviceBase|None:
|
|
209
|
+
# get the local cache for a device, given it's serial number
|
|
210
|
+
for _, remote in self.remotes.items():
|
|
211
|
+
if serial == remote.serial:
|
|
212
|
+
return remote
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
def get_by_uid(self, remote_id:str|MxrDeviceUid) -> DeviceBase|None:
|
|
216
|
+
# get the local cache for a device, given it's unique id
|
|
217
|
+
remote_id = MxrDeviceUid(remote_id)
|
|
218
|
+
if remote_id in self.remotes.keys():
|
|
219
|
+
return self.remotes[remote_id]
|
|
220
|
+
if isinstance(remote_id, str):
|
|
221
|
+
return self.get_by_serial(serial=remote_id)
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
def get_by_stream_ip(self, ip:str, audio:bool=False) -> BayBase|None:
|
|
225
|
+
for _, dev in self.remotes.items():
|
|
226
|
+
if not dev.is_v2ip or dev.v2ip_sources is None or dev.first_input is None:
|
|
227
|
+
continue
|
|
228
|
+
if not audio and (dev.first_input.v2ip_source.video.ip == ip):
|
|
229
|
+
return dev.first_input
|
|
230
|
+
if audio and (dev.first_input.v2ip_source.audio.ip == ip):
|
|
231
|
+
return dev.first_input
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
def get_bay_by_portnum(self, remote_id:str|MxrDeviceUid, portnum:int) -> BayBase|None:
|
|
235
|
+
# get the local cache for a bay, given the device's unique id and port number
|
|
236
|
+
device = self.get_by_uid(remote_id=remote_id) if isinstance(remote_id, MxrDeviceUid) else self.get_by_serial(serial=remote_id)
|
|
237
|
+
if device is None:
|
|
238
|
+
return None
|
|
239
|
+
return device.get_by_portnum(portnum)
|
|
240
|
+
|
|
241
|
+
def get_bay_by_portname(self, remote_id:str|MxrDeviceUid, portname:str) -> BayBase|None:
|
|
242
|
+
device = self.get_by_uid(remote_id=remote_id) if isinstance(remote_id, MxrDeviceUid) else self.get_by_serial(serial=remote_id)
|
|
243
|
+
if device is None:
|
|
244
|
+
return None
|
|
245
|
+
return device.get_by_portname(portname=portname)
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def links(self) -> BayLinks:
|
|
249
|
+
return self._links
|
|
250
|
+
|
|
251
|
+
def transmit(self, data: bytes) -> int:
|
|
252
|
+
return self.conn.transmit(data=data)
|
|
253
|
+
|
|
254
|
+
def tx_discover(self) -> int:
|
|
255
|
+
# transmit a discover frame. all remotes will send a hello frame as response
|
|
256
|
+
pkt:FrameBase = FrameDiscover.construct(self)
|
|
257
|
+
self._discover_timeout = time.time()
|
|
258
|
+
_LOGGER.debug("discovering devices")
|
|
259
|
+
return self.transmit(pkt.frame)
|
|
260
|
+
|
|
261
|
+
def tx_hello(self):
|
|
262
|
+
pkt:FrameBase = FrameHello.construct(self)
|
|
263
|
+
_LOGGER.debug("sending hello")
|
|
264
|
+
self._last_hello = time.time()
|
|
265
|
+
return self.transmit(pkt.frame)
|
|
266
|
+
|
|
267
|
+
def on_connection_made(self) -> None:
|
|
268
|
+
# callback called after the server got started by ConnectionAsync
|
|
269
|
+
self.tx_discover()
|
|
270
|
+
self.tx_hello()
|
|
271
|
+
|
|
272
|
+
def on_datagram_received(self, data: bytes, addr: tuple[str, int]) -> None:
|
|
273
|
+
# called when a udp frame was received
|
|
274
|
+
try:
|
|
275
|
+
frame = process_mxr_frame(self, data, addr)
|
|
276
|
+
if frame is not None:
|
|
277
|
+
_LOGGER.debug(f"rx {addr[0]}: {frame.header.opcode:02X}({len(frame)}) - {str(frame)}")
|
|
278
|
+
except Exception as e:
|
|
279
|
+
_LOGGER.warning(f"failed to decode frame {traceback.format_exc()}")
|
|
280
|
+
raise
|
|
281
|
+
try:
|
|
282
|
+
if frame is not None:
|
|
283
|
+
frame.process()
|
|
284
|
+
except Exception as e:
|
|
285
|
+
_LOGGER.warning(f"failed to process frame: {traceback.format_exc()}")
|
|
286
|
+
raise
|
|
287
|
+
|
|
288
|
+
if self.conn is not None:
|
|
289
|
+
now = time.time()
|
|
290
|
+
if (now - self._last_hello >= 30):
|
|
291
|
+
self.tx_hello()
|
|
292
|
+
|
|
293
|
+
def on_mxr_hello(self, hello_frame:FrameHello) -> Device:
|
|
294
|
+
# hello frame received. register or update the local device cache
|
|
295
|
+
d = self.get_by_uid(hello_frame.remote_id)
|
|
296
|
+
if d is None:
|
|
297
|
+
d = Device(self, hello_frame)
|
|
298
|
+
self.remotes[hello_frame.remote_id] = d
|
|
299
|
+
d.on_mxr_hello(hello_frame)
|
|
300
|
+
return d
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
##################################################
|
|
2
|
+
## MX Remote Python Interface ##
|
|
3
|
+
## ##
|
|
4
|
+
## author: Lars Op den Kamp (lars@opdenkamp.eu) ##
|
|
5
|
+
## copyright (c) 2024 Op den Kamp IT Solutions ##
|
|
6
|
+
##################################################
|
|
7
|
+
|
|
8
|
+
from ..Interface import MxrCallbacks
|
|
9
|
+
import aiohttp
|
|
10
|
+
|
|
11
|
+
class State:
|
|
12
|
+
def __init__(self, callbacks:MxrCallbacks|None=None, http_session:aiohttp.ClientSession|None=None) -> None:
|
|
13
|
+
if callbacks is None:
|
|
14
|
+
self._callbacks = MxrCallbacks()
|
|
15
|
+
else:
|
|
16
|
+
self._callbacks = callbacks
|
|
17
|
+
self._close_session = (http_session is not None)
|
|
18
|
+
self._http_session = http_session
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def http_session(self) -> aiohttp.ClientSession:
|
|
22
|
+
if self._http_session is None:
|
|
23
|
+
self._http_session = aiohttp.ClientSession()
|
|
24
|
+
return self._http_session
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def callbacks(self) -> MxrCallbacks:
|
|
28
|
+
return self._callbacks
|