Skip to content

Commit d565979

Browse files
authored
feat: enhance vehicle discovery metrics and improve user feedback
1 parent c966f7a commit d565979

File tree

15 files changed

+268
-68
lines changed

15 files changed

+268
-68
lines changed

custom_components/ovms/config_flow/__init__.py

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,11 @@ async def async_step_topic_discovery(self, user_input=None):
320320

321321
topics_count = len(self.discovered_topics or [])
322322
metric_count = discovery_result.get("metric_count", topics_count)
323-
discovery_quality = discovery_result.get("discovery_quality", "unknown")
323+
vehicle_type = discovery_result.get("vehicle_type", "generic")
324+
vehicle_name = discovery_result.get("vehicle_name", "Generic OVMS")
325+
expected_count = discovery_result.get("expected_count", 196)
326+
discovery_percentage = discovery_result.get("discovery_percentage", 0)
327+
quality_indicator = discovery_result.get("quality_indicator", "❌")
324328

325329
# Filter to show only metric topics in sample
326330
metric_topics = [t for t in self.discovered_topics if "/metric/" in t]
@@ -334,11 +338,13 @@ async def async_step_topic_discovery(self, user_input=None):
334338
sample_topics = topics_sample + [""] * (5 - len(topics_sample))
335339

336340
_LOGGER.debug(
337-
"Discovered %d topics (%d metrics, quality: %s): %s",
341+
"Discovered %d topics (%d metrics). Vehicle: %s. Coverage: %d%% (%d/%d)",
338342
topics_count,
339343
metric_count,
340-
discovery_quality,
341-
topics_sample,
344+
vehicle_name,
345+
discovery_percentage,
346+
metric_count,
347+
expected_count,
342348
)
343349

344350
# Extract potential vehicle IDs from discovered topics
@@ -354,22 +360,10 @@ async def async_step_topic_discovery(self, user_input=None):
354360
)
355361

356362
self.debug_info["potential_vehicle_ids"] = list(potential_vehicle_ids)
357-
self.debug_info["discovery_quality"] = discovery_quality
363+
self.debug_info["vehicle_type"] = vehicle_type
364+
self.debug_info["discovery_percentage"] = discovery_percentage
358365
_LOGGER.debug("Potential vehicle IDs: %s", potential_vehicle_ids)
359366

360-
# Generate quality message for user feedback
361-
quality_messages = {
362-
"excellent": "✅ Excellent - Your OVMS module is responding well with full metrics.",
363-
"good": "✅ Good - Discovery completed successfully.",
364-
"partial": "⚠️ Partial - Some metrics received. Additional entities may appear when module publishes more data.",
365-
"minimal": "⚠️ Minimal - Very few metrics found. Ensure your OVMS module is online and publishing.",
366-
"none": "❌ No metrics found. Check that your OVMS module is connected and configured for MQTT.",
367-
"unknown": "Discovery completed.",
368-
}
369-
quality_message = quality_messages.get(
370-
discovery_quality, quality_messages["unknown"]
371-
)
372-
373367
# Create a schema without the confirmation checkbox
374368
data_schema = vol.Schema({})
375369

