remoteRF-server 1.7.9__tar.gz → 1.7.11__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 (213) hide show
  1. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/PKG-INFO +1 -1
  2. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/pyproject.toml +1 -1
  3. remoterf_server-1.7.11/src/remoteRF_server/drivers/wifi/__init__.py +6 -0
  4. remoterf_server-1.7.9/src/remoteRF_server/drivers/wifi_dongle/wifi_dongle_schema.py → remoterf_server-1.7.11/src/remoteRF_server/drivers/wifi/wifi_schema.py +6 -7
  5. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/remote_admin_shell.py +1 -1
  6. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/commands.py +7 -1
  7. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/device_manager.py +42 -0
  8. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/federation/cli.py +41 -9
  9. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/global_onboarding.py +14 -1
  10. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/grpc_server.py +113 -8
  11. remoterf_server-1.7.11/src/remoteRF_server/server/json_output.py +30 -0
  12. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/reservation.py +39 -1
  13. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/serverrf_cli.py +21 -5
  14. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/tools/__init__.py +5 -0
  15. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server.egg-info/PKG-INFO +1 -1
  16. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server.egg-info/SOURCES.txt +6 -3
  17. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_commands.py +7 -0
  18. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_contrib_drivers.py +38 -5
  19. remoterf_server-1.7.11/tests/test_default_wifi_schema.py +154 -0
  20. remoterf_server-1.7.11/tests/test_json_commands.py +230 -0
  21. remoterf_server-1.7.11/tests/test_reservation_cancel.py +146 -0
  22. remoterf_server-1.7.9/src/remoteRF_server/drivers/wifi_dongle/__init__.py +0 -1
  23. remoterf_server-1.7.9/tests/test_default_wifi_dongle_schema.py +0 -26
  24. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/LICENSE +0 -0
  25. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/README.md +0 -0
  26. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/setup.cfg +0 -0
  27. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/__init__.py +0 -0
  28. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/common/__init__.py +0 -0
  29. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/common/grpc/__init__.py +0 -0
  30. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/common/grpc/deployment_capabilities.proto +0 -0
  31. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2.py +0 -0
  32. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2_grpc.py +0 -0
  33. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/common/grpc/federation.proto +0 -0
  34. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/common/grpc/federation_pb2.py +0 -0
  35. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/common/grpc/federation_pb2_grpc.py +0 -0
  36. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
  37. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
  38. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
  39. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
  40. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/common/grpc/v2_codec.py +0 -0
  41. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/common/idl/__init__.py +0 -0
  42. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/common/idl/device_schema.py +0 -0
  43. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/common/idl/pluto_mimo_schema.py +0 -0
  44. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/common/idl/pluto_schema.py +0 -0
  45. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/common/idl/schema.py +0 -0
  46. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/common/idl/v2.py +0 -0
  47. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/common/utils/__init__.py +0 -0
  48. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
  49. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/common/utils/api_token.py +0 -0
  50. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/common/utils/db_connection.py +0 -0
  51. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/common/utils/db_location.py +0 -0
  52. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/common/utils/list_string.py +0 -0
  53. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/common/utils/panel.py +0 -0
  54. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/common/utils/process_arg.py +0 -0
  55. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/drivers/__init__.py +0 -0
  56. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
  57. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/drivers/adalm_pluto/pluto_diag.py +0 -0
  58. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/drivers/adalm_pluto/pluto_mimo_schema.py +0 -0
  59. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +0 -0
  60. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/drivers/adalm_pluto/pluto_schema.py +0 -0
  61. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/drivers/adalm_pluto/poe.py +0 -0
  62. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/drivers/bluetooth_diag.py +0 -0
  63. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/drivers/gps_diag.py +0 -0
  64. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/drivers/hackrf/__init__.py +0 -0
  65. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/drivers/hackrf/hackrf_schema.py +0 -0
  66. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/drivers/rtl_sdr/__init__.py +0 -0
  67. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/drivers/rtl_sdr/rtl_sdr_diag.py +0 -0
  68. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/drivers/rtl_sdr/rtl_sdr_schema.py +0 -0
  69. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/drivers/ti_mmwave/__init__.py +0 -0
  70. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/drivers/ti_mmwave/ti_mmwave_schema.py +0 -0
  71. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/drivers/usrp/__init__.py +0 -0
  72. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/drivers/usrp/policy.py +0 -0
  73. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/drivers/usrp/profiles.py +0 -0
  74. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/drivers/usrp/uhd_manifest.py +0 -0
  75. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/drivers/usrp/usrp_diag.py +0 -0
  76. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/drivers/usrp/usrp_schema.py +0 -0
  77. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/drivers/usrp_phaser/__init__.py +0 -0
  78. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/drivers/usrp_phaser/usrp_phaser_schema.py +0 -0
  79. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/host/__init__.py +0 -0
  80. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/host/host_auth_token.py +0 -0
  81. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/host/host_directory_store.py +0 -0
  82. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/host/host_tunnel_server.py +0 -0
  83. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/__init__.py +0 -0
  84. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/acc_perms.py +0 -0
  85. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/cert_provider.py +0 -0
  86. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/console.py +0 -0
  87. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/control.py +0 -0
  88. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/deployment/__init__.py +0 -0
  89. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/deployment/capabilities.py +0 -0
  90. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/diag.py +0 -0
  91. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/direct_path.py +0 -0
  92. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/dynamic_v2.py +0 -0
  93. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/dynamic_v2_bridge.py +0 -0
  94. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/federation/__init__.py +0 -0
  95. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/federation/admin.py +0 -0
  96. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/federation/cluster.py +0 -0
  97. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/federation/contracts.py +0 -0
  98. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/federation/destination.py +0 -0
  99. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/federation/devices.py +0 -0
  100. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/federation/enrollment.py +0 -0
  101. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/federation/global_home.py +0 -0
  102. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/federation/home.py +0 -0
  103. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/federation/home_devices.py +0 -0
  104. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/federation/identity.py +0 -0
  105. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/federation/legacy_state.py +0 -0
  106. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/federation/links.py +0 -0
  107. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/federation/migrations.py +0 -0
  108. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/federation/peers.py +0 -0
  109. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/federation/permissions.py +0 -0
  110. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/federation/principals.py +0 -0
  111. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/federation/runtime.py +0 -0
  112. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/federation/trust.py +0 -0
  113. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/host_direct.py +0 -0
  114. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/host_dynamic_v2.py +0 -0
  115. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/remote_admin.py +0 -0
  116. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/remote_admin_ssh.py +0 -0
  117. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/rpc_manager.py +0 -0
  118. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/schema_registry.py +0 -0
  119. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/startup_check.py +0 -0
  120. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/user_group_cli.py +0 -0
  121. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/user_group_handler.py +0 -0
  122. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/server/v2_errors.py +0 -0
  123. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/tools/gen_certs.py +0 -0
  124. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/tools/gist_status.py +0 -0
  125. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/tools/gist_status_testing.py +0 -0
  126. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server/tools/introspect_uhd.py +0 -0
  127. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
  128. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
  129. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server.egg-info/requires.txt +0 -0
  130. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/remoteRF_server.egg-info/top_level.txt +0 -0
  131. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/rssi/__init__.py +0 -0
  132. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/rssi/cli.py +0 -0
  133. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/rssi/dongle.py +0 -0
  134. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/rssi/safety.py +0 -0
  135. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/src/rssi/scanner.py +0 -0
  136. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_account_hardening.py +0 -0
  137. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_account_names.py +0 -0
  138. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_account_removal.py +0 -0
  139. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_activity_log.py +0 -0
  140. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_availability.py +0 -0
  141. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_bind_host.py +0 -0
  142. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_cert_provider.py +0 -0
  143. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_client_sessions.py +0 -0
  144. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_cluster.py +0 -0
  145. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_console.py +0 -0
  146. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_default_rtl_sdr_schema.py +0 -0
  147. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_default_usrp_schema.py +0 -0
  148. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_deployment_capabilities.py +0 -0
  149. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_detached_device_rpc.py +0 -0
  150. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_device_core_aliases.py +0 -0
  151. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_device_handles.py +0 -0
  152. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_device_init.py +0 -0
  153. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_devices_add_wizard.py +0 -0
  154. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_devices_list.py +0 -0
  155. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_diag.py +0 -0
  156. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_direct_fixed_port.py +0 -0
  157. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_direct_path_server.py +0 -0
  158. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_dynamic_v2.py +0 -0
  159. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_federated_direct_path.py +0 -0
  160. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_federated_dynamic_v2.py +0 -0
  161. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_federation_edges.py +0 -0
  162. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_first_run.py +0 -0
  163. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_gate_d_contracts.py +0 -0
  164. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_gate_e_federation.py +0 -0
  165. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_gate_e_proto.py +0 -0
  166. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_gate_f_enrollment.py +0 -0
  167. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_gate_g1_logging.py +0 -0
  168. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_gate_g_permissions.py +0 -0
  169. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_gate_h_devices.py +0 -0
  170. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_gate_i_direct_path.py +0 -0
  171. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_gen_certs.py +0 -0
  172. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_global_accounts_migration.py +0 -0
  173. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_global_home.py +0 -0
  174. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_global_onboarding.py +0 -0
  175. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_global_v1_disposal.py +0 -0
  176. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_hackrf_schema.py +0 -0
  177. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_home_device_plane.py +0 -0
  178. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_host_direct.py +0 -0
  179. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_host_directory_store.py +0 -0
  180. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_host_dynamic_v2.py +0 -0
  181. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_idl_schema.py +0 -0
  182. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_lab_logins.py +0 -0
  183. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_panel.py +0 -0
  184. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_peer_transport.py +0 -0
  185. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_pluto_compat.py +0 -0
  186. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_pluto_poe.py +0 -0
  187. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_process_arg.py +0 -0
  188. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_proxy_lifecycle.py +0 -0
  189. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_remote_admin.py +0 -0
  190. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_reservation_handoff.py +0 -0
  191. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_reservation_policy_b.py +0 -0
  192. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_reservation_timestamps.py +0 -0
  193. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_rpc_manager_batch.py +0 -0
  194. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_rpc_manager_calln.py +0 -0
  195. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_rpc_manager_host_legacy.py +0 -0
  196. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_rpc_manager_rx_n.py +0 -0
  197. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_rtl_sdr_schema.py +0 -0
  198. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_schema_registry.py +0 -0
  199. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_soak_federation.py +0 -0
  200. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_startup_check.py +0 -0
  201. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_status_push.py +0 -0
  202. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_ti_mmwave_hardware.py +0 -0
  203. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_ti_mmwave_schema.py +0 -0
  204. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_user_group_cli.py +0 -0
  205. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_usrp_b205mini_uhd410_hardware.py +0 -0
  206. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_usrp_client_server_hardware.py +0 -0
  207. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_usrp_client_server_localhost.py +0 -0
  208. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_usrp_n210_client_server_hardware.py +0 -0
  209. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_usrp_preload.py +0 -0
  210. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_usrp_profiles.py +0 -0
  211. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_usrp_remoterf_rx_hardware.py +0 -0
  212. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_usrp_uhd410_hardware.py +0 -0
  213. {remoterf_server-1.7.9 → remoterf_server-1.7.11}/tests/test_wipe.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 1.7.9
