mx-remote 8.1.0__tar.gz → 9.1.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 (271) hide show
  1. {mx_remote-8.1.0 → mx_remote-9.1.0}/PKG-INFO +1 -1
  2. {mx_remote-8.1.0 → mx_remote-9.1.0}/docs/api.md +36 -0
  3. {mx_remote-8.1.0 → mx_remote-9.1.0}/docs/oneip.md +64 -5
  4. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/Interface.py +139 -13
  5. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/Interface.pyi +36 -1
  6. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/Uid.py +11 -8
  7. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/const.py +1 -1
  8. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/Constants.py +53 -3
  9. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/Constants.pyi +26 -0
  10. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.py +91 -14
  11. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.pyi +8 -2
  12. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPOsdSync.py +45 -12
  13. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPOsdSync.pyi +10 -2
  14. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPSources.py +5 -6
  15. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/remote/Bay.py +2 -1
  16. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/remote/Device.py +131 -8
  17. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/remote/Device.pyi +13 -2
  18. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/remote/OsdSync.py +1 -11
  19. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/remote/Remote.py +51 -23
  20. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/remote/Remote.pyi +1 -1
  21. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/complete.py +12 -0
  22. mx_remote-9.1.0/tests/downmix.py +124 -0
  23. mx_remote-9.1.0/tests/osdmessage.py +114 -0
  24. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/osdsync.py +12 -13
  25. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/run.py +3 -0
  26. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/staterequest.py +64 -0
  27. mx_remote-9.1.0/tests/uid.py +50 -0
  28. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/v2ipsettings.py +49 -0
  29. {mx_remote-8.1.0 → mx_remote-9.1.0}/.gitignore +0 -0
  30. {mx_remote-8.1.0 → mx_remote-9.1.0}/AGENTS.md +0 -0
  31. {mx_remote-8.1.0 → mx_remote-9.1.0}/CLAUDE.md +0 -0
  32. {mx_remote-8.1.0 → mx_remote-9.1.0}/LICENSE +0 -0
  33. {mx_remote-8.1.0 → mx_remote-9.1.0}/README.md +0 -0
  34. {mx_remote-8.1.0 → mx_remote-9.1.0}/docs/README.md +0 -0
  35. {mx_remote-8.1.0 → mx_remote-9.1.0}/docs/_static/brand.css +0 -0
  36. {mx_remote-8.1.0 → mx_remote-9.1.0}/docs/_static/favicon.svg +0 -0
  37. {mx_remote-8.1.0 → mx_remote-9.1.0}/docs/_static/logo-dark.svg +0 -0
  38. {mx_remote-8.1.0 → mx_remote-9.1.0}/docs/_static/logo.svg +0 -0
  39. {mx_remote-8.1.0 → mx_remote-9.1.0}/docs/audio.md +0 -0
  40. {mx_remote-8.1.0 → mx_remote-9.1.0}/docs/callbacks.md +0 -0
  41. {mx_remote-8.1.0 → mx_remote-9.1.0}/docs/conf.py +0 -0
  42. {mx_remote-8.1.0 → mx_remote-9.1.0}/docs/configuration.md +0 -0
  43. {mx_remote-8.1.0 → mx_remote-9.1.0}/docs/devices-and-bays.md +0 -0
  44. {mx_remote-8.1.0 → mx_remote-9.1.0}/docs/diagnostics.md +0 -0
  45. {mx_remote-8.1.0 → mx_remote-9.1.0}/docs/index.md +0 -0
  46. {mx_remote-8.1.0 → mx_remote-9.1.0}/docs/multiviewer.md +0 -0
  47. {mx_remote-8.1.0 → mx_remote-9.1.0}/docs/requirements.txt +0 -0
  48. {mx_remote-8.1.0 → mx_remote-9.1.0}/docs/routing.md +0 -0
  49. {mx_remote-8.1.0 → mx_remote-9.1.0}/hatch_build.py +0 -0
  50. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/Uid.pyi +0 -0
  51. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/__init__.py +0 -0
  52. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/__init__.pyi +0 -0
  53. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/compat.py +0 -0
  54. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/compat.pyi +0 -0
  55. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/const.pyi +0 -0
  56. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/deprecated.py +0 -0
  57. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/deprecated.pyi +0 -0
  58. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/main.py +0 -0
  59. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/main.pyi +0 -0
  60. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/BayConfig.py +0 -0
  61. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/BayConfig.pyi +0 -0
  62. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/Data.py +0 -0
  63. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/Data.pyi +0 -0
  64. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/Factory.py +0 -0
  65. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/Factory.pyi +0 -0
  66. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameAmpDolbySettings.py +0 -0
  67. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameAmpDolbySettings.pyi +0 -0
  68. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameAmpZoneSettings.py +0 -0
  69. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameAmpZoneSettings.pyi +0 -0
  70. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameAudioClip.py +0 -0
  71. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameAudioClip.pyi +0 -0
  72. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameAudioSetRoute.py +0 -0
  73. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameAudioSetRoute.pyi +0 -0
  74. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameBase.py +0 -0
  75. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameBase.pyi +0 -0
  76. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameBayConfig.py +0 -0
  77. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameBayConfig.pyi +0 -0
  78. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameBayConfigSecondary.py +0 -0
  79. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameBayConfigSecondary.pyi +0 -0
  80. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameBayContent.py +0 -0
  81. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameBayContent.pyi +0 -0
  82. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameBayHide.py +0 -0
  83. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameBayHide.pyi +0 -0
  84. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameBayStatus.py +0 -0
  85. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameBayStatus.pyi +0 -0
  86. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameConnectStatus.py +0 -0
  87. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameConnectStatus.pyi +0 -0
  88. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameDebug.py +0 -0
  89. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameDebug.pyi +0 -0
  90. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameDiscover.py +0 -0
  91. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameDiscover.pyi +0 -0
  92. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameEDID.py +0 -0
  93. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameEDID.pyi +0 -0
  94. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameEDIDProfile.py +0 -0
  95. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameEDIDProfile.pyi +0 -0
  96. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameFactoryReset.py +0 -0
  97. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameFactoryReset.pyi +0 -0
  98. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameFilterStatus.py +0 -0
  99. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameFilterStatus.pyi +0 -0
  100. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameFirmwareVersion.py +0 -0
  101. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameFirmwareVersion.pyi +0 -0
  102. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameHeader.py +0 -0
  103. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameHeader.pyi +0 -0
  104. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameHello.py +0 -0
  105. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameHello.pyi +0 -0
  106. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameLinks.py +0 -0
  107. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameLinks.pyi +0 -0
  108. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameMeshOperation.py +0 -0
  109. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameMeshOperation.pyi +0 -0
  110. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameMirrorStatus.py +0 -0
  111. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameMirrorStatus.pyi +0 -0
  112. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameMonitoringPulse.py +0 -0
  113. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameMonitoringPulse.pyi +0 -0
  114. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameNetworkStatus.py +0 -0
  115. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameNetworkStatus.pyi +0 -0
  116. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FramePing.py +0 -0
  117. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FramePing.pyi +0 -0
  118. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FramePowerChange.py +0 -0
  119. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FramePowerChange.pyi +0 -0
  120. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameRCAction.py +0 -0
  121. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameRCAction.pyi +0 -0
  122. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameRCIr.py +0 -0
  123. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameRCIr.pyi +0 -0
  124. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameRCKey.py +0 -0
  125. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameRCKey.pyi +0 -0
  126. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameRCSettings.py +0 -0
  127. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameRCSettings.pyi +0 -0
  128. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameReboot.py +0 -0
  129. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameReboot.pyi +0 -0
  130. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameRoutingChange.py +0 -0
  131. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameRoutingChange.pyi +0 -0
  132. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameSetInstaller.py +0 -0
  133. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameSetInstaller.pyi +0 -0
  134. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameSetName.py +0 -0
  135. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameSetName.pyi +0 -0
  136. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameSetRoute.py +0 -0
  137. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameSetRoute.pyi +0 -0
  138. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameSetupStatus.py +0 -0
  139. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameSetupStatus.pyi +0 -0
  140. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameSignalStatus.py +0 -0
  141. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameSignalStatus.pyi +0 -0
  142. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameSignalStatusNew.py +0 -0
  143. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameSignalStatusNew.pyi +0 -0
  144. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameSysTemperature.py +0 -0
  145. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameSysTemperature.pyi +0 -0
  146. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameSystemStatus.py +0 -0
  147. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameSystemStatus.pyi +0 -0
  148. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameTXRCAction.py +0 -0
  149. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameTXRCAction.pyi +0 -0
  150. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameTXRCKey.py +0 -0
  151. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameTXRCKey.pyi +0 -0
  152. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameTime.py +0 -0
  153. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameTime.pyi +0 -0
  154. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameTimeZone.py +0 -0
  155. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameTimeZone.pyi +0 -0
  156. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameTopology.py +0 -0
  157. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameTopology.pyi +0 -0
  158. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameTxIR.py +0 -0
  159. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameTxIR.pyi +0 -0
  160. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameUnitCommand.py +0 -0
  161. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameUnitCommand.pyi +0 -0
  162. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameUnitStatus.py +0 -0
  163. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameUnitStatus.pyi +0 -0
  164. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameUpgradeFPGA.py +0 -0
  165. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameUpgradeFPGA.pyi +0 -0
  166. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPAudio.py +0 -0
  167. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPAudio.pyi +0 -0
  168. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPBayMapping.py +0 -0
  169. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPBayMapping.pyi +0 -0
  170. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPDetectBays.py +0 -0
  171. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPDetectBays.pyi +0 -0
  172. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPLink.py +0 -0
  173. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPLink.pyi +0 -0
  174. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.py +0 -0
  175. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.pyi +0 -0
  176. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPMultiviewer.py +0 -0
  177. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPMultiviewer.pyi +0 -0
  178. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPPowerSave.py +0 -0
  179. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPPowerSave.pyi +0 -0
  180. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPPrivacy.py +0 -0
  181. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPPrivacy.pyi +0 -0
  182. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPSetMaster.py +0 -0
  183. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPSetMaster.pyi +0 -0
  184. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPSettingsAll.py +0 -0
  185. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPSettingsAll.pyi +0 -0
  186. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPSinkSdp.py +0 -0
  187. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPSinkSdp.pyi +0 -0
  188. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPSourceSwitch.py +0 -0
  189. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPSourceSwitch.pyi +0 -0
  190. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPSources.pyi +0 -0
  191. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPStats.py +0 -0
  192. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPStats.pyi +0 -0
  193. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPTestcard.py +0 -0
  194. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPTestcard.pyi +0 -0
  195. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPTiling.py +0 -0
  196. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPTiling.pyi +0 -0
  197. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPVideoWall.py +0 -0
  198. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameV2IPVideoWall.pyi +0 -0
  199. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameVolume.py +0 -0
  200. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameVolume.pyi +0 -0
  201. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameVolumeDown.py +0 -0
  202. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameVolumeDown.pyi +0 -0
  203. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameVolumeSet.py +0 -0
  204. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameVolumeSet.pyi +0 -0
  205. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameVolumeUp.py +0 -0
  206. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/FrameVolumeUp.pyi +0 -0
  207. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/LinkConfig.py +0 -0
  208. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/LinkConfig.pyi +0 -0
  209. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/Multiviewer.py +0 -0
  210. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/Multiviewer.pyi +0 -0
  211. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/Svd.py +0 -0
  212. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/Svd.pyi +0 -0
  213. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/V2IPConfig.py +0 -0
  214. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/V2IPConfig.pyi +0 -0
  215. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/V2IPStats.py +0 -0
  216. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/V2IPStats.pyi +0 -0
  217. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/__init__.py +0 -0
  218. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/__init__.pyi +0 -0
  219. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/proto/svd.csv +0 -0
  220. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/py.typed +0 -0
  221. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/remote/Bay.pyi +0 -0
  222. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/remote/ConnectionAsync.py +0 -0
  223. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/remote/ConnectionAsync.pyi +0 -0
  224. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/remote/Link.py +0 -0
  225. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/remote/Link.pyi +0 -0
  226. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/remote/OsdSync.pyi +0 -0
  227. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/remote/State.py +0 -0
  228. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/remote/State.pyi +0 -0
  229. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/remote/V2IP.py +0 -0
  230. {mx_remote-8.1.0 → mx_remote-9.1.0}/mx_remote/remote/V2IP.pyi +0 -0
  231. {mx_remote-8.1.0 → mx_remote-9.1.0}/pyproject.toml +0 -0
  232. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/audio_ir.py +0 -0
  233. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/audiolock.py +0 -0
  234. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/baycontent.py +0 -0
  235. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/capture.py +0 -0
  236. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/deprecated.py +0 -0
  237. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/e2e.py +0 -0
  238. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/entry.py +0 -0
  239. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/handlers.py +0 -0
  240. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/handlers_cover.py +0 -0
  241. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/hello.py +0 -0
  242. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/idempotent.py +0 -0
  243. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/lengths.py +0 -0
  244. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/merge.py +0 -0
  245. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/mesh.py +0 -0
  246. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/meshbays.py +0 -0
  247. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/newops.py +0 -0
  248. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/offline.py +0 -0
  249. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/offsets.py +0 -0
  250. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/ping.py +0 -0
  251. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/privacy.py +0 -0
  252. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/proto_stamp.py +0 -0
  253. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/protogate.py +0 -0
  254. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/rcsettings.py +0 -0
  255. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/roundtrip.py +0 -0
  256. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/routes.py +0 -0
  257. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/smoke.py +0 -0
  258. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/sources.py +0 -0
  259. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/stats.py +0 -0
  260. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/structs.py +0 -0
  261. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/txresult.py +0 -0
  262. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/unit.py +0 -0
  263. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/unknowns.py +0 -0
  264. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/v2ipptp.py +0 -0
  265. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/v2ipscaling.py +0 -0
  266. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/v2ipsdp.py +0 -0
  267. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/v2ipsubject.py +0 -0
  268. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/v2iptestcard.py +0 -0
  269. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/v2ipvlan.py +0 -0
  270. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/vdetails.py +0 -0
  271. {mx_remote-8.1.0 → mx_remote-9.1.0}/tests/wirefix.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mx_remote