@@ -384,8 +378,10 @@ async def async_step_topic_discovery(self, user_input=None):
384378
description_placeholders={
385379
"topic_count": str(topics_count),
386380
"metric_count": str(metric_count),
387-
"discovery_quality": discovery_quality,
388-
"quality_message": quality_message,
381+
"expected_count": str(expected_count),
382+
"vehicle_name": vehicle_name,
383+
"discovery_percentage": str(discovery_percentage),
384+
"quality_indicator": quality_indicator,
389385
"sample_topic1": sample_topics[0],
390386
"sample_topic2": sample_topics[1],
391387
"sample_topic3": sample_topics[2],

custom_components/ovms/config_flow/topic_discovery.py

Lines changed: 164 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,137 @@
3939
METRIC_REQUEST_TOPIC_TEMPLATE,
4040
ACTIVE_DISCOVERY_TIMEOUT,
4141
LEGACY_DISCOVERY_TIMEOUT,
42-
MINIMUM_DISCOVERY_TOPICS,
43-
GOOD_DISCOVERY_TOPICS,
44-
EXCELLENT_DISCOVERY_TOPICS,
42+
MINIMUM_DISCOVERY_PERCENT,
43+
GOOD_DISCOVERY_PERCENT,
44+
GENERIC_VEHICLE_TYPE,
45+
GENERIC_VEHICLE_NAME,
4546
LOGGER_NAME,
4647
ERROR_CANNOT_CONNECT,
4748
ERROR_TIMEOUT,
4849
ERROR_UNKNOWN,
4950
)
51+
from ..metrics.vehicles import VEHICLE_TYPE_PREFIXES, VEHICLE_TYPE_NAMES
5052

5153
_LOGGER = logging.getLogger(LOGGER_NAME)
5254

5355

56+
def detect_vehicle_type(topics: Set[str]) -> tuple[str, str]:
57+
"""Detect vehicle type from discovered topics.
58+
59+
Looks for vehicle-specific metric prefixes (xvu., xse., xmg., xnl., xrt.)
60+
in the topic paths to determine the vehicle type.
61+
62+
Args:
63+
topics: Set of discovered MQTT topics
64+
65+
Returns:
66+
Tuple of (vehicle_type_id, vehicle_type_name)
67+
e.g., ("vw_eup", "VW e-UP!") or ("generic", "Generic OVMS")
68+
"""
69+
for topic in topics:
70+
# Extract metric path from topic (last part after /metric/)
71+
if "/metric/" in topic:
72+
metric_path = topic.split("/metric/")[-1]
73+
# Check for vehicle-specific prefixes
74+
for prefix, vehicle_type in VEHICLE_TYPE_PREFIXES.items():
75+
if metric_path.startswith(prefix):
76+
vehicle_name = VEHICLE_TYPE_NAMES.get(vehicle_type, vehicle_type)
77+
_LOGGER.debug(
78+
"Detected vehicle type '%s' (%s) from topic: %s",
79+
vehicle_type,
80+
vehicle_name,
81+
topic,
82+
)
83+
return vehicle_type, vehicle_name
84+
85+
return GENERIC_VEHICLE_TYPE, GENERIC_VEHICLE_NAME
86+
87+
88+
def get_expected_metric_count(vehicle_type: str) -> int:
89+
"""Get the expected number of metrics for a vehicle type.
90+
91+
Calculates expected metrics by counting defined metrics from the metrics module.
92+
This provides a dynamic count based on actual metric definitions.
93+
94+
Args:
95+
vehicle_type: Vehicle type identifier (e.g., "vw_eup", "generic")
96+
97+
Returns:
98+
Expected number of metrics for this vehicle type
99+
"""
100+
# Import here to avoid circular imports
101+
# pylint: disable=import-outside-toplevel
102+
from ..metrics import METRIC_DEFINITIONS
103+
104+
# Common categories that apply to all vehicles
105+
common_categories = [
106+
"battery",
107+
"charging",
108+
"climate",
109+
"door",
110+
"location",
111+
"motor",
112+
"trip",
113+
"device",
114+
"diagnostic",
115+
"power",
116+
"network",
117+
"system",
118+
"tire",
119+
]
120+
121+
# Count common metrics
122+
common_count = sum(
123+
1 for v in METRIC_DEFINITIONS.values() if v.get("category") in common_categories
124+
)
125+
126+
# Add vehicle-specific metrics if applicable
127+
if vehicle_type != "generic":
128+
vehicle_count = sum(
129+
1 for v in METRIC_DEFINITIONS.values() if v.get("category") == vehicle_type
130+
)
131+
total = common_count + vehicle_count
132+
_LOGGER.debug(
133+
"Expected metrics for %s: %d common + %d vehicle-specific = %d total",
134+
vehicle_type,
135+
common_count,
136+
vehicle_count,
137+
total,
138+
)
139+
return total
140+
141+
_LOGGER.debug("Expected metrics for generic vehicle: %d common", common_count)
142+
return common_count
143+
144+
145+
def calculate_discovery_percentage(
146+
metric_count: int, expected_count: int
147+
) -> tuple[int, str]:
148+
"""Calculate discovery percentage and quality indicator.
149+
150+
Args:
151+
metric_count: Number of metrics actually discovered
152+
expected_count: Expected number of metrics for this vehicle type
153+
154+
Returns:
155+
Tuple of (percentage, quality_indicator)
156+
Quality indicator is emoji: ✅ (>=70%), ⚠️ (>=30%), ❌ (<30%)
157+
"""
158+
if expected_count <= 0:
159+
return 0, "❌"
160+
161+
percentage = min(100, int((metric_count / expected_count) * 100))
162+
163+
if percentage >= 70:
164+
quality = "✅"
165+
elif percentage >= 30:
166+
quality = "⚠️"
167+
else:
168+
quality = "❌"
169+
170+
return percentage, quality
171+
172+
54173
def format_structure_prefix(config):
55174
"""Format the topic structure prefix based on configuration."""
56175
try:
@@ -380,13 +499,25 @@ def count_metric_topics(topics):
380499
# Count only metric topics, not /client/ echoes (Issue 1 fix)
381500
metric_topics_before = count_metric_topics(discovered_topics)
382501

