Add Dremel 3D Printer integration (#85969)

* Add Dremel 3D Printer integration

* remove validators requirement

* ruff

* uno mas

* uno mas

* uno mas

* uno mas

---------

Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Tom Harris <tomharris@harrisnj.net>
This commit is contained in:
Robert Hillis
2023-05-31 16:55:57 -04:00
committed by GitHub
parent 8d8d0fc9d2
commit a1e9cf1c24
21 changed files with 910 additions and 0 deletions

View File

@ -289,6 +289,8 @@ build.json @home-assistant/supervisor
/tests/components/doorbird/ @oblogic7 @bdraco @flacjacket
/homeassistant/components/dormakaba_dkey/ @emontnemery
/tests/components/dormakaba_dkey/ @emontnemery
/homeassistant/components/dremel_3d_printer/ @tkdrob
/tests/components/dremel_3d_printer/ @tkdrob
/homeassistant/components/dsmr/ @Robbie1221 @frenck
/tests/components/dsmr/ @Robbie1221 @frenck
/homeassistant/components/dsmr_reader/ @depl0y @glodenox

View File

@ -0,0 +1,41 @@
"""The Dremel 3D Printer (3D20, 3D40, 3D45) integration."""
from __future__ import annotations
from dremel3dpy import Dremel3DPrinter
from requests.exceptions import ConnectTimeout, HTTPError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN
from .coordinator import Dremel3DPrinterDataUpdateCoordinator
PLATFORMS = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up Dremel 3D Printer from a config entry."""
try:
api = await hass.async_add_executor_job(
Dremel3DPrinter, config_entry.data[CONF_HOST]
)
except (ConnectTimeout, HTTPError) as ex:
raise ConfigEntryNotReady(
f"Unable to connect to Dremel 3D Printer: {ex}"
) from ex
coordinator = Dremel3DPrinterDataUpdateCoordinator(hass, api)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Dremel config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data.pop(DOMAIN)
return unload_ok

View File

@ -0,0 +1,58 @@
"""Config flow for Dremel 3D Printer (3D20, 3D40, 3D45)."""
from __future__ import annotations
from json.decoder import JSONDecodeError
from typing import Any
from dremel3dpy import Dremel3DPrinter
from requests.exceptions import ConnectTimeout, HTTPError
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_HOST
from homeassistant.data_entry_flow import FlowResult
import homeassistant.helpers.config_validation as cv
from .const import DOMAIN, LOGGER
def _schema_with_defaults(host: str = "") -> vol.Schema:
return vol.Schema({vol.Required(CONF_HOST, default=host): cv.string})
class Dremel3DPrinterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Dremel 3D Printer."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
errors = {}
if user_input is None:
return self.async_show_form(
step_id="user",
data_schema=_schema_with_defaults(),
)
host = user_input[CONF_HOST]
try:
api = await self.hass.async_add_executor_job(Dremel3DPrinter, host)
except (ConnectTimeout, HTTPError, JSONDecodeError):
errors = {"base": "cannot_connect"}
except Exception: # pylint: disable=broad-except
LOGGER.exception("An unknown error has occurred")
errors = {"base": "unknown"}
if errors:
return self.async_show_form(
step_id="user",
errors=errors,
data_schema=_schema_with_defaults(host=host),
)
await self.async_set_unique_id(api.get_serial_number())
self._abort_if_unique_id_configured()
return self.async_create_entry(title=api.get_title(), data={CONF_HOST: host})

View File

@ -0,0 +1,11 @@
"""Constants for the Dremel 3D Printer (3D20, 3D40, 3D45) integration."""
from __future__ import annotations
import logging
LOGGER = logging.getLogger(__package__)
DOMAIN = "dremel_3d_printer"
ATTR_EXTRUDER = "extruder"
ATTR_PLATFORM = "platform"

View File

@ -0,0 +1,36 @@
"""Data update coordinator for the Dremel 3D Printer integration."""
from datetime import timedelta
from dremel3dpy import Dremel3DPrinter
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, LOGGER
class Dremel3DPrinterDataUpdateCoordinator(DataUpdateCoordinator[None]):
"""Class to manage fetching Dremel 3D Printer data."""
config_entry: ConfigEntry
def __init__(self, hass: HomeAssistant, api: Dremel3DPrinter) -> None:
"""Initialize Dremel 3D Printer data update coordinator."""
super().__init__(
hass=hass,
logger=LOGGER,
name=DOMAIN,
update_interval=timedelta(seconds=10),
)
self.api = api
async def _async_update_data(self) -> None:
"""Update data via APIs."""
try:
await self.hass.async_add_executor_job(self.api.refresh)
except RuntimeError as ex:
raise UpdateFailed(
f"Unable to refresh printer information: Printer offline: {ex}"
) from ex

View File

@ -0,0 +1,41 @@
"""Entity representing a Dremel 3D Printer."""
from dremel3dpy import Dremel3DPrinter
from homeassistant.helpers.entity import DeviceInfo, EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import Dremel3DPrinterDataUpdateCoordinator
class Dremel3DPrinterEntity(CoordinatorEntity[Dremel3DPrinterDataUpdateCoordinator]):
"""Defines a Dremel 3D Printer device entity."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: Dremel3DPrinterDataUpdateCoordinator,
description: EntityDescription,
) -> None:
"""Initialize the base device entity."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.config_entry.unique_id}_{description.key}"
@property
def device_info(self) -> DeviceInfo:
"""Return device information about this Dremel printer."""
return DeviceInfo(
identifiers={(DOMAIN, self._api.get_serial_number())},
manufacturer=self._api.get_manufacturer(),
model=self._api.get_model(),
name=self._api.get_title(),
sw_version=self._api.get_firmware_version(),
)
@property
def _api(self) -> Dremel3DPrinter:
"""Return to api from coordinator."""
return self.coordinator.api

View File

@ -0,0 +1,10 @@
{
"domain": "dremel_3d_printer",
"name": "Dremel 3D Printer",
"codeowners": ["@tkdrob"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dremel_3d_printer",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["dremel3dpy==2.1.1"]
}

View File

@ -0,0 +1,284 @@
"""Support for monitoring Dremel 3D Printer sensors."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, timedelta
from dremel3dpy import Dremel3DPrinter
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
PERCENTAGE,
EntityCategory,
UnitOfInformation,
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.util.dt import utcnow
from homeassistant.util.variance import ignore_variance
from .const import ATTR_EXTRUDER, ATTR_PLATFORM, DOMAIN
from .entity import Dremel3DPrinterEntity
@dataclass
class Dremel3DPrinterSensorEntityMixin:
"""Mixin for Dremel 3D Printer sensor."""
value_fn: Callable[[Dremel3DPrinter, str], StateType | datetime]
@dataclass
class Dremel3DPrinterSensorEntityDescription(
SensorEntityDescription, Dremel3DPrinterSensorEntityMixin
):
"""Describes a Dremel 3D Printer sensor."""
available_fn: Callable[[Dremel3DPrinter, str], bool] = lambda api, _: True
SENSOR_TYPES: tuple[Dremel3DPrinterSensorEntityDescription, ...] = (
Dremel3DPrinterSensorEntityDescription(
key="job_phase",
name="Job phase",
icon="mdi:printer-3d",
value_fn=lambda api, _: api.get_printing_status(),
),
Dremel3DPrinterSensorEntityDescription(
key="remaining_time",
name="Remaining time",
device_class=SensorDeviceClass.TIMESTAMP,
available_fn=lambda api, key: api.get_job_status()[key] > 0,
value_fn=ignore_variance(
lambda api, key: utcnow() - timedelta(seconds=api.get_job_status()[key]),
timedelta(minutes=2),
),
),
Dremel3DPrinterSensorEntityDescription(
key="progress",
name="Progress",
icon="mdi:printer-3d-nozzle",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda api, _: api.get_printing_progress(),
),
Dremel3DPrinterSensorEntityDescription(
key="chamber",
name="Chamber",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda api, key: api.get_temperature_type(key),
),
Dremel3DPrinterSensorEntityDescription(
key="platform_temperature",
name="Platform temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda api, _: api.get_temperature_type(ATTR_PLATFORM),
),
Dremel3DPrinterSensorEntityDescription(
key="target_platform_temperature",
name="Target platform temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda api, _: api.get_temperature_attributes(ATTR_PLATFORM)[
"target_temp"
],
),
Dremel3DPrinterSensorEntityDescription(
key="max_platform_temperature",
name="Max platform temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda api, _: api.get_temperature_attributes(ATTR_PLATFORM)[
"max_temp"
],
),
Dremel3DPrinterSensorEntityDescription(
key=ATTR_EXTRUDER,
name="Extruder",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda api, key: api.get_temperature_type(key),
),
Dremel3DPrinterSensorEntityDescription(
key="target_extruder_temperature",
name="Target extruder temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda api, _: api.get_temperature_attributes(ATTR_EXTRUDER)[
"target_temp"
],
),
Dremel3DPrinterSensorEntityDescription(
key="max_extruder_temperature",
name="Max extruder temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda api, _: api.get_temperature_attributes(ATTR_EXTRUDER)[
"max_temp"
],
),
Dremel3DPrinterSensorEntityDescription(
key="network_build",
name="Network build",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda api, key: api.get_job_status()[key],
),
Dremel3DPrinterSensorEntityDescription(
key="filament",
name="Filament",
icon="mdi:printer-3d-nozzle",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda api, key: api.get_job_status()[key],
),
Dremel3DPrinterSensorEntityDescription(
key="elapsed_time",
name="Elapsed time",
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
available_fn=lambda api, _: api.get_printing_status() == "building",
value_fn=ignore_variance(
lambda api, key: utcnow() - timedelta(seconds=api.get_job_status()[key]),
timedelta(minutes=2),
),
),
Dremel3DPrinterSensorEntityDescription(
key="estimated_total_time",
name="Estimated total time",
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
available_fn=lambda api, key: api.get_job_status()[key] > 0,
value_fn=ignore_variance(
lambda api, key: utcnow() - timedelta(seconds=api.get_job_status()[key]),
timedelta(minutes=2),
),
),
Dremel3DPrinterSensorEntityDescription(
key="job_status",
name="Job status",
icon="mdi:printer-3d",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda api, key: api.get_job_status()[key],
),
Dremel3DPrinterSensorEntityDescription(
key="job_name",
name="Job name",
icon="mdi:file",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda api, _: api.get_job_name(),
),
Dremel3DPrinterSensorEntityDescription(
key="api_version",
name="API version",
icon="mdi:api",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda api, key: api.get_printer_info()[key],
),
Dremel3DPrinterSensorEntityDescription(
key="host",
name="Host",
icon="mdi:ip-network",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda api, key: api.get_printer_info()[key],
),
Dremel3DPrinterSensorEntityDescription(
key="connection_type",
name="Connection type",
icon="mdi:network",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda api, key: api.get_printer_info()[key],
),
Dremel3DPrinterSensorEntityDescription(
key="available_storage",
name="Available storage",
native_unit_of_measurement=UnitOfInformation.MEGABYTES,
device_class=SensorDeviceClass.DATA_SIZE,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda api, key: api.get_printer_info()[key] * 100,
),
Dremel3DPrinterSensorEntityDescription(
key="hours_used",
name="Hours used",
icon="mdi:clock",
native_unit_of_measurement=UnitOfTime.HOURS,
device_class=SensorDeviceClass.DURATION,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda api, key: api.get_printer_info()[key],
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the available Dremel 3D Printer sensors."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
async_add_entities(
Dremel3DPrinterSensor(coordinator, description) for description in SENSOR_TYPES
)
class Dremel3DPrinterSensor(Dremel3DPrinterEntity, SensorEntity):
"""Representation of an Dremel 3D Printer sensor."""
entity_description: Dremel3DPrinterSensorEntityDescription
@property
def available(self) -> bool:
"""Return True if the entity is available."""
return super().available and self.entity_description.available_fn(
self._api, self.entity_description.key
)
@property
def native_value(self) -> StateType | datetime:
"""Return the sensor state."""
return self.entity_description.value_fn(self._api, self.entity_description.key)

View File

@ -0,0 +1,18 @@
{
"config": {
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
}
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
}
}

