Skip to content

Commit e8227ba

Browse files
AmadeusWjoostlek
andauthored
Add temperature number entity to set Tool and Bed temperatures to Octoprint (#153712)
Co-authored-by: Joost Lekkerkerker <[email protected]>
1 parent 4d525de commit e8227ba

File tree

6 files changed

+576
-2
lines changed

6 files changed

+576
-2
lines changed

homeassistant/components/octoprint/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,13 @@ def ensure_valid_path(value):
5656
return value
5757

5858

59-
PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CAMERA, Platform.SENSOR]
59+
PLATFORMS = [
60+
Platform.BINARY_SENSOR,
61+
Platform.BUTTON,
62+
Platform.CAMERA,
63+
Platform.NUMBER,
64+
Platform.SENSOR,
65+
]
6066
DEFAULT_NAME = "OctoPrint"
6167
CONF_NUMBER_OF_TOOLS = "number_of_tools"
6268
CONF_BED = "bed"
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"""Support for OctoPrint number entities."""
2+
3+
from __future__ import annotations
4+
5+
import logging
6+
7+
from pyoctoprintapi import OctoprintClient
8+
9+
from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberMode
10+
from homeassistant.config_entries import ConfigEntry
11+
from homeassistant.const import UnitOfTemperature
12+
from homeassistant.core import HomeAssistant, callback
13+
from homeassistant.exceptions import HomeAssistantError
14+
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
15+
from homeassistant.helpers.update_coordinator import CoordinatorEntity
16+
17+
from . import OctoprintDataUpdateCoordinator
18+
from .const import DOMAIN
19+
20+
_LOGGER = logging.getLogger(__name__)
21+
22+
23+
def is_bed(tool_name: str) -> bool:
24+
"""Return True if the tool name indicates a bed."""
25+
return tool_name == "bed"
26+
27+
28+
def is_extruder(tool_name: str) -> bool:
29+
"""Return True if the tool name indicates an extruder."""
30+
return tool_name.startswith("tool") and tool_name[4:].isdigit()
31+
32+
33+
def is_first_extruder(tool_name: str) -> bool:
34+
"""Return True if the tool name indicates the first extruder."""
35+
return tool_name == "tool0"
36+
37+
38+
async def async_setup_entry(
39+
hass: HomeAssistant,
40+
config_entry: ConfigEntry,
41+
async_add_entities: AddConfigEntryEntitiesCallback,
42+
) -> None:
43+
"""Set up the OctoPrint number entities."""
44+
coordinator: OctoprintDataUpdateCoordinator = hass.data[DOMAIN][
45+
config_entry.entry_id
46+
]["coordinator"]
47+
client: OctoprintClient = hass.data[DOMAIN][config_entry.entry_id]["client"]
48+
device_id = config_entry.unique_id
49+
50+
assert device_id is not None
51+
52+
known_tools = set()
53+
54+
@callback
55+
def async_add_tool_numbers() -> None:
56+
if not coordinator.data["printer"]:
57+
return
58+
59+
new_numbers: list[OctoPrintTemperatureNumber] = []
60+
for tool in coordinator.data["printer"].temperatures:
61+
if (
62+
is_extruder(tool.name) or is_bed(tool.name)
63+
) and tool.name not in known_tools:
64+
assert device_id is not None
65+
known_tools.add(tool.name)
66+
new_numbers.append(
67+
OctoPrintTemperatureNumber(
68+
coordinator,
69+
client,
70+
tool.name,
71+
device_id,
72+
)
73+
)
74+
async_add_entities(new_numbers)
75+
76+
config_entry.async_on_unload(coordinator.async_add_listener(async_add_tool_numbers))
77+
78+
if coordinator.data["printer"]:
79+
async_add_tool_numbers()
80+
81+
82+
class OctoPrintTemperatureNumber(
83+
CoordinatorEntity[OctoprintDataUpdateCoordinator], NumberEntity
84+
):
85+
"""Representation of an OctoPrint temperature setter entity."""
86+
87+
_attr_has_entity_name = True
88+
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
89+
_attr_native_min_value = 0
90+
_attr_native_max_value = 300
91+
_attr_native_step = 1
92+
_attr_mode = NumberMode.BOX
93+
_attr_device_class = NumberDeviceClass.TEMPERATURE
94+
95+
def __init__(
96+
self,
97+
coordinator: OctoprintDataUpdateCoordinator,
98+
client: OctoprintClient,
99+
tool: str,
100+
device_id: str,
101+
) -> None:
102+
"""Initialize a new OctoPrint temperature number entity."""
103+
super().__init__(coordinator)
104+
self._api_tool = tool
105+
self._attr_device_info = coordinator.device_info
106+
self._attr_unique_id = f"{device_id}_{tool}_temperature"
107+
self._client = client
108+
self._device_id = device_id
109+
if is_bed(tool):
110+
self._attr_translation_key = "bed_temperature"
111+
elif is_first_extruder(tool):
112+
self._attr_translation_key = "extruder_temperature"
113+
else:
114+
self._attr_translation_key = "extruder_n_temperature"
115+
self._attr_translation_placeholders = {"n": tool[4:]}
116+
117+
@property
118+
def native_value(self) -> float | None:
119+
"""Return the current target temperature."""
120+
if not self.coordinator.data["printer"]:
121+
return None
122+
for tool in self.coordinator.data["printer"].temperatures:
123+
if tool.name == self._api_tool and tool.target_temp is not None:
124+
return tool.target_temp
125+
126+
return None
127+
128+
async def async_set_native_value(self, value: float) -> None:
129+
"""Set the target temperature."""
130+
131+
try:
132+
if is_bed(self._api_tool):
133+
await self._client.set_bed_temperature(int(value))
134+
elif is_extruder(self._api_tool):
135+
await self._client.set_tool_temperature(self._api_tool, int(value))
136+
except Exception as err:
137+
raise HomeAssistantError(
138+
translation_domain=DOMAIN,
139+
translation_key="error_setting_temperature",
140+
translation_placeholders={
141+
"tool": self._api_tool,
142+
},
143+
) from err
144+
145+
# Request coordinator update to reflect the change
146+
await self.coordinator.async_request_refresh()

