Real-Time Streaming Restrictions for the Battery Optimizer

← Learning Hub Lesson 9 · Battery Optimizer Course · Estimated time: 20 minutes

In production, the optimizer no longer runs once and forgets. You get irregular updates from a stream (for example Kafka) about restrictions, derates, and new operational rules. This lesson shows how the optimizer changes when real-time data becomes a first-class input.

What changes?

Common real-time inputs

Architecture update

┌──────────────────────────────┐     ┌──────────────────────┐
│  Kafka stream / events       │──▶  │  Realtime data layer  │
│  • restrictions             │     │  • normalize messages │
│  • battery status           │     │  • active constraint  │
│  • market events            │     │    snapshot           │
└──────────────────────────────┘     └──────────────────────┘
              │                             │
              ▼                             ▼
   ┌──────────────────┐           ┌─────────────────────────┐
   │ Event-driven plan │◀──────────│ Rolling horizon optimizer │
   │ trigger / reopt   │           │ • time-varying bounds    │
   └──────────────────┘           └─────────────────────────┘

From Kafka message to optimizer parameter

@dataclass
class RealtimeRestriction:
    source: str
    valid_from: datetime
    valid_until: datetime
    max_charge_mw: float
    max_discharge_mw: float
    soc_min_pct: float
    soc_max_pct: float
    reserved_energy_mwh: float
    allow_charge: bool
    allow_discharge: bool
    note: str

The real-time feed becomes a time-indexed constraint bundle. The optimizer sees these values as parameter vectors instead of fixed scalars.

Optimizer model with real-time bounds

model.T = pyo.RangeSet(0, H - 1)
model.p_charge_max = pyo.Param(model.T, initialize=p_max_charge, mutable=True)
model.p_discharge_max = pyo.Param(model.T, initialize=p_max_discharge, mutable=True)
model.soc_min = pyo.Param(model.T, initialize=soc_min, mutable=True)
model.soc_max = pyo.Param(model.T, initialize=soc_max, mutable=True)
model.reserve = pyo.Param(model.T, initialize=reserved_energy, mutable=True)

model.p_c = pyo.Var(model.T, bounds=(0, p.p_max))
model.p_d = pyo.Var(model.T, bounds=(0, p.p_max))
model.soc = pyo.Var(model.T, bounds=(0, p.e_max))

# Real-time power limits

def charge_limit(m, t):
    return m.p_c[t] <= m.p_charge_max[t]

model.charge_limit = pyo.Constraint(model.T, rule=charge_limit)

def discharge_limit(m, t):
    return m.p_d[t] <= m.p_discharge_max[t]

model.discharge_limit = pyo.Constraint(model.T, rule=discharge_limit)

# Real-time SoC headroom and reservation

def soc_min_rule(m, t):
    return m.soc[t] >= m.soc_min[t] * p.e_max + m.reserve[t]

model.soc_min_con = pyo.Constraint(model.T, rule=soc_min_rule)


def soc_max_rule(m, t):
    return m.soc[t] <= m.soc_max[t] * p.e_max

model.soc_max_con = pyo.Constraint(model.T, rule=soc_max_rule)

If a restriction forbids discharge for hour 13, the data layer simply sets p_discharge_max[13] = 0. If the BMS reports a 6 MW derate for the next hour, the same parameter is adjusted before solving.

Event-driven reoptimization

Irregular Kafka updates should not be ignored until the next hourly cycle. Your system needs a small event loop that does two things:

def on_realtime_update(message):
    restriction = parse_message(message)
    data_layer.apply_realtime_restriction(restriction)
    optimizer.update_constraints(data_layer.active_constraints)
    schedule = optimizer.solve(current_time, horizon=next_6_hours)
    execution.submit(schedule)
Hard truth: Only commit a schedule that respects the latest active restrictions. The optimizer is useful only if its output is feasible under the current real-time rules.

When the optimizer should not solve

Recommended architecture pattern

Use a hybrid approach:

This keeps the optimizer manageable: it still solves over a horizon, but it also reacts quickly when the system receives a binding stream event.

📖 Recommended reading

Confluent blog: Data pipelines with Kafka — learn how streaming events become operational inputs for decision systems.

What should your optimizer do when a Kafka message arrives saying discharge is forbidden for the next two hours?

💬 Questions? Ask about how to wire Kafka into your optimizer, how to represent temporary restrictions, or how to keep the real-time decision loop safe.

Rechtliches: Impressum