View File

@ -101,6 +101,7 @@ FLOWS = {
"dnsip",
"doorbird",
"dormakaba_dkey",
"dremel_3d_printer",
"dsmr",
"dsmr_reader",
"dunehd",

View File

@ -1172,6 +1172,12 @@
"integration_type": "hub",
"config_flow": false
},
"dremel_3d_printer": {
"name": "Dremel 3D Printer",
"integration_type": "device",
"config_flow": true,
"iot_class": "local_polling"
},
"dsmr": {
"name": "DSMR Slimme Meter",
"integration_type": "hub",

View File

@ -616,6 +616,9 @@ doorbirdpy==2.1.0
# homeassistant.components.dovado
dovado==0.4.1
# homeassistant.components.dremel_3d_printer
dremel3dpy==2.1.1
# homeassistant.components.dsmr
dsmr_parser==0.33

View File

@ -493,6 +493,9 @@ discovery30303==0.2.1
# homeassistant.components.doorbird
doorbirdpy==2.1.0
# homeassistant.components.dremel_3d_printer
dremel3dpy==2.1.1
# homeassistant.components.dsmr
dsmr_parser==0.33

View File

@ -0,0 +1 @@
"""Tests for the Dremel 3D Printer integration."""

View File

@ -0,0 +1,58 @@
"""Configure tests for the Dremel 3D Printer integration."""
from http import HTTPStatus
from unittest.mock import patch
import pytest
import requests_mock
from homeassistant.components.dremel_3d_printer.const import DOMAIN
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, load_fixture
HOST = "1.2.3.4"
CONF_DATA = {CONF_HOST: HOST}
def create_entry(hass: HomeAssistant) -> MockConfigEntry:
"""Create fixture for adding config entry in Home Assistant."""
entry = MockConfigEntry(domain=DOMAIN, data=CONF_DATA, unique_id="123456789")
entry.add_to_hass(hass)
return entry
@pytest.fixture
def config_entry(hass: HomeAssistant) -> MockConfigEntry:
"""Add config entry in Home Assistant."""
return create_entry(hass)
@pytest.fixture
def connection() -> None:
"""Mock Dremel 3D Printer connection."""
mock = requests_mock.Mocker()
mock.post(
f"http://{HOST}:80/command",
response_list=[
{"text": load_fixture("dremel_3d_printer/command_1.json")},
{"text": load_fixture("dremel_3d_printer/command_2.json")},
{"text": load_fixture("dremel_3d_printer/command_1.json")},
{"text": load_fixture("dremel_3d_printer/command_2.json")},
],
)
mock.post(
f"https://{HOST}:11134/getHomeMessage",
text=load_fixture("dremel_3d_printer/get_home_message.json"),
status_code=HTTPStatus.OK,
)
mock.start()
def patch_async_setup_entry():
"""Patch the async entry setup of Dremel 3D Printer."""
return patch(
"homeassistant.components.dremel_3d_printer.async_setup_entry",
return_value=True,
)

View File

@ -0,0 +1,12 @@
{
"SN": "123456789",
"api_version": "1.0.2-alpha",
"error_code": 200,
"ethernet_connected": 1,
"ethernet_ip": "1.2.3.4",
"firmware_version": "v3.0_R02.12.10",
"machine_type": "DREMEL 3D45 IDEA BUILDER",
"message": "success",
"wifi_connected": 1,
"wifi_ip": "1.2.3.5"
}

View File

@ -0,0 +1,22 @@
{
"buildPlate_target_temperature": 60,
"chamber_temperature": 27,
"door_open": 0,
"elaspedtime": 0,
"error_code": 200,
"extruder_target_temperature": 230,
"fanSpeed": 0,
"filament_type ": "ECO-ABS",
"firmware_version": "v3.0_R02.12.10",
"jobname": "D32_Imperial_Credit.gcode",
"jobstatus": "building",
"layer": 0,
"message": "success",
"networkBuild": 1,
"platform_temperature": 60,
"progress": 13.9,
"remaining": 3736,
"status": "busy",
"temperature": 230,
"totalTime": 4340
}

View File

@ -0,0 +1,26 @@
{
"BedTemp": 60,
"BedTempTarget": 60,
"ErrorCode": 200,
"FilamentType": 2,
"FirwareVersion": "v3.0_R02.12.10",
"Message": "success",
"NozzleTemp": 230,
"NozzleTempTarget": 230,
"PreheatBed": 0,
"PreheatNozzle": 0,
"PrinterBedMessage": "Bed 0-100 ℃",
"PrinterCamera": "http://1.2.3.4:10123/?action=stream",
"PrinterFiles": 10,
"PrinterMicrons": "50-300 microns",
"PrinterName": "DREMEL DIGILAB 3D45",
"PrinterNozzleMessage": "Nozzle 0-280 ℃",
"PrinterStatus": "printing",
"PrintererAvailabelStorage": 87,
"PrintingFileName": "D32_Imperial_Credit.gcode",
"PrintingFilePic": "/tmp/mnt/dev/mmcblk0p3/modelFromDevice/pic/D32_Imperial_Credit_gcode.bmp",
"PrintingProgress": 13.9,
"RemainTime": 3736,
"SerialNumber": "123456789",
"UsageCounter": "7"
}

View File

@ -0,0 +1,87 @@
"""Test Dremel 3D Printer config flow."""
from unittest.mock import patch
from requests.exceptions import ConnectTimeout
from homeassistant import data_entry_flow
from homeassistant.components.dremel_3d_printer.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_SOURCE
from homeassistant.core import HomeAssistant
from .conftest import CONF_DATA, patch_async_setup_entry
from tests.common import MockConfigEntry
MOCK = "homeassistant.components.dremel_3d_printer.config_flow.Dremel3DPrinter"
async def test_full_user_flow_implementation(hass: HomeAssistant, connection) -> None:
"""Test the full manual user flow from start to finish."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
with patch_async_setup_entry():
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=CONF_DATA
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "DREMEL 3D45"
assert result["data"] == CONF_DATA
async def test_already_configured(
hass: HomeAssistant, connection, config_entry: MockConfigEntry
) -> None:
"""Test we abort if the device is already configured."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
async def test_cannot_connect(hass: HomeAssistant, connection) -> None:
"""Test we show user form on connection error."""
with patch(MOCK, side_effect=ConnectTimeout):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result["errors"] == {"base": "cannot_connect"}
with patch_async_setup_entry():
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=CONF_DATA
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["data"] == CONF_DATA
async def test_unknown_error(hass: HomeAssistant, connection) -> None:
"""Test we show user form on unknown error."""
with patch(MOCK, side_effect=Exception):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result["errors"] == {"base": "unknown"}
with patch_async_setup_entry():
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=CONF_DATA
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "DREMEL 3D45"
assert result["data"] == CONF_DATA

View File

@ -0,0 +1,80 @@
"""Test Dremel 3D Printer integration."""
from datetime import timedelta
from unittest.mock import patch
from requests.exceptions import ConnectTimeout
from homeassistant.components.dremel_3d_printer.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from tests.common import MockConfigEntry, async_fire_time_changed
async def test_setup(
hass: HomeAssistant, connection, config_entry: MockConfigEntry
) -> None:
"""Test load and unload."""
await hass.config_entries.async_setup(config_entry.entry_id)
assert await async_setup_component(hass, DOMAIN, {})
assert config_entry.state == ConfigEntryState.LOADED
assert await hass.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.NOT_LOADED
assert not hass.data.get(DOMAIN)
async def test_async_setup_entry_not_ready(
hass: HomeAssistant, connection, config_entry: MockConfigEntry
) -> None:
"""Test that it throws ConfigEntryNotReady when exception occurs during setup."""
with patch(
"homeassistant.components.dremel_3d_printer.Dremel3DPrinter",
side_effect=ConnectTimeout,
):
await hass.config_entries.async_setup(config_entry.entry_id)
assert await async_setup_component(hass, DOMAIN, {})
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert config_entry.state == ConfigEntryState.SETUP_RETRY
assert not hass.data.get(DOMAIN)
async def test_update_failed(
hass: HomeAssistant, connection, config_entry: MockConfigEntry
) -> None:
"""Test coordinator throws UpdateFailed after failed update."""
await hass.config_entries.async_setup(config_entry.entry_id)
assert await async_setup_component(hass, DOMAIN, {})
assert config_entry.state == ConfigEntryState.LOADED
with patch(
"homeassistant.components.dremel_3d_printer.Dremel3DPrinter.refresh",
side_effect=RuntimeError,
) as updater:
next_update = dt_util.utcnow() + timedelta(seconds=10)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
updater.assert_called_once()
state = hass.states.get("sensor.dremel_3d45_job_phase")
assert state.state == STATE_UNAVAILABLE
async def test_device_info(
hass: HomeAssistant, connection, config_entry: MockConfigEntry
) -> None:
"""Test device info."""
await hass.config_entries.async_setup(config_entry.entry_id)
assert await async_setup_component(hass, DOMAIN, {})
device_registry = dr.async_get(hass)
device = device_registry.async_get_device({(DOMAIN, config_entry.unique_id)})
assert device.manufacturer == "Dremel"
assert device.model == "3D45"
assert device.name == "DREMEL 3D45"
assert device.sw_version == "v3.0_R02.12.10"

View File

@ -0,0 +1,110 @@
"""Sensor tests for the Dremel 3D Printer integration."""
from datetime import datetime
from unittest.mock import AsyncMock
from freezegun.api import FrozenDateTimeFactory
from homeassistant.components.dremel_3d_printer.const import DOMAIN
from homeassistant.components.sensor import (
ATTR_STATE_CLASS,
SensorDeviceClass,
SensorStateClass,
)
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_UNIT_OF_MEASUREMENT,
PERCENTAGE,
UnitOfInformation,
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from homeassistant.util.dt import UTC
from tests.common import MockConfigEntry
async def test_sensors(
hass: HomeAssistant,
connection,
config_entry: MockConfigEntry,
entity_registry_enabled_by_default: AsyncMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test we get sensor data."""
freezer.move_to(datetime(2023, 5, 31, 13, 30, tzinfo=UTC))
await hass.config_entries.async_setup(config_entry.entry_id)
assert await async_setup_component(hass, DOMAIN, {})
state = hass.states.get("sensor.dremel_3d45_job_phase")
assert state.state == "building"
state = hass.states.get("sensor.dremel_3d45_remaining_time")
assert state.state == "2023-05-31T12:27:44+00:00"
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP
state = hass.states.get("sensor.dremel_3d45_progress")
assert state.state == "13.9"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is PERCENTAGE
assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT
state = hass.states.get("sensor.dremel_3d45_chamber")
assert state.state == "27"
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is UnitOfTemperature.CELSIUS
assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT
state = hass.states.get("sensor.dremel_3d45_platform_temperature")
assert state.state == "60"
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is UnitOfTemperature.CELSIUS
assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT
state = hass.states.get("sensor.dremel_3d45_target_platform_temperature")
assert state.state == "60"
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is UnitOfTemperature.CELSIUS
assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT
state = hass.states.get("sensor.dremel_3d45_max_platform_temperature")
assert state.state == "100"
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is UnitOfTemperature.CELSIUS
assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT
state = hass.states.get("sensor.dremel_3d45_extruder")
assert state.state == "230"
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is UnitOfTemperature.CELSIUS
assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT
state = hass.states.get("sensor.dremel_3d45_target_extruder_temperature")
assert state.state == "230"
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is UnitOfTemperature.CELSIUS
assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT
state = hass.states.get("sensor.dremel_3d45_max_extruder_temperature")
assert state.state == "280"
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is UnitOfTemperature.CELSIUS
assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT
state = hass.states.get("sensor.dremel_3d45_network_build")
assert state.state == "1"
state = hass.states.get("sensor.dremel_3d45_filament")
assert state.state == "ECO-ABS"
state = hass.states.get("sensor.dremel_3d45_elapsed_time")
assert state.state == "2023-05-31T13:30:00+00:00"
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP
state = hass.states.get("sensor.dremel_3d45_estimated_total_time")
assert state.state == "2023-05-31T12:17:40+00:00"
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP
state = hass.states.get("sensor.dremel_3d45_job_status")
assert state.state == "building"
state = hass.states.get("sensor.dremel_3d45_job_name")
assert state.state == "D32_Imperial_Credit"
state = hass.states.get("sensor.dremel_3d45_api_version")
assert state.state == "1.0.2-alpha"
state = hass.states.get("sensor.dremel_3d45_host")
assert state.state == "1.2.3.4"
state = hass.states.get("sensor.dremel_3d45_connection_type")
assert state.state == "eth0"
state = hass.states.get("sensor.dremel_3d45_available_storage")
assert state.state == "8700"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is UnitOfInformation.MEGABYTES
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATA_SIZE
state = hass.states.get("sensor.dremel_3d45_hours_used")
assert state.state == "7"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is UnitOfTime.HOURS
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DURATION