Scheduling

Scheduling is the main value-drive of FlexMeasures. We have two major types of schedulers built-in, for storage devices (usually batteries or hot water storage) and processes (usually in industry).

FlexMeasures computes schedules for energy systems that consist of multiple devices that consume and/or produce a commodity (e.g. electricity or gas). We model a device as an asset with a consumption/production sensor recording power values, and compute schedules only for flexible devices, while taking into account inflexible devices.

Describing flexibility

To compute a schedule, FlexMeasures first needs to assess the flexibility state of the system. This is described by:

  • The flex-context ― information about the system as a whole, in order to assess the value of activating flexibility.

  • Flex-models ― information about the state and possible actions of the flexible device. We will discuss these per scheduled device type.

This information goes beyond the usual time series recorded by an asset’s sensors. It can be sent to FlexMeasures through the API when triggering schedule computation. Also, this information can be persisted on the FlexMeasures data model (in the db), and is editable through the UI (actually, that is design work in progress, currently possible with the flex context).

Note

You can also specify the scheduling resolution to control how often setpoints can change in the schedule. See Scheduling resolution for details on when and how to use custom resolutions.

Let’s dive into the details ― what can you tell FlexMeasures about your optimization problem?

The flex-context

The flex-context is independent of the type of flexible device that is optimized, or which scheduler is used. With the flexibility context, we aim to describe the system in which the flexible assets operate, such as its physical and contractual limitations. For multi-commodity scheduling problems, the flex-context can be defined separately per commodity (e.g. electricity and gas). See A flex-modeling tutorial for storage: Multiple commodities (gas & electricity) for a hands-on example.

Fields can have fixed values, but some fields can also point to sensors, so they will always represent the dynamics of the asset’s environment (as long as that sensor has current data). The full list of flex-context fields follows below. For more details on the possible formats for field values, see Variable quantities.

Where should you set these fields? Within requests to the API or by editing the relevant asset in the UI. If they are not sent in via the API (one of the endpoints triggering schedule computation), the scheduler will look them up on the flex-context field of the asset. And if the asset belongs to a larger system (a hierarchy of assets), the scheduler will also search if parent assets have them set.

Field

Example value

Description

commodity

electricity or gas

Commodity to which this part of the flex-context applies. Defaults to "electricity".

inflexible-device-sensors

[3, 4]

Power sensors representing devices that are relevant, but not flexible in the timing of their demand/supply. For example, a sensor recording rooftop solar power that is connected behind the main meter, and whose production falls under the same contract as the flexible device(s) being scheduled. Their power demand cannot be adjusted but still matters for finding the best schedule for other devices. Must be a list of integers.

aggregate-consumption

{'sensor': 10}

Sensor used to record the aggregate consumption schedule of all flexible and inflexible devices involved when scheduling this asset.

The sign convention is determined by the key name, and is stored on the sensor itself using the consumption_is_positive attribute.

Depending on which output sensors are defined:

  • Only aggregate-consumption defined: the full aggregate power schedule is stored on this sensor using the consumption-positive sign convention (consumption positive, production negative).

  • Only aggregate-production defined: the full aggregate power schedule is stored on the aggregate-production sensor with the production-positive convention (production positive, consumption negative).

  • Both defined: only the non-negative part of the aggregate schedule is stored on this sensor (zero for time steps with net production), and only the non-positive part (sign-flipped) is stored on the aggregate-production sensor.

aggregate-production

{'sensor': 11}

Sensor used to record the aggregate production schedule of all flexible and inflexible devices involved when scheduling this asset.

The sign convention is determined by the key name, and is stored on the sensor itself using the consumption_is_positive attribute.

See the aggregate-consumption field for the full description of the split logic when both sensors are defined.

aggregate-power

{'sensor': 9}

[Deprecated field] Sensor used to record the aggregate power schedule of all flexible and inflexible devices involved when scheduling this asset. To avoid using the field, use aggregate-consumption or aggregate-production instead, which make clear the sign convention.

consumption-price

{'sensor': 5} or 0.29 EUR/kWh

