fix: consume per-entity Gen5W telemetry

This commit is contained in:
seo-amugae committed 2026-10-20 20:02:36 +09:00
1 parent 5f7f4397a0
commit 5d2a6b2e44
4 files changed
+146 -110

No files matched your search

+120 -84
View File
@@ -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(),