diff --git a/.claude/hooks/pre-commit-check.sh b/.claude/hooks/pre-commit-check.sh index ded1eadb57..dc9f083ae8 100755 --- a/.claude/hooks/pre-commit-check.sh +++ b/.claude/hooks/pre-commit-check.sh @@ -11,4 +11,3 @@ if ! output=$(uv run pre-commit run --all-files 2>&1); then exit 2 fi exit 0 - diff --git a/.github/instructions/feature-branch-sync.instructions.md b/.github/instructions/feature-branch-sync.instructions.md index 926d917fac..97abfb9c56 100644 --- a/.github/instructions/feature-branch-sync.instructions.md +++ b/.github/instructions/feature-branch-sync.instructions.md @@ -34,7 +34,7 @@ git commit -m "Merge origin/main into feature branch" ``` This ensures your implementation starts from the latest state of the repository. - + ## Why this matters - Merging later causes merge conflicts to compound diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 46799cd0e8..f4554d2682 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -55,7 +55,7 @@ jobs: - name: Add beliefs run: | docker exec --env-file .env fm-container flexmeasures \ - add beliefs --sensor ${FM_TOY_PRICE_SENSOR_ID} --source toy-user prices-tomorrow.csv --timezone Europe/Amsterdam + add beliefs --sensor ${FM_TOY_PRICE_SENSOR_ID} --source toy-user prices-tomorrow.csv --timezone Europe/Amsterdam --unit EUR/MWh - name: Export TOMORROW run: echo "TOMORROW=$(date --date="next day" '+%Y-%m-%d')" >> $GITHUB_ENV @@ -70,4 +70,4 @@ jobs: run: | docker exec --env-file .env fm-container flexmeasures add schedule --sensor ${FM_TOY_PROCESS_BREAKABLE_SENSOR_ID} --scheduler ProcessScheduler \ --start ${TOMORROW}T00:00:00+02:00 --duration PT24H --flex-context '{"consumption-price": {"sensor": '"${FM_TOY_PRICE_SENSOR_ID}"'}}' \ - --flex-model "{\"duration\": \"PT4H\", \"process-type\": \"BREAKABLE\", \"power\": 0.2, \"time-restrictions\": [{\"start\": \"${TOMORROW}T15:00:00+02:00\", \"duration\": \"PT1H\"}]}" + --flex-model "{\"duration\": \"PT4H\", \"process-type\": \"BREAKABLE\", \"power\": 200, \"time-restrictions\": [{\"start\": \"${TOMORROW}T15:00:00+02:00\", \"duration\": \"PT1H\"}]}" diff --git a/documentation/changelog.rst b/documentation/changelog.rst index a734982fe4..f51da1671d 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -30,6 +30,7 @@ New features * In the UI, asset and sensor lists can be filtered by ID prefix through API-backed search fields [see `PR #2231 `_] * Support configurable lower and upper bounds and snapping for forecast post-processing [see `PR #2273 `_] * Sensor references in flex-model and flex-context support various ways of filtering by source [see `PR #2209 `_] +* Create toy tutorial accounts with kW-scale power sensors and ``EUR/kWh`` day-ahead prices [see `PR #2223 `_] * Let storage scheduling infer missing ``power-capacity`` from directional device capacities before falling back to site capacity, and default the missing opposite capacity to zero when only a non-zero ``consumption-capacity`` or ``production-capacity`` is configured [see `PR #2222 `_] * Support multiple feeders to a shared storage [see `PR #2001 `_, `PR #2321 `_, `PR #2322 `_ and `PR #2325 `_] * The flex-context can now define multiple commodities, each specifying their own prices and grid capacities [see `PR #1946 `_, `PR #2172 `_, `PR #2235 `_, `PR #2271 `_, `PR #2355 `_ and `PR #2380 `_] diff --git a/documentation/dev/docker-compose.rst b/documentation/dev/docker-compose.rst index f74e651f85..5088ee908e 100644 --- a/documentation/dev/docker-compose.rst +++ b/documentation/dev/docker-compose.rst @@ -123,7 +123,7 @@ The charging/discharging schedule should be there: .. code-block:: bash ┌────────────────────────────────────────────────────────────┐ - │ ▐ ▐▀▀▌ ▛▀▀│ 0.5MW + │ ▐ ▐▀▀▌ ▛▀▀│ 500kW │ ▞▌ ▌ ▌ ▌ │ │ ▌▌ ▌ ▐ ▗▘ │ │ ▌▌ ▌ ▐ ▐ │ @@ -131,7 +131,7 @@ The charging/discharging schedule should be there: │ ▐ ▐ ▐ ▝▖ ▞ │ │ ▌ ▐ ▐ ▌ ▌ │ │ ▐ ▝▖ ▌ ▌ ▌ │ - │▀▘───▀▀▀▀▖─────▌────▀▀▀▀▀▀▀▀▀▌─────▐▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▘───│ 0.0MW + │▀▘───▀▀▀▀▖─────▌────▀▀▀▀▀▀▀▀▀▌─────▐▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▘───│ 0kW │ ▌ ▐ ▚ ▌ │ │ ▌ ▞ ▐ ▗▘ │ │ ▌ ▌ ▐ ▞ │ @@ -139,7 +139,7 @@ The charging/discharging schedule should be there: │ ▐ ▐ ▌ ▗▘ │ │ ▐ ▌ ▌ ▐ │ │ ▝▖ ▌ ▌ ▞ │ - │ ▙▄▟ ▐▄▄▌ │ -0.5MW + │ ▙▄▟ ▐▄▄▌ │ -500kW └────────────────────────────────────────────────────────────┘ 10 20 30 40 ██ discharging diff --git a/documentation/features/scheduling.rst b/documentation/features/scheduling.rst index a653211690..6284a26537 100644 --- a/documentation/features/scheduling.rst +++ b/documentation/features/scheduling.rst @@ -686,5 +686,3 @@ Here are some thoughts on further innovation: This is ongoing architecture design work, and therefore happens in development settings, until we are happy with the outcomes. Thoughts welcome :) - Aggregating flexibility of a group of assets (e.g. a neighborhood) and optimizing its aggregated usage (e.g. for grid congestion support) is also an exciting direction for expansion. - - diff --git a/documentation/tut/scripts/run-tutorial-in-docker.sh b/documentation/tut/scripts/run-tutorial-in-docker.sh index 9151806406..c96b78a70b 100755 --- a/documentation/tut/scripts/run-tutorial-in-docker.sh +++ b/documentation/tut/scripts/run-tutorial-in-docker.sh @@ -39,7 +39,7 @@ ${TOMORROW}T23:00:00,7" > prices-tomorrow.csv docker cp prices-tomorrow.csv $CONTAINER_NAME:/app docker exec -it $CONTAINER_NAME flexmeasures add beliefs \ - --sensor ${FM_TOY_PRICE_SENSOR_ID} --source toy-user /app/prices-tomorrow.csv --timezone Europe/Amsterdam + --sensor ${FM_TOY_PRICE_SENSOR_ID} --source toy-user /app/prices-tomorrow.csv --timezone Europe/Amsterdam --unit EUR/MWh echo "[TUTORIAL-RUNNER] creating schedule ..." docker exec -it $CONTAINER_NAME flexmeasures add schedule \ diff --git a/documentation/tut/scripts/run-tutorial2-in-docker.sh b/documentation/tut/scripts/run-tutorial2-in-docker.sh index 4c39673f73..0ed539efea 100755 --- a/documentation/tut/scripts/run-tutorial2-in-docker.sh +++ b/documentation/tut/scripts/run-tutorial2-in-docker.sh @@ -13,30 +13,30 @@ echo "[TUTORIAL-RUNNER] loading solar production data..." TOMORROW=$(date --date="next day" '+%Y-%m-%d') echo "Hour,Production -${TOMORROW}T00:00:00,0.0 -${TOMORROW}T01:00:00,0.0 -${TOMORROW}T02:00:00,0.0 -${TOMORROW}T03:00:00,0.0 -${TOMORROW}T04:00:00,0.01 -${TOMORROW}T05:00:00,0.03 -${TOMORROW}T06:00:00,0.06 -${TOMORROW}T07:00:00,0.1 -${TOMORROW}T08:00:00,0.14 -${TOMORROW}T09:00:00,0.17 -${TOMORROW}T10:00:00,0.19 -${TOMORROW}T11:00:00,0.21 -${TOMORROW}T12:00:00,0.22 -${TOMORROW}T13:00:00,0.21 -${TOMORROW}T14:00:00,0.19 -${TOMORROW}T15:00:00,0.17 -${TOMORROW}T16:00:00,0.14 -${TOMORROW}T17:00:00,0.1 -${TOMORROW}T18:00:00,0.06 -${TOMORROW}T19:00:00,0.03 -${TOMORROW}T20:00:00,0.01 -${TOMORROW}T21:00:00,0.0 -${TOMORROW}T22:00:00,0.0 -${TOMORROW}T23:00:00,0.0" > solar-tomorrow.csv +${TOMORROW}T00:00:00,0 +${TOMORROW}T01:00:00,0 +${TOMORROW}T02:00:00,0 +${TOMORROW}T03:00:00,0 +${TOMORROW}T04:00:00,10 +${TOMORROW}T05:00:00,30 +${TOMORROW}T06:00:00,60 +${TOMORROW}T07:00:00,100 +${TOMORROW}T08:00:00,140 +${TOMORROW}T09:00:00,170 +${TOMORROW}T10:00:00,190 +${TOMORROW}T11:00:00,210 +${TOMORROW}T12:00:00,220 +${TOMORROW}T13:00:00,210 +${TOMORROW}T14:00:00,190 +${TOMORROW}T15:00:00,170 +${TOMORROW}T16:00:00,140 +${TOMORROW}T17:00:00,100 +${TOMORROW}T18:00:00,60 +${TOMORROW}T19:00:00,30 +${TOMORROW}T20:00:00,10 +${TOMORROW}T21:00:00,0 +${TOMORROW}T22:00:00,0 +${TOMORROW}T23:00:00,0" > solar-tomorrow.csv docker cp solar-tomorrow.csv $CONTAINER_NAME:/app/ @@ -44,7 +44,7 @@ echo "[TUTORIAL-RUNNER] adding source ..." docker exec -it $CONTAINER_NAME flexmeasures add source --name "toy-forecaster" --type forecaster echo "[TUTORIAL-RUNNER] adding beliefs ..." -docker exec -it $CONTAINER_NAME flexmeasures add beliefs --sensor ${FM_TOY_SOLAR_SENSOR_ID} --source 4 /app/solar-tomorrow.csv --timezone Europe/Amsterdam +docker exec -it $CONTAINER_NAME flexmeasures add beliefs --sensor ${FM_TOY_SOLAR_SENSOR_ID} --source 2 /app/solar-tomorrow.csv --timezone Europe/Amsterdam echo "[TUTORIAL-RUNNER] showing beliefs ..." docker exec -it $CONTAINER_NAME flexmeasures show beliefs --sensor ${FM_TOY_SOLAR_SENSOR_ID} --start ${TOMORROW}T07:00:00+01:00 --duration PT12H diff --git a/documentation/tut/scripts/run-tutorial3-in-docker.sh b/documentation/tut/scripts/run-tutorial3-in-docker.sh index 5343443654..e0b319a026 100755 --- a/documentation/tut/scripts/run-tutorial3-in-docker.sh +++ b/documentation/tut/scripts/run-tutorial3-in-docker.sh @@ -15,12 +15,12 @@ echo "[TUTORIAL-RUNNER] Computing schedule for PV curtailment (using artificial echo '''{ "consumption-price": [ - {"start": "'${TOMORROW}'T00:00+01", "duration": "PT24H", "value": "10 EUR/MWh"} + {"start": "'${TOMORROW}'T00:00+01", "duration": "PT24H", "value": "0.010 EUR/kWh"} ], "production-price": [ - {"start": "'${TOMORROW}'T05:00+01", "duration": "PT7H", "value": "4 EUR/MWh"}, - {"start": "'${TOMORROW}'T12:00+01", "duration": "PT2H", "value": "-10 EUR/MWh"}, - {"start": "'${TOMORROW}'T14:00+01", "duration": "PT7H", "value": "4 EUR/MWh"} + {"start": "'${TOMORROW}'T05:00+01", "duration": "PT7H", "value": "0.004 EUR/kWh"}, + {"start": "'${TOMORROW}'T12:00+01", "duration": "PT2H", "value": "-0.010 EUR/kWh"}, + {"start": "'${TOMORROW}'T14:00+01", "duration": "PT7H", "value": "0.004 EUR/kWh"} ] }''' > tutorial3-priceprofile-flex-context.json docker cp tutorial3-priceprofile-flex-context.json $CONTAINER_NAME:/app/ @@ -36,7 +36,7 @@ docker exec -it $CONTAINER_NAME flexmeasures show beliefs --sensor ${FM_TOY_SOLA echo "[TUTORIAL-RUNNER] Cleaning solar data for the next steps ..." # remove all previous beliefs on PV sensor so we don't have schedules mixed in the next run (issue 1807 can help with this, so selection by source works) docker exec -it $CONTAINER_NAME flexmeasures delete beliefs --sensor ${FM_TOY_SOLAR_SENSOR_ID} --force -docker exec -it $CONTAINER_NAME flexmeasures add beliefs --sensor ${FM_TOY_SOLAR_SENSOR_ID} --source 4 /app/solar-tomorrow.csv --timezone Europe/Amsterdam +docker exec -it $CONTAINER_NAME flexmeasures add beliefs --sensor ${FM_TOY_SOLAR_SENSOR_ID} --source 2 /app/solar-tomorrow.csv --timezone Europe/Amsterdam echo "[TUTORIAL-RUNNER] Now running both battery and PV together, still using block price profiles ..." docker exec -it $CONTAINER_NAME flexmeasures add schedule --asset ${FM_TOY_BUILDING_ASSET_ID} \ @@ -48,7 +48,7 @@ echo "[TUTORIAL-RUNNER] showing PV and battery schedule ..." docker exec -it $CONTAINER_NAME flexmeasures show beliefs --sensor ${FM_TOY_SOLAR_SENSOR_ID} --sensor ${FM_TOY_BATTERY_SENSOR_ID} --start ${TOMORROW}T07:00:00+01:00 --duration PT12H docker exec -it $CONTAINER_NAME flexmeasures delete beliefs --sensor ${FM_TOY_SOLAR_SENSOR_ID} --force -docker exec -it $CONTAINER_NAME flexmeasures add beliefs --sensor ${FM_TOY_SOLAR_SENSOR_ID} --source 4 /app/solar-tomorrow.csv --timezone Europe/Amsterdam +docker exec -it $CONTAINER_NAME flexmeasures add beliefs --sensor ${FM_TOY_SOLAR_SENSOR_ID} --source 2 /app/solar-tomorrow.csv --timezone Europe/Amsterdam echo "[TUTORIAL-RUNNER] Now running both battery and PV together, with realistic DA prices and larger battery ..." docker exec -it $CONTAINER_NAME flexmeasures add schedule --asset ${FM_TOY_BUILDING_ASSET_ID} \ diff --git a/documentation/tut/scripts/run-tutorial4-in-docker.sh b/documentation/tut/scripts/run-tutorial4-in-docker.sh index b76ed35faa..c3eba206c6 100755 --- a/documentation/tut/scripts/run-tutorial4-in-docker.sh +++ b/documentation/tut/scripts/run-tutorial4-in-docker.sh @@ -15,16 +15,16 @@ echo "[TUTORIAL-RUNNER] Creating three process schedules ..." docker exec -it $CONTAINER_NAME flexmeasures add schedule --sensor ${FM_TOY_PROCESS_INFLEXIBLE_SENSOR_ID} --scheduler ProcessScheduler \ --start ${TOMORROW}T00:00:00+02:00 --duration PT24H \ --flex-context '{"consumption-price": {"sensor": '"${FM_TOY_PRICE_SENSOR_ID}"'}}' \ - --flex-model '{"duration": "PT4H", "process-type": "INFLEXIBLE", "power": 0.2, "time-restrictions": [{"start": "'"${TOMORROW}"'T15:00:00+02:00", "duration": "PT1H"}]}' + --flex-model '{"duration": "PT4H", "process-type": "INFLEXIBLE", "power": 200, "time-restrictions": [{"start": "'"${TOMORROW}"'T15:00:00+02:00", "duration": "PT1H"}]}' docker exec -it $CONTAINER_NAME flexmeasures add schedule --sensor ${FM_TOY_PROCESS_BREAKABLE_SENSOR_ID} --scheduler ProcessScheduler \ --start ${TOMORROW}T00:00:00+02:00 --duration PT24H \ --flex-context '{"consumption-price": {"sensor": '"${FM_TOY_PRICE_SENSOR_ID}"'}}' \ - --flex-model '{"duration": "PT4H", "process-type": "BREAKABLE", "power": 0.2, "time-restrictions": [{"start": "'"${TOMORROW}"'T15:00:00+02:00", "duration": "PT1H"}]}' + --flex-model '{"duration": "PT4H", "process-type": "BREAKABLE", "power": 200, "time-restrictions": [{"start": "'"${TOMORROW}"'T15:00:00+02:00", "duration": "PT1H"}]}' docker exec -it $CONTAINER_NAME flexmeasures add schedule --sensor ${FM_TOY_PROCESS_SHIFTABLE_SENSOR_ID} --scheduler ProcessScheduler \ --start ${TOMORROW}T00:00:00+02:00 --duration PT24H \ --flex-context '{"consumption-price": {"sensor": '"${FM_TOY_PRICE_SENSOR_ID}"'}}' \ - --flex-model '{"duration": "PT4H", "process-type": "SHIFTABLE", "power": 0.2, "time-restrictions": [{"start": "'"${TOMORROW}"'T15:00:00+02:00", "duration": "PT1H"}]}' + --flex-model '{"duration": "PT4H", "process-type": "SHIFTABLE", "power": 200, "time-restrictions": [{"start": "'"${TOMORROW}"'T15:00:00+02:00", "duration": "PT1H"}]}' -echo "Now visit http://localhost:5000/assets/6/graphs to see all three schedules." +echo "Now visit http://localhost:5000/assets/8/graphs to see all three schedules." diff --git a/documentation/tut/scripts/run-tutorial5-in-docker.sh b/documentation/tut/scripts/run-tutorial5-in-docker.sh index 6f34c0ff13..5b43090a4d 100755 --- a/documentation/tut/scripts/run-tutorial5-in-docker.sh +++ b/documentation/tut/scripts/run-tutorial5-in-docker.sh @@ -17,7 +17,7 @@ echo "[TUTORIAL-RUNNER] Setting up toy account with reporters..." echo "[TUTORIAL-RUNNER] Show grid connection capacity ..." docker exec -it $CONTAINER_NAME flexmeasures show beliefs --sensor ${FM_TOY_GRID_CAPACITY_SENSOR_ID} --start ${TOMORROW}T00:00:00+02:00 --duration PT24H --resolution PT1H -docker exec -it $CONTAINER_NAME flexmeasures show data-sources --show-attributes --id 6 +docker exec -it $CONTAINER_NAME flexmeasures show data-sources --show-attributes --id 3 echo "[TUTORIAL-RUNNER] Configure headroom reporter ..." @@ -33,7 +33,7 @@ docker cp headroom-config.json $CONTAINER_NAME:/app echo " { 'input': [{'name': 'grid connection capacity', 'sensor': ${FM_TOY_GRID_CAPACITY_SENSOR_ID}}, - {'name': 'PV', 'sensor': ${FM_TOY_SOLAR_SENSOR_ID}, 'sources': [4]}], + {'name': 'PV', 'sensor': ${FM_TOY_SOLAR_SENSOR_ID}, 'sources': [2]}], 'output': [{'sensor': ${FM_TOY_HEADROOM_SENSOR_ID}}] }" > headroom-parameters.json docker cp headroom-parameters.json $CONTAINER_NAME:/app @@ -56,17 +56,17 @@ echo "[TUTORIAL-RUNNER] now the inflexible process ..." echo " { 'input': [{'sensor': ${FM_TOY_PROCESS_INFLEXIBLE_SENSOR_ID}}], - 'output': [{'sensor': 9}] + 'output': [{'sensor': 15}] }" > inflexible-parameters.json docker cp inflexible-parameters.json $CONTAINER_NAME:/app -docker exec -it $CONTAINER_NAME flexmeasures add report --source 6 \ +docker exec -it $CONTAINER_NAME flexmeasures add report --source 3 \ --parameters inflexible-parameters.json \ --start-offset DB,1D --end-offset DB,2D echo "[TUTORIAL-RUNNER] showing reported data ..." -docker exec -it $CONTAINER_NAME bash -c "flexmeasures show beliefs --sensor 9 --start ${TOMORROW}T00:00:00+01:00 --duration PT24H" +docker exec -it $CONTAINER_NAME bash -c "flexmeasures show beliefs --sensor 15 --start ${TOMORROW}T00:00:00+01:00 --duration PT24H" echo "[TUTORIAL-RUNNER] now the breakable process ..." @@ -74,17 +74,17 @@ echo "[TUTORIAL-RUNNER] now the breakable process ..." echo " { 'input': [{'sensor': ${FM_TOY_PROCESS_BREAKABLE_SENSOR_ID}}], - 'output': [{'sensor': 10}] + 'output': [{'sensor': 16}] }" > breakable-parameters.json docker cp breakable-parameters.json $CONTAINER_NAME:/app -docker exec -it $CONTAINER_NAME flexmeasures add report --source 6 \ +docker exec -it $CONTAINER_NAME flexmeasures add report --source 3 \ --parameters breakable-parameters.json \ --start-offset DB,1D --end-offset DB,2D echo "[TUTORIAL-RUNNER] showing reported data ..." -docker exec -it $CONTAINER_NAME bash -c "flexmeasures show beliefs --sensor 10 --start ${TOMORROW}T00:00:00+01:00 --duration PT24H" +docker exec -it $CONTAINER_NAME bash -c "flexmeasures show beliefs --sensor 16 --start ${TOMORROW}T00:00:00+01:00 --duration PT24H" @@ -93,14 +93,14 @@ echo "[TUTORIAL-RUNNER] now the breakable process ..." echo " { 'input' : [{'sensor': ${FM_TOY_PROCESS_SHIFTABLE_SENSOR_ID}}], - 'output' : [{'sensor': 11}] + 'output' : [{'sensor': 17}] }" > shiftable-parameters.json docker cp shiftable-parameters.json $CONTAINER_NAME:/app -docker exec -it $CONTAINER_NAME flexmeasures add report --source 6 \ +docker exec -it $CONTAINER_NAME flexmeasures add report --source 3 \ --parameters shiftable-parameters.json \ --start-offset DB,1D --end-offset DB,2D echo "[TUTORIAL-RUNNER] showing reported data ..." -docker exec -it $CONTAINER_NAME bash -c "flexmeasures show beliefs --sensor 11 --start ${TOMORROW}T00:00:00+01:00 --duration PT24H" +docker exec -it $CONTAINER_NAME bash -c "flexmeasures show beliefs --sensor 17 --start ${TOMORROW}T00:00:00+01:00 --duration PT24H" diff --git a/documentation/tut/toy-example-expanded.rst b/documentation/tut/toy-example-expanded.rst index 2ec92618b8..042e10c445 100644 --- a/documentation/tut/toy-example-expanded.rst +++ b/documentation/tut/toy-example-expanded.rst @@ -6,7 +6,7 @@ Toy example II: Adding solar production, and a limit on the grid connection ============================================================================ -So far we haven't taken into account any other devices that consume or produce electricity. The battery was free to use all available capacity (which was 500 kVA, both its own maximum charge/discharge rate, and the maximum grid capacity). +So far we haven't taken into account any other devices that consume or produce electricity. The battery was free to use all available capacity (which was 500 kVA, both its own maximum charge/discharge rate, and the maximum grid capacity). What if other devices will be using some of that capacity? Our schedules need to reflect that, so we stay within given limits. @@ -24,36 +24,36 @@ How does it work? Adding PV production forecasts ------------------------------ -First, we'll create a new CSV file with solar forecasts (MW, see the setup for sensor 3 in part I of this tutorial) for tomorrow. +First, we'll create a new CSV file with solar forecasts (kW, see the setup for sensor 9 in part I of this tutorial) for tomorrow. .. code-block:: bash $ TOMORROW=$(date --date="next day" '+%Y-%m-%d') $ echo "Hour,Price - $ ${TOMORROW}T00:00:00,0.0 - $ ${TOMORROW}T01:00:00,0.0 - $ ${TOMORROW}T02:00:00,0.0 - $ ${TOMORROW}T03:00:00,0.0 - $ ${TOMORROW}T04:00:00,0.01 - $ ${TOMORROW}T05:00:00,0.03 - $ ${TOMORROW}T06:00:00,0.06 - $ ${TOMORROW}T07:00:00,0.1 - $ ${TOMORROW}T08:00:00,0.14 - $ ${TOMORROW}T09:00:00,0.17 - $ ${TOMORROW}T10:00:00,0.19 - $ ${TOMORROW}T11:00:00,0.21 - $ ${TOMORROW}T12:00:00,0.22 - $ ${TOMORROW}T13:00:00,0.21 - $ ${TOMORROW}T14:00:00,0.19 - $ ${TOMORROW}T15:00:00,0.17 - $ ${TOMORROW}T16:00:00,0.14 - $ ${TOMORROW}T17:00:00,0.1 - $ ${TOMORROW}T18:00:00,0.06 - $ ${TOMORROW}T19:00:00,0.03 - $ ${TOMORROW}T20:00:00,0.01 - $ ${TOMORROW}T21:00:00,0.0 - $ ${TOMORROW}T22:00:00,0.0 - $ ${TOMORROW}T23:00:00,0.0" > solar-tomorrow.csv + $ ${TOMORROW}T00:00:00,0 + $ ${TOMORROW}T01:00:00,0 + $ ${TOMORROW}T02:00:00,0 + $ ${TOMORROW}T03:00:00,0 + $ ${TOMORROW}T04:00:00,10 + $ ${TOMORROW}T05:00:00,30 + $ ${TOMORROW}T06:00:00,60 + $ ${TOMORROW}T07:00:00,100 + $ ${TOMORROW}T08:00:00,140 + $ ${TOMORROW}T09:00:00,170 + $ ${TOMORROW}T10:00:00,190 + $ ${TOMORROW}T11:00:00,210 + $ ${TOMORROW}T12:00:00,220 + $ ${TOMORROW}T13:00:00,210 + $ ${TOMORROW}T14:00:00,190 + $ ${TOMORROW}T15:00:00,170 + $ ${TOMORROW}T16:00:00,140 + $ ${TOMORROW}T17:00:00,100 + $ ${TOMORROW}T18:00:00,60 + $ ${TOMORROW}T19:00:00,30 + $ ${TOMORROW}T20:00:00,10 + $ ${TOMORROW}T21:00:00,0 + $ ${TOMORROW}T22:00:00,0 + $ ${TOMORROW}T23:00:00,0" > solar-tomorrow.csv Then, we read in the created CSV file as beliefs data. This time, different to above, we want to use a new data source (not the user) ― it represents whoever is making these solar production forecasts. @@ -65,11 +65,11 @@ Setting the data source type to "forecaster" helps FlexMeasures to visually dist .. code-block:: bash $ flexmeasures add source --name "toy-forecaster" --type forecaster - Added source - $ flexmeasures add beliefs --sensor ${FM_TOY_SOLAR_SENSOR_ID} --source 4 solar-tomorrow.csv --timezone Europe/Amsterdam + Added source + $ flexmeasures add beliefs --sensor ${FM_TOY_SOLAR_SENSOR_ID} --source 2 solar-tomorrow.csv --timezone Europe/Amsterdam Successfully created beliefs -The one-hour CSV data is automatically resampled to the 15-minute resolution of the sensor that is recording solar production. We can see solar production in the `FlexMeasures UI `_: +The one-hour CSV data is automatically resampled to the 15-minute resolution of the sensor that is recording solar production. We can see solar production in the `FlexMeasures UI `_: .. image:: https://github.com/FlexMeasures/screenshots/raw/main/tut/toy-schedule/sensor-data-production.png :align: center @@ -96,13 +96,13 @@ This will have an effect on the available headroom for the battery, given the `` --start ${TOMORROW}T07:00+01:00 \ --duration PT12H \ --soc-at-start 50% \ - --flex-context '{"inflexible-production": [{"sensor": 3}]}' + --flex-context '{"inflexible-production": [{"sensor": 9}]}' --flex-model '{"soc-min": "50 kWh"}' \ New schedule is stored. .. tab:: API - Example call: `[POST] http://localhost:5000/api/v3_0/sensors/2/schedules/trigger <../api/v3_0.html#post--api-v3_0-sensors-id-schedules-trigger>`_ (update the start date to tomorrow): + Example call: `[POST] http://localhost:5000/api/v3_0/sensors/8/schedules/trigger <../api/v3_0.html#post--api-v3_0-sensors-id-schedules-trigger>`_ (update the start date to tomorrow): .. code-block:: json :emphasize-lines: 8-10 @@ -145,7 +145,7 @@ This will have an effect on the available headroom for the battery, given the `` host="localhost:5000", ) schedule = await client.trigger_and_get_schedule( - sensor_id=2, # Battery power (sensor ID) + sensor_id=8, # Battery power (sensor ID) start=f"{(date.today() + timedelta(days=1)).isoformat()}T07:00+01:00", duration="PT12H", flex_model={ @@ -153,7 +153,7 @@ This will have an effect on the available headroom for the battery, given the `` "soc-min": "50 kWh", }, flex_context={ - "inflexible-production": [{"sensor": 3}], # solar production + "inflexible-production": [{"sensor": 9}], # solar production }, ) print(schedule) @@ -162,13 +162,13 @@ This will have an effect on the available headroom for the battery, given the `` asyncio.run(client_script()) -We can see the updated scheduling in the `FlexMeasures UI `_: +We can see the updated scheduling in the `FlexMeasures UI `_: .. image:: https://github.com/FlexMeasures/screenshots/raw/main/tut/toy-schedule/sensor-data-charging-with-solar.png :align: center | -The `graphs page for the battery `_ now shows the solar data, too: +The `graphs page for the battery `_ now shows the solar data, too: .. image:: https://github.com/FlexMeasures/screenshots/raw/main/tut/toy-schedule/asset-view-with-solar.png :align: center @@ -194,7 +194,7 @@ In the case of the scheduler that we ran in the previous tutorial, which did not .. note:: You can add arbitrary sensors to a chart using the asset UI or the attribute ``sensors_to_show``. See :ref:`view_asset-data` for more. -A nice feature is that you can check the data connectivity status of your building asset. Now that we have made the schedule, both lamps are green. You can also view it in `FlexMeasures UI `_: +A nice feature is that you can check the data connectivity status of your building asset. Now that we have made the schedule, both lamps are green. You can also view it in `FlexMeasures UI `_: .. image:: https://github.com/FlexMeasures/screenshots/raw/main/tut/toy-schedule/screenshot_building_status.png :align: center diff --git a/documentation/tut/toy-example-from-scratch.rst b/documentation/tut/toy-example-from-scratch.rst index 2f77140060..b8ad4840cb 100644 --- a/documentation/tut/toy-example-from-scratch.rst +++ b/documentation/tut/toy-example-from-scratch.rst @@ -75,7 +75,7 @@ There is more information being used by the scheduler, such as the battery's cap .. tab:: API - Example call: `[POST] http://localhost:5000/api/v3_0/sensors/3/schedules/trigger <../api/v3_0.html#post--api-v3_0-sensors-id-schedules-trigger>`_ (update the start date to tomorrow): + Example call: `[POST] http://localhost:5000/api/v3_0/sensors/8/schedules/trigger <../api/v3_0.html#post--api-v3_0-sensors-id-schedules-trigger>`_ (update the start date to tomorrow): .. code-block:: json @@ -83,7 +83,7 @@ There is more information being used by the scheduler, such as the battery's cap "start": "2025-11-11T07:00+01:00", "duration": "PT12H", "flex-model": [ - "sensor": 2, + "sensor": 8, "soc-at-start": "225kWh", "soc-min": "50 kWh" ] @@ -127,7 +127,7 @@ There is more information being used by the scheduler, such as the battery's cap host="localhost:5000", ) schedule = await client.trigger_and_get_schedule( - sensor_id=2, # battery discharging power sensor + sensor_id=8, # battery discharging power sensor start=f"{(date.today() + timedelta(days=1)).isoformat()}T07:00+01:00", duration="PT12H", flex_model={ @@ -149,11 +149,11 @@ Great. Let's see what we made: .. code-block:: bash - Beliefs for Sensor 'discharging' (ID 2). + Beliefs for Sensor 'discharging' (ID 8). Data spans 12 hours and starts at 2025-11-29 07:00:00+01:00. The time resolution (x-axis) is 15 minutes. ┌────────────────────────────────────────────────────────────┐ - │ ▛▀▜ ▞▀▀▌ ▐▀▀▚ │ 0.5MW + │ ▛▀▜ ▞▀▀▌ ▐▀▀▚ │ 500kW │ ▌ ▌ ▌ ▌ ▐ ▐ │ │ ▗▘ ▌ ▌ ▌ ▐ ▐ │ │ ▐ ▌ ▌ ▐ ▌ ▐ │ @@ -161,7 +161,7 @@ Great. Let's see what we made: │▌ ▐ ▐ ▐ ▐ ▌ ▌│ │▐ ▌ ▐ ▐ ▌ ▐ ▌│ │ ▌ ▌ ▌ ▐ ▌ ▐ ▐│ - │─▚▄▄▌────▀▙▄▄▄▖────▐────▀▚▄▄▄▄▄▄▄▄▖─────▗▄▄▄▄▄▄▄▄▄▄▄▄▄▟────▝│ 0.0MW + │─▚▄▄▌────▀▙▄▄▄▖────▐────▀▚▄▄▄▄▄▄▄▄▖─────▗▄▄▄▄▄▄▄▄▄▄▄▄▄▟────▝│ 0kW │ ▌ ▞ ▐ ▌ │ │ ▚ ▌ ▐ ▗▘ │ │ ▐ ▌ ▐ ▞ │ @@ -169,7 +169,7 @@ Great. Let's see what we made: │ ▝▖ ▐ ▌ ▗▘ │ │ ▌ ▞ ▌ ▐ │ │ ▌ ▌ ▚ ▞ │ - │ ▙▄▄▘ ▐▄▄▌ │ -0.5MW + │ ▙▄▄▘ ▐▄▄▌ │ -500kW └────────────────────────────────────────────────────────────┘ 06:00 09:00 12:00 15:00 ██ discharging (toy-battery) @@ -177,7 +177,7 @@ Great. Let's see what we made: Here, negative values denote output from the grid, so that's when the battery gets charged. -We can also look at the charging schedule in the `FlexMeasures UI `_ (reachable via the asset page for the battery): +We can also look at the charging schedule in the `FlexMeasures UI `_ (reachable via the asset page for the battery): .. image:: https://github.com/FlexMeasures/screenshots/raw/main/tut/toy-schedule/sensor-data-charging.png :align: center @@ -187,7 +187,7 @@ Recall that we only asked for a 12 hour schedule here. We started our schedule * Our scheduler didn't have many opportunities to optimize, but it found some. This battery can fully charge in around an hour, and therefore, it runs two cycles. For instance, in the second cycle it buys at the lowest price (at 2pm) and sells it off at the highest price within the given 12 hours (at 6pm). -The `battery's graph dashboard `_ shows both prices and the schedule. +The `battery's graph dashboard `_ shows both prices and the schedule. .. image:: https://github.com/FlexMeasures/screenshots/raw/main/tut/toy-schedule/asset-view-without-solar.png :align: center diff --git a/documentation/tut/toy-example-multiasset-curtailment.rst b/documentation/tut/toy-example-multiasset-curtailment.rst index 913edcd0b5..7da82ee0cb 100644 --- a/documentation/tut/toy-example-multiasset-curtailment.rst +++ b/documentation/tut/toy-example-multiasset-curtailment.rst @@ -42,12 +42,12 @@ Also, we want to create a situation with negative prices, so curtailment makes s $ # this flex context has negative prices between 12:00 and 14:00 $ echo '''{ "consumption-price": [ - {"start": "'${TOMORROW}'T00:00+01", "duration": "PT24H", "value": "10 EUR/MWh"} + {"start": "'${TOMORROW}'T00:00+01", "duration": "PT24H", "value": "0.010 EUR/kWh"} ], "production-price": [ - {"start": "'${TOMORROW}'T05:00+01", "duration": "PT7H", "value": "4 EUR/MWh"}, - {"start": "'${TOMORROW}'T12:00+01", "duration": "PT2H", "value": "-10 EUR/MWh"}, - {"start": "'${TOMORROW}'T14:00+01", "duration": "PT7H", "value": "4 EUR/MWh"} + {"start": "'${TOMORROW}'T05:00+01", "duration": "PT7H", "value": "0.004 EUR/kWh"}, + {"start": "'${TOMORROW}'T12:00+01", "duration": "PT2H", "value": "-0.010 EUR/kWh"}, + {"start": "'${TOMORROW}'T14:00+01", "duration": "PT7H", "value": "0.004 EUR/kWh"} ] }''' > tutorial3-priceprofile-flex-context.json $ docker cp tutorial3-priceprofile-flex-context.json flexmeasures-server-1:/app/ @@ -60,7 +60,7 @@ Also, we want to create a situation with negative prices, so curtailment makes s .. tab:: API - Example call: `[POST] http://localhost:5000/api/v3_0/sensors/3/schedules/trigger <../api/v3_0.html#post--api-v3_0-sensors-id-schedules-trigger>`_ (update the start date to tomorrow): + Example call: `[POST] http://localhost:5000/api/v3_0/sensors/9/schedules/trigger <../api/v3_0.html#post--api-v3_0-sensors-id-schedules-trigger>`_ (update the start date to tomorrow): .. code-block:: json :emphasize-lines: 14-18 @@ -71,17 +71,17 @@ Also, we want to create a situation with negative prices, so curtailment makes s "flex-model": [ { "consumption-capacity": "0 kW", - "production-capacity": {"sensor": 3, "source-types": ["forecaster"]}, + "production-capacity": {"sensor": 9, "source-types": ["forecaster"]}, } ], "flex-context": { "consumption-price": [ - {"start": "2025-11-18T00:00+01", "duration": "PT24H", "value": "10 EUR/MWh"} + {"start": "2025-11-18T00:00+01", "duration": "PT24H", "value": "0.010 EUR/kWh"} ], "production-price": [ - {"start": "2025-11-18T05:00+01", "duration": "PT7H", "value": "4 EUR/MWh"}, - {"start": "2025-11-18T12:00+01", "duration": "PT2H", "value": "-10 EUR/MWh"}, - {"start": "2025-11-18T14:00+01", "duration": "PT7H", "value": "4 EUR/MWh"} + {"start": "2025-11-18T05:00+01", "duration": "PT7H", "value": "0.004 EUR/kWh"}, + {"start": "2025-11-18T12:00+01", "duration": "PT2H", "value": "-0.010 EUR/kWh"}, + {"start": "2025-11-18T14:00+01", "duration": "PT7H", "value": "0.004 EUR/kWh"} ] } } @@ -103,23 +103,23 @@ Also, we want to create a situation with negative prices, so curtailment makes s async def client_script(): schedule = await client.trigger_and_get_schedule( - sensor_id=3, # PV production sensor + sensor_id=9, # PV production sensor start=f"{date.today().isoformat()}T07:00+01:00", duration="PT12H", flex_model=[ { "consumption-capacity": "0 kW", - "production-capacity": {"sensor": 3, "source-types": ["forecaster"]}, + "production-capacity": {"sensor": 9, "source-types": ["forecaster"]}, } ], flex_context={ "consumption-price": [ - {"start": "2025-11-18T00:00+01", "duration": "PT24H", "value": "10 EUR/MWh"} + {"start": "2025-11-18T00:00+01", "duration": "PT24H", "value": "0.010 EUR/kWh"} ], "production-price": [ - {"start": "2025-11-18T05:00+01", "duration": "PT7H", "value": "4 EUR/MWh"}, - {"start": "2025-11-18T12:00+01", "duration": "PT2H", "value": "-10 EUR/MWh"}, - {"start": "2025-11-18T14:00+01", "duration": "PT7H", "value": "4 EUR/MWh"} + {"start": "2025-11-18T05:00+01", "duration": "PT7H", "value": "0.004 EUR/kWh"}, + {"start": "2025-11-18T12:00+01", "duration": "PT2H", "value": "-0.010 EUR/kWh"}, + {"start": "2025-11-18T14:00+01", "duration": "PT7H", "value": "0.004 EUR/kWh"} ] }, ) @@ -133,11 +133,11 @@ Great. Let's see what we made: echo "[TUTORIAL-RUNNER] showing PV schedule ..." docker exec -it flexmeasures-server-1 flexmeasures show beliefs --sensor ${FM_TOY_SOLAR_SENSOR_ID} --start ${TOMORROW}T07:00:00+01:00 --duration PT12H - Beliefs for Sensor 'production' (ID 3). + Beliefs for Sensor 'production' (ID 9). Data spans 12 hours and starts at 2025-11-29 07:00:00+01:00. The time resolution (x-axis) is 15 minutes. ┌────────────────────────────────────────────────────────────┐ - │ ▞▀▀▀▌ │ 0.2MW + │ ▞▀▀▀▌ │ 200kW │ ▄▄▄▄▞ ▌ ▗▄▄▄▖ │ │ ▗▘ ▚ ▐ ▝▖ │ │ ▛▀▀▀▘ ▐ ▐ ▝▀▀▀▜ │ @@ -145,7 +145,7 @@ Great. Let's see what we made: │ ▄▄▄▄▌ ▐ ▌ ▐▄▄▄▄ │ │ ▗▘ ▐ ▌ ▐ │ │ ▐ ▐ ▌ ▌ │ - │▄▄▄▄▌ ▐ ▌ ▐▄▄▄▄ │ 0.1MW + │▄▄▄▄▌ ▐ ▌ ▐▄▄▄▄ │ 100kW │ ▌ ▌ ▐ │ │ ▌ ▐ ▌ │ │ ▌ ▐ ▐ │ @@ -153,7 +153,7 @@ Great. Let's see what we made: │ ▌ ▐ │ │ ▌ ▐ │ │ ▚ ▐ │ - │▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▐▄▄▄▄▄▄▄▄▌▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁│ -0.0MW + │▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▐▄▄▄▄▄▄▄▄▌▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁│ 0kW └────────────────────────────────────────────────────────────┘ 06:00 09:00 12:00 15:00 ██ production (toy-solar) @@ -189,7 +189,7 @@ Note that we are still passing in the flex-context with block price profiles her .. tab:: API - Example call: `[POST] http://localhost:5000/api/v3_0/assets/2/schedules/trigger <../api/v3_0.html#post--api-v3_0-assets-id-schedules-trigger>`_ (update the start date to tomorrow): + Example call: `[POST] http://localhost:5000/api/v3_0/assets/5/schedules/trigger <../api/v3_0.html#post--api-v3_0-assets-id-schedules-trigger>`_ (update the start date to tomorrow): .. code-block:: json @@ -198,24 +198,24 @@ Note that we are still passing in the flex-context with block price profiles her "duration": "PT12H", "flex-model": [ { - "sensor": 3, + "sensor": 9, "consumption-capacity": "0 kW", - "production-capacity": {"sensor": 3, "source-types": ["forecaster"]}, + "production-capacity": {"sensor": 9, "source-types": ["forecaster"]}, } { - "sensor": 2, + "sensor": 8, "soc-at-start": "225 kWh", "soc-min": "50 kWh" }, ], "flex-context": { "consumption-price": [ - {"start": "2025-11-18T00:00+01", "duration": "PT24H", "value": "10 EUR/MWh"} + {"start": "2025-11-18T00:00+01", "duration": "PT24H", "value": "0.010 EUR/kWh"} ], "production-price": [ - {"start": "2025-11-18T05:00+01", "duration": "PT7H", "value": "4 EUR/MWh"}, - {"start": "2025-11-18T12:00+01", "duration": "PT2H", "value": "-10 EUR/MWh"}, - {"start": "2025-11-18T14:00+01", "duration": "PT7H", "value": "4 EUR/MWh"} + {"start": "2025-11-18T05:00+01", "duration": "PT7H", "value": "0.004 EUR/kWh"}, + {"start": "2025-11-18T12:00+01", "duration": "PT2H", "value": "-0.010 EUR/kWh"}, + {"start": "2025-11-18T14:00+01", "duration": "PT7H", "value": "0.004 EUR/kWh"} ] } } @@ -237,24 +237,24 @@ Note that we are still passing in the flex-context with block price profiles her duration="PT12H", flex_model=[ { - "sensor": 3, # solar production (sensor ID) + "sensor": 9, # solar production (sensor ID) "consumption-capacity": "0 kW", - "production-capacity": {"sensor": 3, "source-types": ["forecaster"]}, + "production-capacity": {"sensor": 9, "source-types": ["forecaster"]}, }, { - "sensor": 2, # battery power (sensor ID) + "sensor": 8, # battery power (sensor ID) "soc-at-start": "225 kWh", "soc-min": "50 kWh", }, ], flex_context={ "consumption-price": [ - {"start": "2025-11-18T00:00+01", "duration": "PT24H", "value": "10 EUR/MWh"} + {"start": "2025-11-18T00:00+01", "duration": "PT24H", "value": "0.010 EUR/kWh"} ], "production-price": [ - {"start": "2025-11-18T05:00+01", "duration": "PT7H", "value": "4 EUR/MWh"}, - {"start": "2025-11-18T12:00+01", "duration": "PT2H", "value": "-10 EUR/MWh"}, - {"start": "2025-11-18T14:00+01", "duration": "PT7H", "value": "4 EUR/MWh"} + {"start": "2025-11-18T05:00+01", "duration": "PT7H", "value": "0.004 EUR/kWh"}, + {"start": "2025-11-18T12:00+01", "duration": "PT2H", "value": "-0.010 EUR/kWh"}, + {"start": "2025-11-18T14:00+01", "duration": "PT7H", "value": "0.004 EUR/kWh"} ] }, ) @@ -264,7 +264,7 @@ The PV inverter should then not need to curtail any production, as the battery c And the battery should get rid of this energy again when prices go up later in the day. -We can confirm this is the case on the updated scheduling in the `FlexMeasures UI `_: +We can confirm this is the case on the updated scheduling in the `FlexMeasures UI `_: .. image:: https://github.com/FlexMeasures/screenshots/raw/main/tut/toy-schedule/sensor-data-multiasset-negativeprices.png :align: center @@ -276,27 +276,27 @@ And here is the CLI version: .. code-block:: bash - Beliefs for Sensors production (ID 3) and discharging (ID 2). + Beliefs for Sensors production (ID 9) and discharging (ID 8). Data spans 12 hours and starts at 2025-11-29 07:00:00+01:00. The time resolution (x-axis) is 15 minutes. ┌────────────────────────────────────────────────────────────┐ │ ▛▀▀│ - │ ▐ │ 0.4MW + │ ▐ │ 400kW │ ▐ │ │ ▞ │ │ ▐▀▌ ▌ │ │ ▐ ▌▄▄▄▄▄▖ ▌ │ - │ ▄▄▄▄▄▞▀▀▞▀▐ ▝▀▚▞▀▚▄▄▄▄▖ ▐ │ 0.2MW + │ ▄▄▄▄▄▞▀▀▞▀▐ ▝▀▚▞▀▚▄▄▄▄▖ ▐ │ 200kW │ ▄▄▄▄▞▀▀▀▀▘ ▌ ▐ ▝▀▀▀▀▚▄▄▄▄ ▐ │ │▄▄▄▄▞ ▗▘ ▐ ▚▄▄▄▄▌ │ │ ▞ ▐ ▌▄▄▄▄│ │ ▗▘ ▐ ▐ │ - │▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▔▔▔▔▌▔▔▔▔▔▔▔▔▔▐▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▔▔▔▔▔│ -0.0MW + │▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▔▔▔▔▌▔▔▔▔▔▔▔▔▔▐▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▔▔▔▔▔│ 0kW │ ▌ ▌ │ │ ▌ ▌ │ │ ▌ ▐ │ │ ▌ ▗▖▐ │ - │ ▐▄▄▄▄▞▀▘▝▘ │ -0.2MW + │ ▐▄▄▄▄▞▀▘▝▘ │ -200kW └────────────────────────────────────────────────────────────┘ 06:00 09:00 12:00 15:00 ██ production (toy-solar) ██ discharging (toy-battery) @@ -317,7 +317,7 @@ We see the battery cycling twice, as before, but now it also soaks up solar prod .. code-block:: bash - Beliefs for Sensors production (ID 3) and discharging (ID 2). + Beliefs for Sensors production (ID 9) and discharging (ID 8). Data spans 12 hours and starts at 2025-11-19 07:00:00+01:00. The time resolution (x-axis) is 15 minutes. @@ -330,7 +330,7 @@ We see the battery cycling twice, as before, but now it also soaks up solar prod │▄▄▌▄▄▀▀▀▀▀▀▀▀▀▐ ▐ ▝▀▀▀▀▀▀▀▀▀▄▄▄▄▄ ▌ ▙▘ │ │ ▌ ▐ ▌ ▚▄▄▄▐▖ █ │ │ ▐ ▌ ▌ ▐▝▀▀▀▐▚▄▄▄▄│ - │▔▔▝▀▀▀▀▀▀▌▔▔▔▔▌▔▔▔▔▔▝▀▀▀▀▀▀▀▀▌▔▔▔▔▔▔▔▔▔▔▞▀▀▀▀▀▀▀▀▀▔▔▔▔▔▔▔▔▔▔│ 0.0MW + │▔▔▝▀▀▀▀▀▀▌▔▔▔▔▌▔▔▔▔▔▝▀▀▀▀▀▀▀▀▌▔▔▔▔▔▔▔▔▔▔▞▀▀▀▀▀▀▀▀▀▔▔▔▔▔▔▔▔▔▔│ 0kW │ ▌ ▐ ▐ ▞ │ │ ▚ ▐ ▐ ▌ │ │ ▐ ▌ ▐ ▌ │ @@ -338,7 +338,7 @@ We see the battery cycling twice, as before, but now it also soaks up solar prod │ ▐ ▐ ▌ ▐ │ │ ▌ ▞ ▌ ▐ │ │ ▌ ▌ ▌ ▌ │ - │ ▙▟ ▐▄▄▄▄▄▄▄▌ │ -0.5MW + │ ▙▟ ▐▄▄▄▄▄▄▄▌ │ -500kW └────────────────────────────────────────────────────────────┘ 09:00 12:00 15:00 18:00 ██ production (toy-solar) ██ discharging (toy-battery) diff --git a/documentation/tut/toy-example-process.rst b/documentation/tut/toy-example-process.rst index 19fd43ba4b..b77ece39a7 100644 --- a/documentation/tut/toy-example-process.rst +++ b/documentation/tut/toy-example-process.rst @@ -8,7 +8,7 @@ Until this point we've been using a static battery, one of the most flexible ene However, in some settings, we can reduce electricity bills by **just** smartly timing the necessary work that we know we have to do. We call this work a "process". In other words, if the process can be displaced, by breaking it into smaller consumption periods or shifting its start time, the process run can match the lower price hours better. -For example, we could have a load that consumes energy at a constant rate (e.g. 200kW) for a fixed duration (e.g. 4h), but there's some flexibility in the start time. In that case, we could find the optimal start time in order to minimize the energy cost. +For example, we could have a load that consumes energy at a constant rate (e.g. 200 kW) for a fixed duration (e.g. 4h), but there's some flexibility in the start time. In that case, we could find the optimal start time in order to minimize the energy cost. Examples of flexible processes are: - Water irrigation in agriculture @@ -35,22 +35,12 @@ Before moving forward, we'll add the `process` asset and three sensors to store .. code-block:: bash $ eval "$(flexmeasures add toy-account --kind process --shell-vars | grep '^FM_TOY_')" - - User with email toy-user@flexmeasures.io already exists in account Docker Toy Account. - The sensor recording day-ahead prices is day-ahead prices (ID: 1). - Created - Created - Created - Created - The sensor recording the power of the inflexible load is Power (Inflexible) (ID: 4). - The sensor recording the power of the breakable load is Power (Breakable) (ID: 5). - The sensor recording the power of the shiftable load is Power (Shiftable) (ID: 6). Trigger an updated schedule ---------------------------- -In this example, we are planning to consume at a 200kW constant power for a period of 4h. +In this example, we are planning to consume at a 200 kW constant power for a period of 4h. This load is to be scheduled for tomorrow, except from the period from 3pm to 4pm (imposed using the ``time-restrictions`` parameter). @@ -62,7 +52,7 @@ Now we are ready to schedule a process. Let's start with the INFLEXIBLE policy, $ flexmeasures add schedule --sensor ${FM_TOY_PROCESS_INFLEXIBLE_SENSOR_ID} --scheduler ProcessScheduler \ --start ${TOMORROW}T00:00:00+02:00 --duration PT24H \ --flex-context '{\"consumption-price\": {\"sensor\": '"${FM_TOY_PRICE_SENSOR_ID}"'}}' \ - --flex-model '{\"duration\": \"PT4H\", \"process-type\": \"INFLEXIBLE\", \"power\": 0.2, \"time-restrictions\": [{\"start\": \"${TOMORROW}T15:00:00+02:00\", \"duration\": \"PT1H\"}]}' \ + --flex-model '{\"duration\": \"PT4H\", \"process-type\": \"INFLEXIBLE\", \"power\": 200, \"time-restrictions\": [{\"start\": \"${TOMORROW}T15:00:00+02:00\", \"duration\": \"PT1H\"}]}' \ Under the INFLEXIBLE policy, the process starts as soon as possible, in this case, coinciding with the start of the planning window. @@ -73,7 +63,7 @@ Following the INFLEXIBLE policy, we'll schedule the same 4h consumption requirem $ flexmeasures add schedule --sensor ${FM_TOY_PROCESS_BREAKABLE_SENSOR_ID} --scheduler ProcessScheduler \ --start ${TOMORROW}T00:00:00+02:00 --duration PT24H \ --flex-context '{\"consumption-price\": {\"sensor\": '"${FM_TOY_PRICE_SENSOR_ID}"'}}' \ - --flex-model '{\"duration\": \"PT4H\", \"process-type\": \"BREAKABLE\", \"power\": 0.2, \"time-restrictions\": [{\"start\": \"${TOMORROW}T15:00:00+02:00\", \"duration\": \"PT1H\"}]}' \ + --flex-model '{\"duration\": \"PT4H\", \"process-type\": \"BREAKABLE\", \"power\": 200, \"time-restrictions\": [{\"start\": \"${TOMORROW}T15:00:00+02:00\", \"duration\": \"PT1H\"}]}' \ The BREAKABLE policy splits or breaks the process into blocks that can be scheduled discontinuously. The smallest possible unit is (currently) determined by the sensor's resolution. @@ -84,14 +74,14 @@ Finally, we'll schedule the process using the SHIFTABLE policy. The 4h block can $ flexmeasures add schedule --sensor ${FM_TOY_PROCESS_SHIFTABLE_SENSOR_ID} --scheduler ProcessScheduler \ --start ${TOMORROW}T00:00:00+02:00 --duration PT24H \ --flex-context '{\"consumption-price\": {\"sensor\": '"${FM_TOY_PRICE_SENSOR_ID}"'}}' \ - --flex-model '{\"duration\": \"PT4H\", \"process-type\": \"SHIFTABLE\", \"power\": 0.2, \"time-restrictions\": [{\"start\": \"${TOMORROW}T15:00:00+02:00\", \"duration\": \"PT1H\"}]}' \ + --flex-model '{\"duration\": \"PT4H\", \"process-type\": \"SHIFTABLE\", \"power\": 200, \"time-restrictions\": [{\"start\": \"${TOMORROW}T15:00:00+02:00\", \"duration\": \"PT1H\"}]}' \ Results --------- The image below shows the resulting schedules following each of the three policies. -You will see similar results in your `FlexMeasures UI `_. +You will see similar results in your `FlexMeasures UI `_. .. image:: https://github.com/FlexMeasures/screenshots/raw/main/tut/toy-schedule/asset-view-process.png @@ -111,15 +101,15 @@ Let's list the power costs which the policies achieved for each of the four bloc +-------------------------+------------+-----------+-----------+ | Block | INFLEXIBLE | BREAKABLE | SHIFTABLE | +=========================+============+===========+===========+ -| 1 | 10.00 | 5.00 | 10.00 | +| 1 | 0.010 | 0.005 | 0.010 | +-------------------------+------------+-----------+-----------+ -| 2 | 11.00 | 4.00 | 8.00 | +| 2 | 0.011 | 0.004 | 0.008 | +-------------------------+------------+-----------+-----------+ -| 3 | 12.00 | 5.50 | 5.00 | +| 3 | 0.012 | 0.0055 | 0.005 | +-------------------------+------------+-----------+-----------+ -| 4 | 15.00 | 7.00 | 4.00 | +| 4 | 0.015 | 0.007 | 0.004 | +-------------------------+------------+-----------+-----------+ -| Average Price (EUR/MWh) | 12.00 | 5.37 | 6.75 | +| Average Price (EUR/kWh) | 0.012 | 0.00537 | 0.00675 | +-------------------------+------------+-----------+-----------+ | Total Cost (EUR) | 9.60 | 4.29 | 5.40 | +-------------------------+------------+-----------+-----------+ diff --git a/documentation/tut/toy-example-reporter.rst b/documentation/tut/toy-example-reporter.rst index 5dbc0b9ad1..e2cc105087 100644 --- a/documentation/tut/toy-example-reporter.rst +++ b/documentation/tut/toy-example-reporter.rst @@ -47,7 +47,7 @@ Run the command below to show the values for our newly-created `grid connection $ TOMORROW=$(date --date="next day" '+%Y-%m-%d') $ flexmeasures show beliefs --sensor ${FM_TOY_GRID_CAPACITY_SENSOR_ID} --start ${TOMORROW}T00:00:00+02:00 --duration PT24H --resolution PT1H - Beliefs for Sensor 'grid connection capacity' (ID 7). + Beliefs for Sensor 'grid connection capacity' (ID 13). Data spans a day and starts at 2025-06-13 00:00:00+02:00. The time resolution (x-axis) is an hour. ┌────────────────────────────────────────────────────────────┐ @@ -55,41 +55,41 @@ Run the command below to show the values for our newly-created `grid connection │ │ │ │ │ │ - │ │ 1.0MW + │ │ 500.5kW │ │ │ │ │ │ - │▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄│ 0.5MW + │▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄│ 500.0kW │ │ │ │ │ │ - │▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁│ 0.0MW + │ │ 499.5kW │ │ │ │ │ │ - │ │ -0.5MW + │ │ 499.0kW └────────────────────────────────────────────────────────────┘ 06:00 12:00 18:00 ██ grid connection capacity (toy-building) -Moreover, we can check the freshly created source ``, which defines the `ProfitOrLossReporter` with the required configuration. +Moreover, we can check the freshly created source ``, which defines the `ProfitOrLossReporter` with the required configuration. You'll notice that the `config` is under the `data_generator` field. That's because reporters belong to a bigger category of classes that also contains the `Schedulers` and `Forecasters`. .. code-block:: bash - $ flexmeasures show data-sources --show-attributes --id 6 + $ flexmeasures show data-sources --show-attributes --id 3 type: reporter ======== ID Name User ID Model Version Attributes ---- ------------ --------- -------------------- --------- ------------------------------------------ - 6 FlexMeasures ProfitOrLossReporter { + 3 FlexMeasures ProfitOrLossReporter { "data_generator": { "config": { - "consumption_price_sensor": 1, + "consumption_price_sensor": 7, "loss_is_positive": true } } @@ -120,13 +120,13 @@ In practice, we need to create the `config` and `parameters`: $ echo " $ { $ 'input': [{'name': 'grid connection capacity', 'sensor': ${FM_TOY_GRID_CAPACITY_SENSOR_ID}}, - $ {'name': 'PV', 'sensor': ${FM_TOY_SOLAR_SENSOR_ID}, 'sources': [4]}], + $ {'name': 'PV', 'sensor': ${FM_TOY_SOLAR_SENSOR_ID}, 'sources': [2]}], $ 'output': [{'sensor': ${FM_TOY_HEADROOM_SENSOR_ID}}] $ }" > headroom-parameters.json -The output sensor (ID: 8) is actually the one created just to store that information - the headroom our battery has when considering solar production. +The output sensor (ID: 14) is actually the one created just to store that information - the headroom our battery has when considering solar production. -We limit the PV input to data with source 4, which is the one created when we added the toy account and is associated with our solar forecasts. This way, we ensure that only the forecasted values are used in the report, and not (also) schedules from the 3rd tutorial. +We limit the PV input to data with source 2, which is the one created for our solar forecasts. This way, we ensure that only the forecasted values are used in the report, and not (also) schedules from the 3rd tutorial. Finally, we can create the report with the following command: @@ -137,7 +137,7 @@ Finally, we can create the report with the following command: --start-offset DB,1D --end-offset DB,2D \ --resolution PT15M -Now we can visualize the diminished headroom in the following `link `_, which should resemble the following image: +Now we can visualize the diminished headroom in the following `link `_, which should resemble the following image: .. image:: https://github.com/FlexMeasures/screenshots/raw/main/tut/toy-schedule/sensor-data-headroom.png :align: center @@ -180,19 +180,19 @@ Define parameters in a JSON file: $ echo " $ { $ 'input': [{'sensor': ${FM_TOY_PROCESS_INFLEXIBLE_SENSOR_ID}}], - $ 'output': [{'sensor': 9}] + $ 'output': [{'sensor': 15}] $ }" > inflexible-parameters.json Create report: .. code-block:: bash - $ flexmeasures add report --source 6 \ + $ flexmeasures add report --source 3 \ --parameters inflexible-parameters.json \ --start-offset DB,1D --end-offset DB,2D -Check the results `here `_. The image should be similar to the one below. +Check the results `here `_. The image should be similar to the one below. .. image:: https://github.com/FlexMeasures/screenshots/raw/main/tut/toy-schedule/sensor-data-inflexible.png :align: center @@ -208,18 +208,18 @@ Define parameters in a JSON file: $ echo " $ { $ 'input': [{'sensor': ${FM_TOY_PROCESS_BREAKABLE_SENSOR_ID}}], - $ 'output': [{'sensor': 10}] + $ 'output': [{'sensor': 16}] $ }" > breakable-parameters.json Create report: .. code-block:: bash - $ flexmeasures add report --source 6 \ + $ flexmeasures add report --source 3 \ --parameters breakable-parameters.json \ --start-offset DB,1D --end-offset DB,2D -Check the results `here `_. The image should be similar to the one below. +Check the results `here `_. The image should be similar to the one below. .. image:: https://github.com/FlexMeasures/screenshots/raw/main/tut/toy-schedule/sensor-data-breakable.png @@ -236,18 +236,18 @@ Define parameters in a JSON file: $ echo " $ { $ 'input': [{'sensor': ${FM_TOY_PROCESS_SHIFTABLE_SENSOR_ID}}], - $ 'output': [{'sensor': 11}] + $ 'output': [{'sensor': 17}] $ }" > shiftable-parameters.json Create report: .. code-block:: bash - $ flexmeasures add report --source 6 \ + $ flexmeasures add report --source 3 \ --parameters shiftable-parameters.json \ --start-offset DB,1D --end-offset DB,2D -Check the results `here `_. The image should be similar to the one below. +Check the results `here `_. The image should be similar to the one below. .. image:: https://github.com/FlexMeasures/screenshots/raw/main/tut/toy-schedule/sensor-data-shiftable.png diff --git a/documentation/tut/toy-example-setup.rst b/documentation/tut/toy-example-setup.rst index a608cabc24..fa5b447eba 100644 --- a/documentation/tut/toy-example-setup.rst +++ b/documentation/tut/toy-example-setup.rst @@ -20,7 +20,7 @@ Below are the ``flexmeasures`` CLI commands we'll run, and which we'll explain s # setup an account with a user, assets for battery & solar and an energy market $ eval "$(flexmeasures add toy-account --kind battery --shell-vars | grep '^FM_TOY_')" # load prices to optimize schedules against - $ flexmeasures add beliefs --sensor ${FM_TOY_PRICE_SENSOR_ID} --source toy-user prices-tomorrow.csv --timezone Europe/Amsterdam + $ flexmeasures add beliefs --sensor ${FM_TOY_PRICE_SENSOR_ID} --source toy-user prices-tomorrow.csv --timezone Europe/Amsterdam --unit EUR/MWh Okay, let's get started! @@ -141,11 +141,11 @@ The data we need for our example is both structural (e.g. a company account, a u Let's create the structural data first. -FlexMeasures offers a command to create a toy account with a battery and expose the relevant IDs as shell variables: +FlexMeasures offers a command to create a toy account with a battery: .. code-block:: bash - $ eval "$(flexmeasures add toy-account --kind battery --shell-vars | grep '^FM_TOY_')" + $ flexmeasures add toy-account --kind battery Generic asset type `solar` created successfully. Generic asset type `wind` created successfully. @@ -154,20 +154,27 @@ FlexMeasures offers a command to create a toy account with a battery and expose Generic asset type `battery` created successfully. Generic asset type `building` created successfully. Generic asset type `process` created successfully. + Generic asset type `heat-storage` created successfully. Creating account Toy Account ... Toy account Toy Account with user toy-user@flexmeasures.io created successfully. You might want to run `flexmeasures show account --id 1` Adding transmission zone type ... Adding NL transmission zone ... - Created day-ahead prices - The sensor recording day-ahead prices is day-ahead prices (ID: 1). - Created - Created discharging - Created - Created production - The sensor recording battery discharging is discharging (ID: 2). - The sensor recording solar forecasts is production (ID: 3). + Created + The sensor recording day-ahead prices is day-ahead prices (ID: 7). + Created + Created + Created + Created + Created + The sensor recording battery discharging is discharging (ID: 8). + The sensor recording solar forecasts is production (ID: 9). + +Now expose the relevant IDs as shell variables for the commands below: +.. code-block:: bash + + $ eval "$(flexmeasures add toy-account --kind battery --shell-vars | grep '^FM_TOY_')" This sets variables such as ``FM_TOY_PRICE_SENSOR_ID``, ``FM_TOY_BATTERY_SENSOR_ID``, ``FM_TOY_SOLAR_SENSOR_ID`` and ``FM_TOY_BUILDING_ASSET_ID`` in your current shell. @@ -195,70 +202,70 @@ If you want, you can inspect what you created in the CLI (we'll also show the UI ID Name Type Parent ID Location ---- ------------ -------- ----------- ----------------- - 2 toy-building building 2 (52.374, 4.88969) - 3 toy-battery battery 2 (52.374, 4.88969) - 4 toy-solar solar 2 (52.374, 4.88969) + 6 toy-battery battery 5 (52.374, 4.88969) + 5 toy-building building (52.374, 4.88969) + 7 toy-solar solar 5 (52.374, 4.88969) .. code-block:: bash :emphasize-lines: 9-10 - $ flexmeasures show asset --id 2 + $ flexmeasures show asset --id 5 - ========================= - Asset toy-building (ID: 2) - ========================= + ========================== + Asset toy-building (ID: 5) + ========================== - Type Location Sensors to show Attributes - -------- ----------------- ------------------- ------------ - building (52.374, 4.88969) Prices: [1] - Power flows: [3, 2] + Type Location Sensors to show Attributes External ID + -------- ----------------- ------------------------------------------------- ------------ ------------- + building (52.374, 4.88969) Prices: asset=5 (flex-context=consumption-price) + Power flows: [9, 8] Flex-Context Flex-Model -------------------------------- ------------ + consumption-price: {'sensor': 7} site-power-capacity: 500 kVA - consumption-price: {'sensor': 1} - ==================================== - Child assets of toy-building (ID: 2) - ==================================== + ===================================== + Child assets of toy-building (ID: 5) + ===================================== - ID Name Type - ------- ----------------- ---------------------------- - 3 toy-battery battery - 4 toy-solar solar + ID Name Type + ---- ----------- ------- + 7 toy-solar solar + 6 toy-battery battery No sensors in asset ... You can see that this building asset has some meta information about how FlexMeasures needs to schedule: -- Within :ref:`flex_context`, we noted where to find the relevant optimization signal for electricity consumption (Sensor 1, which stores day-ahead prices). -- Also, the building has a grid connection capacity of 500 kVA, meaning that the total power flowing into or out of the building cannot exceed this value. +- Within :ref:`flex_context`, we noted where to find the relevant optimization signal for electricity consumption (Sensor 7, which stores day-ahead prices). +- Also, the building has a grid connection capacity of 500 kVA, meaning that the total power flowing into or out of the building cannot exceed this physical limit. Now let's look at the battery asset, as well: .. code-block:: bash :emphasize-lines: 10-12 - $ flexmeasures show asset --id 3 + $ flexmeasures show asset --id 6 =================================== - Asset toy-battery (ID: 3) - Child of asset toy-building (ID: 2) + Asset toy-battery (ID: 6) + Child of asset toy-building (ID: 5) =================================== - Type Location Sensors to show Attributes - ------- ----------------- ------------------- ------------ - battery (52.374, 4.88969) Prices: [1] - Power flows: [3, 2] + Type Location Sensors to show Attributes External ID + ------- ----------------- ------------------- ------------ ------------- + battery (52.374, 4.88969) Prices: 7 + Power flows: [9, 8] Flex-Context Flex-Model -------------- ------------------------- + soc-max: 450 kWh power-capacity: 500 kVA roundtrip-efficiency: 90% - soc-max: 450 kWh ==================================== - Child assets of toy-battery (ID: 3) + Child assets of toy-battery (ID: 6) ==================================== No child assets ... @@ -267,7 +274,7 @@ Now let's look at the battery asset, as well: ID Name Unit Resolution Timezone Attributes ---- ----------- ------ ------------ ---------------- ------------ - 2 discharging MW 15 minutes Europe/Amsterdam + 8 discharging kW 15 minutes Europe/Amsterdam @@ -315,7 +322,7 @@ And on the flex-model of the battery can be seen on its properties page (and is Add some price data --------------------------------------- -Now to add price data. First, we'll create the CSV file with prices (EUR/MWh, see the setup for sensor 1 above) for tomorrow. +Now to add price data. First, we'll create the CSV file with prices in EUR/MWh for tomorrow. The price sensor stores data in EUR/kWh, and we'll ask FlexMeasures to convert the CSV values while loading them. .. code-block:: bash @@ -350,12 +357,12 @@ This is time series data, in FlexMeasures we call *"beliefs"*. Beliefs can also .. code-block:: bash - $ flexmeasures add beliefs --sensor ${FM_TOY_PRICE_SENSOR_ID} --source toy-user prices-tomorrow.csv --timezone Europe/Amsterdam + $ flexmeasures add beliefs --sensor ${FM_TOY_PRICE_SENSOR_ID} --source toy-user prices-tomorrow.csv --timezone Europe/Amsterdam --unit EUR/MWh Successfully created beliefs In FlexMeasures, all beliefs have a data source. Here, we use the username of the user we created earlier. We could also pass a user ID, or the name of a new data source we want to use for CLI scripts. -.. note:: Attention: We created and imported prices where the times have no time zone component! That happens a lot. FlexMeasures can localize them for you to a given timezone. Here, we localized the data to the timezone of the price sensor - ``Europe/Amsterdam`` - so the start time for the first price is `2022-03-03 00:00:00+01:00` (midnight in Amsterdam). +.. note:: Attention: We created and imported prices where the times have no time zone component! That happens a lot. FlexMeasures can localize them for you to a given timezone. Here, we localized the data to the timezone of the price sensor - ``Europe/Amsterdam`` - so the start time for the first price is `2025-11-11 00:00:00+01:00` (midnight in Amsterdam). Let's look at the price data we just loaded: @@ -363,34 +370,34 @@ Let's look at the price data we just loaded: $ flexmeasures show beliefs --sensor ${FM_TOY_PRICE_SENSOR_ID} --start ${TOMORROW}T00:00:00+01:00 --duration PT24H - Beliefs for Sensor 'day-ahead prices' (ID 1). + Beliefs for Sensor 'day-ahead prices' (ID 7). Data spans a day and starts at 2025-11-11 00:00:00+01:00. The time resolution (x-axis) is an hour. ┌────────────────────────────────────────────────────────────┐ - │ ▗▀▚▖ │ - │ ▗▘ ▝▖ │ - │ ▞ ▌ │ - │ ▟ ▐ │ 15EUR/MWh - │ ▗▘ ▝▖ ▗ │ - │ ▗▘ ▚ ▄▞▘▚▖ │ - │ ▞ ▐ ▄▀▘ ▝▄ │ - │ ▄▞ ▌ ▛ ▖ │ - │▀ ▚ ▐ ▝▖ │ - │ ▝▚ ▖ ▗▘ ▝▖ │ 10EUR/MWh - │ ▀▄▄▞▀▄▄ ▗▀▝▖ ▞ ▐ │ - │ ▀▀▜▘ ▝▚ ▗▘ ▚ │ - │ ▌ ▞ ▌│ - │ ▝▖ ▞ ▝│ - │ ▐ ▞ │ - │ ▚ ▗▞ │ 5EUR/MWh - │ ▀▚▄▄▄▄▘ │ + │ ▞▄ │ + │ ▐ ▀▌ │ + │ ▗▘ ▚ │ + │ ▌ ▐ │ 0.015EUR/kWh + │ ▞ ▌ ▗ │ + │ ▐ ▐ ▗▄▀▚▖ │ + │ ▗▘ ▝▖ ▗▞▘ ▝▄ │ + │ ▄▀ ▌ ▐▘ ▝▖ │ + │ ▄▀ ▐ ▗▘ ▝▖ │ + │▞▀ ▝▚ ▄ ▞ ▝▖ │ 0.010EUR/kWh + │ ▀▄▄▀▀▄▄ ▞ ▚ ▗▘ ▐ │ + │ ▀▀▀ ▚▖ ▞ ▚ │ + │ ▚ ▗▘ ▝▖│ + │ ▚ ▗▘ ▝│ + │ ▝▖ ▗▘ │ + │ ▐▄ ▄▘ │ 0.005EUR/kWh + │ ▀▄▄▄▞ │ └────────────────────────────────────────────────────────────┘ - 5 10 15 20 - ██ day-ahead prices + 06:00 12:00 18:00 + ██ day-ahead prices (NL transmission zone) -Again, we can also view these prices in the `FlexMeasures UI `_: +Again, we can also view these prices in the `FlexMeasures UI `_: .. image:: https://github.com/FlexMeasures/screenshots/raw/main/tut/toy-schedule/sensor-data-prices.png :align: center diff --git a/flexmeasures/cli/data_add.py b/flexmeasures/cli/data_add.py index e41ef44cc2..deae66ee25 100755 --- a/flexmeasures/cli/data_add.py +++ b/flexmeasures/cli/data_add.py @@ -2162,7 +2162,7 @@ def add_toy_account(kind: str, name: str, shell_vars: bool): Sensor, name="day-ahead prices", generic_asset=nl_zone, - unit="EUR/MWh", + unit="EUR/kWh", timezone="Europe/Amsterdam", event_resolution=timedelta(minutes=60), knowledge_horizon=( @@ -2185,7 +2185,7 @@ def create_asset_with_one_sensor( asset_name: str, asset_type: str, sensor_name: str, - unit: str = "MW", + unit: str = "kW", parent_asset_id: int | None = None, flex_context: dict | None = None, flex_model: dict | None = None, @@ -2361,7 +2361,7 @@ def create_asset_with_one_sensor( generic_asset=building_asset, timezone="Europe/Amsterdam", event_resolution="P1Y", - unit="MW", + unit="kW", ) db.session.commit() @@ -2377,7 +2377,7 @@ def create_asset_with_one_sensor( belief = TimedBelief( event_start=start_year, belief_time=server_now(), - event_value=0.5, + event_value=500, source=db.session.get(DataSource, 1), sensor=grid_connection_capacity, ) diff --git a/flexmeasures/cli/tests/test_data_add_fresh_db.py b/flexmeasures/cli/tests/test_data_add_fresh_db.py index 910b71e2eb..fbbe8cfcd2 100644 --- a/flexmeasures/cli/tests/test_data_add_fresh_db.py +++ b/flexmeasures/cli/tests/test_data_add_fresh_db.py @@ -433,6 +433,60 @@ def test_add_account( assert result.exit_code == 1 +def test_add_toy_account_battery_uses_kw_sensors_and_kva_capacities(app, fresh_db): + from flexmeasures.cli.data_add import add_toy_account + + result = app.test_cli_runner().invoke(add_toy_account, ["--kind", "battery"]) + + check_command_ran_without_error(result) + + toy_account = fresh_db.session.execute( + select(Account).filter_by(name="Toy Account") + ).scalar_one() + building = fresh_db.session.execute( + select(Asset).filter_by(name="toy-building", owner=toy_account) + ).scalar_one() + battery = fresh_db.session.execute( + select(Asset).filter_by(name="toy-battery", owner=toy_account) + ).scalar_one() + solar = fresh_db.session.execute( + select(Asset).filter_by(name="toy-solar", owner=toy_account) + ).scalar_one() + day_ahead_sensor = fresh_db.session.execute( + select(Sensor).filter_by(name="day-ahead prices") + ).scalar_one() + + assert day_ahead_sensor.unit == "EUR/kWh" + assert building.flex_context["site-power-capacity"] == "500 kVA" + assert battery.flex_model["power-capacity"] == "500 kVA" + assert battery.flex_model["soc-max"] == "450 kWh" + assert battery.sensors[0].unit == "kW" + assert solar.sensors[0].unit == "kW" + + +def test_add_toy_account_reporter_uses_kw_scale_units(app, fresh_db): + from flexmeasures.cli.data_add import add_toy_account + + result = app.test_cli_runner().invoke(add_toy_account, ["--kind", "reporter"]) + + check_command_ran_without_error(result) + + day_ahead_sensor = fresh_db.session.execute( + select(Sensor).filter_by(name="day-ahead prices") + ).scalar_one() + grid_connection_capacity = fresh_db.session.execute( + select(Sensor).filter_by(name="grid connection capacity") + ).scalar_one() + headroom = fresh_db.session.execute( + select(Sensor).filter_by(name="headroom") + ).scalar_one() + + assert day_ahead_sensor.unit == "EUR/kWh" + assert grid_connection_capacity.unit == "kW" + assert headroom.unit == "kW" + assert grid_connection_capacity.search_beliefs().values.flatten().tolist() == [500] + + @pytest.mark.parametrize( "roles_args, expected_roles", [ @@ -647,7 +701,14 @@ def test_add_storage_schedule( result = runner.invoke(add_schedule, cli_input) check_command_ran_without_error(result) - assert len(power_sensor.search_beliefs()) == 48 + schedule = power_sensor.search_beliefs() + max_power = schedule.event_value.abs().max() + assert len(schedule) == 48 + assert power_sensor.unit == "kW" + # The 700 kW quantity override is the highest cap used by this parametrized test. + # The lower bound catches accidentally storing MW-scale values on the kW sensor. + assert max_power > 1 + assert max_power <= 700 def test_add_storage_schedule_uses_state_of_charge_sensor_for_soc_at_start(