174 lines
4.6 KiB
Python
174 lines
4.6 KiB
Python
"""Vehicle state management."""
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import Dict, Optional, Any
|
|
from datetime import datetime
|
|
import threading
|
|
|
|
|
|
@dataclass
|
|
class PIDValue:
|
|
"""Single PID value with metadata.
|
|
|
|
Attributes:
|
|
pid_code: PID code
|
|
name: PID name
|
|
value: Current value
|
|
unit: Unit of measurement
|
|
timestamp: When value was last updated
|
|
"""
|
|
|
|
pid_code: int
|
|
name: str
|
|
value: float
|
|
unit: str
|
|
timestamp: datetime = field(default_factory=datetime.now)
|
|
|
|
def __str__(self) -> str:
|
|
return f"{self.name}: {self.value:.1f} {self.unit}"
|
|
|
|
def age_seconds(self) -> float:
|
|
"""Get age of value in seconds."""
|
|
return (datetime.now() - self.timestamp).total_seconds()
|
|
|
|
|
|
class VehicleState:
|
|
"""Thread-safe container for current vehicle state.
|
|
|
|
Stores latest values for all monitored PIDs with timestamps.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self._values: Dict[int, PIDValue] = {}
|
|
self._lock = threading.RLock()
|
|
self._callbacks: list = []
|
|
|
|
def update(
|
|
self,
|
|
pid_code: int,
|
|
name: str,
|
|
value: float,
|
|
unit: str,
|
|
) -> None:
|
|
"""Update a PID value.
|
|
|
|
Args:
|
|
pid_code: PID code
|
|
name: PID name
|
|
value: New value
|
|
unit: Unit of measurement
|
|
"""
|
|
with self._lock:
|
|
old_value = self._values.get(pid_code)
|
|
new_value = PIDValue(
|
|
pid_code=pid_code,
|
|
name=name,
|
|
value=value,
|
|
unit=unit,
|
|
)
|
|
self._values[pid_code] = new_value
|
|
|
|
for callback in self._callbacks:
|
|
try:
|
|
callback(new_value, old_value)
|
|
except Exception:
|
|
pass
|
|
|
|
def get(self, pid_code: int) -> Optional[PIDValue]:
|
|
"""Get current value for a PID.
|
|
|
|
Args:
|
|
pid_code: PID code
|
|
|
|
Returns:
|
|
PIDValue if available, None otherwise
|
|
"""
|
|
with self._lock:
|
|
return self._values.get(pid_code)
|
|
|
|
def get_all(self) -> Dict[int, PIDValue]:
|
|
"""Get all current values.
|
|
|
|
Returns:
|
|
Dictionary of PID code to PIDValue
|
|
"""
|
|
with self._lock:
|
|
return dict(self._values)
|
|
|
|
def clear(self) -> None:
|
|
"""Clear all stored values."""
|
|
with self._lock:
|
|
self._values.clear()
|
|
|
|
def add_callback(self, callback) -> None:
|
|
"""Add a callback for value updates.
|
|
|
|
Callback signature: callback(new_value: PIDValue, old_value: Optional[PIDValue])
|
|
"""
|
|
self._callbacks.append(callback)
|
|
|
|
def remove_callback(self, callback) -> None:
|
|
"""Remove a callback."""
|
|
if callback in self._callbacks:
|
|
self._callbacks.remove(callback)
|
|
|
|
@property
|
|
def rpm(self) -> Optional[float]:
|
|
"""Get current RPM value."""
|
|
val = self.get(0x0C)
|
|
return val.value if val else None
|
|
|
|
@property
|
|
def speed(self) -> Optional[float]:
|
|
"""Get current speed value (km/h)."""
|
|
val = self.get(0x0D)
|
|
return val.value if val else None
|
|
|
|
@property
|
|
def coolant_temp(self) -> Optional[float]:
|
|
"""Get current coolant temperature (°C)."""
|
|
val = self.get(0x05)
|
|
return val.value if val else None
|
|
|
|
@property
|
|
def throttle(self) -> Optional[float]:
|
|
"""Get current throttle position (%)."""
|
|
val = self.get(0x11)
|
|
return val.value if val else None
|
|
|
|
@property
|
|
def fuel_level(self) -> Optional[float]:
|
|
"""Get current fuel level (%)."""
|
|
val = self.get(0x2F)
|
|
return val.value if val else None
|
|
|
|
@property
|
|
def oil_temp(self) -> Optional[float]:
|
|
"""Get current oil temperature (°C)."""
|
|
val = self.get(0x5C)
|
|
return val.value if val else None
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""Convert state to dictionary."""
|
|
with self._lock:
|
|
return {
|
|
val.name: {
|
|
"value": val.value,
|
|
"unit": val.unit,
|
|
"timestamp": val.timestamp.isoformat(),
|
|
}
|
|
for val in self._values.values()
|
|
}
|
|
|
|
def __str__(self) -> str:
|
|
"""Format state as string."""
|
|
with self._lock:
|
|
if not self._values:
|
|
return "No data"
|
|
|
|
lines = []
|
|
for pid_code in sorted(self._values.keys()):
|
|
val = self._values[pid_code]
|
|
lines.append(f" {val}")
|
|
return "\n".join(lines)
|