diff --git a/pom.xml b/pom.xml index 6df3259b..0c5af927 100644 --- a/pom.xml +++ b/pom.xml @@ -9,11 +9,11 @@ 1.0-SNAPSHOT - 24 + 21 UTF-8 5.13.4 - 3.27.4 - 5.19.0 + 3.27.6 + 5.20.0 diff --git a/src/main/java/com/example/Main.java b/src/main/java/com/example/Main.java index 20a692ac..7e4c866a 100644 --- a/src/main/java/com/example/Main.java +++ b/src/main/java/com/example/Main.java @@ -1,9 +1,221 @@ package com.example; import com.example.api.ElpriserAPI; +import com.example.api.ElpriserAPI.Elpris; +import com.example.api.ElpriserAPI.Prisklass; + +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; public class Main { + public static void main(String[] args) { - ElpriserAPI elpriserAPI = new ElpriserAPI(); + + if (args.length == 0 || (args.length == 1 && args[0].equals("--help"))) { + printHelp(); + return; + } + + String zone = null; + String dateStr = null; + boolean sorted = false; + int chargingHours = 0; + + //Hantera CLI argument + + for (int i = 0; i < args.length; i++) { + switch (args[i]) { + case "--zone" -> { + if (i + 1 < args.length) zone = args[++i]; + } + case "--date" -> { + if (i + 1 < args.length) dateStr = args[++i]; + } + case "--sorted" -> sorted = true; + case "--charging" -> { + if (i + 1 < args.length) { + String argValue = args[++i]; + try { + if (argValue.endsWith("h")) { + chargingHours = Integer.parseInt(argValue.substring(0, argValue.length() - 1)); + } else { + chargingHours = Integer.parseInt(argValue); + } + } catch (NumberFormatException e) { + System.out.println("Fel: Ogiltigt format för --charging. Använd t.ex. 2h, 4h eller 8h."); + return; + } + } + } + case "--help" -> { + printHelp(); + return; + } + } + } + + if (zone == null) { + System.out.println("Fel: Argumentet --zone är obligatoriskt."); + printHelp(); + return; + } + + Prisklass prisklass; + try { + prisklass = Prisklass.valueOf(zone.toUpperCase()); + } catch (IllegalArgumentException e) { + System.out.println("Fel: Ogiltig zon. Använd SE1, SE2, SE3 eller SE4."); + return; + } + + LocalDate datum = LocalDate.now(); + if (dateStr != null) { + try { + datum = LocalDate.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE); + } catch (DateTimeParseException e) { + System.out.println("Fel: Ogiltigt datumformat. Använd YYYY-MM-DD."); + return; + } + } + + //Använda mock data eller ta fram riktiga priser + + ElpriserAPI api = new ElpriserAPI(false); + + List priser = new ArrayList<>(api.getPriser(datum, prisklass)); + + if (chargingHours > 0) { + priser.addAll(api.getPriser(datum.plusDays(1), prisklass)); + } + + if (priser.isEmpty()) { + System.out.println("Ingen data tillgänglig för zon: " + zone + " datum: " + datum); + return; + } + + DecimalFormatSymbols symbols = new DecimalFormatSymbols(Locale.forLanguageTag("sv-SE")); + symbols.setDecimalSeparator(','); + DecimalFormat df = new DecimalFormat("#0.00", symbols); + + DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm"); + DateTimeFormatter hourFormatter = DateTimeFormatter.ofPattern("HH"); + + if (chargingHours > 0) { + if (chargingHours < 2 || chargingHours > 8) { + System.out.println("Fel: Laddningsfönster måste vara 2h, 4h eller 8h."); + return; + } + + if (chargingHours > priser.size()) { + System.out.println("Fel: Kan inte ladda längre än " + priser.size() + " tillgängliga timmar."); + return; + } + + double minSum = Double.MAX_VALUE; + int bestStart = -1; + + for (int i = 0; i <= priser.size() - chargingHours; i++) { + double currentSum = 0; + for (int j = 0; j < chargingHours; j++) { + currentSum += priser.get(i + j).sekPerKWh(); + } + + if (currentSum < minSum) { + minSum = currentSum; + bestStart = i; + } + } + + if (bestStart == -1) { + System.out.println("Kunde inte hitta ett optimalt laddningsfönster."); + return; + } + + Elpris start = priser.get(bestStart); + Elpris end = priser.get(bestStart + chargingHours - 1); + + double totalCostOre = minSum * 100; + double avgOre = totalCostOre / chargingHours; + + System.out.println("Påbörja laddning"); + + System.out.println("Optimalt laddningsfönster (" + chargingHours + "h):"); + System.out.println("Starttid: kl " + start.timeStart().format(timeFormatter)); + System.out.println("Sluttid: kl " + end.timeEnd().format(timeFormatter)); + System.out.println("Total kostnad: " + df.format(totalCostOre) + " öre"); + System.out.println("Medelpris för fönster: " + df.format(avgOre) + " öre"); + + return; + } + + List priserOre = new ArrayList<>(); + double minPrice = Double.MAX_VALUE; + double maxPrice = Double.MIN_VALUE; + double sumPrice = 0.0; + + for (Elpris pris : priser) { + double ore = pris.sekPerKWh() * 100; + priserOre.add(ore); + + if (ore < minPrice) { + minPrice = ore; + } + if (ore > maxPrice) { + maxPrice = ore; + } + sumPrice += ore; + } + + double min = minPrice; + double max = maxPrice; + double avg = priserOre.isEmpty() ? 0.0 : sumPrice / priserOre.size(); + + System.out.println("\nElpriser för " + prisklass + " den " + datum.format(DateTimeFormatter.ISO_DATE) + ":"); + System.out.println("----------------------------------------"); + + List priserForDisplay = new ArrayList<>(priser); + if (sorted) { + priserForDisplay.sort(Comparator.comparingDouble(Elpris::sekPerKWh)); + } + + for (Elpris pris : priserForDisplay) { + String startHour = pris.timeStart().format(hourFormatter); + String endHour = pris.timeEnd().format(hourFormatter); + + String timeRange = startHour + "-" + endHour; + double ore = pris.sekPerKWh() * 100; + System.out.println(timeRange + " " + df.format(ore) + " öre"); + } + + System.out.println("----------------------------------------"); + System.out.println("Lägsta pris: " + df.format(min) + " öre"); + System.out.println("Högsta pris: " + df.format(max) + " öre"); + System.out.println("Medelpris: " + df.format(avg) + " öre"); + } + + private static void printHelp() { + System.out.println(""" + ⚡ Electricity Price Optimizer CLI + + Hjälper dig optimera energianvändningen baserat på timpriser. + + Användning (usage): + java -cp target/classes com.example.Main --zone SE3 --date 2025-09-29 + java -cp target/classes com.example.Main --zone SE1 --charging 4h + + Argument: + --zone SE1|SE2|SE3|SE4 (obligatoriskt) Välj elprisområde. + --date YYYY-MM-DD (valfritt, standard = idag) Datum att hämta priser för. + --sorted (valfritt) Visar prislistan sorterad från billigast till dyrast. + --charging 2h|4h|8h (valfritt) Hittar de billigaste N sammanhängande timmarna för laddning. + --help (valfritt) Visar denna hjälp. + """); } -} +} \ No newline at end of file diff --git a/src/main/java/com/example/api/ElpriserAPI.java b/src/main/java/com/example/api/ElpriserAPI.java index 15085a44..00e0bf39 100644 --- a/src/main/java/com/example/api/ElpriserAPI.java +++ b/src/main/java/com/example/api/ElpriserAPI.java @@ -27,10 +27,10 @@ public final class ElpriserAPI { // En återanvändbar HttpClient-instans private final HttpClient httpClient; - + // Flagga för att styra cachlagring private final boolean cachingEnabled; - + // Ett enkelt minnes-cache. Nyckeln är en kombination av datum och prisklass, t.ex. "2025-08-30_SE3" private final Map> inMemoryCache; @@ -39,11 +39,11 @@ public final class ElpriserAPI { * Användningen av 'record' genererar automatiskt constructor, getters, equals, hashCode och toString. */ public record Elpris( - double sekPerKWh, - double eurPerKWh, - double exr, - ZonedDateTime timeStart, - ZonedDateTime timeEnd + double sekPerKWh, + double eurPerKWh, + double exr, + ZonedDateTime timeStart, + ZonedDateTime timeEnd ) {} /** @@ -59,7 +59,7 @@ public enum Prisklass { * use the String it provides instead of making a real HTTP call. */ private static Supplier mockResponseSupplier = null; - + // New: map mock responses per date, so tests can provide different JSON per day private static java.util.Map datedMockResponses = new java.util.HashMap<>(); @@ -71,7 +71,7 @@ public enum Prisklass { public static void setMockResponse(String jsonResponse) { mockResponseSupplier = () -> jsonResponse; } - + /** * FOR TESTS ONLY: Sets a mock JSON response for a specific date. This allows * tests to simulate availability for one day but not another. @@ -152,9 +152,9 @@ public List getPriser(LocalDate datum, Prisklass prisklass) { // Steg 2: Försök ladda från disk-cache (framtida implementation) var priserFrånDisk = loadFromDiskCache(cacheKey); if (cachingEnabled && priserFrånDisk != null && !priserFrånDisk.isEmpty()) { - System.out.println("Hämtar från disk-cache för " + cacheKey); - inMemoryCache.put(cacheKey, priserFrånDisk); // Lägg i minnes-cachen för snabbare åtkomst nästa gång - return priserFrånDisk; + System.out.println("Hämtar från disk-cache för " + cacheKey); + inMemoryCache.put(cacheKey, priserFrånDisk); // Lägg i minnes-cachen för snabbare åtkomst nästa gång + return priserFrånDisk; } // Check for a mock response before making a network call --- @@ -185,8 +185,8 @@ public List getPriser(LocalDate datum, Prisklass prisklass) { return Collections.emptyList(); } if (response.statusCode() != 200) { - System.err.println("Misslyckades med att hämta priser. Statuskod: " + response.statusCode()); - return Collections.emptyList(); + System.err.println("Misslyckades med att hämta priser. Statuskod: " + response.statusCode()); + return Collections.emptyList(); } List priser = parseSimpleJson(response.body()); @@ -212,7 +212,7 @@ private String buildUrl(LocalDate datum, Prisklass prisklass) { String formattedDate = datum.format(URL_DATE_FORMATTER); return String.format("%s/%s_%s.json", API_BASE_URL, formattedDate, prisklass.name()); } - + private String getCacheKey(LocalDate datum, Prisklass prisklass) { return datum.format(DateTimeFormatter.ISO_LOCAL_DATE) + "_" + prisklass.name(); } @@ -239,7 +239,7 @@ private List parseSimpleJson(String json) { for (String objStr : objects) { // Rensa bort resterande { och } String cleanObjStr = objStr.replace("{", "").replace("}", ""); - + try { // Skapa en temporär map för att hålla värdena för ett objekt Map valueMap = new java.util.HashMap<>(); @@ -253,11 +253,11 @@ private List parseSimpleJson(String json) { // Skapa ett Elpris-objekt från värdena i mappen priser.add(new Elpris( - Double.parseDouble(valueMap.get("SEK_per_kWh")), - Double.parseDouble(valueMap.get("EUR_per_kWh")), - Double.parseDouble(valueMap.get("EXR")), - ZonedDateTime.parse(valueMap.get("time_start")), - ZonedDateTime.parse(valueMap.get("time_end")) + Double.parseDouble(valueMap.get("SEK_per_kWh")), + Double.parseDouble(valueMap.get("EUR_per_kWh")), + Double.parseDouble(valueMap.get("EXR")), + ZonedDateTime.parse(valueMap.get("time_start")), + ZonedDateTime.parse(valueMap.get("time_end")) )); } catch (Exception e) { // Hoppa över objekt som inte kan parsas, logga ett fel @@ -266,9 +266,9 @@ private List parseSimpleJson(String json) { } return priser; } - + // --- Stub-metoder för disk-cache --- - + /** * STUB: Spara data till en fil i en dold katalog i användarens hemkatalog. * Oimplementerad tills vidare. @@ -314,9 +314,9 @@ public static void main(String[] args) { } else { System.out.println("\nDagens elpriser för " + Prisklass.SE3 + " (" + dagensPriser.size() + " st värden):"); // Skriv bara ut de 3 första för att hålla utskriften kort - dagensPriser.stream().limit(3).forEach(pris -> - System.out.printf("Tid: %s, Pris: %.4f SEK/kWh\n", - pris.timeStart().toLocalTime(), pris.sekPerKWh()) + dagensPriser.stream().limit(3).forEach(pris -> + System.out.printf("Tid: %s, Pris: %.4f SEK/kWh\n", + pris.timeStart().toLocalTime(), pris.sekPerKWh()) ); if(dagensPriser.size() > 3) System.out.println("..."); }