Skip to content

Commit

Permalink
Get forecasts for an asset-id instead of passing location (#16)
Browse files Browse the repository at this point in the history
* add asset-id to get forecasts, used GenericAsset(type) instead of Asset(Type)

* refactoring

* refactoring

* remove extra brackets

* add both assets name in the exception message

* add asset-id to get forecasts, used GenericAsset(type) instead of Asset(Type)

* refactoring

* refactoring

* remove extra brackets

* add both assets name in the exception message

* clear exception message

* removed unused package

* add asset-id cli param

* add asset-id to register a weather sensor

* fix sensor schema validation and raise Warnings

* refactoring

* refactor schema and update generic asset and type

* stop code when no close station is found

* update warning message and refactoring

---------

Co-authored-by: Nikolai <nrozanov@iponweb.net>
  • Loading branch information
Ahmad-Wahid and Nikolai committed Mar 12, 2024
1 parent 3790b00 commit 42bed45
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 47 deletions.
55 changes: 45 additions & 10 deletions flexmeasures_openweathermap/cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
from .schemas.weather_sensor import WeatherSensorSchema
from ..utils.modeling import (
get_or_create_weather_station,
get_weather_station_by_asset_id,
)
from ..utils.locating import get_locations
from ..utils.locating import get_locations, get_location_by_asset_id
from ..utils.filing import make_file_path
from ..utils.owm import (
save_forecasts_in_db,
Expand All @@ -21,7 +22,6 @@
)
from ..sensor_specs import mapping


"""
TODO: allow to also pass an asset ID or name for the weather station (instead of location) to both commands?
See https://github.com/SeitaBV/flexmeasures-openweathermap/issues/2
Expand All @@ -39,16 +39,21 @@
required=True,
help=f"Name of the sensor. Has to be from the supported list ({supported_sensors_list})",
)
# @click.option("--generic-asset-id", required=False, help="The asset id of the weather station (you can also give its location).")
@click.option(
"--asset-id",
required=False,
type=int,
help="The asset id of the weather station (you can also give its location).",
)
@click.option(
"--latitude",
required=True,
required=False,
type=float,
help="Latitude of where you want to measure.",
)
@click.option(
"--longitude",
required=True,
required=False,
type=float,
help="Longitude of where you want to measure.",
)
Expand All @@ -69,9 +74,26 @@ def add_weather_sensor(**args):
f"[FLEXMEASURES-OWM] Please correct the following errors:\n{errors}.\n Use the --help flag to learn more."
)
raise click.Abort
if args["asset_id"] is not None:
weather_station = get_weather_station_by_asset_id(args["asset_id"])
elif args["latitude"] is not None and args["longitude"] is not None:
weather_station = get_or_create_weather_station(
args["latitude"], args["longitude"]
)
else:
raise Exception(
"Arguments are missing to register a weather sensor. Provide either '--asset-id' or ('--latitude' and '--longitude')."
)

weather_station = get_or_create_weather_station(args["latitude"], args["longitude"])

sensor = Sensor.query.filter(
Sensor.name == args["name"].lower(),
Sensor.generic_asset == weather_station,
).one_or_none()
if sensor:
click.echo(
f"[FLEXMEASURES-OWM] A '{args['name']}' weather sensor already exists at this weather station (the station's ID is {weather_station.id})."
)
return
fm_sensor_specs = get_supported_sensor_spec(args["name"])
fm_sensor_specs["generic_asset"] = weather_station
fm_sensor_specs["timezone"] = args["timezone"]
Expand All @@ -95,12 +117,18 @@ def add_weather_sensor(**args):
@click.option(
"--location",
type=str,
required=True,
required=False,
help='Measurement location(s). "latitude,longitude" or "top-left-latitude,top-left-longitude:'
'bottom-right-latitude,bottom-right-longitude." The first format defines one location to measure.'
" The second format defines a region of interest with several (>=4) locations"
' (see also the "method" and "num_cells" parameters for details on how to use this feature).',
)
@click.option(
"--asset-id",
type=int,
required=False,
help="ID of a weather station asset - forecasts will be gotten for its location. If present, --location will be ignored.",
)
@click.option(
"--store-in-db/--store-as-json-files",
default=True,
Expand All @@ -125,7 +153,7 @@ def add_weather_sensor(**args):
help="Name of the region (will create sub-folder if you store json files).",
)
@task_with_status_report("get-openweathermap-forecasts")
def collect_weather_data(location, store_in_db, num_cells, method, region):
def collect_weather_data(location, asset_id, store_in_db, num_cells, method, region):
"""
Collect weather forecasts from the OpenWeatherMap API.
This will be done for one or more locations, for which we first identify relevant weather stations.
Expand All @@ -139,7 +167,14 @@ def collect_weather_data(location, store_in_db, num_cells, method, region):
raise Exception(
"[FLEXMEASURES-OWM] Setting OPENWEATHERMAP_API_KEY not available."
)
locations = get_locations(location, num_cells, method)
if asset_id is not None:
locations = [get_location_by_asset_id(asset_id)]
elif location is not None:
locations = get_locations(location, num_cells, method)
else:
raise Warning(
"[FLEXMEASURES-OWM] Pass either location or asset-id to get weather forecasts."
)

# Save the results
if store_in_db:
Expand Down
28 changes: 7 additions & 21 deletions flexmeasures_openweathermap/cli/schemas/weather_sensor.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
from marshmallow import (
Schema,
validates,
validates_schema,
ValidationError,
fields,
validate,
)

import pytz
from flexmeasures import Sensor

from ...utils.modeling import get_or_create_weather_station
from ...utils.owm import get_supported_sensor_spec, get_supported_sensors_str


Expand All @@ -22,8 +19,13 @@ class WeatherSensorSchema(Schema):

name = fields.Str(required=True)
timezone = fields.Str()
latitude = fields.Float(required=True, validate=validate.Range(min=-90, max=90))
longitude = fields.Float(required=True, validate=validate.Range(min=-180, max=180))
asset_id = fields.Int(required=False, allow_none=True)
latitude = fields.Float(
required=False, validate=validate.Range(min=-90, max=90), allow_none=True
)
longitude = fields.Float(
required=False, validate=validate.Range(min=-180, max=180), allow_none=True
)

@validates("name")
def validate_name_is_supported(self, name: str):
Expand All @@ -33,22 +35,6 @@ def validate_name_is_supported(self, name: str):
f"Weather sensors with name '{name}' are not supported by flexmeasures-openweathermap. For now, the following is supported: [{get_supported_sensors_str()}]"
)

