From ea3a9d7c33bf92dae5f1679fe7d336519db032b2 Mon Sep 17 00:00:00 2001 From: Heikki Henriksen Date: Fri, 8 May 2026 12:53:49 +0200 Subject: [PATCH] prusalink: add sd_ready, farm_mode, and status_connect binary sensors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three binary sensors backed by data already fetched by existing coordinators — no pyprusalink changes needed. All inherit the shared `PrusaLinkEntityDescription` (which provides `available_fn` and `supported_fn`) introduced in #170092. | Entity | Source | Default | Created when | |---|---|---|---| | SD card (`info.sd_ready`) | /api/v1/info | Disabled | Printer firmware exposes `sd_ready` | | Farm mode (`info.farm_mode`) | /api/v1/info | Disabled | Printer firmware exposes `farm_mode` | | Connectivity (`printer.status_connect.ok`) | /api/v1/status | Enabled | User has set up PrusaConnect (`status_connect` is in the response) | The Connectivity sensor uses `BinarySensorDeviceClass.CONNECTIVITY`, so HA provides the entity name and `_connectivity` entity_id suffix automatically — no `translation_key` or strings entry needed for it. `supported_fn` filters out unsupported entities at setup time so they are not created (rather than created and marked unavailable). This matches the pattern used by sensor.py and the contract documented in #170092. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/prusalink/binary_sensor.py | 27 ++++++++ homeassistant/components/prusalink/icons.json | 11 +++ .../components/prusalink/strings.json | 6 ++ tests/components/prusalink/conftest.py | 4 ++ .../prusalink/test_binary_sensor.py | 69 ++++++++++++++++++- 5 files changed, 115 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/prusalink/binary_sensor.py b/homeassistant/components/prusalink/binary_sensor.py index 94fb0296308fc..413d257b720de 100644 --- a/homeassistant/components/prusalink/binary_sensor.py +++ b/homeassistant/components/prusalink/binary_sensor.py @@ -8,6 +8,7 @@ from pyprusalink.types_legacy import LegacyPrinterStatus from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -32,6 +33,17 @@ class PrusaLinkBinarySensorEntityDescription( BINARY_SENSORS: dict[str, tuple[PrusaLinkBinarySensorEntityDescription, ...]] = { + "status": ( + PrusaLinkBinarySensorEntityDescription[PrinterStatus]( + key="printer.status_connect", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + value_fn=lambda data: data["printer"]["status_connect"]["ok"], + supported_fn=lambda data: ( + data["printer"].get("status_connect") is not None + and data["printer"]["status_connect"].get("ok") is not None + ), + ), + ), "info": ( PrusaLinkBinarySensorEntityDescription[PrinterInfo]( key="info.mmu", @@ -39,6 +51,20 @@ class PrusaLinkBinarySensorEntityDescription( value_fn=lambda data: data["mmu"], entity_registry_enabled_default=False, ), + PrusaLinkBinarySensorEntityDescription[PrinterInfo]( + key="info.sd_ready", + translation_key="sd_ready", + value_fn=lambda data: data["sd_ready"], + supported_fn=lambda data: data.get("sd_ready") is not None, + entity_registry_enabled_default=False, + ), + PrusaLinkBinarySensorEntityDescription[PrinterInfo]( + key="info.farm_mode", + translation_key="farm_mode", + value_fn=lambda data: data["farm_mode"], + supported_fn=lambda data: data.get("farm_mode") is not None, + entity_registry_enabled_default=False, + ), ), } @@ -57,6 +83,7 @@ async def async_setup_entry( entities.extend( PrusaLinkBinarySensorEntity(coordinator, sensor_description) for sensor_description in binary_sensors + if sensor_description.supported_fn(coordinator.data) ) async_add_entities(entities) diff --git a/homeassistant/components/prusalink/icons.json b/homeassistant/components/prusalink/icons.json index d2b956f10ec24..5f7feab796d98 100644 --- a/homeassistant/components/prusalink/icons.json +++ b/homeassistant/components/prusalink/icons.json @@ -1,5 +1,16 @@ { "entity": { + "binary_sensor": { + "farm_mode": { + "default": "mdi:server-network" + }, + "mmu": { + "default": "mdi:printer-3d-nozzle-alert" + }, + "sd_ready": { + "default": "mdi:micro-sd" + } + }, "button": { "cancel_job": { "default": "mdi:cancel" diff --git a/homeassistant/components/prusalink/strings.json b/homeassistant/components/prusalink/strings.json index aab099813e1ba..4136243bfecfe 100644 --- a/homeassistant/components/prusalink/strings.json +++ b/homeassistant/components/prusalink/strings.json @@ -18,8 +18,14 @@ }, "entity": { "binary_sensor": { + "farm_mode": { + "name": "Farm mode" + }, "mmu": { "name": "MMU" + }, + "sd_ready": { + "name": "SD card" } }, "button": { diff --git a/tests/components/prusalink/conftest.py b/tests/components/prusalink/conftest.py index 713bb1f1acbbc..ec21079d116de 100644 --- a/tests/components/prusalink/conftest.py +++ b/tests/components/prusalink/conftest.py @@ -49,6 +49,8 @@ def mock_info_api() -> Generator[dict[str, Any]]: "hostname": "PrusaXL", "min_extrusion_temp": 170, "location": "Workshop", + "sd_ready": True, + "farm_mode": False, } with patch("pyprusalink.PrusaLink.get_info", return_value=resp): yield resp @@ -84,6 +86,7 @@ def mock_get_status_idle() -> Generator[dict[str, Any]]: "speed": 100, "fan_hotend": 100, "fan_print": 75, + "status_connect": {"ok": True, "message": ""}, }, } with patch("pyprusalink.PrusaLink.get_status", return_value=resp): @@ -112,6 +115,7 @@ def mock_get_status_printing() -> Generator[dict[str, Any]]: "speed": 100, "fan_hotend": 5000, "fan_print": 2500, + "status_connect": {"ok": True, "message": ""}, }, } with patch("pyprusalink.PrusaLink.get_status", return_value=resp): diff --git a/tests/components/prusalink/test_binary_sensor.py b/tests/components/prusalink/test_binary_sensor.py index 474a4e265d150..581347bdc81d1 100644 --- a/tests/components/prusalink/test_binary_sensor.py +++ b/tests/components/prusalink/test_binary_sensor.py @@ -1,13 +1,16 @@ """Test Prusalink sensors.""" +from typing import Any from unittest.mock import patch import pytest -from homeassistant.const import STATE_OFF, Platform +from homeassistant.const import STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry + @pytest.fixture(autouse=True) def setup_binary_sensor_platform_only(): @@ -20,7 +23,7 @@ def setup_binary_sensor_platform_only(): @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_binary_sensors_no_job( - hass: HomeAssistant, mock_config_entry, mock_api + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: None ) -> None: """Test sensors while no job active.""" assert await async_setup_component(hass, "prusalink", {}) @@ -28,3 +31,65 @@ async def test_binary_sensors_no_job( state = hass.states.get("binary_sensor.mock_title_mmu") assert state is not None assert state.state == STATE_OFF + + +async def test_status_connect_enabled_by_default( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: None +) -> None: + """Connect binary sensor is enabled by default and reflects status_connect.ok.""" + assert await async_setup_component(hass, "prusalink", {}) + + state = hass.states.get("binary_sensor.mock_title_connectivity") + assert state is not None + assert state.state == STATE_ON + + +async def test_status_connect_not_created_when_absent( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_api: None, + mock_get_status_idle: dict[str, Any], +) -> None: + """Connect sensor is not created when status_connect is not in the response.""" + del mock_get_status_idle["printer"]["status_connect"] + assert await async_setup_component(hass, "prusalink", {}) + + assert hass.states.get("binary_sensor.mock_title_connectivity") is None + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sd_ready( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: None +) -> None: + """SD card sensor reflects sd_ready from info endpoint.""" + assert await async_setup_component(hass, "prusalink", {}) + + state = hass.states.get("binary_sensor.mock_title_sd_card") + assert state is not None + assert state.state == STATE_ON + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_farm_mode( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: None +) -> None: + """Farm mode sensor reflects farm_mode from info endpoint.""" + assert await async_setup_component(hass, "prusalink", {}) + + state = hass.states.get("binary_sensor.mock_title_farm_mode") + assert state is not None + assert state.state == STATE_OFF + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_farm_mode_not_created_when_absent( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_api: None, + mock_info_api: dict[str, Any], +) -> None: + """Farm mode sensor is not created when farm_mode field is absent from info.""" + del mock_info_api["farm_mode"] + assert await async_setup_component(hass, "prusalink", {}) + + assert hass.states.get("binary_sensor.mock_title_farm_mode") is None