148 lines
3.9 KiB
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
|
|
}
|
|
}
|