fix: consume per-entity Gen5W telemetry
This commit is contained in:
1 parent
5f7f4397a0
commit
5d2a6b2e44
4 files changed
+146
-110
No files matched your search
+120
-84
@@ -32,111 +32,133 @@ final class NiroTelemetrySnapshot
|
||||
public static function summarize(array $snapshot, array $config = []): array
|
||||
{
|
||||
$state = is_array($snapshot['state'] ?? null) ? $snapshot['state'] : [];
|
||||
$sampleEpoch = self::integer($state['_sample_epoch'] ?? 0);
|
||||
$sampleEpoch = self::integer($state['_sample_epoch'] ?? ($snapshot['received_at'] ?? 0));
|
||||
$age = $sampleEpoch > 0 ? max(0, time() - $sampleEpoch) : null;
|
||||
$staleAfter = max(10, (int)($config['stale_after'] ?? 20));
|
||||
$online = (string)($snapshot['availability'] ?? '') === 'online';
|
||||
$serviceTotal = 0;
|
||||
$serviceAvailable = 0;
|
||||
|
||||
foreach ($state as $key => $value) {
|
||||
if (!str_starts_with((string)$key, 'meta_') || !str_ends_with((string)$key, '_dumpsys_ok')) {
|
||||
continue;
|
||||
}
|
||||
$serviceGroups = [
|
||||
['audio_mode', 'rear_view', 'screen_on'],
|
||||
['speed', 'gear_code', 'gear_p_code'],
|
||||
['ignition', 'driver_door', 'engine_running'],
|
||||
['hev_mode_code', 'total_distance_to_empty', 'odometer'],
|
||||
['head_unit_power', 'av_disabled', 'display_off'],
|
||||
['n_framework_available', 'n_current_drive_mode_code'],
|
||||
['climate_on', 'driver_temperature', 'outside_temperature'],
|
||||
];
|
||||
foreach ($serviceGroups as $keys) {
|
||||
$serviceTotal++;
|
||||
if ((string)$value === 'true') {
|
||||
$serviceAvailable++;
|
||||
foreach ($keys as $key) {
|
||||
if (array_key_exists($key, $state)) {
|
||||
$serviceAvailable++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$keyMap = [
|
||||
'ignition' => 'system_root_ign',
|
||||
'reverse' => 'system_root_reverse',
|
||||
'parking' => 'system_root_parking',
|
||||
// The IndividualDriveMode block is a persisted/default structure on
|
||||
// V038. Its Vehicle Speed and Gear Status stay at 0/P while the car
|
||||
// is moving. The root fields are the live MICOM callbacks.
|
||||
'gear' => 'system_root_gear',
|
||||
'speed' => 'system_root_speed',
|
||||
'previous_speed' => 'system_root_prevspeed',
|
||||
'parking_brake' => 'system_root_mparkingbrakestatus',
|
||||
'camera' => 'system_individualdrivemode_fron_rear_camera_status',
|
||||
'drive_mode' => 'system_individualdrivemode_mcurrentdrivemode',
|
||||
'fuel_warning' => 'system_root_fuelwarning',
|
||||
'distance_to_empty' => 'hev_micom_hevstatus_mtotaldte',
|
||||
'battery_soc_12v' => 'system_root_mbatterysoc',
|
||||
'battery_above_60' => 'system_root_mbattery60above',
|
||||
'ldc_battery' => 'system_root_ldcbatterystate',
|
||||
'hev_mode' => 'hev_micom_hevstatus_mhevsystemmode',
|
||||
'hev_charge_state' => 'hev_micom_hevstatus_mchargestate',
|
||||
'motor_usage' => 'hev_micom_hevstatus_melectricmotorusage',
|
||||
'ldc_12v_charge' => 'hev_micom_hevstatus_cf_ldc_12vcharge',
|
||||
'odometer_raw' => 'hev_micom_hevstatus_odometer',
|
||||
'driver_unlock' => 'bcm_micom_bcmstatus_mdrvunlockstate',
|
||||
'passenger_unlock' => 'bcm_micom_bcmstatus_mpasunlockstate',
|
||||
'rear_left_unlock' => 'bcm_micom_bcmstatus_mrlunlockstate',
|
||||
'rear_right_unlock' => 'bcm_micom_bcmstatus_mrrunlockstate',
|
||||
'driver_door' => 'bcm_micom_bcmstatus_mdrvopenedstate',
|
||||
'passenger_door' => 'bcm_micom_bcmstatus_mpasopenedstate',
|
||||
'rear_left_door' => 'bcm_micom_bcmstatus_mrlopenedstate',
|
||||
'rear_right_door' => 'bcm_micom_bcmstatus_mrropenedstate',
|
||||
'trunk' => 'bcm_micom_bcmstatus_mtrunkopenedstate',
|
||||
'lock_state' => 'bcm_micom_bcmstatus_mlockstate',
|
||||
'hazard' => 'bcm_micom_bcmstatus_mlamphazard',
|
||||
'head_lamp' => 'bcm_micom_bcmstatus_mlamphead',
|
||||
'tail_lamp' => 'bcm_micom_bcmstatus_mlamptail',
|
||||
'fog_lamp' => 'bcm_micom_bcmstatus_mlampfog',
|
||||
'driver_seatbelt' => 'bcm_micom_bcmstatus_mdrvseatbelt',
|
||||
'passenger_seatbelt' => 'bcm_micom_bcmstatus_mpasseatbelt',
|
||||
'airbag' => 'bcm_micom_bcmstatus_mairbagstate',
|
||||
'climate_on' => 'datc_micom_datcstatus_mopstatus',
|
||||
'air_conditioner' => 'datc_micom_datcstatus_maconoff',
|
||||
'climate_auto' => 'datc_micom_datcstatus_mauto',
|
||||
'climate_dual' => 'datc_micom_datcstatus_mdual',
|
||||
'driver_temperature' => 'datc_micom_datcstatus_mdrtempc',
|
||||
'passenger_temperature' => 'datc_micom_datcstatus_mpstempc',
|
||||
'outside_temperature' => 'datc_micom_datcstatus_mambienttempc',
|
||||
'blower_level' => 'datc_micom_datcstatus_mmainblowerlevel',
|
||||
'intake_mode' => 'datc_micom_datcstatus_mintake',
|
||||
'co2_warning' => 'datc_micom_datcstatus_mco2warning',
|
||||
'route_guide' => 'cluster_tbt_info_operationstatus',
|
||||
'next_turn_distance' => 'cluster_tbt_subinfo_distance',
|
||||
'speed_limit' => 'cluster_slif_navi_info_speed_limit',
|
||||
'camera_distance' => 'cluster_scc_hu_p_info_rangecamdist',
|
||||
'camera_speed_limit' => 'cluster_scc_hu_p_info_rangecamspdlimit',
|
||||
'av_power' => 'power_root_av_power_status',
|
||||
'head_unit_power' => 'power_root_power_status',
|
||||
'audio_mode' => 'mode_root_current_mode_real',
|
||||
'audio_focus' => 'mode_root_modemanager_has_audiofocus',
|
||||
'audio_volume' => 'system_individualdrivemode_last_main_volume',
|
||||
'mute' => 'system_individualdrivemode_mute_status',
|
||||
'micom_connected' => 'system_root_micom_connected',
|
||||
'boot_completed' => 'system_root_boot_completed',
|
||||
'boot_sequence_finished' => 'system_root_bootsequence_finished',
|
||||
'wireless_charging' => 'system_root_wirelesscharing',
|
||||
'dtc_warning' => 'system_individualdrivemode_dtc_warning',
|
||||
'pdw_front_left' => 'system_individualdrivemode_pdw_fl',
|
||||
'pdw_front_right' => 'system_individualdrivemode_pdw_fr',
|
||||
'pdw_rear_left' => 'system_individualdrivemode_pdw_rl',
|
||||
'pdw_rear_right' => 'system_individualdrivemode_pdw_rr',
|
||||
'ignition' => ['ignition'],
|
||||
'reverse' => ['rear_view'],
|
||||
'parking' => ['parked', 'gear_p_code'],
|
||||
'gear' => ['gear_code', 'gear_p_code'],
|
||||
'speed' => ['speed'],
|
||||
'previous_speed' => [],
|
||||
'parking_brake' => ['parking_brake'],
|
||||
'camera' => ['rear_view'],
|
||||
'drive_mode' => ['drive_mode_code', 'n_current_drive_mode_code'],
|
||||
'fuel_warning' => ['fuel_warning'],
|
||||
'distance_to_empty' => ['total_distance_to_empty'],
|
||||
'battery_soc_12v' => ['battery_soc_12v'],
|
||||
'battery_above_60' => [],
|
||||
'ldc_battery' => ['ldc_12v_charge'],
|
||||
'hev_mode' => ['hev_mode_code'],
|
||||
'hev_charge_state' => ['hev_charge_state_code'],
|
||||
'motor_usage' => ['motor_usage_code'],
|
||||
'ldc_12v_charge' => ['ldc_12v_charge'],
|
||||
'odometer' => ['odometer'],
|
||||
'driver_unlock' => ['driver_unlocked'],
|
||||
'passenger_unlock' => ['passenger_unlocked'],
|
||||
'rear_left_unlock' => ['rear_left_unlocked'],
|
||||
'rear_right_unlock' => ['rear_right_unlocked'],
|
||||
'driver_door' => ['driver_door'],
|
||||
'passenger_door' => ['passenger_door'],
|
||||
'rear_left_door' => ['rear_left_door'],
|
||||
'rear_right_door' => ['rear_right_door'],
|
||||
'trunk' => ['trunk'],
|
||||
'lock_state' => [],
|
||||
'hazard' => ['hazard'],
|
||||
'head_lamp' => ['head_lamp'],
|
||||
'tail_lamp' => ['tail_lamp'],
|
||||
'fog_lamp' => ['fog_lamp'],
|
||||
'driver_seatbelt' => ['driver_seatbelt_code'],
|
||||
'passenger_seatbelt' => ['passenger_seatbelt_code'],
|
||||
'airbag' => ['airbag_code'],
|
||||
'climate_on' => ['climate_on'],
|
||||
'air_conditioner' => ['air_conditioner'],
|
||||
'climate_auto' => ['climate_auto'],
|
||||
'climate_dual' => ['climate_dual'],
|
||||
'driver_temperature' => ['driver_temperature'],
|
||||
'passenger_temperature' => ['passenger_temperature'],
|
||||
'outside_temperature' => ['outside_temperature'],
|
||||
'blower_level' => ['blower_level'],
|
||||
'intake_mode' => ['intake_code'],
|
||||
'co2_warning' => ['co2_warning_code'],
|
||||
'route_guide' => [],
|
||||
'next_turn_distance' => [],
|
||||
'speed_limit' => [],
|
||||
'camera_distance' => [],
|
||||
'camera_speed_limit' => [],
|
||||
'av_power' => ['av_power'],
|
||||
'head_unit_power' => ['head_unit_power'],
|
||||
'audio_mode' => ['audio_mode'],
|
||||
'audio_focus' => [],
|
||||
'audio_volume' => [],
|
||||
'mute' => [],
|
||||
'micom_connected' => ['n_framework_available'],
|
||||
'boot_completed' => ['head_unit_powered'],
|
||||
'boot_sequence_finished' => [],
|
||||
'wireless_charging' => ['wireless_charging_code'],
|
||||
'dtc_warning' => [],
|
||||
'pdw_front_left' => [],
|
||||
'pdw_front_right' => [],
|
||||
'pdw_rear_left' => [],
|
||||
'pdw_rear_right' => [],
|
||||
'n_framework_available' => ['n_framework_available'],
|
||||
'n_drive_mode_valid' => ['n_drive_mode_valid'],
|
||||
'n_current_drive_mode' => ['n_current_drive_mode_code'],
|
||||
'n_drive_mode_type' => ['n_drive_mode_type_code'],
|
||||
'n_asd_on' => ['n_asd_on'],
|
||||
'n_rev_indication' => ['n_rev_indication_code'],
|
||||
'n_drive_mode2_type' => ['n_drive_mode2_type_code'],
|
||||
'n_terrain_supported' => ['n_terrain_supported'],
|
||||
];
|
||||
|
||||
$metrics = [];
|
||||
foreach ($keyMap as $name => $key) {
|
||||
$value = array_key_exists($key, $state) ? (string)$state[$key] : null;
|
||||
foreach ($keyMap as $name => $keys) {
|
||||
$value = null;
|
||||
foreach ($keys as $key) {
|
||||
if (array_key_exists($key, $state)) {
|
||||
$value = (string)$state[$key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
$metrics[$name] = [
|
||||
'value' => $value,
|
||||
'supported' => $value !== null && !self::unsupported($value),
|
||||
];
|
||||
}
|
||||
$metricCount = count(array_filter($metrics, static fn(array $metric): bool => $metric['supported']));
|
||||
|
||||
return [
|
||||
'configured' => true,
|
||||
'available' => $state !== [],
|
||||
'online' => (string)($snapshot['availability'] ?? '') === 'online',
|
||||
'stale' => $age === null || $age > $staleAfter,
|
||||
'online' => $online,
|
||||
'stale' => !$online || $age === null || $age > $staleAfter,
|
||||
'age_seconds' => $age,
|
||||
'sample_epoch' => $sampleEpoch,
|
||||
'metric_count' => max(0, count($state) - (array_key_exists('_sample_epoch', $state) ? 1 : 0)),
|
||||
'metric_count' => $metricCount,
|
||||
'services' => [
|
||||
'available' => $serviceAvailable,
|
||||
'total' => $serviceTotal,
|
||||
@@ -211,6 +233,8 @@ final class NiroTelemetrySnapshot
|
||||
$port = max(1, min(65535, (int)($config['port'] ?? 1883)));
|
||||
$timeout = max(0.5, min(5.0, (float)($config['timeout'] ?? 2.0)));
|
||||
$stateTopic = (string)($config['state_topic'] ?? 'niro/gen5w/state');
|
||||
$statePrefix = rtrim($stateTopic, '/') . '/';
|
||||
$stateFilter = $statePrefix . '#';
|
||||
$availabilityTopic = (string)($config['availability_topic'] ?? 'niro/gen5w/status');
|
||||
$connection = self::openConnection($config, $host, $port, $timeout);
|
||||
$reader = $connection['reader'];
|
||||
@@ -233,13 +257,14 @@ final class NiroTelemetrySnapshot
|
||||
|
||||
$subscribe = pack('n', 1)
|
||||
. self::mqttString($stateTopic) . chr(0)
|
||||
. self::mqttString($stateFilter) . chr(0)
|
||||
. self::mqttString($availabilityTopic) . chr(0);
|
||||
self::writePacket($writer, 0x82, $subscribe);
|
||||
|
||||
$state = null;
|
||||
$state = [];
|
||||
$availability = null;
|
||||
$deadline = microtime(true) + $timeout;
|
||||
while (microtime(true) < $deadline && ($state === null || $availability === null)) {
|
||||
while (microtime(true) < $deadline && ($state === [] || $availability === null)) {
|
||||
$packet = self::readPacket($reader);
|
||||
if (($packet['type'] ?? 0) !== 3) {
|
||||
continue;
|
||||
@@ -251,14 +276,25 @@ final class NiroTelemetrySnapshot
|
||||
throw new RuntimeException('mqtt_state_not_object');
|
||||
}
|
||||
$state = $decoded;
|
||||
} elseif (str_starts_with($topic, $statePrefix)) {
|
||||
$entity = substr($topic, strlen($statePrefix));
|
||||
if (preg_match('/^[a-z0-9_]+$/', $entity) !== 1) {
|
||||
continue;
|
||||
}
|
||||
if ($body === '') {
|
||||
unset($state[$entity]);
|
||||
} else {
|
||||
$state[$entity] = trim($body);
|
||||
}
|
||||
} elseif ($topic === $availabilityTopic) {
|
||||
$availability = trim($body);
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_array($state)) {
|
||||
if ($state === []) {
|
||||
throw new RuntimeException('mqtt_state_missing');
|
||||
}
|
||||
$state['_sample_epoch'] = time();
|
||||
|
||||
return [
|
||||
'received_at' => time(),
|
||||
|
||||
Reference in new issue
Block a user