first commit
This commit is contained in:
commit
b54624be96
226 files changed
+108840
No files matched your search
@@ -0,0 +1,109 @@
|
||||
"""Analyze dashboard.cpsp.id IoT flocks API for date/day mapping."""
|
||||
import json
|
||||
import urllib.request
|
||||
from collections import Counter, defaultdict
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
BASE = "https://dashboard.cpsp.id/api/iot/flocks/"
|
||||
CYCLE_START = datetime(2026, 5, 22).date()
|
||||
CYCLE_END = datetime(2026, 7, 9).date()
|
||||
TZ = timezone(timedelta(hours=7))
|
||||
|
||||
|
||||
def parse_dt(value: str) -> datetime:
|
||||
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
|
||||
|
||||
def fetch_page(page: int) -> dict:
|
||||
url = f"{BASE}?page={page}"
|
||||
with urllib.request.urlopen(url, timeout=60) as resp:
|
||||
return json.loads(resp.read())
|
||||
|
||||
|
||||
def main() -> None:
|
||||
meta = fetch_page(1)
|
||||
total = meta["count"]
|
||||
page_size = len(meta["results"])
|
||||
last_page = (total + page_size - 1) // page_size
|
||||
print(f"total={total} page_size={page_size} last_page={last_page}")
|
||||
|
||||
sample_pages = [1, 2, 3, last_page, last_page - 1]
|
||||
records = []
|
||||
for page in sample_pages:
|
||||
data = fetch_page(page)
|
||||
records.extend(data["results"])
|
||||
print(f" fetched page {page}: {len(data['results'])} rows")
|
||||
|
||||
flock_ids = Counter(r["flock_id"] for r in records)
|
||||
print(f"unique flock_ids in sample: {dict(flock_ids)}")
|
||||
|
||||
r0 = records[0]["payload"]["data"]
|
||||
coop = r0.get("coop") or {}
|
||||
print(f"coop kode={coop.get('kode')} name floor={r0.get('name')} kandang={r0.get('kandang')}")
|
||||
|
||||
by_day = defaultdict(list)
|
||||
by_fetch_date = defaultdict(list)
|
||||
for r in records:
|
||||
d = r["payload"]["data"]
|
||||
day = d.get("day")
|
||||
if day is not None:
|
||||
by_day[day].append(r)
|
||||
fetched = r.get("fetched_at") or d.get("lastUpdate")
|
||||
if fetched:
|
||||
cal = parse_dt(fetched).astimezone(TZ).date()
|
||||
by_fetch_date[cal].append(r)
|
||||
|
||||
print(f"day range: {min(by_day)}..{max(by_day)} ({len(by_day)} unique)")
|
||||
print(f"fetched_at date range: {min(by_fetch_date)}..{max(by_fetch_date)} ({len(by_fetch_date)} unique)")
|
||||
|
||||
print("\nSample day -> metrics (first record per day):")
|
||||
for day in sorted(by_day):
|
||||
if day not in (1, 2, 3, 34, 35, 36, 48, 49):
|
||||
continue
|
||||
d = by_day[day][0]["payload"]["data"]
|
||||
fetched = by_day[day][0].get("fetched_at", "")[:19]
|
||||
cal = CYCLE_START + timedelta(days=day - 1)
|
||||
print(
|
||||
f" day {day:2d} (cycle cal {cal}) fetched={fetched} "
|
||||
f"wind={d.get('wind')} hum={d.get('humidity')} temp={d.get('actualTemperature')} "
|
||||
f"water={d.get('water')} HSI={d.get('HSI')}"
|
||||
)
|
||||
|
||||
# Full scan for one flock: aggregate one row per cycle day using `day` field
|
||||
print("\nScanning all pages for daily rollup by `day` (last snapshot per day)...")
|
||||
daily: dict[int, dict] = {}
|
||||
for page in range(1, last_page + 1):
|
||||
for r in fetch_page(page)["results"]:
|
||||
d = r["payload"]["data"]
|
||||
day = d.get("day")
|
||||
if day is None:
|
||||
continue
|
||||
fetched = r.get("fetched_at")
|
||||
if not fetched:
|
||||
continue
|
||||
ts = parse_dt(fetched)
|
||||
prev = daily.get(day)
|
||||
if prev is None or ts > prev["ts"]:
|
||||
daily[day] = {"ts": ts, "r": r}
|
||||
|
||||
print(f"Collected {len(daily)} unique cycle days from full API")
|
||||
missing = [d for d in range(1, 50) if d not in daily]
|
||||
print(f"Missing days 1..49: {missing}")
|
||||
|
||||
print("\nFull cycle rollup (mapped to calendar via start 2026-05-22):")
|
||||
print(f"{'day':>3} {'date':>10} {'wind':>6} {'humidity':>8} {'avg_temp':>8} {'water':>8} {'HSI':>8}")
|
||||
for day in range(1, 50):
|
||||
entry = daily.get(day)
|
||||
cal = CYCLE_START + timedelta(days=day - 1)
|
||||
if not entry:
|
||||
print(f"{day:3d} {cal} — missing —")
|
||||
continue
|
||||
d = entry["r"]["payload"]["data"]
|
||||
print(
|
||||
f"{day:3d} {cal} {d.get('wind', 0):6} {d.get('humidity', 0):8} "
|
||||
f"{d.get('actualTemperature', 0):8} {d.get('water', 0):8} {d.get('HSI', 0):8.2f}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,111 @@
|
||||
"""Fast sample scan of IoT flocks API — day/date mapping without full pagination."""
|
||||
import json
|
||||
import urllib.request
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
BASE = "https://dashboard.cpsp.id/api/iot/flocks/"
|
||||
CYCLE_START = datetime(2026, 5, 22).date()
|
||||
TZ = timezone(timedelta(hours=7))
|
||||
|
||||
|
||||
def parse_dt(value: str) -> datetime:
|
||||
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
|
||||
|
||||
def fetch_page(page: int) -> dict:
|
||||
with urllib.request.urlopen(f"{BASE}?page={page}", timeout=60) as resp:
|
||||
return json.loads(resp.read())
|
||||
|
||||
|
||||
def summarize_records(records: list) -> None:
|
||||
by_day: dict[int, list] = defaultdict(list)
|
||||
for r in records:
|
||||
day = r["payload"]["data"].get("day")
|
||||
if day is not None:
|
||||
by_day[day].append(r)
|
||||
|
||||
print(f" unique days: {len(by_day)} range {min(by_day)}..{max(by_day)}")
|
||||
for day in sorted(by_day)[:3]:
|
||||
r = max(by_day[day], key=lambda x: parse_dt(x["fetched_at"]))
|
||||
d = r["payload"]["data"]
|
||||
cal = CYCLE_START + timedelta(days=day - 1)
|
||||
print(
|
||||
f" day {day} -> {cal} | wind={d.get('wind')} hum={d.get('humidity')} "
|
||||
f"temp={d.get('actualTemperature')} water={d.get('water')} HSI={d.get('HSI')}"
|
||||
)
|
||||
if len(by_day) > 6:
|
||||
print(" ...")
|
||||
for day in sorted(by_day)[-3:]:
|
||||
r = max(by_day[day], key=lambda x: parse_dt(x["fetched_at"]))
|
||||
d = r["payload"]["data"]
|
||||
cal = CYCLE_START + timedelta(days=day - 1)
|
||||
print(
|
||||
f" day {day} -> {cal} | wind={d.get('wind')} hum={d.get('humidity')} "
|
||||
f"temp={d.get('actualTemperature')} water={d.get('water')} HSI={d.get('HSI')}"
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
p1 = fetch_page(1)
|
||||
total = p1["count"]
|
||||
page_size = len(p1["results"])
|
||||
last_page = (total + page_size - 1) // page_size
|
||||
print(f"API total={total} page_size={page_size} pages={last_page}")
|
||||
|
||||
r0 = p1["results"][0]
|
||||
d0 = r0["payload"]["data"]
|
||||
coop = d0.get("coop") or {}
|
||||
print(f"flock_id={r0['flock_id']} floor={d0.get('name')} site={coop.get('kode')}")
|
||||
print(f"kandang field (Mongo id)={d0.get('kandang')}")
|
||||
|
||||
# Sample: first pages (newest), last pages (oldest), and mid pages
|
||||
sample_pages = sorted({1, 2, 3, 10, 50, 100, 200, 400, 600, last_page, last_page - 1, last_page - 2})
|
||||
all_records = []
|
||||
for page in sample_pages:
|
||||
if page < 1 or page > last_page:
|
||||
continue
|
||||
data = fetch_page(page)
|
||||
all_records.extend(data["results"])
|
||||
days = {r["payload"]["data"].get("day") for r in data["results"]}
|
||||
fdates = {parse_dt(r["fetched_at"]).astimezone(TZ).date() for r in data["results"]}
|
||||
print(f"page {page:4d}: days={min(days)}..{max(days)} fetched_dates={min(fdates)}..{max(fdates)}")
|
||||
|
||||
# Roll up sampled data: latest fetched_at per cycle day
|
||||
daily: dict[int, dict] = {}
|
||||
for r in all_records:
|
||||
day = r["payload"]["data"].get("day")
|
||||
if day is None:
|
||||
continue
|
||||
ts = parse_dt(r["fetched_at"])
|
||||
if day not in daily or ts > daily[day]["ts"]:
|
||||
daily[day] = {"ts": ts, "r": r}
|
||||
|
||||
print(f"\nFrom sample: {len(daily)} unique cycle days seen")
|
||||
missing = [d for d in range(1, 50) if d not in daily]
|
||||
print(f"Missing in sample (1..49): {missing}")
|
||||
|
||||
print("\nField mapping -> iot_panel (exclude experience_temperature):")
|
||||
print(" date = cycle_start + (day - 1) # 2026-05-22 + day-1")
|
||||
print(" wind_speed = payload.data.wind")
|
||||
print(" humidity = payload.data.humidity # likely /10 for %")
|
||||
print(" water_total = payload.data.water")
|
||||
print(" average_temperature = payload.data.actualTemperature # likely /10 for °C")
|
||||
print(" experience_temperature = formula (HSI? TBD)")
|
||||
|
||||
print("\nSample rollup table:")
|
||||
print(f"{'day':>3} {'date':>10} {'wind':>5} {'hum':>6} {'temp_raw':>8} {'water':>6} {'HSI':>7}")
|
||||
for day in range(1, 50):
|
||||
cal = CYCLE_START + timedelta(days=day - 1)
|
||||
entry = daily.get(day)
|
||||
if not entry:
|
||||
continue
|
||||
d = entry["r"]["payload"]["data"]
|
||||
print(
|
||||
f"{day:3d} {cal} {d.get('wind', 0):5} {d.get('humidity', 0):6} "
|
||||
f"{d.get('actualTemperature', 0):8} {d.get('water', 0):6} {d.get('HSI', 0):7.2f}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,175 @@
|
||||
"""Experience temperature for iot_panel (not HSI).
|
||||
|
||||
Experience suhu = average_temperature - chill_effect
|
||||
chill_effect = (wind_speed * chill_factor) - rh_adjustment
|
||||
rh_adjustment = (humidity_pct - 70) / 5
|
||||
wind_speed = sensor wind in m/s (payload.data.wind / 10 when stored in tenths)
|
||||
|
||||
Chill factor is linearly interpolated from bird age (days).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
CHILL_FACTOR_POINTS = (
|
||||
(0, 8.0),
|
||||
(1, 8.0),
|
||||
(7, 7.0),
|
||||
(14, 6.0),
|
||||
(21, 4.5),
|
||||
(28, 3.5),
|
||||
(35, 3.5),
|
||||
(42, 3.0),
|
||||
)
|
||||
|
||||
|
||||
def sensor_celsius(sensor: dict | float | int | None) -> float | None:
|
||||
if sensor is None:
|
||||
return None
|
||||
if isinstance(sensor, (int, float)):
|
||||
return float(sensor)
|
||||
raw = sensor.get("value")
|
||||
if raw is None:
|
||||
return None
|
||||
cal = float(sensor.get("calibration") or 0)
|
||||
return (float(raw) + cal) / 10.0
|
||||
|
||||
|
||||
def sensor_humidity_pct(sensor: dict | float | int | None, fallback: float | None) -> float:
|
||||
if isinstance(sensor, dict):
|
||||
raw = sensor.get("value")
|
||||
if raw is not None:
|
||||
cal = float(sensor.get("calibration") or 0)
|
||||
return (float(raw) + cal) / 10.0
|
||||
if isinstance(sensor, (int, float)):
|
||||
return float(sensor)
|
||||
if fallback is None:
|
||||
return 70.0
|
||||
value = float(fallback)
|
||||
return value / 10.0 if value > 100 else value
|
||||
|
||||
|
||||
def wind_ms(payload_wind: float | int | None, sensors_wind: dict | float | int | None) -> float:
|
||||
"""Use measured wind directly (not kipas aktif × 0.35)."""
|
||||
if isinstance(sensors_wind, dict) and sensors_wind.get("value") is not None:
|
||||
return float(sensors_wind["value"]) / 10.0
|
||||
if isinstance(sensors_wind, (int, float)):
|
||||
return float(sensors_wind)
|
||||
if payload_wind is None:
|
||||
return 0.0
|
||||
return float(payload_wind) / 10.0
|
||||
|
||||
|
||||
def average_inside_temp_c(sensors: dict | None, actual_temperature: float | int | None) -> float:
|
||||
if sensors:
|
||||
readings = [
|
||||
v
|
||||
for v in (
|
||||
sensor_celsius(sensors.get("temperature1")),
|
||||
sensor_celsius(sensors.get("temperature2")),
|
||||
sensor_celsius(sensors.get("temperature3")),
|
||||
)
|
||||
if v is not None
|
||||
]
|
||||
if readings:
|
||||
return sum(readings) / len(readings)
|
||||
if actual_temperature is None:
|
||||
return 0.0
|
||||
value = float(actual_temperature)
|
||||
return value / 10.0 if value > 100 else value
|
||||
|
||||
|
||||
def chicken_chill_factor(age: float) -> float:
|
||||
if age <= CHILL_FACTOR_POINTS[0][0]:
|
||||
return CHILL_FACTOR_POINTS[0][1]
|
||||
for idx in range(1, len(CHILL_FACTOR_POINTS)):
|
||||
prev_age, prev_factor = CHILL_FACTOR_POINTS[idx - 1]
|
||||
next_age, next_factor = CHILL_FACTOR_POINTS[idx]
|
||||
if age > next_age:
|
||||
continue
|
||||
span = next_age - prev_age
|
||||
if span <= 0:
|
||||
return next_factor
|
||||
progress = (age - prev_age) / span
|
||||
return prev_factor + (next_factor - prev_factor) * progress
|
||||
return CHILL_FACTOR_POINTS[-1][1]
|
||||
|
||||
|
||||
def experience_temperature(
|
||||
*,
|
||||
average_temperature: float,
|
||||
wind_speed: float,
|
||||
humidity_pct: float,
|
||||
age_days: float,
|
||||
) -> float:
|
||||
chill_factor = chicken_chill_factor(age_days)
|
||||
rh_adjustment = (humidity_pct - 70.0) / 5.0
|
||||
chill_effect = wind_speed * chill_factor - rh_adjustment
|
||||
return average_temperature - chill_effect
|
||||
|
||||
|
||||
def panel_fields_from_payload(data: dict, age_days: int) -> dict:
|
||||
sensors = data.get("sensors") or {}
|
||||
humidity = sensor_humidity_pct(sensors.get("humidity"), data.get("humidity"))
|
||||
wind_speed = wind_ms(data.get("wind"), sensors.get("wind"))
|
||||
avg_temp = average_inside_temp_c(sensors, data.get("actualTemperature"))
|
||||
water = data.get("water")
|
||||
if water is None and isinstance(sensors.get("water"), dict):
|
||||
water = sensors["water"].get("value")
|
||||
return {
|
||||
"wind_speed": round(wind_speed, 4),
|
||||
"humidity": round(humidity, 4),
|
||||
"water_total": float(water or 0),
|
||||
"average_temperature": round(avg_temp, 4),
|
||||
"experience_temperature": round(
|
||||
experience_temperature(
|
||||
average_temperature=avg_temp,
|
||||
wind_speed=wind_speed,
|
||||
humidity_pct=humidity,
|
||||
age_days=age_days,
|
||||
),
|
||||
4,
|
||||
),
|
||||
"chill_factor": round(chicken_chill_factor(age_days), 4),
|
||||
}
|
||||
|
||||
|
||||
# Lantai 2 has no device in the public flocks API. Use a sibling ObjectId
|
||||
# (same 24-hex shape as Lantai 1) and clone panel rows with a small floor offset.
|
||||
LANTAI_1_IOT_FLOCK_ID = "686df69f407b21002da8750c"
|
||||
LANTAI_2_IOT_FLOCK_ID = "686df69f407b21002da8750d"
|
||||
|
||||
LANTAI_2_ADJUST = {
|
||||
"wind_speed_delta": -0.05,
|
||||
"humidity_delta": 1.2,
|
||||
"average_temperature_delta": -0.3,
|
||||
"water_total_factor": 0.96,
|
||||
}
|
||||
|
||||
|
||||
def clone_lantai2_panel_row(row: dict) -> dict:
|
||||
"""Copy a Lantai 1 daily panel row onto Lantai 2, then recompute experience suhu."""
|
||||
age = int(row["age"])
|
||||
wind = max(0.0, float(row["wind_speed"]) + LANTAI_2_ADJUST["wind_speed_delta"])
|
||||
humidity = min(100.0, max(0.0, float(row["humidity"]) + LANTAI_2_ADJUST["humidity_delta"]))
|
||||
avg_temp = float(row["average_temperature"]) + LANTAI_2_ADJUST["average_temperature_delta"]
|
||||
water = max(0.0, float(row["water_total"]) * LANTAI_2_ADJUST["water_total_factor"])
|
||||
cloned = {
|
||||
"date": row["date"],
|
||||
"age": age,
|
||||
"wind_speed": round(wind, 4),
|
||||
"humidity": round(humidity, 4),
|
||||
"water_total": round(water, 1),
|
||||
"average_temperature": round(avg_temp, 4),
|
||||
"experience_temperature": round(
|
||||
experience_temperature(
|
||||
average_temperature=avg_temp,
|
||||
wind_speed=wind,
|
||||
humidity_pct=humidity,
|
||||
age_days=age,
|
||||
),
|
||||
4,
|
||||
),
|
||||
"chill_factor": round(chicken_chill_factor(age), 4),
|
||||
"cycle_day": row.get("cycle_day"),
|
||||
}
|
||||
return cloned
|
||||
@@ -0,0 +1,97 @@
|
||||
"""Extract real 10-minute IoT snapshots for the Sukawarna Kandang 2 cycle window.
|
||||
|
||||
The public IoT API (dashboard.cpsp.id/api/iot/flocks/) polls the coop every 10
|
||||
minutes, so a full cycle day holds up to 144 rows. This script scans the API's
|
||||
history for flock 686df69f407b21002da8750c (Kandang 2 Lantai 1) inside the
|
||||
seeded cycle window 2026-05-22 .. 2026-07-09 and writes every snapshot to
|
||||
``iot_panel_10min_extract.json``. The Django seed command reads this cache so
|
||||
Kandang 2 Lantai 1 is seeded from real sensor readings; the other floors and
|
||||
Kandang 1 are generated from it.
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import urllib.request
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
from experience_temperature import LANTAI_1_IOT_FLOCK_ID, panel_fields_from_payload
|
||||
|
||||
BASE = "https://dashboard.cpsp.id/api/iot/flocks/"
|
||||
FLOCK_ID = LANTAI_1_IOT_FLOCK_ID
|
||||
CYCLE_START = datetime(2026, 5, 22).date()
|
||||
CYCLE_END = datetime(2026, 7, 9).date()
|
||||
TZ = timezone(timedelta(hours=7))
|
||||
OUT = SCRIPT_DIR / "iot_panel_10min_extract.json"
|
||||
|
||||
|
||||
def parse_dt(value: str) -> datetime:
|
||||
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
|
||||
|
||||
def fetch_page(page: int) -> dict:
|
||||
with urllib.request.urlopen(f"{BASE}?page={page}", timeout=60) as resp:
|
||||
return json.loads(resp.read())
|
||||
|
||||
|
||||
def main() -> None:
|
||||
p1 = fetch_page(1)
|
||||
last_page = (p1["count"] + len(p1["results"]) - 1) // len(p1["results"])
|
||||
hi = min(last_page, 320)
|
||||
print(f"Scanning pages 1..{hi} (of {last_page}) for flock {FLOCK_ID}", flush=True)
|
||||
|
||||
# key: (date, slot_index) -> mapped row (keep the latest snapshot per 10-min slot)
|
||||
rows_by_slot: dict[tuple[str, int], dict] = {}
|
||||
in_window = 0
|
||||
for page in range(1, hi + 1):
|
||||
for r in fetch_page(page)["results"]:
|
||||
if r["flock_id"] != FLOCK_ID:
|
||||
continue
|
||||
fetched = r.get("fetched_at")
|
||||
if not fetched:
|
||||
continue
|
||||
ts = parse_dt(fetched).astimezone(TZ)
|
||||
cal = ts.date()
|
||||
if not (CYCLE_START <= cal <= CYCLE_END):
|
||||
continue
|
||||
in_window += 1
|
||||
d = r["payload"]["data"]
|
||||
age = (cal - CYCLE_START).days + 1
|
||||
fields = panel_fields_from_payload(d, age)
|
||||
slot = (ts.hour * 60 + ts.minute) // 10
|
||||
key = (str(cal), slot)
|
||||
rows_by_slot[key] = {
|
||||
"timestamp": ts.isoformat(),
|
||||
"date": str(cal),
|
||||
"age": age,
|
||||
**fields,
|
||||
}
|
||||
if page % 50 == 0:
|
||||
print(f" page {page}/{hi} — in-window so far: {in_window}", flush=True)
|
||||
|
||||
rows = [rows_by_slot[k] for k in sorted(rows_by_slot, key=lambda k: (k[0], k[1]))]
|
||||
|
||||
out = {
|
||||
"flock_id": FLOCK_ID,
|
||||
"cycle_start": str(CYCLE_START),
|
||||
"cycle_end": str(CYCLE_END),
|
||||
"count": len(rows),
|
||||
"rows": rows,
|
||||
}
|
||||
OUT.write_text(json.dumps(out, indent=2), encoding="utf-8")
|
||||
|
||||
per_date: dict[str, int] = {}
|
||||
for row in rows:
|
||||
per_date[row["date"]] = per_date.get(row["date"], 0) + 1
|
||||
print(f"\nSnapshots in window: {in_window}; unique slots written: {len(rows)}")
|
||||
print(f"Unique dates: {len(per_date)} (expect 49)")
|
||||
counts = sorted(set(per_date.values()))
|
||||
print(f"Rows-per-date distribution: {counts}")
|
||||
low = [d for d, c in per_date.items() if c < 144]
|
||||
print(f"Dates with fewer than 144 rows ({len(low)}): {low[:20]}")
|
||||
print(f"Wrote {OUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,137 @@
|
||||
"""Extract IoT panel rows for Sukawarna cycle 2026-05-22 .. 2026-07-09."""
|
||||
import json
|
||||
import urllib.request
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
BASE = "https://dashboard.cpsp.id/api/iot/flocks/"
|
||||
FLOCK_ID = "686df69f407b21002da8750c"
|
||||
CYCLE_START = datetime(2026, 5, 22).date()
|
||||
CYCLE_END = datetime(2026, 7, 9).date()
|
||||
TZ = timezone(timedelta(hours=7))
|
||||
|
||||
|
||||
def parse_dt(value: str) -> datetime:
|
||||
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
|
||||
|
||||
def fetch_page(page: int) -> dict:
|
||||
with urllib.request.urlopen(f"{BASE}?page={page}", timeout=60) as resp:
|
||||
return json.loads(resp.read())
|
||||
|
||||
|
||||
def to_panel_fields(d: dict) -> dict:
|
||||
"""Map API payload.data -> iot_panel columns (no experience_temperature)."""
|
||||
hum = d.get("humidity") or 0
|
||||
temp = d.get("actualTemperature") or 0
|
||||
return {
|
||||
"wind_speed": float(d.get("wind") or 0),
|
||||
"humidity": float(hum / 10 if hum > 100 else hum),
|
||||
"water_total": float(d.get("water") or 0),
|
||||
"average_temperature": float(temp / 10 if temp > 100 else temp),
|
||||
"hsi": float(d.get("HSI") or 0),
|
||||
"cycle_day": int(d.get("day") or 0),
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
p1 = fetch_page(1)
|
||||
total = p1["count"]
|
||||
page_size = len(p1["results"])
|
||||
last_page = (total + page_size - 1) // page_size
|
||||
print(f"Scanning {last_page} pages for flock {FLOCK_ID}")
|
||||
print(f"Cycle window: {CYCLE_START} .. {CYCLE_END}\n")
|
||||
|
||||
# Strategy 1: filter by fetched_at calendar date in cycle window, pick latest per date
|
||||
by_calendar: dict = {}
|
||||
# Strategy 2: filter by cycle day 1..49 where fetched_at in window, pick latest per day
|
||||
by_cycle_day: dict = defaultdict(list)
|
||||
|
||||
in_window_count = 0
|
||||
for page in range(1, last_page + 1):
|
||||
for r in fetch_page(page)["results"]:
|
||||
if r["flock_id"] != FLOCK_ID:
|
||||
continue
|
||||
fetched = r.get("fetched_at")
|
||||
if not fetched:
|
||||
continue
|
||||
cal = parse_dt(fetched).astimezone(TZ).date()
|
||||
if not (CYCLE_START <= cal <= CYCLE_END):
|
||||
continue
|
||||
in_window_count += 1
|
||||
ts = parse_dt(fetched)
|
||||
d = r["payload"]["data"]
|
||||
day = d.get("day")
|
||||
|
||||
prev = by_calendar.get(cal)
|
||||
if prev is None or ts > prev["ts"]:
|
||||
by_calendar[cal] = {"ts": ts, "r": r, "fields": to_panel_fields(d)}
|
||||
|
||||
if day is not None:
|
||||
by_cycle_day[day].append((ts, r, d))
|
||||
|
||||
if page % 100 == 0:
|
||||
print(f" page {page}/{last_page} — in-window so far: {in_window_count}, dates: {len(by_calendar)}")
|
||||
|
||||
print(f"\nTotal snapshots in cycle window: {in_window_count}")
|
||||
print(f"Unique calendar dates in window: {len(by_calendar)} (expect 49)")
|
||||
|
||||
missing_dates = []
|
||||
d = CYCLE_START
|
||||
while d <= CYCLE_END:
|
||||
if d not in by_calendar:
|
||||
missing_dates.append(d)
|
||||
d += timedelta(days=1)
|
||||
print(f"Missing calendar dates: {len(missing_dates)}")
|
||||
if missing_dates:
|
||||
print(f" first few: {missing_dates[:10]}")
|
||||
print(f" last few: {missing_dates[-10:]}")
|
||||
|
||||
# Per cycle day within window (latest snapshot)
|
||||
day_roll: dict[int, dict] = {}
|
||||
for day, items in by_cycle_day.items():
|
||||
ts, r, d = max(items, key=lambda x: x[0])
|
||||
day_roll[day] = {"ts": ts, "fields": to_panel_fields(d), "cal": parse_dt(r["fetched_at"]).astimezone(TZ).date()}
|
||||
|
||||
print(f"\nUnique cycle `day` values in window: {sorted(day_roll)}")
|
||||
missing_days = [i for i in range(1, 50) if i not in day_roll]
|
||||
print(f"Missing cycle days 1..49 in window: {missing_days}")
|
||||
|
||||
print("\n--- Daily rows for seed (by calendar date, latest snapshot) ---")
|
||||
print(f"{'date':>10} {'age':>3} {'wind':>5} {'hum%':>6} {'temp°C':>7} {'water':>8} {'HSI':>7}")
|
||||
d = CYCLE_START
|
||||
while d <= CYCLE_END:
|
||||
entry = by_calendar.get(d)
|
||||
if entry:
|
||||
f = entry["fields"]
|
||||
print(
|
||||
f"{d} {f['cycle_day']:3d} {f['wind_speed']:5.0f} {f['humidity']:6.1f} "
|
||||
f"{f['average_temperature']:7.1f} {f['water_total']:8.0f} {f['hsi']:7.2f}"
|
||||
)
|
||||
else:
|
||||
age = (d - CYCLE_START).days + 1
|
||||
print(f"{d} {age:3d} — no API data —")
|
||||
d += timedelta(days=1)
|
||||
|
||||
out = Path(__file__).resolve().parent / "iot_panel_cycle_extract.json"
|
||||
payload = {
|
||||
"flock_id": FLOCK_ID,
|
||||
"cycle_start": str(CYCLE_START),
|
||||
"cycle_end": str(CYCLE_END),
|
||||
"rows": [
|
||||
{
|
||||
"date": str(cal),
|
||||
"age": (cal - CYCLE_START).days + 1,
|
||||
**by_calendar[cal]["fields"],
|
||||
}
|
||||
for cal in sorted(by_calendar)
|
||||
],
|
||||
}
|
||||
out.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
||||
print(f"\nWrote {len(payload['rows'])} rows to {out}")
|
||||
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,139 @@
|
||||
"""Extract IoT rows for cycle window — scan only pages likely in date range."""
|
||||
import json
|
||||
import urllib.request
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
from experience_temperature import (
|
||||
LANTAI_1_IOT_FLOCK_ID,
|
||||
LANTAI_2_IOT_FLOCK_ID,
|
||||
LANTAI_2_ADJUST,
|
||||
clone_lantai2_panel_row,
|
||||
panel_fields_from_payload,
|
||||
)
|
||||
|
||||
BASE = "https://dashboard.cpsp.id/api/iot/flocks/"
|
||||
FLOCK_ID = LANTAI_1_IOT_FLOCK_ID
|
||||
CYCLE_START = datetime(2026, 5, 22).date()
|
||||
CYCLE_END = datetime(2026, 7, 9).date()
|
||||
TZ = timezone(timedelta(hours=7))
|
||||
|
||||
|
||||
def parse_dt(value: str) -> datetime:
|
||||
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
|
||||
|
||||
def fetch_page(page: int) -> dict:
|
||||
with urllib.request.urlopen(f"{BASE}?page={page}", timeout=60) as resp:
|
||||
return json.loads(resp.read())
|
||||
|
||||
|
||||
def to_panel_fields(d: dict, age_days: int) -> dict:
|
||||
fields = panel_fields_from_payload(d, age_days)
|
||||
fields["cycle_day"] = int(d.get("day") or 0)
|
||||
return fields
|
||||
|
||||
|
||||
def main() -> None:
|
||||
p1 = fetch_page(1)
|
||||
last_page = (p1["count"] + len(p1["results"]) - 1) // len(p1["results"])
|
||||
|
||||
# Find page range: newest pages first until before cycle start
|
||||
lo, hi = 1, min(last_page, 300)
|
||||
print(f"Scanning pages 1..{hi} (of {last_page})")
|
||||
|
||||
by_calendar: dict = {}
|
||||
in_window = 0
|
||||
for page in range(1, hi + 1):
|
||||
for r in fetch_page(page)["results"]:
|
||||
if r["flock_id"] != FLOCK_ID:
|
||||
continue
|
||||
fetched = r.get("fetched_at")
|
||||
if not fetched:
|
||||
continue
|
||||
cal = parse_dt(fetched).astimezone(TZ).date()
|
||||
if not (CYCLE_START <= cal <= CYCLE_END):
|
||||
continue
|
||||
in_window += 1
|
||||
ts = parse_dt(fetched)
|
||||
d = r["payload"]["data"]
|
||||
age = (cal - CYCLE_START).days + 1
|
||||
prev = by_calendar.get(cal)
|
||||
if prev is None or ts > prev["ts"]:
|
||||
by_calendar[cal] = {"ts": ts, "fields": to_panel_fields(d, age)}
|
||||
|
||||
missing = []
|
||||
d = CYCLE_START
|
||||
while d <= CYCLE_END:
|
||||
if d not in by_calendar:
|
||||
missing.append(d)
|
||||
d += timedelta(days=1)
|
||||
|
||||
print(f"snapshots in window: {in_window}")
|
||||
print(f"unique dates: {len(by_calendar)}/49, missing: {len(missing)}")
|
||||
if missing:
|
||||
print(f"missing: {[str(x) for x in missing]}")
|
||||
|
||||
print(
|
||||
f"\n{'date':>10} {'age':>3} {'wind':>6} {'hum%':>6} {'avgT':>6} {'expT':>6} {'water':>8}"
|
||||
)
|
||||
d = CYCLE_START
|
||||
while d <= CYCLE_END:
|
||||
e = by_calendar.get(d)
|
||||
age = (d - CYCLE_START).days + 1
|
||||
if e:
|
||||
f = e["fields"]
|
||||
print(
|
||||
f"{d} {age:3d} {f['wind_speed']:6.2f} {f['humidity']:6.1f} "
|
||||
f"{f['average_temperature']:6.1f} {f['experience_temperature']:6.1f} "
|
||||
f"{f['water_total']:8.0f}"
|
||||
)
|
||||
else:
|
||||
print(f"{d} {age:3d} — missing —")
|
||||
d += timedelta(days=1)
|
||||
|
||||
lantai1_rows = [
|
||||
{"date": str(c), "age": (c - CYCLE_START).days + 1, **by_calendar[c]["fields"]}
|
||||
for c in sorted(by_calendar)
|
||||
]
|
||||
lantai2_rows = [clone_lantai2_panel_row(row) for row in lantai1_rows]
|
||||
|
||||
out = Path(__file__).resolve().parent / "iot_panel_cycle_extract.json"
|
||||
out.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"cycle_start": str(CYCLE_START),
|
||||
"cycle_end": str(CYCLE_END),
|
||||
"experience_temperature": (
|
||||
"avg_temp - (wind_speed * chill_factor - (humidity-70)/5); "
|
||||
"wind_speed is sensor m/s not fan count"
|
||||
),
|
||||
"flocks": [
|
||||
{
|
||||
"name": "Lantai 1",
|
||||
"iot_flock_id": LANTAI_1_IOT_FLOCK_ID,
|
||||
"source": "api",
|
||||
"rows": lantai1_rows,
|
||||
},
|
||||
{
|
||||
"name": "Lantai 2",
|
||||
"iot_flock_id": LANTAI_2_IOT_FLOCK_ID,
|
||||
"source": "cloned_from_lantai_1",
|
||||
"adjust": LANTAI_2_ADJUST,
|
||||
"rows": lantai2_rows,
|
||||
},
|
||||
],
|
||||
},
|
||||
indent=2,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"\nWrote {out} ({len(lantai1_rows)} Lantai 1 + {len(lantai2_rows)} Lantai 2)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,13 @@
|
||||
import openpyxl
|
||||
|
||||
path = r"D:\DashboardCPSP\rework\dashboard-cpsp\backup\Laporan_Akhir-Siklus_Sukawarna_Kandang-Atas_Akhir-Siklus_2026-05-22_sd_2026-07-09.xlsx"
|
||||
|
||||
wb = openpyxl.load_workbook(path, data_only=True)
|
||||
|
||||
for sheet in ["Feed In", "Feed Use"]:
|
||||
ws = wb[sheet]
|
||||
print(f"\n===== SHEET: {sheet} =====")
|
||||
for row in ws.iter_rows():
|
||||
vals = [(c.coordinate, c.value) for c in row if c.value is not None]
|
||||
if vals:
|
||||
print(vals)
|
||||
@@ -0,0 +1,21 @@
|
||||
"""Generate chicken_weight.chicken_count as weigh-sample size (not leftover birds)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
|
||||
# Deterministic: same cycle always gets the same sample sizes.
|
||||
SAMPLE_RNG_SEED = 20260522
|
||||
SAMPLE_MIN = 180
|
||||
SAMPLE_MAX = 320
|
||||
|
||||
|
||||
def weigh_sample_count(*, average_weight: float, rng: random.Random | None = None) -> int:
|
||||
"""Birds used to compute that day's average_weight.
|
||||
|
||||
0 when there is no BW reading (age 38+ user decision).
|
||||
"""
|
||||
if average_weight is None or float(average_weight) <= 0:
|
||||
return 0
|
||||
rng = rng or random.Random(SAMPLE_RNG_SEED)
|
||||
return rng.randint(SAMPLE_MIN, SAMPLE_MAX)
|
||||
Reference in new issue
Block a user