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