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
3 changes: 3 additions & 0 deletions homeassistant/components/prusalink/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
"filename": {
"default": "mdi:file-image-outline"
},
"location": {
"default": "mdi:map-marker"
},
"material": {
"default": "mdi:palette-swatch-variant"
},
Expand Down
38 changes: 38 additions & 0 deletions homeassistant/components/prusalink/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, ...]] = {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Comment thread
heikkih marked this conversation as resolved.
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,
),
Comment on lines +218 to +224
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So why is location a sensor? If it reflects the current location of the printer, it should probably be set to suggested_area in the device info instead

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — agreed that this is the right architectural choice. Opened #170099 which:

  • Removes the location sensor
  • Sets suggested_area=info_data.get("location") on DeviceInfo, so the printer's configured location is used as a hint when registering the device

The sensor was added in this PR (default-disabled, just merged) so the breaking impact is small — most users never enabled it. suggested_area is non-binding for already-registered devices, so existing setups keep their current area assignment unchanged. New users get the location string proposed as an area on registration (HA matches existing or creates).

Side note for posterity: DeviceEntry.suggested_area is being deprecated in HA 2026.9 (per this blog post), but setting suggested_area via DeviceInfo is still supported and still influences device area on registration. The deprecation only covers reading it back from DeviceEntry.

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,
),
),
}

Expand All @@ -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)
Comment thread
heikkih marked this conversation as resolved.
Expand Down
12 changes: 12 additions & 0 deletions homeassistant/components/prusalink/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down Expand Up @@ -94,6 +100,12 @@
"progress": {
"name": "Progress"
},
"x_position": {
"name": "X-Position"
},
"y_position": {
"name": "Y-Position"
},
"z_height": {
"name": "Z-Height"
}
Expand Down
1 change: 1 addition & 0 deletions tests/components/prusalink/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
76 changes: 76 additions & 0 deletions tests/components/prusalink/test_sensor.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Test Prusalink sensors."""

from datetime import UTC, datetime
from typing import Any
from unittest.mock import patch

import pytest
Expand All @@ -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():
Expand Down Expand Up @@ -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
Comment thread
heikkih marked this conversation as resolved.
Loading