diff --git a/flexmeasures_weather/cli/commands.py b/flexmeasures_weather/cli/commands.py index a95c435..a66e736 100644 --- a/flexmeasures_weather/cli/commands.py +++ b/flexmeasures_weather/cli/commands.py @@ -162,7 +162,11 @@ def collect_weather_data(location, asset_id, store_in_db, num_cells, method, reg a geometrical grid (See the --location parameter). """ - api_key = str(current_app.config.get("WEATHERAPI_KEY", "")) + api_key = str( + current_app.config.get( + "WEATHERAPI_KEY", current_app.config.get("OPENWEATHERMAP_API_KEY", "") + ) + ) if api_key == "": raise Exception("[FLEXMEASURES-WEATHER] Setting WEATHERAPI_KEY not available.") if asset_id is not None: diff --git a/flexmeasures_weather/cli/schemas/weather_sensor.py b/flexmeasures_weather/cli/schemas/weather_sensor.py index d058ce1..5c4039d 100644 --- a/flexmeasures_weather/cli/schemas/weather_sensor.py +++ b/flexmeasures_weather/cli/schemas/weather_sensor.py @@ -28,7 +28,7 @@ class WeatherSensorSchema(Schema): ) @validates("name") - def validate_name_is_supported(self, name: str): + def validate_name_is_supported(self, name: str, **kwargs): if get_supported_sensor_spec(name): return raise ValidationError( @@ -36,7 +36,7 @@ def validate_name_is_supported(self, name: str): ) @validates("timezone") - def validate_timezone(self, timezone: str): + def validate_timezone(self, timezone: str, **kwargs): try: pytz.timezone(timezone) except pytz.UnknownTimeZoneError: diff --git a/flexmeasures_weather/cli/tests/test_get_forecasts.py b/flexmeasures_weather/cli/tests/test_get_forecasts.py index 2f33497..6a2d2b4 100644 --- a/flexmeasures_weather/cli/tests/test_get_forecasts.py +++ b/flexmeasures_weather/cli/tests/test_get_forecasts.py @@ -60,7 +60,10 @@ def test_get_weather_forecasts_no_close_sensors( with caplog.at_level(logging.WARNING): result = runner.invoke( collect_weather_data, - ["--location", f"{weather_station.latitude-5},{weather_station.longitude}"], + [ + "--location", + f"{weather_station.latitude - 5},{weather_station.longitude}", + ], ) print(result.output) assert "Reported task get-weather-forecasts status as True" in result.output diff --git a/flexmeasures_weather/utils/modeling.py b/flexmeasures_weather/utils/modeling.py index 33ed9c4..370b2dc 100644 --- a/flexmeasures_weather/utils/modeling.py +++ b/flexmeasures_weather/utils/modeling.py @@ -16,27 +16,61 @@ else: SOURCE_TYPE = "forecaster" +FM_SUPPORTS_ACCOUNT_LINKED_SOURCES = version.parse( + flexmeasures_version +) >= version.parse("0.32") + +if FM_SUPPORTS_ACCOUNT_LINKED_SOURCES: + from flexmeasures import Account +else: + Account = None + + +def get_or_create_weather_account(): + """Make sure we have an account for the weather provider service.""" + if Account is None: + raise RuntimeError( + "FlexMeasures Account model is unavailable before FlexMeasures 0.32." + ) + account_name = current_app.config.get( + "WEATHER_DATA_SOURCE_NAME", DEFAULT_DATA_SOURCE_NAME + ) + weather_account = Account.query.filter( + Account.name == account_name, + ).one_or_none() + if weather_account is None: + weather_account = Account(name=account_name) + db.session.add(weather_account) + db.session.flush() + return weather_account + def get_or_create_owm_data_source() -> Source: - """Make sure we have an data source""" - return get_or_create_source( + """Make sure we have a weather provider data source of the configured type.""" + source_kwargs = dict( source=current_app.config.get( "WEATHER_DATA_SOURCE_NAME", DEFAULT_DATA_SOURCE_NAME ), source_type=SOURCE_TYPE, flush=False, ) + if FM_SUPPORTS_ACCOUNT_LINKED_SOURCES: + source_kwargs["account"] = get_or_create_weather_account() + return get_or_create_source(**source_kwargs) def get_or_create_owm_data_source_for_derived_data() -> Source: owm_source_name = current_app.config.get( "WEATHER_DATA_SOURCE_NAME", DEFAULT_DATA_SOURCE_NAME ) - return get_or_create_source( + source_kwargs = dict( source=f"FlexMeasures {owm_source_name}", source_type=SOURCE_TYPE, flush=False, ) + if FM_SUPPORTS_ACCOUNT_LINKED_SOURCES: + source_kwargs["account"] = get_or_create_weather_account() + return get_or_create_source(**source_kwargs) def get_or_create_weather_station_type() -> GenericAssetType: diff --git a/flexmeasures_weather/utils/tests/test_modeling.py b/flexmeasures_weather/utils/tests/test_modeling.py index dcea357..b0f35f6 100644 --- a/flexmeasures_weather/utils/tests/test_modeling.py +++ b/flexmeasures_weather/utils/tests/test_modeling.py @@ -1,10 +1,194 @@ +from types import SimpleNamespace + +import pytest + from flexmeasures import Asset -from flexmeasures_weather import DEFAULT_WEATHER_STATION_NAME -from flexmeasures_weather.utils.modeling import get_or_create_weather_station +import flexmeasures_weather.utils.modeling as modeling +from flexmeasures_weather import DEFAULT_DATA_SOURCE_NAME, DEFAULT_WEATHER_STATION_NAME +from flexmeasures_weather.utils.modeling import ( + FM_SUPPORTS_ACCOUNT_LINKED_SOURCES, + SOURCE_TYPE, + get_or_create_owm_data_source, + get_or_create_owm_data_source_for_derived_data, + get_or_create_weather_account, + get_or_create_weather_station, +) def test_creating_two_weather_stations(fresh_db): get_or_create_weather_station(50, 40) get_or_create_weather_station(40, 50) assert Asset.query.filter(Asset.name == DEFAULT_WEATHER_STATION_NAME).count() == 2 + + +# The version-branch tests below still use monkeypatching to isolate source +# creation side effects without requiring multiple FlexMeasures installs. +@pytest.mark.skipif( + not FM_SUPPORTS_ACCOUNT_LINKED_SOURCES, + reason="Weather source accounts are only supported on FlexMeasures >= 0.32.", +) +def test_get_or_create_weather_account(fresh_db): + weather_account = get_or_create_weather_account() + + assert weather_account.name == DEFAULT_DATA_SOURCE_NAME + assert ( + modeling.Account.query.filter( + modeling.Account.name == weather_account.name + ).count() + == 1 + ) + + +@pytest.mark.skipif( + not FM_SUPPORTS_ACCOUNT_LINKED_SOURCES, + reason="Account-linked weather sources are only supported on FlexMeasures >= 0.32.", +) +def test_get_or_create_owm_data_source_registers_weather_source_on_weather_account( + fresh_db, +): + data_source = get_or_create_owm_data_source() + + assert data_source.type == SOURCE_TYPE + assert data_source.account is not None + assert data_source.account.name == data_source.name + + +@pytest.mark.skipif( + not FM_SUPPORTS_ACCOUNT_LINKED_SOURCES, + reason="Account-linked weather sources are only supported on FlexMeasures >= 0.32.", +) +def test_get_or_create_owm_data_source_for_derived_data_uses_weather_account(fresh_db): + derived_data_source = get_or_create_owm_data_source_for_derived_data() + + assert derived_data_source.type == SOURCE_TYPE + assert derived_data_source.account is not None + assert derived_data_source.account.name == DEFAULT_DATA_SOURCE_NAME + + +@pytest.mark.skipif( + not FM_SUPPORTS_ACCOUNT_LINKED_SOURCES, + reason="Account-linked weather sources are only supported on FlexMeasures >= 0.32.", +) +def test_get_or_create_owm_data_source_passes_weather_account_when_supported( + fresh_db, monkeypatch +): + captured_kwargs = {} + + def fake_get_or_create_source(source, source_type, account, flush): + captured_kwargs.update( + dict( + source=source, + source_type=source_type, + account=account, + flush=flush, + ) + ) + return SimpleNamespace(type=source_type, account=account) + + monkeypatch.setattr( + "flexmeasures_weather.utils.modeling.get_or_create_source", + fake_get_or_create_source, + ) + + data_source = get_or_create_owm_data_source() + + assert data_source.type == SOURCE_TYPE + assert captured_kwargs["account"].name == DEFAULT_DATA_SOURCE_NAME + + +@pytest.mark.skipif( + not FM_SUPPORTS_ACCOUNT_LINKED_SOURCES, + reason="Account-linked weather sources are only supported on FlexMeasures >= 0.32.", +) +def test_get_or_create_owm_derived_data_source_passes_weather_account_when_supported( + fresh_db, monkeypatch +): + captured_kwargs = {} + + def fake_get_or_create_source(source, source_type, account, flush): + captured_kwargs.update( + dict( + source=source, + source_type=source_type, + account=account, + flush=flush, + ) + ) + return SimpleNamespace(type=source_type, account=account) + + monkeypatch.setattr( + "flexmeasures_weather.utils.modeling.get_or_create_source", + fake_get_or_create_source, + ) + + data_source = get_or_create_owm_data_source_for_derived_data() + + assert data_source.type == SOURCE_TYPE + assert captured_kwargs["account"].name == DEFAULT_DATA_SOURCE_NAME + + +@pytest.mark.skipif( + FM_SUPPORTS_ACCOUNT_LINKED_SOURCES, + reason="Legacy source creation without accounts is only used on FlexMeasures < 0.32.", +) +def test_get_or_create_owm_data_source_omits_account_when_not_supported(monkeypatch): + captured_kwargs = {} + + def fake_get_or_create_source(source, source_type, flush): + captured_kwargs.update( + dict( + source=source, + source_type=source_type, + flush=flush, + ) + ) + return SimpleNamespace(type=source_type, name=source) + + monkeypatch.setattr( + "flexmeasures_weather.utils.modeling.get_or_create_source", + fake_get_or_create_source, + ) + + data_source = get_or_create_owm_data_source() + + assert data_source.type == SOURCE_TYPE + assert captured_kwargs == { + "source": DEFAULT_DATA_SOURCE_NAME, + "source_type": SOURCE_TYPE, + "flush": False, + } + + +@pytest.mark.skipif( + FM_SUPPORTS_ACCOUNT_LINKED_SOURCES, + reason="Legacy source creation without accounts is only used on FlexMeasures < 0.32.", +) +def test_get_or_create_owm_derived_data_source_omits_account_when_not_supported( + monkeypatch, +): + captured_kwargs = {} + + def fake_get_or_create_source(source, source_type, flush): + captured_kwargs.update( + dict( + source=source, + source_type=source_type, + flush=flush, + ) + ) + return SimpleNamespace(type=source_type, name=source) + + monkeypatch.setattr( + "flexmeasures_weather.utils.modeling.get_or_create_source", + fake_get_or_create_source, + ) + + data_source = get_or_create_owm_data_source_for_derived_data() + + assert data_source.type == SOURCE_TYPE + assert captured_kwargs == { + "source": f"FlexMeasures {DEFAULT_DATA_SOURCE_NAME}", + "source_type": SOURCE_TYPE, + "flush": False, + } diff --git a/flexmeasures_weather/utils/weather.py b/flexmeasures_weather/utils/weather.py index f27f91a..dfcf8bb 100644 --- a/flexmeasures_weather/utils/weather.py +++ b/flexmeasures_weather/utils/weather.py @@ -198,7 +198,7 @@ def call_api( Exception: If an invalid weather provider is configured. """ - provider = str(current_app.config.get("WEATHER_PROVIDER", "")) + provider = str(current_app.config.get("WEATHER_PROVIDER", "OWM")) if provider not in ["OWM", "WAPI"]: raise Exception( "Invalid provider name. Please set WEATHER_PROVIDER setting in config file to either OWM or WAPI, the two permissible options."