esp-rainmaker-cli 1.13.1__tar.gz → 1.14.1__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 (126) hide show
  1. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/PKG-INFO +21 -1
  2. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/esp_rainmaker_cli.egg-info/PKG-INFO +21 -1
  3. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rainmaker/rainmaker.py +139 -1
  4. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rainmaker/version.py +1 -1
  5. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_cmd/group.py +218 -2
  6. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_cmd/node.py +22 -2
  7. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_cmd/provision.py +9 -2
  8. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_lib/node_cache.py +4 -1
  9. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_lib/session.py +148 -0
  10. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/security/security2.py +66 -0
  11. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/integration.py +105 -21
  12. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/raw_config.py +4 -2
  13. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/raw_params.py +5 -2
  14. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/esp_rainmaker_prov.py +35 -16
  15. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/LICENSE +0 -0
  16. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/README.md +0 -0
  17. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/esp_rainmaker_cli.egg-info/SOURCES.txt +0 -0
  18. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/esp_rainmaker_cli.egg-info/dependency_links.txt +0 -0
  19. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/esp_rainmaker_cli.egg-info/entry_points.txt +0 -0
  20. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/esp_rainmaker_cli.egg-info/requires.txt +0 -0
  21. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/esp_rainmaker_cli.egg-info/top_level.txt +0 -0
  22. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rainmaker/__init__.py +0 -0
  23. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_cmd/__init__.py +0 -0
  24. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_cmd/automation.py +0 -0
  25. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_cmd/browserlogin.py +0 -0
  26. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_cmd/cache.py +0 -0
  27. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_cmd/cmd_response.py +0 -0
  28. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_cmd/html/welcome.html +0 -0
  29. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_cmd/stream.py +0 -0
  30. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_cmd/test.py +0 -0
  31. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_cmd/user.py +0 -0
  32. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_lib/__init__.py +0 -0
  33. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_lib/aws_credentials.py +0 -0
  34. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_lib/cmd_response.py +0 -0
  35. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_lib/configmanager.py +0 -0
  36. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_lib/constants.py +0 -0
  37. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_lib/device.py +0 -0
  38. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_lib/envval.py +0 -0
  39. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_lib/exceptions.py +0 -0
  40. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_lib/kvs_streaming.py +0 -0
  41. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_lib/local_control.py +0 -0
  42. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_lib/logger.py +0 -0
  43. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_lib/node.py +0 -0
  44. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_lib/profile_manager.py +0 -0
  45. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_lib/profile_utils.py +0 -0
  46. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_lib/schedule_utils.py +0 -0
  47. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_lib/serverconfig.py +0 -0
  48. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_lib/service.py +0 -0
  49. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_lib/session_store.py +0 -0
  50. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_lib/simple_local_control.py +0 -0
  51. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_lib/user.py +0 -0
  52. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/__init__.py +0 -0
  53. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/__init__.py +0 -0
  54. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/discovery/__init__.py +0 -0
  55. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/discovery/mdns_discovery.py +0 -0
  56. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/prov/__init__.py +0 -0
  57. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/prov/wifi_ctrl.py +0 -0
  58. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/prov/wifi_prov.py +0 -0
  59. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/prov/wifi_scan.py +0 -0
  60. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/security/__init__.py +0 -0
  61. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/security/security.py +0 -0
  62. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/security/security0.py +0 -0
  63. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/security/security1.py +0 -0
  64. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/security/srp6a.py +0 -0
  65. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/transport/__init__.py +0 -0
  66. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/transport/ble_cli.py +0 -0
  67. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/transport/transport.py +0 -0
  68. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/transport/transport_ble.py +0 -0
  69. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/transport/transport_console.py +0 -0
  70. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/transport/transport_http.py +0 -0
  71. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/utils/__init__.py +0 -0
  72. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/utils/convenience.py +0 -0
  73. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_claim/__init__.py +0 -0
  74. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_claim/claim.py +0 -0
  75. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_claim/claim_config.py +0 -0
  76. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/__init__.py +0 -0
  77. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/constants_pb2.py +0 -0
  78. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/esp_local_ctrl.py +0 -0
  79. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/esp_local_ctrl_pb2.py +0 -0
  80. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/esp_prov.py +0 -0
  81. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/esp_rainmaker_ctrl.py +0 -0
  82. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/esp_rmaker_prov_local_ctrl_pb2.py +0 -0
  83. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/proto/__init__.py +0 -0
  84. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/proto/proto_lc.py +0 -0
  85. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/prov/__init__.py +0 -0
  86. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/prov/custom_prov.py +0 -0
  87. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/prov/wifi_ctrl.py +0 -0
  88. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/sec0_pb2.py +0 -0
  89. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/sec1_pb2.py +0 -0
  90. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/sec2_pb2.py +0 -0
  91. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/security/__init__.py +0 -0
  92. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/session_pb2.py +0 -0
  93. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/transport/__init__.py +0 -0
  94. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/utils/__init__.py +0 -0
  95. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/__init__.py +0 -0
  96. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/challenge_response.py +0 -0
  97. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/config/__init__.py +0 -0
  98. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/config/custom_cloud_config_pb2.py +0 -0
  99. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/config/esp_rmaker_chal_resp_pb2.py +0 -0
  100. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/config/esp_rmaker_claim_pb2.py +0 -0
  101. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/config/esp_rmaker_user_mapping_pb2.py +0 -0
  102. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/on_network_chal_resp.py +0 -0
  103. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/proto/__init__.py +0 -0
  104. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/protocomm/__init__.py +0 -0
  105. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/protocomm/python/__init__.py +0 -0
  106. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/protocomm/python/constants_pb2.py +0 -0
  107. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/protocomm/python/sec0_pb2.py +0 -0
  108. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/protocomm/python/sec1_pb2.py +0 -0
  109. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/protocomm/python/sec2_pb2.py +0 -0
  110. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/protocomm/python/session_pb2.py +0 -0
  111. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/prov/__init__.py +0 -0
  112. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/prov/prov_util.py +0 -0
  113. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/prov/user_mapping.py +0 -0
  114. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/security/__init__.py +0 -0
  115. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/transport/__init__.py +0 -0
  116. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/utils/__init__.py +0 -0
  117. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/wifi_provisioning/__init__.py +0 -0
  118. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/wifi_provisioning/python/__init__.py +0 -0
  119. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/wifi_provisioning/python/wifi_config_pb2.py +0 -0
  120. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/wifi_provisioning/python/wifi_constants_pb2.py +0 -0
  121. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/wifi_provisioning/python/wifi_ctrl_pb2.py +0 -0
  122. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/wifi_provisioning/python/wifi_scan_pb2.py +0 -0
  123. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/server_cert/__init__.py +0 -0
  124. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/server_cert/server_cert.pem +0 -0
  125. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/setup.cfg +0 -0
  126. {esp_rainmaker_cli-1.13.1 → esp_rainmaker_cli-1.14.1}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: esp-rainmaker-cli
