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