first commit

This commit is contained in:
Alberto-Audrix committed 2026-09-08 15:21:23 +07:00
commit b54624be96
226 files changed
+108840

No files matched your search

+109
View File
@@ -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()
+111
View File
@@ -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()
+175
View File
@@ -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
+97
View File
@@ -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()
+137
View File
@@ -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()
+139
View File
@@ -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
+13
View File
@@ -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)
+21
View File
@@ -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)