3
- Version: 1.13.1
3
+ Version: 1.14.1
4
4
  Summary: A python utility to perform host based claiming
5
5
  Home-page: https://github.com/espressif/esp-rainmaker-cli
6
6
  Author: Espressif Systems
@@ -122,6 +122,26 @@ Changelog
122
122
 
123
123
  All major changes to ESP RainMaker CLI will be documented in this file.
124
124
 
125
+ ## [1.14.1] - 05-Jun-2026
126
+ ### Bugfixes
127
+ - Security 2 fixes across provisioning and local control:
128
+ - Provisioning auto-detect now reads `prov.sec_ver` from device capabilities
129
+ (was forcing sec1 on sec2 devices, causing firmware "Security version mismatch")
130
+ - `sec_patch_ver` discovered from device capabilities / `esp_local_ctrl/version`,
131
+ enabling the counter-in-IV scheme on ESP-IDF v5.4+ firmware
132
+ - `--sec2_username` option added to `--local` commands (`getnodeconfig`,
133
+ `getparams`, `setparams`); defaults to `wifiprov`, picks up cached
134
+ `local_control_username` from cloud params; `--pop` used as the SRP6a password
135
+ - QR code `username` / `password` fields now honoured by `provision`
136
+ - `Security2.serialize`/`deserialize` for local-control session resume parity with Security 1
137
+
138
+ ## [1.14.0] - 24-Apr-2026
139
+ ### Added
140
+ - New `group sharing` subcommand for sharing device groups / Matter fabrics between users:
141
+ - `add`, `remove`, `list`, `list-requests`, `accept`, `decline`, `cancel` operations
142
+ - Supports primary/secondary roles, sub-roles (1-4), metadata, and ownership transfer (`--transfer`, `--new-role`)
143
+ - Parsed, human-readable output by default; pass `--raw` on any operation to get the underlying JSON response
144
+
125
145
  ## [1.13.1] - 23-Apr-2026
126
146
  ### Added
127
147
  - Node sharing enhancements:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: esp-rainmaker-cli
3
- Version: 1.13.1
3
+ Version: 1.14.1
4
4
  Summary: A python utility to perform host based claiming
5
5
  Home-page: https://github.com/espressif/esp-rainmaker-cli
6
6
  Author: Espressif Systems
@@ -122,6 +122,26 @@ Changelog
122
122
 
123
123
  All major changes to ESP RainMaker CLI will be documented in this file.
124
124
 
