had claude look at the bin player to determine how to take into account the component history for the top nodes

This commit is contained in:
csawatzky 2026-06-23 16:25:31 -06:00
parent a4ed52db1e
commit 939a53d072
4 changed files with 135 additions and 6 deletions

4
package-lock.json generated
View file

@ -46,7 +46,7 @@
"mui-tel-input": "^7.0.0", "mui-tel-input": "^7.0.0",
"notistack": "^3.0.1", "notistack": "^3.0.1",
"openweathermap-ts": "^1.2.10", "openweathermap-ts": "^1.2.10",
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#sendgrid_whitelabel", "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#bin_player_inventory_changes",
"query-string": "^9.2.1", "query-string": "^9.2.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1", "react-beautiful-dnd": "^13.1.1",
@ -11752,7 +11752,7 @@
}, },
"node_modules/protobuf-ts": { "node_modules/protobuf-ts": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#9c0f668d4a56b8216dd71a44c3110a818aaf3541", "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#4bd44bd7e19e7e58344c224b3413a81ece379fed",
"dependencies": { "dependencies": {
"protobufjs": "^6.8.8" "protobufjs": "^6.8.8"
} }

View file

@ -59,7 +59,7 @@
"mui-tel-input": "^7.0.0", "mui-tel-input": "^7.0.0",
"notistack": "^3.0.1", "notistack": "^3.0.1",
"openweathermap-ts": "^1.2.10", "openweathermap-ts": "^1.2.10",
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#sendgrid_whitelabel", "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#bin_player_inventory_changes",
"query-string": "^9.2.1", "query-string": "^9.2.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1", "react-beautiful-dnd": "^13.1.1",

View file

@ -90,6 +90,58 @@ function upsertReading(
} }
} }
/**
* A trimmed record of a single component-settings history entry: just the timestamp and the
* top-node value (`grain_filled_to`) that was in effect from that point forward.
*/
type TopNodeHistoryEntry = {
timestamp: Moment;
topNode: number;
};
/**
* Converts raw `ComponentHistory` entries for one cable into a sorted (ascending) array of
* `TopNodeHistoryEntry` records, keeping only entries where `grain_filled_to` is defined and
* non-zero so that we never overwrite a known top node with a "not set" value.
*/
function buildTopNodeHistory(
history: pond.ComponentHistory[]
): TopNodeHistoryEntry[] {
return history
.filter(h => h.component && h.component.grainFilledTo > 0 && h.timestamp)
.map(h => ({
timestamp: moment(h.timestamp),
topNode: h.component ? h.component.grainFilledTo : 0,
}))
.sort((a, b) => a.timestamp.valueOf() - b.timestamp.valueOf());
}
/**
* Binary-searches a sorted `TopNodeHistoryEntry[]` for the most recent entry whose timestamp
* is at or before `checkpointTime`. Returns `undefined` when no such entry exists (i.e. the
* checkpoint is before the first recorded top-node change).
*/
function topNodeAtTime(
history: TopNodeHistoryEntry[],
checkpointTime: Moment
): number | undefined {
if (history.length === 0) return undefined;
const t = checkpointTime.valueOf();
let lo = 0;
let hi = history.length - 1;
let result: number | undefined = undefined;
while (lo <= hi) {
const mid = (lo + hi) >>> 1;
if (history[mid].timestamp.valueOf() <= t) {
result = history[mid].topNode;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return result;
}
/** /**
* Produces `count` wall-clock times evenly spaced from `start` through `end` (inclusive). * Produces `count` wall-clock times evenly spaced from `start` through `end` (inclusive).
* These are the playback frames not derived from measurement data. * These are the playback frames not derived from measurement data.
@ -360,6 +412,8 @@ function buildStatusCheckpoints(
cableMeasurementResponses: pond.SampleUnitMeasurementsResponse[], cableMeasurementResponses: pond.SampleUnitMeasurementsResponse[],
fanMeasurementResponses: pond.SampleUnitMeasurementsResponse[], fanMeasurementResponses: pond.SampleUnitMeasurementsResponse[],
heaterMeasurementResponses: pond.SampleUnitMeasurementsResponse[], heaterMeasurementResponses: pond.SampleUnitMeasurementsResponse[],
/** Sorted top-node history per cable key. Pass an empty Map when not needed. */
topNodeHistories: Map<string, TopNodeHistoryEntry[]>,
start: Moment, start: Moment,
end: Moment, end: Moment,
count: number = CHECKPOINT_COUNT count: number = CHECKPOINT_COUNT
@ -416,7 +470,19 @@ function buildStatusCheckpoints(
const bucketReadings = cableBucket.get(key) ?? {}; const bucketReadings = cableBucket.get(key) ?? {};
const merged = mergeCarriedReadings(carriedByCable.get(key)!, bucketReadings); const merged = mergeCarriedReadings(carriedByCable.get(key)!, bucketReadings);
carriedByCable.set(key, merged); carriedByCable.set(key, merged);
return buildGrainCableFromReadings(template, merged, time); const cable = buildGrainCableFromReadings(template, merged, time);
// Apply the most recent top-node setting that was in effect at this checkpoint time.
// Falls back to the template's current top_node when no history entry precedes this time.
const history = topNodeHistories.get(key);
if (history) {
const tn = topNodeAtTime(history, time);
if (tn !== undefined) {
cable.topNode = tn;
}
}
return cable;
}); });
// Build fans // Build fans
@ -529,6 +595,18 @@ export default function BinPlayer(props: Props) {
return componentAPI.sampleUnitMeasurements(deviceID, cable.key, startDate, endDate, 100, undefined, undefined, undefined, undefined, true); return componentAPI.sampleUnitMeasurements(deviceID, cable.key, startDate, endDate, 100, undefined, undefined, undefined, undefined, true);
}); });
// Cable top-node history requests — one per cable, covering the full playback range.
const cableHistoryRequests = bin.status.grainCables.map(cable => {
const deviceID = componentDevices.get(cable.key) ?? 0;
console.log(deviceID)
return componentAPI.listHistoryBetween(
deviceID,
cable.key,
startDate.toISOString(),
endDate.toISOString()
);
});
// Fan requests // Fan requests
const fanSampleRequests = bin.status.fans.map(fan => { const fanSampleRequests = bin.status.fans.map(fan => {
const deviceID = componentDevices.get(fan.key) ?? 0; const deviceID = componentDevices.get(fan.key) ?? 0;
@ -541,12 +619,23 @@ export default function BinPlayer(props: Props) {
return componentAPI.sampleUnitMeasurements(deviceID, heater.key, startDate, endDate, 100, undefined, undefined, undefined, undefined, true); return componentAPI.sampleUnitMeasurements(deviceID, heater.key, startDate, endDate, 100, undefined, undefined, undefined, undefined, true);
}); });
const [cableResponses, fanResponses, heaterResponses] = await Promise.all([ const [cableResponses, fanResponses, heaterResponses, cableHistoryResponses] = await Promise.all([
Promise.all(cableSampleRequests), Promise.all(cableSampleRequests),
Promise.all(fanSampleRequests), Promise.all(fanSampleRequests),
Promise.all(heaterSampleRequests), Promise.all(heaterSampleRequests),
Promise.all(cableHistoryRequests),
]); ]);
console.log(cableHistoryResponses)
// Build a per-cable sorted top-node history map for use during checkpoint construction.
const topNodeHistories = new Map<string, TopNodeHistoryEntry[]>();
bin.status.grainCables.forEach((cable, i) => {
const historyResp = cableHistoryResponses[i];
const entries = buildTopNodeHistory(historyResp?.data?.history ?? []);
topNodeHistories.set(cable.key, entries);
});
const checkpoints = buildStatusCheckpoints( const checkpoints = buildStatusCheckpoints(
bin.status.grainCables, bin.status.grainCables,
bin.status.fans, bin.status.fans,
@ -554,6 +643,7 @@ export default function BinPlayer(props: Props) {
cableResponses.map(r => r.data), cableResponses.map(r => r.data),
fanResponses.map(r => r.data), fanResponses.map(r => r.data),
heaterResponses.map(r => r.data), heaterResponses.map(r => r.data),
topNodeHistories,
startDate, startDate,
endDate, endDate,
checkpointCountFromRange(startDate, endDate) checkpointCountFromRange(startDate, endDate)

View file

@ -62,6 +62,14 @@ export interface IComponentAPIContext {
keys?: string[], keys?: string[],
types?: string[] types?: string[]
) => Promise<AxiosResponse<pond.ListComponentHistoryResponse>>; ) => Promise<AxiosResponse<pond.ListComponentHistoryResponse>>;
listHistoryBetween: (
deviceId: string | number,
componentKey: string,
start: string,
end: string,
keys?: string[],
types?: string[]
) => Promise<AxiosResponse<pond.ListComponentHistoryBetweenResponse>>;
// Old measuremnt structure functions, no longer used in codebase // Old measuremnt structure functions, no longer used in codebase
// listMeasurements: ( // listMeasurements: (
// device: number, // device: number,
@ -391,6 +399,36 @@ export default function ComponentProvider(props: PropsWithChildren<Props>) {
}) })
}; };
const listHistoryBetween = (
deviceId: string | number,
componentKey: string,
start: string,
end: string,
keys?: string[],
types?: string[]
) => {
let url = pondURL(
"/devices/" +
deviceId +
"/components/" +
componentKey +
"/historyBetween?start=" +
start +
"&end=" +
end +
(keys ? "&keys=" + keys : "&keys=" + [deviceId.toString()]) +
(types ? "&types=" + types : "&types=" + ["device"])
)
return new Promise<AxiosResponse<pond.ListComponentHistoryBetweenResponse>>((resolve,reject) => {
get<pond.ListComponentHistoryBetweenResponse>(url).then(resp => {
resp.data = pond.ListComponentHistoryBetweenResponse.fromObject(resp.data)
return resolve(resp)
}).catch(err => {
return reject(err)
})
})
};
// const listMeasurements = ( // const listMeasurements = (
// device: number, // device: number,
// component: string, // component: string,
@ -596,6 +634,7 @@ export default function ComponentProvider(props: PropsWithChildren<Props>) {
listForObject: listComponentsForObject, listForObject: listComponentsForObject,
listComponentCardData: listComponentCardData, listComponentCardData: listComponentCardData,
listHistory, listHistory,
listHistoryBetween,
//listMeasurements: listMeasurements, //listMeasurements: listMeasurements,
//sampleMeasurements: sampleMeasurements, //sampleMeasurements: sampleMeasurements,
sampleUnitMeasurements, sampleUnitMeasurements,