"""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