The commodity price (e.g. electricity price) applied to the site’s aggregate consumption. Can be (a sensor recording) market prices, but also CO₂ intensity—whatever fits your optimization problem. [1]

production-price

0.12 EUR/kWh

The commodity price (e.g. electricity price) applied to the site’s aggregate production. Can be (a sensor recording) market prices, but also CO₂ intensity—whatever fits your optimization problem, as long as the unit matches the consumption-price unit. [2]

site-power-capacity

45kVA

Maximum achievable power at the site’s grid connection point, in either direction. Becomes a hard constraint in the optimization problem, which is especially suitable for physical limitations. [3] [4]

site-consumption-capacity

45kW

Maximum consumption power at the site’s grid connection point. If site-power-capacity is defined, the minimum between the site-power-capacity and site-consumption-capacity will be used. [5] If a site-consumption-breach-price is defined, the site-consumption-capacity becomes a soft constraint in the optimization problem. Otherwise, it becomes a hard constraint. [4]

site-production-capacity

0kW

Maximum production power at the site’s grid connection point. If site-power-capacity is defined, the minimum between the site-power-capacity and site-production-capacity will be used. [7] If a site-production-breach-price is defined, the site-production-capacity becomes a soft constraint in the optimization problem. Otherwise, it becomes a hard constraint. [4]

site-peak-consumption

{'sensor': 7}

The site’s previously achieved achieved peak consumption. This value forms the baseline for new peak charges, since any peaks up to this level represent sunk costs. Defaults to 0 kW.

relax-constraints

True

If True (default is False), several constraints are relaxed by setting default breach prices within the optimization problem, leading to the default priority:

  1. Avoid breaching the site consumption/production capacity.

  2. Avoid not meeting SoC minima/maxima.

  3. Avoid breaching the desired device consumption/production capacity.

We recommend to set this field to True to enable the default prices and associated priorities as defined by FlexMeasures. For tighter control over prices and priorities, the breach prices can also be set explicitly (the relevant fields have breach-price in their name).

site-consumption-breach-price

1000 EUR/kW

This penalty value is used to discourage the violation of the site-consumption-capacity constraint in the flex-context. It effectively treats the capacity as a soft constraint, allowing the scheduler to exceed it when necessary but with a high cost. The scheduler will attempt to minimize this cost. It must use the same currency as the other price settings and cannot be negative. The field may define (a sensor recording) contractual penalties, or a theoretical penalty influencing how badly breaches should be avoided. [6] [8]

site-production-breach-price

1000 EUR/kW

This penalty value is used to discourage the violation of the site-production-capacity constraint in the flex-context. It effectively treats the capacity as a soft constraint, allowing the scheduler to exceed it when necessary but with a high cost. The scheduler will attempt to minimize this cost. It must use the same currency as the other price settings and cannot be negative. The field may define (a sensor recording) contractual penalties, or a theoretical penalty influencing how badly breaches should be avoided. [6] [8]

site-peak-consumption-price

260 EUR/MW

Per-kW price applied to any consumption that exceeds the site’s previously achieved peak consumption. This price reflects the cost of increasing the site’s peak further and is used by the scheduler to motivate peak shaving. It must use the same currency as the other price settings and cannot be negative. For large connections, this price is usually stated explicitly on the tariff sheets of their network operator. [6]

site-peak-production

{'sensor': 8}

The site’s previously achieved achieved peak production. This value forms the baseline for new peak charges, since any peaks up to this level represent sunk costs. Defaults to 0 kW.

site-peak-production-price

260 EUR/MW

Per-kW price applied to any production that exceeds the site’s previously achieved peak production. This price reflects the cost of increasing the site’s peak further and is used by the scheduler to motivate peak shaving. It must use the same currency as the other price settings and cannot be negative. For large connections, this price is usually stated explicitly on the tariff sheets of their network operator. [6]

soc-minima-breach-price

120 EUR/kWh

This penalty value is used to discourage the violation of soc-minima constraints in the flex-model, which the scheduler will attempt to minimize. It must use the same currency as the other price settings and cannot be negative. While it’s an internal nudge to steer the scheduler—and doesn’t represent a real-life cost—it should still be chosen in proportion to the actual energy prices at your site. If it’s too high, it will overly dominate other constraints; if it’s too low, it will have no effect. Without this value, the soc-minima become hard constraints, which means that any infeasible state-of-charge minima would prevent a complete schedule from being computed. [6] [8]

