From 4a94ad05037b97db06ecced4ef1f0be4f0243e18 Mon Sep 17 00:00:00 2001 From: Elad Bar Date: Thu, 20 Jun 2024 19:21:26 +0300 Subject: [PATCH 1/2] Fix update data when printer goes online --- CHANGELOG.md | 4 + .../hpprinter/managers/rest_api.py | 90 ++++++++----------- custom_components/hpprinter/manifest.json | 2 +- 3 files changed, 42 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd483a6..b6e94f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 2.0.4 + +- Fix update data when printer goes online [#161](https://github.com/elad-bar/ha-hpprinter/issues/161) + ## 2.0.3 - Add support for `inktank` cartridge type [#162](https://github.com/elad-bar/ha-hpprinter/issues/162) diff --git a/custom_components/hpprinter/managers/rest_api.py b/custom_components/hpprinter/managers/rest_api.py index 2d29a49..6dc0f97 100644 --- a/custom_components/hpprinter/managers/rest_api.py +++ b/custom_components/hpprinter/managers/rest_api.py @@ -37,6 +37,7 @@ def __init__(self, hass, config_manager: HAConfigManager): self._loop = hass.loop self._config_manager = config_manager self._hass = hass + self._endpoints = self._config_manager.endpoints self._session: ClientSession | None = None @@ -49,9 +50,10 @@ def __init__(self, hass, config_manager: HAConfigManager): self._is_connected: bool = False self._device_dispatched: list[str] = [] - self._all_endpoints: list[str] = [] self._support_prefetch: bool = False + self._is_printer_online: bool = False + @property def data(self) -> dict | None: return self._data @@ -119,77 +121,61 @@ def _get_ssl_connector(self): 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 + status = await self._revalidate_printer_status() - if self._support_prefetch: - for endpoint in endpoints: - is_valid = self._config_manager.is_valid_endpoint(endpoint) + if status is None: + raise IntegrationAPIError(PRODUCT_STATUS_ENDPOINT) - if is_valid: - endpoint_uri = endpoint.get("uri") - - self._all_endpoints.append(endpoint_uri) + async def _revalidate_printer_status(self): + now = datetime.now() + now_ts = now.timestamp() - else: - self._all_endpoints = self._config_manager.endpoints.copy() + status_endpoint = PRODUCT_STATUS_ENDPOINT + last_update = self._last_update.get(status_endpoint, 0) - updates = await self._update_data(self._config_manager.endpoints, False) + last_update_diff = int(now_ts - last_update) + interval = self._config_manager.get_update_interval(status_endpoint) - _LOGGER.debug(f"Startup: {updates} endpoints were updated") + if interval > last_update_diff: + return None - endpoints_found = len(self._raw_data.keys()) - is_connected = endpoints_found > 0 - available_endpoints = len(self._all_endpoints) + product_status_data = await self._get_request(status_endpoint) + self._is_printer_online = product_status_data is not None - if is_connected: - _LOGGER.info( - "No support for prefetch endpoint, " - f"{endpoints_found}/{available_endpoints} Endpoints found" - ) - else: - endpoint_urls = ", ".join(self._all_endpoints) + if self._is_printer_online: + self._last_update = {} - raise IntegrationAPIError(endpoint_urls) + return product_status_data - self._is_connected = is_connected + return None async def update(self): - updates = await self._update_data(self._config_manager.endpoints) - - _LOGGER.debug(f"Scheduled update: {updates} endpoints were updated") - - async def update_full(self): - updates = await self._update_data(self._all_endpoints) - - _LOGGER.debug(f"Full update: {updates} endpoints were updated") - - async def _update_data( - self, endpoints: list[str], connectivity_check: bool = True - ) -> int: + product_status_data: dict | None = None endpoints_updated = 0 - if not self._is_connected and connectivity_check: - return endpoints_updated + if not self._is_printer_online: + product_status_data = await self._revalidate_printer_status() + + if product_status_data is None: + return endpoints_updated now = datetime.now() now_ts = now.timestamp() - for endpoint in endpoints: - last_update = ( - self._last_update.get(endpoint, 0) if connectivity_check else 0 - ) + for endpoint in self._endpoints: + last_update = self._last_update.get(endpoint, 0) last_update_diff = int(now_ts - last_update) interval = self._config_manager.get_update_interval(endpoint) if interval > last_update_diff: continue - resource_data = await self._get_request(endpoint) + if endpoint == PRODUCT_STATUS_ENDPOINT and product_status_data is not None: + resource_data = product_status_data + + else: + resource_data = await self._get_request(endpoint) + endpoints_updated += 1 if resource_data is None: @@ -198,15 +184,13 @@ async def _update_data( else: self._raw_data[endpoint] = resource_data - - if connectivity_check: - self._last_update[endpoint] = now.timestamp() + self._last_update[endpoint] = now.timestamp() devices = self._get_devices_data() self._extract_data(devices) - return endpoints_updated + _LOGGER.debug(f"Scheduled update: {endpoints_updated} endpoints were updated") def _extract_data(self, devices: list[dict]): device_data = {} diff --git a/custom_components/hpprinter/manifest.json b/custom_components/hpprinter/manifest.json index e5219e4..de42d71 100644 --- a/custom_components/hpprinter/manifest.json +++ b/custom_components/hpprinter/manifest.json @@ -9,5 +9,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/elad-bar/ha-hpprinter/issues", "requirements": ["xmltodict~=0.13.0", "flatten_json", "defusedxml"], - "version": "2.0.3" + "version": "2.0.4" } From 1ebc0465cefb6a9a715fe0acf0a6069e71cdd6d3 Mon Sep 17 00:00:00 2001 From: Elad Bar Date: Fri, 21 Jun 2024 12:12:29 +0300 Subject: [PATCH 2/2] set integration title as `{make_and_model} ({hostname})` --- CHANGELOG.md | 1 + custom_components/hpprinter/common/consts.py | 4 + .../hpprinter/managers/flow_manager.py | 45 ++-- .../hpprinter/managers/ha_coordinator.py | 5 +- .../hpprinter/managers/rest_api.py | 206 ++++++++++-------- .../hpprinter/models/exceptions.py | 19 -- utils/api_test.py | 39 +++- 7 files changed, 177 insertions(+), 142 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6e94f2..2c51da1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 2.0.4 - Fix update data when printer goes online [#161](https://github.com/elad-bar/ha-hpprinter/issues/161) +- Set integration title as `{make_and_model} ({hostname})` ## 2.0.3 diff --git a/custom_components/hpprinter/common/consts.py b/custom_components/hpprinter/common/consts.py index 7282545..488841c 100644 --- a/custom_components/hpprinter/common/consts.py +++ b/custom_components/hpprinter/common/consts.py @@ -48,7 +48,11 @@ NUMERIC_UNITS_OF_MEASUREMENT = [UNIT_OF_MEASUREMENT_PAGES, UNIT_OF_MEASUREMENT_REFILLS] +MODEL_PROPERTY = "make_and_model" + PRODUCT_STATUS_ENDPOINT = "/DevMgmt/ProductStatusDyn.xml" +PRODUCT_MAIN_ENDPOINT = "/DevMgmt/ProductConfigDyn.xml" + PRODUCT_STATUS_OFFLINE_PAYLOAD = { "ProductStatusDyn": {"Status": [{"StatusCategory": "off"}]} } diff --git a/custom_components/hpprinter/managers/flow_manager.py b/custom_components/hpprinter/managers/flow_manager.py index eabcae4..f418a6d 100644 --- a/custom_components/hpprinter/managers/flow_manager.py +++ b/custom_components/hpprinter/managers/flow_manager.py @@ -9,9 +9,14 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowHandler -from ..common.consts import DATA_KEYS, DEFAULT_NAME +from ..common.consts import ( + DATA_KEYS, + DEFAULT_NAME, + MODEL_PROPERTY, + PRINTER_MAIN_DEVICE, + PRODUCT_MAIN_ENDPOINT, +) from ..models.config_data import ConfigData -from ..models.exceptions import IntegrationAPIError, IntegrationParameterError from .ha_config_manager import HAConfigManager from .rest_api import RestAPIv2 @@ -58,29 +63,35 @@ async def async_step(self, user_input: dict | None = None): api = RestAPIv2(self._hass, self._config_manager) - await api.initialize(True) + await api.initialize() - _LOGGER.debug("User inputs are valid") - title = DEFAULT_NAME + if api.is_online: + _LOGGER.debug("User inputs are valid") - if self._entry is None: - data = copy(user_input) + await api.update([PRODUCT_MAIN_ENDPOINT]) - else: - data = await self.remap_entry_data(user_input) - title = self._entry.title + main_device = api.data.get(PRINTER_MAIN_DEVICE, {}) + model = main_device.get(MODEL_PROPERTY, DEFAULT_NAME) + title = f"{model} ({api.config_data.hostname})" - return self._flow_handler.async_create_entry(title=title, data=data) + if self._entry is None: + data = copy(user_input) - except IntegrationParameterError as ipex: - form_errors = {"base": "error_400"} + else: + data = await self.remap_entry_data(user_input) + title = self._entry.title + + return self._flow_handler.async_create_entry(title=title, data=data) - _LOGGER.warning(f"Failed to setup integration, Error: {ipex}") + else: + form_errors = {"base": "error_404"} + + _LOGGER.warning("Failed to setup integration") - except IntegrationAPIError as iapiex: - form_errors = {"base": "error_404"} + except Exception as ex: + form_errors = {"base": "error_400"} - _LOGGER.warning(f"Failed to setup integration, Error: {iapiex}") + _LOGGER.error(f"Failed to setup integration, Error: {ex}") schema = ConfigData.default_schema(user_input) diff --git a/custom_components/hpprinter/managers/ha_coordinator.py b/custom_components/hpprinter/managers/ha_coordinator.py index 17db09a..79ea116 100644 --- a/custom_components/hpprinter/managers/ha_coordinator.py +++ b/custom_components/hpprinter/managers/ha_coordinator.py @@ -12,6 +12,7 @@ from ..common.consts import ( DOMAIN, + MODEL_PROPERTY, PRINTER_MAIN_DEVICE, SIGNAL_HA_DEVICE_CREATED, SIGNAL_HA_DEVICE_DISCOVERED, @@ -115,7 +116,7 @@ def create_main_device( self._main_device_data = device_data self._main_device_id = device_key - model = device_data.get("make_and_model") + model = device_data.get(MODEL_PROPERTY) serial_number = device_data.get("serial_number") manufacturer = device_data.get("manufacturer_name") @@ -147,7 +148,7 @@ 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") + model = self._main_device_data.get(MODEL_PROPERTY) serial_number = self._main_device_data.get("serial_number") manufacturer = self._main_device_data.get("manufacturer_name") diff --git a/custom_components/hpprinter/managers/rest_api.py b/custom_components/hpprinter/managers/rest_api.py index 6dc0f97..5f36d3f 100644 --- a/custom_components/hpprinter/managers/rest_api.py +++ b/custom_components/hpprinter/managers/rest_api.py @@ -8,7 +8,6 @@ from flatten_json import flatten import xmltodict -from homeassistant.const import CONF_HOST from homeassistant.helpers.aiohttp_client import ( ENABLE_CLEANUP_CLOSED, MAXIMUM_CONNECTIONS, @@ -26,7 +25,6 @@ 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__) @@ -47,12 +45,10 @@ def __init__(self, hass, config_manager: HAConfigManager): self._raw_data: dict = {} - self._is_connected: bool = False - self._device_dispatched: list[str] = [] self._support_prefetch: bool = False - self._is_printer_online: bool = False + self._is_online: bool = False @property def data(self) -> dict | None: @@ -73,33 +69,29 @@ def config_data(self) -> ConfigData | None: return None + @property + def is_online(self) -> bool: + return self._is_online + 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): + async def initialize(self): try: - if not self.config_data.hostname: - raise IntegrationParameterError(CONF_HOST) - if self._session is None: if self._hass is None: self._session = ClientSession(loop=self._loop) else: self._session = async_create_clientsession(hass=self._hass) - await self._load_metadata() + await self._update_product_status_endpoint_data() except Exception as ex: - if throw_exception: - raise ex - exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno @@ -120,77 +112,101 @@ def _get_ssl_connector(self): return connector - async def _load_metadata(self): - status = await self._revalidate_printer_status() + async def _update_endpoint_data(self, endpoint: str) -> bool: + can_update = False - if status is None: - raise IntegrationAPIError(PRODUCT_STATUS_ENDPOINT) + try: + now = datetime.now() + now_ts = now.timestamp() - async def _revalidate_printer_status(self): - now = datetime.now() - now_ts = now.timestamp() + last_update = self._last_update.get(endpoint, 0) - status_endpoint = PRODUCT_STATUS_ENDPOINT - last_update = self._last_update.get(status_endpoint, 0) + last_update_diff = int(now_ts - last_update) + interval = self._config_manager.get_update_interval(endpoint) - last_update_diff = int(now_ts - last_update) - interval = self._config_manager.get_update_interval(status_endpoint) + can_update = last_update_diff >= interval - if interval > last_update_diff: - return None + if can_update: + data = await self._get_request(endpoint) + + self._raw_data[endpoint] = data + self._last_update[endpoint] = now_ts + + except Exception as ex: + exc_type, exc_obj, tb = sys.exc_info() + line_number = tb.tb_lineno - product_status_data = await self._get_request(status_endpoint) - self._is_printer_online = product_status_data is not None + _LOGGER.error( + f"Failed to update endpoint {endpoint} data, Error: {ex}, Line: {line_number}" + ) - if self._is_printer_online: - self._last_update = {} + return can_update - return product_status_data + async def _update_product_status_endpoint_data(self) -> bool: + was_changed = False - return None + try: + status_endpoint = PRODUCT_STATUS_ENDPOINT - async def update(self): - product_status_data: dict | None = None - endpoints_updated = 0 + was_online = self.is_online + was_updated = await self._update_endpoint_data(status_endpoint) - if not self._is_printer_online: - product_status_data = await self._revalidate_printer_status() + if was_updated: + product_status_data = self._raw_data.get(status_endpoint) + self._is_online = product_status_data is not None - if product_status_data is None: - return endpoints_updated + if not self._is_online: + self._raw_data[status_endpoint] = PRODUCT_STATUS_OFFLINE_PAYLOAD - now = datetime.now() - now_ts = now.timestamp() + was_changed = self._is_online != was_online - for endpoint in self._endpoints: - last_update = self._last_update.get(endpoint, 0) - last_update_diff = int(now_ts - last_update) - interval = self._config_manager.get_update_interval(endpoint) + if was_changed: + _LOGGER.debug(f"Device online state changed to {self._is_online}") - if interval > last_update_diff: - continue + got_online = not was_online and self._is_online - if endpoint == PRODUCT_STATUS_ENDPOINT and product_status_data is not None: - resource_data = product_status_data + if got_online: + for endpoint in self._last_update: + if endpoint != PRODUCT_STATUS_ENDPOINT: + self._last_update[endpoint] = 0 - else: - resource_data = await self._get_request(endpoint) + except Exception as ex: + exc_type, exc_obj, tb = sys.exc_info() + line_number = tb.tb_lineno - endpoints_updated += 1 + _LOGGER.error( + f"Failed to update product status data, Error: {ex}, Line: {line_number}" + ) - if resource_data is None: - if endpoint == PRODUCT_STATUS_ENDPOINT: - self._raw_data[endpoint] = PRODUCT_STATUS_OFFLINE_PAYLOAD + return was_changed - else: - self._raw_data[endpoint] = resource_data - self._last_update[endpoint] = now.timestamp() + async def update(self, endpoints: list[str] = None): + try: + _LOGGER.debug(f"Updating data from {self.config_data.hostname}") + + was_changed = await self._update_product_status_endpoint_data() + update_counter = 1 if was_changed else 0 - devices = self._get_devices_data() + if self._is_online: + if endpoints is None: + endpoints = self._config_manager.endpoints - self._extract_data(devices) + for endpoint in endpoints: + was_updated = await self._update_endpoint_data(endpoint) - _LOGGER.debug(f"Scheduled update: {endpoints_updated} endpoints were updated") + if was_updated: + update_counter += 1 + + if update_counter > 0: + devices = self._get_devices_data() + + self._extract_data(devices) + + except Exception as ex: + exc_type, exc_obj, tb = sys.exc_info() + line_number = tb.tb_lineno + + _LOGGER.error(f"Failed to update data, Error: {ex}, Line: {line_number}") def _extract_data(self, devices: list[dict]): device_data = {} @@ -362,9 +378,7 @@ def _get_device_data( return data - async def _get_request( - self, endpoint: str, ignore_error: bool = False - ) -> dict | None: + async def _get_request(self, endpoint: str) -> dict | None: result: dict | None = None start_ts = datetime.now().timestamp() @@ -400,48 +414,48 @@ async def _get_request( 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 - completed_ts = datetime.now().timestamp() - time_taken = completed_ts - start_ts - - _LOGGER.debug( - f"Failed to get response from {endpoint}, " - f"Error: {cre}, " - f"Line: {line_number}, " - f"Time: {time_taken:.3f}s" - ) + exc_type, exc_obj, tb = sys.exc_info() + line_number = tb.tb_lineno + completed_ts = datetime.now().timestamp() + time_taken = completed_ts - start_ts - else: - if not ignore_error: - exc_type, exc_obj, tb = sys.exc_info() - line_number = tb.tb_lineno - completed_ts = datetime.now().timestamp() - time_taken = completed_ts - start_ts - - _LOGGER.error( - f"Failed to get response from {endpoint}, " - f"Error: {cre}, " - f"Line: {line_number}, " - f"Time: {time_taken:.3f}s" - ) + _LOGGER.debug( + f"Failed to get response from {endpoint}, " + f"Error: {cre.status}, " + f"Line: {line_number}, " + f"Time: {time_taken:.3f}s" + ) - except Exception as ex: - if not ignore_error: + else: exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno - completed_ts = datetime.now().timestamp() time_taken = completed_ts - start_ts _LOGGER.error( - f"Failed to get {endpoint}, " - f"Error: {ex}, " + f"Failed to get response from {endpoint}, " + f"Error: {cre.status}, " f"Line: {line_number}, " f"Time: {time_taken:.3f}s" ) + except TimeoutError: + _LOGGER.error(f"Failed to get {endpoint} due to timeout") + + except Exception as ex: + exc_type, exc_obj, tb = sys.exc_info() + line_number = tb.tb_lineno + + completed_ts = datetime.now().timestamp() + time_taken = completed_ts - start_ts + + _LOGGER.error( + f"Failed to get {endpoint}, " + f"Error: {ex}, " + f"Line: {line_number}, " + f"Time: {time_taken:.3f}s" + ) + return result def _clean_data(self, xml) -> dict: diff --git a/custom_components/hpprinter/models/exceptions.py b/custom_components/hpprinter/models/exceptions.py index e255c6b..e69de29 100644 --- a/custom_components/hpprinter/models/exceptions.py +++ b/custom_components/hpprinter/models/exceptions.py @@ -1,19 +0,0 @@ -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/utils/api_test.py b/utils/api_test.py index 970dc4a..31706ae 100644 --- a/utils/api_test.py +++ b/utils/api_test.py @@ -4,7 +4,12 @@ import sys from custom_components.hpprinter import HAConfigManager -from custom_components.hpprinter.common.consts import DATA_KEYS +from custom_components.hpprinter.common.consts import ( + DATA_KEYS, + MODEL_PROPERTY, + PRINTER_MAIN_DEVICE, + PRODUCT_MAIN_ENDPOINT, +) from custom_components.hpprinter.managers.rest_api import RestAPIv2 from homeassistant.core import HomeAssistant @@ -41,16 +46,34 @@ async def initialize(self): await self._config_manager.initialize(self._config_data) self._api = RestAPIv2(hass, self._config_manager) - await self._api.initialize(True) + await self._api.initialize() - for i in range(0, 2): - await self._api.update() + if self._api.is_online: - # print(json.dumps(self._api.data_config, indent=4)) - # print(json.dumps(self._api.data, indent=4)) - # print(json.dumps(self._api.data[PRINTER_MAIN_DEVICE], indent=4)) + await self._api.update([PRODUCT_MAIN_ENDPOINT]) - await asyncio.sleep(5) + main_device = self._api.data.get(PRINTER_MAIN_DEVICE) + model = main_device.get(MODEL_PROPERTY) + title = f"{model} ({self._api.config_data.hostname})" + + print(title) + + for i in range(0, 1): + # self._api.config_data.update({ + # key: os.environ.get(key) if i % 2 == 0 or key != "host" else "127.0.0.1" + # for key in self._api.config_data.to_dict() + #}) + + await self._api.update() + + # print(json.dumps(self._api.data_config, indent=4)) + # print(json.dumps(self._api.data, indent=4)) + # print(json.dumps(self._api.data[PRINTER_MAIN_DEVICE], indent=4)) + + await asyncio.sleep(10) + + else: + _LOGGER.warning("Failed to connect") if __name__ == "__main__":