diff --git a/CHANGELOG.md b/CHANGELOG.md index 39d701ac32..b2d3c2b5a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated AUTHORS.md [#904](https://github.com/ie3-institute/simona/issues/904) - Updated `Gradle` to version V8.10 [#829](https://github.com/ie3-institute/simona/issues/829) - Updated AUTHORS.md [#905](https://github.com/ie3-institute/simona/issues/905) +- Rewrote BMModelTest from groovy to scala [#646](https://github.com/ie3-institute/simona/issues/646) - Prepare ThermalStorageTestData for Storage without storageVolumeLvlMin [#894](https://github.com/ie3-institute/simona/issues/894) ### Fixed diff --git a/build.gradle b/build.gradle index c123cf1e73..b17292a4cb 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ ext { pekkoVersion = '1.0.3' jtsVersion = '1.19.0' confluentKafkaVersion = '7.4.0' - tscfgVersion = '1.0.0' + tscfgVersion = '1.1.3' scapegoatVersion = '3.0.0' testContainerVersion = '0.41.4' @@ -103,7 +103,7 @@ dependencies { /* testing */ testImplementation 'org.spockframework:spock-core:2.3-groovy-4.0' testImplementation 'org.scalatestplus:mockito-3-4_2.13:3.2.10.0' - testImplementation 'org.mockito:mockito-core:5.12.0' // mocking framework + testImplementation 'org.mockito:mockito-core:5.13.0' // mocking framework testImplementation "org.scalatest:scalatest_${scalaVersion}:3.2.19" testRuntimeOnly 'com.vladsch.flexmark:flexmark-all:0.64.8' //scalatest html output testImplementation group: 'org.pegdown', name: 'pegdown', version: '1.6.0' diff --git a/src/main/scala/edu/ie3/simona/model/participant/BMModel.scala b/src/main/scala/edu/ie3/simona/model/participant/BMModel.scala index 418af563d5..19fbe3e601 100644 --- a/src/main/scala/edu/ie3/simona/model/participant/BMModel.scala +++ b/src/main/scala/edu/ie3/simona/model/participant/BMModel.scala @@ -52,7 +52,7 @@ final case class BMModel( /** Saves power output of last cycle. Needed for load gradient */ - private var _lastPower: Option[Power] = None + var _lastPower: Option[Power] = None override def calculatePower( tick: Long, @@ -73,7 +73,7 @@ final case class BMModel( * @return * Active power */ - override protected def calculateActivePower( + override def calculateActivePower( modelState: ConstantState.type, data: BMCalcRelevantData, ): Power = { @@ -100,7 +100,7 @@ final case class BMModel( * @return * factor k1 */ - private def calculateK1(time: ZonedDateTime): Double = { + def calculateK1(time: ZonedDateTime): Double = { val weekendCorr = Vector(0.98, 0.985, 0.982, 0.982, 0.97, 0.96, 0.95, 0.93, 0.925, 0.95, 0.98, 1.01, 1.018, 1.01, 1.01, 0.995, 1, 0.995, 0.99, 0.985, 0.99, 0.98, 0.975, 0.99) @@ -120,7 +120,7 @@ final case class BMModel( * @return * factor k2 */ - private def calculateK2(time: ZonedDateTime): Double = { + def calculateK2(time: ZonedDateTime): Double = { time.getDayOfYear match { case x if x < 150 || x > 243 => 1.03 // correction factor in heating season @@ -138,7 +138,7 @@ final case class BMModel( * @return * heat demand in Megawatt */ - private def calculatePTh( + def calculatePTh( temp: Temperature, k1: Double, k2: Double, @@ -158,7 +158,7 @@ final case class BMModel( * @return * usage */ - private def calculateUsage(pTh: Power): Double = { + def calculateUsage(pTh: Power): Double = { // if demand exceeds capacity -> activate peak load boiler (no effect on electrical output) val maxHeat = Megawatts(43.14) val usageUnchecked = pTh / maxHeat @@ -173,7 +173,7 @@ final case class BMModel( * @return * efficiency */ - private def calculateEff(usage: Double): Double = + def calculateEff(usage: Double): Double = min(0.18 * pow(usage, 3) - 0.595 * pow(usage, 2) + 0.692 * usage + 0.724, 1) /** Calculates electrical output from usage and efficiency @@ -184,7 +184,7 @@ final case class BMModel( * @return * electrical output as Power */ - private def calculateElOutput( + def calculateElOutput( usage: Double, eff: Double, ): Power = { @@ -206,7 +206,7 @@ final case class BMModel( * @return * electrical output after load gradient has been applied */ - private def applyLoadGradient( + def applyLoadGradient( pEl: Power ): Power = { _lastPower match { diff --git a/src/test/groovy/edu/ie3/simona/model/participant/BMModelTest.groovy b/src/test/groovy/edu/ie3/simona/model/participant/BMModelTest.groovy deleted file mode 100644 index f6d83d770b..0000000000 --- a/src/test/groovy/edu/ie3/simona/model/participant/BMModelTest.groovy +++ /dev/null @@ -1,253 +0,0 @@ -/* - * © 2020. TU Dortmund University, - * Institute of Energy Systems, Energy Efficiency and Energy Economics, - * Research group Distribution grid planning and operation - */ - -package edu.ie3.simona.model.participant - -import static edu.ie3.util.quantities.PowerSystemUnits.* -import static tech.units.indriya.unit.Units.PERCENT - -import edu.ie3.datamodel.models.input.NodeInput -import edu.ie3.datamodel.models.input.system.characteristic.CosPhiFixed -import edu.ie3.datamodel.models.input.system.type.BmTypeInput -import edu.ie3.simona.model.participant.ModelState.ConstantState$ -import edu.ie3.simona.model.participant.control.QControl -import edu.ie3.util.scala.OperationInterval -import edu.ie3.util.scala.quantities.EuroPerKilowatthour$ -import edu.ie3.util.scala.quantities.Sq -import scala.Some -import spock.lang.Shared -import spock.lang.Specification -import squants.energy.Kilowatts$ -import squants.energy.Megawatts$ -import squants.market.EUR$ -import squants.thermal.Celsius$ -import tech.units.indriya.quantity.Quantities - -import java.time.ZonedDateTime - -/** - * Test class that tries to cover all special cases of the current implementation of the {@link BMModel} - * - * Test results have been calculated on paper using equations from wiki: https://wiki.ie3.e-technik.tu-dortmund.de/!simona/model:bm_model - */ -class BMModelTest extends Specification { - - @Shared - NodeInput nodeInput - @Shared - BmTypeInput bmType - - def setupSpec() { - // build the NodeInputModel - nodeInput = Mock(NodeInput) - - // build the BMTypesInputModel - bmType = new BmTypeInput( - UUID.fromString("bc06e089-03cd-481e-9e28-228266a148a4"), - "BM Model Test Type 1", - Quantities.getQuantity(0, EURO), - Quantities.getQuantity(0.05d, EURO_PER_KILOWATTHOUR), - Quantities.getQuantity(5d, PERCENT_PER_HOUR), - Quantities.getQuantity(190, KILOVOLTAMPERE), - 1d, - Quantities.getQuantity(100d, PERCENT) - ) - } - - def getStandardModel() { - return new BMModel( - UUID.fromString("1b332f94-03e4-4abe-b142-8fceca689c53"), - "BM Model Test", - OperationInterval.apply(0L, 86400L), - QControl.apply(new CosPhiFixed("cosPhiFixed:{(0.0,1.0)}")), - Sq.create(190, Kilowatts$.MODULE$), - bmType.getCosPhiRated(), - "MockNode", - true, - Sq.create(bmType.opex.value.doubleValue(), EUR$.MODULE$), - Sq.create(0.051d, EuroPerKilowatthour$.MODULE$), - 0.05) - } - - def "Test calculateK1"() { - given: - BMModel bmModel = getStandardModel() - - when: - def k1Calc = bmModel.calculateK1(ZonedDateTime.parse(time)) - - then: - k1Calc == k1Sol - - where: - time || k1Sol - '2019-01-04T05:15:00+01:00[Europe/Berlin]' || 1d // Friday - '2019-01-07T05:15:00+01:00[Europe/Berlin]' || 1d // Monday - '2019-01-05T05:15:00+01:00[Europe/Berlin]' || 0.96d // Saturday, 5:15AM - '2019-01-05T15:59:00+01:00[Europe/Berlin]' || 0.995d // Sunday, 3:59PM - } - - def "Test calculateK2"() { - given: - BMModel bmModel = getStandardModel() - - when: - def k2Calc = bmModel.calculateK2(ZonedDateTime.parse(time)) - - then: - k2Calc == k2Sol - - where: - time || k2Sol - '2019-05-29T05:15:00+02:00[Europe/Berlin]' || 1.03d // Day 149 of the year - '2019-05-30T05:15:00+02:00[Europe/Berlin]' || 0.61d // Day 150 of the year - '2019-08-31T05:15:00+02:00[Europe/Berlin]' || 0.61d // Day 243 of the year - '2019-09-01T05:15:00+02:00[Europe/Berlin]' || 1.03d // Day 244 of the year - } - - def "Test calculatePTh"() { - given: - BMModel bmModel = getStandardModel() - - when: - def pThCalc = bmModel.calculatePTh(Sq.create(temp, Celsius$.MODULE$), k1, k2) - - then: "compare in watts" - pThCalc - Sq.create(pThSol, Megawatts$.MODULE$) < Sq.create(0.0001d, Megawatts$.MODULE$) - - where: - temp | k1 | k2 || pThSol - 19.28d | 1d | 1d || 5.62d // independent of temp - 30d | 2d | 3d || 33.72d - 19.2799999d | 1d | 1d || 5.6147201076d // dependent on temp - 15d | 1.01d | 0.61d || 6.296542d // somewhat realistic - } - - def "Test calculateUsage"() { - given: - BMModel bmModel = getStandardModel() - - when: - def usageCalc = bmModel.calculateUsage(Sq.create(pTh, Megawatts$.MODULE$)) - - then: - Math.abs(usageCalc - usageSol) < 0.00000001 - - where: - pTh || usageSol - 43.14d || 1d // exactly maximum heat - 50d || 1d // more than maximum, cap to 1 - 20d || 0.463606861382d // less than max - 0d || 0d // zero - } - - def "Test calculateEff"() { - given: - BMModel bmModel = getStandardModel() - - when: - def effCalc = bmModel.calculateEff(usage) - - then: - Math.abs(effCalc - effSol) < 0.000000001 - - where: - usage || effSol - 1d || 1d - 0d || 0.724d - 0.75d || 0.98425d - 0.86446317d || 0.993848918615d - } - - def "Test calculateElOutput"() { - when: - BMModel bmModel = new BMModel( - UUID.fromString("8fbaf82d-5170-4636-bd7a-790eccbea880"), - "BM Model Test", - OperationInterval.apply(0L, 86400L), - QControl.apply(new CosPhiFixed("cosPhiFixed:{(0.0,1.0)}")), - Sq.create(190, Kilowatts$.MODULE$), - bmType.getCosPhiRated(), - "MockNode", - true, - Sq.create(bmType.opex.value.doubleValue(), EUR$.MODULE$), - Sq.create(feedInTariff, EuroPerKilowatthour$.MODULE$), - 0.05) - - def pElCalc = bmModel.calculateElOutput(usage, eff) - - then: "compare in watts" - pElCalc - Sq.create(pElSol, Kilowatts$.MODULE$) < Sq.create(0.0001d, Kilowatts$.MODULE$) - - where: - feedInTariff | usage | eff || pElSol - 0.051d | 1d | 1d || -190d // tariff greater than opex => full power - 0.04d | 0.75d | 0.98425d || -140.255625d // tariff too little, only serve heat demand - 0.04d | 1d | 1d || -190d // tariff too little, but max heat demand - } - - def "Test applyLoadGradient"() { - given: - BMModel bmModel = getStandardModel() - bmModel._lastPower = new Some(Sq.create(lastPower, Kilowatts$.MODULE$)) - - when: - def pElCalc = bmModel.applyLoadGradient(Sq.create(pEl, Kilowatts$.MODULE$)) - - then: - pElCalc == Sq.create(pElSol, Kilowatts$.MODULE$) - - where: - lastPower | pEl || pElSol - -100d | -120d || -109.5d // increase of power, more than load gradient allows - -50d | -55d || -55d // increase, within load gradient - -50d | -41d || -41d // decrease, within load gradient - -30d | -15 || -20.5d // decrease, more than load gradient - } - - def "Test calculateP"() { - given: "date, time, a temperature and last power output and the built model" - // construct date and time from string - ZonedDateTime dateTime = ZonedDateTime.parse(time) - - /* Prepare the calculation relevant data */ - BMModel.BMCalcRelevantData relevantData = new BMModel.BMCalcRelevantData(dateTime, Sq.create(temp, Celsius$.MODULE$)) - - BMModel bmModel = new BMModel( - UUID.fromString("08a8134d-04b7-45de-a937-9a55fab4e1af"), - "BM Model Test", - OperationInterval.apply(0L, 86400L), - QControl.apply(new CosPhiFixed("cosPhiFixed:{(0.0,1.0)}")), - Sq.create(190, Kilowatts$.MODULE$), - bmType.getCosPhiRated(), - "MockNode", - costControlled, - Sq.create(bmType.opex.value.doubleValue(), EUR$.MODULE$), - Sq.create(0.051d, EuroPerKilowatthour$.MODULE$), - 0.05) - - // modify data store: add last output power, one hour in the past - bmModel._lastPower = new Some(Sq.create(lastPower, Kilowatts$.MODULE$)) - - when: "the power from the grid is calculated" - def powerCalc = bmModel.calculateActivePower(ConstantState$.MODULE$, relevantData) - - then: "compare in kilowatts" - powerCalc - Sq.create(powerSol, Kilowatts$.MODULE$) < Sq.create(1e-12d, Kilowatts$.MODULE$) - - where: - time | temp | costControlled | lastPower || powerSol - '2019-01-05T05:15:00+01:00[Europe/Berlin]' | 10 | true | -40.0d || -49.5d // weekend day in heating season, power increase capped by load gradient - '2019-01-04T05:15:00+01:00[Europe/Berlin]' | 10 | true | -80.0d || -70.5d // working day in heating season, power decrease capped by load gradient - '2019-01-04T05:15:00+01:00[Europe/Berlin]' | -20 | true | -182.0d || -190d // peek load boiler activated, max output because cost < revenues - '2019-01-04T05:15:00+01:00[Europe/Berlin]' | -7 | true | -182.0d || -190d // close to peak load, max output because cost < revenues - '2019-01-04T05:15:00+01:00[Europe/Berlin]' | -7 | false | -150.0d || -152.16900643778735d // close to peak load, not cost controlled but just serving heat demand - '2019-07-07T10:15:00+02:00[Europe/Berlin]' | 19 | true | -10.0d || -12.099949463243976d // weekend day outside heating season, increase not capped - '2019-07-05T05:15:00+02:00[Europe/Berlin]' | 20 | true | -20.0d || -11.70638561892377d // working day outside heating season, decrease not capped - '2019-07-06T10:15:00+02:00[Europe/Berlin]' | 20 | true | -0.0d || -9.5d // weekend day outside heating season, increase capped - '2019-07-05T05:15:00+02:00[Europe/Berlin]' | 22 | true | -22.0d || -12.5d // working day outside heating season, decrease capped - } -} \ No newline at end of file diff --git a/src/test/scala/edu/ie3/simona/model/participant/BMModelSpec.scala b/src/test/scala/edu/ie3/simona/model/participant/BMModelSpec.scala new file mode 100644 index 0000000000..011dbb9567 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/model/participant/BMModelSpec.scala @@ -0,0 +1,326 @@ +/* + * © 2020. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.model.participant + +import edu.ie3.datamodel.models.input.system.characteristic.CosPhiFixed +import edu.ie3.simona.model.participant.ModelState.ConstantState +import edu.ie3.simona.model.participant.control.QControl +import edu.ie3.simona.test.common.UnitSpec +import edu.ie3.util.scala.OperationInterval +import edu.ie3.util.scala.quantities.EuroPerKilowatthour +import squants.energy.{Kilowatts, Megawatts} +import squants.market.EUR +import squants.thermal.Celsius +import squants.{Power, Temperature} + +import java.time.ZonedDateTime +import java.util.UUID + +/** Test class that tries to cover all special cases of the current + * implementation of the {@link BMModel} + * + * Test results have been calculated on paper using equations from wiki: + * https://wiki.ie3.e-technik.tu-dortmund.de/!simona/model:bm_model + */ + +class BMModelSpec extends UnitSpec { + + implicit val powerTolerance: Power = Kilowatts(1e-4) + implicit val usageTolerance: Double = 1e-12 + + def buildBmModel(): BMModel = { + new BMModel( + UUID.fromString("1b332f94-03e4-4abe-b142-8fceca689c53"), + "BM Model Test", + OperationInterval(0L, 86400L), + QControl(new CosPhiFixed("cosPhiFixed:{(0.0,1.0)}")), + Kilowatts(190), + 1d, + "MockNode", + isCostControlled = true, + EUR(0.05), + EuroPerKilowatthour(0.51d), + 0.05, + ) + } + + "A BMModel" should { + "calculate K1 correctly" in { + val bmModel = buildBmModel() + + val testCases = Table( + ("Time", "K1 Solution"), + ("2019-01-04T05:15:00+01:00[Europe/Berlin]", 1d), // Friday + ("2019-01-07T05:15:00+01:00[Europe/Berlin]", 1d), // Monday + ("2019-01-05T05:15:00+01:00[Europe/Berlin]", 0.96d), // Saturday, 5:15AM + ("2019-01-05T15:59:00+01:00[Europe/Berlin]", 0.995d), // Sunday, 3:59PM + ) + + testCases.foreach { case (time, k1Sol) => + val k1Calc = bmModel.calculateK1(ZonedDateTime.parse(time)) + k1Calc should be(k1Sol) + } + } + } + + "calculate K2 correctly" in { + val bmModel = buildBmModel() + + val testCases = Table( + ("Time", "K2 Solution"), + ( + "2019-05-29T05:15:00+02:00[Europe/Berlin]", + 1.03d, + ), // Day 149 of the year + ( + "2019-05-30T05:15:00+02:00[Europe/Berlin]", + 0.61d, + ), // Day 150 of the year + ( + "2019-08-31T05:15:00+02:00[Europe/Berlin]", + 0.61d, + ), // Day 243 of the year + ("2019-09-01T05:15:00+02:00[Europe/Berlin]", 1.03d), // Day 244 of the year + ) + + testCases.foreach { case (time, k2Sol) => + val k2Calc = bmModel.calculateK2(ZonedDateTime.parse(time)) + k2Calc should be(k2Sol) + } + } + + "calculate PTh correctly" in { + val bmModel = buildBmModel() + + val testCases = Table( + ("Temperature", "K1", "K2", "PTh Sol"), + (19.28, 1d, 1d, 5.62d), // independent of temp + (30d, 2d, 3d, 33.72d), + (19.2799999d, 1d, 1d, 5.6147201076d), // dependent on temp + (15d, 1.01d, 0.61d, 6.296542d), // somewhat realistic + ) + + testCases.foreach { case (temp, k1, k2, pThSol) => + val pThCalc = bmModel.calculatePTh(Celsius(temp), k1, k2) + pThCalc should approximate(Megawatts(pThSol)) + } + } + + "calculate usage correctly" in { + val bmModel = buildBmModel() + + val testCases = Table( + ("PTh", "Usage Solution"), + (43.14d, 1d), // exactly maximum heat + (50d, 1d), // more than maximum, cap to 1 + (20d, 0.463606861382d), // less than max + (0d, 0d), // zero + ) + + testCases.foreach { case (pTh, usageSol) => + val usageCalc = bmModel.calculateUsage(Megawatts(pTh)) + usageCalc should be(usageSol +- usageTolerance) + } + } + + "calculate efficiency correctly" in { + val bmModel = buildBmModel() + + val testCases = Table( + ("Usage", "Efficiency Sol"), + (1d, 1d), + (0d, 0.724d), + (0.75d, 0.98425d), + (0.86446317d, 0.993848918615d), + ) + + testCases.foreach { case (usage, effSol) => + val effCalc = bmModel.calculateEff(usage) + effCalc should be(effSol +- 0.000000001) + } + } + + "calculate electrical output correctly" in { + + val testCases = Table( + ("FeedInTariff", "Usage", "Efficiency", "PEl Sol"), + (0.051d, 1d, 1d, -190d), // tariff greater than opex => full power + ( + 0.04d, + 0.75d, + 0.98425d, + -140.25562499999998d, + ), // tariff too little, only serve heat demand + (0.04d, 1d, 1d, -190d), // tariff too little, but max heat demand + ) + + testCases.foreach { case (feedInTariff, usage, eff, pElSol) => + val bmModel = new BMModel( + UUID.fromString("8fbaf82d-5170-4636-bd7a-790eccbea880"), + "BM Model Test", + OperationInterval(0L, 86400L), + QControl(new CosPhiFixed("cosPhiFixed:{(0.0,1.0)}")), + Kilowatts(190), + 1d, + "MockNode", + isCostControlled = true, + EUR(0.05), + EuroPerKilowatthour(feedInTariff), + 0.05, + ) + + val pElCalc = bmModel.calculateElOutput(usage, eff) + pElCalc.value should be(Kilowatts(pElSol).value +- 1e-4) + } + } + + "apply load gradient correctly" in { + val bmModel = buildBmModel() + + val testCases = Table( + ("Last Power", "PEl", "PEl Sol"), + ( + Kilowatts(-100d), + Kilowatts(-120d), + Kilowatts(-109.5d), + ), // increase of power, more than load gradient allows + ( + Kilowatts(-50d), + Kilowatts(-55d), + Kilowatts(-55d), + ), // increase, within load gradient + ( + Kilowatts(-50d), + Kilowatts(-41d), + Kilowatts(-41d), + ), // decrease, within load gradient + ( + Kilowatts(-30d), + Kilowatts(-15d), + Kilowatts(-20.5d), + ), // decrease, more than load gradient + ) + + testCases.foreach { case (lastPower, pEl, pElSol) => + bmModel._lastPower = Some(lastPower) + val pElCalc = bmModel.applyLoadGradient(pEl) + pElCalc should approximate(pElSol) + } + } + + "calculate P correctly" in { + + val testCases = Table( + ("time", "temp", "costControlled", "lastPower", "powerSol"), + // weekend day in heating season, power increase capped by load gradient + ( + "2019-01-05T05:15:00+01:00[Europe/Berlin]", + Celsius(10.0), + true, + Kilowatts(-40.0), + Kilowatts(-49.5), + ), + // working day in heating season, power decrease capped by load gradient + ( + "2019-01-04T05:15:00+01:00[Europe/Berlin]", + Celsius(10.0), + true, + Kilowatts(-80.0), + Kilowatts(-70.5), + ), + // peak load boiler activated, max output because cost < revenues + ( + "2019-01-04T05:15:00+01:00[Europe/Berlin]", + Celsius(-20.0), + true, + Kilowatts(-182.0), + Kilowatts(-190.0), + ), + // close to peak load, max output because cost < revenues + ( + "2019-01-04T05:15:00+01:00[Europe/Berlin]", + Celsius(-7.0), + true, + Kilowatts(-182.0), + Kilowatts(-190.0), + ), + // close to peak load, not cost controlled but just serving heat demand + ( + "2019-01-04T05:15:00+01:00[Europe/Berlin]", + Celsius(-7.0), + false, + Kilowatts(-150.0), + Kilowatts(-152.16900643778735), + ), + // weekend day outside heating season, increase not capped + ( + "2019-07-07T10:15:00+02:00[Europe/Berlin]", + Celsius(19.0), + true, + Kilowatts(-10.0), + Kilowatts(-12.099949463243976), + ), + // working day outside heating season, decrease not capped + ( + "2019-07-05T05:15:00+02:00[Europe/Berlin]", + Celsius(20.0), + true, + Kilowatts(-20.0), + Kilowatts(-11.70638561892377), + ), + // weekend day outside heating season, increase capped + ( + "2019-07-06T10:15:00+02:00[Europe/Berlin]", + Celsius(20.0), + true, + Kilowatts(0.0), + Kilowatts(-9.5), + ), + // working day outside heating season, decrease capped + ( + "2019-07-05T05:15:00+02:00[Europe/Berlin]", + Celsius(22.0), + true, + Kilowatts(-22.0), + Kilowatts(-12.5), + ), + ) + + forAll(testCases) { + ( + time: String, + temp: Temperature, + costControlled: Boolean, + lastPower: Power, + powerSol: Power, + ) => + val dateTime = ZonedDateTime.parse(time) + val relevantData = BMModel.BMCalcRelevantData(dateTime, Celsius(temp)) + + val bmModel = new BMModel( + UUID.fromString("08a8134d-04b7-45de-a937-9a55fab4e1af"), + "BM Model Test", + OperationInterval(0L, 86400L), + QControl(new CosPhiFixed("cosPhiFixed:{(0.0,1.0)}")), + Kilowatts(190), + 1d, + "MockNode", + costControlled, + EUR(0.05), + EuroPerKilowatthour(0.051d), + 0.05, + ) + + bmModel._lastPower = Some(lastPower) + + val powerCalc = + bmModel.calculateActivePower(ConstantState, relevantData) + + powerCalc should approximate(powerSol) + } + } +}