Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create inverter accessing points #80

Merged
merged 24 commits into from
Apr 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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