soc-maxima-breach-price

120 EUR/kWh

This penalty value is used to discourage the violation of soc-maxima constraints in the flex-model, which the scheduler will attempt to minimize. It must use the same currency as the other price settings and cannot be negative. While it’s an internal nudge to steer the scheduler—and doesn’t represent a real-life cost—it should still be chosen in proportion to the actual energy prices at your site. If it’s too high, it will overly dominate other constraints; if it’s too low, it will have no effect. Without this value, the soc-maxima become hard constraints, which means that any infeasible state-of-charge maxima would prevent a complete schedule from being computed. [6] [8]

consumption-breach-price

10 EUR/kW

This penalty value is used to discourage the violation of the consumption-capacity constraint in the flex-model. It effectively treats the capacity as a soft constraint, allowing the scheduler to exceed it when necessary but with a high cost. The scheduler will attempt to minimize this cost. It must use the same currency as the other price settings and cannot be negative. [6] [8]

production-breach-price

10 EUR/kW

This penalty value is used to discourage the violation of the production-capacity constraint in the flex-model. It effectively treats the capacity as a soft constraint, allowing the scheduler to exceed it when necessary but with a high cost. The scheduler will attempt to minimize this cost. It must use the same currency as the other price settings and cannot be negative. [6] [8]

commitments

[]

Prior commitments. Support for this field in the UI is still under further development, but you can find more information in Commitments.

Note

If no (symmetric, consumption and production) site capacity is defined (also not as defaults), the scheduler will not enforce any bound on the site power. The flexible device can still have its own power limit defined in its flex-model.

The flex-models & corresponding schedulers

FlexMeasures comes with a storage scheduler and a process scheduler, which work with flex models for storages and loads, respectively.

The storage scheduler is suitable for batteries and EV chargers, and is automatically selected when scheduling an asset with one of the following asset types: "battery", "one-way_evse" and "two-way_evse".

The process scheduler is suitable for shiftable, breakable and inflexible loads, and is automatically selected for asset types "process" and "load".

We describe the respective flex models below.

These fields can be configured in the UI editor on the asset properties page or sent through the API (one of the endpoints to trigger schedule computation, or using the FlexMeasures client) or through the CLI (the command to add schedules).

Storage

For storage devices, the FlexMeasures scheduler deals with the state of charge (SoC) for an optimal outcome. You can do a lot with this ― examples for storage devices are:

  • batteries

  • EV batteries connected to charge points

  • hot water storage (“heat batteries”, where the SoC relates to the water temperature)

  • pumped hydro storage (SoC is the water level)

  • water basins (here, SoC is supposed to be low, as water is being pumped out)

  • buffers of energy-intensive chemicals that are needed in other industry processes

The flex-model for storage devices describes to the scheduler what the flexible asset’s state is, and what constraints or preferences should be taken into account.

The full list of flex-model fields for the storage scheduler follows below. For more details on the possible formats for field values, see Variable quantities.

Field

Example value

Description

commodity

electricity or gas

Commodity on which this device acts. Defaults to "electricity".

consumption

{'sensor': 14}

Sensor used to record the scheduled power as seen from a consumption perspective.

The sign convention is determined by the key name, and is stored on the sensor itself using the consumption_is_positive attribute.

Depending on which output sensors are defined:

  • Only consumption defined: the full power schedule is stored on this sensor using the consumption-positive sign convention (consumption positive, production negative).

  • Only production defined: the full power schedule is stored on the production sensor with the production-positive convention (production positive, consumption negative).

  • Both defined: only the non-negative part of the schedule is stored on this sensor (zero for time steps with net production), and only the non-positive part (sign-flipped) is stored on the production sensor.

production

{'sensor': 15}

Sensor used to record the scheduled power as seen from a production perspective.

The sign convention is determined by the key name, and is stored on the sensor itself using the consumption_is_positive attribute.

See the consumption field for the full description of the split logic when both sensors are defined.

