# WashData - Home Assistant integration for appliance cycle monitoring via smart plugs. # Copyright (C) 2026 Lukas Bandura # SPDX-License-Identifier: AGPL-3.0-or-later # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . """Auto-generated by ml_washdata/wash_ml/promotion.py. Do not edit by hand. Embedded WashData model: 'hybrid_curve_quality' (target: 'problem_cycle'). Kind: standardized logistic regression. Runtime dependency: NumPy only. Regenerate with ``./ml.sh experiment`` in the ml_washdata lab and copy the new file. Determinism check at generation time: max_abs_score_diff=6.8331e-08 over 373 rows. Usage in the integration:: from .hybrid_curve_quality_model import score, predict, FEATURE_COLUMNS features = build_runtime_features(...) # must populate FEATURE_COLUMNS is_positive = predict(features) """ from __future__ import annotations import base64 import gzip import json from typing import Mapping import numpy as np MODEL_NAME = 'hybrid_curve_quality' MODEL_TARGET = 'problem_cycle' MODEL_KIND = 'standardized_logistic' TARGET_UNITS = '' # Training artifact for select_threshold only — NOT used at live inference. # The live quality-suspicious gate reads ML_QUALITY_SUSPICIOUS_THRESHOLD from const.py. THRESHOLD = 0.19 FEATURE_COLUMNS = [ 'duration_log_ratio', 'energy_log_ratio', 'peak_log_ratio', 'profile_distance', 'label_margin_positive', 'max_gap_ratio', 'low_power_gap_ratio', 'false_end_energy_ratio', 'sample_density_log', 'peak_density_log', 'local_spike_score', 'local_spike_rate', 'local_noise_score', 'leading_idle_ratio', 'trailing_idle_ratio', 'trimmed_duration_log_ratio', 'flag_pressure', 'shape_fit_penalty', 'shape_active_fraction', 'shape_early_energy_fraction', 'shape_late_energy_fraction', 'shape_mid_trough_depth', 'shape_peak_density', 'shape_max_step_drop', 'shape_max_step_rise', 'shape_active_cv', 'shape_autocorr_lag1', 'shape_derivative_sign_changes', 'shape_plateau_ratio', 'shape_tail_slope', 'has_trace', ] # Provenance (metrics at training time): MODEL_METRICS = json.loads("""{ "owner_holdout": { "accuracy": 0.872, "balanced_accuracy": 0.89997, "f1": 0.898734, "fn": 15, "fp": 1, "positive_rate": 0.576, "precision": 0.986111, "problem_recall": 0.825581, "rows": 125, "specificity": 0.974359, "tn": 38, "tp": 71 }, "synthetic_all": { "accuracy": 1.0, "balanced_accuracy": 0.5, "f1": 1.0, "fn": 0, "fp": 0, "positive_rate": 1.0, "precision": 1.0, "problem_recall": 1.0, "rows": 1255, "specificity": 0.0, "tn": 0, "tp": 1255 } }""") _MODEL_BLOB = ( 'H4sIAAAAAAACA3VW247bOAz9FcOvzaS6X7JPi6L7tBeg230qCkNjK4lR2/L60uls0X/fQzmTmem0yENkiiIPDymRX8vbNszlQe+V' '1txoo3dlHYclTuXhww3bM2GMsZbvaO0151Zua6mtMMLt2N5aJ6URBkshmJLcqh0pC6ad87SU0mqns5QLeIE5vudeSyNNPmaMwo+W' 'UljpLfebrpQqGxPeCuW1yFKulJN5mSFYTXg4Z5o5pXZib5y2XAkyYTkThI5MKCaga7FUWnLvLWHnkjvpHCl4ox3XnqTMCuGV4CTV' 'QltpCZqHKas84eFcGkjJhZMGMq/JLnBLZjJgphwXVmSqaO2yMY99ydVHcJziEQwTT84J5mmbawk4hkwxnJYe9NJ5w5UGYGKHG4Ms' '2Cx2nkGZkHEBsreAmBZcARwRLBzywTI9nkllPMvKiAO8kRfhPNg22YuwzIktSVwII9nmhHEH6s2WcoUAlATF0nBmrCLQSkoHpxek' 'xiuWQ2UMgGymUCKLcG22oiACsmkkCV5UzgKVhMxpktbCpdrKzXkOg7SWYFttYu457NqNL2etyim1wiKTlzIFPKcFsTzFsMSmCkt5' 'KAVq+YaZG+HfC3ZQ7qD4K8YOjJW78gi9dYpVnbq1H3AfPpTNOoWlTUPVpVOVl9CLQ5xO989EYwyfngumdGy7WDXtvIShjhB14TZ2' 'VR+mUztUY5rbpf1M8j58qU5hvB7t0h227+L0THoM3RyrODTVxf3Dxhz6kRzFARYzqgc8z0VdqkNXzWP7KVZznab4nQz2HkVDaucn' 'ajE07XCq2qaLV7/LFNruB9K270H2D4k7duFUjVOc5zXbnc9hjNWxXaoxDqFb7q+yUBM51XGiRRqu8him7v6BgRe7HUL46WbfNtUy' 'pfV0BjHjcr5uPOXqURtJmZc4Vs2UxpfSCfR8D7b+/ChZlwTuJiA68au0iVP7OWTduT0NVX0OwynOj0AIf1gfM5ulC2iu5i6N5PAc' 'ZgQRUE8o7BOIq1Pft1TYhungjIDOp3ZoIKC6a8LUtP8hHcgCCrGtqdwiclSjur+W6Q5cVefUNWldSBDqGnmr78sD3jQ8XOVt6Kh6' 'cXme7uAFsMgm3z6clQpfQ3ng6BvHEf8owEt9b2V1oJtu6FbEup0pK5B4Zzjn+arcdrGvsBe6LtsUGs/wrpzSHXCiW4CLESePbU05' 'orNWSe1RbrAkHf7h1fJv0LsflnNEpFW29TQkvmc/CUhvwWQFioNtYbAXYWSNJ0Fcvp/jz8Ir9JfY2QabbahJ5RuAD6GHg/J8fzuh' 'UIENTv9dQ7cV5ZCWmB+kN6m/bYfYFLkKbnKJFK+Ky3Pzuk7o2l+W4oLppr6vu1g0cYn1kqY9LL1bh6XtY9GvM9TSuFLRFb+9/fX9' 'P+/eVm/++v2fP/78uzhOqS86BF5kC68v9osmLKG4jUe8CwW9Drj/ZPQ93gIClYbiDrepoPq/ye/dXIzdOhfXtBSEcErd/EuxtKfz' 'EnGiXc7Fee3DcDPFz228g6HL2YundY7TzTHU8EaR9Yku9p6uAOp2XHELLsNKJvcim5GNh5Rd1ui0Gi3G0uzB9gb/uW9YJZzFLIN2' 'KRyXCk2StiVnaBa5J6NdcbHNI2jeArWXJxoMCyjVTdc6oahvOpzSioTU29CPbFbFWki3zTYavSkb0IxhmtC5+aEJW0vNDxJcKSGA' 'RqFlWp/nK8EdBpNsjNoaV1vvRjBOcDqGhu8xKG1LWKCeTy4waUlBUsOkMyoPMRqtmsamTRfDyzZ5YBPjk89LULKNTByTj9xGG8ZB' 'HQaAXZ4IMTF595G4Pcc+oHTvwnym+thvKcId61MTu9f0AC7oepGeqYfLksvqulGtQ7ugvkuSnNEg6E2idHL/7X9yriqGnQoAAA==' ) _MODEL_CACHE: dict | None = None def _load() -> dict: global _MODEL_CACHE if _MODEL_CACHE is None: payload = gzip.decompress(base64.b64decode(_MODEL_BLOB.encode("ascii"))) spec = json.loads(payload.decode("utf-8")) _MODEL_CACHE = { "center": np.asarray(spec["center"], dtype=float), "scale": np.asarray(spec["scale"], dtype=float), "coef": np.asarray(spec["coef"], dtype=float), "bias": float(spec["bias"]), "threshold": float(spec["threshold"]), "output_center": float(spec.get("output_center") or 0.0), "output_scale": float(spec.get("output_scale") if spec.get("output_scale") is not None else 1.0), "feature_columns": list(spec["feature_columns"]), } return _MODEL_CACHE def score(features: Mapping[str, float]) -> float: """Return the model probability in [0, 1] for one feature mapping.""" model = _load() vector = np.array( [float(features.get(column) or 0.0) for column in model["feature_columns"]], dtype=float, ) scaled = (vector - model["center"]) / model["scale"] logit = float(scaled @ model["coef"] + model["bias"]) logit = max(-60.0, min(60.0, logit)) return 1.0 / (1.0 + np.exp(-logit)) def predict(features: Mapping[str, float]) -> bool: """True when the example crosses the embedded decision threshold.""" return score(features) >= _load()["threshold"]