mx-remote 9.0.0__tar.gz → 9.2.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-9.0.0 → mx_remote-9.2.0}/PKG-INFO +1 -1
  2. {mx_remote-9.0.0 → mx_remote-9.2.0}/docs/api.md +17 -0
  3. {mx_remote-9.0.0 → mx_remote-9.2.0}/docs/oneip.md +46 -12
  4. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/Interface.py +85 -15
  5. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/Interface.pyi +19 -0
  6. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/Uid.py +11 -8
  7. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/const.py +1 -1
  8. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/Constants.py +24 -1
  9. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/Constants.pyi +11 -0
  10. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.py +66 -6
  11. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPDeviceConfiguration.pyi +8 -2
  12. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPSources.py +5 -6
  13. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/remote/Bay.py +2 -1
  14. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/remote/Device.py +73 -9
  15. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/remote/Device.pyi +7 -2
  16. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/remote/Remote.py +49 -16
  17. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/remote/Remote.pyi +1 -0
  18. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/complete.py +12 -0
  19. mx_remote-9.2.0/tests/downmix.py +124 -0
  20. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/run.py +2 -0
  21. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/staterequest.py +64 -0
  22. mx_remote-9.2.0/tests/uid.py +50 -0
  23. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/v2ipsettings.py +41 -1
  24. {mx_remote-9.0.0 → mx_remote-9.2.0}/.gitignore +0 -0
  25. {mx_remote-9.0.0 → mx_remote-9.2.0}/AGENTS.md +0 -0
  26. {mx_remote-9.0.0 → mx_remote-9.2.0}/CLAUDE.md +0 -0
  27. {mx_remote-9.0.0 → mx_remote-9.2.0}/LICENSE +0 -0
  28. {mx_remote-9.0.0 → mx_remote-9.2.0}/README.md +0 -0
  29. {mx_remote-9.0.0 → mx_remote-9.2.0}/docs/README.md +0 -0
  30. {mx_remote-9.0.0 → mx_remote-9.2.0}/docs/_static/brand.css +0 -0
  31. {mx_remote-9.0.0 → mx_remote-9.2.0}/docs/_static/favicon.svg +0 -0
  32. {mx_remote-9.0.0 → mx_remote-9.2.0}/docs/_static/logo-dark.svg +0 -0
  33. {mx_remote-9.0.0 → mx_remote-9.2.0}/docs/_static/logo.svg +0 -0
  34. {mx_remote-9.0.0 → mx_remote-9.2.0}/docs/audio.md +0 -0
  35. {mx_remote-9.0.0 → mx_remote-9.2.0}/docs/callbacks.md +0 -0
  36. {mx_remote-9.0.0 → mx_remote-9.2.0}/docs/conf.py +0 -0
  37. {mx_remote-9.0.0 → mx_remote-9.2.0}/docs/configuration.md +0 -0
  38. {mx_remote-9.0.0 → mx_remote-9.2.0}/docs/devices-and-bays.md +0 -0
  39. {mx_remote-9.0.0 → mx_remote-9.2.0}/docs/diagnostics.md +0 -0
  40. {mx_remote-9.0.0 → mx_remote-9.2.0}/docs/index.md +0 -0
  41. {mx_remote-9.0.0 → mx_remote-9.2.0}/docs/multiviewer.md +0 -0
  42. {mx_remote-9.0.0 → mx_remote-9.2.0}/docs/requirements.txt +0 -0
  43. {mx_remote-9.0.0 → mx_remote-9.2.0}/docs/routing.md +0 -0
  44. {mx_remote-9.0.0 → mx_remote-9.2.0}/hatch_build.py +0 -0
  45. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/Uid.pyi +0 -0
  46. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/__init__.py +0 -0
  47. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/__init__.pyi +0 -0
  48. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/compat.py +0 -0
  49. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/compat.pyi +0 -0
  50. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/const.pyi +0 -0
  51. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/deprecated.py +0 -0
  52. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/deprecated.pyi +0 -0
  53. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/main.py +0 -0
  54. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/main.pyi +0 -0
  55. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/BayConfig.py +0 -0
  56. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/BayConfig.pyi +0 -0
  57. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/Data.py +0 -0
  58. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/Data.pyi +0 -0
  59. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/Factory.py +0 -0
  60. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/Factory.pyi +0 -0
  61. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameAmpDolbySettings.py +0 -0
  62. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameAmpDolbySettings.pyi +0 -0
  63. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameAmpZoneSettings.py +0 -0
  64. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameAmpZoneSettings.pyi +0 -0
  65. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameAudioClip.py +0 -0
  66. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameAudioClip.pyi +0 -0
  67. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameAudioSetRoute.py +0 -0
  68. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameAudioSetRoute.pyi +0 -0
  69. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameBase.py +0 -0
  70. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameBase.pyi +0 -0
  71. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameBayConfig.py +0 -0
  72. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameBayConfig.pyi +0 -0
  73. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameBayConfigSecondary.py +0 -0
  74. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameBayConfigSecondary.pyi +0 -0
  75. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameBayContent.py +0 -0
  76. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameBayContent.pyi +0 -0
  77. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameBayHide.py +0 -0
  78. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameBayHide.pyi +0 -0
  79. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameBayStatus.py +0 -0
  80. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameBayStatus.pyi +0 -0
  81. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameConnectStatus.py +0 -0
  82. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameConnectStatus.pyi +0 -0
  83. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameDebug.py +0 -0
  84. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameDebug.pyi +0 -0
  85. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameDiscover.py +0 -0
  86. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameDiscover.pyi +0 -0
  87. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameEDID.py +0 -0
  88. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameEDID.pyi +0 -0
  89. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameEDIDProfile.py +0 -0
  90. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameEDIDProfile.pyi +0 -0
  91. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameFactoryReset.py +0 -0
  92. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameFactoryReset.pyi +0 -0
  93. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameFilterStatus.py +0 -0
  94. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameFilterStatus.pyi +0 -0
  95. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameFirmwareVersion.py +0 -0
  96. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameFirmwareVersion.pyi +0 -0
  97. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameHeader.py +0 -0
  98. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameHeader.pyi +0 -0
  99. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameHello.py +0 -0
  100. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameHello.pyi +0 -0
  101. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameLinks.py +0 -0
  102. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameLinks.pyi +0 -0
  103. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameMeshOperation.py +0 -0
  104. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameMeshOperation.pyi +0 -0
  105. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameMirrorStatus.py +0 -0
  106. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameMirrorStatus.pyi +0 -0
  107. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameMonitoringPulse.py +0 -0
  108. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameMonitoringPulse.pyi +0 -0
  109. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameNetworkStatus.py +0 -0
  110. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameNetworkStatus.pyi +0 -0
  111. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FramePing.py +0 -0
  112. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FramePing.pyi +0 -0
  113. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FramePowerChange.py +0 -0
  114. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FramePowerChange.pyi +0 -0
  115. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameRCAction.py +0 -0
  116. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameRCAction.pyi +0 -0
  117. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameRCIr.py +0 -0
  118. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameRCIr.pyi +0 -0
  119. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameRCKey.py +0 -0
  120. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameRCKey.pyi +0 -0
  121. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameRCSettings.py +0 -0
  122. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameRCSettings.pyi +0 -0
  123. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameReboot.py +0 -0
  124. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameReboot.pyi +0 -0
  125. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameRoutingChange.py +0 -0
  126. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameRoutingChange.pyi +0 -0
  127. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameSetInstaller.py +0 -0
  128. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameSetInstaller.pyi +0 -0
  129. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameSetName.py +0 -0
  130. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameSetName.pyi +0 -0
  131. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameSetRoute.py +0 -0
  132. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameSetRoute.pyi +0 -0
  133. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameSetupStatus.py +0 -0
  134. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameSetupStatus.pyi +0 -0
  135. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameSignalStatus.py +0 -0
  136. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameSignalStatus.pyi +0 -0
  137. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameSignalStatusNew.py +0 -0
  138. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameSignalStatusNew.pyi +0 -0
  139. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameSysTemperature.py +0 -0
  140. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameSysTemperature.pyi +0 -0
  141. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameSystemStatus.py +0 -0
  142. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameSystemStatus.pyi +0 -0
  143. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameTXRCAction.py +0 -0
  144. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameTXRCAction.pyi +0 -0
  145. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameTXRCKey.py +0 -0
  146. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameTXRCKey.pyi +0 -0
  147. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameTime.py +0 -0
  148. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameTime.pyi +0 -0
  149. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameTimeZone.py +0 -0
  150. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameTimeZone.pyi +0 -0
  151. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameTopology.py +0 -0
  152. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameTopology.pyi +0 -0
  153. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameTxIR.py +0 -0
  154. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameTxIR.pyi +0 -0
  155. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameUnitCommand.py +0 -0
  156. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameUnitCommand.pyi +0 -0
  157. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameUnitStatus.py +0 -0
  158. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameUnitStatus.pyi +0 -0
  159. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameUpgradeFPGA.py +0 -0
  160. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameUpgradeFPGA.pyi +0 -0
  161. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPAudio.py +0 -0
  162. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPAudio.pyi +0 -0
  163. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPBayMapping.py +0 -0
  164. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPBayMapping.pyi +0 -0
  165. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPDetectBays.py +0 -0
  166. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPDetectBays.pyi +0 -0
  167. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPLink.py +0 -0
  168. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPLink.pyi +0 -0
  169. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.py +0 -0
  170. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPManualSourceSwitch.pyi +0 -0
  171. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPMultiviewer.py +0 -0
  172. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPMultiviewer.pyi +0 -0
  173. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPOsdSync.py +0 -0
  174. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPOsdSync.pyi +0 -0
  175. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPPowerSave.py +0 -0
  176. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPPowerSave.pyi +0 -0
  177. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPPrivacy.py +0 -0
  178. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPPrivacy.pyi +0 -0
  179. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPSetMaster.py +0 -0
  180. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPSetMaster.pyi +0 -0
  181. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPSettingsAll.py +0 -0
  182. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPSettingsAll.pyi +0 -0
  183. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPSinkSdp.py +0 -0
  184. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPSinkSdp.pyi +0 -0
  185. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPSourceSwitch.py +0 -0
  186. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPSourceSwitch.pyi +0 -0
  187. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPSources.pyi +0 -0
  188. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPStats.py +0 -0
  189. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPStats.pyi +0 -0
  190. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPTestcard.py +0 -0
  191. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPTestcard.pyi +0 -0
  192. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPTiling.py +0 -0
  193. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPTiling.pyi +0 -0
  194. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPVideoWall.py +0 -0
  195. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameV2IPVideoWall.pyi +0 -0
  196. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameVolume.py +0 -0
  197. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameVolume.pyi +0 -0
  198. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameVolumeDown.py +0 -0
  199. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameVolumeDown.pyi +0 -0
  200. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameVolumeSet.py +0 -0
  201. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameVolumeSet.pyi +0 -0
  202. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameVolumeUp.py +0 -0
  203. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/FrameVolumeUp.pyi +0 -0
  204. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/LinkConfig.py +0 -0
  205. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/LinkConfig.pyi +0 -0
  206. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/Multiviewer.py +0 -0
  207. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/Multiviewer.pyi +0 -0
  208. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/Svd.py +0 -0
  209. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/Svd.pyi +0 -0
  210. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/V2IPConfig.py +0 -0
  211. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/V2IPConfig.pyi +0 -0
  212. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/V2IPStats.py +0 -0
  213. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/V2IPStats.pyi +0 -0
  214. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/__init__.py +0 -0
  215. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/__init__.pyi +0 -0
  216. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/proto/svd.csv +0 -0
  217. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/py.typed +0 -0
  218. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/remote/Bay.pyi +0 -0
  219. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/remote/ConnectionAsync.py +0 -0
  220. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/remote/ConnectionAsync.pyi +0 -0
  221. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/remote/Link.py +0 -0
  222. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/remote/Link.pyi +0 -0
  223. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/remote/OsdSync.py +0 -0
  224. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/remote/OsdSync.pyi +0 -0
  225. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/remote/State.py +0 -0
  226. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/remote/State.pyi +0 -0
  227. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/remote/V2IP.py +0 -0
  228. {mx_remote-9.0.0 → mx_remote-9.2.0}/mx_remote/remote/V2IP.pyi +0 -0
  229. {mx_remote-9.0.0 → mx_remote-9.2.0}/pyproject.toml +0 -0
  230. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/audio_ir.py +0 -0
  231. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/audiolock.py +0 -0
  232. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/baycontent.py +0 -0
  233. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/capture.py +0 -0
  234. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/deprecated.py +0 -0
  235. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/e2e.py +0 -0
  236. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/entry.py +0 -0
  237. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/handlers.py +0 -0
  238. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/handlers_cover.py +0 -0
  239. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/hello.py +0 -0
  240. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/idempotent.py +0 -0
  241. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/lengths.py +0 -0
  242. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/merge.py +0 -0
  243. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/mesh.py +0 -0
  244. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/meshbays.py +0 -0
  245. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/newops.py +0 -0
  246. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/offline.py +0 -0
  247. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/offsets.py +0 -0
  248. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/osdmessage.py +0 -0
  249. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/osdsync.py +0 -0
  250. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/ping.py +0 -0
  251. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/privacy.py +0 -0
  252. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/proto_stamp.py +0 -0
  253. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/protogate.py +0 -0
  254. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/rcsettings.py +0 -0
  255. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/roundtrip.py +0 -0
  256. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/routes.py +0 -0
  257. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/smoke.py +0 -0
  258. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/sources.py +0 -0
  259. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/stats.py +0 -0
  260. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/structs.py +0 -0
  261. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/txresult.py +0 -0
  262. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/unit.py +0 -0
  263. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/unknowns.py +0 -0
  264. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/v2ipptp.py +0 -0
  265. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/v2ipscaling.py +0 -0
  266. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/v2ipsdp.py +0 -0
  267. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/v2ipsubject.py +0 -0
  268. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/v2iptestcard.py +0 -0
  269. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/v2ipvlan.py +0 -0
  270. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/vdetails.py +0 -0
  271. {mx_remote-9.0.0 → mx_remote-9.2.0}/tests/wirefix.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mx_remote
