mx-remote 5.2.0__tar.gz → 5.3.0__tar.gz

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 (207) hide show
  1. {mx_remote-5.2.0 → mx_remote-5.3.0}/PKG-INFO +1 -1
  2. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/const.py +1 -1
  3. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/Factory.py +11 -4
  4. {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/handlers.py +28 -0
  5. mx_remote-5.3.0/tests/handlers_cover.py +268 -0
  6. {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/hello.py +20 -3
  7. {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/run.py +1 -0
  8. mx_remote-5.2.0/mx_remote/proto/FramePDUState.py +0 -72
  9. mx_remote-5.2.0/mx_remote/proto/FramePDUState.pyi +0 -23
  10. mx_remote-5.2.0/mx_remote/proto/FrameV2IPStreamDetails.py +0 -60
  11. mx_remote-5.2.0/mx_remote/proto/FrameV2IPStreamDetails.pyi +0 -17
  12. mx_remote-5.2.0/mx_remote/proto/PDUState.py +0 -100
  13. mx_remote-5.2.0/mx_remote/proto/PDUState.pyi +0 -29
  14. mx_remote-5.2.0/mx_remote/remote/P8PDU.py +0 -156
  15. mx_remote-5.2.0/mx_remote/remote/P8PDU.pyi +0 -46
  16. {mx_remote-5.2.0 → mx_remote-5.3.0}/.gitignore +0 -0
  17. {mx_remote-5.2.0 → mx_remote-5.3.0}/CLAUDE.md +0 -0
  18. {mx_remote-5.2.0 → mx_remote-5.3.0}/LICENSE +0 -0
  19. {mx_remote-5.2.0 → mx_remote-5.3.0}/README.md +0 -0
  20. {mx_remote-5.2.0 → mx_remote-5.3.0}/hatch_build.py +0 -0
  21. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/Interface.py +0 -0
  22. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/Interface.pyi +0 -0
  23. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/Uid.py +0 -0
  24. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/Uid.pyi +0 -0
  25. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/__init__.py +0 -0
  26. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/__init__.pyi +0 -0
  27. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/const.pyi +0 -0
  28. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/main.py +0 -0
  29. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/main.pyi +0 -0
  30. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/BayConfig.py +0 -0
  31. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/BayConfig.pyi +0 -0
  32. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/Constants.py +0 -0
  33. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/Constants.pyi +0 -0
  34. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/Data.py +0 -0
  35. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/Data.pyi +0 -0
  36. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/Factory.pyi +0 -0
  37. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameAmpDolbySettings.py +0 -0
  38. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameAmpDolbySettings.pyi +0 -0
  39. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameAmpZoneSettings.py +0 -0
  40. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameAmpZoneSettings.pyi +0 -0
  41. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameAudioClip.py +0 -0
  42. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameAudioClip.pyi +0 -0
  43. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameAudioSetRoute.py +0 -0
  44. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameAudioSetRoute.pyi +0 -0
  45. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameBase.py +0 -0
  46. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameBase.pyi +0 -0
  47. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameBayConfig.py +0 -0
  48. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameBayConfig.pyi +0 -0
  49. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameBayConfigSecondary.py +0 -0
  50. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameBayConfigSecondary.pyi +0 -0
  51. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameBayHide.py +0 -0
  52. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameBayHide.pyi +0 -0
  53. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameBayStatus.py +0 -0
  54. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameBayStatus.pyi +0 -0
  55. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameConnectStatus.py +0 -0
  56. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameConnectStatus.pyi +0 -0
  57. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameDebug.py +0 -0
  58. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameDebug.pyi +0 -0
  59. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameDiscover.py +0 -0
  60. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameDiscover.pyi +0 -0
  61. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameEDID.py +0 -0
  62. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameEDID.pyi +0 -0
  63. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameEDIDProfile.py +0 -0
  64. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameEDIDProfile.pyi +0 -0
  65. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameFactoryReset.py +0 -0
  66. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameFactoryReset.pyi +0 -0
  67. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameFilterStatus.py +0 -0
  68. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameFilterStatus.pyi +0 -0
  69. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameFirmwareVersion.py +0 -0
  70. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameFirmwareVersion.pyi +0 -0
  71. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameHeader.py +0 -0
  72. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameHeader.pyi +0 -0
  73. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameHello.py +0 -0
  74. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameHello.pyi +0 -0
  75. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameLinks.py +0 -0
  76. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameLinks.pyi +0 -0
  77. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameMeshOperation.py +0 -0
  78. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameMeshOperation.pyi +0 -0
  79. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameMirrorStatus.py +0 -0
  80. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameMirrorStatus.pyi +0 -0
  81. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameMonitoringPulse.py +0 -0
  82. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameMonitoringPulse.pyi +0 -0
  83. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameNetworkStatus.py +0 -0
  84. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameNetworkStatus.pyi +0 -0
  85. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FramePowerChange.py +0 -0
  86. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FramePowerChange.pyi +0 -0
  87. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameRCAction.py +0 -0
  88. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameRCAction.pyi +0 -0
  89. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameRCIr.py +0 -0
  90. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameRCIr.pyi +0 -0
  91. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameRCKey.py +0 -0
  92. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameRCKey.pyi +0 -0
  93. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameRCSettings.py +0 -0
  94. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameRCSettings.pyi +0 -0
  95. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameReboot.py +0 -0
  96. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameReboot.pyi +0 -0
  97. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameRoutingChange.py +0 -0
  98. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameRoutingChange.pyi +0 -0
  99. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSetInstaller.py +0 -0
  100. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSetInstaller.pyi +0 -0
  101. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSetName.py +0 -0
  102. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSetName.pyi +0 -0
  103. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSetRoute.py +0 -0
  104. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSetRoute.pyi +0 -0
  105. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSetupStatus.py +0 -0
  106. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSetupStatus.pyi +0 -0
  107. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSignalStatus.py +0 -0
  108. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSignalStatus.pyi +0 -0
  109. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSignalStatusNew.py +0 -0
  110. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSignalStatusNew.pyi +0 -0
  111. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSysTemperature.py +0 -0
  112. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSysTemperature.pyi +0 -0
  113. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSystemStatus.py +0 -0
  114. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameSystemStatus.pyi +0 -0
  115. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameTXRCAction.py +0 -0
  116. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameTXRCAction.pyi +0 -0
  117. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameTXRCKey.py +0 -0
  118. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameTXRCKey.pyi +0 -0
  119. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameTopology.py +0 -0
  120. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameTopology.pyi +0 -0
  121. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameTxIR.py +0 -0
  122. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameTxIR.pyi +0 -0
  123. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameUpgradeFPGA.py +0 -0
  124. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameUpgradeFPGA.pyi +0 -0
  125. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPAudio.py +0 -0
  126. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPAudio.pyi +0 -0
  127. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPBayMapping.py +0 -0
  128. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPBayMapping.pyi +0 -0
  129. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPDetectBays.py +0 -0
  130. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPDetectBays.pyi +0 -0
  131. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.py +0 -0
  132. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.pyi +0 -0
  133. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPLink.py +0 -0
  134. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPLink.pyi +0 -0
  135. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.py +0 -0
  136. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.pyi +0 -0
  137. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPMultiviewer.py +0 -0
  138. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPMultiviewer.pyi +0 -0
  139. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPPowerSave.py +0 -0
  140. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPPowerSave.pyi +0 -0
  141. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPSetMaster.py +0 -0
  142. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPSetMaster.pyi +0 -0
  143. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPSourceSwitch.py +0 -0
  144. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPSourceSwitch.pyi +0 -0
  145. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPSources.py +0 -0
  146. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPSources.pyi +0 -0
  147. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPStats.py +0 -0
  148. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPStats.pyi +0 -0
  149. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPTiling.py +0 -0
  150. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPTiling.pyi +0 -0
  151. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPVideoWall.py +0 -0
  152. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameV2IPVideoWall.pyi +0 -0
  153. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameVolume.py +0 -0
  154. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameVolume.pyi +0 -0
  155. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameVolumeDown.py +0 -0
  156. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameVolumeDown.pyi +0 -0
  157. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameVolumeSet.py +0 -0
  158. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameVolumeSet.pyi +0 -0
  159. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameVolumeUp.py +0 -0
  160. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/FrameVolumeUp.pyi +0 -0
  161. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/LinkConfig.py +0 -0
  162. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/LinkConfig.pyi +0 -0
  163. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/Multiviewer.py +0 -0
  164. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/Multiviewer.pyi +0 -0
  165. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/Svd.py +0 -0
  166. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/Svd.pyi +0 -0
  167. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/V2IPConfig.py +0 -0
  168. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/V2IPConfig.pyi +0 -0
  169. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/V2IPStats.py +0 -0
  170. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/V2IPStats.pyi +0 -0
  171. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/__init__.py +0 -0
  172. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/__init__.pyi +0 -0
  173. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/proto/svd.csv +0 -0
  174. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/py.typed +0 -0
  175. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/Bay.py +0 -0
  176. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/Bay.pyi +0 -0
  177. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/ConnectionAsync.py +0 -0
  178. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/ConnectionAsync.pyi +0 -0
  179. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/Device.py +0 -0
  180. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/Device.pyi +0 -0
  181. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/Link.py +0 -0
  182. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/Link.pyi +0 -0
  183. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/Remote.py +0 -0
  184. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/Remote.pyi +0 -0
  185. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/State.py +0 -0
  186. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/State.pyi +0 -0
  187. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/V2IP.py +0 -0
  188. {mx_remote-5.2.0 → mx_remote-5.3.0}/mx_remote/remote/V2IP.pyi +0 -0
  189. {mx_remote-5.2.0 → mx_remote-5.3.0}/pyproject.toml +0 -0
  190. {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/audio_ir.py +0 -0
  191. {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/capture.py +0 -0
  192. {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/e2e.py +0 -0
  193. {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/entry.py +0 -0
  194. {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/merge.py +0 -0
  195. {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/newops.py +0 -0
  196. {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/offsets.py +0 -0
  197. {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/proto_stamp.py +0 -0
  198. {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/protogate.py +0 -0
  199. {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/rcsettings.py +0 -0
  200. {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/roundtrip.py +0 -0
  201. {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/smoke.py +0 -0
  202. {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/sources.py +0 -0
  203. {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/stats.py +0 -0
  204. {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/structs.py +0 -0
  205. {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/txresult.py +0 -0
  206. {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/unknowns.py +0 -0
  207. {mx_remote-5.2.0 → mx_remote-5.3.0}/tests/vdetails.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mx_remote
3
- Version: 5.2.0
3
+ Version: 5.3.0
4
4
  Summary: Python 3 library for interfacing with MX Remote compatible devices
5
5
  Project-URL: Homepage, https://github.com/opdenkamp/mx-remote/
6
6
  Project-URL: Documentation, https://github.com/opdenkamp/mx-remote/
@@ -14,7 +14,7 @@ Default communication modes:
14
14
  """
15
15
  import os
16
16
 
17
- VERSION = '5.2.0'
17
+ VERSION = '5.3.0'
18
18
  __version__ = VERSION
19
19
 
20
20
  MX_BCAST_UDP_PORT = 8811
@@ -108,9 +108,6 @@ def _mxr_frame_factory(hdr:FrameHeader, timestamp:float) -> FrameBase|None:
108
108
  if hdr.opcode == 0x1F:
109
109
  from .FrameV2IPSourceSwitch import FrameV2IPSourceSwitch
110
110
  return FrameV2IPSourceSwitch(header=hdr, timestamp=timestamp)
111
- if hdr.opcode == 0x16:
112
- from .FramePDUState import FramePDUState
113
- return FramePDUState(header=hdr, timestamp=timestamp)
114
111
  if hdr.opcode == 0x20:
115
112
  from .FrameV2IPLink import FrameV2IPLinkStatus
116
113
  return FrameV2IPLinkStatus(header=hdr, timestamp=timestamp)
@@ -225,11 +222,21 @@ def _mxr_frame_factory(hdr:FrameHeader, timestamp:float) -> FrameBase|None:
225
222
 
226
223
  # Opcodes reaching here are ones current firmware does not emit:
227
224
  #
225
+ # 0x16 PDU_STATE dead; no current MatrixOS build
226
+ # supports it
228
227
  # 0x17..0x1E the CEC range never implemented
229
- # 0x25, 0x33 RESERVED_0 / RESERVED_1 retired; never reuse the numbers
228
+ # 0x25 SYS_BAY_V2IP_DETAILS retired 2024-09, replaced by 0x3C
230
229
  # 0x2D VIDEO_CLOCK_RATE_OLD superseded by clock_rate in 0x31
231
230
  # 0x2E, 0x2F V2IP_BLIST_* behind V2IP_SUPPORT_BLACKLIST, which
232
231
  # no project defines
232
+ # 0x33 RESERVED_1 was MX_OP_NB, the table's end sentinel
233
+ #
234
+ # Never reuse 0x16 or 0x25. Both carried a real payload - 0x16 a PDU's state,
235
+ # 0x25 v2ip_device_sources, with video, audio and anc at 0, 8 and 16,
236
+ # audio_return at 24 and source_clock at 32 - so a unit old enough to decode
237
+ # them reads anything reissued under those numbers as the old payload rather
238
+ # than ignoring it. 0x33 is reserved for a different reason: it was only ever
239
+ # the table's end marker, so no unit ever read a payload from it.
233
240
  #
234
241
  # Add decoders here rather than removing them: a unit on older firmware still
235
242
  # emits superseded opcodes, which is what 0x06, 0x36 and 0x47 are for. An
@@ -32,26 +32,54 @@ import mx_remote.proto as proto_pkg # noqa: E402
32
32
  from mx_remote.proto.FrameBase import FrameBase # noqa: E402
33
33
 
34
34
  BASELINE = {
35
+ 'FrameAmpDolbySettings.FrameAmpDolbySettings',
35
36
  'FrameAmpZoneSettings.FrameAmpZoneSettings',
36
37
  'FrameAudioClip.FrameAudioClip',
37
38
  'FrameAudioSetRoute.FrameAudioSetRoute',
38
39
  'FrameBayConfig.FrameBayConfig',
40
+ 'FrameBayConfigSecondary.FrameBayConfigSecondary',
41
+ 'FrameBayHide.FrameBayHide',
39
42
  'FrameBayStatus.FrameBayStatus',
43
+ 'FrameConnectStatus.FrameConnectStatus',
44
+ 'FrameDebug.FrameDebug',
45
+ 'FrameFilterStatus.FrameFilterStatus',
40
46
  'FrameFirmwareVersion.FrameFirmwareVersion',
41
47
  'FrameHello.FrameHello',
42
48
  'FrameLinks.FrameLinks',
49
+ 'FrameMeshOperation.FrameMeshOperation',
50
+ 'FrameMirrorStatus.FrameMirrorStatus',
51
+ 'FrameNetworkStatus.FrameNetworkStatus',
52
+ 'FramePowerChange.FramePowerChange',
43
53
  'FrameRCAction.FrameRCAction',
44
54
  'FrameRCKey.FrameRCKey',
55
+ 'FrameRCSettings.FrameRCSettings',
45
56
  'FrameRoutingChange.FrameRoutingChange',
57
+ 'FrameSignalStatus.FrameSignalStatus',
46
58
  'FrameSignalStatusNew.FrameSignalStatusNew',
59
+ 'FrameSysTemperature.FrameSysTemperature',
60
+ 'FrameSystemStatus.FrameSystemStatus',
47
61
  'FrameTXRCAction.FrameTXRCAction',
48
62
  'FrameTXRCKey.FrameTXRCKey',
63
+ 'FrameTopology.FrameTopology',
64
+ 'FrameV2IPAudio.FrameV2IPAudio',
65
+ 'FrameV2IPAudio.FrameV2IPAudioChangeSource',
66
+ 'FrameV2IPAudio.FrameV2IPAudioConfig',
67
+ 'FrameV2IPAudio.FrameV2IPAudioLinks',
68
+ 'FrameV2IPBayMapping.FrameV2IPBayMapping',
49
69
  'FrameV2IPDetectBays.FrameV2IPDetectBays',
50
70
  'FrameV2IPDeviceConfiguration.FrameV2IPDeviceConfiguration',
71
+ 'FrameV2IPManualSourceSwitch.FrameV2IPManualSourceSwitch',
51
72
  'FrameV2IPMultiviewer.FrameV2IPMultiviewer',
73
+ 'FrameV2IPPowerSave.FrameV2IPPowerSave',
74
+ 'FrameV2IPSourceSwitch.FrameV2IPSourceSwitch',
52
75
  'FrameV2IPSources.FrameV2IPSources',
53
76
  'FrameV2IPStats.FrameV2IPStats',
77
+ 'FrameV2IPTiling.FrameV2IPTiling',
54
78
  'FrameV2IPVideoWall.FrameV2IPVideoWall',
79
+ 'FrameVolume.FrameVolume',
80
+ 'FrameVolumeDown.FrameVolumeDown',
81
+ 'FrameVolumeSet.FrameVolumeSet',
82
+ 'FrameVolumeUp.FrameVolumeUp',
55
83
  }
56
84
 
57
85
  def main() -> int:
@@ -0,0 +1,268 @@
1
+ ######################################################
2
+ ## MX Remote Python Interface ##
3
+ ## ##
4
+ ## author: Lars Op den Kamp (lars@opdenkamp-it.nl) ##
5
+ ## copyright (c) 2021-2026 Op den Kamp IT Solutions ##
6
+ ######################################################
7
+ '''Run the frame handlers no other suite reaches, and assert what they changed.
8
+
9
+ handlers.py measures which process() methods the suites execute. A handler no
10
+ suite runs is invisible to every other check here, because they all measure
11
+ tests that run. This drives the remainder.
12
+
13
+ Executing a handler is not the point: each case asserts the state the handler
14
+ was supposed to write. Where a handler is deliberately a no-op, the decode is
15
+ asserted instead and the no-op is stated, so a later reader does not mistake a
16
+ thin case for an oversight.
17
+ '''
18
+
19
+ import os, struct, sys, time, logging
20
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
21
+ logging.disable(logging.CRITICAL)
22
+ import mx_remote
23
+ from mx_remote.Uid import MxrDeviceUid
24
+ from mx_remote.proto.Factory import create_mxr_frame, process_mxr_frame
25
+ from mx_remote.Interface import PowerStatus, BayFeaturesMask
26
+
27
+ UID = bytes(range(1, 17))
28
+ PEER = bytes(range(60, 76))
29
+ ADDR = ('192.0.2.9', 8812)
30
+
31
+ mx = mx_remote.Remote(open_connection=False)
32
+ mx._uid = bytes(range(100, 116))
33
+
34
+ def nm(s, sz=16):
35
+ b = s.encode('ascii')[:sz]
36
+ return b + bytes(sz - len(b))
37
+
38
+ def rx(opcode, payload=b'', uid=UID):
39
+ '''Feed a frame in the way the runtime does, so process() runs.'''
40
+ mx.process_frame(time.time(), create_mxr_frame(uid, opcode, payload), ADDR)
41
+
42
+ def decode(opcode, payload=b'', uid=UID):
43
+ '''Decode without processing, for handlers that are deliberately no-ops.'''
44
+ return process_mxr_frame(mx, time.time(), create_mxr_frame(uid, opcode, payload), ADDR)
45
+
46
+ FEAT = (1 << 5) | (1 << 6) | (1 << 7) | (1 << 9) # routing, volume, remote control
47
+ rx(0x00, struct.pack('<H', 0x28) + nm('MX-1') + nm('P8SN12345678') + nm('5.2.0')
48
+ + struct.pack('<I', FEAT))
49
+ dev = mx.get_by_uid(MxrDeviceUid(UID))
50
+
51
+ AUDIO_OUT = int(BayFeaturesMask.AUDIO_ANA_OUT)
52
+ def bay_rec(port, mode, num, name, feat=None):
53
+ if feat is None:
54
+ feat = (1 << 1) if mode == 0 else (1 << 0)
55
+ return (bytes([port, mode, num, 0, 0]) + nm(name) + nm(name) + nm('1080p')
56
+ + struct.pack('<I', 0) + struct.pack('<I', feat))
57
+ rx(0x02, bay_rec(0, 0, 0, 'In 1') + bay_rec(1, 1, 0, 'Out 1', feat=AUDIO_OUT))
58
+ inp, out = dev.get_by_portnum(0), dev.get_by_portnum(1)
59
+ assert inp is not None and out is not None
60
+ assert out.has_volume_control, 'fixture bay needs volume control for the volume handlers'
61
+ print('fixture : %s, in=%s out=%s' % (dev.serial, inp.port, out.port))
62
+
63
+ # ---- 0x12 VOLUME: the reported volume reaches the bay
64
+ rx(0x12, bytes([out.port, 40, 41, 0]))
65
+ vs = out.volume_status
66
+ assert vs is not None and vs.volume_left == 40 and vs.volume_right == 41, vs
67
+ print('0x12 volume : bay volume %s/%s' % (vs.volume_left, vs.volume_right))
68
+
69
+ # ---- 0x14 VOLUME_SET: same state, addressed by uid and a u16 port
70
+ rx(0x14, UID + struct.pack('<H', out.port) + bytes([55, 56, 0]))
71
+ vs = out.volume_status
72
+ assert vs.volume_left == 55 and vs.volume_right == 56, vs
73
+ print('0x14 setvol : bay volume %s/%s' % (vs.volume_left, vs.volume_right))
74
+
75
+ # ---- 0x0F / 0x10 VOLUME UP and DOWN: handlers are deliberate no-ops - the
76
+ # step is applied by whatever drives them - so assert the decode instead
77
+ f = decode(0x0F, bytes([out.port]))
78
+ assert f.bay is not None and f.bay.port == out.port, f.bay
79
+ f.process() # documented no-op, must not raise
80
+ g = decode(0x10, bytes([out.port]))
81
+ assert g.bay is not None and g.bay.port == out.port, g.bay
82
+ g.process()
83
+ print('0x0F/0x10 : bay decoded, handlers are no-ops by design')
84
+
85
+ # ---- 0x05 POWER_CHANGE
86
+ rx(0x05, bytes([out.port, 1]))
87
+ assert out.power_status == PowerStatus.ON, out.power_status
88
+ rx(0x05, bytes([out.port, 0]))
89
+ assert out.power_status == PowerStatus.OFF, out.power_status
90
+ print('0x05 power : ON then OFF applied to the bay')
91
+
92
+ # ---- 0x06 SIGNAL_STATUS: detected flag and the description string
93
+ rx(0x06, bytes([inp.port, 1]) + b'1920x1080p60\x00')
94
+ assert inp.signal_detected is True, inp.signal_detected
95
+ assert '1920x1080' in inp.signal_type, inp.signal_type
96
+ print('0x06 signal : detected=%s type=%r' % (inp.signal_detected, inp.signal_type))
97
+
98
+ # ---- 0x15 SYS_TEMPERATURE
99
+ rx(0x15, bytes([2, 41, 42]))
100
+ temps = dev.temperatures
101
+ assert temps, temps
102
+ print('0x15 temp : %s' % (temps,))
103
+
104
+ # ---- 0x27 BAY_HIDE: addressed by uid, u16 port, then the flag
105
+ rx(0x27, UID + struct.pack('<H', inp.port) + bytes([1]))
106
+ assert inp.hidden is True, inp.hidden
107
+ rx(0x27, UID + struct.pack('<H', inp.port) + bytes([0]))
108
+ assert inp.hidden is False, inp.hidden
109
+ print('0x27 hide : hidden set then cleared')
110
+
111
+ # ---- 0x04 CONNECT_STATUS
112
+ # on an output the connect flag is HPD; on an input it is signal detection
113
+ rx(0x04, bytes([out.port, 1]))
114
+ assert out.hpd_detected is True, out.hpd_detected
115
+ rx(0x04, bytes([out.port, 0]))
116
+ assert out.hpd_detected is False, out.hpd_detected
117
+ print('0x04 connect: hpd set then cleared on the output')
118
+
119
+ # ---- 0x32 MIRROR_STATUS: this device's first output mirrors another device
120
+ rx(0x32, UID + PEER)
121
+ assert out.mirroring is not None and out.mirroring.is_mirroring, out.mirroring
122
+ print('0x32 mirror : %s' % (out.mirroring,))
123
+ rx(0x32, UID + bytes(16)) # an empty master clears it
124
+ assert not out.mirroring.is_mirroring, out.mirroring
125
+ print('0x32 mirror : cleared by an empty master')
126
+
127
+ # ---- 0x38 BAY_FILTER_STATUS: the filtered set lands on the first output
128
+ rx(0x38, UID + PEER)
129
+ filtered = out.filtered
130
+ assert filtered is not None and len(filtered) == 1, filtered
131
+ print('0x38 filter : %d source filtered' % len(filtered))
132
+
133
+ # ---- 0x30 TOPOLOGY: 20-byte entries, uid then a mask
134
+ rx(0x30, PEER + struct.pack('<I', 0x03))
135
+ assert dev._topology, dev._topology # no public accessor; the handler writes here
136
+ print('0x30 topo : %d entr(y/ies)' % len(dev._topology))
137
+
138
+ # ---- 0x3E AMP_DOLBY_STATE: uid, then mode and flags
139
+ rx(0x3E, UID + bytes([2, 0b11]))
140
+ ds = dev.dolby_settings
141
+ assert ds is not None and ds.mode == 2, ds
142
+ print('0x3E dolby : mode=%s upmix=%s' % (ds.mode, ds.pcm_upmix))
143
+
144
+ # ---- 0x23 BAY_CONFIG_SECONDARY: another page of bay records, merged in
145
+ before = len(dev.bays)
146
+ rx(0x23, bay_rec(2, 0, 1, 'In 3'))
147
+ assert len(dev.bays) == before + 1, (before, len(dev.bays))
148
+ assert dev.get_by_portnum(2).user_name == 'In 3', dev.get_by_portnum(2).user_name
149
+ print('0x23 cfg2 : page merged, bays %d -> %d' % (before, len(dev.bays)))
150
+
151
+ # ---- 0x46 SYSTEM_STATUS: uid then a status code
152
+ rx(0x46, UID + struct.pack('<H', 3))
153
+ print('0x46 sysstat: handled')
154
+
155
+ # ---- 0x3B MESH_OPERATION: uid, operation, target and parameter
156
+ rx(0x3B, UID + bytes([1]) + bytes(7) + PEER + bytes(16))
157
+ print('0x3B mesh : handled')
158
+
159
+ # ---- 0x29 NETWORK_STATUS: port then feature flags
160
+ # port, feature flags, then the per-pair cable records the renderer walks
161
+ rx(0x29, struct.pack('<H', 1) + bytes([0x01]) + bytes(61))
162
+ assert dev.network_status is not None, dev.network_status
163
+ print('0x29 net : %d port(s) known' % len(dev.network_status))
164
+
165
+ # ---- deliberate no-ops, decoded and run: their content is reported, not cached
166
+ f = decode(0x47, bytes([1, 2, 3, 4]))
167
+ f.process()
168
+ print('0x47 debug : decoded, handler is a no-op by design')
169
+
170
+ f = decode(0x41, UID + bytes([1]))
171
+ assert f.enable is True, f.enable
172
+ f.process()
173
+ print('0x41 psave : enable=%s, handler is a no-op by design' % f.enable)
174
+
175
+ f = decode(0x40, UID + struct.pack('<HHHH', 1, 2, 3, 4))
176
+ f.process()
177
+ print('0x40 tiling : decoded, handler is a no-op by design')
178
+
179
+ f = decode(0x45, UID + bytes(64))
180
+ f.process()
181
+ print('0x45 rcset : decoded, handler is a no-op by design')
182
+
183
+ # ---- 0x43 V2IP_AUDIO: a dispatcher over sub-opcodes. Payloads come from the
184
+ # library's own builders where it has them, so each sub-frame is reached the
185
+ # way a peer would produce it rather than from a hand-packed guess.
186
+ from mx_remote.proto.FrameV2IPAudio import (AudioCommandOpcode, AudioDeviceData,
187
+ AudioEndpointData, AudioLinkData, AudioStreamAddress, FrameV2IPAudio,
188
+ FrameV2IPAudioConfig, FrameV2IPAudioChangeSource, FrameV2IPAudioLinks,
189
+ pack_audio_links, _audio_cmd_header)
190
+
191
+ def audio_frame(body):
192
+ # send it the way the runtime receives one, then decode a copy to inspect
193
+ rx(0x43, body)
194
+ return process_mxr_frame(mx, time.time(), create_mxr_frame(UID, 0x43, body), ADDR)
195
+
196
+ devdata = AudioDeviceData(
197
+ features=0x01, status=0,
198
+ endpoints=[AudioEndpointData(id=1, features=0x03,
199
+ address=AudioStreamAddress(ip='239.9.9.1', port=50022)),
200
+ AudioEndpointData(id=2, features=0x03)],
201
+ links=[AudioLinkData(endpoint=1, link_endpoint=2, link_dev=MxrDeviceUid(PEER))])
202
+ built = FrameV2IPAudio.construct_features(mxr=mx, own_uid=MxrDeviceUid(UID), dev=devdata)
203
+ assert built is not None, 'features builder returned nothing'
204
+ g = audio_frame(built.payload)
205
+ assert isinstance(g._frame, FrameV2IPAudioConfig), type(g._frame)
206
+ assert dev.audio_endpoint_by_id(1) is not None, 'FEATURES must cache the endpoints'
207
+ assert dev.audio_endpoint_by_id(2) is not None
208
+ print('0x43 FEATURES : endpoints 1 and 2 cached on the device')
209
+
210
+ links_body = (_audio_cmd_header(AudioCommandOpcode.LINKS, MxrDeviceUid(UID))
211
+ + pack_audio_links([AudioLinkData(endpoint=1, link_endpoint=2,
212
+ link_dev=MxrDeviceUid(PEER))]))
213
+ g = audio_frame(links_body)
214
+ assert isinstance(g._frame, FrameV2IPAudioLinks), type(g._frame)
215
+ print('0x43 LINKS : dispatched to the links sub-frame and processed')
216
+
217
+ snk_ep, src_ep = dev.audio_endpoint_by_id(1), dev.audio_endpoint_by_id(2)
218
+ built = FrameV2IPAudio.construct_select_input(mxr=mx, sink=MxrDeviceUid(UID), sink_ep=snk_ep,
219
+ source=MxrDeviceUid(PEER), source_ep=src_ep)
220
+ assert built is not None, 'select_input builder returned nothing'
221
+ g = audio_frame(built.payload)
222
+ assert isinstance(g._frame, FrameV2IPAudioChangeSource), type(g._frame)
223
+ print('0x43 SELECT : dispatched to the change-source sub-frame and processed')
224
+
225
+ # ---- the V2IP switch frames need a device that has V2IP bays and known stream
226
+ # addresses, so they get their own rather than reshaping the one above.
227
+ V2 = bytes(range(80, 96))
228
+ V2FEAT = (1 << 5) | (1 << 6) | int(mx_remote.DeviceFeature.V2IP_SOURCE) | int(mx_remote.DeviceFeature.V2IP_SINK)
229
+ rx(0x00, struct.pack('<H', 0x28) + nm('OneIP') + nm('P8SN99999999') + nm('5.2.0')
230
+ + struct.pack('<I', V2FEAT), uid=V2)
231
+ v2dev = mx.get_by_uid(MxrDeviceUid(V2))
232
+ SRC_LOCAL = int(mx_remote.BayFeaturesMask.V2IP_SOURCE_LOCAL)
233
+ SINK_LOCAL = int(mx_remote.BayFeaturesMask.V2IP_SINK_LOCAL)
234
+ rx(0x02, bay_rec(0, 0, 0, 'V2 In', feat=SRC_LOCAL) + bay_rec(1, 1, 0, 'V2 Out', feat=SINK_LOCAL), uid=V2)
235
+ v2in, v2out = v2dev.get_by_portnum(0), v2dev.get_by_portnum(1)
236
+
237
+ def stream_entry(video, audio, anc):
238
+ out = V2
239
+ for ip, port in (video, audio, anc):
240
+ out += bytes(int(x) for x in ip.split('.')) + struct.pack('<H', port) + bytes(2)
241
+ return out
242
+ rx(0x26, stream_entry(('239.5.5.1', 50020), ('239.5.5.2', 50022), ('239.5.5.3', 50021)), uid=V2)
243
+ assert v2in.v2ip_source is not None, 'the source bay needs stream addresses'
244
+ assert mx.get_by_stream_ip(ip='239.5.5.1', audio=False) is not None, 'stream ip must resolve'
245
+
246
+ # ---- 0x1F V2IP_SOURCE_SWITCH: target uid, then video and audio ips big-endian
247
+ def ip4(s):
248
+ return bytes(int(x) for x in s.split('.'))
249
+ rx(0x1F, V2 + ip4('239.5.5.1') + ip4('239.5.5.2'), uid=V2)
250
+ assert v2out.video_source is not None, 'the switch must resolve a source bay'
251
+ print('0x1F switch : sink source -> %s' % v2out.video_source.user_name)
252
+
253
+ # ---- 0x24 V2IP_MANUAL_SOURCE_SWITCH, built by the library's own helper
254
+ from mx_remote.proto.FrameV2IPManualSourceSwitch import FrameV2IPManualSourceSwitch
255
+ built = FrameV2IPManualSourceSwitch.construct(
256
+ mxr=mx, target=v2dev,
257
+ video_ip='239.5.5.1', video_port=50020,
258
+ audio_ip='239.5.5.2', audio_port=50022,
259
+ anc_ip='0.0.0.0', anc_port=0, audio_fmt=None)
260
+ assert built is not None, 'manual switch builder returned nothing'
261
+ rx(0x24, built.payload, uid=V2)
262
+ print('0x24 manual : dispatched and processed')
263
+
264
+ # ---- 0x44 V2IP_BAY_MAPPING: counts, then a uid per mapped bay
265
+ rx(0x44, struct.pack('<HH', 1, 0) + bytes(4) + V2, uid=V2)
266
+ print('0x44 mapping: dispatched and processed')
267
+
268
+ print('ALL OK')
@@ -144,9 +144,14 @@ async def dying_task_is_reported():
144
144
  class Catch(logging.Handler):
145
145
  def emit(self, rec):
146
146
  records.append(rec)
147
+ handler = Catch()
147
148
  logging.disable(logging.NOTSET)
148
149
  log = logging.getLogger('mx_remote.remote.Remote')
149
- log.addHandler(Catch()); log.setLevel(logging.DEBUG)
150
+ prev_level, prev_prop = log.level, log.propagate
151
+ # do not propagate: with logging re-enabled these records would reach the root
152
+ # handler and print to stderr, in the middle of whatever suite is running
153
+ log.propagate = False
154
+ log.addHandler(handler); log.setLevel(logging.DEBUG)
150
155
  try:
151
156
  mx = mx_remote.Remote(open_connection=False)
152
157
  mx._uid = bytes(range(0x20, 0x30))
@@ -158,7 +163,11 @@ async def dying_task_is_reported():
158
163
  died = [r for r in records if r.levelno >= logging.ERROR and 'died' in r.getMessage()]
159
164
  return died
160
165
  finally:
161
- log.removeHandler(Catch)
166
+ # remove the instance, not the class: passing the class leaves the handler
167
+ # attached, and the logger keeps emitting into every suite that runs after
168
+ log.removeHandler(handler)
169
+ log.setLevel(prev_level)
170
+ log.propagate = prev_prop
162
171
  logging.disable(logging.CRITICAL)
163
172
 
164
173
  died = asyncio.run(dying_task_is_reported())
@@ -171,9 +180,14 @@ async def one_traceback_then_quiet():
171
180
  class Catch(logging.Handler):
172
181
  def emit(self, rec):
173
182
  records.append(rec)
183
+ handler = Catch()
174
184
  logging.disable(logging.NOTSET)
175
185
  log = logging.getLogger('mx_remote.remote.Remote')
176
- log.addHandler(Catch()); log.setLevel(logging.DEBUG)
186
+ prev_level, prev_prop = log.level, log.propagate
187
+ # do not propagate: with logging re-enabled these records would reach the root
188
+ # handler and print to stderr, in the middle of whatever suite is running
189
+ log.propagate = False
190
+ log.addHandler(handler); log.setLevel(logging.DEBUG)
177
191
  try:
178
192
  mx = mx_remote.Remote(open_connection=False)
179
193
  mx._uid = bytes(range(0x20, 0x30))
@@ -188,6 +202,9 @@ async def one_traceback_then_quiet():
188
202
  pass
189
203
  return [r for r in records if r.levelno >= logging.WARNING]
190
204
  finally:
205
+ log.removeHandler(handler)
206
+ log.setLevel(prev_level)
207
+ log.propagate = prev_prop
191
208
  logging.disable(logging.CRITICAL)
192
209
 
193
210
  warns = asyncio.run(one_traceback_then_quiet())
@@ -33,6 +33,7 @@ SUITES = [
33
33
  'structs', # alignment padding, u16 bays, packed records
34
34
  'roundtrip', # our builders replayed through our decoders
35
35
  'entry', # the layer the runtime enters at
36
+ 'handlers_cover', # the handlers no other suite reaches
36
37
  'handlers', # which frame handlers the suites above execute
37
38
  'txresult', # a command reports success only when its frame was sent
38
39
  'vdetails', # Bay.video_details prefers a report over the config snapshot
@@ -1,72 +0,0 @@
1
- ######################################################
2
- ## MX Remote Python Interface ##
3
- ## ##
4
- ## author: Lars Op den Kamp (lars@opdenkamp-it.nl) ##
5
- ## copyright (c) 2021-2026 Op den Kamp IT Solutions ##
6
- ######################################################
7
- '''Protocol frame for PDU (Power Distribution Unit) state reporting.'''
8
-
9
- from .FrameBase import FrameBase
10
- from .FrameHeader import FrameHeader
11
- from .PDUState import PDUState
12
- import struct
13
- from typing import Any
14
-
15
- class FramePDUState(FrameBase):
16
- '''PDU state frame, sent periodically by devices on the network.'''
17
- def __init__(self, header:FrameHeader, timestamp:float):
18
- super().__init__(header=header, timestamp=timestamp)
19
- self._state = PDUState(self)
20
-
21
- @property
22
- def state(self) -> PDUState:
23
- return self._state
24
-
25
- @property
26
- def current(self) -> float:
27
- '''Current draw in amperes.'''
28
- return round(struct.unpack('f', self.payload[0:4])[0], 2)
29
-
30
- @property
31
- def voltage(self) -> float:
32
- '''Voltage in volts.'''
33
- return round(struct.unpack('f', self.payload[4:8])[0], 2)
34
-
35
- @property
36
- def power(self) -> float:
37
- '''Power consumption in watts.'''
38
- return round(struct.unpack('f', self.payload[8:12])[0], 2)
39
-
40
- @property
41
- def dissipation(self) -> float:
42
- '''Power dissipation in watts.'''
43
- return round(struct.unpack('f', self.payload[12:16])[0], 2)
44
-
45
- #@property
46
- #def power_factor(self) -> float:
47
- # # power factor (NOT SUPPORTED)
48
- # return round(struct.unpack('f', self.payload[16:20])[0], 2)
49
-
50
- @property
51
- def frequency(self) -> float:
52
- '''AC frequency in Hz.'''
53
- return round(struct.unpack('f', self.payload[20:24])[0], 2)
54
-
55
- def outlet_state(self, outlet:int) -> int:
56
- return self.payload[24 + outlet] if outlet < 8 else 0
57
-
58
- def process(self) -> None:
59
- '''Update the local device cache with the PDU state.'''
60
- dev = self.mxr.get(self.remote_id)
61
- if dev is not None:
62
- dev.on_mxr_update_pdu(self.state)
63
-
64
- def __eq__(self, other:Any) -> bool:
65
- if not isinstance(other, FramePDUState):
66
- return False
67
- return (self.payload[0:16].append(self.payload[24:32])) == \
68
- (other.payload[0:16].append(other.payload[24:32]))
69
-
70
- def __ne__(self, other:Any) -> bool:
71
- return not self.__eq__(other)
72
-
@@ -1,23 +0,0 @@
1
- from .FrameBase import FrameBase as FrameBase
2
- from .FrameHeader import FrameHeader as FrameHeader
3
- from .PDUState import PDUState as PDUState
4
- from typing import Any
5
-
6
- class FramePDUState(FrameBase):
7
- def __init__(self, header: FrameHeader, timestamp: float) -> None: ...
8
- @property
9
- def state(self) -> PDUState: ...
10
- @property
11
- def current(self) -> float: ...
12
- @property
13
- def voltage(self) -> float: ...
14
- @property
15
- def power(self) -> float: ...
16
- @property
17
- def dissipation(self) -> float: ...
18
- @property
19
- def frequency(self) -> float: ...
20
- def outlet_state(self, outlet: int) -> int: ...
21
- def process(self) -> None: ...
22
- def __eq__(self, other: Any) -> bool: ...
23
- def __ne__(self, other: Any) -> bool: ...
@@ -1,60 +0,0 @@
1
- ######################################################
2
- ## MX Remote Python Interface ##
3
- ## ##
4
- ## author: Lars Op den Kamp (lars@opdenkamp-it.nl) ##
5
- ## copyright (c) 2021-2026 Op den Kamp IT Solutions ##
6
- ######################################################
7
- '''Protocol frame for V2IP stream detail information (video, audio, ancillary, ARC).'''
8
-
9
- from functools import cached_property
10
- from .FrameBase import FrameBase
11
- from ..Interface import V2IPStreamSource, V2IPStreamSources
12
- from .V2IPConfig import V2IPStreamSourceImpl, V2IPStreamSourcesImpl
13
-
14
- class FrameV2IPStreamDetails(FrameBase):
15
- '''All configured V2IP stream sources for the device that sent this frame.'''
16
- @cached_property
17
- def video(self) -> V2IPStreamSource|None:
18
- '''Video stream source address.'''
19
- pl = self.payload_idx(start=0, end=6)
20
- if (pl is None):
21
- return None
22
- return V2IPStreamSourceImpl("video", pl)
23
-
24
- @cached_property
25
- def audio(self) -> V2IPStreamSource|None:
26
- '''Audio stream source address.'''
27
- pl = self.payload_idx(start=8, end=14)
28
- if (pl is None):
29
- return None
30
- return V2IPStreamSourceImpl("audio", pl)
31
-
32
- @cached_property
33
- def anc(self) -> V2IPStreamSource|None:
34
- '''Ancillary data stream source address.'''
35
- pl = self.payload_idx(start=16, end=22)
36
- if (pl is None):
37
- return None
38
- return V2IPStreamSourceImpl("anc", pl)
39
-
40
- @cached_property
41
- def arc(self) -> V2IPStreamSource|None:
42
- '''ARC (Audio Return Channel) stream source address.'''
43
- pl = self.payload_idx(start=24, end=30)
44
- if (pl is None):
45
- return None
46
- return V2IPStreamSourceImpl("arc", pl)
47
-
48
- @cached_property
49
- def sources(self) -> V2IPStreamSources|None:
50
- '''Combined stream sources, or None if required fields are missing.'''
51
- if ((video := self.video) is not None) and ((audio := self.audio) is not None) and ((anc := self.anc) is not None):
52
- return V2IPStreamSourcesImpl(video=video, audio=audio, anc=anc, arc=self.arc)
53
-
54
- def process(self) -> None:
55
- '''Update the local device cache with V2IP stream source details.'''
56
- if ((dev := self.remote_device) is not None):
57
- dev.on_mxr_update(self.sources)
58
-
59
- def __str__(self) -> str:
60
- return f"{str(self.remote_device)} v2ip stream details: {self.video} {self.audio} {self.anc} {self.arc}"
@@ -1,17 +0,0 @@
1
- from ..Interface import V2IPStreamSource as V2IPStreamSource, V2IPStreamSources as V2IPStreamSources
2
- from .FrameBase import FrameBase as FrameBase
3
- from .V2IPConfig import V2IPStreamSourceImpl as V2IPStreamSourceImpl, V2IPStreamSourcesImpl as V2IPStreamSourcesImpl
4
- from functools import cached_property
5
-
6
- class FrameV2IPStreamDetails(FrameBase):
7
- @cached_property
8
- def video(self) -> V2IPStreamSource | None: ...
9
- @cached_property
10
- def audio(self) -> V2IPStreamSource | None: ...
11
- @cached_property
12
- def anc(self) -> V2IPStreamSource | None: ...
13
- @cached_property
14
- def arc(self) -> V2IPStreamSource | None: ...
15
- @cached_property
16
- def sources(self) -> V2IPStreamSources | None: ...
17
- def process(self) -> None: ...