Skip to content

Commit

Permalink
Merge pull request #80 from openclimatefix/ap-nv/inverter-endpoints
Browse files Browse the repository at this point in the history
Create inverter accessing points
  • Loading branch information
anyaparekh authored Apr 21, 2023
2 parents a506477 + b0a2e6c commit 1a23431
Show file tree
Hide file tree
Showing 9 changed files with 287 additions and 32 deletions.
57 changes: 29 additions & 28 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion pv_site_api/_db_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

import sqlalchemy as sa
from pvsite_datamodel.read.generation import get_pv_generation_by_sites
from pvsite_datamodel.sqlmodels import ForecastSQL, ForecastValueSQL, SiteSQL
from pvsite_datamodel.sqlmodels import ForecastSQL, ForecastValueSQL, InverterSQL, SiteSQL
from sqlalchemy.orm import Session, aliased

from .pydantic_models import (
Expand Down Expand Up @@ -55,6 +55,12 @@ def _get_forecasts_for_horizon(
return list(session.execute(stmt))


def _get_inverters_by_site(session: Session, site_uuid: str) -> list[Row]:
query = session.query(InverterSQL).filter(InverterSQL.site_uuid == site_uuid)

return query.all()


def _get_latest_forecast_by_sites(session: Session, site_uuids: list[str]) -> list[Row]:
"""Get the latest forecast for given site uuids."""
# Get the latest forecast for each site.
Expand Down
34 changes: 34 additions & 0 deletions pv_site_api/fake.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

from .pydantic_models import (
Forecast,
InverterInformation,
InverterLocation,
InverterProductionState,
Inverters,
InverterValues,
MultiplePVActual,
PVActualValue,
PVSiteAPIStatus,
Expand All @@ -19,6 +24,35 @@
fake_client_uuid = "c97f68cd-50e0-49bb-a850-108d4a9f7b7e"


def make_fake_inverters() -> Inverters:
"""Make fake inverters"""
inverter = InverterValues(
id="string",
vendor="EMA",
chargingLocationId="8d90101b-3f2f-462a-bbb4-1ed320d33bbe",
lastSeen="2020-04-07T17:04:26Z",
isReachable=True,
productionState=InverterProductionState(
productionRate=0,
isProducing=True,
totalLifetimeProduction=100152.56,
lastUpdated="2020-04-07T17:04:26Z",
),
information=InverterInformation(
id="string",
brand="EMA",
model="Sunny Boy",
siteName="Sunny Plant",
installationDate="2020-04-07T17:04:26Z",
),
location=InverterLocation(latitude=10.7197486, longitude=59.9173985),
)
inverters_list = Inverters(
inverters=[inverter],
)
return inverters_list


def make_fake_site() -> PVSites:
"""Make a fake site"""
pv_site = PVSiteMetadata(
Expand Down
40 changes: 39 additions & 1 deletion pv_site_api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import logging
import os

import httpx
import pandas as pd
import sentry_sdk
from dotenv import load_dotenv
Expand All @@ -19,6 +20,7 @@
import pv_site_api

from ._db_helpers import (
_get_inverters_by_site,
does_site_exist,
get_forecasts_by_sites,
get_generation_by_sites,
Expand All @@ -27,6 +29,7 @@
from .fake import (
fake_site_uuid,
make_fake_forecast,
make_fake_inverters,
make_fake_pv_generation,
make_fake_site,
make_fake_status,
Expand All @@ -41,7 +44,7 @@
)
from .redoc_theme import get_redoc_html_with_theme
from .session import get_session
from .utils import get_yesterday_midnight
from .utils import get_inverters_list, get_yesterday_midnight

load_dotenv()

Expand Down Expand Up @@ -355,6 +358,41 @@ def get_pv_estimate_clearsky(site_uuid: str, session: Session = Depends(get_sess
return res


@app.get("/inverters")
async def get_inverters(
session: Session = Depends(get_session),
):
if int(os.environ["FAKE"]):
return make_fake_inverters()

client = session.query(ClientSQL).first()
assert client is not None

async with httpx.AsyncClient() as httpxClient:
headers = {"Enode-User-Id": str(client.client_uuid)}
r = (
await httpxClient.get(
"https://enode-api.production.enode.io/inverters", headers=headers
)
).json()
inverter_ids = [str(value) for value in r]

return await get_inverters_list(session, inverter_ids)


@app.get("/sites/{site_uuid}/inverters")
async def get_inverters_by_site(
site_uuid: str,
session: Session = Depends(get_session),
):
if int(os.environ["FAKE"]):
return make_fake_inverters()

inverter_ids = [inverter.client_id for inverter in _get_inverters_by_site(session, site_uuid)]

return await get_inverters_list(session, inverter_ids)


# get_status: get the status of the system
@app.get("/api_status", response_model=PVSiteAPIStatus)
def get_status(session: Session = Depends(get_session)):
Expand Down
68 changes: 68 additions & 0 deletions pv_site_api/pydantic_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,71 @@ class ClearskyEstimate(BaseModel):
clearsky_estimate: List[ClearskyEstimateValues] = Field(
..., description="List of times and clearsky estimate"
)


class InverterProductionState(BaseModel):
"""Production State data for an inverter"""

productionRate: Optional[float] = Field(..., description="The current production rate in kW")
isProducing: Optional[bool] = Field(
..., description="Whether the solar inverter is actively producing energy or not"
)
totalLifetimeProduction: Optional[float] = Field(
..., description="The total lifetime production in kWh"
)
lastUpdated: Optional[str] = Field(
..., description="ISO8601 UTC timestamp of last received production state update"
)


class InverterInformation(BaseModel):
""" "Inverter information"""

id: str = Field(..., description="Solar inverter vendor ID")
brand: str = Field(..., description="Solar inverter brand")
model: str = Field(..., description="Solar inverter model")
siteName: str = Field(
...,
description="Name of the site, as set by the user on the device/vendor.",
)
installationDate: str = Field(..., description="Solar inverter installation date")


class InverterLocation(BaseModel):
""" "Longitude and Latitude of inverter"""

longitude: Optional[float] = Field(..., description="Longitude in degrees")
latitude: Optional[float] = Field(..., description="Latitude in degrees")


class InverterValues(BaseModel):
"""Inverter Data for a site"""

id: str = Field(..., description="Solar Inverter ID")
vendor: str = Field(
..., description="One of EMA ENPHASE FRONIUS GOODWE GROWATT HUAWEI SMA SOLAREDGE SOLIS"
)
chargingLocationId: Optional[str] = Field(
...,
description="ID of the charging location the solar inverter is currently positioned at.",
)
lastSeen: str = Field(
..., description="The last time the solar inverter was successfully communicated with"
)
isReachable: bool = Field(
...,
description="Whether live data from the solar inverter is currently reachable from Enode.",
)
productionState: InverterProductionState = Field(
..., description="Descriptive information about the production state"
)
information: InverterInformation = Field(
..., description="Descriptive information about the solar inverter"
)
location: InverterLocation = Field(..., description="Solar inverter's GPS coordinates")


class Inverters(BaseModel):
"""Return all Inverter Data"""

inverters: List[InverterValues] = Field(..., description="List of inverter data")
27 changes: 26 additions & 1 deletion pv_site_api/utils.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
""" make fake intensity"""
import asyncio
import math
from datetime import datetime, timedelta, timezone
from typing import List

import httpx
from pvsite_datamodel.sqlmodels import ClientSQL

from .pydantic_models import Inverters, InverterValues

TOTAL_MINUTES_IN_ONE_DAY = 24 * 60


async def get_inverters_list(session, inverter_ids):
client = session.query(ClientSQL).first()
assert client is not None

async with httpx.AsyncClient() as httpxClient:
headers = {"Enode-User-Id": str(client.client_uuid)}
inverters_raw = await asyncio.gather(
*[
httpxClient.get(
f"https://enode-api.production.enode.io/inverters/{id}", headers=headers
)
for id in inverter_ids
]
)
inverters = [InverterValues(**(inverter_raw.json())) for inverter_raw in inverters_raw]

return Inverters(inverters=inverters)


def make_fake_intensity(datetime_utc: datetime) -> float:
"""
Make a fake intesnity value based on the time of the day
Make a fake intensity value based on the time of the day
:param datetime_utc:
:return: intensity, between 0 and 1
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ psycopg2-binary = "^2.9.5"
sqlalchemy = "^1.4.46"
pvsite-datamodel = "^0.1.33"
fastapi = "^0.92.0"
httpx = "^0.23.3"
httpx = "^0.24.0"
sentry-sdk = "^1.16.0"
pvlib = "^0.9.5"

Expand All @@ -26,6 +26,7 @@ pytest = "^7.2.1"
pytest-cov = "^4.0.0"
testcontainers-postgres = "^0.0.1rc1"
ipython = "^8.11.0"
pytest-httpx = "^0.22.0"

[build-system]
requires = ["poetry-core"]
Expand Down
Loading

0 comments on commit 1a23431

Please sign in to comment.