3
- Version: 8.1.0
3
+ Version: 9.1.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/
@@ -85,6 +85,23 @@ place, backgrounds and logo images.
85
85
  .. autodata:: mx_remote.proto.Constants.OSD_SYNC_PERCENT_MAX
86
86
  ```
87
87
 
88
+ ## Overlay messages
89
+
90
+ The message on a unit's output, which the overlay sync opcode carries too.
91
+
92
+ ```{eval-rst}
93
+ .. autoclass:: mx_remote.OsdMessage
94
+ :members:
95
+
96
+ .. autoclass:: mx_remote.proto.Constants.OsdMessageFlag
97
+ :members:
98
+
99
+ .. autoclass:: mx_remote.proto.Constants.OsdMessagePriority
100
+ :members:
101
+
102
+ .. autodata:: mx_remote.proto.Constants.OSD_MESSAGE_TEXT_MAX
103
+ ```
104
+
88
105
  ## Device identity
89
106
 
90
107
  ```{eval-rst}
@@ -151,6 +168,23 @@ bit, and the range of the infrared profiles.
151
168
  .. autodata:: mx_remote.proto.Constants.ONEIP_VLAN_ID_MAX
152
169
  ```
153
170
 
171
+ ## OneIP stereo downmix
172
+
173
+ How a device's output mixes 5.1 and 7.1 PCM down to stereo.
174
+
175
+ ```{eval-rst}
176
+ .. autoclass:: mx_remote.OneIPDownmix
177
+ :members:
178
+
179
+ .. autoclass:: mx_remote.proto.Constants.OneIPDownmixMode
180
+ :members:
181
+
182
+ .. autoclass:: mx_remote.proto.Constants.OneIPDownmixLevel
183
+ :members:
184
+
185
+ .. autodata:: mx_remote.proto.Constants.ONEIP_DOWNMIX_LFE_MAX
186
+ ```
187
+
154
188
  ## OneIP PTP
155
189
 
156
190
  A device's PTP settings, which it reports and a controller writes, and the PTP
@@ -180,6 +214,8 @@ state only the device reports.
180
214
 
181
215
  .. autodata:: mx_remote.proto.Constants.ONEIP_STREAM_TTL_DEFAULT
182
216
 
217
+ .. autodata:: mx_remote.proto.Constants.ONEIP_SINK_ROTATION_MAX
218
+
183
219
  .. autodata:: mx_remote.proto.Constants.ONEIP_PTP_DOMAIN_MAX
184
220
  ```
