184 lines
9.5 KiB
Python
184 lines
9.5 KiB
Python
import math
|
|
import random
|
|
from bots.base import BaseBot
|
|
|
|
|
|
class CracBot(BaseBot):
|
|
"""CRAC/cooling unit monitor — full sensor set."""
|
|
|
|
interval = 60
|
|
|
|
# Physical constants
|
|
RATED_CAPACITY_KW = 80.0
|
|
MAX_AIRFLOW_CFM = 9_000.0
|
|
AIR_DENSITY = 1.2 # kg/m³
|
|
AIR_CP = 1_005.0 # J/kg·K
|
|
MAX_FAN_RPM = 3_000
|
|
RATED_VOLTAGE = 415.0 # V, 3-phase
|
|
|
|
def __init__(self, site_id: str, crac_id: str) -> None:
|
|
super().__init__()
|
|
self.site_id = site_id
|
|
self.crac_id = crac_id
|
|
self._faulted = False
|
|
self._compressor_fault = False
|
|
self._dirty_filter = False
|
|
# Persisted state — realistic starting values
|
|
self._comp_hours = 12_000 + random.randint(0, 4_000)
|
|
self._fan_hours = self._comp_hours + random.randint(500, 1_500)
|
|
self._filter_dp_base = 28.0 + random.uniform(-5, 5)
|
|
|
|
def get_topic(self) -> str:
|
|
return f"bms/{self.site_id}/cooling/{self.crac_id}"
|
|
|
|
def set_scenario(self, name: str | None) -> None:
|
|
super().set_scenario(name)
|
|
self._faulted = (name == "COOLING_FAILURE")
|
|
self._compressor_fault = (name == "COMPRESSOR_FAULT")
|
|
self._dirty_filter = (name == "DIRTY_FILTER")
|
|
|
|
def get_payload(self) -> dict:
|
|
# Accumulate run hours (interval seconds → fraction of an hour)
|
|
hour_inc = self.interval / 3_600
|
|
self._comp_hours += hour_inc
|
|
self._fan_hours += hour_inc
|
|
|
|
# ── Hard fault — unit completely offline ──────────────────────
|
|
if self._faulted:
|
|
return {
|
|
"state": "fault",
|
|
"mode": "fault",
|
|
"setpoint": 22.0,
|
|
"supply_temp": None, "return_temp": None,
|
|
"supply_humidity": None, "return_humidity": None,
|
|
"airflow_cfm": 0, "filter_dp_pa": round(self._filter_dp_base, 1),
|
|
"cooling_capacity_kw": 0, "cooling_capacity_rated_kw": self.RATED_CAPACITY_KW,
|
|
"cooling_capacity_pct": 0, "cop": 0, "sensible_heat_ratio": 0,
|
|
"compressor_state": 0, "compressor_load_pct": 0,
|
|
"compressor_power_kw": 0, "compressor_run_hours": round(self._comp_hours),
|
|
"high_pressure_bar": 0, "low_pressure_bar": 0,
|
|
"discharge_superheat_c": 0, "liquid_subcooling_c": 0,
|
|
"fan_pct": 0, "fan_rpm": 0, "fan_power_kw": 0,
|
|
"fan_run_hours": round(self._fan_hours),
|
|
"total_unit_power_kw": 0, "input_voltage_v": self.RATED_VOLTAGE,
|
|
"input_current_a": 0, "power_factor": 0,
|
|
}
|
|
|
|
# ── Normal operating base values ──────────────────────────────
|
|
supply = 17.5 + random.gauss(0, 0.3)
|
|
return_temp = 28.0 + random.gauss(0, 0.5)
|
|
fan_pct = 62.0 + random.gauss(0, 3)
|
|
|
|
# ── Scenarios ─────────────────────────────────────────────────
|
|
if self._scenario == "FAN_DEGRADATION":
|
|
fan_pct = max(22.0, 62.0 - self._scenario_step * 1.6) + random.gauss(0, 1.5)
|
|
return_temp = 28.0 + min(self._scenario_step * 0.45, 11.0) + random.gauss(0, 0.5)
|
|
supply = 17.5 + min(self._scenario_step * 0.08, 2.0) + random.gauss(0, 0.3)
|
|
|
|
fan_pct = max(10.0, min(100.0, fan_pct))
|
|
|
|
# ── Filter differential pressure ──────────────────────────────
|
|
if self._dirty_filter:
|
|
filter_dp = self._filter_dp_base + min(self._scenario_step * 4.5, 110.0)
|
|
else:
|
|
# Slow natural fouling (resets to base on RESET via _scenario_step=0)
|
|
filter_dp = self._filter_dp_base + self._scenario_step * 0.01
|
|
|
|
# Filter fouling reduces airflow
|
|
airflow_factor = max(0.50, 1.0 - max(0.0, filter_dp - 30) / 300.0)
|
|
airflow_cfm = self.MAX_AIRFLOW_CFM * (fan_pct / 100.0) * airflow_factor
|
|
airflow_m3s = airflow_cfm * 0.000_471_947
|
|
|
|
# ── Cooling capacity ──────────────────────────────────────────
|
|
delta_t = return_temp - supply
|
|
cooling_kw = airflow_m3s * self.AIR_DENSITY * self.AIR_CP * delta_t / 1_000.0
|
|
|
|
compressor_running = True
|
|
if self._compressor_fault:
|
|
# Fan-only — minimal sensible cooling only
|
|
cooling_kw = cooling_kw * 0.08
|
|
fan_pct = 100.0 # fans go full trying to compensate
|
|
compressor_running = False
|
|
|
|
cooling_kw = max(0.0, cooling_kw)
|
|
cooling_cap_pct = min(100.0, (cooling_kw / self.RATED_CAPACITY_KW) * 100.0)
|
|
|
|
# ── Compressor ────────────────────────────────────────────────
|
|
comp_load_pct = min(100.0, (cooling_kw / self.RATED_CAPACITY_KW) * 110.0) if compressor_running else 0.0
|
|
comp_power_kw = (comp_load_pct / 100.0) * 26.0 if compressor_running else 0.0
|
|
|
|
# Refrigerant pressures (R410A-like behaviour)
|
|
if compressor_running:
|
|
high_p = 17.5 + (comp_load_pct / 100.0) * 3.5 + random.gauss(0, 0.2)
|
|
low_p = 5.2 - (comp_load_pct / 100.0) * 0.8 + random.gauss(0, 0.1)
|
|
superheat = 8.0 + random.gauss(0, 0.8)
|
|
subcooling = 4.5 + random.gauss(0, 0.4)
|
|
else:
|
|
# Pressures equalise when compressor is off
|
|
high_p = low_p = 8.0 + random.gauss(0, 0.1)
|
|
superheat = subcooling = 0.0
|
|
|
|
# ── Fan ───────────────────────────────────────────────────────
|
|
fan_rpm = (fan_pct / 100.0) * self.MAX_FAN_RPM
|
|
# Fan power: approximately cubic with speed fraction
|
|
fan_power_kw = 3.5 * (fan_pct / 100.0) ** 3
|
|
|
|
# ── Electrical ────────────────────────────────────────────────
|
|
total_power_kw = comp_power_kw + fan_power_kw + 0.4 # 0.4 kW for controls
|
|
power_factor = max(0.85, min(0.99, 0.93 + random.gauss(0, 0.01)))
|
|
voltage = self.RATED_VOLTAGE + random.gauss(0, 2.0)
|
|
current_a = (total_power_kw * 1_000) / (math.sqrt(3) * voltage * power_factor) if total_power_kw > 0 else 0.0
|
|
|
|
# ── COP & SHR ─────────────────────────────────────────────────
|
|
cop = (cooling_kw / total_power_kw) if total_power_kw > 0 else 0.0
|
|
shr = max(0.0, min(1.0, 0.92 + random.gauss(0, 0.01)))
|
|
|
|
# ── Humidity ──────────────────────────────────────────────────
|
|
supply_humidity = max(20.0, min(80.0, 45.0 + random.gauss(0, 2.0)))
|
|
return_humidity = max(30.0, min(80.0, 55.0 + random.gauss(0, 2.0)))
|
|
|
|
# ── Operating mode ────────────────────────────────────────────
|
|
if self._compressor_fault:
|
|
mode = "fan_only"
|
|
elif self._scenario == "FAN_DEGRADATION":
|
|
mode = "cooling" # still cooling, just degraded
|
|
else:
|
|
mode = "cooling"
|
|
|
|
return {
|
|
"state": "online",
|
|
"mode": mode,
|
|
"setpoint": 22.0,
|
|
# Thermal
|
|
"supply_temp": round(supply, 2),
|
|
"return_temp": round(return_temp, 2),
|
|
"supply_humidity": round(supply_humidity, 1),
|
|
"return_humidity": round(return_humidity, 1),
|
|
"airflow_cfm": round(airflow_cfm),
|
|
"filter_dp_pa": round(filter_dp, 1),
|
|
# Capacity
|
|
"cooling_capacity_kw": round(cooling_kw, 2),
|
|
"cooling_capacity_rated_kw": self.RATED_CAPACITY_KW,
|
|
"cooling_capacity_pct": round(cooling_cap_pct, 1),
|
|
"cop": round(cop, 2),
|
|
"sensible_heat_ratio": round(shr, 2),
|
|
# Compressor
|
|
"compressor_state": 1 if compressor_running else 0,
|
|
"compressor_load_pct": round(comp_load_pct, 1),
|
|
"compressor_power_kw": round(comp_power_kw, 2),
|
|
"compressor_run_hours": round(self._comp_hours),
|
|
"high_pressure_bar": round(max(0.0, high_p), 2),
|
|
"low_pressure_bar": round(max(0.0, low_p), 2),
|
|
"discharge_superheat_c": round(max(0.0, superheat), 1),
|
|
"liquid_subcooling_c": round(max(0.0, subcooling), 1),
|
|
# Fan
|
|
"fan_pct": round(fan_pct, 1),
|
|
"fan_rpm": round(fan_rpm),
|
|
"fan_power_kw": round(fan_power_kw, 2),
|
|
"fan_run_hours": round(self._fan_hours),
|
|
# Electrical
|
|
"total_unit_power_kw": round(total_power_kw, 2),
|
|
"input_voltage_v": round(voltage, 1),
|
|
"input_current_a": round(current_a, 1),
|
|
"power_factor": round(power_factor, 3),
|
|
}
|