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.
Files changed (74) hide show
  1. mx_remote/Interface.py +1656 -0
  2. mx_remote/Uid.py +137 -0
  3. mx_remote/__init__.py +12 -0
  4. mx_remote/api/BayConfig.py +53 -0
  5. mx_remote/api/__init__.py +8 -0
  6. mx_remote/const.py +20 -0
  7. mx_remote/main.py +117 -0
  8. mx_remote/proto/BayConfig.py +104 -0
  9. mx_remote/proto/Constants.py +382 -0
  10. mx_remote/proto/Data.py +142 -0
  11. mx_remote/proto/Factory.py +168 -0
  12. mx_remote/proto/FrameAmpDolbySettings.py +51 -0
  13. mx_remote/proto/FrameAmpZoneSettings.py +123 -0
  14. mx_remote/proto/FrameBase.py +80 -0
  15. mx_remote/proto/FrameBayConfig.py +47 -0
  16. mx_remote/proto/FrameBayConfigSecondary.py +43 -0
  17. mx_remote/proto/FrameBayHide.py +53 -0
  18. mx_remote/proto/FrameBayStatus.py +51 -0
  19. mx_remote/proto/FrameConnectStatus.py +38 -0
  20. mx_remote/proto/FrameDiscover.py +21 -0
  21. mx_remote/proto/FrameEDID.py +20 -0
  22. mx_remote/proto/FrameEDIDProfile.py +24 -0
  23. mx_remote/proto/FrameFilterStatus.py +39 -0
  24. mx_remote/proto/FrameFirmwareVersion.py +40 -0
  25. mx_remote/proto/FrameHeader.py +92 -0
  26. mx_remote/proto/FrameHello.py +77 -0
  27. mx_remote/proto/FrameLinks.py +45 -0
  28. mx_remote/proto/FrameMeshOperation.py +66 -0
  29. mx_remote/proto/FrameMirrorStatus.py +49 -0
  30. mx_remote/proto/FrameNetworkStatus.py +163 -0
  31. mx_remote/proto/FramePDUState.py +71 -0
  32. mx_remote/proto/FramePowerChange.py +38 -0
  33. mx_remote/proto/FrameRCAction.py +50 -0
  34. mx_remote/proto/FrameRCIr.py +17 -0
  35. mx_remote/proto/FrameRCKey.py +40 -0
  36. mx_remote/proto/FrameReboot.py +26 -0
  37. mx_remote/proto/FrameRoutingChange.py +66 -0
  38. mx_remote/proto/FrameSetName.py +27 -0
  39. mx_remote/proto/FrameSignalStatus.py +46 -0
  40. mx_remote/proto/FrameSignalStatusNew.py +285 -0
  41. mx_remote/proto/FrameSysTemperature.py +50 -0
  42. mx_remote/proto/FrameTXRCAction.py +58 -0
  43. mx_remote/proto/FrameTopology.py +36 -0
  44. mx_remote/proto/FrameV2IPDeviceConfiguration.py +86 -0
  45. mx_remote/proto/FrameV2IPLink.py +16 -0
  46. mx_remote/proto/FrameV2IPSetMaster.py +16 -0
  47. mx_remote/proto/FrameV2IPSourceSwitch.py +84 -0
  48. mx_remote/proto/FrameV2IPSources.py +43 -0
  49. mx_remote/proto/FrameV2IPStats.py +55 -0
  50. mx_remote/proto/FrameV2IPStreamDetails.py +46 -0
  51. mx_remote/proto/FrameVolume.py +62 -0
  52. mx_remote/proto/FrameVolumeDown.py +28 -0
  53. mx_remote/proto/FrameVolumeSet.py +81 -0
  54. mx_remote/proto/FrameVolumeUp.py +28 -0
  55. mx_remote/proto/LinkConfig.py +121 -0
  56. mx_remote/proto/PDUState.py +95 -0
  57. mx_remote/proto/Svd.py +69 -0
  58. mx_remote/proto/V2IPConfig.py +71 -0
  59. mx_remote/proto/V2IPStats.py +126 -0
  60. mx_remote/proto/__init__.py +8 -0
  61. mx_remote/proto/svd.csv +157 -0
  62. mx_remote/remote/Bay.py +923 -0
  63. mx_remote/remote/ConnectionAsync.py +132 -0
  64. mx_remote/remote/Device.py +530 -0
  65. mx_remote/remote/Link.py +187 -0
  66. mx_remote/remote/PDU.py +152 -0
  67. mx_remote/remote/Remote.py +300 -0
  68. mx_remote/remote/State.py +28 -0
  69. mx_remote/remote/V2IP.py +83 -0
  70. mx_remote-2.0.0.dist-info/METADATA +90 -0
  71. mx_remote-2.0.0.dist-info/RECORD +74 -0
  72. mx_remote-2.0.0.dist-info/WHEEL +4 -0
  73. mx_remote-2.0.0.dist-info/entry_points.txt +2 -0
  74. mx_remote-2.0.0.dist-info/licenses/LICENSE +11 -0
