Skip to content

Commit 95b38da

Browse files
author
fwmone
committed
### Changed
- take care of device time drift of up to 30 minutes, e. g. device is set to wake up at 6am but wakes at 5:47 (seems to be a firmware bug) - do not set the time to 6am again but to the next time slot (e. g. 18:00 when wake_up_hours is "6,18"). - translated all comments to English
1 parent 1118a94 commit 95b38da

5 files changed

Lines changed: 50 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ 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.10] - 2026-01-30
9+
10+
### Changed
11+
12+
- take care of device time drift of up to 30 minutes, e. g. device is set to wake up at 6am but wakes at 5:47 (seems to be a firmware bug) - do not set the time to 6am again but to the next time slot (e. g. 18:00 when wake_up_hours is "6,18").
13+
- translated all comments to English
14+
815
## [0.1.9] - 2026-01-29
916

1017
### Added - BREAKING CHANGE

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,9 @@ bloomin8_pull:
8484
|*image_dir*|This is where all the images on the Home Assistant server are stored, from which the pull endpoint selects one for the picture frame.|
8585
|*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.|
8686
|*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://<IP-AND-PORT-OF-HOME-ASSISTANT>/local/bloomin8|
87-
|*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.|
87+
|*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).|
8888
|*orientation*|The orientation of the picture frame - P = portrait format, L = landscape format.|
8989
90-
9190
And for the access token in <secrets.yaml>:
9291
9392
```yaml

custom_components/bloomin8_pull/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
7575
except Exception:
7676
pass
7777

78-
hass.data[DOMAIN]["entities"] = [] # hier registrieren sich die Entities, damit wir pushen können
78+
hass.data[DOMAIN]["entities"] = [] # This is where entities register so that we can push
7979

8080
# register the HTTP endpoint
8181
hass.http.register_view(Bloomin8PullView(hass, cfg))
8282
hass.http.register_view(Bloomin8SignalView(hass, cfg))
8383

84-
# Plattformen laden
84+
# Load platforms
8585
for platform in PLATFORMS:
8686
hass.async_create_task(async_load_platform(hass, platform, DOMAIN, {}, config))
8787
return True

custom_components/bloomin8_pull/sensor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,6 @@ def extra_state_attributes(self):
3232
}
3333

3434
async def async_added_to_hass(self):
35-
# registrieren, damit der View später pushen kann
35+
# register so that the view can push later
3636
self.hass.data[DOMAIN]["entities"].append(self)
3737
self.async_write_ha_state()

custom_components/bloomin8_pull/view.py

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from .const import DOMAIN, STATE_BATTERY, STATE_SUCCESS, STATE_LAST_SEEN, STATE_FILE
2222

2323
def calc_recent_max(n_files: int) -> int:
24-
# 50% der Dateien, mindestens 5, maximal 50
24+
# 50% of files, minimum 5, maximum 50
2525
return max(5, min(50, int(round(n_files * 0.5))))
2626

2727
def clear_publish_dir(path: str) -> None:
@@ -82,6 +82,30 @@ def next_wake_time_local(hours: list[int], now_local=None):
8282
first = dt_util.as_local(datetime(tomorrow.year, tomorrow.month, tomorrow.day, hours[0], 0, 0))
8383
return first
8484

85+
def next_wake_time_local_windowed(
86+
hours: list[int],
87+
now_local: datetime | None = None,
88+
drift_window: timedelta = timedelta(minutes=30),
89+
) -> datetime:
90+
"""Return next scheduled local time.
91+
If we're within `drift_window` BEFORE the next slot, skip that slot and return the following one.
92+
"""
93+
if not hours:
94+
raise ValueError("No wake_up_hours configured")
95+
96+
if now_local is None:
97+
now_local = dt_util.now()
98+
99+
first = next_wake_time_local(hours, now_local=now_local)
100+
101+
# If the device calls shortly before the next slot (drift), don't schedule that same slot again.
102+
delta = first - now_local
103+
if timedelta(0) < delta <= drift_window:
104+
# Next slot after `first`
105+
return next_wake_time_local(hours, now_local=first + timedelta(seconds=1))
106+
107+
return first
108+
85109
async def choose_varied(hass, files: list[str], store_key: str = "recent_images") -> str:
86110
RECENT_MAX = calc_recent_max(len(files))
87111

@@ -90,18 +114,18 @@ async def choose_varied(hass, files: list[str], store_key: str = "recent_images"
90114
data = await store.async_load() or {}
91115
recent = deque(data.get("recent", []), maxlen=RECENT_MAX)
92116

93-
# Entfernte Dateien aus recent rauswerfen (dynamische Bildauswahl)
117+
# Remove files from recent (dynamic image selection)
94118
files_set = set(files)
95119
recent = deque([f for f in recent if f in files_set], maxlen=RECENT_MAX)
96120

97-
# Kandidaten: alles, was nicht "recent" ist
121+
# Candidates: anything that is not “recent”
98122
recent_set = set(recent)
99123
candidates = [f for f in files if f not in recent_set]
100124

101-
# Fallback: wenn alles in recent ist, nimm wieder aus allen
125+
# Fallback: if everything is in recent, take from all again
102126
chosen = random.choice(candidates or files)
103127

104-
# recent updaten
128+
# update recent
105129
recent.append(chosen)
106130
await store.async_save({"recent": list(recent)})
107131

@@ -153,12 +177,12 @@ async def get(self, request: web.Request) -> web.Response:
153177
STATE_LAST_SEEN: self.hass.data[DOMAIN]["state"][STATE_LAST_SEEN],
154178
}
155179

156-
# nicht blocking im event loop
180+
# don't block the event loop
157181
await self.hass.async_add_executor_job(_write_json_sync, STATE_FILE, data)
158182
except Exception:
159183
pass
160184

161-
# Push: alle registrierten Entities neu schreiben
185+
# Push: rewrite all registered entities
162186
for ent in self.hass.data[DOMAIN].get("entities", []):
163187
ent.async_write_ha_state()
164188

@@ -172,7 +196,12 @@ async def get(self, request: web.Request) -> web.Response:
172196
publish_webpath: str = self.cfg["publish_webpath"]
173197

174198
hours = parse_wake_up_hours(self.cfg["wake_up_hours"])
175-
next_local = next_wake_time_local(hours)
199+
now_local = dt_util.now()
200+
next_local = next_wake_time_local_windowed(
201+
hours,
202+
now_local=now_local,
203+
drift_window=timedelta(minutes=30),
204+
)
176205
next_utc = dt_util.as_utc(next_local)
177206

178207
orientation: str = self.cfg["orientation"]
@@ -278,12 +307,12 @@ async def get(self, request: web.Request) -> web.Response:
278307
STATE_LAST_SEEN: self.hass.data[DOMAIN]["state"][STATE_LAST_SEEN],
279308
}
280309

281-
# nicht blocking im event loop
310+
# don't block the event loop
282311
await self.hass.async_add_executor_job(_write_json_sync, STATE_FILE, data)
283312
except Exception:
284313
pass
285314

286-
# Push: alle registrierten Entities neu schreiben
315+
# Push: rewrite all registered entities
287316
for ent in self.hass.data[DOMAIN].get("entities", []):
288317
ent.async_write_ha_state()
289318

0 commit comments

Comments
 (0)