initial commit
This commit is contained in:
179
.gitignore
vendored
179
.gitignore
vendored
@ -1,4 +1,104 @@
|
|||||||
# ---> Java
|
/target/
|
||||||
|
!.mvn/wrapper/maven-wrapper.jar
|
||||||
|
|
||||||
|
### STS ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
|
||||||
|
|
||||||
|
# Created by https://www.gitignore.io/api/git,java,maven,eclipse,windows
|
||||||
|
|
||||||
|
### Eclipse ###
|
||||||
|
|
||||||
|
.metadata
|
||||||
|
bin/
|
||||||
|
tmp/
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
|
*.swp
|
||||||
|
*~.nib
|
||||||
|
local.properties
|
||||||
|
.settings/
|
||||||
|
.loadpath
|
||||||
|
.recommenders
|
||||||
|
|
||||||
|
# External tool builders
|
||||||
|
.externalToolBuilders/
|
||||||
|
|
||||||
|
# Locally stored "Eclipse launch configurations"
|
||||||
|
*.launch
|
||||||
|
|
||||||
|
# PyDev specific (Python IDE for Eclipse)
|
||||||
|
*.pydevproject
|
||||||
|
|
||||||
|
# CDT-specific (C/C++ Development Tooling)
|
||||||
|
.cproject
|
||||||
|
|
||||||
|
# CDT- autotools
|
||||||
|
.autotools
|
||||||
|
|
||||||
|
# Java annotation processor (APT)
|
||||||
|
.factorypath
|
||||||
|
|
||||||
|
# PDT-specific (PHP Development Tools)
|
||||||
|
.buildpath
|
||||||
|
|
||||||
|
# sbteclipse plugin
|
||||||
|
.target
|
||||||
|
|
||||||
|
# Tern plugin
|
||||||
|
.tern-project
|
||||||
|
|
||||||
|
# TeXlipse plugin
|
||||||
|
.texlipse
|
||||||
|
|
||||||
|
# STS (Spring Tool Suite)
|
||||||
|
.springBeans
|
||||||
|
|
||||||
|
# Code Recommenders
|
||||||
|
.recommenders/
|
||||||
|
|
||||||
|
# Annotation Processing
|
||||||
|
.apt_generated/
|
||||||
|
|
||||||
|
# Scala IDE specific (Scala & Java development for Eclipse)
|
||||||
|
.cache-main
|
||||||
|
.scala_dependencies
|
||||||
|
.worksheet
|
||||||
|
|
||||||
|
### Eclipse Patch ###
|
||||||
|
# Eclipse Core
|
||||||
|
.project
|
||||||
|
|
||||||
|
# JDT-specific (Eclipse Java Development Tools)
|
||||||
|
.classpath
|
||||||
|
|
||||||
|
# Annotation Processing
|
||||||
|
.apt_generated
|
||||||
|
|
||||||
|
.sts4-cache/
|
||||||
|
|
||||||
|
### Git ###
|
||||||
|
# Created by git for backups. To disable backups in Git:
|
||||||
|
# $ git config --global mergetool.keepBackup false
|
||||||
|
*.orig
|
||||||
|
|
||||||
|
# Created by git when using merge tools for conflicts
|
||||||
|
*.BACKUP.*
|
||||||
|
*.BASE.*
|
||||||
|
*.LOCAL.*
|
||||||
|
*.REMOTE.*
|
||||||
|
*_BACKUP_*.txt
|
||||||
|
*_BASE_*.txt
|
||||||
|
*_LOCAL_*.txt
|
||||||
|
*_REMOTE_*.txt
|
||||||
|
|
||||||
|
### Java ###
|
||||||
# Compiled class file
|
# Compiled class file
|
||||||
*.class
|
*.class
|
||||||
|
|
||||||
@ -22,5 +122,80 @@
|
|||||||
|
|
||||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||||
hs_err_pid*
|
hs_err_pid*
|
||||||
replay_pid*
|
|
||||||
|
|
||||||
|
### Maven ###
|
||||||
|
target/
|
||||||
|
pom.xml.tag
|
||||||
|
pom.xml.releaseBackup
|
||||||
|
pom.xml.versionsBackup
|
||||||
|
pom.xml.next
|
||||||
|
release.properties
|
||||||
|
dependency-reduced-pom.xml
|
||||||
|
buildNumber.properties
|
||||||
|
.mvn/timing.properties
|
||||||
|
.mvn/wrapper/maven-wrapper.jar
|
||||||
|
|
||||||
|
### Windows ###
|
||||||
|
# Windows thumbnail cache files
|
||||||
|
Thumbs.db
|
||||||
|
ehthumbs.db
|
||||||
|
ehthumbs_vista.db
|
||||||
|
|
||||||
|
# Dump file
|
||||||
|
*.stackdump
|
||||||
|
|
||||||
|
# Folder config file
|
||||||
|
[Dd]esktop.ini
|
||||||
|
|
||||||
|
# Recycle Bin used on file shares
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
|
||||||
|
# Windows Installer files
|
||||||
|
*.cab
|
||||||
|
*.msi
|
||||||
|
*.msix
|
||||||
|
*.msm
|
||||||
|
*.msp
|
||||||
|
|
||||||
|
# Windows shortcuts
|
||||||
|
*.lnk
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
build/
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
### Some additional ignores (sort later)
|
||||||
|
*.DS_Store
|
||||||
|
*.sw?
|
||||||
|
.#*
|
||||||
|
*#
|
||||||
|
*~
|
||||||
|
.classpath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
bin
|
||||||
|
build
|
||||||
|
target
|
||||||
|
dependency-reduced-pom.xml
|
||||||
|
*.sublime-*
|
||||||
|
/scratch
|
||||||
|
.gradle
|
||||||
|
README.html
|
||||||
|
*.iml
|
||||||
|
.idea
|
||||||
|
.exercism
|
||||||
85
pom.xml
Normal file
85
pom.xml
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<!-- Coordinates -->
|
||||||
|
<groupId>se.urmo</groupId>
|
||||||
|
<artifactId>electricityalert</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
<name>Electricity Price Alert</name>
|
||||||
|
<description>Alert system for electricity price using Telegram</description>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<!-- Use Spring Boot's managed versions -->
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>3.5.5</version>
|
||||||
|
<relativePath/>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.version>21</java.version>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Web + HTTP client (RestClient) for calling GraphQL endpoint -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Optional: Reactor-based client if you prefer WebClient -->
|
||||||
|
<!--
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||||
|
</dependency>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Configuration binding & validation niceties -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Operational endpoints (health, info) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Optional: Telegram client (simple & actively maintained) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.pengrad</groupId>
|
||||||
|
<artifactId>java-telegram-bot-api</artifactId>
|
||||||
|
<version>9.2.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Lombok (optional) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Tests -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<!-- Boots your app into an executable jar -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
29
setup_project.sh
Normal file
29
setup_project.sh
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
BASE_DIR="src/main"
|
||||||
|
JAVA_DIR="$BASE_DIR/java/se/urmo/electricityalert"
|
||||||
|
RES_DIR="$BASE_DIR/resources"
|
||||||
|
|
||||||
|
# Create directory structure
|
||||||
|
mkdir -p "$JAVA_DIR/config"
|
||||||
|
mkdir -p "$JAVA_DIR/fetcher"
|
||||||
|
mkdir -p "$JAVA_DIR/alert"
|
||||||
|
mkdir -p "$JAVA_DIR/notifier"
|
||||||
|
mkdir -p "$JAVA_DIR/store"
|
||||||
|
mkdir -p "$JAVA_DIR/model"
|
||||||
|
mkdir -p "$RES_DIR"
|
||||||
|
|
||||||
|
# Create empty Java files
|
||||||
|
touch "$JAVA_DIR/ElectricityAlertApplication.java"
|
||||||
|
touch "$JAVA_DIR/config/AppConfig.java"
|
||||||
|
touch "$JAVA_DIR/fetcher/PriceFetcherService.java"
|
||||||
|
touch "$JAVA_DIR/alert/AlertService.java"
|
||||||
|
touch "$JAVA_DIR/notifier/TelegramNotifier.java"
|
||||||
|
touch "$JAVA_DIR/store/InMemoryPriceStore.java"
|
||||||
|
touch "$JAVA_DIR/model/ElectricityPrice.java"
|
||||||
|
|
||||||
|
# Create application.properties
|
||||||
|
touch "$RES_DIR/application.properties"
|
||||||
|
|
||||||
|
echo "Project structure and base files created."
|
||||||
|
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package se.urmo.electricityalert;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
@EnableScheduling
|
||||||
|
public class ElectricityAlertApplication {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(ElectricityAlertApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
package se.urmo.electricityalert.alert;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import se.urmo.electricityalert.model.ElectricityPrice;
|
||||||
|
import se.urmo.electricityalert.notifier.TelegramNotifier;
|
||||||
|
import se.urmo.electricityalert.store.InMemoryPriceStore;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AlertService {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(AlertService.class);
|
||||||
|
|
||||||
|
private final InMemoryPriceStore store;
|
||||||
|
private final TelegramNotifier notifier;
|
||||||
|
private final BigDecimal threshold;
|
||||||
|
|
||||||
|
public AlertService(InMemoryPriceStore store,
|
||||||
|
TelegramNotifier notifier,
|
||||||
|
@Value("${electricity.threshold}") BigDecimal threshold) {
|
||||||
|
this.store = store;
|
||||||
|
this.notifier = notifier;
|
||||||
|
this.threshold = threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check every 5 minutes
|
||||||
|
@Scheduled(fixedRate = 300_000)
|
||||||
|
public void checkPrices() {
|
||||||
|
LocalDateTime oneHourLater = LocalDateTime.now().withMinute(0).withSecond(0).plusHours(1);
|
||||||
|
ElectricityPrice price = store.get(oneHourLater);
|
||||||
|
|
||||||
|
if (price != null && price.price().compareTo(threshold) < 0 && !store.alreadyNotified(oneHourLater)) {
|
||||||
|
String msg = String.format("⚡ In the next hour (%s) the price is %.2f SEK/kWh. Plug in the car!",
|
||||||
|
oneHourLater, price.price());
|
||||||
|
notifier.send(msg);
|
||||||
|
store.markNotified(oneHourLater);
|
||||||
|
log.info("Sent alert: {}", msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,127 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package se.urmo.electricityalert.fetcher.dto;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record PriceInfoDto(Data data) {
|
||||||
|
public record Data(Viewer viewer) {}
|
||||||
|
public record Viewer(List<Home> homes) {}
|
||||||
|
public record Home(CurrentSubscription currentSubscription) {}
|
||||||
|
public record CurrentSubscription(PriceInfo priceInfo) {}
|
||||||
|
public record PriceInfo(PricePoint current, List<PricePoint> today, List<PricePoint> tomorrow) {}
|
||||||
|
public record PricePoint(BigDecimal total, BigDecimal energy, BigDecimal tax, String startsAt) {}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package se.urmo.electricityalert.model;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
public record ElectricityPrice(LocalDateTime hour, BigDecimal price) {
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
package se.urmo.electricityalert.notifier;
|
||||||
|
|
||||||
|
import com.pengrad.telegrambot.TelegramBot;
|
||||||
|
import com.pengrad.telegrambot.request.SendMessage;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class TelegramNotifier {
|
||||||
|
private final TelegramBot bot;
|
||||||
|
private final String chatId;
|
||||||
|
|
||||||
|
public TelegramNotifier(@Value("${telegram.bot.token}") String token,
|
||||||
|
@Value("${telegram.chat.id}") String chatId) {
|
||||||
|
this.bot = new TelegramBot(token);
|
||||||
|
this.chatId = chatId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void send(String message) {
|
||||||
|
bot.execute(new SendMessage(chatId, message));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
package se.urmo.electricityalert.store;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import se.urmo.electricityalert.model.ElectricityPrice;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class InMemoryPriceStore {
|
||||||
|
private final Map<LocalDateTime, ElectricityPrice> prices = new ConcurrentHashMap<>();
|
||||||
|
private final Set<LocalDateTime> notified = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
|
public void put(ElectricityPrice price) {
|
||||||
|
prices.put(price.hour(), price);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ElectricityPrice get(LocalDateTime hour) {
|
||||||
|
return prices.get(hour);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<LocalDateTime, ElectricityPrice> all() {
|
||||||
|
return prices;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean alreadyNotified(LocalDateTime hour) {
|
||||||
|
return notified.contains(hour);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void markNotified(LocalDateTime hour) {
|
||||||
|
notified.add(hour);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/main/resources/application.properties
Normal file
13
src/main/resources/application.properties
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# ? Telegram Bot
|
||||||
|
telegram.bot.token=${TELEGRAM_BOT_TOKEN}
|
||||||
|
telegram.chat.id=${TELEGRAM_CHAT_ID}
|
||||||
|
|
||||||
|
# ? Electricity threshold (can also override via ENV if you want)
|
||||||
|
electricity.threshold=${ELECTRICITY_THRESHOLD:0.50}
|
||||||
|
|
||||||
|
# ? GraphQL
|
||||||
|
graphql.endpoint=${GRAPHQL_ENDPOINT:https://provider.example/graphql}
|
||||||
|
graphql.access-token=${GRAPHQL_ACCESS_TOKEN}
|
||||||
|
|
||||||
|
# ?? Behavior
|
||||||
|
fetch.on-startup=${FETCH_ON_STARTUP:true}
|
||||||
Reference in New Issue
Block a user