- 📖 Table of content
- Features
- 🧩 Requirements
- 📦 Installation
- Configuration
- Entities
- Services
- Automation Examples
- Device Telemetry
- Troubleshooting
- Roadmap / Ideas
- Support / Issues
- License
- 🙏 Note
A small Home Assistant custom integration to upload images from the Home Assistant server to a paperlesspaper e-paper frame using the WireWire API. The integration is designed to work well with Home Assistant automations (e.g. upload a new image twice a day), and provides a “varied random” image selection that avoids repeating the same images too often.
- Upload a random image from an input folder to a paperlesspaper frame via API
- "Varied random" selection:
- remembers recently used images
- recent-window size = 50% of available images (min 5, max 250)
- avoids repetition until the pool is exhausted
- Optional publish/copy of the selected image into
/config/www/...for preview/debugging - Optional cleanup of the publish directory before publishing
- Robust upload retries with exponential backoff
- Gets device information like battery level and last update
- Home Assistant sensors showing:
- last upload timestamp
- current file name
- last result (success/failed/dry_run)
- last HTTP status / error
- battery level / percentage
- last update
- Home Assistant 2024.12 or newer. I personally always work on the current version of Home Assistant, so I cannot guarantee compatibility with older versions.
- Your Home Assistant server needs internet access to connect to the paperlesspaper cloud service. paperlesspaper does not provide an offline service (yet).
- The images to be retrieved have to be available on the Home Assistant server, optimized for the frame, which means: in the correct resolution (800x480px for the 7" frame), in PNG format, and already adjusted for the Spectra 6 display. In my setup, I synchronize the images from a local Immich server and then optimize them automatically. I wrote a script for this. Here are the usage instructions. I also published my optimizer using paperlesspaper's EPD Optimizer here, which works quite well.
- The images must be in <input_dir> (see configuration below). Use PNGs for best results, but JPEGs are also possible.
- Open HACS → Integrations
- Click on “Custom Repositories”
- Add this repository: https://github.com/fwmone/paperlesspaper_push, Category: Integration
- Install paperlesspaper Push
- Restart Home Assistant
- Download this repository
- Copy the custom_components/paperlesspaper_push folder to: /custom_components/paperlesspaper_push (this is usually /config)
- Restart Home Assistant
Please follow the steps for generating an API key.
After creating the API key, stay logged in the web application. In the lefthand menu, click "Devices" (Geräte) and then your frame. Your browser should now show a URL like this: https://web.wirewire.de/6963a0955ded783d12f68bd0/devices/692305173fd4bbc4a8741b43/. The string 692305173fd4bbc4a8741b43 (after /devices/) is YOUR_DEVICE_ID. If you have not already done so, click "Current" (Aktuell) in the lefthand menu and upload a new picture. This automatically creates a paper. After doing so, choose "Library" (Bibliothek) and click the first picture. An editor should open and your browser's URL should show a URL like this https://web.wirewire.de/6963a0955ded783d12f68bd0/library/device/692305173fd4bbc4a8741b43/6987a14a51fd56e10cf43d14. The string 6987a14a51fd56e10cf43d14 is YOUR_PAPER_ID.
If you're familiar with cURL and JSON, do this:
- Retrieve your organization id:
curl https://api.paperlesspaper.de/v1/organizations/ -H "x-api-key: YOUR_API_KEY". - Retrieve your device id and paper id:
curl https://api.paperlesspaper.de/v1/devices?organization=YOUR_ORGANIZATION_ID -H "x-api-key: YOUR_API_KEY". The result contains the keys "id" which is YOUR_DEVICE_ID (do not use "deviceId") and "paper" which is YOUR_PAPER_ID.
This integration currently uses YAML configuration.
Add this to your configuration.yaml:
paperlesspaper_push:
api_key: !secret paperlesspaper_api_key
paper_id: "YOUR_PAPER_ID"
device_id: "YOUR_DEVICE_ID"
# optional:
base_url: https://api.paperlesspaper.de/v1
input_dir: /media/picture-frames/paperlesspaper
publish_dir: /config/www/picture-frames/paperlesspaper
timeout: 30
max_attempts: 4
publish: trueAdd the secrets to secrets.yaml:
paperlesspaper_api_key: "YOUR_API_KEY"Restart Home Assistant after changing YAML.
Place frame-optimized (800x480px for 7" as PNG, no pre-dithering or color optimization required - get optimization script here) images in /media/picture-frames/paperlesspaper (or whatever folder you have configured). Update As of ~20. April 2026, paperlesspaper changed their uploadSingleImage API call so that it includes their new, optimized version of EPDOptimize. Therefore, you do not need to pre-dither the images anymore, only resize it and save as PNG. Pre-dithering / pre-optimizing using EPDOptimize leads to wrong colors.
Supported formats:
- .png (use that for best results)
- .jpg / .jpeg
If enabled, the integration copies the chosen image to: /config/www/picture-frames/paperlesspaper.
This is useful for debugging or previewing the selected image from Home Assistant (served under /local/...).
sensor.paperlesspaper_push_status
- never or last upload timestamp (UTC ISO format)
- current_filename
- last_result
- last_http_status
- last_error
- published_name
paperlesspaper_push.upload_random
Uploads a (varied) random image.
Example:
service: paperlesspaper_push.upload_random
data:
dry_run: false
publish: trueFields:
- dry_run (bool, optional): select/publish only, do not upload
- publish (bool, optional): publish/copy the chosen file to publish_dir
- force_file (string, optional): force a specific file name from the input folder
paperlesspaper_push.reset_recent
Clears the internal "recent images" history list.
Example:
service: paperlesspaper_push.reset_recentalias: Paperlesspaper Upload Morning
trigger:
- platform: time
at: "05:45:00"
action:
- service: paperlesspaper_push.upload_random
data:
publish: true
dry_run: falsealias: Paperlesspaper Upload Afternoon
trigger:
- platform: time
at: "16:45:00"
action:
- service: paperlesspaper_push.upload_random
data:
publish: true
dry_run: falseFor whatever reason, wake up time periods are not precise. So are 12h in reality about 11:50h. Because of that, I created a dynamic upload using the paperlesspaper_push_next_device_sync sensor:
alias: "paperlesspaper: Upload"
description: ""
triggers:
- value_template: >
{% set next = states('sensor.paperlesspaper_push_next_device_sync') %} {%
if next not in ['unknown','unavailable','none',''] %}
{{ as_timestamp(next) - 30*60 <= now().timestamp() }}
{% else %}
false
{% endif %}
trigger: template
conditions:
- condition: template
value_template: >
{% set last = states('sensor.paperlesspaper_push_status') %} {% if last
not in ['unknown','unavailable','none',''] %}
{{ now().timestamp() - as_timestamp(last) > 3600 }}
{% else %}
true
{% endif %}
actions:
- action: paperlesspaper_push.upload_random
data:
publish: true
dry_run: falseIn addition to upload/push functionality, the integration can poll the paperlesspaper API for device telemetry (battery, timestamps, sync state).
The integration will periodically call GET /v1/devices/<device_id> and update the related sensors.
When device_id is configured, the following sensors are created:
-
Battery
- sensor.paperlesspaper_push_battery_voltage (V)
- sensor.paperlesspaper_push_battery (%)
- sensor.paperlesspaper_push_battery_rechargeable (%)
-
Timestamps
- sensor.paperlesspaper_push_last_reachable
- sensor.paperlesspaper_push_next_device_sync
- sensor.paperlesspaper_push_updated_at
- sensor.paperlesspaper_push_loaded_at
Note: The API exposes batLevel as a raw value (typically millivolts for 4×AAA in series). The integration converts it to a percentage using a pragmatic min/max voltage model.
You can trigger an immediate telemetry refresh via the service:
paperlesspaper_push.refresh_device
Battery percentage is derived from the reported raw battery voltage (batLevel).
Default mapping (4×AAA in series) for sensor.paperlesspaper_push_battery:
- 6.2 V → 100%
- 4.0 V → 0%
Default mapping (4×AAA NiMH in series) for sensor.paperlesspaper_push_battery_rechargeable:
- 5.3 V → 100%
- 4.0 V → 0%
Values are clamped to the range and mapped linearly in between.
Shows last pulled image, battery value and last pull time. I use the super handy button cards, that need to be installed beforehand.
type: grid
cards:
- type: heading
icon: mdi:coat-rack
heading: Diele
heading_style: title
- type: markdown
content: |-
<img src="{{
state_attr('binary_sensor.bloomin8_last_pull_success','last_image_url')
}}"
height="400">
card_mod:
style: |
ha-card {
text-align: center;
}
- type: custom:layout-card
layout_type: grid
layout:
grid-template-columns: 1fr 1fr
grid-gap: 6px
margin: "-8px 0 0 0;"
card_margin: 0 0 0 0;
cards:
- type: custom:button-card
entity: sensor.bloomin8_battery
name: Batterie
show_state: true
show_label: true
layout: icon_name_state2nd
styles:
icon:
- height: 32px
card:
- border-radius: 28px
- padding: 10px
- height: 110px
grid:
- grid-template-areas: "\"i\" \"n\" \"s\""
- grid-template-columns: 1fr
- grid-template-rows: 1fr min-content min-content
name:
- justify-self: center
- font-weight: bold
- font-size: 0.9em
state:
- justify-self: center
- font-size: 12px
- padding-top: 1px
tap_action:
action: more-info
- type: custom:button-card
entity: sensor.bloomin8_letzter_pull
name: Letzter Pull
show_state: true
show_label: true
layout: icon_name_state2nd
styles:
icon:
- height: 32px
card:
- border-radius: 28px
- padding: 10px
- height: 110px
grid:
- grid-template-areas: "\"i\" \"n\" \"s\""
- grid-template-columns: 1fr
- grid-template-rows: 1fr min-content min-content
name:
- justify-self: center
- font-weight: bold
- font-size: 0.9em
state:
- justify-self: center
- font-size: 12px
- padding-top: 1px
tap_action:
action: more-info
column_span: 2
# API / Upload Method
The upload is performed using a multipart form-data request similar to:
```bash
curl -X POST "https://api.paperlesspaper.de/v1/papers/uploadSingleImage/<PAPER_ID>" \
-H "x-api-key: <API_KEY>" \
-F "picture=@/path/to/image.png;type=image/png"The Paperlesspaper frame pulls images from the cloud in configurable intervals. Make sure the device is configured to wake up and fetch images.
This integration avoids blocking filesystem operations inside the event loop by using Home Assistant's executor helpers.
If you still see blocking call warnings, please open an issue with logs.
- Config Flow (UI-based configuration)
- Additional sensors (e.g. success/failure binary sensor)
- Optional "keep last N published images" instead of cleaning publish_dir fully
- Support multiple Paper IDs
Please open issues on GitHub if you encounter bugs or have feature requests.
This project is licensed under the terms of the MIT License.
This integration has no official connection to the manufacturer of paperlesspaper frames.
