Changed upsert to INSERT ON CONFLICT DO NOTHING so existing prices are never overwritten. Added unique index on (time_dk, price_area) to enforce no duplicate quarter-hour entries per area. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
92 lines
3.0 KiB
Clojure
92 lines
3.0 KiB
Clojure
(ns pmagnus.elprice.web.controllers.prices
|
|
(:require
|
|
[clojure.data.json :as json]
|
|
[clojure.tools.logging :as log])
|
|
(:import
|
|
[java.net URI URLEncoder]
|
|
[java.net.http HttpClient HttpRequest HttpResponse$BodyHandlers]
|
|
[java.time LocalDate ZoneId]
|
|
[java.time.format DateTimeFormatter]
|
|
[java.sql Timestamp]))
|
|
|
|
(def ^:private dk-zone (ZoneId/of "Europe/Copenhagen"))
|
|
|
|
(def ^:private date-fmt (DateTimeFormatter/ofPattern "yyyy-MM-dd"))
|
|
|
|
(defn- build-url
|
|
"Build the Energi Data Service API URL for day-ahead prices."
|
|
[start-date end-date]
|
|
(str "https://api.energidataservice.dk/dataset/DayAheadPrices"
|
|
"?start=" start-date
|
|
"&end=" end-date
|
|
"&filter=" (URLEncoder/encode (json/write-str {"PriceArea" ["DK1" "DK2"]}) "UTF-8")
|
|
"&sort=TimeDK%20asc"
|
|
"&limit=200"))
|
|
|
|
(defn- fetch-json
|
|
"Fetch JSON from a URL using JDK HttpClient."
|
|
[url]
|
|
(let [client (HttpClient/newHttpClient)
|
|
request (-> (HttpRequest/newBuilder)
|
|
(.uri (URI/create url))
|
|
(.header "Accept" "application/json")
|
|
(.GET)
|
|
(.build))
|
|
response (.send client request (HttpResponse$BodyHandlers/ofString))]
|
|
(when (= 200 (.statusCode response))
|
|
(json/read-str (.body response) :key-fn keyword))))
|
|
|
|
(defn fetch-prices-from-api
|
|
"Fetch today's and tomorrow's prices from Energi Data Service."
|
|
[]
|
|
(let [today (LocalDate/now dk-zone)
|
|
tomorrow (.plusDays today 1)
|
|
;; Fetch from start of today to end of tomorrow
|
|
start (.format today date-fmt)
|
|
end (.format (.plusDays tomorrow 1) date-fmt)
|
|
url (build-url start end)]
|
|
(log/info "Fetching day-ahead prices from" url)
|
|
(when-let [data (fetch-json url)]
|
|
(:records data))))
|
|
|
|
(defn- parse-timestamp
|
|
"Parse an ISO timestamp string to java.sql.Timestamp."
|
|
[s]
|
|
(when s
|
|
(Timestamp/valueOf
|
|
(.replace ^String s "T" " "))))
|
|
|
|
(defn save-records!
|
|
"Insert price records into the database, skipping existing ones."
|
|
[query-fn records]
|
|
(doseq [rec records]
|
|
(query-fn :insert-price!
|
|
{:time-utc (parse-timestamp (:TimeUTC rec))
|
|
:time-dk (parse-timestamp (:TimeDK rec))
|
|
:price-area (:PriceArea rec)
|
|
:price-dkk (:DayAheadPriceDKK rec)
|
|
:price-eur (:DayAheadPriceEUR rec)})))
|
|
|
|
(defn fetch-and-save!
|
|
"Fetch prices from API and save to database."
|
|
[query-fn]
|
|
(when-let [records (fetch-prices-from-api)]
|
|
(log/info "Saving" (count records) "price records")
|
|
(save-records! query-fn records)
|
|
(count records)))
|
|
|
|
(defn get-prices-for-date
|
|
"Get prices for a given LocalDate and price area from the database."
|
|
[query-fn date price-area]
|
|
(query-fn :get-prices-for-date
|
|
{:date (java.sql.Date/valueOf date)
|
|
:price-area price-area}))
|
|
|
|
(defn get-today-prices
|
|
[query-fn]
|
|
(get-prices-for-date query-fn (LocalDate/now dk-zone) "DK1"))
|
|
|
|
(defn get-tomorrow-prices
|
|
[query-fn]
|
|
(get-prices-for-date query-fn (.plusDays (LocalDate/now dk-zone) 1) "DK1"))
|