Eli Safra commited on
Commit
9fbf054
·
1 Parent(s): 271a242

Fix IMS weather staleness, add data freshness endpoint

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. backend/Dockerfile +28 -0
  2. backend/HF_README.md +26 -0
  3. backend/__pycache__/__init__.cpython-312.pyc +0 -0
  4. backend/__pycache__/__init__.cpython-39.pyc +0 -0
  5. backend/api/__pycache__/__init__.cpython-312.pyc +0 -0
  6. backend/api/__pycache__/__init__.cpython-39.pyc +0 -0
  7. backend/api/__pycache__/auth.cpython-312.pyc +0 -0
  8. backend/api/__pycache__/deps.cpython-312.pyc +0 -0
  9. backend/api/__pycache__/events.cpython-312.pyc +0 -0
  10. backend/api/__pycache__/events.cpython-39.pyc +0 -0
  11. backend/api/__pycache__/main.cpython-312.pyc +0 -0
  12. backend/api/main.py +12 -9
  13. backend/api/routes/__pycache__/__init__.cpython-312.pyc +0 -0
  14. backend/api/routes/__pycache__/__init__.cpython-39.pyc +0 -0
  15. backend/api/routes/__pycache__/biology.cpython-312.pyc +0 -0
  16. backend/api/routes/__pycache__/chatbot.cpython-312.pyc +0 -0
  17. backend/api/routes/__pycache__/control.cpython-312.pyc +0 -0
  18. backend/api/routes/__pycache__/energy.cpython-312.pyc +0 -0
  19. backend/api/routes/__pycache__/events.cpython-312.pyc +0 -0
  20. backend/api/routes/__pycache__/events.cpython-39.pyc +0 -0
  21. backend/api/routes/__pycache__/health.cpython-312.pyc +0 -0
  22. backend/api/routes/__pycache__/health.cpython-39.pyc +0 -0
  23. backend/api/routes/__pycache__/login.cpython-312.pyc +0 -0
  24. backend/api/routes/__pycache__/photosynthesis.cpython-312.pyc +0 -0
  25. backend/api/routes/__pycache__/sensors.cpython-312.pyc +0 -0
  26. backend/api/routes/__pycache__/sensors.cpython-39.pyc +0 -0
  27. backend/api/routes/__pycache__/weather.cpython-312.pyc +0 -0
  28. backend/api/routes/health.py +51 -3
  29. backend/requirements.txt +2 -2
  30. backend/workers/__pycache__/__init__.cpython-312.pyc +0 -0
  31. backend/workers/__pycache__/__init__.cpython-39.pyc +0 -0
  32. backend/workers/__pycache__/control_tick.cpython-312.pyc +0 -0
  33. backend/workers/__pycache__/daily_planner.cpython-312.pyc +0 -0
  34. backend/workers/__pycache__/daily_planner.cpython-39.pyc +0 -0
  35. config/__pycache__/settings.cpython-312.pyc +0 -0
  36. config/__pycache__/settings.cpython-39.pyc +0 -0
  37. requirements.txt +14 -6
  38. src/__pycache__/__init__.cpython-312.pyc +0 -0
  39. src/__pycache__/__init__.cpython-39.pyc +0 -0
  40. src/__pycache__/baseline_predictor.cpython-39.pyc +0 -0
  41. src/__pycache__/canopy_photosynthesis.cpython-312.pyc +0 -0
  42. src/__pycache__/canopy_photosynthesis.cpython-39.pyc +0 -0
  43. src/__pycache__/chronos_forecaster.cpython-312.pyc +0 -0
  44. src/__pycache__/chronos_forecaster.cpython-39.pyc +0 -0
  45. src/__pycache__/command_arbiter.cpython-312.pyc +0 -0
  46. src/__pycache__/command_arbiter.cpython-39.pyc +0 -0
  47. src/__pycache__/control_loop.cpython-312.pyc +0 -0
  48. src/__pycache__/control_loop.cpython-39.pyc +0 -0
  49. src/__pycache__/data_providers.cpython-312.pyc +0 -0
  50. src/__pycache__/data_providers.cpython-39.pyc +0 -0
