Rework octoprint (#58040)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Ryan Fleming 2021-10-22 09:25:12 -04:00 committed by GitHub
parent 1a9ac6b657
commit c84fee7c6e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 1485 additions and 392 deletions

View file

@ -1,143 +1,256 @@
"""Support for monitoring OctoPrint sensors."""
from __future__ import annotations
from datetime import timedelta
import logging
import requests
from pyoctoprintapi import OctoprintJobInfo, OctoprintPrinterInfo
from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity
from homeassistant.const import DEVICE_CLASS_TEMPERATURE, PERCENTAGE, TEMP_CELSIUS
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TIMESTAMP,
PERCENTAGE,
TEMP_CELSIUS,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from . import DOMAIN as COMPONENT_DOMAIN, SENSOR_TYPES
from . import DOMAIN as COMPONENT_DOMAIN
_LOGGER = logging.getLogger(__name__)
NOTIFICATION_ID = "octoprint_notification"
NOTIFICATION_TITLE = "OctoPrint sensor setup error"
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the available OctoPrint binary sensors."""
coordinator: DataUpdateCoordinator = hass.data[COMPONENT_DOMAIN][
config_entry.entry_id
]["coordinator"]
device_id = config_entry.unique_id
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the available OctoPrint sensors."""
if discovery_info is None:
return
assert device_id is not None
name = discovery_info["name"]
base_url = discovery_info["base_url"]
monitored_conditions = discovery_info["sensors"]
octoprint_api = hass.data[COMPONENT_DOMAIN][base_url]
tools = octoprint_api.get_tools()
if "Temperatures" in monitored_conditions and not tools:
hass.components.persistent_notification.create(
"Your printer appears to be offline.<br />"
"If you do not want to have your printer on <br />"
" at all times, and you would like to monitor <br /> "
"temperatures, please add <br />"
"bed and/or number&#95;of&#95;tools to your configuration <br />"
"and restart.",
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID,
)
devices = []
types = ["actual", "target"]
for octo_type in monitored_conditions:
if octo_type == "Temperatures":
for tool in tools:
for temp_type in types:
new_sensor = OctoPrintSensor(
api=octoprint_api,
condition=temp_type,
sensor_type=temp_type,
sensor_name=name,
unit=SENSOR_TYPES[octo_type][3],
endpoint=SENSOR_TYPES[octo_type][0],
group=SENSOR_TYPES[octo_type][1],
tool=tool,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
entities: list[SensorEntity] = []
if coordinator.data["printer"]:
printer_info = coordinator.data["printer"]
types = ["actual", "target"]
for tool in printer_info.temperatures:
for temp_type in types:
entities.append(
OctoPrintTemperatureSensor(
coordinator,
tool.name,
temp_type,
device_id,
)
devices.append(new_sensor)
else:
new_sensor = OctoPrintSensor(
api=octoprint_api,
condition=octo_type,
sensor_type=SENSOR_TYPES[octo_type][2],
sensor_name=name,
unit=SENSOR_TYPES[octo_type][3],
endpoint=SENSOR_TYPES[octo_type][0],
group=SENSOR_TYPES[octo_type][1],
icon=SENSOR_TYPES[octo_type][4],
)
devices.append(new_sensor)
add_entities(devices, True)
)
else:
_LOGGER.error("Printer appears to be offline, skipping temperature sensors")
entities.append(OctoPrintStatusSensor(coordinator, device_id))
entities.append(OctoPrintJobPercentageSensor(coordinator, device_id))
entities.append(OctoPrintEstimatedFinishTimeSensor(coordinator, device_id))
entities.append(OctoPrintStartTimeSensor(coordinator, device_id))
async_add_entities(entities)
class OctoPrintSensor(SensorEntity):
class OctoPrintSensorBase(CoordinatorEntity, SensorEntity):
"""Representation of an OctoPrint sensor."""
def __init__(
self,
api,
condition,
sensor_type,
sensor_name,
unit,
endpoint,
group,
tool=None,
icon=None,
device_class=None,
state_class=None,
):
coordinator: DataUpdateCoordinator,
sensor_type: str,
device_id: str,
) -> None:
"""Initialize a new OctoPrint sensor."""
self.sensor_name = sensor_name
if tool is None:
self._name = f"{sensor_name} {condition}"
else:
self._name = f"{sensor_name} {condition} {tool} temp"
self.sensor_type = sensor_type
self.api = api
self._state = None
self._unit_of_measurement = unit
self.api_endpoint = endpoint
self.api_group = group
self.api_tool = tool
self._icon = icon
self._attr_device_class = device_class
self._attr_state_class = state_class
_LOGGER.debug("Created OctoPrint sensor %r", self)
super().__init__(coordinator)
self._sensor_type = sensor_type
self._name = f"Octoprint {sensor_type}"
self._device_id = device_id
@property
def device_info(self):
"""Device info."""
return {
"identifiers": {(COMPONENT_DOMAIN, self._device_id)},
"manufacturer": "Octoprint",
"name": "Octoprint",
}
@property
def unique_id(self):
"""Return a unique id."""
return f"{self._sensor_type}-{self._device_id}"
@property
def name(self):
"""Return the name of the sensor."""
return self._name
class OctoPrintStatusSensor(OctoPrintSensorBase):
"""Representation of an OctoPrint sensor."""
def __init__(self, coordinator: DataUpdateCoordinator, device_id: str) -> None:
"""Initialize a new OctoPrint sensor."""
super().__init__(coordinator, "Current State", device_id)
@property
def native_value(self):
"""Return the state of the sensor."""
sensor_unit = self.unit_of_measurement
if sensor_unit in (TEMP_CELSIUS, PERCENTAGE):
# API sometimes returns null and not 0
if self._state is None:
self._state = 0
return round(self._state, 2)
return self._state
"""Return sensor state."""
printer: OctoprintPrinterInfo = self.coordinator.data["printer"]
if not printer:
return None
@property
def native_unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self._unit_of_measurement
def update(self):
"""Update state of sensor."""
try:
self._state = self.api.update(
self.sensor_type, self.api_endpoint, self.api_group, self.api_tool
)
except requests.exceptions.ConnectionError:
# Error calling the api, already logged in api.update()
return
return printer.state.text
@property
def icon(self):
"""Icon to use in the frontend."""
return self._icon
return "mdi:printer-3d"
@property
def available(self) -> bool:
"""Return if entity is available."""
return self.coordinator.last_update_success and self.coordinator.data["printer"]
class OctoPrintJobPercentageSensor(OctoPrintSensorBase):
"""Representation of an OctoPrint sensor."""
def __init__(self, coordinator: DataUpdateCoordinator, device_id: str) -> None:
"""Initialize a new OctoPrint sensor."""
super().__init__(coordinator, "Job Percentage", device_id)
@property
def native_value(self):
"""Return sensor state."""
job: OctoprintJobInfo = self.coordinator.data["job"]
if not job:
return None
state = job.progress.completion
if not state:
return 0
return round(state, 2)
@property
def native_unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return PERCENTAGE
@property
def icon(self):
"""Icon to use in the frontend."""
return "mdi:file-percent"
class OctoPrintEstimatedFinishTimeSensor(OctoPrintSensorBase):
"""Representation of an OctoPrint sensor."""
def __init__(self, coordinator: DataUpdateCoordinator, device_id: str) -> None:
"""Initialize a new OctoPrint sensor."""
super().__init__(coordinator, "Estimated Finish Time", device_id)
@property
def native_value(self):
"""Return sensor state."""
job: OctoprintJobInfo = self.coordinator.data["job"]
if not job or not job.progress.print_time_left or job.state != "Printing":
return None
read_time = self.coordinator.data["last_read_time"]
return (read_time + timedelta(seconds=job.progress.print_time_left)).isoformat()
@property
def device_class(self):
"""Return the device class of the sensor."""
return DEVICE_CLASS_TIMESTAMP
class OctoPrintStartTimeSensor(OctoPrintSensorBase):
"""Representation of an OctoPrint sensor."""
def __init__(self, coordinator: DataUpdateCoordinator, device_id: str) -> None:
"""Initialize a new OctoPrint sensor."""
super().__init__(coordinator, "Start Time", device_id)
@property
def native_value(self):
"""Return sensor state."""
job: OctoprintJobInfo = self.coordinator.data["job"]
if not job or not job.progress.print_time or job.state != "Printing":
return None
read_time = self.coordinator.data["last_read_time"]
return (read_time - timedelta(seconds=job.progress.print_time)).isoformat()
@property
def device_class(self):
"""Return the device class of the sensor."""
return DEVICE_CLASS_TIMESTAMP
class OctoPrintTemperatureSensor(OctoPrintSensorBase):
"""Representation of an OctoPrint sensor."""
def __init__(
self,
coordinator: DataUpdateCoordinator,
tool: str,
temp_type: str,
device_id: str,
) -> None:
"""Initialize a new OctoPrint sensor."""
super().__init__(coordinator, f"{temp_type} {tool} temp", device_id)
self._temp_type = temp_type
self._api_tool = tool
self._attr_state_class = STATE_CLASS_MEASUREMENT
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return TEMP_CELSIUS
@property
def device_class(self):
"""Return the device class of this entity."""
return DEVICE_CLASS_TEMPERATURE
@property
def native_value(self):
"""Return sensor state."""
printer: OctoprintPrinterInfo = self.coordinator.data["printer"]
if not printer:
return None
for temp in printer.temperatures:
if temp.name == self._api_tool:
return round(
temp.actual_temp
if self._temp_type == "actual"
else temp.target_temp,
2,
)
return None
@property
def available(self) -> bool:
"""Return if entity is available."""
return self.coordinator.last_update_success and self.coordinator.data["printer"]