mx-remote 6.0.0__tar.gz → 6.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 (248) hide show
  1. {mx_remote-6.0.0 → mx_remote-6.3.0}/PKG-INFO +1 -1
  2. {mx_remote-6.0.0 → mx_remote-6.3.0}/docs/oneip.md +7 -0
  3. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/Interface.py +4 -0
  4. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/Interface.pyi +2 -0
  5. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/const.py +1 -1
  6. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/Constants.py +1 -0
  7. mx_remote-6.3.0/mx_remote/proto/FrameDiscover.py +44 -0
  8. mx_remote-6.3.0/mx_remote/proto/FrameDiscover.pyi +12 -0
  9. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameMeshOperation.py +11 -0
  10. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameMeshOperation.pyi +3 -0
  11. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/remote/ConnectionAsync.py +5 -2
  12. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/remote/Device.py +25 -1
  13. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/remote/Device.pyi +2 -1
  14. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/remote/Remote.py +67 -3
  15. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/remote/Remote.pyi +2 -2
  16. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/mesh.py +38 -0
  17. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/run.py +1 -0
  18. mx_remote-6.3.0/tests/staterequest.py +119 -0
  19. mx_remote-6.0.0/mx_remote/proto/FrameDiscover.py +0 -25
  20. mx_remote-6.0.0/mx_remote/proto/FrameDiscover.pyi +0 -8
  21. {mx_remote-6.0.0 → mx_remote-6.3.0}/.gitignore +0 -0
  22. {mx_remote-6.0.0 → mx_remote-6.3.0}/AGENTS.md +0 -0
  23. {mx_remote-6.0.0 → mx_remote-6.3.0}/CLAUDE.md +0 -0
  24. {mx_remote-6.0.0 → mx_remote-6.3.0}/LICENSE +0 -0
  25. {mx_remote-6.0.0 → mx_remote-6.3.0}/README.md +0 -0
  26. {mx_remote-6.0.0 → mx_remote-6.3.0}/docs/README.md +0 -0
  27. {mx_remote-6.0.0 → mx_remote-6.3.0}/docs/_static/brand.css +0 -0
  28. {mx_remote-6.0.0 → mx_remote-6.3.0}/docs/_static/favicon.svg +0 -0
  29. {mx_remote-6.0.0 → mx_remote-6.3.0}/docs/_static/logo-dark.svg +0 -0
  30. {mx_remote-6.0.0 → mx_remote-6.3.0}/docs/_static/logo.svg +0 -0
  31. {mx_remote-6.0.0 → mx_remote-6.3.0}/docs/api.md +0 -0
  32. {mx_remote-6.0.0 → mx_remote-6.3.0}/docs/audio.md +0 -0
  33. {mx_remote-6.0.0 → mx_remote-6.3.0}/docs/callbacks.md +0 -0
  34. {mx_remote-6.0.0 → mx_remote-6.3.0}/docs/conf.py +0 -0
  35. {mx_remote-6.0.0 → mx_remote-6.3.0}/docs/configuration.md +0 -0
  36. {mx_remote-6.0.0 → mx_remote-6.3.0}/docs/devices-and-bays.md +0 -0
  37. {mx_remote-6.0.0 → mx_remote-6.3.0}/docs/diagnostics.md +0 -0
  38. {mx_remote-6.0.0 → mx_remote-6.3.0}/docs/index.md +0 -0
  39. {mx_remote-6.0.0 → mx_remote-6.3.0}/docs/multiviewer.md +0 -0
  40. {mx_remote-6.0.0 → mx_remote-6.3.0}/docs/requirements.txt +0 -0
  41. {mx_remote-6.0.0 → mx_remote-6.3.0}/docs/routing.md +0 -0
  42. {mx_remote-6.0.0 → mx_remote-6.3.0}/hatch_build.py +0 -0
  43. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/Uid.py +0 -0
  44. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/Uid.pyi +0 -0
  45. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/__init__.py +0 -0
  46. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/__init__.pyi +0 -0
  47. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/compat.py +0 -0
  48. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/compat.pyi +0 -0
  49. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/const.pyi +0 -0
  50. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/deprecated.py +0 -0
  51. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/deprecated.pyi +0 -0
  52. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/main.py +0 -0
  53. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/main.pyi +0 -0
  54. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/BayConfig.py +0 -0
  55. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/BayConfig.pyi +0 -0
  56. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/Constants.pyi +0 -0
  57. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/Data.py +0 -0
  58. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/Data.pyi +0 -0
  59. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/Factory.py +0 -0
  60. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/Factory.pyi +0 -0
  61. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameAmpDolbySettings.py +0 -0
  62. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameAmpDolbySettings.pyi +0 -0
  63. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameAmpZoneSettings.py +0 -0
  64. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameAmpZoneSettings.pyi +0 -0
  65. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameAudioClip.py +0 -0
  66. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameAudioClip.pyi +0 -0
  67. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameAudioSetRoute.py +0 -0
  68. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameAudioSetRoute.pyi +0 -0
  69. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameBase.py +0 -0
  70. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameBase.pyi +0 -0
  71. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameBayConfig.py +0 -0
  72. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameBayConfig.pyi +0 -0
  73. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameBayConfigSecondary.py +0 -0
  74. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameBayConfigSecondary.pyi +0 -0
  75. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameBayHide.py +0 -0
  76. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameBayHide.pyi +0 -0
  77. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameBayStatus.py +0 -0
  78. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameBayStatus.pyi +0 -0
  79. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameConnectStatus.py +0 -0
  80. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameConnectStatus.pyi +0 -0
  81. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameDebug.py +0 -0
  82. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameDebug.pyi +0 -0
  83. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameEDID.py +0 -0
  84. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameEDID.pyi +0 -0
  85. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameEDIDProfile.py +0 -0
  86. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameEDIDProfile.pyi +0 -0
  87. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameFactoryReset.py +0 -0
  88. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameFactoryReset.pyi +0 -0
  89. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameFilterStatus.py +0 -0
  90. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameFilterStatus.pyi +0 -0
  91. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameFirmwareVersion.py +0 -0
  92. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameFirmwareVersion.pyi +0 -0
  93. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameHeader.py +0 -0
  94. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameHeader.pyi +0 -0
  95. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameHello.py +0 -0
  96. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameHello.pyi +0 -0
  97. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameLinks.py +0 -0
  98. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameLinks.pyi +0 -0
  99. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameMirrorStatus.py +0 -0
  100. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameMirrorStatus.pyi +0 -0
  101. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameMonitoringPulse.py +0 -0
  102. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameMonitoringPulse.pyi +0 -0
  103. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameNetworkStatus.py +0 -0
  104. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameNetworkStatus.pyi +0 -0
  105. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FramePing.py +0 -0
  106. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FramePing.pyi +0 -0
  107. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FramePowerChange.py +0 -0
  108. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FramePowerChange.pyi +0 -0
  109. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameRCAction.py +0 -0
  110. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameRCAction.pyi +0 -0
  111. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameRCIr.py +0 -0
  112. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameRCIr.pyi +0 -0
  113. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameRCKey.py +0 -0
  114. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameRCKey.pyi +0 -0
  115. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameRCSettings.py +0 -0
  116. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameRCSettings.pyi +0 -0
  117. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameReboot.py +0 -0
  118. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameReboot.pyi +0 -0
  119. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameRoutingChange.py +0 -0
  120. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameRoutingChange.pyi +0 -0
  121. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameSetInstaller.py +0 -0
  122. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameSetInstaller.pyi +0 -0
  123. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameSetName.py +0 -0
  124. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameSetName.pyi +0 -0
  125. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameSetRoute.py +0 -0
  126. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameSetRoute.pyi +0 -0
  127. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameSetupStatus.py +0 -0
  128. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameSetupStatus.pyi +0 -0
  129. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameSignalStatus.py +0 -0
  130. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameSignalStatus.pyi +0 -0
  131. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameSignalStatusNew.py +0 -0
  132. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameSignalStatusNew.pyi +0 -0
  133. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameSysTemperature.py +0 -0
  134. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameSysTemperature.pyi +0 -0
  135. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameSystemStatus.py +0 -0
  136. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameSystemStatus.pyi +0 -0
  137. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameTXRCAction.py +0 -0
  138. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameTXRCAction.pyi +0 -0
  139. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameTXRCKey.py +0 -0
  140. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameTXRCKey.pyi +0 -0
  141. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameTime.py +0 -0
  142. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameTime.pyi +0 -0
  143. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameTimeZone.py +0 -0
  144. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameTimeZone.pyi +0 -0
  145. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameTopology.py +0 -0
  146. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameTopology.pyi +0 -0
  147. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameTxIR.py +0 -0
  148. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameTxIR.pyi +0 -0
  149. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameUpgradeFPGA.py +0 -0
  150. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameUpgradeFPGA.pyi +0 -0
  151. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPAudio.py +0 -0
  152. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPAudio.pyi +0 -0
  153. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPBayMapping.py +0 -0
  154. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPBayMapping.pyi +0 -0
  155. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPDetectBays.py +0 -0
  156. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPDetectBays.pyi +0 -0
  157. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.py +0 -0
  158. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.pyi +0 -0
  159. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPLink.py +0 -0
  160. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPLink.pyi +0 -0
  161. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.py +0 -0
  162. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.pyi +0 -0
  163. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPMultiviewer.py +0 -0
  164. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPMultiviewer.pyi +0 -0
  165. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPPowerSave.py +0 -0
  166. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPPowerSave.pyi +0 -0
  167. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPSetMaster.py +0 -0
  168. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPSetMaster.pyi +0 -0
  169. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPSettingsAll.py +0 -0
  170. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPSettingsAll.pyi +0 -0
  171. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPSourceSwitch.py +0 -0
  172. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPSourceSwitch.pyi +0 -0
  173. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPSources.py +0 -0
  174. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPSources.pyi +0 -0
  175. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPStats.py +0 -0
  176. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPStats.pyi +0 -0
  177. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPTestcard.py +0 -0
  178. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPTestcard.pyi +0 -0
  179. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPTiling.py +0 -0
  180. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPTiling.pyi +0 -0
  181. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPVideoWall.py +0 -0
  182. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameV2IPVideoWall.pyi +0 -0
  183. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameVolume.py +0 -0
  184. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameVolume.pyi +0 -0
  185. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameVolumeDown.py +0 -0
  186. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameVolumeDown.pyi +0 -0
  187. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameVolumeSet.py +0 -0
  188. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameVolumeSet.pyi +0 -0
  189. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameVolumeUp.py +0 -0
  190. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/FrameVolumeUp.pyi +0 -0
  191. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/LinkConfig.py +0 -0
  192. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/LinkConfig.pyi +0 -0
  193. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/Multiviewer.py +0 -0
  194. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/Multiviewer.pyi +0 -0
  195. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/Svd.py +0 -0
  196. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/Svd.pyi +0 -0
  197. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/V2IPConfig.py +0 -0
  198. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/V2IPConfig.pyi +0 -0
  199. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/V2IPStats.py +0 -0
  200. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/V2IPStats.pyi +0 -0
  201. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/__init__.py +0 -0
  202. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/__init__.pyi +0 -0
  203. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/proto/svd.csv +0 -0
  204. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/py.typed +0 -0
  205. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/remote/Bay.py +0 -0
  206. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/remote/Bay.pyi +0 -0
  207. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/remote/ConnectionAsync.pyi +0 -0
  208. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/remote/Link.py +0 -0
  209. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/remote/Link.pyi +0 -0
  210. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/remote/State.py +0 -0
  211. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/remote/State.pyi +0 -0
  212. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/remote/V2IP.py +0 -0
  213. {mx_remote-6.0.0 → mx_remote-6.3.0}/mx_remote/remote/V2IP.pyi +0 -0
  214. {mx_remote-6.0.0 → mx_remote-6.3.0}/pyproject.toml +0 -0
  215. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/audio_ir.py +0 -0
  216. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/audiolock.py +0 -0
  217. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/capture.py +0 -0
  218. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/complete.py +0 -0
  219. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/deprecated.py +0 -0
  220. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/e2e.py +0 -0
  221. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/entry.py +0 -0
  222. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/handlers.py +0 -0
  223. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/handlers_cover.py +0 -0
  224. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/hello.py +0 -0
  225. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/idempotent.py +0 -0
  226. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/lengths.py +0 -0
  227. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/merge.py +0 -0
  228. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/newops.py +0 -0
  229. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/offline.py +0 -0
  230. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/offsets.py +0 -0
  231. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/ping.py +1 -1
  232. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/proto_stamp.py +0 -0
  233. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/protogate.py +0 -0
  234. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/rcsettings.py +0 -0
  235. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/roundtrip.py +0 -0
  236. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/smoke.py +0 -0
  237. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/sources.py +0 -0
  238. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/stats.py +0 -0
  239. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/structs.py +0 -0
  240. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/txresult.py +0 -0
  241. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/unknowns.py +0 -0
  242. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/v2ipscaling.py +0 -0
  243. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/v2ipsettings.py +0 -0
  244. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/v2ipsubject.py +0 -0
  245. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/v2iptestcard.py +0 -0
  246. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/v2ipvlan.py +0 -0
  247. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/vdetails.py +0 -0
  248. {mx_remote-6.0.0 → mx_remote-6.3.0}/tests/wirefix.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mx_remote
3
- Version: 6.0.0
3
+ Version: 6.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://opdenkamp.github.io/mx-remote/
@@ -308,6 +308,7 @@ itself, a profile out of range, or a schedule time that is not a time of day.
308
308
  # mesh operations
309
309
  await device.mesh_promote() # promote to mesh master
310
310
  await device.mesh_remove() # remove from mesh
311
+ await source.auto_assign_oneip_source_addresses() # drop addresses set by hand
311
312
 
312
313
  # firmware versions
313
314
  if device.oneip_firmware_versions:
@@ -315,6 +316,12 @@ if device.oneip_firmware_versions:
315
316
  print(f"{fw_type}: {fw.version}")
316
317
  ```
317
318
 
319
+ A OneIP source acts on `auto_assign_oneip_source_addresses()` only from
320
+ management, which this client announces itself as, and nothing acknowledges it:
321
+ the source's next configuration report carries the addresses it ends up with, in
322
+ `oneip_details`. A device that is not a source, or announces less than protocol
323
+ 0x2B, would ignore the operation, so it is refused before anything is sent.
324
+
318
325
  ### Time zone and time
319
326
 
320
327
  The mesh controller announces its time zone and clock with every periodic
@@ -2804,6 +2804,10 @@ class DeviceBase(ABC):
2804
2804
  async def mesh_promote(self) -> bool:
2805
2805
  '''promote to mesh master'''
2806
2806
 
2807
+ @abstractmethod
2808
+ async def auto_assign_oneip_source_addresses(self) -> bool:
2809
+ '''hand this OneIP source's stream addresses back to automatic assignment'''
2810
+
2807
2811
  @abstractmethod
2808
2812
  async def mesh_remove(self) -> bool:
2809
2813
  '''remove from mesh'''
@@ -1059,6 +1059,8 @@ class DeviceBase(ABC):
1059
1059
  @abstractmethod
1060
1060
  async def mesh_promote(self) -> bool: ...
1061
1061
  @abstractmethod
1062
+ async def auto_assign_oneip_source_addresses(self) -> bool: ...
1063
+ @abstractmethod
1062
1064
  async def mesh_remove(self) -> bool: ...
1063
1065
  @abstractmethod
1064
1066
  async def read_stats(self, enable: bool) -> bool: ...
@@ -14,7 +14,7 @@ Default communication modes:
14
14
  """
15
15
  import os
16
16
 
17
- VERSION = '6.0.0'
17
+ VERSION = '6.3.0'
18
18
  __version__ = VERSION
19
19
 
20
20
  MX_BCAST_UDP_PORT = 8811
@@ -256,6 +256,7 @@ class DeviceFeature(IntFlag):
256
256
  VLAN = (1 << 28)
257
257
  '''Tags its uplink by the VLAN configuration it reports, and takes one written to it.'''
258
258
  BOOT_BIT = (1 << 31)
259
+ '''Flips on every reboot, so a peer that sees it change knows the device restarted.'''
259
260
 
260
261
  class OneIPVideoProcessorFeature(IntFlag):
261
262
  '''What a OneIP device's video processor supports, as the device reports it in
@@ -0,0 +1,44 @@
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 network device discovery.'''
8
+
9
+ import warnings
10
+ from functools import cached_property
11
+ from .FrameBase import FrameBase
12
+ from ..Interface import DeviceRegistry
13
+ from ..Uid import MxrDeviceUid
14
+
15
+ # Payload: empty to ask every device, or the 16 byte uid of the one device
16
+ # asked. Any other length asks every device, which is also how a receiver that
17
+ # predates the uid form reads the uid form.
18
+
19
+ class FrameDiscover(FrameBase):
20
+ '''Discovery frame that asks devices on the network to send their info.'''
21
+ @staticmethod
22
+ def construct(mxr:DeviceRegistry, target:MxrDeviceUid|None=None) -> FrameBase|None:
23
+ '''Build a discover asking every device, or only target.
24
+
25
+ Not refused for a target that predates the uid form: it answers as if
26
+ every device had been asked.'''
27
+ payload = bytes([]) if (target is None) else target.byte_value
28
+ return FrameBase.construct_base(mxr=mxr, opcode=1, protocol=1, payload=payload)
29
+
30
+ @cached_property
31
+ def target_uid(self) -> MxrDeviceUid|None:
32
+ '''The one device asked, or None when every device is.'''
33
+ if (self.payload is None) or (len(self.payload) != 16):
34
+ return None
35
+ return self.payload_uuid(0)
36
+
37
+ def __str__(self) -> str:
38
+ if (self.target_uid is None):
39
+ return "discover devices"
40
+ return f"discover {self.mxr.uid_to_user_string(self.target_uid)}"
41
+
42
+ def constructFrameDiscover(mxr:DeviceRegistry) -> FrameBase|None:
43
+ warnings.warn("use FrameDiscover.construct() instead", DeprecationWarning)
44
+ return FrameDiscover.construct(mxr=mxr)
@@ -0,0 +1,12 @@
1
+ from ..Interface import DeviceRegistry as DeviceRegistry
2
+ from ..Uid import MxrDeviceUid as MxrDeviceUid
3
+ from .FrameBase import FrameBase as FrameBase
4
+ from functools import cached_property
5
+
6
+ class FrameDiscover(FrameBase):
7
+ @staticmethod
8
+ def construct(mxr: DeviceRegistry, target: MxrDeviceUid | None = None) -> FrameBase | None: ...
9
+ @cached_property
10
+ def target_uid(self) -> MxrDeviceUid | None: ...
11
+
12
+ def constructFrameDiscover(mxr: DeviceRegistry) -> FrameBase | None: ...
@@ -22,6 +22,8 @@ class MeshOperation(IntEnum):
22
22
  REGENERATE_ADDRESSES = 3
23
23
  REPORT_CONTROLLER = 4
24
24
  PROMOTE_CONTROLLER = 5
25
+ AUTO_ADDRESSES = 6
26
+ '''Hand the target's source addresses back to automatic assignment.'''
25
27
  REPORT_MEMBERSHIP = 0xFF
26
28
 
27
29
  def __str__(self) -> str:
@@ -37,6 +39,8 @@ class MeshOperation(IntEnum):
37
39
  return "report controller"
38
40
  if self.value == MeshOperation.PROMOTE_CONTROLLER.value:
39
41
  return "promote controller"
42
+ if self.value == MeshOperation.AUTO_ADDRESSES.value:
43
+ return "automatic addresses"
40
44
  if self.value == MeshOperation.REPORT_MEMBERSHIP.value:
41
45
  return "report membership"
42
46
  return "unknown"
@@ -66,6 +70,13 @@ _WIRE_SIZE = 40
66
70
  _ACCEPT_PROTOCOL = 0x1A
67
71
  _INSTALLER_PROTOCOL = 0x1D
68
72
 
73
+ AUTO_ADDRESSES_PROTOCOL = 0x2B
74
+ '''The first protocol version whose devices can act on AUTO_ADDRESSES.
75
+
76
+ Not the stamp: that is this opcode's, and a device between the two takes the
77
+ frame and ignores an operation it does not know. Not every device on this
78
+ version has it either, but none below it does.'''
79
+
69
80
  class FrameMeshOperation(FrameBase):
70
81
  ''' Mesh operation '''
71
82
 
@@ -11,8 +11,11 @@ class MeshOperation(IntEnum):
11
11
  REGENERATE_ADDRESSES: int
12
12
  REPORT_CONTROLLER: int
13
13
  PROMOTE_CONTROLLER: int
14
+ AUTO_ADDRESSES: int
14
15
  REPORT_MEMBERSHIP: int
15
16
 
17
+ AUTO_ADDRESSES_PROTOCOL: int
18
+
16
19
  class FrameMeshOperation(FrameBase):
17
20
  @staticmethod
18
21
  def construct(mxr: DeviceRegistry, operation: MeshOperation, target: DeviceBase, option: DeviceBase | None = None) -> FrameBase | None: ...
@@ -39,8 +39,11 @@ _LOGGER = logging.getLogger(__name__)
39
39
  #
40
40
  # The answer to a discover goes to the group, is jittered so a mesh does not
41
41
  # reply at once, and is expensive - Remote._background_probe() rate-limits it and
42
- # stops once every device reports complete. After that the device's own periodic
43
- # broadcast maintains the state, at worst about 75 seconds stale.
42
+ # stops once every device reports complete. After that a device repeats a frame
43
+ # in only a couple of periodic broadcasts after it changes, and resends
44
+ # everything only every few minutes. So a device first heard, or heard again
45
+ # after it went offline, is asked on its own with a discover whose payload is
46
+ # its 16-byte uid - see Remote._request_state().
44
47
  #
45
48
  # Signal status (0x31) is the one opcode with a targeted request form of its own,
46
49
  # an empty payload or a 16-byte uid, which this library does not send.
@@ -35,7 +35,7 @@ from ..Interface import (
35
35
  )
36
36
  from ..proto.BayConfig import BayConfig
37
37
  from ..proto.FrameHello import FrameHello
38
- from ..proto.FrameMeshOperation import MeshOperation, FrameMeshOperation
38
+ from ..proto.FrameMeshOperation import AUTO_ADDRESSES_PROTOCOL, MeshOperation, FrameMeshOperation
39
39
  from ..proto.FrameV2IPStats import FrameV2IPStats
40
40
  from ..proto.FrameNetworkStatus import NetworkPortStatus
41
41
  from ..proto.FrameReboot import FrameReboot
@@ -1057,6 +1057,30 @@ class Device(DeviceBase):
1057
1057
  return True
1058
1058
  return False
1059
1059
 
1060
+ async def auto_assign_oneip_source_addresses(self) -> bool:
1061
+ '''Hand this OneIP source's stream addresses back to automatic assignment,
1062
+ undoing addresses that were set on it by hand.
1063
+
1064
+ The device takes this only from management, which this client announces
1065
+ itself as. Nothing acknowledges it: the device's next configuration
1066
+ report carries the addresses it ends up with, and oneip_details reads
1067
+ them.
1068
+
1069
+ Refused for a device that is not a OneIP source, and for one below
1070
+ protocol 0x2B, which predates the operation and ignores it.
1071
+ '''
1072
+ if (self.features is None) or (DeviceFeature.ONEIP_SOURCE not in self.features):
1073
+ _LOGGER.warning(f"not handing the addresses of {self} back to automatic assignment: it is not a OneIP source")
1074
+ return False
1075
+ if (self.protocol != 0) and (self.protocol < AUTO_ADDRESSES_PROTOCOL):
1076
+ _LOGGER.warning(f"not handing the addresses of {self} back to automatic assignment: it advertises "
1077
+ f"protocol {self.protocol:02X} and the operation needs {AUTO_ADDRESSES_PROTOCOL:02X}")
1078
+ return False
1079
+ frame = FrameMeshOperation.construct(mxr=self.registry, target=self, operation=MeshOperation.AUTO_ADDRESSES)
1080
+ if (frame is None):
1081
+ return False
1082
+ return self.registry.transmit(frame.frame) == len(frame.frame)
1083
+
1060
1084
  async def mesh_remove(self) -> bool:
1061
1085
  '''Remove this device from the mesh network.'''
1062
1086
  frame = FrameMeshOperation.construct(mxr=self.registry, target=self, operation=MeshOperation.UNREGISTER)
@@ -7,7 +7,7 @@ from ..proto.Constants import DeviceFeature as DeviceFeature, MXR_PROTOCOL_VERSI
7
7
  from ..proto.FrameBase import FrameBase as FrameBase
8
8
  from ..proto.FrameFirmwareVersion import FirmwareType as FirmwareType, FirmwareVersion as FirmwareVersion
9
9
  from ..proto.FrameHello import FrameHello as FrameHello
10
- from ..proto.FrameMeshOperation import FrameMeshOperation as FrameMeshOperation, MeshOperation as MeshOperation
10
+ from ..proto.FrameMeshOperation import AUTO_ADDRESSES_PROTOCOL as AUTO_ADDRESSES_PROTOCOL, FrameMeshOperation as FrameMeshOperation, MeshOperation as MeshOperation
11
11
  from ..proto.FrameNetworkStatus import NetworkPortStatus as NetworkPortStatus
12
12
  from ..proto.FramePing import FramePing as FramePing
13
13
  from ..proto.FrameReboot import FrameReboot as FrameReboot
@@ -223,6 +223,7 @@ class Device(DeviceBase):
223
223
  async def reboot(self) -> bool: ...
224
224
  async def ping(self) -> bool: ...
225
225
  async def mesh_promote(self) -> bool: ...
226
+ async def auto_assign_oneip_source_addresses(self) -> bool: ...
226
227
  async def mesh_remove(self) -> bool: ...
227
228
  async def read_stats(self, enable: bool) -> bool: ...
228
229
  async def set_oneip_auto_scaling(self, enabled: bool) -> bool: ...
@@ -23,8 +23,8 @@ from .ConnectionAsync import ConnectionAsync
23
23
  from ..const import MXR_HELLO_INTERVAL_MIN, MXR_HELLO_INTERVAL_RAND, __version__
24
24
  from .Device import Device
25
25
  from ..Interface import ConnectionCallbacks, DeviceRegistry, MxrDeviceUid, BayLinks, BayBase, DeviceBase, MxrCallbacks, AudioEndpoint, mxr_broadcast_address, OneIPDeviceSettings, OneIPPowerSaveSchedule
26
- from ..proto.Constants import MXR_PROTOCOL_VERSION
27
- from ..proto.FrameDiscover import constructFrameDiscover
26
+ from ..proto.Constants import MXR_PROTOCOL_VERSION, DeviceFeature
27
+ from ..proto.FrameDiscover import FrameDiscover, constructFrameDiscover
28
28
  from ..proto.Factory import process_mxr_frame
29
29
  from ..proto.FrameHello import FrameHello, constructFrameHello
30
30
  from ..proto.FramePing import FramePing
@@ -49,6 +49,42 @@ _LOGGER = logging.getLogger(__name__)
49
49
  # asks one device.
50
50
  _ACCEPT_UNANNOUNCED = (0x00, 0x01, 0x4A)
51
51
 
52
+ # How long a discover to every device stands in for asking any one of them, in
53
+ # seconds. Each device answers it with everything it holds, after a jitter of up
54
+ # to a few seconds, so asking one again inside this window repeats an answer
55
+ # already on its way.
56
+ _DISCOVER_COVERS = 60
57
+
58
+ # What this client had heard of a device before a frame from it: whether it had
59
+ # gone offline, and its reboot toggle as last announced.
60
+ _Heard = tuple[bool, bool]
61
+
62
+ def _heard(device:DeviceBase) -> _Heard:
63
+ features = device.features
64
+ return (not device.online,
65
+ (features is not None) and (DeviceFeature.BOOT_BIT in features))
66
+
67
+ def _missed_state(before:_Heard|None, device:DeviceBase) -> bool:
68
+ '''Whether a device just heard may hold state this client never received.
69
+
70
+ A device repeats a frame only for a short while after it changes, so
71
+ whatever it sent before this client first heard it, or while it was out of
72
+ earshot, may not come again. before is None for a device this frame
73
+ registered.
74
+
75
+ Not asked: a device whose reboot toggle flipped, or that is about to reboot,
76
+ because a booting device sends everything it holds; and a management client,
77
+ which holds nothing to send.
78
+ '''
79
+ features = device.features
80
+ if (features is None) or (DeviceFeature.MANAGER in features) \
81
+ or (DeviceFeature.STATUS_REBOOTING in features):
82
+ return False
83
+ if (before is None):
84
+ return True
85
+ away, boot_bit = before
86
+ return away and (boot_bit == (DeviceFeature.BOOT_BIT in features))
87
+
52
88
  class Remote(DeviceRegistry, ConnectionCallbacks):
53
89
  ''' Main component that handles the network connections and registration of remote devices '''
54
90
 
@@ -85,6 +121,7 @@ class Remote(DeviceRegistry, ConnectionCallbacks):
85
121
  self._target_ip = target_ip
86
122
  self._port = port
87
123
  self._discover_timeout = 0
124
+ self._state_request:MxrDeviceUid|None = None
88
125
  self._addr_filter = addr_filter
89
126
  self._svd = SvdMap()
90
127
  if open_connection:
@@ -403,6 +440,21 @@ class Remote(DeviceRegistry, ConnectionCallbacks):
403
440
  _LOGGER.debug("discovering devices")
404
441
  return self.transmit(pkt.frame)
405
442
 
443
+ def _request_state(self, device:MxrDeviceUid) -> None:
444
+ '''Ask one device for everything it holds, with a discover naming it.
445
+
446
+ Skipped while a discover to every device is recent enough to have
447
+ covered it. A receiver that predates the named form answers it as a
448
+ discover to every device, so each one asked costs the whole mesh's state
449
+ on such a network.
450
+ '''
451
+ if ((time.time() - self._discover_timeout) < _DISCOVER_COVERS):
452
+ return
453
+ pkt = FrameDiscover.construct(mxr=self, target=device)
454
+ if (pkt is not None):
455
+ _LOGGER.debug(f"asking {self.uid_to_user_string(device)} for its state")
456
+ self.transmit(pkt.frame)
457
+
406
458
  def _send_to_all(self, frame:FrameBase|None) -> bool:
407
459
  '''Send a frame to every device, True when all of it was written.'''
408
460
  if (frame is None):
@@ -520,8 +572,13 @@ class Remote(DeviceRegistry, ConnectionCallbacks):
520
572
  self.tx_discover()
521
573
 
522
574
  def process_frame(self, timestamp:float, data: bytes, addr: tuple[str, int]) -> None:
523
- '''Decode and process a received mx_remote frame.'''
575
+ '''Decode and process a received mx_remote frame.
576
+
577
+ A device that may hold state this client missed is left in
578
+ _state_request rather than asked here, so replaying a capture sends
579
+ nothing; on_datagram_received does the asking.'''
524
580
  proc = False
581
+ self._state_request = None
525
582
  try:
526
583
  frame = process_mxr_frame(mxr=self, timestamp=timestamp, data=data, addr=addr)
527
584
  if (frame is not None) and (frame.header.protocol > MXR_PROTOCOL_VERSION):
@@ -557,7 +614,12 @@ class Remote(DeviceRegistry, ConnectionCallbacks):
557
614
  raise
558
615
  try:
559
616
  if (frame is not None) and proc:
617
+ known = self.remotes.get(frame.remote_id)
618
+ before = None if (known is None) else _heard(known)
560
619
  frame.process()
620
+ device = self.remotes.get(frame.remote_id)
621
+ if (device is not None) and _missed_state(before, device):
622
+ self._state_request = frame.remote_id
561
623
  except Exception:
562
624
  _LOGGER.warning(f"failed to process frame: {traceback.format_exc()}")
563
625
  raise
@@ -566,6 +628,8 @@ class Remote(DeviceRegistry, ConnectionCallbacks):
566
628
  '''Called when a UDP frame was received.'''
567
629
  timestamp = time.time()
568
630
  self.process_frame(timestamp=timestamp, data=data, addr=addr)
631
+ if (self._state_request is not None):
632
+ self._request_state(self._state_request)
569
633
 
570
634
  @override
571
635
  def on_mxr_update(self, data:Any) -> None:
@@ -3,10 +3,10 @@ from ..Interface import AudioEndpoint as AudioEndpoint, BayBase as BayBase, BayL
3
3
  from ..Uid import MxrDeviceUid as MxrDeviceUid
4
4
  from ..compat import override as override
5
5
  from ..const import MXR_HELLO_INTERVAL_MIN as MXR_HELLO_INTERVAL_MIN, MXR_HELLO_INTERVAL_RAND as MXR_HELLO_INTERVAL_RAND, MX_BCAST_UDP_PORT as MX_BCAST_UDP_PORT, MX_MCAST_UDP_IP as MX_MCAST_UDP_IP, MX_MCAST_UDP_PORT as MX_MCAST_UDP_PORT, __version__ as __version__
6
- from ..proto.Constants import MXR_PROTOCOL_VERSION as MXR_PROTOCOL_VERSION, ONEIP_DEVICE_SETTINGS_REPORTED_ONLY as ONEIP_DEVICE_SETTINGS_REPORTED_ONLY, ONEIP_IR_PROFILE_MAX as ONEIP_IR_PROFILE_MAX, ONEIP_IR_PROFILE_NOT_SET as ONEIP_IR_PROFILE_NOT_SET, OneIPDeviceSetting as OneIPDeviceSetting
6
+ from ..proto.Constants import DeviceFeature as DeviceFeature, MXR_PROTOCOL_VERSION as MXR_PROTOCOL_VERSION, ONEIP_DEVICE_SETTINGS_REPORTED_ONLY as ONEIP_DEVICE_SETTINGS_REPORTED_ONLY, ONEIP_IR_PROFILE_MAX as ONEIP_IR_PROFILE_MAX, ONEIP_IR_PROFILE_NOT_SET as ONEIP_IR_PROFILE_NOT_SET, OneIPDeviceSetting as OneIPDeviceSetting
7
7
  from ..proto.Factory import process_mxr_frame as process_mxr_frame
8
8
  from ..proto.FrameBase import FrameBase as FrameBase
9
- from ..proto.FrameDiscover import constructFrameDiscover as constructFrameDiscover
9
+ from ..proto.FrameDiscover import FrameDiscover as FrameDiscover, constructFrameDiscover as constructFrameDiscover
10
10
  from ..proto.FrameHello import FrameHello as FrameHello, constructFrameHello as constructFrameHello
11
11
  from ..proto.FramePing import FramePing as FramePing
12
12
  from ..proto.FrameTime import FrameTime as FrameTime
@@ -20,6 +20,7 @@ import mx_remote
20
20
  from mx_remote import MxrDeviceUid, OneIPDeviceSetting as S, OneIPDeviceSettings, OneIPPowerSaveSchedule
21
21
  from mx_remote.proto.Constants import (ONEIP_IR_PROFILE_MAX, ONEIP_IR_PROFILE_NOT_SET,
22
22
  ONEIP_MINUTES_PER_DAY)
23
+ from mx_remote.proto.Constants import MXR_OPCODE_VERSIONS
23
24
  from mx_remote.proto.Factory import create_mxr_frame
24
25
 
25
26
  ADDR = ('192.0.2.9', 8812)
@@ -165,4 +166,41 @@ for ok in (OneIPDeviceSettings(valid=S.IR_PROFILE, ir_profile=ONEIP_IR_PROFILE_M
165
166
  assert call(mx.set_all_oneip_device_settings(ok)) is True, f'{ok} was refused'
166
167
  print('all refused : what every device would ignore is not sent')
167
168
 
169
+ # -------------------------------------------- a source's automatic addresses
170
+
171
+ # Mesh operation 6 names the source, with the parameter and padding zero, and
172
+ # is stamped as every mesh operation is.
173
+ def device(uid, protocol, features, serial):
174
+ rx_from(uid, 0x00, struct.pack('<H', protocol) + name('OneIP') + name(serial) + name('5.0.0')
175
+ + struct.pack('<I', int(features)), protocol=protocol)
176
+ return mx.get_by_uid(MxrDeviceUid(uid))
177
+
178
+ def rx_from(uid, opcode, payload, protocol):
179
+ frame = bytearray(create_mxr_frame(uid, opcode, payload))
180
+ frame[2] = protocol
181
+ mx.process_frame(time.time(), bytes(frame), ADDR)
182
+
183
+ SOURCE = mx_remote.DeviceFeature.ONEIP_SOURCE
184
+ src = device(bytes([0x50]) * 16, 0x2B, SOURCE, 'AA0001')
185
+ old = device(bytes([0x51]) * 16, 0x2A, SOURCE, 'AA0002')
186
+ sink_only = device(bytes([0x52]) * 16, 0x2B, mx_remote.DeviceFeature.ONEIP_SINK, 'AA0003')
187
+
188
+ sent.clear()
189
+ assert call(src.auto_assign_oneip_source_addresses()) is True
190
+ frame = sent.pop()
191
+ assert opcode(frame) == 0x3B and frame[2] == MXR_OPCODE_VERSIONS[0x3B], (opcode(frame), frame[2])
192
+ p = frame[24:]
193
+ assert len(p) == 40, 'a receiver drops a mesh operation shorter than 40 bytes'
194
+ assert p[:4] == bytes([6, 0, 0, 0]), p[:4].hex()
195
+ assert p[4:20] == bytes([0x50]) * 16, 'the frame names another device'
196
+ assert p[20:] == bytes(20), 'the parameter and padding'
197
+
198
+ refused(old.auto_assign_oneip_source_addresses(), 'a source below 0x2B, which ignores the operation')
199
+ refused(sink_only.auto_assign_oneip_source_addresses(), 'a device that is not a source')
200
+
201
+ mx.transmit = lambda data: 0
202
+ assert call(src.auto_assign_oneip_source_addresses()) is False, 'a failed send reported success'
203
+ mx.transmit = lambda data: (sent.append(data), len(data))[1]
204
+ print('auto addrs : mesh operation 6 to a source on 0x2B; anything else is refused')
205
+
168
206
  print('ALL OK')
@@ -52,6 +52,7 @@ SUITES = [
52
52
  'v2ipvlan', # a V2IP device's VLAN configuration, read and written
53
53
  'v2iptestcard', # a V2IP sink's test pattern, tone and lip-sync flash
54
54
  'audiolock', # an audio endpoint's status, and locking its source
55
+ 'staterequest', # a device that may have missed state is asked for it
55
56
  'deprecated', # the V2IP names 5.11.0 exported still resolve, with a warning
56
57
  ]
57
58
 
@@ -0,0 +1,119 @@
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
+ '''A device that may hold state this client missed is asked for it.
8
+
9
+ A device repeats a frame in only a couple of periodic broadcasts after it
10
+ changes, and resends everything only every few minutes. So a device this client
11
+ first hears, or hears again after counting it offline, is sent a discover
12
+ naming it - unless its own boot or a recent discover to every device already
13
+ covers that.
14
+ '''
15
+
16
+ import os, struct, sys, time, logging
17
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
18
+ logging.disable(logging.CRITICAL)
19
+ import mx_remote
20
+ from mx_remote.proto.Factory import create_mxr_frame
21
+ from datetime import timedelta
22
+
23
+ F = mx_remote.DeviceFeature
24
+ ADDR = ('192.0.2.9', 8812)
25
+ HELLO, DISCOVER = 0x00, 0x01
26
+ OURS = bytes(range(0x20, 0x30))
27
+
28
+ def name(s, sz=16):
29
+ b = s.encode('ascii')[:sz]
30
+ return b + bytes(sz - len(b))
31
+
32
+ def uid(n):
33
+ return bytes([n]) * 16
34
+
35
+ def hello(mx, peer, features):
36
+ mx.on_datagram_received(create_mxr_frame(peer, HELLO, struct.pack('<H', 0x2A) + name('ONEIP')
37
+ + name('SR0001') + name('1.0.0')
38
+ + struct.pack('<I', int(features))), ADDR)
39
+
40
+ def silence(mx, peer):
41
+ '''Move the last time peer was heard far enough back to count as offline.'''
42
+ dev = mx.get_by_uid(mx_remote.MxrDeviceUid(peer))
43
+ dev._last_ping -= timedelta(seconds=600)
44
+ assert not dev.online
45
+
46
+ def discovers(sent):
47
+ '''The discovers sent, as their payloads.'''
48
+ return [f[24:] for f in sent if int.from_bytes(f[20:22], 'little') == DISCOVER]
49
+
50
+ def client():
51
+ mx = mx_remote.Remote(open_connection=False)
52
+ mx._uid = OURS
53
+ mx._hello_due = time.time() + 3600 # nothing is due on the clock
54
+ sent = []
55
+ mx.transmit = lambda data: (sent.append(data), len(data))[1]
56
+ return mx, sent
57
+
58
+ ROUTING = F.VIDEO_ROUTING
59
+
60
+ # ---- asked: a device first heard, and one back from offline
61
+ mx, sent = client()
62
+ peer = uid(0x21)
63
+ hello(mx, peer, ROUTING)
64
+ assert discovers(sent) == [peer], 'a device this client first heard'
65
+ print('asked : a device first heard is sent a discover naming it')
66
+
67
+ sent.clear()
68
+ hello(mx, peer, ROUTING)
69
+ assert discovers(sent) == [], 'a device that never went away'
70
+ print('asked : not again while it stays online')
71
+
72
+ silence(mx, peer)
73
+ hello(mx, peer, ROUTING)
74
+ assert discovers(sent) == [peer], 'a device back from offline'
75
+ print('asked : again once it comes back from offline')
76
+
77
+ # ---- not asked: a device that sends everything anyway, or holds nothing
78
+ mx, sent = client()
79
+ rebooted = uid(0x23)
80
+ hello(mx, rebooted, ROUTING)
81
+ silence(mx, rebooted)
82
+ sent.clear()
83
+ hello(mx, rebooted, ROUTING | F.BOOT_BIT)
84
+ assert discovers(sent) == [], 'a device whose reboot toggle flipped'
85
+ print('not asked : a device whose reboot toggle flipped')
86
+
87
+ rebooting = uid(0x24)
88
+ hello(mx, rebooting, ROUTING | F.STATUS_REBOOTING)
89
+ assert discovers(sent) == [], 'a device about to reboot'
90
+ print('not asked : a device about to reboot')
91
+
92
+ manager = uid(0x25)
93
+ hello(mx, manager, F.MANAGER)
94
+ assert discovers(sent) == [], 'a management client'
95
+ print('not asked : a management client')
96
+
97
+ # ---- covered: a discover to every device already asked each one
98
+ mx, sent = client()
99
+ mx.tx_discover()
100
+ sent.clear()
101
+ hello(mx, uid(0x27), ROUTING)
102
+ assert discovers(sent) == [], 'asked again inside the window'
103
+ print('covered : a discover to every device covers one heard within 60s')
104
+
105
+ mx._discover_timeout = time.time() - 61
106
+ hello(mx, uid(0x28), ROUTING)
107
+ assert discovers(sent) == [uid(0x28)], 'not asked once the window had passed'
108
+ print('covered : but not one heard after')
109
+
110
+ # ---- replay: processing a capture asks nothing, there is no one to ask
111
+ mx = mx_remote.Remote(open_connection=False)
112
+ mx._uid = OURS
113
+ mx.process_frame(time.time(), create_mxr_frame(uid(0x29), HELLO, struct.pack('<H', 0x2A) + name('ONEIP')
114
+ + name('SR0001') + name('1.0.0')
115
+ + struct.pack('<I', int(ROUTING))), ADDR)
116
+ assert mx.get_by_uid(mx_remote.MxrDeviceUid(uid(0x29))) is not None
117
+ print('replay : a capture registers the device without sending')
118
+
119
+ print('ALL OK')
@@ -1,25 +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 network device discovery.'''
8
-
9
- import warnings
10
- from .FrameBase import FrameBase
11
- from ..Interface import DeviceRegistry
12
-
13
- class FrameDiscover(FrameBase):
14
- '''Discovery frame that asks all devices on the network to send their info.'''
15
- @staticmethod
16
- def construct(mxr:DeviceRegistry) -> FrameBase|None:
17
- '''Build a discovery broadcast frame for transmission.'''
18
- return FrameBase.construct_base(mxr=mxr, opcode=1, protocol=1)
19
-
20
- def __str__(self) -> str:
21
- return "discover devices"
22
-
23
- def constructFrameDiscover(mxr:DeviceRegistry) -> FrameBase|None:
24
- warnings.warn("use FrameDiscover.construct() instead", DeprecationWarning)
25
- return FrameDiscover.construct(mxr=mxr)
@@ -1,8 +0,0 @@
1
- from ..Interface import DeviceRegistry as DeviceRegistry
2
- from .FrameBase import FrameBase as FrameBase
3
-
4
- class FrameDiscover(FrameBase):
5
- @staticmethod
6
- def construct(mxr: DeviceRegistry) -> FrameBase | None: ...
7
-
8
- def constructFrameDiscover(mxr: DeviceRegistry) -> FrameBase | None: ...
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes