"""Details about printers which are connected to CUPS.""" import importlib import logging from datetime import timedelta import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) ATTR_MARKER_TYPE = 'marker_type' ATTR_MARKER_LOW_LEVEL = 'marker_low_level' ATTR_MARKER_HIGH_LEVEL = 'marker_high_level' ATTR_PRINTER_NAME = 'printer_name' ATTR_DEVICE_URI = 'device_uri' ATTR_PRINTER_INFO = 'printer_info' ATTR_PRINTER_IS_SHARED = 'printer_is_shared' ATTR_PRINTER_LOCATION = 'printer_location' ATTR_PRINTER_MODEL = 'printer_model' ATTR_PRINTER_STATE_MESSAGE = 'printer_state_message' ATTR_PRINTER_STATE_REASON = 'printer_state_reason' ATTR_PRINTER_TYPE = 'printer_type' ATTR_PRINTER_URI_SUPPORTED = 'printer_uri_supported' CONF_PRINTERS = 'printers' CONF_IS_CUPS_SERVER = 'is_cups_server' DEFAULT_HOST = '127.0.0.1' DEFAULT_PORT = 631 DEFAULT_IS_CUPS_SERVER = True ICON_PRINTER = 'mdi:printer' ICON_MARKER = 'mdi:water' SCAN_INTERVAL = timedelta(minutes=1) PRINTER_STATES = { 3: 'idle', 4: 'printing', 5: 'stopped', } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_PRINTERS): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_IS_CUPS_SERVER, default=DEFAULT_IS_CUPS_SERVER): cv.boolean, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, }) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the CUPS sensor.""" host = config.get(CONF_HOST) port = config.get(CONF_PORT) printers = config.get(CONF_PRINTERS) is_cups = config.get(CONF_IS_CUPS_SERVER) if is_cups: data = CupsData(host, port, None) data.update() if data.available is False: _LOGGER.error("Unable to connect to CUPS server: %s:%s", host, port) raise PlatformNotReady() dev = [] for printer in printers: if printer not in data.printers: _LOGGER.error("Printer is not present: %s", printer) continue dev.append(CupsSensor(data, printer)) if "marker-names" in data.attributes[printer]: for marker in data.attributes[printer]["marker-names"]: dev.append(MarkerSensor(data, printer, marker, True)) add_entities(dev, True) return data = CupsData(host, port, printers) data.update() if data.available is False: _LOGGER.error("Unable to connect to IPP printer: %s:%s", host, port) raise PlatformNotReady() dev = [] for printer in printers: dev.append(IPPSensor(data, printer)) if "marker-names" in data.attributes[printer]: for marker in data.attributes[printer]["marker-names"]: dev.append(MarkerSensor(data, printer, marker, False)) add_entities(dev, True) class CupsSensor(Entity): """Representation of a CUPS sensor.""" def __init__(self, data, printer): """Initialize the CUPS sensor.""" self.data = data self._name = printer self._printer = None self._available = False @property def name(self): """Return the name of the sensor.""" return self._name @property def state(self): """Return the state of the sensor.""" if self._printer is None: return None key = self._printer['printer-state'] return PRINTER_STATES.get(key, key) @property def available(self): """Return True if entity is available.""" return self._available @property def icon(self): """Return the icon to use in the frontend, if any.""" return ICON_PRINTER @property def device_state_attributes(self): """Return the state attributes of the sensor.""" if self._printer is None: return None return { ATTR_DEVICE_URI: self._printer['device-uri'], ATTR_PRINTER_INFO: self._printer['printer-info'], ATTR_PRINTER_IS_SHARED: self._printer['printer-is-shared'], ATTR_PRINTER_LOCATION: self._printer['printer-location'], ATTR_PRINTER_MODEL: self._printer['printer-make-and-model'], ATTR_PRINTER_STATE_MESSAGE: self._printer['printer-state-message'], ATTR_PRINTER_STATE_REASON: self._printer['printer-state-reasons'], ATTR_PRINTER_TYPE: self._printer['printer-type'], ATTR_PRINTER_URI_SUPPORTED: self._printer['printer-uri-supported'], } def update(self): """Get the latest data and updates the states.""" self.data.update() self._printer = self.data.printers.get(self._name) self._available = self.data.available class IPPSensor(Entity): """Implementation of the IPPSensor. This sensor represents the status of the printer. """ def __init__(self, data, name): """Initialize the sensor.""" self.data = data self._name = name self._attributes = None self._available = False @property def name(self): """Return the name of the sensor.""" return self._attributes['printer-make-and-model'] @property def icon(self): """Return the icon to use in the frontend.""" return ICON_PRINTER @property def available(self): """Return True if entity is available.""" return self._available @property def state(self): """Return the state of the sensor.""" if self._attributes is None: return None key = self._attributes['printer-state'] return PRINTER_STATES.get(key, key) @property def device_state_attributes(self): """Return the state attributes of the sensor.""" if self._attributes is None: return None state_attributes = {} if 'printer-info' in self._attributes: state_attributes[ATTR_PRINTER_INFO] = \ self._attributes['printer-info'] if 'printer-location' in self._attributes: state_attributes[ATTR_PRINTER_LOCATION] = \ self._attributes['printer-location'] if 'printer-state-message' in self._attributes: state_attributes[ATTR_PRINTER_STATE_MESSAGE] = \ self._attributes['printer-state-message'] if 'printer-state-reasons' in self._attributes: state_attributes[ATTR_PRINTER_STATE_REASON] = \ self._attributes['printer-state-reasons'] if 'printer-uri-supported' in self._attributes: state_attributes[ATTR_PRINTER_URI_SUPPORTED] = \ self._attributes['printer-uri-supported'] return state_attributes def update(self): """Fetch new state data for the sensor.""" self.data.update() self._attributes = self.data.attributes.get(self._name) self._available = self.data.available class MarkerSensor(Entity): """Implementation of the MarkerSensor. This sensor represents the percentage of ink or toner. """ def __init__(self, data, printer, name, is_cups): """Initialize the sensor.""" self.data = data self._name = name self._printer = printer self._index = data.attributes[printer]['marker-names'].index(name) self._is_cups = is_cups self._attributes = None @property def name(self): """Return the name of the sensor.""" return self._name @property def icon(self): """Return the icon to use in the frontend.""" return ICON_MARKER @property def state(self): """Return the state of the sensor.""" if self._attributes is None: return None return self._attributes[self._printer]['marker-levels'][self._index] @property def unit_of_measurement(self): """Return the unit of measurement.""" return "%" @property def device_state_attributes(self): """Return the state attributes of the sensor.""" if self._attributes is None: return None high_level = self._attributes[self._printer]['marker-high-levels'] if isinstance(high_level, list): high_level = high_level[self._index] low_level = self._attributes[self._printer]['marker-low-levels'] if isinstance(low_level, list): low_level = low_level[self._index] marker_types = self._attributes[self._printer]['marker-types'] if isinstance(marker_types, list): marker_types = marker_types[self._index] if self._is_cups: printer_name = self._printer else: printer_name = \ self._attributes[self._printer]['printer-make-and-model'] return { ATTR_MARKER_HIGH_LEVEL: high_level, ATTR_MARKER_LOW_LEVEL: low_level, ATTR_MARKER_TYPE: marker_types, ATTR_PRINTER_NAME: printer_name } def update(self): """Update the state of the sensor.""" # Data fetching is done by CupsSensor/IPPSensor self._attributes = self.data.attributes # pylint: disable=no-name-in-module class CupsData: """Get the latest data from CUPS and update the state.""" def __init__(self, host, port, ipp_printers): """Initialize the data object.""" self._host = host self._port = port self._ipp_printers = ipp_printers self.is_cups = (ipp_printers is None) self.printers = None self.attributes = {} self.available = False def update(self): """Get the latest data from CUPS.""" cups = importlib.import_module('cups') try: conn = cups.Connection(host=self._host, port=self._port) if self.is_cups: self.printers = conn.getPrinters() for printer in self.printers: self.attributes[printer] = conn.getPrinterAttributes( name=printer) else: for ipp_printer in self._ipp_printers: self.attributes[ipp_printer] = conn.getPrinterAttributes( uri="ipp://{}:{}/{}" .format(self._host, self._port, ipp_printer)) self.available = True except RuntimeError: self.available = False