3
+ Version: 1.7.11
4
4
  Summary: RemoteRF server-side control package
5
5
  License: GPL-3.0-or-later
6
6
  Classifier: Programming Language :: Python :: 3
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "remoteRF-server"
7
- version = "1.7.9"
7
+ version = "1.7.11"
8
8
  description = "RemoteRF server-side control package"
9
9
  license = { text = "GPL-3.0-or-later" }
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,6 @@
1
+
2
+ """Built-in Wi-Fi schema support."""
3
+
4
+ from .wifi_schema import WifiSchema
5
+
6
+ __all__ = ["WifiSchema"]
@@ -566,19 +566,18 @@ class WifiDongleDevice:
566
566
  # RemoteRF Device Schema Class
567
567
  # -----------------------------------------------------------------------------
568
568
 
569
- @idl_register("wifi_dongle")
570
- class WifiDongleSchema(DeviceSchema):
569
+ @idl_register("wifi")
570
+ class WifiSchema(DeviceSchema):
571
571
  """
572
572
  RemoteRF Device Schema for USB Wi-Fi Dongles (RSSI Collector & RF Monitor).
573
573
 
574
574
  Exposes live wireless telemetry, accurate linear-power RSSI statistics,
575
575
  batch burst sampling, and deep 802.11 physical layer diagnostics to remote Python clients.
576
576
  """
