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 .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()

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

View File

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

View File

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

View File

@ -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": {

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.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"