from pydantic import BaseModel, conlist, validator from typing import Optional TYPICAL_MEASURE_TYPES = [ "wall_insulation", "roof_insulation", "ventilation", "floor_insulation", "windows", "fireplace", "heating", "hot_water", "low_energy_lighting", "secondary_heating", "solar_pv" ] SPECIFIC_MEASURES = [ # Specific measures # Walls "internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation", # Roof "loft_insulation", "flat_roof_insulation", "room_roof_insulation", # Floor "suspended_floor_insulation", "solid_floor_insulation", # Heating "boiler_upgrade", "high_heat_retention_storage_heater", "air_source_heat_pump", "secondary_heating", # Solar "solar_pv", # Windows Glazing "double_glazing", "secondary_glazing", # Mechanical ventilation "ventilation", # Other "low_energy_lighting", "fireplace", "hot_water", ] NON_INVASIVE_SPECIFIC_MEASURES = [ # Specific measures that will typically come from an energy assessment "trickle_vents", "draught_proofing", "mixed_glazing", # This covers partial double glazing and secondary glazing "cavity_extract_and_refill", ] # This allows us to extend high level categories for measures such as "wall_insulation" to the specific measures # such as "external_wall_insulation", "internal_wall_insulation", "cavity_wall_insulation" MEASURE_MAP = { "wall_insulation": [ "internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation", ], "roof_insulation": ["loft_insulation", "flat_roof_insulation", "room_roof_insulation"], "floor_insulation": ["suspended_floor_insulation", "solid_floor_insulation"], "heating": ["boiler_upgrade", "high_heat_retention_storage_heater", "air_source_heat_pump"], "windows": ["double_glazing", "secondary_glazing"], "heating_controls": ["roomstat_programmer_trvs", "time_temperature_zone_control"] } class PlanTriggerRequest(BaseModel): budget: Optional[float] = None goal: str housing_type: str goal_value: str portfolio_id: int trigger_file_path: str already_installed_file_path: Optional[str] = None patches_file_path: Optional[str] = None non_invasive_recommendations_file_path: Optional[str] = None exclusions: Optional[conlist(str, min_items=1)] = None inclusions: Optional[conlist(str, min_items=1)] = None scenario_name: Optional[str] = "" # If true, will allow us to create multiple plans for the same portfolio, whereas if this is false, if this property # exists in the portfolio, it will be ignored multi_plan: Optional[bool] = False # if False, allows optimisation to be switched off optimise: Optional[bool] = True _allowed_goals = {"Increasing EPC"} _allowed_housing_types = {"Social", "Private"} # Validator to ensure exclusions are within the pre-defined possibilities @validator('exclusions', each_item=True) def check_exclusions(cls, v): if v not in TYPICAL_MEASURE_TYPES + SPECIFIC_MEASURES + NON_INVASIVE_SPECIFIC_MEASURES: raise ValueError(f"{v} is not an allowed exclusion") return v @validator('inclusions', each_item=True) def check_inclusions(cls, v): if v not in TYPICAL_MEASURE_TYPES + SPECIFIC_MEASURES + NON_INVASIVE_SPECIFIC_MEASURES: raise ValueError(f"{v} is not an allowed inclusion") return v # Validator to ensure that the goal is within the pre-defined possibilities @validator('goal') def check_goal(cls, v): if v not in cls._allowed_goals: raise ValueError(f"{v} is not a valid goal") return v # Validator to ensure that the housing type is within the pre-defined possibilities @validator('housing_type') def check_housing_type(cls, v): if v not in cls._allowed_housing_types: raise ValueError(f"{v} is not a valid housing type") return v class MdsRequest(PlanTriggerRequest): # When creating the mds report, we allow an optional list of measures to select from. If this is passed, it will # cause the service to select the optimal package from the list of measures measures: Optional[conlist(str, min_items=1)] = None