mx-remote 9.1.0__tar.gz → 9.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 (274) hide show
  1. {mx_remote-9.1.0 → mx_remote-9.3.0}/PKG-INFO +1 -1
  2. {mx_remote-9.1.0 → mx_remote-9.3.0}/docs/api.md +17 -0
  3. {mx_remote-9.1.0 → mx_remote-9.3.0}/docs/callbacks.md +25 -0
  4. {mx_remote-9.1.0 → mx_remote-9.3.0}/docs/oneip.md +22 -13
  5. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/Interface.py +102 -15
  6. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/Interface.pyi +22 -0
  7. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/const.py +1 -1
  8. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/Constants.py +23 -1
  9. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/Constants.pyi +5 -0
  10. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/Factory.py +3 -0
  11. mx_remote-9.3.0/mx_remote/proto/FrameThirdParty.py +50 -0
  12. mx_remote-9.3.0/mx_remote/proto/FrameThirdParty.pyi +15 -0
  13. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/remote/Device.py +22 -8
  14. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/remote/Device.pyi +1 -0
  15. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/remote/Remote.py +26 -2
  16. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/remote/Remote.pyi +5 -2
  17. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/handlers.py +1 -0
  18. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/run.py +1 -0
  19. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/smoke.py +1 -0
  20. mx_remote-9.3.0/tests/thirdparty.py +146 -0
  21. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/v2ipsettings.py +41 -1
  22. {mx_remote-9.1.0 → mx_remote-9.3.0}/.gitignore +0 -0
  23. {mx_remote-9.1.0 → mx_remote-9.3.0}/AGENTS.md +0 -0
  24. {mx_remote-9.1.0 → mx_remote-9.3.0}/CLAUDE.md +0 -0
  25. {mx_remote-9.1.0 → mx_remote-9.3.0}/LICENSE +0 -0
  26. {mx_remote-9.1.0 → mx_remote-9.3.0}/README.md +0 -0
  27. {mx_remote-9.1.0 → mx_remote-9.3.0}/docs/README.md +0 -0
  28. {mx_remote-9.1.0 → mx_remote-9.3.0}/docs/_static/brand.css +0 -0
  29. {mx_remote-9.1.0 → mx_remote-9.3.0}/docs/_static/favicon.svg +0 -0
  30. {mx_remote-9.1.0 → mx_remote-9.3.0}/docs/_static/logo-dark.svg +0 -0
  31. {mx_remote-9.1.0 → mx_remote-9.3.0}/docs/_static/logo.svg +0 -0
  32. {mx_remote-9.1.0 → mx_remote-9.3.0}/docs/audio.md +0 -0
  33. {mx_remote-9.1.0 → mx_remote-9.3.0}/docs/conf.py +0 -0
  34. {mx_remote-9.1.0 → mx_remote-9.3.0}/docs/configuration.md +0 -0
  35. {mx_remote-9.1.0 → mx_remote-9.3.0}/docs/devices-and-bays.md +0 -0
  36. {mx_remote-9.1.0 → mx_remote-9.3.0}/docs/diagnostics.md +0 -0
  37. {mx_remote-9.1.0 → mx_remote-9.3.0}/docs/index.md +0 -0
  38. {mx_remote-9.1.0 → mx_remote-9.3.0}/docs/multiviewer.md +0 -0
  39. {mx_remote-9.1.0 → mx_remote-9.3.0}/docs/requirements.txt +0 -0
  40. {mx_remote-9.1.0 → mx_remote-9.3.0}/docs/routing.md +0 -0
  41. {mx_remote-9.1.0 → mx_remote-9.3.0}/hatch_build.py +0 -0
  42. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/Uid.py +0 -0
  43. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/Uid.pyi +0 -0
  44. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/__init__.py +0 -0
  45. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/__init__.pyi +0 -0
  46. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/compat.py +0 -0
  47. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/compat.pyi +0 -0
  48. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/const.pyi +0 -0
  49. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/deprecated.py +0 -0
  50. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/deprecated.pyi +0 -0
  51. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/main.py +0 -0
  52. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/main.pyi +0 -0
  53. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/BayConfig.py +0 -0
  54. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/BayConfig.pyi +0 -0
  55. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/Data.py +0 -0
  56. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/Data.pyi +0 -0
  57. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/Factory.pyi +0 -0
  58. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameAmpDolbySettings.py +0 -0
  59. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameAmpDolbySettings.pyi +0 -0
  60. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameAmpZoneSettings.py +0 -0
  61. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameAmpZoneSettings.pyi +0 -0
  62. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameAudioClip.py +0 -0
  63. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameAudioClip.pyi +0 -0
  64. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameAudioSetRoute.py +0 -0
  65. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameAudioSetRoute.pyi +0 -0
  66. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameBase.py +0 -0
  67. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameBase.pyi +0 -0
  68. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameBayConfig.py +0 -0
  69. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameBayConfig.pyi +0 -0
  70. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameBayConfigSecondary.py +0 -0
  71. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameBayConfigSecondary.pyi +0 -0
  72. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameBayContent.py +0 -0
  73. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameBayContent.pyi +0 -0
  74. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameBayHide.py +0 -0
  75. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameBayHide.pyi +0 -0
  76. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameBayStatus.py +0 -0
  77. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameBayStatus.pyi +0 -0
  78. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameConnectStatus.py +0 -0
  79. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameConnectStatus.pyi +0 -0
  80. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameDebug.py +0 -0
  81. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameDebug.pyi +0 -0
  82. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameDiscover.py +0 -0
  83. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameDiscover.pyi +0 -0
  84. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameEDID.py +0 -0
  85. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameEDID.pyi +0 -0
  86. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameEDIDProfile.py +0 -0
  87. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameEDIDProfile.pyi +0 -0
  88. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameFactoryReset.py +0 -0
  89. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameFactoryReset.pyi +0 -0
  90. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameFilterStatus.py +0 -0
  91. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameFilterStatus.pyi +0 -0
  92. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameFirmwareVersion.py +0 -0
  93. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameFirmwareVersion.pyi +0 -0
  94. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameHeader.py +0 -0
  95. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameHeader.pyi +0 -0
  96. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameHello.py +0 -0
  97. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameHello.pyi +0 -0
  98. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameLinks.py +0 -0
  99. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameLinks.pyi +0 -0
  100. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameMeshOperation.py +0 -0
  101. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameMeshOperation.pyi +0 -0
  102. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameMirrorStatus.py +0 -0
  103. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameMirrorStatus.pyi +0 -0
  104. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameMonitoringPulse.py +0 -0
  105. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameMonitoringPulse.pyi +0 -0
  106. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameNetworkStatus.py +0 -0
  107. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameNetworkStatus.pyi +0 -0
  108. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FramePing.py +0 -0
  109. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FramePing.pyi +0 -0
  110. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FramePowerChange.py +0 -0
  111. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FramePowerChange.pyi +0 -0
  112. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameRCAction.py +0 -0
  113. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameRCAction.pyi +0 -0
  114. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameRCIr.py +0 -0
  115. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameRCIr.pyi +0 -0
  116. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameRCKey.py +0 -0
  117. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameRCKey.pyi +0 -0
  118. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameRCSettings.py +0 -0
  119. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameRCSettings.pyi +0 -0
  120. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameReboot.py +0 -0
  121. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameReboot.pyi +0 -0
  122. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameRoutingChange.py +0 -0
  123. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameRoutingChange.pyi +0 -0
  124. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSetInstaller.py +0 -0
  125. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSetInstaller.pyi +0 -0
  126. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSetName.py +0 -0
  127. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSetName.pyi +0 -0
  128. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSetRoute.py +0 -0
  129. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSetRoute.pyi +0 -0
  130. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSetupStatus.py +0 -0
  131. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSetupStatus.pyi +0 -0
  132. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSignalStatus.py +0 -0
  133. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSignalStatus.pyi +0 -0
  134. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSignalStatusNew.py +0 -0
  135. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSignalStatusNew.pyi +0 -0
  136. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSysTemperature.py +0 -0
  137. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSysTemperature.pyi +0 -0
  138. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSystemStatus.py +0 -0
  139. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameSystemStatus.pyi +0 -0
  140. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameTXRCAction.py +0 -0
  141. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameTXRCAction.pyi +0 -0
  142. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameTXRCKey.py +0 -0
  143. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameTXRCKey.pyi +0 -0
  144. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameTime.py +0 -0
  145. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameTime.pyi +0 -0
  146. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameTimeZone.py +0 -0
  147. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameTimeZone.pyi +0 -0
  148. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameTopology.py +0 -0
  149. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameTopology.pyi +0 -0
  150. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameTxIR.py +0 -0
  151. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameTxIR.pyi +0 -0
  152. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameUnitCommand.py +0 -0
  153. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameUnitCommand.pyi +0 -0
  154. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameUnitStatus.py +0 -0
  155. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameUnitStatus.pyi +0 -0
  156. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameUpgradeFPGA.py +0 -0
  157. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameUpgradeFPGA.pyi +0 -0
  158. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPAudio.py +0 -0
  159. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPAudio.pyi +0 -0
  160. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPBayMapping.py +0 -0
  161. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPBayMapping.pyi +0 -0
  162. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPDetectBays.py +0 -0
  163. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPDetectBays.pyi +0 -0
  164. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.py +0 -0
  165. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.pyi +0 -0
  166. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPLink.py +0 -0
  167. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPLink.pyi +0 -0
  168. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.py +0 -0
  169. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.pyi +0 -0
  170. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPMultiviewer.py +0 -0
  171. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPMultiviewer.pyi +0 -0
  172. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPOsdSync.py +0 -0
  173. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPOsdSync.pyi +0 -0
  174. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPPowerSave.py +0 -0
  175. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPPowerSave.pyi +0 -0
  176. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPPrivacy.py +0 -0
  177. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPPrivacy.pyi +0 -0
  178. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPSetMaster.py +0 -0
  179. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPSetMaster.pyi +0 -0
  180. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPSettingsAll.py +0 -0
  181. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPSettingsAll.pyi +0 -0
  182. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPSinkSdp.py +0 -0
  183. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPSinkSdp.pyi +0 -0
  184. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPSourceSwitch.py +0 -0
  185. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPSourceSwitch.pyi +0 -0
  186. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPSources.py +0 -0
  187. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPSources.pyi +0 -0
  188. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPStats.py +0 -0
  189. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPStats.pyi +0 -0
  190. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPTestcard.py +0 -0
  191. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPTestcard.pyi +0 -0
  192. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPTiling.py +0 -0
  193. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPTiling.pyi +0 -0
  194. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPVideoWall.py +0 -0
  195. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameV2IPVideoWall.pyi +0 -0
  196. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameVolume.py +0 -0
  197. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameVolume.pyi +0 -0
  198. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameVolumeDown.py +0 -0
  199. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameVolumeDown.pyi +0 -0
  200. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameVolumeSet.py +0 -0
  201. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameVolumeSet.pyi +0 -0
  202. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameVolumeUp.py +0 -0
  203. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/FrameVolumeUp.pyi +0 -0
  204. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/LinkConfig.py +0 -0
  205. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/LinkConfig.pyi +0 -0
  206. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/Multiviewer.py +0 -0
  207. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/Multiviewer.pyi +0 -0
  208. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/Svd.py +0 -0
  209. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/Svd.pyi +0 -0
  210. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/V2IPConfig.py +0 -0
  211. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/V2IPConfig.pyi +0 -0
  212. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/V2IPStats.py +0 -0
  213. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/V2IPStats.pyi +0 -0
  214. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/__init__.py +0 -0
  215. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/__init__.pyi +0 -0
  216. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/proto/svd.csv +0 -0
  217. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/py.typed +0 -0
  218. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/remote/Bay.py +0 -0
  219. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/remote/Bay.pyi +0 -0
  220. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/remote/ConnectionAsync.py +0 -0
  221. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/remote/ConnectionAsync.pyi +0 -0
  222. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/remote/Link.py +0 -0
  223. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/remote/Link.pyi +0 -0
  224. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/remote/OsdSync.py +0 -0
  225. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/remote/OsdSync.pyi +0 -0
  226. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/remote/State.py +0 -0
  227. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/remote/State.pyi +0 -0
  228. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/remote/V2IP.py +0 -0
  229. {mx_remote-9.1.0 → mx_remote-9.3.0}/mx_remote/remote/V2IP.pyi +0 -0
  230. {mx_remote-9.1.0 → mx_remote-9.3.0}/pyproject.toml +0 -0
  231. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/audio_ir.py +0 -0
  232. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/audiolock.py +0 -0
  233. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/baycontent.py +0 -0
  234. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/capture.py +0 -0
  235. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/complete.py +0 -0
  236. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/deprecated.py +0 -0
  237. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/downmix.py +0 -0
  238. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/e2e.py +0 -0
  239. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/entry.py +0 -0
  240. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/handlers_cover.py +0 -0
  241. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/hello.py +0 -0
  242. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/idempotent.py +0 -0
  243. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/lengths.py +0 -0
  244. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/merge.py +0 -0
  245. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/mesh.py +0 -0
  246. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/meshbays.py +0 -0
  247. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/newops.py +0 -0
  248. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/offline.py +0 -0
  249. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/offsets.py +0 -0
  250. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/osdmessage.py +0 -0
  251. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/osdsync.py +0 -0
  252. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/ping.py +0 -0
  253. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/privacy.py +0 -0
  254. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/proto_stamp.py +0 -0
  255. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/protogate.py +0 -0
  256. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/rcsettings.py +0 -0
  257. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/roundtrip.py +0 -0
  258. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/routes.py +0 -0
  259. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/sources.py +0 -0
  260. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/staterequest.py +0 -0
  261. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/stats.py +0 -0
  262. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/structs.py +0 -0
  263. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/txresult.py +0 -0
  264. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/uid.py +0 -0
  265. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/unit.py +0 -0
  266. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/unknowns.py +0 -0
  267. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/v2ipptp.py +0 -0
  268. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/v2ipscaling.py +0 -0
  269. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/v2ipsdp.py +0 -0
  270. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/v2ipsubject.py +0 -0
  271. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/v2iptestcard.py +0 -0
  272. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/v2ipvlan.py +0 -0
  273. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/vdetails.py +0 -0
  274. {mx_remote-9.1.0 → mx_remote-9.3.0}/tests/wirefix.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mx_remote
