aiohpprinter 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aiohpprinter/__init__.py +51 -0
- aiohpprinter/client.py +322 -0
- aiohpprinter/const.py +39 -0
- aiohpprinter/exceptions.py +35 -0
- aiohpprinter/models.py +179 -0
- aiohpprinter/parsers/__init__.py +38 -0
- aiohpprinter/parsers/adapter.py +38 -0
- aiohpprinter/parsers/common.py +95 -0
- aiohpprinter/parsers/consumable.py +143 -0
- aiohpprinter/parsers/device.py +33 -0
- aiohpprinter/parsers/eprint.py +31 -0
- aiohpprinter/parsers/status.py +36 -0
- aiohpprinter/parsers/usage.py +67 -0
- aiohpprinter/parsers/wifi.py +45 -0
- aiohpprinter/py.typed +0 -0
- aiohpprinter/xml.py +148 -0
- aiohpprinter-1.0.0.dist-info/METADATA +278 -0
- aiohpprinter-1.0.0.dist-info/RECORD +21 -0
- aiohpprinter-1.0.0.dist-info/WHEEL +5 -0
- aiohpprinter-1.0.0.dist-info/licenses/LICENSE +202 -0
- aiohpprinter-1.0.0.dist-info/top_level.txt +1 -0
aiohpprinter/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Copyright 2026 Simone Chemelli and contributors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Async Python library for HP printers exposing an embedded web server (EWS)."""
|
|
5
|
+
|
|
6
|
+
from .client import HpPrinter
|
|
7
|
+
from .exceptions import (
|
|
8
|
+
HpPrinterConnectionError,
|
|
9
|
+
HpPrinterError,
|
|
10
|
+
HpPrinterHttpError,
|
|
11
|
+
HpPrinterParseError,
|
|
12
|
+
HpPrinterTimeoutError,
|
|
13
|
+
HpPrinterUnsupportedError,
|
|
14
|
+
)
|
|
15
|
+
from .models import (
|
|
16
|
+
HpAdapter,
|
|
17
|
+
HpConsumable,
|
|
18
|
+
HpCopyUsage,
|
|
19
|
+
HpEPrint,
|
|
20
|
+
HpFaxUsage,
|
|
21
|
+
HpPrinterData,
|
|
22
|
+
HpPrinterDevice,
|
|
23
|
+
HpPrinterStatus,
|
|
24
|
+
HpPrinterUsage,
|
|
25
|
+
HpScannerUsage,
|
|
26
|
+
HpWifi,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__version__ = "1.0.0"
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"HpAdapter",
|
|
33
|
+
"HpConsumable",
|
|
34
|
+
"HpCopyUsage",
|
|
35
|
+
"HpEPrint",
|
|
36
|
+
"HpFaxUsage",
|
|
37
|
+
"HpPrinter",
|
|
38
|
+
"HpPrinterConnectionError",
|
|
39
|
+
"HpPrinterData",
|
|
40
|
+
"HpPrinterDevice",
|
|
41
|
+
"HpPrinterError",
|
|
42
|
+
"HpPrinterHttpError",
|
|
43
|
+
"HpPrinterParseError",
|
|
44
|
+
"HpPrinterStatus",
|
|
45
|
+
"HpPrinterTimeoutError",
|
|
46
|
+
"HpPrinterUnsupportedError",
|
|
47
|
+
"HpPrinterUsage",
|
|
48
|
+
"HpScannerUsage",
|
|
49
|
+
"HpWifi",
|
|
50
|
+
"__version__",
|
|
51
|
+
]
|
aiohpprinter/client.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# Copyright 2026 Simone Chemelli and contributors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Async client for an HP printer's embedded web server (EWS)."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
import aiohttp
|
|
11
|
+
|
|
12
|
+
from .const import (
|
|
13
|
+
_LOGGER,
|
|
14
|
+
DEFAULT_PORT,
|
|
15
|
+
DEFAULT_TIMEOUT,
|
|
16
|
+
ENDPOINT_ADAPTERS,
|
|
17
|
+
ENDPOINT_CONSUMABLE_CONFIG,
|
|
18
|
+
ENDPOINT_EPRINT_CONFIG,
|
|
19
|
+
ENDPOINT_NET_APPS_SECURE,
|
|
20
|
+
ENDPOINT_PRODUCT_CONFIG,
|
|
21
|
+
ENDPOINT_PRODUCT_STATUS,
|
|
22
|
+
ENDPOINT_PRODUCT_USAGE,
|
|
23
|
+
JSON_CONTENT_TYPES,
|
|
24
|
+
)
|
|
25
|
+
from .exceptions import (
|
|
26
|
+
HpPrinterConnectionError,
|
|
27
|
+
HpPrinterError,
|
|
28
|
+
HpPrinterHttpError,
|
|
29
|
+
HpPrinterParseError,
|
|
30
|
+
HpPrinterTimeoutError,
|
|
31
|
+
)
|
|
32
|
+
from .models import HpPrinterData
|
|
33
|
+
from .parsers import (
|
|
34
|
+
parse_adapters,
|
|
35
|
+
parse_consumables,
|
|
36
|
+
parse_copy_usage,
|
|
37
|
+
parse_device,
|
|
38
|
+
parse_eprint,
|
|
39
|
+
parse_fax_usage,
|
|
40
|
+
parse_printer_usage,
|
|
41
|
+
parse_scanner_usage,
|
|
42
|
+
parse_status,
|
|
43
|
+
parse_wifi,
|
|
44
|
+
)
|
|
45
|
+
from .xml import parse_document, parse_json_document
|
|
46
|
+
|
|
47
|
+
if TYPE_CHECKING:
|
|
48
|
+
from collections.abc import Awaitable, Callable
|
|
49
|
+
from types import TracebackType
|
|
50
|
+
from typing import Any, Self
|
|
51
|
+
|
|
52
|
+
from .models import (
|
|
53
|
+
HpAdapter,
|
|
54
|
+
HpConsumable,
|
|
55
|
+
HpCopyUsage,
|
|
56
|
+
HpEPrint,
|
|
57
|
+
HpFaxUsage,
|
|
58
|
+
HpPrinterDevice,
|
|
59
|
+
HpPrinterStatus,
|
|
60
|
+
HpPrinterUsage,
|
|
61
|
+
HpScannerUsage,
|
|
62
|
+
HpWifi,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class HpPrinter:
|
|
67
|
+
"""Async client for an HP printer's embedded web server (EWS).
|
|
68
|
+
|
|
69
|
+
Session ownership: when no `session` is given, `HpPrinter` creates and
|
|
70
|
+
owns an `aiohttp.ClientSession` for the lifetime of the `async with`
|
|
71
|
+
block (or until :meth:`close` is called). When a `session` is injected
|
|
72
|
+
- the recommended approach for applications such as Home Assistant that
|
|
73
|
+
manage their own connection pooling - the caller owns it and remains
|
|
74
|
+
responsible for closing it; `HpPrinter` never closes a session it did
|
|
75
|
+
not create.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
host: str,
|
|
81
|
+
session: aiohttp.ClientSession | None = None,
|
|
82
|
+
*,
|
|
83
|
+
port: int = DEFAULT_PORT,
|
|
84
|
+
ssl: bool = False,
|
|
85
|
+
timeout: aiohttp.ClientTimeout = DEFAULT_TIMEOUT,
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Initialize the client for the printer at `host`."""
|
|
88
|
+
self._host = host
|
|
89
|
+
self._port = port
|
|
90
|
+
self._ssl = ssl
|
|
91
|
+
self._timeout = timeout
|
|
92
|
+
self._session = session
|
|
93
|
+
self._owns_session = session is None
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def base_url(self) -> str:
|
|
97
|
+
"""Return the printer's EWS base URL."""
|
|
98
|
+
protocol = "https" if self._ssl else "http"
|
|
99
|
+
return f"{protocol}://{self._host}:{self._port}"
|
|
100
|
+
|
|
101
|
+
async def __aenter__(self) -> Self:
|
|
102
|
+
"""Open the client, creating a session if none was injected."""
|
|
103
|
+
if self._session is None:
|
|
104
|
+
self._session = aiohttp.ClientSession()
|
|
105
|
+
|
|
106
|
+
return self
|
|
107
|
+
|
|
108
|
+
async def __aexit__(
|
|
109
|
+
self,
|
|
110
|
+
exc_type: type[BaseException] | None,
|
|
111
|
+
exc: BaseException | None,
|
|
112
|
+
traceback: TracebackType | None,
|
|
113
|
+
) -> None:
|
|
114
|
+
"""Close the client, releasing only a session it created itself."""
|
|
115
|
+
await self.close()
|
|
116
|
+
|
|
117
|
+
async def close(self) -> None:
|
|
118
|
+
"""Close the underlying session, if this client owns it."""
|
|
119
|
+
if self._owns_session and self._session is not None:
|
|
120
|
+
await self._session.close()
|
|
121
|
+
self._session = None
|
|
122
|
+
|
|
123
|
+
async def is_online(self) -> bool:
|
|
124
|
+
"""Return whether the printer currently responds to status requests.
|
|
125
|
+
|
|
126
|
+
Any failure - offline, powered down, unreachable, or an unexpected
|
|
127
|
+
response - is treated as "not online" rather than raised, since that
|
|
128
|
+
is what this check exists to answer. Call :meth:`status` directly to
|
|
129
|
+
distinguish those failure modes.
|
|
130
|
+
"""
|
|
131
|
+
try:
|
|
132
|
+
await self.status()
|
|
133
|
+
except HpPrinterError:
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
async def device(self) -> HpPrinterDevice:
|
|
139
|
+
"""Fetch printer identity information."""
|
|
140
|
+
document = await self._request(ENDPOINT_PRODUCT_CONFIG)
|
|
141
|
+
|
|
142
|
+
return parse_device(document)
|
|
143
|
+
|
|
144
|
+
async def status(self) -> HpPrinterStatus:
|
|
145
|
+
"""Fetch the printer's current operational status."""
|
|
146
|
+
document = await self._request(ENDPOINT_PRODUCT_STATUS)
|
|
147
|
+
|
|
148
|
+
return parse_status(document)
|
|
149
|
+
|
|
150
|
+
async def consumables(self) -> list[HpConsumable]:
|
|
151
|
+
"""Fetch the merged list of ink/toner consumables."""
|
|
152
|
+
config_document = await self._request(ENDPOINT_CONSUMABLE_CONFIG)
|
|
153
|
+
usage_document = await self._request(ENDPOINT_PRODUCT_USAGE)
|
|
154
|
+
|
|
155
|
+
return parse_consumables(config_document, usage_document)
|
|
156
|
+
|
|
157
|
+
async def printer_usage(
|
|
158
|
+
self,
|
|
159
|
+
document: dict[str, Any] | None = None,
|
|
160
|
+
) -> HpPrinterUsage:
|
|
161
|
+
"""Fetch cumulative printer engine page counters.
|
|
162
|
+
|
|
163
|
+
`document` lets callers that already fetched `ProductUsageDyn` (for
|
|
164
|
+
example :meth:`update`) reuse it instead of requesting it again.
|
|
165
|
+
"""
|
|
166
|
+
if document is None:
|
|
167
|
+
document = await self._request(ENDPOINT_PRODUCT_USAGE)
|
|
168
|
+
|
|
169
|
+
return parse_printer_usage(document)
|
|
170
|
+
|
|
171
|
+
async def scanner_usage(
|
|
172
|
+
self,
|
|
173
|
+
document: dict[str, Any] | None = None,
|
|
174
|
+
) -> HpScannerUsage:
|
|
175
|
+
"""Fetch cumulative scanner engine page counters.
|
|
176
|
+
|
|
177
|
+
See :meth:`printer_usage` for the meaning of `document`.
|
|
178
|
+
"""
|
|
179
|
+
if document is None:
|
|
180
|
+
document = await self._request(ENDPOINT_PRODUCT_USAGE)
|
|
181
|
+
|
|
182
|
+
return parse_scanner_usage(document)
|
|
183
|
+
|
|
184
|
+
async def copy_usage(
|
|
185
|
+
self,
|
|
186
|
+
document: dict[str, Any] | None = None,
|
|
187
|
+
) -> HpCopyUsage:
|
|
188
|
+
"""Fetch cumulative copy application page counters.
|
|
189
|
+
|
|
190
|
+
See :meth:`printer_usage` for the meaning of `document`.
|
|
191
|
+
"""
|
|
192
|
+
if document is None:
|
|
193
|
+
document = await self._request(ENDPOINT_PRODUCT_USAGE)
|
|
194
|
+
|
|
195
|
+
return parse_copy_usage(document)
|
|
196
|
+
|
|
197
|
+
async def fax_usage(
|
|
198
|
+
self,
|
|
199
|
+
document: dict[str, Any] | None = None,
|
|
200
|
+
) -> HpFaxUsage:
|
|
201
|
+
"""Fetch cumulative fax application page counters.
|
|
202
|
+
|
|
203
|
+
See :meth:`printer_usage` for the meaning of `document`.
|
|
204
|
+
"""
|
|
205
|
+
if document is None:
|
|
206
|
+
document = await self._request(ENDPOINT_PRODUCT_USAGE)
|
|
207
|
+
|
|
208
|
+
return parse_fax_usage(document)
|
|
209
|
+
|
|
210
|
+
async def adapters(self) -> list[HpAdapter]:
|
|
211
|
+
"""Fetch the printer's network adapters."""
|
|
212
|
+
document = await self._request(ENDPOINT_ADAPTERS)
|
|
213
|
+
|
|
214
|
+
return parse_adapters(document)
|
|
215
|
+
|
|
216
|
+
async def eprint(self) -> HpEPrint:
|
|
217
|
+
"""Fetch HP ePrint cloud printing configuration."""
|
|
218
|
+
document = await self._request(ENDPOINT_EPRINT_CONFIG)
|
|
219
|
+
|
|
220
|
+
return parse_eprint(document)
|
|
221
|
+
|
|
222
|
+
async def wifi(self) -> HpWifi:
|
|
223
|
+
"""Fetch Wireless Direct configuration."""
|
|
224
|
+
document = await self._request(ENDPOINT_NET_APPS_SECURE)
|
|
225
|
+
|
|
226
|
+
return parse_wifi(document)
|
|
227
|
+
|
|
228
|
+
async def update(self) -> HpPrinterData:
|
|
229
|
+
"""Fetch everything the printer currently supports.
|
|
230
|
+
|
|
231
|
+
Only the status endpoint is treated as required: if the printer
|
|
232
|
+
does not answer it, `update` returns immediately with
|
|
233
|
+
`online=False` and every other field left at its default. Every
|
|
234
|
+
other endpoint is optional - a missing or failing one leaves its
|
|
235
|
+
field `None` (or an empty list) without aborting the rest of the
|
|
236
|
+
update, since printer models differ in which features they expose.
|
|
237
|
+
"""
|
|
238
|
+
try:
|
|
239
|
+
status = await self.status()
|
|
240
|
+
except HpPrinterError:
|
|
241
|
+
return HpPrinterData(online=False)
|
|
242
|
+
|
|
243
|
+
usage_document = await self._safe_request(ENDPOINT_PRODUCT_USAGE)
|
|
244
|
+
config_document = await self._safe_request(ENDPOINT_CONSUMABLE_CONFIG)
|
|
245
|
+
|
|
246
|
+
return HpPrinterData(
|
|
247
|
+
online=True,
|
|
248
|
+
device=await self._safe(self.device),
|
|
249
|
+
status=status,
|
|
250
|
+
consumables=parse_consumables(config_document, usage_document),
|
|
251
|
+
printer_usage=(
|
|
252
|
+
parse_printer_usage(usage_document)
|
|
253
|
+
if usage_document is not None
|
|
254
|
+
else None
|
|
255
|
+
),
|
|
256
|
+
scanner_usage=(
|
|
257
|
+
parse_scanner_usage(usage_document)
|
|
258
|
+
if usage_document is not None
|
|
259
|
+
else None
|
|
260
|
+
),
|
|
261
|
+
copy_usage=(
|
|
262
|
+
parse_copy_usage(usage_document) if usage_document is not None else None
|
|
263
|
+
),
|
|
264
|
+
fax_usage=(
|
|
265
|
+
parse_fax_usage(usage_document) if usage_document is not None else None
|
|
266
|
+
),
|
|
267
|
+
adapters=await self._safe(self.adapters) or [],
|
|
268
|
+
eprint=await self._safe(self.eprint),
|
|
269
|
+
wifi=await self._safe(self.wifi),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
async def _safe[T](self, func: Callable[[], Awaitable[T]]) -> T | None:
|
|
273
|
+
"""Await `func`, turning any `HpPrinterError` into `None`."""
|
|
274
|
+
try:
|
|
275
|
+
return await func()
|
|
276
|
+
except HpPrinterError:
|
|
277
|
+
_LOGGER.debug("Optional endpoint unavailable for %s", func.__name__)
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
async def _safe_request(self, endpoint: str) -> dict[str, Any] | None:
|
|
281
|
+
"""Fetch and parse `endpoint`, turning any `HpPrinterError` into `None`."""
|
|
282
|
+
try:
|
|
283
|
+
return await self._request(endpoint)
|
|
284
|
+
except HpPrinterError:
|
|
285
|
+
_LOGGER.debug("Optional endpoint unavailable: %s", endpoint)
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
async def _request(self, endpoint: str) -> dict[str, Any]:
|
|
289
|
+
"""Fetch `endpoint` and parse it into a nested dict document."""
|
|
290
|
+
if self._session is None:
|
|
291
|
+
message = (
|
|
292
|
+
"HpPrinter has no active session; "
|
|
293
|
+
"use it as an async context manager or inject one"
|
|
294
|
+
)
|
|
295
|
+
raise HpPrinterConnectionError(message)
|
|
296
|
+
|
|
297
|
+
url = f"{self.base_url}{endpoint}"
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
async with self._session.get(url, timeout=self._timeout) as response:
|
|
301
|
+
if response.status >= 400: # noqa: PLR2004
|
|
302
|
+
message = f"{url} returned HTTP {response.status}"
|
|
303
|
+
raise HpPrinterHttpError(response.status, message)
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
content = await response.text()
|
|
307
|
+
except UnicodeDecodeError as ex:
|
|
308
|
+
message = f"Could not decode response from {url}: {ex}"
|
|
309
|
+
raise HpPrinterParseError(message) from ex
|
|
310
|
+
|
|
311
|
+
content_type = response.content_type
|
|
312
|
+
except TimeoutError as ex:
|
|
313
|
+
message = f"Timed out requesting {url}"
|
|
314
|
+
raise HpPrinterTimeoutError(message) from ex
|
|
315
|
+
except aiohttp.ClientError as ex:
|
|
316
|
+
message = f"Could not connect to {url}: {ex}"
|
|
317
|
+
raise HpPrinterConnectionError(message) from ex
|
|
318
|
+
|
|
319
|
+
if content_type in JSON_CONTENT_TYPES:
|
|
320
|
+
return parse_json_document(content)
|
|
321
|
+
|
|
322
|
+
return parse_document(content)
|
aiohpprinter/const.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Copyright 2026 Simone Chemelli and contributors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Constants for aiohpprinter."""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from aiohttp import ClientTimeout
|
|
9
|
+
|
|
10
|
+
_LOGGER = logging.getLogger(__package__)
|
|
11
|
+
|
|
12
|
+
DEFAULT_PORT = 80
|
|
13
|
+
DEFAULT_TIMEOUT = ClientTimeout(total=10)
|
|
14
|
+
|
|
15
|
+
# HP embedded web server (EWS) DevMgmt/ePrint/IoMgmt endpoints. These are the
|
|
16
|
+
# same URIs used by the upstream ha-hpprinter integration.
|
|
17
|
+
ENDPOINT_PRODUCT_STATUS = "/DevMgmt/ProductStatusDyn.xml"
|
|
18
|
+
ENDPOINT_PRODUCT_CONFIG = "/DevMgmt/ProductConfigDyn.xml"
|
|
19
|
+
ENDPOINT_CONSUMABLE_CONFIG = "/DevMgmt/ConsumableConfigDyn.xml"
|
|
20
|
+
ENDPOINT_PRODUCT_USAGE = "/DevMgmt/ProductUsageDyn.xml"
|
|
21
|
+
ENDPOINT_ADAPTERS = "/IoMgmt/Adapters"
|
|
22
|
+
ENDPOINT_EPRINT_CONFIG = "/ePrint/ePrintConfigDyn.xml"
|
|
23
|
+
ENDPOINT_NET_APPS_SECURE = "/DevMgmt/NetAppsSecureDyn.xml"
|
|
24
|
+
|
|
25
|
+
# A small number of endpoints (observed on `/IoMgmt/Adapters`) answer with a
|
|
26
|
+
# JSON body instead of XML, using either of these content types.
|
|
27
|
+
JSON_CONTENT_TYPES = frozenset({"application/javascript", "application/json"})
|
|
28
|
+
|
|
29
|
+
# Keys present on every response's root element that carry protocol/schema
|
|
30
|
+
# metadata rather than printer data, dropped once parsed.
|
|
31
|
+
IGNORED_ROOT_KEYS = ("@schemaLocation", "Version")
|
|
32
|
+
|
|
33
|
+
MARKER_COLOR_TO_LABEL_CODE = {
|
|
34
|
+
"Cyan": "C",
|
|
35
|
+
"Yellow": "Y",
|
|
36
|
+
"Magenta": "M",
|
|
37
|
+
"CyanMagentaYellow": "CMY",
|
|
38
|
+
"Black": "K",
|
|
39
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Copyright 2026 Simone Chemelli and contributors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""aiohpprinter library exceptions."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class HpPrinterError(Exception):
|
|
10
|
+
"""Base class for all aiohpprinter errors."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HpPrinterConnectionError(HpPrinterError):
|
|
14
|
+
"""Raised when the printer cannot be reached over the network."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HpPrinterTimeoutError(HpPrinterError):
|
|
18
|
+
"""Raised when a request to the printer times out."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HpPrinterHttpError(HpPrinterError):
|
|
22
|
+
"""Raised when the printer returns an unsuccessful HTTP status code."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, status: int, message: str) -> None:
|
|
25
|
+
"""Initialize the error with the offending HTTP status code."""
|
|
26
|
+
super().__init__(message)
|
|
27
|
+
self.status = status
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class HpPrinterParseError(HpPrinterError):
|
|
31
|
+
"""Raised when a printer response cannot be parsed as valid XML."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class HpPrinterUnsupportedError(HpPrinterError):
|
|
35
|
+
"""Raised when the printer does not expose a required endpoint."""
|
aiohpprinter/models.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Copyright 2026 Simone Chemelli and contributors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Typed printer-domain models returned by :class:`aiohpprinter.HpPrinter`.
|
|
5
|
+
|
|
6
|
+
These describe HP printer concepts (device identity, status, consumables,
|
|
7
|
+
usage counters, ...), not Home Assistant entities. Every field is optional
|
|
8
|
+
unless the printer protocol guarantees it, since firmware and product
|
|
9
|
+
generation determine which elements a given model actually reports.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class HpPrinterDevice:
|
|
23
|
+
"""Identity information for the printer as a whole."""
|
|
24
|
+
|
|
25
|
+
make_and_model: str | None = None
|
|
26
|
+
make_and_model_family: str | None = None
|
|
27
|
+
sku_identifier: str | None = None
|
|
28
|
+
serial_number: str | None = None
|
|
29
|
+
product_number: str | None = None
|
|
30
|
+
manufacturer_name: str | None = None
|
|
31
|
+
manufactured_at: datetime | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True, slots=True)
|
|
35
|
+
class HpPrinterStatus:
|
|
36
|
+
"""The printer's current operational status."""
|
|
37
|
+
|
|
38
|
+
device_status: str | None = None
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def is_ready(self) -> bool:
|
|
42
|
+
"""Return whether the printer is idle and ready to accept jobs."""
|
|
43
|
+
return self.device_status == "ready"
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def is_off(self) -> bool:
|
|
47
|
+
"""Return whether the printer is powered off or unreachable."""
|
|
48
|
+
return self.device_status is None or self.device_status == "off"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class HpConsumable:
|
|
53
|
+
"""A single ink/toner consumable, merged from config and usage data.
|
|
54
|
+
|
|
55
|
+
``consumable_id`` is the value the two source endpoints are joined on:
|
|
56
|
+
the consumable label code (for example ``"C"``, ``"M"``, ``"Y"``,
|
|
57
|
+
``"K"``, or ``"CMY"`` for a combined cartridge).
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
consumable_id: str
|
|
61
|
+
consumable_type: str | None = None
|
|
62
|
+
marker_color: str | None = None
|
|
63
|
+
state: str | None = None
|
|
64
|
+
usage_state: str | None = None
|
|
65
|
+
brand: str | None = None
|
|
66
|
+
station: str | None = None
|
|
67
|
+
percentage_level_remaining: float | None = None
|
|
68
|
+
raw_percentage_level_remaining: float | None = None
|
|
69
|
+
estimated_pages_remaining: int | None = None
|
|
70
|
+
max_capacity: float | None = None
|
|
71
|
+
selectibility_number: str | None = None
|
|
72
|
+
manufacturer_name: str | None = None
|
|
73
|
+
manufactured_at: datetime | None = None
|
|
74
|
+
installed_at: datetime | None = None
|
|
75
|
+
warranty_expires_at: datetime | None = None
|
|
76
|
+
serial_number: str | None = None
|
|
77
|
+
product_number: str | None = None
|
|
78
|
+
supply_serial_number: str | None = None
|
|
79
|
+
unique_id: str | None = None
|
|
80
|
+
counterfeit_refilled_count: int | None = None
|
|
81
|
+
genuine_refilled_count: int | None = None
|
|
82
|
+
total_impressions: int | None = None
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def is_ok(self) -> bool:
|
|
86
|
+
"""Return whether the consumable reports a healthy life state."""
|
|
87
|
+
return self.state in {"ok", "newGenuineHP"}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True, slots=True)
|
|
91
|
+
class HpPrinterUsage:
|
|
92
|
+
"""Cumulative page counters for the printer engine."""
|
|
93
|
+
|
|
94
|
+
total_impressions: int | None = None
|
|
95
|
+
monochrome_impressions: int | None = None
|
|
96
|
+
color_impressions: int | None = None
|
|
97
|
+
simplex_sheets: int | None = None
|
|
98
|
+
duplex_sheets: int | None = None
|
|
99
|
+
jam_events: int | None = None
|
|
100
|
+
mispick_events: int | None = None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True, slots=True)
|
|
104
|
+
class HpScannerUsage:
|
|
105
|
+
"""Cumulative page counters for the scanner engine."""
|
|
106
|
+
|
|
107
|
+
scan_images: int | None = None
|
|
108
|
+
adf_images: int | None = None
|
|
109
|
+
duplex_sheets: int | None = None
|
|
110
|
+
flatbed_images: int | None = None
|
|
111
|
+
jam_events: int | None = None
|
|
112
|
+
mispick_events: int | None = None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass(frozen=True, slots=True)
|
|
116
|
+
class HpCopyUsage:
|
|
117
|
+
"""Cumulative page counters for the copy application."""
|
|
118
|
+
|
|
119
|
+
total_impressions: int | None = None
|
|
120
|
+
adf_images: int | None = None
|
|
121
|
+
flatbed_images: int | None = None
|
|
122
|
+
monochrome_impressions: int | None = None
|
|
123
|
+
color_impressions: int | None = None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(frozen=True, slots=True)
|
|
127
|
+
class HpFaxUsage:
|
|
128
|
+
"""Cumulative page counters for the fax application."""
|
|
129
|
+
|
|
130
|
+
total_impressions: int | None = None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(frozen=True, slots=True)
|
|
134
|
+
class HpAdapter:
|
|
135
|
+
"""A network adapter reported by the printer's I/O management."""
|
|
136
|
+
|
|
137
|
+
name: str | None = None
|
|
138
|
+
connectivity_port_type: str | None = None
|
|
139
|
+
is_connected: bool | None = None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass(frozen=True, slots=True)
|
|
143
|
+
class HpEPrint:
|
|
144
|
+
"""HP ePrint cloud printing configuration."""
|
|
145
|
+
|
|
146
|
+
printer_id: str | None = None
|
|
147
|
+
is_registered: bool | None = None
|
|
148
|
+
cloud_services_enabled: bool | None = None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass(frozen=True, slots=True)
|
|
152
|
+
class HpWifi:
|
|
153
|
+
"""Wireless Direct configuration."""
|
|
154
|
+
|
|
155
|
+
ssid_prefix: str | None = None
|
|
156
|
+
connection_method: str | None = None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@dataclass(frozen=True, slots=True)
|
|
160
|
+
class HpPrinterData:
|
|
161
|
+
"""Aggregate snapshot of everything :meth:`HpPrinter.update` collected.
|
|
162
|
+
|
|
163
|
+
Every section besides ``online`` is `None` (or an empty list) when its
|
|
164
|
+
endpoint is not supported by this printer model, or could not be
|
|
165
|
+
retrieved on this update cycle - see the client's optional-endpoint
|
|
166
|
+
handling for details.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
online: bool
|
|
170
|
+
device: HpPrinterDevice | None = None
|
|
171
|
+
status: HpPrinterStatus | None = None
|
|
172
|
+
consumables: list[HpConsumable] = field(default_factory=list)
|
|
173
|
+
printer_usage: HpPrinterUsage | None = None
|
|
174
|
+
scanner_usage: HpScannerUsage | None = None
|
|
175
|
+
copy_usage: HpCopyUsage | None = None
|
|
176
|
+
fax_usage: HpFaxUsage | None = None
|
|
177
|
+
adapters: list[HpAdapter] = field(default_factory=list)
|
|
178
|
+
eprint: HpEPrint | None = None
|
|
179
|
+
wifi: HpWifi | None = None
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Copyright 2026 Simone Chemelli and contributors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Pure, synchronous XML-to-model parsers, one module per EWS endpoint.
|
|
5
|
+
|
|
6
|
+
Parsing in-memory XML/dict data is not I/O, so unlike the HTTP client these
|
|
7
|
+
functions are plain synchronous callables. Each takes the dict produced by
|
|
8
|
+
:func:`aiohpprinter.xml.parse_document` and returns typed models from
|
|
9
|
+
:mod:`aiohpprinter.models`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from .adapter import parse_adapters
|
|
15
|
+
from .consumable import parse_consumables
|
|
16
|
+
from .device import parse_device
|
|
17
|
+
from .eprint import parse_eprint
|
|
18
|
+
from .status import parse_status
|
|
19
|
+
from .usage import (
|
|
20
|
+
parse_copy_usage,
|
|
21
|
+
parse_fax_usage,
|
|
22
|
+
parse_printer_usage,
|
|
23
|
+
parse_scanner_usage,
|
|
24
|
+
)
|
|
25
|
+
from .wifi import parse_wifi
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"parse_adapters",
|
|
29
|
+
"parse_consumables",
|
|
30
|
+
"parse_copy_usage",
|
|
31
|
+
"parse_device",
|
|
32
|
+
"parse_eprint",
|
|
33
|
+
"parse_fax_usage",
|
|
34
|
+
"parse_printer_usage",
|
|
35
|
+
"parse_scanner_usage",
|
|
36
|
+
"parse_status",
|
|
37
|
+
"parse_wifi",
|
|
38
|
+
]
|