diff --git a/homeassistant/components/prusalink/icons.json b/homeassistant/components/prusalink/icons.json index d2b956f10ec249..e368a87e87ee4e 100644 --- a/homeassistant/components/prusalink/icons.json +++ b/homeassistant/components/prusalink/icons.json @@ -15,6 +15,9 @@ "filename": { "default": "mdi:file-image-outline" }, + "location": { + "default": "mdi:map-marker" + }, "material": { "default": "mdi:palette-swatch-variant" }, diff --git a/homeassistant/components/prusalink/sensor.py b/homeassistant/components/prusalink/sensor.py index d39307137d91f4..1a6af4da1edd80 100644 --- a/homeassistant/components/prusalink/sensor.py +++ b/homeassistant/components/prusalink/sensor.py @@ -46,6 +46,7 @@ class PrusaLinkSensorEntityDescription( """Describes PrusaLink sensor entity.""" available_fn: Callable[[T], bool] = lambda _: True + supported_fn: Callable[[T], bool] = lambda _: True SENSORS: dict[str, tuple[PrusaLinkSensorEntityDescription, ...]] = { @@ -103,6 +104,26 @@ class PrusaLinkSensorEntityDescription( value_fn=lambda data: cast(float, data["printer"]["axis_z"]), entity_registry_enabled_default=False, ), + PrusaLinkSensorEntityDescription[PrinterStatus]( + key="printer.telemetry.x-position", + translation_key="x_position", + native_unit_of_measurement=UnitOfLength.MILLIMETERS, + device_class=SensorDeviceClass.DISTANCE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: cast(float, data["printer"]["axis_x"]), + supported_fn=lambda data: data["printer"].get("axis_x") is not None, + entity_registry_enabled_default=False, + ), + PrusaLinkSensorEntityDescription[PrinterStatus]( + key="printer.telemetry.y-position", + translation_key="y_position", + native_unit_of_measurement=UnitOfLength.MILLIMETERS, + device_class=SensorDeviceClass.DISTANCE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: cast(float, data["printer"]["axis_y"]), + supported_fn=lambda data: data["printer"].get("axis_y") is not None, + entity_registry_enabled_default=False, + ), PrusaLinkSensorEntityDescription[PrinterStatus]( key="printer.telemetry.print-speed", translation_key="print_speed", @@ -194,6 +215,22 @@ class PrusaLinkSensorEntityDescription( value_fn=lambda data: cast(str, data["nozzle_diameter"]), entity_registry_enabled_default=False, ), + PrusaLinkSensorEntityDescription[PrinterInfo]( + key="info.location", + translation_key="location", + value_fn=lambda data: cast(str, data["location"]), + supported_fn=lambda data: data.get("location") is not None, + entity_registry_enabled_default=False, + ), + PrusaLinkSensorEntityDescription[PrinterInfo]( + key="info.min_extrusion_temp", + translation_key="min_extrusion_temp", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + value_fn=lambda data: cast(int, data["min_extrusion_temp"]), + supported_fn=lambda data: data.get("min_extrusion_temp") is not None, + entity_registry_enabled_default=False, + ), ), } @@ -213,6 +250,7 @@ async def async_setup_entry( entities.extend( PrusaLinkSensorEntity(coordinator, sensor_description) for sensor_description in sensors + if sensor_description.supported_fn(coordinator.data) ) async_add_entities(entities) diff --git a/homeassistant/components/prusalink/strings.json b/homeassistant/components/prusalink/strings.json index 3c3b7257dfbd15..c9ae97f9d8d445 100644 --- a/homeassistant/components/prusalink/strings.json +++ b/homeassistant/components/prusalink/strings.json @@ -54,9 +54,15 @@ "heatbed_temperature": { "name": "Heatbed temperature" }, + "location": { + "name": "Location" + }, "material": { "name": "Material" }, + "min_extrusion_temp": { + "name": "Minimum extrusion temperature" + }, "nozzle_diameter": { "name": "Nozzle diameter" }, @@ -94,6 +100,12 @@ "progress": { "name": "Progress" }, + "x_position": { + "name": "X-Position" + }, + "y_position": { + "name": "Y-Position" + }, "z_height": { "name": "Z-Height" } diff --git a/tests/components/prusalink/conftest.py b/tests/components/prusalink/conftest.py index 1ef8120a02633b..713bb1f1acbbce 100644 --- a/tests/components/prusalink/conftest.py +++ b/tests/components/prusalink/conftest.py @@ -48,6 +48,7 @@ def mock_info_api() -> Generator[dict[str, Any]]: "serial": "serial-1337", "hostname": "PrusaXL", "min_extrusion_temp": 170, + "location": "Workshop", } with patch("pyprusalink.PrusaLink.get_info", return_value=resp): yield resp diff --git a/tests/components/prusalink/test_sensor.py b/tests/components/prusalink/test_sensor.py index ead56f6493dac6..28fb267890db29 100644 --- a/tests/components/prusalink/test_sensor.py +++ b/tests/components/prusalink/test_sensor.py @@ -1,6 +1,7 @@ """Test Prusalink sensors.""" from datetime import UTC, datetime +from typing import Any from unittest.mock import patch import pytest @@ -23,6 +24,8 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry + @pytest.fixture(autouse=True) def setup_sensor_platform_only(): @@ -291,3 +294,76 @@ async def test_sensors_active_job( assert state is not None assert state.state == "2500" assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == REVOLUTIONS_PER_MINUTE + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_axis_x_y_sensors( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: None +) -> None: + """Test X and Y axis position sensors.""" + assert await async_setup_component(hass, "prusalink", {}) + + state = hass.states.get("sensor.mock_title_x_position") + assert state is not None + assert state.state == "7.9" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfLength.MILLIMETERS + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.DISTANCE + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + + state = hass.states.get("sensor.mock_title_y_position") + assert state is not None + assert state.state == "8.4" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfLength.MILLIMETERS + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.DISTANCE + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_axis_x_y_not_created_when_absent( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_api: None, + mock_get_status_idle: dict[str, Any], +) -> None: + """X and Y sensors are not created when axis fields are absent from the response.""" + del mock_get_status_idle["printer"]["axis_x"] + del mock_get_status_idle["printer"]["axis_y"] + assert await async_setup_component(hass, "prusalink", {}) + + assert hass.states.get("sensor.mock_title_x_position") is None + assert hass.states.get("sensor.mock_title_y_position") is None + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_location_and_min_extrusion_temp_sensors( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: None +) -> None: + """Test location and minimum extrusion temperature sensors from info endpoint.""" + assert await async_setup_component(hass, "prusalink", {}) + + state = hass.states.get("sensor.mock_title_location") + assert state is not None + assert state.state == "Workshop" + + state = hass.states.get("sensor.mock_title_minimum_extrusion_temperature") + assert state is not None + assert state.state == "170" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE + assert ATTR_STATE_CLASS not in state.attributes + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_location_and_min_extrusion_temp_not_created_when_absent( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_api: None, + mock_info_api: dict[str, Any], +) -> None: + """Location and min extrusion temp sensors are not created when info fields are absent.""" + del mock_info_api["location"] + del mock_info_api["min_extrusion_temp"] + assert await async_setup_component(hass, "prusalink", {}) + + assert hass.states.get("sensor.mock_title_location") is None + assert hass.states.get("sensor.mock_title_minimum_extrusion_temperature") is None