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 [](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 [](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}")