Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions scripts/demo_dhw_day_boundary_fix.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
"""Test script to verify DHW day boundary fix."""

from datetime import datetime, timedelta
from custom_components.effektguard.const import DHW_NORMAL_RUNTIME_MINUTES
from custom_components.effektguard.optimization.dhw_optimizer import IntelligentDHWScheduler
Expand Down
127 changes: 65 additions & 62 deletions scripts/find_duplicate_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,24 @@ def get_project_root() -> Path:

def parse_constants(const_file: Path) -> dict[str, tuple[any, int]]:
"""Parse const.py and extract all constant definitions.

Returns dict of {constant_name: (value, line_number)}
"""
constants = {}

with open(const_file, "r") as f:
content = f.read()
lines = content.split("\n")

# Pattern for constants: NAME: Final = value
pattern = re.compile(r"^([A-Z][A-Z0-9_]*)\s*:\s*Final\s*=\s*(.+?)(?:\s*#.*)?$")

for line_num, line in enumerate(lines, 1):
match = pattern.match(line.strip())
if match:
name = match.group(1)
value_str = match.group(2).strip()

# Try to evaluate the value
try:
# Handle references to other constants
Expand All @@ -54,43 +54,43 @@ def parse_constants(const_file: Path) -> dict[str, tuple[any, int]]:
except (ValueError, SyntaxError):
# Keep as string if can't evaluate
value = value_str

constants[name] = (value, line_num)

return constants


def find_duplicate_values(constants: dict[str, tuple[any, int]]) -> dict[any, list[str]]:
"""Find constants with identical values."""
value_to_names = defaultdict(list)

for name, (value, _) in constants.items():
# Only check numeric values (most likely to be duplicated)
if isinstance(value, (int, float)):
value_to_names[value].append(name)

# Filter to only duplicates
return {v: names for v, names in value_to_names.items() if len(names) > 1}


def find_similar_names(constants: dict[str, tuple[any, int]]) -> list[tuple[str, str, float]]:
"""Find constants with similar names that might be duplicates."""
from difflib import SequenceMatcher

similar = []
names = list(constants.keys())

for i, name1 in enumerate(names):
for name2 in names[i+1:]:
for name2 in names[i + 1 :]:
# Skip if same prefix group (e.g., PROACTIVE_ZONE1 vs PROACTIVE_ZONE2)
# These are intentionally different
if _same_prefix_group(name1, name2):
continue

ratio = SequenceMatcher(None, name1, name2).ratio()
if ratio > 0.7: # 70% similar
similar.append((name1, name2, ratio))

return sorted(similar, key=lambda x: -x[2]) # Sort by similarity


Expand All @@ -100,34 +100,32 @@ def _same_prefix_group(name1: str, name2: str) -> bool:
pattern = re.compile(r"^(.+?)(\d+)(.*)$")
m1 = pattern.match(name1)
m2 = pattern.match(name2)

if m1 and m2:
# Same prefix and suffix, different number
if m1.group(1) == m2.group(1) and m1.group(3) == m2.group(3):
return True

return False


def find_unused_constants(
constants: dict[str, tuple[any, int]],
project_root: Path,
include_test_usage: bool = True
constants: dict[str, tuple[any, int]], project_root: Path, include_test_usage: bool = True
) -> list[tuple[str, int]]:
"""Find constants that are never imported in production code.

Args:
constants: Dict of constant names to (value, line_number)
project_root: Project root path
include_test_usage: If True, also check tests/scripts for usage
"""
unused = []

# Get all Python files in production code (not tests, not scripts)
prod_files = list((project_root / "custom_components" / "effektguard").rglob("*.py"))
const_file = project_root / "custom_components" / "effektguard" / "const.py"
prod_files = [f for f in prod_files if f != const_file]