3
- Version: 9.1.0
3
+ Version: 9.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/
@@ -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)
@@ -225,27 +225,36 @@ goes out at 176 bytes: a receiver with the schedule ignores the settings of a
225
225
  shorter frame, and one whose settings end at the idle minutes takes it all the
226
226
  same.
227
227
 
228
- How far a device powers down is its power save level: reduced, low or off, each
229
- powering more down than the one before. Its unit status says which levels it
230
- supports and what wakes it at each.
228
+ Power save is switched on or off. Switched on, a device powers down by itself
229
+ to its power save level: reduced, low or off, each powering more down than the
230
+ one before. The device picks the level itself, the one that saves the most
231
+ while its input and display can still wake it, and reports it. Its unit status
232
+ says which levels it supports and what wakes it at each.
231
233
 
232
234
  ```python
233
235
  from mx_remote import OneIPPowerSaveLevel, UnitPowerSave
234
236
 
235
- print(settings.power_save_level)
237
+ print(settings.get(OneIPDeviceSetting.POWER_SAVE_DISABLED)) # False: switched on
238
+ print(settings.power_save_level) # the level it picked
236
239
  print(device.unit_status.power_save_level(OneIPPowerSaveLevel.LOW)) # e.g. SUPPORTED|INPUT_WAKE
237
240
 
238
- await device.set_oneip_power_save_level(OneIPPowerSaveLevel.LOW)
241
+ await device.set_oneip_power_save(True)
242
+ await mx.set_all_oneip_power_save(False) # every OneIP device of the mesh
239
243
  ```