3
- Version: 9.0.0
3
+ Version: 9.2.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/
@@ -168,6 +168,23 @@ bit, and the range of the infrared profiles.
168
168
  .. autodata:: mx_remote.proto.Constants.ONEIP_VLAN_ID_MAX
169
169
  ```
170
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
+
171
188
  ## OneIP PTP
172
189
 
173
190
  A device's PTP settings, which it reports and a controller writes, and the PTP
@@ -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.
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.
243
250
 
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.
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
@@ -318,6 +327,31 @@ if ptp is not None:
318
327
  print(ptp.grandmaster) # its clock identity, None while there is none
319
328
  ```
320
329
 
330
+ ### Stereo downmix
331
+
332
+ A device whose output can mix 5.1 and 7.1 PCM down to stereo reports how,
333
+ once its video processor has answered for it: a mode, a level and the percent
334
+ the LFE channel is mixed in at, which a controller writes, and the state and
335
+ channel count, which only the device reports. A device that reports none has
336
+ no downmix to set, so a write to one is refused, as is a mode, level or LFE
337
+ percent out of range. The write sends the mode, level and LFE together; the
338
+ state and channels are never sent.
339
+
340
+ ```python
341
+ from mx_remote import OneIPDownmix
342
+ from mx_remote.proto.Constants import OneIPDownmixMode, OneIPDownmixLevel
343
+
344
+ downmix = device.oneip_downmix # None: no report yet, or no downmix
345
+ if downmix is not None:
346
+ print(downmix.mode, downmix.level, downmix.lfe) # LFE up to ONEIP_DOWNMIX_LFE_MAX
347
+ print(downmix.state, downmix.channels) # the device's own
348
+ await device.set_oneip_downmix(OneIPDownmix(mode=OneIPDownmixMode.AUTO,
349
+ level=OneIPDownmixLevel.NORMALISED, lfe=50))
350
+ ```
351
+
352
+ The downmix ends the settings block, behind a second valid word, so the write
353
+ runs on to 248 bytes and carries no other setting.
354
+
321
355
  ### VLAN
322
356
 
323
357
  A device that announces `DeviceFeature.VLAN` tags its uplink by a VLAN
@@ -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
  """
@@ -2672,6 +2712,16 @@ class OneIPDeviceSettings:
2672
2712
  self._power_save_level = int(power_save_level)
2673
2713
  self._ptp_messages = ptp_messages if (ptp_messages is not None) else OneIPPtpMessages()
2674
2714
 
2715
+ @classmethod
2716
+ def power_save_switch(cls, enabled:bool) -> 'OneIPDeviceSettings':
2717
+ """The settings that switch a device's power save on or off. On is both
2718
+ POWER_SAVE_DISABLED, clear, and POWER_SAVE_LEVEL, at 0."""
2719
+ disabled = OneIPDeviceSetting.POWER_SAVE_DISABLED
2720
+ if enabled:
2721
+ return cls(valid=(disabled | OneIPDeviceSetting.POWER_SAVE_LEVEL),
2722
+ power_save_level=OneIPPowerSaveLevel.NONE)
2723
+ return cls(valid=disabled, flags=disabled)
2724
+
2675
2725
  @property
2676
2726
  def valid(self) -> OneIPDeviceSetting:
2677
2727
  """The settings reported so far."""
@@ -2751,8 +2801,9 @@ class OneIPDeviceSettings:
2751
2801
 
2752
2802
  @property
2753
2803
  def power_save_level(self) -> OneIPPowerSaveLevel|int|None:
2754
- """The level the device powers down to in power save, a raw int for one
2755
- this library does not name, None while it is not reported."""
2804
+ """The level the device powers down to in power save, which it picks
2805
+ itself, a raw int for one this library does not name, None while it is
2806
+ not reported."""
2756
2807
  if (OneIPDeviceSetting.POWER_SAVE_LEVEL not in self._valid):
2757
2808
  return None
2758
2809
  return _wire_enum(OneIPPowerSaveLevel, self._power_save_level)
@@ -2772,18 +2823,14 @@ class OneIPDeviceSettings:
2772
2823
 
2773
2824
  Each bit in this block's ``valid`` replaces its own setting and leaves
2774
2825
  the others alone, so a write about one setting does not clear what was
2775
- known about the rest. The exception is POWER_SAVE_DISABLED: a device
2776
- takes a power save level as switching it off.
2826
+ known about the rest.
2777
2827
  """
2778
2828
  if (previous is None):
2779
2829
  previous = OneIPDeviceSettings()
2780
2830
  valid = self._valid
2781
- flags = (previous._flags & ~valid) | (self._flags & valid)
2782
- if (OneIPDeviceSetting.POWER_SAVE_LEVEL in valid) and (OneIPDeviceSetting.POWER_SAVE_DISABLED not in valid):
2783
- flags &= ~OneIPDeviceSetting.POWER_SAVE_DISABLED
2784
2831
  return OneIPDeviceSettings(
2785
2832
  valid=(previous._valid | valid),
2786
- flags=flags,
2833
+ flags=((previous._flags & ~valid) | (self._flags & valid)),
2787
2834
  ir_profiles=(self._ir_profiles if (OneIPDeviceSetting.IR_PROFILES in valid)
2788
2835
  else previous._ir_profiles),
2789
2836
  ir_profile=(self._ir_profile if (OneIPDeviceSetting.IR_PROFILE in valid)
@@ -2810,9 +2857,11 @@ class OneIPDeviceSettings:
2810
2857
  write about it.
2811
2858
 
2812
2859
  A device applies a setting only if it has it, a profile, PTP mode, PTP
2813
- domain, power save level or PTP DSCP only within its range, a stream
2814
- TTL only when it is not 0, a rotation only by its own step, and never
2815
- what only it can report about itself. The step stays the device's.
2860
+ domain or PTP DSCP only within its range, a stream TTL only when it is
2861
+ not 0, a rotation only by its own step, and never what only it can
2862
+ report about itself. The step stays the device's. It picks its power
2863
+ save level itself, and takes a level it is written, without
2864
+ POWER_SAVE_DISABLED, as switching power save on.
2816
2865
 
2817
2866
  reported may be only the bits the device reported, which says nothing
2818
2867
  of its step, so a rotation other than 0 is then left out.
@@ -2831,15 +2880,20 @@ class OneIPDeviceSettings:
2831
2880
  valid &= ~OneIPDeviceSetting.PTP_MODE
2832
2881
  if (self._ptp_domain > ONEIP_PTP_DOMAIN_MAX):
2833
2882
  valid &= ~OneIPDeviceSetting.PTP_DOMAIN
2834
- if (self._power_save_level > OneIPPowerSaveLevel.OFF):
2835
- valid &= ~OneIPDeviceSetting.POWER_SAVE_LEVEL
2836
2883
  if (self._ptp_messages.dscp > ONEIP_DSCP_MAX):
2837
2884
  valid &= ~OneIPDeviceSetting.PTP_DSCP
2838
2885
  if (self._ptp_messages.stream_ttl == 0):
2839
2886
  valid &= ~OneIPDeviceSetting.STREAM_TTL
2840
2887
  if not own.takes_sink_rotation(self._ptp_messages.sink_rotation):
2841
2888
  valid &= ~OneIPDeviceSetting.SINK_ROTATION
2842
- return OneIPDeviceSettings(valid=valid, flags=self._flags, ir_profiles=self._ir_profiles,
2889
+ flags = self._flags
2890
+ if (OneIPDeviceSetting.POWER_SAVE_LEVEL in valid):
2891
+ valid &= ~OneIPDeviceSetting.POWER_SAVE_LEVEL
2892
+ if (OneIPDeviceSetting.POWER_SAVE_DISABLED not in valid) \
2893
+ and (OneIPDeviceSetting.POWER_SAVE_DISABLED in reported._valid):
2894
+ valid |= OneIPDeviceSetting.POWER_SAVE_DISABLED
2895
+ flags &= ~OneIPDeviceSetting.POWER_SAVE_DISABLED
2896
+ return OneIPDeviceSettings(valid=valid, flags=flags, ir_profiles=self._ir_profiles,
2843
2897
  ir_profile=self._ir_profile, ir_profile_sink=self._ir_profile_sink,
2844
2898
  auto_power_save=self._auto_power_save, power_save=self._power_save,
2845
2899
  ptp_mode=self._ptp_mode, ptp_domain=self._ptp_domain,
@@ -3239,6 +3293,14 @@ class DeviceBase(ABC):
3239
3293
  async def confirm_unit_network(self) -> bool:
3240
3294
  '''keep the unit's pending IP configuration'''
3241
3295
 
3296
+ @property
3297
+ @abstractmethod
3298
+ def oneip_downmix(self) -> OneIPDownmix|None:
3299
+ '''How this OneIP device's output mixes down to stereo, as it last
3300
+ reported, None while it reports no downmix: until its video processor
3301
+ has answered for it, and always for one whose output cannot mix
3302
+ down.'''
3303
+
3242
3304
  @property
3243
3305
  @abstractmethod
3244
3306
  def oneip_ptp(self) -> OneIPPtpState|None:
@@ -3524,14 +3586,22 @@ class DeviceBase(ABC):
3524
3586
  async def set_oneip_ptp_priority1(self, priority1:int) -> bool:
3525
3587
  '''set the priority1 the device announces as a PTP grandmaster'''
3526
3588
 
3589
+ @abstractmethod
3590
+ async def set_oneip_power_save(self, enabled:bool) -> bool:
3591
+ '''switch power save on, so the device powers down by itself to the level it picks, or off'''
3592
+
3527
3593
  @abstractmethod
3528
3594
  async def set_oneip_power_save_level(self, level:OneIPPowerSaveLevel) -> bool:
3529
- '''set the level the device powers down to in power save'''
3595
+ '''write the level the device powers down to in power save, which switches power save on'''
3530
3596
 
3531
3597
  @abstractmethod
3532
3598
  async def set_oneip_ptp_messages(self, which:OneIPDeviceSetting, messages:OneIPPtpMessages) -> bool:
3533
3599
  '''set how the device paces its PTP messages, its SAP timings and its streams' TTL: the values of messages behind the bits of which'''
3534
3600
 
3601
+ @abstractmethod
3602
+ async def set_oneip_downmix(self, downmix:OneIPDownmix) -> bool:
3603
+ '''set how the device's output mixes down to stereo'''
3604
+
3535
3605
  @abstractmethod
3536
3606
  async def set_oneip_vlan(self, vlan:OneIPVlan) -> bool:
3537
3607
  '''change the device's VLAN configuration'''
@@ -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 = ...
@@ -952,6 +962,8 @@ class OneIPTestcard:
952
962
 
953
963
  class OneIPDeviceSettings:
954
964
  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: ...
965
+ @classmethod
966
+ def power_save_switch(cls, enabled: bool) -> OneIPDeviceSettings: ...
955
967
  @property
956
968
  def valid(self) -> OneIPDeviceSetting: ...
957
969
  @property
@@ -1183,6 +1195,9 @@ class DeviceBase(ABC):
1183
1195
  async def confirm_unit_network(self) -> bool: ...
1184
1196
  @property
1185
1197
  @abstractmethod
1198
+ def oneip_downmix(self) -> OneIPDownmix | None: ...
1199
+ @property
1200
+ @abstractmethod
1186
1201
  def oneip_ptp(self) -> OneIPPtpState | None: ...
1187
1202
  @property
1188
1203
  @abstractmethod
@@ -1317,10 +1332,14 @@ class DeviceBase(ABC):
1317
1332
  @abstractmethod
1318
1333
  async def set_oneip_ptp_priority1(self, priority1: int) -> bool: ...
1319
1334
  @abstractmethod
1335
+ async def set_oneip_power_save(self, enabled: bool) -> bool: ...
1336
+ @abstractmethod
1320
1337
  async def set_oneip_power_save_level(self, level: OneIPPowerSaveLevel) -> bool: ...
1321
1338
  @abstractmethod
1322
1339
  async def set_oneip_ptp_messages(self, which: OneIPDeviceSetting, messages: OneIPPtpMessages) -> bool: ...
1323
1340
  @abstractmethod
1341
+ async def set_oneip_downmix(self, downmix: OneIPDownmix) -> bool: ...
1342
+ @abstractmethod
1324
1343
  async def set_oneip_vlan(self, vlan: OneIPVlan) -> bool: ...
1325
1344
  @abstractmethod
1326
1345
  async def set_audio_endpoint_locked(self, endpoint: int, locked: bool) -> bool: ...
@@ -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 = '9.0.0'
17
+ VERSION = '9.2.0'
18
18
  __version__ = VERSION
19
19
 
20
20
  MX_BCAST_UDP_PORT = 8811
@@ -349,7 +349,11 @@ class OneIPDeviceSetting(IntFlag):
349
349
  AES67 = (1 << 17)
350
350
  '''Sends the source's audio as an AES67 flow, where the source allows.'''
351
351
  POWER_SAVE_LEVEL = (1 << 18)
352
- '''The power save level the device powers down to.'''
352
+ '''The level the device powers down to in power save. A device picks it
353
+ itself: the level that saves the most while its input and display can
354
+ still wake it. Written without POWER_SAVE_DISABLED, it switches power save
355
+ on, and the level written is ignored, except by a device whose firmware
356
+ predates picking it, which takes the level as its setting.'''
353
357
  PTP_ANNOUNCE = (1 << 19)
354
358
  '''The interval between the device's PTP announce messages.'''
355
359
  PTP_SYNC = (1 << 20)
@@ -609,6 +613,25 @@ class OneIPPowerSaveLevel(IntEnum):
609
613
  OFF = 3
610
614
  '''Everything but the network powers down.'''
611
615
 
616
+ class OneIPDownmixMode(IntEnum):
617
+ '''When a OneIP output mixes 5.1 and 7.1 PCM down to stereo.'''
618
+ OFF = 0
619
+ '''Never.'''
620
+ AUTO = 1
621
+ '''When the display's EDID lists two PCM channels or fewer.'''
622
+ ALWAYS = 2
623
+ '''Always.'''
624
+
625
+ class OneIPDownmixLevel(IntEnum):
626
+ '''How loud a OneIP output's stereo downmix is.'''
627
+ NORMALISED = 0
628
+ '''Scaled so the mix never clips.'''
629
+ UNSCALED = 1
630
+ '''Unscaled, with dialogue at the level it has in stereo.'''
631
+
632
+ ONEIP_DOWNMIX_LFE_MAX = 100
633
+ """The largest percent a OneIP output mixes its LFE channel in at."""
634
+
612
635
  class UnitPowerSave(IntFlag):
613
636
  '''What a unit offers at one power save level.'''
614
637
  SUPPORTED = (1 << 0)
@@ -236,6 +236,17 @@ class OneIPPowerSaveLevel(IntEnum):
236
236
  LOW: int
237
237
  OFF: int
238
238
 
239
+ class OneIPDownmixMode(IntEnum):
240
+ OFF: int
241
+ AUTO: int
242
+ ALWAYS: int
243
+
244
+ class OneIPDownmixLevel(IntEnum):
245
+ NORMALISED: int
246
+ UNSCALED: int
247
+
248
+ ONEIP_DOWNMIX_LFE_MAX: int
249
+
239
250
  class UnitPowerSave(IntFlag):
240
251
  SUPPORTED: Incomplete
241
252
  INPUT_WAKE: Incomplete
@@ -14,9 +14,9 @@ from .FrameHeader import FrameHeader
14
14
  from ..Uid import MxrDeviceUid
15
15
  from ..Interface import (DeviceBase, DeviceRegistry, DeviceOneIPDetails, DeviceOneIPScalingSettings,
16
16
  DeviceOneIPSink, OneIPAudioFormat, OneIPDeviceSettings, OneIPDscpConfig,
17
- OneIPPowerSaveSchedule, OneIPPtpMessages, OneIPPtpState, OneIPStreamSource,
18
- OneIPVlan, _wire_enum)
19
- from .Constants import (ONEIP_DEVICE_SETTINGS_BLOCK, ONEIP_DEVICE_SETTINGS_PTP, OneIPPtpFlag, OneIPPtpLock, OneIPPtpMode, OneIPDeviceSetting, OneIPVideoProcessorFeature, OneIPVlanFlag, MXR_SCALING_FLAG_AUTO_SCALING,
17
+ OneIPDownmix, OneIPPowerSaveSchedule, OneIPPtpMessages, OneIPPtpState,
18
+ OneIPStreamSource, OneIPVlan, _wire_enum)
19
+ from .Constants import (ONEIP_DEVICE_SETTINGS_BLOCK, ONEIP_DEVICE_SETTINGS_PTP, OneIPDownmixLevel, OneIPDownmixMode, OneIPPtpFlag, OneIPPtpLock, OneIPPtpMode, OneIPDeviceSetting, OneIPVideoProcessorFeature, OneIPVlanFlag, MXR_SCALING_FLAG_AUTO_SCALING,
20
20
  MXR_SCALING_FLAG_MODE_VALID,
21
21
  MXR_SCALING_FLAG_OPTIONS_VALID, MXR_SCALING_FLAG_OPTIONS2_VALID,
22
22
  MXR_SCALING_OPTIONS2_SETTINGS, MxrSignalType, oneip_dscp_value,
@@ -48,13 +48,19 @@ from .V2IPConfig import V2IPStreamSourceImpl, parse_v2ip_av_source
48
48
  # 216..224 the settings' own block: the PTP announce, sync and delay
49
49
  # request intervals as i8, u8 announce timeout, u8 PTP DSCP,
50
50
  # u8 SAP expiry in minutes, u16 SAP interval in seconds, and
51
- # from a device with the stream TTL, u8 TTL and 7 reserved
51
+ # from a device with the stream TTL, u8 TTL, a reserved byte,
52
+ # u16 output rotation and u16 rotation step at 224..230, 2
53
+ # reserved
54
+ # 232..248 from a device with the downmix: u32 second valid word, then
55
+ # the downmix's u8 mode, level, LFE percent, state and channels
56
+ # at 236..241, 7 reserved
52
57
  #
53
58
  # That block is the last of its frame, so it is read by its length: shorter
54
59
  # than 8 carries none of its values, and 8, which every device without the TTL
55
60
  # sends, carries all but the TTL. A write grows it to 16 only when it carries
56
- # the TTL, so the frames other writes send are unchanged for units that
57
- # predate it.
61
+ # the TTL or the rotation, and to 32 only for the downmix, so the frames other
62
+ # writes send are unchanged for units that predate them. A block too short to
63
+ # hold the downmix's channels carries no bit of the second valid word.
58
64
  #
59
65
  # Each of those blocks was appended behind what came before it, leaving every
60
66
  # offset ahead of it where it was, so the length is what says whether one is
@@ -136,6 +142,14 @@ _STREAM_TTL_AT = 8
136
142
  _SINK_ROTATION_AT = 10
137
143
  _SINK_ROTATION_STEP_AT = 12
138
144
  _SETTINGS_BLOCK_LONG_SIZE = 16
145
+ _VALID_EXT_OFFSET = SETTINGS_BLOCK_OFFSET + _SETTINGS_BLOCK_LONG_SIZE
146
+ '''The second valid word of the settings, behind the block of their own.'''
147
+ _DOWNMIX_OFFSET = _VALID_EXT_OFFSET + 4
148
+ _DOWNMIX_SIZE = 5
149
+ _WITH_DOWNMIX_SIZE = SETTINGS_BLOCK_OFFSET + 32
150
+ '''The frame a downmix write sends: the settings block run on to 32 bytes.'''
151
+ _SETTING_EXT_DOWNMIX = 1 << 0
152
+ '''The bit of the second valid word that carries the downmix.'''
139
153
 
140
154
  def carries_settings_block(settings:OneIPDeviceSettings) -> bool:
141
155
  '''Whether settings carry a value only the block of their own can hold.'''
@@ -424,6 +438,28 @@ class FrameV2IPDeviceConfiguration(FrameBase):
424
438
  payload += own_settings_block(settings)
425
439
  return FrameBase.construct_base(target=target, mxr=mxr, opcode=_OPCODE, payload=bytes(payload))
426
440
 
441
+ @staticmethod
442
+ def construct_downmix(mxr:DeviceRegistry, target:Any, target_uid:MxrDeviceUid,
443
+ downmix:OneIPDownmix) -> FrameBase|None:
444
+ '''Build the 0x3C write that changes how a device's output mixes down to
445
+ stereo and nothing else.
446
+
447
+ 248 bytes: the frame construct_settings() builds, with a settings block
448
+ that carries no setting, zeroed to 232, then the second valid word
449
+ carrying the downmix and its mode, level and LFE percent at 236..239.
450
+ The state and channels at 239 and 240 are the device's to report, and
451
+ are left 0.
452
+ '''
453
+ frame = FrameV2IPDeviceConfiguration.construct_settings(
454
+ mxr=mxr, target=target, target_uid=target_uid, settings=OneIPDeviceSettings())
455
+ if (frame is None):
456
+ return None
457
+ payload = bytearray(frame.payload or b"")
458
+ payload += bytes(_WITH_DOWNMIX_SIZE - len(payload))
459
+ struct.pack_into('<IBBB', payload, _VALID_EXT_OFFSET, _SETTING_EXT_DOWNMIX,
460
+ int(downmix.mode), int(downmix.level), downmix.lfe)
461
+ return FrameBase.construct_base(target=target, mxr=mxr, opcode=_OPCODE, payload=bytes(payload))
462
+
427
463
  @staticmethod
428
464
  def construct_vlan(mxr:DeviceRegistry, target:Any, target_uid:MxrDeviceUid,
429
465
  vlan:OneIPVlan) -> FrameBase|None:
@@ -580,6 +616,28 @@ class FrameV2IPDeviceConfiguration(FrameBase):
580
616
  port_state=port_state, lock=_wire_enum(OneIPPtpLock, lock),
581
617
  clock_identity=clock, grandmaster_identity=grandmaster)
582
618
 
619
+ @property
620
+ def carries_downmix(self) -> bool:
621
+ '''Whether the frame is long enough to say whether it carries a downmix.'''
622
+ return (self.payload is not None) and (len(self.payload) >= (_DOWNMIX_OFFSET + _DOWNMIX_SIZE))
623
+
624
+ @cached_property
625
+ def downmix(self) -> OneIPDownmix|None:
626
+ '''The downmix the frame carries, None when it carries none: in a frame
627
+ too short to hold it, or without its bit of the second valid word.
628
+
629
+ From the subject itself, None reports that it has no downmix; from
630
+ another sender it is a write, of which only the mode, level and LFE
631
+ percent count.'''
632
+ if not self.carries_downmix:
633
+ return None
634
+ p = self.payload or b""
635
+ valid_ext, mode, level, lfe, state, channels = struct.unpack_from('<IBBBBB', p, _VALID_EXT_OFFSET)
636
+ if not (valid_ext & _SETTING_EXT_DOWNMIX):
637
+ return None
638
+ return OneIPDownmix(mode=_wire_enum(OneIPDownmixMode, mode), level=_wire_enum(OneIPDownmixLevel, level),
639
+ lfe=lfe, state=state, channels=channels)
640
+
583
641
  def process(self) -> None:
584
642
  '''Update the cache of the device this configuration names, not of its sender.'''
585
643
  if ((dev := self.subject_device) is None):
@@ -595,6 +653,8 @@ class FrameV2IPDeviceConfiguration(FrameBase):
595
653
  dev.on_mxr_update(vlan)
596
654
  if ((ptp := self.ptp) is not None):
597
655
  dev.on_mxr_update(ptp)
656
+ if self.carries_downmix:
657
+ dev.on_mxr_update(self)
598
658
 
599
659
  def __str__(self) -> str:
600
660
  sink_str = f" sink=[{self.sink}]" if (self.sink is not None) else ""
@@ -1,6 +1,6 @@
1
- from ..Interface import DeviceBase as DeviceBase, DeviceOneIPDetails as DeviceOneIPDetails, DeviceOneIPScalingSettings as DeviceOneIPScalingSettings, DeviceOneIPSink as DeviceOneIPSink, DeviceRegistry as DeviceRegistry, OneIPAudioFormat as OneIPAudioFormat, OneIPDeviceSettings as OneIPDeviceSettings, OneIPDscpConfig as OneIPDscpConfig, OneIPPowerSaveSchedule as OneIPPowerSaveSchedule, OneIPPtpMessages as OneIPPtpMessages, OneIPPtpState as OneIPPtpState, OneIPStreamSource as OneIPStreamSource, OneIPVlan as OneIPVlan
1
+ from ..Interface import DeviceBase as DeviceBase, DeviceOneIPDetails as DeviceOneIPDetails, DeviceOneIPScalingSettings as DeviceOneIPScalingSettings, DeviceOneIPSink as DeviceOneIPSink, DeviceRegistry as DeviceRegistry, OneIPAudioFormat as OneIPAudioFormat, OneIPDeviceSettings as OneIPDeviceSettings, OneIPDownmix as OneIPDownmix, OneIPDscpConfig as OneIPDscpConfig, OneIPPowerSaveSchedule as OneIPPowerSaveSchedule, OneIPPtpMessages as OneIPPtpMessages, OneIPPtpState as OneIPPtpState, OneIPStreamSource as OneIPStreamSource, OneIPVlan as OneIPVlan
2
2
  from ..Uid import MxrDeviceUid as MxrDeviceUid
3
- from .Constants import MXR_SCALING_FLAG_AUTO_SCALING as MXR_SCALING_FLAG_AUTO_SCALING, MXR_SCALING_FLAG_MODE_VALID as MXR_SCALING_FLAG_MODE_VALID, MXR_SCALING_FLAG_OPTIONS2_VALID as MXR_SCALING_FLAG_OPTIONS2_VALID, MXR_SCALING_FLAG_OPTIONS_VALID as MXR_SCALING_FLAG_OPTIONS_VALID, MXR_SCALING_OPTIONS2_SETTINGS as MXR_SCALING_OPTIONS2_SETTINGS, MxrSignalType as MxrSignalType, ONEIP_DEVICE_SETTINGS_BLOCK as ONEIP_DEVICE_SETTINGS_BLOCK, ONEIP_DEVICE_SETTINGS_PTP as ONEIP_DEVICE_SETTINGS_PTP, OneIPDeviceSetting as OneIPDeviceSetting, OneIPPtpFlag as OneIPPtpFlag, OneIPPtpLock as OneIPPtpLock, OneIPPtpMode as OneIPPtpMode, OneIPVideoProcessorFeature as OneIPVideoProcessorFeature, OneIPVlanFlag as OneIPVlanFlag, oneip_dscp_value as oneip_dscp_value, oneip_rate_valid as oneip_rate_valid
3
+ from .Constants import MXR_SCALING_FLAG_AUTO_SCALING as MXR_SCALING_FLAG_AUTO_SCALING, MXR_SCALING_FLAG_MODE_VALID as MXR_SCALING_FLAG_MODE_VALID, MXR_SCALING_FLAG_OPTIONS2_VALID as MXR_SCALING_FLAG_OPTIONS2_VALID, MXR_SCALING_FLAG_OPTIONS_VALID as MXR_SCALING_FLAG_OPTIONS_VALID, MXR_SCALING_OPTIONS2_SETTINGS as MXR_SCALING_OPTIONS2_SETTINGS, MxrSignalType as MxrSignalType, ONEIP_DEVICE_SETTINGS_BLOCK as ONEIP_DEVICE_SETTINGS_BLOCK, ONEIP_DEVICE_SETTINGS_PTP as ONEIP_DEVICE_SETTINGS_PTP, OneIPDeviceSetting as OneIPDeviceSetting, OneIPDownmixLevel as OneIPDownmixLevel, OneIPDownmixMode as OneIPDownmixMode, OneIPPtpFlag as OneIPPtpFlag, OneIPPtpLock as OneIPPtpLock, OneIPPtpMode as OneIPPtpMode, OneIPVideoProcessorFeature as OneIPVideoProcessorFeature, OneIPVlanFlag as OneIPVlanFlag, oneip_dscp_value as oneip_dscp_value, oneip_rate_valid as oneip_rate_valid
4
4
  from .FrameBase import FrameBase as FrameBase
5
5
  from .FrameHeader import FrameHeader as FrameHeader
6
6
  from .V2IPConfig import V2IPStreamSourceImpl as V2IPStreamSourceImpl, parse_v2ip_av_source as parse_v2ip_av_source
@@ -48,6 +48,8 @@ class FrameV2IPDeviceConfiguration(FrameBase):
48
48
  @staticmethod
49
49
  def construct_settings(mxr: DeviceRegistry, target: Any, target_uid: MxrDeviceUid, settings: OneIPDeviceSettings) -> FrameBase | None: ...
50
50
  @staticmethod
51
+ def construct_downmix(mxr: DeviceRegistry, target: Any, target_uid: MxrDeviceUid, downmix: OneIPDownmix) -> FrameBase | None: ...
52
+ @staticmethod
51
53
  def construct_vlan(mxr: DeviceRegistry, target: Any, target_uid: MxrDeviceUid, vlan: OneIPVlan) -> FrameBase | None: ...
52
54
  @property
53
55
  def target_uid(self) -> MxrDeviceUid | None: ...
@@ -67,4 +69,8 @@ class FrameV2IPDeviceConfiguration(FrameBase):
67
69
  def vlan(self) -> OneIPVlan | None: ...
68
70
  @cached_property
69
71
  def ptp(self) -> OneIPPtpState | None: ...
72
+ @property
73
+ def carries_downmix(self) -> bool: ...
74
+ @cached_property
75
+ def downmix(self) -> OneIPDownmix | None: ...
70
76
  def process(self) -> None: ...
@@ -26,9 +26,10 @@ class FrameV2IPSources(FrameBase):
26
26
  that list was when the frame was built. None when the payload is neither
27
27
  form this opcode carries.
28
28
 
29
- A list that fits one frame is sent as bare records from the first byte.
30
- A longer one is split into pages, each behind a header. That header is
31
- not a whole record, so the remainder separates the two forms - and a
29
+ A list that fits one frame is sent as bare records from the first byte,
30
+ and a unit that knows no source sends it with none. A longer one is
31
+ split into pages, each behind a header. That header is not a whole
32
+ record, so the remainder separates the two forms - and a
32
33
  payload that is neither is refused rather than read from byte zero,
33
34
  which would shift every record by the header's width and report a full
34
35
  set of plausible addresses belonging to no bay.
@@ -37,9 +38,7 @@ class FrameV2IPSources(FrameBase):
37
38
  rather than a promise about the set, so it says whether this frame is
38
39
  the whole list and nothing more.
39
40
  '''
40
- pl = self.payload
41
- if (pl is None):
42
- return None
41
+ pl = self.payload or b''
43
42
  rest = (len(pl) % _RECORD_SIZE)
44
43
  if (rest == 0):
45
44
  return (0, int(len(pl) / _RECORD_SIZE))
@@ -175,9 +175,10 @@ class Bay(BayBase):
175
175
  device = self.device
176
176
  if (getattr(device, 'local_bay')(self.is_input) is not self):
177
177
  return []
178
+ owner = device.remote_id
178
179
  return [bay for dev in device.registry.remotes.values() if (dev is not device)
179
180
  for bay in dev.bays.values()
180
- if isinstance(bay, Bay) and (bay.mesh_owner is self)]
181
+ if isinstance(bay, Bay) and (bay._v2ip_uid == owner) and (bay.mesh_owner is self)]
181
182
 
182
183
  def register_callback(self, callback:Callable[[BayBase],None]) -> None:
183
184
  '''register a callback, called when the device state changed'''