diff --git a/backend/apis/GoogleSolarApi.py b/backend/apis/GoogleSolarApi.py index 0faf8e72..d6bc58df 100644 --- a/backend/apis/GoogleSolarApi.py +++ b/backend/apis/GoogleSolarApi.py @@ -159,19 +159,42 @@ class GoogleSolarApi: # We now start finding the solar panel configurations self.optimise_solar_configuration(energy_consumption=energy_consumption, is_building=is_building) - def save_to_db(self, session, uprns_to_location): + def save_to_db(self, session, uprns_to_location, scenario_type): if self.insights_data is None: raise ValueError("No api data to store") + if scenario_type not in ["unit", "building"]: + raise Exception("Invalid scenario type. Must be either 'unit' or 'building'") + if not self.need_to_store: return logger.info("Storing to database") + scenarios_data = self.panel_performance.head(1)[ + ["n_panels", "yearly_dc_energy", "total_cost", "panneled_roof_area", "array_warrage", + "initial_ac_kwh_per_year", "lifetime_ac_kwh", "roi"] + ].rename( + columns={ + "n_panels": "number_panels", + "yearly_dc_energy": "yearly_dc_kwh", + "total_cost": "cost", + "panneled_roof_area": "panelled_roof_area", + "array_warrage": "array_kwhp", + "initial_ac_kwh_per_year": "yearly_ac_kwh", + "lifetime_ac_kwh": "lifetime_ac_kwh", + } + ) + + # Adding missing fields with default values + scenarios_data["is_default"] = True + scenarios_data["scenario_type"] = scenario_type + store_batch_data( session=session, api_data=self.insights_data, - uprns_to_location=uprns_to_location + uprns_to_location=uprns_to_location, + scenarios_data=scenarios_data ) @staticmethod @@ -286,13 +309,21 @@ class GoogleSolarApi: roi = (generation_value + surplus_value) / panel_config["total_cost"] generation_deficit = surplus_value + # Calculate expected payback years + if generation_value > 0: + expected_payback_years = panel_config["total_cost"] / ( + generation_value / self.installation_life_span) + else: + expected_payback_years = None # or some high value indicating no payback + # Generation deficit tells us how much more energy we need to meet the generation demand. roi_results.append( { "n_panels": panel_config["n_panels"], "roi": roi, "generation_value": generation_value, - "generation_deficit": generation_deficit + "generation_deficit": generation_deficit, + "expected_payback_years": expected_payback_years } ) @@ -309,6 +340,8 @@ class GoogleSolarApi: ["roi", "generation_deficit", "generation_value"], ascending=[False, True, False] ) + panel_performance["expected_payback_years"] = np.ceil(panel_performance["expected_payback_years"]).astype(int) + self.panel_performance = panel_performance def exclude_north_facing_segments(self): diff --git a/backend/app/db/functions/solar_functions.py b/backend/app/db/functions/solar_functions.py index e8bba137..bf541bb4 100644 --- a/backend/app/db/functions/solar_functions.py +++ b/backend/app/db/functions/solar_functions.py @@ -2,7 +2,7 @@ import datetime import pytz from sqlalchemy.orm import Session from sqlalchemy.orm.exc import NoResultFound -from backend.app.db.models.solar import Solar +from backend.app.db.models.solar import Solar, SolarScenario def get_solar_data(session: Session, longitude: float = None, latitude: float = None, uprn: str = None): @@ -35,28 +35,56 @@ def get_solar_data(session: Session, longitude: float = None, latitude: float = return None, None, False -def store_batch_data(session: Session, api_data: dict, uprns_to_location: list): +def store_batch_data(session: Session, api_data: dict, uprns_to_location: list, scenarios_data: list): """ This function will store the API data to the solar table against all of the UPRNs with longitude and latitude. :param session: The database session :param api_data: The API data to store - :param data_list: A list of dictionaries containing uprn, longitude, and latitude + :param uprns_to_location: A list of dictionaries containing uprn, longitude, and latitude + :param scenarios_data: A list of dictionaries containing scenario data for each UPRN """ try: - # Convert the data_list to a list of dicts for bulk insert - records_to_update = [] + # Insert data into the Solar table and get the IDs + solar_records = [] for data in uprns_to_location: - record = { - 'uprn': data['uprn'], - 'longitude': data['longitude'], - 'latitude': data['latitude'], - 'google_api_response': api_data, - 'updated_at': datetime.datetime.now(pytz.utc) - } - records_to_update.append(record) + solar_record = Solar( + uprn=data['uprn'], + longitude=data['longitude'], + latitude=data['latitude'], + google_api_response=api_data, + updated_at=datetime.datetime.now(pytz.utc) + ) + solar_records.append(solar_record) - # Perform bulk insert or update - session.bulk_insert_mappings(Solar, records_to_update) + session.bulk_save_objects(solar_records) + session.commit() + + # Retrieve the IDs of the inserted records + inserted_ids = [record.id for record in solar_records] + + # Prepare the data for SolarScenario + scenario_records = [] + for index, solar_id in enumerate(inserted_ids): + scenarios = scenarios_data[index] # Assuming scenarios_data has the same order as uprns_to_location + for scenario in scenarios: + scenario_record = SolarScenario( + solar_id=solar_id, + scenario_type=scenario['scenario_type'], + number_panels=scenario['number_panels'], + array_kwhp=scenario['array_kwhp'], + lifetime_dc_kwh=scenario['lifetime_dc_kwh'], + yearly_dc_kwh=scenario['yearly_dc_kwh'], + lifetime_ac_kwh=scenario.get('lifetime_ac_kwh'), # Optional field + yearly_ac_kwh=scenario.get('yearly_ac_kwh'), # Optional field + cost=scenario['cost'], + expected_payback_years=scenario.get('expected_payback_years'), # Optional field + panelled_roof_area=scenario['panelled_roof_area'], + is_default=scenario['is_default'] + ) + scenario_records.append(scenario_record) + + # Insert data into the SolarScenario table + session.bulk_save_objects(scenario_records) session.commit() except Exception as e: diff --git a/backend/app/db/models/solar.py b/backend/app/db/models/solar.py index 9cc51e51..88372bd3 100644 --- a/backend/app/db/models/solar.py +++ b/backend/app/db/models/solar.py @@ -1,6 +1,7 @@ import datetime import pytz -from sqlalchemy import Column, Integer, Float, DateTime, JSON +from enum import Enum as PyEnum +from sqlalchemy import Column, Integer, Float, DateTime, JSON, BigInteger, ForeignKey, Enum, Boolean from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() @@ -19,3 +20,26 @@ class Solar(Base): DateTime, nullable=False, default=datetime.datetime.now(pytz.utc), onupdate=datetime.datetime.now(pytz.utc) ) google_api_response = Column(JSON, nullable=False) + + +class ScenarioType(PyEnum): + unit = "unit" + building = "building" + + +class SolarScenario(Base): + __tablename__ = 'solar_scenario' + + id = Column(BigInteger, primary_key=True, autoincrement=True) + solar_id = Column(BigInteger, ForeignKey('solar.id'), nullable=False) + scenario_type = Column(Enum(ScenarioType), nullable=False) + number_panels = Column(Integer, nullable=False) + array_kwhp = Column(Integer, nullable=False) + lifetime_dc_kwh = Column(Float, nullable=False) + yearly_dc_kwh = Column(Float, nullable=False) + lifetime_ac_kwh = Column(Float) + yearly_ac_kwh = Column(Float) + cost = Column(Float, nullable=False) + expected_payback_years = Column(Float) + panelled_roof_area = Column(Float, nullable=False) + is_default = Column(Boolean, nullable=False) diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index c85382e7..0564fbba 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -431,7 +431,9 @@ async def trigger_plan(body: PlanTriggerRequest): } # Store the data in the database - solar_api_client.save_to_db(session=session, uprns_to_location=building_uprns[building_id]) + solar_api_client.save_to_db( + session=session, uprns_to_location=building_uprns[building_id], scenario_type="building" + ) # Insert this into the properties that have this building id for p in input_properties: