Compare commits
5 Commits
4abaf5ee17
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8aa528ff5b | |||
| 9754ffabaa | |||
| 9c2855fa98 | |||
| 4f89db49b6 | |||
| dfdd4943e1 |
@@ -289,6 +289,7 @@ Die aggregierten Endpunkte sind optimiert für Langzeit-Visualisierungen und red
|
|||||||
---
|
---
|
||||||
|
|
||||||
#### `GET /weather/rain-weekly`
|
#### `GET /weather/rain-weekly`
|
||||||
|
|
||||||
**Gibt wöchentliche Regensummen zurück (Woche = Mo-So)**
|
**Gibt wöchentliche Regensummen zurück (Woche = Mo-So)**
|
||||||
|
|
||||||
**Query Parameter:**
|
**Query Parameter:**
|
||||||
|
|||||||
+18
-13
@@ -270,11 +270,12 @@ async def get_weather_statistics(
|
|||||||
AVG(pressure) as avg_pressure,
|
AVG(pressure) as avg_pressure,
|
||||||
AVG(wind_speed * 1.60934) as avg_wind_speed,
|
AVG(wind_speed * 1.60934) as avg_wind_speed,
|
||||||
MAX(wind_gust * 1.60934) as max_wind_gust,
|
MAX(wind_gust * 1.60934) as max_wind_gust,
|
||||||
(SELECT COALESCE(SUM(daily_max), 0) FROM (
|
(SELECT COALESCE(SUM(daily_max), 0)
|
||||||
|
FROM (
|
||||||
SELECT MAX(rain) as daily_max
|
SELECT MAX(rain) as daily_max
|
||||||
FROM weather_data
|
FROM weather_data d2
|
||||||
WHERE datetime >= NOW() - make_interval(hours => %s)
|
WHERE d2.datetime >= NOW() - make_interval(hours => %s)
|
||||||
GROUP BY DATE(datetime)
|
GROUP BY DATE(d2.datetime)
|
||||||
) sub) as total_rain,
|
) sub) as total_rain,
|
||||||
COUNT(*) as data_points
|
COUNT(*) as data_points
|
||||||
FROM weather_data
|
FROM weather_data
|
||||||
@@ -392,7 +393,7 @@ async def get_hourly_aggregated_data(
|
|||||||
AVG(wind_speed * 1.60934) as wind_speed,
|
AVG(wind_speed * 1.60934) as wind_speed,
|
||||||
MAX(wind_gust * 1.60934) as wind_gust,
|
MAX(wind_gust * 1.60934) as wind_gust,
|
||||||
AVG(wind_dir) as wind_dir,
|
AVG(wind_dir) as wind_dir,
|
||||||
AVG(rain) as rain,
|
MAX(rain) as rain,
|
||||||
AVG(rain_rate) as rain_rate,
|
AVG(rain_rate) as rain_rate,
|
||||||
MAX(received_at) as received_at
|
MAX(received_at) as received_at
|
||||||
FROM weather_data
|
FROM weather_data
|
||||||
@@ -516,25 +517,29 @@ async def get_weekly_rain_data(
|
|||||||
# Bei 365 Tagen: alle verfügbaren Daten zurückgeben
|
# Bei 365 Tagen: alle verfügbaren Daten zurückgeben
|
||||||
if days >= 365:
|
if days >= 365:
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT week_start, SUM(daily_max) as total_rain
|
SELECT
|
||||||
|
date_trunc('week', day) as week_start,
|
||||||
|
SUM(daily_rain) as total_rain
|
||||||
FROM (
|
FROM (
|
||||||
SELECT date_trunc('week', datetime) as week_start, MAX(rain) as daily_max
|
SELECT DATE(datetime) as day, MAX(rain) as daily_rain
|
||||||
FROM weather_data
|
FROM weather_data
|
||||||
GROUP BY date_trunc('week', datetime), DATE(datetime)
|
GROUP BY DATE(datetime)
|
||||||
) sub
|
) sub
|
||||||
GROUP BY week_start
|
GROUP BY date_trunc('week', day)
|
||||||
ORDER BY week_start ASC
|
ORDER BY week_start ASC
|
||||||
""")
|
""")
|
||||||
else:
|
else:
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT week_start, SUM(daily_max) as total_rain
|
SELECT
|
||||||
|
date_trunc('week', day) as week_start,
|
||||||
|
SUM(daily_rain) as total_rain
|
||||||
FROM (
|
FROM (
|
||||||
SELECT date_trunc('week', datetime) as week_start, MAX(rain) as daily_max
|
SELECT DATE(datetime) as day, MAX(rain) as daily_rain
|
||||||
FROM weather_data
|
FROM weather_data
|
||||||
WHERE datetime >= NOW() - make_interval(days => %s)
|
WHERE datetime >= NOW() - make_interval(days => %s)
|
||||||
GROUP BY date_trunc('week', datetime), DATE(datetime)
|
GROUP BY DATE(datetime)
|
||||||
) sub
|
) sub
|
||||||
GROUP BY week_start
|
GROUP BY date_trunc('week', day)
|
||||||
ORDER BY week_start ASC
|
ORDER BY week_start ASC
|
||||||
""", (days,))
|
""", (days,))
|
||||||
results = cursor.fetchall()
|
results = cursor.fetchall()
|
||||||
|
|||||||
+18
-3
@@ -164,6 +164,9 @@ class WeatherDataInput(BaseModel):
|
|||||||
# Vorhersage
|
# Vorhersage
|
||||||
forecast: Optional[int] = None
|
forecast: Optional[int] = None
|
||||||
|
|
||||||
|
# Datenquelle
|
||||||
|
source: Optional[str] = None
|
||||||
|
|
||||||
# ---- Validatoren -----------------------------------------------------
|
# ---- Validatoren -----------------------------------------------------
|
||||||
|
|
||||||
@field_validator("tempOut", "temperature", "tempIn")
|
@field_validator("tempOut", "temperature", "tempIn")
|
||||||
@@ -229,6 +232,13 @@ class WeatherDataInput(BaseModel):
|
|||||||
raise ValueError("rain value out of plausible range")
|
raise ValueError("rain value out of plausible range")
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
@field_validator("source")
|
||||||
|
@classmethod
|
||||||
|
def _source_valid(cls, v: Optional[str]) -> Optional[str]:
|
||||||
|
if v is not None and v not in ("loop", "archive"):
|
||||||
|
raise ValueError("source must be 'loop' or 'archive'")
|
||||||
|
return v
|
||||||
|
|
||||||
# ---- Konvertierungen -------------------------------------------------
|
# ---- Konvertierungen -------------------------------------------------
|
||||||
|
|
||||||
def get_datetime_string(self) -> str:
|
def get_datetime_string(self) -> str:
|
||||||
@@ -330,6 +340,9 @@ def setup_database() -> None:
|
|||||||
cursor.execute(
|
cursor.execute(
|
||||||
"ALTER TABLE weather_data ADD COLUMN IF NOT EXISTS bar_trend INTEGER"
|
"ALTER TABLE weather_data ADD COLUMN IF NOT EXISTS bar_trend INTEGER"
|
||||||
)
|
)
|
||||||
|
cursor.execute(
|
||||||
|
"ALTER TABLE weather_data ADD COLUMN IF NOT EXISTS source VARCHAR"
|
||||||
|
)
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"CREATE INDEX IF NOT EXISTS idx_weather_datetime_desc "
|
"CREATE INDEX IF NOT EXISTS idx_weather_datetime_desc "
|
||||||
"ON weather_data (datetime DESC)"
|
"ON weather_data (datetime DESC)"
|
||||||
@@ -500,6 +513,7 @@ def _store_weather(data: WeatherDataInput) -> dict:
|
|||||||
data.rain,
|
data.rain,
|
||||||
data.get_rain_rate(),
|
data.get_rain_rate(),
|
||||||
data.forecast,
|
data.forecast,
|
||||||
|
data.source,
|
||||||
)
|
)
|
||||||
|
|
||||||
with pool.connection() as conn:
|
with pool.connection() as conn:
|
||||||
@@ -509,8 +523,8 @@ def _store_weather(data: WeatherDataInput) -> dict:
|
|||||||
INSERT INTO weather_data
|
INSERT INTO weather_data
|
||||||
(datetime, temperature, temp_in, humidity, humidity_in,
|
(datetime, temperature, temp_in, humidity, humidity_in,
|
||||||
pressure, bar_trend, wind_speed, wind_gust, wind_dir,
|
pressure, bar_trend, wind_speed, wind_gust, wind_dir,
|
||||||
rain, rain_rate, forecast)
|
rain, rain_rate, forecast, source)
|
||||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
ON CONFLICT (datetime) DO UPDATE SET
|
ON CONFLICT (datetime) DO UPDATE SET
|
||||||
temperature = EXCLUDED.temperature,
|
temperature = EXCLUDED.temperature,
|
||||||
temp_in = EXCLUDED.temp_in,
|
temp_in = EXCLUDED.temp_in,
|
||||||
@@ -523,7 +537,8 @@ def _store_weather(data: WeatherDataInput) -> dict:
|
|||||||
wind_dir = EXCLUDED.wind_dir,
|
wind_dir = EXCLUDED.wind_dir,
|
||||||
rain = EXCLUDED.rain,
|
rain = EXCLUDED.rain,
|
||||||
rain_rate = EXCLUDED.rain_rate,
|
rain_rate = EXCLUDED.rain_rate,
|
||||||
forecast = EXCLUDED.forecast
|
forecast = EXCLUDED.forecast,
|
||||||
|
source = EXCLUDED.source
|
||||||
""",
|
""",
|
||||||
values,
|
values,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Deploy Script für laufschrift
|
# Deploy Script für wetterstation
|
||||||
# Baut das Docker Image und lädt es zu docker.citysensor.de hoch
|
# Baut das Docker Image und lädt es zu docker.citysensor.de hoch
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "wetterstation-frontend",
|
"name": "wetterstation-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.5.7",
|
"version": "1.6.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ function buildUrls(timeRange) {
|
|||||||
const path = days >= 7 ? 'daily-aggregated-range' : 'hourly-aggregated-range'
|
const path = days >= 7 ? 'daily-aggregated-range' : 'hourly-aggregated-range'
|
||||||
return {
|
return {
|
||||||
weatherUrl: `${API_BASE}/weather/${path}?start=${start}&end=${end}`,
|
weatherUrl: `${API_BASE}/weather/${path}?start=${start}&end=${end}`,
|
||||||
rainUrl: null, // TODO: Regen-Aggregation fuer Range implementieren
|
rainUrl: days < 7 ? `${API_BASE}/weather/daily-aggregated-range?start=${start}&end=${end}` : null,
|
||||||
needsCurrent: true,
|
needsCurrent: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,14 +171,14 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
|
|||||||
// Spezieller Suffix für Regen
|
// Spezieller Suffix für Regen
|
||||||
const rainSuffix = useMemo(() => {
|
const rainSuffix = useMemo(() => {
|
||||||
if (typeof timeRange === 'object' && timeRange.type === 'custom') {
|
if (typeof timeRange === 'object' && timeRange.type === 'custom') {
|
||||||
const days = timeRange.days || 1
|
return ' (pro Tag)'
|
||||||
return days >= 7 ? ' (pro Tag)' : ''
|
|
||||||
}
|
}
|
||||||
switch (timeRange) {
|
switch (timeRange) {
|
||||||
case '7d':
|
case '7d':
|
||||||
case '30d':
|
case '30d':
|
||||||
case '365d':
|
|
||||||
return ' (pro Tag)'
|
return ' (pro Tag)'
|
||||||
|
case '365d':
|
||||||
|
return ' (pro Woche)'
|
||||||
default:
|
default:
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@@ -291,30 +291,93 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Vordefinierte Bereiche
|
// Vordefinierte Bereiche
|
||||||
|
const pad = n => String(n).padStart(2, '0')
|
||||||
|
const fmtDate = d => `${pad(d.getDate())}.${pad(d.getMonth() + 1)}`
|
||||||
|
const fmtTime = d => `${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||||
|
|
||||||
switch (timeRange) {
|
switch (timeRange) {
|
||||||
case '24h':
|
case '24h':
|
||||||
xAxisConfig.tickInterval = 4 * 3600 * 1000 // 4 Stunden
|
xAxisConfig.tickPositioner = function() {
|
||||||
xAxisConfig.labels = { format: '{value:%H:%M}', align: 'center' }
|
const positions = []
|
||||||
|
const d = new Date(this.min)
|
||||||
|
d.setMinutes(0, 0, 0)
|
||||||
|
const h = d.getHours()
|
||||||
|
const nextH = Math.ceil(h / 4) * 4
|
||||||
|
if (nextH >= 24) { d.setDate(d.getDate() + 1); d.setHours(0, 0, 0, 0) }
|
||||||
|
else d.setHours(nextH, 0, 0, 0)
|
||||||
|
while (d.getTime() <= this.max) {
|
||||||
|
positions.push(d.getTime())
|
||||||
|
d.setHours(d.getHours() + 4, 0, 0, 0)
|
||||||
|
}
|
||||||
|
return positions
|
||||||
|
}
|
||||||
|
xAxisConfig.labels = {
|
||||||
|
rotation: 0, align: 'center', useHTML: true,
|
||||||
|
formatter: function() {
|
||||||
|
const d = new Date(this.value)
|
||||||
|
return d.getHours() === 0 && d.getMinutes() === 0
|
||||||
|
? `<span style="color:#3b82f6;font-weight:bold">${fmtDate(d)}</span>`
|
||||||
|
: fmtTime(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
xAxisMin = now - 24 * 3600 * 1000
|
xAxisMin = now - 24 * 3600 * 1000
|
||||||
xAxisMax = now
|
xAxisMax = now
|
||||||
tooltipDateFormat = '%d.%m.%Y %H:%M'
|
tooltipDateFormat = '%d.%m.%Y %H:%M'
|
||||||
break
|
break
|
||||||
case '7d':
|
case '7d':
|
||||||
xAxisConfig.labels = { format: '{value:%d.%m}', align: 'center' }
|
xAxisConfig.tickPositioner = function() {
|
||||||
|
const positions = []
|
||||||
|
const d = new Date(this.min)
|
||||||
|
d.setHours(0, 0, 0, 0)
|
||||||
|
while (d.getTime() <= this.max) {
|
||||||
|
positions.push(d.getTime())
|
||||||
|
d.setDate(d.getDate() + 1)
|
||||||
|
}
|
||||||
|
return positions
|
||||||
|
}
|
||||||
|
xAxisConfig.labels = {
|
||||||
|
rotation: 0, align: 'center',
|
||||||
|
formatter: function() { return fmtDate(new Date(this.value)) }
|
||||||
|
}
|
||||||
xAxisMin = now - 7 * 24 * 3600 * 1000
|
xAxisMin = now - 7 * 24 * 3600 * 1000
|
||||||
xAxisMax = now
|
xAxisMax = now
|
||||||
tooltipDateFormat = '%d.%m.%Y'
|
tooltipDateFormat = '%d.%m.%Y'
|
||||||
break
|
break
|
||||||
case '30d':
|
case '30d':
|
||||||
xAxisConfig.labels = { format: '{value:%d.%m}', align: 'center' }
|
xAxisConfig.tickPositioner = function() {
|
||||||
|
const positions = []
|
||||||
|
const d = new Date(this.min)
|
||||||
|
d.setHours(0, 0, 0, 0)
|
||||||
|
while (d.getTime() <= this.max) {
|
||||||
|
positions.push(d.getTime())
|
||||||
|
d.setDate(d.getDate() + 5)
|
||||||
|
}
|
||||||
|
return positions
|
||||||
|
}
|
||||||
|
xAxisConfig.labels = {
|
||||||
|
rotation: 0, align: 'center',
|
||||||
|
formatter: function() { return fmtDate(new Date(this.value)) }
|
||||||
|
}
|
||||||
xAxisMin = now - 30 * 24 * 3600 * 1000
|
xAxisMin = now - 30 * 24 * 3600 * 1000
|
||||||
xAxisMax = now
|
xAxisMax = now
|
||||||
tooltipDateFormat = '%d.%m.%Y'
|
tooltipDateFormat = '%d.%m.%Y'
|
||||||
break
|
break
|
||||||
case '365d':
|
case '365d':
|
||||||
xAxisConfig.labels = { format: '{value:%b %Y}', align: 'center' }
|
xAxisConfig.tickPositioner = function() {
|
||||||
tooltipDateFormat = '%b %Y'
|
const positions = []
|
||||||
// Bei 365d: Min/Max aus vorhandenen Daten berechnen
|
const d = new Date(this.min)
|
||||||
|
d.setDate(1); d.setHours(0, 0, 0, 0)
|
||||||
|
while (d.getTime() <= this.max) {
|
||||||
|
positions.push(d.getTime())
|
||||||
|
d.setMonth(d.getMonth() + 1)
|
||||||
|
}
|
||||||
|
return positions
|
||||||
|
}
|
||||||
|
xAxisConfig.labels = {
|
||||||
|
rotation: 0, align: 'center',
|
||||||
|
formatter: function() { return pad(new Date(this.value).getMonth() + 1) }
|
||||||
|
}
|
||||||
|
tooltipDateFormat = '%d.%m.%Y'
|
||||||
if (sortedData.length > 0) {
|
if (sortedData.length > 0) {
|
||||||
xAxisMin = new Date(sortedData[0].datetime).getTime()
|
xAxisMin = new Date(sortedData[0].datetime).getTime()
|
||||||
xAxisMax = new Date(sortedData[sortedData.length - 1].datetime).getTime()
|
xAxisMax = new Date(sortedData[sortedData.length - 1].datetime).getTime()
|
||||||
@@ -324,8 +387,27 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
xAxisConfig.tickInterval = 4 * 3600 * 1000
|
xAxisConfig.tickPositioner = function() {
|
||||||
xAxisConfig.labels = { format: '{value:%H:%M}', align: 'center' }
|
const positions = []
|
||||||
|
const d = new Date(this.min)
|
||||||
|
d.setMinutes(0, 0, 0)
|
||||||
|
const h = d.getHours()
|
||||||
|
const nextH = Math.ceil(h / 4) * 4
|
||||||
|
if (nextH >= 24) { d.setDate(d.getDate() + 1); d.setHours(0, 0, 0, 0) }
|
||||||
|
else d.setHours(nextH, 0, 0, 0)
|
||||||
|
while (d.getTime() <= this.max) {
|
||||||
|
positions.push(d.getTime())
|
||||||
|
d.setHours(d.getHours() + 4, 0, 0, 0)
|
||||||
|
}
|
||||||
|
return positions
|
||||||
|
}
|
||||||
|
xAxisConfig.labels = {
|
||||||
|
rotation: 0, align: 'center',
|
||||||
|
formatter: function() {
|
||||||
|
const d = new Date(this.value)
|
||||||
|
return d.getHours() === 0 && d.getMinutes() === 0 ? fmtDate(d) : fmtTime(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
xAxisMin = now - 24 * 3600 * 1000
|
xAxisMin = now - 24 * 3600 * 1000
|
||||||
xAxisMax = now
|
xAxisMax = now
|
||||||
tooltipDateFormat = '%d.%m.%Y %H:%M'
|
tooltipDateFormat = '%d.%m.%Y %H:%M'
|
||||||
@@ -639,11 +721,13 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
|
|||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
} else if (typeof timeRange === 'object' && timeRange.type === 'custom') {
|
} else if (typeof timeRange === 'object' && timeRange.type === 'custom') {
|
||||||
// Custom range: tägliche Summen aus sortedData (total_rain ist im daily-aggregated-range enthalten)
|
// Custom range: tägliche Summen — bei kurzen Ranges (<7d) aus rainData (extra Fetch),
|
||||||
|
// bei langen Ranges aus sortedData (daily-aggregated-range enthält total_rain)
|
||||||
yAxisTitle = 'Regen (mm pro Tag)'
|
yAxisTitle = 'Regen (mm pro Tag)'
|
||||||
|
const rainSource = rainData.length > 0 ? rainData : sortedData
|
||||||
series = [{
|
series = [{
|
||||||
name: 'Regen',
|
name: 'Regen',
|
||||||
data: sortedData
|
data: rainSource
|
||||||
.filter(item => item.total_rain != null && item.total_rain > 0)
|
.filter(item => item.total_rain != null && item.total_rain > 0)
|
||||||
.map(item => [new Date(item.datetime).getTime(), item.total_rain]),
|
.map(item => [new Date(item.datetime).getTime(), item.total_rain]),
|
||||||
color: 'rgb(54, 162, 235)',
|
color: 'rgb(54, 162, 235)',
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ load_dotenv(dotenv_path=env_path)
|
|||||||
# Konfiguration
|
# Konfiguration
|
||||||
SQLITE_DB = "data/wview-archive.sdb"
|
SQLITE_DB = "data/wview-archive.sdb"
|
||||||
START_DATE = datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
|
START_DATE = datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
|
||||||
END_DATE = datetime(2026, 2, 8, 0, 0, 0, tzinfo=timezone.utc)
|
END_DATE = datetime(2026, 3, 23, 0, 0, 0, tzinfo=timezone.utc)
|
||||||
|
|
||||||
# PostgreSQL-Konfiguration
|
# PostgreSQL-Konfiguration
|
||||||
DB_HOST = os.getenv('DB_HOST', 'localhost')
|
DB_HOST = os.getenv('DB_HOST', 'localhost')
|
||||||
@@ -96,6 +96,41 @@ def main():
|
|||||||
sqlite_conn.close()
|
sqlite_conn.close()
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Tabelle anlegen falls nicht vorhanden
|
||||||
|
try:
|
||||||
|
pg_cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS weather_data (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
datetime TIMESTAMPTZ NOT NULL,
|
||||||
|
temperature FLOAT,
|
||||||
|
humidity INTEGER,
|
||||||
|
pressure FLOAT,
|
||||||
|
wind_speed FLOAT,
|
||||||
|
wind_gust FLOAT,
|
||||||
|
wind_dir FLOAT,
|
||||||
|
rain FLOAT,
|
||||||
|
rain_rate FLOAT,
|
||||||
|
temp_in FLOAT,
|
||||||
|
humidity_in INTEGER,
|
||||||
|
forecast INTEGER,
|
||||||
|
bar_trend INTEGER,
|
||||||
|
source VARCHAR,
|
||||||
|
received_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(datetime)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
pg_cursor.execute(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_weather_datetime_desc "
|
||||||
|
"ON weather_data (datetime DESC)"
|
||||||
|
)
|
||||||
|
pg_conn.commit()
|
||||||
|
print("✓ Tabelle weather_data bereit")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Fehler beim Anlegen der Tabelle: {e}")
|
||||||
|
sqlite_conn.close()
|
||||||
|
pg_conn.close()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
# Tabelle leeren falls gewünscht
|
# Tabelle leeren falls gewünscht
|
||||||
if TRUNCATE_TABLE:
|
if TRUNCATE_TABLE:
|
||||||
print("\nLeere PostgreSQL-Tabelle weather_data...")
|
print("\nLeere PostgreSQL-Tabelle weather_data...")
|
||||||
@@ -165,11 +200,11 @@ def main():
|
|||||||
pg_cursor.execute("""
|
pg_cursor.execute("""
|
||||||
INSERT INTO weather_data
|
INSERT INTO weather_data
|
||||||
(datetime, temperature, humidity, pressure,
|
(datetime, temperature, humidity, pressure,
|
||||||
wind_speed, wind_gust, wind_dir, rain, rain_rate)
|
wind_speed, wind_gust, wind_dir, rain, rain_rate, source)
|
||||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
ON CONFLICT (datetime) DO NOTHING
|
ON CONFLICT (datetime) DO NOTHING
|
||||||
""", (dt, temp_c, humidity, pressure_hpa,
|
""", (dt, temp_c, humidity, pressure_hpa,
|
||||||
wind_speed_kmh, wind_gust_kmh, windDir, rain_mm, rain_rate_mm))
|
wind_speed_kmh, wind_gust_kmh, windDir, rain_mm, rain_rate_mm, 'wview'))
|
||||||
|
|
||||||
if pg_cursor.rowcount > 0:
|
if pg_cursor.rowcount > 0:
|
||||||
inserted += 1
|
inserted += 1
|
||||||
|
|||||||
Reference in New Issue
Block a user