package de.upapp import org.json.JSONObject import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test import java.time.DayOfWeek.FRIDAY import java.time.DayOfWeek.MONDAY import java.time.DayOfWeek.SUNDAY import java.time.DayOfWeek.THURSDAY import java.time.DayOfWeek.TUESDAY import java.time.LocalDate import java.time.LocalTime import java.time.ZoneId import java.time.ZonedDateTime // Fixtures from the upXlink test suite. class ModelTest { private val timer = parseTimer(JSONObject(""" {"id": "20a560c1", "name": "Timer1", "uri": "/chargingmanager/timers/20a560c1", "state": "idle", "departureTime": "06:35:00", "profile": {"id": "63cf948c", "name": "Ladelimit 80, HVAC", "uri": "/x"}, "cyclic": true, "weekdays": ["monday", "tuesday", "thursday", "friday"]} """)) @Test fun parsesTimer() { assertEquals(1, timer.index) assertFalse(timer.active) assertEquals(setOf(MONDAY, TUESDAY, THURSDAY, FRIDAY), timer.weekdays) assertEquals(LocalTime.of(6, 35), timer.departureTime) assertNull(timer.departureDate) assertEquals("63cf948c", timer.profileId) } @Test fun parsesProfilesAndOptions() { val p = parseProfile(JSONObject(""" {"id": "8269b474", "name": "Ladeort 3", "uri": "/p/8269b474", "operations": ["charge", "climate"], "maxCurrent": 16, "minLevel": 0, "targetLevel": 100, "temperature": 10.0, "powerProvider": {"id": "c8876782", "name": "", "uri": "/x"}} """)) assertEquals(Profile("8269b474", "/p/8269b474", "Ladeort 3", 100, 16, listOf("charge", "climate"), "c8876782"), p) val o = parseOptions(JSONObject(""" {"id": "3900b210", "name": "Optionen", "uri": "/p/3900b210", "operations": ["climateExtSupply", "climate"], "maxCurrent": 32, "minLevel": 100, "targetLevel": 0, "temperature": 30.0} """)) assertTrue(o.climateExtSupply) assertEquals(100, o.minLevel) assertEquals(30.0, o.temperature, 0.0) } @Test fun parsesProviderAndCarData() { val p = parseProvider(JSONObject(""" {"id": "c8876782", "uri": "/pr/c8876782", "cyclic": false, "preferredTimeStart": "22:00:00", "preferredTimeEnd": "05:00:00", "weekdays": ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"]} """)) assertEquals(7, p.weekdays.size) assertEquals(LocalTime.of(22, 0), p.start) assertEquals(84.0, parseBattery(JSONObject("""{"name": "BEV battery", "soc": 84.0}""")).soc!!, 0.0) assertNull(parseBattery(JSONObject("""{"soc": ""}""")).soc) assertEquals("MATSRSECBFQTIXLFYF", parseInfo(JSONObject("""{"vehicleIdenticationNumber": "MATSRSECBFQTIXLFYF"}""")).vin) assertEquals(LocalDate.of(2025, 11, 1), parseTimer(JSONObject( """{"id": "a", "name": "Timer2", "uri": "/t", "state": "scheduled", "departureTime": "07:00:00", "departureDate": "01 Nov 2025"}""" )).departureDate) } @Test fun shiftsOnlyTheTime() { val late = timer.copy(departureTime = LocalTime.of(23, 30), weekdays = setOf(FRIDAY, SUNDAY), departureDate = LocalDate.of(2025, 12, 31)) val local = late.shifted(2 * 3600) assertEquals(LocalTime.of(1, 30), local.departureTime) assertEquals(late.weekdays, local.weekdays) assertEquals(late.departureDate, local.departureDate) assertEquals(late, local.shifted(-2 * 3600)) } @Test fun allows32AmpsOnlyForEUp300() { assertEquals(16, maxCurrents("WVWZZZAAZKD000001").last()) assertEquals(32, maxCurrents("WVWZZZAAZLD000001").last()) assertEquals(32, maxCurrents("TMBZZZAAZKD000001").last()) } @Test fun findsNextDeparture() { val berlin = ZoneId.of("Europe/Berlin") // Wednesday 2025-11-12 12:00. Timer runs Mon, Tue, Thu, Fri at 06:35. val now = ZonedDateTime.of(2025, 11, 12, 12, 0, 0, 0, berlin) assertNull(nextDeparture(listOf(timer), now)) val (_, at) = nextDeparture(listOf(timer.copy(state = TIMER_ON)), now)!! assertEquals(ZonedDateTime.of(2025, 11, 13, 6, 35, 0, 0, berlin), at) } @Test fun pairsUnlinkedProvidersByPosition() { fun profile(id: String) = JSONObject("""{"id": "$id", "uri": "/p/$id", "name": "$id", "operations": [], "maxCurrent": 16, "minLevel": 0, "targetLevel": 80, "temperature": 21.0}""") fun provider(id: String) = JSONObject("""{"id": "$id", "uri": "/pr/$id", "cyclic": false, "preferredTimeStart": "00:00:00", "preferredTimeEnd": "00:00:00"}""") val cm = chargingManagerOf(listOf(profile("options"), profile("home")), emptyList(), listOf(provider("a"), provider("b"))) assertEquals("a", cm.options!!.providerId) assertEquals("b", cm.profiles.single().providerId) assertNull(chargingManagerOf(listOf(profile("options"), profile("home")), emptyList(), listOf(provider("a"))).profiles.single().providerId) } @Test fun matchesIgnoresExtraKeysAndNumberTypes() { val actual = JSONObject("""{"temperature": 24, "profile": {"id": "a", "name": "Home"}, "weekdays": ["monday"]}""") assertTrue(matches(JSONObject("""{"temperature": 24.0, "profile": {"id": "a"}, "weekdays": ["monday"]}"""), actual)) assertFalse(matches(JSONObject("""{"temperature": 25.0}"""), actual)) assertFalse(matches(JSONObject("""{"weekdays": ["monday", "friday"]}"""), actual)) assertTrue(matches(JSONObject("""{"profile": "a"}"""), actual)) assertFalse(matches(JSONObject("""{"profile": "b"}"""), actual)) } }