Full parameter coverage compatible with game V2.2.25.213

- Add ~300 missing parameters with types, ranges, and units
- Add SPECIAL_VARIABLES frozenset to block non-param game endpoints
- Fix batch query to handle {"values": {...}} wrapper
- Fix str-typed params falling back to individual GET (batch returns int codes)
- Handle null/empty values from uninstalled subsystems
- Add is_readable field to NuconParameter for write-only support
- Add 57 write-only parameters: SCRAM, emergency stop, bay hatches/fuel loading,
  RODS_ALL_POS_ORDERED, MSCVs, steam turbine bypass/trip, ejector valves,
  VALVE_OPEN/CLOSE/OFF, chemistry rates, FUN_* event triggers
- Update get_all/get_all_readable/get_all_iter to skip write-only params
- __len__ now reflects readable param count (consistent with get_all)
- Update tests to skip write-only params in write test, handle None values

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dominik Moritz Roth 2026-03-12 16:27:57 +01:00
parent 5cfedceab7
commit 90616dcf69
2 changed files with 386 additions and 100 deletions

View File

@ -53,14 +53,30 @@ CoreState = str
CoolantCoreState = str
RodsState = str
# Known game endpoints that are not regular parameters (JSON/HTML data blobs, meta-commands)
SPECIAL_VARIABLES = frozenset({
'WEBSERVER_BATCH_GET',
'WEBSERVER_LIST_VARIABLES',
'WEBSERVER_LIST_VARIABLES_JSON',
'WEBSERVER_VIEW_VARIABLES',
'VALVE_PANEL_JSON',
'RESISTOR_BANKS_JSON',
'INSTALLED_LOOPS_JSON',
'INVENTORY_HTML',
'MAINTENANCE_REPORT_HTML',
'WEATHER_FORECAST_JSON',
})
class NuconParameter:
def __init__(self, nucon: 'Nucon', id: str, param_type: Type, is_writable: bool, min_val: Optional[Union[int, float]] = None, max_val: Optional[Union[int, float]] = None):
def __init__(self, nucon: 'Nucon', id: str, param_type: Type, is_writable: bool, min_val: Optional[Union[int, float]] = None, max_val: Optional[Union[int, float]] = None, unit: Optional[str] = None, is_readable: bool = True):
self.nucon = nucon
self.id = id
self.param_type = param_type
self.is_writable = is_writable
self.is_readable = is_readable
self.min_val = min_val
self.max_val = max_val
self.unit = unit
@property
def enum_type(self) -> Type[Enum]:
@ -86,6 +102,8 @@ class NuconParameter:
@property
def value(self):
if not self.is_readable:
raise AttributeError(f"Parameter {self.id} is write-only and cannot be read")
return self.nucon.get(self)
@value.setter
@ -99,7 +117,10 @@ class NuconParameter:
self.nucon.set(self, new_value, force)
def __repr__(self):
return f"NuconParameter(id='{self.id}', value={self.value}, param_type={self.param_type.__name__}, is_writable={self.is_writable})"
unit_str = f", unit='{self.unit}'" if self.unit else ""
value_str = f", value={self.value}" if self.is_readable else ""
rw_str = "write-only" if not self.is_readable else f"is_writable={self.is_writable}"
return f"NuconParameter(id='{self.id}'{value_str}, param_type={self.param_type.__name__}, {rw_str}{unit_str})"
def __str__(self):
return self.id
@ -112,103 +133,297 @@ class Nucon:
def _create_parameters(self) -> Dict[str, NuconParameter]:
param_values = {
'CORE_TEMP': (float, False, 0, 1000),
'CORE_TEMP_OPERATIVE': (float, False),
'CORE_TEMP_MAX': (float, False),
'CORE_TEMP_MIN': (float, False),
# --- Game metadata ---
'GAME_VERSION': (str, False),
'GAME_DIFFICULTY': (int, False),
'TIME': (str, False),
'TIME_STAMP': (str, False),
'TIME_DAY': (int, False),
'ALARMS_ACTIVE': (str, False),
'GAME_SIM_SPEED': (float, False),
'AMBIENT_TEMPERATURE': (float, False, None, None, '°C'),
# --- Core thermal/pressure ---
'CORE_TEMP': (float, False, 0, 1000, '°C'),
'CORE_TEMP_OPERATIVE': (float, False, None, None, '°C'),
'CORE_TEMP_MAX': (float, False, None, None, '°C'),
'CORE_TEMP_MIN': (float, False, None, None, '°C'),
'CORE_TEMP_RESIDUAL': (bool, False),
'CORE_PRESSURE': (float, False),
'CORE_PRESSURE_MAX': (float, False),
'CORE_PRESSURE_OPERATIVE': (float, False),
'CORE_INTEGRITY': (float, False),
'CORE_WEAR': (float, False),
'CORE_PRESSURE': (float, False, None, None, 'bar'),
'CORE_PRESSURE_MAX': (float, False, None, None, 'bar'),
'CORE_PRESSURE_OPERATIVE': (float, False, None, None, 'bar'),
'CORE_INTEGRITY': (float, False, 0, 100, '%'),
'CORE_WEAR': (float, False, 0, 100, '%'),
'CORE_STATE': (CoreState, False),
'CORE_STATE_CRITICALITY': (float, False),
'CORE_STATE_CRITICALITY': (float, False, 0, 1),
'CORE_CRITICAL_MASS_REACHED': (bool, False),
'CORE_CRITICAL_MASS_REACHED_COUNTER': (int, False),
'CORE_IMMINENT_FUSION': (bool, False),
'CORE_READY_FOR_START': (bool, False),
'CORE_STEAM_PRESENT': (bool, False),
'CORE_HIGH_STEAM_PRESENT': (bool, False),
'TIME': (str, False),
'TIME_STAMP': (str, False),
# --- Core physics ---
'CORE_FACTOR': (float, False),
'CORE_FACTOR_CHANGE': (float, False),
'CORE_OPERATION_MODE': (str, True),
'CORE_IODINE_GENERATION': (float, False),
'CORE_IODINE_CUMULATIVE': (float, False),
'CORE_XENON_GENERATION': (float, False),
'CORE_XENON_CUMULATIVE': (float, False),
# --- Core fuel / bays (9 bays) ---
'CORE_FUEL_AVG_FISSIONABLE': (float, False, 0, 100, '%'),
'CORE_FUEL_AVG_TEMPERATURE': (float, False, None, None, '°C'),
'CORE_FUEL_AVG_POWER_FACTOR': (float, False, 0, 1),
**{f'CORE_FUEL_{i}_TEMPERATURE': (float, False, None, None, '°C') for i in range(1, 10)},
**{f'CORE_FUEL_{i}_FISSIONABLE': (float, False, 0, 100, '%') for i in range(1, 10)},
**{f'CORE_FUEL_{i}_POWER_FACTOR': (float, False, 0, 1) for i in range(1, 10)},
**{f'CORE_BAY_{i}_STATE': (str, False) for i in range(1, 10)},
**{f'CORE_BAY_{i}_HATCH_OPEN': (bool, False) for i in range(1, 10)},
# --- Core pool ---
'CORE_POOL_PUMP': (int, False), # command-style (LOAD/OFF/REMOVE), not freely settable
'CORE_POOL_COOLANT_TANK_VOLUME': (float, False),
'CORE_PRIMARY_CIRCUIT_COOLING_TANK_VOLUME': (float, False),
'CORE_EXTERNAL_COOLANT_RESERVOIR_VOLUME': (float, False),
# --- Primary coolant (core loop) ---
'COOLANT_CORE_STATE': (CoolantCoreState, False),
'COOLANT_CORE_PRESSURE': (float, False),
'COOLANT_CORE_MAX_PRESSURE': (float, False),
'COOLANT_CORE_VESSEL_TEMPERATURE': (float, False),
'COOLANT_CORE_PRESSURE': (float, False, None, None, 'bar'),
'COOLANT_CORE_MAX_PRESSURE': (float, False, None, None, 'bar'),
'COOLANT_CORE_VESSEL_TEMPERATURE': (float, False, None, None, '°C'),
'COOLANT_CORE_QUANTITY_IN_VESSEL': (float, False),
'COOLANT_CORE_PRIMARY_LOOP_LEVEL': (float, False),
'COOLANT_CORE_PRIMARY_LOOP_LEVEL': (float, False, 0, 100, '%'),
'COOLANT_CORE_FLOW_IN': (float, False),
'COOLANT_CORE_FLOW_OUT': (float, False),
'COOLANT_CORE_FLOW_SPEED': (float, False),
'COOLANT_CORE_FLOW_ORDERED_SPEED': (float, False),
'COOLANT_CORE_FLOW_REACHED_SPEED': (bool, False),
'COOLANT_CORE_QUANTITY_CIRCULATION_PUMPS_PRESENT': (int, False),
'COOLANT_CORE_QUANTITY_FREIGHT_PUMPS_PRESENT': (int, False),
'COOLANT_CORE_CIRCULATION_PUMP_0_STATUS': (PumpStatus, False),
'COOLANT_CORE_CIRCULATION_PUMP_1_STATUS': (PumpStatus, False),
'COOLANT_CORE_CIRCULATION_PUMP_2_STATUS': (PumpStatus, False),
'COOLANT_CORE_CIRCULATION_PUMP_0_DRY_STATUS': (PumpDryStatus, False),
'COOLANT_CORE_CIRCULATION_PUMP_1_DRY_STATUS': (PumpDryStatus, False),
'COOLANT_CORE_CIRCULATION_PUMP_2_DRY_STATUS': (PumpDryStatus, False),
'COOLANT_CORE_CIRCULATION_PUMP_0_OVERLOAD_STATUS': (PumpOverloadStatus, False),
'COOLANT_CORE_CIRCULATION_PUMP_1_OVERLOAD_STATUS': (PumpOverloadStatus, False),
'COOLANT_CORE_CIRCULATION_PUMP_2_OVERLOAD_STATUS': (PumpOverloadStatus, False),
'COOLANT_CORE_CIRCULATION_PUMP_0_ORDERED_SPEED': (float, False),
'COOLANT_CORE_CIRCULATION_PUMP_1_ORDERED_SPEED': (float, False),
'COOLANT_CORE_CIRCULATION_PUMP_2_ORDERED_SPEED': (float, False),
'COOLANT_CORE_CIRCULATION_PUMP_0_SPEED': (float, False),
'COOLANT_CORE_CIRCULATION_PUMP_1_SPEED': (float, False),
'COOLANT_CORE_CIRCULATION_PUMP_2_SPEED': (float, False),
**{f'COOLANT_CORE_CIRCULATION_PUMP_{i}_STATUS': (PumpStatus, False) for i in range(3)},
**{f'COOLANT_CORE_CIRCULATION_PUMP_{i}_DRY_STATUS': (PumpDryStatus, False) for i in range(3)},
**{f'COOLANT_CORE_CIRCULATION_PUMP_{i}_OVERLOAD_STATUS': (PumpOverloadStatus, False) for i in range(3)},
**{f'COOLANT_CORE_CIRCULATION_PUMP_{i}_ORDERED_SPEED': (float, True, 0, 100) for i in range(3)},
**{f'COOLANT_CORE_CIRCULATION_PUMP_{i}_SPEED': (float, False, 0, 100) for i in range(3)},
**{f'COOLANT_CORE_CIRCULATION_PUMP_{i}_CAPACITY': (float, False) for i in range(3)},
# --- Secondary coolant loops ---
**{f'COOLANT_SEC_CIRCULATION_PUMP_{i}_STATUS': (PumpStatus, False) for i in range(3)},
**{f'COOLANT_SEC_CIRCULATION_PUMP_{i}_DRY_STATUS': (PumpDryStatus, False) for i in range(3)},
**{f'COOLANT_SEC_CIRCULATION_PUMP_{i}_OVERLOAD_STATUS': (PumpOverloadStatus, False) for i in range(3)},
**{f'COOLANT_SEC_CIRCULATION_PUMP_{i}_ORDERED_SPEED': (float, True, 0, 100) for i in range(3)},
**{f'COOLANT_SEC_CIRCULATION_PUMP_{i}_SPEED': (float, False, 0, 100) for i in range(3)},
**{f'COOLANT_SEC_CIRCULATION_PUMP_{i}_CAPACITY': (float, False) for i in range(3)},
**{f'COOLANT_SEC_{i}_VOLUME': (float, False) for i in range(3)},
**{f'COOLANT_SEC_{i}_LIQUID_VOLUME': (float, False) for i in range(3)},
**{f'COOLANT_SEC_{i}_PRESSURE': (float, False, None, None, 'bar') for i in range(3)},
**{f'COOLANT_SEC_{i}_TEMPERATURE': (float, False, None, None, '°C') for i in range(3)},
# --- Control rods ---
'RODS_STATUS': (RodsState, False),
#'RODS_MOVEMENT_SPEED': (float, False),
#'RODS_MOVEMENT_SPEED_DECREASED_HIGH_TEMPERATURE': (bool, False),
#'RODS_DEFORMED': (bool, False),
#'RODS_TEMPERATURE': (float, False),
#'RODS_MAX_TEMPERATURE': (float, False),
#'RODS_POS_ORDERED': (float, True),
#'RODS_POS_ACTUAL': (float, False),
#'RODS_POS_REACHED': (bool, False),
#'RODS_QUANTITY': (int, False),
#'RODS_ALIGNED': (bool, False),
'GENERATOR_0_KW': (float, False),
'GENERATOR_1_KW': (float, False),
'GENERATOR_2_KW': (float, False),
'GENERATOR_0_V': (float, False),
'GENERATOR_1_V': (float, False),
'GENERATOR_2_V': (float, False),
'GENERATOR_0_A': (float, False),
'GENERATOR_1_A': (float, False),
'GENERATOR_2_A': (float, False),
'GENERATOR_0_HERTZ': (float, False),
'GENERATOR_1_HERTZ': (float, False),
'GENERATOR_2_HERTZ': (float, False),
'GENERATOR_0_BREAKER': (BreakerStatus, False),
'GENERATOR_1_BREAKER': (BreakerStatus, False),
'GENERATOR_2_BREAKER': (BreakerStatus, False),
'STEAM_TURBINE_0_RPM': (float, False),
'STEAM_TURBINE_1_RPM': (float, False),
'STEAM_TURBINE_2_RPM': (float, False),
'STEAM_TURBINE_0_TEMPERATURE': (float, False),
'STEAM_TURBINE_1_TEMPERATURE': (float, False),
'STEAM_TURBINE_2_TEMPERATURE': (float, False),
'STEAM_TURBINE_0_PRESSURE': (float, False),
'STEAM_TURBINE_1_PRESSURE': (float, False),
'STEAM_TURBINE_2_PRESSURE': (float, False),
'RODS_MOVEMENT_SPEED': (float, False),
'RODS_MOVEMENT_SPEED_DECREASED_HIGH_TEMPERATURE': (bool, False),
'RODS_DEFORMED': (bool, False),
'RODS_TEMPERATURE': (float, False, None, None, '°C'),
'RODS_MAX_TEMPERATURE': (float, False, None, None, '°C'),
'RODS_POS_ORDERED': (float, False, 0, 100, '%'),
'RODS_POS_ACTUAL': (float, False, 0, 100, '%'),
'RODS_POS_REACHED': (bool, False),
'RODS_QUANTITY': (int, False),
'RODS_ALIGNED': (int, False),
**{f'ROD_BANK_POS_{i}_ORDERED': (float, True, 0, 100, '%') for i in range(9)},
**{f'ROD_BANK_POS_{i}_ACTUAL': (float, False, 0, 100, '%') for i in range(9)},
# --- Steam generators ---
**{f'STEAM_GEN_{i}_STATUS': (PumpStatus, False) for i in range(3)},
**{f'STEAM_GEN_{i}_OUTLET': (float, False) for i in range(3)},
**{f'STEAM_GEN_{i}_EVAPORATED': (float, False) for i in range(3)},
**{f'STEAM_GEN_{i}_BOILING_POINT': (float, False, None, None, '°C') for i in range(3)},
**{f'STEAM_GEN_{i}_INLET': (float, False) for i in range(3)},
**{f'STEAM_GEN_{i}_RETURN_FLOW_PLUS_CONDENSED': (float, False) for i in range(3)},
**{f'STEAM_GEN_{i}_VENT_SWITCH': (bool, True) for i in range(3)},
# --- Steam turbines ---
**{f'STEAM_TURBINE_{i}_RPM': (float, False) for i in range(3)},
**{f'STEAM_TURBINE_{i}_TEMPERATURE': (float, False, None, None, '°C') for i in range(3)},
**{f'STEAM_TURBINE_{i}_PRESSURE': (float, False, None, None, 'bar') for i in range(3)},
**{f'STEAM_TURBINE_{i}_TORQUE': (float, False) for i in range(3)},
**{f'STEAM_TURBINE_{i}_INSTALLED': (bool, False) for i in range(3)},
**{f'STEAM_TURBINE_{i}_BYPASS_ACTUAL': (float, False, 0, 100, '%') for i in range(3)},
# --- MSCV valves ---
**{f'MSCV_{i}_OPENING_ACTUAL': (float, False, 0, 100, '%') for i in range(3)},
# --- Main steam valves ---
**{f'VALVE_M0{i}_OPEN': (float, False, 0, 100, '%') for i in range(1, 4)},
# --- Condenser ---
'CONDENSER_VACUUM': (float, False, 0, 100, '%'),
'CONDENSER_VACUUM_RELIEF_VALVE_OPENING': (float, False, 0, 100, '%'),
'CONDENSER_VACUUM_PUMP_ACTIVE': (bool, False),
'CONDENSER_VACUUM_PUMP_MODE': (str, True),
'CONDENSER_VACUUM_PUMP_POWER': (float, False, 0, 100, '%'),
'CONDENSER_TEMPERATURE': (float, False, None, None, '°C'),
'CONDENSER_VOLUME': (float, False),
'CONDENSER_VAPOR_VOLUME': (float, False),
'CONDENSER_CONDENSATE_FLOW_RATE': (float, False),
'CONDENSER_EXTRACTION_FLOW_RATE': (float, False),
'CONDENSER_COOLANT_EVAPORATED': (float, False),
'CONDENSER_PRESSURE': (float, False, None, None, 'bar'),
'CONDENSER_CIRCULATION_PUMP_ACTIVE': (bool, False),
'CONDENSER_CIRCULATION_PUMP_OVERLOAD_STATUS': (bool, False),
'CONDENSER_CIRCULATION_PUMP_SPEED': (float, False, 0, 100),
'CONDENSER_CIRCULATION_PUMP_ORDERED_SPEED': (float, True, 0, 100),
'CONDENSER_CIRCULATION_PUMP_SWITCH': (bool, True),
# --- Vacuum retention / steam ejector ---
'VACUUM_RETENTION_TANK_VOLUME': (float, False),
'VACUUM_RETENTION_TANK_PRESSURE': (float, False, None, None, 'bar'),
'STEAM_EJECTOR_MOTIVE': (float, False),
'STEAM_EJECTOR_STARTUP_MOTIVE_VALVE_ORDERED': (float, False, 0, 100, '%'),
'STEAM_EJECTOR_STARTUP_MOTIVE_VALVE_ACTUAL': (float, False, 0, 100, '%'),
'STEAM_EJECTOR_OPERATIONAL_MOTIVE_VALVE_ORDERED': (float, False, 0, 100, '%'),
'STEAM_EJECTOR_OPERATIONAL_MOTIVE_VALVE_ACTUAL': (float, False, 0, 100, '%'),
'STEAM_EJECTOR_CONDENSER_RETURN_VALVE_ORDERED': (float, False, 0, 100, '%'),
'STEAM_EJECTOR_CONDENSER_RETURN_VALVE_ACTUAL': (float, False, 0, 100, '%'),
# --- Freight pumps ---
'FREIGHT_PUMP_CONDENSER_ACTIVE': (bool, False),
'FREIGHT_PUMP_INTERNAL_ACTIVE': (bool, False),
'FREIGHT_PUMP_EXTERNAL_ACTIVE': (bool, False),
'FREIGHT_PUMP_FEEDWATER_ACTIVE': (bool, False),
'FREIGHT_PUMP_CONDENSER_SWITCH': (bool, True),
'FREIGHT_PUMP_INTERNAL_SWITCH': (bool, True),
'FREIGHT_PUMP_EXTERNAL_SWITCH': (bool, True),
'FREIGHT_PUMP_FEEDWATER_SWITCH': (bool, True),
# --- Generators ---
**{f'GENERATOR_{i}_KW': (float, False, None, None, 'kW') for i in range(3)},
**{f'GENERATOR_{i}_V': (float, False, None, None, 'V') for i in range(3)},
**{f'GENERATOR_{i}_A': (float, False, None, None, 'A') for i in range(3)},
**{f'GENERATOR_{i}_HERTZ': (float, False, None, None, 'Hz') for i in range(3)},
**{f'GENERATOR_{i}_BREAKER': (BreakerStatus, False) for i in range(3)},
# --- Resistor banks ---
'RES_DIVERT_SURPLUS_FROM_MW': (float, False, None, None, 'MW'),
'RES_EFFECTIVELY_DERIVED_ENERGY_MW': (float, False, None, None, 'MW'),
'RES_ABSORPTION_CAPACITY_MW': (float, False, None, None, 'MW'),
**{f'RESISTOR_BANK_0{i}_SWITCH': (bool, True) for i in range(1, 5)},
'RESISTOR_BANKS_MAIN_SWITCH': (bool, True),
# --- Emergency generators ---
'EMERGENCY_GENERATOR_1_MODE': (str, True),
'EMERGENCY_GENERATOR_1_STATUS': (str, False),
'EMERGENCY_GENERATOR_1_PRESSURIZER': (str, False),
'EMERGENCY_GENERATOR_1_FUEL': (float, False),
'EMERGENCY_GENERATOR_1_MAINTENANCE_NEEDED': (bool, False),
'EMERGENCY_GENERATOR_2_MODE': (str, True),
'EMERGENCY_GENERATOR_2_STATUS': (str, False),
'EMERGENCY_GENERATOR_2_PRESSURIZER': (str, False),
'EMERGENCY_GENERATOR_2_FUEL': (float, False),
'EMERGENCY_GENERATOR_2_MAINTENANCE_NEEDED': (bool, False),
# --- Power ---
'POWER_FROM_TURBINE_KW': (float, False, None, None, 'kW'),
'POWER_FROM_EXTERNAL_KW': (float, False, None, None, 'kW'),
'EMERGENCY_GENERATOR_POWER_OUTPUT_KW': (float, False, None, None, 'kW'),
'EMERGENCY_BATTERIES_POWER_OUTPUT_KW': (float, False, None, None, 'kW'),
'EMERGENCY_BATTERIES_MODE': (int, True),
'POWER_DEMAND_MW': (float, False, None, None, 'MW'),
'POWER_MAX_THEORETICAL_FINAL_PLANT_OUTPUT_MW': (float, False, None, None, 'MW'),
'POWER_MAX_THEORETICAL_PLANT_OUTPUT_MW': (float, False, None, None, 'MW'),
# --- Chemistry ---
'CHEM_TRUCK_IN_ZONE': (bool, False),
'CHEM_TRUCK_CONNECTED': (bool, False),
'CHEM_BORON_DOSAGE_ORDERED': (float, False),
'CHEM_BORON_DOSAGE_ACTUAL': (float, False),
'CHEM_BORON_FILTER_ORDERED': (float, False),
'CHEM_BORON_FILTER_ACTUAL': (float, False),
'CHEM_BORON_PPM': (float, False, None, None, 'ppm'),
'CHEMICAL_DOSING_PUMP_STATUS': (PumpStatus, False),
'CHEMICAL_DOSING_PUMP_DRY_STATUS': (PumpDryStatus, False),
'CHEMICAL_DOSING_PUMP_OVERLOAD_STATUS': (PumpOverloadStatus, False),
'CHEMICAL_FILTER_PUMP_STATUS': (PumpStatus, False),
'CHEMICAL_FILTER_PUMP_DRY_STATUS': (PumpDryStatus, False),
'CHEMICAL_FILTER_PUMP_OVERLOAD_STATUS': (PumpOverloadStatus, False),
'CHEMICAL_CLEANING_PUMP_STATUS': (PumpStatus, False),
'CHEMICAL_CLEANING_PUMP_DRY_STATUS': (PumpDryStatus, False),
'CHEMICAL_CLEANING_PUMP_OVERLOAD_STATUS': (PumpOverloadStatus, False),
}
return {
write_only_values = {
# --- Core actions ---
'CORE_SCRAM_BUTTON': (bool, True),
'CORE_EMERGENCY_STOP': (bool, True),
'CORE_END_EMERGENCY_STOP': (bool, True),
'RESET_AO': (bool, True),
# --- Core bay commands (9 bays) ---
**{f'CORE_BAY_{i}_HATCH': (bool, True) for i in range(1, 10)},
**{f'CORE_BAY_{i}_FUEL_LOADING': (int, True) for i in range(1, 10)},
# --- Rods group command ---
'RODS_ALL_POS_ORDERED': (float, True, 0, 100, '%'),
# --- MSCVs (Main Steam Control Valves) ---
**{f'MSCV_{i}_OPENING_ORDERED': (float, True, 0, 100, '%') for i in range(3)},
# --- Steam turbine ---
'STEAM_TURBINE_TRIP': (bool, True),
**{f'STEAM_TURBINE_{i}_BYPASS_ORDERED': (float, True, 0, 100, '%') for i in range(3)},
# --- Steam ejector valves ---
'STEAM_EJECTOR_CONDENSER_RETURN_VALVE': (bool, True),
'STEAM_EJECTOR_OPERATIONAL_MOTIVE_VALVE': (bool, True),
'STEAM_EJECTOR_STARTUP_MOTIVE_VALVE': (bool, True),
# --- Valve commands (take valve name as value) ---
'VALVE_OPEN': (str, True),
'VALVE_CLOSE': (str, True),
'VALVE_OFF': (str, True),
# --- Condenser / emergency generators ---
'CONDENSER_VACUUM_PUMP_START_STOP': (bool, True),
'EMERGENCY_GENERATOR_1_START_STOP': (bool, True),
'EMERGENCY_GENERATOR_2_START_STOP': (bool, True),
# --- Chemistry ---
'CHEM_BORON_DOSAGE_ORDERED_RATE': (float, True, 0, 100, '%'),
'CHEM_BORON_FILTER_ORDERED_SPEED': (float, True, 0, 100, '%'),
# --- Fun / event triggers ---
'FUN_IS_ENABLED': (bool, True),
'FUN_REQUEST_ENABLE': (bool, True),
'FUN_AO_SABOTAGE_ONCE': (bool, True),
'FUN_AO_SABOTAGE_TIME': (float, True),
'FUN_BANK_ROBBERY': (bool, True),
'FUN_BREAKER_TRIP': (bool, True),
'FUN_DECREASE_INTEGRITY': (float, True),
'FUN_FIRE_DRILL': (bool, True),
'FUN_IODINE_SPILL': (bool, True),
'FUN_OIL_SPILL': (bool, True),
'FUN_PUMP_JAM': (bool, True),
'FUN_SHOW_MESSAGE': (str, True),
'FUN_TOGGLE_RANDOM_SWITCH': (bool, True),
'FUN_TRIGGER_AUDIT': (bool, True),
'FUN_WEATHER_CONTROL': (str, True),
'FUN_XENON_SPILL': (bool, True),
}
params = {
name: NuconParameter(self, name, *values)
for name, values in param_values.items()
}
for name, values in write_only_values.items():
params[name] = NuconParameter(self, name, *values, is_readable=False)
return params
def get(self, parameter: Union[str, NuconParameter]) -> Union[float, int, bool, str, Enum]:
if isinstance(parameter, str):
parameter = self._parameters[parameter]
if self.dummy_mode:
return self._get_dummy_value(parameter)
value = self._query(parameter)
def _parse_value(self, parameter: NuconParameter, value: str) -> Union[float, int, bool, str, Enum, None]:
if value == '' or value is None or (isinstance(value, str) and value.lower() == 'null'):
return None
if parameter.enum_type:
try:
return parameter.enum_type(value)
@ -227,6 +442,16 @@ class Nucon:
except ValueError as e:
raise ValueError(f"Failed to convert {value} to {parameter.param_type.__name__} for parameter {parameter.id}: {e}")
def get(self, parameter: Union[str, NuconParameter]) -> Union[float, int, bool, str, Enum]:
if isinstance(parameter, str):
parameter = self._parameters[parameter]
if self.dummy_mode:
return self._get_dummy_value(parameter)
value = self._query(parameter)
return self._parse_value(parameter, value)
def set(self, parameter: Union[str, NuconParameter], value: Union[float, int, bool, str, Enum], force: bool = False) -> None:
if isinstance(parameter, str):
parameter = self._parameters[parameter]
@ -262,6 +487,44 @@ class Nucon:
return response.text.strip()
def _batch_query(self, param_names: List[str]) -> Dict[str, str]:
value = ','.join(param_names)
response = requests.get(self.base_url, params={"variable": "WEBSERVER_BATCH_GET", "value": value})
if response.status_code != 200:
raise Exception(f"Batch query failed. Status code: {response.status_code}")
data = response.json()
# Game returns {"values": {...}, "errors": {...}} wrapper
if isinstance(data, dict) and 'values' in data:
data = data['values']
return {k.upper(): ('' if v is None else str(v)) for k, v in data.items()}
def get_game_variable_names(self) -> List[str]:
response = requests.get(self.base_url, params={"variable": "WEBSERVER_LIST_VARIABLES"})
if response.status_code != 200:
raise Exception(f"Failed to get variable list. Status code: {response.status_code}")
names = []
for line in response.text.strip().splitlines():
line = line.strip()
if not line:
continue
# Format is "GET:var1,var2,..." or "POST:var1,var2,..."
if ':' in line:
line = line.split(':', 1)[1]
names.extend(v.strip() for v in line.split(',') if v.strip())
# Return unique names, excluding known special (non-parameter) endpoints
seen = set()
result = []
for name in names:
upper = name.upper()
if upper not in seen and upper not in SPECIAL_VARIABLES:
seen.add(upper)
result.append(name)
return result
def _set_value(self, parameter: NuconParameter, value: str) -> None:
response = requests.post(self.base_url, params={"variable": parameter.id, "value": value})
@ -300,13 +563,30 @@ class Nucon:
return dict(self.get_multiple_iter(parameters))
def get_all_iter(self) -> Iterator[Tuple[str, Union[float, int, bool, str, Enum]]]:
return self.get_multiple_iter(self._parameters.keys())
if self.dummy_mode:
yield from self.get_multiple_iter(self._parameters.keys())
return
try:
raw = self._batch_query(list(self._parameters.keys()))
except Exception:
raw = {}
for name, param in self._parameters.items():
if not param.is_readable:
continue
raw_value = raw.get(name)
# Batch query returns int codes for str-typed params; use individual query for those
if raw_value is not None and param.param_type != str:
yield name, self._parse_value(param, raw_value)
else:
yield name, self.get(param)
def get_all(self) -> Dict[str, Union[float, int, bool, str, Enum]]:
return dict(self.get_all_iter())
def get_all_readable(self) -> List[NuconParameter]:
return {name: param for name, param in self._parameters.items()}
return {name: param for name, param in self._parameters.items() if param.is_readable}
def get_all_writable(self) -> List[NuconParameter]:
return {name: param for name, param in self._parameters.items() if param.is_writable}
@ -328,4 +608,4 @@ class Nucon:
return list(super().__dir__()) + list(self._parameters.keys())
def __len__(self):
return len(self._parameters)
return sum(1 for p in self._parameters.values() if p.is_readable)

View File

@ -15,39 +15,31 @@ def test_read_all_parameters(nucon):
assert len(all_params) == len(nucon)
for param, value in all_params.items():
param_type = nucon.get_type(param)
assert isinstance(value, param_type), f"Parameter {param.id} has incorrect type. Expected {param_type}, got {type(value)}"
if value is None:
continue # Some params return null/empty when subsystem not installed
assert isinstance(value, param_type), f"Parameter {param} has incorrect type. Expected {param_type}, got {type(value)}"
if param_type == float and value.is_integer() and WARN_FLOAT_COULD_BE_INT:
warnings.warn(f"Parameter {param} is a float but has an integer value: {value}")
if param_type == str:
try:
float(value)
raise ValueError(f"Parameter {param} is a string that looks like a number: {value}")
except ValueError:
pass
try:
bool(value.lower())
raise ValueError(f"Parameter {param} is a string that looks like a boolean: {value}")
except ValueError:
pass
def test_write_writable_parameters(nucon):
writable_params = nucon.get_all_writable()
for param in writable_params:
for param in writable_params.values():
if not param.is_readable:
continue # Skip write-only params (can't read back to verify, and actions like SCRAM are dangerous to trigger)
current_value = param.value
if current_value is None:
continue # Skip params that return null (subsystem not installed)
param.value = current_value
assert param.value == current_value, f"Failed to write to parameter {param.id}"
def test_non_writable_parameters(nucon):
non_writable_params = [param for param in nucon if not param.is_writable]
non_writable_params = [param for param in nucon.get_all_readable().values() if not param.is_writable]
for param in non_writable_params:
# Test that normal set raises an error
with pytest.raises(ValueError, match=f"Parameter {param.id} is not writable"):
param.value = param.value # Attempt to write the current value
# Test that force_set is refused by the webserver
current_value = param.value
with pytest.raises(Exception, match=f"Failed to set parameter {param.id}"):
nucon.set(param, current_value, force=True)
# Note: the game accepts force writes silently (does not return an error)
def test_enum_parameters(nucon):
pump_status = nucon.COOLANT_CORE_CIRCULATION_PUMP_0_STATUS.value
@ -76,5 +68,19 @@ def test_get_multiple_parameters(nucon):
param_type = nucon.get_type(param)
assert isinstance(value, param_type)
def test_param_coverage(nucon):
raw_game_vars = nucon.get_game_variable_names()
game_vars = set(v.upper() for v in raw_game_vars)
our_vars = set(nucon._parameters.keys())
assert game_vars, f"Got empty variable list from game. Raw sample: {raw_game_vars[:5]}"
missing_from_game = our_vars - game_vars
assert not missing_from_game, f"Params defined in NuCon but not found in game: {missing_from_game}\nGame var sample: {sorted(game_vars)[:10]}"
not_in_nucon = game_vars - our_vars
if not_in_nucon:
warnings.warn(f"Game exposes {len(not_in_nucon)} params not defined in NuCon: {sorted(not_in_nucon)}")
if __name__ == "__main__":
pytest.main()