185
221
 
@@ -288,10 +288,23 @@ same block, behind `SAP_EXPIRE` and `SAP_INTERVAL`, and are set the same way;
288
288
  `SAP_LISTEN` and `SAP_ANNOUNCE` are switches like the other on/off settings.
289
289
  A device with an encoder reports its streams' TTL there too, behind
290
290
  `STREAM_TTL`; a TTL of 0 is refused, as a device changes nothing for one.
291
+ A device with an HDMI output reports the angle its output is turned by,
292
+ clockwise, behind `SINK_ROTATION`, with the step its video processor turns by.
293
+ It takes 0 or a multiple of that step up to `ONEIP_SINK_ROTATION_MAX`, so any
294
+ other angle is refused before it is sent, and the step is never written.
295
+ `set_all_oneip_device_settings` can only check the range; each device judges
296
+ the step.
297
+
298
+ ```python
299
+ m = settings.ptp_messages
300
+ print(m.sink_rotation, m.sink_rotation_step) # e.g. 0 and 90
301
+ await device.set_oneip_ptp_messages(OneIPDeviceSetting.SINK_ROTATION,
302
+ OneIPPtpMessages(sink_rotation=180))
303
+ ```
291
304
 
292
305
  Only the values behind the bits named are sent. A DSCP above `ONEIP_DSCP_MAX` is
293
306
  refused, and not cached from another controller's write. A write carrying one of
294
- these runs on to 224 bytes, or 232 with the TTL; every other write keeps the 176
307
+ these runs on to 224 bytes, or 232 with the TTL or the rotation; every other write keeps the 176
295
308
  every device knows.
296
309
 
297
310
  What the device's clock is actually doing is its PTP state, which only the
@@ -305,6 +318,31 @@ if ptp is not None:
305
318
  print(ptp.grandmaster) # its clock identity, None while there is none