backend/Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install dependencies first (layer caching)
6
+ COPY requirements.txt .
7
+ COPY backend/requirements.txt backend/
8
+ RUN pip install --no-cache-dir -r requirements.txt -r backend/requirements.txt
9
+
10
+ # Non-root user for security
11
+ RUN groupadd -r solarwine && useradd -r -g solarwine solarwine
12
+
13
+ # Copy application code and data cache
14
+ COPY src/ src/
15
+ COPY config/ config/
16
+ COPY backend/ backend/
17
+ COPY Data/ Data/
18
+
19
+ ENV PYTHONPATH=/app
20
+
21
+ # Switch to non-root
22
+ USER solarwine
23
+
24
+ # HuggingFace Spaces requires port 7860
25
+ EXPOSE 7860
26
+ HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
27
+ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:7860/api/health')" || exit 1
28
+ CMD ["uvicorn", "backend.api.main:app", "--host", "0.0.0.0", "--port", "7860"]
backend/HF_README.md ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: SolarWine API
3
+ emoji: 🌿
4
+ colorFrom: green
5
+ colorTo: yellow
6
+ sdk: docker
7
+ app_port: 7860
8
+ private: true
9
+ ---
10
+
11
+ # SolarWine API
12
+
13
+ FastAPI backend for the SolarWine agrivoltaic vineyard control system.
14
+
15
+ ## Endpoints
16
+
17
+ - `GET /api/health` — health check
18
+ - `GET /api/weather/current` — current weather (IMS station 43)
19
+ - `GET /api/sensors/snapshot` — vine sensor readings (ThingsBoard)
20
+ - `GET /api/energy/current` — current power output
21
+ - `GET /api/photosynthesis/current` — photosynthesis rate (FvCB/ML)
22
+ - `GET /api/control/status` — last control loop tick
23
+ - `POST /api/chatbot/message` — AI vineyard advisor
24
+ - `GET /api/biology/rules` — biology rules
25
+
26
+ Interactive docs at `/docs`.
backend/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (158 Bytes). View file
 
backend/__pycache__/__init__.cpython-39.pyc ADDED
Binary file (154 Bytes). View file
 
backend/api/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (162 Bytes). View file
 
backend/api/__pycache__/__init__.cpython-39.pyc ADDED
Binary file (158 Bytes). View file
 
backend/api/__pycache__/auth.cpython-312.pyc ADDED
Binary file (3.98 kB). View file
 
backend/api/__pycache__/deps.cpython-312.pyc ADDED
Binary file (928 Bytes). View file
 
backend/api/__pycache__/events.cpython-312.pyc ADDED
Binary file (3.25 kB). View file
 
backend/api/__pycache__/events.cpython-39.pyc ADDED
Binary file (1.93 kB). View file
 
backend/api/__pycache__/main.cpython-312.pyc ADDED
Binary file (12.6 kB). View file
 
backend/api/main.py CHANGED
@@ -74,12 +74,12 @@ def _check_data_paths():
74
  log.warning("Missing %s — %s", rel_path, msg)
75
 
76
 
77
- async def _ims_refresh_loop(interval_sec: int = 6 * 3600):
78
- """Background loop: refresh the IMS weather cache every `interval_sec` seconds.
79
 
80
- Fetches the last 14 days of IMS data, overwrites the local CSV cache,
81
- and invalidates the in-memory WeatherService cache so the next request
82
- picks up fresh data.
83
  """
84
  import asyncio
85
  from backend.api.events import event_bus
@@ -89,17 +89,19 @@ async def _ims_refresh_loop(interval_sec: int = 6 * 3600):
89
  from datetime import date, timedelta
90
  from src.data.ims_client import IMSClient
91
  end = date.today()