125
+ ## [1.14.1] - 05-Jun-2026
126
+ ### Bugfixes
127
+ - Security 2 fixes across provisioning and local control:
128
+ - Provisioning auto-detect now reads `prov.sec_ver` from device capabilities
129
+ (was forcing sec1 on sec2 devices, causing firmware "Security version mismatch")
130
+ - `sec_patch_ver` discovered from device capabilities / `esp_local_ctrl/version`,
131
+ enabling the counter-in-IV scheme on ESP-IDF v5.4+ firmware
132
+ - `--sec2_username` option added to `--local` commands (`getnodeconfig`,
133
+ `getparams`, `setparams`); defaults to `wifiprov`, picks up cached
134
+ `local_control_username` from cloud params; `--pop` used as the SRP6a password
135
+ - QR code `username` / `password` fields now honoured by `provision`
136
+ - `Security2.serialize`/`deserialize` for local-control session resume parity with Security 1
137
+
138
+ ## [1.14.0] - 24-Apr-2026
139
+ ### Added
140
+ - New `group sharing` subcommand for sharing device groups / Matter fabrics between users:
141
+ - `add`, `remove`, `list`, `list-requests`, `accept`, `decline`, `cancel` operations
142
+ - Supports primary/secondary roles, sub-roles (1-4), metadata, and ownership transfer (`--transfer`, `--new-role`)
143
+ - Parsed, human-readable output by default; pass `--raw` on any operation to get the underlying JSON response
144
+
125
145
  ## [1.13.1] - 23-Apr-2026
126
146
  ### Added
127
147
  - Node sharing enhancements:
@@ -15,7 +15,12 @@ from rmaker_cmd.cmd_response import get_cmd_requests, create_cmd_request
15
15
  from rmaker_cmd.provision import provision
16
16
  from rmaker_cmd.test import test
17
17
  from rmaker_lib.logger import log
18
- from rmaker_cmd.group import group_add, group_remove, group_edit, group_list, group_show, group_add_nodes, group_remove_nodes, group_list_nodes
18
+ from rmaker_cmd.group import (group_add, group_remove, group_edit, group_list,
19
+ group_show, group_add_nodes, group_remove_nodes,
20
+ group_list_nodes, group_sharing_add,
21
+ group_sharing_remove, group_sharing_list,
22
+ group_sharing_list_requests, group_sharing_accept,
23
+ group_sharing_decline, group_sharing_cancel)
19
24
  from rmaker_cmd.automation import automation_add, automation_edit, automation_remove, automation_get
20
25
  from rmaker_cmd.cache import cache_manage
21
26
  from rmaker_cmd.stream import stream_video
@@ -280,6 +285,10 @@ def main():
280
285
  choices=[0, 1, 2],
281
286
  default=None,
282
287
  help='Security version for local control (default: 1)')
288
+ getnodeconfig_parser.add_argument('--sec2_username',
289
+ type=str,
290
+ default=None,
291
+ help='SRP6a username for Security 2 local control. Defaults to the username cached from cloud params, else \'wifiprov\'. --pop is used as the password.')
283
292
  getnodeconfig_parser.add_argument('--local-raw',
284
293
  action='store_true',
285
294
  help='Use local control via raw endpoints (get_config with fragmentation) instead of esp_local_ctrl')
@@ -350,6 +359,10 @@ def main():
350
359
  choices=[0, 1, 2],
351
360
  default=None,
352
361
  help='Security version for local control (default: 1)')
362
+ setparams_parser.add_argument('--sec2_username',
363
+ type=str,
364
+ default=None,
365
+ help='SRP6a username for Security 2 local control. Defaults to the username cached from cloud params, else \'wifiprov\'. --pop is used as the password.')
353
366
  setparams_parser.add_argument('--local-raw',
354
367
  action='store_true',
355
368
  help='Use local control via raw endpoints (get_params/set_params) instead of esp_local_ctrl')
@@ -393,6 +406,10 @@ def main():
393
406
  choices=[0, 1, 2],
394
407
  default=None,
395
408
  help='Security version for local control (default: 1)')
409
+ getparams_parser.add_argument('--sec2_username',
410
+ type=str,
411
+ default=None,
412
+ help='SRP6a username for Security 2 local control. Defaults to the username cached from cloud params, else \'wifiprov\'. --pop is used as the password.')
396
413
  getparams_parser.add_argument('--local-raw',
397
414
  action='store_true',
398
415
  help='Use local control via raw endpoints (get_params/set_params) instead of esp_local_ctrl')
@@ -934,6 +951,127 @@ def main():
934
951
  add_profile_argument(group_list_nodes_parser)
935
952
  group_list_nodes_parser.set_defaults(func=group_list_nodes)
936
953
 