# Read all production code
all_code = ""
for file in prod_files:
Expand All @@ -137,7 +135,7 @@ def find_unused_constants(
except Exception:
# Skip files that can't be read (permissions, encoding issues)
pass

# Optionally include tests and scripts
if include_test_usage:
test_files = list((project_root / "tests").rglob("*.py"))
Expand All @@ -149,7 +147,7 @@ def find_unused_constants(
except Exception:
# Skip files that can't be read (permissions, encoding issues)
pass

# Read const.py to check for building block usage (constants used to derive others)
const_code = ""
try:
Expand All @@ -158,7 +156,7 @@ def find_unused_constants(
except Exception:
# Skip if const.py can't be read
pass

# Check each constant
for name, (_, line_num) in constants.items():
# Skip configuration keys (CONF_*) - these are used dynamically
Expand All @@ -176,52 +174,52 @@ def find_unused_constants(
# Skip domain - always used
if name == "DOMAIN":
continue

pattern = re.compile(rf"\b{re.escape(name)}\b")

# Check if used in production/test code
if pattern.search(all_code):
continue

# Check if used as building block in const.py (more than just its definition)
# Count occurrences - if > 1, it's used somewhere else in const.py
matches = list(pattern.finditer(const_code))
if len(matches) > 1:
continue # Used as building block

unused.append((name, line_num))

return sorted(unused, key=lambda x: x[1]) # Sort by line number


def find_unused_imports(project_root: Path) -> list[tuple[Path, str, int]]:
"""Find unused imports across all Python files using ruff.

Returns list of (file_path, message, line_number) tuples.
"""
import subprocess

unused_imports = []

# Check production code and tests
dirs_to_check = [
project_root / "custom_components" / "effektguard",
project_root / "tests",
project_root / "scripts",
]

for check_dir in dirs_to_check:
if not check_dir.exists():
continue

try:
result = subprocess.run(
["ruff", "check", str(check_dir), "--select", "F401", "--output-format", "text"],
capture_output=True,
text=True,
cwd=project_root,
)

# Parse ruff output: file:line:col: F401 message
for line in result.stdout.strip().split("\n"):
if not line or "F401" not in line:
Expand All @@ -238,29 +236,29 @@ def find_unused_imports(project_root: Path) -> list[tuple[Path, str, int]]:
pass
except Exception:
pass

return unused_imports


def find_semantic_duplicates(constants: dict[str, tuple[any, int]]) -> list[tuple[str, str, str]]:
"""Find constants that might be semantically equivalent.

Looks for patterns like:
- EFFECT_MARGIN_WARNING vs EFFECT_PEAK_MARGIN_WARNING
- FOO_THRESHOLD vs FOO_LIMIT
"""
duplicates = []

# Group by base name patterns
patterns = [
(r"_THRESHOLD$", r"_LIMIT$"),
(r"_MAX$", r"_MAXIMUM$"),
(r"_MIN$", r"_MINIMUM$"),
(r"^EFFECT_", r"^EFFECT_PEAK_"),
]

names = list(constants.keys())

for name1 in names:
for pattern1, pattern2 in patterns:
if re.search(pattern1, name1):
Expand All @@ -277,36 +275,41 @@ def find_semantic_duplicates(constants: dict[str, tuple[any, int]]) -> list[tupl
duplicates.append((name1, name2, f"Same value: {val1}"))
elif isinstance(val1, (int, float)) and isinstance(val2, (int, float)):
if abs(val1 - val2) < 0.1: # Very similar values
duplicates.append((name1, name2, f"Similar values: {val1} vs {val2}"))

duplicates.append(
(name1, name2, f"Similar values: {val1} vs {val2}")
)

return duplicates


def main():
parser = argparse.ArgumentParser(description="Find duplicate and unused constants")
parser.add_argument("--remove-unused", action="store_true",
help="Show commands to remove unused constants")
parser.add_argument("--verbose", "-v", action="store_true",
help="Show detailed output")
parser.add_argument("--prod-only", action="store_true",
help="Only check production code (exclude tests/scripts)")
parser.add_argument(
"--remove-unused", action="store_true", help="Show commands to remove unused constants"
)
parser.add_argument("--verbose", "-v", action="store_true", help="Show detailed output")
parser.add_argument(
"--prod-only",
action="store_true",
help="Only check production code (exclude tests/scripts)",
)
args = parser.parse_args()

project_root = get_project_root()
const_file = project_root / "custom_components" / "effektguard" / "const.py"

if not const_file.exists():
print(f"Error: {const_file} not found")
sys.exit(1)

print("=" * 70)
print("CONSTANT ANALYSIS REPORT")
print("=" * 70)

# Parse constants
constants = parse_constants(const_file)
print(f"\nTotal constants defined: {len(constants)}")

# 1. Find duplicate values
print("\n" + "-" * 70)
print("1. DUPLICATE VALUES (same number, different names)")
Expand All @@ -325,7 +328,7 @@ def main():
print(f" - {name} (line {line})")
else:
print(" No non-trivial duplicate values found.")

# 2. Find similar names
print("\n" + "-" * 70)
print("2. SIMILAR NAMES (>70% string similarity)")
Expand All @@ -341,7 +344,7 @@ def main():
print(f" Values: {val1} vs {val2}")
else:
print(" No highly similar names found.")

# 3. Find semantic duplicates
print("\n" + "-" * 70)
print("3. SEMANTIC DUPLICATES (pattern matching)")
Expand All @@ -353,7 +356,7 @@ def main():
print(f" Reason: {reason}")
else:
print(" No semantic duplicates found.")

# 4. Find unused constants
print("\n" + "-" * 70)
print("4. UNUSED CONSTANTS (not imported in production code)")
Expand All @@ -366,14 +369,14 @@ def main():
for name, line in unused:
value = constants[name][0]
print(f" Line {line:4d}: {name} = {value}")

if args.remove_unused:
print("\n To remove these, delete the following lines from const.py:")
for name, line in unused:
print(f" Line {line}: {name}")
else:
print(" All constants are used!")

# 5. Find unused imports across all files
print("\n" + "-" * 70)
print("5. UNUSED IMPORTS (imported but never used in file)")
Expand All @@ -387,7 +390,7 @@ def main():
print("\n Fix with: ruff check --select F401 --fix .")
else:
print(" All imports are used!")

# Summary
print("\n" + "=" * 70)
print("SUMMARY")
Expand All @@ -398,15 +401,15 @@ def main():
print(f" Semantic duplicates: {len(semantic)}")
print(f" Unused constants: {len(unused)}")
print(f" Unused imports: {len(unused_imports)}")

has_issues = unused or unused_imports
if has_issues:
if unused:
print(f"\n ⚠️ {len(unused)} constants can be safely removed!")
if unused_imports:
print(f" ⚠️ {len(unused_imports)} unused imports found!")
return 1

return 0


Expand Down
6 changes: 3 additions & 3 deletions scripts/run_all_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,9 @@ if command -v black &> /dev/null; then
if black custom_components/effektguard/ --check --line-length 100 &> /dev/null; then
echo -e "${GREEN}✓ Black formatting: PASS${NC}"
else
echo -e "${YELLOW}⚠ Black formatting issues detected. Running black...${NC}"
black custom_components/effektguard/ --line-length 100
echo -e "${GREEN}✓ Black formatting: FIXED${NC}"
# A gate that silently rewrites the tree and reports success is not a gate.
echo -e "${RED}✗ Black formatting: FAIL - run: black custom_components/effektguard/ --line-length 100${NC}"
exit 1
fi
else
echo -e "${YELLOW}⚠ Black not installed, skipping formatting check${NC}"
Expand Down
Loading