mx-remote 9.2.0__tar.gz → 9.4.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 (274) hide show
  1. {mx_remote-9.2.0 → mx_remote-9.4.0}/PKG-INFO +1 -1
  2. {mx_remote-9.2.0 → mx_remote-9.4.0}/docs/api.md +17 -0
  3. {mx_remote-9.2.0 → mx_remote-9.4.0}/docs/callbacks.md +25 -0
  4. {mx_remote-9.2.0 → mx_remote-9.4.0}/docs/oneip.md +14 -0
  5. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/Interface.py +90 -0
  6. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/Interface.pyi +27 -0
  7. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/const.py +1 -1
  8. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/Constants.py +30 -0
  9. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/Constants.pyi +10 -0
  10. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/Factory.py +3 -0
  11. mx_remote-9.4.0/mx_remote/proto/FrameThirdParty.py +50 -0
  12. mx_remote-9.4.0/mx_remote/proto/FrameThirdParty.pyi +15 -0
  13. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPSinkSdp.py +56 -4
  14. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPSinkSdp.pyi +15 -0
  15. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/remote/Device.py +102 -4
  16. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/remote/Device.pyi +8 -2
  17. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/remote/Remote.py +20 -2
  18. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/remote/Remote.pyi +4 -2
  19. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/handlers.py +1 -0
  20. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/run.py +1 -0
  21. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/smoke.py +1 -0
  22. mx_remote-9.4.0/tests/thirdparty.py +146 -0
  23. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/unit.py +30 -0
  24. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/v2ipsdp.py +80 -2
  25. {mx_remote-9.2.0 → mx_remote-9.4.0}/.gitignore +0 -0
  26. {mx_remote-9.2.0 → mx_remote-9.4.0}/AGENTS.md +0 -0
  27. {mx_remote-9.2.0 → mx_remote-9.4.0}/CLAUDE.md +0 -0
  28. {mx_remote-9.2.0 → mx_remote-9.4.0}/LICENSE +0 -0
  29. {mx_remote-9.2.0 → mx_remote-9.4.0}/README.md +0 -0
  30. {mx_remote-9.2.0 → mx_remote-9.4.0}/docs/README.md +0 -0
  31. {mx_remote-9.2.0 → mx_remote-9.4.0}/docs/_static/brand.css +0 -0
  32. {mx_remote-9.2.0 → mx_remote-9.4.0}/docs/_static/favicon.svg +0 -0
  33. {mx_remote-9.2.0 → mx_remote-9.4.0}/docs/_static/logo-dark.svg +0 -0
  34. {mx_remote-9.2.0 → mx_remote-9.4.0}/docs/_static/logo.svg +0 -0
  35. {mx_remote-9.2.0 → mx_remote-9.4.0}/docs/audio.md +0 -0
  36. {mx_remote-9.2.0 → mx_remote-9.4.0}/docs/conf.py +0 -0
  37. {mx_remote-9.2.0 → mx_remote-9.4.0}/docs/configuration.md +0 -0
  38. {mx_remote-9.2.0 → mx_remote-9.4.0}/docs/devices-and-bays.md +0 -0
  39. {mx_remote-9.2.0 → mx_remote-9.4.0}/docs/diagnostics.md +0 -0
  40. {mx_remote-9.2.0 → mx_remote-9.4.0}/docs/index.md +0 -0
  41. {mx_remote-9.2.0 → mx_remote-9.4.0}/docs/multiviewer.md +0 -0
  42. {mx_remote-9.2.0 → mx_remote-9.4.0}/docs/requirements.txt +0 -0
  43. {mx_remote-9.2.0 → mx_remote-9.4.0}/docs/routing.md +0 -0
  44. {mx_remote-9.2.0 → mx_remote-9.4.0}/hatch_build.py +0 -0
  45. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/Uid.py +0 -0
  46. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/Uid.pyi +0 -0
  47. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/__init__.py +0 -0
  48. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/__init__.pyi +0 -0
  49. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/compat.py +0 -0
  50. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/compat.pyi +0 -0
  51. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/const.pyi +0 -0
  52. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/deprecated.py +0 -0
  53. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/deprecated.pyi +0 -0
  54. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/main.py +0 -0
  55. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/main.pyi +0 -0
  56. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/BayConfig.py +0 -0
  57. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/BayConfig.pyi +0 -0
  58. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/Data.py +0 -0
  59. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/Data.pyi +0 -0
  60. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/Factory.pyi +0 -0
  61. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameAmpDolbySettings.py +0 -0
  62. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameAmpDolbySettings.pyi +0 -0
  63. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameAmpZoneSettings.py +0 -0
  64. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameAmpZoneSettings.pyi +0 -0
  65. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameAudioClip.py +0 -0
  66. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameAudioClip.pyi +0 -0
  67. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameAudioSetRoute.py +0 -0
  68. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameAudioSetRoute.pyi +0 -0
  69. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameBase.py +0 -0
  70. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameBase.pyi +0 -0
  71. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameBayConfig.py +0 -0
  72. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameBayConfig.pyi +0 -0
  73. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameBayConfigSecondary.py +0 -0
  74. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameBayConfigSecondary.pyi +0 -0
  75. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameBayContent.py +0 -0
  76. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameBayContent.pyi +0 -0
  77. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameBayHide.py +0 -0
  78. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameBayHide.pyi +0 -0
  79. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameBayStatus.py +0 -0
  80. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameBayStatus.pyi +0 -0
  81. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameConnectStatus.py +0 -0
  82. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameConnectStatus.pyi +0 -0
  83. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameDebug.py +0 -0
  84. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameDebug.pyi +0 -0
  85. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameDiscover.py +0 -0
  86. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameDiscover.pyi +0 -0
  87. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameEDID.py +0 -0
  88. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameEDID.pyi +0 -0
  89. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameEDIDProfile.py +0 -0
  90. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameEDIDProfile.pyi +0 -0
  91. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameFactoryReset.py +0 -0
  92. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameFactoryReset.pyi +0 -0
  93. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameFilterStatus.py +0 -0
  94. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameFilterStatus.pyi +0 -0
  95. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameFirmwareVersion.py +0 -0
  96. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameFirmwareVersion.pyi +0 -0
  97. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameHeader.py +0 -0
  98. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameHeader.pyi +0 -0
  99. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameHello.py +0 -0
  100. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameHello.pyi +0 -0
  101. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameLinks.py +0 -0
  102. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameLinks.pyi +0 -0
  103. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameMeshOperation.py +0 -0
  104. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameMeshOperation.pyi +0 -0
  105. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameMirrorStatus.py +0 -0
  106. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameMirrorStatus.pyi +0 -0
  107. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameMonitoringPulse.py +0 -0
  108. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameMonitoringPulse.pyi +0 -0
  109. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameNetworkStatus.py +0 -0
  110. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameNetworkStatus.pyi +0 -0
  111. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FramePing.py +0 -0
  112. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FramePing.pyi +0 -0
  113. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FramePowerChange.py +0 -0
  114. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FramePowerChange.pyi +0 -0
  115. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameRCAction.py +0 -0
  116. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameRCAction.pyi +0 -0
  117. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameRCIr.py +0 -0
  118. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameRCIr.pyi +0 -0
  119. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameRCKey.py +0 -0
  120. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameRCKey.pyi +0 -0
  121. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameRCSettings.py +0 -0
  122. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameRCSettings.pyi +0 -0
  123. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameReboot.py +0 -0
  124. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameReboot.pyi +0 -0
  125. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameRoutingChange.py +0 -0
  126. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameRoutingChange.pyi +0 -0
  127. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSetInstaller.py +0 -0
  128. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSetInstaller.pyi +0 -0
  129. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSetName.py +0 -0
  130. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSetName.pyi +0 -0
  131. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSetRoute.py +0 -0
  132. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSetRoute.pyi +0 -0
  133. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSetupStatus.py +0 -0
  134. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSetupStatus.pyi +0 -0
  135. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSignalStatus.py +0 -0
  136. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSignalStatus.pyi +0 -0
  137. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSignalStatusNew.py +0 -0
  138. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSignalStatusNew.pyi +0 -0
  139. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSysTemperature.py +0 -0
  140. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSysTemperature.pyi +0 -0
  141. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSystemStatus.py +0 -0
  142. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameSystemStatus.pyi +0 -0
  143. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameTXRCAction.py +0 -0
  144. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameTXRCAction.pyi +0 -0
  145. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameTXRCKey.py +0 -0
  146. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameTXRCKey.pyi +0 -0
  147. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameTime.py +0 -0
  148. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameTime.pyi +0 -0
  149. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameTimeZone.py +0 -0
  150. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameTimeZone.pyi +0 -0
  151. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameTopology.py +0 -0
  152. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameTopology.pyi +0 -0
  153. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameTxIR.py +0 -0
  154. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameTxIR.pyi +0 -0
  155. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameUnitCommand.py +0 -0
  156. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameUnitCommand.pyi +0 -0
  157. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameUnitStatus.py +0 -0
  158. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameUnitStatus.pyi +0 -0
  159. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameUpgradeFPGA.py +0 -0
  160. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameUpgradeFPGA.pyi +0 -0
  161. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPAudio.py +0 -0
  162. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPAudio.pyi +0 -0
  163. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPBayMapping.py +0 -0
  164. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPBayMapping.pyi +0 -0
  165. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPDetectBays.py +0 -0
  166. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPDetectBays.pyi +0 -0
  167. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.py +0 -0
  168. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.pyi +0 -0
  169. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPLink.py +0 -0
  170. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPLink.pyi +0 -0
  171. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.py +0 -0
  172. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.pyi +0 -0
  173. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPMultiviewer.py +0 -0
  174. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPMultiviewer.pyi +0 -0
  175. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPOsdSync.py +0 -0
  176. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPOsdSync.pyi +0 -0
  177. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPPowerSave.py +0 -0
  178. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPPowerSave.pyi +0 -0
  179. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPPrivacy.py +0 -0
  180. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPPrivacy.pyi +0 -0
  181. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPSetMaster.py +0 -0
  182. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPSetMaster.pyi +0 -0
  183. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPSettingsAll.py +0 -0
  184. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPSettingsAll.pyi +0 -0
  185. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPSourceSwitch.py +0 -0
  186. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPSourceSwitch.pyi +0 -0
  187. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPSources.py +0 -0
  188. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPSources.pyi +0 -0
  189. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPStats.py +0 -0
  190. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPStats.pyi +0 -0
  191. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPTestcard.py +0 -0
  192. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPTestcard.pyi +0 -0
  193. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPTiling.py +0 -0
  194. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPTiling.pyi +0 -0
  195. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPVideoWall.py +0 -0
  196. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameV2IPVideoWall.pyi +0 -0
  197. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameVolume.py +0 -0
  198. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameVolume.pyi +0 -0
  199. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameVolumeDown.py +0 -0
  200. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameVolumeDown.pyi +0 -0
  201. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameVolumeSet.py +0 -0
  202. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameVolumeSet.pyi +0 -0
  203. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameVolumeUp.py +0 -0
  204. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/FrameVolumeUp.pyi +0 -0
  205. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/LinkConfig.py +0 -0
  206. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/LinkConfig.pyi +0 -0
  207. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/Multiviewer.py +0 -0
  208. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/Multiviewer.pyi +0 -0
  209. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/Svd.py +0 -0
  210. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/Svd.pyi +0 -0
  211. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/V2IPConfig.py +0 -0
  212. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/V2IPConfig.pyi +0 -0
  213. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/V2IPStats.py +0 -0
  214. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/V2IPStats.pyi +0 -0
  215. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/__init__.py +0 -0
  216. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/__init__.pyi +0 -0
  217. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/proto/svd.csv +0 -0
  218. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/py.typed +0 -0
  219. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/remote/Bay.py +0 -0
  220. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/remote/Bay.pyi +0 -0
  221. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/remote/ConnectionAsync.py +0 -0
  222. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/remote/ConnectionAsync.pyi +0 -0
  223. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/remote/Link.py +0 -0
  224. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/remote/Link.pyi +0 -0
  225. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/remote/OsdSync.py +0 -0
  226. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/remote/OsdSync.pyi +0 -0
  227. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/remote/State.py +0 -0
  228. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/remote/State.pyi +0 -0
  229. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/remote/V2IP.py +0 -0
  230. {mx_remote-9.2.0 → mx_remote-9.4.0}/mx_remote/remote/V2IP.pyi +0 -0
  231. {mx_remote-9.2.0 → mx_remote-9.4.0}/pyproject.toml +0 -0
  232. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/audio_ir.py +0 -0
  233. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/audiolock.py +0 -0
  234. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/baycontent.py +0 -0
  235. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/capture.py +0 -0
  236. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/complete.py +0 -0
  237. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/deprecated.py +0 -0
  238. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/downmix.py +0 -0
  239. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/e2e.py +0 -0
  240. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/entry.py +0 -0
  241. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/handlers_cover.py +0 -0
  242. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/hello.py +0 -0
  243. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/idempotent.py +0 -0
  244. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/lengths.py +0 -0
  245. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/merge.py +0 -0
  246. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/mesh.py +0 -0
  247. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/meshbays.py +0 -0
  248. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/newops.py +0 -0
  249. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/offline.py +0 -0
  250. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/offsets.py +0 -0
  251. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/osdmessage.py +0 -0
  252. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/osdsync.py +0 -0
  253. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/ping.py +0 -0
  254. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/privacy.py +0 -0
  255. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/proto_stamp.py +0 -0
  256. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/protogate.py +0 -0
  257. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/rcsettings.py +0 -0
  258. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/roundtrip.py +0 -0
  259. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/routes.py +0 -0
  260. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/sources.py +0 -0
  261. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/staterequest.py +0 -0
  262. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/stats.py +0 -0
  263. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/structs.py +0 -0
  264. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/txresult.py +0 -0
  265. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/uid.py +0 -0
  266. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/unknowns.py +0 -0
  267. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/v2ipptp.py +0 -0
  268. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/v2ipscaling.py +0 -0
  269. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/v2ipsettings.py +0 -0
  270. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/v2ipsubject.py +0 -0
  271. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/v2iptestcard.py +0 -0
  272. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/v2ipvlan.py +0 -0
  273. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/vdetails.py +0 -0
  274. {mx_remote-9.2.0 → mx_remote-9.4.0}/tests/wirefix.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mx_remote
3
- Version: 9.2.0
3
+ Version: 9.4.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://opdenkamp.github.io/mx-remote/
@@ -288,6 +288,23 @@ How a source unit encrypts its streams, as it reports it.
288
288
  :members:
289
289
  ```
290
290
 
291
+ ## Application messages
292
+
293
+ Messages applications exchange over the mesh under their own vendor id.
294
+
295
+ ```{eval-rst}
296
+ .. autoclass:: mx_remote.ThirdPartyMessage
297
+ :members:
298
+
299
+ .. autoclass:: mx_remote.ThirdPartyVendor
300
+ :members:
301
+
302
+ .. autoclass:: mx_remote.proto.Constants.ThirdPartyVendorFlag
303
+ :members:
304
+
305
+ .. autodata:: mx_remote.proto.Constants.THIRD_PARTY_PAYLOAD_MAX
306
+ ```
307
+
291
308
  ## OneIP test card
292
309
 
293
310
  A sink's test pattern, tone and lip-sync flash, and the ranges it accepts.
@@ -67,6 +67,7 @@ Available callback methods:
67
67
  | `on_rc_type_changed` | remote control type changed |
68
68
  | `on_amp_zone_settings_changed` | amplifier zone settings changed |
69
69
  | `on_amp_dolby_settings_changed` | amplifier Dolby settings changed |
70
+ | `on_third_party_message` | another application sent a message under its vendor id |
70
71
 
71
72
  You can also register per-device and per-bay callbacks:
72
73
 
@@ -85,6 +86,30 @@ device.unregister_callback(on_device_changed)
85
86
  bay.unregister_callback(on_bay_changed)
86
87
  ```
