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