954
+ # group sharing
955
+ group_sharing_parser = group_subparsers.add_parser(
956
+ 'sharing',
957
+ help='Share groups / matter fabrics between users',
958
+ formatter_class=argparse.RawTextHelpFormatter,
959
+ description='Share groups or matter fabrics with other users.\n'
960
+ 'Primary users can share groups; secondary users can view '
961
+ 'their sharing or leave groups they have access to.')
962
+ group_sharing_parser.set_defaults(
963
+ func=lambda vars=None: group_sharing_parser.print_help())
964
+ group_sharing_subparsers = group_sharing_parser.add_subparsers(
965
+ dest='sharing_command', help='Group sharing operations')
966
+
967
+ # group sharing add
968
+ gs_add_parser = group_sharing_subparsers.add_parser(
969
+ 'add', help='Share group(s) with a user (or add self-sharing without --user)',
970
+ formatter_class=argparse.RawTextHelpFormatter)
971
+ gs_add_parser.add_argument('--groups', type=str, required=True,
972
+ metavar='<group_ids>',
973
+ help='Comma separated group ids (max 10)')
974
+ gs_add_parser.add_argument('--user', type=str, metavar='<user_name>',
975
+ help='User name (email) to share with. Optional.')
976
+ gs_add_parser.add_argument('--primary', action='store_true',
977
+ help='Share with primary access (default: secondary)')
978
+ gs_add_parser.add_argument('--sub-role', type=int, choices=range(1, 5),
979
+ metavar='<1-4>',
980
+ help='Custom sub role (1-4)')
981
+ gs_add_parser.add_argument('--metadata', type=str, metavar='<json>',
982
+ help='Custom metadata as a JSON string')
983
+ gs_add_parser.add_argument('--transfer', action='store_true',
984
+ help='Transfer ownership of the group(s) to the user')
985
+ gs_add_parser.add_argument('--new-role', type=str, choices=['secondary'],
986
+ help='Role to assign to self after transfer '
987
+ '(only valid with --transfer)')
988
+ gs_add_parser.add_argument('--raw', action='store_true',
989
+ help='Print the raw JSON response instead of parsed output')
990
+ add_profile_argument(gs_add_parser)
991
+ gs_add_parser.set_defaults(func=group_sharing_add)
992
+
993
+ # group sharing remove
994
+ gs_remove_parser = group_sharing_subparsers.add_parser(
995
+ 'remove', help='Remove group sharing with a user',
996
+ formatter_class=argparse.RawTextHelpFormatter)
997
+ gs_remove_parser.add_argument('--groups', type=str, required=True,
998
+ metavar='<group_ids>',
999
+ help='Comma separated group ids')
1000
+ gs_remove_parser.add_argument('--user', type=str, required=True,
1001
+ metavar='<user_name>',
1002
+ help='User name (email) to unshare with')
1003
+ gs_remove_parser.add_argument('--raw', action='store_true',
1004
+ help='Print the raw JSON response instead of parsed output')
1005
+ add_profile_argument(gs_remove_parser)
1006
+ gs_remove_parser.set_defaults(func=group_sharing_remove)
1007
+
1008
+ # group sharing list
1009
+ gs_list_parser = group_sharing_subparsers.add_parser(
1010
+ 'list', help='Show sharing details for groups / matter fabrics',
1011
+ formatter_class=argparse.RawTextHelpFormatter)
1012
+ gs_list_parser.add_argument('--group-id', type=str, metavar='<group_id>',
1013
+ help='Fetch sharing details for a single group')
1014
+ gs_list_parser.add_argument('--sub-groups', action='store_true',
1015
+ help='Include sharing details of sub-groups')
1016
+ gs_list_parser.add_argument('--parent-groups', action='store_true',
1017
+ help='Include sharing details of parent groups')
1018
+ gs_list_parser.add_argument('--metadata', action='store_true',
1019
+ help='Include metadata set during sharing')
1020
+ gs_list_parser.add_argument('--raw', action='store_true',
1021
+ help='Print the raw JSON response instead of parsed output')
1022
+ add_profile_argument(gs_list_parser)
1023
+ gs_list_parser.set_defaults(func=group_sharing_list)
1024
+
1025
+ # group sharing list-requests
1026
+ gs_list_req_parser = group_sharing_subparsers.add_parser(
1027
+ 'list-requests', help='List pending group sharing requests',
1028
+ formatter_class=argparse.RawTextHelpFormatter,
1029
+ description='List pending group sharing requests.\n'
1030
+ 'Use --primary-user to list requests raised by you.\n'
1031
+ 'Without the flag, lists requests you have received.')
1032
+ gs_list_req_parser.add_argument('--id', type=str, metavar='<request_id>',
1033
+ help='Fetch a specific request by id')
1034
+ gs_list_req_parser.add_argument('--primary-user', action='store_true',
1035
+ help='List requests raised by current user '
1036
+ '(default: requests received)')
1037
+ gs_list_req_parser.add_argument('--raw', action='store_true',
1038
+ help='Print the raw JSON response instead of parsed output')
1039
+ add_profile_argument(gs_list_req_parser)
1040
+ gs_list_req_parser.set_defaults(func=group_sharing_list_requests)
1041
+
1042
+ # group sharing accept
1043
+ gs_accept_parser = group_sharing_subparsers.add_parser(
1044
+ 'accept', help='Accept a pending group sharing request')
1045
+ gs_accept_parser.add_argument('--id', type=str, required=True,
1046
+ metavar='<request_id>',
1047
+ help='Id of the sharing request')
1048
+ gs_accept_parser.add_argument('--raw', action='store_true',
1049
+ help='Print the raw JSON response instead of parsed output')
1050
+ add_profile_argument(gs_accept_parser)
1051
+ gs_accept_parser.set_defaults(func=group_sharing_accept)
1052
+
1053
+ # group sharing decline
1054
+ gs_decline_parser = group_sharing_subparsers.add_parser(
1055
+ 'decline', help='Decline a pending group sharing request')
1056
+ gs_decline_parser.add_argument('--id', type=str, required=True,
1057
+ metavar='<request_id>',
1058
+ help='Id of the sharing request')
1059
+ gs_decline_parser.add_argument('--raw', action='store_true',
1060
+ help='Print the raw JSON response instead of parsed output')
1061
+ add_profile_argument(gs_decline_parser)
1062
+ gs_decline_parser.set_defaults(func=group_sharing_decline)
1063
+
1064
+ # group sharing cancel
1065
+ gs_cancel_parser = group_sharing_subparsers.add_parser(
1066
+ 'cancel', help='Cancel a pending group sharing request (primary side)')
1067
+ gs_cancel_parser.add_argument('--id', type=str, required=True,
1068
+ metavar='<request_id>',
1069
+ help='Id of the sharing request')
1070
+ gs_cancel_parser.add_argument('--raw', action='store_true',
1071
+ help='Print the raw JSON response instead of parsed output')
1072
+ add_profile_argument(gs_cancel_parser)
1073
+ gs_cancel_parser.set_defaults(func=group_sharing_cancel)
1074
+
937
1075
  # Automation Management