92
- start = end - timedelta(days=14)
93
  client = IMSClient()
94
  loop = asyncio.get_event_loop()
95
  df = await loop.run_in_executor(
96
  None,
97
  lambda: client.fetch_and_cache(
98
- str(start), str(end), chunk_days=14
99
  ),
100
  )
101
  rows = len(df) if df is not None and not df.empty else 0
102
  log.info("IMS cache refreshed: %d rows (%s → %s)", rows, start, end)
 
 
103
  # Invalidate WeatherService in-memory cache so next request uses fresh data
104
  try:
105
  from backend.api.deps import get_datahub
@@ -107,9 +109,10 @@ async def _ims_refresh_loop(interval_sec: int = 6 * 3600):
107
  hub.weather._df_cache._store.clear()
108
  except Exception:
109
  pass
110
- await event_bus.notify("weather")
 
111
  except Exception as exc:
112
- log.error("IMS refresh failed: %s", exc)
113
  await asyncio.sleep(interval_sec)
114
 
115
 
 
74
  log.warning("Missing %s — %s", rel_path, msg)
75
 
76
 
77
+ async def _ims_refresh_loop(interval_sec: int = 2 * 3600):
78
+ """Background loop: refresh the IMS weather cache every 2 hours.
79
 
80
+ Fetches the last 7 days of IMS data in 3-day chunks (IMS API is
81
+ unreliable with large ranges), overwrites the local CSV cache,
82
+ and invalidates the in-memory WeatherService cache.
83
  """
84
  import asyncio
85
  from backend.api.events import event_bus
 
89
  from datetime import date, timedelta
90
  from src.data.ims_client import IMSClient
91
  end = date.today()
92
+ start = end - timedelta(days=7)
93
  client = IMSClient()
94
  loop = asyncio.get_event_loop()
95
  df = await loop.run_in_executor(
96
  None,
97
  lambda: client.fetch_and_cache(
98
+ str(start), str(end), chunk_days=3
99
  ),
100
  )
101
  rows = len(df) if df is not None and not df.empty else 0
102
  log.info("IMS cache refreshed: %d rows (%s → %s)", rows, start, end)
103
+ if rows == 0:
104
+ log.warning("IMS refresh returned 0 rows — API may be down")
105
  # Invalidate WeatherService in-memory cache so next request uses fresh data
106
  try:
107
  from backend.api.deps import get_datahub
 
109
  hub.weather._df_cache._store.clear()
110
  except Exception:
111
  pass
112
+ if rows > 0:
113
+ await event_bus.notify("weather")
114
  except Exception as exc:
115
+ log.error("IMS refresh failed: %s", exc, exc_info=True)
116
  await asyncio.sleep(interval_sec)
117
 
118
 
backend/api/routes/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (169 Bytes). View file
 
backend/api/routes/__pycache__/__init__.cpython-39.pyc ADDED
Binary file (165 Bytes). View file
 
backend/api/routes/__pycache__/biology.cpython-312.pyc ADDED
Binary file (2.95 kB). View file
 
backend/api/routes/__pycache__/chatbot.cpython-312.pyc ADDED
Binary file (5.88 kB). View file
 
backend/api/routes/__pycache__/control.cpython-312.pyc ADDED
Binary file (3.51 kB). View file
 
backend/api/routes/__pycache__/energy.cpython-312.pyc ADDED
Binary file (3.52 kB). View file
 
backend/api/routes/__pycache__/events.cpython-312.pyc ADDED
Binary file (2.26 kB). View file
 
backend/api/routes/__pycache__/events.cpython-39.pyc ADDED
Binary file (1.56 kB). View file
 
backend/api/routes/__pycache__/health.cpython-312.pyc ADDED
Binary file (2.39 kB). View file
 
backend/api/routes/__pycache__/health.cpython-39.pyc ADDED
Binary file (1.56 kB). View file
 
backend/api/routes/__pycache__/login.cpython-312.pyc ADDED
Binary file (2.78 kB). View file
 
