From 21df5d9528e78590d43e2d5c678af4fc76a28912 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Sun, 19 Jul 2026 22:01:42 +0200 Subject: [PATCH 1/2] refactor: derive the UI flex-model schema from the DB schema Make DBStorageFlexModelSchema the single source of truth for which flex-model fields the UI editor exposes. UI_FLEX_MODEL_SCHEMA is now built by iterating the DB schema's fields against a per-field UI-presentation registry; a DB field without a presentation entry raises at import time, so adding a flex-model field can no longer silently break DB/UI parity. The rendered UI_FLEX_MODEL_SCHEMA is identical (key-for-key, value-for-value) to the previous hand-maintained dict, verified by a committed baseline fixture and a guardrail test. The parity test is repurposed to assert each derived entry is well-formed. OpenAPI spec is unchanged. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_016MLCUiSdXDqDBmg8GbYp1B Signed-off-by: F.N. Claessen --- documentation/changelog.rst | 1 + .../data/schemas/scheduling/__init__.py | 48 +++- flexmeasures/ui/tests/test_utils.py | 61 ++++- .../tests/ui_flex_model_schema_baseline.json | 248 ++++++++++++++++++ 4 files changed, 344 insertions(+), 14 deletions(-) create mode 100644 flexmeasures/ui/tests/ui_flex_model_schema_baseline.json diff --git a/documentation/changelog.rst b/documentation/changelog.rst index ed5b1019f1..bbc42bd9c5 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -49,6 +49,7 @@ Infrastructure / Support * Stop manual runs of the Docker publishing workflow from overwriting the ``latest`` image tag, and let them opt in to it explicitly [see `PR #2316 `_] * Add a pre-commit hook that blocks image files (png, jpg, gif, bmp, tiff, webp, ico, psd) from being committed outside of ``flexmeasures/ui/static/`` and ``documentation/``, to protect the git history from binary bloat; screenshots belong in the ``FlexMeasures/screenshots`` repo instead [see `PR #2315 `_] * Schedulers track devices via a typed device inventory, which classifies every flex-model entry once and serves as the single source of truth for device roles and canonical device indices [see `PR #2321 `_] +* Derive the UI flex-model schema from the storage flex-model DB schema, making the DB schema the single source of truth so that adding a flex-model field can no longer silently break DB/UI parity (a DB field without UI presentation info now fails loudly at import time) [see `PR #2330 `_] Bugfixes ----------- diff --git a/flexmeasures/data/schemas/scheduling/__init__.py b/flexmeasures/data/schemas/scheduling/__init__.py index fdfb71f151..bee68ad689 100644 --- a/flexmeasures/data/schemas/scheduling/__init__.py +++ b/flexmeasures/data/schemas/scheduling/__init__.py @@ -1033,7 +1033,16 @@ def check_prices(self, data: dict, original_data: dict, **kwargs): }, } -UI_FLEX_MODEL_SCHEMA: Dict[str, Dict[str, Any]] = { +# Per-field UI presentation info for the storage flex-model editor. +# +# This registry does NOT define which flex-model fields exist: the field/key set +# is derived from ``DBStorageFlexModelSchema`` (the single source of truth) by +# ``_build_ui_flex_model_schema`` below. This registry only supplies the UI +# presentation values (default, description, editor type token + help string, and +# example units) for each field. Adding a field to the DB schema without a matching +# entry here therefore fails loudly at import time (see the builder), instead of +# silently breaking DB<->UI parity and only surfacing in a full CI run. +_UI_FLEX_MODEL_PRESENTATION: Dict[str, Dict[str, Any]] = { "consumption": { "default": None, "description": rst_to_openapi(metadata.CONSUMPTION.description), @@ -1226,6 +1235,43 @@ def check_prices(self, data: dict, original_data: dict, **kwargs): } +def _build_ui_flex_model_schema() -> Dict[str, Dict[str, Any]]: + """Build the UI flex-model schema from the DB storage flex-model schema. + + ``DBStorageFlexModelSchema`` is the single source of truth for which flex-model + fields exist. For each of its fields we look up the UI presentation info in + ``_UI_FLEX_MODEL_PRESENTATION``. If a DB field has no presentation entry, we + raise right here (at import time), so a newly added flex-model field can no + longer silently break DB<->UI parity (it fails immediately, with a clear + message pointing at the fix, rather than only in a full CI run). + """ + # Imported lazily to avoid an import cycle at module load time. + from flexmeasures.data.schemas.scheduling.storage import DBStorageFlexModelSchema + + schema: Dict[str, Dict[str, Any]] = {} + missing: list[str] = [] + for field_name, field in DBStorageFlexModelSchema().fields.items(): + key = field.data_key or field_name + entry = _UI_FLEX_MODEL_PRESENTATION.get(key) + if entry is None: + missing.append(key) + continue + schema[key] = entry + if missing: + raise ValueError( + "Missing UI presentation info for DBStorageFlexModelSchema field(s): " + f"{missing}. Add an entry for each in `_UI_FLEX_MODEL_PRESENTATION` " + "(flexmeasures/data/schemas/scheduling/__init__.py) so the UI flex-model " + "editor stays in sync with the DB flex-model schema." + ) + return schema + + +# Derived from the DB schema, so the DB schema is the single source of truth for +# which flex-model fields the UI editor exposes (see _build_ui_flex_model_schema). +UI_FLEX_MODEL_SCHEMA: Dict[str, Dict[str, Any]] = _build_ui_flex_model_schema() + + class DBFlexContextSchema(FlexContextSchema, NoTimeSeriesSpecs): commitments = fields.Nested( diff --git a/flexmeasures/ui/tests/test_utils.py b/flexmeasures/ui/tests/test_utils.py index b9925823b7..9ba279b174 100644 --- a/flexmeasures/ui/tests/test_utils.py +++ b/flexmeasures/ui/tests/test_utils.py @@ -101,22 +101,57 @@ def test_ui_flexcontext_schema(): def test_ui_flexmodel_schema(): """ - This test ensures that all fields in the DBStorageFlexModelSchema are also in the UI schema and vice versa. - - This is important to keep in mind when updating either schema. We want to avoid a situation - where a field is added to the DB schema but not to the UI schema, as that would lead to - inconsistencies and potential bugs in the application. + UI_FLEX_MODEL_SCHEMA is now derived from DBStorageFlexModelSchema (the single + source of truth) by _build_ui_flex_model_schema, so DB<->UI parity is guaranteed + by construction: a DB field without UI presentation info raises at import time. + + Here we assert the remaining property that cannot be guaranteed structurally: + that every derived UI entry is well-formed (has the expected keys and a + non-empty backend type token). If someone adds a flex-model field but leaves + its UI presentation info incomplete, this fails with a clear message. """ - ui_flexmodel_schema_fields = [key for key, value in UI_FLEX_MODEL_SCHEMA.items()] - - schema_keys = [] - for value in DBStorageFlexModelSchema().fields.values(): - schema_keys.append(value.data_key if value.data_key else value.name) + schema_keys = { + (value.data_key if value.data_key else value.name) + for value in DBStorageFlexModelSchema().fields.values() + } + + # Parity is by construction: the derived schema covers exactly the DB fields. + assert set(UI_FLEX_MODEL_SCHEMA.keys()) == schema_keys + + for key, entry in UI_FLEX_MODEL_SCHEMA.items(): + assert set(entry.keys()) == { + "default", + "description", + "types", + "example-units", + }, f"UI flex-model entry '{key}' has unexpected keys: {sorted(entry.keys())}" + assert set(entry["types"].keys()) == { + "backend", + "ui", + }, f"UI flex-model entry '{key}' has a malformed 'types' sub-dict." + assert entry["types"][ + "backend" + ], f"UI flex-model entry '{key}' has an empty backend type token." + assert entry["types"][ + "ui" + ], f"UI flex-model entry '{key}' has an empty UI help string." + assert entry["description"], f"UI flex-model entry '{key}' has no description." + + +def test_ui_flexmodel_schema_matches_baseline(): + """Guardrail: the derived UI flex-model schema must be identical, key-for-key + and value-for-value, to the hand-maintained schema captured before the + refactor that made DBStorageFlexModelSchema the single source of truth. + + This proves the UI editor sees exactly the same data as before. + """ + import json + from pathlib import Path - schema_keys = set(schema_keys) - ui_flexmodel_schema_fields = set(ui_flexmodel_schema_fields) + baseline_path = Path(__file__).parent / "ui_flex_model_schema_baseline.json" + baseline = json.loads(baseline_path.read_text()) - assert schema_keys == ui_flexmodel_schema_fields + assert UI_FLEX_MODEL_SCHEMA == baseline class NewAsset: diff --git a/flexmeasures/ui/tests/ui_flex_model_schema_baseline.json b/flexmeasures/ui/tests/ui_flex_model_schema_baseline.json new file mode 100644 index 0000000000..048046b231 --- /dev/null +++ b/flexmeasures/ui/tests/ui_flex_model_schema_baseline.json @@ -0,0 +1,248 @@ +{ + "charging-efficiency": { + "default": null, + "description": "One-way conversion efficiency from the commodity (e.g. electricity) to the storage's state of charge.\nCan be a percentage, a ratio in the range [0,1], or a coefficient of performance (>1).\nDefaults to 100% (no conversion loss).\n", + "example-units": [ + "%" + ], + "types": { + "backend": "typeThree", + "ui": "One fixed value or a dynamic signal (via a sensor)." + } + }, + "commodity": { + "default": "electricity", + "description": "Commodity on which this device acts.\nDefaults to \"electricity\".\n", + "example-units": [ + "electricity", + "gas" + ], + "types": { + "backend": "typeOne", + "ui": "One fixed value only." + } + }, + "consumption": { + "default": null, + "description": "Sensor used to record the scheduled power as seen from a consumption perspective.\n\nThe sign convention is determined by the key name, and is stored on the sensor itself using the consumption_is_positive attribute.\n\nDepending on which output sensors are defined:\n\n- Only consumption defined: the full power schedule is stored on this sensor using the\n consumption-positive sign convention (consumption positive, production negative).\n- Only production defined: the full power schedule is stored on the production sensor\n with the production-positive convention (production positive, consumption negative).\n- Both defined: only the non-negative part of the schedule is stored on this sensor (zero for\n time steps with net production), and only the non-positive part (sign-flipped) is stored on the\n production sensor.\n", + "example-units": [ + "MW", + "kW" + ], + "types": { + "backend": "typeTwo", + "ui": "A sensor which records the scheduled consumption." + } + }, + "consumption-capacity": { + "default": null, + "description": "Device-level power constraint on consumption. How much power can be drawn by this asset.", + "example-units": [ + "MW", + "kW" + ], + "types": { + "backend": "typeThree", + "ui": "One fixed value or a dynamic signal (via a sensor)." + } + }, + "discharging-efficiency": { + "default": null, + "description": "One-way conversion efficiency from the storage's state of charge to the commodity (e.g. electricity).\nDefaults to 100% (no conversion loss).", + "example-units": [ + "%" + ], + "types": { + "backend": "typeThree", + "ui": "One fixed value or a dynamic signal (via a sensor)." + } + }, + "group": { + "default": null, + "description": "Reference to a group of devices whose aggregate power is constrained, given as either a power sensor ({\"sensor\": }) or an asset ({\"asset\": }) - exactly one of the two.\nThe referenced sensor or asset should itself get its own flex-model entry defining the group's power-capacity (hard constraint) and/or consumption-capacity/production-capacity (soft constraints with default breach prices).\nWhen the group is referenced by sensor, the group's scheduled aggregate power is saved to that group sensor.\nWhen the group is referenced by asset (e.g. a sub-EMS asset in the tree), the group entry defines no power sensor of its own; the group's aggregate power is instead saved via that entry's own consumption and/or production output sensors, following the usual output-sensor conventions.\n", + "example-units": [ + "MW", + "kW" + ], + "types": { + "backend": "typeTwo", + "ui": "A power sensor or an asset representing a group of devices; a sensor-referenced group also records the group's scheduled aggregate power, while an asset-referenced group records it via its own consumption/production output sensors." + } + }, + "power-capacity": { + "default": null, + "description": "Symmetric device-level power constraint. How much power can be applied to this asset in either direction.\nIf omitted, the scheduler infers this limit from the greatest of consumption-capacity and production-capacity when either is configured, before falling back to site-power-capacity.\nWhen exactly one of consumption-capacity or production-capacity is configured to non-zero capacity, the missing opposite capacity defaults to zero.", + "example-units": [ + "MW", + "kW" + ], + "types": { + "backend": "typeThree", + "ui": "One fixed value or a dynamic signal (via a sensor)." + } + }, + "prefer-charging-sooner": { + "default": null, + "description": "Tie-breaking policy to apply if conditions are stable, which signals a preference to charge sooner rather than later (defaults to True).\nIt also signals a preference to discharge later.\nBoolean option only.\n", + "example-units": [ + "Boolean" + ], + "types": { + "backend": "typeOne", + "ui": "Boolean option only." + } + }, + "prefer-curtailing-later": { + "default": null, + "description": "Tie-breaking policy to apply if conditions are stable, which signals a preference to curtail both consumption and production later, whichever is applicable (defaults to True).\nBoolean option only.\n", + "example-units": [ + "Boolean" + ], + "types": { + "backend": "typeOne", + "ui": "Boolean option only." + } + }, + "production": { + "default": null, + "description": "Sensor used to record the scheduled power as seen from a production perspective.\n\nThe sign convention is determined by the key name, and is stored on the sensor itself using the consumption_is_positive attribute.\n\nSee the consumption field for the full description of the split logic when both sensors are defined.\n", + "example-units": [ + "MW", + "kW" + ], + "types": { + "backend": "typeTwo", + "ui": "A sensor which records the scheduled production." + } + }, + "production-capacity": { + "default": null, + "description": "Device-level power constraint on production.\nHow much power can be supplied by this asset.\nFor PV curtailment, set this to reference your sensor containing PV power forecasts.\n", + "example-units": [ + "MW", + "kW" + ], + "types": { + "backend": "typeThree", + "ui": "One fixed value or a dynamic signal (via a sensor)." + } + }, + "roundtrip-efficiency": { + "default": null, + "description": "Below 100%, this represents roundtrip losses (of charging & discharging), usually used for batteries.\nCan be a percentage or a ratio in the range [0,1].\nDefaults to 100% (no roundtrip loss).\n", + "example-units": [ + "%" + ], + "types": { + "backend": "typeFive", + "ui": "Fixed value only." + } + }, + "soc-gain": { + "default": [], + "description": "SoC gain per time step, e.g. from a secondary energy source. Useful if energy is inserted by an external process (in-flow).\nThis field allows setting multiple components, either fixed or dynamic, which add up to an aggregated gain.\nThis field represents an energy flow (for instance, in kW) rather than saying something about an (allowed) energy state (for instance, in kWh).\nThe SoC gain is unaffected by the charging efficiency.\n", + "example-units": [ + "MW", + "kW" + ], + "types": { + "backend": "typeFour", + "ui": "Multiple settings possible - either fixed values or dynamic signals (via a sensor)." + } + }, + "soc-max": { + "default": null, + "description": "A constant and non-negotiable upper boundary for all values in the schedule (for storage devices, this defaults to max soc-target, if that is provided).\nIf omitted, no upper boundary is applied.\nIf used, this is regarded as an unsurpassable physical limitation.\nTo set softer boundaries, use the soc-maxima flex-model field instead together with the soc-maxima-breach-price field in the flex-context.\n", + "example-units": [ + "MWh", + "kWh" + ], + "types": { + "backend": "typeThree", + "ui": "One fixed value or a dynamic signal (via a sensor)." + } + }, + "soc-maxima": { + "default": null, + "description": "Set points that form upper boundaries at certain times, e.g. to target an empty heat buffer before a maintenance window.\nIf a soc-maxima-breach-price is defined, the soc-maxima become soft constraints in the optimization problem.\nOtherwise, they become hard constraints.", + "example-units": [ + "MWh", + "kWh" + ], + "types": { + "backend": "typeTwo", + "ui": "A sensor which records the state of charge." + } + }, + "soc-min": { + "default": null, + "description": "A constant and non-negotiable lower boundary for all SoC values in the schedule.\nIf omitted, no lower boundary is applied.\nIf used, this is regarded as an unsurpassable physical limitation.\nTo set softer boundaries, use the soc-minima flex-model field instead together with the soc-minima-breach-price field in the flex-context.\n", + "example-units": [ + "MWh", + "kWh" + ], + "types": { + "backend": "typeThree", + "ui": "One fixed value or a dynamic signal (via a sensor)." + } + }, + "soc-minima": { + "default": null, + "description": "Set points that form lower boundaries, e.g. to target a full car battery in the morning.\nIf a soc-minima-breach-price is defined, the soc-minima become soft constraints in the optimization problem.\nOtherwise, they become hard constraints.. Both single points in time and ranges are possible, see example.", + "example-units": [ + "MWh", + "kWh" + ], + "types": { + "backend": "typeTwo", + "ui": "A sensor which records the state of charge." + } + }, + "soc-targets": { + "default": null, + "description": "\nExact set point(s) of the storage's state of charge that the scheduler needs to realize.\nThese are hard constraints, which means that any infeasible state-of-charge targets would prevent a complete schedule from being computed.\n", + "example-units": [ + "MWh", + "kWh" + ], + "types": { + "backend": "typeTwo", + "ui": "A sensor which records the state of charge." + } + }, + "soc-usage": { + "default": [], + "description": "SoC drain per time step, e.g. from a load or heat sink.\nUseful if energy is extracted by an external process or there are dissipating losses (out-flow).\nThis field allows setting multiple components, either fixed or dynamic, which add up to an aggregated usage.\nThis field represents an energy flow (for instance, in kW) rather than saying something about an (allowed) energy state (for instance, in kWh).\nThe SoC drain is unaffected by the discharging efficiency.\n", + "example-units": [ + "MW", + "kW" + ], + "types": { + "backend": "typeFour", + "ui": "Multiple settings possible - either fixed values or dynamic signals (via a sensor)." + } + }, + "state-of-charge": { + "default": null, + "description": "Sensor used to record the scheduled state of charge. If soc-at-start is omitted, FlexMeasures will also use this field to infer the starting state of charge. For this use case, the field may also contain a time series specification instead. When a sensor is used, its unit may be an energy unit (e.g. MWh or kWh) or a percentage (%). For sensors with a % unit, the soc-max flex-model field must be set to a non-zero value to allow converting between the energy-based schedule and a percentage. Also, the state-of-charge sensor's resolution should be instantaneous (i.e. `PT0M`).", + "example-units": [ + "MWh", + "kWh" + ], + "types": { + "backend": "typeTwo", + "ui": "A sensor which records the state of charge." + } + }, + "storage-efficiency": { + "default": null, + "description": "The efficiency of keeping the storage's state of charge at its present level, used to encode losses over time.\nAs a result, each time step the energy is held longer leads to higher losses.\nThis setting is crucial to some sorts of energy storage, e.g. thermal buffers.\nTo give an example, when this setting is at 95% (or 0.95), this means a loss of 5% per time step. Defaults to 100% (no storage loss over time).\nNote that the storage efficiency used by the scheduler is applied over each time step equal to the scheduling resolution.\nFor example, a storage efficiency of 95 percent per (absolute) day, for scheduling a 1-hour resolution sensor, should be passed as a storage efficiency of 0.951/24 = 0.997865.\nAlternatively, to let FlexMeasures handle the conversion for you, record the storage-efficiency on a dedicated sensor (in this example, with a 24-hour event resolution).\nThen reference that sensor in the storage-efficiency field.\n", + "example-units": [ + "%" + ], + "types": { + "backend": "typeThree", + "ui": "One fixed value or a dynamic signal (via a sensor)." + } + } +} \ No newline at end of file From 3ad10ac6548b516e84f2502ef026a40edf9441c0 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 20 Jul 2026 00:22:39 +0200 Subject: [PATCH 2/2] Address review: drop the baseline guardrail and the changelog entry The baseline-equivalence test and its JSON fixture proved the derived UI schema matches the old one; that's done, so remove them (the well-formedness test remains). Drop the changelog entry too - this is an internal refactor. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_016MLCUiSdXDqDBmg8GbYp1B Signed-off-by: F.N. Claessen --- documentation/changelog.rst | 1 - flexmeasures/ui/tests/test_utils.py | 16 -- .../tests/ui_flex_model_schema_baseline.json | 248 ------------------ 3 files changed, 265 deletions(-) delete mode 100644 flexmeasures/ui/tests/ui_flex_model_schema_baseline.json diff --git a/documentation/changelog.rst b/documentation/changelog.rst index bbc42bd9c5..ed5b1019f1 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -49,7 +49,6 @@ Infrastructure / Support * Stop manual runs of the Docker publishing workflow from overwriting the ``latest`` image tag, and let them opt in to it explicitly [see `PR #2316 `_] * Add a pre-commit hook that blocks image files (png, jpg, gif, bmp, tiff, webp, ico, psd) from being committed outside of ``flexmeasures/ui/static/`` and ``documentation/``, to protect the git history from binary bloat; screenshots belong in the ``FlexMeasures/screenshots`` repo instead [see `PR #2315 `_] * Schedulers track devices via a typed device inventory, which classifies every flex-model entry once and serves as the single source of truth for device roles and canonical device indices [see `PR #2321 `_] -* Derive the UI flex-model schema from the storage flex-model DB schema, making the DB schema the single source of truth so that adding a flex-model field can no longer silently break DB/UI parity (a DB field without UI presentation info now fails loudly at import time) [see `PR #2330 `_] Bugfixes ----------- diff --git a/flexmeasures/ui/tests/test_utils.py b/flexmeasures/ui/tests/test_utils.py index 9ba279b174..b6f18448ad 100644 --- a/flexmeasures/ui/tests/test_utils.py +++ b/flexmeasures/ui/tests/test_utils.py @@ -138,22 +138,6 @@ def test_ui_flexmodel_schema(): assert entry["description"], f"UI flex-model entry '{key}' has no description." -def test_ui_flexmodel_schema_matches_baseline(): - """Guardrail: the derived UI flex-model schema must be identical, key-for-key - and value-for-value, to the hand-maintained schema captured before the - refactor that made DBStorageFlexModelSchema the single source of truth. - - This proves the UI editor sees exactly the same data as before. - """ - import json - from pathlib import Path - - baseline_path = Path(__file__).parent / "ui_flex_model_schema_baseline.json" - baseline = json.loads(baseline_path.read_text()) - - assert UI_FLEX_MODEL_SCHEMA == baseline - - class NewAsset: def __init__(self, db, setup_generic_asset_types, setup_accounts, new_asset_data): self.db = db diff --git a/flexmeasures/ui/tests/ui_flex_model_schema_baseline.json b/flexmeasures/ui/tests/ui_flex_model_schema_baseline.json deleted file mode 100644 index 048046b231..0000000000 --- a/flexmeasures/ui/tests/ui_flex_model_schema_baseline.json +++ /dev/null @@ -1,248 +0,0 @@ -{ - "charging-efficiency": { - "default": null, - "description": "One-way conversion efficiency from the commodity (e.g. electricity) to the storage's state of charge.\nCan be a percentage, a ratio in the range [0,1], or a coefficient of performance (>1).\nDefaults to 100% (no conversion loss).\n", - "example-units": [ - "%" - ], - "types": { - "backend": "typeThree", - "ui": "One fixed value or a dynamic signal (via a sensor)." - } - }, - "commodity": { - "default": "electricity", - "description": "Commodity on which this device acts.\nDefaults to \"electricity\".\n", - "example-units": [ - "electricity", - "gas" - ], - "types": { - "backend": "typeOne", - "ui": "One fixed value only." - } - }, - "consumption": { - "default": null, - "description": "Sensor used to record the scheduled power as seen from a consumption perspective.\n\nThe sign convention is determined by the key name, and is stored on the sensor itself using the consumption_is_positive attribute.\n\nDepending on which output sensors are defined:\n\n- Only consumption defined: the full power schedule is stored on this sensor using the\n consumption-positive sign convention (consumption positive, production negative).\n- Only production defined: the full power schedule is stored on the production sensor\n with the production-positive convention (production positive, consumption negative).\n- Both defined: only the non-negative part of the schedule is stored on this sensor (zero for\n time steps with net production), and only the non-positive part (sign-flipped) is stored on the\n production sensor.\n", - "example-units": [ - "MW", - "kW" - ], - "types": { - "backend": "typeTwo", - "ui": "A sensor which records the scheduled consumption." - } - }, - "consumption-capacity": { - "default": null, - "description": "Device-level power constraint on consumption. How much power can be drawn by this asset.", - "example-units": [ - "MW", - "kW" - ], - "types": { - "backend": "typeThree", - "ui": "One fixed value or a dynamic signal (via a sensor)." - } - }, - "discharging-efficiency": { - "default": null, - "description": "One-way conversion efficiency from the storage's state of charge to the commodity (e.g. electricity).\nDefaults to 100% (no conversion loss).", - "example-units": [ - "%" - ], - "types": { - "backend": "typeThree", - "ui": "One fixed value or a dynamic signal (via a sensor)." - } - }, - "group": { - "default": null, - "description": "Reference to a group of devices whose aggregate power is constrained, given as either a power sensor ({\"sensor\": }) or an asset ({\"asset\": }) - exactly one of the two.\nThe referenced sensor or asset should itself get its own flex-model entry defining the group's power-capacity (hard constraint) and/or consumption-capacity/production-capacity (soft constraints with default breach prices).\nWhen the group is referenced by sensor, the group's scheduled aggregate power is saved to that group sensor.\nWhen the group is referenced by asset (e.g. a sub-EMS asset in the tree), the group entry defines no power sensor of its own; the group's aggregate power is instead saved via that entry's own consumption and/or production output sensors, following the usual output-sensor conventions.\n", - "example-units": [ - "MW", - "kW" - ], - "types": { - "backend": "typeTwo", - "ui": "A power sensor or an asset representing a group of devices; a sensor-referenced group also records the group's scheduled aggregate power, while an asset-referenced group records it via its own consumption/production output sensors." - } - }, - "power-capacity": { - "default": null, - "description": "Symmetric device-level power constraint. How much power can be applied to this asset in either direction.\nIf omitted, the scheduler infers this limit from the greatest of consumption-capacity and production-capacity when either is configured, before falling back to site-power-capacity.\nWhen exactly one of consumption-capacity or production-capacity is configured to non-zero capacity, the missing opposite capacity defaults to zero.", - "example-units": [ - "MW", - "kW" - ], - "types": { - "backend": "typeThree", - "ui": "One fixed value or a dynamic signal (via a sensor)." - } - }, - "prefer-charging-sooner": { - "default": null, - "description": "Tie-breaking policy to apply if conditions are stable, which signals a preference to charge sooner rather than later (defaults to True).\nIt also signals a preference to discharge later.\nBoolean option only.\n", - "example-units": [ - "Boolean" - ], - "types": { - "backend": "typeOne", - "ui": "Boolean option only." - } - }, - "prefer-curtailing-later": { - "default": null, - "description": "Tie-breaking policy to apply if conditions are stable, which signals a preference to curtail both consumption and production later, whichever is applicable (defaults to True).\nBoolean option only.\n", - "example-units": [ - "Boolean" - ], - "types": { - "backend": "typeOne", - "ui": "Boolean option only." - } - }, - "production": { - "default": null, - "description": "Sensor used to record the scheduled power as seen from a production perspective.\n\nThe sign convention is determined by the key name, and is stored on the sensor itself using the consumption_is_positive attribute.\n\nSee the consumption field for the full description of the split logic when both sensors are defined.\n", - "example-units": [ - "MW", - "kW" - ], - "types": { - "backend": "typeTwo", - "ui": "A sensor which records the scheduled production." - } - }, - "production-capacity": { - "default": null, - "description": "Device-level power constraint on production.\nHow much power can be supplied by this asset.\nFor PV curtailment, set this to reference your sensor containing PV power forecasts.\n", - "example-units": [ - "MW", - "kW" - ], - "types": { - "backend": "typeThree", - "ui": "One fixed value or a dynamic signal (via a sensor)." - } - }, - "roundtrip-efficiency": { - "default": null, - "description": "Below 100%, this represents roundtrip losses (of charging & discharging), usually used for batteries.\nCan be a percentage or a ratio in the range [0,1].\nDefaults to 100% (no roundtrip loss).\n", - "example-units": [ - "%" - ], - "types": { - "backend": "typeFive", - "ui": "Fixed value only." - } - }, - "soc-gain": { - "default": [], - "description": "SoC gain per time step, e.g. from a secondary energy source. Useful if energy is inserted by an external process (in-flow).\nThis field allows setting multiple components, either fixed or dynamic, which add up to an aggregated gain.\nThis field represents an energy flow (for instance, in kW) rather than saying something about an (allowed) energy state (for instance, in kWh).\nThe SoC gain is unaffected by the charging efficiency.\n", - "example-units": [ - "MW", - "kW" - ], - "types": { - "backend": "typeFour", - "ui": "Multiple settings possible - either fixed values or dynamic signals (via a sensor)." - } - }, - "soc-max": { - "default": null, - "description": "A constant and non-negotiable upper boundary for all values in the schedule (for storage devices, this defaults to max soc-target, if that is provided).\nIf omitted, no upper boundary is applied.\nIf used, this is regarded as an unsurpassable physical limitation.\nTo set softer boundaries, use the soc-maxima flex-model field instead together with the soc-maxima-breach-price field in the flex-context.\n", - "example-units": [ - "MWh", - "kWh" - ], - "types": { - "backend": "typeThree", - "ui": "One fixed value or a dynamic signal (via a sensor)." - } - }, - "soc-maxima": { - "default": null, - "description": "Set points that form upper boundaries at certain times, e.g. to target an empty heat buffer before a maintenance window.\nIf a soc-maxima-breach-price is defined, the soc-maxima become soft constraints in the optimization problem.\nOtherwise, they become hard constraints.", - "example-units": [ - "MWh", - "kWh" - ], - "types": { - "backend": "typeTwo", - "ui": "A sensor which records the state of charge." - } - }, - "soc-min": { - "default": null, - "description": "A constant and non-negotiable lower boundary for all SoC values in the schedule.\nIf omitted, no lower boundary is applied.\nIf used, this is regarded as an unsurpassable physical limitation.\nTo set softer boundaries, use the soc-minima flex-model field instead together with the soc-minima-breach-price field in the flex-context.\n", - "example-units": [ - "MWh", - "kWh" - ], - "types": { - "backend": "typeThree", - "ui": "One fixed value or a dynamic signal (via a sensor)." - } - }, - "soc-minima": { - "default": null, - "description": "Set points that form lower boundaries, e.g. to target a full car battery in the morning.\nIf a soc-minima-breach-price is defined, the soc-minima become soft constraints in the optimization problem.\nOtherwise, they become hard constraints.. Both single points in time and ranges are possible, see example.", - "example-units": [ - "MWh", - "kWh" - ], - "types": { - "backend": "typeTwo", - "ui": "A sensor which records the state of charge." - } - }, - "soc-targets": { - "default": null, - "description": "\nExact set point(s) of the storage's state of charge that the scheduler needs to realize.\nThese are hard constraints, which means that any infeasible state-of-charge targets would prevent a complete schedule from being computed.\n", - "example-units": [ - "MWh", - "kWh" - ], - "types": { - "backend": "typeTwo", - "ui": "A sensor which records the state of charge." - } - }, - "soc-usage": { - "default": [], - "description": "SoC drain per time step, e.g. from a load or heat sink.\nUseful if energy is extracted by an external process or there are dissipating losses (out-flow).\nThis field allows setting multiple components, either fixed or dynamic, which add up to an aggregated usage.\nThis field represents an energy flow (for instance, in kW) rather than saying something about an (allowed) energy state (for instance, in kWh).\nThe SoC drain is unaffected by the discharging efficiency.\n", - "example-units": [ - "MW", - "kW" - ], - "types": { - "backend": "typeFour", - "ui": "Multiple settings possible - either fixed values or dynamic signals (via a sensor)." - } - }, - "state-of-charge": { - "default": null, - "description": "Sensor used to record the scheduled state of charge. If soc-at-start is omitted, FlexMeasures will also use this field to infer the starting state of charge. For this use case, the field may also contain a time series specification instead. When a sensor is used, its unit may be an energy unit (e.g. MWh or kWh) or a percentage (%). For sensors with a % unit, the soc-max flex-model field must be set to a non-zero value to allow converting between the energy-based schedule and a percentage. Also, the state-of-charge sensor's resolution should be instantaneous (i.e. `PT0M`).", - "example-units": [ - "MWh", - "kWh" - ], - "types": { - "backend": "typeTwo", - "ui": "A sensor which records the state of charge." - } - }, - "storage-efficiency": { - "default": null, - "description": "The efficiency of keeping the storage's state of charge at its present level, used to encode losses over time.\nAs a result, each time step the energy is held longer leads to higher losses.\nThis setting is crucial to some sorts of energy storage, e.g. thermal buffers.\nTo give an example, when this setting is at 95% (or 0.95), this means a loss of 5% per time step. Defaults to 100% (no storage loss over time).\nNote that the storage efficiency used by the scheduler is applied over each time step equal to the scheduling resolution.\nFor example, a storage efficiency of 95 percent per (absolute) day, for scheduling a 1-hour resolution sensor, should be passed as a storage efficiency of 0.951/24 = 0.997865.\nAlternatively, to let FlexMeasures handle the conversion for you, record the storage-efficiency on a dedicated sensor (in this example, with a 24-hour event resolution).\nThen reference that sensor in the storage-efficiency field.\n", - "example-units": [ - "%" - ], - "types": { - "backend": "typeThree", - "ui": "One fixed value or a dynamic signal (via a sensor)." - } - } -} \ No newline at end of file