- π Table of content
- β¨ Features
- π§© Requirements
- π¦ Installation
- βοΈ Configuration
- Pull Control / Vacation Mode
- π« Limitations
- π Entities
- π§ Application examples
- π οΈ Development & status
- π Report a bug
- π Note
bloomin8_pull is a custom integration for Home Assistant that allows content from a BLOOMIN8 e-ink picture frame to be retrieved from the Home Assistant server using a pull mechanism. It implements the Schedule Pull API that BLOOMIN8 has provided as a code example. The official BLOOMIN8 integration is independent of this. It is not required for this integration, but can of course be installed independently.
The integration is aimed in particular at users who do not just want to use BLOOMIN8 as a passive picture frame, but want to control content, status, or image changes automatically and context-dependently. It is still in the very early stages of its development cycle and was created primarily out of my personal desire to be able to display images locally on the frame.
- Pull-based retrieval of content/status information
- Provision of sensors for further processing in automations
- Local communication (no cloud requirement)
- 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.
- The BLOOMIN8 picture frame must be able to access the Home Assistant server.
- The images to be retrieved must be available on the Home Assistant server, optimized for the frame, which means: in the correct resolution (1600x1200px for the 13.3" frame), in JPEG format, and already adjusted for the Spectra 6 display, e.g., brightened or increased in saturation. 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 here.
- The images must be in <image_dir> (see configuration below) as JPEGs and end with the suffix ".jpg".
- Open HACS β Integrations
- Click on "Custom Repositories"
- Add this repository: https://github.com/fwmone/bloomin8_pull, Category: Integration
- Install BLOOMIN8 Pull
- Restart Home Assistant
- Download this repository
- Copy the custom_components/bloomin8_pull folder to: /custom_components/bloomin8_pull (this is usually /config/custom_components)
- Restart Home Assistant
The integration is currently configured via YAML. I added a section to <configuration.yaml> using Home Assistant's file editor:
bloomin8_pull:
access_token: !secret bloomin8_pull_token
image_dir: /media/bloomin8
publish_dir: /config/www/bloomin8
publish_webpath: /local/bloomin8
wake_up_hours: "6,18" # 6:00 and 18:00
orientation: P # P = portrait format, L = landscape format| key | explanation |
|---|---|
| access_token | A token specified by you that the picture frame uses for identification. This should usually be "!secret bloomin8_pull_token." In secrets.yaml (see below), you then store the actual token and transfer this configured token to the picture frame via "token" (see also below). |
| image_dir | This is where all the frame-optimized (1600x1200px for 13.3", optimized colors - get optimization script here) images are stored on the Home Assistant server. From these the pull endpoint selects one for the picture frame. |
| publish_dir | The directory that can be accessed via a web browser. This is usually /config/www, which contains a directory for the picture frame, e.g. bloomin8. The pull endpoint copies the selected image here and replaces it with a new one the next time it is retrieved by first deleting all images inside the folder and then copying it there. |
| publish_webpath | The web path to "publish_dir". For "/config/www/bloomin8," this is usually "/local/bloomin8." Only the path is configured here, not the server address. You specify the server address via the configuration of upstream_url below the image frame. For the image frame, the complete URL would therefore be http:///local/bloomin8 |
| wake_up_hours | At which time should the picture frame retrieve a new image? Specify in comma-separated hours, e.g., "6,18" for 6:00 and 18:00. The component takes care of the device's firmware bug(?) of waking up too early (e. g. 5:47 instead of 6:00) for up to 30 minutes and then skips to the next time slot (-> do not send 6:00 again, but 18:00). |
| orientation | The orientation of the picture frame - P = portrait format, L = landscape format. |
And for the access token in <secrets.yaml>:
bloomin8_pull_token: "<YOUR_TOKEN_HERE>"The configuration contains the crucial services under "1. Device Endpoint: /upstream/pull_settings". For configuration, the picture frame must be accessible via Wi-Fi, so it must be woken up via the BLOOMIN8 mobile app before the services can be accessed. Once configured, it automatically wakes up at the time set in <next_cron_time> and then connects to the Home Assistant server.
Example:
PUT http://{device_ip}/upstream/pull_settings
Content-Type: application/json
{
"upstream_on": true,
"upstream_url": "http://<IP-AND-PORT-OF-HOME-ASSISTANT>",
"token": "<YOUR_TOKEN_HERE>",
"cron_time": "2025-11-01T08:30:00Z"
}For IP-AND-PORT-OF-HOME-ASSISTANT, for example, http://192.168.0.1:8123. The framework then automatically appends the path to the pull service.
You can use the following commands to test:
Test whether the custom component works and provides the pull service:
curl -i -H "X-Access-Token: <YOUR_TOKEN_HERE>" "http://<IP-AND-PORT-OF-HOME-ASSISTANT>/eink_pull?device_id=abc&pull_id=uuid&cron_time=2026-01-04T09:00:00Z&battery=80"Test whether the success call works after the frame has retrieved the image:
curl -i -H "X-Access-Token: <YOUR_TOKEN_HERE>" "http://<IP-AND-PORT-OF-HOME-ASSISTANT>/eink_signal?pull_id=uuid&success=1"This allows you to "configure" your picture frame:
curl -X PUT "http://<IP-OF-BLOOMIN8-FRAME>/upstream/pull_settings" -H "Content-Type: application/json" -d "{\"upstream_on\":true,\"upstream_url\":\"http://<IP-AND-PORT-OF-HOME-ASSISTANT>\",\"token\":\" <YOUR_TOKEN_HERE>\",\"cron_time\":\"2026-01-17T05:00:00Z\"}"<cron_time> must be UTC time.
The BLOOMIN8 Pull Endpoint can be temporarily disabled using a dedicated switch entity. This is useful during vacations or longer absences, where image rotation is not desired.
The integration provides the following switch:
switch.bloomin8_pull_enabled
| State | Behaviour |
|---|---|
on (default) |
Normal pull behaviour, images are rotated as usual |
off |
Pulling is disabled, no new images are selected |
The switch state is persisted and survives Home Assistant restarts.
When pulling is disabled and the BLOOMIN8 device calls the /eink_pull endpoint:
- no new image is selected
- the last successfully displayed image is returned again
- the endpoint does NOT send a HTTP 204 response like required here (see "Case 2: No image available"), because HTTP 204 responses must not contain a body, hence it cannot contain a
next_cron_time. - the device is instructed to retry at a later time via
next_cron_time
This ensures that:
- the currently displayed image remains unchanged
- no images are "skipped" during absence
- unnecessary image changes and energy usage are avoided
The last displayed image URL is persisted and exposed as an attribute on the binary sensor:
binary_sensor.bloomin8_last_pull_success- attribute:
last_image_url
- attribute:
This attribute can be used for:
- diagnostics
- UI linking (e.g. open the currently displayed image)
- internal reuse when pulling is disabled
Currently, only one frame is supported. Actually, you can use with several frames but they have to be in the same orientation.
After successful setup, the integration provides two entities:
- sensor.bloomin8_battery (the frame reports its charge level with each pull)
- binary_sensor.bloomin8_last_pull_success (the frame confirms the retrieval; as an attribute, the sensor returns when it was last retrieved)
The entities can be used directly in dashboards, automations, or scripts.
Shows last provided image, battery value, last frame sync, last push time and next sync time. I use the super handy button cards, that need to be installed beforehand.
type: grid
cards:
- type: heading
icon: mdi:desk
heading: Empore
heading_style: title
- type: markdown
content: >-
<img src="/local/picture-frames/paperlesspaper/{{
state_attr('sensor.paperlesspaper_push_status','published_name') }}"
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;"
cards:
- type: custom:button-card
entity: sensor.paperlesspaper_push_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.paperlesspaper_push_last_reachable
name: Letzter Sync
show_state: true
show_label: true
layout: icon_name_state2nd
state_display: |
[[[
const s = states['sensor.paperlesspaper_push_last_reachable']?.state;
if (!s || ['unknown','unavailable','none','null',''].includes(s)) {
return 'β';
}
const d = new Date(s); // ISO / UTC β lokale Zeit automatisch
if (isNaN(d)) return 'β';
return d.toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit'
});
]]]
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.paperlesspaper_push_status
name: Letzter Push
show_state: true
show_label: true
layout: icon_name_state2nd
state_display: |
[[[
const s = states['sensor.paperlesspaper_push_status']?.state;
if (!s || ['unknown','unavailable','none','null',''].includes(s)) {
return 'β';
}
const d = new Date(s); // ISO / UTC β lokale Zeit automatisch
if (isNaN(d)) return 'β';
return d.toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit'
});
]]]
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.paperlesspaper_push_next_device_sync
name: NΓ€chster Sync
show_state: true
show_label: true
layout: icon_name_state2nd
state_display: |
[[[
const s = states['sensor.paperlesspaper_push_next_device_sync']?.state;
if (!s || ['unknown','unavailable','none','null',''].includes(s)) {
return 'β';
}
const d = new Date(s); // ISO / UTC β lokale Zeit automatisch
if (isNaN(d)) return 'β';
return d.toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit'
});
]]]
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- Automatic image change depending on time of day or weather
- Display of context-related content (e.g., calendar, notes, moods)
- Integration into existing smart home scenarios
This integration is currently under active development. Feedback, bug reports, and pull requests are welcome.
Please use the issue tracker on GitHub:
π https://github.com/fwmone/bloomin8_pull/issues
This integration has no official connection to the manufacturer of BLOOMIN8.
