Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for monitoring OctoPrint 3D printers."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import cast
7 
8 import aiohttp
9 from pyoctoprintapi import OctoprintClient
10 import voluptuous as vol
11 
12 from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
13 from homeassistant.const import (
14  CONF_API_KEY,
15  CONF_BINARY_SENSORS,
16  CONF_DEVICE_ID,
17  CONF_HOST,
18  CONF_MONITORED_CONDITIONS,
19  CONF_NAME,
20  CONF_PATH,
21  CONF_PORT,
22  CONF_PROFILE_NAME,
23  CONF_SENSORS,
24  CONF_SSL,
25  CONF_VERIFY_SSL,
26  EVENT_HOMEASSISTANT_STOP,
27  Platform,
28 )
29 from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
30 from homeassistant.exceptions import ServiceValidationError
33 from homeassistant.helpers.typing import ConfigType
34 from homeassistant.util import slugify as util_slugify
35 from homeassistant.util.ssl import get_default_context, get_default_no_verify_context
36 
37 from .const import CONF_BAUDRATE, DOMAIN, SERVICE_CONNECT
38 from .coordinator import OctoprintDataUpdateCoordinator
39 
40 _LOGGER = logging.getLogger(__name__)
41 
42 
44  """Validate that printers have an unique name."""
45  names = [util_slugify(printer["name"]) for printer in value]
46  vol.Schema(vol.Unique())(names)
47  return value
48 
49 
50 def ensure_valid_path(value):
51  """Validate the path, ensuring it starts and ends with a /."""
52  vol.Schema(cv.string)(value)
53  if value[0] != "/":
54  value = f"/{value}"
55  if value[-1] != "/":
56  value += "/"
57  return value
58 
59 
60 PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CAMERA, Platform.SENSOR]
61 DEFAULT_NAME = "OctoPrint"
62 CONF_NUMBER_OF_TOOLS = "number_of_tools"
63 CONF_BED = "bed"
64 
65 BINARY_SENSOR_TYPES = [
66  "Printing",
67  "Printing Error",
68 ]
69 
70 BINARY_SENSOR_SCHEMA = vol.Schema(
71  {
72  vol.Optional(
73  CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSOR_TYPES)
74  ): vol.All(cv.ensure_list, [vol.In(BINARY_SENSOR_TYPES)]),
75  vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
76  }
77 )
78 
79 SENSOR_TYPES = [
80  "Temperatures",
81  "Current State",
82  "Job Percentage",
83  "Time Remaining",
84  "Time Elapsed",
85 ]
86 
87 SENSOR_SCHEMA = vol.Schema(
88  {
89  vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All(
90  cv.ensure_list, [vol.In(SENSOR_TYPES)]
91  ),
92  vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
93  }
94 )
95 
96 CONFIG_SCHEMA = vol.Schema(
97  vol.All(
98  cv.deprecated(DOMAIN),
99  {
100  DOMAIN: vol.All(
101  cv.ensure_list,
102  [
103  vol.Schema(
104  {
105  vol.Required(CONF_API_KEY): cv.string,
106  vol.Required(CONF_HOST): cv.string,
107  vol.Optional(CONF_SSL, default=False): cv.boolean,
108  vol.Optional(CONF_PORT, default=80): cv.port,
109  vol.Optional(CONF_PATH, default="/"): ensure_valid_path,
110  # Following values are not longer used in the configuration
111  # of the integration and are here for historical purposes
112  vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
113  vol.Optional(
114  CONF_NUMBER_OF_TOOLS, default=0
115  ): cv.positive_int,
116  vol.Optional(CONF_BED, default=False): cv.boolean,
117  vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA,
118  vol.Optional(
119  CONF_BINARY_SENSORS, default={}
120  ): BINARY_SENSOR_SCHEMA,
121  }
122  )
123  ],
124  has_all_unique_names,
125  )
126  },
127  ),
128  extra=vol.ALLOW_EXTRA,
129 )
130 
131 SERVICE_CONNECT_SCHEMA = vol.Schema(
132  {
133  vol.Required(CONF_DEVICE_ID): cv.string,
134  vol.Optional(CONF_PROFILE_NAME): cv.string,
135  vol.Optional(CONF_PORT): cv.string,
136  vol.Optional(CONF_BAUDRATE): cv.positive_int,
137  }
138 )
139 
140 
141 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
142  """Set up the OctoPrint component."""
143  if DOMAIN not in config:
144  return True
145 
146  domain_config = config[DOMAIN]
147 
148  for conf in domain_config:
149  hass.async_create_task(
150  hass.config_entries.flow.async_init(
151  DOMAIN,
152  context={"source": SOURCE_IMPORT},
153  data={
154  CONF_API_KEY: conf[CONF_API_KEY],
155  CONF_HOST: conf[CONF_HOST],
156  CONF_PATH: conf[CONF_PATH],
157  CONF_PORT: conf[CONF_PORT],
158  CONF_SSL: conf[CONF_SSL],
159  },
160  )
161  )
162 
163  return True
164 
165 
166 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
167  """Set up OctoPrint from a config entry."""
168 
169  if DOMAIN not in hass.data:
170  hass.data[DOMAIN] = {}
171 
172  if CONF_VERIFY_SSL not in entry.data:
173  data = {**entry.data, CONF_VERIFY_SSL: True}
174  hass.config_entries.async_update_entry(entry, data=data)
175 
176  connector = aiohttp.TCPConnector(
177  force_close=True,
179  if not entry.data[CONF_VERIFY_SSL]
180  else get_default_context(),
181  )
182  session = aiohttp.ClientSession(connector=connector)
183 
184  @callback
185  def _async_close_websession(event: Event) -> None:
186  """Close websession."""
187  session.detach()
188 
189  hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_close_websession)
190 
191  client = OctoprintClient(
192  host=entry.data[CONF_HOST],
193  session=session,
194  port=entry.data[CONF_PORT],
195  ssl=entry.data[CONF_SSL],
196  path=entry.data[CONF_PATH],
197  )
198 
199  client.set_api_key(entry.data[CONF_API_KEY])
200 
201  coordinator = OctoprintDataUpdateCoordinator(hass, client, entry, 30)
202 
203  await coordinator.async_config_entry_first_refresh()
204 
205  hass.data[DOMAIN][entry.entry_id] = {
206  "coordinator": coordinator,
207  "client": client,
208  }
209 
210  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
211 
212  async def async_printer_connect(call: ServiceCall) -> None:
213  """Connect to a printer."""
214  client = async_get_client_for_service_call(hass, call)
215  await client.connect(
216  printer_profile=call.data.get(CONF_PROFILE_NAME),
217  port=call.data.get(CONF_PORT),
218  baud_rate=call.data.get(CONF_BAUDRATE),
219  )
220 
221  if not hass.services.has_service(DOMAIN, SERVICE_CONNECT):
222  hass.services.async_register(
223  DOMAIN,
224  SERVICE_CONNECT,
225  async_printer_connect,
226  schema=SERVICE_CONNECT_SCHEMA,
227  )
228 
229  return True
230 
231 
232 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
233  """Unload a config entry."""
234  unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
235 
236  if unload_ok:
237  hass.data[DOMAIN].pop(entry.entry_id)
238 
239  return unload_ok
240 
241 
243  hass: HomeAssistant, call: ServiceCall
244 ) -> OctoprintClient:
245  """Get the client related to a service call (by device ID)."""
246  device_id = call.data[CONF_DEVICE_ID]
247  device_registry = dr.async_get(hass)
248 
249  if device_entry := device_registry.async_get(device_id):
250  for entry_id in device_entry.config_entries:
251  if data := hass.data[DOMAIN].get(entry_id):
252  return cast(OctoprintClient, data["client"])
253 
255  translation_domain=DOMAIN,
256  translation_key="missing_client",
257  translation_placeholders={
258  "device_id": device_id,
259  },
260  )
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:141
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:166
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:232
OctoprintClient async_get_client_for_service_call(HomeAssistant hass, ServiceCall call)
Definition: __init__.py:244
ssl.SSLContext get_default_context()
Definition: ssl.py:118
ssl.SSLContext get_default_no_verify_context()
Definition: ssl.py:123