conftest.py 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488
  1. from __future__ import annotations
  2. from contextlib import contextmanager
  3. import pytest
  4. from random import random, seed
  5. from datetime import datetime, timedelta
  6. from sqlalchemy import select
  7. from isodate import parse_duration
  8. import pandas as pd
  9. import numpy as np
  10. from flask import request, jsonify
  11. from flask_sqlalchemy import SQLAlchemy
  12. from flask_security import roles_accepted
  13. from timely_beliefs.sensors.func_store.knowledge_horizons import x_days_ago_at_y_oclock
  14. from werkzeug.exceptions import (
  15. InternalServerError,
  16. BadRequest,
  17. Unauthorized,
  18. Forbidden,
  19. Gone,
  20. )
  21. from flexmeasures.app import create as create_app
  22. from flexmeasures.auth.policy import ADMIN_ROLE, ADMIN_READER_ROLE
  23. from flexmeasures.data.services.users import create_user
  24. from flexmeasures.data.models.generic_assets import GenericAssetType, GenericAsset
  25. from flexmeasures.data.models.data_sources import DataSource
  26. from flexmeasures.data.models.planning.utils import initialize_index
  27. from flexmeasures.data.models.time_series import Sensor, TimedBelief
  28. from flexmeasures.data.models.user import User, Account, AccountRole
  29. """
  30. Useful things for all tests.
  31. # App
  32. One application is made per test session.
  33. # Database
  34. Database recreation and cleanup can happen per test (use fresh_db) or per module (use db).
  35. Having tests inside a module share a database makes those tests faster.
  36. Tests that use fresh_db should be put in a separate module to avoid clashing with the module scoped test db.
  37. For example:
  38. - test_api_v1_1.py contains tests that share a module scoped database
  39. - test_api_v1_1_fresh_db.py contains tests that each get a fresh function-scoped database
  40. Further speed-up may be possible by defining a "package" scoped or even "session" scoped database,
  41. but then tests in different modules need to share data and data modifications can lead to tricky debugging.
  42. # Data
  43. Various fixture below set up data that many tests use.
  44. In case a test needs to use such data with a fresh test database,
  45. that test should also use a fixture that requires the fresh_db.
  46. Such fixtures can be recognised by having fresh_db appended to their name.
  47. """
  48. @pytest.fixture(scope="session")
  49. def app():
  50. print("APP FIXTURE")
  51. test_app = create_app(env="testing")
  52. with test_app.app_context():
  53. yield test_app
  54. print("DONE WITH APP FIXTURE")
  55. @pytest.fixture(scope="module")
  56. def db(app):
  57. """Fresh test db per module."""
  58. with create_test_db(app) as test_db:
  59. yield test_db
  60. @pytest.fixture(scope="function")
  61. def fresh_db(app):
  62. """Fresh test db per function."""
  63. with create_test_db(app) as test_db:
  64. yield test_db
  65. @contextmanager
  66. def create_test_db(app):
  67. """
  68. Provide a db object with the structure freshly created. This assumes a clean database.
  69. It does clean up after itself when it's done (drops everything).
  70. """
  71. print("DB FIXTURE")
  72. # app is an instance of a flask app, _db a SQLAlchemy DB
  73. from flexmeasures.data import db as _db
  74. _db.app = app
  75. with app.app_context():
  76. _db.create_all()
  77. yield _db
  78. print("DB FIXTURE CLEANUP")
  79. # Explicitly close DB connection
  80. _db.session.close()
  81. _db.drop_all()
  82. @pytest.fixture(scope="module")
  83. def setup_accounts(db) -> dict[str, Account]:
  84. return create_test_accounts(db)
  85. @pytest.fixture(scope="function")
  86. def setup_accounts_fresh_db(fresh_db) -> dict[str, Account]:
  87. return create_test_accounts(fresh_db)
  88. def create_test_accounts(db) -> dict[str, Account]:
  89. prosumer_account_role = AccountRole(name="Prosumer", description="A Prosumer")
  90. prosumer_account = Account(
  91. name="Test Prosumer Account", account_roles=[prosumer_account_role]
  92. )
  93. db.session.add(prosumer_account)
  94. supplier_account_role = AccountRole(
  95. name="Supplier", description="A supplier trading on markets"
  96. )
  97. supplier_account = Account(
  98. name="Test Supplier Account", account_roles=[supplier_account_role]
  99. )
  100. db.session.add(supplier_account)
  101. dummy_account_role = AccountRole(
  102. name="Dummy", description="A role we haven't hardcoded anywhere"
  103. )
  104. dummy_account = Account(
  105. name="Test Dummy Account", account_roles=[dummy_account_role]
  106. )
  107. db.session.add(dummy_account)
  108. empty_account = Account(name="Test Empty Account")
  109. db.session.add(empty_account)
  110. multi_role_account = Account(
  111. name="Multi Role Account",
  112. account_roles=[
  113. prosumer_account_role,
  114. supplier_account_role,
  115. dummy_account_role,
  116. ],
  117. )
  118. db.session.add(multi_role_account)
  119. consultancy_account_role = AccountRole(
  120. name="Consultancy", description="A consultancy account"
  121. )
  122. # Create Consultancy and ConsultancyClient account.
  123. # The ConsultancyClient account needs the account id of the Consultancy account so the order is important.
  124. consultancy_account = Account(
  125. name="Test Consultancy Account", account_roles=[consultancy_account_role]
  126. )
  127. db.session.add(consultancy_account)
  128. consultancy_client_account_role = AccountRole(
  129. name="ConsultancyClient",
  130. description="A client of a consultancy",
  131. )
  132. consultancy_account_id = (
  133. db.session.execute(select(Account).filter_by(name="Test Consultancy Account"))
  134. .scalar_one_or_none()
  135. .id
  136. )
  137. consultancy_client_account = Account(
  138. name="Test ConsultancyClient Account",
  139. account_roles=[consultancy_client_account_role],
  140. consultancy_account_id=consultancy_account_id,
  141. )
  142. db.session.add(consultancy_client_account)
  143. return dict(
  144. Prosumer=prosumer_account,
  145. Supplier=supplier_account,
  146. Dummy=dummy_account,
  147. Empty=empty_account,
  148. Multi=multi_role_account,
  149. Consultancy=consultancy_account,
  150. ConsultancyClient=consultancy_client_account,
  151. )
  152. @pytest.fixture(scope="module")
  153. def setup_roles_users(db, setup_accounts) -> dict[str, User]:
  154. return create_roles_users(db, setup_accounts)
  155. @pytest.fixture(scope="function")
  156. def setup_roles_users_fresh_db(fresh_db, setup_accounts_fresh_db) -> dict[str, User]:
  157. return create_roles_users(fresh_db, setup_accounts_fresh_db)
  158. def create_roles_users(db, test_accounts) -> dict[str, User]:
  159. """Create a minimal set of roles and users"""
  160. new_users: list[User] = []
  161. # 3 Prosumer users: 2 plain ones, 1 account admin
  162. new_users.append(
  163. create_user(
  164. username="Test Prosumer User",
  165. email="test_prosumer_user@seita.nl",
  166. account_name=test_accounts["Prosumer"].name,
  167. password="testtest",
  168. # TODO: test some normal user roles later in our auth progress
  169. # user_roles=dict(name="", description=""),
  170. )
  171. )
  172. new_users.append(
  173. create_user(
  174. username="Test Prosumer User 2",
  175. email="test_prosumer_user_2@seita.nl",
  176. account_name=test_accounts["Prosumer"].name,
  177. password="testtest",
  178. user_roles=dict(name="account-admin", description="Admin for this account"),
  179. )
  180. )
  181. new_users.append(
  182. create_user(
  183. username="Test Another Plain Prosumer User",
  184. email="test_prosumer_user_3@seita.nl",
  185. account_name=test_accounts["Prosumer"].name,
  186. password="testtest",
  187. )
  188. )
  189. # A user on an account without any special rights
  190. new_users.append(
  191. create_user(
  192. username="Test Dummy User",
  193. email="test_dummy_user_3@seita.nl",
  194. account_name=test_accounts["Dummy"].name,
  195. password="testtest",
  196. )
  197. )
  198. # Account admin on dummy account
  199. new_users.append(
  200. create_user(
  201. username="Test Dummy Account Admin",
  202. email="test_dummy_account_admin@seita.nl",
  203. account_name=test_accounts["Dummy"].name,
  204. password="testtest",
  205. user_roles=dict(name="account-admin", description="Admin for this account"),
  206. )
  207. )
  208. # A supplier user
  209. new_users.append(
  210. create_user(
  211. username="Test Supplier User",
  212. email="test_supplier_user_4@seita.nl",
  213. account_name=test_accounts["Supplier"].name,
  214. password="testtest",
  215. )
  216. )
  217. # One platform admin
  218. new_users.append(
  219. create_user(
  220. username="Test Admin User",
  221. email="test_admin_user@seita.nl",
  222. account_name=test_accounts[
  223. "Dummy"
  224. ].name, # the account does not give rights
  225. password="testtest",
  226. user_roles=dict(
  227. name=ADMIN_ROLE, description="A user who can do everything."
  228. ),
  229. )
  230. )
  231. # One platform admin reader
  232. new_users.append(
  233. create_user(
  234. username="Test Admin Reader User",
  235. email="test_admin_reader_user@seita.nl",
  236. account_name=test_accounts[
  237. "Dummy"
  238. ].name, # the account does not give rights
  239. password="testtest",
  240. user_roles=dict(
  241. name=ADMIN_READER_ROLE, description="A user who can do everything."
  242. ),
  243. )
  244. )
  245. new_users.append(
  246. create_user(
  247. username="Test Consultant User",
  248. email="test_consultant@seita.nl",
  249. account_name=test_accounts["Consultancy"].name,
  250. password="testtest",
  251. user_roles=dict(name="consultant"),
  252. )
  253. )
  254. new_users.append(
  255. create_user(
  256. username="Test Consultant User without consultant role",
  257. email="test_consultancy_user_without_consultant_access@seita.nl",
  258. account_name=test_accounts["Consultancy"].name,
  259. password="testtest",
  260. )
  261. )
  262. # Consultancy client account user
  263. new_users.append(
  264. create_user(
  265. username="Test Consultancy Client User",
  266. email="test_consultant_client@seita.nl",
  267. account_name=test_accounts["ConsultancyClient"].name,
  268. password="testtest",
  269. )
  270. )
  271. return {user.username: user.id for user in new_users}
  272. @pytest.fixture(scope="module")
  273. def setup_markets(db) -> dict[str, Sensor]:
  274. return create_test_markets(db)
  275. @pytest.fixture(scope="function")
  276. def setup_markets_fresh_db(fresh_db) -> dict[str, Sensor]:
  277. return create_test_markets(fresh_db)
  278. def create_test_markets(db) -> dict[str, Sensor]:
  279. """Create the epex_da market."""
  280. day_ahead = GenericAssetType(
  281. name="day_ahead",
  282. )
  283. epex = GenericAsset(
  284. name="epex",
  285. generic_asset_type=day_ahead,
  286. )
  287. price_sensors = {}
  288. for sensor_name in ("epex_da", "epex_da_production"):
  289. price_sensor = Sensor(
  290. name=sensor_name,
  291. generic_asset=epex,
  292. event_resolution=timedelta(hours=1),
  293. unit="EUR/MWh",
  294. knowledge_horizon=(
  295. x_days_ago_at_y_oclock,
  296. {"x": 1, "y": 12, "z": "Europe/Paris"},
  297. ),
  298. attributes=dict(
  299. daily_seasonality=True,
  300. weekly_seasonality=True,
  301. yearly_seasonality=True,
  302. ),
  303. )
  304. db.session.add(price_sensor)
  305. price_sensors[sensor_name] = price_sensor
  306. db.session.flush() # assign an id, so the markets can be used to set a consumption-price flex-context field on a GenericAsset
  307. return price_sensors
  308. @pytest.fixture(scope="module")
  309. def setup_sources(db) -> dict[str, DataSource]:
  310. return create_sources(db)
  311. @pytest.fixture(scope="function")
  312. def setup_sources_fresh_db(fresh_db) -> dict[str, DataSource]:
  313. return create_sources(fresh_db)
  314. def create_sources(db) -> dict[str, DataSource]:
  315. seita_source = DataSource(name="Seita", type="demo script")
  316. db.session.add(seita_source)
  317. entsoe_source = DataSource(name="ENTSO-E", type="demo script")
  318. db.session.add(entsoe_source)
  319. dummy_schedule_source = DataSource(name="DummySchedule", type="scheduler")
  320. db.session.add(dummy_schedule_source)
  321. forecaster_source = DataSource(name="forecaster name", type="forecaster")
  322. db.session.add(forecaster_source)
  323. reporter_source = DataSource(name="reporter name", type="reporter")
  324. db.session.add(reporter_source)
  325. return {
  326. "Seita": seita_source,
  327. "ENTSO-E": entsoe_source,
  328. "DummySchedule": dummy_schedule_source,
  329. "forecaster": forecaster_source,
  330. "reporter": reporter_source,
  331. }
  332. @pytest.fixture(scope="module")
  333. def setup_generic_assets(
  334. db, setup_generic_asset_types, setup_accounts
  335. ) -> dict[str, GenericAsset]:
  336. """Make some generic assets used throughout."""
  337. return create_generic_assets(db, setup_generic_asset_types, setup_accounts)
  338. @pytest.fixture(scope="function")
  339. def setup_generic_assets_fresh_db(
  340. fresh_db, setup_generic_asset_types_fresh_db, setup_accounts_fresh_db
  341. ) -> dict[str, GenericAsset]:
  342. """Make some generic assets used throughout."""
  343. return create_generic_assets(
  344. fresh_db, setup_generic_asset_types_fresh_db, setup_accounts_fresh_db
  345. )
  346. def create_generic_assets(
  347. db, setup_generic_asset_types, setup_accounts
  348. ) -> dict[str, GenericAsset]:
  349. troposphere = GenericAsset(
  350. name="troposphere", generic_asset_type=setup_generic_asset_types["public_good"]
  351. )
  352. db.session.add(troposphere)
  353. test_battery = GenericAsset(
  354. name="Test grid connected battery storage",
  355. generic_asset_type=setup_generic_asset_types["battery"],
  356. owner=setup_accounts["Prosumer"],
  357. attributes={"some-attribute": "some-value", "sensors_to_show": [1, 2]},
  358. )
  359. db.session.add(test_battery)
  360. test_wind_turbine = GenericAsset(
  361. name="Test wind turbine",
  362. generic_asset_type=setup_generic_asset_types["wind"],
  363. owner=setup_accounts["Supplier"],
  364. )
  365. db.session.add(test_wind_turbine)
  366. test_consultancy_client_asset = GenericAsset(
  367. name="Test ConsultancyClient Asset",
  368. generic_asset_type=setup_generic_asset_types["wind"],
  369. owner=setup_accounts["ConsultancyClient"],
  370. )
  371. db.session.add(test_consultancy_client_asset)
  372. return dict(
  373. troposphere=troposphere,
  374. test_battery=test_battery,
  375. test_wind_turbine=test_wind_turbine,
  376. test_consultancy_client_asset=test_consultancy_client_asset,
  377. )
  378. @pytest.fixture(scope="module")
  379. def setup_generic_asset_types(db) -> dict[str, GenericAssetType]:
  380. """Make some generic asset types used throughout."""
  381. return create_generic_asset_types(db)
  382. @pytest.fixture(scope="function")
  383. def setup_generic_asset_types_fresh_db(fresh_db) -> dict[str, GenericAssetType]:
  384. """Make some generic asset types used throughout."""
  385. return create_generic_asset_types(fresh_db)
  386. def create_generic_asset_types(db) -> dict[str, GenericAssetType]:
  387. public_good = GenericAssetType(
  388. name="public good",
  389. )
  390. db.session.add(public_good)
  391. solar = GenericAssetType(name="solar panel")
  392. db.session.add(solar)
  393. wind = GenericAssetType(name="wind turbine")
  394. db.session.add(wind)
  395. battery = db.session.execute(
  396. select(GenericAssetType).filter_by(name="battery")
  397. ).scalar_one_or_none()
  398. if (
  399. not battery
  400. ): # legacy if-block, because create_test_battery_assets might have created it already - refactor!
  401. battery = GenericAssetType(name="battery")
  402. db.session.add(battery)
  403. weather_station = GenericAssetType(name="weather station")
  404. db.session.add(weather_station)
  405. return dict(
  406. public_good=public_good,
  407. solar=solar,
  408. wind=wind,
  409. battery=battery,
  410. weather_station=weather_station,
  411. )
  412. @pytest.fixture(scope="module")
  413. def setup_assets(
  414. db, setup_accounts, setup_markets, setup_sources, setup_generic_asset_types
  415. ) -> dict[str, GenericAsset]:
  416. return create_assets(
  417. db, setup_accounts, setup_markets, setup_sources, setup_generic_asset_types
  418. )
  419. @pytest.fixture(scope="function")
  420. def setup_assets_fresh_db(
  421. fresh_db,
  422. setup_accounts_fresh_db,
  423. setup_markets_fresh_db,
  424. setup_sources_fresh_db,
  425. setup_generic_asset_types_fresh_db,
  426. ) -> dict[str, GenericAsset]:
  427. return create_assets(
  428. fresh_db,
  429. setup_accounts_fresh_db,
  430. setup_markets_fresh_db,
  431. setup_sources_fresh_db,
  432. setup_generic_asset_types_fresh_db,
  433. )
  434. def create_assets(
  435. db, setup_accounts, setup_markets, setup_sources, setup_asset_types
  436. ) -> dict[str, GenericAsset]:
  437. """Add assets with power sensors to known test accounts."""
  438. assets = []
  439. for asset_name in ["wind-asset-1", "wind-asset-2", "solar-asset-1"]:
  440. asset = GenericAsset(
  441. name=asset_name,
  442. generic_asset_type=(
  443. setup_asset_types["wind"]
  444. if "wind" in asset_name
  445. else setup_asset_types["solar"]
  446. ),
  447. owner=setup_accounts["Prosumer"],
  448. latitude=10,
  449. longitude=100,
  450. flex_context={
  451. "site-power-capacity": "1 MVA",
  452. "consumption-price": {"sensor": setup_markets["epex_da"].id},
  453. },
  454. attributes=dict(
  455. min_soc_in_mwh=0,
  456. max_soc_in_mwh=0,
  457. soc_in_mwh=0,
  458. is_producer=True,
  459. can_curtail=True,
  460. ),
  461. )
  462. sensor = Sensor(
  463. name="power",
  464. generic_asset=asset,
  465. event_resolution=timedelta(minutes=15),
  466. unit="MW",
  467. attributes=dict(
  468. daily_seasonality=True,
  469. yearly_seasonality=True,
  470. ),
  471. )
  472. db.session.add(sensor)
  473. assets.append(asset)
  474. # one day of test data (one complete sine curve)
  475. time_slots = pd.date_range(
  476. datetime(2015, 1, 1), datetime(2015, 1, 1, 23, 45), freq="15min"
  477. ).tz_localize("UTC")
  478. seed(42) # ensure same results over different test runs
  479. add_beliefs(
  480. db=db,
  481. sensor=sensor,
  482. time_slots=time_slots,
  483. values=[
  484. random() * (1 + np.sin(x * 2 * np.pi / (4 * 24)))
  485. for x in range(len(time_slots))
  486. ],
  487. source=setup_sources["Seita"],
  488. )
  489. db.session.commit()
  490. return {asset.name: asset for asset in assets}
  491. @pytest.fixture(scope="module")
  492. def setup_beliefs(db, setup_markets, setup_sources) -> int:
  493. """
  494. Make some beliefs.
  495. :returns: the number of beliefs set up
  496. """
  497. return create_beliefs(db, setup_markets, setup_sources)
  498. @pytest.fixture(scope="function")
  499. def setup_beliefs_fresh_db(
  500. fresh_db, setup_markets_fresh_db, setup_sources_fresh_db
  501. ) -> int:
  502. """
  503. Make some beliefs.
  504. :returns: the number of beliefs set up
  505. """
  506. return create_beliefs(fresh_db, setup_markets_fresh_db, setup_sources_fresh_db)
  507. def create_beliefs(db: SQLAlchemy, setup_markets, setup_sources) -> int:
  508. """
  509. :returns: the number of beliefs set up
  510. """
  511. sensor = db.session.execute(
  512. select(Sensor).filter(Sensor.name == "epex_da")
  513. ).scalar_one_or_none()
  514. beliefs = [
  515. TimedBelief(
  516. sensor=sensor,
  517. source=setup_sources["ENTSO-E"],
  518. event_value=21,
  519. event_start="2021-03-28 16:00+01",
  520. belief_horizon=timedelta(0),
  521. ),
  522. TimedBelief(
  523. sensor=sensor,
  524. source=setup_sources["ENTSO-E"],
  525. event_value=21,
  526. event_start="2021-03-28 17:00+01",
  527. belief_horizon=timedelta(0),
  528. ),
  529. TimedBelief(
  530. sensor=sensor,
  531. source=setup_sources["ENTSO-E"],
  532. event_value=20,
  533. event_start="2021-03-28 17:00+01",
  534. belief_horizon=timedelta(hours=2),
  535. cp=0.2,
  536. ),
  537. TimedBelief(
  538. sensor=sensor,
  539. source=setup_sources["ENTSO-E"],
  540. event_value=21,
  541. event_start="2021-03-28 17:00+01",
  542. belief_horizon=timedelta(hours=2),
  543. cp=0.5,
  544. ),
  545. ]
  546. db.session.add_all(beliefs)
  547. return len(beliefs)
  548. @pytest.fixture(scope="module")
  549. def add_market_prices(
  550. db: SQLAlchemy, setup_assets, setup_markets, setup_sources
  551. ) -> dict[str, Sensor]:
  552. return add_market_prices_common(db, setup_assets, setup_markets, setup_sources)
  553. @pytest.fixture(scope="function")
  554. def add_market_prices_fresh_db(
  555. fresh_db: SQLAlchemy,
  556. setup_assets_fresh_db,
  557. setup_markets_fresh_db,
  558. setup_sources_fresh_db,
  559. ) -> dict[str, Sensor]:
  560. return add_market_prices_common(
  561. fresh_db, setup_assets_fresh_db, setup_markets_fresh_db, setup_sources_fresh_db
  562. )
  563. def add_market_prices_common(
  564. db: SQLAlchemy, setup_assets, setup_markets, setup_sources
  565. ) -> dict[str, Sensor]:
  566. """Add three days of market prices for the EPEX day-ahead market."""
  567. # one day of test data (one complete sine curve)
  568. time_slots = initialize_index(
  569. start=pd.Timestamp("2015-01-01").tz_localize("Europe/Amsterdam"),
  570. end=pd.Timestamp("2015-01-02").tz_localize("Europe/Amsterdam"),
  571. resolution="1H",
  572. )
  573. seed(42) # ensure same results over different test runs
  574. add_beliefs(
  575. db=db,
  576. sensor=setup_markets["epex_da"],
  577. time_slots=time_slots,
  578. values=[
  579. random() * (1 + np.sin(x * 2 * np.pi / 24)) for x in range(len(time_slots))
  580. ],
  581. source=setup_sources["Seita"],
  582. )
  583. add_beliefs(
  584. db=db,
  585. sensor=setup_markets["epex_da_production"],
  586. time_slots=time_slots,
  587. values=[
  588. random() * (1 + np.sin(x * 2 * np.pi / 24)) for x in range(len(time_slots))
  589. ],
  590. source=setup_sources["Seita"],
  591. )
  592. # another day of test data (8 expensive hours, 8 cheap hours, and again 8 expensive hours)
  593. time_slots = initialize_index(
  594. start=pd.Timestamp("2015-01-02").tz_localize("Europe/Amsterdam"),
  595. end=pd.Timestamp("2015-01-03").tz_localize("Europe/Amsterdam"),
  596. resolution="1H",
  597. )
  598. add_beliefs(
  599. db=db,
  600. sensor=setup_markets["epex_da"],
  601. time_slots=time_slots,
  602. values=[100] * 8 + [90] * 8 + [100] * 8,
  603. source=setup_sources["Seita"],
  604. )
  605. # the third day of test data (8 hours with negative prices, followed by 16 expensive hours)
  606. time_slots = initialize_index(
  607. start=pd.Timestamp("2015-01-03").tz_localize("Europe/Amsterdam"),
  608. end=pd.Timestamp("2015-01-04").tz_localize("Europe/Amsterdam"),
  609. resolution="1H",
  610. )
  611. # consumption prices
  612. add_beliefs(
  613. db=db,
  614. sensor=setup_markets["epex_da"],
  615. time_slots=time_slots,
  616. values=[-10] * 8 + [100] * 16,
  617. source=setup_sources["Seita"],
  618. )
  619. # production prices = consumption prices - 40
  620. add_beliefs(
  621. db=db,
  622. sensor=setup_markets["epex_da_production"],
  623. time_slots=time_slots,
  624. values=[-50] * 8 + [60] * 16,
  625. source=setup_sources["Seita"],
  626. )
  627. # consumption prices for staleness tests
  628. time_slots = initialize_index(
  629. start=pd.Timestamp("2016-01-01").tz_localize("Europe/Amsterdam"),
  630. end=pd.Timestamp("2016-01-03").tz_localize("Europe/Amsterdam"),
  631. resolution="1H",
  632. )
  633. values_today = [
  634. random() * (1 + np.sin(x * 2 * np.pi / 24)) for x in range(len(time_slots))
  635. ]
  636. today_beliefs = [
  637. TimedBelief(
  638. event_start=dt,
  639. belief_horizon=timedelta(hours=0),
  640. event_value=val,
  641. source=setup_sources["Seita"],
  642. sensor=setup_markets["epex_da"],
  643. )
  644. for dt, val in zip(time_slots, values_today)
  645. ]
  646. db.session.add_all(today_beliefs)
  647. today_forecaster_beliefs = [
  648. TimedBelief(
  649. event_start=dt,
  650. belief_horizon=timedelta(hours=0),
  651. event_value=val,
  652. source=setup_sources["forecaster"],
  653. sensor=setup_markets["epex_da"],
  654. )
  655. for dt, val in zip(time_slots, values_today)
  656. ]
  657. db.session.add_all(today_forecaster_beliefs)
  658. today_reporter_beliefs = [
  659. TimedBelief(
  660. event_start=dt,
  661. belief_horizon=timedelta(hours=0),
  662. event_value=val,
  663. source=setup_sources["reporter"],
  664. sensor=setup_markets["epex_da"],
  665. )
  666. for dt, val in zip(time_slots, values_today)
  667. ]
  668. db.session.add_all(today_reporter_beliefs)
  669. return {
  670. "epex_da": setup_markets["epex_da"],
  671. "epex_da_production": setup_markets["epex_da_production"],
  672. }
  673. @pytest.fixture(scope="module")
  674. def add_battery_assets(
  675. db: SQLAlchemy,
  676. setup_roles_users,
  677. setup_accounts,
  678. setup_markets,
  679. setup_generic_asset_types,
  680. ) -> dict[str, GenericAsset]:
  681. return create_test_battery_assets(
  682. db, setup_accounts, setup_markets, setup_generic_asset_types
  683. )
  684. @pytest.fixture(scope="function")
  685. def add_battery_assets_fresh_db(
  686. fresh_db,
  687. setup_roles_users_fresh_db,
  688. setup_accounts_fresh_db,
  689. setup_markets_fresh_db,
  690. setup_generic_asset_types_fresh_db,
  691. ) -> dict[str, GenericAsset]:
  692. return create_test_battery_assets(
  693. fresh_db,
  694. setup_accounts_fresh_db,
  695. setup_markets_fresh_db,
  696. setup_generic_asset_types_fresh_db,
  697. )
  698. def create_test_battery_assets(
  699. db: SQLAlchemy, setup_accounts, setup_markets, generic_asset_types
  700. ) -> dict[str, GenericAsset]:
  701. """
  702. Add two battery assets, set their capacity values and their initial SOC.
  703. """
  704. building_type = GenericAssetType(name="building")
  705. db.session.add(building_type)
  706. test_building = GenericAsset(
  707. name="building",
  708. generic_asset_type=building_type,
  709. owner=setup_accounts["Prosumer"],
  710. flex_context={
  711. "site-power-capacity": "2 MVA",
  712. },
  713. )
  714. db.session.add(test_building)
  715. db.session.flush()
  716. battery_type = generic_asset_types["battery"]
  717. test_battery = GenericAsset(
  718. name="Test battery",
  719. owner=setup_accounts["Prosumer"],
  720. generic_asset_type=battery_type,
  721. latitude=10,
  722. longitude=100,
  723. parent_asset_id=test_building.id,
  724. flex_context={
  725. "site-power-capacity": "2 MVA",
  726. "consumption-price": {"sensor": setup_markets["epex_da"].id},
  727. },
  728. attributes={
  729. "max_soc_in_mwh": 5,
  730. "min_soc_in_mwh": 0,
  731. "soc_in_mwh": 2.5,
  732. "soc_datetime": "2015-01-01T00:00+01",
  733. "soc_udi_event_id": 203,
  734. "soc-usage": "0 kW",
  735. "is_consumer": True,
  736. "is_producer": True,
  737. "can_curtail": True,
  738. "can_shift": True,
  739. },
  740. )
  741. test_battery_sensor = Sensor(
  742. name="power",
  743. generic_asset=test_battery,
  744. event_resolution=timedelta(minutes=15),
  745. unit="MW",
  746. attributes=dict(
  747. daily_seasonality=True,
  748. weekly_seasonality=True,
  749. yearly_seasonality=True,
  750. ),
  751. )
  752. db.session.add(test_battery_sensor)
  753. test_battery_sensor_kw = Sensor(
  754. name="power (kW)",
  755. generic_asset=test_battery,
  756. event_resolution=timedelta(minutes=15),
  757. unit="kW",
  758. attributes=dict(
  759. daily_seasonality=True,
  760. weekly_seasonality=True,
  761. yearly_seasonality=True,
  762. ),
  763. )
  764. db.session.add(test_battery_sensor_kw)
  765. test_battery_no_prices = GenericAsset(
  766. name="Test battery with no known prices",
  767. owner=setup_accounts["Prosumer"],
  768. generic_asset_type=battery_type,
  769. latitude=10,
  770. longitude=100,
  771. flex_context={
  772. "site-power-capacity": "2 MVA",
  773. "consumption-price": {"sensor": setup_markets["epex_da"].id},
  774. },
  775. attributes=dict(
  776. max_soc_in_mwh=5,
  777. min_soc_in_mwh=0,
  778. soc_in_mwh=2.5,
  779. soc_datetime="2040-01-01T00:00+01",
  780. soc_udi_event_id=203,
  781. is_consumer=True,
  782. is_producer=True,
  783. can_curtail=True,
  784. can_shift=True,
  785. ),
  786. )
  787. test_battery_sensor_no_prices = Sensor(
  788. name="power",
  789. generic_asset=test_battery_no_prices,
  790. event_resolution=timedelta(minutes=15),
  791. unit="MW",
  792. attributes=dict(
  793. daily_seasonality=True,
  794. weekly_seasonality=True,
  795. yearly_seasonality=True,
  796. ),
  797. )
  798. db.session.add(test_battery_sensor_no_prices)
  799. test_battery_dynamic_power_capacity = GenericAsset(
  800. name="Test battery with dynamic power capacity",
  801. owner=setup_accounts["Prosumer"],
  802. generic_asset_type=battery_type,
  803. latitude=10,
  804. longitude=100,
  805. flex_context={
  806. "site-power-capacity": "10 MVA",
  807. "consumption-price": {"sensor": setup_markets["epex_da"].id},
  808. },
  809. attributes=dict(
  810. max_soc_in_mwh=20,
  811. min_soc_in_mwh=0,
  812. soc_in_mwh=2.0,
  813. ),
  814. )
  815. test_battery_dynamic_capacity_power_sensor = Sensor(
  816. name="power",
  817. generic_asset=test_battery_dynamic_power_capacity,
  818. event_resolution=timedelta(minutes=15),
  819. unit="MW",
  820. attributes=dict(
  821. capacity_in_mw=10,
  822. production_capacity="8 MW",
  823. consumption_capacity="0.5 MW",
  824. ),
  825. )
  826. db.session.add(test_battery_dynamic_capacity_power_sensor)
  827. test_small_battery = GenericAsset(
  828. name="Test small battery",
  829. owner=setup_accounts["Prosumer"],
  830. generic_asset_type=battery_type,
  831. latitude=10,
  832. longitude=100,
  833. flex_context={
  834. "site-power-capacity": "10 kVA",
  835. "consumption-price": {"sensor": setup_markets["epex_da"].id},
  836. },
  837. attributes=dict(
  838. max_soc_in_mwh=0.01,
  839. min_soc_in_mwh=0,
  840. soc_in_mwh=0.005,
  841. soc_datetime="2040-01-01T00:00+01",
  842. soc_udi_event_id=203,
  843. is_consumer=True,
  844. is_producer=True,
  845. can_curtail=True,
  846. can_shift=True,
  847. ),
  848. )
  849. test_battery_sensor_small = Sensor(
  850. name="power",
  851. generic_asset=test_small_battery,
  852. event_resolution=timedelta(minutes=15),
  853. unit="MW",
  854. attributes=dict(
  855. daily_seasonality=True,
  856. weekly_seasonality=True,
  857. yearly_seasonality=True,
  858. ),
  859. )
  860. db.session.add(test_battery_sensor_small)
  861. db.session.flush()
  862. return {
  863. "Test building": test_building,
  864. "Test battery": test_battery,
  865. "Test battery with no known prices": test_battery_no_prices,
  866. "Test small battery": test_small_battery,
  867. "Test battery with dynamic power capacity": test_battery_dynamic_power_capacity,
  868. }
  869. @pytest.fixture(scope="module")
  870. def add_charging_station_assets(
  871. db: SQLAlchemy, setup_accounts, setup_markets
  872. ) -> dict[str, GenericAsset]:
  873. return create_charging_station_assets(db, setup_accounts, setup_markets)
  874. @pytest.fixture(scope="function")
  875. def add_charging_station_assets_fresh_db(
  876. fresh_db: SQLAlchemy, setup_accounts_fresh_db, setup_markets_fresh_db
  877. ) -> dict[str, GenericAsset]:
  878. return create_charging_station_assets(
  879. fresh_db, setup_accounts_fresh_db, setup_markets_fresh_db
  880. )
  881. def create_charging_station_assets(
  882. db: SQLAlchemy, setup_accounts, setup_markets
  883. ) -> dict[str, GenericAsset]:
  884. """Add uni- and bi-directional charging station assets, set their capacity value and their initial SOC."""
  885. oneway_evse = GenericAssetType(name="one-way_evse")
  886. twoway_evse = GenericAssetType(name="two-way_evse")
  887. charging_station = GenericAsset(
  888. name="Test charging station",
  889. owner=setup_accounts["Prosumer"],
  890. generic_asset_type=oneway_evse,
  891. latitude=10,
  892. longitude=100,
  893. flex_context={
  894. "site-power-capacity": "2 MVA",
  895. "consumption-price": {"sensor": setup_markets["epex_da"].id},
  896. },
  897. attributes=dict(
  898. max_soc_in_mwh=5,
  899. min_soc_in_mwh=0,
  900. soc_in_mwh=2.5,
  901. soc_datetime="2015-01-01T00:00+01",
  902. soc_udi_event_id=203,
  903. is_consumer=True,
  904. is_producer=False,
  905. can_curtail=True,
  906. can_shift=True,
  907. ),
  908. )
  909. charging_station_power_sensor = Sensor(
  910. name="power",
  911. generic_asset=charging_station,
  912. unit="MW",
  913. event_resolution=timedelta(minutes=15),
  914. attributes=dict(
  915. daily_seasonality=True,
  916. weekly_seasonality=True,
  917. yearly_seasonality=True,
  918. ),
  919. )
  920. db.session.add(charging_station_power_sensor)
  921. bidirectional_charging_station = GenericAsset(
  922. name="Test charging station (bidirectional)",
  923. owner=setup_accounts["Prosumer"],
  924. generic_asset_type=twoway_evse,
  925. latitude=10,
  926. longitude=100,
  927. flex_context={
  928. "site-power-capacity": "2 MVA",
  929. "consumption-price": {"sensor": setup_markets["epex_da"].id},
  930. },
  931. attributes=dict(
  932. max_soc_in_mwh=5,
  933. min_soc_in_mwh=0,
  934. soc_in_mwh=2.5,
  935. soc_datetime="2015-01-01T00:00+01",
  936. soc_udi_event_id=203,
  937. is_consumer=True,
  938. is_producer=True,
  939. can_curtail=True,
  940. can_shift=True,
  941. ),
  942. )
  943. bidirectional_charging_station_power_sensor = Sensor(
  944. name="power",
  945. generic_asset=bidirectional_charging_station,
  946. unit="MW",
  947. event_resolution=timedelta(minutes=15),
  948. attributes=dict(
  949. daily_seasonality=True,
  950. weekly_seasonality=True,
  951. yearly_seasonality=True,
  952. ),
  953. )
  954. db.session.add(bidirectional_charging_station_power_sensor)
  955. return {
  956. "Test charging station": charging_station,
  957. "Test charging station (bidirectional)": bidirectional_charging_station,
  958. }
  959. @pytest.fixture(scope="module")
  960. def add_assets_with_site_power_limits(
  961. db: SQLAlchemy, setup_accounts, setup_generic_asset_types
  962. ) -> dict[str, Sensor]:
  963. """
  964. Add two batteries with different site power constraints. The first defines a symmetric site-level power limit of 2 MW
  965. by setting the site-power-capacity on the asset db model. The second defines a 900 kW consumption limit and 750 kW production limit.
  966. In addition, the site-power-capacity is also defined to check the fallback strategy.
  967. """
  968. battery_symmetric_site_power_limit = GenericAsset(
  969. name="Battery (with symmetric site limits)",
  970. owner=setup_accounts["Prosumer"],
  971. generic_asset_type=setup_generic_asset_types["battery"],
  972. flex_context={
  973. "site-power-capacity": "2 MVA",
  974. },
  975. attributes=dict(
  976. max_soc_in_mwh=5,
  977. min_soc_in_mwh=0,
  978. ),
  979. )
  980. battery_symmetric_power_sensor = Sensor(
  981. name="power",
  982. generic_asset=battery_symmetric_site_power_limit,
  983. unit="MW",
  984. )
  985. battery_asymmetric_site_power_limit = GenericAsset(
  986. name="Battery (with asymmetric site limits)",
  987. owner=setup_accounts["Prosumer"],
  988. generic_asset_type=setup_generic_asset_types["battery"],
  989. flex_context={
  990. "site-power-capacity": "2 MVA",
  991. "site-consumption-capacity": "900 kW",
  992. "site-production-capacity": "750 kW",
  993. },
  994. attributes=dict(
  995. max_soc_in_mwh=5,
  996. min_soc_in_mwh=0,
  997. ),
  998. )
  999. battery_asymmetric_power_sensor = Sensor(
  1000. name="power",
  1001. generic_asset=battery_asymmetric_site_power_limit,
  1002. unit="MW",
  1003. )
  1004. db.session.add_all(
  1005. [battery_symmetric_power_sensor, battery_asymmetric_power_sensor]
  1006. )
  1007. db.session.flush()
  1008. return {
  1009. "Battery (with symmetric site limits)": battery_symmetric_power_sensor,
  1010. "Battery (with asymmetric site limits)": battery_asymmetric_power_sensor,
  1011. }
  1012. @pytest.fixture(scope="module")
  1013. def add_weather_sensors(db, setup_generic_asset_types) -> dict[str, Sensor]:
  1014. return create_weather_sensors(db, setup_generic_asset_types)
  1015. @pytest.fixture(scope="function")
  1016. def add_weather_sensors_fresh_db(
  1017. fresh_db, setup_generic_asset_types_fresh_db
  1018. ) -> dict[str, Sensor]:
  1019. return create_weather_sensors(fresh_db, setup_generic_asset_types_fresh_db)
  1020. def create_weather_sensors(db: SQLAlchemy, generic_asset_types) -> dict[str, Sensor]:
  1021. """Add a weather station asset with two weather sensors."""
  1022. weather_station = GenericAsset(
  1023. name="Test weather station",
  1024. generic_asset_type=generic_asset_types["weather_station"],
  1025. latitude=33.4843866,
  1026. longitude=126,
  1027. )
  1028. db.session.add(weather_station)
  1029. wind_sensor = Sensor(
  1030. name="wind speed",
  1031. generic_asset=weather_station,
  1032. event_resolution=timedelta(minutes=5),
  1033. unit="m/s",
  1034. )
  1035. db.session.add(wind_sensor)
  1036. temp_sensor = Sensor(
  1037. name="temperature",
  1038. generic_asset=weather_station,
  1039. event_resolution=timedelta(minutes=5),
  1040. unit="°C",
  1041. )
  1042. db.session.add(temp_sensor)
  1043. return {"wind": wind_sensor, "temperature": temp_sensor, "asset": weather_station}
  1044. @pytest.fixture(scope="module")
  1045. def add_sensors(db: SQLAlchemy, setup_generic_assets):
  1046. """Add some generic sensors."""
  1047. height_sensor = Sensor(
  1048. name="height", unit="m", generic_asset=setup_generic_assets["troposphere"]
  1049. )
  1050. db.session.add(height_sensor)
  1051. return height_sensor
  1052. @pytest.fixture(scope="module")
  1053. def battery_soc_sensor(db: SQLAlchemy, setup_generic_assets):
  1054. """Add a battery SOC sensor to the db."""
  1055. return create_battery_soc_sensor(db, setup_generic_assets)
  1056. @pytest.fixture(scope="function")
  1057. def battery_soc_sensor_fresh_db(fresh_db: SQLAlchemy, setup_generic_assets_fresh_db):
  1058. """Add a battery SOC sensor to the fresh db."""
  1059. return create_battery_soc_sensor(fresh_db, setup_generic_assets_fresh_db)
  1060. def create_battery_soc_sensor(db: SQLAlchemy, setup_generic_assets):
  1061. """Add a battery SOC sensor."""
  1062. soc_sensor = Sensor(
  1063. name="state of charge",
  1064. unit="%",
  1065. generic_asset=setup_generic_assets["test_battery"],
  1066. )
  1067. db.session.add(soc_sensor)
  1068. return soc_sensor
  1069. @pytest.fixture
  1070. def run_as_cli(app, monkeypatch):
  1071. """
  1072. Use this to run your test as if it is run from the CLI.
  1073. This is useful where some auth restrictions (e.g. for querying) are in place.
  1074. FlexMeasures is more lenient with them if the CLI is running, as it considers
  1075. the user a sysadmin.
  1076. """
  1077. monkeypatch.setitem(app.config, "PRETEND_RUNNING_AS_CLI", True)
  1078. @pytest.fixture(scope="function")
  1079. def clean_redis(app):
  1080. failed = app.queues["forecasting"].failed_job_registry
  1081. app.queues["forecasting"].empty()
  1082. for job_id in failed.get_job_ids():
  1083. failed.remove(app.queues["forecasting"].fetch_job(job_id))
  1084. app.redis_connection.flushdb()
  1085. @pytest.fixture(scope="session", autouse=True)
  1086. def error_endpoints(app):
  1087. """Adding endpoints for the test session, which can be used to generate errors.
  1088. Adding endpoints only for testing can only be done *before* the first request
  1089. so scope=session and autouse=True are required, as well as adding them in the top
  1090. conftest module."""
  1091. @app.route("/raise-error")
  1092. def error_generator():
  1093. if "type" in request.args:
  1094. if request.args.get("type") == "server_error":
  1095. raise InternalServerError("InternalServerError Test Message")
  1096. if request.args.get("type") == "bad_request":
  1097. raise BadRequest("BadRequest Test Message")
  1098. if request.args.get("type") == "gone":
  1099. raise Gone("Gone Test Message")
  1100. if request.args.get("type") == "unauthorized":
  1101. raise Unauthorized("Unauthorized Test Message")
  1102. if request.args.get("type") == "forbidden":
  1103. raise Forbidden("Forbidden Test Message")
  1104. return jsonify({"message": "Nothing bad happened."}), 200
  1105. @app.route("/protected-endpoint-only-for-admins")
  1106. @roles_accepted(ADMIN_ROLE)
  1107. def vips_only():
  1108. return jsonify({"message": "Nothing bad happened."}), 200
  1109. @pytest.fixture(scope="module")
  1110. def capacity_sensors(db, add_battery_assets, setup_sources):
  1111. battery = add_battery_assets["Test battery with dynamic power capacity"]
  1112. production_capacity_sensor = Sensor(
  1113. name="production capacity",
  1114. generic_asset=battery,
  1115. unit="kW",
  1116. event_resolution="PT15M",
  1117. attributes={"consumption_is_positive": True},
  1118. )
  1119. consumption_capacity_sensor = Sensor(
  1120. name="consumption capacity",
  1121. generic_asset=battery,
  1122. unit="kW",
  1123. event_resolution="PT15M",
  1124. attributes={"consumption_is_positive": True},
  1125. )
  1126. power_capacity_sensor = Sensor(
  1127. name="power capacity",
  1128. generic_asset=battery,
  1129. unit="kW",
  1130. event_resolution="PT15M",
  1131. attributes={"consumption_is_positive": True},
  1132. )
  1133. site_power_capacity_sensor = Sensor(
  1134. name="site power capacity",
  1135. generic_asset=battery,
  1136. unit="kW",
  1137. event_resolution="PT15M",
  1138. attributes={"consumption_is_positive": True},
  1139. )
  1140. db.session.add_all(
  1141. [
  1142. production_capacity_sensor,
  1143. consumption_capacity_sensor,
  1144. site_power_capacity_sensor,
  1145. ]
  1146. )
  1147. db.session.flush()
  1148. time_slots = pd.date_range(
  1149. datetime(2015, 1, 2), datetime(2015, 1, 2, 7, 45), freq="15min"
  1150. ).tz_localize("Europe/Amsterdam")
  1151. add_beliefs(
  1152. db=db,
  1153. sensor=production_capacity_sensor,
  1154. time_slots=time_slots,
  1155. values=[200] * 4 * 4 + [300] * 4 * 4,
  1156. source=setup_sources["Seita"],
  1157. )
  1158. add_beliefs(
  1159. db=db,
  1160. sensor=consumption_capacity_sensor,
  1161. time_slots=time_slots,
  1162. values=[250] * 4 * 4 + [150] * 4 * 4,
  1163. source=setup_sources["Seita"],
  1164. )
  1165. add_beliefs(
  1166. db=db,
  1167. sensor=power_capacity_sensor,
  1168. time_slots=time_slots,
  1169. values=[225] * 4 * 4 + [199] * 4 * 4,
  1170. source=setup_sources["Seita"],
  1171. )
  1172. add_beliefs(
  1173. db=db,
  1174. sensor=site_power_capacity_sensor,
  1175. time_slots=time_slots,
  1176. values=[1300] * 4 * 4 + [1050] * 4 * 4,
  1177. source=setup_sources["Seita"],
  1178. )
  1179. db.session.commit()
  1180. time_slots = pd.date_range(
  1181. datetime(2016, 1, 2), datetime(2016, 1, 2, 7, 45), freq="15min"
  1182. ).tz_localize("Europe/Amsterdam")
  1183. values = [250] * 4 * 4 + [150] * 4 * 4
  1184. beliefs = [
  1185. TimedBelief(
  1186. event_start=dt,
  1187. event_value=val,
  1188. sensor=production_capacity_sensor,
  1189. source=setup_sources["DummySchedule"],
  1190. belief_time="2015-01-02T00:00+01",
  1191. )
  1192. for dt, val in zip(time_slots, values)
  1193. ]
  1194. db.session.add_all(beliefs)
  1195. db.session.commit()
  1196. yield dict(
  1197. production=production_capacity_sensor,
  1198. consumption=consumption_capacity_sensor,
  1199. power_capacity=power_capacity_sensor,
  1200. site_power_capacity=site_power_capacity_sensor,
  1201. )
  1202. @pytest.fixture(scope="module")
  1203. def soc_sensors(db, add_battery_assets, setup_sources) -> tuple:
  1204. """Add battery sensors for instantaneous soc-maxima (in kWh), soc-maxima (in MWh) and soc-targets (in MWh).
  1205. The SoC values on each sensor linearly increase from 0 to 5 MWh.
  1206. """
  1207. battery = add_battery_assets["Test battery with dynamic power capacity"]
  1208. soc_maxima = Sensor(
  1209. name="soc_maxima",
  1210. generic_asset=battery,
  1211. unit="kWh",
  1212. event_resolution=timedelta(0),
  1213. )
  1214. soc_minima = Sensor(
  1215. name="soc_minima",
  1216. generic_asset=battery,
  1217. unit="MWh",
  1218. event_resolution=timedelta(0),
  1219. )
  1220. soc_targets = Sensor(
  1221. name="soc_targets",
  1222. generic_asset=battery,
  1223. unit="MWh",
  1224. event_resolution=timedelta(0),
  1225. )
  1226. db.session.add_all([soc_maxima, soc_minima, soc_targets])
  1227. db.session.flush()
  1228. time_slots = pd.date_range(
  1229. datetime(2015, 1, 1, 2), datetime(2015, 1, 2), freq="15min"
  1230. ).tz_localize("Europe/Amsterdam")
  1231. values = np.arange(len(time_slots)) / (len(time_slots) - 1)
  1232. values = values * 5
  1233. add_beliefs(
  1234. db=db,
  1235. sensor=soc_maxima,
  1236. time_slots=time_slots,
  1237. values=values * 1000, # MWh -> kWh
  1238. source=setup_sources["Seita"],
  1239. )
  1240. add_beliefs(
  1241. db=db,
  1242. sensor=soc_minima,
  1243. time_slots=time_slots,
  1244. values=values,
  1245. source=setup_sources["Seita"],
  1246. )
  1247. add_beliefs(
  1248. db=db,
  1249. sensor=soc_targets,
  1250. time_slots=time_slots,
  1251. values=values,
  1252. source=setup_sources["Seita"],
  1253. )
  1254. soc_schedule = pd.Series(data=values, index=time_slots)
  1255. yield soc_maxima, soc_minima, soc_targets, soc_schedule
  1256. @pytest.fixture(scope="module")
  1257. def setup_multiple_sources(db, add_battery_assets):
  1258. battery = add_battery_assets["Test battery with dynamic power capacity"]
  1259. test_sensor = Sensor(
  1260. name="test sensor",
  1261. generic_asset=battery,
  1262. unit="kW",
  1263. event_resolution=timedelta(minutes=15),
  1264. )
  1265. s1 = DataSource(name="S1", type="type 1")
  1266. s2 = DataSource(name="S2", type="type 2")
  1267. s3 = DataSource(name="S3", type="type 3")
  1268. db.session.add_all([s1, s2, s3, test_sensor])
  1269. for s in [s1, s2]:
  1270. add_beliefs(
  1271. db=db,
  1272. sensor=test_sensor,
  1273. time_slots=[pd.Timestamp("2024-01-01T10:00:00+01:00")],
  1274. values=[1],
  1275. source=s,
  1276. )
  1277. add_beliefs(
  1278. db=db,
  1279. sensor=test_sensor,
  1280. time_slots=[pd.Timestamp("2024-01-02T10:00:00+01:00")],
  1281. values=[1],
  1282. source=s3,
  1283. )
  1284. db.session.commit()
  1285. return test_sensor, s1, s2, s3
  1286. def add_beliefs(
  1287. db,
  1288. sensor: Sensor,
  1289. time_slots: pd.DatetimeIndex,
  1290. values: list[int | float] | np.ndarray,
  1291. source: DataSource,
  1292. ):
  1293. beliefs = [
  1294. TimedBelief(
  1295. event_start=dt,
  1296. belief_horizon=parse_duration("PT0M"),
  1297. event_value=val,
  1298. sensor=sensor,
  1299. source=source,
  1300. )
  1301. for dt, val in zip(time_slots, values)
  1302. ]
  1303. db.session.add_all(beliefs)