speed up the load of Live View and Battery View in Sodistore Home

This commit is contained in:
Yinyin Liu 2026-03-09 11:51:35 +01:00
parent 2e52b9ee15
commit 401d82ea7a
3 changed files with 139 additions and 27 deletions

View File

@ -5,6 +5,66 @@ import { S3Access } from 'src/dataCache/S3/S3Access';
import { JSONRecordData, parseChunkJson } from '../Log/graph.util'; import { JSONRecordData, parseChunkJson } from '../Log/graph.util';
import JSZip from 'jszip'; import JSZip from 'jszip';
// Find the latest chunk file in S3 using ListObjects (single HTTP request)
// Returns the parsed chunk data or FetchResult.notAvailable
export const fetchLatestDataJson = (
s3Credentials?: I_S3Credentials,
maxAgeSeconds: number = 400
): Promise<FetchResult<Record<string, JSONRecordData>>> => {
if (!s3Credentials || !s3Credentials.s3Bucket) {
return Promise.resolve(FetchResult.notAvailable);
}
const s3Access = new S3Access(
s3Credentials.s3Bucket,
s3Credentials.s3Region,
s3Credentials.s3Provider,
s3Credentials.s3Key,
s3Credentials.s3Secret
);
// Use marker to skip files older than maxAgeSeconds
const oldestTimestamp = Math.floor(Date.now() / 1000) - maxAgeSeconds;
const marker = `${oldestTimestamp}.json`;
return s3Access
.list(marker, 50)
.then(async (r) => {
if (r.status !== 200) {
return Promise.resolve(FetchResult.notAvailable);
}
const xml = await r.text();
const parser = new DOMParser();
const doc = parser.parseFromString(xml, 'application/xml');
const keys = Array.from(doc.getElementsByTagName('Key'))
.map((el) => el.textContent)
.filter((key) => key && /^\d+\.json$/.test(key))
.sort((a, b) => Number(b.replace('.json', '')) - Number(a.replace('.json', '')));
if (keys.length === 0) {
return Promise.resolve(FetchResult.notAvailable);
}
// Fetch the most recent chunk file
const latestKey = keys[0];
const res = await s3Access.get(latestKey);
if (res.status !== 200) {
return Promise.resolve(FetchResult.notAvailable);
}
const jsontext = await res.text();
const byteArray = Uint8Array.from(atob(jsontext), (c) =>
c.charCodeAt(0)
);
const zip = await JSZip.loadAsync(byteArray);
const jsonContent = await zip.file('data.json').async('text');
return parseChunkJson(jsonContent);
})
.catch(() => {
return Promise.resolve(FetchResult.tryLater);
});
};
export const fetchDataJson = ( export const fetchDataJson = (
timestamp: UnixTime, timestamp: UnixTime,
s3Credentials?: I_S3Credentials, s3Credentials?: I_S3Credentials,

View File

@ -111,20 +111,52 @@ function SodioHomeInstallation(props: singleInstallationProps) {
return btoa(String.fromCharCode(...combined)); return btoa(String.fromCharCode(...combined));
} }
const fetchDataPeriodically = async () => { // Probe multiple timestamps in parallel, return first successful result
var timeperiodToSearch = 350; const probeTimestampBatch = async (
let res; offsets: number[]
let timestampToFetch; ): Promise<{ res: any; timestamp: UnixTime } | null> => {
const now = UnixTime.now();
const promises = offsets.map(async (offset) => {
const ts = now.earlier(TimeSpan.fromSeconds(offset));
const result = await fetchDataJson(ts, s3Credentials, false);
if (result !== FetchResult.notAvailable && result !== FetchResult.tryLater) {
return { res: result, timestamp: ts };
}
return null;
});
for (var i = 0; i < timeperiodToSearch; i += 30) { const results = await Promise.all(promises);
// Return the most recent hit (smallest offset = first in array)
return results.find((r) => r !== null) || null;
};
const fetchDataPeriodically = async () => {
let res;
let timestampToFetch: UnixTime;
// Search backward in parallel batches of 10 timestamps (2s apart)
// Each batch covers 20 seconds, so 20 batches cover 400 seconds
const batchSize = 10;
const step = 2; // 2-second steps to match even-rounding granularity
const maxAge = 400;
let found = false;
for (let batchStart = 0; batchStart < maxAge; batchStart += batchSize * step) {
if (!continueFetching.current) { if (!continueFetching.current) {
return false; return false;
} }
timestampToFetch = UnixTime.now().earlier(TimeSpan.fromSeconds(i)); const offsets = [];
for (let j = 0; j < batchSize; j++) {
const offset = batchStart + j * step;
if (offset < maxAge) offsets.push(offset);
}
try { try {
res = await fetchDataJson(timestampToFetch, s3Credentials, false); const hit = await probeTimestampBatch(offsets);
if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) { if (hit) {
res = hit.res;
timestampToFetch = hit.timestamp;
found = true;
break; break;
} }
} catch (err) { } catch (err) {
@ -133,7 +165,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
} }
} }
if (i >= timeperiodToSearch) { if (!found) {
setConnected(false); setConnected(false);
setLoading(false); setLoading(false);
return false; return false;
@ -154,10 +186,12 @@ function SodioHomeInstallation(props: singleInstallationProps) {
await timeout(2000); await timeout(2000);
} }
timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(60)); // Advance by 150s to find the next chunk (15 records × 10s interval)
timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(150));
console.log('NEW TIMESTAMP TO FETCH IS ' + timestampToFetch); console.log('NEW TIMESTAMP TO FETCH IS ' + timestampToFetch);
for (i = 0; i < 30; i++) { let foundNext = false;
for (var i = 0; i < 60; i++) {
if (!continueFetching.current) { if (!continueFetching.current) {
return false; return false;
} }
@ -169,6 +203,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
res !== FetchResult.notAvailable && res !== FetchResult.notAvailable &&
res !== FetchResult.tryLater res !== FetchResult.tryLater
) { ) {
foundNext = true;
break; break;
} }
} catch (err) { } catch (err) {
@ -177,24 +212,30 @@ function SodioHomeInstallation(props: singleInstallationProps) {
} }
timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(1)); timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(1));
} }
if (i == 30) { if (!foundNext) {
return false; return false;
} }
} }
}; };
const fetchDataForOneTime = async () => { const fetchDataForOneTime = async () => {
var timeperiodToSearch = 300; // 5 minutes to cover ~2 upload cycles (150s each) // Search backward in parallel batches of 10 timestamps (2s apart)
const batchSize = 10;
const step = 2;
const maxAge = 400;
let res; let res;
let timestampToFetch;
// Search from NOW backward to find the most recent data for (let batchStart = 0; batchStart < maxAge; batchStart += batchSize * step) {
// Step by 50 seconds - data is uploaded every ~150s, so finer steps are wasteful const offsets = [];
for (var i = 0; i < timeperiodToSearch; i += 50) { for (let j = 0; j < batchSize; j++) {
timestampToFetch = UnixTime.now().earlier(TimeSpan.fromSeconds(i)); const offset = batchStart + j * step;
if (offset < maxAge) offsets.push(offset);
}
try { try {
res = await fetchDataJson(timestampToFetch, s3Credentials, false); const hit = await probeTimestampBatch(offsets);
if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) { if (hit) {
res = hit.res;
break; break;
} }
} catch (err) { } catch (err) {
@ -203,11 +244,12 @@ function SodioHomeInstallation(props: singleInstallationProps) {
} }
} }
if (i >= timeperiodToSearch) { if (!res) {
setConnected(false); setConnected(false);
setLoading(false); setLoading(false);
return false; return false;
} }
setConnected(true); setConnected(true);
setLoading(false); setLoading(false);
@ -215,12 +257,6 @@ function SodioHomeInstallation(props: singleInstallationProps) {
const timestamps = Object.keys(res).sort((a, b) => Number(b) - Number(a)); const timestamps = Object.keys(res).sort((a, b) => Number(b) - Number(a));
const latestTimestamp = timestamps[0]; const latestTimestamp = timestamps[0];
setValues(res[latestTimestamp]); setValues(res[latestTimestamp]);
// setValues(
// extractValues({
// time: UnixTime.fromTicks(parseInt(timestamp, 10)),
// value: res[timestamp]
// })
// );
return true; return true;
}; };

View File

@ -32,6 +32,22 @@ export class S3Access {
} }
} }
public list(marker?: string, maxKeys: number = 50): Promise<Response> {
const method = "GET";
const auth = this.createAuthorizationHeader(method, "", "");
const params = new URLSearchParams();
if (marker) params.set("marker", marker);
params.set("max-keys", maxKeys.toString());
const url = this.url + "/" + this.bucket + "/?" + params.toString();
const headers = { Host: this.host, Authorization: auth };
try {
return fetch(url, { method: method, mode: "cors", headers: headers });
} catch {
return Promise.reject();
}
}
private createAuthorizationHeader( private createAuthorizationHeader(
method: string, method: string,
s3Path: string, s3Path: string,