240
244
 
241
- A level this library does not name is refused before it is sent, and not cached
242
- from another controller's write.
243
-
244
- `OneIPDeviceSetting.POWER_SAVE_DISABLED` is a switch that keeps a device from
245
- powering down by itself; a power save request still powers it down. A device
246
- that takes a power save level switches it off, so a level write, this client's
247
- or another sender's, clears it unless the same frame states it. Firmware
248
- without the bit never reports it, and the switch is refused there.
245
+ Switched off, `OneIPDeviceSetting.POWER_SAVE_DISABLED` is on: the device does
246
+ not power down by itself, and a power save request still powers it down.
247
+ Switching on is written as a mesh controller writes it, with that switch clear
248
+ and the power save level at 0, so a device whose firmware still takes the level
249
+ as its setting returns to its own default.
250
+
251
+ `set_oneip_power_save_level()` writes a level, which switches power save on. A
252
+ current device ignores the level; one whose firmware predates picking it takes
253
+ it. A written level is never cached, from this client or another controller:
254
+ `power_save_level` is what the device reports. A level write without the switch
255
+ only clears `POWER_SAVE_DISABLED` on a device that reports it. A level this
256
+ library does not name is refused before it is sent. Firmware without the switch
257
+ never reports it, and switching is refused there.
249
258
 
