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), }