577
- device_type = "wifi_dongle"
577
+ device_type = "wifi"
578
578
  client_class = "WifiDongle"
579
579
  driver_version = "0.1.0"
580
580
 
581
- client_modules = {"wifi_dongle": "remoteRF.drivers.support.wifi_dongle"}
582
581
 
583
582
  @staticmethod
584
583
  def make_device(**kwargs) -> Optional[WifiDongleDevice]:
@@ -599,16 +598,16 @@ class WifiDongleSchema(DeviceSchema):
599
598
  max_history = int(kwargs.get("max_history", 100))
600
599
 
601
600
  try:
602
- print(f"[wifi_dongle] Initializing USB Wi-Fi dongle (interface={interface or 'auto'})...")
601
+ print(f"[wifi] Initializing USB Wi-Fi dongle (interface={interface or 'auto'})...")
603
602
  dev = WifiDongleDevice(
604
603
  interface=interface,
605
604
  auto_rescan=auto_rescan,
606
605
  max_history=max_history
607
606
  )
608
- print(f"[wifi_dongle] Bound to interface '{dev.interface}'. Default route protected.")
607
+ print(f"[wifi] Bound to interface '{dev.interface}'. Default route protected.")
609
608
  return dev
610
609
  except Exception as exc:
611
- print(f"[wifi_dongle] Failed to initialize device: {exc}")
610
+ print(f"[wifi] Failed to initialize device: {exc}")
612
611
  return None
613
612
 
614
613
  # -------------------------------------------------------------------------
