diff --git a/CHANGELOG.md b/CHANGELOG.md index eb39c77589..7854a5e918 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -114,6 +114,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix expected secondaryData in baseStateData [#955](https://github.com/ie3-institute/simona/issues/955) - Improve code quality in fixedloadmodelspec and other tests [#919](https://github.com/ie3-institute/simona/issues/919) - Fix power flow calculation with em agents [#962](https://github.com/ie3-institute/simona/issues/962) +- Fix scheduling at Evcs with more than one Ev at a time without Em [#787](https://github.com/ie3-institute/simona/issues/787) ## [3.0.0] - 2023-08-07 diff --git a/src/main/scala/edu/ie3/simona/agent/participant/evcs/EvcsAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/evcs/EvcsAgentFundamentals.scala index d98e837e48..69b072e55b 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/evcs/EvcsAgentFundamentals.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/evcs/EvcsAgentFundamentals.scala @@ -65,7 +65,7 @@ import squants.{Dimensionless, Each, Power} import java.time.ZonedDateTime import java.util.UUID -import scala.collection.SortedSet +import scala.collection.immutable.SortedSet import scala.reflect.{ClassTag, classTag} protected trait EvcsAgentFundamentals @@ -494,9 +494,10 @@ protected trait EvcsAgentFundamentals val relevantData = createCalcRelevantData(modelBaseStateData, tick) + val lastState = getLastOrInitialStateData(modelBaseStateData, tick) + val updatedBaseStateData = { if (relevantData.arrivals.nonEmpty) { - val lastState = getLastOrInitialStateData(modelBaseStateData, tick) val currentEvs = modelBaseStateData.model.determineCurrentEvs( relevantData, @@ -528,6 +529,15 @@ protected trait EvcsAgentFundamentals modelBaseStateData } + // if the lastState's tick is the same as the actual tick the results have already been determined and announced when we handled the departedEvs + if (lastState.tick != tick) { + determineResultsAnnounceUpdateValueStore( + lastState, + currentTick, + modelBaseStateData, + ) + } + // We're only here if we're not flex-controlled, thus sending a Completion is always right goToIdleReplyCompletionAndScheduleTriggerForNextAction( updatedBaseStateData, diff --git a/src/main/scala/edu/ie3/simona/model/participant/evcs/EvcsModel.scala b/src/main/scala/edu/ie3/simona/model/participant/evcs/EvcsModel.scala index b00ec5e5e3..a1d21252ec 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/evcs/EvcsModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/evcs/EvcsModel.scala @@ -19,7 +19,6 @@ import edu.ie3.simona.model.participant.evcs.uncontrolled.{ ConstantPowerCharging, MaximumPowerCharging, } -import edu.ie3.util.scala.quantities.DefaultQuantities._ import edu.ie3.simona.model.participant.{ CalcRelevantData, FlexChangeIndicator, @@ -32,7 +31,8 @@ import edu.ie3.simona.util.TickUtil.TickLong import edu.ie3.util.quantities.PowerSystemUnits._ import edu.ie3.util.quantities.QuantityUtils.RichQuantityDouble import edu.ie3.util.scala.OperationInterval -import squants.energy.{KilowattHours, Kilowatts} +import edu.ie3.util.scala.quantities.DefaultQuantities._ +import squants.energy.Kilowatts import squants.time.Seconds import squants.{Dimensionless, Energy, Power} import tech.units.indriya.unit.Units.PERCENT @@ -514,13 +514,17 @@ final case class EvcsModel( modelState: EvcsState, data: EvcsRelevantData, ): ApparentPower = - throw new NotImplementedError("Use calculatePowerAndEvSoc() instead.") + throw new NotImplementedError( + "Use calculateNewScheduling() or chargeEv() instead." + ) override protected def calculateActivePower( modelState: EvcsState, data: EvcsRelevantData, ): Power = - throw new NotImplementedError("Use calculatePowerAndEvSoc() instead.") + throw new NotImplementedError( + "Use calculateNewScheduling() or chargeEv() instead." + ) override def determineFlexOptions( data: EvcsRelevantData, diff --git a/src/test/scala/edu/ie3/simona/agent/participant/EvcsAgentModelCalculationSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/EvcsAgentModelCalculationSpec.scala index e4e9f25ff1..9b1502bce4 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/EvcsAgentModelCalculationSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/EvcsAgentModelCalculationSpec.scala @@ -7,8 +7,12 @@ package edu.ie3.simona.agent.participant import com.typesafe.config.ConfigFactory +import edu.ie3.datamodel.models.OperationTime +import edu.ie3.datamodel.models.input.OperatorInput import edu.ie3.datamodel.models.input.system.EvcsInput -import edu.ie3.datamodel.models.input.system.characteristic.QV +import edu.ie3.datamodel.models.input.system.`type`.chargingpoint.ChargingPointTypeUtils +import edu.ie3.datamodel.models.input.system.`type`.evcslocation.EvcsLocationType +import edu.ie3.datamodel.models.input.system.characteristic.{CosPhiFixed, QV} import edu.ie3.datamodel.models.result.system.{EvResult, EvcsResult} import edu.ie3.simona.agent.ValueStore import edu.ie3.simona.agent.grid.GridAgentMessages.{ @@ -61,6 +65,7 @@ import squants.energy._ import squants.{Each, Energy, Power} import java.time.ZonedDateTime +import java.util.UUID import scala.collection.immutable.{SortedMap, SortedSet} class EvcsAgentModelCalculationSpec @@ -1987,6 +1992,386 @@ class EvcsAgentModelCalculationSpec } - } + "provide correct results for three evs charging at same time without Em" in { + val evService = TestProbe("evService") + val resultListener = TestProbe("ResultListener") + + val inputModelUuid = + UUID.fromString("3278d111-b6ce-438c-8b1a-d060be93e520") + val evcsInputModel = new EvcsInput( + inputModelUuid, + "Dummy_EvcsModel", + OperatorInput.NO_OPERATOR_ASSIGNED, + OperationTime.notLimited(), + nodeInputNoSlackNs04KvA, + CosPhiFixed.CONSTANT_CHARACTERISTIC, + null, + ChargingPointTypeUtils.ChargingStationType2, + 4, + 0.95, + EvcsLocationType.HOME, + true, + ) + + val initStateData = ParticipantInitializeStateData[ + EvcsInput, + EvcsRuntimeConfig, + ApparentPower, + ]( + evcsInputModel, + modelConfig = modelConfig, + secondaryDataServices = Iterable( + ActorExtEvDataService(evService.ref) + ), + simulationStartDate = simulationStartDate, + simulationEndDate = simulationEndDate, + resolution = 900L, + requestVoltageDeviationThreshold = requestVoltageDeviationThreshold, + outputConfig = NotifierConfig( + simulationResultInfo = true, + powerRequestReply = false, + flexResult = true, + ), + primaryServiceProxy = primaryServiceProxy.ref, + ) + val evcsAgent = TestFSMRef( + new EvcsAgent( + scheduler = scheduler.ref, + initStateData = initStateData, + listener = Iterable(resultListener.ref), + ) + ) + scheduler.send(evcsAgent, Activation(INIT_SIM_TICK)) + + /* Actor should ask for registration with primary service */ + primaryServiceProxy.expectMsg( + PrimaryServiceRegistrationMessage(inputModelUuid) + ) + /* State should be information handling and having correct state data */ + evcsAgent.stateName shouldBe HandleInformation + evcsAgent.stateData match { + case ParticipantInitializingStateData( + inputModel, + modelConfig, + secondaryDataServices, + simulationStartDate, + simulationEndDate, + resolution, + requestVoltageDeviationThreshold, + outputConfig, + maybeEmAgent, + ) => + inputModel shouldBe SimpleInputContainer(evcsInputModel) + modelConfig shouldBe modelConfig + secondaryDataServices shouldBe Iterable( + ActorExtEvDataService(evService.ref) + ) + simulationStartDate shouldBe simulationStartDate + simulationEndDate shouldBe simulationEndDate + resolution shouldBe resolution + requestVoltageDeviationThreshold shouldBe requestVoltageDeviationThreshold + outputConfig shouldBe NotifierConfig( + simulationResultInfo = true, + powerRequestReply = false, + flexResult = true, + ) + maybeEmAgent shouldBe None + case unsuitableStateData => + fail(s"Agent has unsuitable state data '$unsuitableStateData'.") + } + + /* Refuse registration */ + primaryServiceProxy.send( + evcsAgent, + RegistrationFailedMessage(primaryServiceProxy.ref), + ) + + evService.expectMsg(RegisterForEvDataMessage(evcsInputModel.getUuid)) + evService.send( + evcsAgent, + RegistrationSuccessfulMessage(evService.ref, Some(0)), + ) + + scheduler.expectMsg(Completion(evcsAgent.toTyped, Some(0))) + + /* TICK 0 (expected activation) + - currently no cars + */ + scheduler.send(evcsAgent, Activation(0)) + + evService.send( + evcsAgent, + ProvideEvDataMessage( + 0, + evService.ref, + ArrivingEvs(Seq.empty), + Some(900), + ), + ) + + scheduler.expectMsg(Completion(evcsAgent.toTyped, Some(900))) + + /* TICK 900 + * - ev900 arrives + * - charging with 11 kW + */ + scheduler.send(evcsAgent, Activation(900)) + + val ev900 = EvModelWrapper(evA.copyWithDeparture(3600)) + + evService.send( + evcsAgent, + ProvideEvDataMessage( + 900, + evService.ref, + ArrivingEvs(Seq(ev900)), + Some(1800), + ), + ) + + scheduler.expectMsg(Completion(evcsAgent.toTyped, Some(1800))) + + resultListener.expectMsgType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: EvcsResult) => + result.getInputModel shouldBe evcsInputModel.getUuid + result.getTime shouldBe 0.toDateTime + result.getP should beEquivalentTo(0d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + } + + /* TICK 1800 + * - ev1800 arrives + * - charging with 11 kW + */ + scheduler.send(evcsAgent, Activation(1800)) + + val ev1800 = EvModelWrapper(evB.copyWithDeparture(4500)) + + evService.send( + evcsAgent, + ProvideEvDataMessage( + 1800, + evService.ref, + ArrivingEvs(Seq(ev1800)), + Some(2700), + ), + ) + + scheduler.expectMsg(Completion(evcsAgent.toTyped, Some(2700))) + + resultListener.expectMsgType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: EvResult) => + result.getInputModel match { + case model if model == ev900.uuid => + result.getTime shouldBe 900.toDateTime + result.getP should beEquivalentTo(11d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(0.asPercent, 1e-2) + } + } + + resultListener.expectMsgType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: EvcsResult) => + result.getInputModel shouldBe evcsInputModel.getUuid + result.getTime shouldBe 900.toDateTime + result.getP should beEquivalentTo(11d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + } + + /* TICK 2700 + * - ev2700 arrives + * - charging with 22 kW + */ + scheduler.send(evcsAgent, Activation(2700)) + + val ev2700 = EvModelWrapper(evC.copyWithDeparture(5400)) + + evService.send( + evcsAgent, + ProvideEvDataMessage( + 2700, + evService.ref, + ArrivingEvs(Seq(ev2700)), + None, + ), + ) + + scheduler.expectMsg(Completion(evcsAgent.toTyped, None)) + + resultListener.receiveN(2).foreach { + case ParticipantResultEvent(result: EvResult) => + result.getInputModel match { + case model if model == ev900.uuid => + result.getTime shouldBe 1800.toDateTime + result.getP should beEquivalentTo(11d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(4.74d.asPercent, 1e-2) + case model if model == ev1800.uuid => + result.getTime shouldBe 1800.toDateTime + result.getP should beEquivalentTo(11d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(0.asPercent, 1e-2) + } + } + + resultListener.expectMsgType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: EvcsResult) => + result.getInputModel shouldBe evcsInputModel.getUuid + result.getTime shouldBe 1800.toDateTime + result.getP should beEquivalentTo(22d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + } + + // TICK 3600: ev900 leaves + evService.send( + evcsAgent, + DepartingEvsRequest(3600, Seq(ev900.uuid)), + ) + + evService.expectMsgType[DepartingEvsResponse] match { + case DepartingEvsResponse(evcs, evModels) => + evcs shouldBe evcsInputModel.getUuid + evModels should have size 1 + evModels.headOption match { + case Some(evModel) => + evModel.uuid shouldBe ev900.uuid + evModel.storedEnergy should approximate(KilowattHours(8.25)) + case None => fail("Expected to get at least one ev.") + } + } + + resultListener.receiveN(4).foreach { + case ParticipantResultEvent(result: EvResult) => + result.getInputModel match { + case model if model == ev900.uuid => + result.getTime match { + case time if time == 2700.toDateTime => + result.getP should beEquivalentTo(11d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(9.48d.asPercent, 1e-2) + case time if time == 3600.toDateTime => + result.getP should beEquivalentTo(0d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(14.22d.asPercent, 1e-2) + } + case model if model == ev1800.uuid => + result.getTime shouldBe 2700.toDateTime + result.getP should beEquivalentTo(11d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(3.44d.asPercent, 1e-2) + case model if model == ev2700.uuid => + result.getTime shouldBe 2700.toDateTime + result.getP should beEquivalentTo(22d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(0.asPercent, 1e-2) + } + } + + resultListener.expectMsgType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: EvcsResult) => + result.getInputModel shouldBe evcsInputModel.getUuid + result.getTime shouldBe 2700.toDateTime + result.getP should beEquivalentTo(44d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + } + + // TICK 4500: ev1800 leaves + + evService.send( + evcsAgent, + DepartingEvsRequest(4500, Seq(ev1800.uuid)), + ) + + evService.expectMsgType[DepartingEvsResponse] match { + case DepartingEvsResponse(evcs, evModels) => + evcs shouldBe evcsInputModel.getUuid + evModels should have size 1 + evModels.headOption match { + case Some(evModel) => + evModel.uuid shouldBe ev1800.uuid + evModel.storedEnergy should approximate(KilowattHours(8.25)) + case None => fail("Expected to get at least one ev.") + } + } + + resultListener.receiveN(3).foreach { + case ParticipantResultEvent(result: EvResult) => + result.getInputModel match { + case model if model == ev1800.uuid => + result.getTime match { + case time if time == 3600.toDateTime => + result.getP should beEquivalentTo(11d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(6.88.asPercent, 1e-2) + case time if time == 4500.toDateTime => + result.getP should beEquivalentTo(0d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(10.31.asPercent, 1e-2) + } + case model if model == ev2700.uuid => + result.getTime shouldBe 3600.toDateTime + result.getP should beEquivalentTo(22d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(4.58d.asPercent, 1e-2) + } + } + + resultListener.expectMsgType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: EvcsResult) => + result.getInputModel shouldBe evcsInputModel.getUuid + result.getTime shouldBe 3600.toDateTime + result.getP should beEquivalentTo(33d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + } + + // TICK 5400: ev2700 leaves + + evService.send( + evcsAgent, + DepartingEvsRequest(5400, Seq(ev2700.uuid)), + ) + + evService.expectMsgType[DepartingEvsResponse] match { + case DepartingEvsResponse(evcs, evModels) => + evcs shouldBe evcsInputModel.getUuid + evModels should have size 1 + evModels.headOption match { + case Some(evModel) => + evModel.uuid shouldBe ev2700.uuid + evModel.storedEnergy should approximate(KilowattHours(16.5)) + case None => fail("Expected to get at least one ev.") + } + } + + resultListener.receiveN(2).foreach { + case ParticipantResultEvent(result: EvResult) => + result.getInputModel match { + case model if model == ev2700.uuid => + result.getTime match { + case time if time == 4500.toDateTime => + result.getP should beEquivalentTo(22d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(9.17.asPercent, 1e-2) + case time if time == 5400.toDateTime => + result.getP should beEquivalentTo(0d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + result.getSoc should beEquivalentTo(13.75.asPercent, 1e-2) + } + } + } + + resultListener.expectMsgType[ParticipantResultEvent] match { + case ParticipantResultEvent(result: EvcsResult) => + result.getInputModel shouldBe evcsInputModel.getUuid + result.getTime shouldBe 4500.toDateTime + result.getP should beEquivalentTo(22d.asKiloWatt) + result.getQ should beEquivalentTo(0d.asMegaVar) + } + /* FixMe: We would expect another Evcs Result for the lastTick of 5400 here. + But this can't be calculated since there is no nextTick. + For simulation it is as well necessary to fix this e.g. by writing the lastResults when finishing simulation. + */ + } + } } diff --git a/src/test/scala/edu/ie3/simona/test/common/EvTestData.scala b/src/test/scala/edu/ie3/simona/test/common/EvTestData.scala index ca77f35553..56c90c8e41 100644 --- a/src/test/scala/edu/ie3/simona/test/common/EvTestData.scala +++ b/src/test/scala/edu/ie3/simona/test/common/EvTestData.scala @@ -14,7 +14,7 @@ import java.util.UUID trait EvTestData { protected val evA: MockEvModel = new MockEvModel( - UUID.fromString("73c041c7-68e9-470e-8ca2-21fd7dbd1797"), + UUID.fromString("0-0-0-0-a"), "evA", Quantities.getQuantity(11d, PowerSystemUnits.KILOWATT), Quantities.getQuantity(11d, PowerSystemUnits.KILOWATT), @@ -22,11 +22,19 @@ trait EvTestData { 200, ) protected val evB: MockEvModel = new MockEvModel( - UUID.fromString("6d7d27a1-5cbb-4b73-aecb-dfcc5a6fb22e"), + UUID.fromString("0-0-0-0-b"), "evB", Quantities.getQuantity(11d, PowerSystemUnits.KILOWATT), Quantities.getQuantity(11d, PowerSystemUnits.KILOWATT), Quantities.getQuantity(80d, PowerSystemUnits.KILOWATTHOUR), 200, ) + protected val evC: MockEvModel = new MockEvModel( + UUID.fromString("0-0-0-0-c"), + "evC", + Quantities.getQuantity(22d, PowerSystemUnits.KILOWATT), + Quantities.getQuantity(22d, PowerSystemUnits.KILOWATT), + Quantities.getQuantity(120d, PowerSystemUnits.KILOWATTHOUR), + 200, + ) }