mirror of
https://github.com/home-assistant/core.git
synced 2025-06-25 01:21:51 +02:00
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:
@ -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
|
||||
|
41
homeassistant/components/dremel_3d_printer/__init__.py
Normal file
41
homeassistant/components/dremel_3d_printer/__init__.py
Normal 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
|
58
homeassistant/components/dremel_3d_printer/config_flow.py
Normal file
58
homeassistant/components/dremel_3d_printer/config_flow.py
Normal 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})
|
11
homeassistant/components/dremel_3d_printer/const.py
Normal file
11
homeassistant/components/dremel_3d_printer/const.py
Normal 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"
|
36
homeassistant/components/dremel_3d_printer/coordinator.py
Normal file
36
homeassistant/components/dremel_3d_printer/coordinator.py
Normal 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
|
41
homeassistant/components/dremel_3d_printer/entity.py
Normal file
41
homeassistant/components/dremel_3d_printer/entity.py
Normal 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
|
10
homeassistant/components/dremel_3d_printer/manifest.json
Normal file
10
homeassistant/components/dremel_3d_printer/manifest.json
Normal 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"]
|
||||
}
|
284
homeassistant/components/dremel_3d_printer/sensor.py
Normal file
284
homeassistant/components/dremel_3d_printer/sensor.py
Normal 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)
|
18
homeassistant/components/dremel_3d_printer/strings.json
Normal file
18
homeassistant/components/dremel_3d_printer/strings.json
Normal 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%]"
|
||||
}
|
||||
}
|
||||
}
|
@ -101,6 +101,7 @@ FLOWS = {
|
||||
"dnsip",
|
||||
"doorbird",
|
||||
"dormakaba_dkey",
|
||||
"dremel_3d_printer",
|
||||
"dsmr",
|
||||
"dsmr_reader",
|
||||
"dunehd",
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
1
tests/components/dremel_3d_printer/__init__.py
Normal file
1
tests/components/dremel_3d_printer/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the Dremel 3D Printer integration."""
|
58
tests/components/dremel_3d_printer/conftest.py
Normal file
58
tests/components/dremel_3d_printer/conftest.py
Normal 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,
|
||||
)
|
12
tests/components/dremel_3d_printer/fixtures/command_1.json
Normal file
12
tests/components/dremel_3d_printer/fixtures/command_1.json
Normal 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"
|
||||
}
|
22
tests/components/dremel_3d_printer/fixtures/command_2.json
Normal file
22
tests/components/dremel_3d_printer/fixtures/command_2.json
Normal 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
|
||||
}
|
@ -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"
|
||||
}
|
87
tests/components/dremel_3d_printer/test_config_flow.py
Normal file
87
tests/components/dremel_3d_printer/test_config_flow.py
Normal 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
|
80
tests/components/dremel_3d_printer/test_init.py
Normal file
80
tests/components/dremel_3d_printer/test_init.py
Normal 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"
|
110
tests/components/dremel_3d_printer/test_sensor.py
Normal file
110
tests/components/dremel_3d_printer/test_sensor.py
Normal 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
|
Reference in New Issue
Block a user