Files
lingniu-vehicle-ingest/go/vehicle-gateway/internal/stats/pure_hydrogen.go

148 lines
3.9 KiB
Go

package stats
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope"
)
const (
gb32960FuelCellEngineWorkMode = 2
yutongFuelCellRunningMode = 4
yutongFuelCellDrivingMode = 11
)
// PureHydrogenModeFromEnvelope returns whether hydrogen power participates in
// vehicle propulsion and whether the source value is understood.
//
// GB32960's standard vehicle running mode is authoritative when present:
// mode 1 is pure electric, mode 2 is hybrid, and mode 3 is fuel powered. Mode
// 2 is counted as hydrogen-participating mileage for the hydrogen fleet; mode
// 1 is excluded. Fuel, abnormal, and invalid values are left unclassified.
// Older frames without the standard field fall back to the fuel-cell extension
// engine_work_state. Yutong reports the equivalent state through
// TRIANGLE_STATE; production frames use 1 for inactive and 4/11 for active.
func PureHydrogenModeFromEnvelope(env envelope.FrameEnvelope) (active bool, known bool) {
if env.Protocol == envelope.ProtocolGB32960 {
if value, exists := env.Fields[envelope.FieldVehicleRunningMode]; exists {
mode, ok := integerMode(value)
if !ok {
return false, false
}
switch mode {
case 1:
return false, true
case 2:
return true, true
default:
return false, false
}
}
}
return FuelCellActiveModeFromEnvelope(env)
}
// FuelCellActiveModeFromEnvelope reads the fuel-cell subsystem work state.
// Hydrogen consumption segmentation uses this signal (and its current-based
// fallback) independently from the whole-vehicle running mode used by mileage.
func FuelCellActiveModeFromEnvelope(env envelope.FrameEnvelope) (active bool, known bool) {
value, exists := env.Fields[envelope.FieldFuelCellWorkMode]
if !exists {
return false, false
}
mode, ok := integerMode(value)
if !ok {
return false, false
}
switch env.Protocol {
case envelope.ProtocolGB32960:
switch mode {
case gb32960FuelCellEngineWorkMode:
return true, true
case 0, 1:
return false, true
default:
return false, false
}
case envelope.ProtocolYutongMQTT:
switch mode {
case yutongFuelCellRunningMode, yutongFuelCellDrivingMode:
return true, true
case 1:
return false, true
default:
return false, false
}
default:
return false, false
}
}
// PureHydrogenMileageDelta returns the odometer delta that can be attributed
// to hydrogen-participating operation. The current endpoint owns the interval,
// matching the daily rule: total mileage minus intervals ending in pure-electric
// mode. Missing or invalid current modes remain unclassified.
func PureHydrogenMileageDelta(
previousKM float64,
currentKM float64,
previousActive bool,
previousKnown bool,
currentActive bool,
currentKnown bool,
) float64 {
delta := currentKM - previousKM
if !currentKnown || !currentActive {
return 0
}
if delta < 0 || delta > maxSelectedDailyMileageKM {
return 0
}
return delta
}
func integerMode(value any) (int, bool) {
switch typed := value.(type) {
case json.Number:
parsed, err := strconv.Atoi(typed.String())
return parsed, err == nil
case string:
parsed, err := strconv.Atoi(strings.TrimSpace(typed))
return parsed, err == nil
case int:
return typed, true
case int8:
return int(typed), true
case int16:
return int(typed), true
case int32:
return int(typed), true
case int64:
return int(typed), true
case uint:
return int(typed), true
case uint8:
return int(typed), true
case uint16:
return int(typed), true
case uint32:
return int(typed), true
case uint64:
if typed > uint64(^uint(0)>>1) {
return 0, false
}
return int(typed), true
case float32:
parsed := int(typed)
return parsed, float32(parsed) == typed
case float64:
parsed := int(typed)
return parsed, float64(parsed) == typed
default:
parsed, err := strconv.Atoi(strings.TrimSpace(fmt.Sprint(value)))
return parsed, err == nil
}
}