feat: deep sleep single-fetch with probabilistic weighting
Refactor timerWakeCycle() to fetch a single random photo per wake using cycle-mode-aware server-side filtering via Immich's POST /api/search/random endpoint. Changes: - Add fetchOneRandomAssetId() to ImmichClient with support for isFavorite, takenAfter, takenBefore filter parameters - Add timer_last_date NVS field for chronological cursor persistence - Implement probabilistic filter selection per cycle mode: - Random: unfiltered size=1 - Chronological/ReverseChronological: date cursor in NVS - FavoritesWeighted: 66% favorites, 33% unfiltered - WeightedChronological: 66% recent (30d), 33% unfiltered - WeightedReverseChronological: 66% older (30d), 33% unfiltered - Add retry loop with filter relaxation on empty results - Initialize hardware RTC (RX8130CE) firmware-wide: - Seed system clock from RTC at boot (immediate time) - NTP sync after WiFi connect, write back to RTC - Add time_utils.h with isoNow(), isoNowMinus30d(), hasValidTime() Normal mode (PhotoQueue + displayTask) is completely untouched. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -211,6 +211,68 @@ std::vector<String> ImmichClient::fetchFavoriteAssetIds() {
|
||||
return ids;
|
||||
}
|
||||
|
||||
String ImmichClient::fetchOneRandomAssetId(const RandomFetchFilter& filter) {
|
||||
String url = buildUrl("/api/search/random");
|
||||
|
||||
JsonDocument reqDoc;
|
||||
reqDoc["size"] = 1;
|
||||
reqDoc["type"] = "IMAGE";
|
||||
|
||||
if (!filter.albumIds.empty()) {
|
||||
JsonArray arr = reqDoc["albumIds"].to<JsonArray>();
|
||||
for (auto& id : filter.albumIds) arr.add(id);
|
||||
}
|
||||
if (filter.favoriteOnly) {
|
||||
reqDoc["isFavorite"] = true;
|
||||
}
|
||||
if (filter.takenAfter.length() > 0) {
|
||||
reqDoc["takenAfter"] = filter.takenAfter;
|
||||
}
|
||||
if (filter.takenBefore.length() > 0) {
|
||||
reqDoc["takenBefore"] = filter.takenBefore;
|
||||
}
|
||||
|
||||
String body;
|
||||
serializeJson(reqDoc, body);
|
||||
|
||||
Serial.printf("[immich] fetchOne: %s\n", body.c_str());
|
||||
|
||||
String response = httpPost(url, body);
|
||||
if (response.isEmpty()) {
|
||||
Serial.println("[immich] fetchOne: empty response");
|
||||
return "";
|
||||
}
|
||||
|
||||
JsonDocument doc;
|
||||
DeserializationError err = deserializeJson(doc, response);
|
||||
if (err) {
|
||||
Serial.printf("[immich] fetchOne JSON error: %s\n", err.c_str());
|
||||
return "";
|
||||
}
|
||||
|
||||
JsonArray arr = doc.as<JsonArray>();
|
||||
for (JsonObject asset : arr) {
|
||||
String id = asset["id"].as<String>();
|
||||
if (id.length() == 0) continue;
|
||||
|
||||
// Skip RAW files
|
||||
String filename = asset["originalFileName"] | "";
|
||||
filename.toLowerCase();
|
||||
if (filename.endsWith(".dng") || filename.endsWith(".raw") ||
|
||||
filename.endsWith(".cr2") || filename.endsWith(".cr3") ||
|
||||
filename.endsWith(".nef") || filename.endsWith(".arw") ||
|
||||
filename.endsWith(".orf") || filename.endsWith(".rw2") ||
|
||||
filename.endsWith(".raf") || filename.endsWith(".srw")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
Serial.println("[immich] fetchOne: no usable asset returned");
|
||||
return "";
|
||||
}
|
||||
|
||||
AssetInfo ImmichClient::fetchAssetInfo(const String& assetId) {
|
||||
AssetInfo info;
|
||||
info.id = assetId;
|
||||
|
||||
Reference in New Issue
Block a user