Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Details about printers which are connected to CUPS."""
2 
3 from __future__ import annotations
4 
5 from datetime import timedelta
6 import importlib
7 import logging
8 from typing import Any
9 
10 import voluptuous as vol
11 
13  PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
14  SensorEntity,
15 )
16 from homeassistant.const import CONF_HOST, CONF_PORT, PERCENTAGE
17 from homeassistant.core import HomeAssistant
18 from homeassistant.exceptions import PlatformNotReady
20 from homeassistant.helpers.entity_platform import AddEntitiesCallback
21 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
22 
23 _LOGGER = logging.getLogger(__name__)
24 
25 ATTR_MARKER_TYPE = "marker_type"
26 ATTR_MARKER_LOW_LEVEL = "marker_low_level"
27 ATTR_MARKER_HIGH_LEVEL = "marker_high_level"
28 ATTR_PRINTER_NAME = "printer_name"
29 ATTR_DEVICE_URI = "device_uri"
30 ATTR_PRINTER_INFO = "printer_info"
31 ATTR_PRINTER_IS_SHARED = "printer_is_shared"
32 ATTR_PRINTER_LOCATION = "printer_location"
33 ATTR_PRINTER_MODEL = "printer_model"
34 ATTR_PRINTER_STATE_MESSAGE = "printer_state_message"
35 ATTR_PRINTER_STATE_REASON = "printer_state_reason"
36 ATTR_PRINTER_TYPE = "printer_type"
37 ATTR_PRINTER_URI_SUPPORTED = "printer_uri_supported"
38 
39 CONF_PRINTERS = "printers"
40 CONF_IS_CUPS_SERVER = "is_cups_server"
41 
42 DEFAULT_HOST = "127.0.0.1"
43 DEFAULT_PORT = 631
44 DEFAULT_IS_CUPS_SERVER = True
45 
46 ICON_PRINTER = "mdi:printer"
47 ICON_MARKER = "mdi:water"
48 
49 SCAN_INTERVAL = timedelta(minutes=1)
50 
51 PRINTER_STATES = {3: "idle", 4: "printing", 5: "stopped"}
52 
53 PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
54  {
55  vol.Required(CONF_PRINTERS): vol.All(cv.ensure_list, [cv.string]),
56  vol.Optional(CONF_IS_CUPS_SERVER, default=DEFAULT_IS_CUPS_SERVER): cv.boolean,
57  vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
58  vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
59  }
60 )
61 
62 
64  hass: HomeAssistant,
65  config: ConfigType,
66  add_entities: AddEntitiesCallback,
67  discovery_info: DiscoveryInfoType | None = None,
68 ) -> None:
69  """Set up the CUPS sensor."""
70  host: str = config[CONF_HOST]
71  port: int = config[CONF_PORT]
72  printers: list[str] = config[CONF_PRINTERS]
73  is_cups: bool = config[CONF_IS_CUPS_SERVER]
74 
75  if is_cups:
76  data = CupsData(host, port, None)
77  data.update()
78  if data.available is False:
79  _LOGGER.error("Unable to connect to CUPS server: %s:%s", host, port)
80  raise PlatformNotReady
81  assert data.printers is not None
82 
83  dev: list[SensorEntity] = []
84  for printer in printers:
85  if printer not in data.printers:
86  _LOGGER.error("Printer is not present: %s", printer)
87  continue
88  dev.append(CupsSensor(data, printer))
89 
90  if "marker-names" in data.attributes[printer]:
91  dev.extend(
92  MarkerSensor(data, printer, marker, True)
93  for marker in data.attributes[printer]["marker-names"]
94  )
95 
96  add_entities(dev, True)
97  return
98 
99  data = CupsData(host, port, printers)
100  data.update()
101  if data.available is False:
102  _LOGGER.error("Unable to connect to IPP printer: %s:%s", host, port)
103  raise PlatformNotReady
104 
105  dev = []
106  for printer in printers:
107  dev.append(IPPSensor(data, printer))
108 
109  if "marker-names" in data.attributes[printer]:
110  for marker in data.attributes[printer]["marker-names"]:
111  dev.append(MarkerSensor(data, printer, marker, False))
112 
113  add_entities(dev, True)
114 
115 
117  """Representation of a CUPS sensor."""
118 
119  _attr_icon = ICON_PRINTER
120 
121  def __init__(self, data: CupsData, printer_name: str) -> None:
122  """Initialize the CUPS sensor."""
123  self.datadata = data
124  self._name_name = printer_name
125  self._printer_printer: dict[str, Any] | None = None
126  self._attr_available_attr_available = False
127 
128  @property
129  def name(self) -> str:
130  """Return the name of the entity."""
131  return self._name_name
132 
133  @property
134  def native_value(self):
135  """Return the state of the sensor."""
136  if self._printer_printer is None:
137  return None
138 
139  key = self._printer_printer["printer-state"]
140  return PRINTER_STATES.get(key, key)
141 
142  @property
144  """Return the state attributes of the sensor."""
145  if self._printer_printer is None:
146  return None
147 
148  return {
149  ATTR_DEVICE_URI: self._printer_printer["device-uri"],
150  ATTR_PRINTER_INFO: self._printer_printer["printer-info"],
151  ATTR_PRINTER_IS_SHARED: self._printer_printer["printer-is-shared"],
152  ATTR_PRINTER_LOCATION: self._printer_printer["printer-location"],
153  ATTR_PRINTER_MODEL: self._printer_printer["printer-make-and-model"],
154  ATTR_PRINTER_STATE_MESSAGE: self._printer_printer["printer-state-message"],
155  ATTR_PRINTER_STATE_REASON: self._printer_printer["printer-state-reasons"],
156  ATTR_PRINTER_TYPE: self._printer_printer["printer-type"],
157  ATTR_PRINTER_URI_SUPPORTED: self._printer_printer["printer-uri-supported"],
158  }
159 
160  def update(self) -> None:
161  """Get the latest data and updates the states."""
162  self.datadata.update()
163  assert self.datadata.printers is not None
164  self._printer_printer = self.datadata.printers.get(self.namenamename)
165  self._attr_available_attr_available = self.datadata.available
166 
167 
169  """Implementation of the IPPSensor.
170 
171  This sensor represents the status of the printer.
172  """
173 
174  _attr_icon = ICON_PRINTER
175 
176  def __init__(self, data: CupsData, printer_name: str) -> None:
177  """Initialize the sensor."""
178  self.datadata = data
179  self._printer_name_printer_name = printer_name
180  self._attributes_attributes = None
181  self._attr_available_attr_available = False
182 
183  @property
184  def name(self):
185  """Return the name of the sensor."""
186  return self._attributes_attributes["printer-make-and-model"]
187 
188  @property
189  def native_value(self):
190  """Return the state of the sensor."""
191  if self._attributes_attributes is None:
192  return None
193 
194  key = self._attributes_attributes["printer-state"]
195  return PRINTER_STATES.get(key, key)
196 
197  @property
199  """Return the state attributes of the sensor."""
200  if self._attributes_attributes is None:
201  return None
202 
203  state_attributes = {}
204 
205  if "printer-info" in self._attributes_attributes:
206  state_attributes[ATTR_PRINTER_INFO] = self._attributes_attributes["printer-info"]
207 
208  if "printer-location" in self._attributes_attributes:
209  state_attributes[ATTR_PRINTER_LOCATION] = self._attributes_attributes[
210  "printer-location"
211  ]
212 
213  if "printer-state-message" in self._attributes_attributes:
214  state_attributes[ATTR_PRINTER_STATE_MESSAGE] = self._attributes_attributes[
215  "printer-state-message"
216  ]
217 
218  if "printer-state-reasons" in self._attributes_attributes:
219  state_attributes[ATTR_PRINTER_STATE_REASON] = self._attributes_attributes[
220  "printer-state-reasons"
221  ]
222 
223  if "printer-uri-supported" in self._attributes_attributes:
224  state_attributes[ATTR_PRINTER_URI_SUPPORTED] = self._attributes_attributes[
225  "printer-uri-supported"
226  ]
227 
228  return state_attributes
229 
230  def update(self) -> None:
231  """Fetch new state data for the sensor."""
232  self.datadata.update()
233  self._attributes_attributes = self.datadata.attributes.get(self._printer_name_printer_name)
234  self._attr_available_attr_available = self.datadata.available
235 
236 
238  """Implementation of the MarkerSensor.
239 
240  This sensor represents the percentage of ink or toner.
241  """
242 
243  _attr_icon = ICON_MARKER
244  _attr_native_unit_of_measurement = PERCENTAGE
245 
246  def __init__(self, data: CupsData, printer: str, name: str, is_cups: bool) -> None:
247  """Initialize the sensor."""
248  self.datadata = data
249  self._attr_name_attr_name = name
250  self._printer_printer = printer
251  self._index_index = data.attributes[printer]["marker-names"].index(name)
252  self._is_cups_is_cups = is_cups
253  self._attributes_attributes: dict[str, Any] | None = None
254 
255  @property
256  def native_value(self):
257  """Return the state of the sensor."""
258  if self._attributes_attributes is None:
259  return None
260 
261  return self._attributes_attributes[self._printer_printer]["marker-levels"][self._index_index]
262 
263  @property
265  """Return the state attributes of the sensor."""
266  if self._attributes_attributes is None:
267  return None
268 
269  high_level = self._attributes_attributes[self._printer_printer].get("marker-high-levels")
270  if isinstance(high_level, list):
271  high_level = high_level[self._index_index]
272 
273  low_level = self._attributes_attributes[self._printer_printer].get("marker-low-levels")
274  if isinstance(low_level, list):
275  low_level = low_level[self._index_index]
276 
277  marker_types = self._attributes_attributes[self._printer_printer]["marker-types"]
278  if isinstance(marker_types, list):
279  marker_types = marker_types[self._index_index]
280 
281  if self._is_cups_is_cups:
282  printer_name = self._printer_printer
283  else:
284  printer_name = self._attributes_attributes[self._printer_printer]["printer-make-and-model"]
285 
286  return {
287  ATTR_MARKER_HIGH_LEVEL: high_level,
288  ATTR_MARKER_LOW_LEVEL: low_level,
289  ATTR_MARKER_TYPE: marker_types,
290  ATTR_PRINTER_NAME: printer_name,
291  }
292 
293  def update(self) -> None:
294  """Update the state of the sensor."""
295  # Data fetching is done by CupsSensor/IPPSensor
296  self._attributes_attributes = self.datadata.attributes
297 
298 
299 class CupsData:
300  """Get the latest data from CUPS and update the state."""
301 
302  def __init__(self, host: str, port: int, ipp_printers: list[str] | None) -> None:
303  """Initialize the data object."""
304  self._host_host = host
305  self._port_port = port
306  self._ipp_printers_ipp_printers = ipp_printers
307  self.is_cupsis_cups = ipp_printers is None
308  self.printersprinters: dict[str, dict[str, Any]] | None = None
309  self.attributes: dict[str, Any] = {}
310  self.availableavailable = False
311 
312  def update(self) -> None:
313  """Get the latest data from CUPS."""
314  cups = importlib.import_module("cups")
315 
316  try:
317  conn = cups.Connection(host=self._host_host, port=self._port_port)
318  if self.is_cupsis_cups:
319  self.printersprinters = conn.getPrinters()
320  assert self.printersprinters is not None
321  for printer in self.printersprinters:
322  self.attributes[printer] = conn.getPrinterAttributes(name=printer)
323  else:
324  assert self._ipp_printers_ipp_printers is not None
325  for ipp_printer in self._ipp_printers_ipp_printers:
326  self.attributes[ipp_printer] = conn.getPrinterAttributes(
327  uri=f"ipp://{self._host}:{self._port}/{ipp_printer}"
328  )
329 
330  self.availableavailable = True
331  except RuntimeError:
332  self.availableavailable = False
None __init__(self, str host, int port, list[str]|None ipp_printers)
Definition: sensor.py:302
None __init__(self, CupsData data, str printer_name)
Definition: sensor.py:121
None __init__(self, CupsData data, str printer_name)
Definition: sensor.py:176
None __init__(self, CupsData data, str printer, str name, bool is_cups)
Definition: sensor.py:246
str|UndefinedType|None name(self)
Definition: entity.py:738
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: sensor.py:68
def add_entities(account, async_add_entities, tracked)
Definition: sensor.py:40