128 lines
4.7 KiB
Java
128 lines
4.7 KiB
Java
package se.urmo.electricityalert.fetcher;
|
|
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
|
import org.springframework.context.event.EventListener;
|
|
import org.springframework.http.MediaType;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.scheduling.annotation.Scheduled;
|
|
import org.springframework.web.client.RestClient;
|
|
import se.urmo.electricityalert.fetcher.dto.PriceInfoDto;
|
|
import se.urmo.electricityalert.model.ElectricityPrice;
|
|
import se.urmo.electricityalert.store.InMemoryPriceStore;
|
|
|
|
import java.time.OffsetDateTime;
|
|
import java.time.LocalDateTime;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
@Service
|
|
public class PriceFetcherService {
|
|
|
|
private static final Logger log = LoggerFactory.getLogger(PriceFetcherService.class);
|
|
|
|
private final InMemoryPriceStore store;
|
|
private final RestClient client;
|
|
private final boolean fetchOnStartup;
|
|
|
|
|
|
// Your GraphQL query (exactly as you provided, compacted into a single string)
|
|
private static final String QUERY = """
|
|
{ viewer { homes { currentSubscription { priceInfo {
|
|
current { total energy tax startsAt }
|
|
today { total energy tax startsAt }
|
|
tomorrow { total energy tax startsAt }
|
|
} } } } }
|
|
""";
|
|
|
|
public PriceFetcherService(
|
|
InMemoryPriceStore store,
|
|
@Value("${graphql.endpoint}") String endpoint,
|
|
@Value("${graphql.access-token}") String accessToken,
|
|
@Value("${fetch.on-startup:true}") boolean fetchOnStartup) {
|
|
this.store = store;
|
|
this.fetchOnStartup = fetchOnStartup;
|
|
this.client = RestClient.builder()
|
|
.baseUrl(endpoint)
|
|
.defaultHeader("Authorization", "Bearer " + accessToken)
|
|
.build();
|
|
}
|
|
|
|
/** Runs every day at 13:05. */
|
|
@Scheduled(cron = "0 5 13 * * *")
|
|
public void fetchPrices() {
|
|
fetchPricesInternal();
|
|
}
|
|
|
|
/** One-time fetch after the app is fully started (beans initialized, web server up). */
|
|
@EventListener(ApplicationReadyEvent.class)
|
|
public void onReadyFetchOnce() {
|
|
if (!fetchOnStartup) return;
|
|
log.info("Application started — performing one-time price fetch...");
|
|
fetchPricesInternal();
|
|
}
|
|
|
|
/** Shared logic used by both the scheduled job and the startup hook. */
|
|
private void fetchPricesInternal() {
|
|
try {
|
|
Map<String, Object> body = new HashMap<>();
|
|
body.put("query", QUERY);
|
|
|
|
PriceInfoDto result = client.post()
|
|
.contentType(MediaType.APPLICATION_JSON)
|
|
.accept(MediaType.APPLICATION_JSON)
|
|
.body(body)
|
|
.retrieve()
|
|
.body(PriceInfoDto.class);
|
|
|
|
if (result == null || result.data() == null || result.data().viewer() == null
|
|
|| result.data().viewer().homes() == null || result.data().viewer().homes().isEmpty()) {
|
|
log.warn("GraphQL response missing expected fields; nothing stored.");
|
|
return;
|
|
}
|
|
|
|
var home = result.data().viewer().homes().get(0);
|
|
if (home == null || home.currentSubscription() == null
|
|
|| home.currentSubscription().priceInfo() == null) {
|
|
log.warn("No priceInfo in response; nothing stored.");
|
|
return;
|
|
}
|
|
|
|
var priceInfo = home.currentSubscription().priceInfo();
|
|
int count = 0;
|
|
|
|
// current
|
|
count += storePoints(List.of(priceInfo.current()));
|
|
// today
|
|
if (priceInfo.today() != null) count += storePoints(priceInfo.today());
|
|
// tomorrow
|
|
if (priceInfo.tomorrow() != null) count += storePoints(priceInfo.tomorrow());
|
|
|
|
log.info("Stored {} price points from GraphQL.", count);
|
|
|
|
} catch (Exception e) {
|
|
log.error("Failed to fetch/parse GraphQL prices", e);
|
|
}
|
|
}
|
|
|
|
private int storePoints(List<PriceInfoDto.PricePoint> points) {
|
|
if (points == null || points.isEmpty()) return 0;
|
|
|
|
int stored = 0;
|
|
for (var p : points) {
|
|
if (p == null || p.startsAt() == null || p.total() == null) continue;
|
|
|
|
// startsAt like: 2025-09-12T11:00:00.000+02:00
|
|
OffsetDateTime odt = OffsetDateTime.parse(p.startsAt());
|
|
LocalDateTime hourLocal = odt.toLocalDateTime(); // interpret in local (offset already in string)
|
|
|
|
store.put(new ElectricityPrice(hourLocal, p.total()));
|
|
stored++;
|
|
}
|
|
return stored;
|
|
}
|
|
}
|