383-
# Check if retained messages already gave us enough metrics
502+
# Get expected metrics for percentage calculation
503+
# We detect vehicle type early to calculate thresholds
504+
vehicle_type_early, _ = detect_vehicle_type(discovered_topics)
505+
expected_count_early = get_expected_metric_count(vehicle_type_early)
506+
retained_percentage = (
507+
int((metric_topics_before / expected_count_early) * 100)
508+
if expected_count_early > 0
509+
else 0
510+
)
511+
512+
# Check if retained messages already gave us enough metrics (percentage-based)
384513
# This happens when broker has retained messages from a running OVMS module
385-
if metric_topics_before >= GOOD_DISCOVERY_TOPICS:
514+
if retained_percentage >= GOOD_DISCOVERY_PERCENT:
386515
_LOGGER.info(
387-
"%s - Already received %d metric topics from retained messages, skipping active discovery",
516+
"%s - Already received %d metric topics (%d%%) from retained messages, "
517+
"skipping active discovery",
388518
log_prefix,
389519
metric_topics_before,
520+
retained_percentage,
390521
)
391522
active_discovery_succeeded = True
392523
debug_info["discovery_method"] = "retained"
@@ -501,59 +632,65 @@ def count_metric_topics(topics):
501632
except Exception as ex: # pylint: disable=broad-except
502633
_LOGGER.debug("%s - Error disconnecting: %s", log_prefix, ex)
503634

504-
# Return the results with quality assessment (Issue 2 & 3 fix)
635+
# Process discovery results
505636
topics_count = len(discovered_topics)
506637
metric_topics = [t for t in discovered_topics if "/metric/" in t]
507638
metric_count = len(metric_topics)
508639

509-
# Determine discovery quality for user feedback
510-
if metric_count >= EXCELLENT_DISCOVERY_TOPICS:
511-
discovery_quality = "excellent"
512-
elif metric_count >= GOOD_DISCOVERY_TOPICS:
513-
discovery_quality = "good"
514-
elif metric_count >= MINIMUM_DISCOVERY_TOPICS:
515-
discovery_quality = "partial"
516-
elif metric_count > 0:
517-
discovery_quality = "minimal"
518-
else:
519-
discovery_quality = "none"
640+
# Detect vehicle type and calculate expected metrics
641+
vehicle_type, vehicle_name = detect_vehicle_type(discovered_topics)
642+
expected_count = get_expected_metric_count(vehicle_type)
643+
discovery_percentage, quality_indicator = calculate_discovery_percentage(
644+
metric_count, expected_count
645+
)
520646