@@ -56,7 +56,7 @@ TIERS = {
56
56
  "cluster create": WRITE, "cluster invite": WRITE, "cluster join": WRITE, "cluster kick": WRITE,
57
57
  "cluster cap": WRITE, "cluster peers": WRITE, "restart": WRITE, "reload devices": WRITE,
58
58
  "db export": WRITE, "cluster groups": WRITE, "cluster codes create": WRITE, "cluster accept": WRITE,
59
- "schema download": WRITE,
59
+ "schema download": WRITE, "reservations cancel": WRITE,
60
60
  # write, interactive in the daemon: through the control socket's prompt bridge
61
61
  "users manage": WRITE, "diag": WRITE,
62
62
  # destructive: password again, a typed word, an automatic backup, then -y
@@ -126,6 +126,7 @@ def _table() -> list[Command]:
126
126
  Command(("users prune-proxies",), "Partner-deployment visitors whose home is gone: list them, -y removes [--stale DAYS]", _daemon("cmd_users_prune_proxies"), "Users", where=D),
127
127
  # ---- reservations ----
128
128
  Command(("reservations list",), "List reservations", _daemon("cmd_reservations_list"), "Reservations", where=D),
129
+ Command(("reservations cancel",), "Cancel one reservation: reservations cancel <res_id> [-y]", _daemon("cmd_reservations_cancel"), "Reservations", where=D),
129
130
  Command(("reservations purge",), "Remove ALL reservations", _daemon("cmd_reservations_purge"), "Reservations", where=D),
130
131
  # ---- groups ----
131
132
  Command(("groups list",), "List user groups", _shell("cmd_groups_list"), "Groups"),
@@ -361,7 +362,12 @@ def _run_in_daemon(command: Command, rest: list[str]) -> int:
361
362
  # for a remote admin: that would hand them the live console, past the policy.
362
363
  return console.run_in_console([*command.names[0].split(), *rest])
363
364
  # A prompt inside the daemon is asked here, over the socket (control.call).
364
- reply = control.call([*command.names[0].split(), *rest], timeout=3600.0 if command.interactive else 30.0)
365
+ timeout = (
366
+ 3600.0 if command.interactive
367
+ else 120.0 if command.names[0] == "reload devices"
368
+ else 30.0
369
+ )
370
+ reply = control.call([*command.names[0].split(), *rest], timeout=timeout)
365
371
  if reply is None:
366
372
  print("The server stopped answering.", file=sys.stderr)
367
373
  return 1
@@ -395,6 +395,10 @@ def _connect_from_record(rec: Dict[str, Any], *, connect: bool = True):
395
395
  # Keep those inventories usable while all new writes use the canonical type.
396
396
  if dtype == "pluto":
397
397
  dtype = "adalm_pluto"
398
+ # The built-in Wi-Fi driver's original public type was "wifi_dongle".
399
+ # Keep existing inventories usable while all new schemas use "wifi".
400
+ if dtype == "wifi_dongle":
401
+ dtype = "wifi"
398
402
  ident = (rec.get("IDENT") or "").strip()
399
403
  init_kwargs = rec.get("INIT") or {}
400
404
  if not connect:
@@ -493,6 +497,13 @@ def _packaged_driver_schema_path(filename: str) -> Path:
493
497
  for candidate in sorted(drivers_root.glob(f"*/{filename}")):
494
498
  if candidate.exists():
495
499
  return candidate
500
+ server_drivers_root = Path(__file__).resolve().parents[1] / "drivers"
501
+ direct = server_drivers_root / filename
502
+ if direct.exists():
503
+ return direct
504
+ for candidate in sorted(server_drivers_root.glob(f"*/{filename}")):
505
+ if candidate.exists():
506
+ return candidate
496
507
  return drivers_root / "adalm_pluto" / filename
497
508
 
498
509
  def _driver_registers_type(path: Path, device_type: str) -> bool:
@@ -555,6 +566,27 @@ def _refresh_managed_driver(
555
566
  return True
556
567
 
557
568
 
569
+ def _remove_managed_driver(target: Path) -> bool:
570
+ """Remove a schema only when its managed marker still matches its contents."""
571
+ marker = _managed_driver_marker(target)
572
+ if not target.is_file() or not marker.is_file():
573
+ return False
574
+ try:
575
+ marker_hash = marker.read_text(encoding="utf-8").strip()
576
+ target_hash = _driver_sha256(target)
577
+ normalized_hash = _driver_normalized_sha256(target)
578
+ except OSError:
579
+ return False
580
+ if marker_hash not in (target_hash, normalized_hash):
581
+ return False
582
+ try:
583
+ target.unlink()
584
+ marker.unlink()
585
+ except OSError:
586
+ return False
587
+ return True
588
+
589
+
558
590
  def _seed_default_driver(
559
591
  filename: str,
560
592
  *device_types: str,
@@ -655,6 +687,14 @@ def _seed_default_usrp_phaser_driver() -> None:
655
687
  _seed_default_driver("usrp_phaser_schema.py", "usrp_phaser")
656
688
 
657
689
 
690
+ def _seed_default_wifi_driver() -> None:
691
+ drivers_dir = get_drivers_dir()
692
+ legacy = drivers_dir / "wifi_dongle_schema.py"
693
+ if not _remove_managed_driver(legacy) and legacy.is_file():
694
+ print(f"[drivers] Kept {legacy}: not a managed copy. Remove it, or two Wi-Fi drivers register.")
695
+ _seed_default_driver("wifi_schema.py", "wifi")
696
+
697
+
658
698
  # Only the C library from conda: pip already installed pyadi-iio, and conda's
659
699
  # pulls a cryptography that breaks the pip-installed pyOpenSSL aioquic needs.
660
700
  _LIBIIO_FIX = "conda install -c conda-forge libiio (in the server's conda env; not pyadi-iio, pip already has it)"
@@ -667,6 +707,7 @@ def _load_all_drivers() -> None:
667
707
  _seed_default_hackrf_driver()
668
708
  _seed_default_ti_mmwave_driver()
669
709
  _seed_default_usrp_phaser_driver()
710
+ _seed_default_wifi_driver()
670
711
 
671
712
  # The Pluto drivers need the native libiio behind pylibiio; without it
672
713
  # every one of them fails the same way, which is worth one line, not four.
@@ -690,6 +731,7 @@ def _load_all_drivers() -> None:
690
731
  ("built-in hackrf driver", "..drivers.hackrf.hackrf_schema"),
691
732
  ("built-in TI mmWave driver", "..drivers.ti_mmwave.ti_mmwave_schema"),
692
733
  ("built-in USRP Phaser driver", "..drivers.usrp_phaser.usrp_phaser_schema"),
734
+ ("built-in Wi-Fi driver", "..drivers.wifi.wifi_schema"),
693
735
  ):
694
736
  try:
695
737
  importlib.import_module(module, __package__)
@@ -47,7 +47,27 @@ def _day(epoch) -> str:
47
47
  return datetime.datetime.fromtimestamp(int(epoch)).strftime("%Y-%m-%d")
48
48
 
49
49
 
50
- def cmd_clusters() -> int:
50
+ def _cluster_json(item: dict) -> dict:
51
+ if item["role"] == "owner":
52
+ parties = item.get("members", [])
53
+ else:
54
+ parties = [{
55
+ "deployment_id": item["peer"],
56
+ "display_name": item.get("peer_name"),
57
+ "state": item.get("state"),
58
+ }, *item.get("peers", [])]
59
+ return {
60
+ "name": str(item["name"]),
61
+ "lab_cap": int(item["lab_cap"]) if item.get("lab_cap") else None,
62
+ "members": [{
63
+ "deployment_id": str(member["deployment_id"]),
64
+ "display_name": str(member.get("display_name") or member["deployment_id"]),
65
+ "state": str(member.get("state") or ""),
66
+ } for member in parties],
67
+ }
68
+
69
+
70
+ def cmd_clusters(*, json_output: bool = False) -> int:
51
71
  from ...common.utils.panel import marker, print_panel
52
72
  from . import cluster
53
73
 
@@ -59,8 +79,13 @@ def cmd_clusters() -> int:
59
79
  print(f"Federation is not enabled: {exc}")
60
80
  return 1
61
81
  state = load_state(Path(config["identity_dir"]))
62
- rows = [f"{marker()} {config['display_name']}", ""]
63
82
  items = cluster.list_clusters()
83
+ if json_output:
84
+ from ..json_output import print_json
85
+
86
+ print_json([_cluster_json(item) for item in items])
87
+ return 0
88
+ rows = [f"{marker()} {config['display_name']}", ""]
64
89
  if not items:
65
90
  rows.append("No clusters. Start one with `cluster create <name>`.")
66
91
  me = {"display_name": config["display_name"], "deployment_id": state.deployment_id}
@@ -108,11 +133,8 @@ def cmd_clusters() -> int:
108
133
  return 0
109
134
 
110
135
 
111
- def cmd_cluster_info(name: str) -> int:
136
+ def cmd_cluster_info(name: str, *, json_output: bool = False) -> int:
112
137
  """Every lab in the cluster, each shared group with its limits, each device."""
113
- from ...common.utils.panel import print_panel
114
- from ..device_manager import describe_devices
115
- from ..user_group_cli import _fmt_duration
116
138
  from . import cluster
117
139
 
118
140
  config = load_config()
@@ -120,6 +142,15 @@ def cmd_cluster_info(name: str) -> int:
120
142
  item = {i["name"]: i for i in cluster.list_clusters()}.get(name)
121
143
  if item is None:
122
144
  raise cluster.ClusterError(f"no cluster named {name}")
145
+ if json_output:
146
+ from ..json_output import print_json
147
+
148
+ print_json(_cluster_json(item))
149
+ return 0
150
+ from ...common.utils.panel import print_panel
151
+ from ..device_manager import describe_devices
152
+ from ..user_group_cli import _fmt_duration
153
+
123
154
  me = {"display_name": config["display_name"], "deployment_id": state.deployment_id}
124
155
  mine = {"groups": len(item["groups"]), "detail": [
125
156
  {"name": g["group_name"], "max_reservation_time_sec": g["max_reservation_time_sec"],
@@ -139,7 +170,8 @@ def cmd_cluster_info(name: str) -> int:
139
170
  for index, (lab, role, sharing) in enumerate(labs):
140
171
  if index:
141
172
  rows.append("")
142
- rows.append(f"{_lab(lab)} {role}" + (" (you)" if lab["deployment_id"] == me["deployment_id"] else ""))
173
+ rows.append(f"{_lab(lab)} {role} {lab['deployment_id']}"
174
+ + (" (you)" if lab["deployment_id"] == me["deployment_id"] else ""))
143
175
  if not sharing.get("detail"):
144
176
  rows.append(" shares nothing")
145
177
  for group in sharing["detail"]:
@@ -310,7 +342,7 @@ def cmd_cluster(argv: list[str]) -> int:
310
342
  flags = [a for a in argv[1:] if a.startswith("--")]
311
343
  words = [a for a in argv[1:] if not a.startswith("--")]
312
344
  if verb == "list" and not words:
313
- return cmd_clusters()
345
+ return cmd_clusters(json_output="--json" in flags)
314
346
  if verb == "codes" and (not words or words[0] not in ("create", "list")):
315
347
  print("Usage: cluster codes create [<cluster>] | cluster codes list [<cluster>]", file=sys.stderr)
316
348
  return 2
@@ -370,7 +402,7 @@ def cmd_cluster(argv: list[str]) -> int:
370
402
  + (_n(int(answer), "reservation") if int(answer) else "any number of reservations")
371
403
  + " on your devices at a time.", Sty.GRAY)
372
404
  elif verb == "info" and arg:
373
- return cmd_cluster_info(arg)
405
+ return cmd_cluster_info(arg, json_output="--json" in flags)
374
406
  elif verb == "groups" and len(words) == 1:
375
407
  cluster._find(cluster.load(), arg)
376
408
  return _share_groups(arg)
@@ -1169,7 +1169,18 @@ _STATE_SUMMARY = {
1169
1169
  }
1170
1170
 
1171
1171
 
1172
- def cmd_status(refresh: bool = True) -> int:
1172
+ def cmd_status(refresh: bool = True, *, json_output: bool = False) -> int:
1173
+ if json_output:
1174
+ from .json_output import print_json
1175
+
1176
+ kv = load()
1177
+ print_json({
1178
+ "state": str(kv.get("GLOBAL_STATE") or ("unregistered" if kv else "unconfigured")),
1179
+ "slug": kv.get("GLOBAL_FINAL_NAME") or kv.get("GLOBAL_NAME") or None,
1180
+ "deployment_id": kv.get("GLOBAL_DEPLOYMENT_ID") or None,
1181
+ "relay": kv.get("GLOBAL_RELAY_HOSTNAME") or None,
1182
+ })
1183
+ return 0
1173
1184
  from ..common.utils.panel import marker, print_panel
1174
1185
 
1175
1186
  kv = load()
@@ -1310,6 +1321,8 @@ _COMMANDS = {"status": cmd_status, "join": cmd_join, "leave": cmd_leave}
1310
1321
 
1311
1322
 
1312
1323
  def main(argv) -> int:
1324
+ if argv == ["status", "--json"]:
1325
+ return cmd_status(json_output=True)
1313
1326
  if argv and argv[0] == "verify":
1314
1327
  # A code pasted from email may arrive wrapped across lines.
1315
1328
  return cmd_verify("".join(argv[1:]))
@@ -54,7 +54,8 @@ from .dynamic_v2 import (
54
54
  from .host_dynamic_v2 import host_session_proxy
55
55
  from .acc_perms import perms
56
56
  from ..common.utils import *
57
- from .device_manager import close_local_devices, reload_devices, get_registered_devices, get_all_devices_str, handle_report
57
+ from .device_manager import (close_local_devices, reload_devices, get_registered_devices,
58
+ get_all_devices_str, handle_report, device_inventory_snapshot)
58
59
  from .user_group_cli import make_user_group_local, edit_user_group_local, list_user_groups_local, remove_user_group_local, make_enrollment_codes_local, remove_enrollment_code_local, list_enrollment_codes_local, export_user_groups_csv_local, export_enrollment_codes_csv_local
59
60
  from .user_group_handler import start_usergroup_cleanup_loop
60
61
  from .global_onboarding import start_agent
@@ -681,7 +682,33 @@ def set_acc():
681
682
  elif inpu == 'c':
682
683
  print("Canceled. Exiting ...")
683
684
 
684
- def get_devices():
685
+ def _devices_json() -> list[dict]:
686
+ from .json_output import epoch_ms_iso
687
+
688
+ reg = hts.get_tunnel_registry(create=False)
689
+ live = reg.list_devices() if reg is not None else {}
690
+ rows = []
691
+ for device_id, item in sorted(device_inventory_snapshot().items()):
692
+ ds = live.get(str(device_id))
693
+ rows.append({
694
+ "device_id": int(device_id),
695
+ "name": str(item["name"]),
696
+ "handle": str(item["handle"]),
697
+ "type": str(item["device_type"] or ""),
698
+ "host": f"{ds.host_id} #{ds.host_local_id}" if ds else "local",
699
+ "online": bool(item["online"]),
700
+ "state": None,
701
+ "last_seen": epoch_ms_iso(ds.last_seen_ms) if ds else None,
702
+ })
703
+ return rows
704
+
705
+
706
+ def get_devices(*, json_output: bool = False):
707
+ if json_output:
708
+ from .json_output import print_json
709
+
710
+ print_json(_devices_json())
711
+ return
685
712
  from ..common.utils.panel import print_table
686
713
 
687
714
  reg = hts.get_tunnel_registry(create=False)
@@ -711,7 +738,33 @@ def _fmt_ms_ago(ms: int) -> str:
711
738
  return f"{dt/1000:.1f}s"
712
739
  return f"{dt/60_000:.1f}m"
713
740
 
714
- def _cmd_hosts_status() -> None:
741
+ def _hosts_json() -> list[dict]:
742
+ from .json_output import epoch_ms_iso
743
+
744
+ reg = hts.get_tunnel_registry()
745
+ assert reg is not None
746
+ hosts = reg.list_hosts()
747
+ by_host: dict[str, list[int]] = {}
748
+ for key, ds in reg.list_devices().items():
749
+ try:
750
+ device_id = int(key)
751
+ except (TypeError, ValueError):
752
+ continue
753
+ by_host.setdefault(ds.host_id or "-", []).append(device_id)
754
+ return [{
755
+ "host_id": str(host_id),
756
+ "online": bool(status.online),
757
+ "last_seen": epoch_ms_iso(status.last_seen_ms),
758
+ "devices": sorted(by_host.get(host_id, [])),
759
+ } for host_id, status in sorted(hosts.items())]
760
+
761
+
762
+ def _cmd_hosts_status(*, json_output: bool = False) -> None:
763
+ if json_output:
764
+ from .json_output import print_json
765
+
766
+ print_json(_hosts_json())
767
+ return
715
768
  from ..common.utils.panel import print_table
716
769
 
717
770
  reg = hts.get_tunnel_registry()
@@ -1139,7 +1192,32 @@ def _cmd_detach() -> None:
1139
1192
  printf("Not in the console; the server runs in this terminal. Use quit to stop it.", Sty.GRAY)
1140
1193
 
1141
1194
 
1142
- def _cmd_status() -> None:
1195
+ def _status_json() -> dict:
1196
+ from . import global_onboarding as g
1197
+
1198
+ devices = device_inventory_snapshot()
1199
+ now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
1200
+ active = g._count(
1201
+ reservation_handler.filepath,
1202
+ "SELECT COUNT(*) FROM Reservations WHERE start_time <= ? AND end_time > ?",
1203
+ (now, now),
1204
+ )
1205
+ return {
1206
+ "server_version": g.server_version(),
1207
+ "online": True,
1208
+ "uptime_s": max(0, int((datetime.now() - start_time).total_seconds())),
1209
+ "devices_total": len(devices),
1210
+ "devices_online": sum(bool(item["online"]) for item in devices.values()),
1211
+ "reservations_active": int(active),
1212
+ }
1213
+
1214
+
1215
+ def _cmd_status(*, json_output: bool = False) -> None:
1216
+ if json_output:
1217
+ from .json_output import print_json
1218
+
1219
+ print_json(_status_json())
1220
+ return
1143
1221
  from ..common.utils.panel import marker, print_panel
1144
1222
  from . import global_onboarding as g
1145
1223
 
@@ -1278,16 +1356,43 @@ def _reexec() -> None:
1278
1356
 
1279
1357
 
1280
1358
  # The shared command table calls these by name (commands.py).
1281
- def cmd_status(argv): _cmd_status()
1359
+ def cmd_status(argv): _cmd_status(json_output="--json" in argv)
1282
1360
  def cmd_detach(argv): _cmd_detach()
1283
1361
  def cmd_clear(argv): clear()
1284
- def cmd_hosts_status(argv): _cmd_hosts_status()
1362
+ def cmd_hosts_status(argv): _cmd_hosts_status(json_output="--json" in argv)
1285
1363
  def cmd_hosts_wipe(argv): _cmd_hosts_wipe(yes=("-y" in argv or "--yes" in argv))
1286
1364
  def cmd_users_list(argv): reservation_handler.print_all_accounts()
1287
1365
  def cmd_users_manage(argv): set_acc()
1288
1366
  def cmd_users_perms(argv): perms.print_perms()
1289
1367
  def cmd_users_purge(argv): reservation_handler.remove_all_users()
1290
- def cmd_reservations_list(argv): reservation_handler.print_all_reservations()
1368
+ def cmd_reservations_list(argv): reservation_handler.print_all_reservations(json_output="--json" in argv)
1369
+
1370
+
1371
+ def cmd_reservations_cancel(argv):
1372
+ args = [arg for arg in argv if arg not in ("-y", "--yes")]
1373
+ if len(args) != 1 or any(arg.startswith("-") for arg in args):
1374
+ print("Usage: reservations cancel <res_id> [-y]")
1375
+ return 2
1376
+ try:
1377
+ res_id = int(args[0])
1378
+ if not 0 < res_id < 2**63 or str(res_id) != args[0]:
1379
+ raise ValueError
1380
+ except ValueError:
1381
+ print("Reservation ID must be a positive integer.")
1382
+ return 2
1383
+ row = reservation_handler.cancel_reservation_admin(res_id)
1384
+ if row is None:
1385
+ print(f"Reservation {res_id} was not found or has already ended.")
1386
+ return 1
1387
+ if row is False:
1388
+ print(f"Reservation {res_id} is on a partner lab; the user cancels it from their client.")
1389
+ return 3
1390
+ print(" ".join(str(value) for value in row))
1391
+ try:
1392
+ reservation_handler.update_devices()
1393
+ except Exception as exc: # noqa: BLE001 - the delete is committed; report the pending release
1394
+ print(f"Warning: device release pending ({exc}); it completes on the next device update.")
1395
+ return 0
1291
1396
 
1292
1397
 
1293
1398
  def cmd_users_prune_proxies(argv):
@@ -1353,7 +1458,7 @@ def cmd_stats_purge(argv):
1353
1458
  printf("No activity log to purge.", Sty.GRAY)
1354
1459
  return
1355
1460
  printf(f"Purged activity log: {reservation.LOG_PATH}", Sty.GRAY)
1356
- def cmd_devices_list(argv): get_devices()
1461
+ def cmd_devices_list(argv): get_devices(json_output="--json" in argv)
1357
1462
 
1358
1463
 
1359
1464
  def cmd_diag(argv):
@@ -0,0 +1,30 @@
1
+ # Copyright (C) 2026 RemoteRF
2
+ #
3
+ # This program is free software: you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation, either version 3 of the License, or
6
+ # (at your option) any later version.
7
+
8
+ """Small, shared pieces of the machine-readable serverrf output."""
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from datetime import datetime
14
+
15
+
16
+ def print_json(value) -> None:
17
+ print(json.dumps(value, separators=(",", ":")))
18
+
19
+
20
+ def local_iso(value) -> str:
21
+ """Render a stored local datetime with the server's UTC offset."""
22
+ parsed = value if isinstance(value, datetime) else datetime.fromisoformat(str(value).strip())
23
+ return parsed.astimezone().isoformat()
24
+
25
+
26
+ def epoch_ms_iso(value) -> str | None:
27
+ milliseconds = int(value or 0)
28
+ if milliseconds <= 0:
29
+ return None
30
+ return datetime.fromtimestamp(milliseconds / 1000).astimezone().isoformat()
@@ -802,6 +802,26 @@ class ReservationHandler:
802
802
  def remove_reservation(self, *, res_id):
803
803
  self.cursor.execute('DELETE FROM Reservations WHERE res_id = ?', (res_id,))
804
804
  return {"UC": map_arg(f'Removed reservation {res_id}')}
805
+
806
+ @db_connection
807
+ def cancel_reservation_admin(self, res_id: int):
808
+ row = self.cursor.execute(
809
+ "SELECT res_id, username, device_id, start_time, end_time "
810
+ "FROM Reservations WHERE res_id = ?",
811
+ (int(res_id),),
812
+ ).fetchone()
813
+ if row is None or _parse_reservation_datetime(row[4]) <= datetime.now():
814
+ return None
815
+ # A booking our user holds on a partner lab is cancelled there first (home_devices.cancel).
816
+ if self.cursor.execute(
817
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name='HomeFederatedReservations'"
818
+ ).fetchone() and self.cursor.execute(
819
+ "SELECT 1 FROM HomeFederatedReservations WHERE res_id = ?", (int(res_id),)
820
+ ).fetchone():
821
+ return False
822
+ self.cursor.execute("DELETE FROM Reservations WHERE res_id = ?", (int(res_id),))
823
+ self.cursor.execute("DELETE FROM FederatedReservations WHERE res_id = ?", (int(res_id),))
824
+ return row
805
825
 
806
826
  @db_connection
807
827
  def rm_db(self):
@@ -1336,7 +1356,25 @@ class ReservationHandler:
1336
1356
  return [(_parse_reservation_datetime(a), _parse_reservation_datetime(b)) for a, b in rows]
1337
1357
 
1338
1358
  @db_connection
1339
- def print_all_reservations(self):
1359
+ def print_all_reservations(self, *, json_output: bool = False):
1360
+ if json_output:
1361
+ from .json_output import local_iso, print_json
1362
+
1363
+ rows = self.cursor.execute(
1364
+ "SELECT r.res_id, r.username, r.device_id, r.start_time, r.end_time, "
1365
+ "f.issuer_deployment_id FROM Reservations r LEFT JOIN FederatedReservations f "
1366
+ "ON f.res_id=r.res_id ORDER BY r.res_id"
1367
+ ).fetchall()
1368
+ print_json([{
1369
+ "res_id": int(res_id),
1370
+ "username": str(username),
1371
+ "device_id": int(device_id),
1372
+ "start_time": local_iso(start),
1373
+ "end_time": local_iso(end),
1374
+ "federated": issuer is not None,
1375
+ "issuer_deployment_id": str(issuer) if issuer is not None else None,
1376
+ } for res_id, username, device_id, start, end, issuer in rows])
1377
+ return
1340
1378
  from ..common.utils.panel import print_table
1341
1379
 
1342
1380
  cursor = self.db.cursor()
@@ -28,6 +28,7 @@ from datetime import datetime, timezone
28
28
  import base64
29
29
  import secrets
30
30
  import hashlib
31
+ import json
31
32
  import getpass
32
33
  import yaml
33
34
 
@@ -1485,11 +1486,14 @@ def _device_remove(global_id_str: str) -> int:
1485
1486
  return 0
1486
1487
 
1487
1488
 
1488
- def _device_show() -> int:
1489
+ def _device_show(*, json_output: bool = False) -> int:
1489
1490
  from remoteRF_server.common.utils.panel import print_table
1490
1491
 
1491
1492
  p = _devices_yaml_path()
1492
1493
  if not p.exists() and not _devices_env_path().exists():
1494
+ if json_output:
1495
+ print("[]")
1496
+ return 0
1493
1497
  print(f"No devices configured (missing {p}).")
1494
1498
  return 0
1495
1499
 
@@ -1508,6 +1512,18 @@ def _device_show() -> int:
1508
1512
  if is_device_handle(h):
1509
1513
  handles[gid] = h if h not in seen else f"{h} ignored (duplicate name)"
1510
1514
  seen.add(h)
1515
+ if json_output:
1516
+ print(json.dumps([{
1517
+ "device_id": int(gid),
1518
+ "name": str(record.get("NAME", "")),
1519
+ "handle": str(handles.get(gid, "")),
1520
+ "type": str(record.get("TYPE", "")),
1521
+ "host": "local",
1522
+ "online": False,
1523
+ "state": None,
1524
+ "last_seen": None,
1525
+ } for gid, record in sorted(recs.items())], separators=(",", ":")))
1526
+ return 0
1511
1527
  rows = [
1512
1528
  {"id": gid, "type": r.get("TYPE", ""), "name": r.get("NAME", ""),
1513
1529
  "handle": handles.get(gid, "-"),
@@ -1641,13 +1657,13 @@ def cmd_devices_list(argv: List[str]) -> int:
1641
1657
  if commands._IN_DAEMON:
1642
1658
  from remoteRF_server.server.grpc_server import get_devices
1643
1659
 
1644
- get_devices()
1660
+ get_devices(json_output="--json" in argv)
1645
1661
  return 0
1646
- reply = control.call(["devices", "list"])
1662
+ reply = control.call(["devices", "list", *(["--json"] if "--json" in argv else [])])
1647
1663
  if reply is not None:
1648
1664
  sys.stdout.write(reply[1])
1649
1665
  return reply[0]
1650
- return _device_show()
1666
+ return _device_show(json_output="--json" in argv)
1651
1667
 
1652
1668
 
1653
1669
  # -----------------------------
@@ -2424,7 +2440,7 @@ def cmd_global(argv: List[str]) -> int:
2424
2440
  from remoteRF_server.server.global_onboarding import main as global_main
2425
2441
 
2426
2442
  rc = global_main(list(argv))
2427
- if rc == 0:
2443
+ if rc == 0 and "--json" not in argv:
2428
2444
  _ensure_certificate_names()
2429
2445
  return rc
2430
2446
 
@@ -14,6 +14,11 @@
14
14
  # You should have received a copy of the GNU General Public License
15
15
  # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
16
 
17
+ """RRRFcerts: generate a local CA and a server TLS certificate/key with SANs.
18
+
19
+ Output goes to ~/.config/remoterf/certs by default. Clients should trust ca.crt (not server.crt) and must dial an address that matches one of the SANs.
20
+ """
21
+
17
22
  from __future__ import annotations
18
23
 
19
24
  import argparse
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 1.7.9
3
+ Version: 1.7.11
4
4
  Summary: RemoteRF server-side control package
5
5
  License: GPL-3.0-or-later
6
6
  Classifier: Programming Language :: Python :: 3