306
319
  ```
307
320
 
321
+ ### Stereo downmix
322
+
323
+ A device whose output can mix 5.1 and 7.1 PCM down to stereo reports how,
324
+ once its video processor has answered for it: a mode, a level and the percent
325
+ the LFE channel is mixed in at, which a controller writes, and the state and
326
+ channel count, which only the device reports. A device that reports none has
327
+ no downmix to set, so a write to one is refused, as is a mode, level or LFE
328
+ percent out of range. The write sends the mode, level and LFE together; the
329
+ state and channels are never sent.
330
+
331
+ ```python
332
+ from mx_remote import OneIPDownmix
333
+ from mx_remote.proto.Constants import OneIPDownmixMode, OneIPDownmixLevel
334
+
335
+ downmix = device.oneip_downmix # None: no report yet, or no downmix
336
+ if downmix is not None:
337
+ print(downmix.mode, downmix.level, downmix.lfe) # LFE up to ONEIP_DOWNMIX_LFE_MAX
338
+ print(downmix.state, downmix.channels) # the device's own
339
+ await device.set_oneip_downmix(OneIPDownmix(mode=OneIPDownmixMode.AUTO,
340
+ level=OneIPDownmixLevel.NORMALISED, lfe=50))
341
+ ```
342
+
343
+ The downmix ends the settings block, behind a second valid word, so the write
344
+ runs on to 248 bytes and carries no other setting.
345
+
308
346
  ### VLAN
309
347
 
310
348
  A device that announces `DeviceFeature.VLAN` tags its uplink by a VLAN
@@ -412,13 +450,34 @@ await mx.clear_osd_sync() # stop answering for it
412
450
  Each unit asks for the settings it lacks and takes only those, so this client
413
451
  holds the content and answers until it is replaced or cleared, announcing
414
452
  itself meanwhile as the mesh's overlay sync manager: a unit waking from power
415
- save then takes this content rather than the controller's. The controller
416
- still answers a unit that asks for settings without naming whose sync they are
417
- from, as an older overlay module does, with its own sync, which replaces this
418
- client's on that unit. Logo images are
453
+ save then takes this content rather than the controller's. Logo images are
419
454
  served over HTTP from this client, on `logo_port` or a free port. Content a unit
420
455
  would refuse is refused before anything is sent; see `send_osd_sync`.
421
456
 
457
+ ### Overlay messages
458
+
459
+ A unit shows a message on its output, over the picture, until it restarts. The
460
+ text is markup of at most `OSD_MESSAGE_TEXT_MAX` bytes with no control
461
+ character, shown with a logo: 0 the built-in one, -1 none, 1 to 5 a slot. Its
462
+ priority places it against the unit's own messages, such as Locate Device.
463
+
464
+ ```python
465
+ from mx_remote.proto.Constants import OsdMessagePriority
466
+
467
+ await sink.show_osd_message('Back at <b>3</b>', logo=-1, priority=OsdMessagePriority.TOP)
468
+ await sink.request_osd_message() # the unit answers with what it shows
469
+ await sink.clear_osd_message()
470
+
471
+ print(sink.osd_message) # None until the unit has answered
472
+ ```
473
+
474
+ A unit takes a message only from management, which this client announces
475
+ itself as, and answers every write and request with the message it then has;
476
+ `on_osd_message_changed` reports a change. The answer carries
477
+ `OsdMessageFlag.REFUSED` when the unit did not take the write, as when its
478
+ video processor has no message layer. A text, logo or priority a unit would
479
+ refuse is refused before it is sent, as is a device that is not a OneIP output.
480
+
422
481
  ### Stream encryption
423
482
 
424
483
  A source unit from protocol 0x30 encrypts its video, audio and ancillary
@@ -6,7 +6,7 @@
6
6
  ######################################################
7
7
 
8
8
  from abc import ABC, abstractmethod
9
- from dataclasses import dataclass
9
+ from dataclasses import dataclass, replace
10
10
  from datetime import datetime, timezone
11
11
  from enum import IntEnum
12
12
  import time
@@ -2135,6 +2135,46 @@ class OneIPPtpState:
2135
2135
  return (f"{mode} domain {self.domain} port state {self.port_state} {lock}"
2136
2136
  f"{' followed' if self.followed else ''} clock {self.clock_identity.hex(':')} grandmaster {gm}")
2137
2137
 
2138
+ @dataclass(frozen=True)
2139
+ class OneIPDownmix:
2140
+ """
2141
+ How a OneIP output mixes 5.1 and 7.1 PCM down to stereo, and whether it
2142
+ does now.
2143
+
2144
+ A device reports it once its video processor has answered for it, so never
2145
+ one whose output cannot mix down.
2146
+ """
2147
+ mode: OneIPDownmixMode|int = OneIPDownmixMode.OFF
2148
+ """When the output mixes down; a raw int for a mode this library does not
2149
+ name."""
2150
+ level: OneIPDownmixLevel|int = OneIPDownmixLevel.NORMALISED
2151
+ """How loud the mix is; a raw int for a level this library does not name."""
2152
+ lfe: int = 0
2153
+ """The percent the LFE channel is mixed in at, at most
2154
+ ONEIP_DOWNMIX_LFE_MAX; 0 leaves it out."""
2155
+ state: int = 0
2156
+ """Why the stream is or is not mixed down, as the device's own code. Only
2157
+ the device reports it."""
2158
+ channels: int = 0
2159
+ """The stream's channels while it is mixed down, else 0. Only the device
2160
+ reports it."""
2161
+
2162
+ def is_valid(self) -> bool:
2163
+ """Whether the settings a write carries are in range."""
2164
+ return ((OneIPDownmixMode.OFF <= self.mode <= OneIPDownmixMode.ALWAYS)
2165
+ and (OneIPDownmixLevel.NORMALISED <= self.level <= OneIPDownmixLevel.UNSCALED)
2166
+ and (0 <= self.lfe <= ONEIP_DOWNMIX_LFE_MAX))
2167
+
2168
+ def written(self, write:'OneIPDownmix') -> 'OneIPDownmix':
2169
+ """The settings of write applied to this report, which keeps what only
2170
+ the device reports."""
2171
+ return replace(self, mode=write.mode, level=write.level, lfe=write.lfe)
2172
+
2173
+ def __str__(self) -> str:
2174
+ mode = self.mode.name.lower() if isinstance(self.mode, OneIPDownmixMode) else self.mode
2175
+ level = self.level.name.lower() if isinstance(self.level, OneIPDownmixLevel) else self.level
2176
+ return f"{mode} {level} lfe {self.lfe}% state {self.state} channels {self.channels}"
2177
+
2138
2178
  @dataclass(frozen=True)
2139
2179
  class OneIPVlan:
2140
2180
  """
@@ -2215,8 +2255,8 @@ def _wire_enum(cls:Any, value:int) -> Any:
2215
2255
  @dataclass(frozen=True)
2216
2256
  class OneIPPtpMessages:
2217
2257
  """
2218
- How a OneIP device paces its PTP messages, its SAP timings and its streams'
2219
- TTL, which a frame carries in the same block. Each value is behind its own bit of
2258
+ How a OneIP device paces its PTP messages, its SAP timings, its streams'
2259
+ TTL and its output's rotation, which a frame carries in the same block. Each value is behind its own bit of
2220
2260
  OneIPDeviceSettings.valid, one of ONEIP_DEVICE_SETTINGS_BLOCK.
2221
2261
 
2222
2262
  Intervals are base-2 logarithms of seconds, so -3 is 125 ms and 1 is 2 s.
@@ -2241,6 +2281,19 @@ class OneIPPtpMessages:
2241
2281
  """The TTL of the device's streams, 1 to 255, behind STREAM_TTL. A device
2242
2282
  whose video processor cannot change it keeps the setting and sends
2243
2283
  ONEIP_STREAM_TTL_DEFAULT."""
2284
+ sink_rotation: int = 0
2285
+ """The degrees the device's HDMI output is turned by, clockwise, at most
2286
+ ONEIP_SINK_ROTATION_MAX, behind SINK_ROTATION."""
2287
+ sink_rotation_step: int = 0
2288
+ """The step the output turns by: the device takes only an angle that is a
2289
+ multiple of it, and 0 alone when it is 0. Only the device reports it,
2290
+ behind the same bit."""
2291
+
2292
+ def takes_sink_rotation(self, degrees:int) -> bool:
2293
+ """Whether a device reporting this step takes degrees as its output's
2294
+ rotation."""
2295
+ return (degrees == 0) or ((0 < degrees <= ONEIP_SINK_ROTATION_MAX) and (self.sink_rotation_step != 0)
2296
+ and (degrees % self.sink_rotation_step == 0))
2244
2297
 
2245
2298
  def merge(self, other:'OneIPPtpMessages', which:OneIPDeviceSetting) -> 'OneIPPtpMessages':
2246
2299
  """The values behind the bits of which copied from other, the rest kept."""
@@ -2253,7 +2306,9 @@ class OneIPPtpMessages:
2253
2306
  dscp=(other if (S.PTP_DSCP in which) else self).dscp,
2254
2307
  sap_expire_min=(other if (S.SAP_EXPIRE in which) else self).sap_expire_min,
2255
2308
  sap_interval_s=(other if (S.SAP_INTERVAL in which) else self).sap_interval_s,
2256
- stream_ttl=(other if (S.STREAM_TTL in which) else self).stream_ttl)
2309
+ stream_ttl=(other if (S.STREAM_TTL in which) else self).stream_ttl,
2310
+ sink_rotation=(other if (S.SINK_ROTATION in which) else self).sink_rotation,
2311
+ sink_rotation_step=(other if (S.SINK_ROTATION in which) else self).sink_rotation_step)
2257
2312
 
2258
2313
  def describe(self, valid:OneIPDeviceSetting) -> list[str]:
2259
2314
  """The values behind the bits of valid, one "name=value" each."""
@@ -2266,7 +2321,8 @@ class OneIPPtpMessages:
2266
2321
  (S.PTP_DSCP, 'ptp_dscp', self.dscp),
2267
2322
  (S.SAP_EXPIRE, 'sap_expire_min', self.sap_expire_min),
2268
2323
  (S.SAP_INTERVAL, 'sap_interval_s', self.sap_interval_s),
2269
- (S.STREAM_TTL, 'stream_ttl', self.stream_ttl))
2324
+ (S.STREAM_TTL, 'stream_ttl', self.stream_ttl),
2325
+ (S.SINK_ROTATION, 'sink_rotation', self.sink_rotation))
2270
2326
  if (bit in valid)]
2271
2327
 
2272
2328
  @dataclass(frozen=True)
@@ -2317,8 +2373,7 @@ class OsdSyncRequest:
2317
2373
  missing: bytes
2318
2374
  """The settings it lacks, bit i standing for setting i, 16 bytes."""
2319
2375
  source: MxrDeviceUid
2320
- """Who sent the sync it asks about. Empty when it asks for a new sync, and
2321
- from a unit whose module does not name the source."""
2376
+ """Who sent the sync it asks about. Empty when it asks for a new sync."""
2322
2377
 
2323
2378
  def is_missing(self, index:int) -> bool:
2324
2379
  """Whether the unit lacks setting index."""
@@ -2394,6 +2449,28 @@ class OsdSyncReport:
2394
2449
  reason: str
2395
2450
  """Why it failed or refused any."""
2396
2451
 
2452
+ @dataclass(frozen=True)
2453
+ class OsdMessage:
2454
+ """
2455
+ The message on a unit's output, as the unit last reported it. The unit
2456
+ holds it until it restarts.
2457
+ """
2458
+ flags: OsdMessageFlag
2459
+ """Whether a message is set and shown, whether the unit can show one, and
2460
+ whether it refused the write it answers. Without SET the other fields are
2461
+ empty."""
2462
+ logo: int
2463
+ """The logo shown with it: 0 the built-in logo, -1 none, 1 to 5 a slot."""
2464
+ priority: OsdMessagePriority|int
2465
+ """Where it stands against the unit's own messages."""
2466
+ text: str
2467
+ """Its markup."""
2468
+
2469
+ def __str__(self) -> str:
2470
+ if (OsdMessageFlag.SET not in self.flags):
2471
+ return f"none{' (refused)' if (OsdMessageFlag.REFUSED in self.flags) else ''}"
2472
+ return f"'{self.text}' logo {self.logo} {self.flags!r}"
2473
+
2397
2474
  @dataclass(frozen=True)
2398
2475
  class OsdSyncEntry:
2399
2476
  """One setting of an overlay sync this client sends."""
@@ -2767,16 +2844,25 @@ class OneIPDeviceSettings:
2767
2844
  else previous._power_save_level),
2768
2845
  ptp_messages=previous._ptp_messages.merge(self._ptp_messages, valid))
2769
2846
 
2770
- def as_applied_to(self, reported:OneIPDeviceSetting) -> 'OneIPDeviceSettings':
2847
+ def as_applied_to(self, reported:'OneIPDeviceSettings|OneIPDeviceSetting|None') -> 'OneIPDeviceSettings':
2771
2848
  """
2772
- This block limited to what a device takes from a write about it.
2849
+ This block limited to what a device that reported reported takes from a
2850
+ write about it.
2773
2851
 
2774
2852
  A device applies a setting only if it has it, a profile, PTP mode, PTP
2775
2853
  domain, power save level or PTP DSCP only within its range, a stream
2776
- TTL only when it is not 0, and never what only it can report about
2777
- itself.
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.
2856
+
2857
+ reported may be only the bits the device reported, which says nothing
2858
+ of its step, so a rotation other than 0 is then left out.
2778
2859
  """
2779
- valid = (self._valid & reported) & ~ONEIP_DEVICE_SETTINGS_REPORTED_ONLY
2860
+ if (reported is None):
2861
+ reported = OneIPDeviceSettings()
2862
+ elif not isinstance(reported, OneIPDeviceSettings):
2863
+ reported = OneIPDeviceSettings(valid=OneIPDeviceSetting(reported))
2864
+ own = reported._ptp_messages
2865
+ valid = (self._valid & reported._valid) & ~ONEIP_DEVICE_SETTINGS_REPORTED_ONLY
2780
2866
  if not (0 <= self._ir_profile < ONEIP_IR_PROFILE_MAX):
2781
2867
  valid &= ~OneIPDeviceSetting.IR_PROFILE
2782
2868
  if not (ONEIP_IR_PROFILE_NOT_SET <= self._ir_profile_sink < ONEIP_IR_PROFILE_MAX):
@@ -2791,13 +2877,16 @@ class OneIPDeviceSettings:
2791
2877
  valid &= ~OneIPDeviceSetting.PTP_DSCP
2792
2878
  if (self._ptp_messages.stream_ttl == 0):
2793
2879
  valid &= ~OneIPDeviceSetting.STREAM_TTL
2880
+ if not own.takes_sink_rotation(self._ptp_messages.sink_rotation):
2881
+ valid &= ~OneIPDeviceSetting.SINK_ROTATION
2794
2882
  return OneIPDeviceSettings(valid=valid, flags=self._flags, ir_profiles=self._ir_profiles,
2795
2883
  ir_profile=self._ir_profile, ir_profile_sink=self._ir_profile_sink,
2796
2884
  auto_power_save=self._auto_power_save, power_save=self._power_save,
2797
2885
  ptp_mode=self._ptp_mode, ptp_domain=self._ptp_domain,
2798
2886
  ptp_priority1=self._ptp_priority1,
2799
2887
  power_save_level=self._power_save_level,
2800
- ptp_messages=self._ptp_messages)
2888
+ ptp_messages=replace(self._ptp_messages,
2889
+ sink_rotation_step=own.sink_rotation_step))
2801
2890
 
2802
2891
  def __eq__(self, other:Any) -> bool:
2803
2892
  if not isinstance(other, OneIPDeviceSettings):
@@ -3190,6 +3279,14 @@ class DeviceBase(ABC):
3190
3279
  async def confirm_unit_network(self) -> bool:
3191
3280
  '''keep the unit's pending IP configuration'''
3192
3281
 
3282
+ @property
3283
+ @abstractmethod
3284
+ def oneip_downmix(self) -> OneIPDownmix|None:
3285
+ '''How this OneIP device's output mixes down to stereo, as it last
3286
+ reported, None while it reports no downmix: until its video processor
3287
+ has answered for it, and always for one whose output cannot mix
3288
+ down.'''
3289
+
3193
3290
  @property
3194
3291
  @abstractmethod
3195
3292
  def oneip_ptp(self) -> OneIPPtpState|None:
@@ -3483,6 +3580,10 @@ class DeviceBase(ABC):
3483
3580
  async def set_oneip_ptp_messages(self, which:OneIPDeviceSetting, messages:OneIPPtpMessages) -> bool:
3484
3581
  '''set how the device paces its PTP messages, its SAP timings and its streams' TTL: the values of messages behind the bits of which'''
3485
3582
 
3583
+ @abstractmethod
3584
+ async def set_oneip_downmix(self, downmix:OneIPDownmix) -> bool:
3585
+ '''set how the device's output mixes down to stereo'''
3586
+
3486
3587
  @abstractmethod
3487
3588
  async def set_oneip_vlan(self, vlan:OneIPVlan) -> bool:
3488
3589
  '''change the device's VLAN configuration'''
@@ -3505,6 +3606,26 @@ class DeviceBase(ABC):
3505
3606
  def osd_sync_report(self) -> OsdSyncReport|None:
3506
3607
  '''This unit's last report on taking an overlay sync, None until it has.'''
3507
3608
 
3609
+ @property
3610
+ @abstractmethod
3611
+ def osd_message(self) -> OsdMessage|None:
3612
+ '''The message on this unit's output, as it last reported it to this
3613
+ client, None until it has. A unit reports it only when asked, or when
3614
+ this client writes it.'''
3615
+
3616
+ @abstractmethod
3617
+ async def request_osd_message(self) -> bool:
3618
+ '''ask this unit for the message on its output'''
3619
+
3620
+ @abstractmethod
3621
+ async def show_osd_message(self, text:str, logo:int=-1,
3622
+ priority:OsdMessagePriority=OsdMessagePriority.NORMAL) -> bool:
3623
+ '''show a message on this unit's output, in place of any it has'''
3624
+
3625
+ @abstractmethod
3626
+ async def clear_osd_message(self) -> bool:
3627
+ '''take the message down from this unit's output'''
3628
+
3508
3629
  @property
3509
3630
  @abstractmethod
3510
3631
  def oneip_sink_sdp(self) -> OneIPSinkSdp|None:
@@ -4120,6 +4241,11 @@ class MxrCallbacks:
4120
4241
  _LOGGER.debug(f"{dev} overlay sync {report.seq}: {report.state!r} {report.reason}")
4121
4242
  self.on_device_update(dev)
4122
4243
 
4244
+ def on_osd_message_changed(self, dev:DeviceBase, message:OsdMessage) -> None:
4245
+ ''' called when unit 'dev' reported something different about the message on its output '''
4246
+ _LOGGER.debug(f"{dev} overlay message: {message}")
4247
+ self.on_device_update(dev)
4248
+
4123
4249
  def on_osd_sync_requested(self, dev:DeviceBase, request:OsdSyncRequest) -> None:
4124
4250
  ''' called when 'dev' asked for an overlay sync, or for settings of one '''
4125
4251
  pass
@@ -757,6 +757,16 @@ class OneIPPtpState:
757
757
  @property
758
758
  def grandmaster(self) -> bytes | None: ...
759
759
 
760
+ @dataclass(frozen=True)
761
+ class OneIPDownmix:
762
+ mode: OneIPDownmixMode | int = ...
763
+ level: OneIPDownmixLevel | int = ...
764
+ lfe: int = ...
765
+ state: int = ...
766
+ channels: int = ...
767
+ def is_valid(self) -> bool: ...
768
+ def written(self, write: OneIPDownmix) -> OneIPDownmix: ...
769
+
760
770
  @dataclass(frozen=True)
761
771
  class OneIPVlan:
762
772
  flags: OneIPVlanFlag = ...
@@ -787,6 +797,9 @@ class OneIPPtpMessages:
787
797
  sap_expire_min: int = ...
788
798
  sap_interval_s: int = ...
789
799
  stream_ttl: int = ...
800
+ sink_rotation: int = ...
801
+ sink_rotation_step: int = ...
802
+ def takes_sink_rotation(self, degrees: int) -> bool: ...
790
803
  def merge(self, other: OneIPPtpMessages, which: OneIPDeviceSetting) -> OneIPPtpMessages: ...
791
804
  def describe(self, valid: OneIPDeviceSetting) -> list[str]: ...
792
805
 
@@ -853,6 +866,13 @@ class OsdSyncReport:
853
866
  refused: int
854
867
  reason: str
855
868
 
869
+ @dataclass(frozen=True)
870
+ class OsdMessage:
871
+ flags: OsdMessageFlag
872
+ logo: int
873
+ priority: OsdMessagePriority | int
874
+ text: str
875
+
856
876
  @dataclass(frozen=True)
857
877
  class OsdSyncEntry:
858
878
  value: OsdSyncValue
@@ -968,7 +988,7 @@ class OneIPDeviceSettings:
968
988
  @property
969
989
  def ptp_messages(self) -> OneIPPtpMessages | None: ...
970
990
  def merge(self, previous: OneIPDeviceSettings | None) -> OneIPDeviceSettings: ...
971
- def as_applied_to(self, reported: OneIPDeviceSetting) -> OneIPDeviceSettings: ...
991
+ def as_applied_to(self, reported: OneIPDeviceSettings | OneIPDeviceSetting | None) -> OneIPDeviceSettings: ...
972
992
  def __eq__(self, other: Any) -> bool: ...
973
993
  def __ne__(self, other: Any) -> bool: ...
974
994
 
@@ -1173,6 +1193,9 @@ class DeviceBase(ABC):
1173
1193
  async def confirm_unit_network(self) -> bool: ...
1174
1194
  @property
1175
1195
  @abstractmethod
1196
+ def oneip_downmix(self) -> OneIPDownmix | None: ...
1197
+ @property
1198
+ @abstractmethod
1176
1199
  def oneip_ptp(self) -> OneIPPtpState | None: ...
1177
1200
  @property
1178
1201
  @abstractmethod
@@ -1311,6 +1334,8 @@ class DeviceBase(ABC):
1311
1334
  @abstractmethod
1312
1335
  async def set_oneip_ptp_messages(self, which: OneIPDeviceSetting, messages: OneIPPtpMessages) -> bool: ...
1313
1336
  @abstractmethod
1337
+ async def set_oneip_downmix(self, downmix: OneIPDownmix) -> bool: ...
1338
+ @abstractmethod
1314
1339
  async def set_oneip_vlan(self, vlan: OneIPVlan) -> bool: ...
1315
1340
  @abstractmethod
1316
1341
  async def set_audio_endpoint_locked(self, endpoint: int, locked: bool) -> bool: ...
@@ -1324,6 +1349,15 @@ class DeviceBase(ABC):
1324
1349
  def osd_sync_report(self) -> OsdSyncReport | None: ...
1325
1350
  @property
1326
1351
  @abstractmethod
1352
+ def osd_message(self) -> OsdMessage | None: ...
1353
+ @abstractmethod
1354
+ async def request_osd_message(self) -> bool: ...
1355
+ @abstractmethod
1356
+ async def show_osd_message(self, text: str, logo: int = -1, priority: OsdMessagePriority = ...) -> bool: ...
1357
+ @abstractmethod
1358
+ async def clear_osd_message(self) -> bool: ...
1359
+ @property
1360
+ @abstractmethod
1327
1361
  def oneip_sink_sdp(self) -> OneIPSinkSdp | None: ...
1328
1362
  @property
1329
1363
  @abstractmethod
@@ -1561,6 +1595,7 @@ class MxrCallbacks:
1561
1595
  def on_status_hdbt_connected_changed(self, bay: BayBase, val: bool) -> None: ...
1562
1596
  def on_osd_sync(self, dev: DeviceBase, sync: OsdSync) -> None: ...
1563
1597
  def on_osd_sync_report(self, dev: DeviceBase, report: OsdSyncReport) -> None: ...
1598
+ def on_osd_message_changed(self, dev: DeviceBase, message: OsdMessage) -> None: ...
1564
1599
  def on_osd_sync_requested(self, dev: DeviceBase, request: OsdSyncRequest) -> None: ...
1565
1600
  def on_osd_sync_setting(self, dev: DeviceBase, setting: OsdSyncSetting) -> None: ...
1566
1601
  def on_bay_content_changed(self, bay: BayBase, content: BayContent) -> None: ...
@@ -14,6 +14,7 @@ class MxrDeviceUid:
14
14
 
15
15
  def __init__(self, value: object) -> None:
16
16
  self._value:bytes
17
+ self._str:str|None = None
17
18
 
18
19
  if value is None:
19
20
  self._value = bytes([ 0 for _ in range(16)])
@@ -44,11 +45,11 @@ class MxrDeviceUid:
44
45
 
45
46
  @property
46
47
  def value(self) -> str:
47
- ''' value as human readble string '''
48
- return ''.join('%02x'%i for i in reversed(self._value[0:4])) + '.' + \
49
- ''.join('%02x'%i for i in reversed(self._value[4:8])) + '.' + \
50
- ''.join('%02x'%i for i in reversed(self._value[8:12])) + '.' + \
51
- ''.join('%02x'%i for i in reversed(self._value[12:16]))
48
+ ''' value as human readable string, formatted once, as every hash and
49
+ comparison reads it '''
50
+ if (self._str is None):
51
+ self._str = '.'.join(self._value[i:i + 4][::-1].hex() for i in (0, 4, 8, 12))
52
+ return self._str
52
53
 
53
54
  @property
54
55
  def empty(self) -> bool:
@@ -70,12 +71,14 @@ class MxrDeviceUid:
70
71
  return str(self)
71
72
 
72
73
  def __eq__(self, value: object) -> bool:
73
- if isinstance(value, str) or isinstance(value, MxrDeviceUid):
74
- return str(self) == str(value)
74
+ if isinstance(value, MxrDeviceUid):
75
+ return self.value == value.value
76
+ if isinstance(value, str):
77
+ return self.value == value
75
78
  return False
76
79
 
77
80
  def __hash__(self) -> int:
78
- return hash(str(self))
81
+ return hash(self.value)
79
82
 
80
83
  class MxrBayUidOld:
81
84
  '''Legacy bay identifier using serial number and port name.'''
@@ -14,7 +14,7 @@ Default communication modes:
14
14
  """
15
15
  import os
16
16
 
17
- VERSION = '8.1.0'
17
+ VERSION = '9.1.0'
18
18
  __version__ = VERSION
19
19
 
20
20
  MX_BCAST_UDP_PORT = 8811
@@ -379,6 +379,9 @@ class OneIPDeviceSetting(IntFlag):
379
379
  CLOUD = (1 << 30)
380
380
  '''Connects to the cloud service. A device that does not report it
381
381
  connects regardless.'''
382
+ SINK_ROTATION = (1 << 31)
383
+ '''The angle the device's HDMI output is turned by. Reported only by a
384
+ device with an HDMI output.'''
382
385
 
383
386
  ONEIP_DEVICE_SETTING_SWITCHES = OneIPDeviceSetting(0xFF) | OneIPDeviceSetting.AES67 \
384
387
  | OneIPDeviceSetting.SAP_LISTEN | OneIPDeviceSetting.SAP_ANNOUNCE | OneIPDeviceSetting.POWER_SAVE_DISABLED \
@@ -398,14 +401,17 @@ ONEIP_DEVICE_SETTINGS_PTP_MESSAGES = OneIPDeviceSetting.PTP_ANNOUNCE | OneIPDevi
398
401
  """The PTP message settings."""
399
402
 
400
403
  ONEIP_DEVICE_SETTINGS_BLOCK = ONEIP_DEVICE_SETTINGS_PTP_MESSAGES | OneIPDeviceSetting.SAP_EXPIRE \
401
- | OneIPDeviceSetting.SAP_INTERVAL | OneIPDeviceSetting.STREAM_TTL
404
+ | OneIPDeviceSetting.SAP_INTERVAL | OneIPDeviceSetting.STREAM_TTL | OneIPDeviceSetting.SINK_ROTATION
402
405
  """The settings whose values a frame carries in a block of their own behind the
403
- rest, OneIPPtpMessages: the PTP message settings, the two SAP values and the
404
- stream TTL."""
406
+ rest, OneIPPtpMessages: the PTP message settings, the two SAP values, the
407
+ stream TTL and the output's rotation."""
405
408
 
406
409
  ONEIP_STREAM_TTL_DEFAULT = 64
407
410
  """The TTL a OneIP device's streams carry until it is set."""
408
411
 
412
+ ONEIP_SINK_ROTATION_MAX = 359
413
+ """The largest angle a OneIP device's HDMI output is turned by, in degrees."""
414
+
409
415
  class OneIPPtpMode(IntEnum):
410
416
  '''How a OneIP device takes part in PTP.'''
411
417
  OFF = 0
@@ -434,6 +440,31 @@ class OsdSyncWhat(IntFlag):
434
440
  LOGO_SLOTS = (1 << 3)
435
441
  '''The logo images in the four slots.'''
436
442
 
443
+ class OsdMessageFlag(IntFlag):
444
+ '''What a unit says of the message on its output.'''
445
+ SET = (1 << 0)
446
+ '''A message is set. In a write: show it, rather than take it down.'''
447
+ SHOWN = (1 << 1)
448
+ '''The output shows it, rather than another message ahead of it.'''
449
+ SUPPORTED = (1 << 2)
450
+ '''The unit's video processor composes the message layer.'''
451
+ REFUSED = (1 << 3)
452
+ '''The unit did not take the write it answers, and reports the message it
453
+ kept.'''
454
+
455
+ class OsdMessagePriority(IntEnum):
456
+ '''Where a message stands against the unit's own messages: its upgrade,
457
+ Locate Device and encrypted source messages.'''
458
+ LOW = 0
459
+ '''After them.'''
460
+ NORMAL = 1
461
+ '''Between them.'''
462
+ TOP = 2
463
+ '''Before them.'''
464
+
465
+ OSD_MESSAGE_TEXT_MAX = 255
466
+ """The longest message text a unit takes, in bytes."""
467
+
437
468
  class OsdSyncState(IntEnum):
438
469
  '''How far a unit has got with taking an overlay sync.'''
439
470
  TAKING = 0
@@ -578,6 +609,25 @@ class OneIPPowerSaveLevel(IntEnum):
578
609
  OFF = 3
579
610
  '''Everything but the network powers down.'''
580
611
 
612
+ class OneIPDownmixMode(IntEnum):
613
+ '''When a OneIP output mixes 5.1 and 7.1 PCM down to stereo.'''
614
+ OFF = 0
615
+ '''Never.'''
616
+ AUTO = 1
617
+ '''When the display's EDID lists two PCM channels or fewer.'''
618
+ ALWAYS = 2
619
+ '''Always.'''
620
+
621
+ class OneIPDownmixLevel(IntEnum):
622
+ '''How loud a OneIP output's stereo downmix is.'''
623
+ NORMALISED = 0
624
+ '''Scaled so the mix never clips.'''
625
+ UNSCALED = 1
626
+ '''Unscaled, with dialogue at the level it has in stereo.'''
627
+
628
+ ONEIP_DOWNMIX_LFE_MAX = 100
629
+ """The largest percent a OneIP output mixes its LFE channel in at."""
630
+
581
631
  class UnitPowerSave(IntFlag):
582
632
  '''What a unit offers at one power save level.'''
583
633
  SUPPORTED = (1 << 0)