938
1076
  automation_parser = subparsers.add_parser('automations',
939
1077
  help='Manage automation triggers')
@@ -5,4 +5,4 @@
5
5
  # SPDX-License-Identifier: Apache-2.0
6
6
 
7
7
  # This file contains the version information for the ESP RainMaker CLI
8
- VERSION = "1.13.1"
8
+ VERSION = "1.14.1"
@@ -3,8 +3,17 @@
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
5
  from rmaker_lib.profile_utils import get_session_with_profile
6
+ import datetime
6
7
  import json
7
- from rmaker_cmd.node import _print_node_details
8
+ import sys
9
+ from rmaker_cmd.node import (
10
+ _get_description,
11
+ _get_request_expiration,
12
+ _get_request_id,
13
+ _get_status,
14
+ _print_api_error,
15
+ _print_node_details,
16
+ )
8
17
 
9
18
 
10
19
  def _handle_error(e, operation):
@@ -249,4 +258,211 @@ def group_list_nodes(vars=None):
249
258
  else:
250
259
  print(f"{idx}. {name} (ID: {group_id})")
251
260
  except Exception as e:
252
- _handle_error(e, "list nodes in group")
261
+ _handle_error(e, "list nodes in group")
262
+
263
+
264
+ def _split_groups(groups_arg):
265
+ return [g.strip() for g in groups_arg.split(',') if g.strip()]
266
+
267
+
268
+ def _print_group_sharing_request_details(resp, is_primary_user=False):
269
+ requests_in_resp = resp.get('sharing_requests') or []
270
+ if not requests_in_resp:
271
+ print("No pending requests")
272
+ return
273
+ request_exists = False
274
+ for request in requests_in_resp:
275
+ if request.get('request_status', '').lower() == 'declined':
276
+ continue
277
+ request_exists = True
278
+ print("\n{:<15}: {}".format('Request Id', request['request_id']))
279
+ print("{:<15}: {}".format('Group Id(s)', ','.join(request.get('group_ids', []))))
280
+ group_names = request.get('group_names') or []
281
+ if group_names:
282
+ print("{:<15}: {}".format('Group Name(s)', ','.join(group_names)))
283
+ if is_primary_user:
284
+ if 'user_name' in request and request['user_name']:
285
+ print("{:<15}: {}".format('Shared with', request['user_name']))
286
+ else:
287
+ if 'primary_user_name' in request and request['primary_user_name']:
288
+ print("{:<15}: {}".format('Shared by', request['primary_user_name']))
289
+ if 'user_role' in request and request['user_role']:
290
+ print("{:<15}: {}".format('Role', request['user_role']))
291
+ print(_get_request_expiration(request))
292
+
293
+ if not request_exists:
294
+ print("No pending requests")
295
+
296
+
297
+ def _print_group_sharing_details(resp):
298
+ try:
299
+ groups_in_resp = resp.get('group_sharing') or []
300
+ if not groups_in_resp:
301
+ print("No shared groups")
302
+ return
303
+ for group in groups_in_resp:
304
+ print("\nGroup Id: {}".format(group.get('group_id', '')))
305
+ users = group.get('users') or {}
306
+ primary_users = ','.join(users.get('primary') or [])
307
+ secondary_users = ','.join(users.get('secondary') or [])
308
+ print("{:<7}: {:<9}: {}".format('Users', 'Primary', primary_users), end='')
309
+ if secondary_users:
310
+ print("\n{:>18}: {}".format('Secondary', secondary_users), end='')
311
+ print()
312
+ except KeyError:
313
+ print("Error in displaying details...Please check API Json...Exiting...")
314
+ sys.exit(0)
315
+
316
+
317
+ def _emit(resp, raw, render):
318
+ if raw:
319
+ print(json.dumps(resp, indent=2))
320
+ return
321
+ if isinstance(resp, dict) and resp.get('status', '').lower() != 'success' and 'error_code' in resp:
322
+ _print_api_error(resp)
323
+ return
324
+ render(resp)
325
+
326
+
327
+ def group_sharing_add(vars=None):
328
+ try:
329
+ s = get_session_with_profile(vars or {})
330
+ groups = _split_groups(vars['groups'])
331
+ if not groups:
332
+ print("At least one group id must be provided in --groups")
333
+ return
334
+ user = vars.get('user')
335
+ primary = vars.get('primary') if vars.get('primary') else None
336
+ sub_role = vars.get('sub_role')
337
+ transfer = vars.get('transfer') if vars.get('transfer') else None
338
+ new_role = vars.get('new_role')
339
+ metadata = None
340
+ metadata_arg = vars.get('metadata')
341
+ if metadata_arg:
342
+ try:
343
+ metadata = json.loads(metadata_arg)
344
+ except Exception as e:
345
+ print("Invalid JSON for --metadata:", e)
346
+ return
347
+ print("Sharing group(s) with user")
348
+ resp = s.add_user_group_sharing(
349
+ groups=groups,
350
+ user_name=user,
351
+ primary=primary,
352
+ sub_role=sub_role,
353
+ metadata=metadata,
354
+ transfer=transfer,
355
+ new_role=new_role,
356
+ )
357
+
358
+ def render(r):
359
+ if r.get('status', '').lower() == 'success':
360
+ print("{:<11}: {}".format('Status', _get_status(r)))
361
+ print("{:<11}: {}".format('Description', _get_description(r)))
362
+ if 'request_id' in r:
363
+ print("{:<11}: {}".format('Request Id', _get_request_id(r)))
364
+
365
+ _emit(resp, vars.get('raw', False), render)
366
+ except Exception as e:
367
+ _handle_error(e, "add group sharing")
368
+
369
+
370
+ def group_sharing_remove(vars=None):
371
+ try:
372
+ s = get_session_with_profile(vars or {})
373
+ groups = _split_groups(vars['groups'])
374
+ if not groups:
375
+ print("At least one group id must be provided in --groups")
376
+ return
377
+ print("Removing group sharing")
378
+ resp = s.remove_user_group_sharing(
379
+ groups=','.join(groups),
380
+ user_name=vars['user'],
381
+ )
382
+
383
+ def render(r):
384
+ if r.get('status', '').lower() == 'success':
385
+ print("{}: {}".format(_get_status(r), _get_description(r)))
386
+
387
+ _emit(resp, vars.get('raw', False), render)
388
+ except Exception as e:
389
+ _handle_error(e, "remove group sharing")
390
+
391
+
392
+ def group_sharing_list(vars=None):
393
+ try:
394
+ s = get_session_with_profile(vars or {})
395
+ resp = s.get_user_group_sharing(
396
+ group_id=vars.get('group_id'),
397
+ sub_groups=vars.get('sub_groups', False),
398
+ parent_groups=vars.get('parent_groups', False),
399
+ metadata=vars.get('metadata', False),
400
+ )
401
+
402
+ def render(r):
403
+ print("Displaying group sharing details")
404
+ _print_group_sharing_details(r)
405
+
406
+ _emit(resp, vars.get('raw', False), render)
407
+ except Exception as e:
408
+ _handle_error(e, "list group sharing")
409
+
410
+
411
+ def group_sharing_list_requests(vars=None):
412
+ try:
413
+ s = get_session_with_profile(vars or {})
414
+ is_primary = vars.get('primary_user', False)
415
+ resp = s.get_user_group_sharing_requests(
416
+ request_id=vars.get('id'),
417
+ primary_user=is_primary,
418
+ )
419
+
420
+ def render(r):
421
+ print("Displaying pending group sharing requests")
422
+ print("Current (logged-in) user is set as {} user".format(
423
+ 'Primary' if is_primary else 'Secondary'))
424
+ _print_group_sharing_request_details(r, is_primary_user=is_primary)
425
+
426
+ _emit(resp, vars.get('raw', False), render)
427
+ except Exception as e:
428
+ _handle_error(e, "list group sharing requests")
429
+
430
+
431
+ def _respond_group_sharing(vars, accept):
432
+ action = 'accept' if accept else 'decline'
433
+ try:
434
+ s = get_session_with_profile(vars or {})
435
+ resp = s.respond_user_group_sharing_request(
436
+ request_id=vars['id'], accept=accept)
437
+
438
+ def render(r):
439
+ if r.get('status', '').lower() == 'success':
440
+ print("{:<11}: {}".format('Status', _get_status(r)))
441
+ print("{:<11}: {}".format('Description', _get_description(r)))
442
+
443
+ _emit(resp, vars.get('raw', False), render)
444
+ except Exception as e:
445
+ _handle_error(e, "{} group sharing request".format(action))
446
+
447
+
448
+ def group_sharing_accept(vars=None):
449
+ _respond_group_sharing(vars, accept=True)
450
+
451
+
452
+ def group_sharing_decline(vars=None):
453
+ _respond_group_sharing(vars, accept=False)
454
+
455
+
456
+ def group_sharing_cancel(vars=None):
457
+ try:
458
+ s = get_session_with_profile(vars or {})
459
+ print("Cancelling request")
460
+ resp = s.remove_user_group_sharing_request(request_id=vars['id'])
461
+
462
+ def render(r):
463
+ if r.get('status', '').lower() == 'success':
464
+ print("{}: {}".format(_get_status(r), _get_description(r)))
465
+
466
+ _emit(resp, vars.get('raw', False), render)
467
+ except Exception as e:
468
+ _handle_error(e, "cancel group sharing request")
@@ -78,6 +78,9 @@ def _build_local_options_with_cache(vars_dict, node_cache=None, session_store=No
78
78
  sec_ver = raw_sec_ver if raw_sec_ver is not None else 1
79
79
  nodeid = vars_dict.get('nodeid', '')
80
80
 
81
+ user_sec2_username = vars_dict.get('sec2_username') # None => not supplied
82
+ sec2_username = user_sec2_username
83
+
81
84
  if node_cache and not pop:
82
85
  capability = node_cache.get_local_control_capability(nodeid)
83
86
  if capability:
@@ -99,6 +102,9 @@ def _build_local_options_with_cache(vars_dict, node_cache=None, session_store=No
99
102
  log.debug(f"Using cached POP for node {nodeid}")
100
103
  if lc_info.get('sec_ver') is not None and not explicit_sec_ver:
101
104
  sec_ver = lc_info['sec_ver']
105
+ if sec2_username is None and lc_info.get('username'):
106
+ sec2_username = lc_info['username']
107
+ log.debug(f"Using cached sec2 username for node {nodeid}")
102
108
 
103
109
  if not pop and not explicit_sec_ver:
104
110
  try:
@@ -114,15 +120,23 @@ def _build_local_options_with_cache(vars_dict, node_cache=None, session_store=No
114
120
  pop = lc_info.get('pop', '')
115
121
  if lc_info.get('sec_ver') is not None:
116
122
  sec_ver = lc_info['sec_ver']
123
+ if sec2_username is None and lc_info.get('username'):
124
+ sec2_username = lc_info['username']
117
125
  log.debug(f"Resolved POP from cloud for node {nodeid}")
118
126
  except Exception as e:
119
127
  log.debug(f"Failed to auto-resolve POP from cloud: {e}")
120
128
 
129
+ if not sec2_username:
130
+ sec2_username = 'wifiprov'
131
+ if sec_ver == 2:
132
+ print(f"Using default Security 2 username '{sec2_username}'")
133
+
121
134
  local_options = {
122
135
  'pop': pop,
123
136
  'transport': vars_dict.get('transport', 'http'),
124
137
  'port': vars_dict.get('port', 8080),
125
138
  'sec_ver': sec_ver,
139
+ 'sec2_username': sec2_username,
126
140
  'local_raw': vars_dict.get('local_raw', False),
127
141
  'device_name': vars_dict.get('device_name', None),
128
142
  'node_cache': node_cache,
@@ -1259,6 +1273,7 @@ def get_node_config(vars=None):
1259
1273
  'transport': vars.get('transport', 'ble'),
1260
1274
  'port': vars.get('port', 8080),
1261
1275
  'sec_ver': vars.get('sec_ver') if vars.get('sec_ver') is not None else 1,
1276
+ 'sec2_username': vars.get('sec2_username', 'wifiprov') or 'wifiprov',
1262
1277
  'device_name': vars.get('device_name', None),
1263
1278
  'timestamp': timestamp
1264
1279
  }
@@ -1312,7 +1327,8 @@ def get_node_config(vars=None):
1312
1327
  'pop': vars.get('pop', ''),
1313
1328
  'transport': vars.get('transport', 'http'),
1314
1329
  'port': vars.get('port', 8080),
1315
- 'sec_ver': vars.get('sec_ver') if vars.get('sec_ver') is not None else 1
1330
+ 'sec_ver': vars.get('sec_ver') if vars.get('sec_ver') is not None else 1,
1331
+ 'sec2_username': vars.get('sec2_username', 'wifiprov') or 'wifiprov',
1316
1332
  }
1317
1333
 
1318
1334
  node_config = run_local_control_operation(
@@ -1326,7 +1342,8 @@ def get_node_config(vars=None):
1326
1342
  'pop': vars.get('pop', ''),
1327
1343
  'transport': vars.get('transport', 'http'),
1328
1344
  'port': vars.get('port', 8080),
1329
- 'sec_ver': vars.get('sec_ver') if vars.get('sec_ver') is not None else 0
1345
+ 'sec_ver': vars.get('sec_ver') if vars.get('sec_ver') is not None else 0,
1346
+ 'sec2_username': vars.get('sec2_username', 'wifiprov') or 'wifiprov',
1330
1347
  }
1331
1348
 
1332
1349
  log.info("Using simplified local control (security level 0)")
@@ -1567,6 +1584,7 @@ def set_params(vars=None):
1567
1584
  'transport': vars.get('transport', 'http'),
1568
1585
  'port': vars.get('port', 8080),
1569
1586
  'sec_ver': vars.get('sec_ver') if vars.get('sec_ver') is not None else 0,
1587
+ 'sec2_username': vars.get('sec2_username', 'wifiprov') or 'wifiprov',
1570
1588
  'local_raw': vars.get('local_raw', False),
1571
1589
  'device_name': vars.get('device_name', None)
1572
1590
  }
@@ -1753,6 +1771,7 @@ def get_params(vars=None):
1753
1771
  'transport': vars.get('transport', 'http'),
1754
1772
  'port': vars.get('port', 8080),
1755
1773
  'sec_ver': vars.get('sec_ver') if vars.get('sec_ver') is not None else 1,
1774
+ 'sec2_username': vars.get('sec2_username', 'wifiprov') or 'wifiprov',
1756
1775
  'local_raw': vars.get('local_raw', False),
1757
1776
  'device_name': vars.get('device_name', None),
1758
1777
  'timestamp': timestamp if not (proxy_report and vars.get('local_raw', False) and not timestamp_explicitly_provided) else timestamp,
@@ -1779,6 +1798,7 @@ def get_params(vars=None):
1779
1798
  'transport': vars.get('transport', 'http'),
1780
1799
  'port': vars.get('port', 8080),
1781
1800
  'sec_ver': vars.get('sec_ver') if vars.get('sec_ver') is not None else 0,
1801
+ 'sec2_username': vars.get('sec2_username', 'wifiprov') or 'wifiprov',
1782
1802
  'local_raw': vars.get('local_raw', False),
1783
1803
  'device_name': vars.get('device_name', None),
1784
1804
  'timestamp': timestamp if not (proxy_report and vars.get('local_raw', False) and not timestamp_explicitly_provided) else timestamp,
@@ -117,8 +117,15 @@ def provision(vars=None):
117
117
  raise ValueError("Proof of possession (pop) may be required depending on security scheme. "
118
118
  "Use --pop, provide via --qrcode, or specify --sec_ver to skip pop requirement "
119
119
  "(Security 0 and 2 don't require pop).")
120
- sec2_username = vars.get('sec2_username', '')
121
- sec2_password = vars.get('sec2_password', '')
120
+
121
+ # sec2 credentials: explicit options override QR code values.
122
+ # QR code fields: 'username' and 'password' (sec2 SRP6a credentials).
123
+ sec2_username = (vars.get('sec2_username')
124
+ or qrcode_data.get('username')
125
+ or '')
126
+ sec2_password = (vars.get('sec2_password')
127
+ or qrcode_data.get('password')
128
+ or '')
122
129
  ssid = vars.get('ssid')
123
130
  passphrase = vars.get('passphrase')
124
131
  no_wifi = vars.get('no_wifi', False)
@@ -56,7 +56,7 @@ def _get_cache_base_dir(profile_config=None):
56
56
 
57
57
  def extract_local_control_info(params_or_details):
58
58
  """
59
- Extract local control info (POP, sec_ver, transport, port) from
59
+ Extract local control info (POP, sec_ver, username, transport, port) from
60
60
  node params or node details response data.
61
61
 
62
62
  Scans for a service/device named "Local Control" or similar that
@@ -74,6 +74,9 @@ def extract_local_control_info(params_or_details):
74
74
  lower_key = key.lower().replace(' ', '_').replace('-', '_')
75
75
  if lower_key in ('pop', 'proof_of_possession'):
76
76
  info['pop'] = str(value)
77
+ elif lower_key in ('username', 'user_name', 'local_control_username'):
78
+ if value:
79
+ info['username'] = str(value)
77
80
  elif lower_key == 'sec_ver':
78
81
  try:
79
82
  info['sec_ver'] = int(value)