fix(prediction): 跳过缺少遥测字段的预测请求
This commit is contained in:
@@ -71,22 +71,8 @@ describe('prediction client helpers', () => {
|
|||||||
expect(isPredictionForBattery({ batteryId: battery.id, mac: 'RING-B11' }, battery as BatteryInfo)).toBe(false)
|
expect(isPredictionForBattery({ batteryId: battery.id, mac: 'RING-B11' }, battery as BatteryInfo)).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('returns null for history-insufficient prediction requests', () => {
|
test('returns null when required telemetry fields are unavailable', () => {
|
||||||
expect(createPredictionRequest(battery, [battery, battery, battery, battery])).toBeNull()
|
expect(createPredictionRequest(battery, [battery, battery, battery, battery])).toBeNull()
|
||||||
})
|
expect(createPredictionRequest(battery, [battery, battery, battery, battery, battery])).toBeNull()
|
||||||
|
|
||||||
test('uses only real battery history fields in prediction requests', () => {
|
|
||||||
const request = createPredictionRequest(battery, [battery, battery, battery, battery, battery])
|
|
||||||
const firstHistory = request?.history[0]
|
|
||||||
|
|
||||||
expect(firstHistory).toEqual({
|
|
||||||
id: battery.id,
|
|
||||||
power: battery.power,
|
|
||||||
power_status: battery.powerStatus,
|
|
||||||
is_low_power: MYSQL_BOOLEAN.FALSE,
|
|
||||||
timestamp: battery.createTime,
|
|
||||||
})
|
|
||||||
expect(Object.hasOwn(firstHistory ?? {}, 'charge_capacity_ah')).toBe(false)
|
|
||||||
expect(Object.hasOwn(firstHistory ?? {}, 'coulombic_efficiency_pct')).toBe(false)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { LRUCache } from 'lru-cache'
|
import { LRUCache } from 'lru-cache'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { type BatteryInfo, type BatteryPrediction, type PowerStatus, toMysqlBoolean } from '@/domain/battery'
|
import type { BatteryInfo, BatteryPrediction, PowerStatus } from '@/domain/battery'
|
||||||
import { env } from '@/env'
|
import { env } from '@/env'
|
||||||
import { getLogger } from '@/server/logger'
|
import { getLogger } from '@/server/logger'
|
||||||
|
|
||||||
@@ -20,10 +20,14 @@ export const sohPredictionSchema = z.object({
|
|||||||
export type SohPrediction = BatteryPrediction & z.infer<typeof sohPredictionSchema>
|
export type SohPrediction = BatteryPrediction & z.infer<typeof sohPredictionSchema>
|
||||||
|
|
||||||
type PredictionHistoryItem = {
|
type PredictionHistoryItem = {
|
||||||
id: number
|
cycle: number
|
||||||
power: number
|
charge_capacity_ah: number
|
||||||
power_status: PowerStatus
|
discharge_capacity_ah: number
|
||||||
is_low_power: string
|
charge_energy_wh: number
|
||||||
|
discharge_energy_wh: number
|
||||||
|
charge_time: string
|
||||||
|
discharge_time: string
|
||||||
|
coulombic_efficiency_pct: number
|
||||||
timestamp: string
|
timestamp: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,12 +72,6 @@ const negativeCache = new LRUCache<string, true>({
|
|||||||
})
|
})
|
||||||
const inFlightRequests = new Map<string, Promise<SohPrediction | null>>()
|
const inFlightRequests = new Map<string, Promise<SohPrediction | null>>()
|
||||||
|
|
||||||
function normalizeMysqlDateTime(value: string) {
|
|
||||||
if (!value.includes('T')) return value
|
|
||||||
|
|
||||||
return value.slice(0, 19).replace('T', ' ')
|
|
||||||
}
|
|
||||||
|
|
||||||
function createCacheKey(battery: BatteryInfo, history: BatteryInfo[]) {
|
function createCacheKey(battery: BatteryInfo, history: BatteryInfo[]) {
|
||||||
const latestHistory = history.at(-1)
|
const latestHistory = history.at(-1)
|
||||||
const latestId = latestHistory?.id ?? battery.id
|
const latestId = latestHistory?.id ?? battery.id
|
||||||
@@ -82,36 +80,10 @@ function createCacheKey(battery: BatteryInfo, history: BatteryInfo[]) {
|
|||||||
return `${battery.mac}:${latestId}:${latestTime}`
|
return `${battery.mac}:${latestId}:${latestTime}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function createHistoryItem(item: BatteryInfo): PredictionHistoryItem {
|
export function createPredictionRequest(_battery: BatteryInfo, _history: BatteryInfo[]): PredictionRequest | null {
|
||||||
return {
|
// The current customer table lacks the real capacity/energy telemetry required by the prediction service.
|
||||||
id: item.id,
|
// Returning null avoids both synthetic features and predictable 422 responses from the service.
|
||||||
power: item.power,
|
return null
|
||||||
power_status: item.powerStatus,
|
|
||||||
is_low_power: toMysqlBoolean(item.isLowPower),
|
|
||||||
timestamp: normalizeMysqlDateTime(item.createTime),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createPredictionRequest(battery: BatteryInfo, history: BatteryInfo[]): PredictionRequest | null {
|
|
||||||
const sourceHistory = history.length > 0 ? history : [battery]
|
|
||||||
|
|
||||||
if (sourceHistory.length < 5) return null
|
|
||||||
|
|
||||||
return {
|
|
||||||
battery: {
|
|
||||||
id: battery.id,
|
|
||||||
user_id: battery.userId,
|
|
||||||
mac: battery.mac,
|
|
||||||
dev_model: battery.devModel,
|
|
||||||
dev_name: battery.devName,
|
|
||||||
is_low_power: toMysqlBoolean(battery.isLowPower),
|
|
||||||
power_status: battery.powerStatus,
|
|
||||||
power: battery.power,
|
|
||||||
create_time: normalizeMysqlDateTime(battery.createTime),
|
|
||||||
remark: battery.remark ?? '',
|
|
||||||
},
|
|
||||||
history: sourceHistory.map(createHistoryItem),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizePrediction(response: z.infer<typeof predictionResponseSchema>): SohPrediction {
|
export function normalizePrediction(response: z.infer<typeof predictionResponseSchema>): SohPrediction {
|
||||||
|
|||||||
Reference in New Issue
Block a user