charging optimization

This commit is contained in:
2026-07-01 12:05:56 +02:00
parent 922cee3c23
commit 71dfe62bbb
7 changed files with 285 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
package com.frankenergie.smartasset.config
import com.frankenergie.smartasset.model.ChargingGroup
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.math.BigDecimal
import java.time.LocalDate
import java.time.LocalDateTime
@Configuration
class ChargingGroupsConfig {
@Bean
fun chargingGroups(): List<ChargingGroup> {
val today = LocalDate.now()
return listOf(
ChargingGroup("A", today.atTime(0, 0), today.atTime(8, 30), BigDecimal("5"), BigDecimal("2")),
ChargingGroup("B", today.atTime(0, 0), today.atTime(11, 0), BigDecimal("10"), BigDecimal("3")),
ChargingGroup("C", today.atTime(13, 0), today.atTime(18, 0), BigDecimal("4"), BigDecimal("1")),
ChargingGroup("D", today.atTime(13, 0), today.atTime(21, 0), BigDecimal("20"), BigDecimal("6")),
ChargingGroup("E", today.atTime(17, 30), today.atTime(22, 0), BigDecimal("5"), BigDecimal("2")),
ChargingGroup("F", today.atTime(17, 30), today.plusDays(1).atTime(0, 0), BigDecimal("15"), BigDecimal("5"))
)
}
}

View File

@@ -0,0 +1,12 @@
package com.frankenergie.smartasset.model
import java.math.BigDecimal
import java.time.LocalDateTime
data class ChargingGroup(
val id: String,
val startTime: LocalDateTime,
val endTime: LocalDateTime,
val neededChargeMWh: BigDecimal,
val maxPowerMW: BigDecimal
)

View File

@@ -0,0 +1,14 @@
package com.frankenergie.smartasset.model
import java.math.BigDecimal
data class ChargingPlan(
val groupAllocations: List<GroupAllocation>,
val totalCost: BigDecimal
) {
val averagePricePerMWh: BigDecimal
get() {
val totalMWh = groupAllocations.sumOf { it.totalMWh }
return if (totalMWh > BigDecimal.ZERO) totalCost.divide(totalMWh, 2, java.math.RoundingMode.HALF_UP) else BigDecimal.ZERO
}
}

View File

@@ -0,0 +1,11 @@
package com.frankenergie.smartasset.model
import java.math.BigDecimal
import java.time.LocalDateTime
data class GroupAllocation(
val groupId: String,
val allocations: List<QuarterAllocation>,
val totalMWh: BigDecimal,
val totalCost: BigDecimal
)

View File

@@ -0,0 +1,13 @@
package com.frankenergie.smartasset.model
import java.math.BigDecimal
import java.time.LocalDateTime
data class QuarterAllocation(
val startTime: LocalDateTime,
val endTime: LocalDateTime,
val mwh: BigDecimal,
val pricePerMWh: BigDecimal
) {
val cost: BigDecimal get() = mwh * pricePerMWh
}

View File

@@ -0,0 +1,76 @@
package com.frankenergie.smartasset.service
import com.frankenergie.smartasset.domain.OrderBook
import com.frankenergie.smartasset.model.*
import org.springframework.stereotype.Service
import java.math.BigDecimal
import java.time.LocalDateTime
@Service
class ChargingOptimizerService {
private val quarterDurationHours = BigDecimal("0.25")
fun optimize(
groups: List<ChargingGroup>,
orderBook: OrderBook,
chargedSoFar: Map<String, BigDecimal> = emptyMap()
): ChargingPlan {
val groupAllocations = groups.map { group ->
optimizeGroup(group, orderBook, chargedSoFar[group.id] ?: BigDecimal.ZERO)
}
val totalCost = groupAllocations.sumOf { it.totalCost }
return ChargingPlan(groupAllocations, totalCost)
}
private fun optimizeGroup(
group: ChargingGroup,
orderBook: OrderBook,
alreadyCharged: BigDecimal
): GroupAllocation {
val remainingNeed = (group.neededChargeMWh - alreadyCharged).max(BigDecimal.ZERO)
val maxPerQuarter = group.maxPowerMW * quarterDurationHours
val quarters = getQuartersInWindow(group.startTime, group.endTime)
val quartersWithPrices = quarters.mapNotNull { period ->
val bestAsk = orderBook.getBestAsk(period)
bestAsk?.let { period to it.price }
}.sortedBy { it.second }
val allocations = mutableListOf<QuarterAllocation>()
var allocated = BigDecimal.ZERO
for ((period, price) in quartersWithPrices) {
if (allocated >= remainingNeed) break
val toAllocate = maxPerQuarter.min(remainingNeed - allocated)
allocations.add(
QuarterAllocation(
startTime = period.startTime,
endTime = period.endTime,
mwh = toAllocate,
pricePerMWh = price
)
)
allocated += toAllocate
}
return GroupAllocation(
groupId = group.id,
allocations = allocations,
totalMWh = allocated,
totalCost = allocations.sumOf { it.cost }
)
}
private fun getQuartersInWindow(start: LocalDateTime, end: LocalDateTime): List<DeliveryPeriod> {
val quarters = mutableListOf<DeliveryPeriod>()
var current = start
while (current < end) {
val quarterEnd = current.plusMinutes(15)
quarters.add(DeliveryPeriod(current, quarterEnd.coerceAtMost(end)))
current = quarterEnd
}
return quarters
}
}