Skip to content

Commit

Permalink
Initial
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielWTE committed Apr 8, 2024
1 parent e7a4920 commit 76642ea
Show file tree
Hide file tree
Showing 28 changed files with 1,396 additions and 185 deletions.
4 changes: 4 additions & 0 deletions .env.example
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=''
50 changes: 50 additions & 0 deletions .github/workflows/ci-cd.yml
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 }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

# production
/build
.env

# misc
.DS_Store
Expand Down
12 changes: 12 additions & 0 deletions Dockerfile
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"]
37 changes: 2 additions & 35 deletions README.md
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
83 changes: 83 additions & 0 deletions app/api/collect/route.ts
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 });
}
}
86 changes: 86 additions & 0 deletions app/api/fetch/route.ts
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 removed app/favicon.ico
Binary file not shown.
30 changes: 2 additions & 28 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,6 @@
@tailwind components;
@tailwind utilities;

:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}

@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}

body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}

@layer utilities {
.text-balance {
text-wrap: balance;
}
}
background-color: #f7fafc;
}
3 changes: 1 addition & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import "./globals.css";
const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "EPEX SPOT AT - Statistics",
};

export default function RootLayout({
Expand Down
Loading

0 comments on commit 76642ea

Please sign in to comment.