diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index f320ef16..1b374a4f 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -28,3 +28,19 @@ jobs: -exec sh -c 'mv $1 $1.tmp; jq ".data|=sort_by(.feature)" --sort-keys $1.tmp > $1; rm $1.tmp' shell {} ";" - name: 🔍 Verify run: git diff --name-only --exit-code + + deprecations: + name: validate deprecation database + runs-on: ubuntu-latest + steps: + - name: ⤵️ Check out code from GitHub + uses: actions/checkout@v6.0.2 + - name: Set up Python + uses: actions/setup-python@v6.2.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Update deprecation database + run: python check_deprecations.py --update + - name: Verify no changes + run: git diff --exit-code + if: always() diff --git a/check_deprecations.py b/check_deprecations.py new file mode 100755 index 00000000..fe0e6935 --- /dev/null +++ b/check_deprecations.py @@ -0,0 +1,299 @@ +#!/usr/bin/env python3 +"""Check for deprecated Viessmann API features and maintain the deprecation database. + +Usage: + check_deprecations.py Report using the database + check_deprecations.py --update Rescan test data, update database + check_deprecations.py --update f.json Also ingest a fresh device dump + +The database (tests/deprecated_features.json) accumulates deprecation info from +test response files and fresh device dumps. Features deprecated in one API response +are deprecated everywhere, so we merge and propagate across all sources. + +Exit code 1 if deprecated features are used in code (outside the ignore list). +""" + +import argparse +import glob +import json +import re +import sys +from collections import OrderedDict +from datetime import date, datetime +from pathlib import Path + +ROOT = Path(__file__).parent +DB_PATH = ROOT / "tests" / "deprecated_features.json" +RESPONSE_DIR = ROOT / "tests" / "response" +PYVICARE_DIR = ROOT / "PyViCare" + + +def load_database(): + """Load the deprecation database, or return empty if it doesn't exist.""" + if DB_PATH.exists(): + with open(DB_PATH) as f: + data = json.load(f) + return data.get("features", {}) + return {} + + +def save_database(features): + """Save the deprecation database.""" + output = { + "_meta": { + "description": "Known deprecated Viessmann API features, merged from test data and device dumps.", + "updated": str(date.today()), + "feature_count": len(features), + }, + "features": OrderedDict(sorted(features.items())), + } + with open(DB_PATH, "w") as f: + json.dump(output, f, indent=2) + f.write("\n") + + +def scan_json_file(filepath, source_label): + """Extract deprecated features from a JSON file. Returns dict of feature -> info.""" + found = {} + with open(filepath) as f: + data = json.load(f) + + items = data.get("data", data) if isinstance(data, dict) else data + if not isinstance(items, list): + return found + + for item in items: + dep = item.get("deprecated") + if dep: + feat = item["feature"] + found[feat] = { + "removalDate": dep.get("removalDate", ""), + "info": dep.get("info", ""), + "source": source_label, + } + return found + + +def update_database(extra_files=None): + """Rescan test data and optional extra files, merge into database. Returns new features.""" + db = load_database() + db_snapshot = json.dumps(db, sort_keys=True) + new_features = {} + today = str(date.today()) + + def add_feature(feat, info, source_label): + if feat not in db: + db[feat] = { + "removalDate": info["removalDate"], + "info": info["info"], + "firstSeenIn": source_label, + "firstSeenOn": today, + "sources": [], + } + new_features[feat] = {**info, "source": source_label} + if source_label not in db[feat]["sources"]: + db[feat]["sources"].append(source_label) + + # Scan test response files + for filepath in sorted(glob.glob(f"{RESPONSE_DIR}/*.json")): + filename = Path(filepath).name + for feat, info in scan_json_file(filepath, filename).items(): + add_feature(feat, info, filename) + + # Scan extra dump files + for filepath in extra_files or []: + path = Path(filepath) + if not path.exists(): + print(f"Warning: {filepath} not found, skipping", file=sys.stderr) + continue + label = f"dump:{path.name}" + for feat, info in scan_json_file(filepath, label).items(): + add_feature(feat, info, label) + + if json.dumps(db, sort_keys=True) != db_snapshot: + save_database(db) + return new_features + + +def find_code_usage(): + """Find all feature paths referenced via getProperty() in PyViCare code.""" + usage = {} # feature pattern -> [files] + sources = {} # filename -> full source content + + for filepath in sorted(glob.glob(f"{PYVICARE_DIR}/**/*.py", recursive=True)): + filename = Path(filepath).name + with open(filepath) as f: + content = f.read() + sources[filename] = content + + for match in re.findall(r'getProperty\(\s*f?"([^"]+)"\s*\)', content): + normalized = re.sub(r"\{[^}]+\}", "*", match) + if normalized not in usage: + usage[normalized] = [] + usage[normalized].append(filename) + + return usage, sources + + +def feature_matches_code(feature, code_usage, sources): + """Check if a deprecated feature is used in code. + + For wildcard matches (e.g., heating.circuits.*.operating.programs.*), + verifies that the specific segment value appears as a string literal + in the source file to avoid false positives from dynamic iteration. + """ + matching_files = [] + for pattern, files in code_usage.items(): + if "*" in pattern: + regex = re.escape(pattern).replace(r"\*", r"([^.]+)") + m = re.fullmatch(regex, feature) + if m: + segments = m.groups() + all_confirmed = True + for seg in segments: + if seg.isdigit(): + continue + for f in files: + if f"'{seg}'" in sources.get(f, "") or f'"{seg}"' in sources.get(f, ""): + break + else: + all_confirmed = False + if all_confirmed: + matching_files.extend(files) + elif pattern == feature: + matching_files.extend(files) + return list(set(matching_files)) + + +def report(db): + """Print deprecation report and return exit code.""" + code_usage, sources = find_code_usage() + today = date.today() + + used_in_code = [] + past_due = [] + upcoming = [] + + for feature in sorted(db): + info = db[feature] + removal_str = info.get("removalDate", "") + replacement = info.get("info", "") + feature_sources = info.get("sources", []) + code_files = feature_matches_code(feature, code_usage, sources) + + try: + removal_date = datetime.strptime(removal_str, "%Y-%m-%d").date() + is_past_due = removal_date <= today + days = (removal_date - today).days + date_label = f"{removal_str} ({'PAST DUE' if is_past_due else f'{days} days left'})" + except (ValueError, TypeError): + removal_date = None + is_past_due = False + date_label = removal_str or "unknown" + + entry = { + "feature": feature, + "date_label": date_label, + "replacement": replacement, + "sources": feature_sources, + "code_files": code_files, + "is_past_due": is_past_due, + } + + if code_files: + used_in_code.append(entry) + elif is_past_due: + past_due.append(entry) + else: + upcoming.append(entry) + + def print_entry(entry): + print(f" {entry['feature']}") + print(f" Removal: {entry['date_label']}") + if entry["replacement"] and entry["replacement"] != "none": + print(f" Replaced by: {entry['replacement']}") + if entry.get("code_files"): + print(f" Used in code: {', '.join(entry['code_files'])}") + dump_sources = [s for s in entry.get("sources", []) if s.startswith("dump:")] + if dump_sources: + print(f" From dump: {', '.join(s.removeprefix('dump:') for s in dump_sources)}") + print() + + def collapse_entries(entries): + """Group entries that only differ by numeric indices (e.g. rooms.0, rooms.1).""" + groups = {} # pattern -> {entry (first), count} + for entry in entries: + pattern = re.sub(r"\b\d+\b", "*", entry["feature"]) + if pattern in groups: + groups[pattern]["count"] += 1 + else: + collapsed = dict(entry) + collapsed["feature"] = pattern + groups[pattern] = {"entry": collapsed, "count": 1} + result = [] + for pattern, group in groups.items(): + entry = group["entry"] + if group["count"] > 1: + entry["feature"] = f"{pattern} ({group['count']}x)" + result.append(entry) + return result + + if used_in_code: + print("=== WARNING: Deprecated features used in code ===\n") + for entry in collapse_entries(used_in_code): + print_entry(entry) + + if past_due: + print(f"=== Past removal date (not used in code): {len(past_due)} features ===\n") + for entry in collapse_entries(past_due): + print_entry(entry) + + if upcoming: + print("=== Upcoming deprecations ===\n") + for entry in collapse_entries(upcoming): + print_entry(entry) + + total = len(db) + in_code = len(used_in_code) + print("=== Summary ===") + print(f"{total} deprecated features in database") + print(f"{in_code} used in code {'(ACTION NEEDED)' if in_code else '(clean)'}") + print(f"{len(past_due)} past removal date") + print(f"{len(upcoming)} upcoming") + + return 1 if in_code else 0 + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--update", action="store_true", help="Rescan test data and update the database") + parser.add_argument("dumps", nargs="*", help="Additional dump files to ingest (implies --update)") + args = parser.parse_args() + + if args.dumps: + args.update = True + + if args.update: + new = update_database(args.dumps) + if new: + print(f"=== {len(new)} NEW deprecated features found ===\n") + for feat, info in sorted(new.items()): + print(f" {feat}") + print(f" Removal: {info['removalDate']}") + if info["info"] and info["info"] != "none": + print(f" Replaced by: {info['info']}") + print(f" Discovered in: {info['source']}") + print() + else: + print("Database up to date, no new deprecated features found.\n") + + db = load_database() + if not db: + print("No deprecation database found. Run with --update first.", file=sys.stderr) + sys.exit(1) + + sys.exit(report(db)) + + +if __name__ == "__main__": + main() diff --git a/tests/deprecated_features.json b/tests/deprecated_features.json new file mode 100644 index 00000000..139cf6c3 --- /dev/null +++ b/tests/deprecated_features.json @@ -0,0 +1,1052 @@ +{ + "_meta": { + "description": "Known deprecated Viessmann API features, merged from test data and device dumps.", + "updated": "2026-02-10", + "feature_count": 106 + }, + "features": { + "heating.buffer.hysteresis": { + "removalDate": "2024-09-15", + "info": "replaced by heating.bufferCylinder.hysteresis", + "firstSeenIn": "VitovalorPT2.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "VitovalorPT2.json" + ] + }, + "heating.buffer.sensors.temperature.main": { + "removalDate": "2024-09-15", + "info": "replaced by heating.bufferCylinder.sensors.temperature.main", + "firstSeenIn": "Vitocal111S.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitocal111S.json", + "Vitocal200S-with-Vitovent300W.json", + "Vitocal200S_AWB-M-E-AC-201.D10.json", + "Vitocal222S.json", + "Vitocal250A.json", + "Vitocal252.json", + "Vitocal333G-with-Vitovent300F.json", + "Vitodens200W_B2HF.json", + "VitovalorPT2.json", + "dump:device_0_features.json" + ] + }, + "heating.buffer.sensors.temperature.top": { + "removalDate": "2024-09-15", + "info": "replaced by heating.bufferCylinder.sensors.temperature.top", + "firstSeenIn": "Ecotronic.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Ecotronic.json", + "Vitocal111S.json", + "Vitocal200S-with-Vitovent300W.json", + "Vitocal200S_AWB-M-E-AC-201.D10.json", + "Vitocal333G-with-Vitovent300F.json", + "dump:device_0_features.json" + ] + }, + "heating.circuits.0.operating.programs.noDemand": { + "removalDate": "2024-09-15", + "info": "replaced by heating.circuits.N.operating.programs.reducedEnergySaving", + "firstSeenIn": "Vitodens050W.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitodens050W.json", + "Vitodens100W.json", + "Vitodens200W_B2HF.json", + "Vitodens_100_BHC_0421.json" + ] + }, + "heating.circuits.0.operating.programs.summerEco": { + "removalDate": "2024-09-15", + "info": "replaced by heating.circuits.N.operating.programs.reducedEnergySaving and heating.circuits.0.operating.programs.eco", + "firstSeenIn": "Vitocal252.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitocal252.json" + ] + }, + "heating.circuits.1.operating.programs.noDemand": { + "removalDate": "2024-09-15", + "info": "replaced by heating.circuits.N.operating.programs.reducedEnergySaving", + "firstSeenIn": "Vitodens100W.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitodens100W.json", + "Vitodens200W_B2HF.json", + "Vitodens_100_BHC_0421.json" + ] + }, + "heating.circuits.1.operating.programs.summerEco": { + "removalDate": "2024-09-15", + "info": "replaced by heating.circuits.N.operating.programs.reducedEnergySaving and heating.circuits.0.operating.programs.eco", + "firstSeenIn": "Vitocal252.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitocal252.json" + ] + }, + "heating.circuits.2.operating.programs.noDemand": { + "removalDate": "2024-09-15", + "info": "replaced by heating.circuits.N.operating.programs.reducedEnergySaving", + "firstSeenIn": "Vitodens200W_B2HF.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitodens200W_B2HF.json" + ] + }, + "heating.circuits.2.operating.programs.summerEco": { + "removalDate": "2024-09-15", + "info": "replaced by heating.circuits.N.operating.programs.reducedEnergySaving and heating.circuits.0.operating.programs.eco", + "firstSeenIn": "Vitocal252.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitocal252.json" + ] + }, + "heating.circuits.3.operating.programs.noDemand": { + "removalDate": "2024-09-15", + "info": "replaced by heating.circuits.N.operating.programs.reducedEnergySaving", + "firstSeenIn": "Vitodens200W_B2HF.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitodens200W_B2HF.json" + ] + }, + "heating.circuits.3.operating.programs.summerEco": { + "removalDate": "2024-09-15", + "info": "replaced by heating.circuits.N.operating.programs.reducedEnergySaving and heating.circuits.0.operating.programs.eco", + "firstSeenIn": "Vitocal252.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitocal252.json" + ] + }, + "heating.configuration.dhw.highDemand.threshold": { + "removalDate": "2024-09-15", + "info": "replaced by heating.dhw.configuration.highDemand.threshold", + "firstSeenIn": "Vitocal222S.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitocal222S.json", + "Vitocal250A.json" + ] + }, + "heating.configuration.dhw.highDemand.timeframe": { + "removalDate": "2024-09-15", + "info": "replaced by heating.dhw.configuration.highDemand.timeframe", + "firstSeenIn": "Vitocal222S.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitocal222S.json", + "Vitocal250A.json" + ] + }, + "heating.configuration.dhw.temperature.comfortCharging": { + "removalDate": "2024-09-15", + "info": "replaced by heating.dhw.configuration.temperature.comfortCharging", + "firstSeenIn": "Vitocal222S.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitocal222S.json", + "Vitocal250A.json" + ] + }, + "heating.configuration.dhw.temperature.dhwCylinder.max": { + "removalDate": "2024-09-15", + "info": "replaced by heating.dhw.configuration.temperature.dhwCylinder.max", + "firstSeenIn": "Vitocal111S.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitocal111S.json", + "Vitocal200S-with-Vitovent300W.json", + "Vitocal200S_AWB-M-E-AC-201.D10.json", + "Vitocal333G-with-Vitovent300F.json", + "dump:device_0_features.json" + ] + }, + "heating.configuration.dhw.temperature.hotWaterStorage.max": { + "removalDate": "2024-09-15", + "info": "replaced by heating.configuration.dhw.temperature.dhwCylinder.max", + "firstSeenIn": "Vitocal200S_AWB-M-E-AC-201.D10.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitocal200S_AWB-M-E-AC-201.D10.json", + "Vitocal333G-with-Vitovent300F.json", + "dump:device_0_features.json" + ] + }, + "heating.configuration.gasType": { + "removalDate": "2025-09-15", + "info": "replaced by heating.gas.configuration.type", + "firstSeenIn": "Vitodens100W.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitodens100W.json", + "Vitodens200W_B2HF.json" + ] + }, + "heating.configuration.houseLocation": { + "removalDate": "2025-03-15", + "info": "replaced by device.configuration.houseLocation", + "firstSeenIn": "Vitocal222S.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitocal222S.json", + "Vitocal250A.json", + "Vitodens100W.json", + "Vitodens200W_B2HF.json", + "VitovalorPT2.json" + ] + }, + "heating.cop.green": { + "removalDate": "2025-12-31", + "info": "replaced by heating.cop.photovoltaic", + "firstSeenIn": "dump:device_0_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_0_features.json" + ] + }, + "heating.device.variant": { + "removalDate": "2025-03-15", + "info": "replaced by device.variant", + "firstSeenIn": "Ecotronic.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Ecotronic.json", + "Vitocal222S.json", + "Vitocal250A.json", + "Vitopure350.json" + ] + }, + "heating.dhw.comfort": { + "removalDate": "2024-09-15", + "info": "replaced by heating.dhw.operating.modes.active / heating.dhw.operating.modes.eco / heating.dhw.operating.modes.comfort", + "firstSeenIn": "Vitodens050W.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitodens050W.json", + "Vitodens100W.json", + "Vitodens_100_BHC_0421.json" + ] + }, + "heating.dhw.sensors.temperature.hotWaterStorage": { + "removalDate": "2024-09-15", + "info": "replaced by heating.dhw.sensors.temperature.dhwCylinder", + "firstSeenIn": "Ecotronic.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Ecotronic.json", + "Vitocal111S.json", + "Vitocal200S-with-Vitovent300W.json", + "Vitocal200S_AWB-M-E-AC-201.D10.json", + "Vitocal222S.json", + "Vitocal250A.json", + "Vitocal252.json", + "Vitocal333G-with-Vitovent300F.json", + "Vitodens050W.json", + "Vitodens100NA.json", + "Vitodens100W.json", + "Vitodens200W_B2HF.json", + "Vitodens_100_BHC_0421.json", + "Vitoladens300-C_J3RA.json", + "VitovalorPT2.json", + "dump:device_0_features.json" + ] + }, + "heating.dhw.sensors.temperature.hotWaterStorage.bottom": { + "removalDate": "2024-09-15", + "info": "replaced by heating.dhw.sensors.temperature.dhwCylinder.bottom", + "firstSeenIn": "Vitocal111S.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitocal111S.json", + "Vitocal200S-with-Vitovent300W.json", + "Vitocal200S_AWB-M-E-AC-201.D10.json", + "Vitocal333G-with-Vitovent300F.json", + "VitovalorPT2.json", + "dump:device_0_features.json" + ] + }, + "heating.dhw.sensors.temperature.hotWaterStorage.midBottom": { + "removalDate": "2024-09-15", + "info": "replaced by heating.dhw.sensors.temperature.dhwCylinder.midBottom", + "firstSeenIn": "VitovalorPT2.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "VitovalorPT2.json" + ] + }, + "heating.dhw.sensors.temperature.hotWaterStorage.middle": { + "removalDate": "2024-09-15", + "info": "replaced by heating.dhw.sensors.temperature.dhwCylinder.middle", + "firstSeenIn": "Vitocal222S.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitocal222S.json", + "Vitocal250A.json", + "Vitocal252.json", + "VitovalorPT2.json" + ] + }, + "heating.dhw.sensors.temperature.hotWaterStorage.top": { + "removalDate": "2024-09-15", + "info": "replaced by heating.dhw.sensors.temperature.dhwCylinder.top", + "firstSeenIn": "Vitocal111S.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitocal111S.json", + "Vitocal200S-with-Vitovent300W.json", + "Vitocal200S_AWB-M-E-AC-201.D10.json", + "Vitocal222S.json", + "Vitocal250A.json", + "Vitocal252.json", + "Vitocal333G-with-Vitovent300F.json", + "VitovalorPT2.json", + "dump:device_0_features.json" + ] + }, + "heating.external.lock": { + "removalDate": "2024-09-15", + "info": "replaced by device.lock.external", + "firstSeenIn": "Vitocal222S.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitocal222S.json", + "Vitocal250A.json", + "VitovalorPT2.json" + ] + }, + "heating.fuelCell.electricalEnergyConsumption.value": { + "removalDate": "2024-09-15", + "info": "replaced by fuelCell.electricalEnergyConsumption.value", + "firstSeenIn": "VitovalorPT2.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "VitovalorPT2.json" + ] + }, + "heating.fuelCell.managers.energy": { + "removalDate": "2024-09-15", + "info": "replaced by fuelCell.managers.energy", + "firstSeenIn": "VitovalorPT2.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "VitovalorPT2.json" + ] + }, + "heating.fuelCell.managers.energy.prediction.power.consumption": { + "removalDate": "2024-09-15", + "info": "replaced by fuelCell.managers.energy.prediction.power.consumption", + "firstSeenIn": "VitovalorPT2.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "VitovalorPT2.json" + ] + }, + "heating.fuelCell.managers.energy.prediction.runtime": { + "removalDate": "2024-09-15", + "info": "replaced by fuelCell.managers.energy.prediction.runtime", + "firstSeenIn": "VitovalorPT2.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "VitovalorPT2.json" + ] + }, + "heating.fuelCell.managers.energy.timeTillNextStart": { + "removalDate": "2024-09-15", + "info": "replaced by fuelCell.managers.energy.timeTillNextStart", + "firstSeenIn": "VitovalorPT2.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "VitovalorPT2.json" + ] + }, + "heating.fuelCell.operating.phase": { + "removalDate": "2024-09-15", + "info": "replaced by fuelCell.operating.phase", + "firstSeenIn": "VitovalorPT2.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "VitovalorPT2.json" + ] + }, + "heating.fuelCell.prediction.heating.deficit": { + "removalDate": "2024-09-15", + "info": "replaced by fuelCell.prediction.deficit", + "firstSeenIn": "VitovalorPT2.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "VitovalorPT2.json" + ] + }, + "heating.fuelCell.sensors.temperature.return": { + "removalDate": "2024-09-15", + "info": "replaced by fuelCell.sensors.temperature.return", + "firstSeenIn": "VitovalorPT2.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "VitovalorPT2.json" + ] + }, + "heating.fuelCell.sensors.temperature.supply": { + "removalDate": "2024-09-15", + "info": "replaced by fuelCell.sensors.temperature.supply", + "firstSeenIn": "VitovalorPT2.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "VitovalorPT2.json" + ] + }, + "heating.fuelCell.statistics": { + "removalDate": "2024-09-15", + "info": "replaced by fuelCell.statistics", + "firstSeenIn": "VitovalorPT2.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "VitovalorPT2.json" + ] + }, + "heating.noise.reduction.operating.programs.active": { + "removalDate": "2024-09-15", + "info": "replaced by heating.noise.reduction.operating.state", + "firstSeenIn": "Vitocal222S.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitocal222S.json", + "Vitocal250A.json" + ] + }, + "heating.noise.reduction.operating.programs.maxReduced": { + "removalDate": "2024-09-15", + "info": "replaced by heating.noise.reduction.levels.maxReduced", + "firstSeenIn": "Vitocal222S.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitocal222S.json", + "Vitocal250A.json" + ] + }, + "heating.noise.reduction.operating.programs.notReduced": { + "removalDate": "2024-09-15", + "info": "replaced by heating.noise.reduction.levels.notReduced", + "firstSeenIn": "Vitocal222S.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitocal222S.json", + "Vitocal250A.json" + ] + }, + "heating.noise.reduction.operating.programs.slightlyReduced": { + "removalDate": "2024-09-15", + "info": "replaced by heating.noise.reduction.levels.slightlyReduced", + "firstSeenIn": "Vitocal222S.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitocal222S.json", + "Vitocal250A.json" + ] + }, + "heating.scop.dhw": { + "removalDate": "2024-09-15", + "info": "replaced by heating.spf.dhw", + "firstSeenIn": "Vitocal222S.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitocal222S.json", + "Vitocal250A.json" + ] + }, + "heating.scop.heating": { + "removalDate": "2024-09-15", + "info": "replaced by heating.spf.heating", + "firstSeenIn": "Vitocal222S.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitocal222S.json", + "Vitocal250A.json" + ] + }, + "heating.scop.total": { + "removalDate": "2024-09-15", + "info": "replaced by heating.spf.total", + "firstSeenIn": "Vitocal222S.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitocal222S.json", + "Vitocal250A.json" + ] + }, + "rooms.0.configuration.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.0.sensors.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.1.configuration.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.1.sensors.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.10.configuration.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.10.sensors.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.11.configuration.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.11.sensors.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.12.configuration.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.12.sensors.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.13.configuration.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.13.sensors.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.14.configuration.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.14.sensors.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.15.configuration.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.15.sensors.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.16.configuration.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.16.sensors.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.17.configuration.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.17.sensors.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.18.configuration.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.18.sensors.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.19.configuration.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.19.sensors.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.2.configuration.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.2.sensors.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.20.configuration.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.20.sensors.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.21.configuration.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.21.sensors.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.22.configuration.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.22.sensors.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.23.configuration.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.23.sensors.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.3.configuration.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.3.sensors.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.4.configuration.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.4.sensors.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.5.configuration.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.5.sensors.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.6.configuration.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.6.sensors.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.7.configuration.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.7.sensors.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.8.configuration.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.8.sensors.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.9.configuration.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.9.sensors.window.openState": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.features.supplyChannel0FTDCUseValveInformation": { + "removalDate": "2025-09-09", + "info": "replaced by rooms.features.supplyChannel0PumpControl", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.features.supplyChannel1FTDCUseValveInformation": { + "removalDate": "2025-09-09", + "info": "replaced by rooms.features.supplyChannel1PumpControl", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.features.supplyChannel2FTDCUseValveInformation": { + "removalDate": "2025-09-09", + "info": "replaced by rooms.features.supplyChannel2PumpControl", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "rooms.features.supplyChannel3FTDCUseValveInformation": { + "removalDate": "2025-09-09", + "info": "replaced by rooms.features.supplyChannel3PumpControl", + "firstSeenIn": "dump:device_RoomControl-1_features.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "dump:device_RoomControl-1_features.json" + ] + }, + "ventilation.operating.programs.comfort": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "Vitocal111S.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitocal111S.json", + "Vitocal200S-with-Vitovent300W.json" + ] + }, + "ventilation.operating.programs.eco": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "Vitocal111S.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitocal111S.json", + "Vitocal200S-with-Vitovent300W.json" + ] + }, + "ventilation.operating.programs.forcedLevelFour": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "VitoairFs300E.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "VitoairFs300E.json" + ] + }, + "ventilation.operating.programs.holiday": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "Vitocal111S.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitocal111S.json", + "Vitocal200S-with-Vitovent300W.json" + ] + }, + "ventilation.operating.programs.levelFour": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "VitoairFs300E.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "VitoairFs300E.json", + "Vitocal111S.json", + "Vitocal200S-with-Vitovent300W.json" + ] + }, + "ventilation.operating.programs.levelOne": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "VitoairFs300E.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "VitoairFs300E.json", + "Vitocal111S.json", + "Vitocal200S-with-Vitovent300W.json" + ] + }, + "ventilation.operating.programs.levelThree": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "VitoairFs300E.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "VitoairFs300E.json", + "Vitocal111S.json", + "Vitocal200S-with-Vitovent300W.json" + ] + }, + "ventilation.operating.programs.levelTwo": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "VitoairFs300E.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "VitoairFs300E.json", + "Vitocal111S.json", + "Vitocal200S-with-Vitovent300W.json" + ] + }, + "ventilation.operating.programs.silent": { + "removalDate": "2024-09-15", + "info": "none", + "firstSeenIn": "VitoairFs300E.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "VitoairFs300E.json" + ] + }, + "ventilation.sensors.airQuality": { + "removalDate": "2024-09-15", + "info": "replaced by ventilation.airQuality.abstract", + "firstSeenIn": "Vitopure350.json", + "firstSeenOn": "2026-02-10", + "sources": [ + "Vitopure350.json" + ] + } + } +} diff --git a/tests/test_TestForMissingProperties.py b/tests/test_TestForMissingProperties.py index 5f8a21b9..883dd2e9 100644 --- a/tests/test_TestForMissingProperties.py +++ b/tests/test_TestForMissingProperties.py @@ -1,3 +1,4 @@ +import json import re import unittest from os import listdir @@ -412,6 +413,18 @@ def read_all_deprecated_features(self): response_files = [f for f in listdir(response_path) if isfile(join(response_path, f))] all_features = {} + + # Load from deprecation database (maintained by check_deprecations.py) + db_path = join(dirname(__file__), 'deprecated_features.json') + if isfile(db_path): + with open(db_path) as f: + db = json.load(f) + for name, info in db.get('features', {}).items(): + normalized = re.sub(r"\b\d\b", "0", name) + if normalized not in all_features: + all_features[normalized] = {'files': info.get('sources', [])} + + # Also scan test response files directly (catches new deprecations not yet in db) for response in response_files: data = readJson(join(response_path, response)) if "data" in data: @@ -420,15 +433,8 @@ def read_all_deprecated_features(self): name = re.sub(r"\b\d\b", "0", feature["feature"]) if name not in all_features: all_features[name] = {'files': []} - all_features[name]['files'].append(response) - # name = re.sub(r"\b\d\b", "0", feature["feature"]) - # isDeprecated = feature["deprecated"] if "deprecated" in feature else None - # if name not in all_features: - # all_features[name] = {'files': []} - # if feature['isEnabled'] and feature['properties'] != {}: - # all_features[name]['files'].append(response) return all_features def read_all_features(self):