"""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)