-
Notifications
You must be signed in to change notification settings - Fork 0
Ecoclient Simvue Python API Extension #752
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
Changes from 4 commits
1ab36df
ddabec6
4978446
3916018
333d374
223d4c3
bd66480
8f79810
bd10c44
3aab367
2f104e8
08dc7a0
933ba92
f885caf
5f5fcfa
6d1917d
3e08099
eacbe45
af4d0e3
3acce87
bae9ade
30da207
6538fc6
4de2f5f
01f8c0b
b2811f0
462e7be
ebef998
444805d
1626f17
9c5317e
ff41785
b0545e5
74f00f2
37d900b
2437e37
01b5ff8
bbe4b4f
d352d7e
4099b79
ff59423
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
""" | ||
Simvue Eco | ||
========== | ||
|
||
Contains functionality for green IT, monitoring emissions etc. | ||
NOTE: The metrics calculated by these methods should be used for relative | ||
comparisons only. Any values returned should not be taken as absolute. | ||
|
||
""" | ||
__date__ = "2025-03-06" | ||
|
||
from .emissions_monitor import CO2Monitor as CO2Monitor | ||
|
||
__all__ = ["CO2Monitor"] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
""" | ||
CO2 Signal API Client | ||
===================== | ||
|
||
Provides inteface to the CO2 Signal API, | ||
which provides real-time data on the carbon intensity of | ||
electricity generation in different countries. | ||
""" | ||
|
||
__date__ = "2025-02-27" | ||
|
||
import requests | ||
import pydantic | ||
import functools | ||
import http | ||
import logging | ||
import datetime | ||
import geocoder | ||
import geocoder.location | ||
import typing | ||
|
||
|
||
class CO2SignalData(pydantic.BaseModel): | ||
datetime: datetime.datetime | ||
carbon_intensity: float | ||
fossil_fuel_percentage: float | ||
|
||
|
||
class CO2SignalResponse(pydantic.BaseModel): | ||
disclaimer: str | ||
country_code: str | ||
status: str | ||
data: CO2SignalData | ||
carbon_intensity_units: str | ||
|
||
@classmethod | ||
def from_json_response(cls, json_response: dict) -> "CO2SignalResponse": | ||
_data: dict[str, typing.Any] = json_response["data"] | ||
_co2_signal_data = CO2SignalData( | ||
datetime=datetime.datetime.fromisoformat(_data["datetime"]), | ||
carbon_intensity=_data["carbonIntensity"], | ||
fossil_fuel_percentage=_data["fossilFuelPercentage"], | ||
) | ||
return cls( | ||
disclaimer=json_response["_disclaimer"], | ||
country_code=json_response["countryCode"], | ||
status=json_response["status"], | ||
data=_co2_signal_data, | ||
carbon_intensity_units=json_response["units"]["carbonIntensity"], | ||
) | ||
|
||
|
||
@functools.lru_cache() | ||
def _call_geocoder_query() -> typing.Any: | ||
"""Call GeoCoder API for IP location | ||
|
||
Cached so this API is only called once per session as required. | ||
""" | ||
return geocoder.ip("me") | ||
|
||
|
||
class APIClient(pydantic.BaseModel): | ||
""" | ||
CO2 Signal API Client | ||
|
||
Provides an interface to the Electricity Maps API. | ||
""" | ||
|
||
co2_api_endpoint: pydantic.HttpUrl = pydantic.HttpUrl( | ||
"https://api.co2signal.com/v1/latest" | ||
) | ||
co2_api_token: pydantic.SecretStr | None = None | ||
timeout: pydantic.PositiveInt = 10 | ||
|
||
def __init__(self, *args, **kwargs) -> None: | ||
"""Initialise the CO2 Signal API client. | ||
|
||
Parameters | ||
---------- | ||
co2_api_endpoint : str | ||
endpoint for CO2 signal API | ||
co2_api_token: str | ||
RECOMMENDED. The API token for the CO2 Signal API, default is None. | ||
timeout : int | ||
timeout for API | ||
""" | ||
super().__init__(*args, **kwargs) | ||
self._logger = logging.getLogger(self.__class__.__name__) | ||
|
||
if not self.co2_api_token: | ||
self._logger.warning( | ||
"⚠️ No API token provided for CO2 Signal, it is recommended " | ||
kzscisoft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
|
||
self._get_user_location_info() | ||
|
||
def _get_user_location_info(self) -> None: | ||
"""Retrieve location information for the current user.""" | ||
self._logger.info("📍 Determining current user location.") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should probably add a disclaimer somewhere that we are doing this... Also do we need to consider anything in terms of data protection for collecting this? I guess not because its staying local to their machine There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh actually no the exact lat and long get sent to the geocoder API - I think definitely need to consider whether there are GDPR etc issues there |
||
_current_user_loc_data: geocoder.location.BBox = _call_geocoder_query() | ||
self._latitude: float | ||
self._longitude: float | ||
self._latitude, self._longitude = _current_user_loc_data.latlng | ||
self._two_letter_country_code: str = _current_user_loc_data.country # type: ignore | ||
|
||
def get(self) -> CO2SignalResponse: | ||
"""Get the current data""" | ||
_params: dict[str, float | str] = { | ||
"lat": self._latitude, | ||
"lon": self._longitude, | ||
"countryCode": self._two_letter_country_code, | ||
} | ||
|
||
if self.co2_api_token: | ||
_params["auth-token"] = self.co2_api_token.get_secret_value() | ||
|
||
self._logger.debug(f"🍃 Retrieving carbon intensity data for: {_params}") | ||
_response = requests.get(f"{self.co2_api_endpoint}", params=_params) | ||
|
||
if not _response.status_code == http.HTTPStatus.OK: | ||
raise RuntimeError( | ||
"Failed to retrieve current CO2 signal data for" | ||
f" country '{self._two_letter_country_code}': {_response.text}" | ||
) | ||
|
||
return CO2SignalResponse.from_json_response(_response.json()) | ||
|
||
@property | ||
def country_code(self) -> str: | ||
"""Returns the country code""" | ||
return self._two_letter_country_code | ||
|
||
@property | ||
def latitude(self) -> float: | ||
"""Returns current latitude""" | ||
return self._latitude | ||
|
||
@property | ||
def longitude(self) -> float: | ||
"""Returns current longitude""" | ||
return self._longitude |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would call this 'system_metrics_interval' as it now applies to both resources and emissions metrics