state-of-charge

{'sensor': 12}

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).

soc-at-start

3.1 kWh

The (estimated) state of charge at the beginning of the schedule (for storage devices, this defaults to 0). Usually added to each scheduling request. [9]

soc-unit

kWh

[Deprecated field] The unit used to interpret any SoC related flex-model value that does not mention a unit itself (only applies to numeric values, so not to string values). To avoid using this field, mention the unit in each field explicitly (for instance, "3.1 kWh" rather than 3.1). Only kWh and MWh are allowed.

soc-min

2.5 kWh

A constant and non-negotiable lower boundary for all SoC values in the schedule. If omitted, no lower boundary is applied. If used, this is regarded as an unsurpassable physical limitation. To set softer boundaries, use the soc-minima flex-model field instead together with the soc-minima-breach-price field in the flex-context. [9]

soc-max

7 kWh

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). If omitted, no upper boundary is applied. If used, this is regarded as an unsurpassable physical limitation. To set softer boundaries, use the soc-maxima flex-model field instead together with the soc-maxima-breach-price field in the flex-context. [9]

soc-minima

[{'datetime': '2024-02-05T08:00:00+01:00', 'value': '8.2 kWh'}, {'value': '51 kWh', 'start': '2024-02-05T12:00:00+01:00', 'end': '2024-02-05T13:30:00+01:00'}]

Set points that form lower boundaries, e.g. to target a full car battery in the morning. If a soc-minima-breach-price is defined, the soc-minima become soft constraints in the optimization problem. Otherwise, they become hard constraints. [10]. Both single points in time and ranges are possible, see example.

soc-maxima

[{'value': '51 kWh', 'start': '2024-02-05T12:00:00+01:00', 'end': '2024-02-05T13:30:00+01:00'}]

Set points that form upper boundaries at certain times, e.g. to target an empty heat buffer before a maintenance window. If a soc-maxima-breach-price is defined, the soc-maxima become soft constraints in the optimization problem. Otherwise, they become hard constraints. [11]

soc-targets

[{'datetime': '2024-02-05T08:00:00+01:00', 'value': '3.2 kWh'}]

Exact set point(s) of the storage’s state of charge that the scheduler needs to realize. These are hard constraints, which means that any infeasible state-of-charge targets would prevent a complete schedule from being computed.

soc-gain

['100 Wh/h', {'sensor': 34}]

SoC gain per time step, e.g. from a secondary energy source. Useful if energy is inserted by an external process (in-flow). This field allows setting multiple components, either fixed or dynamic, which add up to an aggregated gain. This field represents an energy flow (for instance, in kW) rather than saying something about an (allowed) energy state (for instance, in kWh). The SoC gain is unaffected by the charging efficiency.

soc-usage

['100 Wh/h', {'sensor': 23}]

SoC drain per time step, e.g. from a load or heat sink. Useful if energy is extracted by an external process or there are dissipating losses (out-flow). This field allows setting multiple components, either fixed or dynamic, which add up to an aggregated usage. This field represents an energy flow (for instance, in kW) rather than saying something about an (allowed) energy state (for instance, in kWh). The SoC drain is unaffected by the discharging efficiency.

roundtrip-efficiency

90%

Below 100%, this represents roundtrip losses (of charging & discharging), usually used for batteries. Can be a percentage or a ratio in the range [0,1]. Defaults to 100% (no roundtrip loss). [9]

charging-efficiency

.9

One-way conversion efficiency from the commodity (e.g. electricity) to the storage’s state of charge. Can be a percentage, a ratio in the range [0,1], or a coefficient of performance (>1). Defaults to 100% (no conversion loss).

discharging-efficiency

90%

One-way conversion efficiency from the storage’s state of charge to the commodity (e.g. electricity). Defaults to 100% (no conversion loss).

storage-efficiency

99.9%

The efficiency of keeping the storage’s state of charge at its present level, used to encode losses over time. As a result, each time step the energy is held longer leads to higher losses. This setting is crucial to some sorts of energy storage, e.g. thermal buffers. To 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). Note that the storage efficiency used by the scheduler is applied over each time step equal to the scheduling resolution. For 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.95^{1/24} = 0.997865\). Alternatively, 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). Then reference that sensor in the storage-efficiency field.