@validates_schema(skip_on_field_errors=False)
def validate_name_is_unique_in_weather_station(self, data, **kwargs):
if "name" not in data or "latitude" not in data or "longitude" not in data:
return # That's a different validation problem
weather_station = get_or_create_weather_station(
data["latitude"], data["longitude"]
)
sensor = Sensor.query.filter(
Sensor.name == data["name"].lower(),
Sensor.generic_asset == weather_station,
).one_or_none()
if sensor:
raise ValidationError(
f"A '{data['name']}' weather sensor already exists at this weather station (the station's ID is {weather_station.id})."
)

@validates("timezone")
def validate_timezone(self, timezone: str):
try:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,4 @@ def test_get_weather_forecasts_no_close_sensors(
assert (
"Reported task get-openweathermap-forecasts status as True" in result.output
)
assert "No sufficiently close weather sensor found" in caplog.text
assert "no sufficiently close weather sensor found" in caplog.text
7 changes: 4 additions & 3 deletions flexmeasures_openweathermap/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from flask_sqlalchemy import SQLAlchemy
from flexmeasures.app import create as create_flexmeasures_app
from flexmeasures.conftest import db, fresh_db # noqa: F401
from flexmeasures import Asset, AssetType, Sensor
from flexmeasures.data.models.generic_assets import GenericAsset, GenericAssetType
from flexmeasures.data.models.time_series import Sensor

from flexmeasures_openweathermap import WEATHER_STATION_TYPE_NAME

Expand Down Expand Up @@ -42,10 +43,10 @@ def add_weather_sensors_fresh_db(fresh_db) -> Dict[str, Sensor]: # noqa: F811

def create_weather_sensors(db: SQLAlchemy): # noqa: F811
"""Add a weather station asset with two weather sensors."""
weather_station_type = AssetType(name=WEATHER_STATION_TYPE_NAME)
weather_station_type = GenericAssetType(name=WEATHER_STATION_TYPE_NAME)
db.session.add(weather_station_type)

weather_station = Asset(
weather_station = GenericAsset(
name="Test weather station",
generic_asset_type=weather_station_type,
latitude=33.4843866,
Expand Down
23 changes: 20 additions & 3 deletions flexmeasures_openweathermap/utils/locating.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
from flask import current_app

from flexmeasures.utils.grid_cells import LatLngGrid, get_cell_nums
from flexmeasures import Asset, Sensor
from flexmeasures import Sensor
from flexmeasures.data.models.generic_assets import GenericAsset
from flexmeasures.utils import flexmeasures_inflection

from .. import WEATHER_STATION_TYPE_NAME
Expand Down Expand Up @@ -92,19 +93,35 @@ def find_weather_sensor_by_location(
n=1,
)
if weather_sensor is not None:
weather_station: Asset = weather_sensor.generic_asset
weather_station: GenericAsset = weather_sensor.generic_asset
if abs(
location[0] - weather_station.location[0]
) > max_degree_difference_for_nearest_weather_sensor or abs(
location[1] - weather_station.location[1]
> max_degree_difference_for_nearest_weather_sensor
):
current_app.logger.warning(
f"[FLEXMEASURES-OWM] No sufficiently close weather sensor found (within {max_degree_difference_for_nearest_weather_sensor} {flexmeasures_inflection.pluralize('degree', max_degree_difference_for_nearest_weather_sensor)} distance) for measuring {sensor_name}! We're looking for: {location}, closest available: ({weather_station.location})"
f"[FLEXMEASURES-OWM] We found a weather station, but no sufficiently close weather sensor found (within {max_degree_difference_for_nearest_weather_sensor} {flexmeasures_inflection.pluralize('degree', max_degree_difference_for_nearest_weather_sensor)} distance) for measuring {sensor_name}! We're looking for: {location}, closest available: ({weather_station.location})"
)
else:
current_app.logger.warning(
"[FLEXMEASURES-OWM] No weather sensor set up yet for measuring %s. Try the register-weather-sensor CLI task."
% sensor_name
)
return weather_sensor


def get_location_by_asset_id(asset_id: int) -> Tuple[float, float]:
"""Get location for forecasting by passing an asset id"""
asset = GenericAsset.query.filter(
GenericAsset.generic_asset_type_id == asset_id
).one_or_none()
if asset.generic_asset_type.name != WEATHER_STATION_TYPE_NAME:
raise Exception(
f"Asset {asset} does not seem to be a weather station we should use ― we expect an asset with type '{WEATHER_STATION_TYPE_NAME}'."
)
if asset is None:
raise Exception(
"[FLEXMEASURES-OWM] No asset found for the given asset id %s." % asset_id
)
return (asset.latitude, asset.longitude)
36 changes: 27 additions & 9 deletions flexmeasures_openweathermap/utils/modeling.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from packaging import version

from flask import current_app
from flexmeasures import Asset, AssetType, Source, __version__ as flexmeasures_version
from flexmeasures.data.models.generic_assets import GenericAsset, GenericAssetType
from flexmeasures import Source, __version__ as flexmeasures_version
from flexmeasures.data import db
from flexmeasures.data.services.data_sources import get_or_create_source

Expand Down Expand Up @@ -38,35 +39,52 @@ def get_or_create_owm_data_source_for_derived_data() -> Source:
)


def get_or_create_weather_station_type() -> AssetType:
def get_or_create_weather_station_type() -> GenericAssetType:
"""Make sure a weather station type exists"""
weather_station_type = AssetType.query.filter(
AssetType.name == WEATHER_STATION_TYPE_NAME,
weather_station_type = GenericAssetType.query.filter(
GenericAssetType.name == WEATHER_STATION_TYPE_NAME,
).one_or_none()
if weather_station_type is None:
weather_station_type = AssetType(
weather_station_type = GenericAssetType(
name=WEATHER_STATION_TYPE_NAME,
description="A weather station with various sensors.",
)
db.session.add(weather_station_type)
return weather_station_type


def get_or_create_weather_station(latitude: float, longitude: float) -> Asset:
def get_or_create_weather_station(latitude: float, longitude: float) -> GenericAsset:
"""Make sure a weather station exists at this location."""
station_name = current_app.config.get(
"WEATHER_STATION_NAME", DEFAULT_WEATHER_STATION_NAME
)
weather_station = Asset.query.filter(
Asset.latitude == latitude, Asset.longitude == longitude
weather_station = GenericAsset.query.filter(
GenericAsset.latitude == latitude, GenericAsset.longitude == longitude
).one_or_none()
if weather_station is None:
weather_station_type = get_or_create_weather_station_type()
weather_station = Asset(
weather_station = GenericAsset(
name=station_name,
generic_asset_type=weather_station_type,
latitude=latitude,
longitude=longitude,
)
db.session.add(weather_station)
return weather_station


def get_weather_station_by_asset_id(asset_id: int) -> GenericAsset:
weather_station = GenericAsset.query.filter(
GenericAsset.generic_asset_type_id == asset_id
).one_or_none()
if weather_station is None:
raise Exception(
f"[FLEXMEASURES-OWM] Weather station is not present for the given asset id '{asset_id}'."
)

if weather_station.latitude is None or weather_station.longitude is None:
raise Exception(
f"[FLEXMEASURES-OWM] Weather station {weather_station} is missing location information [Latitude, Longitude]."
)

return weather_station

0 comments on commit 42bed45

Please sign in to comment.