homeassistant/components/octoprint/strings.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,23 @@
3636
"get_api_key": "Open the OctoPrint UI and select **Allow** on the Access Request for **Home Assistant**."
3737
}
3838
},
39+
"entity": {
40+
"number": {
41+
"bed_temperature": {
42+
"name": "Bed temperature"
43+
},
44+
"extruder_temperature": {
45+
"name": "Extruder temperature"
46+
},
47+
"extruder_n_temperature": {
48+
"name": "Extruder {n} temperature"
49+
}
50+
}
51+
},
3952
"exceptions": {
53+
"error_setting_temperature": {
54+
"message": "Error setting target {tool} temperature"
55+
},
4056
"missing_client": {
4157
"message": "No client for device ID: {device_id}"
4258
}

tests/components/octoprint/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ async def init_integration(
5353
platform: Platform,
5454
printer: dict[str, Any] | UndefinedType | None = UNDEFINED,
5555
job: dict[str, Any] | None = None,
56-
) -> None:
56+
) -> MockConfigEntry:
5757
"""Set up the octoprint integration in Home Assistant."""
5858
printer_info: OctoprintPrinterInfo | None = None
5959
if printer is UNDEFINED:
@@ -102,3 +102,4 @@ async def init_integration(
102102
await hass.async_block_till_done()
103103

104104
assert config_entry.state is ConfigEntryState.LOADED
105+
return config_entry
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# serializer version: 1
2+
# name: test_numbers[number.octoprint_bed_temperature-entry]
3+
EntityRegistryEntrySnapshot({
4+
'aliases': set({
5+
}),
6+
'area_id': None,
7+
'capabilities': dict({
8+
'max': 300,
9+
'min': 0,
10+
'mode': <NumberMode.BOX: 'box'>,
11+
'step': 1,
12+
}),
13+
'config_entry_id': <ANY>,
14+
'config_subentry_id': <ANY>,
15+
'device_class': None,
16+
'device_id': <ANY>,
17+
'disabled_by': None,
18+
'domain': 'number',
19+
'entity_category': None,
20+
'entity_id': 'number.octoprint_bed_temperature',
21+
'has_entity_name': True,
22+
'hidden_by': None,
23+
'icon': None,
24+
'id': <ANY>,
25+
'labels': set({
26+
}),
27+
'name': None,
28+
'options': dict({
29+
}),
30+
'original_device_class': <NumberDeviceClass.TEMPERATURE: 'temperature'>,
31+
'original_icon': None,
32+
'original_name': 'Bed temperature',
33+
'platform': 'octoprint',
34+
'previous_unique_id': None,
35+
'suggested_object_id': None,
36+
'supported_features': 0,
37+
'translation_key': 'bed_temperature',
38+
'unique_id': 'uuid_bed_temperature',
39+
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
40+
})
41+
# ---
42+
# name: test_numbers[number.octoprint_bed_temperature-state]
43+
StateSnapshot({
44+
'attributes': ReadOnlyDict({
45+
'device_class': 'temperature',
46+
'friendly_name': 'OctoPrint Bed temperature',
47+
'max': 300,
48+
'min': 0,
49+
'mode': <NumberMode.BOX: 'box'>,
50+
'step': 1,
51+
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
52+
}),
53+
'context': <ANY>,
54+
'entity_id': 'number.octoprint_bed_temperature',
55+
'last_changed': <ANY>,
56+
'last_reported': <ANY>,
57+
'last_updated': <ANY>,
58+
'state': '60.0',
59+
})
60+
# ---
61+
# name: test_numbers[number.octoprint_extruder_1_temperature-entry]
62+
EntityRegistryEntrySnapshot({
63+
'aliases': set({
64+
}),
65+
'area_id': None,
66+
'capabilities': dict({
67+
'max': 300,
68+
'min': 0,
69+
'mode': <NumberMode.BOX: 'box'>,
70+
'step': 1,
71+
}),
72+
'config_entry_id': <ANY>,
73+
'config_subentry_id': <ANY>,
74+
'device_class': None,
75+
'device_id': <ANY>,
76+
'disabled_by': None,
77+
'domain': 'number',
78+
'entity_category': None,
79+
'entity_id': 'number.octoprint_extruder_1_temperature',
80+
'has_entity_name': True,
81+
'hidden_by': None,
82+
'icon': None,
83+
'id': <ANY>,
84+
'labels': set({
85+
}),
86+
'name': None,
87+
'options': dict({
88+
}),
89+
'original_device_class': <NumberDeviceClass.TEMPERATURE: 'temperature'>,
90+
'original_icon': None,
91+
'original_name': 'Extruder 1 temperature',
92+
'platform': 'octoprint',
93+
'previous_unique_id': None,
94+
'suggested_object_id': None,
95+
'supported_features': 0,
96+
'translation_key': 'extruder_n_temperature',
97+
'unique_id': 'uuid_tool1_temperature',
98+
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
99+
})
100+
# ---
101+
# name: test_numbers[number.octoprint_extruder_1_temperature-state]
102+
StateSnapshot({
103+
'attributes': ReadOnlyDict({
104+
'device_class': 'temperature',
105+
'friendly_name': 'OctoPrint Extruder 1 temperature',
106+
'max': 300,
107+
'min': 0,
108+
'mode': <NumberMode.BOX: 'box'>,
109+
'step': 1,
110+
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
111+
}),
112+
'context': <ANY>,
113+
'entity_id': 'number.octoprint_extruder_1_temperature',
114+
'last_changed': <ANY>,
115+
'last_reported': <ANY>,
116+
'last_updated': <ANY>,
117+
'state': '31.0',
118+
})
119+
# ---
120+
# name: test_numbers[number.octoprint_extruder_temperature-entry]
121+
EntityRegistryEntrySnapshot({
122+
'aliases': set({
123+
}),
124+
'area_id': None,
125+
'capabilities': dict({
126+
'max': 300,
127+
'min': 0,
128+
'mode': <NumberMode.BOX: 'box'>,
129+
'step': 1,
130+
}),
131+
'config_entry_id': <ANY>,
132+
'config_subentry_id': <ANY>,
133+
'device_class': None,
134+
'device_id': <ANY>,
135+
'disabled_by': None,
136+
'domain': 'number',
137+
'entity_category': None,
138+
'entity_id': 'number.octoprint_extruder_temperature',
139+
'has_entity_name': True,
140+
'hidden_by': None,
141+
'icon': None,
142+
'id': <ANY>,
143+
'labels': set({
144+
}),
145+
'name': None,
146+
'options': dict({
147+
}),
148+
'original_device_class': <NumberDeviceClass.TEMPERATURE: 'temperature'>,
149+
'original_icon': None,
150+
'original_name': 'Extruder temperature',
151+
'platform': 'octoprint',
152+
'previous_unique_id': None,
153+
'suggested_object_id': None,
154+
'supported_features': 0,
155+
'translation_key': 'extruder_temperature',
156+
'unique_id': 'uuid_tool0_temperature',
157+
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
158+
})
159+
# ---
160+
# name: test_numbers[number.octoprint_extruder_temperature-state]
161+
StateSnapshot({
162+
'attributes': ReadOnlyDict({
163+
'device_class': 'temperature',
164+
'friendly_name': 'OctoPrint Extruder temperature',
165+
'max': 300,
166+
'min': 0,
167+
'mode': <NumberMode.BOX: 'box'>,
168+
'step': 1,
169+
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
170+
}),
171+
'context': <ANY>,
172+
'entity_id': 'number.octoprint_extruder_temperature',
173+
'last_changed': <ANY>,
174+
'last_reported': <ANY>,
175+
'last_updated': <ANY>,
176+
'state': '37.83136',
177+
})
178+
# ---

0 commit comments

Comments
 (0)