"""Load the irrigation model trained in session 16 and serve predictions (session 17).""" from datetime import datetime, timedelta import config import db import weather _model = {"bundle": None, "mtime": None} def pct_to_vwc(pct): """Capacitive sensor % -> volumetric water content using the 2-point calibration in config.""" return config.VWC_AT_0_PCT + (config.VWC_AT_100_PCT - config.VWC_AT_0_PCT) * (pct / 100.0) def _load(): if not config.MODEL_PATH.exists(): return None mtime = config.MODEL_PATH.stat().st_mtime if _model["mtime"] != mtime: # hot-reload when you retrain the model import joblib try: _model["bundle"] = joblib.load(config.MODEL_PATH) except Exception as exc: # e.g. trained with another scikit-learn version -> retrain (s16) print("model load failed, using the rule fallback:", exc) _model["bundle"] = None _model["mtime"] = mtime return _model["bundle"] def predict(node_id): latest = db.rows("SELECT * FROM readings WHERE node_id=? ORDER BY ts DESC LIMIT 1", (node_id,)) if not latest or latest[0]["soil_moisture_pct"] is None: return {"ok": False, "error": "no readings for this node yet"} r = latest[0] since = (datetime.fromisoformat(r["ts"]) - timedelta(hours=24)).isoformat(timespec="seconds") old = db.rows("SELECT soil_moisture_pct FROM readings WHERE node_id=? AND ts<=? AND soil_moisture_pct IS NOT NULL " "ORDER BY ts DESC LIMIT 1", (node_id, since)) w = weather.current() vwc = pct_to_vwc(r["soil_moisture_pct"]) features = { "soil_vwc": vwc, "vwc_drop_24h": (pct_to_vwc(old[0]["soil_moisture_pct"]) - vwc) if old else 0.0, "air_temp": r["air_temp_c"] if r["air_temp_c"] is not None else 25.0, "air_humidity": r["air_humidity_pct"] if r["air_humidity_pct"] is not None else 60.0, "soil_temp": w["soil_temp"], "et0_24h": w["et0_24h"], "rain_24h": w["rain_24h"], "radiation": w["radiation"], "wind": w["wind"], "hour": datetime.fromisoformat(r["ts"]).hour, } bundle = _load() if bundle is None: # graceful fallback: agronomic rule prob = 1.0 if vwc < 0.30 else 0.0 source = "rule (model file not found - run ai/s16_irrigation_classifier.py)" else: import pandas as pd X = pd.DataFrame([[features[f] for f in bundle["features"]]], columns=bundle["features"]) prob = float(bundle["model"].predict_proba(X)[0][1]) source = "ml" rec = "irrigate" if prob >= config.IRRIGATION_THRESHOLD else "wait" if w.get("rain_next_24h", 0) >= 5: rec, note = "wait", "rain expected in the next 24 h" else: note = "" # depth to refill the 40 cm root zone to field capacity, capped at one practical irrigation (40 mm) litres_m2 = round(min(40.0, max(0.0, (config.VWC_AT_100_PCT * 0.8 - vwc)) * 1000 * 0.40), 1) if rec == "irrigate" else 0 return {"ok": True, "node_id": node_id, "probability": round(prob, 3), "threshold": config.IRRIGATION_THRESHOLD, "recommendation": rec, "note": note, "source": source, "vwc": round(vwc, 3), "water_mm": litres_m2, "weather": w, "features": {k: round(v, 3) if isinstance(v, float) else v for k, v in features.items()}}