250
259
  `OneIPDeviceSetting.CLOUD` is a switch for whether a device connects to the
251
260
  cloud service. A device that does not report it connects regardless, and the
@@ -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."""
@@ -2712,6 +2774,16 @@ class OneIPDeviceSettings:
2712
2774
  self._power_save_level = int(power_save_level)
2713
2775
  self._ptp_messages = ptp_messages if (ptp_messages is not None) else OneIPPtpMessages()
2714
2776
 
2777
+ @classmethod
2778
+ def power_save_switch(cls, enabled:bool) -> 'OneIPDeviceSettings':
2779
+ """The settings that switch a device's power save on or off. On is both
2780
+ POWER_SAVE_DISABLED, clear, and POWER_SAVE_LEVEL, at 0."""
2781
+ disabled = OneIPDeviceSetting.POWER_SAVE_DISABLED
2782
+ if enabled:
2783
+ return cls(valid=(disabled | OneIPDeviceSetting.POWER_SAVE_LEVEL),
2784
+ power_save_level=OneIPPowerSaveLevel.NONE)
2785
+ return cls(valid=disabled, flags=disabled)
2786
+
2715
2787
  @property
2716
2788
  def valid(self) -> OneIPDeviceSetting:
2717
2789
  """The settings reported so far."""
@@ -2791,8 +2863,9 @@ class OneIPDeviceSettings:
2791
2863
 
2792
2864
  @property
2793
2865
  def power_save_level(self) -> OneIPPowerSaveLevel|int|None:
2794
- """The level the device powers down to in power save, a raw int for one
2795
- this library does not name, None while it is not reported."""
2866
+ """The level the device powers down to in power save, which it picks
2867
+ itself, a raw int for one this library does not name, None while it is
2868
+ not reported."""
2796
2869
  if (OneIPDeviceSetting.POWER_SAVE_LEVEL not in self._valid):
2797
2870
  return None
2798
2871
  return _wire_enum(OneIPPowerSaveLevel, self._power_save_level)
@@ -2812,18 +2885,14 @@ class OneIPDeviceSettings:
2812
2885
 
2813
2886
  Each bit in this block's ``valid`` replaces its own setting and leaves
2814
2887
  the others alone, so a write about one setting does not clear what was
2815
- known about the rest. The exception is POWER_SAVE_DISABLED: a device
2816
- takes a power save level as switching it off.
2888
+ known about the rest.
2817
2889
  """
