From abf7a33f37073d51a02c026c56636e5f6fab9476 Mon Sep 17 00:00:00 2001 From: Jan Seidl Date: Mon, 9 Mar 2026 07:59:45 +0000 Subject: [PATCH 1/2] add integration quality scale tracking --- quality_scale.yaml | 397 +++++++++++++++++++++++++++++++++++++++++ requirements-test.txt | 1 + scripts/quality-status | 378 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 776 insertions(+) create mode 100644 quality_scale.yaml create mode 100644 scripts/quality-status diff --git a/quality_scale.yaml b/quality_scale.yaml new file mode 100644 index 00000000..520c034f --- /dev/null +++ b/quality_scale.yaml @@ -0,0 +1,397 @@ +# Home Assistant Integration Quality Scale Tracking +# Magic Areas - https://github.com/jseidl/magic-areas +# Documentation: https://developers.home-assistant.io/docs/core/integration-quality-scale/ + +--- +# BRONZE TIER +# Minimum requirements for a production-ready integration +rules: + # Configuration & Setup + config-flow: + status: done + comment: | + Full UI-based configuration flow implemented in config_flow.py. + - Supports both regular areas and meta-areas (Global, Interior, Exterior, Floors) + - Uses data_description in translations for field context + - Proper use of ConfigEntry.data and ConfigEntry.options + Evidence: custom_components/magic_areas/config_flow.py + + config-flow-test-coverage: + status: done + comment: | + Comprehensive test coverage for config flow. + Tests located in tests/config_flow/ directory. + Evidence: tests/config_flow/ + + test-before-configure: + status: done + comment: | + Config flow validates area existence before configuration. + No external connection to test (local only integration). + Validates area objects and prevents invalid configurations. + Evidence: config_flow.py lines 170-177 (area validation) + + test-before-setup: + status: done + comment: | + Integration checks validity during initialization via async_migrate_entry. + Validates config version and prevents downgrade issues. + Evidence: __init__.py async_migrate_entry() function + + unique-config-entry: + status: done + comment: | + Uses async_set_unique_id(area_object.id) and _abort_if_unique_id_configured() + to prevent duplicate area configurations. + Evidence: config_flow.py lines 169-170 + + # Entity Requirements + entity-unique-id: + status: done + comment: | + All entities implement unique IDs via _generaete_unique_id() method. + Format: magicareas_feature_domain_areaname_name + Evidence: base/entities.py lines 64, 99-122 + + has-entity-name: + status: done + comment: | + All entities set _attr_has_entity_name = True + Evidence: base/entities.py line 26 + + runtime-data: + status: done + comment: | + Uses ConfigEntry.runtime_data for storing runtime data. + Evidence: __init__.py + + entity-event-setup: + status: done + comment: | + Entity events subscribed in correct lifecycle methods. + Listeners setup in async_added_to_hass() with proper cleanup. + Evidence: binary_sensor/presence.py async_added_to_hass() and async_on_remove() + + # Documentation + docs-high-level-description: + status: done + comment: | + Comprehensive high-level description in README.md and docs/source/index.md. + Clearly explains presence-aware automation and area intelligence. + Evidence: README.md, docs/source/index.md + + docs-installation-instructions: + status: done + comment: | + Step-by-step installation via HACS and manual installation provided. + Includes prerequisites and post-install setup. + Evidence: docs/source/how-to/installation.md + + docs-removal-instructions: + status: done + comment: | + Standard Home Assistant integration removal process applies. + Documented in installation guide. + Evidence: docs/source/how-to/installation.md + + # Code Organization + common-modules: + status: done + comment: | + Well-organized code structure with common patterns in: + - base/ (MagicArea, MagicEntity classes) + - helpers/ (area helpers, timers) + - const/ (modular constants by feature) + Evidence: Project structure + + brands: + status: done + comment: | + Branding assets available. + Integration visible in HACS with proper branding. + Evidence: HACS default repository listing + + # Service Actions (Exempt - No Custom Actions) + action-setup: + status: exempt + comment: | + Integration does not register custom service actions. + Only uses standard entity platform services (turn_on, turn_off, etc.) + + docs-actions: + status: exempt + comment: | + No custom service actions to document. + Standard entity services documented via entity translations. + + # Polling (Exempt - Event-Driven) + appropriate-polling: + status: exempt + comment: | + Integration is event-driven, not polling-based. + Reacts to state changes via event listeners. + Evidence: async_track_state_change_event throughout codebase + + # Dependencies (Exempt - No External Dependencies) + dependency-transparency: + status: exempt + comment: | + No external dependencies (manifest.json requirements: []) + Purely local calculation-based integration. + Evidence: manifest.json + +# SILVER TIER +# Enhanced reliability and maintainability + # Config Entry Management + config-entry-unloading: + status: done + comment: | + Full config entry unloading implemented in async_unload_entry. + Properly unloads platforms, removes listeners, and cleans up data. + Evidence: __init__.py async_unload_entry() + + # Entity Availability + entity-unavailable: + status: done + comment: | + Entities (groups/aggregates) follow standard Home Assistant group behavior. + Individual presence sensors handle unavailable states correctly. + Evidence: binary_sensor/presence.py lines 611-621 (skips unavailable sensors) + + # Integration Metadata + integration-owner: + status: done + comment: | + Integration owner defined in manifest.json: @jseidl + Evidence: manifest.json codeowners + + # Logging + log-when-unavailable: + status: done + comment: | + Logging for unavailable entities handled by Home Assistant core for groups. + Presence tracking logs sensor unavailability appropriately. + Evidence: binary_sensor/presence.py debug logging + + # Documentation + docs-configuration-parameters: + status: done + comment: | + All configuration parameters documented via translations with data_description. + Extensive parameter descriptions in en.json and 8 other languages. + Evidence: translations/en.json (80+ data_description entries) + + docs-installation-parameters: + status: done + comment: | + Installation parameters documented in installation guide and getting started. + UI configuration means most parameters self-documented. + Evidence: docs/source/how-to/installation.md, getting-started.md + + # Testing + test-coverage: + status: pending + comment: | + Current coverage: ~85% + Required for Silver tier: >95% + Need to increase coverage by ~10% across all modules. + Evidence: Current test coverage reports + + # Performance + parallel-updates: + status: pending + comment: | + PARALLEL_UPDATES not specified in platform files. + Should add to light.py, fan.py, sensor.py, etc. + Evidence: No PARALLEL_UPDATES found in platform files + + # Authentication (Exempt - No Auth) + reauthentication-flow: + status: exempt + comment: | + Integration does not require authentication. + Purely local, no cloud services or credentials. + + # Service Actions (Exempt) + action-exceptions: + status: exempt + comment: | + No custom service actions, therefore no action exceptions to handle. + +# GOLD TIER +# Enhanced user experience and comprehensive documentation + # Device Management + devices: + status: done + comment: | + Creates devices with DeviceInfo for all areas. + Proper identifiers, manufacturer, model, and translations. + Evidence: base/entities.py device_info property + + dynamic-devices: + status: done + comment: | + Areas can be dynamically added/removed via config flow. + Supports adding new areas without restart. + Evidence: Config flow supports iterative area addition + + stale-devices: + status: done + comment: | + Stale/removed entities cleaned up via cleanup_removed_entries() + Automatically removes entities when features disabled or entities removed. + Evidence: util.py cleanup_removed_entries() used in all platforms + + # Entity Features + entity-category: + status: done + comment: | + Control switches properly assigned EntityCategory.CONFIG + Evidence: switch/*.py _attr_entity_category = EntityCategory.CONFIG + + entity-device-class: + status: done + comment: | + Device classes used appropriately throughout: + - BinarySensorDeviceClass.OCCUPANCY for presence + - Sensor device classes for aggregates + - Cover, fan, climate device classes preserved + Evidence: Throughout codebase + + entity-translations: + status: done + comment: | + Full entity translation support with 9 languages: + de, en, es, fr, nl-NL, pl, pt-BR, sv, ta + Uses _attr_translation_key and translation_placeholders + Evidence: translations/*.json, base/entities.py + + entity-disabled-by-default: + status: done + comment: | + No diagnostic/noisy entities requiring default disabling. + Presence sensor attributes can be noisy but are essential for setup/debugging. + Design decision: Keep enabled for initial setup support. + + icon-translations: + status: pending + comment: | + Icon translations not yet implemented. + Currently uses static icons from feature_info. + Evidence: base/entities.py sets _attr_icon statically + + exception-translations: + status: pending + comment: | + Exception messages not yet translatable. + Config flow errors in translations but not runtime exceptions. + Evidence: No exception translation mechanism found + + # Documentation - Use Cases & Examples + docs-data-update: + status: done + comment: | + Presence tracking data update mechanism thoroughly documented. + Explains sensor polling, event-driven updates, and state transitions. + Evidence: docs/source/concepts/presence-sensing.md + + docs-examples: + status: done + comment: | + Extensive automation examples in implementation ideas library. + Room-by-room examples, energy aggregates, climate comfort, etc. + Evidence: docs/source/how-to/library/ + + docs-known-limitations: + status: done + comment: | + Known limitations documented in troubleshooting and feature docs. + Wasp-in-a-box requirements, aggregate prerequisites, etc. + Evidence: docs/source/how-to/troubleshooting.md, feature docs + + docs-supported-devices: + status: done + comment: | + Supported device types documented in presence sensing concepts. + Lists binary_sensor device classes, media players, device trackers, etc. + Evidence: docs/source/concepts/presence-sensing.md + + docs-supported-functions: + status: done + comment: | + All features comprehensively documented with supported functionality. + 13 features with dedicated documentation pages. + Evidence: docs/source/features/*.md (13 feature docs) + + docs-troubleshooting: + status: done + comment: | + Detailed troubleshooting guide with logging setup and common issues. + Evidence: docs/source/how-to/troubleshooting.md + + docs-use-cases: + status: done + comment: | + Comprehensive use case documentation with room-by-room implementation ideas. + Evidence: docs/source/how-to/library/implementation-ideas-for-every-room.md + + # Advanced Features + diagnostics: + status: pending + comment: | + Diagnostics integration not yet implemented. + Would be useful for debugging area state and configuration issues. + No async_get_config_entry_diagnostics found. + + reconfiguration-flow: + status: pending + comment: | + Reconfiguration flow not implemented. + Currently requires going through options flow or removing/re-adding. + No async_step_reconfigure found. + + repair-issues: + status: pending + comment: | + Repair issues not implemented. + Potential use case: Wasp-in-a-box requires aggregates feature. + May not be critical for this integration type. + No repair flows found. + + # Discovery (Exempt) + discovery: + status: exempt + comment: | + Integration does not implement discovery. + Areas are manually configured via UI - appropriate for this integration type. + + discovery-update-info: + status: exempt + comment: | + No discovery implemented, therefore no discovery info updates needed. + +# PLATINUM TIER +# Highest code quality standards + # Type Safety + strict-typing: + status: pending + comment: | + Integration uses mypy for type checking (configured in pyproject.toml and pre-commit). + However, missing TYPE_CHECKING pattern and from __future__ import annotations. + Need to add strict typing imports for full Platinum compliance. + Evidence: mypy in pre-commit, pyproject.toml; no TYPE_CHECKING imports found + + # Dependencies (Exempt) + async-dependency: + status: exempt + comment: | + No external dependencies to be async. + Integration is purely local with no external libraries. + Evidence: manifest.json requirements: [] + + inject-websession: + status: exempt + comment: | + No dependency libraries requiring websession injection. + No network calls or HTTP clients used. + Evidence: No external dependencies diff --git a/requirements-test.txt b/requirements-test.txt index 1493e773..cde1a076 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,3 +1,4 @@ -r requirements.txt pre-commit~=4.5 tox +PyYAML>=6.0 diff --git a/scripts/quality-status b/scripts/quality-status new file mode 100644 index 00000000..0667d898 --- /dev/null +++ b/scripts/quality-status @@ -0,0 +1,378 @@ +#!/usr/bin/env python3 +"""Quality Scale status checker for Magic Areas. + +Parses quality_scale.yaml and displays current tier status, +completion percentages, and pending items. +""" + +import argparse +import sys +from collections import defaultdict +from pathlib import Path + +# Try importing PyYAML +try: + import yaml +except ImportError: + print("Error: PyYAML not found.") + print("Please install test requirements:") + print(" pip install -r requirements-test.txt") + sys.exit(1) + +# ANSI color codes +COLORS = { + "green": "\033[92m", + "yellow": "\033[93m", + "red": "\033[91m", + "gray": "\033[90m", + "bold": "\033[1m", + "reset": "\033[0m", +} + +# Tier configuration +TIERS = ["BRONZE", "SILVER", "GOLD", "PLATINUM"] +TIER_EMOJI = { + "complete": "✅", + "in_progress": "🟡", + "not_started": "🔴", +} + + +def colorize(text, color="reset"): + """Add ANSI color codes to text.""" + return f"{COLORS.get(color, '')}{text}{COLORS['reset']}" + + +def load_quality_scale(yaml_path): + """Load and parse quality_scale.yaml.""" + with open(yaml_path, "r") as f: + data = yaml.safe_load(f) + return data.get("rules", {}) + + +def extract_tier_from_comments(file_path): + """Extract tier sections from YAML file by parsing comments.""" + tier_rules = defaultdict(list) + current_tier = None + + with open(file_path, "r") as f: + for line in f: + # Check for tier header comments + if "# BRONZE TIER" in line: + current_tier = "BRONZE" + elif "# SILVER TIER" in line: + current_tier = "SILVER" + elif "# GOLD TIER" in line: + current_tier = "GOLD" + elif "# PLATINUM TIER" in line: + current_tier = "PLATINUM" + # Extract rule names (indented with 2 spaces, followed by :) + elif ( + current_tier + and line.strip() + and not line.startswith("#") + and line.startswith(" ") + and not line.startswith(" ") + and ":" in line + ): + rule_name = line.strip().split(":")[0] + if rule_name and rule_name != "rules": + tier_rules[current_tier].append(rule_name) + + return tier_rules + + +def get_rule_summary(rule_name, rule_data): + """Extract first line of comment for display.""" + comment = rule_data.get("comment", "").strip() + if comment: + # Get first non-empty line + first_line = comment.split("\n")[0].strip() + return first_line + return "" + + +def calculate_tier_stats(tier_rules, all_rules): + """Calculate statistics for each tier.""" + stats = {} + + for tier, rules in tier_rules.items(): + done = 0 + pending = 0 + exempt = 0 + + for rule in rules: + if rule in all_rules: + status = all_rules[rule].get("status", "").lower() + if status == "done": + done += 1 + elif status == "exempt": + exempt += 1 + elif status == "pending": + pending += 1 + + total = len(rules) + # Completion is based on done + exempt (exempt rules don't block tier) + completion = ((done + exempt) / total * 100) if total > 0 else 0 + + # Determine tier status + if pending == 0: + tier_status = "complete" + elif done > 0 or exempt > 0: + tier_status = "in_progress" + else: + tier_status = "not_started" + + stats[tier] = { + "total": total, + "done": done, + "pending": pending, + "exempt": exempt, + "completion": completion, + "status": tier_status, + } + + return stats + + +def get_current_tier(tier_stats): + """Determine highest achieved tier (all rules done/exempt).""" + for tier in TIERS: + if tier in tier_stats: + if tier_stats[tier]["pending"] > 0: + # First tier with pending items, previous tier is achieved + tier_index = TIERS.index(tier) + if tier_index > 0: + return TIERS[tier_index - 1] + else: + return None # No tier achieved yet + # All tiers complete + return TIERS[-1] + + +def print_summary(tier_rules, all_rules, tier_stats, current_tier): + """Print tier summary.""" + print( + f"\n{colorize('🏆 Magic Areas - Integration Quality Scale Status', 'bold')}\n" + ) + print("━" * 60) + + for tier in TIERS: + if tier not in tier_stats: + continue + + stats = tier_stats[tier] + emoji = TIER_EMOJI[stats["status"]] + + status_text = stats["status"].replace("_", " ").upper() + completion_text = f"{stats['completion']:.1f}%" + + if stats["status"] == "complete": + color = "green" + elif stats["status"] == "in_progress": + color = "yellow" + else: + color = "red" + + tier_line = f"{emoji} {tier} TIER - {status_text} ({completion_text})" + print(colorize(tier_line, color)) + print( + f" {stats['done']}/{stats['total']} rules ({stats['done']} done, {stats['exempt']} exempt, {stats['pending']} pending)" + ) + + print("\n" + "━" * 60) + + # Current tier status + if current_tier: + print( + f"\n{colorize('Current Tier: ' + current_tier, 'green')} {TIER_EMOJI['complete']}" + ) + else: + print(f"\n{colorize('Current Tier: None (working toward Bronze)', 'yellow')}") + + # Next tier + if current_tier: + current_index = TIERS.index(current_tier) + if current_index < len(TIERS) - 1: + next_tier = TIERS[current_index + 1] + next_pending = tier_stats[next_tier]["pending"] + print(f"Next Tier: {next_tier} ({next_pending} items pending)") + else: + bronze_pending = tier_stats["BRONZE"]["pending"] + print(f"Next Tier: BRONZE ({bronze_pending} items pending)") + + +def print_pending_items(tier_rules, all_rules, tier_stats): + """Print pending items for each tier.""" + has_pending = any(stats["pending"] > 0 for stats in tier_stats.values()) + + if not has_pending: + print(f"\n{colorize('🎉 All tiers complete! No pending items.', 'green')}") + return + + print(f"\n{colorize('━━━━━━━━ PENDING ITEMS TO COMPLETE ━━━━━━━━', 'bold')}\n") + + for tier in TIERS: + if tier not in tier_stats or tier_stats[tier]["pending"] == 0: + continue + + print( + colorize(f"{tier} TIER ({tier_stats[tier]['pending']} pending):", "yellow") + ) + + for rule in tier_rules[tier]: + if ( + rule in all_rules + and all_rules[rule].get("status", "").lower() == "pending" + ): + summary = get_rule_summary(rule, all_rules[rule]) + print(f" • {colorize(rule, 'yellow')}") + if summary: + # Print first line of comment, truncated if too long + if len(summary) > 80: + summary = summary[:77] + "..." + print(f" {colorize(summary, 'gray')}") + + print() + + +def print_verbose(tier_rules, all_rules, tier_stats): + """Print all rules with their status.""" + print(f"\n{colorize('━━━━━━━━ ALL RULES ━━━━━━━━', 'bold')}\n") + + for tier in TIERS: + if tier not in tier_rules: + continue + + print(colorize(f"═══ {tier} TIER ═══", "bold")) + print() + + for rule in tier_rules[tier]: + if rule not in all_rules: + continue + + status = all_rules[rule].get("status", "").lower() + summary = get_rule_summary(rule, all_rules[rule]) + + if status == "done": + icon = "✅" + color = "green" + elif status == "exempt": + icon = "⊘" + color = "gray" + else: + icon = "⏸" + color = "yellow" + + print(f"{icon} {colorize(rule, color)} [{status.upper()}]") + if summary: + if len(summary) > 80: + summary = summary[:77] + "..." + print(f" {colorize(summary, 'gray')}") + + print() + + +def print_markdown(tier_rules, all_rules, tier_stats): + """Print markdown table output.""" + print("\n# Magic Areas - Quality Scale Status\n") + + # Summary table + print("| Tier | Status | Completion | Done | Exempt | Pending |") + print("|------|--------|------------|------|--------|---------|") + + for tier in TIERS: + if tier not in tier_stats: + continue + + stats = tier_stats[tier] + emoji = TIER_EMOJI[stats["status"]] + status = stats["status"].replace("_", " ").title() + + print( + f"| {emoji} **{tier}** | {status} | {stats['completion']:.1f}% | {stats['done']} | {stats['exempt']} | {stats['pending']} |" + ) + + # Pending items + print("\n## Pending Items\n") + + for tier in TIERS: + if tier not in tier_stats or tier_stats[tier]["pending"] == 0: + continue + + print(f"\n### {tier} Tier ({tier_stats[tier]['pending']} pending)\n") + + for rule in tier_rules[tier]: + if ( + rule in all_rules + and all_rules[rule].get("status", "").lower() == "pending" + ): + summary = get_rule_summary(rule, all_rules[rule]) + print(f"- **{rule}**") + if summary: + print(f" - {summary}") + + +def main(): + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Check Magic Areas integration quality scale status", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + "--verbose", + "-v", + action="store_true", + help="Show all rules including done and exempt", + ) + parser.add_argument( + "--markdown", "-m", action="store_true", help="Output in markdown format" + ) + + args = parser.parse_args() + + # Find quality_scale.yaml + script_dir = Path(__file__).parent + project_dir = script_dir.parent + yaml_path = project_dir / "quality_scale.yaml" + + if not yaml_path.exists(): + print(f"Error: quality_scale.yaml not found at {yaml_path}") + sys.exit(1) + + # Load data + all_rules = load_quality_scale(yaml_path) + tier_rules = extract_tier_from_comments(yaml_path) + tier_stats = calculate_tier_stats(tier_rules, all_rules) + current_tier = get_current_tier(tier_stats) + + # Print output + if args.markdown: + print_markdown(tier_rules, all_rules, tier_stats) + elif args.verbose: + print_summary(tier_rules, all_rules, tier_stats, current_tier) + print_verbose(tier_rules, all_rules, tier_stats) + else: + print_summary(tier_rules, all_rules, tier_stats, current_tier) + print_pending_items(tier_rules, all_rules, tier_stats) + + # Overall stats + if not args.markdown: + total_rules = sum(stats["total"] for stats in tier_stats.values()) + total_complete = sum( + stats["done"] + stats["exempt"] for stats in tier_stats.values() + ) + total_pending = sum(stats["pending"] for stats in tier_stats.values()) + overall_pct = (total_complete / total_rules * 100) if total_rules > 0 else 0 + + print("━" * 60) + print( + f"📊 OVERALL: {total_complete}/{total_rules} rules complete ({overall_pct:.1f}%)" + ) + if total_pending > 0: + print(f"🎯 {total_pending} items remaining across all tiers") + print() + + +if __name__ == "__main__": + main() From 21f23a777abd055a563646680543c17c49569196 Mon Sep 17 00:00:00 2001 From: Jan Seidl Date: Tue, 10 Mar 2026 07:14:39 +0000 Subject: [PATCH 2/2] config-flow-test-coverage is not done, but close --- quality_scale.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quality_scale.yaml b/quality_scale.yaml index 520c034f..d2409a7f 100644 --- a/quality_scale.yaml +++ b/quality_scale.yaml @@ -17,10 +17,11 @@ rules: Evidence: custom_components/magic_areas/config_flow.py config-flow-test-coverage: - status: done + status: pending comment: | Comprehensive test coverage for config flow. Tests located in tests/config_flow/ directory. + Currently at ~90% Evidence: tests/config_flow/ test-before-configure: