A Home Assistant custom integration that triggers document scans on any eSCL/AirScan-capable network scanner and surfaces per-job state through a sensor โ including live page progress for ADF batches, completion, cancellation, and the scanner's own error reasons.
Ships with a companion Lovelace card so a "Scan now" tile on your dashboard is a single tap.
๐ก Sister project: for printing PDFs to the same multifunction printers, see ha-ipp-print โ same architecture (per-job sensor + Lovelace card + bus events) targeting IPP/IPPS instead of eSCL.
Home Assistant's built-in printer integrations (brother, ipp, the various
HACS HP/Epson components) are all read-only โ they poll device status
sensors but cannot trigger scans. The only existing HACS scan-trigger
integration is Brother-DCP-1610W-specific and produces a single JPEG via
WSD SOAP. Nothing in the ecosystem targets eSCL, even though eSCL is the
vendor-neutral standard that nearly every modern multifunction device
supports (HP, Canon, Epson, modern Brother, Kyocera, Xerox, โฆ).
escl_scan talks eSCL directly:
ScannerStatusto probe and detect ADF vs Platen sourceScanJobs(POST) to create a job with aScanSettingsenvelopeScanJobs/{uuid}/NextDocument(GET) to pull each page as the scanner produces itScanJobs/{uuid}(DELETE) to cancel
Job state flows into sensor.printer_current_scan (state + filename
- pages_done + source + timestamps), and
escl_scan_state_changed/escl_scan_completedevents fire on the bus so you can wire up automations (mobile notifications with the PDF attached, auto-upload to Paperless, etc.).
- ๐ Direct eSCL submission โ no SANE, no CUPS, no driver layer
- ๐ Auto-discovered via mDNS (
_uscan/_uscans) โ shows up under Discovered - ๐ Multi-page ADF batches merged into one PDF (per-page scanners included)
- ๐ Full-bed scan region and duplex from the device's own
ScannerCapabilities - ๐ Per-scan sensor (
sensor.printer_current_scan) with live page progress - ๐
button.<scanner>_scan_nowentity +escl_scan.start/escl_scan.cancelservices for automations - ๐ Bus events for state changes and completion
- ๐ Cancel-Job support
- ๐จ Lovelace card with one-tap scan and status display
- ๐ Bearer-token authenticated file download endpoint
- โ๏ธ Config flow โ no YAML required
- ๐ Works with the legacy ciphers some HP LaserJets ship with (opt-in)
- Home Assistant 2024.12 or newer (2026.3+ for the integration icon)
- A network scanner that supports eSCL / AirScan (most modern MFPs do)
- The scanner reachable from your HA host (typically port 443 or 80)
| Device | Platen | ADF | Duplex | Notes |
|---|---|---|---|---|
| HP Color LaserJet MFP M283fdw | โ | โ | n/a (simplex ADF) | Discovered via mDNS; capabilities, DPI snap, cancel, copy-to-folder verified live on HA 2026.9.1 |
Works with yours? Open a device report and it gets added here.
HACS โ search eSCL Scan โ Download โ restart Home Assistant. No custom repository needed.
- Copy
custom_components/escl_scan/into your<config>/custom_components/directory - Restart Home Assistant
Most eSCL scanners advertise themselves on the LAN, so the scanner usually appears under Settings โ Devices & Services โ Discovered; press Configure, confirm, done. Credentials and TLS settings can be adjusted afterwards in the integration's options.
For manual setup: Settings โ Devices & Services โ Add Integration โ eSCL Scan and fill in:
| Field | Notes |
|---|---|
| Hostname or IP | e.g. scanner.local or 192.168.1.50 |
| Port | 443 for HTTPS (default), 80 for HTTP |
| Use TLS | On for HTTPS |
| User | Only required if scanner uses basic auth |
| Password | Only required if scanner uses basic auth |
| Verify TLS | Off for self-signed certs (most consumer scanners) |
| Allow legacy cipher suites | Enable if you see SSLV3_ALERT_HANDSHAKE_FAILURE in the logs |
The flow does a quick eSCL ScannerStatus probe before saving โ any 200
response confirms the network/auth path works. It then reads
ScannerCapabilities (best-effort) for the model name, serial number, bed
size per source, duplex support, and the supported resolutions:
- the scan region is the full bed of the chosen source (A4, Letter, Legal ADF โ whatever the device reports), so nothing gets cropped;
- a requested DPI the device doesn't offer snaps to the nearest supported one;
- duplex is only sent for Feeder scans on a duplex-capable ADF.
Options (gear icon on the integration) hold the defaults: DPI, color mode, duplex, and file retention.
The integration registers the card globally โ no resources: block needed.
type: custom:escl-scan-card
title: Scan now # optional, defaults to "Scan now"
entity: sensor.printer_current_scan # optional; auto-detected if renamedFor automations, use the services instead of the REST API (no token needed):
# Scan the ADF in grayscale, both sides, and grab the resulting scan_id
action: escl_scan.start
data:
source: Feeder # optional: Platen | Feeder (auto-detected if omitted)
dpi: 300 # optional, snaps to a supported resolution
color: gray # optional: color | gray
duplex: true # optional, Feeder + duplex ADF only
response_variable: scan
# Cancel the current scan (or pass scan_id: ...)
action: escl_scan.cancelescl_scan.start returns the same dict the sensor exposes as attributes
(scan_id, source, dpi, state, โฆ). A button.<scanner>_scan_now
entity is created too, so a stock Tile or Button card works without the
custom card.
triggers:
- trigger: event
event_type: escl_scan_completed
conditions:
- condition: template
value_template: "{{ trigger.event.data.state == 'completed' }}"
actions:
- action: notify.mobile_app_phone
data:
message: "Scan ready: {{ trigger.event.data.pages_done }} page(s)"
data:
url: "{{ trigger.event.data.file_url }}"sensor.printer_current_scan
| Field | Value |
|---|---|
| state | idle / pending / processing / processing-stopped / canceled / aborted / completed / failed |
| attributes.scan_id | Internal scan id (matches the file endpoint) |
| attributes.filename | Auto-generated filename (e.g. scan-20260524-153012-adf.pdf) |
| attributes.pages_done | Pages pulled from the scanner so far (final PDF page count on completion) |
| attributes.source | Platen or Feeder |
| attributes.duplex | true when both sides were requested (Feeder + duplex ADF only) |
| attributes.state_reasons | The scanner's eSCL JobStateReasons |
| attributes.submitted_at / finished_at | ISO timestamps |
| attributes.file_url | Download URL once complete (/api/escl_scan/file/{id}) |
| attributes.file_path | Absolute path of the stored PDF once complete (for shell_command, Paperless uploads, โฆ) |
| attributes.copied_to | Path of the copy made by copy to folder, if enabled |
Bus events you can trigger automations from:
escl_scan_state_changedโ every observed state changeescl_scan_completedโ once per terminal transition (completed / canceled / aborted)
Both carry the full scan dict as event.data.
The integration registers three HA HTTP views (all requires_auth = true):
Optional JSON body to override defaults: {"dpi": 600, "color": "gray", "source": "Feeder", "duplex": true}. Returns:
{"ok": true, "scan_id": "abc123", "source": "Feeder", "dpi": 600, "color": "gray", "duplex": true, "state": "pending"}Returns 409 if a scan is already running โ the scanner is single-job
hardware, so starts are serialized.
JSON body {"scan_id": "abc123"}. Returns {"ok": true} on success.
Streams the PDF. Returns 404 if the scan isn't complete yet or has been TTL-purged.
Options โ Also copy finished scans to this folder. Every completed scan is
copied there (written under a temp name and renamed, so folder watchers never
see a half-written file). Point it at a Paperless-ngx consume directory and
scans become documents with no automation at all. The folder must be listed
in configuration.yaml:
homeassistant:
allowlist_external_dirs:
- /media/paperless/consumeCopies are never purged by the retention TTL.
- Download diagnostics (device page โ โฎ โ Download diagnostics) and
attach it to bug reports: it contains the parsed
ScannerCapabilities, the current/tracked scans, and redacted entry data. - Debug logging:
logger: logs: custom_components.escl_scan: debug
SSLV3_ALERT_HANDSHAKE_FAILUREโ enable Allow legacy cipher suites.- Scan fails with "truncated PDF" โ the DPI/colour combination isn't supported; with capabilities available the DPI snaps automatically, so try a different colour mode.
- "scanner busy" โ another client (phone, PC) has an active job; wait or cancel it on the device.
- Card shows "Configuration error" briefly on a slow reload โ expected, the card self-heals within a few seconds.
- No OCR. Scans land as image-mode PDFs. Pair with Paperless-ngx or an OCR-capable bus-event listener for searchable text.
- 1h TTL on stored PDFs by default (configurable in options; a periodic sweep purges expired files even when no new scan runs).
- One scanner, one scan at a time. The integration allows a single config
entry, and a start request while a scan is in progress is rejected (
409) rather than clobbering the running job. - HP LaserJets: some models only offer non-PFS TLS ciphers. Enable "Allow legacy cipher suites" in the config flow.
custom_components/escl_scan/
โโโ __init__.py # entry setup, HTTP views, card registration
โโโ button.py # button.<scanner>_scan_now
โโโ config_flow.py # user + zeroconf flows, options flow
โโโ coordinator.py # scan lifecycle driver, copy-to-folder, file retention
โโโ const.py
โโโ diagnostics.py
โโโ manifest.json
โโโ scanner.py # eSCL wire format + client (capabilities, jobs, streaming)
โโโ sensor.py # sensor.printer_current_scan
โโโ services.py # escl_scan.start / escl_scan.cancel
โโโ services.yaml
โโโ static/card.js # the Lovelace card + its visual editor
โโโ brand/ # integration icon (HA 2026.3+ inline brands)
โโโ strings.json, icons.json
โโโ translations/en.json
python3 -m venv .venv && .venv/bin/pip install -r requirements-test.txt
.venv/bin/pytest -q # parsers, scanner client, coordinator, flows, services, views
.venv/bin/ruff check custom_components tests
npm ci && npm run test:card # jsdom tests for the card
Releases: bump version in manifest.json, add a CHANGELOG section, push a
vX.Y.Z tag โ the release workflow publishes the GitHub release from the
CHANGELOG entry (HACS only installs releases).
Pull requests welcome.
MIT