521647
debug_info["topics_count"] = topics_count
522648
debug_info["metric_count"] = metric_count
523-
debug_info["discovery_quality"] = discovery_quality
649+
debug_info["vehicle_type"] = vehicle_type
650+
debug_info["vehicle_name"] = vehicle_name
651+
debug_info["expected_count"] = expected_count
652+
debug_info["discovery_percentage"] = discovery_percentage
524653
debug_info["discovered_topics"] = (
525654
list(discovered_topics)
526655
if len(discovered_topics) < 50
527656
else list(discovered_topics)[:50]
528657
)
529658

530659
_LOGGER.debug(
531-
"%s - Discovery complete. Found %d topics (%d metric topics, quality: %s): %s",
660+
"%s - Discovery complete. Found %d topics (%d metric topics). "
661+
"Vehicle: %s. Coverage: %d%% (%d/%d expected)",
532662
log_prefix,
533663
topics_count,
534664
metric_count,
535-
discovery_quality,
536-
list(metric_topics)[:10],
665+
vehicle_name,
666+
discovery_percentage,
667+
metric_count,
668+
expected_count,
537669
)
538670

539-
# Add warning if few metric topics found
671+
# Build result
540672
result = {
541673
"success": True,
542674
"discovered_topics": discovered_topics,
543675
"topic_count": topics_count,
544676
"metric_count": metric_count,
545-
"discovery_quality": discovery_quality,
677+
"vehicle_type": vehicle_type,
678+
"vehicle_name": vehicle_name,
679+
"expected_count": expected_count,
680+
"discovery_percentage": discovery_percentage,
681+
"quality_indicator": quality_indicator,
546682
"debug_info": debug_info,
547683
}
548684

549-
if metric_count < MINIMUM_DISCOVERY_TOPICS:
685+
if discovery_percentage < MINIMUM_DISCOVERY_PERCENT:
550686
result["warning"] = "few_topics"
551687
_LOGGER.warning(
552-
"%s - Only %d metric topics found (minimum recommended: %d). "
688+
"%s - Only %d metric topics found (%d%%, minimum recommended: %d%%). "
553689
"Check that your OVMS module is online and publishing metrics.",
554690
log_prefix,
555691
metric_count,
556-
MINIMUM_DISCOVERY_TOPICS,
692+
discovery_percentage,
693+
MINIMUM_DISCOVERY_PERCENT,
557694
)
558695

559696
return result

custom_components/ovms/const.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -172,11 +172,15 @@
172172
ACTIVE_DISCOVERY_TIMEOUT = 10 # seconds to wait after requesting metrics
173173
LEGACY_DISCOVERY_TIMEOUT = 60 # fallback timeout for older firmware
174174

175-
# Discovery quality thresholds
176-
# Minimum metric topics needed for a "good" discovery (excludes /client/ topics)
177-
MINIMUM_DISCOVERY_TOPICS = 10 # Below this, show warning to user
178-
GOOD_DISCOVERY_TOPICS = 50 # Considered a good discovery
179-
EXCELLENT_DISCOVERY_TOPICS = 100 # Excellent discovery with most metrics
175+
# Discovery thresholds (percentage-based)
176+
# These are percentages of expected metrics for the detected vehicle type
177+
MINIMUM_DISCOVERY_PERCENT = 5 # Below this, show warning to user
178+
# Threshold for skipping active discovery when enough retained messages exist
179+
GOOD_DISCOVERY_PERCENT = 25 # Skip active discovery if this percentage already found
180+
181+
# Generic vehicle fallback (vehicle-specific values are in metrics/vehicles/)
182+
GENERIC_VEHICLE_TYPE = "generic"
183+
GENERIC_VEHICLE_NAME = "Generic OVMS"
180184

181185
# Logger
182186
LOGGER_NAME = "custom_components.ovms"

0 commit comments

Comments
 (0)