import random from bots.base import BaseBot class GeneratorBot(BaseBot): """Diesel generator — normally on standby, runs on GENERATOR_FAILURE / ATS_TRANSFER scenarios.""" interval = 60 RATED_KW = 500.0 TANK_LITRES = 2_000.0 # full tank BURN_RATE_LH = 90.0 # litres/hour at full load (~45 l/h at 50% load) def __init__(self, site_id: str, gen_id: str) -> None: super().__init__() self.site_id = site_id self.gen_id = gen_id self._running = False self._testing = False self._fault = False self._fuel_l = self.TANK_LITRES * (0.75 + random.uniform(-0.05, 0.10)) self._run_hours = 1_200 + random.randint(0, 400) self._load_kw = 0.0 def get_topic(self) -> str: return f"bms/{self.site_id}/generator/{self.gen_id}" def set_scenario(self, name: str | None) -> None: super().set_scenario(name) self._running = name in ("GENERATOR_FAILURE", "ATS_TRANSFER") self._testing = (name == "GENERATOR_TEST") self._fault = (name == "GENERATOR_FAULT") if name is None: self._running = False self._testing = False self._fault = False def get_payload(self) -> dict: hour_inc = self.interval / 3_600 if self._fault: return { "state": "fault", "fuel_pct": round(self._fuel_l / self.TANK_LITRES * 100, 1), "fuel_litres": round(self._fuel_l, 0), "fuel_rate_lph": 0.0, "load_kw": 0.0, "load_pct": 0.0, "run_hours": round(self._run_hours, 1), "voltage_v": 0.0, "frequency_hz": 0.0, "engine_rpm": 0.0, "oil_pressure_bar": 0.0, "coolant_temp_c": 0.0, "exhaust_temp_c": 28.0, "alternator_temp_c": 30.0, "power_factor": 0.0, "battery_v": 0.0, } if self._running or self._testing: self._run_hours += hour_inc target_load = ( self.RATED_KW * (0.55 + random.gauss(0, 0.02)) if self._running else self.RATED_KW * (0.20 + random.gauss(0, 0.01)) ) self._load_kw = target_load load_frac = self._load_kw / self.RATED_KW # Burn fuel burn = self.BURN_RATE_LH * load_frac * hour_inc self._fuel_l = max(0.0, self._fuel_l - burn) # Low fuel scenario if self._scenario == "GENERATOR_LOW_FUEL": self._fuel_l = max(0.0, self._fuel_l - 8.0) state = "running" if self._running else "test" voltage = 415.0 + random.gauss(0, 1.5) frequency = 50.0 + random.gauss(0, 0.05) oil_p = 4.2 + random.gauss(0, 0.1) coolant = 78.0 + random.gauss(0, 2.0) battery_v = 24.3 + random.gauss(0, 0.1) # 1500 RPM at 50 Hz; slight droop under load engine_rpm = 1500.0 - (load_frac * 8.0) + random.gauss(0, 2.0) # Exhaust rises with load: ~180°C at idle, ~430°C at full load exhaust_temp = 180.0 + load_frac * 250.0 + random.gauss(0, 5.0) # Alternator winding temperature alt_temp = 45.0 + load_frac * 35.0 + random.gauss(0, 1.5) # Power factor typical for resistive/inductive DC loads pf = 0.87 + load_frac * 0.05 + random.gauss(0, 0.005) pf = round(min(0.99, max(0.80, pf)), 3) else: self._load_kw = 0.0 load_frac = 0.0 state = "standby" voltage = 0.0 frequency = 0.0 oil_p = 0.0 coolant = 25.0 + random.gauss(0, 1.0) battery_v = 27.1 + random.gauss(0, 0.1) # trickle-charged standby battery engine_rpm = 0.0 exhaust_temp = 28.0 + random.gauss(0, 1.0) # ambient exhaust stack temp alt_temp = 30.0 + random.gauss(0, 0.5) pf = 0.0 fuel_pct = min(100.0, self._fuel_l / self.TANK_LITRES * 100) # Actual fuel consumption rate for this interval fuel_rate = self.BURN_RATE_LH * load_frac if load_frac > 0 else 0.0 return { "state": state, "fuel_pct": round(fuel_pct, 1), "fuel_litres": round(self._fuel_l, 0), "fuel_rate_lph": round(fuel_rate, 1), "load_kw": round(self._load_kw, 1), "load_pct": round(load_frac * 100, 1), "run_hours": round(self._run_hours, 1), "voltage_v": round(voltage, 1), "frequency_hz": round(frequency, 2), "engine_rpm": round(engine_rpm, 0), "oil_pressure_bar": round(oil_p, 2), "coolant_temp_c": round(coolant, 1), "exhaust_temp_c": round(exhaust_temp, 1), "alternator_temp_c": round(alt_temp, 1), "power_factor": pf, "battery_v": round(battery_v, 2), }