@@ -0,0 +1,132 @@
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 asyncio
9
+ import logging
10
+ import os
11
+ import socket
12
+ import ipaddress
13
+ from typing import Coroutine, Tuple
14
+ from ..Interface import ConnectionCallbacks, mxr_valid_addresses
15
+
16
+ _LOGGER = logging.getLogger(__name__)
17
+
18
+ def is_posix_os() -> bool:
19
+ return (os.name == 'posix')
20
+
21
+ class ConnectionAsync(asyncio.DatagramProtocol):
22
+ ''' send and receive UDP data '''
23
+ def __init__(self, callbacks:ConnectionCallbacks, target_ip:str, port:int, local_ip:str|None=None) -> None:
24
+ self._transport = None
25
+ self._callbacks = callbacks
26
+ self._target_ip = target_ip
27
+ self._local_ip = local_ip
28
+ self._port = port
29
+ self._closed = False
30
+ self._tx_socket:socket.socket = None
31
+ super().__init__()
32
+
33
+ @property
34
+ def tx_socket(self) -> socket.socket:
35
+ return self._tx_socket
36
+
37
+ @property
38
+ def is_open(self) -> bool:
39
+ return (self._transport is not None) and (not self._closed)
40
+
41
+ @property
42
+ def target_ip(self) -> str:
43
+ return self._target_ip
44
+
45
+ @property
46
+ def port(self) -> str:
47
+ return self._port
48
+
49
+ @property
50
+ def local_ip(self) -> str:
51
+ if (self._local_ip is None) or (len(self._local_ip) == 0):
52
+ addresses = mxr_valid_addresses()
53
+ if len(addresses) > 0:
54
+ self._local_ip = addresses[0]
55
+ return self._local_ip
56
+
57
+ @property
58
+ def is_multicast(self) -> bool:
59
+ return ipaddress.IPv4Address(self.target_ip).is_multicast
60
+
61
+ def _create_tx_socket(self) -> socket.socket:
62
+ local_ip = self.local_ip
63
+ if local_ip is None:
64
+ raise Exception("failed to find local ip address")
65
+ _LOGGER.debug(f"open tx socket {local_ip}:{self.port}")
66
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
67
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
68
+
69
+ if self.is_multicast:
70
+ sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 3)
71
+ else:
72
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
73
+
74
+ if is_posix_os():
75
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
76
+ sock.bind((local_ip, self.port))
77
+
78
+ return sock
79
+
80
+ def _create_rx_socket(self) -> socket.socket:
81
+ _LOGGER.debug(f"open rx socket {self.target_ip}:{self.port}")
82
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
83
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
84
+ if is_posix_os():
85
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
86
+
87
+ sock.bind(('', self.port))
88
+
89
+ if self.is_multicast:
90
+ sock.setsockopt(
91
+ socket.IPPROTO_IP,
92
+ socket.IP_ADD_MEMBERSHIP,
93
+ socket.inet_aton(self.target_ip) + socket.inet_aton(self.local_ip)
94
+ )
95
+ _LOGGER.debug(f"rx multicast joined")
96
+ else:
97
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
98
+
99
+ return sock
100
+
101
+ async def start_srv(self) -> Coroutine:
102
+ _LOGGER.debug(f"starting service on {self.target_ip}:{self.port}")
103
+ try:
104
+ loop = asyncio.get_event_loop()
105
+ self._closed = False
106
+ self._tx_socket = self._create_tx_socket()
107
+ return await loop.create_datagram_endpoint(
108
+ lambda: self, sock=self._create_rx_socket())
109
+ except Exception as e:
110
+ _LOGGER.warning(f"failed to start mx_remote service: {e}")
111
+ raise
112
+
113
+ def close(self) -> None:
114
+ if self.is_open:
115
+ _LOGGER.debug(f"closing {self.target_ip}:{self.port}")
116
+ self._transport.close()
117
+ self._closed = True
118
+
119
+ def connection_made(self, transport:asyncio.DatagramTransport) -> None:
120
+ _LOGGER.debug(f"listening on {self.target_ip}:{self.port} - {str(type(transport))}")
121
+ self._transport = transport
122
+ self._callbacks.on_connection_made()
123
+
124
+ def datagram_received(self, data: bytes, addr: Tuple[str, int]) -> None:
125
+ self._callbacks.on_datagram_received(data, addr)
126
+
127
+ def transmit(self, data: bytes) -> int:
128
+ if self._closed:
129
+ return 0
130
+
131
+ _LOGGER.debug(f"tx to {self.target_ip}:{self.port} (mcast:{self.is_multicast})")
132
+ return self.tx_socket.sendto(data, (self.target_ip, self.port))
@@ -0,0 +1,530 @@
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 aiohttp
9
+ from .Bay import Bay
10
+ from ..Interface import MxrCallbacks, V2IPStreamSources, AmpDolbySettings, DeviceStatus, DeviceV2IPDetailsBase
11
+ from .PDU import PDU
12
+ from ..proto.BayConfig import BayConfig
13
+ from ..proto.FrameHello import FrameHello
14
+ from ..proto.FrameSysTemperature import FrameSysTemperature
15
+ from ..proto.FrameNetworkStatus import NetworkPortStatus
16
+ from ..proto.PDUState import PDUState
17
+ from ..proto.V2IPStats import V2IPDeviceStats
18
+ from ..Uid import MxrDeviceUid
19
+ from typing import Any
20
+ from datetime import datetime
21
+ import logging
22
+ import time
23
+
24
+ from ..Interface import DeviceBase, BayBase, DeviceRegistry, DeviceFeatures
25
+
26
+ _LOGGER = logging.getLogger(__name__)
27
+
28
+ class Device(DeviceBase):
29
+ ''' remote device '''
30
+
31
+ def __init__(self, registry:DeviceRegistry, hello:FrameHello) -> None:
32
+ # initialise a new device after receiving a hello frame
33
+ self._bays:dict[str, BayBase] = {}
34
+ self._registry = registry
35
+ self._hello = hello
36
+ self._temperature = None
37
+ self._pdu = None
38
+ self._link_config_received = False
39
+ self._last_ping = datetime.now()
40
+ self._online = True
41
+ self._have_config = False
42
+ self._dolby_settings:AmpDolbySettings|None = None
43
+ self._v2ip_sources:list[V2IPStreamSources] = None
44
+ self._network:dict[int, NetworkPortStatus] = {}
45
+ self._v2ip_stats:V2IPDeviceStats = None
46
+ self._v2ip_details:DeviceV2IPDetailsBase = None
47
+ self._mesh_master_uid:MxrDeviceUid = None
48
+ self._rebooting = False
49
+ self._hello_received = time.time()
50
+ self._dev_callbacks:list[callable] = []
51
+
52
+ def register_callback(self, callback:callable) -> None:
53
+ '''register a callback, called when the device state changed'''
54
+ self._dev_callbacks.append(callback)
55
+
56
+ def unregister_callback(self, callback:callable) -> None:
57
+ '''unregister a callback'''
58
+ if callback in self._dev_callbacks:
59
+ self._dev_callbacks.remove(callback)
60
+
61
+ def call_callbacks(self) -> None:
62
+ for callback in self._dev_callbacks:
63
+ callback(self)
64
+
65
+ @property
66
+ def status(self) -> DeviceStatus:
67
+ if self.online:
68
+ if self.rebooting:
69
+ return DeviceStatus.REBOOTING
70
+ if self.booting:
71
+ return DeviceStatus.BOOTING
72
+ return DeviceStatus.ONLINE
73
+ return DeviceStatus.OFFLINE
74
+
75
+ @property
76
+ def bays(self) -> dict[str, BayBase]:
77
+ return self._bays
78
+
79
+ @property
80
+ def callbacks(self) -> MxrCallbacks:
81
+ return self._registry.callbacks
82
+
83
+ @property
84
+ def registry(self) -> DeviceRegistry:
85
+ return self._registry
86
+
87
+ @property
88
+ def online(self) -> bool:
89
+ # check whether this device has pinged in the last minute
90
+ return (datetime.now() - self._last_ping).total_seconds() < 120
91
+
92
+ @property
93
+ def rebooting(self) -> bool:
94
+ if self._rebooting:
95
+ return True
96
+ return self.online and self._hello.features.status_rebooting
97
+
98
+ @property
99
+ def booting(self) -> bool:
100
+ return self.online and not self._hello.features.status_rebooting and self.features.booting
101
+
102
+ def check_online(self) -> None:
103
+ if self.online != self._online:
104
+ self._online = not self._online
105
+ if not self._online:
106
+ self._have_config = False
107
+ self.callbacks.on_device_online_status_changed(self, self._online)
108
+ self.call_callbacks()
109
+
110
+ def on_link_config_received(self) -> None:
111
+ self._link_config_received = True
112
+ self._check_config_complete()
113
+
114
+ @property
115
+ def v2ip_sources(self) -> list[V2IPStreamSources]:
116
+ return self._v2ip_sources
117
+
118
+ @v2ip_sources.setter
119
+ def v2ip_sources(self, sources:list[V2IPStreamSources]) -> None:
120
+ if (self._v2ip_sources is None) or (self._v2ip_sources != sources):
121
+ self._v2ip_sources = sources
122
+ self.call_callbacks()
123
+
124
+ def v2ip_source(self, bay:BayBase) -> V2IPStreamSources|None:
125
+ if not bay.is_input or not bay.device.is_v2ip:
126
+ return None
127
+ if self._v2ip_sources is None:
128
+ return None
129
+ if bay.bay >= len(self._v2ip_sources):
130
+ return None
131
+ return self._v2ip_sources[bay.bay]
132
+
133
+ @property
134
+ def v2ip_stats(self) -> V2IPDeviceStats:
135
+ return self._v2ip_stats
136
+
137
+ @v2ip_stats.setter
138
+ def v2ip_stats(self, stats:V2IPDeviceStats) -> None:
139
+ self._v2ip_stats = stats
140
+ self.call_callbacks()
141
+
142
+ @property
143
+ def v2ip_details(self) -> DeviceV2IPDetailsBase:
144
+ return self._v2ip_details
145
+
146
+ @v2ip_details.setter
147
+ def v2ip_details(self, details:DeviceV2IPDetailsBase) -> None:
148
+ self._v2ip_details = details
149
+ self.call_callbacks()
150
+
151
+ @property
152
+ def v2ip_source_local(self) -> V2IPStreamSources|None:
153
+ input = self.first_input
154
+ if (input is None):
155
+ return None
156
+ return self.v2ip_source(input)
157
+
158
+ @property
159
+ def configuration_complete(self) -> bool:
160
+ '''check whether all configuration info for this device has been received'''
161
+ if not self.has_bays:
162
+ return False
163
+ if self.is_v2ip and (self.v2ip_sources is None):
164
+ return False
165
+ return not self.need_link_config
166
+
167
+ def check_configuration_complete_timeout(self) -> bool:
168
+ if self.configuration_complete:
169
+ # info received
170
+ return True
171
+ if ((time.time() - self._hello_received) > 15):
172
+ # configuration incomplete after 15 seconds
173
+ return False
174
+ # waiting for the timeout to pass
175
+ return True
176
+
177
+ @property
178
+ def protocol(self) -> int:
179
+ return self._hello.supported_protocol
180
+
181
+ @property
182
+ def name(self) -> str:
183
+ # remote device name
184
+ name = self._hello.device_name
185
+ if (len(name.strip()) == 0):
186
+ return "<unnamed>"
187
+ return name
188
+
189
+ @property
190
+ def address(self) -> str:
191
+ # remote ip address
192
+ return self._hello.address
193
+
194
+ @property
195
+ def serial(self) -> str:
196
+ # device serial number
197
+ return self._hello.serial
198
+
199
+ @property
200
+ def remote_id(self) -> MxrDeviceUid:
201
+ # device uid
202
+ return self._hello.remote_id
203
+
204
+ @property
205
+ def version(self) -> str:
206
+ # remote firwmare version
207
+ return self._hello.version
208
+
209
+ @property
210
+ def is_v2ip(self) -> bool:
211
+ return self.features.v2ip_sink or self.features.v2ip_source
212
+
213
+ @property
214
+ def has_local_source(self) -> bool:
215
+ '''True if this device has at least 1 local source'''
216
+ return self.first_input.is_local
217
+
218
+ @property
219
+ def has_local_sink(self) -> bool:
220
+ '''True if this device has at least 1 local sink'''
221
+ return self.first_output.is_local
222
+
223
+ @property
224
+ def is_video_matrix(self) -> bool:
225
+ # video matrix or not
226
+ return self.features.video_routing
227
+
228
+ @property
229
+ def is_audio_matrix(self) -> bool:
230
+ # audio matrix or not
231
+ return self.features.audio_routing and not self.features.video_routing
232
+
233
+ @property
234
+ def is_amp(self) -> bool:
235
+ # amp or not
236
+ return self.features.volume_control and self.features.audio_routing and not self.features.video_routing
237
+
238
+ @property
239
+ def temperatures(self) -> dict[str,int]:
240
+ if self._temperature is None:
241
+ return {}
242
+ temperatures = self._temperature.temperature
243
+ if self.is_v2ip:
244
+ return {
245
+ 'System': temperatures[0] if len(temperatures) > 0 else -1,
246
+ 'FPGA': temperatures[1] if len(temperatures) > 1 else -1,
247
+ 'Switch': temperatures[2] if len(temperatures) > 2 else -1,
248
+ }
249
+ rv = {}
250
+ cnt = 1
251
+ for temperature in temperatures:
252
+ rv[f'Sensor {cnt}'] = temperature
253
+ cnt += 1
254
+ return rv
255
+
256
+ @property
257
+ def pdu(self) -> PDU:
258
+ return self._pdu
259
+
260
+ @property
261
+ def pdu_connected(self) -> bool:
262
+ pdu = self.pdu
263
+ if pdu is not None:
264
+ return pdu.connected
265
+ return False
266
+
267
+ @property
268
+ def features(self) -> DeviceFeatures:
269
+ return self._hello.features
270
+
271
+ @property
272
+ def has_bays(self) -> bool:
273
+ # check whether the configuration for all bays has been received
274
+ return len(self.bays) >= (self.nb_inputs + self.nb_outputs)
275
+
276
+ @property
277
+ def inputs(self) -> dict[str, BayBase]:
278
+ # all sources available on this device
279
+ rv = {}
280
+ for _, bay in self.bays.items():
281
+ if bay.is_input and not bay.hidden:
282
+ rv[bay.bay_name] = bay
283
+ return rv
284
+
285
+ @property
286
+ def first_input(self) -> BayBase:
287
+ for _, bay in self.bays.items():
288
+ if bay.is_input:
289
+ return bay
290
+ return None
291
+
292
+ @property
293
+ def nb_inputs(self) -> int:
294
+ return len(self.inputs)
295
+
296
+ @property
297
+ def outputs(self) -> dict[str, BayBase]:
298
+ # all sinks available on this device
299
+ rv = {}
300
+ for _, bay in self.bays.items():
301
+ if bay.is_output:
302
+ rv[bay.bay_name] = bay
303
+ return rv
304
+
305
+ @property
306
+ def first_output(self) -> BayBase:
307
+ for _, bay in self.bays.items():
308
+ if bay.is_output:
309
+ return bay
310
+ return None
311
+
312
+ @property
313
+ def nb_outputs(self) -> int:
314
+ return len(self.outputs)
315
+
316
+ @property
317
+ def nb_hdbt(self) -> int:
318
+ # TODO hardcoded
319
+ if self.name[0:4] == 'FF88':
320
+ return 8
321
+ if self.name == 'PROAMP8':
322
+ return 0
323
+ if (self.name == 'FFMB44') or (self.name == 'FFMS44') or (self.name == 'SP14'):
324
+ return 4
325
+ #unknown model
326
+ return 0
327
+
328
+ @property
329
+ def model_name(self) -> str:
330
+ if self.is_v2ip:
331
+ if self.has_local_source and self.has_local_sink:
332
+ return 'OneIP TZ'
333
+ if self.has_local_source:
334
+ return 'OneIP TX'
335
+ return 'OneIP RX'
336
+ if (self.name == 'PROAMP8'):
337
+ return 'ProAmp8'
338
+ if (self.name == 'FFMB44'):
339
+ return 'neo:4 Bronze'
340
+ if (self.name == 'FFMS44'):
341
+ return 'neo:4 Silver'
342
+ if (self.name == 'FF88SA'):
343
+ return 'neo:X'
344
+ if (self.name == 'FF88S'):
345
+ return 'neo:X'
346
+ if (self.name == 'FF88'):
347
+ return 'neo:8'
348
+ if (self.name == 'SP14'):
349
+ return 'neo:4 Splitter'
350
+ return self.name
351
+
352
+ @property
353
+ def mesh_master(self) -> 'DeviceBase':
354
+ if not self.is_v2ip or (self._mesh_master_uid is None):
355
+ return self
356
+ return self.registry.get_by_uid(remote_id=self._mesh_master_uid)
357
+
358
+ @mesh_master.setter
359
+ def mesh_master(self, master:MxrDeviceUid) -> None:
360
+ if (self._mesh_master_uid is None) or (self._mesh_master_uid != master):
361
+ self._mesh_master_uid = master
362
+ self.call_callbacks()
363
+
364
+ @property
365
+ def is_mesh_master(self) -> bool:
366
+ return self.features.mesh_master
367
+
368
+ @property
369
+ def need_link_config(self) -> bool:
370
+ # check whether the link configuration has been received
371
+ if (self.is_amp or self.is_video_matrix or self.is_audio_matrix or self.is_v2ip):
372
+ return not self._link_config_received
373
+ return False
374
+
375
+ def get_by_portnum(self, portnum: int) -> BayBase:
376
+ # get a bay given its port number
377
+ if portnum in self.bays.keys():
378
+ return self.bays[portnum]
379
+ return None
380
+
381
+ def get_by_portname(self, portname: str) -> BayBase:
382
+ # get a bay given its port name
383
+ for _, bay in self.bays.items():
384
+ if bay.bay_name == portname:
385
+ return bay
386
+ return None
387
+
388
+ def on_mxr_hello(self, hello_frame:FrameHello) -> None:
389
+ # received a new hello frame from this device. update local info
390
+ self._last_ping = datetime.now()
391
+ changed = (self._hello != hello_frame)
392
+ self._hello = hello_frame
393
+ self._rebooting = False
394
+ if changed:
395
+ # tell callbacks that this device changed
396
+ self.callbacks.on_device_config_changed(self)
397
+ self.call_callbacks()
398
+
399
+ def on_mxr_temperature(self, temperature_frame:FrameSysTemperature) -> None:
400
+ changed = self._temperature is None or (self._temperature != temperature_frame)
401
+ self._temperature = temperature_frame
402
+ if changed:
403
+ # tell callbacks that this device changed
404
+ self.callbacks.on_device_temperature_changed(self)
405
+ self.call_callbacks()
406
+
407
+ def on_mxr_update_pdu(self, pdu_frame:PDUState) -> None:
408
+ self._last_ping = datetime.now()
409
+ if self._pdu is None:
410
+ self._pdu = PDU(self, pdu_frame)
411
+ self.callbacks.on_pdu_registered(self._pdu)
412
+ self.call_callbacks()
413
+ else:
414
+ self._pdu.on_mxr_update(pdu_frame)
415
+
416
+ @property
417
+ def dolby_settings(self) -> AmpDolbySettings|None:
418
+ return self._dolby_settings
419
+
420
+ @dolby_settings.setter
421
+ def dolby_settings(self, settings:AmpDolbySettings) -> None:
422
+ changed = (self._dolby_settings is None) or (self._dolby_settings != settings)
423
+ self._dolby_settings = settings
424
+ if changed:
425
+ self.callbacks.on_amp_dolby_settings_changed(self, settings)
426
+ self.call_callbacks()
427
+
428
+ def _check_config_complete(self) -> None:
429
+ if self.configuration_complete and not self._have_config:
430
+ # tell callbacks that all bays got registered for this device
431
+ self._have_config = True
432
+ self.callbacks.on_device_config_complete(self)
433
+ self.call_callbacks()
434
+
435
+ def on_mxr_bay_config(self, data:BayConfig) -> None:
436
+ self._last_ping = datetime.now()
437
+ bay = self.get_by_portnum(data.port)
438
+ isnew = (bay is None)
439
+ if bay is None:
440
+ bay = Bay(dev=self, data=data)
441
+ self.bays[data.port] = bay
442
+ bay.on_mxr_bay_config(data)
443
+ if isnew:
444
+ self.callbacks.on_bay_registered(bay)
445
+ self._check_config_complete()
446
+ self.call_callbacks()
447
+
448
+ @property
449
+ def amp_dolby_channels(self) -> int:
450
+ rv = 0
451
+ for _, bay in self.bays.items():
452
+ if bay.dolby_input is not None:
453
+ rv += 1
454
+ return rv
455
+
456
+ @property
457
+ def network_status(self) -> dict[int, NetworkPortStatus]:
458
+ return self._network
459
+
460
+ def update_network_status(self, status:NetworkPortStatus):
461
+ self._network[status.port] = status
462
+ self.call_callbacks()
463
+
464
+ async def get_api(self, uri:str) -> Any:
465
+ cmd = f"http://{self.address}/{uri}"
466
+ _LOGGER.debug(f"tx: {cmd}")
467
+ try:
468
+ async with self.registry.http_session.get(cmd) as resp:
469
+ data = await resp.json()
470
+ if data['Result']:
471
+ return data
472
+ except Exception as err:
473
+ _LOGGER.warning(err)
474
+ return None
475
+
476
+ async def get_log(self) -> str|None:
477
+ cmd = f"http://{self.address}/system/log"
478
+ _LOGGER.debug(f"tx: {cmd}")
479
+ try:
480
+ session:aiohttp.ClientSession = self.registry.http_session
481
+ async with session.get(cmd) as resp:
482
+ data = await resp.read()
483
+ return data.decode('ascii', 'replace')
484
+ except Exception as err:
485
+ _LOGGER.warning(err)
486
+ return None
487
+
488
+ async def reboot(self) -> bool:
489
+ from ..proto.FrameReboot import FrameReboot
490
+ from ..proto.FrameBase import FrameBase
491
+ frame:FrameBase = FrameReboot.construct(mxr=self.registry, target=self)
492
+ if frame is not None:
493
+ self.registry.transmit(frame.frame)
494
+ self._rebooting = True
495
+ return True
496
+ return False
497
+
498
+ async def mesh_promote(self) -> bool:
499
+ from ..proto.FrameMeshOperation import FrameMeshOperation, MeshOperation
500
+ from ..proto.FrameBase import FrameBase
501
+ frame:FrameBase = FrameMeshOperation.construct(mxr=self.registry, target=self, operation=MeshOperation.PROMOTE_MASTER)
502
+ if frame is not None:
503
+ self.registry.transmit(frame.frame)
504
+ return True
505
+ return False
506
+
507
+ async def mesh_remove(self) -> bool:
508
+ from ..proto.FrameMeshOperation import FrameMeshOperation, MeshOperation
509
+ from ..proto.FrameBase import FrameBase
510
+ frame:FrameBase = FrameMeshOperation.construct(mxr=self.registry, target=self, operation=MeshOperation.UNREGISTER)
511
+ if frame is not None:
512
+ self.registry.transmit(frame.frame)
513
+ return True
514
+ return False
515
+
516
+ async def read_stats(self, enable:bool) -> bool:
517
+ from ..proto.FrameV2IPStats import FrameV2IPStats
518
+ from ..proto.FrameBase import FrameBase
519
+ frame:FrameBase = FrameV2IPStats.construct(registry=self.registry, device=self, enable=enable)
520
+ if frame is not None:
521
+ self.registry.transmit(frame.frame)
522
+ return True
523
+ return False
524
+
525
+ def __str__(self) -> str:
526
+ return f"({self.serial} {self.name})"
527
+
528
+ def __eq__(self, other) -> bool:
529
+ return isinstance(other, DeviceBase) and \
530
+ (self.remote_id == other.remote_id)