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("...");
}