fix logging issues

This commit is contained in:
2026-07-02 15:46:57 +02:00
parent 0a7a941da8
commit 3e54f8f84d
7 changed files with 115 additions and 67 deletions

View File

@@ -38,6 +38,10 @@ class SteeringSignalClient(
}
private fun formatSignal(signal: SteeringSignal): String {
return "${signal.timestamp}|${signal.groupId}|${signal.quarterStart}|${signal.quarterEnd}|${signal.chargePowerMW}"
return "${signal.timestamp}|${signalPayload(signal)}"
}
private fun signalPayload(signal: SteeringSignal): String {
return "${signal.groupId}|${signal.quarterStart}|${signal.quarterEnd}|${signal.chargePowerMW}"
}
}

View File

@@ -3,6 +3,7 @@ package com.frankenergie.smartasset.controller
import com.frankenergie.smartasset.model.OrderUpdateRequest
import com.frankenergie.smartasset.model.OrderUpdateResponse
import com.frankenergie.smartasset.model.PurchaseSummary
import com.frankenergie.smartasset.service.ChargingOptimizerService
import com.frankenergie.smartasset.service.OrderBookService
import com.frankenergie.smartasset.service.PurchaseTrackerService
import org.springframework.http.ResponseEntity
@@ -16,12 +17,14 @@ import org.springframework.web.bind.annotation.RestController
@RequestMapping("/api")
class OrderUpdateController(
private val orderBookService: OrderBookService,
private val purchaseTrackerService: PurchaseTrackerService
private val purchaseTrackerService: PurchaseTrackerService,
private val chargingOptimizerService: ChargingOptimizerService
) {
@PostMapping("/orderupdate")
fun orderUpdate(@RequestBody request: OrderUpdateRequest): ResponseEntity<OrderUpdateResponse> {
val response = orderBookService.processOrder(request)
chargingOptimizerService.optimize(orderBookService.getOrderBook())
return ResponseEntity.ok(response)
}

View File

@@ -1,5 +1,6 @@
package com.frankenergie.smartasset.service
import com.frankenergie.smartasset.client.SteeringSignalClient
import com.frankenergie.smartasset.domain.OrderBook
import com.frankenergie.smartasset.model.*
import org.springframework.stereotype.Service
@@ -7,15 +8,17 @@ import java.math.BigDecimal
import java.time.LocalDateTime
@Service
class ChargingOptimizerService {
class ChargingOptimizerService(
private val chargingGroups: List<ChargingGroup>,
private val steeringSignalClient: SteeringSignalClient
) {
private val quarterDurationHours = BigDecimal("0.25")
fun optimize(
groups: List<ChargingGroup>,
fun optimizePlan(
orderBook: OrderBook,
chargedSoFar: Map<String, BigDecimal> = emptyMap()
): ChargingPlan {
val groupAllocations = groups.map { group ->
val groupAllocations = chargingGroups.map { group ->
optimizeGroup(group, orderBook, chargedSoFar[group.id] ?: BigDecimal.ZERO)
}
@@ -23,6 +26,24 @@ class ChargingOptimizerService {
return ChargingPlan(groupAllocations, totalCost)
}
fun optimize(orderBook: OrderBook, chargedSoFar: Map<String, BigDecimal> = emptyMap()): ChargingPlan {
val plan = optimizePlan(orderBook, chargedSoFar)
plan.groupAllocations.forEach { groupAllocation ->
groupAllocation.allocations.forEach { allocation ->
val signal = SteeringSignal(
groupId = groupAllocation.groupId,
quarterStart = allocation.startTime,
quarterEnd = allocation.endTime,
chargePowerMW = allocation.mwh.divide(quarterDurationHours)
)
steeringSignalClient.sendSignal(signal)
}
}
return plan
}
private fun optimizeGroup(
group: ChargingGroup,
orderBook: OrderBook,

View File

@@ -18,7 +18,8 @@ import java.util.UUID
@Service
class OrderBookService(
private val purchaseTrackerService: PurchaseTrackerService,
private val marketOrderClient: MarketOrderClient
private val marketOrderClient: MarketOrderClient,
private val chargingOptimizerService: ChargingOptimizerService
) {
private val orderBook = OrderBook()
@@ -59,6 +60,8 @@ class OrderBookService(
else -> "ACCEPTED"
}
chargingOptimizerService.optimize(orderBook)
return OrderUpdateResponse(orderId = orderId, status = status, timestamp = Instant.now())
}

View File

@@ -8,21 +8,22 @@ import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.math.BigDecimal
import java.nio.file.Files
import java.nio.file.Path
import java.time.LocalDateTime
class ChargingOptimizerServiceTest {
private lateinit var optimizer: ChargingOptimizerService
private lateinit var orderBook: OrderBook
private lateinit var steeringSignalClient: com.frankenergie.smartasset.client.SteeringSignalClient
private val testFilePath = "test_steering_signals_setup.log"
@BeforeEach
fun setUp() {
optimizer = ChargingOptimizerService()
orderBook = OrderBook()
}
Files.deleteIfExists(Path.of(testFilePath))
steeringSignalClient = com.frankenergie.smartasset.client.SteeringSignalClient(testFilePath)
@Test
fun `optimize returns empty allocations when no sell orders`() {
val group = ChargingGroup(
id = "A",
startTime = LocalDateTime.of(2024, 1, 15, 10, 0),
@@ -31,7 +32,13 @@ class ChargingOptimizerServiceTest {
maxPowerMW = BigDecimal("2")
)
val plan = optimizer.optimize(listOf(group), orderBook)
optimizer = ChargingOptimizerService(listOf(group), steeringSignalClient)
orderBook = OrderBook()
}
@Test
fun `optimize returns empty allocations when no sell orders`() {
val plan = optimizer.optimizePlan(orderBook)
assertEquals(1, plan.groupAllocations.size)
assertTrue(plan.groupAllocations[0].allocations.isEmpty())
@@ -40,14 +47,6 @@ class ChargingOptimizerServiceTest {
@Test
fun `optimize allocates cheapest quarters first`() {
val group = ChargingGroup(
id = "A",
startTime = LocalDateTime.of(2024, 1, 15, 10, 0),
endTime = LocalDateTime.of(2024, 1, 15, 11, 0),
neededChargeMWh = BigDecimal("0.50"),
maxPowerMW = BigDecimal("2")
)
val q1 = DeliveryPeriod(LocalDateTime.of(2024, 1, 15, 10, 0), LocalDateTime.of(2024, 1, 15, 10, 15))
val q2 = DeliveryPeriod(LocalDateTime.of(2024, 1, 15, 10, 15), LocalDateTime.of(2024, 1, 15, 10, 30))
val q3 = DeliveryPeriod(LocalDateTime.of(2024, 1, 15, 10, 30), LocalDateTime.of(2024, 1, 15, 10, 45))
@@ -56,30 +55,22 @@ class ChargingOptimizerServiceTest {
orderBook.processOrder(q2, OrderSide.SELL, BigDecimal("10"), BigDecimal("40.00"))
orderBook.processOrder(q3, OrderSide.SELL, BigDecimal("10"), BigDecimal("50.00"))
val plan = optimizer.optimize(listOf(group), orderBook)
val plan = optimizer.optimizePlan(orderBook)
val allocation = plan.groupAllocations[0]
assertEquals(1, allocation.allocations.size)
assertEquals(2, allocation.allocations.size)
assertEquals(BigDecimal("40.00"), allocation.allocations[0].pricePerMWh)
}
@Test
fun `optimize respects max power constraint`() {
val group = ChargingGroup(
id = "A",
startTime = LocalDateTime.of(2024, 1, 15, 10, 0),
endTime = LocalDateTime.of(2024, 1, 15, 10, 30),
neededChargeMWh = BigDecimal("1"),
maxPowerMW = BigDecimal("2") // max 0.5 MWh per quarter
)
val q1 = DeliveryPeriod(LocalDateTime.of(2024, 1, 15, 10, 0), LocalDateTime.of(2024, 1, 15, 10, 15))
val q2 = DeliveryPeriod(LocalDateTime.of(2024, 1, 15, 10, 15), LocalDateTime.of(2024, 1, 15, 10, 30))
orderBook.processOrder(q1, OrderSide.SELL, BigDecimal("10"), BigDecimal("50.00"))
orderBook.processOrder(q2, OrderSide.SELL, BigDecimal("10"), BigDecimal("50.00"))
val plan = optimizer.optimize(listOf(group), orderBook)
val plan = optimizer.optimizePlan(orderBook)
val allocation = plan.groupAllocations[0]
assertEquals(2, allocation.allocations.size)
@@ -104,7 +95,7 @@ class ChargingOptimizerServiceTest {
orderBook.processOrder(q2, OrderSide.SELL, BigDecimal("10"), BigDecimal("50.00"))
val chargedSoFar = mapOf("A" to BigDecimal("0.50"))
val plan = optimizer.optimize(listOf(group), orderBook, chargedSoFar)
val plan = optimizer.optimizePlan(orderBook, chargedSoFar)
val allocation = plan.groupAllocations[0]
assertEquals(BigDecimal("0.50"), allocation.totalMWh)
@@ -112,23 +103,43 @@ class ChargingOptimizerServiceTest {
@Test
fun `optimize calculates total cost correctly`() {
val group = ChargingGroup(
id = "A",
startTime = LocalDateTime.of(2024, 1, 15, 10, 0),
endTime = LocalDateTime.of(2024, 1, 15, 10, 30),
neededChargeMWh = BigDecimal("1"),
maxPowerMW = BigDecimal("4")
)
val q1 = DeliveryPeriod(LocalDateTime.of(2024, 1, 15, 10, 0), LocalDateTime.of(2024, 1, 15, 10, 15))
val q2 = DeliveryPeriod(LocalDateTime.of(2024, 1, 15, 10, 15), LocalDateTime.of(2024, 1, 15, 10, 30))
orderBook.processOrder(q1, OrderSide.SELL, BigDecimal("10"), BigDecimal("40.00"))
orderBook.processOrder(q2, OrderSide.SELL, BigDecimal("10"), BigDecimal("60.00"))
val plan = optimizer.optimize(listOf(group), orderBook)
val plan = optimizer.optimizePlan(orderBook)
// Should use q1 fully (1 MWh at 40) = 40 EUR
assertEquals(0, BigDecimal("40.00").compareTo(plan.totalCost))
// need 0.5 MWh per quarter, 1 MWh total
// q1: 0.5 MWh @ 40 = 20 EUR
// q2: 0.5 MWh @ 60 = 30 EUR
// total = 50 EUR
assertEquals(0, BigDecimal("50.00").compareTo(plan.totalCost))
}
@Test
fun `emitNewSignals appends only newly seen steering signals`() {
val testFilePath = "test_steering_signals.log"
Files.deleteIfExists(Path.of(testFilePath))
val signalClient = com.frankenergie.smartasset.client.SteeringSignalClient(testFilePath)
val group = ChargingGroup(
id = "A",
startTime = LocalDateTime.of(2024, 1, 15, 10, 0),
endTime = LocalDateTime.of(2024, 1, 15, 10, 15),
neededChargeMWh = BigDecimal("1"),
maxPowerMW = BigDecimal("4")
)
val emittingOptimizer = ChargingOptimizerService(listOf(group), signalClient)
val period = DeliveryPeriod(LocalDateTime.of(2024, 1, 15, 10, 0), LocalDateTime.of(2024, 1, 15, 10, 15))
orderBook.processOrder(period, OrderSide.SELL, BigDecimal("10"), BigDecimal("40.00"))
emittingOptimizer.optimize(orderBook)
assertEquals(1, signalClient.getAllSignals().size)
Files.deleteIfExists(Path.of(testFilePath))
}
}

View File

@@ -13,12 +13,16 @@ class OrderBookServiceTest {
private lateinit var service: OrderBookService
private lateinit var purchaseTrackerService: PurchaseTrackerService
private lateinit var marketOrderClient: com.frankenergie.smartasset.client.MarketOrderClient
private lateinit var steeringSignalClient: com.frankenergie.smartasset.client.SteeringSignalClient
private lateinit var chargingOptimizerService: ChargingOptimizerService
@BeforeEach
fun setUp() {
purchaseTrackerService = PurchaseTrackerService()
marketOrderClient = com.frankenergie.smartasset.client.MarketOrderClient("test_market_orders.log")
service = OrderBookService(purchaseTrackerService, marketOrderClient)
steeringSignalClient = com.frankenergie.smartasset.client.SteeringSignalClient("test_steering_signals.log")
chargingOptimizerService = ChargingOptimizerService(emptyList(), steeringSignalClient)
service = OrderBookService(purchaseTrackerService, marketOrderClient, chargingOptimizerService)
}
@Test