-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
28 changed files
with
1,396 additions
and
185 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
MONGODB_URL='mongodb://localhost:27017/' | ||
SMART_ENERGY_EPEX_SPOT_AT_ENDPOINT_URL='https://apis.smartenergy.at/market/v1/price' | ||
KV_REST_API_URL='' | ||
KV_REST_API_TOKEN='' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
name: Build | ||
|
||
on: | ||
push: | ||
branches: ['main'] | ||
workflow_dispatch: | ||
|
||
env: | ||
REGISTRY: ghcr.io | ||
IMAGE_NAME: ${{ github.repository }} | ||
|
||
jobs: | ||
build-and-push-image: | ||
runs-on: ubuntu-latest | ||
permissions: | ||
contents: read | ||
packages: write | ||
|
||
steps: | ||
- name: Checkout repository | ||
uses: actions/checkout@v4 | ||
|
||
- name: Cache NPM dependencies | ||
uses: actions/cache@v3 | ||
with: | ||
path: | | ||
**/node_modules | ||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} | ||
restore-keys: ${{ runner.os }}-node- | ||
|
||
- name: Log in to the Container registry | ||
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 | ||
with: | ||
registry: ${{ env.REGISTRY }} | ||
username: ${{ github.actor }} | ||
password: ${{ secrets.GITHUB_TOKEN }} | ||
|
||
- name: Extract metadata (tags, labels) for Docker | ||
id: meta | ||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 | ||
with: | ||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | ||
|
||
- name: Build and push Docker image | ||
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 | ||
with: | ||
context: . | ||
push: true | ||
tags: ${{ steps.meta.outputs.tags }} | ||
labels: ${{ steps.meta.outputs.labels }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,6 +15,7 @@ | |
|
||
# production | ||
/build | ||
.env | ||
|
||
# misc | ||
.DS_Store | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
FROM node:18-alpine | ||
|
||
WORKDIR /app | ||
|
||
COPY . . | ||
RUN npm i | ||
|
||
ENV NEXT_PUBLIC_API_URL= | ||
|
||
RUN npm run build | ||
|
||
CMD ["npm", "run", "start"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,36 +1,3 @@ | ||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). | ||
This mini-project collects hourly energy prices using the smartENERGY EPEX Spot AT API and saves them to a MongoDB database. It also includes a web dashboard to display the collected data. Additionally, the project calculates the optimal times to charge devices, such as batteries or electric cars, based on the hourly prices available for the next 24 hours. | ||
|
||
## Getting Started | ||
|
||
First, run the development server: | ||
|
||
```bash | ||
npm run dev | ||
# or | ||
yarn dev | ||
# or | ||
pnpm dev | ||
# or | ||
bun dev | ||
``` | ||
|
||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. | ||
|
||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. | ||
|
||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. | ||
|
||
## Learn More | ||
|
||
To learn more about Next.js, take a look at the following resources: | ||
|
||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. | ||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. | ||
|
||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! | ||
|
||
## Deploy on Vercel | ||
|
||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. | ||
|
||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. | ||
## Table of Contents |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import { NextRequest, NextResponse } from 'next/server'; | ||
import data from '@/database/models/data'; | ||
import connectDB from '@/database/connect'; | ||
import crypto from 'crypto'; | ||
import { Ratelimit } from "@upstash/ratelimit"; | ||
import { kv } from '@vercel/kv'; | ||
import { convertUTCtoGMT2, formatFetchedData, toLocalTimeISOString } from '@/utils/format'; | ||
import { calculateBestTimes } from '@/utils/calculate'; | ||
|
||
export async function GET(req: NextRequest) { | ||
try { | ||
|
||
const rateLimit = new Ratelimit({ | ||
redis: kv, | ||
limiter: Ratelimit.slidingWindow(10, '1 m'), | ||
}) | ||
|
||
const { success } = await rateLimit.limit(req.headers.get('x-real-ip') as string || req.headers.get('x-forwarded-for') as string || 'guest'); | ||
|
||
if (!success) { | ||
return NextResponse.json({ message: 'Rate limit exceeded' }, { status: 429 }); | ||
} | ||
|
||
const request = await fetch(process.env.SMART_ENERGY_EPEX_SPOT_AT_ENDPOINT_URL as string, { | ||
method: 'GET', | ||
headers: { | ||
'Content-Type': 'application/json', | ||
}, | ||
}) | ||
|
||
const response = await request.json(); | ||
|
||
if (!request.ok) { | ||
return NextResponse.json({ message: 'An error occurred while fetching data' }, { status: 500 }); | ||
} | ||
|
||
await connectDB(); | ||
|
||
const formattedData = formatFetchedData(response.data); | ||
|
||
const splitData = formattedData.reduce((acc: any, curr) => { | ||
const date = convertUTCtoGMT2(curr.date); | ||
if (!acc[date]) { | ||
acc[date] = []; | ||
} | ||
acc[date].push(curr); | ||
return acc; | ||
}, {}); | ||
|
||
for (const date in splitData) { | ||
const hash = crypto.createHash('sha256'); | ||
hash.update(JSON.stringify(splitData[date])); | ||
const hashValue = hash.digest('hex'); | ||
|
||
const existingData = await data.findOne | ||
({ | ||
hash: hashValue, | ||
}); | ||
|
||
if (!existingData) { | ||
const calculatedData = calculateBestTimes(splitData[date]); | ||
|
||
const newData = new data({ | ||
hash: hashValue, | ||
tariff: response.tariff, | ||
unit: response.unit, | ||
interval: 60, // because we're formatting the 15 minute records to 1 hour | ||
data: splitData[date], | ||
energy_date: new Date(toLocalTimeISOString(date)), | ||
data_clarification: calculatedData, | ||
}); | ||
|
||
await newData.save(); | ||
} | ||
} | ||
|
||
return NextResponse.json({ message: 'Data fetched and saved successfully' }, { status: 200 }); | ||
|
||
} catch (err) { | ||
console.log(err); | ||
return NextResponse.json({ message: 'An error occurred', error: err }, { status: 500 }); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import { NextRequest, NextResponse } from 'next/server'; | ||
import connectDB from '@/database/connect'; | ||
import { Ratelimit } from "@upstash/ratelimit"; | ||
import { kv } from '@vercel/kv'; | ||
import data from '@/database/models/data'; | ||
import { timeframes } from '@/config/timeframes'; | ||
import { calculateEndDate, calculateStartDate } from '@/utils/calculate'; | ||
|
||
interface Zone { | ||
entries: { time: string, value: number }[]; | ||
} | ||
|
||
interface Clarifications { | ||
low: Zone; | ||
mid: Zone; | ||
high: Zone; | ||
} | ||
|
||
interface CombinedData { | ||
energyData: any[]; | ||
clarifications: Clarifications; | ||
} | ||
|
||
export async function GET(req: NextRequest) { | ||
try { | ||
const timeframe = req.nextUrl.searchParams.get('timeframe'); | ||
|
||
if (!timeframe) { | ||
return NextResponse.json({ message: 'Please provide a timeframe' }, { status: 400 }); | ||
} | ||
|
||
const selectedTimeframe = timeframes.find((tf) => tf.value === timeframe); | ||
if (!selectedTimeframe) { | ||
return NextResponse.json({ message: 'Invalid timeframe' }, { status: 400 }); | ||
} | ||
|
||
const rateLimit = new Ratelimit({ | ||
redis: kv, | ||
limiter: Ratelimit.slidingWindow(100, '1 m'), | ||
}) | ||
|
||
const { success } = await rateLimit.limit(req.headers.get('x-real-ip') as string || req.headers.get('x-forwarded-for') as string || 'guest'); | ||
|
||
if (!success) { | ||
return NextResponse.json({ message: 'Rate limit exceeded' }, { status: 429 }); | ||
} | ||
|
||
await connectDB(); | ||
|
||
const startDate = calculateStartDate(selectedTimeframe.value); | ||
const endDate = calculateEndDate(startDate, selectedTimeframe.duration, selectedTimeframe.unit); | ||
const query = startDate ? { energy_date: { $gte: startDate, $lte: endDate } } : {}; | ||
|
||
const fetchedData = await data.find(query); | ||
|
||
const combinedData: CombinedData = { | ||
energyData: [], | ||
clarifications: { | ||
low: { entries: [] }, | ||
mid: { entries: [] }, | ||
high: { entries: [] }, | ||
} | ||
}; | ||
|
||
fetchedData.forEach(doc => { | ||
combinedData.energyData.push(...doc.data); | ||
|
||
if (doc.data_clarification && doc.data_clarification.length > 0) { | ||
const clarifications = doc.data_clarification[0]; | ||
const zones = ['low', 'mid', 'high'] as const; | ||
zones.forEach(zone => { | ||
combinedData.clarifications[zone].entries = clarifications[zone].map((item: any) => ({ | ||
time: item.time, | ||
value: item.value | ||
})); | ||
}); | ||
} | ||
}); | ||
|
||
return NextResponse.json({ message: 'Data fetched successfully', data: combinedData }, { status: 200 }); | ||
|
||
} catch (err) { | ||
console.log(err); | ||
return NextResponse.json({ message: 'An error occurred', error: err }, { status: 500 }); | ||
} | ||
} |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.