87
88
 
89
+ ## Application messages
90
+
91
+ Applications built on this library can message each other over the mesh. A
92
+ message carries a vendor id, then up to `THIRD_PARTY_PAYLOAD_MAX` (1444) bytes
93
+ that only the application reads. The vendor id is the vendor's IEEE OUI, or an
94
+ identifier it chose, such as three letters:
95
+
96
+ ```python
97
+ from mx_remote import ThirdPartyVendor
98
+
99
+ ODK = ThirdPartyVendor.private(b'ODK')
100
+
101
+ await mx.send_third_party(ODK, b'hello') # False when over 1444 bytes
102
+
103
+ class MyCallbacks(mx_remote.MxrCallbacks):
104
+ def on_third_party_message(self, dev, message):
105
+ if message.vendor.same_vendor(ODK):
106
+ print(f"{dev} says {message.payload!r}")
107
+ ```
108
+
109
+ Every other client that has heard this one announce itself receives the
110
+ message; this client does not. No unit reads it, and nothing acknowledges it,
111
+ so an application that needs to know a message arrived has its peers answer.
112
+
88
113
  ---
89
114
 
90
115
  [Documentation index](README.md) | [Project README](https://github.com/opdenkamp/mx-remote#readme)
@@ -432,6 +432,14 @@ application, which this client announces itself as, and only from protocol
432
432
  after its last page, so one is refused before it is sent. Nothing acknowledges
433
433
  the pages; `oneip_sink_sdp` reads what the output reports next.
434
434
 
435
+ From protocol 0x31 an output also reports the description it plays, when it
436
+ starts a stream other than by its mesh controller's command and when asked:
437
+
438
+ ```python
439
+ await sink.request_oneip_sink_description() # False below protocol 0x31
440
+ print(sink.oneip_sink_description) # None until reported, and once it plays none
441
+ ```
442
+
435
443
  ### Overlay sync
436
444
 
437
445
  A management unit can have the other units copy its overlay captions, logo
@@ -553,8 +561,14 @@ await device.set_unit_network(UnitNetConfig(
553
561
  ip='10.1.2.3', netmask='255.255.255.0', gateway='10.1.2.254', dns1='10.1.2.254'))
554
562
  await device.confirm_unit_network() # keep it, once the unit reports it
555
563
  await device.set_unit_dhcp()
564
+ await device.locate_unit(timed=True) # flash its LED for 30 s; timed=False until stopped
565
+ await device.stop_locating_unit()
556
566
  ```
557
567
 
568
+ A unit is located only once its status carries `UnitFlag.LOCATE_SUPPORTED`. Its
569
+ board-ID LED flashes and an output shows "Locate Device"; `UnitFlag.LOCATE` in
570
+ the cached status says so from the moment the command is sent.
571
+
558
572
  A changed IP configuration applies at once and reverts unless the mesh
559
573
  controller confirms it in time; `status.network_pending` says whether it still
560
574
  waits. Each command returns False without sending for a device that is not a
@@ -2601,6 +2601,68 @@ class OneIPPrivacy:
2601
2601
  def __str__(self) -> str:
2602
2602
  return f"video {self.video}, audio {self.audio}, anc {self.anc}"
2603
2603
 
2604
+ @dataclass(frozen=True)
2605
+ class ThirdPartyVendor:
2606
+ """
2607
+ Whose message a third-party message is: a flag byte, then three bytes
2608
+ naming the vendor.
2609
+
2610
+ The three bytes are the vendor's IEEE OUI, in the order it is written, or
2611
+ with ThirdPartyVendorFlag.NOT_OUI set an identifier of the vendor's own
2612
+ choosing, such as three ASCII letters. The flag keeps the two spaces apart,
2613
+ so a chosen identifier never collides with a registered OUI.
2614
+ """
2615
+ flags: ThirdPartyVendorFlag
2616
+ """The flag byte, at offset 0."""
2617
+ id: bytes
2618
+ """The vendor's three identifying bytes, at offsets 1 to 3."""
2619
+
2620
+ def __post_init__(self) -> None:
2621
+ if (len(self.id) != 3):
2622
+ raise ValueError(f"a vendor id is 3 bytes, not {len(self.id)}")
2623
+
2624
+ @classmethod
2625
+ def oui(cls, oui:bytes) -> 'ThirdPartyVendor':
2626
+ """A vendor named by its IEEE OUI."""
2627
+ return cls(flags=ThirdPartyVendorFlag(0), id=bytes(oui))
2628
+
2629
+ @classmethod
2630
+ def private(cls, id:bytes) -> 'ThirdPartyVendor':
2631
+ """A vendor named by an identifier it chose, rather than an OUI."""
2632
+ return cls(flags=ThirdPartyVendorFlag.NOT_OUI, id=bytes(id))
2633
+
2634
+ def same_vendor(self, other:'ThirdPartyVendor') -> bool:
2635
+ """Whether other names the same vendor.
2636
+
2637
+ Only ThirdPartyVendorFlag.NOT_OUI and the three identifying bytes say
2638
+ who the vendor is, so the other flag bits are left out."""
2639
+ return ((ThirdPartyVendorFlag.NOT_OUI in self.flags) == (ThirdPartyVendorFlag.NOT_OUI in other.flags)) \
2640
+ and (self.id == other.id)
2641
+
2642
+ def __str__(self) -> str:
2643
+ if (ThirdPartyVendorFlag.NOT_OUI in self.flags):
2644
+ return f"vendor {self.id!r}"
2645
+ return f"vendor OUI {self.id.hex(':')}"
2646
+
2647
+ @dataclass(frozen=True)
2648
+ class ThirdPartyMessage:
2649
+ """
2650
+ A message an application sent to every device, under its own vendor id.
2651
+
2652
+ The library reads only the vendor id. What follows is the application's,
2653
+ and arrives exactly as it was sent.
2654
+ """
2655
+ vendor: ThirdPartyVendor
2656
+ """Which application's message this is. The library checks none: each
2657
+ application ignores every message whose vendor is not its own."""
2658
+ payload: bytes
2659
+ """The application's bytes, at most THIRD_PARTY_PAYLOAD_MAX."""
2660
+ address: str|None
2661
+ """Where the datagram came from."""
2662
+
2663
+ def __str__(self) -> str:
2664
+ return f"{self.vendor}: {len(self.payload)} bytes"
2665
+
2604
2666
  @dataclass(frozen=True)
2605
2667
  class OneIPTestTone:
2606
2668
  """A OneIP sink's test tone."""
@@ -3293,6 +3355,15 @@ class DeviceBase(ABC):
3293
3355
  async def confirm_unit_network(self) -> bool:
3294
3356
  '''keep the unit's pending IP configuration'''
3295
3357
 
3358
+ @abstractmethod
3359
+ async def locate_unit(self, timed:bool=False) -> bool:
3360
+ '''flash the unit's board-ID LED and show "Locate Device" on its output,
3361
+ for 30 s when timed is set, until stop_locating_unit() otherwise'''
3362
+
3363
+ @abstractmethod
3364
+ async def stop_locating_unit(self) -> bool:
3365
+ '''stop locating the unit'''
3366
+
3296
3367
  @property
3297
3368
  @abstractmethod
3298
3369
  def oneip_downmix(self) -> OneIPDownmix|None:
@@ -3678,6 +3749,18 @@ class DeviceBase(ABC):
3678
3749
  async def request_oneip_sink_sdp(self) -> bool:
3679
3750
  '''ask this OneIP output what it plays from a session description now'''
3680
3751
 
3752
+ @property
3753
+ @abstractmethod
3754
+ def oneip_sink_description(self) -> str|None:
3755
+ '''The session description this OneIP output last reported playing, None
3756
+ until it has and once it reports playing none. An output reports one
3757
+ when it starts a stream other than by its mesh controller's command,
3758
+ and when asked with request_oneip_sink_description().'''
3759
+
3760
+ @abstractmethod
3761
+ async def request_oneip_sink_description(self) -> bool:
3762
+ '''ask this OneIP output for the session description it plays'''
3763
+
3681
3764
  @abstractmethod
3682
3765
  async def set_oneip_test_pattern(self, pattern:OneIPTestPattern, colour:int=0) -> bool:
3683
3766
  '''show a test pattern on this sink's output'''
@@ -4370,6 +4453,13 @@ class MxrCallbacks:
4370
4453
  _LOGGER.debug(f"{device} dolby settings changed")
4371
4454
  self.on_device_update(device)
4372
4455
 
4456
+ def on_third_party_message(self, dev:DeviceBase, message:ThirdPartyMessage) -> None:
4457
+ ''' called when the application on 'dev' sent a message under its own
4458
+ vendor id. A message from a sender this client has not yet heard
4459
+ announce itself is dropped, as every frame is, and this client never
4460
+ receives its own. '''
4461
+ pass
4462
+
4373
4463
  # The V2IP spelling of a name here, from before the rename to OneIP.
4374
4464
  from .deprecated import alias_members as _alias_members, module_getattr as _module_getattr
4375
4465
  # Installed through globals() so the generated stubs carry no __getattr__, which
@@ -930,6 +930,23 @@ class OneIPPrivacy:
930
930
  audio: OneIPPrivacyStream
931
931
  anc: OneIPPrivacyStream
932
932
 
933
+ @dataclass(frozen=True)
934
+ class ThirdPartyVendor:
935
+ flags: ThirdPartyVendorFlag
936
+ id: bytes
937
+ def __post_init__(self) -> None: ...
938
+ @classmethod
939
+ def oui(cls, oui: bytes) -> ThirdPartyVendor: ...
940
+ @classmethod
941
+ def private(cls, id: bytes) -> ThirdPartyVendor: ...
942
+ def same_vendor(self, other: ThirdPartyVendor) -> bool: ...
943
+
944
+ @dataclass(frozen=True)
945
+ class ThirdPartyMessage:
946
+ vendor: ThirdPartyVendor
947
+ payload: bytes
948
+ address: str | None
949
+
933
950
  @dataclass(frozen=True)
934
951
  class OneIPTestTone:
935
952
  mode: OneIPToneMode | int = ...
@@ -1193,6 +1210,10 @@ class DeviceBase(ABC):
1193
1210
  async def set_unit_network(self, network: UnitNetConfig) -> bool: ...
1194
1211
  @abstractmethod
1195
1212
  async def confirm_unit_network(self) -> bool: ...
1213
+ @abstractmethod
1214
+ async def locate_unit(self, timed: bool = False) -> bool: ...
1215
+ @abstractmethod
1216
+ async def stop_locating_unit(self) -> bool: ...
1196
1217
  @property
1197
1218
  @abstractmethod
1198
1219
  def oneip_downmix(self) -> OneIPDownmix | None: ...
@@ -1376,6 +1397,11 @@ class DeviceBase(ABC):
1376
1397
  async def set_oneip_sdp_screen(self, screen: int) -> bool: ...
1377
1398
  @abstractmethod
1378
1399
  async def request_oneip_sink_sdp(self) -> bool: ...
1400
+ @property
1401
+ @abstractmethod
1402
+ def oneip_sink_description(self) -> str | None: ...
1403
+ @abstractmethod
1404
+ async def request_oneip_sink_description(self) -> bool: ...
1379
1405
  @abstractmethod
1380
1406
  async def set_oneip_test_pattern(self, pattern: OneIPTestPattern, colour: int = 0) -> bool: ...
1381
1407
  @abstractmethod
@@ -1620,3 +1646,4 @@ class MxrCallbacks:
1620
1646
  def on_rc_type_changed(self, bay: BayBase, rc_type: RCType | None) -> None: ...
1621
1647
  def on_amp_zone_settings_changed(self, bay: BayBase, settings: AmpZoneSettings) -> None: ...
1622
1648
  def on_amp_dolby_settings_changed(self, device: DeviceBase, settings: AmpDolbySettings) -> None: ...
1649
+ def on_third_party_message(self, dev: DeviceBase, message: ThirdPartyMessage) -> None: ...
@@ -14,7 +14,7 @@ Default communication modes:
14
14
  """
15
15
  import os
16
16
 
17
- VERSION = '9.2.0'
17
+ VERSION = '9.4.0'
18
18
  __version__ = VERSION
19
19
 
20
20
  MX_BCAST_UDP_PORT = 8811
@@ -123,6 +123,9 @@ MXR_OPCODE_VERSIONS: dict[int, int] = {
123
123
  0x52: 0x2E, # V2IP_SINK_SDP
124
124
  0x53: 0x2F, # V2IP_OSD_SYNC
125
125
  0x54: 0x30, # V2IP_PRIVACY
126
+ # The lowest stamp, so that every receiver's ceiling passes it. No device
127
+ # decodes the payload, so there is no layout for a stamp to announce.
128
+ 0x55: 0x01, # THIRD_PARTY
126
129
  }
127
130
  """Per-opcode minimum compatible protocol version, mirroring the firmware's
128
131
  own opcode table. Transmitters stamp the
@@ -681,6 +684,11 @@ class UnitFlag(IntFlag):
681
684
  '''The boot count is reported.'''
682
685
  FAN_SPEED = (1 << 15)
683
686
  '''The fan speed is reported.'''
687
+ LOCATE_SUPPORTED = (1 << 16)
688
+ '''The unit takes UnitCommandKind.LOCATE.'''
689
+ LOCATE = (1 << 17)
690
+ '''The unit is being located: its board-ID LED flashes, and an output shows
691
+ "Locate Device".'''
684
692
 
685
693
  class UnitCommandKind(IntEnum):
686
694
  '''What a unit command asks of its unit.'''
@@ -692,6 +700,8 @@ class UnitCommandKind(IntEnum):
692
700
  '''Change the IP configuration.'''
693
701
  NET_CONFIRM = 3
694
702
  '''Keep a pending IP configuration.'''
703
+ LOCATE = 4
704
+ '''Start or stop locating the unit.'''
695
705
 
696
706
  class UnitCommandFlag(IntFlag):
697
707
  '''The flags of a unit command.'''
@@ -700,6 +710,11 @@ class UnitCommandFlag(IntFlag):
700
710
  already.'''
701
711
  DHCP = (1 << 1)
702
712
  '''With NET_CONFIG: use DHCP, ignoring the addresses.'''
713
+ LOCATE_ON = (1 << 2)
714
+ '''With LOCATE: locate it until stopped. Neither this nor LOCATE_TIMED
715
+ stops it.'''
716
+ LOCATE_TIMED = (1 << 3)
717
+ '''With LOCATE: locate it for 30 s.'''
703
718
 
704
719
  UNIT_MAX_TEMPERATURES = 6
705
720
  """The most temperature sensors a unit reports."""
@@ -822,6 +837,21 @@ ONEIP_VLAN_PORT_SFP = 0
822
837
  ONEIP_VLAN_ID_MAX = 4094
823
838
  """The highest VLAN id: 0 means untagged, and 4095 is reserved by IEEE 802.1Q."""
824
839
 
840
+ class ThirdPartyVendorFlag(IntFlag):
841
+ '''The first byte of a third-party vendor id, ahead of its three
842
+ identifying bytes. Bits without a name are kept as they arrived.'''
843
+ NOT_OUI = (1 << 0)
844
+ '''The three bytes are an identifier the vendor chose, rather than its
845
+ IEEE OUI.'''
846
+
847
+ THIRD_PARTY_PAYLOAD_MAX = 1444
848
+ """The largest payload a third-party message carries.
849
+
850
+ A frame this size fills one 1500-byte Ethernet frame exactly: 20 bytes of IPv4
851
+ header, 8 of UDP, the 24-byte frame header and the 4-byte vendor id leave 1444.
852
+ A larger send is refused rather than fragmented, because a lost fragment loses
853
+ the whole datagram and nothing on these paths retries."""
854
+
825
855
  BAY_FEATURE_DOLBY_IN_POS = 24
826
856
 
827
857
  class BayFeaturesMask(IntFlag):
@@ -271,16 +271,21 @@ class UnitFlag(IntFlag):
271
271
  POWER_CHECK: Incomplete
272
272
  BOOT_COUNT: Incomplete
273
273
  FAN_SPEED: Incomplete
274
+ LOCATE_SUPPORTED: Incomplete
275
+ LOCATE: Incomplete
274
276
 
275
277
  class UnitCommandKind(IntEnum):
276
278
  CLEAR_STATUS: int
277
279
  VIDEO_PROCESSOR_UPGRADE: int
278
280
  NET_CONFIG: int
279
281
  NET_CONFIRM: int
282
+ LOCATE: int
280
283
 
281
284
  class UnitCommandFlag(IntFlag):
282
285
  FORCE: Incomplete
283
286
  DHCP: Incomplete
287
+ LOCATE_ON: Incomplete
288
+ LOCATE_TIMED: Incomplete
284
289
 
285
290
  UNIT_MAX_TEMPERATURES: int
286
291
 
@@ -340,6 +345,11 @@ class OneIPVlanFlag(IntFlag):
340
345
  ONEIP_VLAN_PORTS: int
341
346
  ONEIP_VLAN_PORT_SFP: int
342
347
  ONEIP_VLAN_ID_MAX: int
348
+
349
+ class ThirdPartyVendorFlag(IntFlag):
350
+ NOT_OUI: Incomplete
351
+
352
+ THIRD_PARTY_PAYLOAD_MAX: int
343
353
  BAY_FEATURE_DOLBY_IN_POS: int
344
354
 
345
355
  class BayFeaturesMask(IntFlag):
@@ -249,6 +249,9 @@ def _mxr_frame_factory(hdr:FrameHeader, timestamp:float) -> FrameBase|None:
249
249
  if hdr.opcode == 0x54:
250
250
  from .FrameV2IPPrivacy import FrameV2IPPrivacy
251
251
  return FrameV2IPPrivacy(header=hdr, timestamp=timestamp)
252
+ if hdr.opcode == 0x55:
253
+ from .FrameThirdParty import FrameThirdParty
254
+ return FrameThirdParty(header=hdr, timestamp=timestamp)
252
255
  if hdr.opcode == 0x50:
253
256
  from .FrameUnitCommand import FrameUnitCommand
254
257
  return FrameUnitCommand(header=hdr, timestamp=timestamp)
@@ -0,0 +1,50 @@
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 THIRD_PARTY (opcode 0x55): messages applications
8
+ exchange over the mesh.'''
9
+
10
+ from functools import cached_property
11
+ from ..compat import override
12
+ from .Constants import ThirdPartyVendorFlag
13
+ from .FrameBase import FrameBase
14
+ from ..Interface import DeviceRegistry, ThirdPartyMessage, ThirdPartyVendor
15
+
16
+ # The flag byte at 0 and the three identifying bytes at 1, then the
17
+ # application's bytes to the end of the frame.
18
+ VENDOR_SIZE = 4
19
+
20
+ class FrameThirdParty(FrameBase):
21
+ '''An application's own message, under its vendor id.'''
22
+ @staticmethod
23
+ def construct(mxr:DeviceRegistry, vendor:ThirdPartyVendor, payload:bytes) -> FrameBase|None:
24
+ '''A message to every device: the vendor's flag byte and three
25
+ identifying bytes, then payload as it was given.'''
26
+ data = bytes([int(vendor.flags)]) + vendor.id + bytes(payload)
27
+ return FrameBase.construct_base(mxr=mxr, opcode=0x55, payload=data)
28
+
29
+ @cached_property
30
+ def message(self) -> ThirdPartyMessage|None:
31
+ '''The message, whatever its vendor: telling them apart is the
32
+ application's. None for a payload too short for the vendor id.
33
+ Everything after the id is the application's, so the length is a
34
+ minimum and nothing past it is read.'''
35
+ p = self.payload or b''
36
+ if (len(p) < VENDOR_SIZE):
37
+ return None
38
+ vendor = ThirdPartyVendor(flags=ThirdPartyVendorFlag(p[0]), id=bytes(p[1:VENDOR_SIZE]))
39
+ return ThirdPartyMessage(vendor=vendor, payload=bytes(p[VENDOR_SIZE:]), address=self.address)
40
+
41
+ @override
42
+ def process(self) -> None:
43
+ '''Hand the message to the callbacks, as sent by the device it came from.'''
44
+ if ((message := self.message) is not None) and ((dev := self.remote_device) is not None):
45
+ dev.callbacks.on_third_party_message(dev, message)
46
+
47
+ def __str__(self) -> str:
48
+ if (self.message is None):
49
+ return "third-party message <dropped>"
50
+ return f"third-party message from {self.message}"
@@ -0,0 +1,15 @@
1
+ from ..Interface import DeviceRegistry as DeviceRegistry, ThirdPartyMessage as ThirdPartyMessage, ThirdPartyVendor as ThirdPartyVendor
2
+ from ..compat import override as override
3
+ from .Constants import ThirdPartyVendorFlag as ThirdPartyVendorFlag
4
+ from .FrameBase import FrameBase as FrameBase
5
+ from functools import cached_property
6
+
7
+ VENDOR_SIZE: int
8
+
9
+ class FrameThirdParty(FrameBase):
10
+ @staticmethod
11
+ def construct(mxr: DeviceRegistry, vendor: ThirdPartyVendor, payload: bytes) -> FrameBase | None: ...
12
+ @cached_property
13
+ def message(self) -> ThirdPartyMessage | None: ...
14
+ @override
15
+ def process(self) -> None: ...
@@ -7,6 +7,7 @@
7
7
  '''Protocol frame for V2IP_SINK_SDP (opcode 0x52): what a OneIP output plays
8
8
  from a session description, and the commands that hand it one.'''
9
9
 
10
+ from dataclasses import dataclass
10
11
  from functools import cached_property
11
12
  import ipaddress
12
13
  import struct
@@ -30,9 +31,11 @@ from ..Uid import MxrDeviceUid
30
31
  #
31
32
  # A page follows the header with the description's length, the page's offset
32
33
  # into it and the page's length, all u16, two reserved bytes, and at most
33
- # SDP_PAGE_LEN bytes of the description.
34
+ # SDP_PAGE_LEN bytes of the description. An output reports the description it
35
+ # plays in pages of the same shape.
34
36
  SDP_HEADER_SIZE = 20
35
37
  _STATUS_SIZE = 80
38
+ _PAGE_HEADER_SIZE = 8
36
39
 
37
40
  SDP_STATUS = 0
38
41
  '''The output's report.'''
@@ -44,10 +47,35 @@ SDP_SCREEN = 3
44
47
  '''What the output shows while it plays audio alone, in the value byte.'''
45
48
  SDP_REQUEST = 4
46
49
  '''Report now.'''
50
+ SDP_DESCRIPTION = 5
51
+ '''A page of the description an output reports playing.'''
52
+
53
+ SDP_REQUEST_DESCRIPTION = (1 << 0)
54
+ '''The bit of a request's value that asks for the description too.'''
55
+
56
+ SDP_DESCRIPTION_PROTOCOL = 0x31
57
+ '''The first protocol version whose outputs report the description they play.
58
+ Not the stamp: that is the opcode's, and an output between the two answers a
59
+ request for the description with its status alone.'''
60
+
61
+ SDP_PAGE_TIMEOUT = 5
62
+ '''How long an output may take between two pages of one description, in
63
+ seconds, before the pages it sent are dropped.'''
47
64
 
48
65
  SDP_PAGE_LEN = 1024
49
66
  '''The most bytes of a session description one page carries.'''
50
67
 
68
+ @dataclass(frozen=True)
69
+ class SdpDescriptionPage:
70
+ '''A page of the description an output reports playing.'''
71
+ total: int
72
+ '''The length of the whole description.'''
73
+ offset: int
74
+ '''Where this page's text starts in it.'''
75
+ text: bytes
76
+ timestamp: float
77
+ '''When the page arrived.'''
78
+
51
79
  def _cstr(data:bytes) -> str:
52
80
  return data.split(b'\0', 1)[0].decode('ascii', errors='replace')
53
81
 
@@ -109,14 +137,38 @@ class FrameV2IPSinkSdp(FrameBase):
109
137
  # that fills its field stops one byte short of it.
110
138
  name=_cstr(s[24:71]), layout=_cstr(s[72:79]))
111
139
 
140
+ @cached_property
141
+ def description_page(self) -> SdpDescriptionPage|None:
142
+ '''A page of the description the output reports playing, None for
143
+ another type, for a page whose header names a device other than its
144
+ sender, which only the output itself reports, and for a frame too short
145
+ for the text its page header declares.'''
146
+ p = self.payload or b''
147
+ if (self.kind != SDP_DESCRIPTION) or (self.target_uid != self.remote_id) \
148
+ or (len(p) < SDP_HEADER_SIZE + _PAGE_HEADER_SIZE):
149
+ return None
150
+ total, offset, length = struct.unpack_from('<HHH', p, SDP_HEADER_SIZE)
151
+ start = SDP_HEADER_SIZE + _PAGE_HEADER_SIZE
152
+ if (len(p) < start + length):
153
+ return None
154
+ return SdpDescriptionPage(total=total, offset=offset, text=bytes(p[start:start + length]),
155
+ timestamp=self.timestamp)
156
+
112
157
  @override
113
158
  def process(self) -> None:
114
- '''Record an output's report against the output that sent it. The other
115
- types are commands for an output, which only that output acts on.'''
116
- if ((status := self.status) is not None) and ((dev := self.remote_device) is not None):
159
+ '''Record an output's report and the pages of its description against
160
+ the output that sent them. The other types are commands for an output,
161
+ which only that output acts on.'''
162
+ if ((dev := self.remote_device) is None):
163
+ return
164
+ if ((status := self.status) is not None):
117
165
  dev.on_mxr_update(status)
166
+ elif ((page := self.description_page) is not None):
167
+ dev.on_mxr_update(page)
118
168
 
119
169
  def __str__(self) -> str:
120
170
  if (self.status is not None):
121
171
  return f"OneIP sink SDP: {self.status}"
172
+ if ((page := self.description_page) is not None):
173
+ return f"OneIP sink SDP description: bytes {page.offset} to {page.offset + len(page.text)} of {page.total}"
122
174
  return f"OneIP sink SDP command {self.kind} for {self.uid_to_user_string(self.target_uid)}"
@@ -3,6 +3,8 @@ from ..Uid import MxrDeviceUid as MxrDeviceUid
3
3
  from ..compat import override as override
4
4
  from .Constants import OneIPSdpClockMode as OneIPSdpClockMode, OneIPSdpClockState as OneIPSdpClockState, OneIPSdpEncoding as OneIPSdpEncoding, OneIPSdpFlag as OneIPSdpFlag, OneIPSdpMediaClock as OneIPSdpMediaClock, OneIPSdpRefClock as OneIPSdpRefClock
5
5
  from .FrameBase import FrameBase as FrameBase
6
+ from _typeshed import Incomplete
7
+ from dataclasses import dataclass
6
8
  from functools import cached_property
7
9
  from typing import Any
8
10
 
@@ -12,8 +14,19 @@ SDP_PAGE: int
12
14
  SDP_STOP: int
13
15
  SDP_SCREEN: int
14
16
  SDP_REQUEST: int
17
+ SDP_DESCRIPTION: int
18
+ SDP_REQUEST_DESCRIPTION: Incomplete
19
+ SDP_DESCRIPTION_PROTOCOL: int
20
+ SDP_PAGE_TIMEOUT: int
15
21
  SDP_PAGE_LEN: int
16
22
 
23
+ @dataclass(frozen=True)
24
+ class SdpDescriptionPage:
25
+ total: int
26
+ offset: int
27
+ text: bytes
28
+ timestamp: float
29
+
17
30
  class FrameV2IPSinkSdp(FrameBase):
18
31
  @staticmethod
19
32
  def construct_command(mxr: DeviceRegistry, target: Any, target_uid: MxrDeviceUid, kind: int, value: int = 0) -> FrameBase | None: ...
@@ -25,5 +38,7 @@ class FrameV2IPSinkSdp(FrameBase):
25
38
  def kind(self) -> int | None: ...
26
39
  @cached_property
27
40
  def status(self) -> OneIPSinkSdp | None: ...
41
+ @cached_property
42
+ def description_page(self) -> SdpDescriptionPage | None: ...
28
43
  @override
29
44
  def process(self) -> None: ...