Waiting for wifi device to become ready before creating hotspot.

This commit is contained in:
Roland 2026-03-28 10:48:34 -06:00
parent 53f4e29b64
commit de25242d61
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
2 changed files with 77 additions and 13 deletions

View file

@ -175,6 +175,32 @@ def enable_hotspot() -> dict:
timeout=10,
)
# Wait for device to become ready after radio on.
# On real hardware, the device transitions from 'unavailable' to 'disconnected'
# which can take a few seconds.
import time
device_ready = False
for _ in range(20): # 20 * 0.5s = 10s max
check = subprocess.run(
["nmcli", "-t", "-f", "DEVICE,STATE", "device", "status"],
capture_output=True,
text=True,
timeout=5,
)
if check.returncode == 0:
for line in check.stdout.strip().splitlines():
parts = line.split(":")
if len(parts) >= 2 and parts[0] == device and parts[1] != "unavailable":
device_ready = True
break
if device_ready:
break
time.sleep(0.5)
if not device_ready:
logger.warning("WiFi device %s did not become ready after turning radio on", device)
return {"success": False, "error": f"WiFi device {device} not ready after turning radio on"}
# Create hotspot using NetworkManager
logger.info("Enabling hotspot on %s with SSID %s", device, ssid)
result = subprocess.run(

View file

@ -357,7 +357,6 @@ class TestEnableHotspot:
def test_uses_config_values(self):
"""Should use get_config_value for device, ssid, and password."""
with mock.patch("controller.lib.admin.is_docker_mode", return_value=False):
# Mock get_config_value to return custom values
def mock_get_config(key, default=None):
config_map = {
'hotspot.device': 'wlan1',
@ -367,20 +366,59 @@ class TestEnableHotspot:
return config_map.get(key, default)
with mock.patch("controller.lib.admin.get_config_value", side_effect=mock_get_config):
mock_run = mock.Mock()
mock_run.returncode = 0
with mock.patch("subprocess.run", return_value=mock_run) as mock_subprocess:
enable_hotspot()
radio_on = mock.Mock(returncode=0, stdout="", stderr="")
device_ready = mock.Mock(returncode=0, stdout="wlan1:disconnected\n", stderr="")
hotspot_ok = mock.Mock(returncode=0, stdout="", stderr="")
with mock.patch("subprocess.run", side_effect=[radio_on, device_ready, hotspot_ok]) as mock_subprocess:
with mock.patch("time.sleep"):
enable_hotspot()
# Verify the hotspot was created with config values
calls = mock_subprocess.call_args_list
# Find the hotspot creation call (nmcli device wifi hotspot)
hotspot_call = [c for c in calls if len(c[0][0]) > 2 and 'hotspot' in c[0][0]]
if hotspot_call:
cmd_args = hotspot_call[0][0][0]
assert "wlan1" in cmd_args
assert "TestSSID" in cmd_args
assert "testpassword123" in cmd_args
calls = mock_subprocess.call_args_list
hotspot_call = [c for c in calls if 'hotspot' in c[0][0]]
assert hotspot_call, "Hotspot creation call not found"
cmd_args = hotspot_call[0][0][0]
assert "wlan1" in cmd_args
assert "TestSSID" in cmd_args
assert "testpassword123" in cmd_args
def test_waits_for_device_ready_after_radio_on(self):
"""Should poll device status after turning radio on, waiting for device
to transition from 'unavailable' before creating hotspot.
Real RPi behavior: radio on takes ~2s for device to become 'disconnected'."""
with mock.patch("controller.lib.admin.is_docker_mode", return_value=False):
# Simulate: radio on succeeds, device initially unavailable, then disconnected, then hotspot succeeds
radio_on_result = mock.Mock(returncode=0, stdout="", stderr="")
device_unavailable = mock.Mock(returncode=0, stdout="wlan0:unavailable\n", stderr="")
device_ready = mock.Mock(returncode=0, stdout="wlan0:disconnected\n", stderr="")
hotspot_result = mock.Mock(returncode=0, stdout="", stderr="")
with mock.patch("subprocess.run", side_effect=[
radio_on_result, # nmcli radio wifi on
device_unavailable, # nmcli -t device status (not ready)
device_ready, # nmcli -t device status (ready)
hotspot_result, # nmcli device wifi hotspot
]):
with mock.patch("time.sleep"): # Don't actually sleep in tests
result = enable_hotspot()
assert result["success"] is True
def test_fails_if_device_never_becomes_ready(self):
"""Should fail with a clear error if device stays unavailable after radio on."""
with mock.patch("controller.lib.admin.is_docker_mode", return_value=False):
radio_on_result = mock.Mock(returncode=0, stdout="", stderr="")
device_unavailable = mock.Mock(returncode=0, stdout="wlan0:unavailable\n", stderr="")
# Device never becomes ready (all polls return unavailable)
with mock.patch("subprocess.run", side_effect=[
radio_on_result,
] + [device_unavailable] * 20):
with mock.patch("time.sleep"):
result = enable_hotspot()
assert result["success"] is False
assert "not ready" in result.get("error", "").lower() or "unavailable" in result.get("error", "").lower()
class TestDisableHotspot: