Add PrusaLink nozzle and mmu support (#120436)

Co-authored-by: Stefan Agner <stefan@agner.ch>
This commit is contained in:
Pierre Mavro 2024-07-19 19:15:42 +02:00 committed by GitHub
parent c0732fbb1d
commit cafff3eddf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 183 additions and 4 deletions

View File

@ -23,13 +23,19 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .config_flow import ConfigFlow from .config_flow import ConfigFlow
from .const import DOMAIN from .const import DOMAIN
from .coordinator import ( from .coordinator import (
InfoUpdateCoordinator,
JobUpdateCoordinator, JobUpdateCoordinator,
LegacyStatusCoordinator, LegacyStatusCoordinator,
PrusaLinkUpdateCoordinator, PrusaLinkUpdateCoordinator,
StatusCoordinator, 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: 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), "legacy_status": LegacyStatusCoordinator(hass, api),
"status": StatusCoordinator(hass, api), "status": StatusCoordinator(hass, api),
"job": JobUpdateCoordinator(hass, api), "job": JobUpdateCoordinator(hass, api),
"info": InfoUpdateCoordinator(hass, api),
} }
for coordinator in coordinators.values(): for coordinator in coordinators.values():
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()

View File

@ -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)

View File

@ -10,7 +10,13 @@ from time import monotonic
from typing import TypeVar from typing import TypeVar
from httpx import ConnectError 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 pyprusalink.types import InvalidAuth, PrusaLinkError
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -94,3 +100,11 @@ class JobUpdateCoordinator(PrusaLinkUpdateCoordinator[JobInfo]):
async def _fetch_data(self) -> JobInfo: async def _fetch_data(self) -> JobInfo:
"""Fetch the printer data.""" """Fetch the printer data."""
return await self.api.get_job() 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()

View File

@ -24,6 +24,9 @@
"filename": { "filename": {
"default": "mdi:file-image-outline" "default": "mdi:file-image-outline"
}, },
"nozzle_diameter": {
"default": "mdi:printer-3d-nozzle"
},
"print_start": { "print_start": {
"default": "mdi:clock-start" "default": "mdi:clock-start"
}, },

View File

@ -7,7 +7,7 @@ from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Generic, TypeVar, cast 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 pyprusalink.types_legacy import LegacyPrinterStatus
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
@ -33,7 +33,7 @@ from . import PrusaLinkEntity
from .const import DOMAIN from .const import DOMAIN
from .coordinator import PrusaLinkUpdateCoordinator from .coordinator import PrusaLinkUpdateCoordinator
T = TypeVar("T", PrinterStatus, LegacyPrinterStatus, JobInfo) T = TypeVar("T", PrinterStatus, LegacyPrinterStatus, JobInfo, PrinterInfo)
@dataclass(frozen=True) @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,
),
),
} }

View File

@ -23,6 +23,11 @@
} }
}, },
"entity": { "entity": {
"binary_sensor": {
"mmu": {
"name": "MMU"
}
},
"sensor": { "sensor": {
"printer_state": { "printer_state": {
"state": { "state": {
@ -78,6 +83,9 @@
}, },
"z_height": { "z_height": {
"name": "Z-Height" "name": "Z-Height"
},
"nozzle_diameter": {
"name": "Nozzle Diameter"
} }
}, },
"button": { "button": {

View File

@ -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

View File

@ -101,6 +101,10 @@ async def test_sensors_no_job(hass: HomeAssistant, mock_config_entry, mock_api)
assert state is not None assert state is not None
assert state.state == "PLA" 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") state = hass.states.get("sensor.mock_title_print_flow")
assert state is not None assert state is not None
assert state.state == "100" assert state.state == "100"
@ -205,6 +209,10 @@ async def test_sensors_idle_job_mk3(
assert state is not None assert state is not None
assert state.state == "PLA" 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") state = hass.states.get("sensor.mock_title_print_flow")
assert state is not None assert state is not None
assert state.state == "100" assert state.state == "100"