prefer-charging-sooner

True

Tie-breaking policy to apply if conditions are stable, which signals a preference to charge sooner rather than later (defaults to True). It also signals a preference to discharge later. Boolean option only.

prefer-curtailing-later

True

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). Boolean option only.

power-capacity

50 kVA

Symmetric device-level power constraint. How much power can be applied to this asset in either direction. If 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. When exactly one of consumption-capacity or production-capacity is configured to non-zero capacity, the missing opposite capacity defaults to zero. [11]

consumption-capacity

{'sensor': 56}

Device-level power constraint on consumption. How much power can be drawn by this asset. [11]

production-capacity

0 kW (only consumption)

Device-level power constraint on production. How much power can be supplied by this asset. For PV curtailment, set this to reference your sensor containing PV power forecasts. [11]

For more details on the possible formats for field values, see Variable quantities.

Usually, not the whole flexibility model is needed. FlexMeasures can infer missing values in the flex model, and even get them (as default) from the sensor’s attributes.

You can add new storage schedules with the CLI command flexmeasures add schedule.

If you model devices that buffer energy (e.g. thermal energy storage systems connected to heat pumps), we can use the same flexibility parameters described above for storage devices. However, here are some tips to model a buffer correctly:

  • Describe the thermal energy content in kWh or MWh.

  • Set soc-minima to the accumulative usage forecast.

  • Set charging-efficiency to the sensor describing the COP values.

  • Set storage-efficiency to a value below 100% to model (heat) loss.

For a hands-on example of a heat buffer fed by multiple devices, see A flex-modeling tutorial for storage: Multiple feeds into shared storage.

What happens if the flex model describes an infeasible problem for the storage scheduler? Excellent question! It is highly important for a robust operation that these situations still lead to a somewhat good outcome. From our practical experience, we derived a StorageFallbackScheduler. It simplifies an infeasible situation by just starting to charge, discharge, or do neither, depending on the first target state of charge and the capabilities of the asset.

Of course, we also log a failure in the scheduling job, so it’s important to take note of these failures. Often, mis-configured flex models are the reason.

For a hands-on tutorial on using some of the storage flex-model fields, head over to A flex-modeling tutorial for storage: Vehicle-to-grid use case and the API documentation for triggering schedules. For further hands-on examples, see A flex-modeling tutorial for storage: Multiple feeds into shared storage (multiple devices feeding one shared storage) and A flex-modeling tutorial for storage: Multiple commodities (gas & electricity) (devices on different commodities scheduled together).

Finally, are you interested in the linear programming details behind the storage scheduler? Then head over to Storage device scheduler: Linear model! You can also review the current flex-model for storage in the code, at flexmeasures.data.schemas.scheduling.storage.StorageFlexModelSchema.

Shiftable loads (processes)

For processes that can be shifted or interrupted, but have to happen at a constant rate (of consumption), FlexMeasures provides the ProcessScheduler. Some examples from practice (usually industry) could be:

  • A centrifuge’s daily work of combing through sludge water. Depends on amount of sludge present.

  • Production processes with a target amount of output until the end of the current shift. The target usually comes out of production planning.

  • Application of coating under hot temperature, with fixed number of times it needs to happen before some deadline.

Field

Example value

Description

power

"15kW"

Nominal power of the load.

duration

"PT4H"

Time that the load needs to lasts.

optimization_direction

"MAX"

Objective of the scheduler, to maximize ("MAX") or minimize ("MIN").

time_restrictions

[{"start": "2015-01-02T08:00:00+01:00", "duration": "PT2H"}]

Time periods in which the load cannot be scheduled to run.

process_type

"INFLEXIBLE", "SHIFTABLE" or "BREAKABLE"

Is the load inflexible and should it run as soon as possible? Or can the process’s start time be shifted? Or can it even be broken up into smaller segments?

You can review the current flex-model for processes in the code, at flexmeasures.data.schemas.scheduling.process.ProcessSchedulerFlexModelSchema.

