package stats import ( "context" "database/sql" "encoding/json" "errors" "math" "sort" "strings" "time" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/telemetry" ) const ( defaultHydrogenNoiseKG = 0.05 defaultHydrogenMaxDropKG = 20.0 hydrogenActiveCurrentA = 1.0 hydrogenMolarMassKGPerMol = 0.00201588 universalGasConstant = 8.314472 hydrogenSegmentWindowSize = 5 hydrogenSegmentMinSamples = hydrogenSegmentWindowSize * 2 hydrogenSegmentMaxGap = 5 * time.Minute hydrogenRecoveryWindow = time.Minute hydrogenRecoveryPressure = 8.0 ) var hydrogenPressureFieldKeys = []string{ "gb32960.fuel_cell.max_hydrogen_pressure_mpa", "fuel_cell_max_hydrogen_pressure_mpa", } var hydrogenTemperatureFieldKeys = []string{ "gb32960.fuel_cell.max_hydrogen_temperature_c", "fuel_cell_max_hydrogen_temperature_c", } var hydrogenCurrentFieldKeys = []string{ "gb32960.fuel_cell.fuel_cell_current_a", "fuel_cell_current_a", } var hydrogenDensityA = [...]float64{0.05888460, -0.06136111, -0.002650473, 0.002731125, 0.001802374, -0.001150707, 0.00009588528, -0.0000001109040, 0.0000000001264403} var hydrogenDensityB = [...]float64{1.325, 1.87, 2.5, 2.8, 2.938, 3.14, 3.37, 3.75, 4.0} var hydrogenDensityC = [...]float64{1, 1, 2, 2, 2.42, 2.63, 3, 4, 5} const HydrogenStreamStateTableSQL = `CREATE TABLE IF NOT EXISTS vehicle_open_hydrogen_stream_state ( vin VARCHAR(64) NOT NULL, stat_date DATE NOT NULL, source_endpoint VARCHAR(128) NOT NULL DEFAULT '', first_mass_kg DECIMAL(18,3) NOT NULL, last_mass_kg DECIMAL(18,3) NOT NULL, cycle_min_mass_kg DECIMAL(18,3) NOT NULL, consumption_kg DECIMAL(18,3) NOT NULL DEFAULT 0, sample_count BIGINT UNSIGNED NOT NULL DEFAULT 1, refuel_count INT UNSIGNED NOT NULL DEFAULT 0, abnormal_drop_count INT UNSIGNED NOT NULL DEFAULT 0, tank_capacity_l DECIMAL(12,2) NOT NULL, first_pressure_mpa DECIMAL(10,3) NOT NULL, last_pressure_mpa DECIMAL(10,3) NOT NULL, first_temperature_c DECIMAL(10,3) NOT NULL, last_temperature_c DECIMAL(10,3) NOT NULL, calculation_method VARCHAR(32) NOT NULL DEFAULT 'PRESSURE_NIST', first_event_time DATETIME(3) NOT NULL, last_event_time DATETIME(3) NOT NULL, last_event_id VARCHAR(128) NOT NULL DEFAULT '', last_fuel_cell_active TINYINT(1) NOT NULL DEFAULT 0, last_fuel_cell_state_known TINYINT(1) NOT NULL DEFAULT 0, quality_status VARCHAR(24) NOT NULL DEFAULT 'NO_DATA', quality_reason VARCHAR(255) NOT NULL DEFAULT '有效压力质量样本不足2条', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (vin, stat_date, source_endpoint), KEY idx_hydrogen_stream_date_quality (stat_date, quality_status, vin) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci` const HydrogenSegmentStreamStateTableSQL = `CREATE TABLE IF NOT EXISTS vehicle_open_hydrogen_segment_stream_state ( vin VARCHAR(64) NOT NULL, stat_date DATE NOT NULL, source_endpoint VARCHAR(128) NOT NULL DEFAULT '', finalized_consumption_kg DECIMAL(18,3) NOT NULL DEFAULT 0, projected_consumption_kg DECIMAL(18,3) NOT NULL DEFAULT 0, sample_count BIGINT UNSIGNED NOT NULL DEFAULT 0, refuel_count INT UNSIGNED NOT NULL DEFAULT 0, abnormal_drop_count INT UNSIGNED NOT NULL DEFAULT 0, eligible_interval_count BIGINT UNSIGNED NOT NULL DEFAULT 0, qualified_segment_count INT UNSIGNED NOT NULL DEFAULT 0, last_mass_kg DECIMAL(18,3) NOT NULL DEFAULT 0, last_event_time DATETIME(3) NOT NULL, last_event_id VARCHAR(128) NOT NULL DEFAULT '', state_json JSON NOT NULL, calculation_method VARCHAR(80) NOT NULL DEFAULT 'PRESSURE_NIST_VALID_BOUNDARY_CHARGE_CYCLE_V3_5', quality_status VARCHAR(24) NOT NULL DEFAULT 'NO_DATA', quality_reason VARCHAR(255) NOT NULL DEFAULT '有效工作段关键帧不足10条', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (vin, stat_date), KEY idx_hydrogen_segment_stream_date_quality (stat_date, quality_status, vin), CONSTRAINT chk_hydrogen_segment_stream_quality CHECK (quality_status IN ('OK', 'NO_DATA', 'SUSPECT')) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci` type HydrogenStreamSample struct { VIN string Date string SourceEndpoint string EventID string EventTime time.Time MassKG float64 TankCapacityLiters float64 PressureMPa float64 TemperatureC float64 NoiseKG float64 RefuelThresholdKG float64 FuelCellActive bool FuelCellStateKnown bool ConsumptionEligible bool MileageKM float64 MileageKnown bool SOCPercent float64 SOCKnown bool VehicleState int VehicleStateKnown bool ChargeState int ChargeStateKnown bool RunningMode int RunningModeKnown bool FuelCellPowerKW float64 FuelCellPowerKnown bool } type HydrogenStreamResult struct { Found int Written int Duplicate int Invalid int NotCurrent int } type hydrogenStreamEndpoint struct { MassKG float64 `json:"massKg"` NoiseKG float64 `json:"noiseKg"` } type hydrogenStreamSegmentWindow struct { Count int64 `json:"count"` First []hydrogenStreamEndpoint `json:"first"` Tail []hydrogenStreamEndpoint `json:"tail"` CycleMinimumMassKG float64 `json:"cycleMinimumMassKg"` } type hydrogenSegmentStreamState struct { SourceEndpoint string `json:"sourceEndpoint"` FinalizedConsumptionKG float64 `json:"finalizedConsumptionKg"` SampleCount int64 `json:"sampleCount"` RefuelCount int64 `json:"refuelCount"` AbnormalDropCount int64 `json:"abnormalDropCount"` EligibleIntervalCount int64 `json:"eligibleIntervalCount"` QualifiedSegmentCount int64 `json:"qualifiedSegmentCount"` FirstMassKG float64 `json:"firstMassKg"` RemainingMassKG float64 `json:"remainingMassKg"` MetadataCycleMinimumKG float64 `json:"metadataCycleMinimumKg"` LastObservedMassKG float64 `json:"lastObservedMassKg"` TankCapacityLiters float64 `json:"tankCapacityLiters"` FirstPressureMPa float64 `json:"firstPressureMpa"` LastPressureMPa float64 `json:"lastPressureMpa"` FirstTemperatureC float64 `json:"firstTemperatureC"` LastTemperatureC float64 `json:"lastTemperatureC"` FirstEventTime time.Time `json:"firstEventTime"` LastEventTime time.Time `json:"lastEventTime"` LastEventID string `json:"lastEventId"` LastFuelCellActive bool `json:"lastFuelCellActive"` LastFuelCellStateKnown bool `json:"lastFuelCellStateKnown"` Segment hydrogenStreamSegmentWindow `json:"segment"` } type hydrogenStreamBaseline struct { SourceEndpoint string ConsumptionKG float64 FirstMassKG float64 LastMassKG float64 SampleCount int64 RefuelCount int64 QualityStatus string } func newHydrogenSegmentStreamState(sample HydrogenStreamSample, baseline *hydrogenStreamBaseline) hydrogenSegmentStreamState { state := hydrogenSegmentStreamState{ SourceEndpoint: sample.SourceEndpoint, FirstMassKG: sample.MassKG, RemainingMassKG: sample.MassKG, MetadataCycleMinimumKG: sample.MassKG, LastObservedMassKG: sample.MassKG, TankCapacityLiters: sample.TankCapacityLiters, FirstPressureMPa: sample.PressureMPa, LastPressureMPa: sample.PressureMPa, FirstTemperatureC: sample.TemperatureC, LastTemperatureC: sample.TemperatureC, FirstEventTime: sample.EventTime, LastEventTime: sample.EventTime, LastEventID: sample.EventID, LastFuelCellActive: sample.FuelCellActive, LastFuelCellStateKnown: sample.FuelCellStateKnown, SampleCount: 1, } if baseline != nil { state.SourceEndpoint = firstNonEmptyHydrogen(sample.SourceEndpoint, baseline.SourceEndpoint) state.FinalizedConsumptionKG = baseline.ConsumptionKG state.SampleCount += baseline.SampleCount state.RefuelCount = baseline.RefuelCount if baseline.FirstMassKG >= 0 { state.FirstMassKG = baseline.FirstMassKG } if baseline.LastMassKG >= 0 { state.RemainingMassKG = baseline.LastMassKG } if baseline.QualityStatus == "OK" || baseline.QualityStatus == "SUSPECT" { state.EligibleIntervalCount = 1 state.QualifiedSegmentCount = 1 } if baseline.QualityStatus == "SUSPECT" { state.AbnormalDropCount = 1 } } if hydrogenStreamCanStartSegment(sample) { state.startSegment(sample) } return state } func (state *hydrogenSegmentStreamState) add(sample HydrogenStreamSample) bool { if !sample.EventTime.After(state.LastEventTime) { return false } previousTime := state.LastEventTime previousMass := state.LastObservedMassKG previousPressure := state.LastPressureMPa intervalEligible := hydrogenStreamIntervalEligible( state.LastFuelCellActive, state.LastFuelCellStateKnown, sample.FuelCellActive, sample.FuelCellStateKnown, ) if intervalEligible || (sample.FuelCellStateKnown && sample.FuelCellActive) || sample.MassKG-state.RemainingMassKG > sample.NoiseKG { state.RemainingMassKG = sample.MassKG } delta := state.MetadataCycleMinimumKG - sample.MassKG switch { case sample.MassKG-state.MetadataCycleMinimumKG > sample.RefuelThresholdKG: if !hydrogenStreamRapidRecovery(previousTime, previousPressure, sample) { state.RefuelCount++ } state.MetadataCycleMinimumKG = sample.MassKG case delta > sample.NoiseKG && delta <= defaultHydrogenMaxDropKG: state.MetadataCycleMinimumKG = sample.MassKG } if intervalEligible { state.EligibleIntervalCount++ } switch { case state.Segment.Count == 0 || !intervalEligible: state.flushSegment() if hydrogenStreamCanStartSegment(sample) { state.startSegment(sample) } case sample.EventTime.Sub(previousTime) > hydrogenSegmentMaxGap: state.flushSegment() state.startSegment(sample) case previousMass-sample.MassKG > defaultHydrogenMaxDropKG: state.AbnormalDropCount++ state.flushSegment() state.startSegment(sample) case sample.MassKG-state.Segment.CycleMinimumMassKG > sample.RefuelThresholdKG: state.flushSegment() state.startSegment(sample) default: state.appendSegmentSample(sample) if sample.MassKG < state.Segment.CycleMinimumMassKG { state.Segment.CycleMinimumMassKG = sample.MassKG } } state.SourceEndpoint = sample.SourceEndpoint state.SampleCount++ state.LastObservedMassKG = sample.MassKG state.TankCapacityLiters = sample.TankCapacityLiters state.LastPressureMPa = sample.PressureMPa state.LastTemperatureC = sample.TemperatureC state.LastEventTime = sample.EventTime state.LastEventID = sample.EventID state.LastFuelCellActive = sample.FuelCellActive state.LastFuelCellStateKnown = sample.FuelCellStateKnown return true } func (state *hydrogenSegmentStreamState) startSegment(sample HydrogenStreamSample) { state.Segment = hydrogenStreamSegmentWindow{CycleMinimumMassKG: sample.MassKG} state.appendSegmentSample(sample) } func (state *hydrogenSegmentStreamState) appendSegmentSample(sample HydrogenStreamSample) { endpoint := hydrogenStreamEndpoint{MassKG: sample.MassKG, NoiseKG: sample.NoiseKG} state.Segment.Count++ if len(state.Segment.First) < hydrogenSegmentWindowSize { state.Segment.First = append(state.Segment.First, endpoint) } if len(state.Segment.Tail) == hydrogenSegmentWindowSize { copy(state.Segment.Tail, state.Segment.Tail[1:]) state.Segment.Tail[len(state.Segment.Tail)-1] = endpoint return } state.Segment.Tail = append(state.Segment.Tail, endpoint) } func (state *hydrogenSegmentStreamState) flushSegment() { if state.Segment.Count >= hydrogenSegmentMinSamples { state.QualifiedSegmentCount++ state.FinalizedConsumptionKG += hydrogenStreamEndpointDrop(state.Segment.First, state.Segment.Tail) } state.Segment = hydrogenStreamSegmentWindow{} } func (state hydrogenSegmentStreamState) projectedConsumptionKG() float64 { consumption := state.FinalizedConsumptionKG if state.Segment.Count >= hydrogenSegmentMinSamples { consumption += hydrogenStreamEndpointDrop(state.Segment.First, state.Segment.Tail) } return roundHydrogenKG(consumption) } func (state hydrogenSegmentStreamState) quality() (string, string) { qualified := state.QualifiedSegmentCount if state.Segment.Count >= hydrogenSegmentMinSamples { qualified++ } switch { case state.SampleCount < 2: return "NO_DATA", "有效车载氢量样本不足2条" case state.EligibleIntervalCount == 0: return "NO_DATA", "无燃料电池工作状态下的有效压力区间" case qualified == 0: return "NO_DATA", "有效工作段关键帧不足10条" case state.AbnormalDropCount > 0: return "SUSPECT", "存在超过阈值的异常下降" default: return "OK", "" } } func hydrogenStreamCanStartSegment(sample HydrogenStreamSample) bool { return !sample.FuelCellStateKnown || sample.FuelCellActive } func hydrogenStreamIntervalEligible(previousActive, previousKnown, currentActive, currentKnown bool) bool { if previousKnown || currentKnown { return previousKnown && previousActive && currentKnown && currentActive } return true } func hydrogenStreamRapidRecovery(previousTime time.Time, previousPressure float64, sample HydrogenStreamSample) bool { elapsed := sample.EventTime.Sub(previousTime) return elapsed > 0 && elapsed <= hydrogenRecoveryWindow && sample.PressureMPa-previousPressure >= hydrogenRecoveryPressure } func hydrogenStreamEndpointDrop(first, tail []hydrogenStreamEndpoint) float64 { if len(first) < hydrogenSegmentWindowSize || len(tail) < hydrogenSegmentWindowSize { return 0 } startMass := hydrogenStreamMedianMass(first) endMass := hydrogenStreamMedianMass(tail) noise := defaultHydrogenNoiseKG for _, endpoint := range first { noise = math.Max(noise, endpoint.NoiseKG) } for _, endpoint := range tail { noise = math.Max(noise, endpoint.NoiseKG) } drop := startMass - endMass if drop <= noise { return 0 } return drop } func hydrogenStreamMedianMass(values []hydrogenStreamEndpoint) float64 { masses := make([]float64, len(values)) for index, value := range values { masses[index] = value.MassKG } sort.Float64s(masses) return masses[len(masses)/2] } func roundHydrogenKG(value float64) float64 { return math.Round(value*1000) / 1000 } func firstNonEmptyHydrogen(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return strings.TrimSpace(value) } } return "" } func HydrogenDensityKGPerM3(pressureMPa, temperatureC float64) (float64, bool) { temperatureK := temperatureC + 273.15 if pressureMPa < 0 || pressureMPa > 70 || temperatureK < 220 || temperatureK > 1000 { return 0, false } z := 1.0 for i := range hydrogenDensityA { z += hydrogenDensityA[i] * math.Pow(100/temperatureK, hydrogenDensityB[i]) * math.Pow(pressureMPa, hydrogenDensityC[i]) } if z <= 0 { return 0, false } molPerLiter := pressureMPa * 1000 / (universalGasConstant * temperatureK * z) return molPerLiter * hydrogenMolarMassKGPerMol * 1000, true } func PressureHydrogenMassKG(pressureMPa, temperatureC, tankCapacityLiters float64) (float64, bool) { if tankCapacityLiters <= 0 || tankCapacityLiters > 10000 { return 0, false } density, ok := HydrogenDensityKGPerM3(pressureMPa, temperatureC) if !ok { return 0, false } mass := density * tankCapacityLiters / 1000 return mass, !math.IsNaN(mass) && !math.IsInf(mass, 0) && mass >= 0 && mass <= 500 } func HydrogenStreamSampleFromEnvelope(env envelope.FrameEnvelope, loc *time.Location, now time.Time, tankCapacityLiters float64) (HydrogenStreamSample, string, bool) { if env.Protocol != envelope.ProtocolGB32960 { return HydrogenStreamSample{}, "unsupported_protocol", false } vin := strings.ToUpper(strings.TrimSpace(env.VIN)) if len(vin) != 17 { return HydrogenStreamSample{}, "invalid_vin", false } pressure, pressureOK := firstHydrogenNumber(env.Fields, hydrogenPressureFieldKeys) if !pressureOK { return HydrogenStreamSample{}, "missing_pressure", false } temperature, temperatureOK := firstHydrogenNumber(env.Fields, hydrogenTemperatureFieldKeys) if !temperatureOK { return HydrogenStreamSample{}, "missing_temperature", false } if !validHydrogenPressureTemperature(pressure, temperature) { return HydrogenStreamSample{}, "invalid_pressure_temperature", false } if tankCapacityLiters <= 0 || tankCapacityLiters > 10000 { return HydrogenStreamSample{}, "missing_tank_capacity", false } mass, ok := PressureHydrogenMassKG(pressure, temperature, tankCapacityLiters) if !ok { return HydrogenStreamSample{}, "invalid_pressure_mass", false } pressureStepMass, _ := PressureHydrogenMassKG(math.Max(0, pressure-0.2), temperature, tankCapacityLiters) noiseKG := math.Max(defaultHydrogenNoiseKG, mass-pressureStepMass) noiseKG = math.Min(noiseKG, 1.0) refuelThresholdKG := math.Max(1.0, mass*0.05) fuelCellActive, fuelCellStateKnown := FuelCellActiveModeFromEnvelope(env) if !fuelCellStateKnown { if currentA, currentOK := firstHydrogenNumber(env.Fields, hydrogenCurrentFieldKeys); currentOK && currentA >= 0 { fuelCellActive = currentA > hydrogenActiveCurrentA fuelCellStateKnown = true } } mileageKM, mileageKnown := firstHydrogenNumber(env.Fields, []string{ envelope.FieldTotalMileageKM, "gb32960.vehicle.total_mileage_km", }) socPercent, socKnown := firstHydrogenNumber(env.Fields, []string{ envelope.FieldSOCPercent, "gb32960.vehicle.soc_percent", }) vehicleStateValue, vehicleStateKnown := firstHydrogenNumber(env.Fields, []string{ "vehicle_status", "gb32960.vehicle.vehicle_status", }) chargeStateValue, chargeStateKnown := firstHydrogenNumber(env.Fields, []string{ "charge_status", "gb32960.vehicle.charge_status", }) runningModeValue, runningModeKnown := firstHydrogenNumber(env.Fields, []string{ envelope.FieldVehicleRunningMode, "gb32960.vehicle.running_mode", }) fuelCellVoltage, voltageKnown := firstHydrogenNumber(env.Fields, []string{ "fuel_cell_voltage_v", "gb32960.fuel_cell.fuel_cell_voltage_v", }) fuelCellCurrent, currentKnown := firstHydrogenNumber(env.Fields, hydrogenCurrentFieldKeys) fuelCellPowerKnown := voltageKnown && currentKnown && fuelCellVoltage >= 0 && fuelCellCurrent >= 0 if loc == nil { loc = time.FixedZone("Asia/Shanghai", 8*3600) } eventMS, _, ok := envelope.NormalizedEventTimeMSWithReason(env) if !ok { return HydrogenStreamSample{}, "missing_event_time", false } eventTime := time.UnixMilli(eventMS).In(loc) if now.IsZero() { now = time.Now() } date := eventTime.Format("2006-01-02") if date != now.In(loc).Format("2006-01-02") { return HydrogenStreamSample{}, "not_current_date", false } return HydrogenStreamSample{ VIN: vin, Date: date, SourceEndpoint: strings.TrimSpace(env.SourceEndpoint), EventID: env.StableEventID(), EventTime: eventTime, MassKG: mass, TankCapacityLiters: tankCapacityLiters, PressureMPa: pressure, TemperatureC: temperature, NoiseKG: noiseKG, RefuelThresholdKG: refuelThresholdKG, FuelCellActive: fuelCellActive, FuelCellStateKnown: fuelCellStateKnown, ConsumptionEligible: !fuelCellStateKnown || fuelCellActive, MileageKM: mileageKM, MileageKnown: mileageKnown, SOCPercent: socPercent, SOCKnown: socKnown && socPercent >= 0 && socPercent <= 100, VehicleState: int(vehicleStateValue), VehicleStateKnown: vehicleStateKnown, ChargeState: int(chargeStateValue), ChargeStateKnown: chargeStateKnown, RunningMode: int(runningModeValue), RunningModeKnown: runningModeKnown, FuelCellPowerKW: fuelCellVoltage * fuelCellCurrent / 1000, FuelCellPowerKnown: fuelCellPowerKnown, }, "", true } func validHydrogenPressureTemperature(pressureMPa, temperatureC float64) bool { return pressureMPa > 0 && pressureMPa <= 70 && temperatureC > -40 && temperatureC <= 726.85 && !math.IsNaN(pressureMPa) && !math.IsInf(pressureMPa, 0) && !math.IsNaN(temperatureC) && !math.IsInf(temperatureC, 0) } func firstHydrogenNumber(fields map[string]any, keys []string) (float64, bool) { for _, key := range keys { if value, ok := telemetry.Number(fields, key); ok { return value, true } } return 0, false } func AppendHydrogenStream(ctx context.Context, exec Execer, env envelope.FrameEnvelope, loc *time.Location, now time.Time, tankCapacityLiters float64) (HydrogenStreamResult, error) { sample, reason, ok := HydrogenStreamSampleFromEnvelope(env, loc, now, tankCapacityLiters) if !ok { result := HydrogenStreamResult{} switch reason { case "unsupported_protocol", "missing_pressure", "missing_temperature": return result, nil case "not_current_date": result.NotCurrent = 1 default: result.Invalid = 1 } return result, nil } result := HydrogenStreamResult{Found: 1} beginner, ok := exec.(txBeginner) if !ok { return result, sql.ErrTxDone } tx, err := beginner.BeginTx(ctx, nil) if err != nil { return result, err } defer tx.Rollback() state, found, err := selectHydrogenSegmentStreamState(ctx, tx, sample.VIN, sample.Date) if err != nil { return result, err } if found { if !state.add(sample) { result.Duplicate = 1 return result, tx.Commit() } if err := updateHydrogenSegmentStreamState(ctx, tx, sample.VIN, sample.Date, state); err != nil { return result, err } } else { baseline, baselineFound, err := selectHydrogenStreamBaseline(ctx, tx, sample.VIN, sample.Date) if err != nil { return result, err } if !baselineFound { baseline = nil } state = newHydrogenSegmentStreamState(sample, baseline) if err := insertHydrogenSegmentStreamState(ctx, tx, sample.VIN, sample.Date, state); err != nil { return result, err } } if err := projectHydrogenSegmentStreamDaily(ctx, tx, sample.VIN, sample.Date, state); err != nil { return result, err } if err := tx.Commit(); err != nil { return result, err } result.Written = 1 return result, nil } func selectHydrogenSegmentStreamState(ctx context.Context, tx *sql.Tx, vin, date string) (hydrogenSegmentStreamState, bool, error) { var encoded []byte err := tx.QueryRowContext(ctx, selectHydrogenSegmentStreamStateSQL, vin, date).Scan(&encoded) if errors.Is(err, sql.ErrNoRows) { return hydrogenSegmentStreamState{}, false, nil } if err != nil { return hydrogenSegmentStreamState{}, false, err } var state hydrogenSegmentStreamState if err := json.Unmarshal(encoded, &state); err != nil { return hydrogenSegmentStreamState{}, false, err } return state, true, nil } func selectHydrogenStreamBaseline(ctx context.Context, tx *sql.Tx, vin, date string) (*hydrogenStreamBaseline, bool, error) { var baseline hydrogenStreamBaseline var firstMass, lastMass sql.NullFloat64 err := tx.QueryRowContext(ctx, selectHydrogenStreamBaselineSQL, vin, date).Scan( &baseline.SourceEndpoint, &baseline.ConsumptionKG, &firstMass, &lastMass, &baseline.SampleCount, &baseline.RefuelCount, &baseline.QualityStatus, ) if errors.Is(err, sql.ErrNoRows) { return nil, false, nil } if err != nil { return nil, false, err } baseline.FirstMassKG = -1 baseline.LastMassKG = -1 if firstMass.Valid { baseline.FirstMassKG = firstMass.Float64 } if lastMass.Valid { baseline.LastMassKG = lastMass.Float64 } return &baseline, true, nil } func insertHydrogenSegmentStreamState(ctx context.Context, tx *sql.Tx, vin, date string, state hydrogenSegmentStreamState) error { encoded, err := json.Marshal(state) if err != nil { return err } qualityStatus, qualityReason := state.quality() _, err = tx.ExecContext(ctx, insertHydrogenSegmentStreamStateSQL, vin, date, state.SourceEndpoint, state.FinalizedConsumptionKG, state.projectedConsumptionKG(), state.SampleCount, state.RefuelCount, state.AbnormalDropCount, state.EligibleIntervalCount, state.QualifiedSegmentCount, state.RemainingMassKG, state.LastEventTime, state.LastEventID, encoded, qualityStatus, qualityReason, ) return err } func updateHydrogenSegmentStreamState(ctx context.Context, tx *sql.Tx, vin, date string, state hydrogenSegmentStreamState) error { encoded, err := json.Marshal(state) if err != nil { return err } qualityStatus, qualityReason := state.quality() _, err = tx.ExecContext(ctx, updateHydrogenSegmentStreamStateSQL, state.SourceEndpoint, state.FinalizedConsumptionKG, state.projectedConsumptionKG(), state.SampleCount, state.RefuelCount, state.AbnormalDropCount, state.EligibleIntervalCount, state.QualifiedSegmentCount, state.RemainingMassKG, state.LastEventTime, state.LastEventID, encoded, qualityStatus, qualityReason, vin, date, ) return err } func projectHydrogenSegmentStreamDaily(ctx context.Context, tx *sql.Tx, vin, date string, state hydrogenSegmentStreamState) error { qualityStatus, qualityReason := state.quality() _, err := tx.ExecContext(ctx, projectHydrogenSegmentStreamDailySQL, vin, date, state.SourceEndpoint, state.projectedConsumptionKG(), state.FirstMassKG, state.RemainingMassKG, state.SampleCount, state.RefuelCount, qualityStatus, qualityReason, ) return err } const selectHydrogenSegmentStreamStateSQL = `SELECT state_json FROM vehicle_open_hydrogen_segment_stream_state WHERE vin=? AND stat_date=? FOR UPDATE` const selectHydrogenStreamBaselineSQL = `SELECT source_endpoint,consumption_kg,first_mass_kg,last_mass_kg, sample_count,refuel_count,quality_status FROM vehicle_open_daily_energy WHERE vin=? AND stat_date=? AND energy_type='HYDROGEN' LIMIT 1` const insertHydrogenSegmentStreamStateSQL = `INSERT INTO vehicle_open_hydrogen_segment_stream_state( vin,stat_date,source_endpoint,finalized_consumption_kg,projected_consumption_kg, sample_count,refuel_count,abnormal_drop_count,eligible_interval_count,qualified_segment_count, last_mass_kg,last_event_time,last_event_id,state_json,calculation_method,quality_status,quality_reason ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,'PRESSURE_NIST_VALID_BOUNDARY_CHARGE_CYCLE_V3_5',?,?)` const updateHydrogenSegmentStreamStateSQL = `UPDATE vehicle_open_hydrogen_segment_stream_state SET source_endpoint=?,finalized_consumption_kg=?,projected_consumption_kg=?,sample_count=?, refuel_count=?,abnormal_drop_count=?,eligible_interval_count=?,qualified_segment_count=?, last_mass_kg=?,last_event_time=?,last_event_id=?,state_json=?, calculation_method='PRESSURE_NIST_VALID_BOUNDARY_CHARGE_CYCLE_V3_5',quality_status=?,quality_reason=? WHERE vin=? AND stat_date=?` const projectHydrogenSegmentStreamDailySQL = `INSERT INTO vehicle_open_daily_energy( vin,stat_date,energy_type,source_endpoint,consumption_kg,unit,first_mass_kg,last_mass_kg, sample_count,refuel_count,quality_status,quality_reason,calculated_at ) VALUES(?,?,'HYDROGEN',?,?,'kg',?,?,?,?,?,?,NOW(3)) ON DUPLICATE KEY UPDATE source_endpoint=VALUES(source_endpoint),consumption_kg=VALUES(consumption_kg),unit='kg', first_mass_kg=VALUES(first_mass_kg),last_mass_kg=VALUES(last_mass_kg), sample_count=VALUES(sample_count),refuel_count=VALUES(refuel_count), quality_status=VALUES(quality_status),quality_reason=VALUES(quality_reason),calculated_at=VALUES(calculated_at)`