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
mx_remote/Interface.py ADDED
@@ -0,0 +1,1656 @@
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 abc import ABC, abstractmethod
9
+ import ipaddress
10
+ import logging
11
+ import netifaces
12
+ from .proto import RCKey
13
+ from .proto.Constants import *
14
+ from .proto.Data import VolumeMuteStatus
15
+ from .proto.PDUState import PDUState
16
+ from .proto.V2IPStats import V2IPDeviceStats
17
+ import socket
18
+ import struct
19
+ from typing import Any
20
+ from .Uid import MxrDeviceUid, MxrBayUid
21
+
22
+ _LOGGER = logging.getLogger(__name__)
23
+
24
+ def mxr_valid_addresses() -> list[str]:
25
+ """
26
+ Get the list of valid local_ip addresses that can be used
27
+
28
+ Returns:
29
+ addresses (list[str]): list of IP addressses that can be used for the local_ip parameter
30
+ """
31
+ addresses = []
32
+ for iface in netifaces.interfaces():
33
+ if netifaces.AF_INET in netifaces.ifaddresses(iface):
34
+ addr = netifaces.ifaddresses(iface)[netifaces.AF_INET][0]['addr']
35
+ if not ipaddress.IPv4Address(addr).is_loopback:
36
+ addresses.append(addr)
37
+ return addresses
38
+
39
+ class DeviceStatus(Enum):
40
+ """
41
+ Status of a device on the network
42
+ """
43
+
44
+ ONLINE = 0
45
+ """ unit is online """
46
+
47
+ OFFLINE = 1
48
+ """ unit is offline """
49
+
50
+ REBOOTING = 2
51
+ """ unit indicated that it is going to reboot """
52
+
53
+ BOOTING = 3
54
+ """ unit is booting """
55
+
56
+ INACTIVE = 4
57
+ """ bay is inactive (V2IP encoder/decoder idle) """
58
+
59
+ def __str__(self) -> str:
60
+ if self.value == DeviceStatus.ONLINE.value:
61
+ return 'Online'
62
+ if self.value == DeviceStatus.OFFLINE.value:
63
+ return 'Offline'
64
+ if self.value == DeviceStatus.REBOOTING.value:
65
+ return 'Rebooting'
66
+ if self.value == DeviceStatus.BOOTING.value:
67
+ return 'Booting'
68
+ if self.value == DeviceStatus.INACTIVE.value:
69
+ return 'Inactive'
70
+ return 'Unknown'
71
+
72
+ def __repr__(self) -> str:
73
+ return str(self)
74
+
75
+ class AmpDolbySettings:
76
+ """
77
+ Dolby Digital settings for an amplifier
78
+ """
79
+
80
+ mode:int
81
+ """ Dolby mode """
82
+
83
+ pcm_upmix:bool
84
+ """ PCM upmixing enabled """
85
+
86
+ class AmpZoneSettings:
87
+ """
88
+ Zone specific settings for an amplifier input/output
89
+ """
90
+
91
+ gain_left: int
92
+ """ gain level left channel """
93
+
94
+ gain_right: int
95
+ """ gain level right channel """
96
+
97
+ volume_min: int
98
+ """ minimum volume level """
99
+
100
+ volume_max: int
101
+ """ maximum volume level """
102
+
103
+ delay_left: int
104
+ """ audio delay left channel (ms) """
105
+
106
+ delay_right: int
107
+ """ audio delay right channel (ms) """
108
+
109
+ bass: int
110
+ """ bass level """
111
+
112
+ treble: int
113
+ """ treble level """
114
+
115
+ bridged: int
116
+ """ bridging mode setting """
117
+
118
+ power_mode: int
119
+ """ auto power off setting """
120
+
121
+ power_level: int
122
+ """ auto power off level """
123
+
124
+ power_timeout: int
125
+ """ auto power off timeout """
126
+
127
+ eq_left: list[int]
128
+ """ equalizer left channel """
129
+
130
+ eq_right: list[int]
131
+ """ equalizer right channel """
132
+
133
+ class V2IPStreamSource:
134
+ """
135
+ V2IP multicast IP address and port number
136
+ """
137
+ def __init__(self, label:str, data:bytes) -> None:
138
+ if len(data) < 6:
139
+ raise Exception(f"invalid size: {len(data)}")
140
+ self._label = label
141
+ self._ip = int.from_bytes(data[0:4], "big")
142
+ self._port = int(data[5]) << 8 | int(data[4])
143
+
144
+ @property
145
+ def label(self) -> str:
146
+ """ user friendly description of this stream """
147
+ return self._label
148
+
149
+ @property
150
+ def ip(self) -> str:
151
+ """ multicast IP address """
152
+ return socket.inet_ntoa(struct.pack('!L', self._ip))
153
+
154
+ @property
155
+ def port(self) -> int:
156
+ """ UDP port number """
157
+ return self._port
158
+
159
+ def __eq__(self, value: object) -> bool:
160
+ return (str(self) == str(value))
161
+
162
+ def __str__(self) -> str:
163
+ return f"{self.label}={self.ip}:{self.port}"
164
+
165
+ def __repr__(self) -> str:
166
+ return str(self)
167
+
168
+ class V2IPStreamSources:
169
+ """
170
+ All V2IP multicast IP addresses and port numbers used by a device
171
+ """
172
+
173
+ @property
174
+ @abstractmethod
175
+ def video(self) -> V2IPStreamSource:
176
+ ''' video stream source '''
177
+
178
+ @property
179
+ @abstractmethod
180
+ def audio(self) -> V2IPStreamSource:
181
+ ''' audio stream source '''
182
+
183
+ @property
184
+ @abstractmethod
185
+ def anc(self) -> V2IPStreamSource:
186
+ ''' ancillary stream source '''
187
+
188
+ @property
189
+ @abstractmethod
190
+ def arc(self) -> V2IPStreamSource:
191
+ ''' audio return stream source '''
192
+
193
+ class BayBase(ABC):
194
+ """
195
+ A bay (input/output) of an mx_remote device
196
+ """
197
+
198
+ @property
199
+ @abstractmethod
200
+ def status(self) -> DeviceStatus:
201
+ '''bay status'''
202
+
203
+ @property
204
+ @abstractmethod
205
+ def callbacks(self) -> 'MxrCallbacks':
206
+ ''' mx_remote callbacks '''
207
+
208
+ @property
209
+ @abstractmethod
210
+ def device(self) -> 'DeviceBase':
211
+ ''' device to which this bay belongs '''
212
+
213
+ @property
214
+ @abstractmethod
215
+ def bay_uid(self) -> MxrBayUid:
216
+ ''' unique id of this bay '''
217
+
218
+ @property
219
+ @abstractmethod
220
+ def port(self) -> int:
221
+ ''' port number '''
222
+
223
+ @property
224
+ @abstractmethod
225
+ def is_local(self) -> bool:
226
+ ''' local or remote bay '''
227
+
228
+ @property
229
+ @abstractmethod
230
+ def bay_name(self) -> str:
231
+ ''' bay name for logging (mode / number)'''
232
+
233
+ @property
234
+ @abstractmethod
235
+ def user_name(self) -> str:
236
+ ''' name set up by the user '''
237
+
238
+ @user_name.setter
239
+ @abstractmethod
240
+ def user_name(self, val:str) -> None:
241
+ ''' mx_remote update of the user set name '''
242
+
243
+ @property
244
+ @abstractmethod
245
+ def has_default_name(self) -> bool:
246
+ ''' default name not changed by the user '''
247
+
248
+ @property
249
+ @abstractmethod
250
+ def edid_profile(self) -> EdidProfile:
251
+ ''' edid profile used by the source '''
252
+
253
+ @property
254
+ @abstractmethod
255
+ def bay_label(self) -> str:
256
+ '''user friendly label for this bay'''
257
+
258
+ @property
259
+ @abstractmethod
260
+ def features_mask(self) -> BayStatusMask:
261
+ ''' features/status '''
262
+
263
+ @property
264
+ @abstractmethod
265
+ def is_v2ip_source(self) -> bool:
266
+ '''V2IP source device'''
267
+
268
+ @property
269
+ @abstractmethod
270
+ def is_v2ip_sink(self) -> bool:
271
+ '''V2IP sink device'''
272
+
273
+ @property
274
+ @abstractmethod
275
+ def is_v2ip_remote_sink(self) -> bool:
276
+ '''V2IP remote sink bay'''
277
+
278
+ @property
279
+ @abstractmethod
280
+ def is_v2ip_remote_source(self) -> bool:
281
+ '''V2IP remote source bay'''
282
+
283
+ @property
284
+ @abstractmethod
285
+ def is_v2ip_remote(self) -> bool:
286
+ '''V2IP remote bay'''
287
+
288
+ @property
289
+ @abstractmethod
290
+ def dolby_input(self) -> int:
291
+ '''Dolby Digital input'''
292
+
293
+ @property
294
+ def dolby_input_bay(self) -> 'BayBase':
295
+ '''Dolby Digital input bay used by this audio output bay'''
296
+
297
+ @property
298
+ @abstractmethod
299
+ def features(self) -> list[str]:
300
+ '''List of supported features as strings'''
301
+
302
+ @property
303
+ @abstractmethod
304
+ def has_volume_control(self) -> bool:
305
+ '''Volume control supported by this bay'''
306
+
307
+ @property
308
+ @abstractmethod
309
+ def is_input(self) -> bool:
310
+ '''Source bay'''
311
+
312
+ @property
313
+ @abstractmethod
314
+ def is_output(self) -> bool:
315
+ '''Sink bay'''
316
+
317
+ @property
318
+ @abstractmethod
319
+ def mode(self) -> str:
320
+ '''Bay mode name'''
321
+
322
+ @property
323
+ def other_mode(self) -> str:
324
+ '''Bay mode name of the opposite side (so Output if this bay is an Input)'''
325
+
326
+ @property
327
+ @abstractmethod
328
+ def bay(self) -> int:
329
+ '''Bay number'''
330
+
331
+ @property
332
+ @abstractmethod
333
+ def available(self) -> bool:
334
+ '''True if available'''
335
+
336
+ @property
337
+ @abstractmethod
338
+ def is_hdmi(self) -> bool:
339
+ '''True if this is an HDMI input or output'''
340
+
341
+ @property
342
+ @abstractmethod
343
+ def is_hdbaset(self) -> bool:
344
+ '''True if this is a HDBaseT bay'''
345
+
346
+ @property
347
+ @abstractmethod
348
+ def is_audio(self) -> bool:
349
+ '''True if this is audio input or output bay'''
350
+
351
+ @property
352
+ @abstractmethod
353
+ def video_source(self) -> 'BayBase':
354
+ '''Current video source (output only)'''
355
+
356
+ @property
357
+ @abstractmethod
358
+ def audio_source(self) -> 'BayBase':
359
+ '''Current audio source (output only)'''
360
+
361
+ @property
362
+ @abstractmethod
363
+ def powered_on(self) -> bool:
364
+ '''True the connected device supports CEC and reports that the device is powered on'''
365
+
366
+ @property
367
+ @abstractmethod
368
+ def powered_off(self) -> bool:
369
+ '''True the connected device supports CEC and reports that the device is powered off'''
370
+
371
+ @property
372
+ @abstractmethod
373
+ def power_status(self) -> str:
374
+ '''Power status as string'''
375
+
376
+ @property
377
+ @abstractmethod
378
+ def faulty(self) -> bool:
379
+ '''True if a fault was detected'''
380
+
381
+ @property
382
+ @abstractmethod
383
+ def hidden(self) -> bool:
384
+ '''True if flagged as hidden'''
385
+
386
+ @property
387
+ @abstractmethod
388
+ def poe_powered(self) -> bool:
389
+ '''True if PoE has been enabled (HDBaseT only)'''
390
+
391
+ @property
392
+ @abstractmethod
393
+ def hdbt_connected(self) -> bool:
394
+ '''HDBaseT receiver connected'''
395
+
396
+ @property
397
+ @abstractmethod
398
+ def signal_detected(self) -> bool:
399
+ '''Video signal detected (matrix/oneip) or audio signal detected (proamp)'''
400
+
401
+ @property
402
+ @abstractmethod
403
+ def signal_type(self) -> str:
404
+ '''Audio or video signal type'''
405
+
406
+ @property
407
+ @abstractmethod
408
+ def hpd_detected(self) -> bool:
409
+ '''Hotplug detected'''
410
+
411
+ @property
412
+ @abstractmethod
413
+ def cec_detected(self) -> bool:
414
+ '''Connected device supports HDMI-CEC'''
415
+
416
+ @property
417
+ @abstractmethod
418
+ def mirroring(self) -> str:
419
+ '''The name of the bay if mirroring has been set up'''
420
+
421
+ @property
422
+ @abstractmethod
423
+ def filtered(self) -> str:
424
+ '''Filtered bays'''
425
+
426
+ @property
427
+ @abstractmethod
428
+ def arc(self) -> str:
429
+ '''Audio return channel status'''
430
+
431
+ @property
432
+ @abstractmethod
433
+ def volume(self) -> int:
434
+ '''Current volume level (percentage)'''
435
+
436
+ @property
437
+ @abstractmethod
438
+ def muted(self) -> bool:
439
+ '''True if audio has been muted'''
440
+
441
+ @property
442
+ @abstractmethod
443
+ def online(self) -> bool:
444
+ '''True if online'''
445
+
446
+ @property
447
+ @abstractmethod
448
+ def rebooting(self) -> bool:
449
+ '''True if rebooting'''
450
+
451
+ @property
452
+ @abstractmethod
453
+ def booting(self) -> bool:
454
+ '''True if booting'''
455
+
456
+ @property
457
+ @abstractmethod
458
+ def is_primary(self) -> bool:
459
+ '''True if this bay is the primary bay in a mirroring setup'''
460
+
461
+ @property
462
+ @abstractmethod
463
+ def primary(self) -> 'BayBase':
464
+ '''The primary bay in a mirroring setup'''
465
+
466
+ @property
467
+ @abstractmethod
468
+ def v2ip_source(self) -> V2IPStreamSources|None:
469
+ '''V2IP source address information'''
470
+
471
+ @property
472
+ @abstractmethod
473
+ def link(self) -> 'BayLink':
474
+ '''mx-remote virtual link configuration (proamp<->matrix)'''
475
+
476
+ @property
477
+ @abstractmethod
478
+ def linked_bay(self) -> 'BayBase':
479
+ '''linked bay if an mx-remote virtual link has been set up'''
480
+
481
+ @property
482
+ @abstractmethod
483
+ def link_configured(self) -> bool:
484
+ '''mx-remote virtual link configured (proamp<->matrix)'''
485
+
486
+ @property
487
+ @abstractmethod
488
+ def link_connected(self) -> bool:
489
+ '''mx-remote virtual link connected (proamp<->matrix)'''
490
+
491
+ @property
492
+ @abstractmethod
493
+ def volume_status(self) -> VolumeMuteStatus:
494
+ '''volume and mute status'''
495
+
496
+ @property
497
+ @abstractmethod
498
+ def amp_settings(self) -> AmpZoneSettings|None:
499
+ '''proamp zone settings'''
500
+
501
+ @property
502
+ @abstractmethod
503
+ def encoder_disabled(self) -> bool:
504
+ ''' video/audio encoder disabled '''
505
+
506
+ @property
507
+ @abstractmethod
508
+ def decoder_disabled(self) -> bool:
509
+ ''' video/audio decoder disabled '''
510
+
511
+ @abstractmethod
512
+ async def set_name(self, name:str) -> bool:
513
+ '''change the name of abay'''
514
+
515
+ @abstractmethod
516
+ async def select_video_source(self, port:int, opt:bool=True) -> bool:
517
+ '''change the video source of an output bay'''
518
+
519
+ @abstractmethod
520
+ async def select_video_source_by_user_name(self, name:str, opt:bool=True) -> bool:
521
+ '''change the video source of an output bay'''
522
+
523
+ @abstractmethod
524
+ async def select_audio_source(self, source:Any) -> bool:
525
+ '''change the audio source of an output bay'''
526
+
527
+ @abstractmethod
528
+ async def select_edid_profile(self, profile:EdidProfile) -> bool:
529
+ '''change the edid profile of an input bay'''
530
+
531
+ @abstractmethod
532
+ async def set_hidden(self, hidden:bool) -> bool:
533
+ '''change the hidden status of a bay'''
534
+
535
+ @abstractmethod
536
+ async def power_on(self) -> bool:
537
+ '''power on the remote device if CEC is supported'''
538
+
539
+ @abstractmethod
540
+ async def power_off(self) -> bool:
541
+ '''power off the remote device if CEC is supported'''
542
+
543
+ @abstractmethod
544
+ def volume_up(self) -> bool:
545
+ '''change the volume if supported'''
546
+
547
+ @abstractmethod
548
+ def volume_down(self) -> bool:
549
+ '''change the volume if supported'''
550
+
551
+ @abstractmethod
552
+ def volume_set(self, volume:int) -> bool:
553
+ '''change the volume if supported'''
554
+
555
+ @abstractmethod
556
+ def mute_set(self, mute:bool) -> bool:
557
+ '''change the mute status if supported'''
558
+
559
+ @abstractmethod
560
+ async def send_key(self, key:int) -> bool:
561
+ '''send a remote control key press to the device'''
562
+
563
+ @abstractmethod
564
+ def on_mxr_bay_status(self, data:BayStatusMask) -> None:
565
+ '''internal callback'''
566
+
567
+ @abstractmethod
568
+ def register_callback(self, callback:callable) -> None:
569
+ '''register a callback, called when the bay state changed'''
570
+
571
+ @abstractmethod
572
+ def unregister_callback(self, callback:callable) -> None:
573
+ '''unregister a callback'''
574
+
575
+ @abstractmethod
576
+ def call_callbacks(self) -> None:
577
+ '''notify callbacks that this bay has changed'''
578
+
579
+ class DeviceFeatures:
580
+ """
581
+ Features and status of an mx_remote device
582
+ """
583
+
584
+ def __init__(self, value:int) -> None:
585
+ self._features = value
586
+
587
+ @property
588
+ def value(self) -> int:
589
+ return self._features
590
+
591
+ @property
592
+ def ir_rx(self) -> bool:
593
+ """ IR receive supported """
594
+ return ((self._features & MXR_DEVICE_FEATURE_IR_RX) != 0)
595
+
596
+ @property
597
+ def ir_tx(self) -> bool:
598
+ """ IR blast supported """
599
+ return ((self._features & MXR_DEVICE_FEATURE_IR_TX) != 0)
600
+
601
+ @property
602
+ def cec(self) -> bool:
603
+ """ HDMI-CEC supported """
604
+ return ((self._features & MXR_DEVICE_FEATURE_CEC) != 0)
605
+
606
+ @property
607
+ def v2ip_source(self) -> bool:
608
+ """ V2IP source """
609
+ return ((self._features & MXR_DEVICE_FEATURE_V2IP_SOURCE) != 0)
610
+
611
+ @property
612
+ def v2ip_sink(self) -> bool:
613
+ """ V2IP sink """
614
+ return ((self._features & MXR_DEVICE_FEATURE_V2IP_SINK) != 0)
615
+
616
+ @property
617
+ def video_routing(self) -> bool:
618
+ """ video routing supported """
619
+ return ((self._features & MXR_DEVICE_FEATURE_VIDEO_ROUTING) != 0)
620
+
621
+ @property
622
+ def audio_routing(self) -> bool:
623
+ """ (independent) audio routing supported """
624
+ return ((self._features & MXR_DEVICE_FEATURE_AUDIO_ROUTING) != 0)
625
+
626
+ @property
627
+ def volume_control(self) -> bool:
628
+ """ volume control supported """
629
+ return ((self._features & MXR_DEVICE_FEATURE_VOLUME_CONTROL) != 0)
630
+
631
+ @property
632
+ def arc(self) -> bool:
633
+ """ audio return channel supported """
634
+ return ((self._features & MXR_DEVICE_FEATURE_AUDIO_RETURN) != 0)
635
+
636
+ @property
637
+ def remote_control(self) -> bool:
638
+ """ remote contro pass through supported """
639
+ return ((self._features & MXR_DEVICE_FEATURE_REMOTE_CONTROL) != 0)
640
+
641
+ @property
642
+ def setup_completed(self) -> bool:
643
+ """ device setup flagged as completed """
644
+ return ((self._features & MXR_DEVICE_FEATURE_SETUP_COMPLETED) != 0)
645
+
646
+ @property
647
+ def mesh_master(self) -> bool:
648
+ """ master device of a V2IP mesh """
649
+ return ((self._features & MXR_DEVICE_FEATURE_MESH_MASTER) != 0)
650
+
651
+ @property
652
+ def status_notify(self) -> bool:
653
+ """ notification registered in system status """
654
+ return ((self._features & MXR_DEVICE_FEATURE_STATUS_NOTIFY) != 0)
655
+
656
+ @property
657
+ def status_warning(self) -> bool:
658
+ """ warning registered in system status """
659
+ return ((self._features & MXR_DEVICE_FEATURE_STATUS_WARNING) != 0)
660
+
661
+ @property
662
+ def status_error(self) -> bool:
663
+ """ error registered in system status """
664
+ return ((self._features & MXR_DEVICE_FEATURE_STATUS_ERROR) != 0)
665
+
666
+ @property
667
+ def status_rebooting(self) -> bool:
668
+ """ device is going to reboot """
669
+ return ((self._features & MXR_DEVICE_FEATURE_STATUS_REBOOTING) != 0)
670
+
671
+ @property
672
+ def mesh_member(self) -> bool:
673
+ """ member of a V2IP mesh """
674
+ return ((self._features & MXR_DEVICE_FEATURE_MESH_MEMBER) != 0)
675
+
676
+ @property
677
+ def audio_amp(self) -> bool:
678
+ """ audio amplifier """
679
+ return ((self._features & MXR_DEVICE_FEATURE_AUDIO_AMPLIFIER) != 0)
680
+
681
+ @property
682
+ def booting(self) -> bool:
683
+ """ device is booting """
684
+ return ((self._features & MXR_DEVICE_FEATURE_BOOTING) != 0)
685
+
686
+ @property
687
+ def manager(self) -> bool:
688
+ """ device is allowed to manage mx_remote devices """
689
+ return ((self._features & MXR_DEVICE_FEATURE_MANAGER) != 0)
690
+
691
+ @property
692
+ def boot_bit(self) -> bool:
693
+ """ bit that is flipped every time the device reboots """
694
+ return ((self._features & MXR_DEVICE_FEATURE_BOOT_BIT) != 0)
695
+
696
+ def __eq__(self, value: object) -> bool:
697
+ if (not isinstance(value, DeviceFeatures)):
698
+ return False
699
+ return self._features == value._features
700
+
701
+ @property
702
+ def features(self) -> list[str]:
703
+ """ supported features as list of string descriptions """
704
+ ft:list[str] = []
705
+ if self.ir_rx:
706
+ ft.append('IR RX')
707
+ if self.ir_tx:
708
+ ft.append('IR TX')
709
+ if self.cec:
710
+ ft.append('CEC')
711
+ if self.v2ip_source:
712
+ ft.append('V2IP source')
713
+ if self.v2ip_sink:
714
+ ft.append('V2IP sink')
715
+ if self.video_routing:
716
+ ft.append('video routing')
717
+ if self.audio_routing:
718
+ ft.append('audio routing')
719
+ if self.volume_control:
720
+ ft.append('volume control')
721
+ if self.arc:
722
+ ft.append('ARC')
723
+ if self.remote_control:
724
+ ft.append('remote control')
725
+ if self.setup_completed:
726
+ ft.append('setup completed')
727
+ if self.mesh_master:
728
+ ft.append('mesh master')
729
+ if self.status_notify:
730
+ ft.append('status notify')
731
+ if self.status_warning:
732
+ ft.append('status warning')
733
+ if self.status_error:
734
+ ft.append('status error')
735
+ if self.status_rebooting:
736
+ ft.append('status rebooting')
737
+ if self.mesh_member:
738
+ ft.append('mesh member')
739
+ if self.audio_amp:
740
+ ft.append('audio amp')
741
+ if self.booting:
742
+ ft.append('booting')
743
+ if self.manager:
744
+ ft.append('manager')
745
+ return ft
746
+
747
+ def __str__(self) -> str:
748
+ return str(self.features)
749
+
750
+ def __repr__(self) -> str:
751
+ return str(self)
752
+
753
+ class DeviceV2IPDetailsBase(ABC):
754
+ """ V2IP stream source details for a device """
755
+
756
+ @property
757
+ @abstractmethod
758
+ def has_config(self) -> bool:
759
+ """ configuation known """
760
+
761
+ @property
762
+ @abstractmethod
763
+ def video(self) -> V2IPStreamSource|None:
764
+ """ video stream source """
765
+
766
+ @property
767
+ @abstractmethod
768
+ def audio(self) -> V2IPStreamSource:
769
+ """ audio stream source """
770
+
771
+ @property
772
+ @abstractmethod
773
+ def anc(self) -> V2IPStreamSource:
774
+ """ ancillary stream source """
775
+
776
+ @property
777
+ @abstractmethod
778
+ def arc(self) -> V2IPStreamSource:
779
+ """ audio return channel stream source """
780
+
781
+ @property
782
+ @abstractmethod
783
+ def tx_rate(self) -> int:
784
+ """ transmit rate in Mbit/s """
785
+
786
+ def __eq__(self, value: object) -> bool:
787
+ if isinstance(value, DeviceV2IPDetailsBase):
788
+ return (self.video == value.video) \
789
+ and (self.audio == value.audio) \
790
+ and (self.anc == value.anc) \
791
+ and (self.arc == value.arc) \
792
+ and (self.tx_rate == value.tx_rate)
793
+ return False
794
+
795
+ class UtpLinkSpeed(Enum):
796
+ ''' UTP link speed '''
797
+
798
+ UNKNOWN = 0
799
+ ''' unknown speed '''
800
+
801
+ L_10M = 1
802
+ ''' 10Mbit/s '''
803
+
804
+ L_100M = 2
805
+ ''' 100Mbit/s '''
806
+
807
+ L_200M = 3
808
+ ''' 200Mbit/s '''
809
+
810
+ L_1G = 4
811
+ ''' 1Gbit/s '''
812
+
813
+ def __str__(self) -> str:
814
+ if self.value == 1:
815
+ return '10Mbit/s'
816
+ if self.value == 2:
817
+ return '100Mbit/s'
818
+ if self.value == 3:
819
+ return '200Mbit/s'
820
+ if self.value == 4:
821
+ return '1Gbit/s'
822
+ return 'Unknown'
823
+
824
+ def __repr__(self) -> str:
825
+ return str(self)
826
+
827
+ class UtpLinkErrorStatus(ABC):
828
+ ''' UTP link error status bits '''
829
+
830
+ @property
831
+ @abstractmethod
832
+ def in_error(self):
833
+ ''' rx errors detected '''
834
+
835
+ @property
836
+ @abstractmethod
837
+ def in_fcs_error(self):
838
+ ''' rx FCS errors detected '''
839
+
840
+ @property
841
+ @abstractmethod
842
+ def in_collision(self):
843
+ ''' rx collisions detected '''
844
+
845
+ @property
846
+ @abstractmethod
847
+ def out_deferred(self):
848
+ ''' tx deferred detected '''
849
+
850
+ @property
851
+ @abstractmethod
852
+ def out_excessive(self):
853
+ ''' tx excessive detected '''
854
+
855
+ @property
856
+ @abstractmethod
857
+ def polarity_error(self):
858
+ ''' polarity differences between pairs detected '''
859
+
860
+ @property
861
+ @abstractmethod
862
+ def skew_warning(self):
863
+ ''' clock skew > 8 detected '''
864
+
865
+ @property
866
+ @abstractmethod
867
+ def length_warning(self):
868
+ ''' different pair lengths detected '''
869
+
870
+ class UtpCableStatus(ABC):
871
+ '''' UTP cable pair status '''
872
+
873
+ @property
874
+ @abstractmethod
875
+ def polarity(self) -> bool:
876
+ ''' positive or negative polarity '''
877
+
878
+ @property
879
+ @abstractmethod
880
+ def pair(self) -> int:
881
+ ''' pair number '''
882
+
883
+ @property
884
+ @abstractmethod
885
+ def skew(self) -> int:
886
+ ''' detected clock skew '''
887
+
888
+ @property
889
+ @abstractmethod
890
+ def length(self) -> int:
891
+ ''' detected length in meters '''
892
+
893
+ class NetworkPortStatus(ABC):
894
+ ''' detailed status of a network port'''
895
+
896
+ @property
897
+ @abstractmethod
898
+ def port(self) -> int:
899
+ '''port number'''
900
+
901
+ @property
902
+ @abstractmethod
903
+ def errors(self) -> UtpLinkErrorStatus:
904
+ ''' link error status '''
905
+
906
+ @property
907
+ @abstractmethod
908
+ def vct_status(self) -> list[str]:
909
+ ''' virtual cable test results '''
910
+
911
+ @property
912
+ @abstractmethod
913
+ def link_speed(self) -> UtpLinkSpeed:
914
+ ''' link speed '''
915
+
916
+ @property
917
+ @abstractmethod
918
+ def link_full_duplex(self) -> bool:
919
+ ''' full duplex or half duplex '''
920
+
921
+ @property
922
+ @abstractmethod
923
+ def name(self) -> str:
924
+ ''' description of the port '''
925
+
926
+ @property
927
+ @abstractmethod
928
+ def ip(self) -> str:
929
+ ''' IP address '''
930
+
931
+ @property
932
+ @abstractmethod
933
+ def querier(self) -> str:
934
+ ''' detected IGMP querier or 0.0.0.0 if not detected'''
935
+
936
+ @property
937
+ @abstractmethod
938
+ def cable_status(self) -> UtpCableStatus:
939
+ ''' utp cable pair status '''
940
+
941
+ class DeviceBase(ABC):
942
+ ''' an mx_remote device on the network '''
943
+
944
+ @property
945
+ @abstractmethod
946
+ def status(self) -> DeviceStatus:
947
+ '''device status'''
948
+
949
+ @property
950
+ @abstractmethod
951
+ def name(self) -> str:
952
+ '''device name'''
953
+
954
+ @abstractmethod
955
+ def registry(self) -> 'DeviceRegistry':
956
+ '''local device information registry'''
957
+
958
+ @property
959
+ @abstractmethod
960
+ def configuration_complete(self) -> bool:
961
+ '''check whether all configuration info for this device has been received'''
962
+
963
+ @property
964
+ @abstractmethod
965
+ def model_name(self) -> str:
966
+ '''Model name'''
967
+
968
+ @property
969
+ @abstractmethod
970
+ def callbacks(self) -> 'MxrCallbacks':
971
+ '''callbacks for this device'''
972
+
973
+ @property
974
+ @abstractmethod
975
+ def remote_id(self) -> MxrDeviceUid:
976
+ '''unique id'''
977
+
978
+ @property
979
+ @abstractmethod
980
+ def version(self) -> str:
981
+ '''firmware version'''
982
+
983
+ @property
984
+ @abstractmethod
985
+ def address(self) -> str:
986
+ '''IP address'''
987
+
988
+ @property
989
+ @abstractmethod
990
+ def features(self) -> DeviceFeatures:
991
+ '''supported features'''
992
+
993
+ @property
994
+ @abstractmethod
995
+ def serial(self) -> str:
996
+ '''serial number'''
997
+
998
+ @property
999
+ @abstractmethod
1000
+ def bays(self) -> dict[str, BayBase]:
1001
+ '''device inputs and outputs'''
1002
+
1003
+ @property
1004
+ @abstractmethod
1005
+ def inputs(self) -> dict[str, BayBase]:
1006
+ '''device inputs'''
1007
+
1008
+ @abstractmethod
1009
+ def nb_inputs(self) -> int:
1010
+ '''number of inputs'''
1011
+
1012
+ @property
1013
+ @abstractmethod
1014
+ def first_input(self) -> BayBase:
1015
+ '''the first local input'''
1016
+
1017
+ @property
1018
+ @abstractmethod
1019
+ def outputs(self) -> dict[str, BayBase]:
1020
+ '''device outputs'''
1021
+
1022
+ @abstractmethod
1023
+ def nb_outputs(self) -> int:
1024
+ '''number of outputs'''
1025
+
1026
+ @property
1027
+ @abstractmethod
1028
+ def first_output(self) -> BayBase:
1029
+ '''the first local output'''
1030
+
1031
+ @property
1032
+ @abstractmethod
1033
+ def online(self) -> bool:
1034
+ '''True if online'''
1035
+
1036
+ @property
1037
+ @abstractmethod
1038
+ def rebooting(self) -> bool:
1039
+ '''True if rebooting'''
1040
+
1041
+ @property
1042
+ @abstractmethod
1043
+ def booting(self) -> bool:
1044
+ '''True if booting'''
1045
+
1046
+ @property
1047
+ @abstractmethod
1048
+ def is_amp(self) -> bool:
1049
+ '''True if as an audio amplifier'''
1050
+
1051
+ @property
1052
+ @abstractmethod
1053
+ def amp_dolby_channels(self) -> int:
1054
+ '''number of dolby input channels'''
1055
+
1056
+ @property
1057
+ @abstractmethod
1058
+ def nb_hdbt(self) -> int:
1059
+ '''number of HDBaseT inputs and outputs'''
1060
+
1061
+ @property
1062
+ @abstractmethod
1063
+ def registry(self) -> 'DeviceRegistry':
1064
+ '''local device registry'''
1065
+
1066
+ @property
1067
+ @abstractmethod
1068
+ def is_v2ip(self) -> bool:
1069
+ '''True if this a OneIP device'''
1070
+
1071
+ @property
1072
+ @abstractmethod
1073
+ def has_local_source(self) -> bool:
1074
+ '''True if this device has at least 1 local source'''
1075
+
1076
+ @property
1077
+ @abstractmethod
1078
+ def has_local_sink(self) -> bool:
1079
+ '''True if this device has at least 1 local sink'''
1080
+
1081
+ @property
1082
+ @abstractmethod
1083
+ def is_video_matrix(self) -> bool:
1084
+ '''True if this device supports video matrixing'''
1085
+
1086
+ @property
1087
+ @abstractmethod
1088
+ def is_audio_matrix(self) -> bool:
1089
+ '''True if thie device supports audio matrixing'''
1090
+
1091
+ @property
1092
+ @abstractmethod
1093
+ def temperatures(self) -> dict[str,int]:
1094
+ ''' temperature sensor reports '''
1095
+
1096
+ @property
1097
+ @abstractmethod
1098
+ def v2ip_sources(self) -> list[V2IPStreamSources]:
1099
+ '''V2IP stream source addresses'''
1100
+
1101
+ @property
1102
+ @abstractmethod
1103
+ def v2ip_stats(self) -> V2IPDeviceStats:
1104
+ '''V2IP encoder/decoder statistics'''
1105
+
1106
+ @property
1107
+ @abstractmethod
1108
+ def v2ip_details(self) -> DeviceV2IPDetailsBase:
1109
+ '''V2IP encoder/decoder configuration'''
1110
+
1111
+ @property
1112
+ @abstractmethod
1113
+ def v2ip_source_local(self) -> V2IPStreamSources|None:
1114
+ ''' local v2ip source addresses '''
1115
+
1116
+ @property
1117
+ @abstractmethod
1118
+ def mesh_master(self) -> 'DeviceBase':
1119
+ '''The device that is the master device in the V2IP mesh to which this device belongs'''
1120
+
1121
+ @mesh_master.setter
1122
+ @abstractmethod
1123
+ def mesh_master(self, master:MxrDeviceUid) -> None:
1124
+ '''Change the master device of this device'''
1125
+
1126
+ @property
1127
+ @abstractmethod
1128
+ def is_mesh_master(self) -> bool:
1129
+ '''True if this device is the master device of a V2IP mesh'''
1130
+
1131
+ @property
1132
+ @abstractmethod
1133
+ def dolby_settings(self) -> AmpDolbySettings|None:
1134
+ '''Dolby Digital settings (proamp)'''
1135
+
1136
+ @abstractmethod
1137
+ def v2ip_source(self, bay:BayBase) -> V2IPStreamSources|None:
1138
+ '''Get the V2IP source addresses for the given bay'''
1139
+
1140
+ @abstractmethod
1141
+ def get_by_portnum(self, portnum: int) -> BayBase:
1142
+ '''Get the bay with the given number on this device'''
1143
+
1144
+ @abstractmethod
1145
+ def get_by_portname(self, portname: str) -> BayBase:
1146
+ '''Get the bay with the given port name (not user set name) on this device'''
1147
+
1148
+ @property
1149
+ @abstractmethod
1150
+ def network_status(self) -> dict[int, NetworkPortStatus]:
1151
+ '''network status for all ports'''
1152
+
1153
+ @abstractmethod
1154
+ def update_network_status(self, status:NetworkPortStatus):
1155
+ '''internal callback'''
1156
+
1157
+ @abstractmethod
1158
+ def on_link_config_received(self) -> None:
1159
+ '''internal callback'''
1160
+
1161
+ @abstractmethod
1162
+ async def get_api(self, uri:str) -> Any:
1163
+ '''call an HTTP API method and return the result'''
1164
+
1165
+ @abstractmethod
1166
+ def register_callback(self, callback:callable) -> None:
1167
+ '''register a callback, called when the device state changed'''
1168
+
1169
+ @abstractmethod
1170
+ def unregister_callback(self, callback:callable) -> None:
1171
+ '''unregister a callback'''
1172
+
1173
+ @abstractmethod
1174
+ async def reboot(self) -> bool:
1175
+ '''reboot this device'''
1176
+
1177
+ @abstractmethod
1178
+ async def mesh_promote(self) -> bool:
1179
+ '''promote to mesh master'''
1180
+
1181
+ @abstractmethod
1182
+ async def mesh_remove(self) -> bool:
1183
+ '''remove from mesh'''
1184
+
1185
+ @abstractmethod
1186
+ async def read_stats(self, enable:bool) -> bool:
1187
+ '''start or stop dumping stats'''
1188
+
1189
+ @abstractmethod
1190
+ async def get_log(self) -> str|None:
1191
+ '''read the log from the device and return it as string'''
1192
+
1193
+ class BayLink:
1194
+ ''' a virtual mx_remote link between bays, like an amp output that's linked to a oneip sink '''
1195
+
1196
+ def __init__(self, registry:'DeviceRegistry', bay:BayBase, linked_serial:str, linked_bay:str, features:int) -> None:
1197
+ self._bay = bay
1198
+ self._registry = registry
1199
+ self._linked_serial = linked_serial
1200
+ self._linked_bay = linked_bay
1201
+ self._features = features
1202
+
1203
+ @property
1204
+ def serial(self) -> str:
1205
+ ''' serial number of the linked device '''
1206
+ return self._linked_serial
1207
+
1208
+ @property
1209
+ def linked_bay_name(self) -> str:
1210
+ ''' bay name of the linked bay '''
1211
+ return self._linked_bay
1212
+
1213
+ @property
1214
+ def bay(self) -> BayBase:
1215
+ ''' origin bay '''
1216
+ return self._bay
1217
+
1218
+ @property
1219
+ def linked_bay(self) -> BayBase|None:
1220
+ ''' linked bay '''
1221
+ if not self.linked:
1222
+ return None
1223
+ return self._registry.get_bay_by_portname(remote_id=self.serial, portname=self.linked_bay_name)
1224
+
1225
+ @property
1226
+ def linked(self) -> bool:
1227
+ ''' True if a link has been set up '''
1228
+ return (len(self.serial) != 0) and (len(self.linked_bay_name) != 0)
1229
+
1230
+ @property
1231
+ def other_link(self) -> 'BayLink|None':
1232
+ ''' the link instance of the linked bay '''
1233
+ return self._registry.links.get(self.linked_bay)
1234
+
1235
+ @property
1236
+ def connected(self) -> bool:
1237
+ ''' True if both sides have been set up '''
1238
+ other_link = self.other_link
1239
+ return (other_link is not None) and (other_link.linked) and (other_link.serial == self.bay.device.serial) and (other_link.linked_bay_name == self.bay.bay_name)
1240
+
1241
+ @property
1242
+ def online(self) -> bool:
1243
+ ''' True if both sides are online '''
1244
+ if self.connected:
1245
+ return self.bay.device.online and self.other_link.bay.device.online
1246
+ return False
1247
+
1248
+ @property
1249
+ def is_audio(self) -> bool:
1250
+ ''' True if this is an audio link '''
1251
+ m = self.features_mask
1252
+ return (m & MX_LINK_FEATURE_AUDIO_OPTICAL) != 0 or \
1253
+ (m & MX_LINK_FEATURE_AUDIO_ANALOG) != 0
1254
+
1255
+ @property
1256
+ def is_video(self) -> bool:
1257
+ ''' True if this is a video link '''
1258
+ m = self.features_mask
1259
+ return (m & MX_LINK_FEATURE_VIDEO_HDMI) != 0
1260
+
1261
+ @property
1262
+ def features(self) -> list[str]:
1263
+ ''' supported link features as list of string '''
1264
+ ft = []
1265
+ m = self.features_mask
1266
+ if (m & MX_LINK_FEATURE_VIDEO_HDMI):
1267
+ ft.append("HDMI")
1268
+ if (m & MX_LINK_FEATURE_AUDIO_OPTICAL):
1269
+ ft.append("optical audio")
1270
+ if (m & MX_LINK_FEATURE_AUDIO_ANALOG):
1271
+ ft.append("analog audio")
1272
+ if (m & MX_LINK_FEATURE_IR):
1273
+ ft.append("IR")
1274
+ if (m & MX_LINK_FEATURE_RC):
1275
+ ft.append("RC")
1276
+ return ft
1277
+
1278
+ @property
1279
+ def features_mask(self) -> int:
1280
+ ''' supported link features as bitmask '''
1281
+ if not self.connected:
1282
+ return 0
1283
+ left = self.bay.features_mask
1284
+ right = self.linked_bay.features_mask
1285
+ rv = 0
1286
+ if (left & MX_BAY_FEATURE_HDMI_OUT):
1287
+ if (right & MX_BAY_FEATURE_HDMI_IN):
1288
+ rv |= MX_LINK_FEATURE_VIDEO_HDMI
1289
+ if (left & MX_BAY_FEATURE_HDMI_IN):
1290
+ if (right & MX_BAY_FEATURE_HDMI_OUT):
1291
+ rv |= MX_LINK_FEATURE_VIDEO_HDMI
1292
+ if (left & MX_BAY_FEATURE_AUDIO_DIG_OUT):
1293
+ if (right & MX_BAY_FEATURE_AUDIO_DIG_IN):
1294
+ rv |= MX_LINK_FEATURE_AUDIO_OPTICAL
1295
+ if (left & MX_BAY_FEATURE_AUDIO_DIG_IN):
1296
+ if (right & MX_BAY_FEATURE_AUDIO_DIG_OUT):
1297
+ rv |= MX_LINK_FEATURE_AUDIO_OPTICAL
1298
+ if (left & MX_BAY_FEATURE_AUDIO_ANA_OUT):
1299
+ if (right & MX_BAY_FEATURE_AUDIO_ANA_IN):
1300
+ rv |= MX_LINK_FEATURE_AUDIO_ANALOG
1301
+ if (left & MX_BAY_FEATURE_AUDIO_ANA_IN):
1302
+ if (right & MX_BAY_FEATURE_AUDIO_ANA_OUT):
1303
+ rv |= MX_LINK_FEATURE_AUDIO_ANALOG
1304
+ if (left & MX_BAY_FEATURE_IR_OUT):
1305
+ if (right & MX_BAY_FEATURE_IR_IN):
1306
+ rv |= MX_LINK_FEATURE_IR
1307
+ if (left & MX_BAY_FEATURE_IR_IN):
1308
+ if (right & MX_BAY_FEATURE_IR_OUT):
1309
+ rv |= MX_LINK_FEATURE_IR
1310
+ if (left & MX_BAY_FEATURE_RC_OUT):
1311
+ if (right & MX_BAY_FEATURE_RC_IN):
1312
+ rv |= MX_LINK_FEATURE_RC
1313
+ if (left & MX_BAY_FEATURE_RC_IN):
1314
+ if (right & MX_BAY_FEATURE_RC_OUT):
1315
+ rv |= MX_LINK_FEATURE_RC
1316
+ return rv
1317
+
1318
+ def __eq__(self, value: object) -> bool:
1319
+ if not isinstance(value, BayLink):
1320
+ return False
1321
+ return (self.serial == value.serial) and (self.bay == value.bay) and (self.features == value.features)
1322
+
1323
+ def __str__(self) -> str:
1324
+ return f"{self.serial}:{self.bay}:{self.features}"
1325
+
1326
+ def __hash__(self) -> int:
1327
+ return hash(str(self))
1328
+
1329
+ class BayLinks:
1330
+ ''' linked bay configurations for all devices '''
1331
+
1332
+ def __init__(self, registry:'DeviceRegistry') -> None:
1333
+ self._registry = registry
1334
+ self._links:dict[MxrBayUid, BayLink] = {}
1335
+
1336
+ @property
1337
+ def callbacks(self) -> 'MxrCallbacks':
1338
+ return self._registry.callbacks
1339
+
1340
+ @property
1341
+ def registry(self) -> 'DeviceRegistry':
1342
+ return self._registry
1343
+
1344
+ def _on_link(self, bay:BayBase, new_link:BayLink) -> None:
1345
+ if new_link.linked:
1346
+ self.callbacks.on_bay_linked(bay, new_link.serial, new_link.bay, new_link.features),
1347
+ other_bay = new_link.linked_bay
1348
+ if other_bay is not None:
1349
+ self.callbacks.on_bay_linked(other_bay, bay.device.serial, bay.bay_name, new_link.features)
1350
+
1351
+ def is_primary(self, bay:BayBase) -> bool:
1352
+ if not bay.bay_uid in self._links.keys():
1353
+ return True
1354
+ link = self._links[bay.bay_uid]
1355
+ other_bay = link.linked_bay
1356
+ if other_bay is None:
1357
+ return True
1358
+ if bay.device.is_amp != other_bay.device.is_amp:
1359
+ return bay.device.is_amp
1360
+ return str(bay.device.remote_id) < str(other_bay.device.remote_id)
1361
+
1362
+
1363
+ def update(self, bay:BayBase, linked_serial:str, linked_bay:str, features:int) -> None:
1364
+ new_link = BayLink(bay=bay, registry=self.registry, linked_serial=linked_serial, linked_bay=linked_bay, features=features)
1365
+ if bay.bay_uid in self._links.keys():
1366
+ old = self._links[bay.bay_uid]
1367
+ if old != new_link:
1368
+ if old.linked:
1369
+ self.callbacks.on_bay_unlinked(bay, old.serial, old.bay)
1370
+ old_bay = old.linked_bay
1371
+ if old_bay is not None:
1372
+ self.callbacks.on_bay_unlinked(old_bay, bay.device.serial, bay.bay_name)
1373
+ self._on_link(bay=bay, new_link=new_link)
1374
+ self._links[bay.bay_uid] = new_link
1375
+ else:
1376
+ self._on_link(bay=bay, new_link=new_link)
1377
+ self._links[bay.bay_uid] = new_link
1378
+
1379
+ def get(self, bay:BayBase|None) -> BayLink|None:
1380
+ if bay is None:
1381
+ return None
1382
+ if bay.bay_uid in self._links.keys():
1383
+ return self._links[bay.bay_uid]
1384
+ return None
1385
+
1386
+ class DeviceRegistry(ABC):
1387
+ ''' all mx_remote devices on the network '''
1388
+
1389
+ @property
1390
+ @abstractmethod
1391
+ def local_ip(self) -> str:
1392
+ '''local ip address'''
1393
+
1394
+ @property
1395
+ @abstractmethod
1396
+ def broadcast(self) -> bool:
1397
+ '''broadcast or multicast'''
1398
+
1399
+ @property
1400
+ @abstractmethod
1401
+ def library_version(self) -> str:
1402
+ ''' version of the mx_remote library '''
1403
+
1404
+ @property
1405
+ @abstractmethod
1406
+ def protocol_version(self) -> int:
1407
+ ''' protocol version used by this library '''
1408
+
1409
+ @property
1410
+ @abstractmethod
1411
+ def net_protocol_version_max(self) -> int:
1412
+ ''' highest protocol version used by devices on the network '''
1413
+
1414
+ @property
1415
+ @abstractmethod
1416
+ def net_protocol_version_min(self) -> int:
1417
+ ''' lowest protocol version used by devices on the network '''
1418
+
1419
+ @property
1420
+ @abstractmethod
1421
+ def uid_raw(self) -> bytes:
1422
+ ''' uid of this device as bytes '''
1423
+
1424
+ @property
1425
+ @abstractmethod
1426
+ def uid(self) -> MxrDeviceUid:
1427
+ ''' uid of this device '''
1428
+
1429
+ @property
1430
+ @abstractmethod
1431
+ def name(self) -> str:
1432
+ ''' device name '''
1433
+
1434
+ @property
1435
+ @abstractmethod
1436
+ def callbacks(self) -> 'MxrCallbacks':
1437
+ ''' callbacks to call when the device is updated '''
1438
+
1439
+ @abstractmethod
1440
+ def transmit(self, data: bytes) -> int:
1441
+ ''' transmit data to this device (broadcast/multicast) '''
1442
+
1443
+ @property
1444
+ @abstractmethod
1445
+ def links(self) -> BayLinks:
1446
+ ''' linked bay configurations for all devices '''
1447
+
1448
+ @abstractmethod
1449
+ def get_by_serial(self, serial:str) -> DeviceBase|None:
1450
+ ''' get a device by its serial number '''
1451
+
1452
+ @abstractmethod
1453
+ def get_by_uid(self, remote_id:str|MxrDeviceUid) -> DeviceBase|None:
1454
+ ''' get a device by its unique id '''
1455
+
1456
+ @abstractmethod
1457
+ def get_bay_by_portnum(self, remote_id:str|MxrDeviceUid, portnum:int) -> BayBase|None:
1458
+ ''' get a bay of a device by its unique id and port number '''
1459
+
1460
+ @abstractmethod
1461
+ def get_bay_by_portname(self, remote_id:str|MxrDeviceUid, portname:str) -> BayBase|None:
1462
+ ''' get a bay of a device by its unique id and port name '''
1463
+
1464
+ @abstractmethod
1465
+ def get_by_stream_ip(self, ip:str, audio:bool=False) -> BayBase|None:
1466
+ ''' get a bay of a device by its V2IP stream address '''
1467
+
1468
+ class ConnectionCallbacks(ABC):
1469
+ @property
1470
+ @abstractmethod
1471
+ def target_ip(self) -> str:
1472
+ '''target ip address'''
1473
+
1474
+ @abstractmethod
1475
+ def on_connection_made(self) -> None:
1476
+ '''called when the socket was opened'''
1477
+
1478
+ @abstractmethod
1479
+ def on_datagram_received(self, data: bytes, addr: tuple[str, int]) -> None:
1480
+ '''called when a datagram was received'''
1481
+
1482
+ class MxrCallbacks:
1483
+ ''' callbacks that can be used by an external application to get notified when a status changes '''
1484
+
1485
+ def on_device_update(self, dev:DeviceBase) -> None:
1486
+ ''' called when properties of 'dev' have been updated '''
1487
+ pass
1488
+
1489
+ def on_bay_update(self, bay:BayBase) -> None:
1490
+ ''' called when properties of 'bay' have been updated '''
1491
+ pass
1492
+
1493
+ def on_device_config_changed(self, dev:DeviceBase) -> None:
1494
+ ''' called when device configuration properties of 'dev' have been updated '''
1495
+ self.on_device_update(dev)
1496
+
1497
+ def on_device_config_complete(self, dev:DeviceBase) -> None:
1498
+ ''' called when device configuration of 'dev' had been received fully '''
1499
+ _LOGGER.debug(f"{dev} configuration complete")
1500
+ self.on_device_update(dev)
1501
+
1502
+ def on_device_online_status_changed(self, dev:DeviceBase, online:bool) -> None:
1503
+ ''' called when the online status of 'dev' changed '''
1504
+ _LOGGER.debug(f"{dev} online status changed to {online}")
1505
+ self.on_device_update(dev)
1506
+
1507
+ def on_bay_registered(self, bay:BayBase) -> None:
1508
+ ''' called when a new bay was registered by mx_remote '''
1509
+ _LOGGER.debug(f"{bay} registered: {bay.features}")
1510
+ self.on_bay_update(bay)
1511
+
1512
+ def on_device_temperature_changed(self, dev:DeviceBase) -> None:
1513
+ ''' called when the temperature values of 'dev' changed '''
1514
+ _LOGGER.debug(f"{dev} temperature: {dev.temperatures}")
1515
+ self.on_device_update(dev)
1516
+
1517
+ def on_power_changed(self, bay:BayBase, power:str) -> None:
1518
+ ''' called when the power status of 'bay' changed '''
1519
+ _LOGGER.debug(f"{bay} power status {power}")
1520
+ self.on_bay_update(bay)
1521
+
1522
+ def on_name_changed(self, bay:BayBase, user_name:str) -> None:
1523
+ ''' called when the name that's set up by the user of 'bay' changed '''
1524
+ _LOGGER.debug(f"{bay} name changed: {user_name}")
1525
+ self.on_bay_update(bay)
1526
+
1527
+ def on_status_signal_detected_changed(self, bay:BayBase, val:bool) -> None:
1528
+ ''' called when the signal detect status of 'bay' changed '''
1529
+ lval = "signal detected" if val else "no signal"
1530
+ _LOGGER.debug(f"{bay} {lval}")
1531
+ self.on_bay_update(bay)
1532
+
1533
+ def on_status_faulty_changed(self, bay:BayBase, val:bool) -> None:
1534
+ ''' called when the fault status of 'bay' changed '''
1535
+ lval = "FAULT" if val else "healthy"
1536
+ _LOGGER.debug(f"{bay} {lval}")
1537
+ self.on_bay_update(bay)
1538
+
1539
+ def on_status_hidden_changed(self, bay:BayBase, val:bool) -> None:
1540
+ ''' called when the hidden status of 'bay' changed '''
1541
+ lval = "hidden" if val else "visible"
1542
+ _LOGGER.debug(f"{bay} {lval}")
1543
+ self.on_bay_update(bay)
1544
+
1545
+ def on_status_poe_powered_changed(self, bay:BayBase, val:bool) -> None:
1546
+ ''' called when the PoE power status of 'bay' changed '''
1547
+ lval = "on" if val else "off"
1548
+ _LOGGER.debug(f"{bay} PoE {lval}")
1549
+ self.on_bay_update(bay)
1550
+
1551
+ def on_status_hdbt_connected_changed(self, bay:BayBase, val:bool) -> None:
1552
+ ''' called when the HDBaseT connection status of 'bay' changed '''
1553
+ lval = "up" if val else "down"
1554
+ _LOGGER.debug(f"{bay} HDBaseT link {lval}")
1555
+ self.on_bay_update(bay)
1556
+
1557
+ def on_status_signal_type_changed(self, bay:BayBase, val:str) -> None:
1558
+ ''' called when the detected signal of 'bay' changed '''
1559
+ _LOGGER.debug(f"{bay} signal type: {val}")
1560
+ self.on_bay_update(bay)
1561
+
1562
+ def on_status_hpd_detected_changed(self, bay:BayBase, val:bool) -> None:
1563
+ ''' called when the HPD value of 'bay' changed '''
1564
+ lval = "detected" if val else "lost"
1565
+ _LOGGER.debug(f"{bay} hotplug {lval}")
1566
+ self.on_bay_update(bay)
1567
+
1568
+ def on_status_cec_detected_changed(self, bay:BayBase, val: bool) -> None:
1569
+ ''' called when a CEC device was detected on 'bay' '''
1570
+ lval = "detected" if val else "not found"
1571
+ _LOGGER.debug(f"{bay} HDMI-CEC device {lval}")
1572
+ self.on_bay_update(bay)
1573
+
1574
+ def on_status_arc_changed(self, bay:BayBase, val:str) -> None:
1575
+ ''' called when the audio return channel status of 'bay' changed '''
1576
+ _LOGGER.info(f"{bay} ARC: {val}")
1577
+ self.on_bay_update(bay)
1578
+
1579
+ def on_volume_changed(self, bay:BayBase, volume:VolumeMuteStatus) -> None:
1580
+ ''' called when the volume/mute status of 'bay' changed '''
1581
+ muted_str = ""
1582
+ volume_str = ""
1583
+ if volume.muted is not None:
1584
+ muted_str = " not muted" if not volume.muted else " muted"
1585
+ if volume.volume is not None:
1586
+ volume_str = " volume {}%".format(volume.volume)
1587
+ _LOGGER.debug(f"{bay}{volume_str}{muted_str}")
1588
+ self.on_bay_update(bay)
1589
+
1590
+ def on_key_pressed(self, bay:BayBase, key:RCKey) -> None:
1591
+ ''' called when a key press was detected on 'bay' '''
1592
+ _LOGGER.debug(f"{bay} key pressed: {key}")
1593
+
1594
+ def on_action_received(self, bay:BayBase, action:RCAction) -> None:
1595
+ ''' called when a remote control action was detected on 'bay' '''
1596
+ _LOGGER.debug(f"{bay} action: {action}")
1597
+
1598
+ def on_video_source_changed(self, bay:BayBase, video_source:BayBase) -> None:
1599
+ ''' called when a video source changed was detected on 'bay' '''
1600
+ _LOGGER.debug(f"{bay} video routed to {video_source}")
1601
+ self.on_bay_update(bay)
1602
+
1603
+ def on_audio_source_changed(self, bay:BayBase, audio_source:BayBase) -> None:
1604
+ ''' called when an audio source changed was detected on 'bay' '''
1605
+ _LOGGER.debug(f"{bay} audio routed to {audio_source}")
1606
+ self.on_bay_update(bay)
1607
+
1608
+ def on_pdu_registered(self, pdu:PDUState) -> None:
1609
+ ''' called when a Pulse-Eight PDU was detected that's connected to an mx_remote device '''
1610
+ _LOGGER.debug(f"{pdu.dev} pdu registered: {pdu}")
1611
+
1612
+ def on_pdu_changed(self, pdu:PDUState) -> None:
1613
+ ''' called when a state of 'pdu' changed '''
1614
+ _LOGGER.debug(f"{pdu.dev} pdu: {pdu}")
1615
+
1616
+ def on_bay_linked(self, bay:BayBase, linked_serial:str, linked_bay:str, features:int) -> None:
1617
+ ''' called when a bay link was detected '''
1618
+ _LOGGER.debug(f"{bay} linked to {linked_serial}:{linked_bay}")
1619
+ self.on_device_update(bay.device)
1620
+ self.on_bay_update(bay)
1621
+
1622
+ def on_bay_unlinked(self, bay:BayBase, linked_serial:str, linked_bay:str) -> None:
1623
+ ''' called when a bay link was removed '''
1624
+ _LOGGER.debug(f"{bay} unlinked from {linked_serial}:{linked_bay}")
1625
+ self.on_device_update(bay.device)
1626
+ self.on_bay_update(bay)
1627
+
1628
+ def on_mirror_status_changed(self, bay:BayBase, mirror:MxrDeviceUid|None) -> None:
1629
+ ''' called when a bay mirroring setup change was detected '''
1630
+ _LOGGER.debug(f"{bay} mirror {mirror}")
1631
+ self.on_bay_update(bay)
1632
+
1633
+ def on_filter_status_changed(self, bay:BayBase, filtered:list[MxrDeviceUid]) -> None:
1634
+ ''' called when a bay filtering setup change was detected '''
1635
+ _LOGGER.debug(f"{bay} filtered {filtered}")
1636
+ self.on_bay_update(bay)
1637
+
1638
+ def on_edid_profile_changed(self, bay:BayBase, profile:EdidProfile) -> None:
1639
+ ''' called when a source EDID profile was changed '''
1640
+ _LOGGER.debug(f"{bay} edid profile changed to {profile}")
1641
+ self.on_bay_update(bay)
1642
+
1643
+ def on_rc_type_changed(self, bay:BayBase, rc_type:RCType) -> None:
1644
+ ''' called when a source remote control type was changed '''
1645
+ _LOGGER.debug(f"{bay} rc type changed to {rc_type}")
1646
+ self.on_bay_update(bay)
1647
+
1648
+ def on_amp_zone_settings_changed(self, bay:BayBase, settings:AmpZoneSettings) -> None:
1649
+ ''' called when amp zone settings were changed '''
1650
+ _LOGGER.debug(f"{bay} amp zone settings changed")
1651
+ self.on_bay_update(bay)
1652
+
1653
+ def on_amp_dolby_settings_changed(self, device:DeviceBase, settings:AmpDolbySettings) -> None:
1654
+ ''' called when amp dolby settings were changed '''
1655
+ _LOGGER.debug(f"{device} dolby settings changed")
1656
+ self.on_device_update(device)