You can add new shiftable-process schedules with the CLI command flexmeasures add schedule. Make sure to use the --scheduler ProcessScheduler option to use the in-built process scheduler.

Note

Currently, the ProcessScheduler uses only the consumption-price field of the flex-context, so it ignores any site capacities and inflexible devices.

The schedule

A schedule produced by FlexMeasures is a series of power values for each flexible device (represented by its power sensor), covering the scheduling window at the scheduling resolution.

For detailed constraint analysis (unresolved constraints and margins), use the GET /api/v3_0/jobs/<uuid> endpoint, which provides structured information about constraints organized by asset. See the Accessing constraint results section below for details.

Inspecting schedules

It can be crucial to inspect how your scheduling job is doing. Here are some ways to do that:

Errors

FlexMeasures will validate flex-config and asset & sensor IDs before starting the job, and let you know (in the console or API response) what went wrong.

Checking the status via the API

There is an API endpoint specifically for checking status, result and configuration info for jobs: GET /api/v3_0/jobs/{uuid} returns JSON with the job status, result, queue and function metadata, timestamps, and exception traceback information for failed jobs. For scheduling jobs specifically, this includes the constraint analysis described in Accessing constraint results below.

Checking the status via the CLI

There is also a CLI command, which basically mirrors what the API endpoint does (see above). Here is an example call:

flexmeasures jobs inspect-job --job 40ac6f2e-690d-4865-8203-429e54179112

The asset status page: listing jobs and more info

Each asset has a status page where you can find recent jobs which were run in the context of this asset. Clicking the “Info” button will give you a lot more insights into the jobs’ configuration than the above methods.

https://github.com/FlexMeasures/screenshots/raw/main/screenshot_status_page_job_info.png

The RQ-dashboard: complete overview

Internally, jobs are queued with the python-rq library. For this, a job dashboard is available, which users with the admin role can access via the menu. This gives a complete overview over all jobs running in FlexMeasures.

You find your jobs via the queues, see screenshot below. Clicking a job gives you more information, similar to the status page.

https://github.com/FlexMeasures/screenshots/raw/main/screenshot_rq_dashboard.png

Accessing constraint results

When a schedule is computed for a device with state-of-charge constraints, FlexMeasures analyzes whether the constraints can be met.

Use the jobs endpoint (GET /api/v3_0/jobs/<uuid>) to retrieve detailed constraint analysis for all assets involved in the scheduling job, organized by asset ID. This endpoint is useful when you want to inspect constraint violations without retrieving the full schedule.

Consider a site (asset ID 123) with four assets, each with a power sensor:

  • Sensors 1 & 2: Inflexible devices (e.g. PV panel and building load)

  • Sensors 3 & 4: Flexible devices (e.g. a battery and an EV charger), each with a state-of-charge sensor (sensors 5 and 6, respectively)

The scheduling workflow looks like this:

  1. Trigger the schedule for site asset 123 via POST /api/v3_0/assets/123/schedules/trigger. The endpoint returns a job UUID, e.g. "5d28df1b-9f16-4177-ae43-6e750d80fad3".

  2. Retrieve the scheduled power series for the flexible devices once scheduling is done, via GET /api/v3_0/sensors/3/schedules/<uuid> and GET /api/v3_0/sensors/4/schedules/<uuid>. Each response contains the power setpoints for that device:

    {
        "values": [0.5, 1.0, 1.5, 0.0],
        "start": "2024-01-15T08:00:00+00:00",
        "duration": "PT4H",
        "unit": "kW"
    }
    
  3. Retrieve constraint analysis for all flexible assets via GET /api/v3_0/jobs/<uuid>. The result field in the response shows whether the state-of-charge targets for sensors 5 and 6 could be met, and by how much. For a finished StorageScheduler job, result is always an object with unresolved and resolved constraint analysis (as shown below); both arrays are simply empty when the flex model defines no soc-minima/soc-maxima, or when a scheduler other than StorageScheduler was used.

The constraint results distinguish between:

  • Constraints that were unresolved: Soft constraints that could not be satisfied during optimization, with the shortfall or excess reported as their violation.

  • Constraints that were resolved: Soft constraints that were satisfied, with the headroom remaining reported as their margin.

