176 lines
5.8 KiB
Python
176 lines
5.8 KiB
Python
"""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
|