diff --git a/homeassistant/components/prusalink/__init__.py b/homeassistant/components/prusalink/__init__.py index 9d6096748dd..62eeb91d3e1 100644 --- a/homeassistant/components/prusalink/__init__.py +++ b/homeassistant/components/prusalink/__init__.py @@ -23,13 +23,19 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .config_flow import ConfigFlow from .const import DOMAIN from .coordinator import ( + InfoUpdateCoordinator, JobUpdateCoordinator, LegacyStatusCoordinator, PrusaLinkUpdateCoordinator, StatusCoordinator, ) -PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.CAMERA, Platform.SENSOR] +PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.CAMERA, + Platform.SENSOR, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -48,6 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "legacy_status": LegacyStatusCoordinator(hass, api), "status": StatusCoordinator(hass, api), "job": JobUpdateCoordinator(hass, api), + "info": InfoUpdateCoordinator(hass, api), } for coordinator in coordinators.values(): await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/prusalink/binary_sensor.py b/homeassistant/components/prusalink/binary_sensor.py new file mode 100644 index 00000000000..abeb79c2876 --- /dev/null +++ b/homeassistant/components/prusalink/binary_sensor.py @@ -0,0 +1,96 @@ +"""PrusaLink binary sensors.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Generic, TypeVar + +from pyprusalink.types import JobInfo, PrinterInfo, PrinterStatus +from pyprusalink.types_legacy import LegacyPrinterStatus + +from homeassistant.components.binary_sensor import ( + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import PrusaLinkEntity +from .const import DOMAIN +from .coordinator import PrusaLinkUpdateCoordinator + +T = TypeVar("T", PrinterStatus, LegacyPrinterStatus, JobInfo, PrinterInfo) + + +@dataclass(frozen=True) +class PrusaLinkBinarySensorEntityDescriptionMixin(Generic[T]): + """Mixin for required keys.""" + + value_fn: Callable[[T], bool] + + +@dataclass(frozen=True) +class PrusaLinkBinarySensorEntityDescription( + BinarySensorEntityDescription, + PrusaLinkBinarySensorEntityDescriptionMixin[T], + Generic[T], +): + """Describes PrusaLink sensor entity.""" + + available_fn: Callable[[T], bool] = lambda _: True + + +BINARY_SENSORS: dict[str, tuple[PrusaLinkBinarySensorEntityDescription, ...]] = { + "info": ( + PrusaLinkBinarySensorEntityDescription[PrinterInfo]( + key="info.mmu", + translation_key="mmu", + value_fn=lambda data: data["mmu"], + entity_registry_enabled_default=False, + ), + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up PrusaLink sensor based on a config entry.""" + coordinators: dict[str, PrusaLinkUpdateCoordinator] = hass.data[DOMAIN][ + entry.entry_id + ] + + entities: list[PrusaLinkEntity] = [] + for coordinator_type, binary_sensors in BINARY_SENSORS.items(): + coordinator = coordinators[coordinator_type] + entities.extend( + PrusaLinkBinarySensorEntity(coordinator, sensor_description) + for sensor_description in binary_sensors + ) + + async_add_entities(entities) + + +class PrusaLinkBinarySensorEntity(PrusaLinkEntity, BinarySensorEntity): + """Defines a PrusaLink binary sensor.""" + + entity_description: PrusaLinkBinarySensorEntityDescription + + def __init__( + self, + coordinator: PrusaLinkUpdateCoordinator, + description: PrusaLinkBinarySensorEntityDescription, + ) -> None: + """Initialize a PrusaLink sensor entity.""" + super().__init__(coordinator=coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}" + + @property + def is_on(self) -> bool: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/prusalink/coordinator.py b/homeassistant/components/prusalink/coordinator.py index 1d1989119fa..1d887983931 100644 --- a/homeassistant/components/prusalink/coordinator.py +++ b/homeassistant/components/prusalink/coordinator.py @@ -10,7 +10,13 @@ from time import monotonic from typing import TypeVar from httpx import ConnectError -from pyprusalink import JobInfo, LegacyPrinterStatus, PrinterStatus, PrusaLink +from pyprusalink import ( + JobInfo, + LegacyPrinterStatus, + PrinterInfo, + PrinterStatus, + PrusaLink, +) from pyprusalink.types import InvalidAuth, PrusaLinkError from homeassistant.config_entries import ConfigEntry @@ -94,3 +100,11 @@ class JobUpdateCoordinator(PrusaLinkUpdateCoordinator[JobInfo]): async def _fetch_data(self) -> JobInfo: """Fetch the printer data.""" return await self.api.get_job() + + +class InfoUpdateCoordinator(PrusaLinkUpdateCoordinator[PrinterInfo]): + """Info update coordinator.""" + + async def _fetch_data(self) -> PrinterInfo: + """Fetch the printer data.""" + return await self.api.get_info() diff --git a/homeassistant/components/prusalink/icons.json b/homeassistant/components/prusalink/icons.json index 4d97ea76ddd..578cb5e5d0c 100644 --- a/homeassistant/components/prusalink/icons.json +++ b/homeassistant/components/prusalink/icons.json @@ -24,6 +24,9 @@ "filename": { "default": "mdi:file-image-outline" }, + "nozzle_diameter": { + "default": "mdi:printer-3d-nozzle" + }, "print_start": { "default": "mdi:clock-start" }, diff --git a/homeassistant/components/prusalink/sensor.py b/homeassistant/components/prusalink/sensor.py index 80998d680d2..96cd4979b11 100644 --- a/homeassistant/components/prusalink/sensor.py +++ b/homeassistant/components/prusalink/sensor.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from datetime import datetime, timedelta from typing import Generic, TypeVar, cast -from pyprusalink.types import JobInfo, PrinterState, PrinterStatus +from pyprusalink.types import JobInfo, PrinterInfo, PrinterState, PrinterStatus from pyprusalink.types_legacy import LegacyPrinterStatus from homeassistant.components.sensor import ( @@ -33,7 +33,7 @@ from . import PrusaLinkEntity from .const import DOMAIN from .coordinator import PrusaLinkUpdateCoordinator -T = TypeVar("T", PrinterStatus, LegacyPrinterStatus, JobInfo) +T = TypeVar("T", PrinterStatus, LegacyPrinterStatus, JobInfo, PrinterInfo) @dataclass(frozen=True) @@ -189,6 +189,16 @@ SENSORS: dict[str, tuple[PrusaLinkSensorEntityDescription, ...]] = { ), ), ), + "info": ( + PrusaLinkSensorEntityDescription[PrinterInfo]( + key="info.nozzle_diameter", + translation_key="nozzle_diameter", + native_unit_of_measurement=UnitOfLength.MILLIMETERS, + device_class=SensorDeviceClass.DISTANCE, + value_fn=lambda data: cast(str, data["nozzle_diameter"]), + entity_registry_enabled_default=False, + ), + ), } diff --git a/homeassistant/components/prusalink/strings.json b/homeassistant/components/prusalink/strings.json index bb32770e357..7c6f0bbf2dd 100644 --- a/homeassistant/components/prusalink/strings.json +++ b/homeassistant/components/prusalink/strings.json @@ -23,6 +23,11 @@ } }, "entity": { + "binary_sensor": { + "mmu": { + "name": "MMU" + } + }, "sensor": { "printer_state": { "state": { @@ -78,6 +83,9 @@ }, "z_height": { "name": "Z-Height" + }, + "nozzle_diameter": { + "name": "Nozzle Diameter" } }, "button": { diff --git a/tests/components/prusalink/test_binary_sensor.py b/tests/components/prusalink/test_binary_sensor.py new file mode 100644 index 00000000000..c39b15471c6 --- /dev/null +++ b/tests/components/prusalink/test_binary_sensor.py @@ -0,0 +1,33 @@ +"""Test Prusalink sensors.""" + +from unittest.mock import PropertyMock, patch + +import pytest + +from homeassistant.const import STATE_OFF, Platform +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +@pytest.fixture(autouse=True) +def setup_binary_sensor_platform_only(): + """Only setup sensor platform.""" + with ( + patch("homeassistant.components.prusalink.PLATFORMS", [Platform.BINARY_SENSOR]), + patch( + "homeassistant.helpers.entity.Entity.entity_registry_enabled_default", + PropertyMock(return_value=True), + ), + ): + yield + + +async def test_binary_sensors_no_job( + hass: HomeAssistant, mock_config_entry, mock_api +) -> None: + """Test sensors while no job active.""" + assert await async_setup_component(hass, "prusalink", {}) + + state = hass.states.get("binary_sensor.mock_title_mmu") + assert state is not None + assert state.state == STATE_OFF diff --git a/tests/components/prusalink/test_sensor.py b/tests/components/prusalink/test_sensor.py index b15e9198da6..c0693626600 100644 --- a/tests/components/prusalink/test_sensor.py +++ b/tests/components/prusalink/test_sensor.py @@ -101,6 +101,10 @@ async def test_sensors_no_job(hass: HomeAssistant, mock_config_entry, mock_api) assert state is not None assert state.state == "PLA" + state = hass.states.get("sensor.mock_title_nozzle_diameter") + assert state is not None + assert state.state == "0.4" + state = hass.states.get("sensor.mock_title_print_flow") assert state is not None assert state.state == "100" @@ -205,6 +209,10 @@ async def test_sensors_idle_job_mk3( assert state is not None assert state.state == "PLA" + state = hass.states.get("sensor.mock_title_nozzle_diameter") + assert state is not None + assert state.state == "0.4" + state = hass.states.get("sensor.mock_title_print_flow") assert state is not None assert state.state == "100"