For each device, the soc-minima/soc-maxima value under unresolved or resolved is a list of entries — one per violated slot (unresolved) or per met slot with its margin (resolved), ordered chronologically. By default, every violated or met slot is listed (this is not currently configurable via the API). Each list entry includes:

  • datetime: ISO 8601 UTC timestamp of that slot.

  • violation (unresolved only): Magnitude of the violation at that slot (shortage for minima, excess for maxima).

  • margin (resolved only): Headroom remaining at that slot.

Both violation and margin are always reported as positive numbers (magnitudes), never negative — whether a violation is a shortage or an excess follows from the constraint type (soc-minima vs. soc-maxima), not from its sign.

Suppose you schedule a battery device (asset ID 42) with the following constraints:

  • soc-minima: Battery must stay above 60 kWh

  • soc-maxima: Battery must not exceed 100 kWh

If the optimization cannot satisfy the minimum constraint at 10:30 UTC (falling short by 20 kWh) and again at 10:45 UTC (falling short by 15 kWh), but does satisfy the maximum constraint with margins of 40 kWh at 11:00 UTC and 35 kWh at 12:00 UTC, the constraint results would show:

Response via GET /api/v3_0/jobs/<uuid>:

{
    "status": "FINISHED",
    "message": "Scheduling job finished.",
    "result": {
        "unresolved": [
            {
                "asset": 42,
                "soc-minima": [
                    {
                        "datetime": "2024-01-15T10:30:00+00:00",
                        "violation": "20.0 kWh"
                    },
                    {
                        "datetime": "2024-01-15T10:45:00+00:00",
                        "violation": "15.0 kWh"
                    }
                ]
            }
        ],
        "resolved": [
            {
                "asset": 42,
                "soc-maxima": [
                    {
                        "datetime": "2024-01-15T11:00:00+00:00",
                        "margin": "40.0 kWh"
                    },
                    {
                        "datetime": "2024-01-15T12:00:00+00:00",
                        "margin": "35.0 kWh"
                    }
                ]
            }
        ]
    }
}

When constraints are all met:

An empty unresolved array indicates successful optimization. However, check the margins in resolved to understand how tight the constraints were:

  • Large margins (e.g., 50 kWh) suggest the device has significant flexibility headroom.

  • Small margins (e.g., 5 kWh) indicate the constraints were nearly violated.

  • Zero margin would mean the device hit the exact constraint limit.

Use case: If you see very small margins, you may want to relax constraints or provide additional flexibility to create a more robust schedule.

When constraints are unresolved:

Unresolved constraints indicate the optimization problem was over-constrained. Common causes:

  • Conflicting constraints, such as a high minimum on too short notice.

  • Insufficient headroom within the grid capacity, caused by inflexible devices.

The violation values tell you how much shortfall exists:

  • For soc-minima violations: The shortage in kWh. The device could not charge enough.

  • For soc-maxima violations: The excess in kWh. The device could not discharge enough.

Use case: If a battery is reporting 20 kWh shortage for a planned trip, you may need to:

  • Allow more time for charging.

  • Install a larger battery.

  • Reduce the minimum SoC requirement.

  • Stretch the minimum SoC requirement over a longer time period (using the duration field) to continue charging in case the user plugs out later than expected.

  • Warn the user about the shortfall.

  • If the battery is in an EV, charge en-route.

When no constraints are defined:

If unresolved and resolved are both empty, no state-of-charge constraints were set.

Note

Hard constraints (soc-targets) are never reported in results because the scheduler enforces them strictly by definition. If a hard constraint cannot be met, the entire scheduling job will fail, not produce results with violations.

Work on other schedulers

We believe the two schedulers (and their flex-models) we describe here are covering a lot of use cases already. Here are some thoughts on further innovation:

  • Writing your own scheduler. You can always write your own scheduler (see Plugin Customization). You then might want to add your own flex model, as well. FlexMeasures will let the scheduler decide which flexibility model is relevant and how it should be validated.

  • We also aim to model situations with more than one flexible asset, and that have different types of flexibility (e.g. EV charging and smart heating in the same site). 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.