2818
2890
  if (previous is None):
2819
2891
  previous = OneIPDeviceSettings()
2820
2892
  valid = self._valid
2821
- flags = (previous._flags & ~valid) | (self._flags & valid)
2822
- if (OneIPDeviceSetting.POWER_SAVE_LEVEL in valid) and (OneIPDeviceSetting.POWER_SAVE_DISABLED not in valid):
2823
- flags &= ~OneIPDeviceSetting.POWER_SAVE_DISABLED
2824
2893
  return OneIPDeviceSettings(
2825
2894
  valid=(previous._valid | valid),
2826
- flags=flags,
2895
+ flags=((previous._flags & ~valid) | (self._flags & valid)),
2827
2896
  ir_profiles=(self._ir_profiles if (OneIPDeviceSetting.IR_PROFILES in valid)
2828
2897
  else previous._ir_profiles),
2829
2898
  ir_profile=(self._ir_profile if (OneIPDeviceSetting.IR_PROFILE in valid)
@@ -2850,9 +2919,11 @@ class OneIPDeviceSettings:
2850
2919
  write about it.
2851
2920
 
2852
2921
  A device applies a setting only if it has it, a profile, PTP mode, PTP
2853
- domain, power save level or PTP DSCP only within its range, a stream
2854
- TTL only when it is not 0, a rotation only by its own step, and never
2855
- what only it can report about itself. The step stays the device's.
2922
+ domain or PTP DSCP only within its range, a stream TTL only when it is
2923
+ not 0, a rotation only by its own step, and never what only it can
2924
+ report about itself. The step stays the device's. It picks its power
2925
+ save level itself, and takes a level it is written, without
2926
+ POWER_SAVE_DISABLED, as switching power save on.
2856
2927
 
2857
2928
  reported may be only the bits the device reported, which says nothing
2858
2929
  of its step, so a rotation other than 0 is then left out.
@@ -2871,15 +2942,20 @@ class OneIPDeviceSettings:
2871
2942
  valid &= ~OneIPDeviceSetting.PTP_MODE
2872
2943
  if (self._ptp_domain > ONEIP_PTP_DOMAIN_MAX):
2873
2944
  valid &= ~OneIPDeviceSetting.PTP_DOMAIN
2874
- if (self._power_save_level > OneIPPowerSaveLevel.OFF):
2875
- valid &= ~OneIPDeviceSetting.POWER_SAVE_LEVEL
2876
2945
  if (self._ptp_messages.dscp > ONEIP_DSCP_MAX):
2877
2946
  valid &= ~OneIPDeviceSetting.PTP_DSCP
2878
2947
  if (self._ptp_messages.stream_ttl == 0):
2879
2948
  valid &= ~OneIPDeviceSetting.STREAM_TTL
2880
2949
  if not own.takes_sink_rotation(self._ptp_messages.sink_rotation):
2881
2950
  valid &= ~OneIPDeviceSetting.SINK_ROTATION
2882
- return OneIPDeviceSettings(valid=valid, flags=self._flags, ir_profiles=self._ir_profiles,
2951
+ flags = self._flags
2952
+ if (OneIPDeviceSetting.POWER_SAVE_LEVEL in valid):
2953
+ valid &= ~OneIPDeviceSetting.POWER_SAVE_LEVEL
2954
+ if (OneIPDeviceSetting.POWER_SAVE_DISABLED not in valid) \
2955
+ and (OneIPDeviceSetting.POWER_SAVE_DISABLED in reported._valid):
2956
+ valid |= OneIPDeviceSetting.POWER_SAVE_DISABLED
2957
+ flags &= ~OneIPDeviceSetting.POWER_SAVE_DISABLED
2958
+ return OneIPDeviceSettings(valid=valid, flags=flags, ir_profiles=self._ir_profiles,
2883
2959
  ir_profile=self._ir_profile, ir_profile_sink=self._ir_profile_sink,
2884
2960
  auto_power_save=self._auto_power_save, power_save=self._power_save,
2885
2961
  ptp_mode=self._ptp_mode, ptp_domain=self._ptp_domain,
@@ -3572,9 +3648,13 @@ class DeviceBase(ABC):
3572
3648
  async def set_oneip_ptp_priority1(self, priority1:int) -> bool:
3573
3649
  '''set the priority1 the device announces as a PTP grandmaster'''
3574
3650
 
3651
+ @abstractmethod
3652
+ async def set_oneip_power_save(self, enabled:bool) -> bool:
3653
+ '''switch power save on, so the device powers down by itself to the level it picks, or off'''
3654
+
3575
3655
  @abstractmethod
3576
3656
  async def set_oneip_power_save_level(self, level:OneIPPowerSaveLevel) -> bool:
3577
- '''set the level the device powers down to in power save'''
3657
+ '''write the level the device powers down to in power save, which switches power save on'''
3578
3658
 
3579
3659
  @abstractmethod
3580
3660
  async def set_oneip_ptp_messages(self, which:OneIPDeviceSetting, messages:OneIPPtpMessages) -> bool:
@@ -4352,6 +4432,13 @@ class MxrCallbacks:
4352
4432
  _LOGGER.debug(f"{device} dolby settings changed")
4353
4433
  self.on_device_update(device)
4354
4434
 
4435
+ def on_third_party_message(self, dev:DeviceBase, message:ThirdPartyMessage) -> None:
4436
+ ''' called when the application on 'dev' sent a message under its own
4437
+ vendor id. A message from a sender this client has not yet heard
4438
+ announce itself is dropped, as every frame is, and this client never
4439
+ receives its own. '''
4440
+ pass
4441
+
4355
4442
  # The V2IP spelling of a name here, from before the rename to OneIP.
4356
4443
  from .deprecated import alias_members as _alias_members, module_getattr as _module_getattr
4357
4444
  # 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 = ...
@@ -962,6 +979,8 @@ class OneIPTestcard:
962
979
 
963
980
  class OneIPDeviceSettings:
964
981
  def __init__(self, valid: OneIPDeviceSetting = ..., flags: OneIPDeviceSetting = ..., ir_profiles: int = 0, ir_profile: int = 0, ir_profile_sink: int = 0, auto_power_save: int = 0, power_save: OneIPPowerSaveSchedule | None = None, ptp_mode: OneIPPtpMode | int = ..., ptp_domain: int = 0, ptp_priority1: int = 0, power_save_level: OneIPPowerSaveLevel | int = ..., ptp_messages: OneIPPtpMessages | None = None) -> None: ...
982
+ @classmethod
983
+ def power_save_switch(cls, enabled: bool) -> OneIPDeviceSettings: ...
965
984
  @property
966
985
  def valid(self) -> OneIPDeviceSetting: ...
967
986
  @property
@@ -1330,6 +1349,8 @@ class DeviceBase(ABC):
1330
1349
  @abstractmethod
1331
1350
  async def set_oneip_ptp_priority1(self, priority1: int) -> bool: ...
1332
1351
  @abstractmethod
1352
+ async def set_oneip_power_save(self, enabled: bool) -> bool: ...
1353
+ @abstractmethod
1333
1354
  async def set_oneip_power_save_level(self, level: OneIPPowerSaveLevel) -> bool: ...
1334
1355
  @abstractmethod
1335
1356
  async def set_oneip_ptp_messages(self, which: OneIPDeviceSetting, messages: OneIPPtpMessages) -> bool: ...
@@ -1616,3 +1637,4 @@ class MxrCallbacks:
1616
1637
  def on_rc_type_changed(self, bay: BayBase, rc_type: RCType | None) -> None: ...
1617
1638
  def on_amp_zone_settings_changed(self, bay: BayBase, settings: AmpZoneSettings) -> None: ...
1618
1639
  def on_amp_dolby_settings_changed(self, device: DeviceBase, settings: AmpDolbySettings) -> None: ...
1640
+ 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.1.0'
17
+ VERSION = '9.3.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
@@ -349,7 +352,11 @@ class OneIPDeviceSetting(IntFlag):
349
352
  AES67 = (1 << 17)
350
353
  '''Sends the source's audio as an AES67 flow, where the source allows.'''
351
354
  POWER_SAVE_LEVEL = (1 << 18)
352
- '''The power save level the device powers down to.'''
355
+ '''The level the device powers down to in power save. A device picks it
356
+ itself: the level that saves the most while its input and display can
357
+ still wake it. Written without POWER_SAVE_DISABLED, it switches power save
358
+ on, and the level written is ignored, except by a device whose firmware
359
+ predates picking it, which takes the level as its setting.'''
353
360
  PTP_ANNOUNCE = (1 << 19)
354
361
  '''The interval between the device's PTP announce messages.'''
355
362
  PTP_SYNC = (1 << 20)
@@ -818,6 +825,21 @@ ONEIP_VLAN_PORT_SFP = 0
818
825
  ONEIP_VLAN_ID_MAX = 4094
819
826
  """The highest VLAN id: 0 means untagged, and 4095 is reserved by IEEE 802.1Q."""
820
827
 
828
+ class ThirdPartyVendorFlag(IntFlag):
829
+ '''The first byte of a third-party vendor id, ahead of its three
830
+ identifying bytes. Bits without a name are kept as they arrived.'''
831
+ NOT_OUI = (1 << 0)
832
+ '''The three bytes are an identifier the vendor chose, rather than its
833
+ IEEE OUI.'''
834
+
835
+ THIRD_PARTY_PAYLOAD_MAX = 1444
836
+ """The largest payload a third-party message carries.
837
+
838
+ A frame this size fills one 1500-byte Ethernet frame exactly: 20 bytes of IPv4
839
+ header, 8 of UDP, the 24-byte frame header and the 4-byte vendor id leave 1444.
840
+ A larger send is refused rather than fragmented, because a lost fragment loses
841
+ the whole datagram and nothing on these paths retries."""
842
+
821
843
  BAY_FEATURE_DOLBY_IN_POS = 24
822
844
 
823
845
  class BayFeaturesMask(IntFlag):
@@ -340,6 +340,11 @@ class OneIPVlanFlag(IntFlag):
340
340
  ONEIP_VLAN_PORTS: int
341
341
  ONEIP_VLAN_PORT_SFP: int
342
342
  ONEIP_VLAN_ID_MAX: int
343
+
344
+ class ThirdPartyVendorFlag(IntFlag):
345
+ NOT_OUI: Incomplete
346
+
347
+ THIRD_PARTY_PAYLOAD_MAX: int
343
348
  BAY_FEATURE_DOLBY_IN_POS: int
344
349
 
345
350
  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: ...
@@ -76,7 +76,6 @@ from typing import Any, Callable
76
76
  from ..compat import override
77
77
  from datetime import datetime
78
78
  from enum import Enum
79
- import dataclasses
80
79
  import logging
81
80
  import time
82
81
 
@@ -1572,12 +1571,28 @@ class Device(DeviceBase):
1572
1571
  return self._send_v2ip_settings(OneIPDeviceSettings(
1573
1572
  valid=OneIPDeviceSetting.PTP_PRIORITY1, ptp_priority1=priority1))
1574
1573
 
1575
- async def set_oneip_power_save_level(self, level:OneIPPowerSaveLevel) -> bool:
1576
- '''Set the level this OneIP device powers down to in power save.
1574
+ async def set_oneip_power_save(self, enabled:bool) -> bool:
1575
+ '''Switch this OneIP device's power save on, so it powers down by itself
1576
+ to the level it picks, or off, so it powers down only when asked.
1577
1577
 
1578
- A level this library does not name is refused, since a device ignores it.
1579
- One the unit does not offer is one its unit_status reports without
1580
- UnitPowerSave.SUPPORTED. The terms of set_oneip_setting() apply.
1578
+ On is written as a mesh controller writes it: both POWER_SAVE_DISABLED,
1579
+ clear, and POWER_SAVE_LEVEL, at 0, so a device whose firmware takes the
1580
+ level as its setting returns to its own default. The terms of
1581
+ set_oneip_setting() apply.
1582
+ '''
1583
+ return self._send_v2ip_settings(OneIPDeviceSettings.power_save_switch(enabled))
1584
+
1585
+ async def set_oneip_power_save_level(self, level:OneIPPowerSaveLevel) -> bool:
1586
+ '''Write the level this OneIP device powers down to in power save, which
1587
+ switches power save on.
1588
+
1589
+ A device picks its level itself and ignores the one written, except one
1590
+ whose firmware predates picking it, which takes the level as its
1591
+ setting. A level that unit does not offer is one its unit_status
1592
+ reports without UnitPowerSave.SUPPORTED. A level this library does not
1593
+ name is refused. The level is not cached: oneip_settings reads what the
1594
+ device reports. set_oneip_power_save() switches power save on or off
1595
+ without naming a level. The terms of set_oneip_setting() apply.
1581
1596
  '''
1582
1597
  if not (OneIPPowerSaveLevel.NONE <= level <= OneIPPowerSaveLevel.OFF):
1583
1598
  _LOGGER.warning(f"not setting power save level {level} on {self}: no such level")
@@ -1612,7 +1627,6 @@ class Device(DeviceBase):
1612
1627
  _LOGGER.warning(f"not turning the output of {self} by {messages.sink_rotation} degrees: "
1613
1628
  f"it turns by steps of {own.sink_rotation_step}")
1614
1629
  return False
1615
- messages = dataclasses.replace(messages, sink_rotation_step=own.sink_rotation_step)
1616
1630
  return self._send_v2ip_settings(OneIPDeviceSettings(valid=which, ptp_messages=messages))
1617
1631
 
1618
1632
  def _on_oneip_downmix(self, downmix:OneIPDownmix|None, reported:bool) -> None:
@@ -1932,7 +1946,7 @@ class Device(DeviceBase):
1932
1946
  return False
1933
1947
  if self.registry.transmit(frame.frame) != len(frame.frame):
1934
1948
  return False
1935
- self._merge_v2ip_settings(settings)
1949
+ self._merge_v2ip_settings(settings.as_applied_to(reported))
1936
1950
  return True
1937
1951
 
1938
1952
  @property
@@ -270,6 +270,7 @@ class Device(DeviceBase):
270
270
  async def set_oneip_ptp_mode(self, mode: OneIPPtpMode) -> bool: ...
271
271
  async def set_oneip_ptp_domain(self, domain: int) -> bool: ...
272
272
  async def set_oneip_ptp_priority1(self, priority1: int) -> bool: ...
273
+ async def set_oneip_power_save(self, enabled: bool) -> bool: ...
273
274
  async def set_oneip_power_save_level(self, level: OneIPPowerSaveLevel) -> bool: ...
274
275
  async def set_oneip_ptp_messages(self, which: OneIPDeviceSetting, messages: OneIPPtpMessages) -> bool: ...
275
276
  async def set_oneip_downmix(self, downmix: OneIPDownmix) -> bool: ...
@@ -22,12 +22,13 @@ from datetime import datetime, timezone
22
22
  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
- from ..Interface import ConnectionCallbacks, DeviceRegistry, MxrDeviceUid, BayLinks, BayBase, DeviceBase, MxrCallbacks, AudioEndpoint, mxr_broadcast_address, OneIPDeviceSettings, OneIPPowerSaveSchedule, OneIPPtpMessages, OsdSyncContent, OsdSyncRequest
26
- from ..proto.Constants import MXR_PROTOCOL_VERSION, DeviceFeature, OsdSyncWhat
25
+ from ..Interface import ConnectionCallbacks, DeviceRegistry, MxrDeviceUid, BayLinks, BayBase, DeviceBase, MxrCallbacks, AudioEndpoint, mxr_broadcast_address, OneIPDeviceSettings, OneIPPowerSaveSchedule, OneIPPtpMessages, OsdSyncContent, OsdSyncRequest, ThirdPartyVendor
26
+ from ..proto.Constants import MXR_PROTOCOL_VERSION, THIRD_PARTY_PAYLOAD_MAX, DeviceFeature, OsdSyncWhat
27
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
31
+ from ..proto.FrameThirdParty import FrameThirdParty
31
32
  from ..proto.FrameBase import FrameBase
32
33
  from ..proto.FrameTime import FrameTime
33
34
  from ..proto.FrameTimeZone import FrameTimeZone
@@ -561,12 +562,35 @@ class Remote(DeviceRegistry, ConnectionCallbacks):
561
562
  return False
562
563
  return self._send_to_all(FrameV2IPSettingsAll.construct(mxr=self, settings=settings))
563
564
 
565
+ async def set_all_oneip_power_save(self, enabled:bool) -> bool:
566
+ '''Switch power save on or off on every OneIP device of the mesh with
567
+ one frame, written as Device.set_oneip_power_save() writes it.
568
+ Otherwise as set_all_oneip_device_settings().'''
569
+ return await self.set_all_oneip_device_settings(OneIPDeviceSettings.power_save_switch(enabled))
570
+
564
571
  async def request_all_oneip_privacy(self) -> bool:
565
572
  '''Ask every OneIP source unit of the mesh how its streams are
566
573
  encrypted, as Device.request_oneip_privacy() asks one. A unit below
567
574
  protocol 0x30 ignores it.'''
568
575
  return self._send_to_all(FrameV2IPPrivacy.construct_request(mxr=self, target=None, target_uid=None))
569
576
 
577
+ async def send_third_party(self, vendor:ThirdPartyVendor, payload:bytes) -> bool:
578
+ '''Send an application's own message to every device, under vendor.
579
+ True when all of it was written.
580
+
581
+ Every other client of this library that has heard this one announce
582
+ itself receives it at MxrCallbacks.on_third_party_message(); this client
583
+ does not, and neither does one sharing its uid. No unit is meant to read
584
+ it. Nothing acknowledges it, so an application that needs to know it
585
+ arrived has to have its peers answer.
586
+
587
+ A payload over THIRD_PARTY_PAYLOAD_MAX bytes is refused.'''
588
+ if (len(payload) > THIRD_PARTY_PAYLOAD_MAX):
589
+ _LOGGER.warning(f"not sending a third-party message: {len(payload)} bytes, "
590
+ f"the most is {THIRD_PARTY_PAYLOAD_MAX}")
591
+ return False
592
+ return self._send_to_all(FrameThirdParty.construct(mxr=self, vendor=vendor, payload=payload))
593
+
570
594
  async def set_mesh_time_zone(self, zone:str, rule:str) -> bool:
571
595
  '''Set the time zone of every device that hears it.
572
596