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,923 @@
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
+ import mx_remote.proto as proto
10
+ import logging
11
+ from typing import Any
12
+ from ..proto.Constants import BayStatusMask, EdidProfile, RCType, RCAction, RCKey
13
+ from ..proto.BayConfig import BayConfig
14
+ from ..proto.Data import VolumeMuteStatus
15
+ from ..proto.FrameBase import FrameBase
16
+ from ..proto.FrameV2IPSourceSwitch import FrameV2IPSourceSwitch
17
+ from ..proto.FrameEDIDProfile import FrameEDIDProfile
18
+ from ..proto.FrameBayHide import FrameBayHide
19
+ from ..proto.FrameSetName import FrameSetName
20
+ from ..Interface import BayBase, DeviceBase, BayLink, MxrCallbacks, V2IPStreamSources, AmpZoneSettings, DeviceStatus
21
+ from ..Uid import MxrBayUid
22
+
23
+ _LOGGER = logging.getLogger(__name__)
24
+
25
+ class Bay(BayBase):
26
+ ARC_NONE = 'Inactive'
27
+ ARC_HDMI = 'HDMI'
28
+ ARC_OPTICAL = 'optical'
29
+ ARC_ANALOG = 'analog'
30
+
31
+ def __init__(self, dev:DeviceBase, data:BayConfig) -> None:
32
+ self._dev = dev
33
+ self._port_number = data.port
34
+ self._port_name = data.bay_name
35
+ self._user_name = None
36
+ self._features = data.features
37
+ self._mbay_id = None
38
+ self._video_source = None
39
+ self._audio_source = None
40
+ self._power_status = None
41
+ self._faulty = None
42
+ self._hidden = None
43
+ self._poe_powered = None
44
+ self._hdbt_connected = None
45
+ self._signal_detected = None
46
+ self._signal_type = None
47
+ self._hpd_detected = None
48
+ self._cec_detected = None
49
+ self._arc = self.ARC_NONE
50
+ self._audio_volume = None
51
+ self._rc_type = None
52
+ self._edid_profile = None
53
+ self._mirror = None
54
+ self._decoder_disabled = None
55
+ self._encoder_disabled = None
56
+ self._status_mask = data.status
57
+ self._amp_settings:AmpZoneSettings|None = None
58
+ self._filtered = []
59
+ self._bay_callbacks:list[callable] = []
60
+
61
+ def register_callback(self, callback:callable) -> None:
62
+ '''register a callback, called when the device state changed'''
63
+ self._bay_callbacks.append(callback)
64
+
65
+ def unregister_callback(self, callback:callable) -> None:
66
+ '''unregister a callback'''
67
+ if callback in self._bay_callbacks:
68
+ self._bay_callbacks.remove(callback)
69
+
70
+ def call_callbacks(self) -> None:
71
+ for callback in self._bay_callbacks:
72
+ callback(self)
73
+
74
+ @property
75
+ def rebooting(self) -> bool:
76
+ '''True if rebooting'''
77
+ return self.device.rebooting
78
+
79
+ @property
80
+ def booting(self) -> bool:
81
+ '''True if booting'''
82
+ return self.device.booting
83
+
84
+ @property
85
+ def status(self) -> DeviceStatus:
86
+ if self.online:
87
+ if self.rebooting:
88
+ return DeviceStatus.REBOOTING
89
+ if self.booting:
90
+ return DeviceStatus.BOOTING
91
+ if self.status_mask.encoder_disabled or self.status_mask.decoder_disabled:
92
+ return DeviceStatus.INACTIVE
93
+ return DeviceStatus.ONLINE
94
+ return DeviceStatus.OFFLINE
95
+
96
+ @property
97
+ def v2ip_source(self) -> V2IPStreamSources|None:
98
+ return self.device.v2ip_source(self)
99
+
100
+ @property
101
+ def callbacks(self) -> MxrCallbacks:
102
+ return self.device.callbacks
103
+
104
+ @property
105
+ def device(self) -> DeviceBase:
106
+ # remote device
107
+ return self._dev
108
+
109
+ @property
110
+ def bay_uid(self) -> MxrBayUid:
111
+ return MxrBayUid(self.device.remote_id, self.port)
112
+
113
+ @property
114
+ def online(self) -> bool:
115
+ # check whether the device to which this bay belongs is online
116
+ return self.device.online
117
+
118
+ @property
119
+ def port(self) -> int:
120
+ # port number used for mxremote operations
121
+ return self._port_number
122
+
123
+ @property
124
+ def bay_name(self) -> str:
125
+ # mbay name
126
+ return self._port_name
127
+
128
+ @property
129
+ def user_name(self) -> str:
130
+ # name set up by the user
131
+ return self._user_name if (self._user_name is not None) \
132
+ else self.bay_name
133
+
134
+ @user_name.setter
135
+ def user_name(self, val:str) -> None:
136
+ prev = self.user_name
137
+ self._user_name = val
138
+ if (self.user_name != prev):
139
+ self.callbacks.on_name_changed(self, self.user_name)
140
+ self.call_callbacks()
141
+
142
+ @property
143
+ def has_default_name(self) -> bool:
144
+ # default name set or custom name set
145
+ return (self.user_name == self.bay_name)
146
+
147
+ @property
148
+ def bay_label(self) -> str:
149
+ # bay name + user name, used for logging
150
+ name = self.bay_name
151
+ user_name = self.user_name
152
+ if user_name != name:
153
+ return "{} ({})".format(name, user_name)
154
+ return name
155
+
156
+ @property
157
+ def features_mask(self) -> int:
158
+ # supported features for this bay (bitmask)
159
+ return self._features if self._features is not None else 0
160
+
161
+ @features_mask.setter
162
+ def features_mask(self, val:int) -> None:
163
+ self._features = val
164
+
165
+ @property
166
+ def is_local(self) -> bool:
167
+ return not self.is_v2ip_remote
168
+
169
+ @property
170
+ def is_v2ip_source(self) -> bool:
171
+ mask = self.features_mask
172
+ return ((mask & proto.MX_BAY_FEATURE_V2IP_SOURCE_REMOTE) != 0) \
173
+ or ((mask & proto.MX_BAY_FEATURE_V2IP_SOURCE_LOCAL) != 0)
174
+
175
+ @property
176
+ def is_v2ip_sink(self) -> bool:
177
+ mask = self.features_mask
178
+ return ((mask & proto.MX_BAY_FEATURE_V2IP_SINK_REMOTE) != 0) \
179
+ or ((mask & proto.MX_BAY_FEATURE_V2IP_SINK_LOCAL) != 0)
180
+
181
+ @property
182
+ def is_v2ip_remote_sink(self) -> bool:
183
+ return ((self.features_mask & proto.MX_BAY_FEATURE_V2IP_SINK_REMOTE) != 0)
184
+
185
+ @property
186
+ def is_v2ip_remote_source(self) -> bool:
187
+ return ((self.features_mask & proto.MX_BAY_FEATURE_V2IP_SOURCE_REMOTE) != 0)
188
+
189
+ @property
190
+ def is_v2ip_remote(self) -> bool:
191
+ return self.is_v2ip_remote_sink or self.is_v2ip_remote_source
192
+
193
+ @property
194
+ def dolby_input(self) -> int:
195
+ # if dolby mode is set, the input bay that provides the audio source
196
+ features = self.features_mask
197
+ if (features & proto.MX_BAY_FEATURE_DOLBY):
198
+ # TODO fix mx_remote offset
199
+ return 'Input {}'.format('9') #((features >> proto.MX_BAY_FEATURE_DOLBY_IN_POS) & 0xF)
200
+ return None
201
+
202
+ @property
203
+ def dolby_input_bay(self) -> BayBase:
204
+ db = self.dolby_input
205
+ if db is None:
206
+ return None
207
+ return self.device.get_by_portname(db)
208
+
209
+ @property
210
+ def features(self) -> list[str]:
211
+ # list of supported features for this bay
212
+ rv = []
213
+ mask = self.features_mask
214
+ if (mask & proto.MX_BAY_FEATURE_HDMI_OUT):
215
+ rv.append('HDMI output')
216
+ if (mask & proto.MX_BAY_FEATURE_HDMI_IN):
217
+ rv.append('HDMI input')
218
+ if (mask & proto.MX_BAY_FEATURE_AUDIO_DIG_OUT):
219
+ rv.append('digital audio output')
220
+ if (mask & proto.MX_BAY_FEATURE_AUDIO_DIG_IN):
221
+ rv.append('digital audio input')
222
+ if (mask & proto.MX_BAY_FEATURE_AUDIO_ANA_OUT):
223
+ rv.append('analog audio output')
224
+ if (mask & proto.MX_BAY_FEATURE_AUDIO_ANA_IN):
225
+ rv.append('analog audio input')
226
+ if (mask & proto.MX_BAY_FEATURE_IR_OUT):
227
+ rv.append('IR transmitter')
228
+ if (mask & proto.MX_BAY_FEATURE_IR_IN):
229
+ rv.append('IR receiver')
230
+ if (mask & proto.MX_BAY_FEATURE_AUDIO_AMP_OUT):
231
+ rv.append('amplifier audio output')
232
+ if (mask & proto.MX_BAY_FEATURE_RC_OUT):
233
+ rv.append('remote control out')
234
+ if (mask & proto.MX_BAY_FEATURE_RC_IN):
235
+ rv.append('remote control in')
236
+ if (mask & proto.MX_BAY_FEATURE_V2IP_SOURCE_REMOTE):
237
+ rv.append('V2IP remote source')
238
+ if (mask & proto.MX_BAY_FEATURE_V2IP_SINK_REMOTE):
239
+ rv.append('V2IP remote sink')
240
+ if (mask & proto.MX_BAY_FEATURE_V2IP_SOURCE_LOCAL):
241
+ rv.append('V2IP source')
242
+ if (mask & proto.MX_BAY_FEATURE_V2IP_SINK_LOCAL):
243
+ rv.append('V2IP sink')
244
+ if (mask & proto.MX_BAY_FEATURE_DOLBY):
245
+ rv.append('dolby')
246
+ if (mask & proto.MX_BAY_FEATURE_AUTO_OFF):
247
+ rv.append('auto off')
248
+ return rv
249
+
250
+ @property
251
+ def has_volume_control(self) -> bool:
252
+ mask = self.features_mask
253
+ if (mask & proto.MX_BAY_FEATURE_AUDIO_ANA_OUT):
254
+ return True
255
+ if (mask & proto.MX_BAY_FEATURE_AUDIO_AMP_OUT):
256
+ return True
257
+ return False
258
+
259
+ @property
260
+ def is_input(self) -> bool:
261
+ mask = self.features_mask
262
+ return ((mask & proto.MX_BAY_FEATURE_HDMI_IN) != 0) \
263
+ or ((mask & proto.MX_BAY_FEATURE_AUDIO_DIG_IN) != 0) \
264
+ or ((mask & proto.MX_BAY_FEATURE_AUDIO_ANA_IN) != 0) \
265
+ or ((mask & proto.MX_BAY_FEATURE_V2IP_SOURCE_REMOTE) != 0) \
266
+ or ((mask & proto.MX_BAY_FEATURE_V2IP_SOURCE_LOCAL) != 0)
267
+
268
+ @property
269
+ def is_output(self) -> bool:
270
+ mask = self.features_mask
271
+ return ((mask & proto.MX_BAY_FEATURE_HDMI_OUT) != 0) \
272
+ or ((mask & proto.MX_BAY_FEATURE_AUDIO_AMP_OUT) != 0) \
273
+ or ((mask & proto.MX_BAY_FEATURE_AUDIO_DIG_OUT) != 0) \
274
+ or ((mask & proto.MX_BAY_FEATURE_AUDIO_ANA_OUT) != 0) \
275
+ or ((mask & proto.MX_BAY_FEATURE_V2IP_SINK_REMOTE) != 0) \
276
+ or ((mask & proto.MX_BAY_FEATURE_V2IP_SINK_LOCAL) != 0)
277
+
278
+ @property
279
+ def mode(self) -> str:
280
+ # bay mode used by the web api and logging
281
+ if self.is_output:
282
+ return 'Output'
283
+ if self.is_input:
284
+ return 'Input'
285
+ return 'unknown'
286
+
287
+ @property
288
+ def other_mode(self) -> str:
289
+ # bay mode used by the web api and logging
290
+ if self.is_output:
291
+ return 'Input'
292
+ if self.is_input:
293
+ return 'Output'
294
+ return 'unknown'
295
+
296
+ @property
297
+ def bay(self) -> int:
298
+ # bay number used by the web api
299
+ return self._mbay_id if (self._mbay_id is not None) \
300
+ else int(self.bay_name[len(self.mode)+1:])
301
+
302
+ @bay.setter
303
+ def bay(self, val:int) -> None:
304
+ if self._mbay_id is None:
305
+ self._mbay_id = val
306
+
307
+ @property
308
+ def available(self) -> bool:
309
+ if self.faulty or self.hidden or not self.online:
310
+ return False
311
+ if self.is_hdbaset and not self.hdbt_connected:
312
+ return False
313
+ if self.device.is_amp:
314
+ if self.is_output:
315
+ return (self.bay == 0) or (self.bay >= self.device.amp_dolby_channels)
316
+ return (self.bay > self.device.amp_dolby_channels)
317
+ return True
318
+
319
+ @property
320
+ def is_hdmi(self) -> bool:
321
+ # HDMI bay
322
+ mask = self.features_mask
323
+ return ((mask & proto.MX_BAY_FEATURE_HDMI_OUT) != 0) or ((mask & proto.MX_BAY_FEATURE_HDMI_IN) != 0)
324
+
325
+ @property
326
+ def is_hdbaset(self) -> bool:
327
+ #HDBaseT bay
328
+ # TODO add to proto
329
+ return self.is_hdmi and self.is_output and (self.bay < self.device.nb_hdbt)
330
+
331
+ @property
332
+ def is_audio(self) -> bool:
333
+ # audio bay
334
+ if self.is_hdmi:
335
+ return False
336
+ mask = self.features_mask
337
+ return ((mask & proto.MX_BAY_FEATURE_AUDIO_DIG_OUT) != 0) or ((mask & proto.MX_BAY_FEATURE_AUDIO_DIG_IN) != 0) or \
338
+ ((mask & proto.MX_BAY_FEATURE_AUDIO_ANA_OUT) != 0) or ((mask & proto.MX_BAY_FEATURE_AUDIO_ANA_IN) != 0) or \
339
+ ((mask & proto.MX_BAY_FEATURE_AUDIO_AMP_OUT) != 0)
340
+
341
+ @property
342
+ def edid_profile(self) -> EdidProfile:
343
+ if not self.is_hdmi or not self.is_input:
344
+ return None
345
+ return EdidProfile(self._edid_profile)
346
+
347
+ @edid_profile.setter
348
+ def edid_profile(self, val:int) -> None:
349
+ if not self.is_hdmi or not self.is_input:
350
+ return
351
+ if ((self._edid_profile is None) or (self._edid_profile != val)):
352
+ self._edid_profile = val
353
+ self.callbacks.on_edid_profile_changed(self, self.edid_profile)
354
+ self.call_callbacks()
355
+
356
+ @property
357
+ def rc_type(self) -> RCType:
358
+ if not self.is_hdmi or not self.is_input:
359
+ return None
360
+ return RCType(self._rc_type)
361
+
362
+ @rc_type.setter
363
+ def rc_type(self, val:int) -> None:
364
+ if not self.is_hdmi or not self.is_input:
365
+ return
366
+ if ((self._rc_type is None) or (self._rc_type != val)):
367
+ self._rc_type = val
368
+ self.callbacks.on_rc_type_changed(self, self.rc_type)
369
+ self.call_callbacks()
370
+
371
+ @property
372
+ def video_source(self) -> BayBase:
373
+ if not self.is_output:
374
+ return None
375
+ # current video source bay
376
+ return self._video_source
377
+
378
+ @video_source.setter
379
+ def video_source(self, source:BayBase) -> None:
380
+ # set the cached video source bay
381
+ if not self.is_output:
382
+ return
383
+ if source is None:
384
+ self._video_source = source
385
+ return
386
+ if (self._video_source is None) or (source != self._video_source):
387
+ self._video_source = source
388
+ self.callbacks.on_video_source_changed(self, source)
389
+ self.call_callbacks()
390
+
391
+ async def select_edid_profile(self, profile:EdidProfile) -> bool:
392
+ frame:FrameBase = FrameEDIDProfile.construct(mxr=self.device.registry, target=self.device, profile=profile)
393
+ if frame is not None:
394
+ self.device.registry.transmit(frame.frame)
395
+ self.edid_profile = profile
396
+ return True
397
+ return False
398
+
399
+ async def set_hidden(self, hidden:bool) -> bool:
400
+ frame:FrameBase = FrameBayHide.construct(mxr=self.device.registry, target=self, hidden=hidden)
401
+ if frame is not None:
402
+ self.device.registry.transmit(frame.frame)
403
+ self.hidden = hidden
404
+ return True
405
+ return False
406
+
407
+ async def select_audio_source(self, source:int|BayBase|str) -> bool:
408
+ if not self.is_v2ip_sink:
409
+ return False
410
+ if isinstance(source, int):
411
+ source = self.device.get_by_portnum(source)
412
+ frame:FrameBase = FrameV2IPSourceSwitch.construct(mxr=self.device.registry, target=self, audio=source)
413
+ if frame is not None:
414
+ self.device.registry.transmit(frame.frame)
415
+ return True
416
+ return False
417
+
418
+ async def select_video_source(self, port:int, opt:bool=True) -> bool:
419
+ if not self.is_output:
420
+ return False
421
+ if self.is_v2ip_sink:
422
+ source_bay = self.device.get_by_portnum(port)
423
+ if source_bay is not None:
424
+ frame:FrameBase = FrameV2IPSourceSwitch.construct(mxr=self.device.registry, target=self, video=source_bay)
425
+ if frame is not None:
426
+ self.device.registry.transmit(frame.frame)
427
+ return True
428
+ return await self.device.get_api(f"port/set/{port}/{self.bay}/{1 if opt else 0}") is not None
429
+
430
+ async def select_video_source_by_user_name(self, name:str, opt:bool=True) -> bool:
431
+ source = None
432
+ for _, bay in self.device.inputs.items():
433
+ if bay.user_name == name:
434
+ source = bay
435
+ break
436
+ if source is None:
437
+ return False
438
+ return await self.select_video_source(source.port, opt)
439
+
440
+ async def set_name(self, name:str) -> bool:
441
+ frame:FrameBase = FrameSetName.construct(mxr=self.device.registry, target=self, name=name)
442
+ if frame is not None:
443
+ self.device.registry.transmit(frame.frame)
444
+ self.user_name = name
445
+ return True
446
+ return False
447
+
448
+ @property
449
+ def audio_source(self) -> BayBase:
450
+ if not self.is_output:
451
+ return None
452
+ # current audio source bay
453
+ if self._audio_source is None:
454
+ return self.video_source
455
+ return self._audio_source
456
+
457
+ @audio_source.setter
458
+ def audio_source(self, source:BayBase) -> None:
459
+ if not self.is_output:
460
+ return
461
+ # set the cached audio source bay
462
+ if source is None:
463
+ self._audio_source = source
464
+ return
465
+ prev = self.audio_source
466
+ if (self._audio_source is None) or (source != self._audio_source):
467
+ self._audio_source = source
468
+ if prev != self.audio_source:
469
+ self.callbacks.on_audio_source_changed(self, self.audio_source)
470
+ self.call_callbacks()
471
+
472
+ @property
473
+ def powered_on(self) -> bool:
474
+ # connected device powered on
475
+ return (self._power_status is not None) and (self._power_status == 'on')
476
+
477
+ @property
478
+ def powered_off(self) -> bool:
479
+ # connected device powered off
480
+ return (self._power_status is not None) and (self._power_status == 'off')
481
+
482
+ @property
483
+ def power_status(self) -> str:
484
+ # device power status
485
+ if not self.available or self.powered_off:
486
+ return "off"
487
+ if self.powered_on:
488
+ return "on"
489
+ if self.is_hdmi:
490
+ if self.is_input:
491
+ return "on" if self.signal_detected else "off"
492
+ if self.is_output and not self.hpd_detected:
493
+ return "off"
494
+ if not self.signal_detected:
495
+ return "off"
496
+ if self.is_hdbaset and not self.hdbt_connected:
497
+ return "off"
498
+ elif self.is_audio:
499
+ if self.muted:
500
+ return "off"
501
+ return "on" if (self.signal_detected) else "off"
502
+ return "unknown"
503
+
504
+ @power_status.setter
505
+ def power_status(self, power:str) -> None:
506
+ prev = self.power_status
507
+ self._power_status = power
508
+ if (self.power_status != prev):
509
+ self.callbacks.on_power_changed(self, power)
510
+ self.call_callbacks()
511
+
512
+ async def tx_action(self, action:RCAction) -> bool:
513
+ from ..proto.FrameRCAction import FrameRCAction
514
+ pkt:FrameBase = FrameRCAction.construct(mxr=self.device.registry, target=self, action=action)
515
+ return self.device.registry.transmit(pkt.frame)
516
+
517
+ async def power_on(self) -> bool:
518
+ if await self.tx_action(RCAction.ACTION_POWER_ON):
519
+ self.power_status = 'on'
520
+ return True
521
+ return False
522
+
523
+ async def power_off(self) -> bool:
524
+ if await self.tx_action(RCAction.ACTION_POWER_OFF):
525
+ self.power_status = 'off'
526
+ return True
527
+ return False
528
+
529
+ @property
530
+ def faulty(self) -> bool:
531
+ # bay is faulty
532
+ return (self._faulty is not None) and self._faulty
533
+
534
+ @faulty.setter
535
+ def faulty(self, val:bool) -> None:
536
+ prev = self.faulty
537
+ self._faulty = val
538
+ if prev != self.faulty and (prev or val):
539
+ self.callbacks.on_status_faulty_changed(self, val)
540
+ self.call_callbacks()
541
+
542
+ @property
543
+ def hidden(self) -> bool:
544
+ # bay is hidden
545
+ return (self._hidden is not None) and self._hidden
546
+
547
+ @hidden.setter
548
+ def hidden(self, val:bool) -> None:
549
+ prev = self.hidden
550
+ self._hidden = val
551
+ if prev != self.hidden and (prev or val):
552
+ self.callbacks.on_status_hidden_changed(self, val)
553
+ self.call_callbacks()
554
+
555
+ @property
556
+ def poe_powered(self) -> bool:
557
+ # bay poe is powered
558
+ return (self._poe_powered is not None) and self._poe_powered
559
+
560
+ @poe_powered.setter
561
+ def poe_powered(self, val:bool) -> None:
562
+ prev = self.poe_powered
563
+ self._poe_powered = val
564
+ if prev != self.poe_powered and (not prev or not val):
565
+ self.callbacks.on_status_poe_powered_changed(self, val)
566
+ self.call_callbacks()
567
+
568
+ @property
569
+ def hdbt_connected(self) -> bool:
570
+ # hdbt link up
571
+ return (self._hdbt_connected is not None) and self._hdbt_connected
572
+
573
+ @hdbt_connected.setter
574
+ def hdbt_connected(self, val:bool) -> None:
575
+ prev = self.hdbt_connected
576
+ self._hdbt_connected = val
577
+ if prev != self.hdbt_connected:
578
+ self.callbacks.on_status_hdbt_connected_changed(self, val)
579
+ self.call_callbacks()
580
+
581
+ @property
582
+ def signal_detected(self) -> bool:
583
+ # video/audio signal detected
584
+ return (self._signal_detected is not None) and self._signal_detected
585
+
586
+ @signal_detected.setter
587
+ def signal_detected(self, val:bool) -> None:
588
+ prev = self.signal_detected
589
+ self._signal_detected = val
590
+ if prev != self.signal_detected:
591
+ self.callbacks.on_status_signal_detected_changed(self, val)
592
+ self.call_callbacks()
593
+
594
+ @property
595
+ def encoder_disabled(self) -> bool:
596
+ # video/audio encoder disabled
597
+ return (self._encoder_disabled is not None) and self._encoder_disabled
598
+
599
+ @encoder_disabled.setter
600
+ def encoder_disabled(self, val:bool) -> None:
601
+ prev = self.encoder_disabled
602
+ self._encoder_disabled = val
603
+ if prev != self.decoder_disabled:
604
+ self.callbacks.on_bay_update(self)
605
+ self.call_callbacks()
606
+
607
+ @property
608
+ def decoder_disabled(self) -> bool:
609
+ # video/audio decoder disabled
610
+ return (self._decoder_disabled is not None) and self._decoder_disabled
611
+
612
+ @decoder_disabled.setter
613
+ def decoder_disabled(self, val:bool) -> None:
614
+ prev = self._decoder_disabled
615
+ self._decoder_disabled = val
616
+ if prev != self.decoder_disabled:
617
+ self.callbacks.on_bay_update(self)
618
+ self.call_callbacks()
619
+
620
+ @property
621
+ def signal_type(self) -> str:
622
+ # video/audio signal type
623
+ return self._signal_type if (self._signal_type is not None) else 'unknown'
624
+
625
+ @signal_type.setter
626
+ def signal_type(self, val:str) -> None:
627
+ prev = self.signal_type
628
+ self._signal_type = val
629
+ if prev != self.signal_type:
630
+ self.callbacks.on_status_signal_type_changed(self, val)
631
+ self.call_callbacks()
632
+
633
+ @property
634
+ def hpd_detected(self) -> bool:
635
+ # hotplug detected
636
+ return (self._hpd_detected is not None) and self._hpd_detected
637
+
638
+ @hpd_detected.setter
639
+ def hpd_detected(self, val:bool) -> None:
640
+ prev = self.hpd_detected
641
+ self._hpd_detected = val
642
+ if prev != self.hpd_detected:
643
+ self.callbacks.on_status_hpd_detected_changed(self, val)
644
+ self.call_callbacks()
645
+
646
+ @property
647
+ def cec_detected(self) -> bool:
648
+ # CEC capable device detected
649
+ return (self._cec_detected is not None) and self._cec_detected
650
+
651
+ @cec_detected.setter
652
+ def cec_detected(self, val:bool) -> None:
653
+ prev = self.cec_detected
654
+ self._cec_detected = val
655
+ if prev != self.cec_detected:
656
+ self.callbacks.on_status_cec_detected_changed(self, val)
657
+ self.call_callbacks()
658
+
659
+ @property
660
+ def mirroring(self) -> str:
661
+ return self._mirror
662
+
663
+ @mirroring.setter
664
+ def mirroring(self, val) -> None:
665
+ prev = self.mirroring
666
+ self._mirror = val
667
+ if prev != val:
668
+ self.callbacks.on_mirror_status_changed(self, val)
669
+ self.call_callbacks()
670
+
671
+ @property
672
+ def filtered(self) -> str:
673
+ return self._filtered
674
+
675
+ @filtered.setter
676
+ def filtered(self, val) -> None:
677
+ prev = self.filtered
678
+ self._filtered = val
679
+ if prev != val:
680
+ self.callbacks.on_filter_status_changed(self, val)
681
+ self.call_callbacks()
682
+
683
+ @property
684
+ def arc(self) -> str:
685
+ # audio return channel status
686
+ return self._arc
687
+
688
+ @arc.setter
689
+ def arc(self, val:str) -> None:
690
+ prev = self.arc
691
+ self._arc = val
692
+ if prev != self.arc:
693
+ self.callbacks.on_status_arc_changed(self, val)
694
+ self.call_callbacks()
695
+
696
+ @property
697
+ def volume_status(self) -> VolumeMuteStatus:
698
+ # volume and mute status
699
+
700
+ # # handle amp dolby modes
701
+ # if self.device.is_amp:
702
+ # if self.is_output:
703
+ # if (self.bay >= self.device.amp_dolby_channels):
704
+ # return self.device.get_by_portname('Input {}'.format(self.bay + 1)).volume_status
705
+ # return self.device.get_by_portname('Input 9').volume_status
706
+
707
+ # check mx_remote links
708
+ primary = self.primary
709
+ if primary != self:
710
+ return primary.volume_status
711
+ return self._audio_volume
712
+
713
+ @volume_status.setter
714
+ def volume_status(self, other:VolumeMuteStatus) -> None:
715
+ # # handle amp dolby modes
716
+ # if self.device.is_amp:
717
+ # if self.is_output:
718
+ # if (self.bay >= self.device.amp_dolby_channels):
719
+ # self.device.get_by_portname('Input {}'.format(self.bay + 1)).volume_status = other
720
+ # return
721
+ # self.device.get_by_portname('Input 9').volume_status = other
722
+ # return
723
+
724
+ primary = self.primary
725
+ if primary != self:
726
+ primary.volume_status = other
727
+ return
728
+
729
+ changed = False
730
+ if self._audio_volume is None:
731
+ self._audio_volume = other
732
+ changed = True
733
+ else:
734
+ changed = self._audio_volume.update(other)
735
+
736
+ if changed:
737
+ self.callbacks.on_volume_changed(self, self.volume_status)
738
+ self.call_callbacks()
739
+ lbay = self.linked_bay
740
+ if lbay is not None:
741
+ self.callbacks.on_volume_changed(lbay, self.volume_status)
742
+ self.call_callbacks()
743
+
744
+ if self.device.is_amp and self.is_input:
745
+ if (self.bay == 8):
746
+ nb = 0
747
+ while nb < self.device.amp_dolby_channels:
748
+ self.callbacks.on_volume_changed(self.device.get_by_portname('Output {}'.format(nb + 1)), self.volume_status)
749
+ self.device.get_by_portname('Output {}'.format(nb + 1)).call_callbacks()
750
+ nb = nb + 1
751
+ return
752
+ self.callbacks.on_volume_changed(self.device.get_by_portname('Output {}'.format(self.bay + 1)), self.volume_status)
753
+ self.device.get_by_portname('Output {}'.format(nb + 1)).call_callbacks()
754
+
755
+ @property
756
+ def volume(self) -> int:
757
+ # current volume
758
+ vs = self.volume_status
759
+ return vs.volume if vs is not None else None
760
+
761
+ @property
762
+ def muted(self) -> bool:
763
+ # muted or not
764
+ vs = self.volume_status
765
+ return vs.muted if vs is not None else None
766
+
767
+ @property
768
+ def amp_settings(self) -> AmpZoneSettings|None:
769
+ return self._amp_settings
770
+
771
+ @amp_settings.setter
772
+ def amp_settings(self, settings:AmpZoneSettings) -> None:
773
+ changed = (self._amp_settings is None) or (self._amp_settings != settings)
774
+ self._amp_settings = settings
775
+ if changed:
776
+ self.callbacks.on_amp_zone_settings_changed(self, settings)
777
+ self.call_callbacks()
778
+
779
+ def volume_up(self) -> bool:
780
+ return self.volume_set(self.volume + 1)
781
+
782
+ def volume_down(self) -> bool:
783
+ return self.volume_set(self.volume - 1)
784
+
785
+ def volume_set(self, volume:int, muted:bool=None) -> bool:
786
+ ''' Change the volume on the remote device '''
787
+ from ..proto.FrameVolumeSet import FrameVolumeSet
788
+ if not self.has_volume_control:
789
+ # remote device doesn't support volume control
790
+ return False
791
+
792
+ new_value = self.volume_status
793
+ if new_value is None:
794
+ # no known value, create a new one
795
+ new_value = VolumeMuteStatus(volume_left=volume, volume_right=volume, muted_left=(volume != 0), muted_right=(volume != 0))
796
+ else:
797
+ # update the volume
798
+ new_value.volume = volume
799
+ self.volume_status = new_value
800
+
801
+ if (self.volume is None) or (volume > self.volume):
802
+ # unmute
803
+ self.volume_status.muted = False
804
+
805
+ if muted is not None:
806
+ # update the mute value if provided
807
+ new_value.muted = muted
808
+
809
+ # send the update to the remote device
810
+ pkt:FrameBase = FrameVolumeSet.construct(mxr=self.device.registry, target=self, volume=new_value)
811
+ if self.device.registry.transmit(pkt.frame):
812
+ self.callbacks.on_volume_changed(bay=self, volume=new_value)
813
+ return True
814
+ return False
815
+
816
+ def mute_set(self, mute:bool) -> bool:
817
+ return self.volume_set(self.volume, mute)
818
+
819
+ async def send_key(self, key:int) -> bool:
820
+ cmd = "key/sendkey/{}/{}/{}".format(str(key), self.mode, self.bay)
821
+ _LOGGER.info(cmd)
822
+ return await self.device.get_api(cmd) is not None
823
+
824
+ @property
825
+ def is_primary(self) -> bool:
826
+ return self.device.registry.links.is_primary(self)
827
+
828
+ @property
829
+ def primary(self) -> BayBase:
830
+ # primary bay if linked. this is the source type bay for linked bays. this bay is it's own primary if not linked
831
+ if self.link_configured and not self.is_primary:
832
+ return self.link.linked_bay
833
+ return self
834
+
835
+ @property
836
+ def linked_bay(self) -> BayBase:
837
+ # linked bay if linked, None if not linked
838
+ if self.link_configured:
839
+ return self.link.linked_bay
840
+ return None
841
+
842
+ @property
843
+ def link(self) -> BayLink|None:
844
+ return self.device.registry.links.get(self)
845
+
846
+ @property
847
+ def link_configured(self) -> bool:
848
+ link = self.link
849
+ return (link is not None) and (link.linked)
850
+
851
+ @property
852
+ def link_connected(self) -> bool:
853
+ return self.link.connected
854
+
855
+ @property
856
+ def link_online(self) -> bool:
857
+ return self.link.online
858
+
859
+ def on_key_pressed(self, key:RCKey) -> None:
860
+ self.callbacks.on_key_pressed(self, key)
861
+ self.call_callbacks()
862
+
863
+ def on_action_received(self, action:RCAction) -> None:
864
+ self.callbacks.on_action_received(self, action)
865
+ self.call_callbacks()
866
+
867
+ def on_mxr_bay_status(self, data:BayStatusMask) -> None:
868
+ self.faulty = data.fault
869
+ self.hidden = data.hidden
870
+ self.poe_powered = data.powered
871
+ self.hdbt_connected = data.hdbt_connected
872
+ self.hpd_detected = data.hpd_detected
873
+ self.cec_detected = data.cec_detected
874
+ self.signal_detected = data.signal_detected
875
+ self.encoder_disabled = data.encoder_disabled
876
+ self.decoder_disabled = data.decoder_disabled
877
+
878
+ if not data.cec_detected:
879
+ self.power_status = 'unknown'
880
+ elif data.powered_on:
881
+ self.power_status = 'on'
882
+ elif data.powered_off:
883
+ self.power_status = 'off'
884
+ else:
885
+ self.power_status = 'unknown'
886
+ if data.audio_arc_hdmi:
887
+ self.arc = self.ARC_HDMI
888
+ elif data.audio_arc_optical:
889
+ self.arc = self.ARC_OPTICAL
890
+ elif data.audio_arc_analog:
891
+ self.arc = self.ARC_ANALOG
892
+ else:
893
+ self.arc = self.ARC_NONE
894
+
895
+ def on_mxr_bay_config(self, data:BayConfig) -> None:
896
+ self.features_mask = data.features
897
+ self.status_mask = data.status
898
+ self.user_name = data.user_name
899
+ self.bay = data.bay
900
+ self.on_mxr_bay_status(data.status)
901
+ if not data.status.signal_detected or not self.device.is_v2ip:
902
+ self.signal_type = data.signal_type
903
+ if self.is_output:
904
+ self.video_source = self.device.get_by_portnum(data.video_source)
905
+ self.audio_source = self.device.get_by_portnum(data.audio_source)
906
+ else:
907
+ self.rc_type = data.rc_type
908
+ self.edid_profile = data.edid_profile
909
+
910
+ def on_mxr_volume_update(self, data:VolumeMuteStatus) -> None:
911
+ self.volume_status = data
912
+
913
+ def __str__(self) -> str:
914
+ if self.is_v2ip_source:
915
+ if self.v2ip_source is None:
916
+ return f"{self.device.serial} {self.bay_label} <unknown mcast address>"
917
+ return f"{self.device.serial} {self.bay_label} {self.v2ip_source.video}"
918
+ return f"{self.device.serial} {self.bay_label}"
919
+
920
+ def __eq__(self, other:Any) -> bool:
921
+ return isinstance(other, BayBase) and \
922
+ (self.device == other.device) and \
923
+ (self.port == other.port)