Skip to content

Commit f5d8da1

Browse files
author
fwmone
committed
### Added
- Added battery level and last updated sensors
1 parent a8576d9 commit f5d8da1

9 files changed

Lines changed: 313 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/).
77

8+
## [0.1.1] - 2026-02-07
9+
10+
### Added
11+
12+
- Added battery level and last updated sensors
13+
814
## [0.1.0] - 2026-02-03
915

1016
### Added

README.md

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@
1313
- [Sensor](#sensor)
1414
- [State:](#state)
1515
- [Attributes:](#attributes)
16-
- [Services](#services)
17-
- [Automation Examples](#automation-examples)
16+
- [Services](#services)
17+
- [Automation Examples](#automation-examples)
18+
- [Device Telemetry](#device-telemetry)
19+
- [Telemetry sensors](#telemetry-sensors)
20+
- [Manual refresh (optional)](#manual-refresh-optional)
21+
- [Battery percentage calculation](#battery-percentage-calculation)
1822
- [API / Upload Method](#api--upload-method)
1923
- [Troubleshooting](#troubleshooting)
2024
- [Upload succeeds but frame shows old image](#upload-succeeds-but-frame-shows-old-image)
@@ -26,21 +30,24 @@
2630

2731
# Features
2832

29-
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.
33+
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.
3034

31-
- Upload a random image from an input folder to a Paperlesspaper frame via API
35+
- Upload a random image from an input folder to a paperlesspaper frame via API
3236
- "Varied random" selection:
3337
- remembers recently used images
3438
- recent-window size = **50% of available images** (min 5, max 50)
3539
- avoids repetition until the pool is exhausted
3640
- Optional publish/copy of the selected image into `/config/www/...` for preview/debugging
3741
- Optional cleanup of the publish directory before publishing
3842
- Robust upload retries with exponential backoff
39-
- Home Assistant sensor showing:
43+
- Gets device information like battery level and last update
44+
- Home Assistant sensors showing:
4045
- last upload timestamp
4146
- current file name
4247
- last result (success/failed/dry_run)
4348
- last HTTP status / error
49+
- battery level / percentage
50+
- last update
4451

4552
# 📦 Installation
4653

@@ -60,14 +67,17 @@ A small Home Assistant custom integration to **upload images from the Home Assis
6067

6168
# Configuration
6269

70+
Please follow the steps for [generating an API key](https://paperlesspaper.de/posts/api).
71+
6372
This integration currently uses YAML configuration.
6473

6574
Add this to your `configuration.yaml`:
6675

6776
```yaml
6877
paperlesspaper_push:
6978
api_key: !secret paperlesspaper_api_key
70-
paper_id: !secret paperlesspaper_paper_id
79+
paper_id: "YOUR_PAPER_ID"
80+
device_id: "YOUR_DEVICE_ID"
7181

7282
# optional:
7383
base_url: https://api.memo.wirewire.de/v1
@@ -82,7 +92,6 @@ Add the secrets to secrets.yaml:
8292
8393
```yaml
8494
paperlesspaper_api_key: "YOUR_API_KEY"
85-
paperlesspaper_paper_id: "YOUR_PAPER_ID"
8695
```
8796
8897
Restart Home Assistant after changing YAML.
@@ -119,7 +128,7 @@ This is useful for debugging or previewing the selected image from Home Assistan
119128
- last_error
120129
- published_name
121130

122-
## Services
131+
# Services
123132
```paperlesspaper_push.upload_random```
124133

125134
Uploads a (varied) random image.
@@ -148,7 +157,7 @@ Example:
148157
service: paperlesspaper_push.reset_recent
149158
```
150159

151-
## Automation Examples
160+
# Automation Examples
152161

153162
Upload twice per day:
154163

@@ -170,6 +179,44 @@ action:
170179
- service: paperlesspaper_push.upload_random
171180
```
172181

182+
# Device Telemetry
183+
184+
In addition to upload/push functionality, the integration can poll the paperlesspaper API for **device telemetry** (battery, timestamps, sync state).
185+
186+
The integration will periodically call ```GET /v1/devices/<device_id>``` and update the related sensors.
187+
188+
## Telemetry sensors
189+
190+
When device_id is configured, the following sensors are created:
191+
192+
- Battery
193+
- sensor.paperlesspaper_push_battery_voltage (V)
194+
- sensor.paperlesspaper_push_battery (%)
195+
196+
- Timestamps
197+
- sensor.paperlesspaper_push_last_reachable
198+
- sensor.paperlesspaper_push_next_device_sync
199+
- sensor.paperlesspaper_push_updated_at
200+
- sensor.paperlesspaper_push_loaded_at
201+
202+
Note: The API exposes batLevel as a raw value (typically millivolts for 4×AAA in series).
203+
The integration converts it to a percentage using a pragmatic min/max voltage model.
204+
205+
## Manual refresh (optional)
206+
207+
You can trigger an immediate telemetry refresh via the service:
208+
```paperlesspaper_push.refresh_device```
209+
210+
## Battery percentage calculation
211+
212+
Battery percentage is derived from the reported raw battery voltage (`batLevel`).
213+
214+
Default mapping (4×AAA in series):
215+
- 6.4 V → 100%
216+
- 4.8 V → 0%
217+
218+
Values are clamped to the range and mapped linearly in between.
219+
173220
# API / Upload Method
174221

175222
The upload is performed using a multipart form-data request similar to:

custom_components/paperlesspaper_push/__init__.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from homeassistant.helpers.storage import Store
66
from homeassistant.helpers.typing import ConfigType
77

8+
from .coordinator import PaperlesspaperDeviceCoordinator
9+
810
from .const import (
911
DOMAIN,
1012
CONF_API_KEY,
@@ -15,6 +17,9 @@
1517
CONF_TIMEOUT,
1618
CONF_MAX_ATTEMPTS,
1719
CONF_PUBLISH,
20+
CONF_DEVICE_ID,
21+
CONF_SCAN_INTERVAL,
22+
DEFAULT_SCAN_INTERVAL,
1823
DEFAULT_BASE_URL,
1924
DEFAULT_INPUT_DIR,
2025
DEFAULT_PUBLISH_DIR,
@@ -29,6 +34,7 @@
2934
SERVICE_FIELD_FORCE_FILE,
3035
SERVICE_FIELD_DRY_RUN,
3136
SERVICE_FIELD_PUBLISH,
37+
SERVICE_REFRESH_DEVICE,
3238
ATTR_CURRENT_FILENAME,
3339
ATTR_LAST_RESULT,
3440
ATTR_LAST_HTTP_STATUS,
@@ -79,6 +85,23 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
7985
CONF_PUBLISH: bool(cfg.get(CONF_PUBLISH, DEFAULT_PUBLISH)),
8086
}
8187

88+
device_id = cfg.get(CONF_DEVICE_ID)
89+
scan_interval = int(cfg.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL))
90+
91+
if device_id:
92+
coordinator = PaperlesspaperDeviceCoordinator(
93+
hass=hass,
94+
api_key=api_key,
95+
base_url=hass.data[DOMAIN]["config"][CONF_BASE_URL],
96+
device_id=device_id,
97+
scan_interval_s=scan_interval,
98+
)
99+
hass.data[DOMAIN]["device_coordinator"] = coordinator
100+
hass.data[DOMAIN]["device_unique_prefix"] = f"{DOMAIN}_{device_id}"
101+
102+
# Initial fetch so sensors have values right away (YAML setup, no config entry)
103+
await coordinator.async_refresh()
104+
82105
# Stores
83106
hass.data[DOMAIN]["store_state"] = Store(hass, STORE_VERSION, STORE_KEY_STATE)
84107
hass.data[DOMAIN]["store_recent"] = Store(hass, STORE_VERSION, STORE_KEY_RECENT)
@@ -203,7 +226,14 @@ async def handle_reset_recent(call):
203226
if dispatcher:
204227
dispatcher()
205228

229+
async def handle_refresh_device(call):
230+
coordinator = hass.data.get(DOMAIN, {}).get("device_coordinator")
231+
if coordinator:
232+
await coordinator.async_request_refresh()
233+
206234
hass.services.async_register(DOMAIN, SERVICE_UPLOAD_RANDOM, handle_upload_random)
207235
hass.services.async_register(DOMAIN, SERVICE_RESET_RECENT, handle_reset_recent)
236+
hass.services.async_register(DOMAIN, SERVICE_REFRESH_DEVICE, handle_refresh_device)
208237

209238
return True
239+

custom_components/paperlesspaper_push/const.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@
88
CONF_TIMEOUT = "timeout"
99
CONF_MAX_ATTEMPTS = "max_attempts"
1010
CONF_PUBLISH = "publish"
11+
CONF_DEVICE_ID = "device_id"
12+
CONF_SCAN_INTERVAL = "scan_interval"
1113

1214
DEFAULT_BASE_URL = "https://api.memo.wirewire.de/v1"
1315
DEFAULT_INPUT_DIR = "/media/picture-frames/paperlesspaper"
1416
DEFAULT_PUBLISH_DIR = "/config/www/picture-frames/paperlesspaper"
1517
DEFAULT_TIMEOUT = 30
1618
DEFAULT_MAX_ATTEMPTS = 4
1719
DEFAULT_PUBLISH = True
20+
DEFAULT_SCAN_INTERVAL = 900 # 15 min
1821

1922
STORE_VERSION = 1
2023
STORE_KEY_STATE = f"{DOMAIN}_state"
@@ -33,3 +36,4 @@
3336
SERVICE_FIELD_FORCE_FILE = "force_file"
3437
SERVICE_FIELD_DRY_RUN = "dry_run"
3538
SERVICE_FIELD_PUBLISH = "publish"
39+
SERVICE_REFRESH_DEVICE = "refresh_device"
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import logging
2+
from datetime import timedelta
3+
4+
from homeassistant.core import HomeAssistant
5+
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
6+
from homeassistant.helpers.aiohttp_client import async_get_clientsession
7+
8+
from .const import DOMAIN
9+
10+
_LOGGER = logging.getLogger(__name__)
11+
12+
13+
class PaperlesspaperDeviceCoordinator(DataUpdateCoordinator[dict]):
14+
def __init__(self, hass: HomeAssistant, api_key: str, base_url: str, device_id: str, scan_interval_s: int):
15+
self.hass = hass
16+
self._api_key = api_key
17+
self._base_url = base_url.rstrip("/")
18+
self._device_id = device_id
19+
20+
super().__init__(
21+
hass,
22+
_LOGGER,
23+
name=f"{DOMAIN}_device",
24+
update_interval=timedelta(seconds=scan_interval_s),
25+
)
26+
27+
async def _async_update_data(self) -> dict:
28+
session = async_get_clientsession(self.hass)
29+
url = f"{self._base_url}/devices/{self._device_id}"
30+
headers = {"x-api-key": self._api_key}
31+
32+
try:
33+
async with session.get(url, headers=headers, timeout=30) as resp:
34+
text = await resp.text()
35+
if resp.status != 200:
36+
raise UpdateFailed(f"HTTP {resp.status}: {text[:3000]}")
37+
return await resp.json()
38+
except Exception as e:
39+
raise UpdateFailed(str(e)) from e

0 commit comments

Comments
 (0)