backend/api/routes/__pycache__/photosynthesis.cpython-312.pyc ADDED
Binary file (3.33 kB). View file
 
backend/api/routes/__pycache__/sensors.cpython-312.pyc ADDED
Binary file (7.37 kB). View file
 
backend/api/routes/__pycache__/sensors.cpython-39.pyc ADDED
Binary file (1.63 kB). View file
 
backend/api/routes/__pycache__/weather.cpython-312.pyc ADDED
Binary file (4.06 kB). View file
 
backend/api/routes/health.py CHANGED
@@ -1,13 +1,14 @@
1
- """Health check endpoint."""
2
 
3
  from __future__ import annotations
4
 
5
  import asyncio
6
  import os
7
 
8
- from fastapi import APIRouter
9
 
10
- from backend.api.deps import get_redis_client
 
11
 
12
  router = APIRouter()
13
 
@@ -47,3 +48,50 @@ async def health():
47
  "ims_configured": bool(os.environ.get("IMS_API_TOKEN")),
48
  "gemini_configured": bool(os.environ.get("GOOGLE_API_KEY")),
49
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Health check endpoints."""
2
 
3
  from __future__ import annotations
4
 
5
  import asyncio
6
  import os
7
 
8
+ from fastapi import APIRouter, Depends
9
 
10
+ from backend.api.deps import get_datahub, get_redis_client
11
+ from src.data.data_providers import DataHub
12
 
13
  router = APIRouter()
14
 
 
48
  "ims_configured": bool(os.environ.get("IMS_API_TOKEN")),
49
  "gemini_configured": bool(os.environ.get("GOOGLE_API_KEY")),
50
  }
51
+
52
+
53
+ @router.get("/health/data")
54
+ async def health_data(hub: DataHub = Depends(get_datahub)):
55
+ """Data freshness check — shows age of each cached data source."""
56
+ sources = {}
57
+
58
+ # Weather
59
+ try:
60
+ wx = hub.weather.get_current()
61
+ if wx and "error" not in wx:
62
+ sources["weather"] = {
63
+ "age_minutes": round(float(wx.get("age_minutes", -1)), 1),
64
+ "last_reading": wx.get("timestamp_local"),
65
+ "ok": float(wx.get("age_minutes", 9999)) < 120,
66
+ }
67
+ else:
68
+ sources["weather"] = {"ok": False, "error": wx.get("error", "unavailable")}
69
+ except Exception as exc:
70
+ sources["weather"] = {"ok": False, "error": str(exc)}
71
+
72
+ # Sensors
73
+ try:
74
+ snap = hub.vine_sensors.get_snapshot(light=True)
75
+ if snap and "error" not in snap:
76
+ stale = snap.get("staleness_minutes", -1)
77
+ sources["sensors"] = {
78
+ "age_minutes": round(float(stale), 1) if stale is not None else None,
79
+ "ok": stale is not None and float(stale) < 30,
80
+ }
81
+ else:
82
+ sources["sensors"] = {"ok": False, "error": snap.get("error", "unavailable")}
83
+ except Exception as exc:
84
+ sources["sensors"] = {"ok": False, "error": str(exc)}
85
+
86
+ # Energy
87
+ try:
88
+ en = hub.energy.get_current()
89
+ if en and "error" not in en:
90
+ sources["energy"] = {"ok": True, "power_kw": en.get("power_kw")}
91
+ else:
92
+ sources["energy"] = {"ok": False, "error": en.get("error", "unavailable")}
93
+ except Exception as exc:
94
+ sources["energy"] = {"ok": False, "error": str(exc)}
95
+
96
+ all_ok = all(s.get("ok", False) for s in sources.values())
97
+ return {"status": "ok" if all_ok else "degraded", "sources": sources}
backend/requirements.txt CHANGED
@@ -1,7 +1,7 @@
1
- # Backend-specific dependencies
2
  fastapi>=0.115.0
3
  uvicorn[standard]>=0.34.0
4
  pydantic>=2.0
5
  slowapi>=0.1.9
6
  PyJWT>=2.8.0
7
- sentry-sdk[fastapi]>=2.0
 
1
+ # Backend-specific dependencies (on top of root requirements.txt)
2
  fastapi>=0.115.0
3
  uvicorn[standard]>=0.34.0
4
  pydantic>=2.0
5
  slowapi>=0.1.9
6
  PyJWT>=2.8.0
7
+ sentry-sdk[fastapi]>=2.0 # optional: set SENTRY_DSN to enable
backend/workers/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (166 Bytes). View file
 
backend/workers/__pycache__/__init__.cpython-39.pyc ADDED
Binary file (160 Bytes). View file
 
backend/workers/__pycache__/control_tick.cpython-312.pyc ADDED
Binary file (6.21 kB). View file
 
backend/workers/__pycache__/daily_planner.cpython-312.pyc ADDED
Binary file (6.22 kB). View file
 
backend/workers/__pycache__/daily_planner.cpython-39.pyc ADDED
Binary file (2.12 kB). View file
 
config/__pycache__/settings.cpython-312.pyc ADDED
Binary file (3.93 kB). View file
 
config/__pycache__/settings.cpython-39.pyc ADDED
Binary file (3.37 kB). View file
 
requirements.txt CHANGED
@@ -1,10 +1,18 @@
1
- # API-only dependencies (slim no torch/chronos/streamlit)
2
- pandas>=2.0
3
- numpy>=1.26
4
- scikit-learn>=1.3
5
- requests>=2.31
6
- python-dotenv>=1.0
 
 
 
 
 
 
7
  xgboost>=2.0
8
  pvlib>=0.10.0
9
  astral>=3.2
 
 
10
  google-genai>=1.0
 
1
+ # Photosynthesis Prediction Model - dependencies
2
+ # Install: pip install -r requirements.txt
3
+
4
+ pandas==2.3.3
5
+ numpy==2.4.2
6
+ scikit-learn==1.8.0
7
+ matplotlib==3.10.8
8
+ seaborn==0.13.2
9
+ requests==2.32.5
10
+ python-dotenv==1.2.1
11
+ streamlit==1.54.0
12
+ plotly==6.5.2
13
  xgboost>=2.0
14
  pvlib>=0.10.0
15
  astral>=3.2
16
+ chronos-forecasting>=2.0
17
+ torch>=2.0
18
  google-genai>=1.0
src/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (2.27 kB). View file
 
src/__pycache__/__init__.cpython-39.pyc ADDED
Binary file (1.74 kB). View file
 
src/__pycache__/baseline_predictor.cpython-39.pyc ADDED
Binary file (6.53 kB). View file
 
src/__pycache__/canopy_photosynthesis.cpython-312.pyc ADDED
Binary file (307 Bytes). View file
 
src/__pycache__/canopy_photosynthesis.cpython-39.pyc ADDED
Binary file (295 Bytes). View file
 
src/__pycache__/chronos_forecaster.cpython-312.pyc ADDED
Binary file (25.1 kB). View file
 
src/__pycache__/chronos_forecaster.cpython-39.pyc ADDED
Binary file (296 Bytes). View file
 
src/__pycache__/command_arbiter.cpython-312.pyc ADDED
Binary file (11.9 kB). View file
 
src/__pycache__/command_arbiter.cpython-39.pyc ADDED
Binary file (8.8 kB). View file
 
src/__pycache__/control_loop.cpython-312.pyc ADDED
Binary file (32.6 kB). View file
 
src/__pycache__/control_loop.cpython-39.pyc ADDED
Binary file (19.6 kB). View file
 
src/__pycache__/data_providers.cpython-312.pyc ADDED
Binary file (282 Bytes). View file
 
src/__pycache__/data_providers.cpython-39.pyc ADDED
Binary file (270 Bytes). View file