diff --git a/CHANGELOG.md b/CHANGELOG.md index a0f32af..f07b9bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,57 @@ # Changelog +## 2.0.0 + +- Fix "detected blocking call to open inside the event loop by custom integration" error + +## 2.0.0b10 + +- Add support for mapping of multicolor consumable (CyanMagentaYellow) +- Change the matching of consumable to its details by marker color instead of station + +## 2.0.0b9 + +- Add fallback mechanism for consumables, if station is not available, will use color mapping +- Fix hassfest failure caused by invalid enums values for translation + +## 2.0.0b8 + +- Fix async dispatcher send +- Change all sensors with date device class to timestamp [#127](https://github.com/elad-bar/ha-hpprinter/issues/127) +- Add fallback mechanism for consumables, if station is not available, will use color mapping + +## 2.0.0b7 + +- Safe code blocks (try / catch / log) for generating entities +- Fix logic of constructing device name if cartridge type is not available + +## 2.0.0b6 + +- When constructing device name, avoid null parts of it [#113](https://github.com/elad-bar/ha-hpprinter/issues/113) +- Changed the logic of errors from not found endpoints [#120](https://github.com/elad-bar/ha-hpprinter/issues/120) + - On initial load / setting up integration - one of the endpoints must return valid response, otherwise the integration will fail to load. + - After the integration loaded, it will update data periodically, + - If one of the endpoints will return 404 (not found) - the data related to it will get reset, DEBUG message will be logged (instead of ERROR) + - If printer goes offline, all data will be set as Unknown. + +## 2.0.0b5 + +- Support no prefetch mode +- Fix all translations + +## v2.0.0b3 + +- Fix entity translations +- Fix main device manufacture date + +## v2.0.0b2 + +- Fix wrong library usage for slugify, causing wrong translation key to get picked up + +## v2.0.0b1 + +- Refactor to full HP Printer EWS support + ## v1.0.12 - Fix missing references [Issue #103](https://github.com/elad-bar/ha-hpprinter/issues/103) diff --git a/README.md b/README.md index 0a4063c..ad2e188 100644 --- a/README.md +++ b/README.md @@ -6,104 +6,199 @@ Configuration support multiple HP Printer devices through Configuration -> Integ [Changelog](https://github.com/elad-bar/ha-hpprinter/blob/master/CHANGELOG.md) -### How to set it up: +## How to -Look for "HP Printers Integration" and install +### Requirements -#### Requirements +- HP Printer with EWS (Embedded Web Server) support -- HP Printer supporting XML API - to check printer's compatibility to the component try to get to the printer's XML API (replace placeholder with real IP / Hostname): - `http://{IP}/DevMgmt/ProductStatusDyn.xml` +### Installations via HACS [![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg)](https://github.com/hacs/integration) -#### Basic configuration +- In HACS, look for "HP Printer" and install and restart +- If integration was not found, please add custom repository `elad-bar/hpprinter` as integration +- In Settings --> Devices & Services - (Lower Right) "Add Integration" -- Configuration should be done via Configuration -> Integrations. -- In case you are already using that integration with YAML Configuration - please remove it -- Integration supports **multiple** devices -- In the setup form, the following details are mandatory: - - Name - Unique - - Host (or IP) -- Upon submitting the form of creating an integration, a request to the printer will take place and will cause failure in case: - - Unsupported API - - Invalid server details - when cannot reach host +### Setup -#### Settings for Monitoring interfaces, devices, tracked devices and update interval +To add integration use Configuration -> Integrations -> Add `HP Printer` +Integration supports **multiple** accounts and devices -_Configuration -> Integrations -> {Integration} -> Options_
+| Fields name | Type | Required | Default | Description | +| ----------- | ------- | -------- | ------- | -------------------------------------------- | +| Host | Textbox | + | - | Defines hostname or IP of the HP Printer EWS | +| Port | Textbox | + | 80 | Defines port of the HP Printer EWS | +| Is SSL | Boolean | + | False | Defines which protocol to use HTTP/S | -``` -Name - Unique -Host (or IP) -Update Interval: Textbox, number of seconds to update entities, default=60 -Log level: Drop-down list, change component's log level (more details below), default=Default -Should store responses?: Check-box, saves XML and JSON files for debugging purpose, default=False -``` +It is also possible to change configuration after setting up using integration configuration. -###### Log Level's drop-down +#### Validation errors -New feature to set the log level for the component without need to set log_level in `customization:` and restart or call manually `logger.set_level` and loose it after restart. +| Errors | +| ------------------------------------------------------ | +| Invalid parameters provided | +| HP Printer Embedded Web Server (EWS) not was not found | -Upon startup or integration's option update, based on the value chosen, the component will make a service call to `logger.set_level` for that component with the desired value, +## Devices -In case `Default` option is chosen, flow will skip calling the service, after changing from any other option to `Default`, it will not take place automatically, only after restart +Will extract data of the relevant devices, devices that are not available will be ignored. -###### Store responses +### Main device -Stores the XML and JSON of each request and final JSON to files, Path in CONFIG_PATH/\*, -Files that will be generated (Prefix to the file is name of the integration): +Device that holds entities related to the integration and relations to other sub devices as described below. -- ProductUsageDyn.XML - Raw XML from HP Printer of Usage Details -- ProductUsageDyn.json - JSON based on the Raw XML of Usage Details after transformed by the component -- ConsumableConfigDyn.XML - Raw XML from HP Printer of consumable details -- ConsumableConfigDyn.json - JSON based on the Raw XML of consumable details after transformed by the component -- ProductConfigDyn.XML - Raw XML from HP Printer of Config Details -- ProductConfigDyn.json - JSON based on the Raw XML of Config Details after transformed by the component -- Final.json - JSON based on the 2 JSONs above, merged into simpler data structure for the HA to create sensor based on +_Binary Sensor_ -## Components: +- ePrint Registered +- ePrint Status -#### Device status - Binary Sensor +_Sensor_ -``` -State: connected? -``` +- Manufacture Date -#### Printer details - Sensor +### Printer -``` -State: # of pages printed -Attributes: - Color - # of printed documents using color cartridges - Monochrome - # of printed documents using black cartridges - Jams - # of print jobs jammed - Cancelled - # of print jobs that were cancelled -``` +Device holds entities of sensors related to number of pages printed and relation to sub devices of consumables -#### Scanner details - Sensor (For AIO only) +_Sensor_ +- Total pages printed +- Total black-and-white pages printed +- Total color pages printed +- Total single-sided pages printed +- Total double-sided pages printed +- Total jams +- Total miss picks + +### Scanner + +Device holds entities of sensors related to number of pages scanned + +_Sensor_ + +- Total scanned pages +- Total scanned pages from ADF +- Total double-sided pages scanned +- Total pages from scanner glass +- Total jams +- Total miss picks + +### Copy + +Device holds entities of sensors related to number of pages copied + +_Sensor_ + +- Total copies +- Total copies from ADF +- Total pages from scanner glass +- Total black-and-white copies +- Total color copies + +### Fax + +Device holds entities of sensors related to number of pages faxed + +_Sensor_ + +- Total faxed + +### Consumable + +Devices (device per consumable) holds entities related to consumable (Ink, Toner, Printhead) of a printer device + +_Binary Sensor_ + +- Status + +_Sensor_ + +- Station +- Type +- Installation Date +- Level (will not be available for Printhead) +- Expiration Date (will not be available for Printhead) +- Remaining (will not be available for Printhead) +- Counterfeit Refilled +- Genuine Refilled +- Manufacture Date + +## Troubleshooting + +Before opening an issue, please provide logs and diagnostic file data related to the issue. + +### Logs + +For debug log level, please add the following to your config.yaml + +```yaml +logger: + default: warning + logs: + custom_components.hpprinter: debug ``` -State: # of pages scanned -Attributes: - ADF - # of scanned documents from the ADF - Duplex - # of scanned documents from the ADF using duplex mode - Flatbed - # of scanned documents from the flatbed - Jams - # of scanned jammed - Mispick - # of scanned documents failed to take the document from the feeder -``` -#### Cartridges details - Sensor (Per cartridge) +Or use the HA capability in device page: + +1. Settings +2. Devices & Services +3. HP Printer +4. 3 dots menu +5. Enable debug logging + +When done and would like to extract the log, repeat steps, in step #5 - Disable debug logging + +### Diagnostic details +Please attach also diagnostic details of the integration, available in: + +1. Settings +2. Devices & Services +3. HP Printer +4. 3 dots menu +5. Download diagnostics + +Diagnostic file contains 3 section related to data extracted from the device: + +- data.debug.rawData - Raw data extracted from all endpoints of the device, from that source you can extract ideas for additional entities to suggest +- data.debug.devicesConfig - Configuration of mapping to convert data from HP Printer EWS to HA devices and entities, that will be the section that new entities will be added +- data.debug.devicesData - Data extracted for HA entities, just relevant data points, according to mapped objects available in section `data.debug.devicesConfig` + +## Translations + +Integration translated from English to: + +- German +- Danish +- Spanish +- French +- Dutch +- Norwegian +- Polish +- Portuguese + +Translation is being auto-generated from Google Translate using `utils/generate_translations.py` script, + +```json +{ + "en": "en", + "de": "de", + "dk": "da", + "es": "es", + "fr": "fr", + "nb": "no", + "nl": "nl", + "pl": "pl", + "pt-BR": "pt" +} ``` -State: Remaining level % -Attributes: - Color - Type - Ink / Toner / Print head - Station - Position of the cartridge - Product Number - Serial Number - Manufactured By - Manufactured At - Warranty Expiration Date - Installed At + +If you would like to add new translation language, please add to the `DESTINATION_LANGUAGES` constant the relevant language, +format is: + +```json +{ + "HA language": "Google Translate language" +} ``` + +Script is translating only, new missing values, it will not override translated values. diff --git a/__main__.py b/__main__.py deleted file mode 100644 index b1281e9..0000000 --- a/__main__.py +++ /dev/null @@ -1,82 +0,0 @@ -from custom_components.hpprinter.managers.HPDeviceData import * -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_NAME -from homeassistant.core import HomeAssistant - -logging.basicConfig(level=logging.DEBUG, filename="myapp.log", filemode="w") - -_LOGGER = logging.getLogger(__name__) - - -class Test: - def __init__(self): - _LOGGER.info("Started") - - self._data = None - self._config_manager = ConfigManager() - - data = {CONF_HOST: "", CONF_NAME: DEFAULT_NAME} - - config_entry: ConfigEntry = ConfigEntry( - version=0, - minor_version=0, - domain=DOMAIN, - title=DEFAULT_NAME, - data=data, - source="", - connection_class="", - system_options={}, - ) - print("1.1") - self._config_manager.update(config_entry) - - print("1.2") - - self._config_manager.data.file_reader = self.file_data_provider - - print("1.3") - - hass = HomeAssistant() - - print("1.4") - - self._device_data = HPDeviceData(hass, self._config_manager) - - async def async_parse(self): - print("2.1") - - await self._device_data.update() - - print("2.2") - - json_data = json.dumps(self._device_data.device_data) - _LOGGER.debug(json_data) - - print("2.3") - - await self.terminate() - - async def terminate(self): - print("4.1") - - await self._device_data.terminate() - - print("4.2") - - @staticmethod - def file_data_provider(data_type): - print(f"3.{data_type}") - - with open(f"samples/ink/{data_type}.json") as json_file: - data = json.load(json_file) - - return data - - -if __name__ == "__main__": - # execute only if run as the entry point into the program - - t = Test() - hass = HomeAssistant() - - hass.loop.run_until_complete(t.async_parse()) diff --git a/custom_components/hpprinter/__init__.py b/custom_components/hpprinter/__init__.py index 7c6730a..e8f66e4 100644 --- a/custom_components/hpprinter/__init__.py +++ b/custom_components/hpprinter/__init__.py @@ -1,20 +1,13 @@ -""" -This component provides support for HP Printers. -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/hpprinter/ -""" -from custom_components.hpprinter.helpers import ( - async_set_ha, - clear_ha, - get_ha, - handle_log_level, -) +import logging +import sys + from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME +from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant -from .helpers.const import * -from .managers.HPDeviceData import * +from .common.consts import DEFAULT_NAME, DOMAIN +from .managers.ha_config_manager import HAConfigManager +from .managers.ha_coordinator import HACoordinator _LOGGER = logging.getLogger(__name__) @@ -24,50 +17,62 @@ async def async_setup(_hass, _config): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up a HP Printer component.""" + """Set up a Shinobi Video component.""" initialized = False try: - await handle_log_level(hass, entry) + entry_config = {key: entry.data[key] for key in entry.data} + + config_manager = HAConfigManager(hass, entry) + await config_manager.initialize(entry_config) + + is_initialized = config_manager.is_initialized + + if is_initialized: + coordinator = HACoordinator(hass, config_manager) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + if hass.is_running: + await coordinator.initialize() - _LOGGER.debug(f"Starting async_setup_entry of {DOMAIN}") - entry.add_update_listener(async_options_updated) - name = entry.data.get(CONF_NAME) + else: + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, coordinator.on_home_assistant_start + ) - await async_set_ha(hass, name, entry) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, coordinator.on_home_assistant_stop + ) - initialized = True + _LOGGER.info("Finished loading integration") + + initialized = is_initialized except Exception as ex: exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno - _LOGGER.error(f"Failed to load HP Printer, error: {ex}, line: {line_number}") + _LOGGER.error( + f"Failed to load {DEFAULT_NAME}, error: {ex}, line: {line_number}" + ) return initialized async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - name = entry.data.get(CONF_NAME) - ha = get_ha(hass, name) - - if ha is not None: - await ha.async_remove() + _LOGGER.info(f"Unloading {DOMAIN} integration, Entry ID: {entry.entry_id}") - clear_ha(hass, name) + coordinator: HACoordinator = hass.data[DOMAIN][entry.entry_id] - return True + await coordinator.config_manager.remove(entry.entry_id) + platforms = coordinator.config_manager.platforms -async def async_options_updated(hass: HomeAssistant, entry: ConfigEntry): - """Triggered by config entry options updates.""" - await handle_log_level(hass, entry) + for platform in platforms: + await hass.config_entries.async_forward_entry_unload(entry, platform) - _LOGGER.info(f"async_options_updated, Entry: {entry.as_dict()} ") + del hass.data[DOMAIN][entry.entry_id] - name = entry.data.get(CONF_NAME) - ha = get_ha(hass, name) - - if ha is not None: - await ha.async_update_entry(entry) + return True diff --git a/custom_components/hpprinter/api/HPPrinterAPI.py b/custom_components/hpprinter/api/HPPrinterAPI.py deleted file mode 100644 index 460da0e..0000000 --- a/custom_components/hpprinter/api/HPPrinterAPI.py +++ /dev/null @@ -1,286 +0,0 @@ -from asyncio import sleep -import json -import logging -import sys -from typing import Optional - -import aiohttp -import xmltodict - -from homeassistant.helpers.aiohttp_client import async_create_clientsession - -from . import LoginError -from ..helpers.const import * -from ..managers.configuration_manager import ConfigManager -from ..models.config_data import ConfigData - -_LOGGER = logging.getLogger(__name__) - - -class HPPrinterAPI: - def __init__(self, hass, config_manager: ConfigManager, data_type=None): - self._config_manager = config_manager - - self._hass = hass - self._data_type = data_type - self._data = None - self._session = None - - self.initialize() - - @property - def data(self): - return self._data - - @property - def config_data(self) -> Optional[ConfigData]: - if self._config_manager is not None: - return self._config_manager.data - - return None - - @property - def url(self): - config_data = self.config_data - - url = f"{config_data.protocol}://{config_data.host}:{config_data.port}/DevMgmt/{self._data_type}.xml" - - return url - - def initialize(self): - try: - self._session = async_create_clientsession( - hass=self._hass, auto_cleanup=True - ) - - except Exception as ex: - exc_type, exc_obj, tb = sys.exc_info() - line_number = tb.tb_lineno - - _LOGGER.error( - f"Failed to initialize Printer API, error: {ex}, line: {line_number}" - ) - - async def terminate(self): - try: - if self._session is not None and not self._session.closed: - await self._session.close() - - await sleep(3) - - self._session = None - - except Exception as ex: - exc_type, exc_obj, tb = sys.exc_info() - line_number = tb.tb_lineno - - _LOGGER.error( - f"Failed to terminate Printer API, error: {ex}, line: {line_number}" - ) - - async def get_data(self): - try: - self._data = None - - _LOGGER.debug(f"Updating {self._data_type} from {self.config_data.host}") - - file_reader = self.config_data.file_reader - - if file_reader is None: - printer_data = await self.async_get() - else: - printer_data = file_reader(self._data_type) - - result = {} - - if printer_data is not None: - for root_key in printer_data: - root_item = printer_data[root_key] - - item = self.extract_data(root_item, root_key) - - if item is not None: - result[root_key] = item - - self._data = result - - json_data = json.dumps(self._data) - - self.save_file("json", json_data) - - except Exception as ex: - exc_type, exc_obj, tb = sys.exc_info() - line_number = tb.tb_lineno - - _LOGGER.error( - f"Failed to update data ({self._data_type}) and parse it, Error: {ex}, Line: {line_number}" - ) - - return self._data - - def save_file(self, extension, content, file_name: Optional[str] = None): - if self.config_data.should_store: - if file_name is None: - file_name = self._data_type - - with open(f"{self.config_data.name}-{file_name}.{extension}", "w") as file: - file.write(content) - - async def async_get(self, throw_exception: bool = False): - result = None - status_code = 400 - - try: - _LOGGER.debug(f"Retrieving {self._data_type} from {self.config_data.host}") - - async with self._session.get( - self.url, ssl=False, timeout=aiohttp.ClientTimeout(total=10) - ) as response: - status_code = response.status - response.raise_for_status() - - content = await response.text() - - self.save_file("xml", content) - - for ns in NAMESPACES_TO_REMOVE: - content = content.replace(f"{ns}:", "") - - json_data = xmltodict.parse(content) - - result = json_data - - except Exception as ex: - exc_type, exc_obj, tb = sys.exc_info() - line_number = tb.tb_lineno - - _LOGGER.info( - f"Cannot retrieve data ({self._data_type}) from printer, Error: {ex}, Line: {line_number}" - ) - - if throw_exception and status_code > 399: - raise LoginError(status_code) - - return result - - def extract_data(self, data_item, data_item_key): - try: - ignore = data_item_key in IGNORE_ITEMS - is_default_array = data_item_key in ARRAY_AS_DEFAULT - - if ignore: - return None - - elif isinstance(data_item, dict): - return self.extract_ordered_dictionary(data_item, data_item_key) - - elif isinstance(data_item, list) and not is_default_array: - return self.extract_array(data_item, data_item_key) - - else: - return data_item - except Exception as ex: - exc_type, exc_obj, tb = sys.exc_info() - line_number = tb.tb_lineno - - _LOGGER.error( - f"Failed to extract {data_item_key} of {data_item}, Error: {ex}, Line: {line_number}" - ) - - def extract_ordered_dictionary(self, data_item, item_key): - try: - result = {} - - for data_item_key in data_item: - next_item = data_item[data_item_key] - - item = self.extract_data(next_item, data_item_key) - - if item is not None: - result[data_item_key] = item - - return result - except Exception as ex: - exc_type, exc_obj, tb = sys.exc_info() - line_number = tb.tb_lineno - error_details = f"Error: {ex}, Line: {line_number}" - - _LOGGER.error( - f"Failed to extract from dictionary {item_key} of {data_item}, {error_details}" - ) - - def extract_array(self, data_item, item_key): - try: - result = {} - keys = ARRAY_KEYS.get(item_key, []) - index = 0 - - for current_item in data_item: - next_item_key = item_key - item = {} - for key in current_item: - next_item = current_item[key] - - item_data = self.extract_data(next_item, key) - - if item_data is not None: - item[key] = item_data - - if key in keys: - next_item_key = f"{next_item_key}_{item[key]}" - - if len(keys) == 0: - next_item_key = f"{next_item_key}_{index}" - - result[next_item_key] = item - - index += 1 - - return result - except Exception as ex: - exc_type, exc_obj, tb = sys.exc_info() - line_number = tb.tb_lineno - - _LOGGER.error( - f"Failed to extract from array {item_key} of {data_item}, Error: {ex}, Line: {line_number}" - ) - - @staticmethod - def clean_parameter(data_item, data_key, default_value="N/A"): - result = data_item.get(data_key, {}) - - if not isinstance(result, str): - result = result.get("#text", 0) - - if not isinstance(result, str): - result = default_value - - return result - - -class ConsumableConfigDynPrinterDataAPI(HPPrinterAPI): - def __init__(self, hass, config_manager: ConfigManager): - data_type = "ConsumableConfigDyn" - - super().__init__(hass, config_manager, data_type) - - -class ProductUsageDynPrinterDataAPI(HPPrinterAPI): - def __init__(self, hass, config_manager: ConfigManager): - data_type = "ProductUsageDyn" - - super().__init__(hass, config_manager, data_type) - - -class ProductStatusDynDataAPI(HPPrinterAPI): - def __init__(self, hass, config_manager: ConfigManager): - data_type = "ProductStatusDyn" - - super().__init__(hass, config_manager, data_type) - - -class ProductConfigDynDataAPI(HPPrinterAPI): - def __init__(self, hass, config_manager: ConfigManager): - data_type = "ProductConfigDyn" - - super().__init__(hass, config_manager, data_type) diff --git a/custom_components/hpprinter/api/__init__.py b/custom_components/hpprinter/api/__init__.py deleted file mode 100644 index d46a402..0000000 --- a/custom_components/hpprinter/api/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -class LoginError(Exception): - def __init__(self, status_code): - self._status_code = status_code - - @property - def status_code(self): - return self._status_code diff --git a/custom_components/hpprinter/binary_sensor.py b/custom_components/hpprinter/binary_sensor.py index d2f0c4d..40678cb 100644 --- a/custom_components/hpprinter/binary_sensor.py +++ b/custom_components/hpprinter/binary_sensor.py @@ -1,68 +1,54 @@ -""" -Support for HP Printer binary sensors. -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.hp_printer/ -""" -from __future__ import annotations - import logging -from homeassistant.components.binary_sensor import ( - BinarySensorDeviceClass, - BinarySensorEntity, -) +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_STATE, Platform from homeassistant.core import HomeAssistant -from .helpers.const import * -from .models.base_entity import HPPrinterEntity, async_setup_base_entry -from .models.entity_data import EntityData +from .common.base_entity import BaseEntity, async_setup_base_entry +from .common.entity_descriptions import IntegrationBinarySensorEntityDescription +from .managers.ha_coordinator import HACoordinator _LOGGER = logging.getLogger(__name__) -CURRENT_DOMAIN = DOMAIN_BINARY_SENSOR - - -def get_binary_sensor(hass: HomeAssistant, integration_name: str, entity: EntityData): - binary_sensor = HPPrinterBinarySensor() - binary_sensor.initialize(hass, integration_name, entity, CURRENT_DOMAIN) - - return binary_sensor - - -async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities): - """Set up HP Printer based off an entry.""" +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +): await async_setup_base_entry( - hass, entry, async_add_entities, CURRENT_DOMAIN, get_binary_sensor + hass, + entry, + Platform.BINARY_SENSOR, + HABinarySensorEntity, + async_add_entities, ) -async def async_unload_entry(_hass, config_entry): - _LOGGER.info(f"async_unload_entry {CURRENT_DOMAIN}: {config_entry}") - - return True +class HABinarySensorEntity(BaseEntity, BinarySensorEntity): + """Representation of a sensor.""" + def __init__( + self, + entity_description: IntegrationBinarySensorEntityDescription, + coordinator: HACoordinator, + device_key: str, + ): + super().__init__(entity_description, coordinator, device_key) -class HPPrinterBinarySensor(BinarySensorEntity, HPPrinterEntity): - """Representation a binary sensor that is updated by HP Printer.""" + self._attr_device_class = entity_description.device_class + self._entity_on_values = entity_description.on_values - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return bool(self.entity.state) + self._set_value() - @property - def device_class(self) -> BinarySensorDeviceClass | str | None: - """Return the class of this sensor.""" - return self.entity.binary_sensor_device_class + def _set_value(self): + state = self.get_value() - async def async_added_to_hass_local(self): - _LOGGER.info(f"Added new {self.name}") + is_on = str(state).lower() in self._entity_on_values - def _immediate_update(self, previous_state: bool): - if previous_state != self.entity.state: - _LOGGER.debug( - f"{self.name} updated from {previous_state} to {self.entity.state}" - ) + self._attr_is_on = is_on + self._attr_extra_state_attributes = {ATTR_STATE: state} - super()._immediate_update(previous_state) + def _handle_coordinator_update(self) -> None: + """Fetch new state parameters for the sensor.""" + self._set_value() + super()._handle_coordinator_update() diff --git a/custom_components/hpprinter/common/__init__.py b/custom_components/hpprinter/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_components/hpprinter/common/base_entity.py b/custom_components/hpprinter/common/base_entity.py new file mode 100644 index 0000000..e9408e2 --- /dev/null +++ b/custom_components/hpprinter/common/base_entity.py @@ -0,0 +1,146 @@ +import logging +import sys + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util import slugify + +from ..managers.ha_coordinator import HACoordinator +from .consts import DOMAIN, SIGNAL_HA_DEVICE_CREATED +from .entity_descriptions import IntegrationEntityDescription + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_base_entry( + hass: HomeAssistant, + entry: ConfigEntry, + platform: Platform, + entity_type: type, + async_add_entities, +): + @callback + def _async_handle_device( + entry_id: str, device_key: str, device_data: dict, device_config: dict + ): + if entry.entry_id != entry_id: + return + + coordinator: HACoordinator = hass.data[DOMAIN][entry.entry_id] + + _async_handle_device_created( + coordinator, + platform, + entity_type, + async_add_entities, + device_key, + device_data, + device_config, + ) + + entry.async_on_unload( + async_dispatcher_connect(hass, SIGNAL_HA_DEVICE_CREATED, _async_handle_device) + ) + + +def _async_handle_device_created( + coordinator: HACoordinator, + platform: Platform, + entity_type: type, + async_add_entities, + device_key: str, + device_data: dict, + device_config: dict, +): + entities = [] + + device_type = device_config.get("device_type") + + entity_descriptions: list[ + IntegrationEntityDescription + ] = coordinator.config_manager.get_entity_descriptions( + platform, device_type, device_data + ) + + for entity_description in entity_descriptions: + try: + entity = entity_type(entity_description, coordinator, device_key) + + entities.append(entity) + + except Exception as ex: + exc_type, exc_obj, tb = sys.exc_info() + line_number = tb.tb_lineno + + _LOGGER.error( + f"Failed to initialize {platform}.{entity_description.key}, " + f"Device Type: {device_type}, " + f"Error: {ex}, Line: {line_number}" + ) + + entity_keys = [entity.unique_id for entity in entities] + + entity_keys_str = ", ".join(entity_keys) + + _LOGGER.debug( + f"Setting up {platform} {len(entities)} entities, Keys: {entity_keys_str}" + ) + + if entities: + async_add_entities(entities, True) + + +class BaseEntity(CoordinatorEntity): + _translations: dict + + def __init__( + self, + entity_description: IntegrationEntityDescription, + coordinator: HACoordinator, + device_key: str, + ): + super().__init__(coordinator) + + self.entity_description = entity_description + + self._device_key = device_key + self._device_type = entity_description.device_type + + device_info = coordinator.get_device(device_key) + + entity_name = coordinator.config_manager.get_entity_name( + entity_description, device_info + ) + + unique_id_parts = [ + DOMAIN, + device_key, + entity_description.platform, + entity_description.key, + ] + + unique_id = slugify("_".join(unique_id_parts)) + + self._attr_device_info = device_info + self._attr_name = entity_name + self._attr_unique_id = unique_id + self._attr_icon = entity_description.icon + + @property + def local_coordinator(self) -> HACoordinator: + return self.coordinator + + def get_data(self) -> dict: + data = self.local_coordinator.get_device_data(self._device_key) + + return data + + def get_value(self) -> str: + data = self.local_coordinator.get_device_value( + self._device_key, self.entity_description.key + ) + + return data diff --git a/custom_components/hpprinter/common/consts.py b/custom_components/hpprinter/common/consts.py new file mode 100644 index 0000000..d9c31d8 --- /dev/null +++ b/custom_components/hpprinter/common/consts.py @@ -0,0 +1,48 @@ +from datetime import timedelta + +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL + +MANUFACTURER = "HP" +DEFAULT_NAME = "HP Printer" +DOMAIN = "hpprinter" +DATA_HP_PRINTER = f"data_{DOMAIN}" + +INK_ICON = "mdi:cup-water" +PAGES_ICON = "mdi:book-open-page-variant" +SCANNER_ICON = "mdi:scanner" + +PROTOCOLS = {True: "https", False: "http"} + +NOT_AVAILABLE = "N/A" + +PRINTER_STATUS = { + "ready": "On", + "scanProcessing": "Scanning", + "copying": "Copying", + "processing": "Printing", + "cancelJob": "Cancelling Job", + "inPowerSave": "Idle", + "": "Off", +} + +IGNORED_KEYS = ["@schemaLocation", "Version"] + +SIGNAL_HA_DEVICE_CREATED = f"signal_{DOMAIN}_device_created" +SIGNAL_HA_DEVICE_DISCOVERED = f"signal_{DOMAIN}_device_discovered" +CONFIGURATION_FILE = f"{DOMAIN}.config.json" +LEGACY_KEY_FILE = f"{DOMAIN}.key" + +UPDATE_API_INTERVAL = timedelta(minutes=5) + +DEFAULT_ENTRY_ID = "config" +CONF_UPDATE_INTERVAL = "update_interval" +CONF_TITLE = "title" + +DEFAULT_PORT = 80 + +DATA_KEYS = [CONF_HOST, CONF_PORT, CONF_SSL] + +UNIT_OF_MEASUREMENT_PAGES = "pages" +UNIT_OF_MEASUREMENT_REFILLS = "refills" + +NUMERIC_UNITS_OF_MEASUREMENT = [UNIT_OF_MEASUREMENT_PAGES, UNIT_OF_MEASUREMENT_REFILLS] diff --git a/custom_components/hpprinter/common/entity_descriptions.py b/custom_components/hpprinter/common/entity_descriptions.py new file mode 100644 index 0000000..01b9fdb --- /dev/null +++ b/custom_components/hpprinter/common/entity_descriptions.py @@ -0,0 +1,28 @@ +from dataclasses import dataclass + +from homeassistant.components.binary_sensor import BinarySensorEntityDescription +from homeassistant.components.sensor import SensorEntityDescription +from homeassistant.const import Platform +from homeassistant.helpers.entity import EntityDescription + + +@dataclass(frozen=True, kw_only=True) +class IntegrationEntityDescription(EntityDescription): + platform: Platform | None = None + device_type: str | None = None + exclude: dict | None = None + + +@dataclass(frozen=True, kw_only=True) +class IntegrationBinarySensorEntityDescription( + BinarySensorEntityDescription, IntegrationEntityDescription +): + platform: Platform | None = Platform.BINARY_SENSOR + on_values: list[str] | None = None + + +@dataclass(frozen=True, kw_only=True) +class IntegrationSensorEntityDescription( + SensorEntityDescription, IntegrationEntityDescription +): + platform: Platform | None = Platform.SENSOR diff --git a/custom_components/hpprinter/common/parameter_type.py b/custom_components/hpprinter/common/parameter_type.py new file mode 100644 index 0000000..7e28ede --- /dev/null +++ b/custom_components/hpprinter/common/parameter_type.py @@ -0,0 +1,6 @@ +from enum import StrEnum + + +class ParameterType(StrEnum): + DATA_POINTS = "data_points" + ENDPOINT_VALIDATIONS = "endpoint_validations" diff --git a/custom_components/hpprinter/config_flow.py b/custom_components/hpprinter/config_flow.py index d1badfc..07d7c21 100644 --- a/custom_components/hpprinter/config_flow.py +++ b/custom_components/hpprinter/config_flow.py @@ -1,22 +1,21 @@ -"""Config flow to configure HPPrinter.""" +"""Config flow to configure.""" +from __future__ import annotations + import logging from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME from homeassistant.core import callback -from .helpers import get_ha -from .helpers.const import * -from .managers.config_flow_manager import ConfigFlowManager -from .models import AlreadyExistsError, LoginError +from .common.consts import DOMAIN +from .managers.flow_manager import IntegrationFlowManager _LOGGER = logging.getLogger(__name__) @config_entries.HANDLERS.register(DOMAIN) -class HPPrinterFlowHandler(config_entries.ConfigFlow): - """Handle a HPPrinter config flow.""" +class DomainFlowHandler(config_entries.ConfigFlow): + """Handle a domain config flow.""" VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL @@ -24,105 +23,32 @@ class HPPrinterFlowHandler(config_entries.ConfigFlow): def __init__(self): super().__init__() - self._config_flow = ConfigFlowManager() - @staticmethod @callback def async_get_options_flow(config_entry): """Get the options flow for this handler.""" - return HPPrinterOptionsFlowHandler(config_entry) + return DomainOptionsFlowHandler(config_entry) async def async_step_user(self, user_input=None): """Handle a flow start.""" - _LOGGER.debug(f"Starting async_step_user of {DOMAIN}") - - errors = None - - self._config_flow.initialize(self.hass) - - if user_input is not None: - self._config_flow.update_data(user_input, True) - - name = self._config_flow.config_data.name - - ha = get_ha(self.hass, name) - - if ha is None: - errors = await self._config_flow.valid_login() - else: - _LOGGER.warning(f"{DEFAULT_NAME} ({name}) already configured") - - return self.async_abort( - reason="already_configured", description_placeholders=user_input - ) - - if errors is None: - _LOGGER.info(f"Storing configuration data: {user_input}") - - return self.async_create_entry(title=name, data=user_input) - - data_schema = self._config_flow.get_default_data() + flow_manager = IntegrationFlowManager(self.hass, self) - return self.async_show_form( - step_id="user", data_schema=data_schema, errors=errors - ) + return await flow_manager.async_step(user_input) - async def async_step_import(self, info): - """Import existing configuration from Z-Wave.""" - _LOGGER.debug(f"Starting async_step_import of {DOMAIN}") - return self.async_create_entry( - title="HPPrinter (import from configuration.yaml)", - data=info, - ) +class DomainOptionsFlowHandler(config_entries.OptionsFlow): + """Handle domain options.""" - -class HPPrinterOptionsFlowHandler(config_entries.OptionsFlow): - """Handle HP Printer options.""" + _config_entry: ConfigEntry def __init__(self, config_entry: ConfigEntry): - """Initialize HP Printer options flow.""" + """Initialize domain options flow.""" super().__init__() - self._config_flow = ConfigFlowManager(config_entry) + self._config_entry = config_entry async def async_step_init(self, user_input=None): - """Manage the HP Printer options.""" - return await self.async_step_hp_printer_additional_settings(user_input) - - async def async_step_hp_printer_additional_settings(self, user_input=None): - errors = None - - self._config_flow.initialize(self.hass) - - if user_input is not None: - new_user_input = None - - try: - new_user_input = await self._config_flow.update_options( - user_input, True - ) - - except LoginError as lex: - _LOGGER.warning("Cannot complete login") - - errors = lex.errors - - except AlreadyExistsError as aeex: - new_name = aeex.entry.data.get(CONF_NAME) - - _LOGGER.warning(f"Cannot update host to: {new_name}") - - errors = {"base": "already_configured"} - - if errors is None: - return self.async_create_entry(title="", data=new_user_input) - - data_schema = self._config_flow.get_default_options() + """Manage the domain options.""" + flow_manager = IntegrationFlowManager(self.hass, self, self._config_entry) - return self.async_show_form( - step_id="hp_printer_additional_settings", - data_schema=data_schema, - errors=errors, - description_placeholders=self._config_flow.data, - ) + return await flow_manager.async_step(user_input) diff --git a/custom_components/hpprinter/diagnostics.py b/custom_components/hpprinter/diagnostics.py new file mode 100644 index 0000000..dc9e0e2 --- /dev/null +++ b/custom_components/hpprinter/diagnostics.py @@ -0,0 +1,103 @@ +"""Diagnostics support for Tuya.""" +from __future__ import annotations + +import logging +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.device_registry import DeviceEntry + +from .common.consts import DOMAIN +from .managers.ha_coordinator import HACoordinator + +_LOGGER = logging.getLogger(__name__) + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + _LOGGER.debug("Starting diagnostic tool") + + return await _async_get_diagnostics(hass, entry) + + +async def async_get_device_diagnostics( + hass: HomeAssistant, entry: ConfigEntry, device: DeviceEntry +) -> dict[str, Any]: + """Return diagnostics for a device entry.""" + return await _async_get_diagnostics(hass, entry, device) + + +async def _async_get_diagnostics( + hass: HomeAssistant, + entry: ConfigEntry, + _device: DeviceEntry | None = None, +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + _LOGGER.debug("Getting diagnostic information") + + coordinator: HACoordinator = hass.data[DOMAIN][entry.entry_id] + + data = { + "disabled_by": entry.disabled_by, + "disabled_polling": entry.pref_disable_polling, + "debug": await coordinator.get_debug_data(), + } + + return data + + +@callback +def _async_device_as_dict( + hass: HomeAssistant, identifiers, additional_data: dict +) -> dict[str, Any]: + """Represent a Shinobi monitor as a dictionary.""" + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + + ha_device = device_registry.async_get_device(identifiers=identifiers) + data = {} + + if ha_device: + data["device"] = { + "name": ha_device.name, + "name_by_user": ha_device.name_by_user, + "disabled": ha_device.disabled, + "disabled_by": ha_device.disabled_by, + "parameters": additional_data, + "entities": [], + } + + ha_entities = er.async_entries_for_device( + entity_registry, + device_id=ha_device.id, + include_disabled_entities=True, + ) + + for entity_entry in ha_entities: + state = hass.states.get(entity_entry.entity_id) + state_dict = None + if state: + state_dict = dict(state.as_dict()) + + # The context doesn't provide useful information in this case. + state_dict.pop("context", None) + + data["device"]["entities"].append( + { + "disabled": entity_entry.disabled, + "disabled_by": entity_entry.disabled_by, + "entity_category": entity_entry.entity_category, + "device_class": entity_entry.device_class, + "original_device_class": entity_entry.original_device_class, + "icon": entity_entry.icon, + "original_icon": entity_entry.original_icon, + "unit_of_measurement": entity_entry.unit_of_measurement, + "state": state_dict, + } + ) + + return data diff --git a/custom_components/hpprinter/helpers/__init__.py b/custom_components/hpprinter/helpers/__init__.py deleted file mode 100644 index bcffd4e..0000000 --- a/custom_components/hpprinter/helpers/__init__.py +++ /dev/null @@ -1,53 +0,0 @@ -import logging -import sys - -from homeassistant.components.logger import DOMAIN as DOMAIN_LOGGER, SERVICE_SET_LEVEL -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant - -from ..managers.home_assistant import HPPrinterHomeAssistant -from .const import * - -_LOGGER = logging.getLogger(__name__) - - -def clear_ha(hass: HomeAssistant, name): - if DATA_HP_PRINTER not in hass.data: - hass.data[DATA_HP_PRINTER] = {} - - del hass.data[DATA_HP_PRINTER][name] - - -def get_ha(hass: HomeAssistant, host): - ha_data = hass.data.get(DATA_HP_PRINTER, {}) - ha = ha_data.get(host) - - return ha - - -async def async_set_ha(hass: HomeAssistant, name, entry: ConfigEntry): - try: - if DATA_HP_PRINTER not in hass.data: - hass.data[DATA_HP_PRINTER] = {} - - instance = HPPrinterHomeAssistant(hass) - - await instance.async_init(entry) - - hass.data[DATA_HP_PRINTER][name] = instance - except Exception as ex: - exc_type, exc_obj, tb = sys.exc_info() - line_number = tb.tb_lineno - - _LOGGER.error(f"Failed to async_set_ha, error: {ex}, line: {line_number}") - - -async def handle_log_level(hass: HomeAssistant, entry: ConfigEntry): - log_level = entry.options.get(CONF_LOG_LEVEL, LOG_LEVEL_DEFAULT) - - if log_level == LOG_LEVEL_DEFAULT: - return - - log_level_data = {f"custom_components.{DOMAIN}": log_level.lower()} - - await hass.services.async_call(DOMAIN_LOGGER, SERVICE_SET_LEVEL, log_level_data) diff --git a/custom_components/hpprinter/helpers/const.py b/custom_components/hpprinter/helpers/const.py deleted file mode 100644 index 89d55f6..0000000 --- a/custom_components/hpprinter/helpers/const.py +++ /dev/null @@ -1,151 +0,0 @@ -from homeassistant.components.binary_sensor import DOMAIN as DOMAIN_BINARY_SENSOR -from homeassistant.components.sensor import DOMAIN as DOMAIN_SENSOR - -MANUFACTURER = "HP" -DEFAULT_NAME = "HP Printer" -DOMAIN = "hpprinter" -DATA_HP_PRINTER = f"data_{DOMAIN}" -SIGNAL_UPDATE_HP_PRINTER = f"updates_{DOMAIN}" -NOTIFICATION_ID = f"{DOMAIN}_notification" -NOTIFICATION_TITLE = f"{DEFAULT_NAME} Setup" - -SENSOR_ENTITY_ID = "sensor.{}_{}" -BINARY_SENSOR_ENTITY_ID = "binary_sensor.{}_{}" - -NAMESPACES_TO_REMOVE = [ - "ccdyn", - "ad", - "dd", - "dd2", - "pudyn", - "psdyn", - "xsd", - "pscat", - "locid", - "prdcfgdyn2", - "prdcfgdyn", -] - -CONF_STORE_DATA = "store_data" -CONF_UPDATE_INTERVAL = "update_interval" -CONF_LOG_LEVEL = "log_level" - -ENTITY_ICON = "icon" -ENTITY_STATE = "state" -ENTITY_ATTRIBUTES = "attributes" -ENTITY_NAME = "name" -ENTITY_MODEL = "model" -ENTITY_MODEL_FAMILY = "model-family" -ENTITY_DEVICE_NAME = "device-name" -ENTITY_UNIQUE_ID = "unique-id" -ENTITY_BINARY_SENSOR_DEVICE_CLASS = "binary-sensor-device-class" -ENTITY_SENSOR_DEVICE_CLASS = "sensor-device-class" -ENTITY_SENSOR_STATE_CLASS = "sensor-state-class" - -ENTITY_STATUS = "entity-status" -ENTITY_STATUS_EMPTY = None -ENTITY_STATUS_READY = f"{ENTITY_STATUS}-ready" -ENTITY_STATUS_CREATED = f"{ENTITY_STATUS}-created" -ENTITY_STATUS_MODIFIED = f"{ENTITY_STATUS}-modified" -ENTITY_STATUS_IGNORE = f"{ENTITY_STATUS}-ignore" -ENTITY_STATUS_CANCELLED = f"{ENTITY_STATUS}-cancelled" - -ENTITY_DISABLED = "disabled" - -PRINTER_CURRENT_STATUS = "status" -PRINTER_SENSOR = "Printer" - -INK_ICON = "mdi:cup-water" -PAGES_ICON = "mdi:book-open-page-variant" -SCANNER_ICON = "mdi:scanner" - -PROTOCOLS = {True: "https", False: "http"} - -IGNORE_ITEMS = [ - "@xsi:schemaLocation", - "@xmlns:xsd", - "@xmlns:dd", - "@xmlns:dd2", - "@xmlns:ccdyn", - "@xmlns:xsi", - "@xmlns:pudyn", - "@xmlns:ad", - "@xmlns:psdyn", - "@xmlns:pscat", - "@xmlns:locid", - "@xmlns:locid", - "@xmlns:prdcfgdyn", - "@xmlns:prdcfgdyn2", - "@xmlns:pudyn", - "PECounter", -] - -ARRAY_KEYS = { - "UsageByMedia": [], - "SupportedConsumable": ["ConsumableTypeEnum", "ConsumableLabelCode"], - "SupportedConsumableInfo": ["ConsumableUsageType"], - "EmailAlertCategories": ["AlertCategory"], -} - -ARRAY_AS_DEFAULT = [ - "AlertDetailsUserAction", - "ConsumableStateAction", - "AlertCategory", - "ResourceURI", - "Language", - "AutoOnEvent", - "DaysOfWeek", -] - -HP_DEVICE_CONNECTIVITY = "Connectivity" -HP_DEVICE_STATUS = "Status" -HP_DEVICE_PRINTER = "Printer" -HP_DEVICE_SCANNER = "Scanner" -HP_DEVICE_CARTRIDGES = "Cartridges" - -HP_DEVICE_PRINTER_STATE = "Total" -HP_DEVICE_SCANNER_STATE = "Total" -HP_DEVICE_CARTRIDGE_STATE = "Remaining" - -HP_DEVICE_IS_ONLINE = "IsOnline" - -HP_HEAD_TYPE_INK = "ink" -HP_HEAD_TYPE_PRINT_HEAD = "printhead" -HP_ORGANIC_PHOTO_CONDUCTOR = "OPC" -HP_ORGANIC_PHOTO_CONDUCTOR_NAME = "Organic Photo Conductor" - -NOT_AVAILABLE = "N/A" - -HP_INK_MAPPING = {"C": "Cyan", "Y": "Yellow", "M": "Magenta", "K": "Black"} - -SIGNAL_UPDATE_BINARY_SENSOR = f"{DEFAULT_NAME}_{DOMAIN_BINARY_SENSOR}_SIGNAL_UPDATE" -SIGNAL_UPDATE_SENSOR = f"{DEFAULT_NAME}_{DOMAIN_SENSOR}_SIGNAL_UPDATE" - -SIGNALS = { - DOMAIN_BINARY_SENSOR: SIGNAL_UPDATE_BINARY_SENSOR, - DOMAIN_SENSOR: SIGNAL_UPDATE_SENSOR, -} - -LOG_LEVEL_DEFAULT = "Default" -LOG_LEVEL_DEBUG = "Debug" -LOG_LEVEL_INFO = "Info" -LOG_LEVEL_WARNING = "Warning" -LOG_LEVEL_ERROR = "Error" - -LOG_LEVELS = [ - LOG_LEVEL_DEFAULT, - LOG_LEVEL_DEBUG, - LOG_LEVEL_INFO, - LOG_LEVEL_WARNING, - LOG_LEVEL_ERROR, -] - -PRINTER_STATUS = { - "ready": "On", - "scanProcessing": "Scanning", - "copying": "Copying", - "processing": "Printing", - "cancelJob": "Cancelling Job", - "inPowerSave": "Idle", - "": "Off", -} diff --git a/custom_components/hpprinter/managers/HPDeviceData.py b/custom_components/hpprinter/managers/HPDeviceData.py deleted file mode 100644 index 780eced..0000000 --- a/custom_components/hpprinter/managers/HPDeviceData.py +++ /dev/null @@ -1,445 +0,0 @@ -from custom_components.hpprinter.api.HPPrinterAPI import * - -from ..models.config_data import ConfigData -from .storage_manager import StorageManager - -_LOGGER = logging.getLogger(__name__) - - -class HPDeviceData: - device_data: dict - - def __init__(self, hass, config_manager: ConfigManager): - self._hass = hass - self._config_manager = config_manager - - self._storage_manager = StorageManager(self._hass, self._config_manager) - - self._usage_data_manager = ProductUsageDynPrinterDataAPI( - hass, self._config_manager - ) - self._consumable_data_manager = ConsumableConfigDynPrinterDataAPI( - hass, self._config_manager - ) - self._product_config_manager = ProductConfigDynDataAPI( - hass, self._config_manager - ) - self._product_status_manager = ProductStatusDynDataAPI( - hass, self._config_manager - ) - - self._usage_data = None - self._consumable_data = None - self._product_config_data = None - self._product_status_data = None - self.device_data = {} - - @property - def config_data(self) -> ConfigData: - return self._config_manager.data - - @property - def name(self): - return self.config_data.name - - @property - def host(self): - return self.config_data.host - - async def initialize(self): - _LOGGER.debug("Initialize") - - self.device_data = await self._storage_manager.async_load_from_store() - - if self.device_data is None: - self.device_data = {} - - self.device_data[PRINTER_CURRENT_STATUS] = PRINTER_STATUS[""] - self.device_data[HP_DEVICE_IS_ONLINE] = False - - async def terminate(self): - await self._usage_data_manager.terminate() - await self._consumable_data_manager.terminate() - await self._product_config_manager.terminate() - await self._product_status_manager.terminate() - - async def update(self): - try: - self.device_data["Name"] = self.config_data.name - - self._usage_data = await self._usage_data_manager.get_data() - self._consumable_data = await self._consumable_data_manager.get_data() - self._product_config_data = await self._product_config_manager.get_data() - self._product_status_data = await self._product_status_manager.get_data() - - data_list = [ - self._usage_data, - self._consumable_data, - self._product_config_data, - self._product_status_data, - ] - - is_online = True - - for item in data_list: - if item is None: - is_online = False - break - - if is_online: - self.set_usage_data() - self.set_consumable_data() - self.set_product_config_data() - self.set_product_status_data() - else: - self.device_data[PRINTER_CURRENT_STATUS] = PRINTER_STATUS[""] - - self.device_data[HP_DEVICE_IS_ONLINE] = is_online - - if is_online: - await self._storage_manager.async_save_to_store(self.device_data) - - except Exception as ex: - exc_type, exc_obj, tb = sys.exc_info() - line_number = tb.tb_lineno - error_details = f"Error: {ex}, Line: {line_number}" - - _LOGGER.error( - f"Failed to update data ({self.name} @{self.host}) and parse it, {error_details}" - ) - - def set_consumable_data(self): - try: - if self._consumable_data is not None: - root = self._consumable_data.get("ConsumableConfigDyn", {}) - consumables_info = root.get("ConsumableInfo", []) - - if "ConsumableLabelCode" in consumables_info: - self.set_printer_consumable_data(consumables_info) - else: - for consumable_key in consumables_info: - consumable = consumables_info[consumable_key] - - self.set_printer_consumable_data(consumable) - - except Exception as ex: - exc_type, exc_obj, tb = sys.exc_info() - line_number = tb.tb_lineno - error_details = f"Error: {ex}, Line: {line_number}" - - _LOGGER.error( - f"Failed to parse consumable data ({self.name} @{self.host}), {error_details}" - ) - - def set_product_config_data(self): - try: - if self._product_config_data is not None: - root = self._product_config_data.get("ProductConfigDyn", {}) - product_information = root.get("ProductInformation", {}) - self.device_data[ENTITY_MODEL] = product_information.get("MakeAndModel") - - except Exception as ex: - exc_type, exc_obj, tb = sys.exc_info() - line_number = tb.tb_lineno - - _LOGGER.error( - f"Failed to parse usage data ({self.name} @{self.host}), Error: {ex}, Line: {line_number}" - ) - - def set_product_status_data(self): - try: - if self._product_status_data is not None: - root = self._product_status_data.get("ProductStatusDyn", {}) - status = root.get("Status", []) - printer_status = "" - - if "StatusCategory" in status: - printer_status = self.clean_parameter(status, "StatusCategory") - else: - for item in status: - status_item = status[item] - if "LocString" not in status_item: - printer_status = self.clean_parameter( - status_item, "StatusCategory" - ) - - self.device_data[PRINTER_CURRENT_STATUS] = PRINTER_STATUS.get( - printer_status, printer_status - ) - - except Exception as ex: - exc_type, exc_obj, tb = sys.exc_info() - line_number = tb.tb_lineno - - _LOGGER.error( - f"Failed to parse usage data ({self.name} @{self.host}), Error: {ex}, Line: {line_number}" - ) - - def set_usage_data(self): - try: - if self._usage_data is not None: - root = self._usage_data.get("ProductUsageDyn", {}) - printer_data = root.get("PrinterSubunit") - scanner_data = root.get("ScannerEngineSubunit") - consumables_data = root.get("ConsumableSubunit") - - if printer_data is not None: - self.set_printer_usage_data(printer_data) - - if scanner_data is not None: - self.set_scanner_usage_data(scanner_data) - - if consumables_data is not None: - printer_consumables = consumables_data.get("Consumable") - - if printer_consumables is not None: - if "ConsumableStation" in printer_consumables: - self.set_printer_consumable_usage_data(printer_consumables) - else: - for key in printer_consumables: - consumable = printer_consumables.get(key) - - if consumable is not None: - self.set_printer_consumable_usage_data(consumable) - - except Exception as ex: - exc_type, exc_obj, tb = sys.exc_info() - line_number = tb.tb_lineno - - _LOGGER.error( - f"Failed to parse usage data ({self.name} @{self.host}), Error: {ex}, Line: {line_number}" - ) - - def set_printer_usage_data(self, printer_data): - try: - total_printed_pages = self.clean_parameter( - printer_data, "TotalImpressions", "0" - ) - - color_printed_pages = self.clean_parameter(printer_data, "ColorImpressions") - monochrome_printed_pages = self.clean_parameter( - printer_data, "MonochromeImpressions" - ) - - printer_jams = self.clean_parameter(printer_data, "Jams") - if printer_jams == NOT_AVAILABLE: - printer_jams = self.clean_parameter(printer_data, "JamEvents", "0") - - cancelled_print_jobs_number = self.clean_parameter( - printer_data, "TotalFrontPanelCancelPresses" - ) - - self.device_data[HP_DEVICE_PRINTER] = { - HP_DEVICE_PRINTER_STATE: total_printed_pages, - "Color": color_printed_pages, - "Monochrome": monochrome_printed_pages, - "Jams": printer_jams, - "Cancelled": cancelled_print_jobs_number, - } - - except Exception as ex: - exc_type, exc_obj, tb = sys.exc_info() - line_number = tb.tb_lineno - - _LOGGER.error( - f"Failed to set printer data ({self.name} @{self.host}), Error: {ex}, Line: {line_number}" - ) - - def set_scanner_usage_data(self, scanner_data): - try: - scan_images_count = self.clean_parameter(scanner_data, "ScanImages") - adf_images_count = self.clean_parameter(scanner_data, "AdfImages") - duplex_sheets_count = self.clean_parameter(scanner_data, "DuplexSheets") - flatbed_images = self.clean_parameter(scanner_data, "FlatbedImages") - scanner_jams = self.clean_parameter(scanner_data, "JamEvents", "0") - scanner_mispick = self.clean_parameter(scanner_data, "MispickEvents", "0") - - if scan_images_count == NOT_AVAILABLE: - new_scan_images_count = 0 - - if adf_images_count != NOT_AVAILABLE and int(adf_images_count) > 0: - new_scan_images_count = int(adf_images_count) - - if flatbed_images != NOT_AVAILABLE and int(flatbed_images) > 0: - new_scan_images_count = new_scan_images_count + int(flatbed_images) - - scan_images_count = new_scan_images_count - - self.device_data[HP_DEVICE_SCANNER] = { - HP_DEVICE_SCANNER_STATE: scan_images_count, - "ADF": adf_images_count, - "Duplex": duplex_sheets_count, - "Flatbed": flatbed_images, - "Jams": scanner_jams, - "Mispick": scanner_mispick, - } - - except Exception as ex: - exc_type, exc_obj, tb = sys.exc_info() - line_number = tb.tb_lineno - - _LOGGER.error( - f"Failed to set scanner data ({self.name} @{self.host}), Error: {ex}, Line: {line_number}" - ) - - def set_printer_consumable_usage_data(self, printer_consumable_data): - try: - color = self.clean_parameter(printer_consumable_data, "MarkerColor") - head_type = self.clean_parameter( - printer_consumable_data, "ConsumableTypeEnum" - ).capitalize() - station = self.clean_parameter(printer_consumable_data, "ConsumableStation") - - if NOT_AVAILABLE in head_type.upper() or NOT_AVAILABLE in color: - _LOGGER.info(f"Skipped setting using data for {head_type} {color}") - - return - - cartridge_key = f"{head_type} {color}" - - should_create_cartridges = False - should_create_cartridge = False - - cartridges = self.device_data.get(HP_DEVICE_CARTRIDGES) - if cartridges is None: - cartridges = {} - should_create_cartridges = True - - cartridge = cartridges.get(cartridge_key) - - if cartridge is None: - cartridge = {} - should_create_cartridge = True - - cartridge["Color"] = color - cartridge["Type"] = head_type - cartridge["Station"] = station - - if should_create_cartridge: - cartridges[cartridge_key] = cartridge - - if should_create_cartridges: - self.device_data[HP_DEVICE_CARTRIDGES] = cartridges - - except Exception as ex: - exc_type, exc_obj, tb = sys.exc_info() - line_number = tb.tb_lineno - error_details = f"Error: {ex}, Line: {line_number}" - - _LOGGER.error( - f"Failed to set printer consumable usage data ({self.name} @{self.host}), {error_details}" - ) - - def set_printer_consumable_data(self, printer_consumable_data): - try: - consumable_label_code = self.clean_parameter( - printer_consumable_data, "ConsumableLabelCode" - ) - head_type = self.clean_parameter( - printer_consumable_data, "ConsumableTypeEnum" - ).capitalize() - product_number = self.clean_parameter( - printer_consumable_data, "ProductNumber" - ) - serial_number = self.clean_parameter( - printer_consumable_data, "SerialNumber" - ) - remaining = self.clean_parameter( - printer_consumable_data, "ConsumablePercentageLevelRemaining", "0" - ) - - installation = printer_consumable_data.get("Installation", {}) - installation_data = self.clean_parameter(installation, "Date") - - manufacturer = printer_consumable_data.get("Manufacturer", {}) - manufactured_by = self.clean_parameter(manufacturer, "Name").rstrip() - manufactured_at = self.clean_parameter(manufacturer, "Date") - - warranty = printer_consumable_data.get("Warranty", {}) - expiration_date = self.clean_parameter(warranty, "ExpirationDate") - - if head_type == HP_HEAD_TYPE_PRINT_HEAD: - color = consumable_label_code - else: - color_map = [] - - if consumable_label_code == HP_ORGANIC_PHOTO_CONDUCTOR: - color = HP_ORGANIC_PHOTO_CONDUCTOR_NAME - else: - for color_letter in consumable_label_code: - mapped_color = HP_INK_MAPPING.get(color_letter, color_letter) - - color_map.append(mapped_color) - - color = "".join(color_map) - - if color == consumable_label_code: - _LOGGER.warning( - f"Head type {head_type} color mapping for {consumable_label_code} not available" - ) - - if NOT_AVAILABLE in head_type.upper() or NOT_AVAILABLE in color: - _LOGGER.info(f"Skipped setting {head_type} {color}") - - return - - cartridge_key = f"{head_type} {color}" - - should_create_cartridges = False - should_create_cartridge = False - - cartridges = self.device_data.get(HP_DEVICE_CARTRIDGES) - if cartridges is None: - cartridges = {} - should_create_cartridges = True - - cartridge = cartridges.get(cartridge_key) - - if cartridge is None: - cartridge = {} - should_create_cartridge = True - - if head_type == HP_HEAD_TYPE_PRINT_HEAD: - cartridge["Color"] = color - cartridge["Type"] = head_type - - else: - cartridge["Product Number"] = product_number - cartridge["Serial Number"] = serial_number - cartridge["Manufactured By"] = manufactured_by - cartridge["Manufactured At"] = manufactured_at - cartridge["Warranty Expiration Date"] = expiration_date - - cartridge["Installed At"] = installation_data - cartridge[HP_DEVICE_CARTRIDGE_STATE] = remaining - - if should_create_cartridge: - cartridges[cartridge_key] = cartridge - - if should_create_cartridges: - self.device_data[HP_DEVICE_CARTRIDGES] = cartridges - - except Exception as ex: - exc_type, exc_obj, tb = sys.exc_info() - line_number = tb.tb_lineno - - error_details = f"Error: {str(ex)}, Line: {line_number}" - - _LOGGER.error( - f"Failed to set printer consumable data ({self.name} @{self.host}), {error_details}" - ) - - @staticmethod - def clean_parameter(data_item, data_key, default_value=NOT_AVAILABLE): - if data_item is None: - result = default_value - else: - result = data_item.get(data_key, {}) - - if not isinstance(result, str): - result = result.get("#text", 0) - - if not isinstance(result, str): - result = default_value - - return result diff --git a/custom_components/hpprinter/managers/config_flow_manager.py b/custom_components/hpprinter/managers/config_flow_manager.py deleted file mode 100644 index ef47963..0000000 --- a/custom_components/hpprinter/managers/config_flow_manager.py +++ /dev/null @@ -1,189 +0,0 @@ -import logging -from typing import Optional - -import voluptuous as vol - -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_NAME -from homeassistant.helpers import config_validation as cv - -from .. import LoginError -from ..api.HPPrinterAPI import ProductConfigDynDataAPI -from ..helpers.const import * -from ..managers.configuration_manager import ConfigManager -from ..models import AlreadyExistsError -from ..models.config_data import ConfigData - -_LOGGER = logging.getLogger(__name__) -_CONF_ARR = [CONF_NAME, CONF_HOST] - - -class ConfigFlowManager: - config_manager: ConfigManager - options: Optional[dict] - data: Optional[dict] - config_entry: ConfigEntry - - def __init__(self, config_entry: Optional[ConfigEntry] = None): - self.config_entry = config_entry - - self.options = None - self.data = None - self._pre_config = False - - if config_entry is not None: - self._pre_config = True - - self.update_data(self.config_entry.data) - - self._is_initialized = True - self._auth_error = False - self._hass = None - - def initialize(self, hass): - self._hass = hass - - if not self._pre_config: - self.options = {} - self.data = {} - - self.config_manager = ConfigManager() - - self._update_entry() - - @property - def config_data(self) -> ConfigData: - return self.config_manager.data - - async def update_options(self, options: dict, update_entry: bool = False): - new_options = {} - validate_login = False - config_entries = None - - if update_entry: - config_entries = self._hass.config_entries - - data = self.config_entry.data - name_changed = False - - for conf in _CONF_ARR: - if data.get(conf) != options.get(conf): - validate_login = True - - if conf == CONF_NAME: - name_changed = True - - if name_changed: - entries = config_entries.async_entries(DOMAIN) - - for entry in entries: - entry_item: ConfigEntry = entry - - if entry_item.unique_id == self.config_entry.unique_id: - continue - - if options.get(CONF_NAME) == entry_item.data.get(CONF_NAME): - raise AlreadyExistsError(entry_item) - - new_options = {} - for key in options: - new_options[key] = options[key] - - if update_entry: - for conf in _CONF_ARR: - if conf in new_options: - self.data[conf] = new_options[conf] - - del new_options[conf] - - self.options = new_options - - self._update_entry() - - if validate_login: - errors = await self.valid_login() - - if errors is None: - config_entries.async_update_entry(self.config_entry, data=self.data) - else: - raise LoginError(errors) - - return new_options - - def update_data(self, data: dict, update_entry: bool = False): - new_data = None - - if data is not None: - new_data = {} - for key in data: - new_data[key] = data[key] - - self.data = new_data - - if update_entry: - self._update_entry() - - def _update_entry(self): - entry = ConfigEntry( - version=0, - minor_version=0, - domain="", - title="", - data=self.data, - source="", - options=self.options, - ) - - self.config_manager.update(entry) - - @staticmethod - def get_default_data(): - fields = { - vol.Required(CONF_NAME, default=DEFAULT_NAME): str, - vol.Required(CONF_HOST): str, - } - - data_schema = vol.Schema(fields) - - return data_schema - - def get_default_options(self): - config_data = self.config_data - - fields = { - vol.Required(CONF_NAME, default=config_data.name): str, - vol.Required(CONF_HOST, default=config_data.host): str, - vol.Optional(CONF_STORE_DATA, default=config_data.should_store): bool, - vol.Required( - CONF_UPDATE_INTERVAL, default=config_data.update_interval - ): cv.positive_int, - vol.Required(CONF_LOG_LEVEL, default=config_data.log_level): vol.In( - LOG_LEVELS - ), - } - - data_schema = vol.Schema(fields) - - return data_schema - - async def valid_login(self): - errors = None - - config_data = self.config_manager.data - - api = ProductConfigDynDataAPI(self._hass, self.config_manager) - - try: - await api.async_get(True) - except LoginError as ex: - _LOGGER.info( - f"Unable to access {DEFAULT_NAME} ({config_data.host}), HTTP Status Code {ex.status_code}" - ) - - status_code = ex.status_code - if status_code not in [400, 404]: - status_code = 400 - - errors = {"base": f"error_{status_code}"} - - return errors diff --git a/custom_components/hpprinter/managers/configuration_manager.py b/custom_components/hpprinter/managers/configuration_manager.py deleted file mode 100644 index d170075..0000000 --- a/custom_components/hpprinter/managers/configuration_manager.py +++ /dev/null @@ -1,41 +0,0 @@ -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_SSL - -from ..helpers.const import * -from ..models.config_data import ConfigData - - -class ConfigManager: - data: ConfigData - config_entry: ConfigEntry - - def update(self, config_entry: ConfigEntry): - data = config_entry.data - options = config_entry.options - - result = ConfigData() - - result.name = data.get(CONF_NAME) - result.host = data.get(CONF_HOST) - result.port = data.get(CONF_PORT, 80) - result.ssl = data.get(CONF_SSL, False) - result.should_store = self._get_config_data_item( - CONF_STORE_DATA, options, data, False - ) - result.update_interval = self._get_config_data_item( - CONF_UPDATE_INTERVAL, options, data, 60 - ) - result.log_level = self._get_config_data_item( - CONF_LOG_LEVEL, options, data, LOG_LEVEL_DEFAULT - ) - - self.config_entry = config_entry - self.data = result - - @staticmethod - def _get_config_data_item(key, options, data, default_value=None): - data_result = data.get(key, default_value) - - result = options.get(key, data_result) - - return result diff --git a/custom_components/hpprinter/managers/device_manager.py b/custom_components/hpprinter/managers/device_manager.py deleted file mode 100644 index 276b7a1..0000000 --- a/custom_components/hpprinter/managers/device_manager.py +++ /dev/null @@ -1,87 +0,0 @@ -import logging - -from homeassistant.const import ( - ATTR_CONFIGURATION_URL, - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, -) -from homeassistant.helpers.device_registry import async_get - -from ..helpers.const import * -from ..managers.HPDeviceData import HPDeviceData - -_LOGGER = logging.getLogger(__name__) - - -class DeviceManager: - def __init__(self, hass, ha): - self._hass = hass - self._ha = ha - - self._devices = {} - - @property - def data_manager(self) -> HPDeviceData: - return self._ha.data_manager - - @property - def data(self): - return self.data_manager.device_data - - @property - def name(self): - return self.data_manager.name - - async def async_remove_entry(self, entry_id): - dr = async_get(self._hass) - dr.async_clear_config_entry(entry_id) - - async def delete_device(self, name): - _LOGGER.info(f"Deleting device {name}") - - device = self._devices[name] - - device_identifiers = device.get("identifiers") - device_connections = device.get("connections", {}) - - dr = async_get(self._hass) - - device = dr.async_get_device(device_identifiers, device_connections) - - if device is not None: - dr.async_remove_device(device.id) - - async def async_remove(self): - for device_name in self._devices: - await self.delete_device(device_name) - - def get(self, name): - return self._devices.get(name, {}) - - def set(self, name, device_info): - self._devices[name] = device_info - - def update(self): - self.generate_device_info() - - def generate_device_info(self): - device_model = self.data.get(ENTITY_MODEL, self.name) - device_model_family = self.data.get(ENTITY_MODEL_FAMILY, self.name) - - device_id = f"{DEFAULT_NAME}-{self.name}-{device_model_family}" - - device_info = { - ATTR_IDENTIFIERS: {(DOMAIN, device_id)}, - ATTR_NAME: device_model_family, - ATTR_MANUFACTURER: MANUFACTURER, - ATTR_MODEL: device_model, - } - - if self.data_manager.device_data[HP_DEVICE_IS_ONLINE]: - device_info[ - ATTR_CONFIGURATION_URL - ] = f"{PROTOCOLS[self.data_manager.config_data.ssl]}://{self.data_manager.config_data.host}" - - self.set(DEFAULT_NAME, device_info) diff --git a/custom_components/hpprinter/managers/entity_manager.py b/custom_components/hpprinter/managers/entity_manager.py deleted file mode 100644 index 57960f2..0000000 --- a/custom_components/hpprinter/managers/entity_manager.py +++ /dev/null @@ -1,361 +0,0 @@ -import logging -import sys -from typing import Optional - -from homeassistant.components.binary_sensor import BinarySensorDeviceClass -from homeassistant.components.sensor import SensorStateClass -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_registry import EntityRegistry - -from ..helpers.const import * -from ..managers.HPDeviceData import HPDeviceData -from ..models.config_data import ConfigData -from ..models.entity_data import EntityData - -_LOGGER = logging.getLogger(__name__) - - -def _get_camera_binary_sensor_key(topic, event_type): - key = f"{topic}_{event_type}".lower() - - return key - - -class EntityManager: - hass: HomeAssistant - ha = None - entities: dict - domain_component_manager: dict - - def __init__(self, hass, ha): - self.hass = hass - self.ha = ha - self.domain_component_manager = {} - self.entities = {} - - @property - def data_manager(self) -> HPDeviceData: - return self.ha.data_manager - - @property - def data(self): - return self.data_manager.device_data - - @property - def entity_registry(self) -> EntityRegistry: - return self.ha.entity_registry - - @property - def config_data(self) -> ConfigData: - return self.ha.config_data - - def set_domain_component(self, domain, async_add_entities, component): - self.domain_component_manager[domain] = { - "async_add_entities": async_add_entities, - "component": component, - } - - def is_device_name_in_use(self, device_name): - result = False - - for entity in self.get_all_entities(): - if entity.device_name == device_name: - result = True - break - - return result - - def get_all_entities(self) -> list[EntityData]: - entities = [] - for domain in self.entities: - for name in self.entities[domain]: - entity = self.entities[domain][name] - - entities.append(entity) - - return entities - - def check_domain(self, domain): - if domain not in self.entities: - self.entities[domain] = {} - - def get_entities(self, domain) -> dict[str, EntityData]: - self.check_domain(domain) - - return self.entities[domain] - - def get_entity(self, domain, name) -> Optional[EntityData]: - entities = self.get_entities(domain) - entity = entities.get(name) - - return entity - - def get_entity_status(self, domain, name): - entity = self.get_entity(domain, name) - - status = ENTITY_STATUS_EMPTY if entity is None else entity.status - - return status - - def set_entity_status(self, domain, name, status): - if domain in self.entities and name in self.entities[domain]: - self.entities[domain][name].status = status - - def delete_entity(self, domain, name): - if domain in self.entities and name in self.entities[domain]: - del self.entities[domain][name] - - def set_entity(self, domain, name, data: EntityData): - try: - self.check_domain(domain) - - self.entities[domain][name] = data - except Exception as ex: - self.log_exception( - ex, f"Failed to set_entity, domain: {domain}, name: {name}" - ) - - async def create_components(self): - cartridges_data = self.data.get(HP_DEVICE_CARTRIDGES) - - self.create_status_binary_sensor() - self.create_status_sensor() - self.create_printer_sensor() - self.create_scanner_sensor() - - if cartridges_data is not None: - for key in cartridges_data: - cartridge = cartridges_data.get(key) - - if cartridge is not None: - self.create_cartridge_sensor(cartridge, key) - - def update(self): - self.hass.async_create_task(self._async_update()) - - async def _async_update(self): - step = "Mark as ignore" - try: - step = "Create components" - - await self.create_components() - - step = "Start updating" - - for domain in SIGNALS: - step = f"Start updating domain {domain}" - - entities_to_add = [] - domain_component_manager = self.domain_component_manager[domain] - domain_component = domain_component_manager["component"] - async_add_entities = domain_component_manager["async_add_entities"] - - entities = dict(self.get_entities(domain)) - - for entity_key in entities: - step = f"Start updating {domain} -> {entity_key}" - - entity = entities[entity_key] - - entity_id = self.entity_registry.async_get_entity_id( - domain, DOMAIN, entity.unique_id - ) - - if entity.status == ENTITY_STATUS_CREATED: - entity_item = self.entity_registry.async_get(entity_id) - - step = f"Mark as created - {domain} -> {entity_key}" - - entity_component = domain_component( - self.hass, self.config_data.name, entity - ) - - if entity_id is not None: - entity_component.entity_id = entity_id - - state = self.hass.states.get(entity_id) - - if state is None: - restored = True - else: - restored = state.attributes.get("restored", False) - - if restored: - _LOGGER.info( - f"Entity {entity.name} restored | {entity_id}" - ) - - if restored: - if entity_item is None or not entity_item.disabled: - entities_to_add.append(entity_component) - else: - entities_to_add.append(entity_component) - - entity.status = ENTITY_STATUS_READY - - if entity_item is not None: - entity.disabled = entity_item.disabled - - step = f"Add entities to {domain}" - - if len(entities_to_add) > 0: - async_add_entities(entities_to_add, True) - - except Exception as ex: - self.log_exception(ex, f"Failed to update, step: {step}") - - def create_status_sensor(self): - status = self.data.get(PRINTER_CURRENT_STATUS, "Off") - - name = self.data.get("Name", DEFAULT_NAME) - entity_name = f"{name} {HP_DEVICE_STATUS}" - - device_name = DEFAULT_NAME - unique_id = entity_name - - icon = self.get_printer_icon() - - attributes = {"friendly_name": entity_name} - - entity = EntityData() - - entity.unique_id = unique_id - entity.name = entity_name - entity.attributes = attributes - entity.icon = icon - entity.device_name = device_name - entity.binary_sensor_device_class = BinarySensorDeviceClass.CONNECTIVITY - entity.state = status - - self.set_entity(DOMAIN_SENSOR, entity_name, entity) - - def get_printer_name(self): - printer_name = self.data.get("Name", DEFAULT_NAME) - - return printer_name - - def is_online(self): - is_online = self.data.get(HP_DEVICE_IS_ONLINE, False) - - return is_online - - def get_printer_icon(self): - is_online = self.is_online() - - icon = "mdi:printer" if is_online else "mdi:printer-off" - - return icon - - def create_status_binary_sensor(self): - is_online = self.is_online() - - name = self.data.get("Name", DEFAULT_NAME) - entity_name = f"{name} {HP_DEVICE_CONNECTIVITY}" - unique_id = f"{DEFAULT_NAME}-{DOMAIN_BINARY_SENSOR}-{entity_name}" - device_name = DEFAULT_NAME - icon = self.get_printer_icon() - - attributes = {"friendly_name": entity_name} - - entity = EntityData() - - entity.unique_id = unique_id - entity.name = entity_name - entity.attributes = attributes - entity.icon = icon - entity.device_name = device_name - entity.state = is_online - entity.binary_sensor_device_class = BinarySensorDeviceClass.CONNECTIVITY - - self.set_entity(DOMAIN_BINARY_SENSOR, entity_name, entity) - - def create_printer_sensor(self): - printer_data = self.data.get(HP_DEVICE_PRINTER) - - if printer_data is not None: - name = self.get_printer_name() - entity_name = f"{name} {HP_DEVICE_PRINTER}" - unique_id = f"{DEFAULT_NAME}-{DOMAIN_SENSOR}-{entity_name}" - device_name = DEFAULT_NAME - - state = printer_data.get(HP_DEVICE_PRINTER_STATE) - - attributes = {"unit_of_measurement": "Pages", "friendly_name": entity_name} - - for key in printer_data: - if key != HP_DEVICE_PRINTER_STATE: - attributes[key] = printer_data[key] - - entity = EntityData() - - entity.unique_id = unique_id - entity.name = entity_name - entity.attributes = attributes - entity.icon = PAGES_ICON - entity.device_name = device_name - entity.state = state - entity.sensor_state_class = SensorStateClass.TOTAL_INCREASING - - self.set_entity(DOMAIN_SENSOR, entity_name, entity) - - def create_scanner_sensor(self): - scanner_data = self.data.get(HP_DEVICE_SCANNER) - - if scanner_data is not None: - name = self.get_printer_name() - entity_name = f"{name} {HP_DEVICE_SCANNER}" - unique_id = f"{DEFAULT_NAME}-{DOMAIN_SENSOR}-{entity_name}" - device_name = DEFAULT_NAME - - state = scanner_data.get(HP_DEVICE_SCANNER_STATE) - - attributes = {"unit_of_measurement": "Pages", "friendly_name": entity_name} - - for key in scanner_data: - if key != HP_DEVICE_SCANNER_STATE: - attributes[key] = scanner_data[key] - - entity = EntityData() - - entity.unique_id = unique_id - entity.name = entity_name - entity.attributes = attributes - entity.icon = SCANNER_ICON - entity.device_name = device_name - entity.state = state - entity.sensor_state_class = SensorStateClass.TOTAL_INCREASING - - self.set_entity(DOMAIN_SENSOR, entity_name, entity) - - def create_cartridge_sensor(self, cartridge, key): - name = self.get_printer_name() - entity_name = f"{name} {key}" - unique_id = f"{DEFAULT_NAME}-{DOMAIN_SENSOR}-{entity_name}" - device_name = DEFAULT_NAME - - state = cartridge.get(HP_DEVICE_CARTRIDGE_STATE, 0) - - attributes = {"unit_of_measurement": "%", "friendly_name": entity_name} - - for key in cartridge: - if key != HP_DEVICE_CARTRIDGE_STATE: - attributes[key] = cartridge[key] - - entity = EntityData() - - entity.unique_id = unique_id - entity.name = entity_name - entity.attributes = attributes - entity.icon = INK_ICON - entity.device_name = device_name - entity.state = state - entity.sensor_state_class = SensorStateClass.MEASUREMENT - - self.set_entity(DOMAIN_SENSOR, entity_name, entity) - - @staticmethod - def log_exception(ex, message): - exc_type, exc_obj, tb = sys.exc_info() - line_number = tb.tb_lineno - - _LOGGER.error(f"{message}, Error: {str(ex)}, Line: {line_number}") diff --git a/custom_components/hpprinter/managers/flow_manager.py b/custom_components/hpprinter/managers/flow_manager.py new file mode 100644 index 0000000..eabcae4 --- /dev/null +++ b/custom_components/hpprinter/managers/flow_manager.py @@ -0,0 +1,109 @@ +"""Config flow to configure.""" +from __future__ import annotations + +from copy import copy +import logging +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowHandler + +from ..common.consts import DATA_KEYS, DEFAULT_NAME +from ..models.config_data import ConfigData +from ..models.exceptions import IntegrationAPIError, IntegrationParameterError +from .ha_config_manager import HAConfigManager +from .rest_api import RestAPIv2 + +_LOGGER = logging.getLogger(__name__) + + +class IntegrationFlowManager: + _hass: HomeAssistant + _entry: ConfigEntry | None + + _flow_handler: FlowHandler + _flow_id: str + + _config_manager: HAConfigManager + + def __init__( + self, + hass: HomeAssistant, + flow_handler: FlowHandler, + entry: ConfigEntry | None = None, + ): + self._hass = hass + self._flow_handler = flow_handler + self._entry = entry + self._flow_id = "user" if entry is None else "init" + self._config_manager = HAConfigManager(self._hass, None) + + async def async_step(self, user_input: dict | None = None): + """Manage the domain options.""" + _LOGGER.info(f"Config flow started, Step: {self._flow_id}, Input: {user_input}") + + form_errors = None + + if user_input is None: + if self._entry is None: + user_input = {} + + else: + user_input = {key: self._entry.data[key] for key in self._entry.data} + + else: + try: + await self._config_manager.initialize(user_input) + + api = RestAPIv2(self._hass, self._config_manager) + + await api.initialize(True) + + _LOGGER.debug("User inputs are valid") + title = DEFAULT_NAME + + if self._entry is None: + data = copy(user_input) + + else: + data = await self.remap_entry_data(user_input) + title = self._entry.title + + return self._flow_handler.async_create_entry(title=title, data=data) + + except IntegrationParameterError as ipex: + form_errors = {"base": "error_400"} + + _LOGGER.warning(f"Failed to setup integration, Error: {ipex}") + + except IntegrationAPIError as iapiex: + form_errors = {"base": "error_404"} + + _LOGGER.warning(f"Failed to setup integration, Error: {iapiex}") + + schema = ConfigData.default_schema(user_input) + + return self._flow_handler.async_show_form( + step_id=self._flow_id, data_schema=schema, errors=form_errors + ) + + async def remap_entry_data(self, options: dict[str, Any]) -> dict[str, Any]: + config_options = {} + config_data = {} + + entry = self._entry + entry_data = entry.data + + for key in options: + if key in DATA_KEYS: + config_data[key] = options.get(key, entry_data.get(key)) + + else: + config_options[key] = options.get(key) + + self._hass.config_entries.async_update_entry( + entry, data=config_data, title=entry.title + ) + + return config_options diff --git a/custom_components/hpprinter/managers/ha_config_manager.py b/custom_components/hpprinter/managers/ha_config_manager.py new file mode 100644 index 0000000..58b991a --- /dev/null +++ b/custom_components/hpprinter/managers/ha_config_manager.py @@ -0,0 +1,437 @@ +from datetime import timedelta +import json +import logging +import os +from pathlib import Path + +import aiofiles + +from homeassistant.config_entries import STORAGE_VERSION, ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import translation +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.json import JSONEncoder +from homeassistant.helpers.storage import Store + +from ..common.consts import ( + CONF_UPDATE_INTERVAL, + CONFIGURATION_FILE, + DEFAULT_ENTRY_ID, + DEFAULT_NAME, + DOMAIN, +) +from ..common.entity_descriptions import ( + IntegrationBinarySensorEntityDescription, + IntegrationEntityDescription, + IntegrationSensorEntityDescription, +) +from ..common.parameter_type import ParameterType +from ..models.config_data import ConfigData + +_LOGGER = logging.getLogger(__name__) + + +class HAConfigManager: + _translations: dict | None + _entry: ConfigEntry | None + _entry_id: str + _entry_title: str + _config_data: ConfigData + _store: Store | None + + def __init__(self, hass: HomeAssistant | None, entry: ConfigEntry | None): + self._hass = hass + + self._data = None + self.platforms = [] + + self._entity_descriptions: list[IntegrationEntityDescription] | None = None + + self._translations = None + + self._endpoints: list[str] | None = None + + self._data_points: dict | None = None + self._exclude_uri_list: list[str] | None = None + self._exclude_type_list: list[str] | None = None + + self._entry = entry + self._entry_id = DEFAULT_ENTRY_ID if entry is None else entry.entry_id + self._entry_title = DEFAULT_NAME if entry is None else entry.title + + self._config_data = ConfigData() + + self._is_initialized = False + self._store = None + + if hass is not None: + self._store = Store( + hass, STORAGE_VERSION, CONFIGURATION_FILE, encoder=JSONEncoder + ) + + @property + def is_initialized(self) -> bool: + is_initialized = self._is_initialized + + return is_initialized + + @property + def entry_id(self) -> str: + entry_id = self._entry_id + + return entry_id + + @property + def entry_title(self) -> str: + entry_title = self._entry_title + + return entry_title + + @property + def entry(self) -> ConfigEntry: + entry = self._entry + + return entry + + @property + def config_data(self) -> ConfigData: + config_data = self._config_data + + return config_data + + @property + def update_interval(self) -> timedelta: + interval = self._data.get(CONF_UPDATE_INTERVAL, 5) + result = timedelta(minutes=interval) + + return result + + @property + def endpoints(self) -> list[str] | None: + endpoints = self._endpoints + + return endpoints + + @property + def data_points(self) -> dict | None: + data_points = self._data_points + + return data_points + + async def initialize(self, entry_config: dict): + await self._load() + + self._config_data.update(entry_config) + + await self._load_exclude_endpoints_configuration() + await self._load_data_points_configuration() + + self._load_entity_descriptions() + + if self._hass: + self._translations = await translation.async_get_translations( + self._hass, self._hass.config.language, "entity", {DOMAIN} + ) + + self._is_initialized = True + + async def remove(self, entry_id: str): + if self._store is None: + return + + store_data = await self._store.async_load() + + entries = [DEFAULT_ENTRY_ID, entry_id] + + if store_data is not None: + should_save = False + data = {key: store_data[key] for key in store_data} + + for rm_entry_id in entries: + if rm_entry_id in store_data: + data.pop(rm_entry_id) + + should_save = True + + if should_save: + await self._store.async_save(data) + + def get_entity_name( + self, entity_description: IntegrationEntityDescription, device_info: DeviceInfo + ) -> str: + translation_key = entity_description.translation_key + platform = entity_description.platform + + device_name = device_info.get("name") + translation_key = f"component.{DOMAIN}.entity.{platform}.{translation_key}.name" + + translated_name = self._translations.get( + translation_key, entity_description.name + ) + + if translated_name is None or translated_name == "": + entity_name = f"{device_name} {entity_description.name}" + + _LOGGER.warning( + f"Translations not found, " + f"Key: {translation_key}, " + f"Entity: {entity_description.name}" + ) + + else: + entity_name = f"{device_name} {translated_name}" + + _LOGGER.debug( + f"Translations requested, " + f"Key: {translation_key}, " + f"Entity: {entity_description.name}, " + f"Value: {translated_name}" + ) + + return entity_name + + async def set_update_interval(self, value: int): + _LOGGER.debug(f"Set update interval in minutes to to {value}") + + self._data[CONF_UPDATE_INTERVAL] = value + + await self._save() + + def get_debug_data(self) -> dict: + data = self._config_data.to_dict() + + for key in self._data: + data[key] = self._data[key] + + return data + + async def _load(self): + self._data = None + + await self._load_config_from_file() + + if self._data is None: + self._data = {} + + default_configuration = self._get_defaults() + + for key in default_configuration: + value = default_configuration[key] + + if key not in self._data: + self._data[key] = value + + await self._save() + + async def _load_config_from_file(self): + if self._store is not None: + store_data = await self._store.async_load() + + if store_data is not None: + self._data = store_data.get(self._entry_id) + + @staticmethod + def _get_defaults() -> dict: + data = {CONF_UPDATE_INTERVAL: 5} + + return data + + async def _save(self): + if self._store is None: + return + + should_save = False + store_data = await self._store.async_load() + + if store_data is None: + store_data = {} + + entry_data = store_data.get(self._entry_id, {}) + + _LOGGER.debug( + f"Storing config data: {json.dumps(self._data)}, " + f"Exiting: {json.dumps(entry_data)}" + ) + + for key in self._data: + stored_value = entry_data.get(key) + + current_value = self._data.get(key) + + if stored_value != current_value: + should_save = True + + entry_data[key] = self._data[key] + + if DEFAULT_ENTRY_ID in store_data: + store_data.pop(DEFAULT_ENTRY_ID) + should_save = True + + if should_save: + store_data[self._entry_id] = entry_data + + await self._store.async_save(store_data) + + def _load_entity_descriptions(self): + self._entity_descriptions = [] + + for data_point in self._data_points: + device_type = data_point.get("device_type") + properties = data_point.get("properties") + + for property_key in properties: + property_data = properties[property_key] + + if "platform" in property_data: + property_platform = property_data.get("platform") + exclude = property_data.get("exclude") + device_class = property_data.get("device_class") + icon = property_data.get("icon") + translation_key = property_key + + if property_platform == str(Platform.BINARY_SENSOR): + on_values = [ + value.lower() + for value in property_data.get("on_values", []) + ] + + entity_description = IntegrationBinarySensorEntityDescription( + key=property_key, + name=property_key, + device_type=device_type, + exclude=exclude, + on_values=on_values, + device_class=device_class, + icon=icon, + translation_key=translation_key, + ) + + self._entity_descriptions.append(entity_description) + + elif property_platform == str(Platform.SENSOR): + unit_of_measurement = property_data.get("unit_of_measurement") + options = property_data.get("options") + + entity_description = IntegrationSensorEntityDescription( + key=property_key, + name=property_key, + device_type=device_type, + exclude=exclude, + native_unit_of_measurement=unit_of_measurement, + device_class=device_class, + icon=icon, + translation_key=translation_key, + options=options, + ) + + self._entity_descriptions.append(entity_description) + + self._update_platforms() + + def _update_platforms(self): + for entity_description in self._entity_descriptions: + if ( + entity_description.platform not in self.platforms + and entity_description.platform is not None + ): + self.platforms.append(entity_description.platform) + + def get_entity_descriptions( + self, platform: Platform, device_type: str, device_data: dict + ) -> list[IntegrationEntityDescription]: + entity_descriptions = [ + entity_description + for entity_description in self._entity_descriptions + if self._is_valid_entity( + entity_description, device_data, device_type, platform + ) + ] + + return entity_descriptions + + @staticmethod + def _is_valid_entity( + entity_description: IntegrationEntityDescription, + data: dict, + device_type: str, + platform: Platform, + ) -> bool: + key = entity_description.key + exclude = entity_description.exclude + + is_valid = ( + entity_description.platform == platform + and entity_description.device_type == device_type + and key in data + ) + + if is_valid and exclude: + for exclude_key in exclude: + exclude_value = exclude[exclude_key] + + if data.get(exclude_key) == exclude_value: + is_valid = False + break + + return is_valid + + async def _load_data_points_configuration(self): + self._endpoints = [] + + self._data_points = await self._get_parameters(ParameterType.DATA_POINTS) + + endpoint_objects = self._data_points + + for endpoint in endpoint_objects: + endpoint_uri = endpoint.get("endpoint") + + if ( + endpoint_uri not in self._endpoints + and endpoint_uri not in self._exclude_uri_list + ): + self._endpoints.append(endpoint_uri) + + async def _load_exclude_endpoints_configuration(self): + endpoints = await self._get_parameters(ParameterType.ENDPOINT_VALIDATIONS) + + self._exclude_uri_list = endpoints.get("exclude_uri") + self._exclude_type_list = endpoints.get("exclude_type") + + @staticmethod + async def _get_parameters(parameter_type: ParameterType) -> dict: + config_file = f"{parameter_type}.json" + current_path = Path(__file__) + parent_directory = current_path.parents[1] + file_path = os.path.join(parent_directory, "parameters", config_file) + + file = await aiofiles.open(file_path) + content = await file.read() + await file.close() + + data = json.loads(content) + + return data + + def is_valid_endpoint(self, endpoint: dict): + endpoint_type = endpoint.get("type") + uri = endpoint.get("uri") + methods = endpoint.get("methods", ["get"]) + + is_invalid_type = endpoint_type in self._exclude_type_list + invalid_endpoint_uri = uri in self._exclude_uri_list + invalid_uri_resource = uri.endswith("Cap.xml") + invalid_uri_parameter = "{" in uri or "}" in uri + invalid_methods = "get" not in methods + + invalid_data = [ + is_invalid_type, + invalid_uri_resource, + invalid_uri_parameter, + invalid_methods, + invalid_endpoint_uri, + ] + + is_valid = True not in invalid_data + + return is_valid diff --git a/custom_components/hpprinter/managers/ha_coordinator.py b/custom_components/hpprinter/managers/ha_coordinator.py new file mode 100644 index 0000000..5bf00d3 --- /dev/null +++ b/custom_components/hpprinter/managers/ha_coordinator.py @@ -0,0 +1,337 @@ +import logging +import sys + +from homeassistant.core import Event +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import slugify + +from ..common.consts import ( + DOMAIN, + SIGNAL_HA_DEVICE_CREATED, + SIGNAL_HA_DEVICE_DISCOVERED, +) +from .ha_config_manager import HAConfigManager +from .rest_api import RestAPIv2 + +_LOGGER = logging.getLogger(__name__) + + +class HACoordinator(DataUpdateCoordinator): + """My custom coordinator.""" + + def __init__( + self, + hass, + config_manager: HAConfigManager, + ): + """Initialize my coordinator.""" + super().__init__( + hass, + _LOGGER, + name=config_manager.entry_title, + update_interval=config_manager.update_interval, + update_method=self._async_update_data, + ) + + self._api = RestAPIv2(hass, config_manager) + self._config_manager = config_manager + self._devices: dict[str, DeviceInfo] = {} + + self._main_device_data: dict | None = None + self._main_device_id: str | None = None + + self._device_handlers = { + "Main": self.create_main_device, + "Consumable": self.create_consumable_device, + "Adapter": self.create_adapter_device, + } + + self.config_entry.async_on_unload( + async_dispatcher_connect( + hass, SIGNAL_HA_DEVICE_DISCOVERED, self._on_device_discovered + ) + ) + + @property + def api(self) -> RestAPIv2: + return self._api + + @property + def config_manager(self) -> HAConfigManager: + return self._config_manager + + @property + def entry_id(self) -> str: + return self._config_manager.entry_id + + @property + def entry_title(self) -> str: + return self._config_manager.entry_title + + async def on_home_assistant_start(self, _event_data: Event): + await self.initialize() + + async def on_home_assistant_stop(self, _event_data: Event): + await self._api.terminate() + + async def initialize(self): + _LOGGER.debug("Initializing coordinator") + + entry = self.config_manager.entry + platforms = self.config_manager.platforms + await self.hass.config_entries.async_forward_entry_setups(entry, platforms) + + _LOGGER.info(f"Start loading {DOMAIN} integration, Entry ID: {entry.entry_id}") + + await self._api.initialize() + + await self.async_config_entry_first_refresh() + + def create_main_device( + self, device_key: str, device_data: dict, device_config: dict + ): + try: + self._main_device_data = device_data + self._main_device_id = device_key + + model = device_data.get("make_and_model") + serial_number = device_data.get("serial_number") + manufacturer = device_data.get("manufacturer_name") + + device_identifier = (DOMAIN, self._main_device_id) + + device_info = DeviceInfo( + identifiers={device_identifier}, + name=self.entry_title, + model=model, + serial_number=serial_number, + manufacturer=manufacturer, + ) + + self._devices[device_key] = device_info + + except Exception as ex: + exc_type, exc_obj, tb = sys.exc_info() + line_number = tb.tb_lineno + _LOGGER.error( + f"Failed to create main device, " + f"Device Key: {device_key}, " + f"Data: {device_data}, " + f"Config: {device_config}, " + f"Error: {ex}, " + f"Line: {line_number}" + ) + + def create_sub_unit_device( + self, device_key: str, device_data: dict, device_config: dict + ): + try: + model = self._main_device_data.get("make_and_model") + serial_number = self._main_device_data.get("serial_number") + manufacturer = self._main_device_data.get("manufacturer_name") + + device_type = device_config.get("device_type") + + device_unique_id = slugify(f"{self.entry_id}.{device_key}") + + sub_unit_device_name = f"{self.entry_title} {device_type}" + + device_identifier = (DOMAIN, device_unique_id) + + device_info = DeviceInfo( + identifiers={device_identifier}, + name=sub_unit_device_name, + model=model, + serial_number=serial_number, + manufacturer=manufacturer, + via_device=(DOMAIN, self._main_device_id), + ) + + self._devices[device_key] = device_info + + except Exception as ex: + exc_type, exc_obj, tb = sys.exc_info() + line_number = tb.tb_lineno + _LOGGER.error( + f"Failed to sub unit device, " + f"Device Key: {device_key}, " + f"Data: {device_data}, " + f"Config: {device_config}, " + f"Error: {ex}, " + f"Line: {line_number}" + ) + + def create_consumable_device( + self, device_key: str, device_data: dict, device_config: dict + ): + try: + printer_device_unique_id = slugify(f"{self.entry_id}.printer") + + device_name_parts = [self.entry_title] + cartridge_type: str = device_data.get("consumable_type_enum") + cartridge_color = device_data.get("marker_color") + manufacturer = device_data.get("consumable_life_state_brand") + serial_number = device_data.get("serial_number") + + model = device_data.get("consumable_selectibility_number") + + if cartridge_type == "printhead": + model = cartridge_type.capitalize() + else: + device_name_parts.append(cartridge_color) + + if cartridge_type is not None: + device_name_parts.append(cartridge_type.capitalize()) + + device_name_parts = [ + device_name_part + for device_name_part in device_name_parts + if device_name_part is not None + ] + + device_unique_id = slugify(f"{self.entry_id}.{device_key}") + + cartridge_device_name = " ".join(device_name_parts) + + device_identifier = (DOMAIN, device_unique_id) + + device_info = DeviceInfo( + identifiers={device_identifier}, + name=cartridge_device_name, + model=model, + serial_number=serial_number, + manufacturer=manufacturer, + via_device=(DOMAIN, printer_device_unique_id), + ) + + self._devices[device_key] = device_info + + except Exception as ex: + exc_type, exc_obj, tb = sys.exc_info() + line_number = tb.tb_lineno + _LOGGER.error( + f"Failed to consumable device, " + f"Device Key: {device_key}, " + f"Data: {device_data}, " + f"Config: {device_config}, " + f"Error: {ex}, " + f"Line: {line_number}" + ) + + def create_adapter_device( + self, device_key: str, device_data: dict, device_config: dict + ): + try: + serial_number = self._main_device_data.get("serial_number") + manufacturer = self._main_device_data.get("manufacturer_name") + + adapter_name = device_data.get("hardware_config_name").upper() + + port_type_key = "hardware_config_device_connectivity_port_type" + model = device_data.get(port_type_key).replace("Embedded", "").upper() + + device_type = device_config.get("device_type") + + device_unique_id = slugify(f"{self.entry_id}.{device_key}") + + adapter_device_name = f"{self.entry_title} {device_type} {adapter_name}" + + device_identifier = (DOMAIN, device_unique_id) + + device_info = DeviceInfo( + identifiers={device_identifier}, + name=adapter_device_name, + model=model, + serial_number=serial_number, + manufacturer=manufacturer, + via_device=(DOMAIN, self._main_device_id), + ) + + self._devices[device_key] = device_info + + except Exception as ex: + exc_type, exc_obj, tb = sys.exc_info() + line_number = tb.tb_lineno + _LOGGER.error( + f"Failed to adapter device, " + f"Device Key: {device_key}, " + f"Data: {device_data}, " + f"Config: {device_config}, " + f"Error: {ex}, " + f"Line: {line_number}" + ) + + def get_device(self, device_key: str) -> DeviceInfo | None: + result = self._devices.get(device_key) + + return result + + def get_device_data(self, device_key: str): + data = self._api.data.get(device_key, {}) + + return data + + def get_device_value(self, device_key: str, key: str | None): + data = self.get_device_data(device_key) + + if key and data is not None: + return data.get(key) + + return data + + async def get_debug_data(self) -> dict: + await self._api.update_full() + + data = { + "rawData": self._api.raw_data, + "devicesData": self._api.data, + "devicesConfig": self._api.data_config, + } + + return data + + async def _async_update_data(self): + try: + await self._api.update() + + return self._api.data + + except Exception as err: + raise UpdateFailed(f"Error communicating with API: {err}") + + async def _on_device_discovered( + self, entry_id: str, device_key: str, device_data: dict, device_config: dict + ): + if entry_id != self.config_entry.entry_id: + return + + handlers = [ + device_prefix + for device_prefix in self._device_handlers + if device_key.startswith(device_prefix) + ] + + if handlers: + handler_key = handlers[0] + handler = self._device_handlers[handler_key] + + handler(device_key, device_data, device_config) + + else: + self.create_sub_unit_device(device_key, device_data, device_config) + + async_dispatcher_send( + self.hass, + SIGNAL_HA_DEVICE_CREATED, + self.entry_id, + device_key, + device_data, + device_config, + ) + + self.hass.create_task(self.async_request_refresh()) diff --git a/custom_components/hpprinter/managers/home_assistant.py b/custom_components/hpprinter/managers/home_assistant.py deleted file mode 100644 index a690265..0000000 --- a/custom_components/hpprinter/managers/home_assistant.py +++ /dev/null @@ -1,220 +0,0 @@ -""" -Support for HP Printer. -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/hpprinter/ -""" -from datetime import datetime, timedelta -import logging -import sys -from typing import Optional - -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity_registry import EntityRegistry, async_get -from homeassistant.helpers.event import async_track_time_interval - -from ..helpers.const import * -from ..managers.HPDeviceData import HPDeviceData -from ..managers.configuration_manager import ConfigManager -from ..managers.device_manager import DeviceManager -from ..managers.entity_manager import EntityManager -from ..models.config_data import ConfigData - -_LOGGER = logging.getLogger(__name__) - - -class HPPrinterHomeAssistant: - def __init__(self, hass: HomeAssistant): - self._hass = hass - - self._remove_async_track_time = None - - self._is_initialized = False - self._is_updating = False - - self._entity_registry = None - - self._api = None - self._entity_manager = None - self._device_manager = None - self._data_manager = None - - self._config_manager = ConfigManager() - - def update_entities(now): - self._hass.async_create_task(self.async_update(now)) - - self._update_entities = update_entities - - @property - def data(self): - return self._data_manager.device_data - - @property - def data_manager(self) -> HPDeviceData: - return self._data_manager - - @property - def entity_manager(self) -> EntityManager: - return self._entity_manager - - @property - def device_manager(self) -> DeviceManager: - return self._device_manager - - @property - def entity_registry(self) -> EntityRegistry: - return self._entity_registry - - @property - def config_data(self) -> Optional[ConfigData]: - if self._config_manager is not None: - return self._config_manager.data - - return None - - async def async_init(self, entry: ConfigEntry): - try: - self._config_manager.update(entry) - - self._data_manager = HPDeviceData(self._hass, self._config_manager) - self._entity_manager = EntityManager(self._hass, self) - self._device_manager = DeviceManager(self._hass, self) - - self._hass.loop.create_task(self._async_init()) - except Exception as ex: - exc_type, exc_obj, tb = sys.exc_info() - line_number = tb.tb_lineno - - _LOGGER.error(f"Failed to async_init, error: {ex}, line: {line_number}") - - async def _async_init(self): - await self._data_manager.initialize() - - self._entity_registry = async_get(self._hass) - - load = self._hass.config_entries.async_forward_entry_setup - - for domain in SIGNALS: - await load(self._config_manager.config_entry, domain) - - self._is_initialized = True - - await self.async_update_entry() - - async def async_update_entry(self, entry: ConfigEntry = None): - _LOGGER.debug("Updating config entry") - - is_update = entry is not None - - if is_update: - _LOGGER.info(f"Handling ConfigEntry change: {entry.as_dict()}") - - previous_interval = self.config_data.update_interval - - self._config_manager.update(entry) - - current_interval = self.config_data.update_interval - - is_interval_changed = previous_interval != current_interval - - if is_interval_changed and self._remove_async_track_time is not None: - msg = f"ConfigEntry interval changed from {previous_interval} to {current_interval}" - _LOGGER.info(msg) - - self._remove_async_track_time() - self._remove_async_track_time = None - else: - entry = self._config_manager.config_entry - - _LOGGER.info(f"Handling ConfigEntry initialization: {entry.as_dict()}") - - current_interval = self.config_data.update_interval - - if self._remove_async_track_time is None: - interval = timedelta(seconds=current_interval) - - self._remove_async_track_time = async_track_time_interval( - self._hass, self._update_entities, interval - ) - - await self.async_update(datetime.now()) - - async def async_remove(self): - config_entry = self._config_manager.config_entry - _LOGGER.info(f"Removing current integration - {config_entry.title}") - - if self._remove_async_track_time is not None: - self._remove_async_track_time() - self._remove_async_track_time = None - - unload = self._hass.config_entries.async_forward_entry_unload - - for domain in SIGNALS: - await unload(config_entry, domain) - - await self._device_manager.async_remove() - - _LOGGER.info(f"Current integration ({config_entry.title}) removed") - - async def async_update(self, event_time): - if not self._is_initialized: - _LOGGER.info(f"NOT INITIALIZED - Failed updating @{event_time}") - return - - try: - if self._is_updating: - _LOGGER.debug(f"Skip updating @{event_time}") - return - - _LOGGER.debug(f"Updating @{event_time}") - - self._is_updating = True - - await self.data_manager.update() - - self.device_manager.update() - self.entity_manager.update() - - await self.dispatch_all() - except Exception as ex: - exc_type, exc_obj, tb = sys.exc_info() - line_number = tb.tb_lineno - - _LOGGER.error(f"Failed to async_update, Error: {ex}, Line: {line_number}") - - self._is_updating = False - - async def delete_entity(self, domain, name): - try: - entity = self.entity_manager.get_entity(domain, name) - device_name = entity.device_name - unique_id = entity.unique_id - - self.entity_manager.delete_entity(domain, name) - - device_in_use = self.entity_manager.is_device_name_in_use(device_name) - - entity_id = self.entity_registry.async_get_entity_id( - domain, DOMAIN, unique_id - ) - self.entity_registry.async_remove(entity_id) - - if not device_in_use: - await self.device_manager.delete_device(device_name) - except Exception as ex: - exc_type, exc_obj, tb = sys.exc_info() - line_number = tb.tb_lineno - - _LOGGER.error(f"Failed to delete_entity, Error: {ex}, Line: {line_number}") - - async def dispatch_all(self): - if not self._is_initialized: - _LOGGER.info("NOT INITIALIZED - Failed discovering components") - return - - for domain in SIGNALS: - signal = SIGNALS.get(domain) - - async_dispatcher_send(self._hass, signal) diff --git a/custom_components/hpprinter/managers/rest_api.py b/custom_components/hpprinter/managers/rest_api.py new file mode 100644 index 0000000..1752970 --- /dev/null +++ b/custom_components/hpprinter/managers/rest_api.py @@ -0,0 +1,423 @@ +import json +import logging +import sys + +from aiohttp import ClientResponseError, ClientSession, ClientTimeout, TCPConnector +from defusedxml import ElementTree +from flatten_json import flatten +import xmltodict + +from homeassistant.const import CONF_HOST +from homeassistant.helpers.aiohttp_client import ( + ENABLE_CLEANUP_CLOSED, + MAXIMUM_CONNECTIONS, + MAXIMUM_CONNECTIONS_PER_HOST, +) +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.util import slugify, ssl +from homeassistant.util.ssl import SSLCipherList + +from ..common.consts import IGNORED_KEYS, SIGNAL_HA_DEVICE_DISCOVERED +from ..models.config_data import ConfigData +from ..models.exceptions import IntegrationAPIError, IntegrationParameterError +from .ha_config_manager import HAConfigManager + +_LOGGER = logging.getLogger(__name__) + + +class RestAPIv2: + def __init__(self, hass, config_manager: HAConfigManager): + self._loop = hass.loop + self._config_manager = config_manager + self._hass = hass + + self._session: ClientSession | None = None + + self._data: dict = {} + self._data_config: dict = {} + + self._raw_data: dict = {} + + self._is_connected: bool = False + + self._device_dispatched: list[str] = [] + self._all_endpoints: list[str] = [] + self._support_prefetch: bool = False + + @property + def data(self) -> dict | None: + return self._data + + @property + def data_config(self) -> dict | None: + return self._data_config + + @property + def raw_data(self) -> dict | None: + return self._raw_data + + @property + def config_data(self) -> ConfigData | None: + if self._config_manager is not None: + return self._config_manager.config_data + + return None + + async def terminate(self): + _LOGGER.info("Terminating session to HP Printer EWS") + + self._is_connected = False + + if self._session is not None: + await self._session.close() + + self._session = None + + async def initialize(self, throw_exception: bool = False): + try: + if not self.config_data.hostname: + raise IntegrationParameterError(CONF_HOST) + + if self._session is None: + self._session = ClientSession( + loop=self._loop, connector=self._get_ssl_connector() + ) + + await self._load_metadata() + + except Exception as ex: + if throw_exception: + raise ex + + exc_type, exc_obj, tb = sys.exc_info() + line_number = tb.tb_lineno + + _LOGGER.warning( + f"Failed to initialize session, Error: {ex}, Line: {line_number}" + ) + + def _get_ssl_connector(self): + ssl_context = ssl.create_no_verify_ssl_context(SSLCipherList.INTERMEDIATE) + + connector = TCPConnector( + loop=self._loop, + enable_cleanup_closed=ENABLE_CLEANUP_CLOSED, + ssl=ssl_context, + limit=MAXIMUM_CONNECTIONS, + limit_per_host=MAXIMUM_CONNECTIONS_PER_HOST, + ) + + return connector + + async def _load_metadata(self): + self._all_endpoints = [] + + endpoints = await self._get_request("/Prefetch?type=dtree", True) + + self._support_prefetch = endpoints is not None + is_connected = self._support_prefetch + + if self._support_prefetch: + for endpoint in endpoints: + is_valid = self._config_manager.is_valid_endpoint(endpoint) + + if is_valid: + endpoint_uri = endpoint.get("uri") + + self._all_endpoints.append(endpoint_uri) + + else: + self._all_endpoints = self._config_manager.endpoints.copy() + + await self._update_data(self._config_manager.endpoints, False) + + endpoints_found = len(self._raw_data.keys()) + is_connected = endpoints_found > 0 + available_endpoints = len(self._all_endpoints) + + if is_connected: + _LOGGER.info( + "No support for prefetch endpoint, " + f"{endpoints_found}/{available_endpoints} Endpoints found" + ) + else: + endpoint_urls = ", ".join(self._all_endpoints) + + raise IntegrationAPIError(endpoint_urls) + + self._is_connected = is_connected + + async def update(self): + await self._update_data(self._config_manager.endpoints) + + async def update_full(self): + await self._update_data(self._all_endpoints) + + async def _update_data(self, endpoints: list[str], connectivity_check: bool = True): + if not self._is_connected and connectivity_check: + return + + for endpoint in endpoints: + resource_data = await self._get_request(endpoint) + + self._raw_data[endpoint] = resource_data + + devices = self._get_devices_data() + + self._extract_data(devices) + + def _extract_data(self, devices: list[dict]): + device_data = {} + device_config = {} + + for item in devices: + item_config = item.get("config") + item_data = item.get("data") + + device_type = item_config.get("device_type") + identifier = item_config.get("identifier") + properties = item_config.get("properties") + flat = item_config.get("flat", False) + + device_key = device_type + + if identifier is not None: + identifier_key = identifier.get("key") + identifier_mapping = identifier.get("mapping") + + key_data = item_data.get(identifier_key) + + device_id = ( + item_data.get(identifier_key) + if identifier_mapping is None + else identifier_mapping.get(key_data) + ) + + if flat: + new_items_data = { + slugify(f"{device_id}_{key}"): item_data[key] + for key in item_data + if key != identifier_key + } + + new_properties = { + slugify(f"{device_id}_{key}"): properties[key] + for key in properties + if key != identifier_key + } + + item_data = new_items_data + properties = new_properties + + else: + device_key = f"{device_type}.{device_id}" + + data = device_data[device_key] if device_key in device_data else {} + has_data = len(list(item_data.keys())) > 0 + data.update(item_data) + + if has_data: + device_data[device_key] = data + + if device_key in device_config: + config = device_config[device_key] + config["properties"].update(properties) + + else: + device_config[device_key] = { + "device_type": device_type, + "properties": properties, + } + + self._data_config = device_config + self._data = device_data + + for device_key in self._data: + self.device_data_changed(device_key) + + @staticmethod + def _get_device_from_list( + data: list[dict], identifier_key: str, device_id + ) -> dict | None: + data_items = [ + data_item + for data_item in data + if data_item.get(identifier_key) == device_id + ] + + if data_items: + return data_items[0] + + else: + return None + + def _get_device_config(self, device_type: str) -> dict | None: + data_configs = [ + data_point + for data_point in self._config_manager.data_points + if device_type == data_point.get("name") + ] + + if data_configs: + return data_configs[0] + + else: + return None + + @staticmethod + def _get_data_section(data: dict, path: str) -> dict: + path_parts = path.split(".") + result = data + + if result is not None: + for path_part in path_parts: + result = result.get(path_part) + + return result + + def _get_devices_data(self): + devices = [] + + for data_point in self._config_manager.data_points: + endpoint = data_point.get("endpoint") + path = data_point.get("path") + properties = data_point.get("properties") + + if endpoint is not None: + data_item = self._raw_data.get(endpoint) + + data = self._get_data_section(data_item, path) + + if properties is not None: + if isinstance(data, list): + devices.extend( + [ + self._get_device_data(data_item, properties, data_point) + for data_item in data + ] + ) + + else: + device = self._get_device_data(data, properties, data_point) + + devices.append(device) + + return devices + + @staticmethod + def _get_device_data( + data_item: dict, properties: dict, device_config: dict + ) -> dict: + device_data = {} + + data_item_flat = {} if data_item is None else flatten(data_item, ".") + + for property_key in properties: + property_details = properties.get(property_key) + property_path = property_details.get("path") + + value = data_item_flat.get(property_path) + + if value is not None: + device_data[property_key] = value + + data = {"config": device_config, "data": device_data} + + return data + + async def _get_request( + self, endpoint: str, ignore_error: bool = False + ) -> dict | None: + result: dict | None = None + try: + url = f"{self.config_data.url}{endpoint}" + + timeout = ClientTimeout(connect=3, sock_read=10) + + async with self._session.get(url, timeout=timeout) as response: + response.raise_for_status() + + if response.content_type == "application/javascript": + content = await response.text() + result = json.loads(content) + + else: + content = await response.text() + result = self._clean_data(content) + + if result is not None: + result_keys = list(result.keys()) + root_key = result_keys[0] + + for ignored_key in IGNORED_KEYS: + if ignored_key in result[root_key]: + del result[root_key][ignored_key] + + _LOGGER.debug(f"Request to {url}") + + except ClientResponseError as cre: + if cre.status == 404: + if not ignore_error: + exc_type, exc_obj, tb = sys.exc_info() + line_number = tb.tb_lineno + _LOGGER.debug( + f"Failed to get response from {endpoint}, Error: {cre}, Line: {line_number}" + ) + + else: + if not ignore_error: + exc_type, exc_obj, tb = sys.exc_info() + line_number = tb.tb_lineno + _LOGGER.error( + f"Failed to get response from {endpoint}, Error: {cre}, Line: {line_number}" + ) + + except Exception as ex: + if not ignore_error: + exc_type, exc_obj, tb = sys.exc_info() + line_number = tb.tb_lineno + _LOGGER.error( + f"Failed to get {endpoint}, Error: {ex}, Line: {line_number}" + ) + + return result + + def _clean_data(self, xml) -> dict: + xml_data = ElementTree.fromstring(xml) + + self._strip_namespace(xml_data) + + data = xmltodict.parse(ElementTree.tostring(xml_data)) + + return data + + def _strip_namespace(self, el): + if el.tag.startswith("{"): + el.tag = el.tag.split("}", 1)[1] + + keys = list(el.attrib.keys()) + + for k in keys: + if k.startswith("{"): + k2 = k.split("}", 1)[1] + el.attrib[k2] = el.attrib[k] + del el.attrib[k] + + for child in el: + self._strip_namespace(child) + + def device_data_changed(self, device_key: str): + device_data = self._data.get(device_key) + device_config = self._data_config.get(device_key) + + if device_key not in self._device_dispatched: + self._device_dispatched.append(device_key) + + dispatcher_send( + self._hass, + SIGNAL_HA_DEVICE_DISCOVERED, + self._config_manager.entry_id, + device_key, + device_data, + device_config, + ) diff --git a/custom_components/hpprinter/managers/storage_manager.py b/custom_components/hpprinter/managers/storage_manager.py deleted file mode 100644 index 5773b73..0000000 --- a/custom_components/hpprinter/managers/storage_manager.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Storage handlers.""" -import logging - -from homeassistant.helpers.json import JSONEncoder -from homeassistant.helpers.storage import Store -from homeassistant.util import slugify - -from ..helpers.const import * -from ..models.config_data import ConfigData -from .configuration_manager import ConfigManager - -STORAGE_VERSION = 1 - -_LOGGER = logging.getLogger(__name__) - - -class StorageManager: - def __init__(self, hass, config_manager: ConfigManager): - self._hass = hass - self._config_manager = config_manager - - @property - def config_data(self) -> ConfigData: - config_data = None - - if self._config_manager is not None: - config_data = self._config_manager.data - - return config_data - - @property - def file_name(self): - file_name = f".{DOMAIN}.{slugify(self.config_data.name)}" - - return file_name - - async def async_load_from_store(self): - """Load the retained data from store and return de-serialized data.""" - store = Store(self._hass, STORAGE_VERSION, self.file_name, encoder=JSONEncoder) - - data = await store.async_load() - - return data - - async def async_save_to_store(self, data): - """Generate dynamic data to store and save it to the filesystem.""" - store = Store(self._hass, STORAGE_VERSION, self.file_name, encoder=JSONEncoder) - - await store.async_save(data) diff --git a/custom_components/hpprinter/manifest.json b/custom_components/hpprinter/manifest.json index f2fb8c8..b633132 100644 --- a/custom_components/hpprinter/manifest.json +++ b/custom_components/hpprinter/manifest.json @@ -8,6 +8,6 @@ "documentation": "https://github.com/elad-bar/ha-hpprinter", "iot_class": "local_polling", "issue_tracker": "https://github.com/elad-bar/ha-hpprinter/issues", - "requirements": ["xmltodict==0.12.0"], - "version": "1.0.12" + "requirements": ["xmltodict~=0.13.0", "flatten_json", "defusedxml"], + "version": "2.0.0" } diff --git a/custom_components/hpprinter/models/base_entity.py b/custom_components/hpprinter/models/base_entity.py deleted file mode 100644 index 58bd169..0000000 --- a/custom_components/hpprinter/models/base_entity.py +++ /dev/null @@ -1,153 +0,0 @@ -import logging -import sys -from typing import Any, Callable, Optional - -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity - -from ..helpers import get_ha -from ..helpers.const import * -from .entity_data import EntityData - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_base_entry( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities, - domain: str, - component: Callable[[HomeAssistant, Any, EntityData], Any], -): - """Set up HP Printer based off an entry.""" - _LOGGER.debug(f"Starting async_setup_entry {domain}") - - try: - entry_data = entry.data - name = entry_data.get(CONF_NAME) - - ha = get_ha(hass, name) - entity_manager = ha.entity_manager - entity_manager.set_domain_component(domain, async_add_entities, component) - except Exception as ex: - exc_type, exc_obj, tb = sys.exc_info() - line_number = tb.tb_lineno - - _LOGGER.error(f"Failed to load {domain}, error: {ex}, line: {line_number}") - - -class HPPrinterEntity(Entity): - """Representation a binary sensor that is updated by HP Printer.""" - - hass: HomeAssistant = None - integration_name: str = None - entity: EntityData = None - remove_dispatcher = None - current_domain: str = None - - ha = None - entity_manager = None - device_manager = None - - def initialize( - self, - hass: HomeAssistant, - integration_name: str, - entity: EntityData, - current_domain: str, - ): - self.hass = hass - self.integration_name = integration_name - self.entity = entity - self.remove_dispatcher = None - self.current_domain = current_domain - - self.ha = get_ha(self.hass, self.integration_name) - - if self.ha is None: - _LOGGER.error( - f"HPPrinterHomeAssistant was not found for {self.integration_name}" - ) - - else: - self.entity_manager = self.ha.entity_manager - self.device_manager = self.ha.device_manager - - @property - def unique_id(self) -> Optional[str]: - """Return the name of the node.""" - return self.entity.unique_id - - @property - def device_info(self): - return self.device_manager.get(self.entity.device_name) - - @property - def name(self): - """Return the name of the node.""" - return self.entity.name - - @property - def icon(self): - """Return the name of the node.""" - return self.entity.icon - - @property - def should_poll(self): - """Return the polling state.""" - return False - - @property - def extra_state_attributes(self): - """Return true if the binary sensor is on.""" - return self.entity.attributes - - async def async_added_to_hass(self): - """Register callbacks.""" - async_dispatcher_connect( - self.hass, SIGNALS[self.current_domain], self._schedule_immediate_update - ) - - await self.async_added_to_hass_local() - - async def async_will_remove_from_hass(self) -> None: - if self.remove_dispatcher is not None: - self.remove_dispatcher() - self.remove_dispatcher = None - - await self.async_will_remove_from_hass_local() - - @callback - def _schedule_immediate_update(self): - self.hass.async_create_task(self._async_schedule_immediate_update()) - - async def _async_schedule_immediate_update(self): - if self.entity_manager is None: - _LOGGER.debug( - f"Cannot update {self.current_domain} - Entity Manager is None | {self.name}" - ) - else: - if self.entity is not None: - previous_state = self.entity.state - - entity = self.entity_manager.get_entity(self.current_domain, self.name) - - if entity.disabled: - _LOGGER.debug(f"Skip updating {self.name}, Entity is disabled") - - else: - self.entity = entity - if self.entity is not None: - self._immediate_update(previous_state) - - async def async_added_to_hass_local(self): - pass - - async def async_will_remove_from_hass_local(self): - pass - - def _immediate_update(self, previous_state: int): - self.async_schedule_update_ha_state(True) diff --git a/custom_components/hpprinter/models/config_data.py b/custom_components/hpprinter/models/config_data.py index 97dac6a..8e5d1de 100644 --- a/custom_components/hpprinter/models/config_data.py +++ b/custom_components/hpprinter/models/config_data.py @@ -1,47 +1,76 @@ -from typing import Any +import voluptuous as vol +from voluptuous import Schema -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_SSL +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL -from ..helpers.const import * +from ..common.consts import DEFAULT_PORT, PROTOCOLS class ConfigData: - name: str - host: str - ssl: bool - port: int - should_store: bool - update_interval: int - log_level: str - file_reader: Any + _host: str | None + _ssl: bool | None + _port: int | None def __init__(self): - self.name = "" - self.host = "" - self.ssl = False - self.port = 80 - self.should_store = False - self.update_interval = 60 - self.log_level = LOG_LEVEL_DEFAULT - self.file_reader = None + self._host = "" + self._ssl = False + self._port = DEFAULT_PORT + + @property + def hostname(self) -> str: + return self._host + + @property + def port(self) -> int: + return self._port + + @property + def is_ssl(self) -> bool: + return self._ssl @property def protocol(self): - protocol = PROTOCOLS[self.ssl] + protocol = PROTOCOLS[self._ssl] return protocol + @property + def url(self): + url = f"{self.protocol}://{self.hostname}:{self.port}" + + return url + + def update(self, data: dict): + self._ssl = str(data.get(CONF_SSL, False)).lower() == str(True).lower() + self._host = data.get(CONF_HOST) + self._port = data.get(CONF_PORT, DEFAULT_PORT) + + if self._port is None: + self._port = DEFAULT_PORT + + def to_dict(self): + obj = {CONF_HOST: self._host, CONF_PORT: self._port, CONF_SSL: self._ssl} + + return obj + def __repr__(self): - obj = { - CONF_NAME: self.name, - CONF_HOST: self.host, - CONF_SSL: self.ssl, - CONF_PORT: self.port, - CONF_STORE_DATA: self.should_store, - CONF_UPDATE_INTERVAL: self.update_interval, - CONF_LOG_LEVEL: self.log_level, + to_string = f"{self.to_dict()}" + + return to_string + + @staticmethod + def default_schema(user_input: dict | None) -> Schema: + if user_input is None: + user_input = {} + + new_user_input = { + vol.Required(CONF_HOST, default=user_input.get(CONF_HOST)): str, + vol.Required( + CONF_PORT, default=user_input.get(CONF_PORT, DEFAULT_PORT) + ): int, + vol.Optional(CONF_SSL, default=user_input.get(CONF_SSL, False)): bool, } - to_string = f"{obj}" + schema = vol.Schema(new_user_input) - return to_string + return schema diff --git a/custom_components/hpprinter/models/entity_data.py b/custom_components/hpprinter/models/entity_data.py deleted file mode 100644 index c505fb4..0000000 --- a/custom_components/hpprinter/models/entity_data.py +++ /dev/null @@ -1,52 +0,0 @@ -from typing import Optional - -from homeassistant.components.binary_sensor import BinarySensorDeviceClass -from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass - -from ..helpers.const import * - - -class EntityData: - unique_id: str - name: str - state: int - attributes: dict - icon: str - device_name: str - status: str - disabled: bool - binary_sensor_device_class: Optional[BinarySensorDeviceClass] - sensor_device_class: Optional[SensorDeviceClass] - sensor_state_class: Optional[SensorStateClass] - - def __init__(self): - self.unique_id = "" - self.name = "" - self.state = 0 - self.attributes = {} - self.icon = "" - self.device_name = "" - self.status = ENTITY_STATUS_CREATED - self.disabled = False - self.binary_sensor_device_class = None - self.sensor_device_class = None - self.sensor_state_class = None - - def __repr__(self): - obj = { - ENTITY_NAME: self.name, - ENTITY_STATE: self.state, - ENTITY_ATTRIBUTES: self.attributes, - ENTITY_ICON: self.icon, - ENTITY_DEVICE_NAME: self.device_name, - ENTITY_STATUS: self.status, - ENTITY_UNIQUE_ID: self.unique_id, - ENTITY_DISABLED: self.disabled, - ENTITY_BINARY_SENSOR_DEVICE_CLASS: self.binary_sensor_device_class, - ENTITY_SENSOR_DEVICE_CLASS: self.sensor_device_class, - ENTITY_SENSOR_STATE_CLASS: self.sensor_state_class, - } - - to_string = f"{obj}" - - return to_string diff --git a/custom_components/hpprinter/models/exceptions.py b/custom_components/hpprinter/models/exceptions.py new file mode 100644 index 0000000..e255c6b --- /dev/null +++ b/custom_components/hpprinter/models/exceptions.py @@ -0,0 +1,19 @@ +from homeassistant.exceptions import HomeAssistantError + + +class IntegrationParameterError(HomeAssistantError): + def __init__(self, parameter): + self._parameter = parameter + self._message = f"Invalid parameter value provided, Parameter: {parameter}" + + def __str__(self): + return self._message + + +class IntegrationAPIError(HomeAssistantError): + def __init__(self, url): + self._url = url + self._message = f"Failed to connect to URL: {url}" + + def __str__(self): + return self._message diff --git a/custom_components/hpprinter/parameters/data_points.json b/custom_components/hpprinter/parameters/data_points.json new file mode 100644 index 0000000..29fe500 --- /dev/null +++ b/custom_components/hpprinter/parameters/data_points.json @@ -0,0 +1,375 @@ +[ + { + "name": "Main", + "endpoint": "/DevMgmt/ProductConfigDyn.xml", + "path": "ProductConfigDyn.ProductInformation", + "device_type": "Main", + "properties": { + "make_and_model": { + "path": "MakeAndModel" + }, + "make_and_model_family": { + "path": "MakeAndModelFamily" + }, + "sku_identifier": { + "path": "SKUIdentifier" + }, + "serial_number": { + "path": "SerialNumber" + }, + "product_number": { + "path": "ProductNumber" + }, + "manufacturer_name": { + "path": "Manufacturer.Name" + }, + "manufacture_at": { + "path": "Manufacturer.Date", + "platform": "sensor", + "device_class": "timestamp" + } + } + }, + { + "name": "Consumable", + "endpoint": "/DevMgmt/ConsumableConfigDyn.xml", + "path": "ConsumableConfigDyn.ConsumableInfo", + "device_type": "Consumable", + "identifier": { + "key": "consumable_label_code" + }, + "properties": { + "consumable_label_code": { + "path": "ConsumableLabelCode" + }, + "consumable_life_state_consumable_state": { + "path": "ConsumableLifeState.ConsumableState", + "platform": "binary_sensor", + "on_values": ["ok", "newGenuineHP"], + "device_class": "plug" + }, + "consumable_life_state_brand": { + "path": "ConsumableLifeState.Brand" + }, + "consumable_station": { + "path": "ConsumableStation", + "platform": "sensor" + }, + "consumable_type_enum": { + "path": "ConsumableTypeEnum", + "platform": "sensor", + "device_class": "enum", + "options": ["ink", "inkcartridge", "printhead", "toner"] + }, + "installation_date": { + "path": "Installation.Date", + "platform": "sensor", + "device_class": "timestamp" + }, + "capacity_max_capacity": { + "path": "Capacity.MaxCapacity" + }, + "consumable_percentage_level_remaining": { + "path": "ConsumablePercentageLevelRemaining", + "platform": "sensor", + "unit_of_measurement": "%", + "exclude": { + "consumable_type_enum": "printhead" + } + }, + "consumable_selectibility_number": { + "path": "ConsumableSelectibilityNumber" + }, + "manufacturer_name": { + "path": "Manufacturer.Name" + }, + "manufacture_at": { + "path": "Manufacturer.Date", + "platform": "sensor", + "device_class": "timestamp", + "exclude": { + "consumable_type_enum": "printhead" + } + }, + "serial_number": { + "path": "SerialNumber" + }, + "product_number": { + "path": "ProductNumber" + }, + "warranty_expiration_date": { + "path": "Warranty.ExpirationDate", + "platform": "sensor", + "device_class": "timestamp", + "exclude": { + "consumable_type_enum": "printhead" + } + }, + "consumable_unique_id": { + "path": "ConsumableUniqueID" + } + } + }, + { + "name": "Consumable Usage", + "endpoint": "/DevMgmt/ProductUsageDyn.xml", + "path": "ProductUsageDyn.ConsumableSubunit.Consumable", + "device_type": "Consumable", + "identifier": { + "key": "marker_color", + "mapping": { + "Cyan": "C", + "Yellow": "Y", + "Magenta": "M", + "CyanMagentaYellow": "CMY", + "Black": "K" + } + }, + "properties": { + "consumable_station": { + "path": "ConsumableStation" + }, + "marker_color": { + "path": "MarkerColor" + }, + "estimated_pages_remaining": { + "path": "EstimatedPagesRemaining", + "platform": "sensor", + "unit_of_measurement": "pages", + "exclude": { + "consumable_type_enum": "printhead" + } + }, + "consumable_state": { + "path": "ConsumableState" + }, + "consumable_raw_percentage_level_remaining": { + "path": "ConsumableRawPercentageLevelRemaining" + }, + "supply_serial_number": { + "path": "SupplySerialNumber.#text" + }, + "refilled_count_counterfeit_refilled_count": { + "path": "RefilledCount.CounterfeitRefilledCount.#text", + "platform": "sensor", + "unit_of_measurement": "refills", + "icon": "mdi:format-color-fill" + }, + "refilled_count_genuine_refilled_count": { + "path": "RefilledCount.GenuineRefilledCount", + "platform": "sensor", + "unit_of_measurement": "refills", + "icon": "mdi:format-color-fill" + } + } + }, + { + "name": "Printer", + "endpoint": "/DevMgmt/ProductUsageDyn.xml", + "path": "ProductUsageDyn.PrinterSubunit", + "device_type": "Printer", + "properties": { + "total_impressions": { + "path": "TotalImpressions.#text", + "platform": "sensor", + "unit_of_measurement": "pages", + "icon": "mdi:file-document-check" + }, + "monochrome_impressions": { + "path": "MonochromeImpressions", + "platform": "sensor", + "unit_of_measurement": "pages", + "icon": "mdi:file-document-check" + }, + "color_impressions": { + "path": "ColorImpressions", + "platform": "sensor", + "unit_of_measurement": "pages", + "icon": "mdi:file-document-check" + }, + "simplex_sheets": { + "path": "SimplexSheets", + "platform": "sensor", + "unit_of_measurement": "pages", + "icon": "mdi:file-document-check" + }, + "duplex_sheets": { + "path": "DuplexSheets.#text", + "platform": "sensor", + "unit_of_measurement": "pages", + "icon": "mdi:file-document-multiple" + }, + "jam_events": { + "path": "JamEvents.#text", + "platform": "sensor", + "unit_of_measurement": "pages", + "icon": "mdi:file-document-remove" + }, + "mispick_events": { + "path": "MispickEvents", + "platform": "sensor", + "unit_of_measurement": "pages", + "icon": "mdi:file-document-minus" + } + } + }, + { + "name": "Scanner", + "endpoint": "/DevMgmt/ProductUsageDyn.xml", + "path": "ProductUsageDyn.ScannerEngineSubunit", + "device_type": "Scanner", + "properties": { + "scan_images": { + "path": "ScanImages.#text", + "platform": "sensor", + "unit_of_measurement": "pages", + "icon": "mdi:credit-card-scan" + }, + "adf_images": { + "path": "AdfImages.#text", + "platform": "sensor", + "unit_of_measurement": "pages", + "icon": "mdi:credit-card-scan" + }, + "duplex_sheets": { + "path": "DuplexSheets.#text", + "platform": "sensor", + "unit_of_measurement": "pages", + "icon": "mdi:credit-card-scan" + }, + "flatbed_images": { + "path": "FlatbedImages", + "platform": "sensor", + "unit_of_measurement": "pages", + "icon": "mdi:credit-card-scan" + }, + "jam_events": { + "path": "JamEvents", + "platform": "sensor", + "unit_of_measurement": "pages", + "icon": "mdi:credit-card-scan" + }, + "mispick_events": { + "path": "MispickEvents", + "platform": "sensor", + "unit_of_measurement": "pages", + "icon": "mdi:credit-card-scan" + } + } + }, + { + "name": "Copy", + "endpoint": "/DevMgmt/ProductUsageDyn.xml", + "path": "ProductUsageDyn.CopyApplicationSubunit", + "device_type": "Copy", + "properties": { + "total_impressions": { + "path": "TotalImpressions.#text", + "platform": "sensor", + "unit_of_measurement": "pages", + "icon": "mdi:content-copy" + }, + "adf_images": { + "path": "AdfImages", + "platform": "sensor", + "unit_of_measurement": "pages", + "icon": "mdi:content-copy" + }, + "flatbed_images": { + "path": "FlatbedImages", + "platform": "sensor", + "unit_of_measurement": "pages", + "icon": "mdi:content-copy" + }, + "monochrome_impressions": { + "path": "MonochromeImpressions", + "platform": "sensor", + "unit_of_measurement": "pages", + "icon": "mdi:content-copy" + }, + "color_impressions": { + "path": "ColorImpressions", + "platform": "sensor", + "unit_of_measurement": "pages", + "icon": "mdi:content-copy" + } + } + }, + { + "name": "Fax", + "endpoint": "/DevMgmt/ProductUsageDyn.xml", + "path": "ProductUsageDyn.FaxApplicationSubunit", + "device_type": "Fax", + "properties": { + "total_impressions": { + "path": "TotalImpressions.#text", + "platform": "sensor", + "unit_of_measurement": "pages", + "icon": "mdi:email-fast" + } + } + }, + { + "name": "Adapter", + "endpoint": "/IoMgmt/Adapters", + "path": "Adapters.Adapter", + "device_type": "Main", + "identifier": { + "key": "hardware_config_name" + }, + "flat": true, + "properties": { + "hardware_config_name": { + "path": "HardwareConfig.Name" + }, + "hardware_config_device_connectivity_port_type": { + "path": "HardwareConfig.DeviceConnectivityPortType", + "platform": "sensor" + }, + "hardware_config_is_connected": { + "path": "HardwareConfig.IsConnected", + "platform": "binary_sensor", + "on_values": ["true"], + "device_class": "connectivity" + } + } + }, + { + "name": "ePrint", + "endpoint": "/ePrint/ePrintConfigDyn.xml", + "path": "ePrintConfigDyn", + "device_type": "Main", + "properties": { + "printer_id": { + "path": "PrinterID" + }, + "registration_state": { + "path": "RegistrationState", + "platform": "binary_sensor", + "on_values": ["registered"], + "device_class": "plug", + "icon": "mdi:cloud-print" + }, + "cloud_services_switch_status": { + "path": "CloudServicesSwitch.Status", + "platform": "binary_sensor", + "on_values": ["enabled"], + "device_class": "connectivity" + } + } + }, + { + "name": "Wifi", + "endpoint": "/DevMgmt/NetAppsSecureDyn.xml", + "path": "NetAppsSecureDyn.WirelessDirectConfig", + "device_type": "Main", + "properties": { + "ssid_prefix": { + "path": "SSIDPrefix" + }, + "connection_method": { + "path": "ConnectionMethod" + } + } + } +] diff --git a/custom_components/hpprinter/parameters/endpoint_validations.json b/custom_components/hpprinter/parameters/endpoint_validations.json new file mode 100644 index 0000000..dba3741 --- /dev/null +++ b/custom_components/hpprinter/parameters/endpoint_validations.json @@ -0,0 +1,18 @@ +{ + "exclude_type": ["ns", "feature", "manifest"], + "exclude_uri": [ + "/DevMgmt/InternalPrintDyn.xml", + "/Scan/SPF", + "/Jobs/JobList", + "/CachedData/Info", + "/CachedData/Files", + "/ePrint/EmailAddress", + "/ePrint/PrinterSignature", + "/ePrint/XMPPConfiguration", + "/ePrint/ClaimInfo", + "/IoMgmt/Adapters/Wifi1/ClientList", + "/WalkupScanToComp/WalkupScanToCompEvent", + "/FirmwareUpdate/FirmwareUpdateDyn.xml", + "/FirmwareUpdate/WebFWUpdate/State" + ] +} diff --git a/custom_components/hpprinter/sensor.py b/custom_components/hpprinter/sensor.py index 5c5abbc..b3c0cea 100644 --- a/custom_components/hpprinter/sensor.py +++ b/custom_components/hpprinter/sensor.py @@ -1,73 +1,73 @@ -""" -Support for HP Printer binary sensors. -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.hp_printer/ -""" -from __future__ import annotations - +from datetime import datetime import logging -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntity, - SensorStateClass, -) +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, Platform from homeassistant.core import HomeAssistant -from .helpers.const import * -from .models.base_entity import HPPrinterEntity, async_setup_base_entry -from .models.entity_data import EntityData +from .common.base_entity import BaseEntity, async_setup_base_entry +from .common.consts import NUMERIC_UNITS_OF_MEASUREMENT +from .common.entity_descriptions import IntegrationSensorEntityDescription +from .managers.ha_coordinator import HACoordinator _LOGGER = logging.getLogger(__name__) -CURRENT_DOMAIN = DOMAIN_SENSOR - - -def get_device_tracker(hass: HomeAssistant, integration_name: str, entity: EntityData): - sensor = HPPrinterSensor() - sensor.initialize(hass, integration_name, entity, CURRENT_DOMAIN) - - return sensor - -async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities): - """Set up HP Printer based off an entry.""" +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +): await async_setup_base_entry( - hass, entry, async_add_entities, CURRENT_DOMAIN, get_device_tracker + hass, + entry, + Platform.SENSOR, + HASensorEntity, + async_add_entities, ) -async def async_unload_entry(_hass, config_entry): - _LOGGER.info(f"async_unload_entry {CURRENT_DOMAIN}: {config_entry}") +class HASensorEntity(BaseEntity, SensorEntity): + """Representation of a sensor.""" + + def __init__( + self, + entity_description: IntegrationSensorEntityDescription, + coordinator: HACoordinator, + device_key: str, + ): + super().__init__(entity_description, coordinator, device_key) + + self._attr_device_class = entity_description.device_class + self._attr_native_unit_of_measurement = ( + entity_description.native_unit_of_measurement + ) - return True + self._set_value() + def _set_value(self): + state = self.get_value() -class HPPrinterSensor(SensorEntity, HPPrinterEntity): - """Representation a binary sensor that is updated by HP Printer.""" + if state is not None: + if self.native_unit_of_measurement in [PERCENTAGE]: + state = float(state) - @property - def native_value(self): - """Return the state of the sensor.""" - return self.entity.state + elif self.native_unit_of_measurement in NUMERIC_UNITS_OF_MEASUREMENT: + state = int(state) - @property - def device_class(self) -> SensorDeviceClass | str | None: - """Return the class of this sensor.""" - return self.entity.sensor_device_class + if self.device_class == SensorDeviceClass.DATE: + state = datetime.fromisoformat(state) - @property - def state_class(self) -> SensorStateClass | str | None: - """Return the class of this sensor.""" - return self.entity.sensor_state_class + elif self.device_class == SensorDeviceClass.TIMESTAMP: + tz = datetime.now().astimezone().tzinfo + ts = datetime.fromisoformat(state).timestamp() + state = datetime.fromtimestamp(ts, tz=tz) - async def async_added_to_hass_local(self): - _LOGGER.info(f"Added new {self.name}") + elif self.device_class == SensorDeviceClass.ENUM: + state = state.lower() - def _immediate_update(self, previous_state: bool): - if previous_state != self.entity.state: - _LOGGER.debug( - f"{self.name} updated from {previous_state} to {self.entity.state}" - ) + self._attr_native_value = state - super()._immediate_update(previous_state) + def _handle_coordinator_update(self) -> None: + """Fetch new state parameters for the sensor.""" + self._set_value() + super()._handle_coordinator_update() diff --git a/custom_components/hpprinter/strings.json b/custom_components/hpprinter/strings.json index 9e1beba..bac87c5 100644 --- a/custom_components/hpprinter/strings.json +++ b/custom_components/hpprinter/strings.json @@ -1,40 +1,136 @@ { "config": { + "abort": { + "already_configured": "HP Printer integration ({name}) already configured" + }, + "error": { + "error_400": "Invalid server details, please try manually access to `http://{IP}/DevMgmt/ProductStatusDyn.xml` (Replace placeholder with IP or Hostname), in case it's accessible, please report the issue with logs", + "error_404": "Unsupported API" + }, "step": { "user": { - "title": "Set up HP Printer", "data": { "host": "Host", - "name": "Name" - } + "name": "Name", + "port": "Port number", + "ssl": "Is SSL" + }, + "title": "Set up HP Printer" + } + } + }, + "entity": { + "binary_sensor": { + "consumable_life_state_consumable_state": { + "name": "Status" + }, + "cloud_services_switch_status": { + "name": "ePrint Status" + }, + "hardware_config_is_connected": { + "name": "Connected" + }, + "registration_state": { + "name": "ePrint Registered" } }, - "abort": { - "already_configured": "HP Printer integration ({name}) already configured" - }, - "error": { - "error_400": "Invalid server details, please try manually access to `http://{IP}//DevMgmt/ProductStatusDyn.xml` (Replace placeholder with IP or Hostname), in case it's accessible, please report the issue with logs", - "error_404": "Unsupported API" + "sensor": { + "consumable_percentage_level_remaining": { + "name": "Level" + }, + "consumable_station": { + "name": "Station" + }, + "consumable_type_enum": { + "name": "Type", + "state": { + "ink": "Ink", + "inkcartridge": "Ink", + "printhead": "Printhead", + "toner": "Toner" + } + }, + "estimated_pages_remaining": { + "name": "Remaining" + }, + "installation_date": { + "name": "Installation Date" + }, + "manufacture_at": { + "name": "Manufacture Date" + }, + "refilled_count_counterfeit_refilled_count": { + "name": "Counterfeit Refilled" + }, + "refilled_count_genuine_refilled_count": { + "name": "Genuine Refilled" + }, + "warranty_expiration_date": { + "name": "Expiration Date" + }, + "adf_images": { + "name": "Total pages from ADF" + }, + "color_impressions": { + "name": "Total color pages" + }, + "flatbed_images": { + "name": "Total pages from scanner glass" + }, + "monochrome_impressions": { + "name": "Total black-and-white pages" + }, + "total_impressions": { + "name": "Total pages" + }, + "hardware_config_device_connectivity_port_type": { + "name": "Port Type" + }, + "duplex_sheets": { + "name": "Total double-sided pages" + }, + "jam_events": { + "name": "Total jams" + }, + "mispick_events": { + "name": "Total miss picks" + }, + "simplex_sheets": { + "name": "Total single-sided pages" + }, + "scan_images": { + "name": "Total pages" + } } }, "options": { + "error": { + "already_configured": "HP Printer integration ({name}) already configured", + "error_400": "Invalid server details, please try manually access to `http://{IP}/DevMgmt/ProductStatusDyn.xml` (Replace placeholder with IP or Hostname), in case it's accessible, please report the issue with logs", + "error_404": "Unsupported API" + }, "step": { "hp_printer_additional_settings": { - "title": "Options for HP Printer.", - "description": "Define additional settings for HP Printer integration", "data": { "host": "Host", - "name": "Name", - "update_interval": "Update interval (Seconds)", "log_level": "Log level", - "store_data": "Should store responses?" - } + "name": "Name", + "store_data": "Should store responses?", + "update_interval": "Update interval (Seconds)" + }, + "description": "Define additional settings for HP Printer integration", + "title": "Options for HP Printer." + }, + "init": { + "data": { + "host": "Hostname or IP", + "port": "Port number", + "ssl": "Is SSL", + "update_interval": "Update interval (Seconds)" + }, + "description": "Define additional settings for HP Printer integration", + "title": "Options for HP Printer." } - }, - "error": { - "error_400": "Invalid server details, please try manually access to `http://{IP}//DevMgmt/ProductStatusDyn.xml` (Replace placeholder with IP or Hostname), in case it's accessible, please report the issue with logs", - "error_404": "Unsupported API", - "already_configured": "HP Printer integration ({name}) already configured" } } } diff --git a/custom_components/hpprinter/translations/de.json b/custom_components/hpprinter/translations/de.json index 452a231..2e9dbc0 100644 --- a/custom_components/hpprinter/translations/de.json +++ b/custom_components/hpprinter/translations/de.json @@ -1,12 +1,135 @@ { "config": { + "abort": { + "already_configured": "HP -Druckerintegration ({Name}) bereits konfiguriert" + }, + "error": { + "error_400": "Ung\u00fcltige Serverdetails, bitte versuchen Sie manuell auf `http: // {ip} // devmgmt/productStatusdyn.xml` (Ersetzen Sie Platzhalter mit IP oder Hostname).", + "error_404": "Nicht unterst\u00fctzte API" + }, "step": { "user": { - "title": "HP Drucker einrichten ", "data": { - "host": "Host", - "name": "Name" + "host": "Gastgeber", + "name": "Name", + "port": "Port-Nummer", + "ssl": "Ist SSL" + }, + "title": "Richten Sie den HP -Drucker ein" + } + } + }, + "entity": { + "binary_sensor": { + "cloud_services_switch_status": { + "name": "ePrint Status" + }, + "consumable_life_state_consumable_state": { + "name": "Status" + }, + "hardware_config_is_connected": { + "name": "In Verbindung gebracht" + }, + "registration_state": { + "name": "ePrint Eingetragen" + } + }, + "sensor": { + "adf_images": { + "name": "Gesamtseiten von ADF" + }, + "color_impressions": { + "name": "Gesamtfarbseiten" + }, + "consumable_percentage_level_remaining": { + "name": "Ebene" + }, + "consumable_station": { + "name": "Bahnhof" + }, + "consumable_type_enum": { + "name": "Typ", + "state": { + "ink": "Tinte", + "inkcartridge": "Tinte", + "printhead": "Druckkopf", + "toner": "Toner" } + }, + "duplex_sheets": { + "name": "Gesamt zweiseitige Seiten" + }, + "estimated_pages_remaining": { + "name": "\u00dcbrig" + }, + "flatbed_images": { + "name": "Gesamtseiten aus Scannerglas" + }, + "hardware_config_device_connectivity_port_type": { + "name": "Port -Typ" + }, + "installation_date": { + "name": "Installationsdatum" + }, + "jam_events": { + "name": "Gesamtmarmelade" + }, + "manufacture_at": { + "name": "Herstellungsdatum" + }, + "mispick_events": { + "name": "Total Miss Picks" + }, + "monochrome_impressions": { + "name": "Gesamtschwarz-Wei\u00df-Seiten" + }, + "refilled_count_counterfeit_refilled_count": { + "name": "Gef\u00e4lscht nachgedacht" + }, + "refilled_count_genuine_refilled_count": { + "name": "Echt nachgedacht" + }, + "scan_images": { + "name": "Alle Seiten" + }, + "simplex_sheets": { + "name": "Gesamt einseitige Seiten" + }, + "total_impressions": { + "name": "Alle Seiten" + }, + "warranty_expiration_date": { + "name": "Verfallsdatum" + } + } + }, + "options": { + "error": { + "already_configured": "HP -Druckerintegration ({Name}) bereits konfiguriert", + "error_400": "Ung\u00fcltige Serverdetails, bitte versuchen Sie manuell auf `http: // {ip} // devmgmt/productStatusdyn.xml` (Ersetzen Sie Platzhalter mit IP oder Hostname).", + "error_404": "Nicht unterst\u00fctzte API" + }, + "step": { + "hp_printer_additional_settings": { + "data": { + "host": "Gastgeber", + "log_level": "Protokollebene", + "name": "Name", + "store_data": "Sollte die Antworten speichern?", + "update_interval": "Aktualisierungsintervall (Sekunden)" + }, + "description": "Definieren Sie zus\u00e4tzliche Einstellungen f\u00fcr die HP -Druckerintegration", + "title": "Optionen f\u00fcr HP -Drucker." + }, + "init": { + "data": { + "host": "Hostname oder IP", + "port": "Port-Nummer", + "ssl": "Ist SSL", + "update_interval": "Aktualisierungsintervall (Sekunden)" + }, + "description": "Definieren Sie zus\u00e4tzliche Einstellungen f\u00fcr die HP -Druckerintegration", + "title": "Optionen f\u00fcr HP -Drucker." } } } diff --git a/custom_components/hpprinter/translations/dk.json b/custom_components/hpprinter/translations/dk.json index 09cb76b..437c2bb 100644 --- a/custom_components/hpprinter/translations/dk.json +++ b/custom_components/hpprinter/translations/dk.json @@ -1,40 +1,136 @@ { "config": { + "abort": { + "already_configured": "HP -printerintegration ({navn}) allerede konfigureret" + }, + "error": { + "error_400": "Ugyldigt serveroplysninger, pr\u00f8v manuelt adgang til `http: // {ip} // devmgmt/produktstatusdyn.xml` (udskift pladsholder med IP eller v\u00e6rtsnavn), hvis det er tilg\u00e6ngeligt, skal du rapportere problemet med logfiler", + "error_404": "Ikke -underst\u00f8ttet API" + }, "step": { "user": { - "title": "Konfigurer HP Printer ", "data": { - "host": "Vært", - "name": "Navn" - } + "host": "V\u00e6rt", + "name": "Navn", + "port": "Portnummer", + "ssl": "Er SSL" + }, + "title": "Opret HP -printer" + } + } + }, + "entity": { + "binary_sensor": { + "cloud_services_switch_status": { + "name": "ePrint Status" + }, + "consumable_life_state_consumable_state": { + "name": "Status" + }, + "hardware_config_is_connected": { + "name": "Tilsluttet" + }, + "registration_state": { + "name": "ePrint Registreret" } }, - "abort": { - "already_configured": "HP Printer integration ({name}) er allerede konfigureret" - }, - "error": { - "error_400": "Ugyldige serveroplysninger, prøv venligst manuelt at få adgang til `http://{IP}//DevMgmt/ProductStatusDyn.xml` (Erstat pladsholder med IP eller værtsnavn ), hvis det er tilgængeligt, bedes du rapportere problemet med logfiler", - "error_404": "Ikke understøttet API" + "sensor": { + "adf_images": { + "name": "Samlede sider fra ADF" + }, + "color_impressions": { + "name": "Samlede farvesider" + }, + "consumable_percentage_level_remaining": { + "name": "Niveau" + }, + "consumable_station": { + "name": "Station" + }, + "consumable_type_enum": { + "name": "Type", + "state": { + "ink": "Bl\u00e6k", + "inkcartridge": "Bl\u00e6k", + "printhead": "Printhead", + "toner": "Toner" + } + }, + "duplex_sheets": { + "name": "Samlede dobbeltsidede sider" + }, + "estimated_pages_remaining": { + "name": "Resterende" + }, + "flatbed_images": { + "name": "Samlede sider fra scannerglas" + }, + "hardware_config_device_connectivity_port_type": { + "name": "Porttype" + }, + "installation_date": { + "name": "Installationsdato" + }, + "jam_events": { + "name": "Samlede syltet\u00f8j" + }, + "manufacture_at": { + "name": "Fremstillingsdato" + }, + "mispick_events": { + "name": "Total Miss Picks" + }, + "monochrome_impressions": { + "name": "Samlede sort-hvide sider" + }, + "refilled_count_counterfeit_refilled_count": { + "name": "Forfalsket genopfyldt" + }, + "refilled_count_genuine_refilled_count": { + "name": "\u00c6gte genopfyldt" + }, + "scan_images": { + "name": "Samlede sider" + }, + "simplex_sheets": { + "name": "Samlede enkeltsidede sider" + }, + "total_impressions": { + "name": "Samlede sider" + }, + "warranty_expiration_date": { + "name": "Udl\u00f8bsdato" + } } }, "options": { + "error": { + "already_configured": "HP -printerintegration ({navn}) allerede konfigureret", + "error_400": "Ugyldigt serveroplysninger, pr\u00f8v manuelt adgang til `http: // {ip} // devmgmt/produktstatusdyn.xml` (udskift pladsholder med IP eller v\u00e6rtsnavn), hvis det er tilg\u00e6ngeligt, skal du rapportere problemet med logfiler", + "error_404": "Ikke -underst\u00f8ttet API" + }, "step": { "hp_printer_additional_settings": { - "title": "Indstillinger for HP Printer.", - "description": "Definer yderligere indstillinger for HP Printer integration ", "data": { - "host": "Vært", + "host": "V\u00e6rt", + "log_level": "Logniveau", "name": "Navn", - "update_interval": "Opdateringsinterval (sekunder)", - "log_level": "Log level", - "store_data": "Skal svar gemmes?" - } + "store_data": "Skal gemme svar?", + "update_interval": "Opdateringsinterval (sekunder)" + }, + "description": "Definer yderligere indstillinger til HP -printerintegration", + "title": "Valgmuligheder til HP -printer." + }, + "init": { + "data": { + "host": "V\u00e6rtsnavn eller IP", + "port": "Portnummer", + "ssl": "Er SSL", + "update_interval": "Opdateringsinterval (sekunder)" + }, + "description": "Definer yderligere indstillinger til HP -printerintegration", + "title": "Valgmuligheder til HP -printer." } - }, - "error": { - "error_400": "Ugyldige serveroplysninger, prøv venligst manuelt at få adgang til `http://{IP}//DevMgmt/ProductStatusDyn.xml` (Erstat pladsholder med IP eller værtsnavn ), hvis det er tilgængeligt, bedes du rapportere problemet med logfiler", - "error_404": "Ikke understøttet API", - "already_configured": "HP Printer integration ({name}) er allerede konfigureret" } } } diff --git a/custom_components/hpprinter/translations/en.json b/custom_components/hpprinter/translations/en.json index 9e1beba..b7f3090 100644 --- a/custom_components/hpprinter/translations/en.json +++ b/custom_components/hpprinter/translations/en.json @@ -1,40 +1,136 @@ { "config": { + "abort": { + "already_configured": "HP Printer integration ({name}) already configured" + }, + "error": { + "error_400": "Invalid server details, please try manually access to `http://{IP}/DevMgmt/ProductStatusDyn.xml` (Replace placeholder with IP or Hostname), in case it's accessible, please report the issue with logs", + "error_404": "Unsupported API" + }, "step": { "user": { - "title": "Set up HP Printer", "data": { "host": "Host", - "name": "Name" - } + "name": "Name", + "port": "Port number", + "ssl": "Is SSL" + }, + "title": "Set up HP Printer" + } + } + }, + "entity": { + "binary_sensor": { + "cloud_services_switch_status": { + "name": "ePrint Status" + }, + "consumable_life_state_consumable_state": { + "name": "Status" + }, + "hardware_config_is_connected": { + "name": "Connected" + }, + "registration_state": { + "name": "ePrint Registered" } }, - "abort": { - "already_configured": "HP Printer integration ({name}) already configured" - }, - "error": { - "error_400": "Invalid server details, please try manually access to `http://{IP}//DevMgmt/ProductStatusDyn.xml` (Replace placeholder with IP or Hostname), in case it's accessible, please report the issue with logs", - "error_404": "Unsupported API" + "sensor": { + "adf_images": { + "name": "Total pages from ADF" + }, + "color_impressions": { + "name": "Total color pages" + }, + "consumable_percentage_level_remaining": { + "name": "Level" + }, + "consumable_station": { + "name": "Station" + }, + "consumable_type_enum": { + "name": "Type", + "state": { + "ink": "Ink", + "inkcartridge": "Ink", + "printhead": "Printhead", + "toner": "Toner" + } + }, + "duplex_sheets": { + "name": "Total double-sided pages" + }, + "estimated_pages_remaining": { + "name": "Remaining" + }, + "flatbed_images": { + "name": "Total pages from scanner glass" + }, + "hardware_config_device_connectivity_port_type": { + "name": "Port Type" + }, + "installation_date": { + "name": "Installation Date" + }, + "jam_events": { + "name": "Total jams" + }, + "manufacture_at": { + "name": "Manufacture Date" + }, + "mispick_events": { + "name": "Total miss picks" + }, + "monochrome_impressions": { + "name": "Total black-and-white pages" + }, + "refilled_count_counterfeit_refilled_count": { + "name": "Counterfeit Refilled" + }, + "refilled_count_genuine_refilled_count": { + "name": "Genuine Refilled" + }, + "scan_images": { + "name": "Total pages" + }, + "simplex_sheets": { + "name": "Total single-sided pages" + }, + "total_impressions": { + "name": "Total pages" + }, + "warranty_expiration_date": { + "name": "Expiration Date" + } } }, "options": { + "error": { + "already_configured": "HP Printer integration ({name}) already configured", + "error_400": "Invalid server details, please try manually access to `http://{IP}/DevMgmt/ProductStatusDyn.xml` (Replace placeholder with IP or Hostname), in case it's accessible, please report the issue with logs", + "error_404": "Unsupported API" + }, "step": { "hp_printer_additional_settings": { - "title": "Options for HP Printer.", - "description": "Define additional settings for HP Printer integration", "data": { "host": "Host", - "name": "Name", - "update_interval": "Update interval (Seconds)", "log_level": "Log level", - "store_data": "Should store responses?" - } + "name": "Name", + "store_data": "Should store responses?", + "update_interval": "Update interval (Seconds)" + }, + "description": "Define additional settings for HP Printer integration", + "title": "Options for HP Printer." + }, + "init": { + "data": { + "host": "Hostname or IP", + "port": "Port number", + "ssl": "Is SSL", + "update_interval": "Update interval (Seconds)" + }, + "description": "Define additional settings for HP Printer integration", + "title": "Options for HP Printer." } - }, - "error": { - "error_400": "Invalid server details, please try manually access to `http://{IP}//DevMgmt/ProductStatusDyn.xml` (Replace placeholder with IP or Hostname), in case it's accessible, please report the issue with logs", - "error_404": "Unsupported API", - "already_configured": "HP Printer integration ({name}) already configured" } } } diff --git a/custom_components/hpprinter/translations/es.json b/custom_components/hpprinter/translations/es.json index d1ba5a0..536ab21 100644 --- a/custom_components/hpprinter/translations/es.json +++ b/custom_components/hpprinter/translations/es.json @@ -1,40 +1,136 @@ { "config": { + "abort": { + "already_configured": "Integraci\u00f3n de la impresora HP ({nombre}) ya configurada" + }, + "error": { + "error_400": "Detalles del servidor no v\u00e1lidos, intente acceso manualmente a `http: // {ip} // devmgmt/productStatusDyn.xml` (reemplace el marcador de posici\u00f3n con IP o nombre de host), en caso de que est\u00e9 accesible, informe el problema con los registros", + "error_404": "API sin apoyo" + }, "step": { "user": { - "title": "Configurar impresora HP", "data": { - "host": "Host", - "name": "Nombre" - } + "host": "Anfitriona Anfitri\u00f3n", + "name": "Nombre", + "port": "N\u00famero de puerto", + "ssl": "Es ssl" + }, + "title": "Configurar la impresora HP" + } + } + }, + "entity": { + "binary_sensor": { + "cloud_services_switch_status": { + "name": "ePrint Estado" + }, + "consumable_life_state_consumable_state": { + "name": "Estado" + }, + "hardware_config_is_connected": { + "name": "Conectada Conectado" + }, + "registration_state": { + "name": "ePrint Registrado" } }, - "abort": { - "already_configured": "La integración de la impresora HP ({name}) ya está configurada" - }, - "error": { - "error_400": "Los detalles del servidor no son válidos", - "error_404": "API no compatible" + "sensor": { + "adf_images": { + "name": "P\u00e1ginas totales de ADF" + }, + "color_impressions": { + "name": "P\u00e1ginas de colores totales" + }, + "consumable_percentage_level_remaining": { + "name": "Nivel" + }, + "consumable_station": { + "name": "Estaci\u00f3n" + }, + "consumable_type_enum": { + "name": "Tipo", + "state": { + "ink": "Tinta", + "inkcartridge": "Tinta", + "printhead": "Cabezal", + "toner": "Virador" + } + }, + "duplex_sheets": { + "name": "P\u00e1ginas totales de doble cara" + }, + "estimated_pages_remaining": { + "name": "Restante" + }, + "flatbed_images": { + "name": "P\u00e1ginas totales del vidrio del esc\u00e1ner" + }, + "hardware_config_device_connectivity_port_type": { + "name": "Tipo de puerto" + }, + "installation_date": { + "name": "Fecha de instalaci\u00f3n" + }, + "jam_events": { + "name": "Mermeladas totales" + }, + "manufacture_at": { + "name": "Fecha de fabricacion" + }, + "mispick_events": { + "name": "Total de las selecciones de la se\u00f1orita" + }, + "monochrome_impressions": { + "name": "Total de p\u00e1ginas en blanco y negro" + }, + "refilled_count_counterfeit_refilled_count": { + "name": "Falsificado" + }, + "refilled_count_genuine_refilled_count": { + "name": "Regilado genuino" + }, + "scan_images": { + "name": "Paginas totales" + }, + "simplex_sheets": { + "name": "P\u00e1ginas totales de un solo lado" + }, + "total_impressions": { + "name": "Paginas totales" + }, + "warranty_expiration_date": { + "name": "Fecha de caducidad" + } } }, "options": { + "error": { + "already_configured": "Integraci\u00f3n de la impresora HP ({nombre}) ya configurada", + "error_400": "Detalles del servidor no v\u00e1lidos, intente acceso manualmente a `http: // {ip} // devmgmt/productStatusDyn.xml` (reemplace el marcador de posici\u00f3n con IP o nombre de host), en caso de que est\u00e9 accesible, informe el problema con los registros", + "error_404": "API sin apoyo" + }, "step": { "hp_printer_additional_settings": { - "title": "Opciones para la impresora HP.", - "description": "Definir configuraciones adicionales para la integración de la impresora HP", "data": { - "host": "Host", + "host": "Anfitriona Anfitri\u00f3n", + "log_level": "Nivel de registro", "name": "Nombre", - "update_interval": "Intervalo de actualización (segundos)", - "log_level": "Nivel del registro", - "store_data": "Debería almacenar respuestas?" - } + "store_data": "\u00bfDeber\u00edan las respuestas de la tienda?", + "update_interval": "Intervalo de actualizaci\u00f3n (segundos)" + }, + "description": "Definir configuraciones adicionales para la integraci\u00f3n de la impresora HP", + "title": "Opciones para la impresora HP." + }, + "init": { + "data": { + "host": "Nombre de host o IP", + "port": "N\u00famero de puerto", + "ssl": "Es ssl", + "update_interval": "Intervalo de actualizaci\u00f3n (segundos)" + }, + "description": "Definir configuraciones adicionales para la integraci\u00f3n de la impresora HP", + "title": "Opciones para la impresora HP." } - }, - "error": { - "error_400": "Los detalles del servidor no son válidos", - "error_404": "API no compatible", - "already_configured": "La integración de la impresora HP ({name}) ya está configurada" } } } diff --git a/custom_components/hpprinter/translations/fr.json b/custom_components/hpprinter/translations/fr.json index 231cfa9..51619d3 100644 --- a/custom_components/hpprinter/translations/fr.json +++ b/custom_components/hpprinter/translations/fr.json @@ -1,12 +1,135 @@ { "config": { + "abort": { + "already_configured": "HP Imprimante Integration ({name}) d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "error_400": "D\u00e9tails du serveur non valides, veuillez essayer un acc\u00e8s manuellement \u00e0 `http: // {ip} // devmgmt / productStatusDyn.xml` (Remplacez l'espace r\u00e9serv\u00e9 par IP ou nom d'h\u00f4te), au cas o\u00f9 il est accessible, veuillez signaler le probl\u00e8me avec les journaux", + "error_404": "API non pris en charge" + }, "step": { "user": { - "title": "Configurer l'imprimante HP", "data": { - "host": "Hôte", - "name": "Nom" + "host": "H\u00f4tesse H\u00f4te", + "name": "Nom", + "port": "Num\u00e9ro de port", + "ssl": "Est ssl" + }, + "title": "Configurer l'imprimante HP" + } + } + }, + "entity": { + "binary_sensor": { + "cloud_services_switch_status": { + "name": "ePrint Statut" + }, + "consumable_life_state_consumable_state": { + "name": "Statut" + }, + "hardware_config_is_connected": { + "name": "Connect\u00e9" + }, + "registration_state": { + "name": "ePrint enregistr\u00e9" + } + }, + "sensor": { + "adf_images": { + "name": "Pages totales de l'ADF" + }, + "color_impressions": { + "name": "Pages de couleur totale" + }, + "consumable_percentage_level_remaining": { + "name": "Niveau" + }, + "consumable_station": { + "name": "Gare" + }, + "consumable_type_enum": { + "name": "Taper", + "state": { + "ink": "Encre", + "inkcartridge": "Encre", + "printhead": "T\u00eate d'impression", + "toner": "Toner" } + }, + "duplex_sheets": { + "name": "Pages totales double face" + }, + "estimated_pages_remaining": { + "name": "Restante Restant" + }, + "flatbed_images": { + "name": "Pages totales du verre du scanner" + }, + "hardware_config_device_connectivity_port_type": { + "name": "Type de port" + }, + "installation_date": { + "name": "Date d'installation" + }, + "jam_events": { + "name": "Jams totaux" + }, + "manufacture_at": { + "name": "Date de fabrication" + }, + "mispick_events": { + "name": "Picks Miss Total" + }, + "monochrome_impressions": { + "name": "Pages totales en noir et blanc" + }, + "refilled_count_counterfeit_refilled_count": { + "name": "Contrefait rempli" + }, + "refilled_count_genuine_refilled_count": { + "name": "Authentique rempli" + }, + "scan_images": { + "name": "Pages totales" + }, + "simplex_sheets": { + "name": "Pages totales \u00e0 un c\u00f4t\u00e9 unique" + }, + "total_impressions": { + "name": "Pages totales" + }, + "warranty_expiration_date": { + "name": "Date d'expiration" + } + } + }, + "options": { + "error": { + "already_configured": "HP Imprimante Integration ({name}) d\u00e9j\u00e0 configur\u00e9", + "error_400": "D\u00e9tails du serveur non valides, veuillez essayer un acc\u00e8s manuellement \u00e0 `http: // {ip} // devmgmt / productStatusDyn.xml` (Remplacez l'espace r\u00e9serv\u00e9 par IP ou nom d'h\u00f4te), au cas o\u00f9 il est accessible, veuillez signaler le probl\u00e8me avec les journaux", + "error_404": "API non pris en charge" + }, + "step": { + "hp_printer_additional_settings": { + "data": { + "host": "H\u00f4tesse H\u00f4te", + "log_level": "Niveau de journal", + "name": "Nom", + "store_data": "Doit les r\u00e9ponses du magasin?", + "update_interval": "Mettre \u00e0 jour l'intervalle (secondes)" + }, + "description": "D\u00e9finir des param\u00e8tres suppl\u00e9mentaires pour l'int\u00e9gration de l'imprimante HP", + "title": "Options pour l'imprimante HP." + }, + "init": { + "data": { + "host": "Nom d'h\u00f4te ou IP", + "port": "Num\u00e9ro de port", + "ssl": "Est ssl", + "update_interval": "Mettre \u00e0 jour l'intervalle (secondes)" + }, + "description": "D\u00e9finir des param\u00e8tres suppl\u00e9mentaires pour l'int\u00e9gration de l'imprimante HP", + "title": "Options pour l'imprimante HP." } } } diff --git a/custom_components/hpprinter/translations/nb.json b/custom_components/hpprinter/translations/nb.json index ed17b4e..9da6742 100644 --- a/custom_components/hpprinter/translations/nb.json +++ b/custom_components/hpprinter/translations/nb.json @@ -1,40 +1,136 @@ { "config": { + "abort": { + "already_configured": "HP Printer Integration ({name}) allerede konfigurert" + }, + "error": { + "error_400": "Ugyldige serverdetaljer, pr\u00f8v manuelt tilgang til `http: // {ip} // devmgmt/produktstatusdyn.xml` (erstatt plassholder med IP eller vertsnavn), i tilfelle det er tilgjengelig, vennligst rapporter problemet med logger", + "error_404": "Ikke st\u00f8ttet API" + }, "step": { "user": { - "title": "Sett opp HP-skriver", "data": { "host": "Vert", - "name": "Navn" - } + "name": "Navn", + "port": "Portnummer", + "ssl": "Er SSL" + }, + "title": "Sett opp HP -skriver" + } + } + }, + "entity": { + "binary_sensor": { + "cloud_services_switch_status": { + "name": "ePrint Status" + }, + "consumable_life_state_consumable_state": { + "name": "Status" + }, + "hardware_config_is_connected": { + "name": "Tilkoblet" + }, + "registration_state": { + "name": "ePrint Registrert" } }, - "abort": { - "already_configured": "HP-skriverintegrasjon ({name}) er allerede konfigurert" - }, - "error": { - "error_400": "Ugyldige serveropplysninger, prøv manuelt å få tilgang til `http://{IP}//DevMgmt/ProductStatusDyn.xml` (Erstatt plassholder med IP eller vertsnavn), hvis det er tilgjengelig, vennligst rapporter problemet med logger", - "error_404": "API støttes ikke" + "sensor": { + "adf_images": { + "name": "Totalt sider fra ADF" + }, + "color_impressions": { + "name": "Total fargesider" + }, + "consumable_percentage_level_remaining": { + "name": "Niv\u00e5" + }, + "consumable_station": { + "name": "Stasjon" + }, + "consumable_type_enum": { + "name": "Type", + "state": { + "ink": "Blekk", + "inkcartridge": "Blekk", + "printhead": "Skrivehode", + "toner": "Toner" + } + }, + "duplex_sheets": { + "name": "Totalt tosidige sider" + }, + "estimated_pages_remaining": { + "name": "Gjenst\u00e5ende" + }, + "flatbed_images": { + "name": "Totalt sider fra skannerglass" + }, + "hardware_config_device_connectivity_port_type": { + "name": "Porttype" + }, + "installation_date": { + "name": "Installasjonsdato" + }, + "jam_events": { + "name": "Total syltet\u00f8y" + }, + "manufacture_at": { + "name": "Produksjonsdato" + }, + "mispick_events": { + "name": "Totalt glipp av valg" + }, + "monochrome_impressions": { + "name": "Totalt svart-hvitt sider" + }, + "refilled_count_counterfeit_refilled_count": { + "name": "Forfalsket p\u00e5fyllt" + }, + "refilled_count_genuine_refilled_count": { + "name": "Ekte p\u00e5fyllt" + }, + "scan_images": { + "name": "Totalt sider" + }, + "simplex_sheets": { + "name": "Totalt ensidige sider" + }, + "total_impressions": { + "name": "Totalt sider" + }, + "warranty_expiration_date": { + "name": "Utl\u00f8psdato" + } } }, "options": { + "error": { + "already_configured": "HP Printer Integration ({name}) allerede konfigurert", + "error_400": "Ugyldige serverdetaljer, pr\u00f8v manuelt tilgang til `http: // {ip} // devmgmt/produktstatusdyn.xml` (erstatt plassholder med IP eller vertsnavn), i tilfelle det er tilgjengelig, vennligst rapporter problemet med logger", + "error_404": "Ikke st\u00f8ttet API" + }, "step": { "hp_printer_additional_settings": { - "title": "Alternativer for HP-skriver.", - "description": "Definer tilleggsinnstillinger for HP-skriverintegrasjon", "data": { "host": "Vert", + "log_level": "Loggniv\u00e5", "name": "Navn", - "update_interval": "Oppdateringsintervall (sekunder)", - "log_level": "Loggnivå", - "store_data": "Bør lagre svar?" - } + "store_data": "B\u00f8r lagre svar?", + "update_interval": "Oppdateringsintervall (sekunder)" + }, + "description": "Definer flere innstillinger for HP -skriverintegrasjon", + "title": "Alternativer for HP -skriver." + }, + "init": { + "data": { + "host": "Vertsnavn eller ip", + "port": "Portnummer", + "ssl": "Er SSL", + "update_interval": "Oppdateringsintervall (sekunder)" + }, + "description": "Definer flere innstillinger for HP -skriverintegrasjon", + "title": "Alternativer for HP -skriver." } - }, - "error": { - "error_400": "Ugyldige serveropplysninger. Prøv manuelt å få tilgang ti `http://{IP}//DevMgmt/ProductStatusDyn.xml` (Erstatt plassholder med IP eller vertsnavn), hvis det er tilgjengelig, vennligst rapporter problemet med logger", - "error_404": "API støttes ikke", - "already_configured": "HP-skriverintegrasjon ({name}) er allerede konfigurert" } } } diff --git a/custom_components/hpprinter/translations/nl.json b/custom_components/hpprinter/translations/nl.json index b3d0ffb..3697915 100644 --- a/custom_components/hpprinter/translations/nl.json +++ b/custom_components/hpprinter/translations/nl.json @@ -1,16 +1,136 @@ { "config": { + "abort": { + "already_configured": "HP Printer Integration ({Name}) al geconfigureerd" + }, + "error": { + "error_400": "Ongeldige servergegevens, probeer handmatig toegang tot `http: // {ip} // devmgmt/ProductStatusyn.xml` (Vervang Placeholder door IP of hostnaam), Rapporteer het probleem met logs met logs met logs", + "error_404": "Niet -ondersteunde API" + }, "step": { "user": { - "title": "HP Printer instellen", "data": { - "host": "Host", - "name": "Naam" - } + "host": "Gastheer", + "name": "Naam", + "port": "Poortnummer", + "ssl": "Is SSL" + }, + "title": "HP -printer instellen" + } + } + }, + "entity": { + "binary_sensor": { + "cloud_services_switch_status": { + "name": "ePrint Toestand" + }, + "consumable_life_state_consumable_state": { + "name": "Toestand" + }, + "hardware_config_is_connected": { + "name": "Verbonden" + }, + "registration_state": { + "name": "ePrint Geregistreerd" } }, + "sensor": { + "adf_images": { + "name": "Totale pagina's van ADF" + }, + "color_impressions": { + "name": "Totale kleurpagina's" + }, + "consumable_percentage_level_remaining": { + "name": "Niveau" + }, + "consumable_station": { + "name": "Station" + }, + "consumable_type_enum": { + "name": "Type", + "state": { + "ink": "Inkt", + "inkcartridge": "Inkt", + "printhead": "Printhead", + "toner": "Toner" + } + }, + "duplex_sheets": { + "name": "Totale dubbelzijdige pagina's" + }, + "estimated_pages_remaining": { + "name": "Overig" + }, + "flatbed_images": { + "name": "Totale pagina's van scannerglas" + }, + "hardware_config_device_connectivity_port_type": { + "name": "Poorttype" + }, + "installation_date": { + "name": "Installatie datum" + }, + "jam_events": { + "name": "Totale jam" + }, + "manufacture_at": { + "name": "Productiedatum" + }, + "mispick_events": { + "name": "Totale Miss Picks" + }, + "monochrome_impressions": { + "name": "Totaal zwart-witpagina's" + }, + "refilled_count_counterfeit_refilled_count": { + "name": "Vervalste bijgevuld" + }, + "refilled_count_genuine_refilled_count": { + "name": "Echt bijgevuld" + }, + "scan_images": { + "name": "Totale pagina's" + }, + "simplex_sheets": { + "name": "Totaal enkelzijdige pagina's" + }, + "total_impressions": { + "name": "Totale pagina's" + }, + "warranty_expiration_date": { + "name": "uiterste houdbaarheidsdatum" + } + } + }, + "options": { "error": { - "cannot_reach_printer": "HP Printer is onbereikbaar vanwege een van de volgende redenen: de hostnaam is verkeerd, de printer is niet verbonden of de printer ondersteund de API in deze integratie niet" + "already_configured": "HP Printer Integration ({Name}) al geconfigureerd", + "error_400": "Ongeldige servergegevens, probeer handmatig toegang tot `http: // {ip} // devmgmt/ProductStatusyn.xml` (Vervang Placeholder door IP of hostnaam), Rapporteer het probleem met logs met logs met logs", + "error_404": "Niet -ondersteunde API" + }, + "step": { + "hp_printer_additional_settings": { + "data": { + "host": "Gastheer", + "log_level": "Log niveau", + "name": "Naam", + "store_data": "Moeten de reacties opslaan?", + "update_interval": "Update interval (seconden)" + }, + "description": "Definieer extra instellingen voor HP -printerintegratie", + "title": "Opties voor HP -printer." + }, + "init": { + "data": { + "host": "Hostnaam of ip", + "port": "Poortnummer", + "ssl": "Is SSL", + "update_interval": "Update interval (seconden)" + }, + "description": "Definieer extra instellingen voor HP -printerintegratie", + "title": "Opties voor HP -printer." + } } } } diff --git a/custom_components/hpprinter/translations/pl.json b/custom_components/hpprinter/translations/pl.json index 4434588..f184312 100644 --- a/custom_components/hpprinter/translations/pl.json +++ b/custom_components/hpprinter/translations/pl.json @@ -1,40 +1,136 @@ { "config": { + "abort": { + "already_configured": "Integracja drukarki HP ({name}) ju\u017c skonfigurowana" + }, + "error": { + "error_400": "Nieprawid\u0142owe szczeg\u00f3\u0142y serwera, spr\u00f3buj r\u0119cznie uzyska\u0107 dost\u0119p do `http: // {ip} // devmgmt/productStatusdyn.xml` (zamie\u0144 symbol zast\u0119pczy na IP lub nazwa hosta), je\u015bli jest dost\u0119pny, zg\u0142o\u015b problem z dziennikami", + "error_404": "Nieobs\u0142ugiwany API" + }, "step": { "user": { - "title": "Skonfiguruj drukarkę HP", "data": { - "host": "Host lub IP", - "name": "Nazwa" - } + "host": "Gospodarz", + "name": "Nazwa", + "port": "Numer portu", + "ssl": "Jest SSL" + }, + "title": "Skonfiguruj drukark\u0119 HP" + } + } + }, + "entity": { + "binary_sensor": { + "cloud_services_switch_status": { + "name": "ePrint Status" + }, + "consumable_life_state_consumable_state": { + "name": "Status" + }, + "hardware_config_is_connected": { + "name": "Po\u0142\u0105czony" + }, + "registration_state": { + "name": "ePrint Zarejestrowany" } }, - "abort": { - "already_configured": "Integracja drukarki HP ({name}) jest już skonfigurowana" - }, - "error": { - "error_400": "Nieprawidłowe dane serwera, spróbuj ręcznie uzyskać otworzyć `http://{IP}//DevMgmt/ProductStatusDyn.xml` (Zamień tekst zastępczy na IP lub nazwę hosta), jeśli to możliwe - zgłoś problem wraz z logami.", - "error_404": "Niewspierane API" + "sensor": { + "adf_images": { + "name": "Ca\u0142kowite strony z ADF" + }, + "color_impressions": { + "name": "Ca\u0142kowite strony kolorowe" + }, + "consumable_percentage_level_remaining": { + "name": "Poziom" + }, + "consumable_station": { + "name": "Stacja" + }, + "consumable_type_enum": { + "name": "Typ", + "state": { + "ink": "Atrament", + "inkcartridge": "Atrament", + "printhead": "Printhead", + "toner": "Toner" + } + }, + "duplex_sheets": { + "name": "Ca\u0142kowita dwustronna strony" + }, + "estimated_pages_remaining": { + "name": "Pozosta\u0142y" + }, + "flatbed_images": { + "name": "Ca\u0142kowite strony ze szk\u0142a skanera" + }, + "hardware_config_device_connectivity_port_type": { + "name": "Typ portu" + }, + "installation_date": { + "name": "Data instalacji" + }, + "jam_events": { + "name": "Ca\u0142kowite zaci\u0119cia" + }, + "manufacture_at": { + "name": "Data produkcji" + }, + "mispick_events": { + "name": "Total Miss Pick" + }, + "monochrome_impressions": { + "name": "Ca\u0142kowite czarno-bia\u0142e strony" + }, + "refilled_count_counterfeit_refilled_count": { + "name": "FRAKTHICE FOLOKLED" + }, + "refilled_count_genuine_refilled_count": { + "name": "Oryginalny nape\u0142niony" + }, + "scan_images": { + "name": "Wszystkie strony" + }, + "simplex_sheets": { + "name": "Ca\u0142kowita jednostronna strony" + }, + "total_impressions": { + "name": "Wszystkie strony" + }, + "warranty_expiration_date": { + "name": "Termin wa\u017cno\u015bci" + } } }, "options": { + "error": { + "already_configured": "Integracja drukarki HP ({name}) ju\u017c skonfigurowana", + "error_400": "Nieprawid\u0142owe szczeg\u00f3\u0142y serwera, spr\u00f3buj r\u0119cznie uzyska\u0107 dost\u0119p do `http: // {ip} // devmgmt/productStatusdyn.xml` (zamie\u0144 symbol zast\u0119pczy na IP lub nazwa hosta), je\u015bli jest dost\u0119pny, zg\u0142o\u015b problem z dziennikami", + "error_404": "Nieobs\u0142ugiwany API" + }, "step": { "hp_printer_additional_settings": { - "title": "Opcje dla drukarki HP.", - "description": "Zdefiniuj dodatkowe ustawienia dla integracji drukarki HP", "data": { - "host": "Host lub IP", + "host": "Gospodarz", + "log_level": "Poziom dziennika", "name": "Nazwa", - "update_interval": "Interwał aktualizacji (sekundy)", - "log_level": "Poziom logowania", - "store_data": "Czy należy przechowywać odpowiedzi?" - } + "store_data": "Powinien przechowywa\u0107 odpowiedzi?", + "update_interval": "Interwa\u0142 aktualizacji (sekundy)" + }, + "description": "Zdefiniuj dodatkowe ustawienia integracji drukarki HP", + "title": "Opcje drukarki HP." + }, + "init": { + "data": { + "host": "Nazwa hosta lub IP", + "port": "Numer portu", + "ssl": "Jest SSL", + "update_interval": "Interwa\u0142 aktualizacji (sekundy)" + }, + "description": "Zdefiniuj dodatkowe ustawienia integracji drukarki HP", + "title": "Opcje drukarki HP." } - }, - "error": { - "error_400": "Nieprawidłowe dane serwera, spróbuj ręcznie uzyskać otworzyć `http://{IP}//DevMgmt/ProductStatusDyn.xml` (Zamień tekst zastępczy na IP lub nazwę hosta), jeśli to możliwe - zgłoś problem wraz z logami.", - "error_404": "Niewspierane API", - "already_configured": "Integracja drukarki HP ({name}) jest już skonfigurowana" } } } diff --git a/custom_components/hpprinter/translations/pt-BR.json b/custom_components/hpprinter/translations/pt-BR.json index 035499e..5a8db2e 100644 --- a/custom_components/hpprinter/translations/pt-BR.json +++ b/custom_components/hpprinter/translations/pt-BR.json @@ -1,40 +1,136 @@ { "config": { + "abort": { + "already_configured": "Integra\u00e7\u00e3o da impressora HP ({nome}) j\u00e1 configurada" + }, + "error": { + "error_400": "Detalhes do servidor inv\u00e1lido, tente acessar manualmente para `http: // {ip} // devmgmt/productStatusdyn.xml` (substitua o espa\u00e7o reservado por IP ou nome de host), caso esteja acess\u00edvel, relate o problema com os logs", + "error_404": "API n\u00e3o suportada" + }, "step": { "user": { - "title": "Configurar HP Printer", "data": { - "host": "Host", - "name": "Nome" - } + "host": "Hospedar", + "name": "Nome", + "port": "N\u00famero da porta", + "ssl": "\u00c9 ssl" + }, + "title": "Configurar impressora HP" + } + } + }, + "entity": { + "binary_sensor": { + "cloud_services_switch_status": { + "name": "ePrint Status" + }, + "consumable_life_state_consumable_state": { + "name": "Status" + }, + "hardware_config_is_connected": { + "name": "Conectada Conectado" + }, + "registration_state": { + "name": "ePrint Registrado" } }, - "abort": { - "already_configured": "Integração HP Printer ({name}) já configurada" - }, - "error": { - "error_400": "Detalhes do servidor inválidos, tente acessar manualmente `http://{IP}//DevMgmt/ProductStatusDyn.xml` (Substituir o marcador de posição por IP ou Hostname), caso esteja acessível, informe o problema com os logs", - "error_404": "API Sem suporte" + "sensor": { + "adf_images": { + "name": "P\u00e1ginas totais do ADF" + }, + "color_impressions": { + "name": "P\u00e1ginas coloridas totais" + }, + "consumable_percentage_level_remaining": { + "name": "N\u00edvel" + }, + "consumable_station": { + "name": "Esta\u00e7\u00e3o" + }, + "consumable_type_enum": { + "name": "Tipo", + "state": { + "ink": "Tinta", + "inkcartridge": "Tinta", + "printhead": "Cabe\u00e7ote de impress\u00e3o", + "toner": "Toner" + } + }, + "duplex_sheets": { + "name": "P\u00e1ginas totais de dupla face" + }, + "estimated_pages_remaining": { + "name": "Restante" + }, + "flatbed_images": { + "name": "P\u00e1ginas totais de vidro do scanner" + }, + "hardware_config_device_connectivity_port_type": { + "name": "Tipo de porta" + }, + "installation_date": { + "name": "Data de instala\u00e7\u00e3o" + }, + "jam_events": { + "name": "Total Jams" + }, + "manufacture_at": { + "name": "Data de fabrica\u00e7\u00e3o" + }, + "mispick_events": { + "name": "Total Miss Picks" + }, + "monochrome_impressions": { + "name": "P\u00e1ginas totais em preto e branco" + }, + "refilled_count_counterfeit_refilled_count": { + "name": "Falsificado reabastecido" + }, + "refilled_count_genuine_refilled_count": { + "name": "Reabastecido genu\u00edno" + }, + "scan_images": { + "name": "P\u00e1ginas totais" + }, + "simplex_sheets": { + "name": "P\u00e1ginas totais de um lado" + }, + "total_impressions": { + "name": "P\u00e1ginas totais" + }, + "warranty_expiration_date": { + "name": "Data de validade" + } } }, "options": { + "error": { + "already_configured": "Integra\u00e7\u00e3o da impressora HP ({nome}) j\u00e1 configurada", + "error_400": "Detalhes do servidor inv\u00e1lido, tente acessar manualmente para `http: // {ip} // devmgmt/productStatusdyn.xml` (substitua o espa\u00e7o reservado por IP ou nome de host), caso esteja acess\u00edvel, relate o problema com os logs", + "error_404": "API n\u00e3o suportada" + }, "step": { "hp_printer_additional_settings": { - "title": "Opções para HP Printer.", - "description": "Defina configurações adicionais para a integração HP Printer", "data": { - "host": "Host", + "host": "Hospedar", + "log_level": "N\u00edvel de log", "name": "Nome", - "update_interval": "Intervalo de atualização (segundos)", - "log_level": "Nível de registro", - "store_data": "Deve armazenar respostas?" - } + "store_data": "Deve armazenar respostas?", + "update_interval": "Intervalo de atualiza\u00e7\u00e3o (segundos)" + }, + "description": "Defina configura\u00e7\u00f5es adicionais para a integra\u00e7\u00e3o da impressora HP", + "title": "Op\u00e7\u00f5es para a impressora HP." + }, + "init": { + "data": { + "host": "Nome do host ou IP", + "port": "N\u00famero da porta", + "ssl": "\u00c9 ssl", + "update_interval": "Intervalo de atualiza\u00e7\u00e3o (segundos)" + }, + "description": "Defina configura\u00e7\u00f5es adicionais para a integra\u00e7\u00e3o da impressora HP", + "title": "Op\u00e7\u00f5es para a impressora HP." } - }, - "error": { - "error_400": "Detalhes do servidor inválidos, tente acessar manualmente `http://{IP}//DevMgmt/ProductStatusDyn.xml` (Substituir o marcador de posição por IP ou Hostname), caso esteja acessível, informe o problema com os logs", - "error_404": "API sem suporte", - "already_configured": "Integração HP Printer ({name}) já configurada" } } } diff --git a/info.md b/info.md index e572680..ad2e188 100644 --- a/info.md +++ b/info.md @@ -6,104 +6,199 @@ Configuration support multiple HP Printer devices through Configuration -> Integ [Changelog](https://github.com/elad-bar/ha-hpprinter/blob/master/CHANGELOG.md) -### How to set it up: +## How to -Look for "HP Printers Integration" and install +### Requirements -#### Requirements +- HP Printer with EWS (Embedded Web Server) support -- HP Printer supporting XML API - to check printer's compatibility to the component try to get to the printer's XML API (replace placeholder with real IP / Hostname): - `http://{IP}//DevMgmt/ProductStatusDyn.xml` +### Installations via HACS [![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg)](https://github.com/hacs/integration) -#### Basic configuration +- In HACS, look for "HP Printer" and install and restart +- If integration was not found, please add custom repository `elad-bar/hpprinter` as integration +- In Settings --> Devices & Services - (Lower Right) "Add Integration" -- Configuration should be done via Configuration -> Integrations. -- In case you are already using that integration with YAML Configuration - please remove it -- Integration supports **multiple** devices -- In the setup form, the following details are mandatory: - - Name - Unique - - Host (or IP) -- Upon submitting the form of creating an integration, a request to the printer will take place and will cause failure in case: - - Unsupported API - - Invalid server details - when cannot reach host +### Setup -#### Settings for Monitoring interfaces, devices, tracked devices and update interval +To add integration use Configuration -> Integrations -> Add `HP Printer` +Integration supports **multiple** accounts and devices -_Configuration -> Integrations -> {Integration} -> Options_
+| Fields name | Type | Required | Default | Description | +| ----------- | ------- | -------- | ------- | -------------------------------------------- | +| Host | Textbox | + | - | Defines hostname or IP of the HP Printer EWS | +| Port | Textbox | + | 80 | Defines port of the HP Printer EWS | +| Is SSL | Boolean | + | False | Defines which protocol to use HTTP/S | -``` -Name - Unique -Host (or IP) -Update Interval: Textbox, number of seconds to update entities, default=60 -Log level: Drop-down list, change component's log level (more details below), default=Default -Should store responses?: Check-box, saves XML and JSON files for debugging purpose, default=False -``` +It is also possible to change configuration after setting up using integration configuration. -###### Log Level's drop-down +#### Validation errors -New feature to set the log level for the component without need to set log_level in `customization:` and restart or call manually `logger.set_level` and loose it after restart. +| Errors | +| ------------------------------------------------------ | +| Invalid parameters provided | +| HP Printer Embedded Web Server (EWS) not was not found | -Upon startup or integration's option update, based on the value chosen, the component will make a service call to `logger.set_level` for that component with the desired value, +## Devices -In case `Default` option is chosen, flow will skip calling the service, after changing from any other option to `Default`, it will not take place automatically, only after restart +Will extract data of the relevant devices, devices that are not available will be ignored. -###### Store responses +### Main device -Stores the XML and JSON of each request and final JSON to files, Path in CONFIG_PATH/\*, -Files that will be generated (Prefix to the file is name of the integration): +Device that holds entities related to the integration and relations to other sub devices as described below. -- ProductUsageDyn.XML - Raw XML from HP Printer of Usage Details -- ProductUsageDyn.json - JSON based on the Raw XML of Usage Details after transformed by the component -- ConsumableConfigDyn.XML - Raw XML from HP Printer of consumable details -- ConsumableConfigDyn.json - JSON based on the Raw XML of consumable details after transformed by the component -- ProductConfigDyn.XML - Raw XML from HP Printer of Config Details -- ProductConfigDyn.json - JSON based on the Raw XML of Config Details after transformed by the component -- Final.json - JSON based on the 2 JSONs above, merged into simpler data structure for the HA to create sensor based on +_Binary Sensor_ -## Components: +- ePrint Registered +- ePrint Status -#### Device status - Binary Sensor +_Sensor_ -``` -State: connected? -``` +- Manufacture Date -#### Printer details - Sensor +### Printer -``` -State: # of pages printed -Attributes: - Color - # of printed documents using color cartridges - Monochrome - # of printed documents using black cartridges - Jams - # of print jobs jammed - Cancelled - # of print jobs that were cancelled -``` +Device holds entities of sensors related to number of pages printed and relation to sub devices of consumables -#### Scanner details - Sensor (For AIO only) +_Sensor_ +- Total pages printed +- Total black-and-white pages printed +- Total color pages printed +- Total single-sided pages printed +- Total double-sided pages printed +- Total jams +- Total miss picks + +### Scanner + +Device holds entities of sensors related to number of pages scanned + +_Sensor_ + +- Total scanned pages +- Total scanned pages from ADF +- Total double-sided pages scanned +- Total pages from scanner glass +- Total jams +- Total miss picks + +### Copy + +Device holds entities of sensors related to number of pages copied + +_Sensor_ + +- Total copies +- Total copies from ADF +- Total pages from scanner glass +- Total black-and-white copies +- Total color copies + +### Fax + +Device holds entities of sensors related to number of pages faxed + +_Sensor_ + +- Total faxed + +### Consumable + +Devices (device per consumable) holds entities related to consumable (Ink, Toner, Printhead) of a printer device + +_Binary Sensor_ + +- Status + +_Sensor_ + +- Station +- Type +- Installation Date +- Level (will not be available for Printhead) +- Expiration Date (will not be available for Printhead) +- Remaining (will not be available for Printhead) +- Counterfeit Refilled +- Genuine Refilled +- Manufacture Date + +## Troubleshooting + +Before opening an issue, please provide logs and diagnostic file data related to the issue. + +### Logs + +For debug log level, please add the following to your config.yaml + +```yaml +logger: + default: warning + logs: + custom_components.hpprinter: debug ``` -State: # of pages scanned -Attributes: - ADF - # of scanned documents from the ADF - Duplex - # of scanned documents from the ADF using duplex mode - Flatbed - # of scanned documents from the flatbed - Jams - # of scanned jammed - Mispick - # of scanned documents failed to take the document from the feeder -``` -#### Cartridges details - Sensor (Per cartridge) +Or use the HA capability in device page: + +1. Settings +2. Devices & Services +3. HP Printer +4. 3 dots menu +5. Enable debug logging + +When done and would like to extract the log, repeat steps, in step #5 - Disable debug logging + +### Diagnostic details +Please attach also diagnostic details of the integration, available in: + +1. Settings +2. Devices & Services +3. HP Printer +4. 3 dots menu +5. Download diagnostics + +Diagnostic file contains 3 section related to data extracted from the device: + +- data.debug.rawData - Raw data extracted from all endpoints of the device, from that source you can extract ideas for additional entities to suggest +- data.debug.devicesConfig - Configuration of mapping to convert data from HP Printer EWS to HA devices and entities, that will be the section that new entities will be added +- data.debug.devicesData - Data extracted for HA entities, just relevant data points, according to mapped objects available in section `data.debug.devicesConfig` + +## Translations + +Integration translated from English to: + +- German +- Danish +- Spanish +- French +- Dutch +- Norwegian +- Polish +- Portuguese + +Translation is being auto-generated from Google Translate using `utils/generate_translations.py` script, + +```json +{ + "en": "en", + "de": "de", + "dk": "da", + "es": "es", + "fr": "fr", + "nb": "no", + "nl": "nl", + "pl": "pl", + "pt-BR": "pt" +} ``` -State: Remaining level % -Attributes: - Color - Type - Ink / Toner / Print head - Station - Position of the cartridge - Product Number - Serial Number - Manufactured By - Manufactured At - Warranty Expiration Date - Installed At + +If you would like to add new translation language, please add to the `DESTINATION_LANGUAGES` constant the relevant language, +format is: + +```json +{ + "HA language": "Google Translate language" +} ``` + +Script is translating only, new missing values, it will not override translated values. diff --git a/requirements.txt b/requirements.txt index e8472a8..8af47d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,19 @@ pre-commit -homeassistant +homeassistant~=2024.3.0b0 -aiohttp -xmltodict -voluptuous +voluptuous~=0.13.1 + +aiohttp~=3.9.1 +xmltodict~=0.13.0 + +defusedxml~=0.7.1 +flatten_json + +requests~=2.27 +python-lokalise-api~=1.6 +python-dotenv~=0.20 +googletrans==4.0.0rc1 +translators~= 5.4 +deep-translator~=1.9 + +python-slugify~=4.0.1 diff --git a/setup.cfg b/setup.cfg index 1620535..9d64dac 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,8 +29,6 @@ force_grid_wrap=0 use_parentheses=True line_length=88 indent = " " -# by default isort don't check module indexes -not_skip = __init__.py # will group `import x` and `from x import` of the same module. force_sort_within_sections = true sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER diff --git a/utils/api_test.py b/utils/api_test.py new file mode 100644 index 0000000..2798cfd --- /dev/null +++ b/utils/api_test.py @@ -0,0 +1,72 @@ +import asyncio +import json +import logging +import os +import sys + +from custom_components.hpprinter import HAConfigManager +from custom_components.hpprinter.common.consts import DATA_KEYS +from custom_components.hpprinter.managers.rest_api import RestAPIv2 +from homeassistant.core import HomeAssistant + +DEBUG = str(os.environ.get("DEBUG", False)).lower() == str(True).lower() + +log_level = logging.DEBUG if DEBUG else logging.INFO + +root = logging.getLogger() +root.setLevel(log_level) + +stream_handler = logging.StreamHandler(sys.stdout) +stream_handler.setLevel(log_level) +formatter = logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s") +stream_handler.setFormatter(formatter) +root.addHandler(stream_handler) + +_LOGGER = logging.getLogger(__name__) + + +class APITest: + def __init__(self): + self._api: RestAPIv2 | None = None + self._config_manager: HAConfigManager | None = None + + self._config_data = { + key: os.environ.get(key) + for key in DATA_KEYS + } + + async def initialize(self): + hass = HomeAssistant(".") + + self._config_manager = HAConfigManager(None, None) + await self._config_manager.initialize(self._config_data) + + self._api = RestAPIv2(hass, self._config_manager) + await self._api.initialize(True) + + await self._api.update() + + print(json.dumps(self._api.data_config, indent=4)) + print(json.dumps(self._api.data, indent=4)) + + async def terminate(self): + await self._api.terminate() + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + + instance = APITest() + + try: + loop.create_task(instance.initialize()) + loop.run_forever() + + except KeyboardInterrupt: + _LOGGER.info("Aborted") + + except Exception as rex: + _LOGGER.error(f"Error: {rex}") + + finally: + loop.run_until_complete(instance.terminate()) diff --git a/utils/generate_translations.py b/utils/generate_translations.py new file mode 100644 index 0000000..4585a70 --- /dev/null +++ b/utils/generate_translations.py @@ -0,0 +1,197 @@ +import asyncio +import json +import logging +import os +from pathlib import Path +import sys + +from flatten_json import flatten, unflatten +import translators as ts + +DEBUG = str(os.environ.get("DEBUG", False)).lower() == str(True).lower() + +log_level = logging.DEBUG if DEBUG else logging.INFO + +root = logging.getLogger() +root.setLevel(log_level) + +logging.getLogger("urllib3").setLevel(logging.WARNING) + +stream_handler = logging.StreamHandler(sys.stdout) +stream_handler.setLevel(log_level) +formatter = logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s") +stream_handler.setFormatter(formatter) +root.addHandler(stream_handler) + +_LOGGER = logging.getLogger(__name__) + +SOURCE_LANGUAGE = "en" +DESTINATION_LANGUAGES = { + "en": "en", + "de": "de", + "dk": "da", + "es": "es", + "fr": "fr", + "nb": "no", + "nl": "nl", + "pl": "pl", + "pt-BR": "pt" +} + +TRANSLATION_PROVIDER = "google" +E_PRINT_TERM = "ePrint" +E_PRINT_PLACEHOLDER = "***" +FLAT_SEPARATOR = "." + + +class TranslationGenerator: + def __init__(self): + self._config = self._get_parameters() + self._source_translations = self._get_source_translations() + + self._destinations = DESTINATION_LANGUAGES + + async def initialize(self): + values = flatten(self._source_translations, FLAT_SEPARATOR) + value_keys = list(values.keys()) + last_key = value_keys[len(value_keys) - 1] + + _LOGGER.info( + f"Process will translate {len(values)} sentences " + f"to {len(list(self._destinations.keys()))} languages" + ) + + for lang in self._destinations: + original_values = values.copy() + translated_data = self._get_translations(lang) + translated_values = flatten(translated_data, FLAT_SEPARATOR) + + provider_lang = self._destinations[lang] + lang_cache = {} + + lang_title = provider_lang.upper() + + for key in original_values: + english_value = original_values[key] + + if not isinstance(english_value, str): + continue + + if key in translated_values: + translated_value = translated_values[key] + + _LOGGER.debug( + f"Skip translation to '{lang_title}', " + f"translation of '{english_value}' already exists - '{translated_value}'" + ) + + continue + + if english_value in lang_cache: + translated_value = lang_cache[english_value] + + _LOGGER.debug( + f"Skip translation to '{lang_title}', " + f"translation of '{english_value}' available in cache - {translated_value}" + ) + + elif lang == SOURCE_LANGUAGE: + translated_value = english_value + + _LOGGER.debug( + f"Skip translation to '{lang_title}', " + f"source and destination languages are the same - {translated_value}" + ) + + else: + has_e_print = E_PRINT_TERM in english_value + original_english_value = english_value + + if has_e_print: + english_value = english_value.replace(E_PRINT_TERM, E_PRINT_PLACEHOLDER) + + sleep_seconds = 10 if last_key == key else 0 + + translated_value = ts.translate_text( + english_value, + translator=TRANSLATION_PROVIDER, + to_language=provider_lang, + sleep_seconds=sleep_seconds + ) + + if has_e_print: + translated_value = translated_value.replace(E_PRINT_PLACEHOLDER, E_PRINT_TERM) + + lang_cache[english_value] = translated_value + + _LOGGER.debug(f"Translating '{original_english_value}' to {lang_title}: {translated_value}") + + translated_values[key] = translated_value + + translated_data = unflatten(translated_values, FLAT_SEPARATOR) + + self._save_translations(lang, translated_data) + + @staticmethod + def _get_parameters() -> dict: + config_file = "data_points.json" + current_path = Path(__file__) + parent_directory = current_path.parents[1] + file_path = os.path.join(parent_directory, "custom_components", "hpprinter", "parameters", config_file) + + with open(file_path) as f: + data = json.load(f) + + return data + + @staticmethod + def _get_source_translations() -> dict: + current_path = Path(__file__) + parent_directory = current_path.parents[1] + file_path = os.path.join(parent_directory, "custom_components", "hpprinter", "strings.json") + + with open(file_path) as f: + data = json.load(f) + + return data + + @staticmethod + def _get_translations(lang: str): + current_path = Path(__file__) + parent_directory = current_path.parents[1] + file_path = os.path.join(parent_directory, "custom_components", "hpprinter", "translations", f"{lang}.json") + + if os.path.exists(file_path): + with open(file_path) as file: + data = json.load(file) + else: + data = {} + + return data + + @staticmethod + def _save_translations(lang: str, data: dict): + current_path = Path(__file__) + parent_directory = current_path.parents[1] + file_path = os.path.join(parent_directory, "custom_components", "hpprinter", "translations", f"{lang}.json") + + with open(file_path, "w+") as file: + file.write(json.dumps(data, indent=4)) + + _LOGGER.info(f"Translation for {lang.upper()} stored") + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + + instance = TranslationGenerator() + + try: + loop.create_task(instance.initialize()) + loop.run_forever() + + except KeyboardInterrupt: + _LOGGER.info("Aborted") + + except Exception as rex: + _LOGGER.error(f"Error: {rex}")