diff --git a/coordinator.py b/coordinator.py index 601db8f..b9b2692 100644 --- a/coordinator.py +++ b/coordinator.py @@ -342,6 +342,7 @@ async def _async_fetch_data(self) -> None: update_available = False new_image_id: str | None = None if options.get(CONF_CHECK_FOR_UPDATES, False): + self.set_pending_state("pulling") pull_cmd = ( f"{docker_cmd} pull {image_name} > /dev/null 2>&1;" f" {docker_cmd} image inspect {image_name} --format '{{{{.Id}}}}'" diff --git a/frontend/ssh-docker-panel.js b/frontend/ssh-docker-panel.js index 4a30ae2..cfd0fbf 100644 --- a/frontend/ssh-docker-panel.js +++ b/frontend/ssh-docker-panel.js @@ -169,6 +169,7 @@ class SshDockerPanel extends HTMLElement { case "removing": return "#c0392b"; case "stopping": return "#e67e22"; case "creating": return "#2980b9"; + case "pulling": return "#2471a3"; case "refreshing": return "#7f8c8d"; default: return "#95a5a6"; } @@ -196,7 +197,7 @@ class SshDockerPanel extends HTMLElement { // Conditional button visibility per the requirements. // Create/Recreate: only if docker_create is available; label changes based on container state. - const transitionalStates = ["creating", "initializing", "restarting", "starting", "stopping", "removing", "refreshing"]; + const transitionalStates = ["creating", "initializing", "restarting", "starting", "stopping", "removing", "refreshing", "pulling"]; const isTransitional = transitionalStates.includes(state); const showCreate = !isTransitional && attrs.docker_create_available === true; const createLabel = state !== "unavailable" @@ -208,8 +209,8 @@ class SshDockerPanel extends HTMLElement { const showStart = !isTransitional && stoppedStates.includes(state); const showStop = !isTransitional && state === "running"; const showRemove = !isTransitional && state !== "unavailable" && state !== "unknown"; - const showRefresh = state !== "initializing"; - const showLogs = state !== "unavailable" && state !== "unknown" && state !== "initializing"; + const showRefresh = state !== "initializing" && state !== "pulling"; + const showLogs = state !== "unavailable" && state !== "unknown" && state !== "initializing" && state !== "pulling"; const actionButtons = [ showCreate ? `` : "", @@ -248,7 +249,7 @@ class SshDockerPanel extends HTMLElement { const allContainers = this._getAllContainers(); - const states = ["running", "exited", "paused", "restarting", "starting", "dead", "created", "removing", "stopping", "creating", "initializing", "unavailable", "refreshing"]; + const states = ["running", "exited", "paused", "restarting", "starting", "dead", "created", "removing", "stopping", "creating", "initializing", "pulling", "unavailable", "refreshing"]; const counts = { all: allContainers.length }; for (const s of states) { counts[s] = allContainers.filter((c) => c.state === s).length; diff --git a/strings.json b/strings.json index 508a8b3..d64f15e 100644 --- a/strings.json +++ b/strings.json @@ -183,6 +183,7 @@ "recreating": "Recreating", "dead": "Dead", "unavailable": "Unavailable", + "pulling": "Pulling", "refreshing": "Refreshing" } } diff --git a/tests/unit_tests/test_sensor.py b/tests/unit_tests/test_sensor.py index b5c9c82..873635e 100644 --- a/tests/unit_tests/test_sensor.py +++ b/tests/unit_tests/test_sensor.py @@ -378,6 +378,39 @@ async def mock_ssh_run(hass, options, command, timeout=DEFAULT_TIMEOUT): self.assertTrue(listener_called) + async def test_pulling_pending_state_set_during_update_check(self): + """Test that the 'pulling' pending state is shown while the image pull is in progress.""" + coordinator, sensor = _make_sensor(options={ + "host": "192.168.1.100", + "username": "user", + "password": "pass", + "docker_command": "docker", + "check_known_hosts": True, + "auto_update": False, + CONF_CHECK_FOR_UPDATES: True, + }) + observed_pending_state_during_pull = [] + + async def mock_ssh_run(hass, options, command, timeout=DEFAULT_TIMEOUT): + if "docker inspect" in command and "pull" not in command: + # docker inspect — container running with same image + return "running;2023-01-01T00:00:00Z;nginx:latest;sha256:abc123", 0 + if "pull" in command: + # docker pull + image inspect — record pending state at this moment + observed_pending_state_during_pull.append(coordinator._pending_state) + return "sha256:abc123", 0 + # docker_create availability check + return "", 0 + + with patch("ssh_docker.coordinator._ssh_run", mock_ssh_run): + await sensor.async_update() + + # The pending state must have been "pulling" when the pull command ran. + self.assertEqual(observed_pending_state_during_pull, ["pulling"]) + # After the full refresh the pending state is cleared. + self.assertIsNone(coordinator._pending_state) + self.assertEqual(sensor.native_value, "running") + class TestAsyncAddedToHass(unittest.IsolatedAsyncioTestCase): """Test the async_added_to_hass lifecycle method.""" diff --git a/translations/de.json b/translations/de.json index f5f3e49..ec04a41 100644 --- a/translations/de.json +++ b/translations/de.json @@ -183,6 +183,7 @@ "recreating": "Wird neu erstellt", "dead": "Abgestürzt", "unavailable": "Nicht verfügbar", + "pulling": "Image wird geladen", "refreshing": "Wird aktualisiert" } } diff --git a/translations/en.json b/translations/en.json index 508a8b3..d64f15e 100644 --- a/translations/en.json +++ b/translations/en.json @@ -183,6 +183,7 @@ "recreating": "Recreating", "dead": "Dead", "unavailable": "Unavailable", + "pulling": "Pulling", "refreshing": "Refreshing" } }