Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}}}}'"
Expand Down
9 changes: 5 additions & 4 deletions frontend/ssh-docker-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Expand Down Expand Up @@ -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"
Expand All @@ -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 ? `<button class="action-btn create-btn" data-action="create" data-entity="${entityId}">${createLabel}</button>` : "",
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@
"recreating": "Recreating",
"dead": "Dead",
"unavailable": "Unavailable",
"pulling": "Pulling",
"refreshing": "Refreshing"
}
}
Expand Down
33 changes: 33 additions & 0 deletions tests/unit_tests/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
1 change: 1 addition & 0 deletions translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@
"recreating": "Wird neu erstellt",
"dead": "Abgestürzt",
"unavailable": "Nicht verfügbar",
"pulling": "Image wird geladen",
"refreshing": "Wird aktualisiert"
}
}
Expand Down
1 change: 1 addition & 0 deletions translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@
"recreating": "Recreating",
"dead": "Dead",
"unavailable": "Unavailable",
"pulling": "Pulling",
"refreshing": "Refreshing"
}
}
Expand Down
Loading